Custom Code Experiment •
JUNO: Trailing Text
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>JUNO - Trailing Text Animation</title>
<link href="https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
.stage {
position: relative;
width: 100%;
height: 100%;
}
.layer {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Great Vibes', cursive;
font-size: 28vw;
font-weight: 500;
letter-spacing: 0.02em;
line-height: 1;
-webkit-text-stroke: .3vw #111;
text-stroke: .3vw #111;
pointer-events: none;
will-change: transform;
transform-origin: center center;
}
.layer:nth-child(1) { color: #222A26; z-index: 1; }
.layer:nth-child(2) { color: #0AFF81; z-index: 2; }
.layer:nth-child(3) { color: #51ECE9; z-index: 3; }
.layer:nth-child(4) { color: #F31688; z-index: 4; }
.layer:nth-child(5) { color: #ECBD51; z-index: 5; }
</style>
</head>
<body>
<div class="stage">
<div class="layer">JUNO</div>
<div class="layer">JUNO</div>
<div class="layer">JUNO</div>
<div class="layer">JUNO</div>
<div class="layer">JUNO</div>
</div>
<script>
const layers = Array.from(document.querySelectorAll('.layer'));
const front = layers[4];
const trails = layers.slice(0, 4);
const corners = [
{ rot: 14, ty: 6 },
{ rot: -14, ty: 6 },
{ rot: -14, ty: -6 },
{ rot: 14, ty: -6 },
];
const DURATION = 7000;
const LEG = DURATION / 4;
const lagMs = [110, 230, 370, 540];
const depthScale = [1.25, 1.6, 2.05, 2.6];
function easeOvershoot(t) {
const c4 = (2 * Math.PI) / 3;
if (t === 0) return 0;
if (t === 1) return 1;
return Math.pow(2, -8 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
}
function easeInOut(t) {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
function getTransform(elapsed) {
const t_loop = ((elapsed % DURATION) + DURATION) % DURATION;
const leg = Math.floor(t_loop / LEG);
const t = easeInOut((t_loop % LEG) / LEG);
const from = corners[leg % 4];
const to = corners[(leg + 1) % 4];
return {
rot: from.rot + (to.rot - from.rot) * t,
ty: from.ty + (to.ty - from.ty) * t,
};
}
const springs = trails.map(() => ({ rot: 0, ty: 0, vRot: 0, vTy: 0 }));
const stiffness = 0.12;
const damping = 0.62;
const history = [];
const MAX_LAG = lagMs[lagMs.length - 1] + 300;
let startTime = null;
let lastTime = null;
function tick(now) {
if (!startTime) startTime = now;
if (!lastTime) lastTime = now;
const elapsed = now - startTime;
const dt = Math.min(now - lastTime, 32);
lastTime = now;
const current = getTransform(elapsed);
history.push({ time: elapsed, ...current });
while (history.length > 1 && history[0].time < elapsed - MAX_LAG) {
history.shift();
}
front.style.transform =
`rotate(${current.rot}deg) translateY(${current.ty}vh)`;
trails.forEach((el, i) => {
const targetTime = elapsed - lagMs[i];
let entry = history[0];
for (const h of history) {
if (h.time <= targetTime) entry = h;
else break;
}
const s = depthScale[i];
const targetRot = entry.rot * s;
const targetTy = entry.ty * s;
const sp = springs[i];
const fRot = (targetRot - sp.rot) * stiffness;
const fTy = (targetTy - sp.ty) * stiffness;
sp.vRot = sp.vRot * damping + fRot;
sp.vTy = sp.vTy * damping + fTy;
sp.rot += sp.vRot;
sp.ty += sp.vTy;
el.style.transform =
`rotate(${sp.rot}deg) translateY(${sp.ty}vh)`;
});
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
</script>
</body>
</html>