Skip to content

feat: animation groups for choreographing sequences across blocks#32

Merged
jonlaing merged 1 commit into
mainfrom
feat/animation-groups
Jul 6, 2026
Merged

feat: animation groups for choreographing sequences across blocks#32
jonlaing merged 1 commit into
mainfrom
feat/animation-groups

Conversation

@jonlaing

@jonlaing jonlaing commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a declarative sequencing primitive for animations that span multiple animated blocks. Solves the "word-by-word intro" case where each word is its own each and word N should only start after word N-1's letter cascade finishes.

const [greeting, name, tagline] = yield* Animation.sequence(3);

$.div({}, collect(
  each(greetingLetters, {
    key: (l) => l.id,
    render: (l) => $.span({}, $.of(l.char)),
    animate: { enter: "letter-in", stagger: stagger(40), group: greeting },
  }),
  each(nameLetters, {
    key: (l) => l.id,
    render: (l) => $.span({}, $.of(l.char)),
    animate: { enter: "letter-in", stagger: stagger(40), group: name },
  }),
  each(taglineLetters, {
    key: (l) => l.id,
    render: (l) => $.span({}, $.of(l.char)),
    animate: { enter: "letter-in", stagger: stagger(40), group: tagline },
  }),
));

API

New namespace Animation on @effex/dom:

  • Animation.group() — creates a group handle. Gate is closed initially, opens explicitly via sequence/parallel.
  • Animation.sequence(count) — returns count groups wired end-to-end. Group 0's gate is open immediately; group N's gate opens when group N-1's animations all complete.
  • Animation.parallel(count) — returns count groups with all gates open. Handy for concurrent segments (future: nest inside sequence for mixed choreography).

New option on AnimationOptions:

  • animate.group?: AnimationGroup — attach this animation to a group. The animation registers with the group synchronously, awaits the gate before starting, and signals completion when finished.

Runtime

  • Registration is a plain synchronous JS mutation (counter++) so all sibling animations register before any completion decrements — safe because JS is single-threaded and reconcile drives addSlot calls in the same fiber before yielding to any forked animation fiber.
  • Fork model uses the pattern already in place from Inline animations serialize the reconcile loop #25: enter animations fork into the slot scope, wrapped so they _awaitGate before runEnterAnimation and Effect.ensuring(_complete) after (so interruption still decrements the counter).
  • Gate opening cascades via one Effect.forkDaemon per link — daemons outlive the render scope and get GC'd with the group.

Design notes

  • Late registrations — animations that register after a group has already finalized run immediately (gate is already open). Matches the one-shot intro use case; new items added post-sequence behave like ordinary animations.
  • Idempotence_complete's zero-crossing is guarded by doneResolved, so subsequent register/complete cycles don't re-fire the done signal.
  • No cross-scope leaks — daemons only hold Deferreds; when the groups are unreachable, they're GC'd.

Test plan

  • 11 new unit tests in groups.test.ts covering gate/done semantics, sequence chaining, parallel behavior, late registration, _awaitGate blocking
  • pnpm exec tsc --noEmit clean
  • Full suite: 715 pass / 2 skipped (was 704 / 2)
  • Manually verify the choreography works end-to-end in the portfolio site headline

Deferred to follow-ups

  • Nested sequence(sequence(...), parallel(...)) composition — current API takes a count, not a shape
  • overlap option on sequence (each group's gate opens n ms before the prior finishes)
  • Debug/trace helper

Closes #28

🤖 Generated with Claude Code

Adds Animation.group / Animation.sequence / Animation.parallel plus an animate.group option on AnimationOptions. Multiple animated blocks can share a group; sequence() wires groups end-to-end so later blocks wait for earlier blocks' animations to complete.

Runtime: each animation registers with its group synchronously (plain mutable counter, safe under JS single-threading) before forking. The forked fiber awaits the group's gate Deferred, runs the animation, and signals completion via Effect.ensuring so interruption still decrements. When a group's pending count returns to zero after having been non-zero, its done Deferred resolves, which cascades to open the next group's gate.

Late registrations arriving after a group has finalized run immediately — matches the one-shot-intro use case.

Closes #28

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploying effex-api with  Cloudflare Pages  Cloudflare Pages

Latest commit: b650fd8
Status: ✅  Deploy successful!
Preview URL: https://b3bbf1ea.effex-api.pages.dev
Branch Preview URL: https://feat-animation-groups.effex-api.pages.dev

View logs

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying effex with  Cloudflare Pages  Cloudflare Pages

Latest commit: b650fd8
Status: ✅  Deploy successful!
Preview URL: https://e66d93c3.effex.pages.dev
Branch Preview URL: https://feat-animation-groups.effex.pages.dev

View logs

@jonlaing jonlaing merged commit 3e7cb20 into main Jul 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add animation groups (Animation.sequence / Animation.parallel)

1 participant