Files
wehub-resource-sync 85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:58:35 +08:00

7.9 KiB
Raw Permalink Blame History

name, description, metadata
name description metadata
center-outward-expansion Elements start clustered at screen center and expand outward to their final positions, driven by a shared progress value.
tags
expansion, scatter, center, reveal, layout, sync, burst

Center-Outward Expansion

Elements begin at a shared center point and radiate outward to their final positions. The expansion can be the entry beat itself, or driven by another animation's progress (e.g. a counting number growing) for coordinated motion.

How It Works

Each element has a targetX/Y (its final layout position) and a shared centerX/Y. A progress value (0→1) interpolates each element between center and target:

const x = centerX + (targetX - centerX) * progress;
const y = centerY + (targetY - centerY) * progress;

When progress = 0 all elements overlap at the center; when progress = 1 they're at their final spots.

HTML

<div
  class="scene"
  data-composition-id="burst-scene"
  data-start="0"
  data-duration="3"
  data-track-index="0"
>
  <div class="burst-wrap">
    <div class="burst-item" data-target-x="-360" data-target-y="-180">{itemA}</div>
    <div class="burst-item" data-target-x="360" data-target-y="-180">{itemB}</div>
    <div class="burst-item" data-target-x="-360" data-target-y="180">{itemC}</div>
    <div class="burst-item" data-target-x="360" data-target-y="180">{itemD}</div>
    <div class="burst-item" data-target-x="0" data-target-y="-360">{itemE}</div>
    <div class="burst-item" data-target-x="0" data-target-y="360">{itemF}</div>
  </div>
</div>

CSS

.scene {
  position: relative;
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  background: {bgColor};
}
.burst-wrap {
  position: relative;
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
}
.burst-item {
  position: absolute;
  /* Items start at the wrap center via the absolute + 50% trick.
     We tween translate offsets via GSAP, not left/top. */
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);

  width: {itemSize};
  height: {itemSize};
  display: grid;
  place-items: center;
  background: {itemBgColor};
  border-radius: 28px;
  font-family: {font};
  font-weight: 900;
  font-size: 96px;
  color: {textColor};
  will-change: transform;
}

GSAP Timeline

<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
  window.__timelines = window.__timelines || {};
  const tl = gsap.timeline({ paused: true });

  const items = document.querySelectorAll(".burst-item");

  // Each element gets its own from→to that lerps center (translate(-50%, -50%))
  // → target offset. xPercent/yPercent bakes the self-centering; x/y animates
  // toward the target.
  items.forEach((el, i) => {
    const targetX = Number(el.dataset.targetX);
    const targetY = Number(el.dataset.targetY);
    tl.fromTo(
      el,
      { xPercent: -50, yPercent: -50, x: 0, y: 0, scale: 0.6, opacity: 0 },
      {
        x: targetX,
        y: targetY,
        scale: 1,
        opacity: 1,
        duration: EXPAND_DUR,
        ease: EXPAND_EASE,
      },
      i * STAGGER + ENTRY_AT, // stagger; ENTRY_AT offsets the burst beat
    );
  });

  window.__timelines["burst-scene"] = tl;
</script>

How to Choose Values

  • ITEM_COUNT — number of elements in the burst

    • Range: 38
    • Effects: 3 = sparse; 8 = busy. > 8 causes visual chaos where cards overlap mid-expansion
    • Constraints: at low counts, prefer wider angular spread (target positions further apart)
  • EXPAND_DUR — duration of each item's center → target tween

    • Range: 1.01.8 s
    • Effects: shorter = snappy burst; longer = floats outward
    • Constraints: if driven by a counter, must equal the counter's duration (chord)
  • EXPAND_EASE — shared ease across all items

    • Discrete choice: power2.out, power3.out, expo.out
    • Selection: power3.out is the default — fling out then settle. power2.out is gentler. expo.out makes them stop dramatically. Avoid in easings (they read as items being sucked back in mid-air).
    • Constraint: if driven by another animation, must be identical to the driver's ease
  • STAGGER — gap between successive items' start times

    • Range: 0.040.08 s
    • Effects: < 0.04 = simultaneous chord; > 0.08 feels lazy / arpeggiated
    • Constraints: ITEM_COUNT × STAGGER must be < EXPAND_DUR or the last items still moving when others have landed reads as ragged
  • ENTRY_AT — offset applied to the whole burst start

    • Range: 0 0.5 s
    • Effects: > 0 gives a beat of compositional quiet before the burst
  • START_PROGRESS — fraction of the center→target path where items begin (for partially-spread variant)

    • Range: 0 (exact center) 0.5
    • Effects: 0 = full cluster, dramatic spread; 0.3 = avoids initial pile-up at center

Variations

Synced expansion (driven by a counter)

If the burst should mirror a counting animation's progress:

// Counter tween defines a state.value 0 → TARGET over COUNT_DUR
const counterState = { value: 0 };
const burstState = { p: 0 };

// Shared tween — same duration, same ease — visually a "chord"
tl.to(
  counterState,
  {
    value: COUNT_TARGET,
    duration: COUNT_DUR,
    ease: COUNT_EASE,
    onUpdate: () => (counterEl.textContent = Math.round(counterState.value).toLocaleString()),
  },
  0,
);

tl.to(
  burstState,
  {
    p: 1,
    duration: COUNT_DUR,
    ease: COUNT_EASE,
    onUpdate: () =>
      items.forEach((el) => {
        const tx = Number(el.dataset.targetX) * burstState.p;
        const ty = Number(el.dataset.targetY) * burstState.p;
        el.style.transform = `translate(-50%, -50%) translate(${tx}px, ${ty}px)`;
      }),
  },
  0,
);

Starting partially-spread

To avoid the initial clustered mess (6+ elements stacked at center), start at START_PROGRESS:

{ x: targetX * START_PROGRESS, y: targetY * START_PROGRESS, scale: 0.4, opacity: 0 }

Idle micro-float at final position

Pair with sine-wave-loop after expansion lands — keeps elements alive instead of frozen.

Key Principles

  • Driver vs driven — if the burst stands on its own, use a per-item stagger; if it shadows another animation (counter, audio beat), share the same eased progress so they read as one beat
  • Stagger inside the 0.04-0.08 s band — too tight and the cluster never separates visually, too loose and the burst feels lazy
  • Out-easing for the expansion — out-easing makes items "fling" out then settle. In-easing looks like they're sucked back in mid-air
  • Element count: 3-8 — fewer feels empty, more causes visual chaos at the center where cards overlap mid-expansion
  • Don't put a label below the burst as the "real headline" — if you do, the eye snaps to the label and ignores the burst. The burst IS the beat. If a label is needed, use big block-caps and reveal it post-burst, in the same stacked layout.

Critical Constraints

  • Timeline must be paused: gsap.timeline({ paused: true })
  • Registry key = data-composition-id
  • Use translate, not left/top — translating composes cleanly with the centering translate(-50%, -50%) trick; mutating left/top fights the centering and causes pixel jitter
  • will-change: transform on burst items — many simultaneous transforms benefit from compositor hints
  • No position: absolute parents inside burst-wrap other than items themselves — sibling absolute elements would steal the centered baseline

Combinations

Pairs with HF skills

  • /hyperframes-animation — timeline + stagger
  • /hyperframes-core — composition wiring
  • /hyperframes-clihyperframes lint