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

7.2 KiB
Raw Permalink Blame History

name, description, metadata
name description metadata
card-morph-anchor Container morphs dimensions and border-radius between shots, serving as a visual transition anchor.
tags
morph, anchor, transition, border-radius, container, shape

Card Morph Anchor

A free-floating container morphs apparent size, corner radius, and surface treatment between two shots — the morph itself IS the transition; the viewer's eye tracks the persistent container. Distinct from anchored-layout-expand.md (an edge-pinned live layout participant that grows along one axis and reflows neighbors — here nothing is pushed) and theme-crossfade-morph.md (a whole-theme reskin under a fixed anchor — here a single container changes shape).

How It Works

Since width/height tweens are forbidden, substitute uniform scale for apparent size; the remaining morph channels are paint-only: borderRadius, background, boxShadow. All channels ride ONE tween (one ease, one duration) so the shape morphs in lockstep. Content choreography: old content fades out during the first ~40% of the morph, new content fades in during the last ~40% — the shape-only gap between is the natural "blink." Optionally the morph card itself fades at the very end, revealing the real next-shot element rendered behind it.

Recipe

<!-- inside a standard scene clip (frames-core) -->
<!-- DOM order = stacking: the anchor renders BEFORE the card, so the card is on top -->
<div class="next-shot-anchor"><img src="{nextShotAnchor}" alt="anchor" /></div>
<div class="morph-card">
  <div class="content-old">{shotOneContent}</div>
  <div class="content-new">{shotTwoContent}</div>
</div>
.morph-card {
  width: SHOT_ONE_W;
  height: SHOT_ONE_H; /* shot-1 geometry; the morph is scale, never width/height */
  border-radius: SHOT_ONE_RADIUS;
  background: {surfaceShotOne};
  overflow: hidden; /* content must clip during the shape change */
  display: grid;
  place-items: center;
  will-change: transform;
}
.content-old,
.content-new {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}
.content-new {
  opacity: 0; /* author its inner sizes at apparent-size ÷ END_SCALE — it scales with the card */
}
.next-shot-anchor {
  position: absolute;
  opacity: 0; /* fades in as the morph card fades out */
}
const END_SCALE = SHOT_TWO_W / SHOT_ONE_W; // uniform — keep the two shots aspect-matched

// Hold shot 1 for HOLD_BEAT first — an instant morph reads as glitchy.

// One tween, all channels: uniform scale + paint-only properties.
tl.to(
  ".morph-card",
  {
    scale: END_SCALE,
    borderRadius: SHOT_TWO_RADIUS / END_SCALE, // borderRadius is pre-scale — divide to land the APPARENT radius
    background: "{surfaceShotTwo}",
    boxShadow: "{shadowShotTwo}",
    duration: MORPH_DUR,
    ease: "power2.inOut",
  },
  MORPH_START,
);

tl.to(
  ".content-old",
  { opacity: 0, duration: MORPH_DUR * OLD_FADE_FRAC, ease: "power1.in" },
  MORPH_START,
);
tl.to(
  ".content-new",
  { opacity: 1, duration: MORPH_DUR * NEW_FADE_FRAC, ease: "power1.out" },
  MORPH_START + MORPH_DUR * (1 - NEW_FADE_FRAC),
);

// Optional handoff — card fades out over the pixel-identical real anchor.
tl.to(
  ".morph-card",
  { opacity: 0, duration: MORPH_DUR * FINAL_FADE_FRAC, ease: "power1.in", immediateRender: false },
  MORPH_START + MORPH_DUR * (1 - FINAL_FADE_FRAC),
);
tl.to(
  ".next-shot-anchor",
  { opacity: 1, duration: MORPH_DUR * FINAL_FADE_FRAC, ease: "power1.out" },
  MORPH_START + MORPH_DUR * (1 - FINAL_FADE_FRAC),
);

Morph channels

channel how
apparent size uniform scale — the substitution for the forbidden width/height tween; aspect preserved
borderRadius paint-only; pre-scale units — tween to APPARENT_RADIUS / END_SCALE, ≤ half the smaller side
background paint-only; gradients interpolate only with equal stop counts (solid→solid: backgroundColor)
boxShadow paint-only; base shadow → accent glow shifts emphasis

Variations

  • Landing on a non-centered target (dock icon, sidebar slot): add x/y to the same tween, computed as the FLIP-style delta between the card's and the target's rects — getBoundingClientRect() both at build time (single-scene only, per the contract) and tween the difference. Don't hand-compute from CSS values: paddings, borders, and parent transforms compound, and center-vs-edge arithmetic is the classic off-by-half bug.
  • Aspect change between shots: uniform scale preserves aspect — morph to the nearest uniform fit and let the crossfade/handoff absorb the small delta, or drop the handoff and hold the card's final state.

Values

token range notes
HOLD_BEAT 0.6–1.5s ≥ shot 1's entry settle; the viewer must register shot 1 first
MORPH_DUR 0.6–1.2s < 0.5s can't fit both content fades
END_SCALE SHOT_TWO_W / SHOT_ONE_W icon-sized handoffs typically land at 80–400px apparent width
SHOT_TWO_RADIUS ≤ min(W, H)/2 apparent half the smaller side = perfect circle; beyond is clamped
OLD/NEW_FADE_FRAC 0.3–0.5 each, sum ≤ 1 the gap between is the shape-only "blink"
FINAL_FADE_FRAC 0 (no handoff) or 0.1–0.2 only when a pixel-identical anchor exists
ease power2.inOut canonical power3/expo.inOut OK; never back/elastic — overshoot fights the shape change

Critical Constraints

  • ❗ Uniform-scale substitution — never tween width/height; scale + the paint-only channels (borderRadius, background, boxShadow) are the ONLY morph properties.
  • ❗ Handoff anchor must be pixel-identical to the card's final state — same apparent size, radius, background, shadow, inner icon dimensions. Any delta = a visible pop during the crossfade. Can't match exactly? Drop the handoff and hold the morph card.
  • ❗ Stacking by DOM order, never a z-index snap mid-fade — render the anchor before the card; a tl.set({ zIndex }) during an active opacity tween flips stacking before the fade finishes and flickers.
  • overflow: hidden on the card — content must clip as the radius changes.
  • Hold a beat before morphing; same ease family for shape and crossfade (mixed eases read unsynchronized).

See also

anchored-layout-expand (edge-pinned one-axis growth with reflow) · theme-crossfade-morph (whole-theme reskin under a fixed anchor) · scale-swap-transition (content swap without shape change) · sine-wave-loop (a breath on the final state).