Files
zooqueen 62bbc7766c skills: give each skill the directory its name declares
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.
2026-08-13 18:17:24 -07:00

5.5 KiB
Raw Permalink Blame History

name, description, metadata
name description metadata
hacker-flip-3d Character-level 3D rotation with random glyph substitution for a decryption reveal effect.
tags
text, 3d, reveal, decode, hacker, randomization, perspective

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: top for a falling-flap look.
  • Center spin — transform-origin: center reads as a barrel roll, not a flap.
  • Number-only pool — restrict GLYPHS to digits for a price / countdown decode.
  • Two-pass decode — chain two FLIP_DURATION tweens 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

  • perspective on the scene root REQUIRED — without parent perspective, rotateX renders as a 2D squash, not a 3D flip; transform-style: preserve-3d on 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; onUpdate work 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).