Every SKILL.md already declared the new name — name: frames, frames-cli, frames-core, and so on — but the directories were never moved, so the tree carried frames-* in content and hyperframes-* on disk. Everything that resolves a skill by path missed: the core-skill list, the plugin marketplace pin, and the docs the CLI reads at runtime. Eight tests failed on main because the files they open do not exist. The directories now match what the files say they are, and the guide the claude-design contract reads takes the name that contract already used.
5.5 KiB
name, description, metadata
| name | description | metadata | ||
|---|---|---|---|---|
| hacker-flip-3d | Character-level 3D rotation with random glyph substitution for a decryption reveal effect. |
|
Hacker Flip 3D Reveal
Characters flip down from 90° in 3D while cycling through pseudo-random glyphs, then settle on the target character — a "decryption" / airport flap-display reveal. Resolves to a short target word (typically a brand or label).
How It Works
Each character gets its own per-char tween from rotateX: 90deg (hidden, hinged at the bottom edge) to 0deg (upright), staggered across the word. Below REVEAL_THRESHOLD progress the char displays a seeded pseudo-random glyph that reshuffles every few frames; past it, the real target character clicks into place — so the eye catches the right letter just as the flip settles. A hidden ghost copy of the full word reserves layout width so narrow flicker glyphs never shift the line.
Recipe
<!-- inside a standard scene clip (frames-core) -->
<div class="hacker-text-wrap" id="hacker-text" data-target="{phrase}">
<!-- ghost row + per-char spans injected by the setup script -->
</div>
/* the scene root (or nearest 3D ancestor) MUST set perspective: 1500px */
.hacker-text-wrap {
font-family: {monoFont}; /* monospace so flicker glyphs hold width */
font-weight: 900;
font-size: HACKER_FONT_SIZE;
position: relative; /* ghost stacks absolutely behind the live row */
}
.hacker-char {
display: inline-block;
transform-origin: bottom; /* flap-display hinge */
transform-style: preserve-3d;
}
.hacker-ghost {
opacity: 0;
pointer-events: none;
position: absolute;
inset: 0 auto auto 0;
}
const wrap = document.getElementById("hacker-text");
const targetWord = wrap.dataset.target;
const GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%&*";
// Ghost row (reserves width) + live per-char spans
const ghost = document.createElement("div");
ghost.className = "hacker-ghost";
ghost.textContent = targetWord;
wrap.appendChild(ghost);
const charEls = [...targetWord].map((ch) => {
const span = document.createElement("span");
span.className = "hacker-char";
span.textContent = ch === " " ? " " : ch;
span.dataset.target = ch;
wrap.appendChild(span);
return span;
});
// Index-seeded hash — same frame always yields the same glyph
function pseudoGlyph(seed) {
const h = ((seed * 9301 + 49297) % 233280) / 233280;
return GLYPHS[Math.floor(h * GLYPHS.length)];
}
charEls.forEach((el, i) => {
const state = { p: 0 };
tl.to(
state,
{
p: 1,
duration: FLIP_DURATION,
ease: "power3.out",
onUpdate: () => {
if (state.p < REVEAL_THRESHOLD) {
el.textContent = pseudoGlyph(i * 1000 + Math.floor(state.p * 100));
} else {
el.textContent = el.dataset.target === " " ? " " : el.dataset.target;
}
el.style.transform = `rotateX(${90 - state.p * 90}deg)`;
el.style.opacity = Math.min(1, state.p * 2);
},
},
i * CHAR_STAGGER,
);
});
Variations
- Top-down hinge —
transform-origin: topfor a falling-flap look. - Center spin —
transform-origin: centerreads as a barrel roll, not a flap. - Number-only pool — restrict
GLYPHSto digits for a price / countdown decode. - Two-pass decode — chain two
FLIP_DURATIONtweens with different glyph pools (symbols → letters → real) for a longer reveal.
Values
| token | range | notes |
|---|---|---|
| HACKER_FONT_SIZE | 6–10% of viewport min-dimension | the flip IS the focal beat; ghost must use the identical size |
| FLIP_DURATION | 0.4–1.0s | under 0.4s the flicker phase has no time; over 1.0s drags |
| CHAR_STAGGER | 0.03–0.08s | total decode = CHAR_STAGGER × (chars − 1) + FLIP_DURATION — fit the phase budget |
| REVEAL_THRESHOLD | 0.5–0.7 | lower reveals too early (no tension); higher reads as a hard end-reveal |
| FLICKER_RATE | 3–6 frames per glyph swap | <3 looks like noise; >6 looks like discrete typing |
Reference: ../../examples/proof-logo-chain.html (163px, 0.55s, 0.033s, 0.6).
Critical Constraints
perspectiveon the scene root REQUIRED — without parent perspective,rotateXrenders as a 2D squash, not a 3D flip;transform-style: preserve-3don each char.- Ghost placeholder with identical content + font must back the live chars — without it, narrow glyphs shift the layout mid-flicker (monospace preferred; the ghost makes a proportional face recoverable).
- Flicker seed = char index + quantized progress — the same frame must show the same glyph.
- Flicker rate ≥ ~3 frames per swap;
onUpdatework stays O(1) per char per frame. - Center the flip dead-center and add NO decorative chrome (timestamp lines, "// AUTH" tags, status dots) — the flip is the beat. A necessary secondary label is BIG typography (56–72px caps + tracking) in the same stack, never a tiny corner annotation.
See also
card-morph-anchor (flip reveals a phrase, card morphs into the next shot) · counting-dynamic-scale (the numeric counterpart).