<section class="demo">
<div class="demo__pin">
<canvas class="demo__canvas" id="seq" width="640" height="640"></canvas>
<p class="demo__label"><span id="frame">01</span> / 48</p>
</div>
</section>.demo {
height: 280vh;
background: #08080a;
color: #f4f4f2;
font-family: "Helvetica Neue", system-ui, sans-serif;
}
.demo__pin {
position: sticky;
top: 0;
height: 100vh;
display: grid;
place-items: center;
}
.demo__canvas {
width: min(72vw, 420px);
height: auto;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.06);
}
.demo__label {
position: absolute;
bottom: 8vh;
margin: 0;
font-size: 11px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #5a5a62;
}const canvas = document.querySelector("#seq");
const ctx = canvas.getContext("2d");
const label = document.querySelector("#frame");
const FRAMES = 48;
const frames = [];
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function roundRect(c, x, y, w, h, r) {
c.beginPath();
c.moveTo(x + r, y);
c.arcTo(x + w, y, x + w, y + h, r);
c.arcTo(x + w, y + h, x, y + h, r);
c.arcTo(x, y + h, x, y, r);
c.arcTo(x, y, x + w, y, r);
c.closePath();
}
function paint(c, t) {
const w = c.canvas.width;
const h = c.canvas.height;
c.fillStyle = "#0c0c10";
c.fillRect(0, 0, w, h);
c.save();
c.translate(w / 2, h / 2);
for (let i = 0; i < 8; i += 1) {
const p = i / 8;
c.save();
c.rotate(t * Math.PI * 2 * (0.35 + p * 0.7));
const rw = 90 + p * 170;
const rh = 210 - p * 70;
c.strokeStyle = "rgba(244,244,242," + (0.08 + p * 0.1) + ")";
c.lineWidth = 1.4;
roundRect(c, -rw / 2, -rh / 2, rw, rh, 22);
c.stroke();
c.restore();
}
c.restore();
}
for (let i = 0; i < FRAMES; i += 1) {
const off = document.createElement("canvas");
off.width = canvas.width;
off.height = canvas.height;
paint(off.getContext("2d"), i / FRAMES);
frames.push(off);
}
const state = { frame: 8 };
function render() {
const index = Math.round(state.frame) % FRAMES;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(frames[index], 0, 0);
label.textContent = String(index + 1).padStart(2, "0");
}
render();
if (reduce) {
state.frame = 24;
render();
} else if (window.ScrollTrigger) {
gsap.to(state, {
frame: FRAMES - 1,
ease: "none",
onUpdate: render,
scrollTrigger: {
trigger: ".demo",
start: "top top",
end: "bottom bottom",
scrub: 0.4,
},
});
}