Files
hyperframes/skills/hyperframes-animation/rules/coordinate-target-zoom.md
T
WaterrrForever 853256403b feat(skills): c2v mining pass — 7 new blueprints, 10 new rules, compacted recipe corpus (#2680)
* feat(skills): c2v mining pass over animation blueprints and rules

Compacts ~45 existing animation rules/blueprints into tighter recipe form
(net -3.4k lines) and adds 17 mined from the c2v corpus:

- 7 blueprints: agent-progress-theater, camera-journey, fixed-anchor-cycle,
  panel-edit-live-sync, prompt-type-submit-generate,
  transcript-scroll-artifact-reveal, zoom-out-workspace-reveal
- 10 rules: 3d-camera-flight, anchored-layout-expand, chart-scrub-readout,
  chromatic-glitch, control-target-sync, cursor-drag, gradient-text-sweep,
  multi-cursor-choreography, particle-burst, theme-crossfade-morph






Both indexes updated.

* feat(skills): sync product-launch script bank with mined blueprint roles

The role->blueprint script bank in product-launch-video/story-design.md is
kept 1:1 with blueprints-index role declarations, which the c2v mining pass
expanded. Adds the 25 missing entries (script-shape descriptor + example
lines + pattern): 13 for the 7 new blueprints, 12 for role widenings on 7
existing ones (cursor-ui-demo, dataviz-countup, titlecard-reveal, et al.),
and states the 1:1 sync contract in the bank's intro.

* docs(skills): cover constellation-hub scatter-drift variant in the script bank

Review follow-up on #2680: the SOCIAL_PROOF constellation-hub entry
patterned only the orbit shape; the c2v pass added a scatter-drift
end-card variant with the opposite geometry (no hub, no ring). Adds an
example line and extends the pattern so a scatter-drift beat's VO isn't
steered toward the orbit shape.
2026-07-21 17:28:55 +08:00

7.4 KiB
Raw Blame History

name, description, metadata
name description metadata
coordinate-target-zoom Zoom into a specific non-centered element by combining scale with counter-translation — target ends at viewport center after the zoom completes.
tags
camera, zoom, scale, translate, target, off-center, focus

Coordinate Target Zoom

A simple scale > 1 on a wrapper pushes off-center content OFF the visible canvas. To zoom into a specific non-centered element, apply scale AND an inverse translation in lockstep so the target lands at viewport center.

How It Works

Two nested wrappers, separated concerns — never scale and translate on the SAME element (translate * scalescale * translate in CSS transform composition):

  1. Outer wrapper applies scale (the zoom) around transform-origin: 50% 50%
  2. Inner wrapper applies translate(x, y) (the counter-shift)

The counter-translate is the negation of the target's offset from viewport center:

T = -offset

Derivation: the inner translate moves the target to offset + T in pre-scale units; the outer scale S (around center) maps that to S × (offset + T); landing at center means S × (offset + T) = 0T = -offset. The formula does NOT depend on S — the translate is identical at 1.5×, 2×, or 3×. A common wrong intuition is T = -offset × (S - 1): it coincidentally matches at S = 2 and is wrong at every other scale.

⚠️ This is the NESTED-wrapper formula. The single-wrapper camera in viewport-change.md puts translate(x,y) scale(S) on ONE element, where CSS applies scale first — there the counter-translate is T = -offset × S. The two formulas are not interchangeable; match the formula to the wrapper structure.

Getting the offset

T = -offset is only as good as offset. The #1 way this pattern ships broken is hand-computing offset from a layout formula, getting the sign or magnitude wrong, and letting the zoom amplify a small error off-screen. Default to measuring the target's real laid-out center; reserve the formula for symmetric rows.

Default — measure the actual center (works for ANY layout). Immune to sign errors because it reads the rendered DOM, not a mental model:

await document.fonts.ready; // metrics final; fallback fonts are 1030px off → tens of px after a 3×+ zoom
const W = 1920,
  H = 1080;
const r = document.getElementById("target-card").getBoundingClientRect();
const TARGET_OFFSET_X = r.left + r.width / 2 - W / 2;
const TARGET_OFFSET_Y = r.top + r.height / 2 - H / 2;

Measure once at setup and bake — never per-frame in onUpdate. Because the measurement is async (fonts.ready), build and register the timeline inside the same async setup so the baked offset is ready before window.__timelines[id] is published.

Shortcut — symmetric equal-width row ONLY:

const index_offset = targetIndex - (N - 1) / 2;
const TARGET_OFFSET_X = index_offset * (CARD_WIDTH + CARD_GAP);

⚠️ This assumes every sibling is the same width. The moment the row is asymmetric, it gives the wrong answer — often the wrong sign: the heavier side shifts the centered target the opposite way you'd guess (e.g. companion(220) + gap + wordmark + gap + chip(110) puts the wordmark ~55px right of center, but "chip companion" intuition says left). For anything but equal cards, measure.

Headroom budget — cap the scale from the measured size. A zoom multiplies any centering error; keep the target ≤ ~88% of the canvas at peak:

const maxScale = Math.min((0.88 * W) / r.width, (0.88 * H) / r.height);
const ZOOM_SCALE = Math.min(DESIRED_SCALE, maxScale);

A target filling 97%+ of the frame reads as cut-off the instant its center is slightly off — and a hand-baked offset always is. (The perception gate flags this as primary-offscreen; data-layout-allow-overflow does not exempt it.)

Recipe

<div class="zoom-outer" id="zoom-outer">
  <div class="zoom-inner" id="zoom-inner">
    <div class="content">
      <div class="card">{other}</div>
      <div class="card target" id="target-card">{target}</div>
      <div class="card">{other}</div>
    </div>
  </div>
</div>
.scene {
  overflow: hidden; /* REQUIRED — at zoom > 1 the scaled content leaks past the frame */
}
.zoom-outer {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  transform-origin: 50% 50%; /* center scaling is what the counter-translate math assumes */
  will-change: transform;
}
.zoom-inner {
  display: grid;
  place-items: center;
  will-change: transform;
}
// TARGET_OFFSET_X/Y and ZOOM_SCALE come from "Getting the offset" — measured
// at setup (after fonts.ready), baked. Counter-translation = -offset.
const counterX = -TARGET_OFFSET_X;
const counterY = -TARGET_OFFSET_Y;

// Scale and counter-translate MUST share position, duration, AND ease —
// otherwise the target visibly wanders mid-zoom.
tl.to("#zoom-outer", { scale: ZOOM_SCALE, duration: ZOOM_DUR, ease: "power3.inOut" }, ZOOM_AT);
tl.to(
  "#zoom-inner",
  { x: counterX, y: counterY, duration: ZOOM_DUR, ease: "power3.inOut" },
  ZOOM_AT,
);

Variations

  • Zoom out (target → wide view): reverse the phases — start zoomed-in, then tween to scale: 1 + x: 0, y: 0; the "reveal" beat is the panorama.
  • Multi-target zoom sequence: chain zooms (target A → pause → target B → pull back); each segment needs its own counter-translation pair.

Values

token range notes
ZOOM_SCALE 1.5× modest → 3× dominant → 5×+ extreme cap via the headroom budget; raster media needs sourceResolution ≥ rendered × ZOOM_SCALE
ZOOM_DUR 1.02.0s under 0.8s feels like a teleport, over 2.5s drags; both tweens share it
ZOOM_AT after the layout lands + 0.51.5s give the viewer time to scan the layout before the camera commits
DWELL ≥ 1.0s after the zoom settles 1.52s ideal — the viewer must be able to read the target (climax dwell)

Critical Constraints

  • Outer scales, inner translates — never both transforms on one element; nested wrappers keep the math clean.
  • transform-origin: 50% 50% on the outer wrapper — non-center origin breaks the counter-translate derivation.
  • overflow: hidden on the scene root — zoomed content leaks past the frame otherwise.
  • Scale and counter-translate share duration + ease at the same timeline position, or the target drifts mid-zoom.
  • Offset measured once at setup (after fonts.ready), baked — never recomputed per-frame, never hand-derived for a non-symmetric layout (wrong sign → target shoved off-frame).
  • Scale within the headroom budget — target ≤ ~88% of the canvas at peak, derived from the measured size.

See also

viewport-change.md (single-wrapper form, T = -offset × S) · multi-phase-camera.md (a zoom phase inside a phased camera) · sine-wave-loop.md (idle breathing after the zoom settles) · discrete-text-sequence.md (text assembly in the target before the zoom).