모션·인터랙션 / Cadence
Prompt Intro
로딩 · 페이지 전환 · 텍스트 모션. 미리보기와 원본 코드·프롬프트를 이곳에서 확인하세요.
미리보기 안에서 스크롤하거나 마우스를 움직여 모션을 확인하세요.
HTML · CSS · JavaScript 전체 코드
<!doctype html>
<!--
Prompt Intro
An opening transition. A prompt box comes in at the centre and a prompt types itself
into it; a pointer curves in and clicks Generate; the box glows (two lights running
clockwise round its rim, a breathing halo, one wave out from the edge), then folds into
the centre and the page bursts open from there: a rounded clip grows from the folded
box to the whole screen while the photo settles from 115%, and the hero text rises in.
As a demo it loops (the page holds, fades back to white, the intro plays again); click
anywhere to replay. Every value is read off one clock in render(t).
Dependencies: none (plain HTML, CSS and JS).
Tweak (in the script):
- PROMPT / CPS: the prompt and its typing speed (22 characters a second)
- BLINK: caret cycle (1.06s); IN / IN_DY: box in over 0.1–0.7s from 24px below, expo out
- CLICK_AFTER: 0.26s from the last character to the click; the pointer travels 0.4s, ease in-out
- BURST_AFTER: 1.14s from the click to the burst; BURST: 1.2s, expo out
- FOLD / FOLD_SCALE: the box folds from 0.18s before to 0.06s after the burst, to scale 0.1
- SPIN: 470° the rim lights travel; WAVE: 46px wave over 0.8s
- PHOTO_ZOOM: 1.15 → 1; TEXT_AFTER / TEXT_STAGGER / TEXT_IN / TEXT_DY: 0.6s, 0.12s, 0.9s, 24px
- HOLD / BACK: 2.5s hold, 0.6s fade back (the demo loop; drop the loop for a one-off intro)
From the "Design with Skills" explainer, animation 05 (own project).
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prompt Intro</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; margin: 0; overflow: hidden; }
body { background: #fff; color: #000; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
.pi-stage {
position: fixed;
inset: 0;
overflow: hidden;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
background: #fff;
}
/* The intro: the prompt box at the centre of a white ground */
.pi-intro { position: absolute; inset: 0; display: grid; place-items: center; padding: 0 16px; }
.pi-wrap { position: relative; width: min(820px, 100%); opacity: 0; will-change: transform, opacity; }
.pi-box {
position: relative;
padding: clamp(18px, 2.1vw, 30px) clamp(20px, 2.2vw, 32px) clamp(16px, 1.7vw, 24px);
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: clamp(16px, 1.53vw, 22px);
background: #fff;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.08);
}
/* The field keeps the height of the whole prompt, so it never jumps as lines wrap */
.pi-field { display: grid; font-size: clamp(18px, 2.1vw, 30px); line-height: 1.4; letter-spacing: -0.015em; }
.pi-ghost, .pi-text { grid-area: 1 / 1; }
.pi-ghost { visibility: hidden; }
.pi-text i { font-style: normal; color: #8e8e8e; }
.pi-caret { display: inline-block; width: 2px; height: 1.05em; margin: 0 1px; background: #000; vertical-align: -0.17em; }
.pi-row { display: flex; justify-content: flex-end; margin-top: clamp(18px, 2.1vw, 30px); }
.pi-btn {
display: flex;
align-items: center;
gap: clamp(7px, 0.7vw, 10px);
height: clamp(44px, 3.9vw, 56px);
padding: 0 clamp(18px, 1.8vw, 26px) 0 clamp(15px, 1.5vw, 22px);
border-radius: clamp(11px, 1vw, 14px);
background: #000;
color: #fff;
font-size: clamp(15px, 1.46vw, 21px);
font-weight: 500;
letter-spacing: -0.01em;
will-change: transform;
}
.pi-btn svg { width: 0.95em; height: 0.95em; fill: #fff; }
/* The glow on Generate: a blurred halo, a crisp rim 2px outside the border, one wave */
.pi-glow, .pi-rim, .pi-wave { position: absolute; pointer-events: none; opacity: 0; }
.pi-glow { inset: -10px; border-radius: 32px; filter: blur(26px); }
.pi-rim { inset: -2px; border-radius: calc(clamp(16px, 1.53vw, 22px) + 2px); }
.pi-wave { inset: -1px; border: 1.5px solid #8e8e8e; border-radius: calc(clamp(16px, 1.53vw, 22px) + 1px); }
.pi-ptr {
position: absolute; top: 0; left: 0; z-index: 3;
width: 30px; height: 40px;
opacity: 0; pointer-events: none;
transform-origin: 2.5px 2.5px;
will-change: transform, opacity;
}
.pi-ptr svg { display: block; width: 30px; height: 40px; overflow: visible; }
/* The page the intro opens onto: a full-screen photograph and a wordmark */
.pi-page { position: absolute; inset: 0; z-index: 2; overflow: hidden; visibility: hidden; background: #000; will-change: clip-path, opacity; }
.pi-photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; will-change: transform; }
.pi-shade { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 60%); }
.pi-hero {
position: absolute;
left: clamp(16px, 2.5vw, 32px);
right: clamp(16px, 2.5vw, 32px);
bottom: clamp(28px, 6vh, 64px);
display: flex; flex-direction: column; align-items: flex-start;
color: #fff;
}
.pi-hero > span { display: block; opacity: 0; will-change: transform, opacity; }
.pi-eyebrow { font-size: 12px; font-weight: 500; letter-spacing: 0.25em; text-transform: uppercase; line-height: 1.6; color: rgba(255, 255, 255, 0.8); }
.pi-mark { margin: 0.04em 0 0.12em -0.05em; font-size: clamp(64px, 14vw, 200px); font-weight: 500; letter-spacing: -0.04em; line-height: 1; }
.pi-line { font-size: clamp(16px, 1.6vw, 22px); line-height: 1.4; color: rgba(255, 255, 255, 0.85); }
</style>
</head>
<body>
<div class="pi-stage" id="stage" role="img" aria-label="Intro: a prompt is typed and sent, the box glows, then folds away as the page opens from the centre">
<div class="pi-intro" aria-hidden="true">
<div class="pi-wrap" id="pwrap">
<div class="pi-glow" id="pGlow"></div>
<div class="pi-rim" id="pRim"></div>
<div class="pi-wave" id="pWave"></div>
<div class="pi-box" id="pBox">
<div class="pi-field">
<div class="pi-ghost" id="pGhost"></div>
<div class="pi-text" id="pText"></div>
</div>
<div class="pi-row">
<div class="pi-btn" id="pBtn">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 1.5 C12.9 7.6 16.4 11.1 22.5 12 C16.4 12.9 12.9 16.4 12 22.5 C11.1 16.4 7.6 12.9 1.5 12 C7.6 11.1 11.1 7.6 12 1.5 Z"/></svg>
<span>Generate</span>
</div>
</div>
</div>
</div>
<div class="pi-ptr" id="ptr"><svg viewBox="0 0 24 32" aria-hidden="true"><path d="M2 2 L2 25 L8 19.5 L12 28.5 L15.5 27 L11.6 18.2 L19.5 18.2 Z" fill="#000" stroke="#fff" stroke-width="1.6" stroke-linejoin="round"/></svg></div>
</div>
<div class="pi-page" id="page">
<!-- Swap the picsum.photos placeholder for your own landscape (3:2) photo. -->
<img class="pi-photo" id="photo" src="https://picsum.photos/seed/cadence-1/2400/1600" alt="" width="2400" height="1600" draggable="false">
<div class="pi-shade"></div>
<div class="pi-hero">
<span class="pi-eyebrow" id="hEyebrow">A photo studio</span>
<span class="pi-mark" id="hMark">Northlight</span>
<span class="pi-line" id="hLine">Quiet pictures, carefully made.</span>
</div>
</div>
</div>
<script>
(function () {
"use strict";
/* Constants */
const PROMPT = "Make a calm editorial site for a photo studio named Northlight.";
const PLACEHOLDER = "Describe what you want to make";
const CPS = 22; // characters a second
const BLINK = 1.06; // s, the caret's cycle (on for half)
const IN = [0.1, 0.7]; // s, the box comes in…
const IN_DY = 24; // px, …from this far below
const T_TYPE = 0.8; // s, typing starts
const CLICK_AFTER = 0.26; // s from the last character to the click
const BURST_AFTER = 1.14; // s from the click to the burst
const BURST = 1.2; // s, the burst
const FOLD = [-0.18, 0.06]; // s around the burst: the box folds into the centre…
const FOLD_SCALE = 0.1; // …to this scale
const PTR_FROM = [250, 200]; // px, the pointer's start from the button (at an 880px-wide box)
const PTR_VIA = [150, 40]; // px, the curve's control point
const PTR_TIP = [12, 6]; // px, the tip just off the button's centre
const SPIN = 470; // degrees the lights run, from the click to just after the burst
const WAVE = 46; // px, the wave's reach
const PRESS_GREY = [58, 58, 58]; // the button's pressed colour
const BURST_FROM = 0.12; // the page opens from the folded box at this scale
const PHOTO_ZOOM = 1.15; // the photo settles from this scale
const TEXT_AFTER = 0.6; // s after the burst starts, the hero text rises…
const TEXT_STAGGER = 0.12; // s, …one line after another…
const TEXT_IN = 0.9; // s, …each over this long…
const TEXT_DY = 24; // px, …from this far below
const HOLD = 2.5; // s, the page held before the loop
const BACK = 0.6; // s, the page fades back to white
/* Derived beats */
const T_TYPED = T_TYPE + PROMPT.length / CPS;
const T_CLICK = T_TYPED + CLICK_AFTER;
const T_BURST = T_CLICK + BURST_AFTER;
const T_TEXT = T_BURST + TEXT_AFTER;
const T_DONE = T_TEXT + 2 * TEXT_STAGGER + TEXT_IN;
const T_BACK = T_DONE + HOLD;
const T_LOOP = T_BACK + BACK;
/* Helpers */
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
const lerp = (a, b, p) => a + (b - a) * p;
const seg = (t, a, b) => clamp((t - a) / (b - a), 0, 1);
const smooth = (x) => x * x * (3 - 2 * x);
const expoOut = (x) => (x >= 1 ? 1 : 1 - Math.pow(2, -10 * x));
const expo = (u) => 1 - Math.pow(2, -10 * clamp(u, 0, 1));
const inOut = (u) => (u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2);
const mix = (A, B, p) => [lerp(A[0], B[0], p), lerp(A[1], B[1], p), lerp(A[2], B[2], p)];
const rgb = (c) => "rgb(" + Math.round(c[0]) + "," + Math.round(c[1]) + "," + Math.round(c[2]) + ")";
/* Elements */
const $ = (id) => document.getElementById(id);
const stage = $("stage"), wrap = $("pwrap"), box = $("pBox"), btn = $("pBtn");
const text = $("pText"), ghost = $("pGhost"), ptr = $("ptr");
const rim = $("pRim"), halo = $("pGlow"), wave = $("pWave");
const page = $("page"), photo = $("photo");
const heroLines = [$("hEyebrow"), $("hMark"), $("hLine")];
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
ghost.textContent = PROMPT;
/* The glow: two lights running clockwise round the rim, each a bright head with a tail, over a faint even rim */
const cone = (a, k, base) =>
"conic-gradient(from " + a.toFixed(2) + "deg at 50% 50%, rgba(0,0,0,0) 0deg, rgba(0,0,0,0) 70deg, rgba(0,0,0," + k +
") 172deg, rgba(0,0,0,0) 180deg, rgba(0,0,0,0) 250deg, rgba(0,0,0," + k + ") 352deg, rgba(0,0,0,0) 360deg), linear-gradient(rgba(0,0,0," +
base + "), rgba(0,0,0," + base + "))";
const RIM = [0.85, 0.12]; // head, base
const HALO = [0.3, 0.05];
/* Layout (in the stage's pixels, measured with the box at rest) */
let geo = null;
function layout() {
const prev = wrap.style.transform;
wrap.style.transform = "none";
const s = stage.getBoundingClientRect();
const b = box.getBoundingClientRect();
const k = btn.getBoundingClientRect();
wrap.style.transform = prev;
const u = b.width / 880;
const cx = b.left + b.width / 2 - s.left, cy = b.top + b.height / 2 - s.top;
const bx = k.left + k.width / 2 - s.left + PTR_TIP[0] * u, by = k.top + k.height / 2 - s.top + PTR_TIP[1] * u;
// the lights set off from the button: its bearing from the box's centre, clockwise from the top
const bearing = (Math.atan2(k.left + k.width / 2 - s.left - cx, -(k.top + k.height / 2 - s.top - cy)) * 180) / Math.PI;
geo = { u, cx, cy, bw: b.width, bh: b.height, sw: s.width, sh: s.height, bx, by, spin0: bearing - 178, radius: parseFloat(getComputedStyle(box).borderTopLeftRadius) };
}
/* render(t): every value read off one clock */
let lastHTML = null;
const CARET = '<span class="pi-caret"></span>';
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<");
function promptAt(t) {
const n = Math.round(PROMPT.length * seg(t, T_TYPE, T_TYPED));
const typing = t > T_TYPE && t < T_TYPED, since = t < T_TYPE ? t : t - T_TYPED;
const caret = t < T_CLICK && (typing || since % BLINK < BLINK / 2);
const html = n ? esc(PROMPT.slice(0, n)) + (caret ? CARET : "") : (caret ? CARET : "") + "<i>" + PLACEHOLDER + "</i>";
if (html !== lastHTML) { text.innerHTML = html; lastHTML = html; }
}
function render(t) {
const g = geo;
/* the prompt: in, typed, the pointer to Generate and a click; the glow; then it folds into the centre */
promptAt(t);
const pIn = seg(t, IN[0], IN[1]), fold = seg(t, T_BURST + FOLD[0], T_BURST + FOLD[1]);
wrap.style.opacity = (smooth(pIn) * (1 - smooth(seg(fold, 0.4, 1)))).toFixed(3);
wrap.style.transform = "translate3d(0," + ((1 - expoOut(pIn)) * IN_DY).toFixed(2) + "px,0) scale(" + lerp(1, FOLD_SCALE, inOut(fold)).toFixed(4) + ")";
const press = smooth(seg(t, T_CLICK, T_CLICK + 0.08)) * (1 - smooth(seg(t, T_CLICK + 0.1, T_CLICK + 0.26)));
btn.style.transform = "scale(" + (1 - 0.05 * press) + ")";
btn.style.background = rgb(mix([0, 0, 0], PRESS_GREY, press));
const mv = inOut(seg(t, T_CLICK - 0.46, T_CLICK - 0.06));
const qx = g.bx + PTR_FROM[0] * g.u, qy = g.by + PTR_FROM[1] * g.u, vx = g.bx + PTR_VIA[0] * g.u, vy = g.by + PTR_VIA[1] * g.u;
const px = lerp(lerp(qx, vx, mv), lerp(vx, g.bx, mv), mv), py = lerp(lerp(qy, vy, mv), lerp(vy, g.by, mv), mv);
ptr.style.opacity = (smooth(seg(t, T_CLICK - 0.46, T_CLICK - 0.3)) * (1 - smooth(seg(t, T_CLICK + 0.14, T_CLICK + 0.3)))).toFixed(3);
ptr.style.transform = "translate3d(" + (px - 2.5).toFixed(2) + "px," + (py - 2.5).toFixed(2) + "px,0) scale(" + (1 - 0.1 * press) + ")";
/* the glow: up in 0.3s from the click, breathing a little while the lights run */
const glow = smooth(seg(t, T_CLICK + 0.02, T_CLICK + 0.32));
if (glow > 0) {
const spin = g.spin0 + SPIN * seg(t, T_CLICK, T_BURST + 0.06), breath = 0.9 + 0.1 * Math.sin((t - T_CLICK) * Math.PI * 2.6);
rim.style.background = cone(spin, RIM[0], RIM[1]);
rim.style.opacity = glow;
halo.style.background = cone(spin, HALO[0], HALO[1]);
halo.style.opacity = glow * breath;
} else {
rim.style.opacity = 0;
halo.style.opacity = 0;
}
const wu = seg(t, T_CLICK + 0.02, T_CLICK + 0.8), wr = 1 + WAVE * expoOut(wu);
wave.style.inset = -wr + "px";
wave.style.borderRadius = g.radius + wr + "px";
wave.style.opacity = wu > 0 && wu < 1 ? 0.7 * (1 - smooth(wu)) : 0;
box.style.borderColor = "rgba(0,0,0," + lerp(0.12, 0.45, glow).toFixed(3) + ")";
box.style.boxShadow = "0 30px 80px rgba(0,0,0," + (0.08 * (1 - glow)).toFixed(3) + ")";
/* the page bursts out of the fold: a rounded clip from the folded box to the whole screen */
const burst = expo(seg(t, T_BURST, T_BURST + BURST));
if (t < T_BURST) {
page.style.visibility = "hidden";
} else {
page.style.visibility = "visible";
const w = lerp(g.bw * BURST_FROM, g.sw, burst), h = lerp(g.bh * BURST_FROM, g.sh, burst);
const x = lerp(g.cx - (g.bw * BURST_FROM) / 2, 0, burst), y = lerp(g.cy - (g.bh * BURST_FROM) / 2, 0, burst);
const r = lerp(g.radius, 0, burst);
page.style.clipPath =
"inset(" + y.toFixed(2) + "px " + (g.sw - x - w).toFixed(2) + "px " + (g.sh - y - h).toFixed(2) + "px " + x.toFixed(2) + "px round " + r.toFixed(2) + "px)";
photo.style.transform = "scale(" + lerp(PHOTO_ZOOM, 1, burst).toFixed(4) + ")";
}
page.style.opacity = (1 - smooth(seg(t, T_BACK, T_BACK + BACK))).toFixed(3);
heroLines.forEach((el, i) => {
const a = seg(t, T_TEXT + i * TEXT_STAGGER, T_TEXT + i * TEXT_STAGGER + TEXT_IN);
el.style.opacity = smooth(a).toFixed(3);
el.style.transform = "translate3d(0," + ((1 - expoOut(a)) * TEXT_DY).toFixed(2) + "px,0)";
});
}
/* The clock */
let t = 0, last = performance.now();
function frame(now) {
const dt = Math.min((now - last) / 1000, 1 / 20);
last = now;
t += dt;
if (t >= T_LOOP) t = 0;
render(t);
requestAnimationFrame(frame);
}
layout();
// Reduced motion: show the page as it ends, still.
if (reduced) {
render(T_DONE);
window.addEventListener("resize", () => { layout(); render(T_DONE); });
return;
}
render(0);
window.addEventListener("resize", () => { layout(); render(t); });
// Click: replay. Once the page is open it fades back to white first, then the intro plays.
function replay() {
t = t >= T_BURST && t < T_BACK ? T_BACK : 0;
render(t);
}
stage.addEventListener("click", replay);
// The clock waits for the photo (and the font, for the field's height), at most 4s.
const ready = Promise.all([
photo.decode ? photo.decode().catch(() => {}) : Promise.resolve(),
document.fonts ? document.fonts.ready : Promise.resolve(),
]);
Promise.race([ready, new Promise((r) => setTimeout(r, 4000))]).then(() => {
layout();
last = performance.now();
requestAnimationFrame(frame);
});
})();
</script>
<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/vce701823e78c4301a35b7e07294d87301792088814092" integrity="sha512-UKNk8GbxkABxKcXYuLkO16EleLGUkcvJWtpSNmExE0Too3ZaFAxTscNWEc1OdAv5/BikHawapSCaiD2XCmp+Bw==" data-cf-beacon='{"version":"2024.11.0","token":"c54fa41b83884ade97f239ecb41aee46","r":1,"spa":2}' crossorigin="anonymous"></script>
</body>
</html>
원본 구현 프롬프트
# Prompt Intro: a prompt is typed and sent, then the box folds away as the page bursts open
**Goal:** build an opening transition where a prompt box types a prompt, a pointer clicks *Generate*, the box glows, then folds into the centre while the real page opens out of that point.
## Visual behaviour
Everything is driven by one clock `t` (seconds from the start), and every value is computed from `t` each frame (`render(t)`), so the sequence can be scrubbed, replayed or looped.
1. **Box in (0.1 s → 0.7 s).** A white prompt box (hairline border, soft shadow, large radius) fades in at the centre of a white screen and rises from 24 px below to its place. Opacity uses smoothstep, the rise uses expo-out.
2. **Placeholder and caret.** Until typing starts, the field shows a grey placeholder ("Describe what you want to make") with a 2 px black caret in front of it, blinking on a 1.06 s cycle (on for the first half).
3. **Typing (from 0.8 s).** The prompt types itself at **22 characters per second** (the number of characters shown is `round(length × progress)`). The caret stays solid while typing, then resumes blinking. The field is sized to the full prompt from the start (an invisible copy of the text in the same grid cell), so the box never grows as lines wrap.
4. **Pointer and click.** Let `T_typed = 0.8 + length / 22` and `T_click = T_typed + 0.26`.
- A black arrow pointer (white outline) fades in from `T_click − 0.46` to `T_click − 0.30` and travels along a quadratic Bézier from `T_click − 0.46` to `T_click − 0.06` (cubic in-out). Start point: 250 px right and 200 px below the button's centre; control point: 150 px right, 40 px below; end: 12 px right and 6 px below the button's centre. (Offsets are for an 880 px wide box; scale them with the box width.)
- On the click the button dips: scale 1 → 0.95 and its colour black → `rgb(58,58,58)` over 0.08 s, back over `T_click + 0.10 → +0.26` (smoothstep). The pointer scales to 0.9 with it, then fades out from `T_click + 0.14` to `+0.30`. The caret disappears at the click.
5. **Glow (from the click).** Three layers sit behind the box and fade up from `T_click + 0.02` to `+0.32`:
- **Rim:** 2 px outside the border. Background: a conic gradient with two bright heads opposite each other, each with a tail behind it, over a faint flat rim: `conic-gradient(from A, transparent 0°, transparent 70°, rgba(0,0,0,.85) 172°, transparent 180°, transparent 250°, rgba(0,0,0,.85) 352°, transparent 360°)` over `rgba(0,0,0,.12)`.
- **Halo:** the same gradient (heads 0.3, base 0.05), 10 px outside the box, `filter: blur(26px)`, its opacity breathing `0.9 + 0.1 × sin((t − T_click) × 2.6π)`.
- The angle `A` starts so the heads set off from the button (the button's bearing from the box centre, minus 178°) and turns **470° clockwise** between the click and 0.06 s after the burst, linearly.
- **Wave:** a 1.5 px grey (`#8e8e8e`) outline expands from the border to 46 px outside it over 0.8 s (expo-out), opacity 0.7 fading to 0 (smoothstep). Its radius grows with it.
- The box border strengthens from 12% to 45% black and its drop shadow fades out.
6. **Fold.** `T_burst = T_click + 1.14`. From `T_burst − 0.18` to `T_burst + 0.06` the whole box scales 1 → 0.1 about its centre (cubic in-out), fading out over the last 60% of that.
7. **Burst (T_burst → T_burst + 1.2 s, expo-out: `1 − 2^(−10u)`).** The page underneath (a full-screen photo with a dark gradient at the bottom and hero text) becomes visible inside a rounded `clip-path: inset(... round r)`. The clip starts as the box's rectangle at 12% of its size, centred on the box, with the box's corner radius, and grows to the full viewport with radius 0. The photo inside settles from scale 1.15 to 1 on the same curve.
8. **Hero text.** From `T_burst + 0.6`, three lines (eyebrow, big wordmark, one-line tagline) rise in one after another, 0.12 s apart, each over 0.9 s: opacity 0 → 1 (smoothstep), 24 px → 0 (expo-out).
9. **Loop (demo only).** The page holds 2.5 s after the last line lands, fades to white over 0.6 s (smoothstep), and the sequence starts again. Clicking anywhere replays: before the burst it restarts at 0; once the page is open it jumps to the fade-back first.
## Exact values
| What | Value |
|---|---|
| Box in | 0.1–0.7 s, 24 px rise, expo-out (opacity smoothstep) |
| Caret | 2 px wide, 1.05 em tall, blink 1.06 s (50% on) |
| Typing | starts 0.8 s, 22 chars/s |
| Click | 0.26 s after the last character |
| Pointer travel | 0.4 s, cubic in-out, quadratic Bézier |
| Button press | scale 0.95, black → rgb(58,58,58) |
| Glow up | 0.3 s after the click |
| Rim lights | 470° clockwise, click → burst + 0.06 s |
| Wave | 46 px, 0.8 s, expo-out, opacity 0.7 → 0 |
| Burst | 1.14 s after the click, 1.2 s, expo-out (≈ `cubic-bezier(0.16, 1, 0.3, 1)`) |
| Fold | −0.18 s → +0.06 s around the burst, scale 1 → 0.1 |
| Clip start | 12% of the box size, box radius → 0 |
| Photo | scale 1.15 → 1 |
| Hero text | +0.6 s after burst, 0.12 s stagger, 0.9 s each, 24 px |
| Loop | hold 2.5 s, fade 0.6 s |
Easing helpers: `smooth(x) = x²(3 − 2x)`, `expoOut(x) = 1 − 2^(−10x)`, cubic in-out `x < .5 ? 4x³ : 1 − (−2x + 2)³ / 2`.
## Markup
```html
<div class="stage"> <!-- fixed, full screen, white -->
<div class="intro"> <!-- grid, centres the box -->
<div class="wrap"> <!-- width min(820px, 100%); gets opacity + transform -->
<div class="glow"></div> <div class="rim"></div> <div class="wave"></div>
<div class="box">
<div class="field"><div class="ghost"></div><div class="text"></div></div>
<div class="row"><div class="btn">✦ Generate</div></div>
</div>
</div>
<div class="pointer"><svg>…arrow…</svg></div>
</div>
<div class="page"> <!-- absolute, inset 0, above the intro, visibility hidden until the burst -->
<img class="photo"> <div class="shade"></div>
<div class="hero"><span>Eyebrow</span><span>Wordmark</span><span>Tagline</span></div>
</div>
</div>
```
- Field: one grid cell holding a hidden copy of the full prompt (`visibility: hidden`) and the visible typed text, so the height is fixed. Font size `clamp(18px, 2.1vw, 30px)`, line-height 1.4.
- Box padding and radius scale with the viewport (`clamp(16px, 1.53vw, 22px)` radius). Generate: a black pill, white label and a four-point sparkle icon.
- Measure the box, button and stage once (with the box's transform removed) and on resize; all positions above are relative to those measurements.
## Libraries
None. A `requestAnimationFrame` loop advances `t` by the frame delta (capped at 1/20 s so a background tab doesn't jump) and calls `render(t)`. Wait for the photo to decode (`img.decode()`) and for fonts (`document.fonts.ready`), at most 4 s, before starting the clock so nothing pops in mid-sequence. GSAP timelines would work too, but a single `render(t)` keeps every layer in sync and makes replay trivial.
## Responsive and reduced motion
- The box is `min(820px, 100%)` with 16 px side padding; type, padding and pointer offsets scale with the box width. Re-measure on resize and re-render at the current `t`.
- `prefers-reduced-motion: reduce`: no animation. Render the final state (page open, text in place) once and keep it.
## Acceptance criteria
- The box rises in, the placeholder shows with a blinking caret, then the prompt types at a steady 22 chars/s without the box changing height.
- The pointer curves in from the lower right and clicks Generate 0.26 s after the last character; the button visibly dips.
- On the click, two dark lights run clockwise round the rim from the button, the halo breathes, one wave rings out.
- 1.14 s after the click the box folds into its centre and the page opens from that point in a rounded clip to full screen in 1.2 s, the photo settling from 115%.
- The three hero lines rise in one after another.
- Replay (click) never stacks or glitches: it restarts cleanly, or fades the open page out first.
- With reduced motion, only the final page is shown.
보관·활용 메모
Cadence에서 제공하는 코드와 프롬프트, 미리보기 영상을 이 사이트에 보관했습니다. 코드는 다운로드한 원본으로 제공하며 실행 화면은 별도로 격리되어 있습니다. 코드의 외부 라이브러리·샘플 이미지 요청은 유지됩니다.
보관일: 2026-10-26 · 프로젝트 적용: 별도 검증 필요
원본 코드·프롬프트·제작자 정보를 함께 보관했습니다. 실제 납품 시 코드와 포함 소재의 이용 조건을 확인하세요.
코드 SHA-256: 0aa031ef9ac974e1b52e80bac34e316454797530352ae66d8933817069419875