<div id="scene-container" role="img" aria-label="Memphis kinetic typo extravaganza">
</div>
<div>
<a class="credits fade-in" href="https://codepen.io/ScavengerFrontend" target="blank">★ art & code ★ <br/>by Anna <br/> Scavenger ↗</a>
</div>
<script type="x-shader/x-vertex" id="matcap-vs">
varying vec2 vN;
void main() {
vec3 e = normalize(vec3(modelViewMatrix * vec4(position, 1.0)));
vec3 n = normalize(normalMatrix * normal);
vec3 r = reflect(e, n);
float m = 2.0 * sqrt(pow(r.x, 2.0) + pow( r.y, 2.0) + pow(r.z + 1.0, 2.0));
vN = r.xy / m + 0.5;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script type="x-shader/x-fragment" id="matcap-fs">
uniform sampler2D tMatCap;
varying vec2 vN;
void main() {
vec3 base = texture2D(tMatCap, vN).rgb;
gl_FragColor = vec4(base, 1.0);
}
</script>
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #cfd1d1;
text-align: right;
font-size: 1rem;
touch-action: pan-x;
font-family: Arial;
}
#scene-container {
position: absolute;
width: 100vw;
height: 100vh;
background: rgb(193,193,193);
background: linear-gradient(180deg, rgba(193,193,193,1) 0%, rgba(228,195,206,1) 47%, rgba(255,116,166,1) 100%);
}
.credits {
position: absolute;
bottom: 0;
right: 0;
text-transform: none;
text-decoration: none;
text-align: left;
color: black;
padding: 0.35rem 0.5rem;
margin: 0.75rem 0.75rem;
font-size: 0.75rem;
border: 1.5px solid black;
transition: background 200ms ease-in;
border-radius: 0.2rem;
opacity: 0.0;
}
a:hover {
background: rgba(255, 251, 215, 0.5);
}
.fade-in {
animation: fadeIn 2s ease-out 4.5s forwards;
-moz-animation: fadeIn 2s ease-out 4.5s forwards;
-webkit-animation: fadeIn 2s ease-out 4.5s forwards;
}
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
@-moz-keyframes fadeIn {
0% { color: rgba(0, 0, 0, 0.1); }
100% { color: rgba(0, 0, 0, 0.85); }
}
@-webkit-keyframes fadeIn {
0% { color: rgba(0, 0, 0, 0.1); }
100% { color: rgba(0, 0, 0, 0.85); }
}
/* @media (max-width: 860px) {
a.fade-in {
display: none;
}
} */
/*
★ Move your mouse over to experience magic ★
Memphis 3D Kinetic Typography
Thanks to @prisoner849 for helping me with the marshmallow geometry.
Thanks to Davide Prati for the Palm Generator:
https://davideprati.com/projects/palm-generator.html
art & code by
Anna Scavenger, March 2020
https://twitter.com/ouchpixels
License: You can remix, adapt, and build upon my code non-commercially.
https://cdnjs.cloudflare.com/ajax/libs/three.js/r120/three.min.js
*/
import { BufferGeometryUtils } from "https://unpkg.com/three@0.120.0/examples/jsm/utils/BufferGeometryUtils.js";
'use strict';
let container, camera, scene, renderer;
let palm;
let letterMLeft, letterMRight, letterM2, sphereM2, sphere2M2;
let coneM, letterE, marshmallowH, cylinderH2, cylinderH3, cylinderH4, letterI, letterS, halfTorusS, halfTorusS2, halfTorusS3, halfTorusS4, discS2;
let halfTorus, halfTorus2, halfTorus3, torusP, sphereP;
let mouseX = 0;
let mouseY = 0;
const a = Math.PI / 2;
const matcapURL = "https://s3-us-west-2.amazonaws.com/s.cdpn.io/911157/matcapGold_256_optimized.jpg";
const isMobile = /(Android|iPhone|iOS|iPod|iPad)/i.test(navigator.userAgent);
window.onload = function() {
init();
render();
};
function init() {
container = document.querySelector("#scene-container");
scene = new THREE.Scene();
createCamera();
createLights();
createRenderer();
createGeometries();
createMeshes();
document.addEventListener("mousemove", onMouseMove, false);
document.addEventListener("touchmove", onTouch, false);
window.addEventListener("resize", onWindowResize);
}
function createCamera() {
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 2, 21);
camera.lookAt(0, 0.25, 0);
camera.aspect = window.innerWidth / window.innerHeight;
const cameraZ = 21;
if (camera.aspect < 1 && camera.aspect > 0.75) {
camera.position.z = cameraZ * 1.5;
} else if (camera.aspect < 0.75) {
camera.position.z = cameraZ * 2.4;
} else {
camera.position.z = cameraZ;
}
}
function createLights() {
const ambientLight = new THREE.HemisphereLight(0xddeeff, 0x202020, 2.25);
ambientLight.position.set(-10, 200, -1000);
const mainLight = new THREE.DirectionalLight(0xffffff, 1.0);
mainLight.position.set(-50, 100, 10);
const mainLight2 = new THREE.DirectionalLight(0xFF002d, 8);
mainLight2.position.set(50, 10, 10);
mainLight2.target.position.set(1, 100, 0);
scene.add(ambientLight, mainLight, mainLight2);
}
function createGeometries() {
const cylinder = new THREE.CylinderGeometry( 0.75, 0.75, 3, 26, 20 );
const cylinderWide = new THREE.CylinderGeometry( 1.1, 1.1, 0.75, 35, 5 );
const cylinderThin = new THREE.CylinderBufferGeometry( 0.35, 0.35, 3, 20 );
const disc = new THREE.CylinderGeometry( 0.75, 0.75, 0.25, 32 );
disc.applyMatrix4(new THREE.Matrix4().makeTranslation( 0, 0.125, 0 ));
const sphere = new THREE.SphereBufferGeometry( 0.75, 22, 22 );
const halfSphere = new THREE.SphereBufferGeometry( 0.75, 15, 15, 0, Math.PI );
const torus = new THREE.TorusBufferGeometry( 1.25, 0.25, 16, 40 );
const halfTorus = new THREE.TorusBufferGeometry( 1.25, 0.35, 16, 20, Math.PI );
const cone = new THREE.ConeBufferGeometry( 1.25, 2.25, 38 );
cone.applyMatrix4(new THREE.Matrix4().makeTranslation( 0, 1.125, 0 ));
const boxFlat = new THREE.BoxBufferGeometry( 1.25, 0.35, 1.25 );
return {
cylinder,
cylinderThin,
cylinderWide,
disc,
sphere,
halfSphere,
halfTorus,
torus,
cone,
boxFlat
};
}
function createMaterials() {
const white = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.9,
metalness: 0.2,
flatShading: false,
side: THREE.DoubleSide
});
white.color.convertSRGBToLinear();
const black = new THREE.MeshStandardMaterial({
color: 0x000000,
roughness: 0.2,
metalness: 0.6,
flatShading: false,
side: THREE.DoubleSide
});
black.color.convertSRGBToLinear();
const blue = new THREE.MeshStandardMaterial({
color: 0x8fcbea,
roughness: 0.9,
metalness: 0.1,
flatShading: false,
side: THREE.DoubleSide
});
blue.color.convertSRGBToLinear();
const pink = new THREE.MeshPhongMaterial({
color: 0xffc0dd
});
pink.color.convertSRGBToLinear();
const green2 = new THREE.MeshStandardMaterial({
color: 0xbffbcb,
roughness: 0.1,
metalness: 0.3,
flatShading: true,
side: THREE.DoubleSide
});
green2.color.convertSRGBToLinear();
const textureLoader = new THREE.TextureLoader();
const matcapTex = textureLoader.load(matcapURL);
const matcapGold = new THREE.ShaderMaterial({
transparent: false,
side: THREE.DoubleSide,
uniforms: {
tMatCap: {
type: "t",
value: matcapTex
}
},
vertexShader: document.querySelector("#matcap-vs").textContent,
fragmentShader: document.querySelector("#matcap-fs").textContent,
flatShading: false
});
return {
white,
pink,
green2,
blue,
black,
matcapGold
};
}
function createMeshes() {
const geometries = createGeometries();
const materials = createMaterials();
palm = createPalm();
scene.add(palm);
const pixelMat = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.1,
metalness: 0.25,
vertexColors: THREE.FaceColors,
flatShading: false
});
// letter M
const letterM = new THREE.Group();
const halfSphere = new THREE.Mesh(geometries.halfSphere, materials.black);
halfSphere.position.y = 3.25;
halfSphere.rotation.x = -Math.PI / 2;
halfSphere.matrixAutoUpdate = false;
halfSphere.updateMatrix();
const cylinder = new THREE.Mesh(geometries.cylinder, pixelMat);
colorVertices(geometries.cylinder);
cylinder.position.y = 1.75;
const disc = new THREE.Mesh(geometries.disc, materials.black);
disc.matrixAutoUpdate = false;
disc.updateMatrix();
const sphere = new THREE.Mesh(geometries.sphere, materials.blue);
sphere.position.y = -0.5;
sphere.matrixAutoUpdate = false;
sphere.updateMatrix();
const disc2 = new THREE.Mesh(geometries.disc, materials.matcapGold);
disc2.position.y = -1.25;
disc2.matrixAutoUpdate = false;
disc2.updateMatrix();
letterMLeft = new THREE.Group();
letterMLeft.add(halfSphere, cylinder, disc, disc2, sphere);
letterMLeft.position.x = -2;
letterMRight = letterMLeft.clone();
letterMRight.position.x = 2;
coneM = new THREE.Mesh(geometries.cone, materials.matcapGold);
coneM.rotation.x = Math.PI;
coneM.position.y = 3.25;
letterM.add(letterMLeft, coneM, letterMRight);
letterM.scale.set(1.05, 1.05, 1.05);
// letter E
letterE = new THREE.Group();
const halfTorusE = new THREE.Mesh(geometries.halfTorus, materials.white);
const halfTorus2E = halfTorusE.clone();
halfTorus2E.rotation.z = Math.PI / 2;
const halfTorus3E = halfTorusE.clone();
halfTorus3E.rotation.z = Math.PI / 2;
halfTorus3E.position.y = -2.5;
const halfTorus4E = halfTorus3E.clone();
halfTorus4E.rotation.z = Math.PI;
const discE = new THREE.Mesh(geometries.disc, materials.matcapGold);
discE.position.set(1.25, -0.1, 0);
discE.scale.set(0.45, 0.45, 0.45);
const disc2E = discE.clone();
disc2E.position.y = -2.5;
const halfSphereE = new THREE.Mesh(geometries.halfSphere, materials.black);
halfSphereE.position.set(0, -1.25, 0);
halfSphereE.scale.set(0.45, 0.45, 0.45);
halfSphereE.rotation.y = Math.PI / 2;
letterE.add(halfTorusE, halfTorus2E, halfTorus3E, halfTorus4E);
letterE.add(discE, disc2E, halfSphereE);
// letter M2
letterM2 = new THREE.Group();
halfTorus = new THREE.Mesh(geometries.halfTorus, materials.matcapGold);
halfTorus2 = halfTorus.clone();
halfTorus2.position.x = 2.5;
halfTorus3 = halfTorus.clone();
halfTorus3.position.x = 5;
const cylinderThinM = new THREE.Mesh(geometries.cylinderThin, materials.matcapGold);
cylinderThinM.position.set(1.25, -1.5, 0);
const cylinderThinM2 = cylinderThinM.clone();
cylinderThinM2.position.x = 3.75;
const cylinderThinM3 = cylinderThinM.clone();
cylinderThinM3.position.x = 6.25;
const halfTorus4 = halfTorus.clone();
halfTorus4.position.set(7.5, -3, 0);
halfTorus4.rotation.x = Math.PI;
halfTorus4.matrixAutoUpdate = false;
halfTorus4.updateMatrix();
sphereM2 = new THREE.Mesh(geometries.sphere, materials.black);
sphereM2.position.set(2.5, 4, 0);
sphereM2.scale.set(1.1, 1.1, 1.1);
sphere2M2 = new THREE.Mesh(geometries.sphere, materials.green2);
sphere2M2.scale.set(0.7, 0.7, 0.7);
sphere2M2.position.set(8.75, -2.5, 0);
const halfSphereM2 = new THREE.Mesh(geometries.halfSphere, materials.black);
halfSphereM2.rotation.x = -Math.PI / 2;
halfSphereM2.scale.set(0.45, 0.45, 0.45);
halfSphereM2.position.set(1.25, 0, 0);
const halfSphere2M2 = halfSphereM2.clone();
halfSphere2M2.position.set(3.75, 0, 0);
halfSphere2M2.matrixAutoUpdate = false;
halfSphere2M2.updateMatrix();
const halfSphere3M2 = halfSphereM2.clone();
halfSphere3M2.position.set(6.25, 0, 0);
halfSphere3M2.matrixAutoUpdate = false;
halfSphere3M2.updateMatrix();
letterM2.add(halfTorus, halfTorus2, halfTorus3, sphereM2, sphere2M2);
letterM2.add(cylinderThinM, cylinderThinM2, cylinderThinM3, halfTorus4);
letterM2.add(halfSphereM2, halfSphere2M2, halfSphere3M2);
letterM2.scale.set(0.8, 0.8, 0.8);
// letter P
const letterP = new THREE.Group();
torusP = new THREE.Mesh(geometries.torus, materials.matcapGold);
const cylinderThinP = new THREE.Mesh(geometries.cylinderThin, materials.matcapGold);
cylinderThinP.position.set(-1.25, -1.5, 0);
cylinderThinP.matrixAutoUpdate = false;
cylinderThinP.updateMatrix();
const boxFlatP = new THREE.Mesh(geometries.boxFlat, materials.black);
boxFlatP.position.set(-1.25, -3.1, 0);
boxFlatP.matrixAutoUpdate = false;
boxFlatP.updateMatrix();
sphereP = new THREE.Mesh(geometries.sphere, materials.white);
sphereP.position.set(0, -2.75, 0);
sphereP.scale.set(0.9, 0.9, 0.9);
letterP.add(torusP, sphereP);
letterP.add(cylinderThinP, boxFlatP);
letterP.scale.set(1.1, 1.1, 1.1);
// letter H
const letterH = new THREE.Group();
marshmallowH = new THREE.Mesh(createMarshmallow(), [materials.blue, materials.pink, materials.blue, materials.black]);
marshmallowH.scale.set(0.8, 0.65, 0.8);
const discH = new THREE.Mesh(geometries.disc, materials.matcapGold);
discH.scale.set(1.5, 1.5, 1.5);
discH.position.y = 2.75;
const discH2 = discH.clone();
discH2.position.y = -2;
const letterHLeft = new THREE.Group();
letterHLeft.add(marshmallowH, discH, discH2);
const letterHRight = new THREE.Group();
const cylinderVert = new THREE.Mesh(geometries.cylinderThin, materials.matcapGold);
cylinderVert.scale.set(0.35, 0.75, 0.35);
cylinderVert.position.set(0, 4, 0);
cylinderVert.matrixAutoUpdate = false;
cylinderVert.updateMatrix();
cylinderH2 = new THREE.Mesh(geometries.cylinder, pixelMat);
cylinderH2.scale.set(1.1, 1.1, 1.1);
cylinderH2.position.y = 1.25;
cylinderH3 = new THREE.Mesh(geometries.cylinderWide, pixelMat);
colorVertices(geometries.cylinderWide);
cylinderH3.position.y = -0.75;
cylinderH4 = cylinderH3.clone();
cylinderH4.scale.set(1.2, 1.2, 1.2);
cylinderH4.position.y = -1.55;
const discH3 = discH.clone();
discH3.position.set(0, 2.75, 0);
letterHRight.add(cylinderH2, cylinderH3, cylinderH4, discH3, cylinderVert);
letterHRight.position.x = 3.5;
const cylinderHoriz = new THREE.Mesh(geometries.cylinderThin, materials.matcapGold);
cylinderHoriz.rotation.z = Math.PI / 2;
cylinderHoriz.position.set( 1.8, 0.5, -0.5 );
cylinderHoriz.scale.set(1.7, 0.75, 1.7);
cylinderHoriz.matrixAutoUpdate = false;
cylinderHoriz.updateMatrix();
letterH.add(letterHLeft, letterHRight, cylinderHoriz);
// letter I
letterI = letterHLeft.clone();
letterI.children[1].material = materials.white;
letterI.children[1].matrixAutoUpdate = false;
letterI.children[1].updateMatrix();
letterI.children[2].material = materials.white;
letterI.children[2].matrixAutoUpdate = false;
letterI.children[2].updateMatrix();
// letter S
letterS = new THREE.Group();
halfTorusS = new THREE.Mesh(geometries.halfTorus, materials.green2);
halfTorusS.rotation.z = Math.PI / 2;
halfTorusS2 = halfTorusS.clone();
halfTorusS2.rotation.z = 0;
halfTorusS3 = halfTorusS.clone();
halfTorusS3.rotation.z = - Math.PI / 2;
halfTorusS3.position.y = -2.5;
halfTorusS4 = halfTorusS.clone();
halfTorusS4.rotation.z = Math.PI;
halfTorusS4.position.y = -2.5;
const discS = new THREE.Mesh(geometries.disc, materials.matcapGold);
discS.scale.set(0.75, 1, 0.75);
discS.position.set(1.25, -0.225, 0);
discS2 = discS.clone();
discS2.position.set(-1.25, -2.5, 0);
const halfSphereS = new THREE.Mesh(geometries.halfSphere, materials.matcapGold);
halfSphereS.scale.set(0.45, 0.45, 0.45);
halfSphereS.rotation.y = -Math.PI / 2;
halfSphereS.position.y = -1.25;
halfSphereS.matrixAutoUpdate = false;
halfSphereS.updateMatrix();
const halfSphere2S = halfSphereS.clone();
halfSphere2S.rotation.y = 0;
halfSphere2S.position.set(1.25, -2.5, 0);
halfSphere2S.matrixAutoUpdate = false;
halfSphere2S.updateMatrix();
letterS.add(halfTorusS, halfTorusS2, halfTorusS3, halfTorusS4, discS2);
letterS.add(halfSphereS, halfSphere2S, discS);
// position all letters
letterM.position.set(-6.75, 1.25, 0);
letterE.position.set(-1, 3.75, 0);
letterM2.position.set(3.5, 2.75, 0);
letterP.position.set(-7.9, -3.5, 0);
letterH.position.set(-4.5, -5, 0);
letterI.position.set(2.25, -5, 0);
letterS.position.set(6.5, -3, 0);
palm.position.set(2.25, -2.75, 0);
scene.add(letterM, letterE, letterM2, letterP, letterH, letterI, letterS);
}
function createMarshmallow() {
const c = new THREE.CylinderBufferGeometry(0.5, 0.5, 7, 34, 34);
c.translate(0.5, 1, 0.5);
let pos = c.attributes.position;
let vec3 = new THREE.Vector3();
let axis = new THREE.Vector3(0, 1, 0);
for (let i = 0; i < pos.count; i++) {
vec3.fromBufferAttribute(pos, i);
vec3.applyAxisAngle(axis, (vec3.y / c.parameters.height*1) * Math.PI * 2);
pos.setXYZ(i, vec3.x, vec3.y, vec3.z);
}
let geoms = [];
geoms.push(c);
let mat = new THREE.Matrix4();
for (let i = 1; i < 4; i++) {
let g = c.clone();
mat.makeRotationAxis(axis, i * Math.PI * 0.5);
mat.applyToBufferAttribute(g.attributes.position);
geoms.push(g);
}
let m = BufferGeometryUtils.mergeBufferGeometries(geoms, true);
return m;
}
function animate() {
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
function createRenderer() {
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setClearColor(0x000000, 0);
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio > 1.5 ? 1.5 : 1.2);
renderer.gammaFactor = 2.2;
renderer.outputEncoding = THREE.GammaEncoding;
renderer.physicallyCorrectLights = true;
container.appendChild(renderer.domElement);
}
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
update();
}
function update() {
// letter M
letterMLeft.children[1].rotation.y = 2 * a * mouseX;
letterMRight.children[1].rotation.y = - 2 * a * mouseX;
coneM.rotation.x = 2 * a + a * mouseX;
// letter E
letterE.rotation.y = - a * mouseX;
// letter M2
halfTorus.position.y = 0 + Math.abs(2 * mouseX);
halfTorus2.position.y = 0 + Math.abs(2 * mouseX);
(Math.abs(mouseX) < 0.075 ? halfTorus2.rotation.x = 0 : halfTorus2.rotation.x = 3.14 * mouseX);
halfTorus3.position.y = 0 + Math.abs(2 * mouseX);
sphereM2.position.y = 4 - Math.abs(2.1 * mouseX);
sphere2M2.rotation.y = 0 + mouseX;
// letter P
torusP.rotation.x = - a * mouseX;
torusP.rotation.z = - a * mouseX;
torusP.scale.set(1 - Math.abs(0.2 * mouseX),1 - Math.abs(0.2 * mouseX),1 - Math.abs(0.2 * mouseX));
sphereP.position.y = -2.75 + Math.abs(2.75 * mouseX);
sphereP.scale.set(0.9 + Math.abs(0.1 * mouseX), 0.9 + Math.abs(0.1 * mouseX),0.9 + Math.abs(0.1 * mouseX));
// letter H
marshmallowH.rotation.y = a * mouseX;
cylinderH2.rotation.y = - a * mouseX;
cylinderH3.rotation.y = a * mouseX;
cylinderH4.rotation.y = - a * mouseX;
// letter I
palm.rotation.y += 0.001 * Math.PI * 2;
letterI.children[0].rotation.y = - a * mouseX;
// letter S
halfTorusS.rotation.z = a - a * Math.abs(mouseX);
halfTorusS2.rotation.z = - a * Math.abs(mouseX);
halfTorusS3.rotation.z = - a + -a * (mouseX);
discS2.rotation.x = a / 3 * Math.abs(mouseX);
}
function onMouseMove(e) {
mouseX = (e.clientX / window.innerWidth) * 2 - 1;
mouseY = -(e.clientY / window.innerHeight) * 2 + 1;
}
function onTouch(e) {
let x = e.changedTouches[0].clientX;
let y = e.changedTouches[0].clientY;
mouseX = (x / window.innerWidth) * 2 - 1;
mouseY = (y / window.innerWidth) * 2 - 1;
}
function onWindowResize() {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
}
This Pen doesn't use any external CSS resources.