Files
Garry Tanandcapy-ai[bot] c2e01e4ba7 v1.91.264.0 feat(design-motion): new /design-motion skill for showcase loops, adapted from Elaya's design-to-motion (GSTA-92 phase 2) (#3430)
* feat(design-html): craft section and free craft check, adapted from Elaya design skills (GSTA-92)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(GSTA-92): restore the full CHANGELOG under the Unreleased entry; drop a $-digit literal from craft.md; count only craft-check executions in design-html-slop-gate

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(design-motion): new /design-motion skill: showcase MP4 and GIF loops from a design, adapted from Elaya design-to-motion (GSTA-92 phase 2)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(design-motion): run design-motion-showcase in the marathon tier; the periodic census is at its 38-slice cap (GSTA-92)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(design-motion): review and QA fixes before ship

- encode: refuse unknown options and --formats tokens, validate --pad (#RRGGBB[AA])
  and --crop (compare too) so nothing but a colour or crop reaches ffmpeg's filter graph
- encode: export refuses a capture with missing window frames; check prints
  MOTION_SETTINGS for any threshold/phase override; contact sheet drops -fps_mode
- capture: portrait-aware scale, --cycle must be whole 60 fps frames, stills with
  CSS animations disabled, pauseAt tolerates slow loads, clears stale manifest/stills
- skill: <motion-dir> placeholder instead of a cross-block shell variable,
  GSTACK_FFMPEG-aware probe, stop points before the first capture, doctrine fixes
- tests: refusal rows, portrait scale, cycle guard

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(design-motion): capture-check-export pipeline on real Chromium and ffmpeg

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(design-motion): second review cycle

- capture: refuse an --out that holds a still or frames but no capture.json
  instead of deleting them; --cycle must be a multiple of 0.05 s so the seam
  frame lands exactly one cycle later for every PRE (nearest value round-trips)
- encode: a capture without a mask ends 'MOTION_RESULT: clean (overflow not checked)'
- skill: Step 5 names window.__mark in the capture contract
- tests: foreign/previous --out handling on real Chromium, cycle guard and PRE sweep

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(design-motion): third review cycle and adversarial findings

- capture: an ownership marker (.gstack-motion-capture) replaces the
  capture.json test, so the rerun after MOTION_CONTRACT_MISSING or a crash
  works and a foreign --out is still refused; offscreen lazy images are made
  eager and font/image loading is bounded at 30 s instead of hanging
- encode: refuse a capture.json whose fields are not what capture writes
  (they reach ffmpeg args and paths); a cards overflow with no story frames
  is a planning error, and a zero-frame job never passes
- tests: retry after a refused page, no-mask clean line, lazy image still,
  manifest validator, empty story span

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(design-motion): mask coverage, manifest relations, Windows stop, Figma token off argv

- check fails overflow when the grown mask covers >90% of the frame (an app
  frame whose clip surfaces fill the stage passed vacuously); --mask-grow is
  0-32; --mask refuses braces and semicolons
- manifestError checks fps/windowFrames >= 1 and that the window fits the frames
- skill: native Windows stops before the first capture call; the Figma token
  reaches curl on stdin (-H @-) instead of argv
- tests: split the --out ownership scenario out of the contract test, full-frame
  mask, --still --scale 2 size and STILL_OK, manifest relations, coverage parse

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(design-motion): bounded frames, atomic exports, full-bleed masks

- capture: each frame is bounded at 10 s (a __tick that never returns fails
  with MOTION_CAPTURE_FAILED instead of hanging); a --mask that matches no
  element stops with MOTION_MASK_EMPTY
- check: a grown mask covering >90% of the frame (full-bleed app frame)
  reports overflow as skipped and ends 'clean (overflow not checked)'
- export: writes .partial- files and renames on success, so a failed
  re-export keeps the last good file; a GIF fps that does not divide the
  cycle is refused; a missing GSTACK_FFMPEG override is named
- fidelity: URL source is untrusted data; full-bleed note
- tests: spinning __tick, empty mask, failed re-export, drifting gif fps

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(design-motion): app-only coverage skip, gif fps cap, early empty mask

- check: the >90% coverage skip applies only to an app frame's own surfaces
  (ungrown mask); in cards, or when --mask-grow pushes coverage over 90%,
  overflow fails with what to change (a cards capture with real overflow
  passed as 'clean (overflow not checked)' after a large grow)
- export: --gif-fps/--small-fps capped at 50 (GIF delays are 1/100 s)
- compare resolves its paths (a colon in a relative path read as a protocol)
- capture: MOTION_MASK_EMPTY is checked before the first frame
- tests: cards vs app full-frame mask, fps cap, no frames on an empty mask

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(design-motion): seek story transitions from their own frame

- capture: an animation first seen in a frame where window.__mark did not
  move (a story's class toggle) is seeked from that frame instead of the
  last mark, so it eases in the export instead of popping ~100 ms ahead
- encode: an empty numeric option is refused instead of read as 0
- figma: only fill a file key and node id that look like Figma's
- tests: class-toggled transition mid-fade at its midpoint, blank --min-db

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: PROJECT_STRUCTURE names Elaya's MIT material and licenses/MIT-elaya.txt

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-27 03:04:06 +00:00

8.7 KiB
Raw Permalink Blame History

Motion: empty state, load, stories, cycle

Every timing, distance and curve comes from the defaults table at the end. Never invent ad hoc values; change the table instead.

Start empty

The first frame of every showcase load and every export is the empty state (on a website the doctrine wins: the markup is the finished state until the intro fires): the background plus the static joined surfaces for an app frame, the background alone for separate cards. Empty becomes loaded; never open on the finished frame.

Loading choreography

Elements rise from just below and resolve from blur to sharp, staggered in reading order.

Kind For From
card Separate cards, with their shadow translateY(24px) scale(0.985) blur(6px), opacity 0
up Text, headers, values, rows translateY(10px) blur(4px), opacity 0
down Top bars and toolbars translateY(-8px) blur(4px)
left Sidebar items, list rows, labels translateX(-10px) blur(4px)
pop Icons, badges, avatars, buttons, dots scale(0.82-0.88) from the element's own centre, blur(4px)
photo Images skeleton shimmer, then blur(14px) saturate(0.6) to sharp
wipe Bars, fills, highlights left-to-right clip-path in stage pixels: right inset from stageW − x (zero width, no sliver) to stageW − (x + w); top and bottom stay at y and stageH − (y + h)
line / grow Rules, ticks, timelines scaleX(0) / scaleY(0) from the start edge
donut Rings, gauges conic mask sweeping clockwise from 12 o'clock
tray Sheets, trays, folders up 40 px from inside the container's bottom edge, clipped by it
fade Layers under another layer opacity only
  • Containers load in reading order 80-90 ms apart (cards start about 0.15 s before their own contents). Inside one: header, labels, values, then visuals; items in a group stagger 30-80 ms. The whole intro takes 2 to 2.5 s.
  • Covered layers arrive last and leave first, so they never ghost through their cover.
  • Skeletons match the real shape and sit 1 px inside it, so no halo shows.
  • Data bars, meters and highlights fill like progress: a wipe with --io over 0.7-1.1 s. Never add a loading line or any shape the design lacks.

Stories: one per visual

The intro only opens the show. The main act is each visual acting out what it does, on a loop. Write each story as one sentence before building it ("a new lead lands on top and the older ones step back").

Visual Story
Stack (leads, notifications, files) A new item lands on top with a blur, the rest step back a slot, the last leaves
Line or area chart A scrubber glides across, a dot rides the curve, the tooltip value counts
Bar chart, grid of cells Bars breathe like a live feed and settle on the design heights
Donut, gauge Segments ease outward one at a time and back, or the ring sweeps again
Table, list A soft highlight inspects rows one by one; a preview loads for the active row
Calendar A highlight searches for an open slot, the event glides into it and back
Flow, connectors Pulses travel the connectors in flow direction and light each node they pass
Prompt, form The prompt deletes and retypes glyph by glyph; the result resolves from blur
Counters, KPIs Numbers count up with the chart that drives them
Long content It scrolls slowly inside its clip and returns
Logo row (showcase only) A marquee moving exactly one full set per loop
Media player The playhead and progress advance
  • Start and end on the exact design. At rest the frame equals ?still. Clear inline styles a story set (style.transform = '') rather than leaving translate(0), which antialiases differently and fails the rest check.
  • Use the design's own elements. Helpers a story needs match the design's style and are absent from ?still.
  • Text changes only when it is the point: counters (live text in the design's font, landing on the design's exact text) and typing (one glyph every 45-70 ms, caret at the edge; deleting runs a little faster).
  • Obvious, not busy. A card with a story shows a clear beat every one to two seconds; if a viewer cannot tell it is animated within two seconds, the story is too timid. A calm screen may rest after load if the user prefers; ask once.
  • Text-only cards get no story. They rise with their card and rest.
  • Readable in the 800×500 GIF. A moving element under about 4 px there gets enlarged within the design's style or a bigger story.
  • Offset the cards. Stories never act in unison; attention travels in reading order.
  • One story per card, 3 to 8 s. A short story may run twice in the story phase; every run ends at rest before the dissolve.
  • Stories are eased keyframe tracks on the JS clock:
// [[time, value], ...] eased per segment; values may be numbers or arrays
const easeInOut = t => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2); // --io, never past 1
function track(keys, t, ease = easeInOut) {
  if (t <= keys[0][0]) return keys[0][1];
  for (let i = 1; i < keys.length; i++) {
    const [t1, v1] = keys[i], [t0, v0] = keys[i - 1];
    if (t <= t1) {
      const k = ease(Math.min(1, (t - t0) / (t1 - t0 || 1)));
      return Array.isArray(v0) ? v0.map((v, j) => v + (v1[j] - v) * k) : v0 + (v1 - v0) * k;
    }
  }
  return keys[keys.length - 1][1];
}

Accents sit on top of a story, never instead of one, and only on elements the design already has: a pulse ring on a live indicator or play button (1.8-2.2 s, ring 1 → 2.6, opacity 0.55 → 0), a sheen on a photo or button (a band 38% wide, 1.5 s pass every 3.4 s), a skeleton glimmer on placeholder lines (2.4 s). Accents and helpers fade in over 0.6 s once the intro lands, and are gone before the dissolve. None of them ships running on a website.

The loop cycle (showcase)

Phase Default
Build in 0 → 2.4 s
Stories 2.4 → 11.0 s
Dissolve 11.0 → 11.6 s (opacity, 0.5 s)
Empty beat 11.6 → 12.0 s

The story phase holds at least one full story of every card; lengthen the cycle rather than cut one short. The dissolve returns to the empty state: an app frame dissolves only its contents, separate cards dissolve too, the background never. Reset everything: one frame with transitions disabled resets the CSS, and every JS-driven value (masks, tracks, helpers, typed text, pulses) goes back to its empty value too, or it ghosts through the empty beat. The live page keeps its stories running forever after the intro; only ?loop and the exports cycle.

Easing and camera

Token Curve Use
--e cubic-bezier(0.16, 1, 0.3, 1) Entrance transforms
--e2 cubic-bezier(0.33, 1, 0.68, 1) Opacity and blur
--io cubic-bezier(0.65, 0, 0.35, 1) Wipes, fills, sweeps, camera moves
linear linear Constant motion only (shimmer, playback)

No zoom and no pan unless asked. When asked, only the app frame scales toward the items, with --io, and it is back at rest before the dissolve.

Website sections

On a website the intro starts when about 30% of the section is visible (IntersectionObserver, once; never a scroll listener), the story pauses off screen and resumes on return, stagger stays inside the section, and prefers-reduced-motion: reduce shows the final state with no story. Map the page section by section: bento and feature cards with a visual get the card intro and a story; a product or dashboard mockup gets the loading intro, and a story only if the user says yes when asked; text-only sections (headlines, testimonials, FAQ, pricing, CTA, footer) get nothing from this skill. Under the one-moment rule, propose one section, not all of them.

Defaults

Value Default
Rise 10 px (up), 8 px (down), 10 px (left), 24 px + scale(0.985) (card), 40 px (tray)
Entrance blur 4 px; cards 6 px; photos 14 px
Pop start scale 0.82-0.88
Durations (showcase) transforms 1.05 s --e (cards 0.9 s); opacity and blur 0.7-0.8 s --e2 (cards 0.6 s); wipes 0.7 s, slow bars 1.1 s --io
Durations (website) 50-700 ms
Stagger containers 80-90 ms, items 30-80 ms
Typing one glyph per 45-70 ms, caret blink 1 s (showcase only)
Story starts 2.4 s, 3-8 s per card, offset between cards
Cycle 12 s (dissolve 11.0, reset 11.6), longer when a story needs it
Exports MP4 60 fps CRF 15 up to 1920×1200; loop GIF 960 px 20 fps; small GIF 800×500 20 fps

Adapted from Elaya's ai-design-skills design-to-motion (MIT, © 2026 Elaya), rewritten for gstack. See NOTICE.md.