Paper crane
A bold orange hero with giant LAUNCH lettering where a WebGL paper crane, with a soft shadow, flaps and glides upward and passes between the back and front copies of the word, over a drifting glow and rising specks.
Assets
Code: MIT license. three.js 0.184.0 (MIT) · Imagery generated with Google Gemini
<!doctype html>
<html lang="en"
><head
><script>
/* 6to9 pattern harness */ (function () {
var parentPaused = false,
hidden = document.hidden,
p = (window.__pattern = { paused: false, reducedMotion: false, maxDpr: 2 });
function sync() {
p.paused = parentPaused || hidden;
}
sync();
try {
p.reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
} catch (e) {}
document.addEventListener("visibilitychange", function () {
hidden = document.hidden;
sync();
});
addEventListener("message", function (e) {
var d = e.data || {};
if (d.pattern === "pause") {
parentPaused = true;
sync();
}
if (d.pattern === "play") {
parentPaused = false;
sync();
}
});
})();</script
><meta charset="utf-8" /><meta
name="viewport"
content="width=device-width,initial-scale=1"
/><title>Fold — Launch</title
><script type="importmap">
{
"imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.184.0/build/three.module.js" }
}</script
><style>
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
height: 100%;
background: #ff8a1f;
overflow: hidden;
font-family: "Helvetica Neue", Helvetica, Arial, system-ui, sans-serif;
color: #120b07;
}
.hero {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background: radial-gradient(120% 90% at 50% 38%, #ffa23c 0%, #ff8a1f 55%, #f57a10 100%);
}
.glow {
position: absolute;
z-index: 0;
width: 70vmax;
height: 70vmax;
left: 50%;
top: 50%;
margin: -35vmax 0 0 -35vmax;
background: radial-gradient(circle, rgba(255, 214, 150, 0.55), rgba(255, 214, 150, 0) 60%);
animation: gl 9s ease-in-out infinite alternate;
}
@keyframes gl {
from {
transform: translate(-18vw, 8vh) scale(0.9);
}
to {
transform: translate(18vw, -8vh) scale(1.1);
}
}
.specks {
position: absolute;
inset: 0;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.specks i {
position: absolute;
bottom: -20px;
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(255, 236, 200, 0.6);
animation: rise 9s linear infinite;
}
@keyframes rise {
from {
transform: translateY(0);
}
to {
transform: translateY(-115vh);
}
}
.rm .glow,
.rm .specks i,
.rm .fb {
animation: none;
}
.pz .glow,
.pz .specks i,
.pz .fb {
animation-play-state: paused;
}
.stack {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.stack h1 {
margin: 0;
margin-top: -7vh;
font-family: "Arial Black", "Helvetica Neue", Impact, system-ui, sans-serif;
font-weight: 900;
font-size: min(21.5vw, 50vh);
line-height: 0.8;
letter-spacing: -0.06em;
color: #120b07;
text-align: center;
white-space: nowrap;
padding-right: 0.06em;
}
.stack h1 span {
display: inline;
}
.back {
z-index: 1;
}
canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 2;
display: block;
}
.front {
z-index: 3;
--y: 300vh;
-webkit-mask-image: linear-gradient(
to bottom,
transparent calc(var(--y) - 6px),
#000 calc(var(--y) + 6px)
);
mask-image: linear-gradient(
to bottom,
transparent calc(var(--y) - 6px),
#000 calc(var(--y) + 6px)
);
}
nav {
position: absolute;
z-index: 5;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 28px 4vw;
font-weight: 800;
letter-spacing: -0.02em;
}
nav .brand {
font-size: 20px;
display: flex;
align-items: center;
gap: 10px;
}
nav .brand i {
display: block;
width: 18px;
height: 18px;
background: #120b07;
clip-path: polygon(0 100%, 50% 0, 100% 100%, 50% 70%);
}
nav .m {
font-size: 14px;
font-weight: 700;
border: 2px solid #120b07;
border-radius: 99px;
padding: 8px 18px;
}
.bottom {
position: absolute;
z-index: 5;
left: 4vw;
right: 4vw;
bottom: 5vh;
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 24px;
}
.bottom p {
margin: 0;
max-width: 30ch;
font-size: clamp(16px, 1.5vw, 22px);
line-height: 1.35;
font-weight: 600;
letter-spacing: -0.01em;
}
.btn {
display: inline-flex;
align-items: center;
gap: 12px;
background: #120b07;
color: #ffa23c;
font-weight: 800;
font-size: 17px;
padding: 18px 30px;
border-radius: 99px;
text-decoration: none;
transition:
transform 0.25s ease,
box-shadow 0.25s ease;
box-shadow: 0 10px 30px rgba(90, 30, 0, 0.3);
}
.btn:hover {
transform: translateY(-3px) scale(1.03);
}
.btn b {
display: inline-block;
transition: transform 0.25s;
}
.btn:hover b {
transform: translateX(4px);
}
.fb {
display: none;
position: absolute;
z-index: 2;
left: 50%;
top: 50%;
width: min(56vw, 520px);
height: min(40vw, 380px);
margin: -20vh 0 0 -28vw;
animation: fl 6s ease-in-out infinite alternate;
}
.fb div {
position: absolute;
inset: 0;
background: repeating-linear-gradient(0deg, #e9e2d0 0 6px, #cfc7b2 6px 8px);
filter: drop-shadow(30px 50px 25px rgba(90, 30, 0, 0.35));
}
.fb .a {
clip-path: polygon(0 30%, 55% 55%, 100% 0, 62% 70%, 48% 100%, 40% 62%);
}
.fb .b {
clip-path: polygon(88% 12%, 100% 8%, 96% 22%, 70% 62%);
filter: brightness(0.85);
}
@keyframes fl {
from {
transform: translateY(60px) rotate(-3deg);
}
to {
transform: translateY(-60px) rotate(3deg);
}
}
.nogl .fb {
display: block;
}
@media (max-aspect-ratio: 1/1) {
.stack h1 {
font-size: 38vw;
line-height: 0.82;
margin-top: -10vh;
}
.stack h1 span {
display: block;
}
.bottom {
flex-direction: column;
align-items: flex-start;
gap: 20px;
bottom: 4.5vh;
}
.fb {
margin-left: -28vw;
width: 70vw;
}
}
</style></head
><body
><div class="hero" id="hero"
><div class="glow"></div
><div class="specks"
><i style="left: 8%; animation-delay: -1s; animation-duration: 8s"></i
><i
style="left: 22%; animation-delay: -5s; animation-duration: 11s; width: 6px; height: 6px"
></i
><i style="left: 37%; animation-delay: -3s; animation-duration: 9s"></i
><i
style="left: 53%; animation-delay: -7s; animation-duration: 12s; width: 7px; height: 7px"
></i
><i style="left: 68%; animation-delay: -2s; animation-duration: 10s"></i
><i
style="left: 82%; animation-delay: -6s; animation-duration: 8.5s; width: 6px; height: 6px"
></i
><i style="left: 93%; animation-delay: -4s; animation-duration: 13s"></i></div
><nav
><div class="brand"><i></i>Fold</div><div class="m">Menu</div></nav
><div class="stack back"
><h1><span>LAU</span><span>NCH</span></h1></div
><canvas id="c"></canvas
><div class="stack front" id="front" aria-hidden="true"
><h1><span>LAU</span><span>NCH</span></h1></div
><div class="fb"><div class="a"></div><div class="b"></div></div
><div class="bottom"
><p
>Fold your ideas into flight. A calm, lightweight toolkit for shipping what comes next.</p
><a class="btn" href="#">Start folding <b>→</b></a></div
></div
><script type="module">
const P = window.__pattern || { paused: false, reducedMotion: false, maxDpr: 2 };
const hero = document.getElementById("hero");
const front = document.getElementById("front");
const canvas = document.getElementById("c");
if (P.reducedMotion) hero.classList.add("rm");
function fallback() {
hero.classList.add("nogl");
canvas.style.display = "none";
}
setInterval(function () {
hero.classList.toggle("pz", !!P.paused);
}, 200);
async function boot() {
let THREE;
try {
THREE = await import("three");
} catch (e) {
fallback();
return;
}
try {
init(THREE);
} catch (e) {
fallback();
}
}
function makeStrip() {
const c = document.createElement("canvas");
c.width = 2360;
c.height = 258;
const x = c.getContext("2d");
for (let i = 0; i < 8; i++) {
const fl = Math.sin((i / 8) * Math.PI * 2);
x.save();
x.translate(i * 295 + 147, 135);
const wy = -70 - fl * 45;
x.fillStyle = "#efe8d6";
x.strokeStyle = "#8a8372";
x.lineWidth = 2;
x.beginPath();
x.moveTo(-10, 0);
x.lineTo(-95, wy);
x.lineTo(35, -8);
x.closePath();
x.fill();
x.stroke();
x.fillStyle = "#e0d8c2";
x.beginPath();
x.moveTo(-110, 38);
x.lineTo(-10, -8);
x.lineTo(60, 12);
x.lineTo(95, -48);
x.lineTo(104, -44);
x.lineTo(70, 24);
x.lineTo(-20, 40);
x.closePath();
x.fill();
x.stroke();
x.fillStyle = "#f5f0e2";
x.beginPath();
x.moveTo(5, 0);
x.lineTo(110, wy + 10);
x.lineTo(50, 10);
x.closePath();
x.fill();
x.stroke();
x.fillStyle = "#3a362c";
for (let k = 0; k < 5; k++) x.fillRect(-70 + k * 14, 12 + k * 2 - k * 0, 10, 2);
x.restore();
}
return c;
}
function init(THREE) {
const renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true, antialias: true });
renderer.setClearColor(0x000000, 0);
const scene = new THREE.Scene();
const cam = new THREE.OrthographicCamera(-1, 1, 1, -1, -1000, 1000);
const vert =
"uniform float uTime;varying vec2 vUv;void main(){vUv=uv;vec3 p=position;float ax=abs(p.x)*2.0;float flap=sin(uTime*2.4);p.y+=flap*0.045*pow(ax,1.6);p.y+=sin(uTime*1.6+p.x*6.0)*0.01;p.x+=sin(uTime*1.1+p.y*4.0)*0.008;p.x*=1.0-0.02*flap*ax;gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.0);}";
const frag =
"uniform sampler2D uMap;uniform float uFrame;uniform float uShadow;uniform float uOpacity;varying vec2 vUv;vec4 fr(float i,vec2 uv){float x=(mod(i,8.0)+clamp(uv.x,0.003,0.997))/8.0;return texture2D(uMap,vec2(x,clamp(uv.y,0.0,1.0)));}vec4 mf(vec2 uv){float f=uFrame;float i=floor(f);float m=smoothstep(0.55,1.0,fract(f));vec4 a=fr(i,uv);vec4 b=fr(i+1.0,uv);float al=mix(a.a,b.a,m);vec3 c=mix(a.rgb*a.a,b.rgb*b.a,m);return vec4(c/max(al,0.0001),al);}void main(){if(uShadow>0.5){float s=0.0;for(int k=0;k<12;k++){float a=float(k)*2.39996;float r=sqrt(float(k)+0.5)/3.5*0.04;vec2 o=vec2(cos(a),sin(a))*r;vec4 t=mf(vUv+o);float inside=step(0.0,vUv.x+o.x)*step(vUv.x+o.x,1.0);s+=t.a*inside;}s/=12.0;gl_FragColor=vec4(0.25,0.07,0.0,s*0.34*uOpacity);}else{vec4 c=mf(vUv);gl_FragColor=vec4(c.rgb,c.a*uOpacity);}}";
let loaded = false;
let dirty = true;
let mC = null,
mS = null;
const tex = new THREE.TextureLoader().setCrossOrigin("anonymous").load(
"https://agents-dev.6to9.ai/visuals/ed56d36c3c1eb464dc62253585a8c493.png",
function () {
loaded = true;
dirty = true;
},
undefined,
function () {
useFallbackTex();
},
);
tex.minFilter = THREE.LinearFilter;
tex.magFilter = THREE.LinearFilter;
tex.generateMipmaps = false;
function useFallbackTex() {
const t2 = new THREE.CanvasTexture(makeStrip());
t2.minFilter = THREE.LinearFilter;
t2.magFilter = THREE.LinearFilter;
t2.generateMipmaps = false;
if (mC) {
mC.uniforms.uMap.value = t2;
mS.uniforms.uMap.value = t2;
}
loaded = true;
dirty = true;
}
function mk(sh) {
return new THREE.ShaderMaterial({
vertexShader: vert,
fragmentShader: frag,
transparent: true,
depthTest: false,
depthWrite: false,
uniforms: {
uMap: { value: tex },
uTime: { value: 0 },
uFrame: { value: 0 },
uShadow: { value: sh },
uOpacity: { value: 0 },
},
});
}
const geo = new THREE.PlaneGeometry(1, 1, 32, 24);
mC = mk(0);
mS = mk(1);
const crane = new THREE.Mesh(geo, mC);
const shadow = new THREE.Mesh(geo, mS);
shadow.renderOrder = 0;
crane.renderOrder = 1;
scene.add(shadow);
scene.add(crane);
let vw = 1,
vh = 1,
W = 1,
H = 1;
function resize() {
vw = window.innerWidth;
vh = window.innerHeight;
const dpr = Math.min(window.devicePixelRatio || 1, (P && P.maxDpr) || 2);
renderer.setPixelRatio(dpr);
renderer.setSize(vw, vh, false);
cam.left = -vw / 2;
cam.right = vw / 2;
cam.top = vh / 2;
cam.bottom = -vh / 2;
cam.updateProjectionMatrix();
const portrait = vw < vh;
W = portrait ? vw * 1.05 : Math.min(vw * 0.44, (vh * 0.78 * 295) / 258);
H = (W * 258) / 295;
dirty = true;
}
window.addEventListener("resize", resize);
resize();
let op = 0;
function update(t, dt) {
const portrait = vw < vh;
const p = (t / 11) % 1;
const yc = -vh / 2 - H * 0.55 + p * (vh + H * 1.1);
const amp = portrait ? 0.08 : 0.14;
const xc = Math.sin(p * Math.PI * 1.8 + 0.5) * vw * amp;
const rot = Math.cos(p * Math.PI * 1.8 + 0.5) * 0.1 + Math.sin(t * 0.9) * 0.035;
crane.position.set(xc, yc, 0);
crane.scale.set(W, H, 1);
crane.rotation.z = rot;
shadow.position.set(xc + W * 0.07, yc - H * 0.22, 0);
shadow.scale.set(W * 1.05, H * 1.05, 1);
shadow.rotation.z = rot;
const f = (t * 8) % 8;
for (const m of [mC, mS]) {
m.uniforms.uTime.value = t;
m.uniforms.uFrame.value = f;
}
op = loaded ? Math.min(1, op + dt * 1.2) : 0;
if (P.reducedMotion && loaded) op = 1;
mC.uniforms.uOpacity.value = op;
mS.uniforms.uOpacity.value = op;
front.style.setProperty("--y", vh / 2 - yc - H * 0.08 + "px");
}
let t = 0.5,
last = performance.now();
const still = 5.5;
function frame(now) {
requestAnimationFrame(frame);
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
if (P.reducedMotion) {
if (dirty) {
update(still, 1);
renderer.render(scene, cam);
dirty = false;
}
return;
}
if (P.paused) return;
t += dt;
update(t, dt);
renderer.render(scene, cam);
}
requestAnimationFrame(frame);
}
boot();
</script></body
></html
>
Made by 6to9. Back to all motion patterns.