From 7d53dcf4054b1e2f843f8b03689643a80dae992d Mon Sep 17 00:00:00 2001 From: ukimsanov Date: Wed, 20 May 2026 14:00:25 -0700 Subject: [PATCH 1/6] =?UTF-8?q?feat(skill):=20hyperframes=20core=20?= =?UTF-8?q?=E2=80=94=20remove=20prescriptive=20tables,=20bundle=20text-eff?= =?UTF-8?q?ects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites the standalone `hyperframes` skill (the main authoring skill used by every hyperframes user, not just the website-to-hyperframes pipeline) to remove prescriptive lookup tables that drove monoculture output, restore tone, and bundle 24 named text animation effects directly into the skill so agents don't need a separate install. This is a +9951/-567 change touching 61 files in `skills/hyperframes/`. It deserves its own review separate from the capture pipeline and the website-to-hyperframes pipeline because it affects every hyperframes user — not just the website-to-video flow. **Prescriptive tables removed / restructured** External rater feedback across two rounds identified six lookup tables agents were pasting wholesale as recipes: - `visual-styles.md` YAML blocks — completely replaced. Old version had 8 styles with full YAML token blocks (colors / typography / motion / transition names). Agents copy-pasted. New version renames to actual design traditions (Swiss / Late-Modernist Editorial / Punk / Maximalist / Computational / Humanist / Vernacular / Cinematic) and replaces YAML with prose: "what it teaches / where it resonates / pitfalls when borrowing." No lookup table. - `motion-principles.md` — complete rewrite. Old version opened every section with "You know these rules but violate them. Stop." / "You will try to use 14px. Don't." New version: "Common defaults that produce monoculture" framing. All load-bearing GSAP rules preserved verbatim (those are correct and critical). - `beat-direction.md` rhythm table — removed. Replaced with questions that derive rhythm from brand + storyboard. Verb table regrouped by physical character (Impact / Directional / Reveals / Organic / Mechanical) without energy labels. - `transitions.md` Energy → Transition table + Mood → Type table — removed named transitions, replaced with motion-quality descriptions (Soft/organic, Directional/purposeful, Percussive/instant). Mixing documented: CSS crossfade + shader in the same HyperShader composition (verified working). - `dynamic-techniques.md` energy table — restructured with explanatory principles (highlight amplitude, exit style, cycle variation) before showing the table as calibration reference. - `techniques.md` "When to Use What" table — deleted. Replaced with "choose techniques based on beat concept, not video genre." - `typography.md` — "Guardrails / You know these rules but violate them" → "Defaults to watch for." Banned fonts gain a caveat: if the brand actually uses one of these fonts, use it. - `video-composition.md` — fixed density contradiction ("8–10 visual elements" removed; sparse beats are intentional). **Text-effects bundle (new)** 24 named text-animation effects shipped as paired specs: - `assets/text-effects/effects/.json` — GSAP-specific recipe agents can paste verbatim - `assets/text-effects/specs/.json` — portable motion contract (engine-agnostic, so the same effect can be re-implemented in any animation library) Catalog at `references/text-effects.md`. Storyboards reference effects by name (typewriter, kinetic-center-build, shimmer-sweep, …) instead of saying "fades in," which produced inconsistent typography across beats. Effects organized by target: - Per-character (7): soft-blur-in, per-character-rise, typewriter, bottom-up-letters, top-down-letters, stagger-from-{center,edges} - Per-word (8): per-word-crossfade, spring-scale-in, shared-axis-y, blur-out-up, kinetic-center-build, short-slide-{right,down}, depth-parallax-words - Per-line (2): mask-reveal-up, line-by-line-slide - Whole element (7): micro-scale-fade, shimmer-sweep, fade-through, shared-axis-{x,z}, scale-down-fade, focus-blur-resolve Sources adapted from `pixel-point/animate-text`; copied into the repo so users don't need a separate install. **Misc cleanups** - `house-style.md` — light/dark prescription removed; defer to brand. - `prompt-expansion.md` — `design.md` → `DESIGN.md` casing fixed. - `html-in-canvas-patterns.md` — Three.js 0.147.0 (legacy `examples/js/`) → 0.181.2 (`examples/jsm/` ESM imports); `Math.random()` in the shatter example → mulberry32 seeded PRNG so output is deterministic. **.gitignore + CLAUDE.md** - `.gitignore` catches per-brand video project directories agents leave at the repo root (`huly-*/`, `raycast-*/`, `*-demo-*/`, `test-runs/`, `test-outputs/`) plus the `videos/` folder conventions. - `CLAUDE.md` documents the local CLI for `capture` + `snapshot` (since the published `npx hyperframes` doesn't yet include the capture pipeline improvements from this stack) and the local shader-transitions build copy convention. --- .gitignore | 15 + CLAUDE.md | 23 + skills/hyperframes/SKILL.md | 3 +- .../text-effects/effects/blur-out-up.json | 335 +++++++++++ .../effects/bottom-up-letters.json | 348 ++++++++++++ .../effects/depth-parallax-words.json | 53 ++ .../text-effects/effects/fade-through.json | 339 +++++++++++ .../effects/focus-blur-resolve.json | 339 +++++++++++ .../effects/kinetic-center-build.json | 469 ++++++++++++++++ .../effects/line-by-line-slide.json | 335 +++++++++++ .../text-effects/effects/mask-reveal-up.json | 339 +++++++++++ .../effects/micro-scale-fade.json | 331 +++++++++++ .../effects/per-character-rise.json | 347 ++++++++++++ .../effects/per-word-crossfade.json | 348 ++++++++++++ .../text-effects/effects/scale-down-fade.json | 335 +++++++++++ .../text-effects/effects/shared-axis-x.json | 49 ++ .../text-effects/effects/shared-axis-y.json | 335 +++++++++++ .../text-effects/effects/shared-axis-z.json | 335 +++++++++++ .../text-effects/effects/shimmer-sweep.json | 335 +++++++++++ .../effects/short-slide-down.json | 464 ++++++++++++++++ .../effects/short-slide-right.json | 330 +++++++++++ .../text-effects/effects/soft-blur-in.json | 351 ++++++++++++ .../text-effects/effects/spring-scale-in.json | 331 +++++++++++ .../effects/stagger-from-center.json | 50 ++ .../effects/stagger-from-edges.json | 50 ++ .../effects/top-down-letters.json | 348 ++++++++++++ .../text-effects/effects/typewriter.json | 331 +++++++++++ .../text-effects/specs/blur-out-up.json | 44 ++ .../text-effects/specs/bottom-up-letters.json | 57 ++ .../specs/depth-parallax-words.json | 48 ++ .../text-effects/specs/fade-through.json | 48 ++ .../specs/focus-blur-resolve.json | 48 ++ .../specs/kinetic-center-build.json | 84 +++ .../specs/line-by-line-slide.json | 40 ++ .../text-effects/specs/mask-reveal-up.json | 44 ++ .../text-effects/specs/micro-scale-fade.json | 40 ++ .../specs/per-character-rise.json | 56 ++ .../specs/per-word-crossfade.json | 57 ++ .../text-effects/specs/scale-down-fade.json | 44 ++ .../text-effects/specs/shared-axis-x.json | 44 ++ .../text-effects/specs/shared-axis-y.json | 44 ++ .../text-effects/specs/shared-axis-z.json | 44 ++ .../text-effects/specs/shimmer-sweep.json | 44 ++ .../text-effects/specs/short-slide-down.json | 83 +++ .../text-effects/specs/short-slide-right.json | 68 +++ .../text-effects/specs/soft-blur-in.json | 60 ++ .../text-effects/specs/spring-scale-in.json | 40 ++ .../specs/stagger-from-center.json | 45 ++ .../specs/stagger-from-edges.json | 45 ++ .../text-effects/specs/top-down-letters.json | 57 ++ .../assets/text-effects/specs/typewriter.json | 40 ++ skills/hyperframes/house-style.md | 6 +- .../hyperframes/references/beat-direction.md | 129 ++++- .../references/dynamic-techniques.md | 26 +- .../references/html-in-canvas-patterns.md | 504 +++++++++++++++++ .../references/motion-principles.md | 112 ++-- .../references/prompt-expansion.md | 6 +- skills/hyperframes/references/techniques.md | 524 +++++++++++++++++- skills/hyperframes/references/text-effects.md | 105 ++++ skills/hyperframes/references/transitions.md | 96 ++-- skills/hyperframes/references/typography.md | 42 +- .../references/video-composition.md | 6 +- skills/hyperframes/visual-styles.md | 508 ++++------------- 63 files changed, 9989 insertions(+), 567 deletions(-) create mode 100644 skills/hyperframes/assets/text-effects/effects/blur-out-up.json create mode 100644 skills/hyperframes/assets/text-effects/effects/bottom-up-letters.json create mode 100644 skills/hyperframes/assets/text-effects/effects/depth-parallax-words.json create mode 100644 skills/hyperframes/assets/text-effects/effects/fade-through.json create mode 100644 skills/hyperframes/assets/text-effects/effects/focus-blur-resolve.json create mode 100644 skills/hyperframes/assets/text-effects/effects/kinetic-center-build.json create mode 100644 skills/hyperframes/assets/text-effects/effects/line-by-line-slide.json create mode 100644 skills/hyperframes/assets/text-effects/effects/mask-reveal-up.json create mode 100644 skills/hyperframes/assets/text-effects/effects/micro-scale-fade.json create mode 100644 skills/hyperframes/assets/text-effects/effects/per-character-rise.json create mode 100644 skills/hyperframes/assets/text-effects/effects/per-word-crossfade.json create mode 100644 skills/hyperframes/assets/text-effects/effects/scale-down-fade.json create mode 100644 skills/hyperframes/assets/text-effects/effects/shared-axis-x.json create mode 100644 skills/hyperframes/assets/text-effects/effects/shared-axis-y.json create mode 100644 skills/hyperframes/assets/text-effects/effects/shared-axis-z.json create mode 100644 skills/hyperframes/assets/text-effects/effects/shimmer-sweep.json create mode 100644 skills/hyperframes/assets/text-effects/effects/short-slide-down.json create mode 100644 skills/hyperframes/assets/text-effects/effects/short-slide-right.json create mode 100644 skills/hyperframes/assets/text-effects/effects/soft-blur-in.json create mode 100644 skills/hyperframes/assets/text-effects/effects/spring-scale-in.json create mode 100644 skills/hyperframes/assets/text-effects/effects/stagger-from-center.json create mode 100644 skills/hyperframes/assets/text-effects/effects/stagger-from-edges.json create mode 100644 skills/hyperframes/assets/text-effects/effects/top-down-letters.json create mode 100644 skills/hyperframes/assets/text-effects/effects/typewriter.json create mode 100644 skills/hyperframes/assets/text-effects/specs/blur-out-up.json create mode 100644 skills/hyperframes/assets/text-effects/specs/bottom-up-letters.json create mode 100644 skills/hyperframes/assets/text-effects/specs/depth-parallax-words.json create mode 100644 skills/hyperframes/assets/text-effects/specs/fade-through.json create mode 100644 skills/hyperframes/assets/text-effects/specs/focus-blur-resolve.json create mode 100644 skills/hyperframes/assets/text-effects/specs/kinetic-center-build.json create mode 100644 skills/hyperframes/assets/text-effects/specs/line-by-line-slide.json create mode 100644 skills/hyperframes/assets/text-effects/specs/mask-reveal-up.json create mode 100644 skills/hyperframes/assets/text-effects/specs/micro-scale-fade.json create mode 100644 skills/hyperframes/assets/text-effects/specs/per-character-rise.json create mode 100644 skills/hyperframes/assets/text-effects/specs/per-word-crossfade.json create mode 100644 skills/hyperframes/assets/text-effects/specs/scale-down-fade.json create mode 100644 skills/hyperframes/assets/text-effects/specs/shared-axis-x.json create mode 100644 skills/hyperframes/assets/text-effects/specs/shared-axis-y.json create mode 100644 skills/hyperframes/assets/text-effects/specs/shared-axis-z.json create mode 100644 skills/hyperframes/assets/text-effects/specs/shimmer-sweep.json create mode 100644 skills/hyperframes/assets/text-effects/specs/short-slide-down.json create mode 100644 skills/hyperframes/assets/text-effects/specs/short-slide-right.json create mode 100644 skills/hyperframes/assets/text-effects/specs/soft-blur-in.json create mode 100644 skills/hyperframes/assets/text-effects/specs/spring-scale-in.json create mode 100644 skills/hyperframes/assets/text-effects/specs/stagger-from-center.json create mode 100644 skills/hyperframes/assets/text-effects/specs/stagger-from-edges.json create mode 100644 skills/hyperframes/assets/text-effects/specs/top-down-letters.json create mode 100644 skills/hyperframes/assets/text-effects/specs/typewriter.json create mode 100644 skills/hyperframes/references/html-in-canvas-patterns.md create mode 100644 skills/hyperframes/references/text-effects.md diff --git a/.gitignore b/.gitignore index 983925dfd..037b814d3 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ Thumbs.db # non-generated assets (logos, svgs) that should stay in the repo. docs/images/ +videos/ + # IDE .vscode/ .idea/ @@ -74,6 +76,7 @@ examples/* !examples/k8s-jobs !examples/k8s-jobs/** packages/studio/data/ + .desloppify/ .worktrees/ @@ -103,10 +106,22 @@ captures/ cursor-tests/ basecamp-video/ launch-video*/ +!skills/launch-video/ ab-test/ compositions/ video-6-2-patched/ claude-design-hyperframes-video/ +# Per-site video work at the repo root (huly-*, raycast-*, etc.) +# Anything under videos/ is already covered above, but agents sometimes write +# project dirs to the repo root when iterating. Catch the common per-brand +# patterns and any *-demo-N variants the *-demo/ rule above misses. +huly-*/ +raycast-*/ +*-demo-*/ +test-runs/ +test-outputs/ + +# Claude Code worktrees + superpowers docs .claude/worktrees/ .claude/ docs/superpowers/ diff --git a/CLAUDE.md b/CLAUDE.md index 4c8b6990a..2f3edad0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,29 @@ packages/ studio/ → Browser-based composition editor UI ``` +## Local CLI + +The published `npx hyperframes` is a released version and does **not** include local changes. Use the local CLI for commands where changes are in flight: + +```bash +# Local CLI (use instead of `npx hyperframes` for capture and snapshot): +npx tsx packages/cli/src/cli.ts + +# Examples: +npx tsx packages/cli/src/cli.ts capture -o videos//capture +npx tsx packages/cli/src/cli.ts snapshot videos/ --frames +``` + +Commands with local changes: **capture** (paginated contact sheets, fit:contain, SVG root scan), **snapshot** (3-col contact sheet). All other commands (lint, validate, preview, render) can use `npx hyperframes` as-is. + +For **shader transitions**, copy the local build into the video project instead of using the CDN — the local build includes CSS crossfade mixing support and other fixes not yet published: + +```bash +cp packages/shader-transitions/dist/index.global.js /hyper-shader-local.js +``` + +Then reference `hyper-shader-local.js` instead of the `@hyperframes/shader-transitions` CDN URL in `index.html`. + ## Development ```bash diff --git a/skills/hyperframes/SKILL.md b/skills/hyperframes/SKILL.md index f68c52dac..c06bfaa6d 100644 --- a/skills/hyperframes/SKILL.md +++ b/skills/hyperframes/SKILL.md @@ -473,7 +473,8 @@ Skip on small edits (fixing a color, adjusting one duration). Run on new composi - **[references/beat-direction.md](references/beat-direction.md)** — Beat planning: concept, mood, choreography verbs, rhythm templates, transition decisions, depth layers. **Always read for multi-scene compositions.** - **[references/typography.md](references/typography.md)** — Typography: font pairing, OpenType features, dark-background adjustments, font discovery script. **Always read** — every composition has text. - **[references/motion-principles.md](references/motion-principles.md)** — Motion design principles, image motion treatment, load-bearing GSAP rules. **Always read** — every composition has motion. -- **[references/techniques.md](references/techniques.md)** — 11 visual techniques with code patterns: SVG drawing, Canvas 2D, CSS 3D, kinetic type, Lottie, video compositing, typing effect, variable fonts, MotionPath, velocity transitions, audio-reactive. Read when planning techniques per beat. +- **[references/techniques.md](references/techniques.md)** — 20 visual techniques with code patterns: SVG drawing, Canvas 2D, CSS 3D, kinetic type, Lottie, video compositing, typing, variable fonts, MotionPath, velocity transitions, audio-reactive, frosted glass, clip-path reveals, WebGL shaders, impact lines, device mockups, aurora gradients, floating particles, terminal UI, moodboard layouts. Adapt the patterns — don't copy-paste. +- **[references/html-in-canvas-patterns.md](references/html-in-canvas-patterns.md)** — HTML-in-Canvas patterns: live DOM as GPU texture via `drawElementImage` + `layoutsubtree`. Shared boilerplate + ~6 effect recipes (iPhone/MacBook mockups, liquid glass, magnetic, portal, shatter, text cursor). Use for 1–3 hero beats per video. - **[references/narration.md](references/narration.md)** — Pacing, tone, script structure, number pronunciation, opening line patterns. Read when the composition includes voiceover or TTS. - **[references/design-picker.md](references/design-picker.md)** — Create a design.md via visual picker. Read when no design.md exists and the user wants to create one. - **[visual-styles.md](visual-styles.md)** — 8 named visual styles with hex palettes, GSAP easing signatures, and shader pairings. Read when user names a style or when generating design.md. diff --git a/skills/hyperframes/assets/text-effects/effects/blur-out-up.json b/skills/hyperframes/assets/text-effects/effects/blur-out-up.json new file mode 100644 index 000000000..7222f453e --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/blur-out-up.json @@ -0,0 +1,335 @@ +{ + "id": "blur-out-up", + "visibility": "visible", + "portable_spec": { + "id": "blur-out-up", + "display_name": "Blur Out Up", + "description": "Words arrive clean and depart upward with increasing blur for airy exits.", + "inspiration": "Apple-style light typography where exit has more character than entry.", + "target": "per-word", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 560, + "stagger_ms": 28, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 10, + "blur_px": 6 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 480, + "stagger_ms": 24, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -14, + "blur_px": 8 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 170, + "micro_delay_ms": 35 + }, + "usage_notes": "Works best on short phrases; avoid very long lines to keep swap time tight." + }, + "showcase": { + "content": { + "sample": "Clear in, airy out.", + "samples": ["Clear in, airy out.", "Lightweight typography.", "Exit with grace."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "blur-out-up" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 35, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 560, + "source_stagger_ms": 28, + "scaled_duration_ms": 403, + "scaled_stagger_ms": 20, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)" + }, + "exit": { + "source_duration_ms": 480, + "source_stagger_ms": 24, + "scaled_duration_ms": 346, + "scaled_stagger_ms": 17, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "per-word", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/bottom-up-letters.json b/skills/hyperframes/assets/text-effects/effects/bottom-up-letters.json new file mode 100644 index 000000000..c1d18ef40 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/bottom-up-letters.json @@ -0,0 +1,348 @@ +{ + "id": "bottom-up-letters", + "visibility": "visible", + "portable_spec": { + "id": "bottom-up-letters", + "display_name": "Bottom-Up Letters", + "description": "Letters rise from below in a pronounced staircase, one symbol at a time, with zero blur.", + "inspiration": "Apple-style keynote typography, sharp lower-thirds, and clean editorial word swaps.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.18, 1, 0.32, 1)", + "enter": { + "duration_ms": 400, + "stagger_ms": 88, + "easing": "cubic-bezier(0.18, 1, 0.32, 1)", + "from": { + "opacity": 0, + "y_px": 46 + }, + "to": { + "opacity": 1, + "y_px": 0 + } + }, + "exit": { + "duration_ms": 280, + "stagger_ms": 28, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "y_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -14 + } + }, + "swap": { + "mode": "sequential", + "overlap_ms": 0, + "micro_delay_ms": 35, + "scenario_spec": { + "entry_condition": "Use when short words or compact headlines should build upward letter by letter with completely crisp glyph edges.", + "switch_order": [ + "Run old text exit first so the slot clears cleanly.", + "Wait micro_delay_ms after exit.", + "Start new text enter from below with per-character stagger." + ], + "verification": [ + "Letters never blur during enter or exit.", + "The reveal clearly reads bottom-up rather than typewriter-left-to-right.", + "Spacing remains stable while characters settle." + ], + "fallback": { + "if_motion_feels_too_tall": "Reduce enter from.y_px from 46 to 36.", + "if_readability_drops": "Increase stagger_ms from 88 to 100 for even more separation." + } + } + }, + "usage_notes": "Best for short single words, labels, or compact headline swaps at 40px+. This version is intentionally more staged than per-character-rise: very large per-symbol delay, fewer simultaneous letters on screen, and a taller lift from below." + }, + "showcase": { + "content": { + "sample": "Shift", + "samples": ["Shift", "Stage", "Letter"] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "bottom-up-letters" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 35, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 400, + "source_stagger_ms": 88, + "scaled_duration_ms": 288, + "scaled_stagger_ms": 63, + "easing": "cubic-bezier(0.18, 1, 0.32, 1)" + }, + "exit": { + "source_duration_ms": 280, + "source_stagger_ms": 28, + "scaled_duration_ms": 202, + "scaled_stagger_ms": 20, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "per-character", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/depth-parallax-words.json b/skills/hyperframes/assets/text-effects/effects/depth-parallax-words.json new file mode 100644 index 000000000..8503f98cc --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/depth-parallax-words.json @@ -0,0 +1,53 @@ +{ + "id": "depth-parallax-words", + "visibility": "hidden", + "portable_spec": { + "id": "depth-parallax-words", + "display_name": "Depth Parallax Words", + "description": "Per-word depth motion with scale and vertical drift for layered readability.", + "inspiration": "Product landing pages combining depth cues with clean typography.", + "target": "per-word", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 700, + "stagger_ms": 70, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 18, + "scale": 0.92, + "blur_px": 3 + }, + "to": { + "opacity": 1, + "y_px": 0, + "scale": 1, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 500, + "stagger_ms": 45, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "scale": 1, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -10, + "scale": 1.05, + "blur_px": 2 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 180, + "micro_delay_ms": 30 + }, + "usage_notes": "Use short copy blocks and moderate stagger to avoid visual overload." + }, + "showcase": null +} diff --git a/skills/hyperframes/assets/text-effects/effects/fade-through.json b/skills/hyperframes/assets/text-effects/effects/fade-through.json new file mode 100644 index 000000000..956830347 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/fade-through.json @@ -0,0 +1,339 @@ +{ + "id": "fade-through", + "visibility": "visible", + "portable_spec": { + "id": "fade-through", + "display_name": "Fade Through", + "description": "A Material-style content transition: old fades out, new fades in with a soft delay.", + "inspiration": "Google Material fade through transitions for same-level UI changes.", + "target": "whole", + "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", + "enter": { + "duration_ms": 420, + "stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0, 0, 1)", + "from": { + "opacity": 0, + "y_px": 6, + "scale": 0.99, + "blur_px": 2 + }, + "to": { + "opacity": 1, + "y_px": 0, + "scale": 1, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 260, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 1, 1)", + "from": { + "opacity": 1, + "y_px": 0, + "scale": 1, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -4, + "scale": 1, + "blur_px": 0 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 20, + "micro_delay_ms": 60 + }, + "usage_notes": "Best for replacing content in the same layout slot without directional meaning." + }, + "showcase": { + "content": { + "sample": "Calm transitions.", + "samples": ["Calm transitions.", "Fade through content.", "Focus shifts smoothly."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "fade-through" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 60, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 420, + "source_stagger_ms": 0, + "scaled_duration_ms": 302, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0, 0, 1)" + }, + "exit": { + "source_duration_ms": 260, + "source_stagger_ms": 0, + "scaled_duration_ms": 187, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 1, 1)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "whole", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/focus-blur-resolve.json b/skills/hyperframes/assets/text-effects/effects/focus-blur-resolve.json new file mode 100644 index 000000000..969f8d632 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/focus-blur-resolve.json @@ -0,0 +1,339 @@ +{ + "id": "focus-blur-resolve", + "visibility": "visible", + "portable_spec": { + "id": "focus-blur-resolve", + "display_name": "Focus Blur Resolve", + "description": "A premium focus pull from heavy blur to crisp text, then a soft blur-out exit.", + "inspiration": "Apple-style hero transitions that resolve detail with cinematic restraint.", + "target": "whole", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 760, + "stagger_ms": 0, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 14, + "blur_px": 14, + "scale": 1.01 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0, + "scale": 1 + } + }, + "exit": { + "duration_ms": 520, + "stagger_ms": 0, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0, + "scale": 1 + }, + "to": { + "opacity": 0, + "y_px": -10, + "blur_px": 10, + "scale": 1 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 160, + "micro_delay_ms": 35 + }, + "usage_notes": "Best on large headlines where blur distance reads as intentional and premium." + }, + "showcase": { + "content": { + "sample": "Focus resolves clearly.", + "samples": ["Focus resolves clearly.", "Detail emerges.", "Then softly recedes."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "focus-blur-resolve" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 35, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 760, + "source_stagger_ms": 0, + "scaled_duration_ms": 547, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)" + }, + "exit": { + "source_duration_ms": 520, + "source_stagger_ms": 0, + "scaled_duration_ms": 374, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "whole", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/kinetic-center-build.json b/skills/hyperframes/assets/text-effects/effects/kinetic-center-build.json new file mode 100644 index 000000000..bcef847f1 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/kinetic-center-build.json @@ -0,0 +1,469 @@ +{ + "id": "kinetic-center-build", + "visibility": "visible", + "portable_spec": { + "id": "kinetic-center-build", + "display_name": "Kinetic Center Build", + "description": "A word appears in the center; each new word enters from right to left with a soft blur and pushes the existing line until the full phrase locks centered.", + "inspiration": "Apple keynote kinetic editorial typography and sequential phrase builds.", + "target": "per-word", + "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "enter": { + "duration_ms": 360, + "stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "from": { + "opacity": 0, + "y_px": 6, + "scale": 0.992, + "blur_px": 3.5 + }, + "to": { + "opacity": 1, + "y_px": 0, + "scale": 1, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 260, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 0.2, 1)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -6, + "blur_px": 2.5 + } + }, + "swap": { + "mode": "sequential", + "overlap_ms": 0, + "micro_delay_ms": 220, + "scenario_spec": { + "entry_condition": "Use when a short phrase should be built word-by-word, with each new word entering from the right and physically re-centering the existing line.", + "switch_order": [ + "Show the first word in the center.", + "Bring the second word in from right to left while shifting the first word left.", + "Bring the third word in from right to left while shifting the first two words so the final phrase stays centered." + ], + "verification": [ + "Each new word visibly pushes the existing words rather than simply fading in.", + "The completed phrase ends centered and evenly spaced.", + "The motion reads as one kinetic line build, not as three isolated reveals." + ], + "fallback": { + "if_push_is_too_subtle": "Increase build.entry_offset_px from 96 to 120.", + "if_phrase_feels_too_slow": "Reduce build.push_duration_ms from 480 to 420." + } + } + }, + "build": { + "entry_direction": "from-right", + "line_alignment": "center", + "first_word_duration_ms": 340, + "push_duration_ms": 430, + "entry_offset_px": 88, + "word_gap_px": 10, + "first_word_y_px": 6, + "entry_scale": 0.992, + "entry_blur_px": 3.5, + "reflow_blur_px": 0.8, + "exit_y_px": -6, + "exit_blur_px": 2.5, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)", + "phrase_samples": [ + ["Words", "push", "left"], + ["Type", "locks", "center"], + ["Build", "the", "line"] + ] + }, + "usage_notes": "Layout-aware effect: each incoming word changes the target x-position of the whole line. Best for short three-word phrases; implementation requires measuring word widths and animating existing words to new positions. A small entry and reflow blur helps the push feel smoother without extending the timing." + }, + "showcase": { + "content": { + "sample": "Words push left.", + "phrases": [ + ["Words", "push", "left"], + ["Type", "locks", "center"], + ["Build", "the", "line"] + ] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "kinetic-center-build" + }, + "renderer": { + "id": "kinetic-center-build", + "source": "catalog-override", + "params": { + "entry_direction": "from-right", + "line_alignment": "center", + "first_word_duration_ms": 340, + "push_duration_ms": 430, + "entry_offset_px": 88, + "word_gap_px": 10, + "first_word_y_px": 6, + "entry_scale": 0.992, + "entry_blur_px": 3.5, + "reflow_blur_px": 0.8, + "exit_y_px": -6, + "exit_blur_px": 2.5, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)", + "phrase_samples": [ + ["Words", "push", "left"], + ["Type", "locks", "center"], + ["Build", "the", "line"] + ] + }, + "recipe": { + "id": "kinetic-center-build", + "summary": "Build a centered horizontal phrase word by word; each incoming word enters from the right and pushes existing words into newly centered positions.", + "required_measurements": ["offsetWidth for every word after appending the incoming word"], + "algorithm": [ + "Create a relative kinetic line container using the kinetic-line-host stage preset.", + "For each phrase word, append an absolutely centered word span.", + "Measure all child widths and compute centered x positions: totalWidth = sum(widths) + word_gap_px * (count - 1); cursor starts at -totalWidth / 2; each word position is cursor + width / 2.", + "First word enters at x=0 with first_word_y_px, entry_scale, entry_blur_px, and opacity 0, then settles to x=0/y=0/scale=1/blur=0/opacity=1.", + "For later words, animate existing words from previous x positions to next centered x positions while the incoming word starts at targetX + entry_offset_px and lands at targetX.", + "Use an intermediate keyframe around offset 0.52 for existing-word reflow blur and 0.6 for incoming-word settle blur.", + "After every push, snap all words to exact final poses to avoid accumulated engine drift.", + "Exit all words together from current centered x positions with exit_y_px and exit_blur_px, then clear the line." + ], + "frame_materialization": { + "coordinate_space": "x/y values are renderer pixel coordinates and are not multiplied by runtime.y_travel_multiplier.", + "transform": "translate(-50%, -50%) translate3d(x, y, 0) scale(scale)", + "filter": "blur(blur)", + "opacity": "unit opacity" + }, + "keyframe_recipe": { + "first_word": [ + { + "offset": 0, + "x": 0, + "y": "build.first_word_y_px", + "scale": "build.entry_scale", + "blur": "build.entry_blur_px", + "opacity": 0 + }, + { + "offset": 0.58, + "x": 0, + "y": "build.first_word_y_px * 0.35", + "scale": 0.998, + "blur": "build.entry_blur_px * 0.45", + "opacity": 0.78 + }, + { + "offset": 1, + "x": 0, + "y": 0, + "scale": 1, + "blur": 0, + "opacity": 1 + } + ], + "existing_word_push": [ + { + "offset": 0, + "x": "currentX", + "y": 0, + "scale": 1, + "blur": 0, + "opacity": 1 + }, + { + "offset": 0.52, + "x": "mix(currentX, nextX, 0.58)", + "y": 0, + "scale": 1, + "blur": "build.reflow_blur_px", + "opacity": 1 + }, + { + "offset": 1, + "x": "nextX", + "y": 0, + "scale": 1, + "blur": 0, + "opacity": 1 + } + ], + "incoming_word_push": [ + { + "offset": 0, + "x": "targetX + build.entry_offset_px", + "y": 0, + "scale": "build.entry_scale", + "blur": "build.entry_blur_px", + "opacity": 0 + }, + { + "offset": 0.6, + "x": "mix(targetX + build.entry_offset_px, targetX, 0.72)", + "y": 0, + "scale": 0.998, + "blur": "build.entry_blur_px * 0.38", + "opacity": 0.84 + }, + { + "offset": 1, + "x": "targetX", + "y": 0, + "scale": 1, + "blur": 0, + "opacity": 1 + } + ], + "exit_word": [ + { + "offset": 0, + "x": "position", + "y": 0, + "scale": 1, + "blur": 0, + "opacity": 1 + }, + { + "offset": 0.52, + "x": "position", + "y": "build.exit_y_px * 0.45", + "scale": 1, + "blur": "build.exit_blur_px * 0.55", + "opacity": 0.62 + }, + { + "offset": 1, + "x": "position", + "y": "build.exit_y_px", + "scale": 1, + "blur": "build.exit_blur_px", + "opacity": 0 + } + ] + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["build-phrase", "hold", "exit-phrase", "gap"], + "replacement_behavior": "phrase-loop", + "hold_ms": 706, + "micro_delay_ms": 0, + "gap_ms": 158 + }, + "timing": { + "first_word": { + "source_duration_ms": 340, + "scaled_duration_ms": 245, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" + }, + "push": { + "source_duration_ms": 430, + "scaled_duration_ms": 310, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" + }, + "exit": { + "source_duration_ms": 260, + "scaled_duration_ms": 187, + "easing": "cubic-bezier(0.4, 0, 0.2, 1)" + }, + "hold_ms": 706, + "gap_ms": 158 + }, + "stage": { + "preset": "kinetic-line-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + }, + "kinetic_container": { + "requirement": "Use a relative-positioned inline host large enough for the phrase; exact dimensions belong to the consuming UI.", + "position": "relative", + "coordinate_origin": "center" + }, + "kinetic_word": { + "backface_visibility": "hidden", + "left": "50%", + "position": "absolute", + "top": "50%", + "white_space": "nowrap", + "absolute_centered": true, + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "kinetic-center-build", + "target": "per-word", + "stagger_mode": "normal", + "coordinate_space": "renderer-pixels", + "y_travel_multiplier": 1, + "y_travel_multiplier_note": "runtime.y_travel_multiplier is not applied to kinetic build coordinates; x/y values in build params are final transform pixels.", + "transform_order": "translate(-50%, -50%) translate3d(x_px, y_px, 0) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "follow renderer recipe algorithm" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Measure word widths after appending each incoming word.", + "Compute centered x positions from measured widths and word_gap_px.", + "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", + "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow x is mix(currentX, nextX, 0.58) at offset 0.52; incoming-word settle x is mix(startX, targetX, 0.72) at offset 0.6.", + "Exit uses a three-keyframe path with offset 0.52 at y = exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Measure word widths after appending each incoming word.", + "Compute centered x positions from measured widths and word_gap_px.", + "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", + "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow x is mix(currentX, nextX, 0.58) at offset 0.52; incoming-word settle x is mix(startX, targetX, 0.72) at offset 0.6.", + "Exit uses a three-keyframe path with offset 0.52 at y = exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Measure word widths after appending each incoming word.", + "Compute centered x positions from measured widths and word_gap_px.", + "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", + "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow x is mix(currentX, nextX, 0.58) at offset 0.52; incoming-word settle x is mix(startX, targetX, 0.72) at offset 0.6.", + "Exit uses a three-keyframe path with offset 0.52 at y = exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "All engines", + "notes": [ + "Do not apply runtime.y_travel_multiplier to kinetic build x/y coordinates; buildKineticFrame uses the build params as final transform pixels.", + "Use explicit offset keyframes for the intermediate reflow frames, then snap final styles after each push to avoid layout drift." + ] + } + ], + "reproduction_notes": [ + "On the site this effect is layout-aware. Measure word widths, compute centered x positions for the whole phrase, and animate existing words to their next positions while the incoming word enters from the right.", + "For site parity, scale duration and stagger timing by 0.72. Keep kinetic build x/y params as raw renderer pixel coordinates; runtime.y_travel_multiplier applies to generic/title frame conversion, not to buildKineticFrame coordinates.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/line-by-line-slide.json b/skills/hyperframes/assets/text-effects/effects/line-by-line-slide.json new file mode 100644 index 000000000..4a1edd4c4 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/line-by-line-slide.json @@ -0,0 +1,335 @@ +{ + "id": "line-by-line-slide", + "visibility": "visible", + "portable_spec": { + "id": "line-by-line-slide", + "display_name": "Line-by-Line Slide", + "description": "Each line enters from the left with a staggered slide and exits to the right for a flowing paragraph reveal.", + "inspiration": "Apple landing page subheads and section headers that breathe line by line.", + "target": "per-line", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 900, + "stagger_ms": 120, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "x_px": -48 + }, + "to": { + "opacity": 1, + "x_px": 0 + } + }, + "exit": { + "duration_ms": 600, + "stagger_ms": 80, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "x_px": 0 + }, + "to": { + "opacity": 0, + "x_px": 48 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 20 + }, + "usage_notes": "Great for 2-line or 3-line headings. This variant keeps swap non-overlapping to avoid content intersections. Reduce x-distance for narrow layouts to keep motion tight on mobile." + }, + "showcase": { + "content": { + "sample": "Think different.\nDo more.", + "samples": [ + "Think different.\nDo more.", + "Built for speed.\nMade to last.", + "Clear ideas.\nClean motion." + ] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "line-by-line-slide" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 20, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 900, + "source_stagger_ms": 120, + "scaled_duration_ms": 648, + "scaled_stagger_ms": 86, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)" + }, + "exit": { + "source_duration_ms": 600, + "source_stagger_ms": 80, + "scaled_duration_ms": 432, + "scaled_stagger_ms": 58, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "per-line", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/mask-reveal-up.json b/skills/hyperframes/assets/text-effects/effects/mask-reveal-up.json new file mode 100644 index 000000000..65b2f9e34 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/mask-reveal-up.json @@ -0,0 +1,339 @@ +{ + "id": "mask-reveal-up", + "visibility": "visible", + "portable_spec": { + "id": "mask-reveal-up", + "display_name": "Mask Reveal Up", + "description": "Lines reveal upward with a soft masked feel and compact stagger.", + "inspiration": "Apple section transitions where multiline copy rises in with control.", + "target": "per-line", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 760, + "stagger_ms": 90, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 30, + "blur_px": 6 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 520, + "stagger_ms": 70, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -22, + "blur_px": 6 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 210, + "micro_delay_ms": 35 + }, + "usage_notes": "Best for two-line and three-line headings where line order should stay readable." + }, + "showcase": { + "content": { + "sample": "Designed to move.\nBuilt to focus.", + "samples": [ + "Designed to move.\nBuilt to focus.", + "Quiet motion.\nStrong hierarchy.", + "Premium feel.\nEvery frame." + ] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "mask-reveal-up" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 35, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 760, + "source_stagger_ms": 90, + "scaled_duration_ms": 547, + "scaled_stagger_ms": 65, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)" + }, + "exit": { + "source_duration_ms": 520, + "source_stagger_ms": 70, + "scaled_duration_ms": 374, + "scaled_stagger_ms": 50, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "per-line", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/micro-scale-fade.json b/skills/hyperframes/assets/text-effects/effects/micro-scale-fade.json new file mode 100644 index 000000000..a27d7fa30 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/micro-scale-fade.json @@ -0,0 +1,331 @@ +{ + "id": "micro-scale-fade", + "visibility": "visible", + "portable_spec": { + "id": "micro-scale-fade", + "display_name": "Micro Scale Fade", + "description": "A calm, tiny scale pop used as subtle premium polish for labels and headings.", + "inspiration": "Apple system status copy, secondary UI labels, and lightweight onboarding micro-animations.", + "target": "whole", + "signature_easing": "cubic-bezier(0.32, 0.72, 0, 1)", + "enter": { + "duration_ms": 600, + "stagger_ms": 0, + "easing": "cubic-bezier(0.32, 0.72, 0, 1)", + "from": { + "opacity": 0, + "scale": 0.96 + }, + "to": { + "opacity": 1, + "scale": 1 + } + }, + "exit": { + "duration_ms": 400, + "stagger_ms": 0, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "scale": 1 + }, + "to": { + "opacity": 0, + "scale": 0.96 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 20 + }, + "usage_notes": "Use this for single words or short titles. This variant keeps swap non-overlapping to avoid content intersections. For paragraphs, switch target to per-word to avoid perceivable lag." + }, + "showcase": { + "content": { + "sample": "Welcome to motion.", + "samples": ["Welcome to motion.", "Small details matter.", "Quietly premium."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "micro-scale-fade" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 20, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 600, + "source_stagger_ms": 0, + "scaled_duration_ms": 432, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.32, 0.72, 0, 1)" + }, + "exit": { + "source_duration_ms": 400, + "source_stagger_ms": 0, + "scaled_duration_ms": 288, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "whole", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/per-character-rise.json b/skills/hyperframes/assets/text-effects/effects/per-character-rise.json new file mode 100644 index 000000000..c5cd35513 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/per-character-rise.json @@ -0,0 +1,347 @@ +{ + "id": "per-character-rise", + "visibility": "visible", + "portable_spec": { + "id": "per-character-rise", + "display_name": "Per-Character Rise", + "description": "Letters slide up from below with no blur — crisp, deliberate, kinetic. Apple's clean tvOS-style reveal.", + "inspiration": "Apple tvOS, Fitness+ intros, iPadOS home screen title appearances.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "enter": { + "duration_ms": 700, + "stagger_ms": 24, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "from": { + "opacity": 0, + "y_px": 32 + }, + "to": { + "opacity": 1, + "y_px": 0 + } + }, + "exit": { + "duration_ms": 420, + "stagger_ms": 14, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "y_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -24 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 210, + "scenario_spec": { + "entry_condition": "Use for headline replacement where each character must remain crisp and readable throughout the switch.", + "switch_order": [ + "Start old text exit at t=0ms.", + "Start new text enter at t=exit_total_ms-overlap_ms.", + "Use a single active headline layer after enter starts to avoid stacked glyph artifacts." + ], + "verification": [ + "Characters never blur during swap.", + "No visible pause appears between exit and enter phases.", + "Swap keeps staircase rhythm from stagger settings." + ], + "fallback": { + "if_glyphs_collide": "Lower overlap_ms to 140.", + "if_motion_feels_slow": "Reduce enter stagger_ms from 24 to 18." + } + } + }, + "usage_notes": "Works on 40px+ headlines. Zero blur keeps it sharp — that's the key distinction from soft-blur-in. Stagger 24ms gives it quicker momentum; don't go below 16ms or it flattens." + }, + "showcase": { + "content": { + "sample": "One more thing.", + "samples": ["One more thing.", "Fast and fluid.", "Sharp by design."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "per-character-rise" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 0, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 700, + "source_stagger_ms": 24, + "scaled_duration_ms": 504, + "scaled_stagger_ms": 17, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" + }, + "exit": { + "source_duration_ms": 420, + "source_stagger_ms": 14, + "scaled_duration_ms": 302, + "scaled_stagger_ms": 10, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "per-character", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/per-word-crossfade.json b/skills/hyperframes/assets/text-effects/effects/per-word-crossfade.json new file mode 100644 index 000000000..5e1341a06 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/per-word-crossfade.json @@ -0,0 +1,348 @@ +{ + "id": "per-word-crossfade", + "visibility": "visible", + "portable_spec": { + "id": "per-word-crossfade", + "display_name": "Per-Word Crossfade", + "description": "Words gently fade into place one after another, with a short vertical drift for a calm keynote rhythm.", + "inspiration": "Apple product announcements and section title transitions where words are readable but still alive.", + "target": "per-word", + "signature_easing": "cubic-bezier(0.16, 1, 0.3, 1)", + "enter": { + "duration_ms": 700, + "stagger_ms": 70, + "easing": "cubic-bezier(0.16, 1, 0.3, 1)", + "from": { + "opacity": 0, + "y_px": 8 + }, + "to": { + "opacity": 1, + "y_px": 0 + } + }, + "exit": { + "duration_ms": 500, + "stagger_ms": 40, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "y_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -6 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 170, + "micro_delay_ms": 70, + "scenario_spec": { + "entry_condition": "Use when phrase-level content changes and word readability is more important than per-character flair.", + "switch_order": [ + "Start old text exit at t=0ms.", + "Start new text enter at t=exit_total_ms-overlap_ms+micro_delay_ms.", + "Advance word groups in the same stagger direction for old and new text." + ], + "verification": [ + "Word boundaries stay readable during overlap.", + "No two identical word positions stay stacked for more than one stagger step.", + "Swap cadence stays calm and editorial, without abrupt jumps." + ], + "fallback": { + "if_words_stack_visibly": "Increase micro_delay_ms to 90.", + "if_total_swap_is_too_long": "Reduce enter stagger_ms to 55 and overlap_ms to 120." + } + } + }, + "usage_notes": "Best for medium phrases and headings; for long copy prefer per-word only up to 16–18 words to keep total stagger time readable. micro_delay_ms helps prevent old/new words from visibly stacking during swaps." + }, + "showcase": { + "content": { + "sample": "Beautifully, unmistakably simple.", + "samples": ["Beautifully simple.", "Designed for focus.", "Built for people."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "per-word-crossfade" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 70, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 700, + "source_stagger_ms": 70, + "scaled_duration_ms": 504, + "scaled_stagger_ms": 50, + "easing": "cubic-bezier(0.16, 1, 0.3, 1)" + }, + "exit": { + "source_duration_ms": 500, + "source_stagger_ms": 40, + "scaled_duration_ms": 360, + "scaled_stagger_ms": 29, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "per-word", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/scale-down-fade.json b/skills/hyperframes/assets/text-effects/effects/scale-down-fade.json new file mode 100644 index 000000000..4b05a2344 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/scale-down-fade.json @@ -0,0 +1,335 @@ +{ + "id": "scale-down-fade", + "visibility": "visible", + "portable_spec": { + "id": "scale-down-fade", + "display_name": "Scale Down Fade", + "description": "Subtle premium settle-in with a restrained scale-down fade on exit.", + "inspiration": "Apple product copy transitions where motion remains quiet and precise.", + "target": "whole", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 520, + "stagger_ms": 0, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 8, + "scale": 1.04 + }, + "to": { + "opacity": 1, + "y_px": 0, + "scale": 1 + } + }, + "exit": { + "duration_ms": 380, + "stagger_ms": 0, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "scale": 1 + }, + "to": { + "opacity": 0, + "y_px": -8, + "scale": 0.94 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 130, + "micro_delay_ms": 20 + }, + "usage_notes": "Safe default for product UIs where copy should feel polished but not animated." + }, + "showcase": { + "content": { + "sample": "Quietly refined.", + "samples": ["Quietly refined.", "Polished transitions.", "A soft close."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "scale-down-fade" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 20, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 520, + "source_stagger_ms": 0, + "scaled_duration_ms": 374, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)" + }, + "exit": { + "source_duration_ms": 380, + "source_stagger_ms": 0, + "scaled_duration_ms": 274, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "whole", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/shared-axis-x.json b/skills/hyperframes/assets/text-effects/effects/shared-axis-x.json new file mode 100644 index 000000000..c96f8c179 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/shared-axis-x.json @@ -0,0 +1,49 @@ +{ + "id": "shared-axis-x", + "visibility": "hidden", + "portable_spec": { + "id": "shared-axis-x", + "display_name": "Shared Axis X", + "description": "Horizontal shared-axis transition for sibling destinations with continuity.", + "inspiration": "Google Material shared axis (X) transitions.", + "target": "whole", + "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", + "enter": { + "duration_ms": 500, + "stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0, 0, 1)", + "from": { + "opacity": 0, + "x_px": 24, + "scale": 0.98 + }, + "to": { + "opacity": 1, + "x_px": 0, + "scale": 1 + } + }, + "exit": { + "duration_ms": 360, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 1, 1)", + "from": { + "opacity": 1, + "x_px": 0, + "scale": 1 + }, + "to": { + "opacity": 0, + "x_px": -20, + "scale": 0.98 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 120, + "micro_delay_ms": 20 + }, + "usage_notes": "Use when moving between same-level views where horizontal direction conveys progress." + }, + "showcase": null +} diff --git a/skills/hyperframes/assets/text-effects/effects/shared-axis-y.json b/skills/hyperframes/assets/text-effects/effects/shared-axis-y.json new file mode 100644 index 000000000..e9705d282 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/shared-axis-y.json @@ -0,0 +1,335 @@ +{ + "id": "shared-axis-y", + "visibility": "visible", + "portable_spec": { + "id": "shared-axis-y", + "display_name": "Word Cut Staircase", + "description": "Per-word hard-cut transition with staircase timing for sharp editorial swaps.", + "inspiration": "Hard-cut typography timing with stepped word sequencing.", + "target": "per-word", + "signature_easing": "steps(1, end)", + "enter": { + "duration_ms": 180, + "stagger_ms": 78, + "easing": "steps(1, end)", + "from": { + "opacity": 0, + "y_px": 0, + "scale": 1 + }, + "to": { + "opacity": 1, + "y_px": 0, + "scale": 1 + } + }, + "exit": { + "duration_ms": 140, + "stagger_ms": 78, + "easing": "steps(1, end)", + "from": { + "opacity": 1, + "y_px": 0, + "scale": 1 + }, + "to": { + "opacity": 0, + "y_px": 0, + "scale": 1 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 28 + }, + "usage_notes": "Use for bold word-by-word hard cuts. No overlap keeps phrase swaps visually clean." + }, + "showcase": { + "content": { + "sample": "Layered navigation.", + "samples": ["Layered navigation.", "Hierarchy made clear.", "Depth with restraint."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "shared-axis-y" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 28, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 180, + "source_stagger_ms": 78, + "scaled_duration_ms": 140, + "scaled_stagger_ms": 56, + "easing": "steps(1, end)" + }, + "exit": { + "source_duration_ms": 140, + "source_stagger_ms": 78, + "scaled_duration_ms": 140, + "scaled_stagger_ms": 56, + "easing": "steps(1, end)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "per-word", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/shared-axis-z.json b/skills/hyperframes/assets/text-effects/effects/shared-axis-z.json new file mode 100644 index 000000000..2bce463de --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/shared-axis-z.json @@ -0,0 +1,335 @@ +{ + "id": "shared-axis-z", + "visibility": "visible", + "portable_spec": { + "id": "shared-axis-z", + "display_name": "Shared Axis Z", + "description": "Scale-based shared-axis transition for focus shifts and context depth.", + "inspiration": "Google Material shared axis (Z), adapted for typography swaps.", + "target": "whole", + "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", + "enter": { + "duration_ms": 520, + "stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0, 0, 1)", + "from": { + "opacity": 0, + "scale": 0.9, + "blur_px": 2 + }, + "to": { + "opacity": 1, + "scale": 1, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 360, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 1, 1)", + "from": { + "opacity": 1, + "scale": 1, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "scale": 1.06, + "blur_px": 1 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 100, + "micro_delay_ms": 20 + }, + "usage_notes": "Use for emphasizing focus transitions where scale communicates depth." + }, + "showcase": { + "content": { + "sample": "Zooming between states.", + "samples": ["Zooming between states.", "Elevate and settle.", "Scale with purpose."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "shared-axis-z" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 20, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 520, + "source_stagger_ms": 0, + "scaled_duration_ms": 374, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0, 0, 1)" + }, + "exit": { + "source_duration_ms": 360, + "source_stagger_ms": 0, + "scaled_duration_ms": 259, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 1, 1)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "whole", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/shimmer-sweep.json b/skills/hyperframes/assets/text-effects/effects/shimmer-sweep.json new file mode 100644 index 000000000..35dcd6029 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/shimmer-sweep.json @@ -0,0 +1,335 @@ +{ + "id": "shimmer-sweep", + "visibility": "visible", + "portable_spec": { + "id": "shimmer-sweep", + "display_name": "Shimmer Sweep", + "description": "A subtle sweep across a clean headline, blending in while gliding from left to center.", + "inspiration": "Premium hero copy transitions where a short soft push is used before settle.", + "target": "whole", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 850, + "stagger_ms": 0, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "x_px": -22, + "blur_px": 8 + }, + "to": { + "opacity": 1, + "x_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 650, + "stagger_ms": 0, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "x_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "x_px": 22, + "blur_px": 8 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 36 + }, + "usage_notes": "Use as a premium micro-transition for title swaps and copy refreshes. This variant avoids overlap between outgoing and incoming text." + }, + "showcase": { + "content": { + "sample": "Shiny details.", + "samples": ["Shiny details.", "Glide with intent.", "Soft and precise."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "shimmer-sweep" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 36, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 850, + "source_stagger_ms": 0, + "scaled_duration_ms": 612, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)" + }, + "exit": { + "source_duration_ms": 650, + "source_stagger_ms": 0, + "scaled_duration_ms": 468, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "whole", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/short-slide-down.json b/skills/hyperframes/assets/text-effects/effects/short-slide-down.json new file mode 100644 index 000000000..96db9f38e --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/short-slide-down.json @@ -0,0 +1,464 @@ +{ + "id": "short-slide-down", + "visibility": "visible", + "portable_spec": { + "id": "short-slide-down", + "display_name": "Short Slide Down", + "description": "Each new word drops in from above into its own line and pushes the existing stack downward until a centered three-line composition locks in place.", + "inspiration": "Keynote-style editorial headings where motion is present but tightly restrained.", + "target": "per-word", + "custom_renderer": "kinetic-top-build", + "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "enter": { + "duration_ms": 520, + "stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "from": { + "opacity": 0, + "y_px": -24, + "blur_px": 2.4, + "scale": 0.992 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0, + "scale": 1 + } + }, + "exit": { + "duration_ms": 320, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 0.2, 1)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0, + "scale": 1 + }, + "to": { + "opacity": 0, + "y_px": 10, + "blur_px": 1.2, + "scale": 1 + } + }, + "build": { + "first_word_duration_ms": 360, + "push_duration_ms": 500, + "exit_duration_ms": 320, + "hold_ms": 1100, + "between_phrases_ms": 180, + "entry_offset_y_px": -28, + "line_gap_px": 12, + "first_word_y_px": -14, + "entry_scale": 0.992, + "entry_blur_px": 2.4, + "reflow_blur_px": 0.7, + "exit_y_px": 10, + "exit_blur_px": 1.2, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)" + }, + "swap": { + "mode": "sequential", + "overlap_ms": 0, + "micro_delay_ms": 70, + "scenario_spec": { + "entry_condition": "Use when three short words should build into a vertical stack, with each new word dropping from above and physically re-centering the composition.", + "switch_order": [ + "Show the first word in the center with a short top-down drop.", + "Bring the second word into a lower line while shifting the first word upward into the stack.", + "Bring the third word into the bottom line while shifting the first two words upward so the final three-line stack stays centered." + ], + "verification": [ + "Each new word visibly pushes the existing words rather than simply fading in.", + "The completed phrase ends as three centered lines with even vertical spacing.", + "The motion reads as one kinetic stacked build with a top-down entry direction." + ], + "fallback": { + "if_drop_is_too_subtle": "Increase build.entry_offset_y_px from -28 to -36.", + "if_phrase_feels_too_slow": "Reduce build.push_duration_ms from 500 to 460." + } + } + }, + "usage_notes": "Best on short three-word headings where each word can live on its own line. Keep the vertical drop compact so the motion still feels editorial, and let the stacking displacement carry most of the energy. For longer phrases, reduce entry_offset_y_px or switch to a softer shared-slide pattern." + }, + "showcase": { + "content": { + "sample": "Build from above.", + "phrases": [ + ["Drop", "into", "place"], + ["Words", "settle", "lower"], + ["Build", "from", "above"] + ] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "short-slide-down" + }, + "renderer": { + "id": "kinetic-top-build", + "source": "spec", + "params": { + "first_word_duration_ms": 360, + "push_duration_ms": 500, + "exit_duration_ms": 320, + "hold_ms": 1100, + "between_phrases_ms": 180, + "entry_offset_y_px": -28, + "line_gap_px": 12, + "first_word_y_px": -14, + "entry_scale": 0.992, + "entry_blur_px": 2.4, + "reflow_blur_px": 0.7, + "exit_y_px": 10, + "exit_blur_px": 1.2, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)" + }, + "recipe": { + "id": "kinetic-top-build", + "summary": "Build a centered vertical stack word by word; each incoming word drops from above and pushes existing words into newly centered y positions.", + "required_measurements": ["offsetHeight for every word after appending the incoming word"], + "algorithm": [ + "Create a relative kinetic stack container using the kinetic-stack-host stage preset.", + "For each phrase word, append an absolutely centered word span.", + "Measure all child heights and compute centered y positions: totalHeight = sum(heights) + line_gap_px * (count - 1); cursor starts at -totalHeight / 2; each word position is cursor + height / 2.", + "First word enters at y=first_word_y_px with entry_scale, entry_blur_px, and opacity 0, then settles to y=0/scale=1/blur=0/opacity=1.", + "For later words, animate existing words from previous y positions to next centered y positions while the incoming word starts at targetY + entry_offset_y_px and lands at targetY.", + "Use an intermediate keyframe around offset 0.52 for existing-word reflow blur and 0.6 for incoming-word settle blur.", + "After every push, snap all words to exact final poses to avoid accumulated engine drift.", + "Exit all words together from current centered y positions with exit_y_px and exit_blur_px, then clear the stack." + ], + "frame_materialization": { + "coordinate_space": "x/y values are renderer pixel coordinates and are not multiplied by runtime.y_travel_multiplier.", + "transform": "translate(-50%, -50%) translate3d(0, y, 0) scale(scale)", + "filter": "blur(blur)", + "opacity": "unit opacity" + }, + "keyframe_recipe": { + "first_word": [ + { + "offset": 0, + "x": 0, + "y": "build.first_word_y_px", + "scale": "build.entry_scale", + "blur": "build.entry_blur_px", + "opacity": 0 + }, + { + "offset": 0.58, + "x": 0, + "y": "build.first_word_y_px * 0.35", + "scale": 0.998, + "blur": "build.entry_blur_px * 0.45", + "opacity": 0.78 + }, + { + "offset": 1, + "x": 0, + "y": 0, + "scale": 1, + "blur": 0, + "opacity": 1 + } + ], + "existing_word_push": [ + { + "offset": 0, + "x": 0, + "y": "currentY", + "scale": 1, + "blur": 0, + "opacity": 1 + }, + { + "offset": 0.52, + "x": 0, + "y": "mix(currentY, nextY, 0.58)", + "scale": 1, + "blur": "build.reflow_blur_px", + "opacity": 1 + }, + { + "offset": 1, + "x": 0, + "y": "nextY", + "scale": 1, + "blur": 0, + "opacity": 1 + } + ], + "incoming_word_push": [ + { + "offset": 0, + "x": 0, + "y": "targetY + build.entry_offset_y_px", + "scale": "build.entry_scale", + "blur": "build.entry_blur_px", + "opacity": 0 + }, + { + "offset": 0.6, + "x": 0, + "y": "mix(targetY + build.entry_offset_y_px, targetY, 0.72)", + "scale": 0.998, + "blur": "build.entry_blur_px * 0.38", + "opacity": 0.84 + }, + { + "offset": 1, + "x": 0, + "y": "targetY", + "scale": 1, + "blur": 0, + "opacity": 1 + } + ], + "exit_word": [ + { + "offset": 0, + "x": 0, + "y": "position", + "scale": 1, + "blur": 0, + "opacity": 1 + }, + { + "offset": 0.52, + "x": 0, + "y": "position + build.exit_y_px * 0.45", + "scale": 1, + "blur": "build.exit_blur_px * 0.55", + "opacity": 0.62 + }, + { + "offset": 1, + "x": 0, + "y": "position + build.exit_y_px", + "scale": 1, + "blur": "build.exit_blur_px", + "opacity": 0 + } + ] + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["build-phrase", "hold", "exit-phrase", "gap"], + "replacement_behavior": "phrase-loop", + "hold_ms": 792, + "micro_delay_ms": 0, + "gap_ms": 130 + }, + "timing": { + "first_word": { + "source_duration_ms": 360, + "scaled_duration_ms": 259, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" + }, + "push": { + "source_duration_ms": 500, + "scaled_duration_ms": 360, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" + }, + "exit": { + "source_duration_ms": 320, + "scaled_duration_ms": 230, + "easing": "cubic-bezier(0.4, 0, 0.2, 1)" + }, + "hold_ms": 792, + "gap_ms": 130 + }, + "stage": { + "preset": "kinetic-stack-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + }, + "kinetic_container": { + "requirement": "Use a relative-positioned block host large enough for the stack; exact dimensions belong to the consuming UI.", + "position": "relative", + "coordinate_origin": "center" + }, + "kinetic_word": { + "backface_visibility": "hidden", + "left": "50%", + "position": "absolute", + "top": "50%", + "white_space": "nowrap", + "absolute_centered": true, + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "kinetic-top-build", + "target": "per-word", + "stagger_mode": "normal", + "coordinate_space": "renderer-pixels", + "y_travel_multiplier": 1, + "y_travel_multiplier_note": "runtime.y_travel_multiplier is not applied to kinetic build coordinates; x/y values in build params are final transform pixels.", + "transform_order": "translate(-50%, -50%) translate3d(0, y_px, 0) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "follow renderer recipe algorithm" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Measure word heights after appending each incoming word.", + "Compute centered y positions from measured heights and line_gap_px.", + "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", + "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow y is mix(currentY, nextY, 0.58) at offset 0.52; incoming-word settle y is mix(startY, targetY, 0.72) at offset 0.6.", + "Exit uses a three-keyframe path with offset 0.52 at y = position + exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Measure word heights after appending each incoming word.", + "Compute centered y positions from measured heights and line_gap_px.", + "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", + "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow y is mix(currentY, nextY, 0.58) at offset 0.52; incoming-word settle y is mix(startY, targetY, 0.72) at offset 0.6.", + "Exit uses a three-keyframe path with offset 0.52 at y = position + exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Measure word heights after appending each incoming word.", + "Compute centered y positions from measured heights and line_gap_px.", + "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", + "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow y is mix(currentY, nextY, 0.58) at offset 0.52; incoming-word settle y is mix(startY, targetY, 0.72) at offset 0.6.", + "Exit uses a three-keyframe path with offset 0.52 at y = position + exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "All engines", + "notes": [ + "Do not apply runtime.y_travel_multiplier to kinetic build x/y coordinates; buildKineticFrame uses the build params as final transform pixels.", + "Use explicit offset keyframes for the intermediate reflow frames, then snap final styles after each push to avoid layout drift." + ] + } + ], + "reproduction_notes": [ + "On the site this effect builds a centered vertical stack. Measure line heights, compute centered y positions for the stack, and animate existing words upward as the incoming word drops into the next line.", + "For site parity, scale duration and stagger timing by 0.72. Keep kinetic build x/y params as raw renderer pixel coordinates; runtime.y_travel_multiplier applies to generic/title frame conversion, not to buildKineticFrame coordinates.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/short-slide-right.json b/skills/hyperframes/assets/text-effects/effects/short-slide-right.json new file mode 100644 index 000000000..1755ac24f --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/short-slide-right.json @@ -0,0 +1,330 @@ +{ + "id": "short-slide-right", + "visibility": "visible", + "portable_spec": { + "id": "short-slide-right", + "display_name": "Short Slide Right", + "description": "The whole phrase glides in from the left as one compact move, while the words themselves are revealed in sequence only through opacity.", + "inspiration": "Keynote-style editorial headings where motion is present but tightly restrained.", + "target": "per-word", + "custom_renderer": "shared-slide-opacity-stage", + "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "enter": { + "duration_ms": 520, + "stagger_ms": 92, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "from": { + "opacity": 1, + "x_px": -24, + "blur_px": 1.2 + }, + "to": { + "opacity": 1, + "x_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 320, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 0.2, 1)", + "from": { + "opacity": 1, + "x_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "x_px": 12, + "blur_px": 1 + } + }, + "build": { + "word_opacity_duration_ms": 210, + "word_opacity_from": 0, + "word_opacity_to": 1 + }, + "swap": { + "mode": "sequential", + "overlap_ms": 0, + "micro_delay_ms": 70, + "scenario_spec": { + "entry_condition": "Use when the heading should feel like one shared horizontal motion, but the words should reveal progressively.", + "switch_order": [ + "Start the whole phrase from one shared left offset.", + "Animate the phrase transform once, with no per-word positional delay.", + "Reveal each word with only opacity stagger so the ordering reads clearly." + ], + "verification": [ + "The phrase position starts and ends in sync for all words.", + "Only opacity is staggered across the words.", + "The amplitude stays compact enough to feel controlled, not swishy." + ], + "fallback": { + "if_motion_feels_too_wide": "Reduce enter.from.x_px from -24 to -18.", + "if_reveal_reads_too_fast": "Increase enter.stagger_ms from 92 to 108.", + "if_words_feel_too_ghosted": "Increase build.word_opacity_duration_ms from 210 to 240." + } + } + }, + "usage_notes": "Best on three-word headings where word order matters. Keep the horizontal travel compact and shared; the phrase should read as one move, with staging communicated only by opacity. For longer phrases, reduce stagger_ms or shorten the opacity duration so the cascade does not drag." + }, + "showcase": { + "content": { + "sample": "Move with intent.", + "samples": ["Move with intent.", "Words glide across.", "Build the rhythm."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "short-slide-right" + }, + "renderer": { + "id": "shared-slide-opacity-stage", + "source": "spec", + "params": { + "word_opacity_duration_ms": 210, + "word_opacity_from": 0, + "word_opacity_to": 1 + }, + "recipe": { + "id": "shared-slide-opacity-stage", + "summary": "Move the full phrase as one title-level transform while staggering only word opacity.", + "required_dom": [ + "one h3.text-animation-title for the full phrase transform", + "word spans are nested inside the title and only receive opacity animation" + ], + "algorithm": [ + "Split text as per-word by default.", + "Apply titleFrame(enter.from) to the h3 and word_opacity_from to each word span.", + "Start the h3 transform animation and every word opacity animation in the same tick; do not wait for the title transform to finish before starting word opacity.", + "Animate the h3 once from enter.from to enter.to using scaled enter duration.", + "Animate every word opacity from word_opacity_from to word_opacity_to with index * scaled enter.stagger_ms delay.", + "Hold, then animate only the h3 from exit.from to exit.to, clear the stage, wait gap_ms, advance to the next phrase, and repeat." + ], + "frame_materialization": { + "title_transform": "translate3d(x_px, y_px * runtime.y_travel_multiplier, 0) scale(scale)", + "title_filter": "blur(blur_px)", + "word_animation_properties": ["opacity"] + }, + "initial_state": { + "before_enter": [ + "Set the title element to titleFrame(enter.from).", + "Set every non-space word span opacity to build.word_opacity_from before starting any enter tween.", + "Whitespace spans should preserve layout but do not receive opacity tweens." + ], + "before_exit": ["Set the title element to titleFrame(exit.from)."] + }, + "verification": [ + "A GSAP implementation must call gsap.set(wordNodes, { opacity: word_opacity_from }) or assign equivalent inline styles before gsap.to(wordNodes, { opacity: word_opacity_to, ... }).", + "A Motion implementation must initialize every word span opacity to word_opacity_from before animate(... opacity: [word_opacity_from, word_opacity_to] ...).", + "A loop implementation must preserve exit and gap timing; an enter-only reveal is not an exact reproduction." + ] + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 0, + "gap_ms": 320 + }, + "timing": { + "enter_title": { + "source_duration_ms": 520, + "source_stagger_ms": 92, + "scaled_duration_ms": 374, + "scaled_stagger_ms": 66, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" + }, + "enter_word_opacity": { + "source_duration_ms": 210, + "scaled_duration_ms": 151, + "delay_step_ms": 66, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" + }, + "exit_title": { + "source_duration_ms": 320, + "source_stagger_ms": 0, + "scaled_duration_ms": 230, + "scaled_stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 0.2, 1)" + }, + "total_formulas": { + "enter_total_ms": "enter_title.scaled_duration_ms", + "exit_total_ms": "exit_title.scaled_duration_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "shared-slide-opacity-stage", + "target": "per-word", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "follow renderer recipe algorithm" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Animate the title-level transform and every word opacity animation concurrently.", + "Before starting enter animations, set every non-space word span to build.word_opacity_from; otherwise the opacity reveal will be invisible.", + "For GSAP, call gsap.set(wordNodes, { opacity: build.word_opacity_from }) before one batched gsap.to(wordNodes, { opacity: build.word_opacity_to, stagger, ... }) tween; do not create one opacity tween per word unless the delays are non-uniform.", + "For Motion, assign style.opacity = build.word_opacity_from before animate(wordNode, { opacity: [from, to] }, ...).", + "Use enter_title for the phrase transform and enter_word_opacity for word fades.", + "Exit animates only the title-level frame, then clears/replaces content according to playback.", + "Do not ship an enter-only reveal when exact playback is requested; include hold, exit, gap, phrase advance, cancellation, and final-frame snapping." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Animate the title-level transform and every word opacity animation concurrently.", + "Before starting enter animations, set every non-space word span to build.word_opacity_from; otherwise the opacity reveal will be invisible.", + "For GSAP, call gsap.set(wordNodes, { opacity: build.word_opacity_from }) before one batched gsap.to(wordNodes, { opacity: build.word_opacity_to, stagger, ... }) tween; do not create one opacity tween per word unless the delays are non-uniform.", + "For Motion, assign style.opacity = build.word_opacity_from before animate(wordNode, { opacity: [from, to] }, ...).", + "Use enter_title for the phrase transform and enter_word_opacity for word fades.", + "Exit animates only the title-level frame, then clears/replaces content according to playback.", + "Do not ship an enter-only reveal when exact playback is requested; include hold, exit, gap, phrase advance, cancellation, and final-frame snapping." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Animate the title-level transform and every word opacity animation concurrently.", + "Before starting enter animations, set every non-space word span to build.word_opacity_from; otherwise the opacity reveal will be invisible.", + "For GSAP, call gsap.set(wordNodes, { opacity: build.word_opacity_from }) before one batched gsap.to(wordNodes, { opacity: build.word_opacity_to, stagger, ... }) tween; do not create one opacity tween per word unless the delays are non-uniform.", + "For Motion, assign style.opacity = build.word_opacity_from before animate(wordNode, { opacity: [from, to] }, ...).", + "Use enter_title for the phrase transform and enter_word_opacity for word fades.", + "Exit animates only the title-level frame, then clears/replaces content according to playback.", + "Do not ship an enter-only reveal when exact playback is requested; include hold, exit, gap, phrase advance, cancellation, and final-frame snapping." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + } + ], + "reproduction_notes": [ + "On the site this effect moves the full phrase as one shared horizontal transform. Preserve a single phrase-level translation and reveal word order only through opacity timing.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/soft-blur-in.json b/skills/hyperframes/assets/text-effects/effects/soft-blur-in.json new file mode 100644 index 000000000..03bf7a2fb --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/soft-blur-in.json @@ -0,0 +1,351 @@ +{ + "id": "soft-blur-in", + "visibility": "visible", + "portable_spec": { + "id": "soft-blur-in", + "display_name": "Soft Blur", + "description": "Per-character fade-in with a gentle blur and upward motion. Apple's signature hero-title reveal.", + "inspiration": "Apple keynote intros; iPhone, Mac, and Vision Pro product page headlines; macOS system UI reveals.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 900, + "stagger_ms": 25, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 16, + "blur_px": 12 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 600, + "stagger_ms": 15, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -16, + "blur_px": 12 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 300, + "scenario_spec": { + "entry_condition": "Use when text is replaced in the same layout slot and both strings remain visually stable in one block.", + "switch_order": [ + "Start old text exit at t=0ms.", + "Start new text enter at t=exit_total_ms-overlap_ms.", + "Keep both text layers mounted only during the overlap window." + ], + "verification": [ + "No hard-cut frame appears between old and new text.", + "Blur stays readable during overlap on desktop and mobile.", + "Total swap duration remains below 1300ms for default sample length." + ], + "fallback": { + "if_overlap_looks_heavy": "Reduce overlap_ms to 180 and exit blur_px to 8.", + "if_copy_is_long": "Switch target to per-word and reduce enter stagger_ms to 15." + } + } + }, + "usage_notes": "Works best on hero titles 48px+ against solid backgrounds. On body text (<24px), reduce blur_px to 6 and stagger_ms to 15. Avoid on very long strings (>40 chars) — total stagger becomes too long; in that case switch target to 'per-word'." + }, + "showcase": { + "content": { + "sample": "Think different.", + "samples": ["Think different.", "Built to flow.", "Motion with intent."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "soft-blur-in" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 0, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 900, + "source_stagger_ms": 25, + "scaled_duration_ms": 648, + "scaled_stagger_ms": 18, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)" + }, + "exit": { + "source_duration_ms": 600, + "source_stagger_ms": 15, + "scaled_duration_ms": 432, + "scaled_stagger_ms": 11, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "per-character", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/spring-scale-in.json b/skills/hyperframes/assets/text-effects/effects/spring-scale-in.json new file mode 100644 index 000000000..103558e32 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/spring-scale-in.json @@ -0,0 +1,331 @@ +{ + "id": "spring-scale-in", + "visibility": "visible", + "portable_spec": { + "id": "spring-scale-in", + "display_name": "Spring Scale In", + "description": "Words pop in with a soft overshoot scale, like a physical spring settling into place.", + "inspiration": "iOS app icons bouncing into the home screen, macOS Dock, widget appearances, Vision Pro floating UI pops.", + "target": "per-word", + "signature_easing": "cubic-bezier(0.34, 1.56, 0.64, 1)", + "enter": { + "duration_ms": 360, + "stagger_ms": 95, + "easing": "cubic-bezier(0.34, 1.56, 0.64, 1)", + "from": { + "opacity": 0, + "scale": 0.7 + }, + "to": { + "opacity": 1, + "scale": 1 + } + }, + "exit": { + "duration_ms": 200, + "stagger_ms": 80, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "scale": 1 + }, + "to": { + "opacity": 0, + "scale": 0.8 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 35 + }, + "usage_notes": "The overshoot comes from cubic-bezier y2 > 1 (1.56). Per-word is the sweet spot - per-character at this easing feels too bouncy. Stagger is intentionally high here to create a visible staircase effect. This variant uses no overlap on swap to avoid content crossing during transitions." + }, + "showcase": { + "content": { + "sample": "Fast. Crisp. Fluid.", + "samples": ["Fast. Crisp. Fluid.", "Pop into place.", "Smooth by default."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "spring-scale-in" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 35, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 360, + "source_stagger_ms": 95, + "scaled_duration_ms": 259, + "scaled_stagger_ms": 68, + "easing": "cubic-bezier(0.34, 1.56, 0.64, 1)" + }, + "exit": { + "source_duration_ms": 200, + "source_stagger_ms": 80, + "scaled_duration_ms": 144, + "scaled_stagger_ms": 58, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "per-word", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/stagger-from-center.json b/skills/hyperframes/assets/text-effects/effects/stagger-from-center.json new file mode 100644 index 000000000..6c17b86f7 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/stagger-from-center.json @@ -0,0 +1,50 @@ +{ + "id": "stagger-from-center", + "visibility": "hidden", + "portable_spec": { + "id": "stagger-from-center", + "display_name": "Stagger from Center", + "description": "Characters reveal from the center outward to emphasize the keyword core.", + "inspiration": "Product hero typography where center-weighted emphasis drives attention.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "stagger_mode": "center-out", + "enter": { + "duration_ms": 620, + "stagger_ms": 22, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 12, + "blur_px": 3 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 420, + "stagger_ms": 16, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -8, + "blur_px": 3 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 150, + "micro_delay_ms": 20 + }, + "usage_notes": "Use on short words or compact titles; long text reduces the center-emphasis effect." + }, + "showcase": null +} diff --git a/skills/hyperframes/assets/text-effects/effects/stagger-from-edges.json b/skills/hyperframes/assets/text-effects/effects/stagger-from-edges.json new file mode 100644 index 000000000..9c5ee8de1 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/stagger-from-edges.json @@ -0,0 +1,50 @@ +{ + "id": "stagger-from-edges", + "visibility": "hidden", + "portable_spec": { + "id": "stagger-from-edges", + "display_name": "Stagger from Edges", + "description": "Characters start from both edges and converge toward the center.", + "inspiration": "Directional typography reveals used in modern product hero systems.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "stagger_mode": "edges-in", + "enter": { + "duration_ms": 620, + "stagger_ms": 22, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 12, + "blur_px": 3 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 420, + "stagger_ms": 16, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -8, + "blur_px": 3 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 150, + "micro_delay_ms": 20 + }, + "usage_notes": "Effective for medium word lengths where edge-to-center motion remains readable." + }, + "showcase": null +} diff --git a/skills/hyperframes/assets/text-effects/effects/top-down-letters.json b/skills/hyperframes/assets/text-effects/effects/top-down-letters.json new file mode 100644 index 000000000..8b4eef36d --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/top-down-letters.json @@ -0,0 +1,348 @@ +{ + "id": "top-down-letters", + "visibility": "visible", + "portable_spec": { + "id": "top-down-letters", + "display_name": "Top-Down Letters", + "description": "Letters descend from above in a pronounced staircase, one symbol at a time, with zero blur.", + "inspiration": "Apple-style keynote typography, crisp editorial headers, and controlled top-down word reveals.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.18, 1, 0.32, 1)", + "enter": { + "duration_ms": 400, + "stagger_ms": 88, + "easing": "cubic-bezier(0.18, 1, 0.32, 1)", + "from": { + "opacity": 0, + "y_px": -46 + }, + "to": { + "opacity": 1, + "y_px": 0 + } + }, + "exit": { + "duration_ms": 280, + "stagger_ms": 28, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "y_px": 0 + }, + "to": { + "opacity": 0, + "y_px": 14 + } + }, + "swap": { + "mode": "sequential", + "overlap_ms": 0, + "micro_delay_ms": 35, + "scenario_spec": { + "entry_condition": "Use when short words or compact headlines should build downward letter by letter with completely crisp glyph edges.", + "switch_order": [ + "Run old text exit first so the slot clears cleanly.", + "Wait micro_delay_ms after exit.", + "Start new text enter from above with per-character stagger." + ], + "verification": [ + "Letters never blur during enter or exit.", + "The reveal clearly reads top-down rather than typewriter-left-to-right.", + "Spacing remains stable while characters settle." + ], + "fallback": { + "if_motion_feels_too_tall": "Reduce enter from.y_px from -46 to -36.", + "if_readability_drops": "Increase stagger_ms from 88 to 100 for even more separation." + } + } + }, + "usage_notes": "Best for short single words, labels, or compact headline swaps at 40px+. This is the top-down counterpart to bottom-up-letters: very large per-symbol delay, fewer simultaneous letters on screen, and a tall drop from above." + }, + "showcase": { + "content": { + "sample": "Signal", + "samples": ["Signal", "Header", "Vector"] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "top-down-letters" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 35, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 400, + "source_stagger_ms": 88, + "scaled_duration_ms": 288, + "scaled_stagger_ms": 63, + "easing": "cubic-bezier(0.18, 1, 0.32, 1)" + }, + "exit": { + "source_duration_ms": 280, + "source_stagger_ms": 28, + "scaled_duration_ms": 202, + "scaled_stagger_ms": 20, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "per-character", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/effects/typewriter.json b/skills/hyperframes/assets/text-effects/effects/typewriter.json new file mode 100644 index 000000000..41639bb04 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/effects/typewriter.json @@ -0,0 +1,331 @@ +{ + "id": "typewriter", + "visibility": "visible", + "portable_spec": { + "id": "typewriter", + "display_name": "Typewriter", + "description": "Per-character stepped reveal with a minimal editorial typing rhythm.", + "inspiration": "System-like text build patterns in Apple presentation and utility UI.", + "target": "per-character", + "signature_easing": "steps(1, end)", + "enter": { + "duration_ms": 240, + "stagger_ms": 46, + "easing": "steps(1, end)", + "from": { + "opacity": 0, + "y_px": 0 + }, + "to": { + "opacity": 1, + "y_px": 0 + } + }, + "exit": { + "duration_ms": 260, + "stagger_ms": 10, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "y_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -4 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 85 + }, + "usage_notes": "Good for short copy. Keep line length moderate so stepping stays intentional." + }, + "showcase": { + "content": { + "sample": "Precision in motion.", + "samples": ["Precision in motion.", "Write. Pause. Continue."] + }, + "content_usage": { + "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", + "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", + "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." + }, + "sample_source": { + "asset": "assets/samples.json", + "key": "typewriter" + }, + "renderer": { + "id": "generic-stagger", + "source": "default", + "params": {}, + "recipe": { + "id": "generic-stagger", + "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", + "required_dom": [ + "one h3.text-animation-title per phrase", + "one span.text-animation-unit per split part", + "animate only non-space parts for per-word targets", + "span.text-animation-unit.line uses display:block for per-line targets" + ], + "split_rules": { + "whole": "single animated unit containing the full text", + "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", + "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", + "per-line": "split on explicit \"\\n\"; each line is an animated block span" + }, + "stagger_rank_algorithms": { + "normal": "rank equals DOM unit index", + "reverse": "rank 0 starts at last animated unit and proceeds backward", + "center-out": "sort animated indices by absolute distance from center, ties by lower index", + "edges-in": "alternate left edge, right edge, then move inward" + }, + "frame_materialization": { + "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "filter": "blur(blur_px)", + "opacity_default": 1, + "scale_default": 1, + "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", + "fill": "final frame must remain applied after each phase completes" + }, + "loop_algorithm": [ + "Wait initial_delay_ms before starting the first enter.", + "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", + "After the first enter completes, wait hold_ms.", + "Loop from the visible phrase: animate current units through exit.", + "Create next phrase off-DOM and apply enter.from.", + "After the exit completes, wait micro_delay_ms.", + "Replace the stage contents with the next phrase and animate enter.", + "After the next enter completes, wait gap_ms.", + "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." + ], + "canonical_loop_pseudocode": [ + "current = createPhrase(firstText); append(current); await enter(current);", + "while active:", + " await sleep(hold_ms);", + " await exit(current);", + " next = createPhrase(nextText); applyEnterFrom(next);", + " await sleep(micro_delay_ms);", + " replaceStage(next);", + " current = next;", + " await enter(current);", + " await sleep(gap_ms);", + "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." + ], + "loop_invariants": [ + "The initial phrase enters exactly once before the loop body.", + "Every later phrase enters exactly once immediately after replacement.", + "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", + "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." + ], + "current_site_swap_support": { + "uses_micro_delay_ms": true, + "uses_overlap_ms": false, + "branches_on_swap_mode": false, + "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." + } + } + }, + "runtime": { + "preset": "website-default", + "speed_multiplier": 0.72, + "hold_ms": 550, + "gap_ms": 320, + "y_travel_multiplier": 0.58, + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + } + }, + "playback": { + "kind": "loop", + "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], + "replacement_behavior": "exit-before-enter", + "hold_ms": 550, + "micro_delay_ms": 85, + "gap_ms": 320 + }, + "timing": { + "enter": { + "source_duration_ms": 240, + "source_stagger_ms": 46, + "scaled_duration_ms": 173, + "scaled_stagger_ms": 33, + "easing": "steps(1, end)" + }, + "exit": { + "source_duration_ms": 260, + "source_stagger_ms": 10, + "scaled_duration_ms": 187, + "scaled_stagger_ms": 7, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)" + }, + "total_formulas": { + "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", + "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" + } + }, + "stage": { + "preset": "default-text-host", + "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", + "container": { + "requirement": "Provide a host element for the animated title.", + "perspective_px": 900, + "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." + }, + "title": { + "requirement": "Animate the phrase container when the renderer recipe uses title frames.", + "display": "inline-block", + "transform_style": "preserve-3d", + "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." + }, + "unit": { + "backface_visibility": "hidden", + "display": "inline-block", + "line_display": "block", + "transform_origin": "50% 55%", + "white_space": "pre", + "will_change": ["transform", "opacity", "filter"] + } + }, + "rendering_contract": { + "renderer": "generic-stagger", + "target": "per-character", + "stagger_mode": "normal", + "y_travel_multiplier": 0.58, + "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", + "fill_behavior": "retain final frame after each phase", + "initial_delay_ms": { + "mode": "random-range", + "min": 0, + "max": 400 + }, + "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" + }, + "library_selection": { + "supported_adapters": ["waapi", "motion", "gsap"], + "aliases": { + "web animations api": "waapi", + "waapi": "waapi", + "motion": "motion", + "motion.dev": "motion", + "motion react": "motion", + "framer motion": "motion", + "gsap": "gsap", + "greensock": "gsap" + }, + "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", + "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." + }, + "library_adapters": { + "waapi": { + "target_library": "Web Animations API", + "install": "none; native browser Element.animate", + "import_statement": null, + "time_unit": "milliseconds", + "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", + "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", + "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", + "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", + "cancellation": "cancel active Animation objects and clear pending timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "motion": { + "target_library": "Motion for React / motion.dev", + "install": "pnpm add motion", + "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", + "time_unit": "seconds for delay and duration options", + "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", + "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", + "verification": [ + "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", + "The Motion times array length must match each animated property array length for that tween.", + "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", + "Exact reproduction must include exit/replacement playback, not only initial enter tweens." + ], + "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", + "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", + "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + }, + "gsap": { + "target_library": "GSAP", + "install": "pnpm add gsap", + "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", + "time_unit": "seconds for delay and duration options", + "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", + "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", + "verification": [ + "Initialize first-frame styles with gsap.set before starting a tween.", + "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", + "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", + "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." + ], + "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", + "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", + "cancellation": "kill active tweens/timelines and clear timers on teardown.", + "renderer_notes": [ + "Create split units from target and animate only the animated units.", + "Delay each unit by stagger rank * scaled_stagger_ms.", + "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", + "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", + "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", + "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", + "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." + ] + } + }, + "engine_notes": [ + { + "engine": "WAAPI", + "notes": [ + "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", + "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." + ] + }, + { + "engine": "Motion", + "notes": [ + "Use imperative animate(element, keyframes, options) when reproducing the site loops.", + "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." + ] + }, + { + "engine": "GSAP", + "notes": [ + "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", + "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." + ] + }, + { + "engine": "CSS", + "notes": [ + "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", + "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." + ] + } + ], + "reproduction_notes": [ + "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", + "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", + "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." + ] + } +} diff --git a/skills/hyperframes/assets/text-effects/specs/blur-out-up.json b/skills/hyperframes/assets/text-effects/specs/blur-out-up.json new file mode 100644 index 000000000..c2cdf79ac --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/blur-out-up.json @@ -0,0 +1,44 @@ +{ + "id": "blur-out-up", + "display_name": "Blur Out Up", + "description": "Words arrive clean and depart upward with increasing blur for airy exits.", + "inspiration": "Apple-style light typography where exit has more character than entry.", + "target": "per-word", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 560, + "stagger_ms": 28, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 10, + "blur_px": 6 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 480, + "stagger_ms": 24, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -14, + "blur_px": 8 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 170, + "micro_delay_ms": 35 + }, + "usage_notes": "Works best on short phrases; avoid very long lines to keep swap time tight." +} diff --git a/skills/hyperframes/assets/text-effects/specs/bottom-up-letters.json b/skills/hyperframes/assets/text-effects/specs/bottom-up-letters.json new file mode 100644 index 000000000..1241f8656 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/bottom-up-letters.json @@ -0,0 +1,57 @@ +{ + "id": "bottom-up-letters", + "display_name": "Bottom-Up Letters", + "description": "Letters rise from below in a pronounced staircase, one symbol at a time, with zero blur.", + "inspiration": "Apple-style keynote typography, sharp lower-thirds, and clean editorial word swaps.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.18, 1, 0.32, 1)", + "enter": { + "duration_ms": 400, + "stagger_ms": 88, + "easing": "cubic-bezier(0.18, 1, 0.32, 1)", + "from": { + "opacity": 0, + "y_px": 46 + }, + "to": { + "opacity": 1, + "y_px": 0 + } + }, + "exit": { + "duration_ms": 280, + "stagger_ms": 28, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "y_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -14 + } + }, + "swap": { + "mode": "sequential", + "overlap_ms": 0, + "micro_delay_ms": 35, + "scenario_spec": { + "entry_condition": "Use when short words or compact headlines should build upward letter by letter with completely crisp glyph edges.", + "switch_order": [ + "Run old text exit first so the slot clears cleanly.", + "Wait micro_delay_ms after exit.", + "Start new text enter from below with per-character stagger." + ], + "verification": [ + "Letters never blur during enter or exit.", + "The reveal clearly reads bottom-up rather than typewriter-left-to-right.", + "Spacing remains stable while characters settle." + ], + "fallback": { + "if_motion_feels_too_tall": "Reduce enter from.y_px from 46 to 36.", + "if_readability_drops": "Increase stagger_ms from 88 to 100 for even more separation." + } + } + }, + "usage_notes": "Best for short single words, labels, or compact headline swaps at 40px+. This version is intentionally more staged than per-character-rise: very large per-symbol delay, fewer simultaneous letters on screen, and a taller lift from below." +} diff --git a/skills/hyperframes/assets/text-effects/specs/depth-parallax-words.json b/skills/hyperframes/assets/text-effects/specs/depth-parallax-words.json new file mode 100644 index 000000000..2755d6937 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/depth-parallax-words.json @@ -0,0 +1,48 @@ +{ + "id": "depth-parallax-words", + "display_name": "Depth Parallax Words", + "description": "Per-word depth motion with scale and vertical drift for layered readability.", + "inspiration": "Product landing pages combining depth cues with clean typography.", + "target": "per-word", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 700, + "stagger_ms": 70, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 18, + "scale": 0.92, + "blur_px": 3 + }, + "to": { + "opacity": 1, + "y_px": 0, + "scale": 1, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 500, + "stagger_ms": 45, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "scale": 1, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -10, + "scale": 1.05, + "blur_px": 2 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 180, + "micro_delay_ms": 30 + }, + "usage_notes": "Use short copy blocks and moderate stagger to avoid visual overload." +} diff --git a/skills/hyperframes/assets/text-effects/specs/fade-through.json b/skills/hyperframes/assets/text-effects/specs/fade-through.json new file mode 100644 index 000000000..f5035f59f --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/fade-through.json @@ -0,0 +1,48 @@ +{ + "id": "fade-through", + "display_name": "Fade Through", + "description": "A Material-style content transition: old fades out, new fades in with a soft delay.", + "inspiration": "Google Material fade through transitions for same-level UI changes.", + "target": "whole", + "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", + "enter": { + "duration_ms": 420, + "stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0, 0, 1)", + "from": { + "opacity": 0, + "y_px": 6, + "scale": 0.99, + "blur_px": 2 + }, + "to": { + "opacity": 1, + "y_px": 0, + "scale": 1, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 260, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 1, 1)", + "from": { + "opacity": 1, + "y_px": 0, + "scale": 1, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -4, + "scale": 1, + "blur_px": 0 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 20, + "micro_delay_ms": 60 + }, + "usage_notes": "Best for replacing content in the same layout slot without directional meaning." +} diff --git a/skills/hyperframes/assets/text-effects/specs/focus-blur-resolve.json b/skills/hyperframes/assets/text-effects/specs/focus-blur-resolve.json new file mode 100644 index 000000000..d92202348 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/focus-blur-resolve.json @@ -0,0 +1,48 @@ +{ + "id": "focus-blur-resolve", + "display_name": "Focus Blur Resolve", + "description": "A premium focus pull from heavy blur to crisp text, then a soft blur-out exit.", + "inspiration": "Apple-style hero transitions that resolve detail with cinematic restraint.", + "target": "whole", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 760, + "stagger_ms": 0, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 14, + "blur_px": 14, + "scale": 1.01 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0, + "scale": 1 + } + }, + "exit": { + "duration_ms": 520, + "stagger_ms": 0, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0, + "scale": 1 + }, + "to": { + "opacity": 0, + "y_px": -10, + "blur_px": 10, + "scale": 1 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 160, + "micro_delay_ms": 35 + }, + "usage_notes": "Best on large headlines where blur distance reads as intentional and premium." +} diff --git a/skills/hyperframes/assets/text-effects/specs/kinetic-center-build.json b/skills/hyperframes/assets/text-effects/specs/kinetic-center-build.json new file mode 100644 index 000000000..6b2f02df4 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/kinetic-center-build.json @@ -0,0 +1,84 @@ +{ + "id": "kinetic-center-build", + "display_name": "Kinetic Center Build", + "description": "A word appears in the center; each new word enters from right to left with a soft blur and pushes the existing line until the full phrase locks centered.", + "inspiration": "Apple keynote kinetic editorial typography and sequential phrase builds.", + "target": "per-word", + "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "enter": { + "duration_ms": 360, + "stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "from": { + "opacity": 0, + "y_px": 6, + "scale": 0.992, + "blur_px": 3.5 + }, + "to": { + "opacity": 1, + "y_px": 0, + "scale": 1, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 260, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 0.2, 1)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -6, + "blur_px": 2.5 + } + }, + "swap": { + "mode": "sequential", + "overlap_ms": 0, + "micro_delay_ms": 220, + "scenario_spec": { + "entry_condition": "Use when a short phrase should be built word-by-word, with each new word entering from the right and physically re-centering the existing line.", + "switch_order": [ + "Show the first word in the center.", + "Bring the second word in from right to left while shifting the first word left.", + "Bring the third word in from right to left while shifting the first two words so the final phrase stays centered." + ], + "verification": [ + "Each new word visibly pushes the existing words rather than simply fading in.", + "The completed phrase ends centered and evenly spaced.", + "The motion reads as one kinetic line build, not as three isolated reveals." + ], + "fallback": { + "if_push_is_too_subtle": "Increase build.entry_offset_px from 96 to 120.", + "if_phrase_feels_too_slow": "Reduce build.push_duration_ms from 480 to 420." + } + } + }, + "build": { + "entry_direction": "from-right", + "line_alignment": "center", + "first_word_duration_ms": 340, + "push_duration_ms": 430, + "entry_offset_px": 88, + "word_gap_px": 10, + "first_word_y_px": 6, + "entry_scale": 0.992, + "entry_blur_px": 3.5, + "reflow_blur_px": 0.8, + "exit_y_px": -6, + "exit_blur_px": 2.5, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)", + "phrase_samples": [ + ["Words", "push", "left"], + ["Type", "locks", "center"], + ["Build", "the", "line"] + ] + }, + "usage_notes": "Layout-aware effect: each incoming word changes the target x-position of the whole line. Best for short three-word phrases; implementation requires measuring word widths and animating existing words to new positions. A small entry and reflow blur helps the push feel smoother without extending the timing." +} diff --git a/skills/hyperframes/assets/text-effects/specs/line-by-line-slide.json b/skills/hyperframes/assets/text-effects/specs/line-by-line-slide.json new file mode 100644 index 000000000..500bee8eb --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/line-by-line-slide.json @@ -0,0 +1,40 @@ +{ + "id": "line-by-line-slide", + "display_name": "Line-by-Line Slide", + "description": "Each line enters from the left with a staggered slide and exits to the right for a flowing paragraph reveal.", + "inspiration": "Apple landing page subheads and section headers that breathe line by line.", + "target": "per-line", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 900, + "stagger_ms": 120, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "x_px": -48 + }, + "to": { + "opacity": 1, + "x_px": 0 + } + }, + "exit": { + "duration_ms": 600, + "stagger_ms": 80, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "x_px": 0 + }, + "to": { + "opacity": 0, + "x_px": 48 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 20 + }, + "usage_notes": "Great for 2-line or 3-line headings. This variant keeps swap non-overlapping to avoid content intersections. Reduce x-distance for narrow layouts to keep motion tight on mobile." +} diff --git a/skills/hyperframes/assets/text-effects/specs/mask-reveal-up.json b/skills/hyperframes/assets/text-effects/specs/mask-reveal-up.json new file mode 100644 index 000000000..da8017a66 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/mask-reveal-up.json @@ -0,0 +1,44 @@ +{ + "id": "mask-reveal-up", + "display_name": "Mask Reveal Up", + "description": "Lines reveal upward with a soft masked feel and compact stagger.", + "inspiration": "Apple section transitions where multiline copy rises in with control.", + "target": "per-line", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 760, + "stagger_ms": 90, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 30, + "blur_px": 6 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 520, + "stagger_ms": 70, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -22, + "blur_px": 6 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 210, + "micro_delay_ms": 35 + }, + "usage_notes": "Best for two-line and three-line headings where line order should stay readable." +} diff --git a/skills/hyperframes/assets/text-effects/specs/micro-scale-fade.json b/skills/hyperframes/assets/text-effects/specs/micro-scale-fade.json new file mode 100644 index 000000000..009a0492a --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/micro-scale-fade.json @@ -0,0 +1,40 @@ +{ + "id": "micro-scale-fade", + "display_name": "Micro Scale Fade", + "description": "A calm, tiny scale pop used as subtle premium polish for labels and headings.", + "inspiration": "Apple system status copy, secondary UI labels, and lightweight onboarding micro-animations.", + "target": "whole", + "signature_easing": "cubic-bezier(0.32, 0.72, 0, 1)", + "enter": { + "duration_ms": 600, + "stagger_ms": 0, + "easing": "cubic-bezier(0.32, 0.72, 0, 1)", + "from": { + "opacity": 0, + "scale": 0.96 + }, + "to": { + "opacity": 1, + "scale": 1 + } + }, + "exit": { + "duration_ms": 400, + "stagger_ms": 0, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "scale": 1 + }, + "to": { + "opacity": 0, + "scale": 0.96 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 20 + }, + "usage_notes": "Use this for single words or short titles. This variant keeps swap non-overlapping to avoid content intersections. For paragraphs, switch target to per-word to avoid perceivable lag." +} diff --git a/skills/hyperframes/assets/text-effects/specs/per-character-rise.json b/skills/hyperframes/assets/text-effects/specs/per-character-rise.json new file mode 100644 index 000000000..eb4bbf905 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/per-character-rise.json @@ -0,0 +1,56 @@ +{ + "id": "per-character-rise", + "display_name": "Per-Character Rise", + "description": "Letters slide up from below with no blur — crisp, deliberate, kinetic. Apple's clean tvOS-style reveal.", + "inspiration": "Apple tvOS, Fitness+ intros, iPadOS home screen title appearances.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "enter": { + "duration_ms": 700, + "stagger_ms": 24, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "from": { + "opacity": 0, + "y_px": 32 + }, + "to": { + "opacity": 1, + "y_px": 0 + } + }, + "exit": { + "duration_ms": 420, + "stagger_ms": 14, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "y_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -24 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 210, + "scenario_spec": { + "entry_condition": "Use for headline replacement where each character must remain crisp and readable throughout the switch.", + "switch_order": [ + "Start old text exit at t=0ms.", + "Start new text enter at t=exit_total_ms-overlap_ms.", + "Use a single active headline layer after enter starts to avoid stacked glyph artifacts." + ], + "verification": [ + "Characters never blur during swap.", + "No visible pause appears between exit and enter phases.", + "Swap keeps staircase rhythm from stagger settings." + ], + "fallback": { + "if_glyphs_collide": "Lower overlap_ms to 140.", + "if_motion_feels_slow": "Reduce enter stagger_ms from 24 to 18." + } + } + }, + "usage_notes": "Works on 40px+ headlines. Zero blur keeps it sharp — that's the key distinction from soft-blur-in. Stagger 24ms gives it quicker momentum; don't go below 16ms or it flattens." +} diff --git a/skills/hyperframes/assets/text-effects/specs/per-word-crossfade.json b/skills/hyperframes/assets/text-effects/specs/per-word-crossfade.json new file mode 100644 index 000000000..020151aa0 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/per-word-crossfade.json @@ -0,0 +1,57 @@ +{ + "id": "per-word-crossfade", + "display_name": "Per-Word Crossfade", + "description": "Words gently fade into place one after another, with a short vertical drift for a calm keynote rhythm.", + "inspiration": "Apple product announcements and section title transitions where words are readable but still alive.", + "target": "per-word", + "signature_easing": "cubic-bezier(0.16, 1, 0.3, 1)", + "enter": { + "duration_ms": 700, + "stagger_ms": 70, + "easing": "cubic-bezier(0.16, 1, 0.3, 1)", + "from": { + "opacity": 0, + "y_px": 8 + }, + "to": { + "opacity": 1, + "y_px": 0 + } + }, + "exit": { + "duration_ms": 500, + "stagger_ms": 40, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "y_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -6 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 170, + "micro_delay_ms": 70, + "scenario_spec": { + "entry_condition": "Use when phrase-level content changes and word readability is more important than per-character flair.", + "switch_order": [ + "Start old text exit at t=0ms.", + "Start new text enter at t=exit_total_ms-overlap_ms+micro_delay_ms.", + "Advance word groups in the same stagger direction for old and new text." + ], + "verification": [ + "Word boundaries stay readable during overlap.", + "No two identical word positions stay stacked for more than one stagger step.", + "Swap cadence stays calm and editorial, without abrupt jumps." + ], + "fallback": { + "if_words_stack_visibly": "Increase micro_delay_ms to 90.", + "if_total_swap_is_too_long": "Reduce enter stagger_ms to 55 and overlap_ms to 120." + } + } + }, + "usage_notes": "Best for medium phrases and headings; for long copy prefer per-word only up to 16–18 words to keep total stagger time readable. micro_delay_ms helps prevent old/new words from visibly stacking during swaps." +} diff --git a/skills/hyperframes/assets/text-effects/specs/scale-down-fade.json b/skills/hyperframes/assets/text-effects/specs/scale-down-fade.json new file mode 100644 index 000000000..2eae5ab03 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/scale-down-fade.json @@ -0,0 +1,44 @@ +{ + "id": "scale-down-fade", + "display_name": "Scale Down Fade", + "description": "Subtle premium settle-in with a restrained scale-down fade on exit.", + "inspiration": "Apple product copy transitions where motion remains quiet and precise.", + "target": "whole", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 520, + "stagger_ms": 0, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 8, + "scale": 1.04 + }, + "to": { + "opacity": 1, + "y_px": 0, + "scale": 1 + } + }, + "exit": { + "duration_ms": 380, + "stagger_ms": 0, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "scale": 1 + }, + "to": { + "opacity": 0, + "y_px": -8, + "scale": 0.94 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 130, + "micro_delay_ms": 20 + }, + "usage_notes": "Safe default for product UIs where copy should feel polished but not animated." +} diff --git a/skills/hyperframes/assets/text-effects/specs/shared-axis-x.json b/skills/hyperframes/assets/text-effects/specs/shared-axis-x.json new file mode 100644 index 000000000..7ed127236 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/shared-axis-x.json @@ -0,0 +1,44 @@ +{ + "id": "shared-axis-x", + "display_name": "Shared Axis X", + "description": "Horizontal shared-axis transition for sibling destinations with continuity.", + "inspiration": "Google Material shared axis (X) transitions.", + "target": "whole", + "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", + "enter": { + "duration_ms": 500, + "stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0, 0, 1)", + "from": { + "opacity": 0, + "x_px": 24, + "scale": 0.98 + }, + "to": { + "opacity": 1, + "x_px": 0, + "scale": 1 + } + }, + "exit": { + "duration_ms": 360, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 1, 1)", + "from": { + "opacity": 1, + "x_px": 0, + "scale": 1 + }, + "to": { + "opacity": 0, + "x_px": -20, + "scale": 0.98 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 120, + "micro_delay_ms": 20 + }, + "usage_notes": "Use when moving between same-level views where horizontal direction conveys progress." +} diff --git a/skills/hyperframes/assets/text-effects/specs/shared-axis-y.json b/skills/hyperframes/assets/text-effects/specs/shared-axis-y.json new file mode 100644 index 000000000..662b8cc8f --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/shared-axis-y.json @@ -0,0 +1,44 @@ +{ + "id": "shared-axis-y", + "display_name": "Word Cut Staircase", + "description": "Per-word hard-cut transition with staircase timing for sharp editorial swaps.", + "inspiration": "Hard-cut typography timing with stepped word sequencing.", + "target": "per-word", + "signature_easing": "steps(1, end)", + "enter": { + "duration_ms": 180, + "stagger_ms": 78, + "easing": "steps(1, end)", + "from": { + "opacity": 0, + "y_px": 0, + "scale": 1 + }, + "to": { + "opacity": 1, + "y_px": 0, + "scale": 1 + } + }, + "exit": { + "duration_ms": 140, + "stagger_ms": 78, + "easing": "steps(1, end)", + "from": { + "opacity": 1, + "y_px": 0, + "scale": 1 + }, + "to": { + "opacity": 0, + "y_px": 0, + "scale": 1 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 28 + }, + "usage_notes": "Use for bold word-by-word hard cuts. No overlap keeps phrase swaps visually clean." +} diff --git a/skills/hyperframes/assets/text-effects/specs/shared-axis-z.json b/skills/hyperframes/assets/text-effects/specs/shared-axis-z.json new file mode 100644 index 000000000..fc1a23f3e --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/shared-axis-z.json @@ -0,0 +1,44 @@ +{ + "id": "shared-axis-z", + "display_name": "Shared Axis Z", + "description": "Scale-based shared-axis transition for focus shifts and context depth.", + "inspiration": "Google Material shared axis (Z), adapted for typography swaps.", + "target": "whole", + "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", + "enter": { + "duration_ms": 520, + "stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0, 0, 1)", + "from": { + "opacity": 0, + "scale": 0.9, + "blur_px": 2 + }, + "to": { + "opacity": 1, + "scale": 1, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 360, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 1, 1)", + "from": { + "opacity": 1, + "scale": 1, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "scale": 1.06, + "blur_px": 1 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 100, + "micro_delay_ms": 20 + }, + "usage_notes": "Use for emphasizing focus transitions where scale communicates depth." +} diff --git a/skills/hyperframes/assets/text-effects/specs/shimmer-sweep.json b/skills/hyperframes/assets/text-effects/specs/shimmer-sweep.json new file mode 100644 index 000000000..6dea8f43e --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/shimmer-sweep.json @@ -0,0 +1,44 @@ +{ + "id": "shimmer-sweep", + "display_name": "Shimmer Sweep", + "description": "A subtle sweep across a clean headline, blending in while gliding from left to center.", + "inspiration": "Premium hero copy transitions where a short soft push is used before settle.", + "target": "whole", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 850, + "stagger_ms": 0, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "x_px": -22, + "blur_px": 8 + }, + "to": { + "opacity": 1, + "x_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 650, + "stagger_ms": 0, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "x_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "x_px": 22, + "blur_px": 8 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 36 + }, + "usage_notes": "Use as a premium micro-transition for title swaps and copy refreshes. This variant avoids overlap between outgoing and incoming text." +} diff --git a/skills/hyperframes/assets/text-effects/specs/short-slide-down.json b/skills/hyperframes/assets/text-effects/specs/short-slide-down.json new file mode 100644 index 000000000..ef2900f7f --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/short-slide-down.json @@ -0,0 +1,83 @@ +{ + "id": "short-slide-down", + "display_name": "Short Slide Down", + "description": "Each new word drops in from above into its own line and pushes the existing stack downward until a centered three-line composition locks in place.", + "inspiration": "Keynote-style editorial headings where motion is present but tightly restrained.", + "target": "per-word", + "custom_renderer": "kinetic-top-build", + "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "enter": { + "duration_ms": 520, + "stagger_ms": 0, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "from": { + "opacity": 0, + "y_px": -24, + "blur_px": 2.4, + "scale": 0.992 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0, + "scale": 1 + } + }, + "exit": { + "duration_ms": 320, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 0.2, 1)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0, + "scale": 1 + }, + "to": { + "opacity": 0, + "y_px": 10, + "blur_px": 1.2, + "scale": 1 + } + }, + "build": { + "first_word_duration_ms": 360, + "push_duration_ms": 500, + "exit_duration_ms": 320, + "hold_ms": 1100, + "between_phrases_ms": 180, + "entry_offset_y_px": -28, + "line_gap_px": 12, + "first_word_y_px": -14, + "entry_scale": 0.992, + "entry_blur_px": 2.4, + "reflow_blur_px": 0.7, + "exit_y_px": 10, + "exit_blur_px": 1.2, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)" + }, + "swap": { + "mode": "sequential", + "overlap_ms": 0, + "micro_delay_ms": 70, + "scenario_spec": { + "entry_condition": "Use when three short words should build into a vertical stack, with each new word dropping from above and physically re-centering the composition.", + "switch_order": [ + "Show the first word in the center with a short top-down drop.", + "Bring the second word into a lower line while shifting the first word upward into the stack.", + "Bring the third word into the bottom line while shifting the first two words upward so the final three-line stack stays centered." + ], + "verification": [ + "Each new word visibly pushes the existing words rather than simply fading in.", + "The completed phrase ends as three centered lines with even vertical spacing.", + "The motion reads as one kinetic stacked build with a top-down entry direction." + ], + "fallback": { + "if_drop_is_too_subtle": "Increase build.entry_offset_y_px from -28 to -36.", + "if_phrase_feels_too_slow": "Reduce build.push_duration_ms from 500 to 460." + } + } + }, + "usage_notes": "Best on short three-word headings where each word can live on its own line. Keep the vertical drop compact so the motion still feels editorial, and let the stacking displacement carry most of the energy. For longer phrases, reduce entry_offset_y_px or switch to a softer shared-slide pattern." +} diff --git a/skills/hyperframes/assets/text-effects/specs/short-slide-right.json b/skills/hyperframes/assets/text-effects/specs/short-slide-right.json new file mode 100644 index 000000000..5b91954c2 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/short-slide-right.json @@ -0,0 +1,68 @@ +{ + "id": "short-slide-right", + "display_name": "Short Slide Right", + "description": "The whole phrase glides in from the left as one compact move, while the words themselves are revealed in sequence only through opacity.", + "inspiration": "Keynote-style editorial headings where motion is present but tightly restrained.", + "target": "per-word", + "custom_renderer": "shared-slide-opacity-stage", + "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "enter": { + "duration_ms": 520, + "stagger_ms": 92, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "from": { + "opacity": 1, + "x_px": -24, + "blur_px": 1.2 + }, + "to": { + "opacity": 1, + "x_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 320, + "stagger_ms": 0, + "easing": "cubic-bezier(0.4, 0, 0.2, 1)", + "from": { + "opacity": 1, + "x_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "x_px": 12, + "blur_px": 1 + } + }, + "build": { + "word_opacity_duration_ms": 210, + "word_opacity_from": 0, + "word_opacity_to": 1 + }, + "swap": { + "mode": "sequential", + "overlap_ms": 0, + "micro_delay_ms": 70, + "scenario_spec": { + "entry_condition": "Use when the heading should feel like one shared horizontal motion, but the words should reveal progressively.", + "switch_order": [ + "Start the whole phrase from one shared left offset.", + "Animate the phrase transform once, with no per-word positional delay.", + "Reveal each word with only opacity stagger so the ordering reads clearly." + ], + "verification": [ + "The phrase position starts and ends in sync for all words.", + "Only opacity is staggered across the words.", + "The amplitude stays compact enough to feel controlled, not swishy." + ], + "fallback": { + "if_motion_feels_too_wide": "Reduce enter.from.x_px from -24 to -18.", + "if_reveal_reads_too_fast": "Increase enter.stagger_ms from 92 to 108.", + "if_words_feel_too_ghosted": "Increase build.word_opacity_duration_ms from 210 to 240." + } + } + }, + "usage_notes": "Best on three-word headings where word order matters. Keep the horizontal travel compact and shared; the phrase should read as one move, with staging communicated only by opacity. For longer phrases, reduce stagger_ms or shorten the opacity duration so the cascade does not drag." +} diff --git a/skills/hyperframes/assets/text-effects/specs/soft-blur-in.json b/skills/hyperframes/assets/text-effects/specs/soft-blur-in.json new file mode 100644 index 000000000..8e9143191 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/soft-blur-in.json @@ -0,0 +1,60 @@ +{ + "id": "soft-blur-in", + "display_name": "Soft Blur", + "description": "Per-character fade-in with a gentle blur and upward motion. Apple's signature hero-title reveal.", + "inspiration": "Apple keynote intros; iPhone, Mac, and Vision Pro product page headlines; macOS system UI reveals.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "enter": { + "duration_ms": 900, + "stagger_ms": 25, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 16, + "blur_px": 12 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 600, + "stagger_ms": 15, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -16, + "blur_px": 12 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 300, + "scenario_spec": { + "entry_condition": "Use when text is replaced in the same layout slot and both strings remain visually stable in one block.", + "switch_order": [ + "Start old text exit at t=0ms.", + "Start new text enter at t=exit_total_ms-overlap_ms.", + "Keep both text layers mounted only during the overlap window." + ], + "verification": [ + "No hard-cut frame appears between old and new text.", + "Blur stays readable during overlap on desktop and mobile.", + "Total swap duration remains below 1300ms for default sample length." + ], + "fallback": { + "if_overlap_looks_heavy": "Reduce overlap_ms to 180 and exit blur_px to 8.", + "if_copy_is_long": "Switch target to per-word and reduce enter stagger_ms to 15." + } + } + }, + "usage_notes": "Works best on hero titles 48px+ against solid backgrounds. On body text (<24px), reduce blur_px to 6 and stagger_ms to 15. Avoid on very long strings (>40 chars) — total stagger becomes too long; in that case switch target to 'per-word'." +} diff --git a/skills/hyperframes/assets/text-effects/specs/spring-scale-in.json b/skills/hyperframes/assets/text-effects/specs/spring-scale-in.json new file mode 100644 index 000000000..11ed9d668 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/spring-scale-in.json @@ -0,0 +1,40 @@ +{ + "id": "spring-scale-in", + "display_name": "Spring Scale In", + "description": "Words pop in with a soft overshoot scale, like a physical spring settling into place.", + "inspiration": "iOS app icons bouncing into the home screen, macOS Dock, widget appearances, Vision Pro floating UI pops.", + "target": "per-word", + "signature_easing": "cubic-bezier(0.34, 1.56, 0.64, 1)", + "enter": { + "duration_ms": 360, + "stagger_ms": 95, + "easing": "cubic-bezier(0.34, 1.56, 0.64, 1)", + "from": { + "opacity": 0, + "scale": 0.7 + }, + "to": { + "opacity": 1, + "scale": 1 + } + }, + "exit": { + "duration_ms": 200, + "stagger_ms": 80, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "scale": 1 + }, + "to": { + "opacity": 0, + "scale": 0.8 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 35 + }, + "usage_notes": "The overshoot comes from cubic-bezier y2 > 1 (1.56). Per-word is the sweet spot - per-character at this easing feels too bouncy. Stagger is intentionally high here to create a visible staircase effect. This variant uses no overlap on swap to avoid content crossing during transitions." +} diff --git a/skills/hyperframes/assets/text-effects/specs/stagger-from-center.json b/skills/hyperframes/assets/text-effects/specs/stagger-from-center.json new file mode 100644 index 000000000..6f07e6d9e --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/stagger-from-center.json @@ -0,0 +1,45 @@ +{ + "id": "stagger-from-center", + "display_name": "Stagger from Center", + "description": "Characters reveal from the center outward to emphasize the keyword core.", + "inspiration": "Product hero typography where center-weighted emphasis drives attention.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "stagger_mode": "center-out", + "enter": { + "duration_ms": 620, + "stagger_ms": 22, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 12, + "blur_px": 3 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 420, + "stagger_ms": 16, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -8, + "blur_px": 3 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 150, + "micro_delay_ms": 20 + }, + "usage_notes": "Use on short words or compact titles; long text reduces the center-emphasis effect." +} diff --git a/skills/hyperframes/assets/text-effects/specs/stagger-from-edges.json b/skills/hyperframes/assets/text-effects/specs/stagger-from-edges.json new file mode 100644 index 000000000..b67437ac7 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/stagger-from-edges.json @@ -0,0 +1,45 @@ +{ + "id": "stagger-from-edges", + "display_name": "Stagger from Edges", + "description": "Characters start from both edges and converge toward the center.", + "inspiration": "Directional typography reveals used in modern product hero systems.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "stagger_mode": "edges-in", + "enter": { + "duration_ms": 620, + "stagger_ms": 22, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { + "opacity": 0, + "y_px": 12, + "blur_px": 3 + }, + "to": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + } + }, + "exit": { + "duration_ms": 420, + "stagger_ms": 16, + "easing": "cubic-bezier(0.64, 0, 0.78, 0)", + "from": { + "opacity": 1, + "y_px": 0, + "blur_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -8, + "blur_px": 3 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 150, + "micro_delay_ms": 20 + }, + "usage_notes": "Effective for medium word lengths where edge-to-center motion remains readable." +} diff --git a/skills/hyperframes/assets/text-effects/specs/top-down-letters.json b/skills/hyperframes/assets/text-effects/specs/top-down-letters.json new file mode 100644 index 000000000..e255459da --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/top-down-letters.json @@ -0,0 +1,57 @@ +{ + "id": "top-down-letters", + "display_name": "Top-Down Letters", + "description": "Letters descend from above in a pronounced staircase, one symbol at a time, with zero blur.", + "inspiration": "Apple-style keynote typography, crisp editorial headers, and controlled top-down word reveals.", + "target": "per-character", + "signature_easing": "cubic-bezier(0.18, 1, 0.32, 1)", + "enter": { + "duration_ms": 400, + "stagger_ms": 88, + "easing": "cubic-bezier(0.18, 1, 0.32, 1)", + "from": { + "opacity": 0, + "y_px": -46 + }, + "to": { + "opacity": 1, + "y_px": 0 + } + }, + "exit": { + "duration_ms": 280, + "stagger_ms": 28, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "y_px": 0 + }, + "to": { + "opacity": 0, + "y_px": 14 + } + }, + "swap": { + "mode": "sequential", + "overlap_ms": 0, + "micro_delay_ms": 35, + "scenario_spec": { + "entry_condition": "Use when short words or compact headlines should build downward letter by letter with completely crisp glyph edges.", + "switch_order": [ + "Run old text exit first so the slot clears cleanly.", + "Wait micro_delay_ms after exit.", + "Start new text enter from above with per-character stagger." + ], + "verification": [ + "Letters never blur during enter or exit.", + "The reveal clearly reads top-down rather than typewriter-left-to-right.", + "Spacing remains stable while characters settle." + ], + "fallback": { + "if_motion_feels_too_tall": "Reduce enter from.y_px from -46 to -36.", + "if_readability_drops": "Increase stagger_ms from 88 to 100 for even more separation." + } + } + }, + "usage_notes": "Best for short single words, labels, or compact headline swaps at 40px+. This is the top-down counterpart to bottom-up-letters: very large per-symbol delay, fewer simultaneous letters on screen, and a tall drop from above." +} diff --git a/skills/hyperframes/assets/text-effects/specs/typewriter.json b/skills/hyperframes/assets/text-effects/specs/typewriter.json new file mode 100644 index 000000000..df5070f80 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/specs/typewriter.json @@ -0,0 +1,40 @@ +{ + "id": "typewriter", + "display_name": "Typewriter", + "description": "Per-character stepped reveal with a minimal editorial typing rhythm.", + "inspiration": "System-like text build patterns in Apple presentation and utility UI.", + "target": "per-character", + "signature_easing": "steps(1, end)", + "enter": { + "duration_ms": 240, + "stagger_ms": 46, + "easing": "steps(1, end)", + "from": { + "opacity": 0, + "y_px": 0 + }, + "to": { + "opacity": 1, + "y_px": 0 + } + }, + "exit": { + "duration_ms": 260, + "stagger_ms": 10, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { + "opacity": 1, + "y_px": 0 + }, + "to": { + "opacity": 0, + "y_px": -4 + } + }, + "swap": { + "mode": "crossfade", + "overlap_ms": 0, + "micro_delay_ms": 85 + }, + "usage_notes": "Good for short copy. Keep line length moderate so stepping stays intentional." +} diff --git a/skills/hyperframes/house-style.md b/skills/hyperframes/house-style.md index 892c2ebed..2631484f8 100644 --- a/skills/hyperframes/house-style.md +++ b/skills/hyperframes/house-style.md @@ -1,6 +1,6 @@ # House Style -Creative direction for compositions when no `design.md` is provided. These are starting points — override anything that doesn't serve the content. When a `design.md` exists, its brand values take precedence; house-style fills gaps. +Creative direction for compositions when no `DESIGN.md` is provided. These are starting points — override anything that doesn't serve the content. When a `DESIGN.md` exists, its brand values take precedence; house-style fills gaps. ## Before Writing HTML @@ -24,7 +24,7 @@ If the content genuinely calls for one of these — centered layout for a solemn ## Color -- Match light/dark to content: food, wellness, kids → light. Tech, cinema, finance → dark. +- Match light/dark to the brand, not the category. A wellness brand might be dark if its identity is sophisticated. A fintech might be light if it's consumer-first. Check the DESIGN.md or the user's direction before defaulting. - One accent hue. Same background across all scenes. - Tint neutrals toward your accent (even subtle warmth/coolness beats dead gray). - **Contrast:** enforced by `hyperframes validate` (WCAG AA). Text must be readable with decoratives removed. @@ -44,7 +44,7 @@ Ideas (mix and match, 2-5 per scene): All decoratives should have slow ambient GSAP animation — breathing, drift, pulse. Static decoratives feel dead. -**Decorative count vs motion count.** The "2-5 per scene" count refers to decorative _elements_. If a project's `design.md` says "single ambient motion per scene", it means one looping motion applied to these decoratives (a shared breath/drift/pulse) — not one element total. A scene with 4 decoratives sharing one breathing motion is correct; a scene with 1 decorative is under-dressed. +**Decorative count vs motion count.** The "2-5 per scene" count refers to decorative _elements_. If a project's `DESIGN.md` says "single ambient motion per scene", it means one looping motion applied to these decoratives (a shared breath/drift/pulse) — not one element total. A scene with 4 decoratives sharing one breathing motion is correct; a scene with 1 decorative is under-dressed. ## Motion diff --git a/skills/hyperframes/references/beat-direction.md b/skills/hyperframes/references/beat-direction.md index 04b0e12d1..5be413817 100644 --- a/skills/hyperframes/references/beat-direction.md +++ b/skills/hyperframes/references/beat-direction.md @@ -15,6 +15,10 @@ The first describes pixels. The second describes an experience. Write the second Each beat should have: +**For text animations:** pick a named effect from [`text-effects.md`](text-effects.md) and name it by ID in the storyboard. Don't describe "text fades in" — write `soft-blur-in` or `kinetic-center-build`. The sub-agent reads the effect JSON and implements it exactly. + +--- + ### Concept The big idea for this beat in 2-3 sentences. What visual WORLD are we in? What metaphor drives it? What should the viewer FEEL? This is the most important part — everything else flows from it. @@ -29,15 +33,17 @@ Cultural and design references, not hex codes: ### Animation choreography -Specific motion verbs per element — not "it animates in" but HOW: +Specific motion verbs per element — not "it animates in" but HOW. Verbs come from the beat's concept and content, not from an energy bucket. A wellness brand's "slow" beat might still have something that DROPS if the content is about letting go. A stats beat might FLOAT if the brand's identity is weightless. -| Energy | Verbs | Example | -| ------------- | --------------------------------------------- | ------------------------------------- | -| High impact | SLAMS, CRASHES, PUNCHES, STAMPS, SHATTERS | "$1.9T" SLAMS in from left at -5° | -| Medium energy | CASCADE, SLIDES, DROPS, FILLS, DRAWS | Three cards CASCADE in staggered 0.3s | -| Low energy | types on, FLOATS, morphs, COUNTS UP, fades in | Counter COUNTS UP from 0 to 135K | +The vocabulary of motion verbs (organized by physical character, not by energy level): -Every element gets a verb. If you can't name the verb, the element is not yet designed. +**Impact / weight:** SLAMS, CRASHES, PUNCHES, STAMPS, SHATTERS, DROPS (with force) +**Directional / deliberate:** SLIDES, PUSHES, PULLS, WIPES, CUTS +**Reveals / builds:** DRAWS, FILLS, GROWS, EXPANDS, ASSEMBLES, COUNTS UP +**Organic / ambient:** FLOATS, DRIFTS, BREATHES, PULSES, ORBITS, MORPHS +**Mechanical / precise:** TYPES ON, CLICKS, LOCKS IN, SNAPS, STEPS + +Every element gets a verb. If you can't name the verb, the element is not yet designed. The verb should follow from the beat's concept — not from a lookup of what "high energy" or "low energy" beats use. ### Transition @@ -51,23 +57,82 @@ How this beat hands off to the next. Specify the type and parameters. | Any moment the music/VO punctuates with a downbeat or SFX hit | Beats that ease from one composition into the next with shared motion vocabulary | Sequences of 3+ quick tempo-matched switches | | Brand moments where the transition itself _is_ the visual | Minimal/editorial pacing | Anytime a 0.3-0.8s transition would feel too slow | -Rule of thumb: if the beat is the _centerpiece_ of the video, shader-transition into it. If the beat is connective tissue, CSS-transition. A brand reel of 5-7 beats usually wants 1-2 shader transitions (the hero reveal + the CTA) and the rest CSS or hard cuts — too many shader transitions flatten their impact. - -**CSS transitions** (choose from `skills/hyperframes/references/transitions/catalog.md`): - -- Velocity-matched upward: exit `y:-150, blur:30px, 0.33s power2.in` → entry `y:150→0, blur:30px→0, 1.0s power2.out` -- Whip pan: exit `x:-400, blur:24px, 0.3s power3.in` → entry `x:400→0, blur:24px→0, 0.3s power3.out` -- Blur through: exit `blur:20px, 0.3s` → entry `blur:20px→0, 0.25s power3.out` -- Zoom through: exit `scale:1→1.2, blur:20px, 0.2s power3.in` → entry `scale:0.75→1, blur:20px→0, 0.5s expo.out` -- Hard cut / smash cut (for rapid-fire sequences) - -**Shader transitions** (choose from `packages/shader-transitions/README.md`): - -- Cross-Warp Morph (organic, versatile) — 0.5-0.8s, power2.inOut -- Cinematic Zoom (professional momentum) — 0.4-0.6s, power2.inOut -- Gravitational Lens (otherworldly) — 0.6-1.0s, power2.inOut -- Glitch (aggressive, high energy) — 0.3-0.5s -- See `packages/shader-transitions/README.md` for the full API, available shaders, and setup +Rule of thumb: if the beat is the _centerpiece_ of the video, shader-transition into it. If the beat is connective tissue, a CSS crossfade is fine. A brand reel of 5-7 beats usually wants 1-2 shader transitions (the hero reveal + the CTA) — too many flatten their impact. + +**Mixing shader and CSS crossfade transitions in one composition is supported.** Omit `shader` on any transition entry to get a smooth opacity crossfade. HyperShader manages all scene visibility regardless: + +```js +var tl = HyperShader.init({ + bgColor: "#0a0a0f", + scenes: ["s1", "s2", "s3", "s4"], + transitions: [ + { time: 4.0, shader: "sdf-iris", duration: 0.7 }, // WebGL shader + { time: 8.5, duration: 0.8 }, // no shader → CSS crossfade + { time: 13.0, shader: "domain-warp", duration: 0.6 }, + ], +}); +// Add beat animations to the returned tl AFTER init() +tl.fromTo("#hero", { opacity: 0 }, { opacity: 1, duration: 0.6 }, 0.2); +window.__timelines["main"] = tl; +``` + +Let HyperShader create the timeline — don't pass a pre-built `timeline:` option. Add all composition tweens to the returned `tl` after the call. + +**CSS transitions** — 30+ patterns across 13 categories. Full code in `skills/hyperframes/references/transitions/`. Pick based on the energy and feel: + +| Category | Patterns | Motion character | +| ------------------ | ------------------------------------------------------------------------ | -------------------------------------------------------------------------- | +| **Push / slide** | Push slide, vertical push, elastic push, squeeze | Content moves through the frame as if on a continuous surface | +| **Scale / zoom** | Zoom through, zoom out | Perspective shifts — moving toward or away from content | +| **Radial / clip** | Circle iris, diamond iris, diagonal split | Geometric reveal — content emerges or is covered by a shape | +| **3D** | 3D card flip | Physical — content flips like a tangible object | +| **Dissolve** | Crossfade, blur crossfade, focus pull, color dip | Overlap and blend — both scenes exist simultaneously during the transition | +| **Cover / blinds** | Staggered color blocks, horizontal blinds (6/12 strips), vertical blinds | Structural — content is sliced, layered, or covered | +| **Light** | Light leak overlays, overexposure burn, film burn | Organic film — light bleeds across the frame | +| **Distortion** | Glitch (CSS RGB jitter), chromatic aberration, ripple, VHS tape | Instability — the image itself appears to malfunction | +| **Blur** | Blur through, directional blur | Soft defocus — content blurs in or out | +| **Mechanical** | Shutter (two-half), clock wipe (9-point wedge) | Precision — transitions with visible mechanical logic | +| **Grid** | Grid dissolve (12/120 cells) | Fragmentation — the frame breaks into pieces | +| **Destruction** | Page burn (SVG clip-path + canvas rim) | Dramatic decay — the previous scene is destroyed | +| **Other** | Gravity drop, morph circle | Physical or shape-based motion that doesn't fit other categories | + +Common quick-picks: + +- **Velocity-matched upward**: exit `y:-150, blur:30px, 0.33s power2.in` → entry `y:150→0, blur:30px→0, 1.0s power2.out` +- **Whip pan**: exit `x:-400, blur:24px, 0.3s power3.in` → entry `x:400→0, blur:24px→0, 0.3s power3.out` +- **Blur through**: exit `blur:20px, 0.3s` → entry `blur:20px→0, 0.25s power3.out` +- **Zoom through**: exit `scale:1→1.2, blur:20px, 0.2s power3.in` → entry `scale:0.75→1, blur:20px→0, 0.5s expo.out` +- **Hard cut / smash cut**: instant, for rapid-fire sequences + +Timing presets: snappy (0.2s), smooth (0.4s), gentle (0.6s), dramatic (0.5s), instant (0.15s), luxe (0.7s). + +**Shader transitions** — 14 built-in WebGL GPU effects. Install with `npx hyperframes add `. Full API in shader-transitions docs. + +| Shader | Visual description | Duration range | +| ----------------------- | ---------------------------------------------------------------------------------------------- | -------------- | +| **domain-warp** | Organic FBM dissolve — both scenes warp toward each other with an accent flash at the midpoint | 0.5–0.8s | +| **ridged-burn** | Multifractal mask reveals the incoming scene through a burn ramp with sparks at the edge | 0.5–0.8s | +| **whip-pan** | 10-sample horizontal motion blur + lateral crossfade — reads like a camera pan between shots | 0.3–0.5s | +| **sdf-iris** | Circle SDF expands from center, with accent-tinted glow rings at the expanding edge | 0.5–0.7s | +| **ripple-waves** | Radial standing-wave UV displacement — content ripples outward as scenes cross | 0.6–1.0s | +| **gravitational-lens** | Pinch pull toward center + R/B chromatic separation — content bends inward then releases | 0.6–1.0s | +| **cinematic-zoom** | 12 RGB-offset radial zoom blur samples — motion streak radiating from center | 0.4–0.6s | +| **chromatic-split** | R/B radial channel shift outward, G fixed — channels separate then rejoin | 0.3–0.5s | +| **swirl-vortex** | CCW swirl with FBM noise — content spirals away and the new scene spirals in | 0.5–0.8s | +| **thermal-distortion** | Vertical sine + FBM horizontal displacement — heat-haze shimmer across the frame | 0.5–0.8s | +| **flash-through-white** | Fade through white midpoint — almost invisible at 0.01s, noticeable at 0.3s | 0.01s–0.3s | +| **cross-warp-morph** | FBM vector field displaces both scenes; a third FBM biases the wipe direction | 0.5–0.8s | +| **light-leak** | Fixed off-frame light source with exponential falloff, warmth, and a ridge flare | 0.5–0.8s | +| **glitch** | Line displacement + RGB lateral split + scan modulation + posterization + flicker | 0.3–0.5s | + +**You are not limited to what's listed here.** These are the built-in options, but you can and should: + +- **Write custom GLSL shaders** from scratch for unique transition effects +- **Search online** for shader code (ShaderToy, GLSL Sandbox, GitHub) and adapt it +- **Build custom CSS transitions** that aren't in any category — combine clip-path, transforms, filters in new ways +- **Ask the user** to provide or find specific effects if you need something specialized + +If the storyboard calls for an effect that doesn't exist yet — build it. The framework renders anything a browser can run. ### Depth layers @@ -88,12 +153,16 @@ What sounds at what moment: Before writing HTML, declare your scene rhythm: which scenes are quick hits, which are holds, where do shaders land, where does energy peak. Name the pattern — fast-fast-SLOW-fast-SHADER-hold — before implementing. -| Video type | Typical rhythm pattern | -| ---------------------- | --------------------------------- | -| Social ad (15s) | hook-PUNCH-hold-CTA | -| Product demo (30-60s) | slow-build-BUILD-PEAK-breathe-CTA | -| Launch teaser (10-20s) | SLAM-proof-SLAM-hold | -| Brand reel (20-45s) | drift-build-PEAK-drift-resolve | +**Derive the rhythm from the storyboard and the brand, not from a lookup.** A 15-second social ad for an architectural firm and a 15-second social ad for a gaming brand have different rhythms — both are 15 seconds, but one is slow-reveal-hold-CTA and the other is rapid-fire-SLAM-hook. Video type sets constraints (duration, approximate beat count); the brand and content determine whether those beats are slow or fast, sparse or dense, dramatic or controlled. + +Questions that drive rhythm decisions: + +- What emotional journey should the viewer take? Where is the peak moment? +- Where does the narration land its heaviest emphasis? That's usually where energy should peak. +- What does the brand's own visual pacing suggest — unhurried or urgent? +- How many beats can the duration actually support without feeling rushed or padded? + +A social ad that tries to hook in 2s, showcase 3 features, and end with a CTA in 15s will feel like noise. Sometimes "hook-hold-CTA" with one strong feature is the right rhythm for 15 seconds. Name the rhythm you've planned before implementing. --- diff --git a/skills/hyperframes/references/dynamic-techniques.md b/skills/hyperframes/references/dynamic-techniques.md index c0cab609f..af271e39a 100644 --- a/skills/hyperframes/references/dynamic-techniques.md +++ b/skills/hyperframes/references/dynamic-techniques.md @@ -4,13 +4,25 @@ You are here because SKILL.md told you to read this file before writing animatio ## Technique Selection by Energy -| Energy level | Highlight | Exit | Cycle pattern | -| ------------ | ------------------------------------- | ------------------- | ----------------------------------------- | -| High | Karaoke with accent glow + scale pop | Scatter or drop | Alternate highlight styles every 2 groups | -| Medium-high | Karaoke with color pop | Scatter or collapse | Alternate every 3 groups | -| Medium | Karaoke (subtle, white only) | Fade + slide | Alternate every 3 groups | -| Medium-low | Karaoke (minimal scale change) | Fade | Single style, vary ease per group | -| Low | Karaoke (warm tones, slow transition) | Collapse | Alternate every 4 groups | +Captions are a constrained surface — the highlight and exit technique is closely tied to how much intensity the spoken content carries. The table below is a calibration reference. If DESIGN.md or the storyboard specifies a caption style, that overrides anything here. + +The core principle: **all energy levels use karaoke highlight as the baseline.** The difference is intensity — not the technique type. + +**What changes with energy:** + +- **Highlight intensity:** high energy gets accent color + glow + 15% scale pop on active words. Low energy gets a gentle white shift with 3% scale. The karaoke behavior is the same; the amplitude is different. +- **Exit style:** high energy exits scatter or drop (the word leaves with motion). Low energy exits collapse (the word simply fades or shrinks). The exit should express the same energy as the content. +- **Cycle variation:** high energy alternates highlight styles every 2 groups for variety. Low energy uses a single consistent style, varying only the ease. Variation itself creates energy; consistency creates calm. + +Calibration reference (starting points, not rules): + +| Energy level | Highlight amplitude | Exit | Cycle variation | +| ------------ | ----------------------------------- | ------------------- | --------------- | +| High | Accent color + glow + 15% scale pop | Scatter or drop | Every 2 groups | +| Medium-high | Color pop, no glow | Scatter or collapse | Every 3 groups | +| Medium | White shift only | Fade + slide | Every 3 groups | +| Medium-low | Minimal scale change | Fade | Single style | +| Low | Warm tones, slow transition | Collapse | Single style | **All energy levels use karaoke highlight as the baseline.** The difference is intensity — high energy gets accent color + glow + 15% scale pop on active words, low energy gets a gentle white shift with 3% scale. diff --git a/skills/hyperframes/references/html-in-canvas-patterns.md b/skills/hyperframes/references/html-in-canvas-patterns.md new file mode 100644 index 000000000..bd6fd8f79 --- /dev/null +++ b/skills/hyperframes/references/html-in-canvas-patterns.md @@ -0,0 +1,504 @@ +# HTML-in-Canvas Patterns + +HyperFrames' most powerful visual capability. Capture ANY live HTML/CSS as a GPU texture, then render it through WebGL shaders, Three.js 3D scenes, or post-processing effects — at 60fps, pixel-perfect, with every CSS feature supported. + +**Read this file when a beat deserves cinematic treatment beyond flat GSAP animations.** Use for 1-3 hero beats per video, not every beat. The rest can use standard GSAP — the contrast between flat beats and HTML-in-Canvas beats IS part of the visual storytelling. + +--- + +## Core Boilerplate (same in every HTML-in-Canvas composition) + +Every HTML-in-Canvas effect shares this structure. Learn this once, adapt it for any effect. + +```html + + +
+ +
+
+ + + +``` + +```js +// 3. Feature detection — always check, always provide fallback +function isHiCSupported() { + var tc = document.createElement("canvas"); + if (!("layoutSubtree" in tc)) return false; + tc.setAttribute("layoutsubtree", ""); + var ctx = tc.getContext("2d"); + return ctx && typeof ctx.drawElementImage === "function"; +} +var apiOk = isHiCSupported(); + +// 4. Capture function — call this every frame in onUpdate +var capCanvas = document.getElementById("hic-source"); +var capCtx = capCanvas.getContext("2d"); +function captureContent() { + if (apiOk) { + capCtx.drawElementImage(document.getElementById("hic-content"), 0, 0, 1920, 1080); + } +} + +// 5. Drive from GSAP timeline — capture + render every frame +tl.to( + proxy, + { + /* your animation properties */ + duration: BEAT_DURATION, + ease: "sine.inOut", + onUpdate: function () { + captureContent(); + // render your effect here (Three.js or WebGL2) + }, + }, + 0, +); +``` + +**Fallback:** When `drawElementImage` is not available (preview without Chrome flag), draw a solid-color placeholder or use Canvas 2D text. The HyperFrames renderer auto-enables the flag — the effect WILL work in the final video. See the liquid-glass block for a complete fallback example. + +--- + +## Effect Catalog + +### 1. 3D Rotation with Bloom (Three.js) + +**What it looks like:** Content floats in 3D space, slowly rotating with cinematic glow around bright edges. Like a product screenshot displayed in a dark theater. + +**When to use:** Hero product showcase, feature reveal, CTA with premium feel. + +**Key Three.js components:** `PlaneGeometry` + `CanvasTexture` + `EffectComposer` + `UnrealBloomPass` + +```js +// After the boilerplate above, add: +var scene3d = new THREE.Scene(); +var camera = new THREE.PerspectiveCamera(45, 1920 / 1080, 0.1, 100); +camera.position.set(0, 0, 4); + +var renderer = new THREE.WebGLRenderer({ + canvas: document.getElementById("hic-output"), + antialias: true, + alpha: true, +}); +renderer.setSize(1920, 1080); + +var texture = new THREE.CanvasTexture(capCanvas); +var mesh = new THREE.Mesh( + new THREE.PlaneGeometry(3.6, 2.2), + new THREE.MeshBasicMaterial({ map: texture }), +); +scene3d.add(mesh); + +// Post-processing: bloom for cinematic glow +var composer = new THREE.EffectComposer(renderer); +composer.addPass(new THREE.RenderPass(scene3d, camera)); +composer.addPass(new THREE.UnrealBloomPass(new THREE.Vector2(1920, 1080), 0.3, 0.4, 0.85)); + +var proxy = { rotY: -0.12, zoom: 4.2 }; +tl.to( + proxy, + { + rotY: 0.12, + zoom: 3.6, + duration: BEAT_DURATION, + ease: "sine.inOut", + onUpdate: function () { + captureContent(); + texture.needsUpdate = true; + mesh.rotation.y = proxy.rotY; + camera.position.z = proxy.zoom; + composer.render(); + }, + }, + 0, +); +``` + +**Load Three.js and post-processing via ESM (use a `type="module"` script):** + +```html + +``` + +The `examples/js/` path was removed in Three.js r152. Use `examples/jsm/` (ES modules) with `three@0.181.2` — the version used by the HyperFrames Three.js adapter. + +--- + +### 2. Magnetic Cursor Distortion (Raw WebGL2) + +**What it looks like:** Content warps and bends toward a moving point, like a magnet pulling on pixels. Chromatic aberration splits RGB channels at the distortion site. + +**When to use:** Interactive feel, product demo with cursor, "look at THIS feature" moment. + +**Key technique:** Custom fragment shader with Gaussian warp + chromatic split. No Three.js needed — just raw WebGL2. + +```js +// WebGL2 setup +var gl = document.getElementById("hic-output").getContext("webgl2", { + alpha: false, + preserveDrawingBuffer: true, +}); + +// Vertex shader — full-screen quad +var VS = `#version 300 es +in vec2 a_pos; +out vec2 v_uv; +void main() { + v_uv = a_pos * 0.5 + 0.5; + gl_Position = vec4(a_pos, 0.0, 1.0); +}`; + +// Fragment shader — magnetic warp + chromatic aberration +var FS = `#version 300 es +precision highp float; +in vec2 v_uv; +out vec4 fragColor; +uniform sampler2D u_tex; +uniform vec2 u_cursor; // cursor position (0-1) +uniform float u_strength; // warp strength (0-1) + +void main() { + vec2 uv = v_uv; + vec2 delta = uv - u_cursor; + float dist = length(delta); + float warp = u_strength * exp(-dist * dist * 8.0); + vec2 warped = uv - delta * warp * 0.3; + + // Chromatic aberration at distortion site + float aberration = warp * 0.008; + float r = texture(u_tex, warped + vec2(aberration, 0.0)).r; + float g = texture(u_tex, warped).g; + float b = texture(u_tex, warped - vec2(aberration, 0.0)).b; + fragColor = vec4(r, g, b, 1.0); +}`; + +// Compile, link, setup quad geometry, upload texture... +// (See registry/blocks/vfx-magnetic/vfx-magnetic.html for complete implementation) + +// Drive cursor position from GSAP +var proxy = { cx: 0.2, cy: 0.5, strength: 0.0 }; +tl.to( + proxy, + { + cx: 0.8, + cy: 0.4, + strength: 1.0, + duration: BEAT_DURATION, + ease: "power2.inOut", + onUpdate: function () { + captureContent(); + // Upload texture, set uniforms, draw + gl.uniform2f(cursorLoc, proxy.cx, proxy.cy); + gl.uniform1f(strengthLoc, proxy.strength); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + }, + }, + 0, +); +``` + +--- + +### 3. Shatter / Fragment Explosion (Three.js) + +**What it looks like:** Content breaks into geometric fragments that fly apart, revealing what's behind. + +**When to use:** Dramatic transition, "breaking free" moment, tension release. + +**Key technique:** Subdivide the source texture into triangle mesh fragments using BufferGeometry, then animate each fragment's position/rotation with GSAP. + +Study `registry/blocks/vfx-shatter/vfx-shatter.html` for the complete 1156-line implementation. The core idea: + +```js +// 1. Capture content to texture (same boilerplate) +// Seeded PRNG for determinism — Math.random() is banned +function mulberry32(seed) { + return function () { + seed |= 0; + seed = (seed + 0x6d2b79f5) | 0; + var t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + t ^= t + Math.imul(t ^ (t >>> 7), 61 | t); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} +var rng = mulberry32(42); + +// 2. Create N triangle fragments from the texture +var fragments = []; +for (var i = 0; i < NUM_FRAGMENTS; i++) { + var geom = new THREE.BufferGeometry(); + var mesh = new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ map: texture })); + scene3d.add(mesh); + fragments.push({ mesh: mesh, targetPos: randomExplosionVector(rng), delay: rng() * 0.5 }); +} + +// 3. Animate: first hold still, then EXPLODE +tl.to({}, { duration: holdTime }, 0); +fragments.forEach(function (frag) { + tl.to( + frag.mesh.position, + { + x: frag.targetPos.x, + y: frag.targetPos.y, + z: frag.targetPos.z, + duration: 0.8, + ease: "power3.in", + }, + holdTime + frag.delay, + ); + tl.to( + frag.mesh.rotation, + { x: rng() * 4, y: rng() * 4, duration: 0.8, ease: "power2.in" }, + holdTime + frag.delay, + ); +}); +``` + +--- + +### 4. Liquid / Fluid Surface (Three.js) + +**What it looks like:** Content floats above a rippling liquid surface with real-time wave dynamics. Or content IS the surface, undulating like water. + +**When to use:** Organic/premium feel, ambient background, "living" product showcase. + +**Key technique:** Subdivided PlaneGeometry with vertex displacement driven by noise functions in a vertex shader. + +Study `registry/blocks/vfx-liquid-background/vfx-liquid-background.html` for the 1244-line implementation. Core idea: + +```js +// Custom vertex shader with wave displacement +var vertexShader = ` + varying vec2 vUv; + uniform float u_time; + void main() { + vUv = uv; + vec3 pos = position; + // Sine wave displacement + pos.z += sin(pos.x * 3.0 + u_time * 2.0) * 0.15; + pos.z += cos(pos.y * 2.5 + u_time * 1.5) * 0.1; + gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); + } +`; + +var mesh = new THREE.Mesh( + new THREE.PlaneGeometry(4, 3, 64, 64), // heavily subdivided for smooth waves + new THREE.ShaderMaterial({ + vertexShader: vertexShader, + fragmentShader: `varying vec2 vUv; uniform sampler2D u_tex; + void main() { gl_FragColor = texture2D(u_tex, vUv); }`, + uniforms: { + u_tex: { value: texture }, + u_time: { value: 0 }, + }, + }), +); +``` + +--- + +### 5. Portal / Dimensional Reveal (Three.js) + +**What it looks like:** A glowing circular portal opens and content emerges through it from another dimension. + +**When to use:** Product reveal, "entering the app" moment, hero feature introduction. + +Study `registry/blocks/vfx-portal/vfx-portal.html` for the complete 863-line implementation. + +--- + +## When to Use HTML-in-Canvas vs Standard GSAP + +| Scenario | Use | Why | +| -------------------------------- | ------------------------------------ | ------------------------------------ | +| Hero product screenshot showcase | HTML-in-Canvas (3D rotation + bloom) | Makes flat UI feel cinematic | +| Feature list / stats | Standard GSAP | Content-focused, doesn't need 3D | +| CTA / brand reveal | HTML-in-Canvas (portal or magnetic) | Makes the moment memorable | +| Social proof / logos | Standard GSAP | Orderly cascade, trust is steady | +| Transition between acts | HTML-in-Canvas (shatter) | Dramatic act break | +| Background atmosphere | HTML-in-Canvas (liquid surface) | Premium ambient feel | +| Quick feature cards | Standard GSAP | Speed matters, 3D would slow it down | + +--- + +## More Effects You Can Build + +These aren't in the VFX blocks — build them yourself from the core boilerplate + a custom fragment shader. Each effect is a single GLSL function applied to the captured texture. + +### 6. Noise Dissolve + +Content dissolves into noise particles, revealing what's behind. Great for transitions. + +```glsl +// Fragment shader — noise-based dissolve +uniform float u_progress; // 0.0 = fully visible, 1.0 = fully dissolved +uniform sampler2D u_tex; + +float hash(vec2 p) { + return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); +} + +void main() { + vec2 uv = v_uv; + float noise = hash(uv * 50.0); + float threshold = u_progress; + if (noise < threshold) { + // Edge glow at the dissolve boundary + float edge = smoothstep(threshold - 0.05, threshold, noise); + fragColor = vec4(1.0, 0.6, 0.2, 1.0) * (1.0 - edge); // orange edge glow + } else { + fragColor = texture(u_tex, uv); + } +} +``` + +### 7. Holographic / Iridescent + +Content gets a rainbow-shifting holographic sheen that moves with time. Premium, futuristic feel. + +```glsl +uniform float u_time; +uniform sampler2D u_tex; + +void main() { + vec4 color = texture(u_tex, v_uv); + // Iridescent color shift based on position + time + float angle = v_uv.x * 6.28 + v_uv.y * 3.14 + u_time * 0.5; + vec3 holo = vec3( + sin(angle) * 0.5 + 0.5, + sin(angle + 2.094) * 0.5 + 0.5, + sin(angle + 4.189) * 0.5 + 0.5 + ); + // Blend holographic over content (subtle overlay) + fragColor = vec4(mix(color.rgb, holo, 0.15 + 0.1 * sin(u_time)), color.a); +} +``` + +### 8. Scan Lines + CRT + +Retro CRT monitor look — scan lines, slight curvature, phosphor glow. Great for "code" or "terminal" beats. + +```glsl +uniform sampler2D u_tex; +uniform float u_time; + +void main() { + vec2 uv = v_uv; + // Barrel distortion (CRT curvature) + vec2 centered = uv - 0.5; + float dist = dot(centered, centered); + uv = uv + centered * dist * 0.15; + + vec4 color = texture(u_tex, uv); + // Scan lines + float scanline = sin(uv.y * 800.0) * 0.04; + color.rgb -= scanline; + // Slight RGB offset (phosphor) + color.r = texture(u_tex, uv + vec2(0.001, 0.0)).r; + color.b = texture(u_tex, uv - vec2(0.001, 0.0)).b; + // Vignette + float vignette = 1.0 - dist * 2.0; + fragColor = vec4(color.rgb * vignette, 1.0); +} +``` + +### 9. Frosted Glass Blur + +Content behind frosted glass — visible but softened, with subtle light refraction. Good for "behind the scenes" or "coming soon" moments. + +```glsl +uniform sampler2D u_tex; +uniform float u_blur; // 0.0 = clear, 1.0 = full frost + +void main() { + vec2 uv = v_uv; + vec4 color = vec4(0.0); + // Box blur with offset + float radius = u_blur * 0.015; + for (float x = -2.0; x <= 2.0; x += 1.0) { + for (float y = -2.0; y <= 2.0; y += 1.0) { + color += texture(u_tex, uv + vec2(x, y) * radius); + } + } + color /= 25.0; + // Add frost noise texture + float frost = fract(sin(dot(uv * 200.0, vec2(12.9898, 78.233))) * 43758.5453); + color.rgb += frost * 0.03 * u_blur; + fragColor = color; +} +``` + +### 10. Pixel Sort / Glitch Art + +Pixels rearrange themselves in vertical or horizontal strips — digital art aesthetic. Great for tech/creative brands. + +```glsl +uniform sampler2D u_tex; +uniform float u_intensity; // 0-1 + +void main() { + vec2 uv = v_uv; + // Random horizontal displacement per row + float row = floor(uv.y * 80.0); + float noise = fract(sin(row * 127.1) * 43758.5); + float displace = step(0.7, noise) * u_intensity * 0.1; + // Shift UV with RGB split + float r = texture(u_tex, uv + vec2(displace, 0.0)).r; + float g = texture(u_tex, uv).g; + float b = texture(u_tex, uv - vec2(displace * 0.5, 0.0)).b; + fragColor = vec4(r, g, b, 1.0); +} +``` + +--- + +## Creating ANY Custom Effect + +The fragment shaders above are templates. The pattern is always: + +1. **Capture your HTML content** with `drawElementImage` (the boilerplate at the top) +2. **Upload the captured canvas as a WebGL texture** +3. **Write a fragment shader** that reads from the texture and outputs modified colors +4. **Drive shader uniforms from GSAP** via `onUpdate` + +Any GLSL effect from ShaderToy, The Book of Shaders, CodePen, or anywhere else can be adapted: + +1. Find an effect you like (search "GLSL [effect name]" or browse shadertoy.com) +2. Copy the fragment shader +3. Replace `iResolution` with `vec2(1920.0, 1080.0)`, `iTime` with your `u_time` uniform +4. Add `uniform sampler2D u_tex;` for the captured content texture +5. Wire the uniforms to GSAP proxy values + +**Geometry ideas beyond flat planes:** + +- `SphereGeometry` — content mapped onto a globe (world map, global reach) +- `CylinderGeometry` — content on a rotating cylinder (carousel/scroll feel) +- `TorusGeometry` — content wrapped around a ring (infinity, cycle) +- `BoxGeometry` — content on a 3D box (product packaging, dice) +- GLTF models — content mapped as screen texture on phone, laptop, monitor (see `vfx-iphone-device`) + +**Post-processing stacking** (Three.js EffectComposer): + +- Bloom + film grain = cinematic +- Bloom + chromatic aberration = lens effect +- Depth of field + vignette = focused attention +- Film grain + scan lines = retro +- Multiple passes stack — add as many as you want + +**You are not limited to the effects listed here.** If you can imagine a visual treatment, you can build it. The HTML-in-Canvas API gives you the source material (any HTML rendered as a texture), and WebGL/Three.js gives you unlimited creative control over how that material is presented. diff --git a/skills/hyperframes/references/motion-principles.md b/skills/hyperframes/references/motion-principles.md index 011d59883..8e5bc2f9c 100644 --- a/skills/hyperframes/references/motion-principles.md +++ b/skills/hyperframes/references/motion-principles.md @@ -1,82 +1,90 @@ # Motion Principles -## Guardrails +## Common defaults that produce monoculture -You know these rules but you violate them. Stop. +These are the patterns LLMs reach for without thinking. None of them are wrong in isolation — they're wrong as defaults. If every scene of every video lands on the same easing, the same speed, and the same entrance direction, the compositions blur into one another no matter what the brand is. -- **Don't use the same ease on every tween.** You default to `power2.out` on everything. Vary eases like you vary font weights — no more than 2 independent tweens with the same ease in a scene. -- **Don't use the same speed on everything.** You default to 0.4-0.5s for everything. The slowest scene should be 3× slower than the fastest. Vary duration deliberately. -- **Don't enter everything from the same direction.** You default to `y: 30, opacity: 0` on every element. Vary: from left, from right, from scale, opacity-only, letter-spacing. -- **Don't use the same stagger on every scene.** Each scene needs its own rhythm. -- **Don't use ambient zoom on every scene.** Pick different ambient motion per scene: slow pan, subtle rotation, scale push, color shift, or nothing. Stillness after motion is powerful. -- **Don't start at t=0.** Offset the first animation 0.1-0.3s. Zero-delay feels like a jump cut. +- **Same ease on every tween.** `power2.out` is the most common default. Aim for variety: no more than two independent tweens sharing an ease within a scene. Eases are like font weights — vary them deliberately. +- **Same speed on every tween.** 0.4–0.5s is a common default that flattens rhythm. The slowest motion in a scene should be roughly 3× slower than the fastest. Vary duration so the eye can tell what's important. +- **Same entrance direction.** `y: 30, opacity: 0` is the universal LLM entrance. The same scene can use entrances from left, from right, from scale, from blur, opacity-only, letter-spacing — each one says something different about the element. +- **Same stagger across scenes.** Each scene should have its own rhythm. A 0.08s stagger in beat 1 and a 0.15s stagger in beat 2 makes the two beats feel like different moments. +- **Ambient zoom on every scene.** Slow-scale-up is the default ambient motion and it telegraphs "LLM-generated video." Vary the ambient motion per scene: slow pan, subtle rotation, color temperature shift, gentle drift — and sometimes nothing. Stillness after motion has real weight. +- **First animation at t=0.** Zero-delay feels like a jump cut. Offset the opening 0.1–0.3s so the scene reads as composed rather than thrown together. -## What You Don't Do Without Being Told +## Easing is emotion, not technique -### Easing is emotion, not technique +The motion is the verb. The easing is the adverb. A slide-in with `expo.out` feels confident. With `sine.inOut`, dreamy. With `elastic.out`, playful. Same motion, three different meanings. Choose the adverb deliberately. -The transition is the verb. The easing is the adverb. A slide-in with `expo.out` = confident. With `sine.inOut` = dreamy. With `elastic.out` = playful. Same motion, different meaning. Choose the adverb deliberately. +**Direction rules:** -**Direction rules — these are not optional:** +- `.out` for elements entering. Starts fast, decelerates. Feels responsive. This is the default for entrances. +- `.in` for elements leaving. Starts slow, accelerates away. Sends them off with momentum. +- `.inOut` for elements moving between positions, neither entering nor leaving the scene. -- `.out` for elements entering. Starts fast, decelerates. Feels responsive. This is your default. -- `.in` for elements leaving. Starts slow, accelerates away. Throws them off. -- `.inOut` for elements moving between positions. +Ease-in on an entrance feels sluggish. Ease-out on an exit feels reluctant. These are the most common reversals and they're worth checking your work against. -You get this backwards constantly. Ease-in for entrances feels sluggish. Ease-out for exits feels reluctant. +## Speed expresses weight -### Speed communicates weight +Duration is one of the most direct ways a composition communicates what it values. Faster motion reads as confident, urgent, kinetic — it gives the viewer less time to study what's happening, which means the work has to land in fewer frames. Slower motion reads as deliberate, considered, weighty — the viewer has time to take in the element, which means each element has to earn that attention. -- Fast (0.15-0.3s) — energy, urgency, confidence -- Medium (0.3-0.5s) — professional, most content -- Slow (0.5-0.8s) — gravity, luxury, contemplation -- Very slow (0.8-2.0s) — cinematic, emotional, atmospheric +Useful calibration ranges (not prescriptions — what a duration _expresses_ depends on what surrounds it): -### Scene structure: build / breathe / resolve +- **0.15–0.3s** — quick, percussive, kinetic. The motion reads as something happening _to_ the frame. +- **0.3–0.5s** — comfortable, professional. The motion reads as composed and reliable. +- **0.5–0.8s** — deliberate. The motion has visible weight and asks for attention. +- **0.8s+** — atmospheric. The motion becomes part of what the scene _is_, not something happening within it. -Every scene has three phases. You dump everything in the build and leave nothing for breathe or resolve. +A composition that uses only one of these ranges feels one-note. Mix them — a scene where the headline takes 0.7s to settle and the supporting details land in 0.25s creates contrast that reinforces hierarchy without needing different colors or sizes. -- **Build (0-30%)** — elements enter, staggered. Don't dump everything at once. -- **Breathe (30-70%)** — content visible, alive with ONE ambient motion. -- **Resolve (70-100%)** — exit or decisive end. Exits are faster than entrances. +## Scene structure: build, breathe, resolve -### Transitions are meaning +Every scene has three phases. The most common failure is dumping everything into the build and leaving nothing for the other two. -- **Crossfade** = "this continues" -- **Hard cut** = "wake up" / disruption -- **Slow dissolve** = "drift with me" +- **Build (0–30%)** — elements enter, staggered. Not all at once. +- **Breathe (30–70%)** — content visible, alive with one ambient motion. The viewer reads, registers, settles. +- **Resolve (70–100%)** — exit or decisive end. Exits are faster than entrances (see Asymmetry below). -You crossfade everything. Use hard cuts for disruption and register shifts. +A scene that's all build feels like a slideshow. A scene with no breathe phase doesn't let the content land. -### Choreography is hierarchy +## Transitions carry meaning -The element that moves first is perceived as most important. Stagger in order of importance, not DOM order. Don't wait for completion — overlap entries. Total stagger sequence under 500ms regardless of item count. +The transition type tells the viewer how two scenes relate: -### Asymmetry +- **Crossfade** — "this continues." Connective tissue between related ideas. +- **Hard cut** — "wake up" or a register shift. Disruption, surprise, percussive emphasis. +- **Slow dissolve** — "drift with me." Atmospheric, meditative, between-thoughts. -Entrances need longer than exits. A card takes 0.4s to appear but 0.25s to disappear. +Crossfade is the default and it's defensible most of the time. The thing to watch for is using it for everything — when every transition is a crossfade, the viewer stops registering scene changes as meaningful. Hard cuts and slow dissolves are tools for the moments where the change in scene _is_ the message. -## Visual Composition +## Choreography is hierarchy -You build for the web. Video frames are not pages. +The element that moves first is perceived as most important. Stagger in order of importance, not DOM order. Don't wait for one entrance to complete before starting the next — overlap entries. Total stagger sequence under 500ms regardless of item count keeps the scene from feeling like a slow drip. -- **Two focal points minimum per scene.** The eye needs somewhere to travel. Never a single text block floating in empty space. -- **Fill the frame.** Hero text: 60-80% of width. You will try to use web-sized elements. Don't. -- **Three layers minimum per scene.** Background treatment (glow, oversized faded type, color panel). Foreground content. Accent elements (dividers, labels, data bars). -- **Background is not empty.** Radial glows, oversized faded type bleeding off-frame, subtle border panels, hairline rules. Pure solid #000 reads as "nothing loaded." -- **Anchor to edges.** Pin content to left/top or right/bottom. Centered-and-floating is a web pattern. -- **Split frames.** Data panel on the left, content on the right. Top bar with metadata, full-width below. Zone-based layouts, not centered stacks. -- **Use structural elements.** Rules, dividers, border panels. They create paths for the eye and animate well (scaleX from 0). +## Asymmetry between entrances and exits -## Image Motion Treatment +Entrances need longer than exits. A card might take 0.4s to appear but 0.25s to disappear — entrances build presence, exits remove it, and remove takes less time than build. -Never embed a raw flat image. Every image must have motion treatment: +## Visual composition -- **Perspective tilt**: use `gsap.set(el, { transformPerspective: 1200, rotationY: -8 })` + `box-shadow` — creates depth. Do NOT use CSS `transform: perspective(...)` as GSAP will overwrite it. -- **Slow zoom (Ken Burns)**: GSAP `scale: 1` → `1.04` over beat duration — makes photos cinematic -- **Device frame**: Wrap in a laptop/phone shape using CSS `border-radius` and `box-shadow` -- **Floating UI**: Extract a key element and animate it at a different z-depth for parallax -- **Scroll reveal**: Clip the image to a viewport window and animate `y` position +Video frames are not web pages. Web layout patterns that work on a scrollable page often look broken in a fixed-frame composition. + +- **Two focal points minimum per scene.** The eye needs somewhere to travel. A single text block floating in empty space reads as unfinished. +- **Fill the frame.** Hero text typically wants 60–80% of frame width. Web type sizes — 16px body, 32px headlines — disappear at video distance. +- **Three layers minimum.** Background treatment (glow, oversized faded type, color panel), foreground content, accent elements (dividers, labels, data bars). A scene with only one layer reads flat. +- **Background is not empty.** Radial glows, oversized faded type bleeding off-frame, subtle border panels, hairline rules. Pure solid `#000` reads as "nothing loaded." +- **Anchor to edges.** Pin content to left/top or right/bottom. Centered-and-floating is a web pattern that looks lost on a 16:9 canvas. +- **Split frames.** Data panel on the left, content on the right. Top bar with metadata, full-width below. Zone-based layouts beat centered stacks. +- **Use structural elements.** Rules, dividers, border panels. They create paths for the eye and animate well (`scaleX` from 0). + +## Image motion treatment + +Embedded images shouldn't sit flat — every image earns some motion treatment: + +- **Perspective tilt** — `gsap.set(el, { transformPerspective: 1200, rotationY: -8 })` plus a `box-shadow` creates depth. Do NOT use CSS `transform: perspective(...)`; GSAP will overwrite it. +- **Slow zoom (Ken Burns)** — GSAP `scale: 1` → `1.04` over beat duration. Makes photos feel cinematic rather than pasted in. +- **Device frame** — wrap in a laptop or phone shape using `border-radius` and `box-shadow`. +- **Floating UI** — extract a key element and animate it at a different z-depth for parallax. +- **Scroll reveal** — clip the image to a viewport window and animate `y` position. ## Load-Bearing GSAP Rules @@ -132,7 +140,7 @@ Rules below came out of two independent website-to-hyperframes builds (2026-04-2 tl.to(".aura", { scale: 1.08, yoyo: true, repeat: 5, duration: 1.2 }, 0); ``` -- **Hard-kill every scene boundary, not just captions.** The caption hard-kill rule above generalizes: any element whose visibility changes at a beat boundary needs a deterministic `tl.set()` kill after its fade, because later tweens on the same element (or `immediateRender` from a sibling tween) can resurrect it. Apply to every element with an exit animation: +- **Hard-kill every scene boundary, not just captions.** The same hard-kill pattern from `captions.md` generalizes to all elements with exit animations: any element whose visibility changes at a beat boundary needs a deterministic `tl.set()` kill after its fade, because later tweens on the same element (or `immediateRender` from a sibling tween) can resurrect it. Apply to every element with an exit animation: ```js tl.to(el, { opacity: 0, duration: 0.3 }, beatEnd); diff --git a/skills/hyperframes/references/prompt-expansion.md b/skills/hyperframes/references/prompt-expansion.md index ab504eee1..e08075e52 100644 --- a/skills/hyperframes/references/prompt-expansion.md +++ b/skills/hyperframes/references/prompt-expansion.md @@ -8,12 +8,12 @@ Runs AFTER design direction is established (Step 1). The expansion consumes desi Read before generating: -- `design.md` (if it exists) — extract brand colors, fonts, mood, and constraints. The expansion cites these exact values (hex codes, font names); it does not invent new ones. +- `DESIGN.md` (if it exists) — extract brand colors, fonts, mood, and constraints. The expansion cites these exact values (hex codes, font names); it does not invent new ones. - [beat-direction.md](beat-direction.md) — per-beat planning format (concept, mood, choreography verbs, transitions, depth layers, rhythm). The expansion outputs each scene using this format. - [video-composition.md](video-composition.md) — video-medium rules for density, scale, and color presence. The expansion applies these automatically. - [../house-style.md](../house-style.md) — its rules for Background Layer (2-5 decoratives), Color, Motion, Typography apply to every scene. The expansion writes output that conforms to them. -If `design.md` doesn't exist yet, run Step 1 (Design system) first. Expansion without a design context produces generic scene breakdowns that later agents ignore. +If `DESIGN.md` doesn't exist yet, run Step 1 (Design system) first. Expansion without a design context produces generic scene breakdowns that later agents ignore. ## Why always run it @@ -40,7 +40,7 @@ Expand into a full production prompt with these sections: 1. **Title + style block** — cite design.md's exact hex values, font names, and mood. Do NOT invent a palette — quote what the design provides. -2. **Rhythm declaration** — name the scene rhythm before detailing any scene. Example: `hook-PUNCH-breathe-CTA` or `slow-build-BUILD-PEAK-breathe-CTA`. See [beat-direction.md](beat-direction.md) for rhythm templates by video type. +2. **Rhythm declaration** — name the scene rhythm before detailing any scene. Example: `hook-PUNCH-breathe-CTA` or `slow-build-BUILD-PEAK-breathe-CTA`. Derive the rhythm from the brand and the storyboard's emotional arc — see [beat-direction.md](beat-direction.md) for the considerations that drive this decision. 3. **Global rules** — parallax layers, micro-motion requirements, transition style, primary + accent transitions. Match energy to mood (calm → slow eases, high → snappy eases). diff --git a/skills/hyperframes/references/techniques.md b/skills/hyperframes/references/techniques.md index b460fa845..ca7f28fb6 100644 --- a/skills/hyperframes/references/techniques.md +++ b/skills/hyperframes/references/techniques.md @@ -1,9 +1,42 @@ # Visual Techniques Reference -10 proven techniques from production HyperFrames videos. Use these in your storyboard and compositions to create visually rich, professional output. Each technique includes a minimal code pattern you can adapt. +20 proven techniques from production HyperFrames videos. Use these in your storyboard and compositions to create visually rich, professional output. Each technique includes a minimal code pattern you can adapt. These are NOT advanced — they're standard motion design patterns that every composition should use at least 2-3 of. +**These are starting points, not copy-paste templates.** Every code pattern below is a minimal working example from a real production video. Adapt them to your needs — change colors, sizes, timings, easings, element counts, layout. Combine techniques, mix parts from different patterns, invent variations. The goal is to understand the PRINCIPLE behind each technique so you can build something original, not to reproduce these examples exactly. + +## Table of Contents + +**Named text animation effects** (per-character, per-word, per-line, whole-element) with exact GSAP specs are in [`text-effects.md`](text-effects.md) — 24 bundled effects, no install needed. Use those for all headline and label animations instead of inventing timing from scratch. + +**HTML-in-Canvas patterns** (live DOM as GPU texture: iPhone/MacBook mockups, liquid glass, magnetic, portal, shatter, text cursor — using `drawElementImage` + `layoutsubtree`) are in [`html-in-canvas-patterns.md`](html-in-canvas-patterns.md) — 504 lines, one shared boilerplate + ~6 effect recipes. Use for 1–3 hero beats per video, not every beat. + +--- + +| # | Technique | What it does | Best for | +| --- | ----------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------ | +| 1 | **SVG Path Drawing** | Logos/icons draw themselves stroke by stroke | Logo reveals, diagram builds, connector lines | +| 2 | **Canvas 2D Procedural Art** | Animated noise, particles, data viz — frame-by-frame via GSAP proxy | Generative backgrounds, ambient texture | +| 3 | **CSS 3D Transforms** | Card flips, perspective grids, folding panels | Product reveals, comparison scenes | +| 4 | **Per-Word Kinetic Typography** | Text animates word-by-word with stagger timing | Thesis statements, key messages, quotes | +| 5 | **Lottie Animation** | Captured or external Lottie plays as overlay/background | Brand animations, micro-interactions | +| 6 | **Video Compositing** | Product videos play inline, masked, overlaid | Demo footage, screen recordings | +| 7 | **Character-by-Character Typing** | Terminal-style code reveals, search bar interactions | Developer tools, CLI demos | +| 8 | **Variable Font Axis Animation** | Weight, width, slant, optical size morph over time | Premium typography, brand wordmarks | +| 9 | **GSAP MotionPathPlugin** | Elements follow SVG curves, orbital motion, spirals | Dynamic entrances, connector animations | +| 10 | **Velocity-Matched Transitions** | Outgoing blur/translate matches incoming for seamless cuts | Beat transitions, scene changes | +| 11 | **Audio-Reactive Animation** | Elements pulse to narration frequency bands | Background textures, text glow, ambient motion | +| 12 | **Frosted Glass Panels** | `backdrop-filter: blur` + translucent backgrounds + inset glow | Premium UI cards, feature overlays, modals | +| 13 | **Clip-Path Reveal Masks** | Fixed window that content slides through (text/images enter from one side) | Headline intros, image reveals, wipe effects | +| 14 | **WebGL Fragment Shader Art** | Full GPU generative backgrounds — FBM domain warp, cosine palettes | Hero backgrounds, atmospheric scenes | +| 15 | **Impact Line on Text Drop** | Horizontal line expands at text landing point — physical weight feel | Title cards, stat reveals, single-word emphasis | +| 16 | **Device Mockups (Laptop + Phone)** | CSS-only MacBook/iPhone with realistic chrome, scrolling content | Product showcases, website demos, app reveals | +| 17 | **Aurora Gradient Backgrounds** | Multi-blob radial gradients that drift — depth without Canvas/WebGL | Dark cinematic scenes, CTA backgrounds | +| 18 | **Floating Particles** | CSS particles with glow shadows, staggered float | Ambient texture, premium atmosphere | +| 19 | **Terminal UI with Typing** | macOS chrome + typed commands + scaffold output lines | Developer CTAs, CLI demos, product onboarding | +| 20 | **Moodboard / Editorial Layout** | Paper texture, decorative rules, pinned cards at angles, connecting lines | Brand reels, design showcases, editorial content | + --- ## 1. SVG Path Drawing @@ -155,7 +188,7 @@ The slide distance DECAYS per word (80→12px) — mimics a camera settling. Vector animations that play inside a composition. Use for logos, character animations, icons. ```html - + +

Feature

+

Description text

+ + +``` + +The gradient background provides subtle directional light. The inset shadow adds a top edge highlight. Works on both dark and light backgrounds — adjust rgba values accordingly. + +--- + +## 13. Clip-Path Reveal Masks + +A fixed window that content slides through — text or images enter from one side and are clipped by an invisible boundary. Different from SVG path drawing: the mask is static, the content moves. + +```html +
+
Your headline text here
+
+ + +``` + +Variations: `clip-path: circle(0% at 50% 50%)` → `circle(100%)` for iris reveals. `clip-path: polygon(...)` for custom shapes. + +--- + +## 14. WebGL Fragment Shader Art + +Full GPU generative backgrounds — domain-warped FBM noise, cosine palette coloring, iridescent organic patterns. Far richer than Canvas 2D. + +```html + + +``` + +Always include a Canvas 2D gradient fallback for environments without WebGL. + +--- + +## 15. Impact Line on Text Drop + +Text drops in with overshoot bounce. At the moment of impact, a thin horizontal line expands outward from the baseline — sells the physical weight of the landing. + +```html +
hyperframes
+
+ + +``` + +The line appears slightly after the text lands (0.15s offset). It expands, then fades while growing wider — simulating dissipating energy. + +--- + +## 16. Device Mockups (Laptop + Phone) + +CSS-only laptop and phone frames with realistic chrome, shadows, and perspective. Content scrolls inside the screen. + +```html + +
+
+
+
+ +
+
+
+
+
+
+ +``` + +Animate the content scrolling inside the screen with `tl.to(".site-img", { y: -SCROLL_DISTANCE, duration: 5, ease: "power1.inOut" })`. Add inspector callout annotations that appear at each scroll stop. + +For **phone mockups**: same structure but with dynamic island, status bar, home indicator, and 3D perspective via `transform: perspective(2200px) rotateY(-12deg)`. + +--- + +## 17. Aurora Gradient Backgrounds + +Multi-blob radial gradients that drift slowly — creates depth and atmosphere without Canvas/WebGL complexity. + +```html +
+ + +``` + +Derive colors from DESIGN.md's brand palette. 4-5 blobs at different positions create natural color mixing. Use brand accent as one blob, complementary tones for the rest. + +--- + +## 18. Floating Particles + +CSS-only particles with absolute positioning, glow shadows, and staggered float animation. Adds premium ambient texture. + +```html +
+
+
+
+ +
+ + +``` + +--- + +## 19. Terminal UI with Typing + +macOS-style terminal chrome (traffic light dots) with typed commands, scaffold output lines, and cursor blink. Use for developer-focused videos, CTA sections, and product demos. + +```html +
+
+ + + + Terminal — zsh +
+
+
+ + + | +
+
+
Creating project...
+
index.html
+
meta.json
+
Done
+
+
+
+ +``` + +--- + +## 20. Moodboard / Editorial Layout + +Print-inspired compositions with paper texture, decorative rules, pinned cards at angles, and connecting gold lines. Works for brand reels and design-forward content. + +```html +
+
+
+
+ BRAND NAME +
+
+
+ +
Hero Section
+
+ +
+ +``` + +Pin cards at slightly random angles (±2-5deg). Connect related cards with thin gold SVG lines that draw in. Use paper-like warm backgrounds (#f1ece2, #fbf8f1). + +--- + +## Easing Vocabulary + +GSAP offers a deep easing library. Every composition should use at least 3 different easings — using `power2.out` for everything produces flat, monotonous motion. Think of easings as tone of voice: a video that only whispers is boring; one that varies between whisper, normal, and punch is engaging. + +**The full palette** (each family has `.in`, `.out`, `.inOut` variants): + +| Family | Character | Typical use | +| -------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `power1`–`power4` | Gentle (1) to aggressive (4) acceleration curves | General purpose. power2 is the workhorse, power4 for dramatic snaps | +| `back(N)` | Overshoot then settle. N controls how far past the target (1=subtle, 4=wild) | Logo reveals, badge pops, card entrances. `back.out(2.5)` for playful, `back.out(1.2)` for elegant | +| `elastic(amp, freq)` | Spring bounce. amp=magnitude, freq=oscillation speed | Panel scatter, energetic drops, fun reveals | +| `bounce` | Ball-drop bouncing | Physical interactions, icons landing, score counters | +| `expo` | Extreme acceleration curve (much steeper than power4) | Premium/luxury reveals, dramatic entrances | +| `sine` | Smooth, organic, no hard edges | Ambient float, breathing, Ken Burns, anything that loops. `.inOut` for yoyo motion | +| `circ` | Circular acceleration (starts very fast, ends very gentle or vice versa) | Camera moves, scene transitions, orbital motion | +| `steps(N)` | Discrete N-step jumps, no interpolation | Typing effects, cursor blink, counter ticks, retro/digital aesthetics | + +**Mood mapping:** Match easing character to the beat's emotional content. Smooth/organic easings (`sine`, `power1`) feel contemplative and drifting. Aggressive deceleration (`power4.out`, `expo.out`) feels snappy and confident. Spring overshoot (`back.out`) feels bouncy and physical. The storyboard's mood description should guide which character fits — not a formula. + +--- + +## Choosing techniques + +Don't match techniques to video type on autopilot — match them to the **concept of the specific beat**. Ask: what visual treatment makes this exact idea land? A beat about speed needs motion that communicates speed; a beat about precision needs geometry and structure; a beat about warmth needs texture and organic drift. -| Video energy | Techniques to combine | -| ------------------------------ | --------------------------------------------------------------- | -| High impact (launches, promos) | Per-word typography + velocity transitions + counter animations | -| Cinematic (tours, stories) | SVG path drawing + video compositing + 3D transforms | -| Technical (dev tools, APIs) | Character typing + Canvas 2D procedural + MotionPath | -| Premium (luxury, enterprise) | Variable font animation + Lottie + slow velocity transitions | -| Data-driven (stats, metrics) | Canvas 2D procedural + counter animations + SVG path drawing | +Read the storyboard beat's concept and mood, then scan this list for techniques whose _visual character_ serves that concept. Any technique can appear in any video type — the question is whether it earns its place in this beat. diff --git a/skills/hyperframes/references/text-effects.md b/skills/hyperframes/references/text-effects.md new file mode 100644 index 000000000..0d5595e5f --- /dev/null +++ b/skills/hyperframes/references/text-effects.md @@ -0,0 +1,105 @@ +# Text Effects — Bundled Catalog + +24 named text animation effects, bundled into HyperFrames. No separate install needed. + +**Spec files:** + +- `skills/hyperframes/assets/text-effects/effects/.json` — exact GSAP implementation recipe +- `skills/hyperframes/assets/text-effects/specs/.json` — portable motion contract + +**How to use:** pick an effect that fits the brand, mood, and content. Read its `.json` file. Use `showcase.library_adapters.gsap` for exact easing strings, durations, stagger values, and keyframe data. + +--- + +## Catalog + +### Per-Character + +| ID | Enter duration | Stagger | Ease | Character | +| --------------------- | -------------- | ------- | ----------------------------- | -------------------------------------------------------------------------------- | +| `soft-blur-in` | 648ms | 18ms | `cubic-bezier(0.22,1,0.36,1)` | Each letter fades in with a gentle upward drift and blur. Smooth, airy, premium. | +| `per-character-rise` | 504ms | 17ms | `cubic-bezier(0.2,0.8,0.2,1)` | Letters slide up from below, no blur. Crisp, deliberate, kinetic. | +| `typewriter` | 173ms | 33ms | `steps(1,end)` | Per-character stepped reveal. Discrete, mechanical, editorial. | +| `bottom-up-letters` | 288ms | 63ms | `cubic-bezier(0.18,1,0.32,1)` | Letters rise from below in a pronounced staircase, one symbol at a time. | +| `top-down-letters` | 288ms | 63ms | `cubic-bezier(0.18,1,0.32,1)` | Same staircase but descending from above. | +| `stagger-from-center` | — | — | — | Characters reveal outward from the center. Emphasizes the keyword core. | +| `stagger-from-edges` | — | — | — | Characters converge inward from both edges toward the center. | + +### Per-Word + +| ID | Enter duration | Stagger | Ease | Character | +| ---------------------- | -------------- | ------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `per-word-crossfade` | 504ms | 50ms | `cubic-bezier(0.16,1,0.3,1)` | Words gently fade in with a short vertical drift. Calm, sequential. | +| `spring-scale-in` | 259ms | 68ms | `cubic-bezier(0.34,1.56,0.64,1)` | Words pop in with a spring overshoot. Physical, bouncy, playful. | +| `shared-axis-y` | 140ms | 56ms | `steps(1,end)` | Hard-cut word-by-word with staircase timing. Sharp, editorial. | +| `blur-out-up` | 403ms | 20ms | `cubic-bezier(0.22,1,0.36,1)` | Words arrive clean and exit upward with increasing blur. Airy exit. | +| `kinetic-center-build` | custom | — | custom | Each word locks center as phrase builds right-to-left with soft blur. Layout-aware renderer — read `showcase.rendering_contract`. | +| `short-slide-right` | custom | — | custom | Whole phrase glides in from the left as one move; words reveal only by opacity. Layout-aware — read `showcase.rendering_contract`. | +| `short-slide-down` | custom | — | custom | Each word drops from above and pushes the stack down until centered. Layout-aware — read `showcase.rendering_contract`. | +| `depth-parallax-words` | — | — | — | Per-word depth motion with scale and vertical drift. Layered readability. | + +### Per-Line + +| ID | Enter duration | Stagger | Ease | Character | +| -------------------- | -------------- | ------- | ----------------------------- | ------------------------------------------------------------------ | +| `mask-reveal-up` | 547ms | 65ms | `cubic-bezier(0.22,1,0.36,1)` | Lines clip-reveal upward. Contained, intentional, masked feel. | +| `line-by-line-slide` | 648ms | 86ms | `cubic-bezier(0.22,1,0.36,1)` | Lines slide in from left, exit to right. Flowing paragraph rhythm. | + +### Whole Element + +| ID | Enter duration | Ease | Character | +| -------------------- | -------------- | ----------------------------- | ------------------------------------------------------------------------------ | +| `micro-scale-fade` | 432ms | `cubic-bezier(0.32,0.72,0,1)` | Tiny scale pop and fade. Barely perceptible — premium polish. | +| `shimmer-sweep` | 612ms | `cubic-bezier(0.22,1,0.36,1)` | Subtle horizontal sweep glides from left to center. | +| `fade-through` | 302ms | `cubic-bezier(0.2,0,0,1)` | Old content fades out, new fades in with a soft delay. Material-style swap. | +| `shared-axis-z` | 374ms | `cubic-bezier(0.2,0,0,1)` | Scale-based depth transition. One context fades out small, next fades in full. | +| `scale-down-fade` | 374ms | `cubic-bezier(0.22,1,0.36,1)` | Content settles with a slight scale-down on exit. Restrained, premium. | +| `focus-blur-resolve` | 547ms | `cubic-bezier(0.22,1,0.36,1)` | Heavy blur resolves to sharp clarity on enter, returns to soft blur on exit. | +| `shared-axis-x` | — | — | Horizontal sibling transition for sequential destinations. | + +--- + +## Implementation in GSAP + +**Step 1:** Read the effect JSON — `skills/hyperframes/assets/text-effects/effects/.json` + +**Step 2:** In `showcase.library_adapters.gsap` find: + +- `import_statement` — which GSAP plugins to register (`CustomEase` is almost always needed) +- `easing_conversion` — exact easing string or `CustomEase.create()` call +- `start_animation` pattern — how to initialize the tween + +**Step 3:** Get timing from `showcase.timing.enter`: + +- `scaled_duration_ms` → convert to seconds for GSAP (`/ 1000`) +- `scaled_stagger_ms` → stagger value in seconds +- `easing` → register as CustomEase + +**Step 4:** Split text yourself. Span-wrap each character/word/line before the timeline starts. Apply `gsap.set()` to set initial state, then `tl.to()` for the enter animation with stagger. + +**For layout-aware effects** (`kinetic-center-build`, `short-slide-right`, `short-slide-down`): read `showcase.rendering_contract` and `showcase.renderer` in the effect JSON. These have custom layout algorithms that manage DOM position — not just stagger timing. + +**Register CustomEase before using cubic-bezier strings:** + +```js +gsap.registerPlugin(CustomEase); +const ease = CustomEase.create("custom", "cubic-bezier(0.22, 1, 0.36, 1)"); +``` + +--- + +## In the Storyboard + +Every text element in every beat must name an effect by ID. Not "headline fades in" — read the catalog, pick what fits the brand/mood/beat, and name the specific effect. + +Format (these are format placeholders — the effect you choose should fit this specific brand and beat, not default to any particular ID): + +```markdown +**Text Animations:** + +- [element, e.g. "main headline"]: `[effect-id]` — skills/hyperframes/assets/text-effects/effects/[id].json +- [element, e.g. "eyebrow label"]: `[effect-id]` — skills/hyperframes/assets/text-effects/effects/[id].json +- [element, e.g. "body copy 3 lines"]: `[effect-id]` — skills/hyperframes/assets/text-effects/effects/[id].json +``` + +Sub-agents read the named JSON file and implement from `showcase.library_adapters.gsap` — no creative invention needed. diff --git a/skills/hyperframes/references/transitions.md b/skills/hyperframes/references/transitions.md index 3b5404bba..9e3d65b08 100644 --- a/skills/hyperframes/references/transitions.md +++ b/skills/hyperframes/references/transitions.md @@ -11,50 +11,61 @@ These are non-negotiable for every multi-scene composition: 3. **Exit animations are BANNED** except on the final scene. Do NOT use `gsap.to()` to animate elements out before a transition fires. The transition IS the exit. Outgoing scene content must be fully visible when the transition starts — the transition handles the visual handoff. 4. **Final scene exception:** The last scene MAY fade elements out (e.g., fade to black at the end of the composition). This is the only scene where exit animations are allowed. -## Energy → Primary Transition +## Energy → Transition Character -| Energy | CSS Primary | Shader Primary | Accent | Duration | Easing | -| ---------------------------------------- | ---------------------------- | ------------------------------------ | ------------------------------ | --------- | ---------------------- | -| **Calm** (wellness, brand story, luxury) | Blur crossfade, focus pull | Cross-warp morph, thermal distortion | Light leak, circle iris | 0.5-0.8s | `sine.inOut`, `power1` | -| **Medium** (corporate, SaaS, explainer) | Push slide, staggered blocks | Whip pan, cinematic zoom | Squeeze, vertical push | 0.3-0.5s | `power2`, `power3` | -| **High** (promos, sports, music, launch) | Zoom through, overexposure | Ridged burn, glitch, chromatic split | Staggered blocks, gravity drop | 0.15-0.3s | `power4`, `expo` | +The energy of a beat tells you what motion character the transition should have — not which specific transition to use. The motion character is a quality you derive from the brand and content, then find a transition that has that quality. -Pick ONE primary (60-70% of scene changes) + 1-2 accents. Never use a different transition for every scene. +**Soft/organic character:** transitions that breathe, dissolve, or drift. Nothing sharp, mechanical, or percussive. Duration 0.5–0.8s, smooth easing curves. -## Mood → Transition Type +**Directional/purposeful character:** transitions that move content decisively. Clear direction, readable momentum. Duration 0.3–0.5s, clean deceleration. -Think about what the transition _communicates_, not just what it looks like. +**Percussive/instant character:** transitions that hit like a cut. Immediate, almost hard-cut energy. Duration 0.15–0.3s, aggressive or near-instant easing. -| Mood | Transitions | Why it works | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | -| **Warm / inviting** | Light leak, blur crossfade, focus pull, film burn · **Shader:** thermal distortion, light leak, cross-warp morph | Soft edges, warm color washes. Nothing sharp or mechanical. | -| **Cold / clinical** | Squeeze, zoom out, blinds, shutter, grid dissolve · **Shader:** gravitational lens | Content transforms mechanically — compressed, shrunk, sliced, gridded. | -| **Editorial / magazine** | Push slide, vertical push, diagonal split, shutter · **Shader:** whip pan | Like turning a page or slicing a layout. Clean directional movement. | -| **Tech / futuristic** | Grid dissolve, staggered blocks, blinds, chromatic aberration · **Shader:** glitch, chromatic split | Grid dissolve is the core "data" transition. Shader glitch adds posterization + scan lines. | -| **Tense / edgy** | Glitch, VHS, chromatic aberration, ripple · **Shader:** ridged burn, glitch, domain warp | Instability, distortion, digital breakdown. Ridged burn adds sharp lightning-crack edges. | -| **Playful / fun** | Elastic push, 3D flip, circle iris, morph circle, clock wipe · **Shader:** ripple waves, swirl vortex | Overshoot, bounce, rotation, expansion. Swirl vortex adds organic spiral distortion. | -| **Dramatic / cinematic** | Zoom through, zoom out, gravity drop, overexposure, color dip to black · **Shader:** cinematic zoom, gravitational lens, domain warp | Scale, weight, light extremes. Shader transitions add per-pixel depth. | -| **Premium / luxury** | Focus pull, blur crossfade, color dip to black · **Shader:** cross-warp morph, thermal distortion | Restraint. Cross-warp morph flows both scenes into each other organically. | -| **Retro / analog** | Film burn, light leak, VHS, clock wipe · **Shader:** light leak | Organic imperfection. Warm color bleeds, scan line displacement. | +These are calibration ranges, not recipes. A brand that treats its "high energy" section with restraint might use 0.4s for a moment that another brand transitions in 0.2s — both are correct for their brand. Pick ONE character that defines the video's primary transitions, then use 1–2 contrasting moments as intentional accents. See the **Mood → Motion Quality** section below to find transitions with the right character for a given mood. + +## Mood → Motion Quality + +Think about what the transition _communicates_, not what it looks like. The question is: **what motion quality serves this mood?** Then find transitions that have that quality in the catalog (`transitions/catalog.md`). + +| Mood | Motion quality that fits | Why | +| ------------------------ | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | +| **Warm / inviting** | Soft edges, dissolving, color-temperature washes — nothing sharp, mechanical, or percussive | Warmth reads as continuity and flow; hard cuts or compression feel cold | +| **Cold / clinical** | Mechanical transformation — compression, slicing, gridding, precision | The content appears to be processed or structured, reinforcing a systematic quality | +| **Editorial / magazine** | Clean directional movement — like turning a page | Feels like content is being browsed or curated, not revealed | +| **Tech / futuristic** | Data-like fragmentation, digital displacement, scan artifacts | Transition feels computational rather than physical | +| **Tense / edgy** | Instability, distortion, displacement — something slightly wrong about the image | Introduces friction where smooth transitions would release tension | +| **Playful / fun** | Overshoot, expansion, rotation — motion with personality and bounce | Transitions that feel like objects rather than effects | +| **Dramatic / cinematic** | Scale, weight, light extremes — the cut is an event, not a bridge | Every shader and every hard cut carries narrative gravity | +| **Premium / luxury** | Restraint — transitions that are barely visible, or invisible | Luxury communicates through what it withholds | +| **Retro / analog** | Organic imperfection — light bleed, scan lines, color wash | Physical film artifacts; imperfection as authenticity | + +Use this table to derive what **quality** the transition should have, then look at the specific options in `transitions/catalog.md` to find one that has that quality for this brand. The transitions listed in the catalog are all available; none are reserved for a specific mood. ## Narrative Position -| Position | Use | Why | -| -------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------- | -| **Opening** | Your most distinctive transition. Match the mood. 0.4-0.6s | Sets the visual language for the entire piece. | -| **Between related points** | Your primary transition. Consistent. 0.3s | Don't distract — the content is continuing. | -| **Topic change** | Something different from your primary. Staggered blocks, shutter, squeeze. | Signals "new section" — the viewer's brain resets. | -| **Climax / hero reveal** | Your boldest accent. Fastest or most dramatic. | This is the payoff — spend your best transition here. | -| **Wind-down** | Return to gentle. Blur crossfade, crossfade. 0.5-0.7s | Let the viewer exhale after the climax. | -| **Outro** | Slowest, simplest. Crossfade, color dip to black. 0.6-1.0s | Closure. Don't introduce new energy at the end. | +Each position in the video has a different job to do. What transition you pick for each should come from the brand's motion character (derived from visual-vocabulary.md) and the storyboard's intent — not from a rule about "climax = boldest." + +- **Opening** — establishes the motion language for the entire video. Make a deliberate choice; whatever you pick here sets the viewer's expectation for everything that follows. +- **Between related points** — should be almost invisible. The content is continuing; the transition shouldn't draw attention to itself. Consistency matters more than distinctiveness here. +- **Topic change** — needs enough contrast from your primary that it signals "something different is starting." The contrast is in motion character, not just duration. +- **Climax / hero reveal** — this is the moment the video has been building to. The transition should feel earned by what came before. "Use your boldest transition here" is a default, not a rule — the climax of a restrained editorial piece might be a hard cut. +- **Wind-down** — returns to a motion character that allows the viewer to exhale. Matches the opening in tone, not necessarily in technique. +- **Outro** — no new energy. Slowest and simplest in the video. Closure. + +## Blur and Motion Intensity + +Blur and duration should express the energy of the content, not match a lookup table. The ranges below are calibration references — starting points to adjust from based on what the brand and storyboard call for. + +Higher-energy transitions: shorter duration, less blur, no hold at peak. The motion is immediate. +Lower-energy transitions: longer duration, more blur, longer hold at peak. The motion has weight. + +Calibration ranges (not prescriptions): -## Blur Intensity by Energy +- Soft/organic: blur 20–30px, duration 0.8–1.2s, hold 0.3–0.5s +- Directional/purposeful: blur 8–15px, duration 0.4–0.6s, hold 0.1–0.2s +- Percussive/instant: blur 3–6px, duration 0.2–0.3s, no hold -| Energy | Blur | Duration | Hold at peak | -| ---------- | ------- | -------- | ------------ | -| **Calm** | 20-30px | 0.8-1.2s | 0.3-0.5s | -| **Medium** | 8-15px | 0.4-0.6s | 0.1-0.2s | -| **High** | 3-6px | 0.2-0.3s | 0s | +A brand that uses these as a formula will produce transitions that feel the same across every video. A brand-derived choice asks: what blur and duration expresses the weight this transition should have? ## Presets @@ -92,7 +103,22 @@ CSS transitions animate scene containers with opacity, transforms, clip-path, an **Both are first-class options.** Shaders are provided by the `@hyperframes/shader-transitions` package — import from the package instead of writing raw GLSL. CSS transitions are simpler to set up. Choose based on the effect you want, not based on which is easier. -When a composition uses shader transitions, ALL transitions in that composition should be shader-based (the WebGL canvas replaces DOM-based scene switching). Don't mix CSS and shader transitions in the same composition. +**Mixing is supported.** You can have some transitions use WebGL shaders and others use a CSS crossfade in the same composition. Omit the `shader` field on any `TransitionConfig` entry to get a smooth opacity crossfade instead of a WebGL effect: + +```js +var tl = HyperShader.init({ + bgColor: "#000", + accentColor: "#6366f1", + scenes: ["s1", "s2", "s3", "s4"], + transitions: [ + { time: 4.0, shader: "sdf-iris", duration: 0.7 }, // WebGL shader + { time: 8.5, duration: 0.8 }, // no shader → CSS crossfade + { time: 13.0, shader: "domain-warp", duration: 0.6 }, // WebGL shader + ], +}); +``` + +HyperShader manages all scene visibility regardless of transition type. Let it create the timeline (don't pass `timeline:` into `init()`) and add your beat animations to the returned `tl` after the call. ## Shader-Compatible CSS Rules diff --git a/skills/hyperframes/references/typography.md b/skills/hyperframes/references/typography.md index 2a2ee52c5..d97fc0b98 100644 --- a/skills/hyperframes/references/typography.md +++ b/skills/hyperframes/references/typography.md @@ -2,34 +2,38 @@ The compiler embeds supported fonts — just write `font-family` in CSS. -## Banned +## Banned fonts -Training-data defaults that every LLM reaches for. These produce monoculture across compositions. +These are the fonts every LLM reaches for. They produce monoculture across compositions even when nothing else about the compositions is similar: Inter, Roboto, Open Sans, Noto Sans, Arimo, Lato, Source Sans, PT Sans, Nunito, Poppins, Outfit, Sora, Playfair Display, Cormorant Garamond, Bodoni Moda, EB Garamond, Cinzel, Prata, Syne -**Syne in particular** is the most overused "distinctive" display font. It is an instant AI design tell. +**Syne in particular** is the most overused "distinctive" display font. When you see it, it reads as AI-generated before anything else about the composition registers. -## Guardrails +If a brand's actual identity uses one of these fonts, that's a different situation — use the brand's font. The ban applies to the default-reach, not to the cases where the font is genuinely the right choice. -You know these rules but you violate them. Stop. +## Defaults to watch for -- **Don't pair two sans-serifs.** You do this constantly — one for headlines, one for body. Cross the boundary: serif + sans, or sans + mono. -- **One expressive font per scene.** You pick two interesting fonts trying to make it "better." One performs, one recedes. -- **Weight contrast must be extreme.** You default to 400 vs 700. Video needs 300 vs 900. The difference must be visible in motion at a glance. -- **Video sizes, not web sizes.** Body: 20px minimum. Headlines: 60px+. Data labels: 16px. You will try to use 14px. Don't. +These are the patterns that produce same-looking compositions even when the brands are different: -## What You Don't Do Without Being Told +- **Two sans-serifs paired together** — one for headlines, one for body. The pairing has no tension because both fonts come from the same family of forms. Cross the boundary: serif + sans, or sans + mono. The pairing should embody some contradiction in the content. +- **Two expressive fonts in one scene** — one will perform and the other will recede no matter what you do. Pick which font is doing the work and let the other be quiet. +- **Weight contrast at 400 vs 700** — fine for web, invisible at video distance. Video needs more dramatic contrast: 300 vs 900, or 200 vs 800. The weight difference should be readable in motion at a glance. +- **Web type sizes** — body text at 14–16px disappears on a 1920×1080 frame. Video minimums: body 20px+, headlines 60px+, data labels 16px+. If a font-size is under 20px in a composition, there should be a specific reason. -- **Tension should mean something.** Don't pattern-match pairings. Ask WHY these two fonts disagree. The pairing should embody the content's contradiction — mechanical vs human, public vs private, institutional vs personal. If you can't articulate the tension, it's arbitrary. -- **Register switching.** Assign different fonts to different communicative modes — one voice for statements, another for data, another for attribution. Not hierarchy on a page. Voices in a conversation. -- **Tension can live inside a single font.** A font that looks familiar but is secretly strange creates tension with the viewer's expectations, not with another font. -- **One variable changed = dramatic contrast.** Same letterforms, monospaced vs proportional. Same family at different optical sizes. Changing only rhythm while everything else stays constant. -- **Double personality works.** Two expressive fonts can coexist if they share an attitude (both irreverent, both precise) even when their forms are completely different. -- **Time is hierarchy.** The first element to appear is the most important. In video, sequence replaces position. -- **Motion is typography.** How a word enters carries as much meaning as the font. A 0.1s slam vs a 2s fade — same font, completely different message. -- **Fixed reading time.** 3 seconds on screen = must be readable in 2. Fewer words, larger type. -- **Tracking tighter than web.** -0.03em to -0.05em on display sizes. Video encoding compresses letter detail. +## Principles + +What follows isn't a checklist — these are principles for thinking about type in video specifically: + +- **Tension should mean something.** When two fonts are paired, the pairing should embody some contradiction the content itself contains — mechanical vs human, public vs private, institutional vs personal. If you can't articulate the tension, the pairing is arbitrary and reads as such. +- **Register switching.** Different fonts can carry different communicative modes — one voice for statements, another for data, another for attribution. This isn't hierarchy on a page; it's voices in a conversation. Each font is saying something different _about_ the content it carries. +- **Tension can live inside a single font.** A font that looks familiar but is secretly strange creates tension with the viewer's expectations — no second font required. Some of the strongest type-driven compositions use one font confidently. +- **One variable changed = dramatic contrast.** Same letterforms, monospaced vs proportional. Same family at different optical sizes. Changing only rhythm while everything else stays constant produces contrast without complexity. +- **Double personality works when fonts share attitude.** Two expressive fonts can coexist if they share an underlying attitude (both irreverent, both precise, both eccentric) even when their visible forms are completely different. +- **Time is hierarchy.** The first element to appear is the most important. In video, sequence replaces position — what would be top-of-page on the web is first-to-enter on screen. +- **Motion is typography.** How a word enters carries as much meaning as the font it's set in. A 0.1s slam and a 2s fade say completely different things with the same letters. +- **Fixed reading time.** Three seconds on screen means the line has to be readable in two. That forces fewer words and larger type than web type-setting habits assume. +- **Tracking tighter than web.** Display sizes in video want `-0.03em` to `-0.05em` of letter-spacing. Video encoding compresses fine letter detail, and tighter tracking compensates. ## Finding Fonts diff --git a/skills/hyperframes/references/video-composition.md b/skills/hyperframes/references/video-composition.md index e0758eba0..c7dfa9ced 100644 --- a/skills/hyperframes/references/video-composition.md +++ b/skills/hyperframes/references/video-composition.md @@ -12,7 +12,7 @@ design.md defines what the brand looks like: colors, fonts, personality, constra ## Density -A beat with 3 elements looks empty. A beat with 8-10 feels alive. +Density depends on the beat's purpose. A dramatic single-word reveal or a keynote-style hero moment can have 1-2 elements — that's intentional. A data showcase or feature grid should have 8-10. Match the storyboard's intent, not a number target. Every scene needs: @@ -20,7 +20,9 @@ Every scene needs: - **Midground content** — the actual message. Cards, stats, code blocks, images. - **Foreground accents** — dividers, labels, data bars, registration marks, monospace metadata. The details that make it feel produced, not generated. -Aim for 8-10 visual elements per scene. Two of those should be decorative elements the user didn't ask for — you add them because empty frames look broken. +Every scene should have content at all three layers — background, midground, and foreground — even a sparse beat. A 1-element hero moment isn't really 1 element if the background has a radial glow and the foreground has a corner label; it's a 1-element _focal point_ with two layers around it doing quiet work. That's different from a frame with one element and nothing else, which reads as unfinished. + +Total element count follows from the storyboard's density spec for that beat. The thing to check for is whether all three layers are present, not whether the count hits a target. ## Color Presence diff --git a/skills/hyperframes/visual-styles.md b/skills/hyperframes/visual-styles.md index 55f618815..fc1c75127 100644 --- a/skills/hyperframes/visual-styles.md +++ b/skills/hyperframes/visual-styles.md @@ -1,443 +1,143 @@ # Visual Style Library -Named visual identities for HyperFrames videos. Each style is grounded in a real graphic design tradition and expressed as a DESIGN.md-compatible token block. Use them as starters — copy the YAML into your project's `design.md` front matter, then customize. +A collection of design traditions that have produced influential motion and editorial work. **Each entry is a lens, not a recipe.** Read them to expand your vocabulary for what visual identities are possible — not to paste a token block into DESIGN.md and call it done. -**How to pick:** Match mood first, content second. Ask: _"What should the viewer FEEL?"_ +## Why no YAML -**How to use:** Copy the style's YAML token block into `design.md` front matter. Add `## Overview`, `## Colors`, `## Typography`, `## Elevation`, `## Components`, `## Do's and Don'ts` prose sections to complete the file. +Earlier versions of this file shipped each tradition as a complete YAML token block — colors, typography, motion easings, transition picks. Agents pasted them wholesale and produced one "premium" video that looked like every other "premium" video. The token block was the problem: it made the tradition feel like a stamp rather than a perspective. -## Quick Reference +The values in any real DESIGN.md should come from the captured brand — its actual colors, its actual typography, its actual rhythm — not from a stranger's recipe. These traditions can inform _how you think_ about that brand. They should not supply _what you write_. -| Style | Mood | Best for | Transition shader | -| --------------- | --------------------- | ---------------------------------- | --------------------------------- | -| Swiss Pulse | Clinical, precise | SaaS, data, dev tools, metrics | Cinematic Zoom or SDF Iris | -| Velvet Standard | Premium, timeless | Luxury, enterprise, keynotes | Cross-Warp Morph | -| Deconstructed | Industrial, raw | Tech launches, security, punk | Glitch or Whip Pan | -| Maximalist Type | Loud, kinetic | Big announcements, launches | Ridged Burn | -| Data Drift | Futuristic, immersive | AI, ML, cutting-edge tech | Gravitational Lens or Domain Warp | -| Soft Signal | Intimate, warm | Wellness, personal stories, brand | Thermal Distortion | -| Folk Frequency | Cultural, vivid | Consumer apps, food, communities | Swirl Vortex or Ripple Waves | -| Shadow Cut | Dark, cinematic | Dramatic reveals, security, exposé | Domain Warp | +## How to use this file ---- +1. **Read DESIGN.md first.** What does this brand's visual identity actually look like? Where is the energy? What restraint does it show? What does it amplify? +2. **Then scan the traditions below.** Do any of them resonate with what the brand is already doing? "This brand has Swiss-tradition discipline" or "this brand has Sagmeister warmth" is a useful observation. It means: I have a precedent for thinking about how this brand could move. +3. **Translate the lesson, not the values.** If Swiss tradition resonates, the lesson is _grid discipline and content-as-structure_. The values — colors, type sizes, exact easings — still come from this brand, not from Müller-Brockmann. -## 1. Swiss Pulse — Josef Müller-Brockmann - -**Mood:** Clinical, precise | **Best for:** SaaS dashboards, developer tools, APIs, metrics - -```yaml -name: Swiss Pulse -colors: - primary: "#1a1a1a" - on-primary: "#ffffff" - accent: "#0066FF" -typography: - headline: - fontFamily: Helvetica Neue - fontSize: 5rem - fontWeight: 700 - label: - fontFamily: Inter - fontSize: 0.875rem - fontWeight: 400 - stat: - fontFamily: Helvetica Neue - fontSize: 7rem - fontWeight: 700 -rounded: - none: 0px - sm: 2px -spacing: - sm: 8px - md: 16px - lg: 32px -motion: - energy: high - easing: - entry: "expo.out" - exit: "power4.in" - ambient: "none" - duration: - entrance: 0.4 - hold: 1.5 - transition: 0.6 - atmosphere: - - grid-lines - - registration-marks - transition: cinematic-zoom -``` - -Grid-locked compositions. Every element snaps to an invisible 12-column grid. Numbers dominate the frame at 80–120px. Animated counters count up from 0. Hard cuts, no decorative transitions. Nothing floats. +If none of the eight traditions below resonate with the brand you're working on, that's fine and common. There are more design traditions than fit on one page; this is a starting vocabulary, not a closed set. --- -## 2. Velvet Standard — Massimo Vignelli - -**Mood:** Premium, timeless | **Best for:** Luxury products, enterprise software, keynotes, investor decks - -```yaml -name: Velvet Standard -colors: - primary: "#0a0a0a" - on-primary: "#ffffff" - accent: "#1a237e" -typography: - headline: - fontFamily: Inter - fontSize: 3rem - fontWeight: 300 - letterSpacing: 0.15em - textTransform: uppercase - body: - fontFamily: Inter - fontSize: 1rem - fontWeight: 300 - lineHeight: 1.6 -rounded: - sm: 0px - md: 2px -spacing: - sm: 16px - md: 32px - lg: 64px -motion: - energy: calm - easing: - entry: "sine.inOut" - exit: "power1.in" - ambient: "sine.inOut" - duration: - entrance: 1.2 - hold: 3.0 - transition: 1.5 - atmosphere: - - subtle-grain - - hairline-rules - transition: cross-warp-morph -``` - -Generous negative space. Symmetrical, centered, architectural precision. Thin sans-serif, ALL CAPS, wide letter-spacing. Sequential reveals with long holds. Nothing snaps — everything glides with intention. Luxury takes its time. +## 1. Swiss / International Typographic — Josef Müller-Brockmann, Emil Ruder, Armin Hofmann + +**What it teaches:** Content is structure. The grid is not decoration — it is the design. Type carries the work; ornament weakens it. Information should be findable, not announced. + +**Where it resonates:** Brands that already show grid discipline. Sites where typography does the heavy lifting. Dashboards, dev tools, B2B platforms with monospace or tightly-tracked sans-serif. Brands whose homepage uses one bold weight and no decorative elements. + +**Pitfalls when borrowing it:** Don't import "minimalism" if the brand actually has personality. Brockmann is restrained because the work demands restraint, not because restraint is universally premium. A brand with playful copy and warm color paired with a Swiss-grid motion treatment reads as cold and confused. + +**Tags:** `#grid #editorial-discipline #content-as-structure #high-contrast #restrained-motion` --- -## 3. Deconstructed — Neville Brody - -**Mood:** Industrial, raw | **Best for:** Tech news, developer launches, security products, punk-energy reveals - -```yaml -name: Deconstructed -colors: - primary: "#1a1a1a" - on-primary: "#f0f0f0" - accent: "#D4501E" -typography: - headline: - fontFamily: Space Grotesk - fontSize: 4rem - fontWeight: 700 - label: - fontFamily: Space Mono - fontSize: 0.75rem - fontWeight: 700 - textTransform: uppercase -rounded: - none: 0px -spacing: - sm: 4px - md: 12px - lg: 24px -motion: - energy: high - easing: - entry: "back.out(2.5)" - exit: "steps(8)" - ambient: "elastic.out(1.2, 0.4)" - duration: - entrance: 0.3 - hold: 1.0 - transition: 0.5 - atmosphere: - - scan-lines - - glitch-artifacts - - grain-overlay - transition: glitch -``` - -Type at angles, overlapping edges, escaping frames. Bold industrial weight. Gritty textures: scan-line effects, glitch artifacts baked into design. Text SLAMS and SHATTERS. Letters scramble then snap to final position. Intentional irregularity — nothing should feel polished. +## 2. Late-Modernist Editorial — Massimo Vignelli, Unimark + +**What it teaches:** Six typefaces are enough for a lifetime. Whitespace is a design element, not an absence of one. Hierarchy is achieved through scale and spacing, not through ornament or color. + +**Where it resonates:** Brands with confident reductive identities — heavy spacing, single-weight wordmarks, architectural composition. Enterprise software with mature design systems. Brands that don't try to seem premium because they already are. + +**Pitfalls when borrowing it:** Vignelli's restraint comes from rigorous editing, not from "use less stuff." Pulling out elements until a frame is sparse, without thinking about what's left, produces emptiness rather than restraint. The remaining elements have to earn the space they occupy. + +**Tags:** `#architectural-spacing #typographic-confidence #unornamented #slow-deliberate #single-weight-identity` --- -## 4. Maximalist Type — Paula Scher - -**Mood:** Loud, kinetic | **Best for:** Big product launches, milestone announcements, high-energy hype videos - -```yaml -name: Maximalist Type -colors: - primary: "#0a0a0a" - on-primary: "#ffffff" - accent-red: "#E63946" - accent-yellow: "#FFD60A" -typography: - headline: - fontFamily: Anton - fontSize: 8rem - fontWeight: 400 - textTransform: uppercase - subhead: - fontFamily: Space Grotesk - fontSize: 3rem - fontWeight: 700 -rounded: - none: 0px -spacing: - sm: 0px - md: 8px -motion: - energy: high - easing: - entry: "expo.out" - exit: "back.out(1.8)" - ambient: "power3.out" - duration: - entrance: 0.3 - hold: 0.8 - transition: 0.4 - atmosphere: - - type-layers - - color-blocks - transition: ridged-burn -``` - -Text IS the visual. Overlapping type layers at different scales and angles, filling 50–80% of frame. Bold saturated colors — maximum contrast. Everything kinetic: slamming, sliding, scaling. 2–3 second rapid-fire scenes. No static moments. Fast arrivals, hard stops. +## 3. Punk / Post-Modern Print — Neville Brody, David Carson, The Face, Ray Gun + +**What it teaches:** Rules can be broken when the breaking is the point. Type can escape its baseline. Imperfection, overlap, and intentional irregularity have a place in mature design. Texture and grain are content, not noise. + +**Where it resonates:** Brands with raw edges in their actual identity — dev tools with terminal aesthetics that lean into the rough texture, music platforms, brands that intentionally feel handmade. Sites where the typography is doing something specifically _off_ — a heading at a slight angle, a logo that's deliberately not perfectly aligned. + +**Pitfalls when borrowing it:** This is the tradition agents lean toward when they want a video to "feel edgy" for a brand that isn't actually edgy. Adding glitch, scan lines, and overlapping type to a clean fintech brand makes it look like the agent didn't read the brief. The brand has to already contain the irregularity for this lens to amplify what's there. + +**Tags:** `#irregular #broken-grid #texture-as-content #post-rules #raw-edges` --- -## 5. Data Drift — Refik Anadol - -**Mood:** Futuristic, immersive | **Best for:** AI products, ML platforms, data companies, speculative tech - -```yaml -name: Data Drift -colors: - primary: "#0a0a0a" - on-primary: "#e0e0e0" - accent-purple: "#7c3aed" - accent-cyan: "#06b6d4" -typography: - headline: - fontFamily: Inter - fontSize: 2.5rem - fontWeight: 200 - letterSpacing: 0.05em - body: - fontFamily: Inter - fontSize: 0.875rem - fontWeight: 300 -rounded: - sm: 4px - md: 12px - full: 9999px -spacing: - sm: 16px - md: 32px - lg: 64px -motion: - energy: moderate - easing: - entry: "sine.inOut" - exit: "power2.out" - ambient: "sine.inOut" - duration: - entrance: 1.0 - hold: 2.5 - transition: 1.5 - atmosphere: - - particle-field - - light-traces - - radial-glow - transition: gravitational-lens -``` - -Thin futuristic sans-serif — floating, weightless, minimal. Fluid morphing compositions. Extreme scale shifts (micro → macro). Particles coalesce into numbers. Light traces data paths through the frame. Smooth, continuous, organic. Nothing hard. +## 4. American Maximalist — Paula Scher, Pentagram, Herb Lubalin + +**What it teaches:** Type can be the image. Scale is communication. Filling the frame with one massive word can say more than a layout of small ones. Color saturation is a tool, not a tell. + +**Where it resonates:** Brands with confident loud identities — bold wordmarks that dominate their own homepage, hero headlines at 120px+, primary colors used at full saturation. Consumer brands that are unapologetic. Public-facing organizations that need to be readable from the back of a room. + +**Pitfalls when borrowing it:** Scher's maximalism is calibrated — every loud element is chosen and the quiet space around it is what makes it land. Loud-on-loud everywhere is noise, not maximalism. If a brand's homepage already uses careful negative space, applying this lens flattens what makes the brand work. + +**Tags:** `#type-as-image #massive-scale #saturated #unapologetic #public-facing` --- -## 6. Soft Signal — Stefan Sagmeister - -**Mood:** Intimate, warm | **Best for:** Wellness brands, personal stories, lifestyle products, human-centered apps - -```yaml -name: Soft Signal -colors: - primary: "#FFF8EC" - on-primary: "#2a2a2a" - accent-amber: "#F5A623" - accent-rose: "#C4A3A3" - accent-sage: "#8FAF8C" -typography: - headline: - fontFamily: Playfair Display - fontSize: 3rem - fontWeight: 400 - fontStyle: italic - body: - fontFamily: Inter - fontSize: 1rem - fontWeight: 300 - lineHeight: 1.7 -rounded: - sm: 8px - md: 16px - lg: 24px - full: 9999px -spacing: - sm: 12px - md: 24px - lg: 48px -motion: - energy: calm - easing: - entry: "sine.inOut" - exit: "power1.inOut" - ambient: "sine.inOut" - duration: - entrance: 1.0 - hold: 3.0 - transition: 1.5 - atmosphere: - - soft-gradient - - warm-grain - transition: thermal-distortion -``` - -Handwritten-style or humanist serif fonts. Personal, lowercase, delicate. Close-up framing: single element fills the frame. Slow drifts and floats, never snaps. Soft organic motion. Nothing should feel hurried or polished. Intimate, never corporate. +## 5. Computational / Generative — Refik Anadol, Casey Reas, Joshua Davis + +**What it teaches:** Form can emerge from process. Particle systems, fluid simulations, and noise fields are not chrome — they are the work. Continuous motion can carry an entire piece without a single hard cut. + +**Where it resonates:** Brands where data, AI, or computation is the actual product. Sites that already use WebGL, shaders, or canvas effects in their hero section. Brands whose color palette is gradient-native rather than flat. Companies whose identity _is_ the simulation of something. + +**Pitfalls when borrowing it:** The "AI brand" trap is real — every AI startup gets a particle-field treatment regardless of whether their actual identity is computational. If the captured site is flat, structured, and information-dense, applying generative motion is a contradiction. Particles for the sake of "feeling AI" are decoration the brand didn't ask for. + +**Tags:** `#generative #continuous-motion #simulation-as-form #gradient-native #fluid-composition` --- -## 7. Folk Frequency — Eduardo Terrazas - -**Mood:** Cultural, vivid | **Best for:** Consumer apps, food platforms, community products, festive launches - -```yaml -name: Folk Frequency -colors: - primary: "#ffffff" - on-primary: "#1a1a1a" - accent-pink: "#FF1493" - accent-blue: "#0047AB" - accent-yellow: "#FFE000" - accent-green: "#009B77" -typography: - headline: - fontFamily: Fredoka One - fontSize: 4rem - fontWeight: 400 - body: - fontFamily: Nunito - fontSize: 1rem - fontWeight: 600 -rounded: - sm: 8px - md: 16px - lg: 32px - full: 9999px -spacing: - sm: 8px - md: 16px - lg: 32px -motion: - energy: high - easing: - entry: "back.out(1.6)" - exit: "elastic.out(1, 0.5)" - ambient: "sine.inOut" - duration: - entrance: 0.5 - hold: 1.5 - transition: 0.8 - atmosphere: - - pattern-tiles - - confetti-burst - - color-blocks - transition: swirl-vortex -``` - -Bold warm rounded type. Pattern and repetition — folk art rhythm and density. Layered compositions with rich visual texture. Every frame feels handcrafted. Colorful motion: elements bounce, pop, spin into place with joy. Overshoots feel intentional. Celebratory energy. +## 6. Humanist / Personal — Stefan Sagmeister, Marian Bantjes, Louise Fili + +**What it teaches:** Design can feel made by a person. Handwriting, hand-set serifs, and visible imperfection can communicate intimacy where a system cannot. Close framing — one element filling the frame — invites the viewer in. + +**Where it resonates:** Wellness brands, lifestyle products, food, personal services, anything where warmth is the actual proposition. Brands using humanist serifs, organic illustrations, or photography that emphasizes hands and texture. Sites with first-person copy. + +**Pitfalls when borrowing it:** This lens softens, which can dilute a brand that wants to feel sharp. Don't import warmth into a brand whose identity is precision. And don't conflate "humanist" with "lowercase serif everywhere" — Sagmeister's work is conceptual first, typographic second. + +**Tags:** `#humanist #warm #close-framing #personal-voice #handmade-feel` --- -## 8. Shadow Cut — Hans Hillmann - -**Mood:** Dark, cinematic | **Best for:** Security products, dramatic reveals, investigative content, intense launches - -```yaml -name: Shadow Cut -colors: - primary: "#0a0a0a" - on-primary: "#f0f0f0" - surface: "#3a3a3a" - accent: "#C1121F" -typography: - headline: - fontFamily: Oswald - fontSize: 4rem - fontWeight: 700 - textTransform: uppercase - body: - fontFamily: Inter - fontSize: 0.875rem - fontWeight: 400 -rounded: - none: 0px - sm: 2px -spacing: - sm: 8px - md: 16px - lg: 48px -motion: - energy: moderate - easing: - entry: "power3.out" - exit: "power4.in" - ambient: "sine.inOut" - duration: - entrance: 0.8 - hold: 2.5 - transition: 1.2 - atmosphere: - - deep-shadow - - vignette - - grain-overlay - transition: domain-warp -``` - -Near-monochrome: deep blacks, cold greys, stark white + one blood accent. Sharp angular text like film noir title cards. Heavy contrast, no softness. Elements emerge from darkness — reveal is the narrative. Slow creeping push-ins, dramatic scale reveals. The pause before the hit matters. Domain Warp dissolves reality before the next scene. +## 7. Cultural / Vernacular — Eduardo Terrazas, Yusaku Kamekura, Saul Bass + +**What it teaches:** Pattern is rhythm. Repetition with variation can carry visual interest where a single hero element cannot. Folk and vernacular sources have a richness that "designed" work often lacks. Color combinations from non-Western design traditions read as vivid without reading as childish. + +**Where it resonates:** Consumer apps with cultural specificity, food platforms, community products, festive launches. Brands whose color palette uses multiple saturated primaries rather than one accent. Sites with pattern-as-background rather than gradient-as-background. + +**Pitfalls when borrowing it:** Vernacular sources are specific to cultures and contexts. Importing Terrazas-style patterns for a generic SaaS launch is appropriation-via-aesthetics. The lens applies when the brand has actual cultural specificity to amplify, not when an agent wants the video to "feel fun." + +**Tags:** `#pattern-rhythm #saturated-multi-color #cultural-specificity #celebratory #repetition-with-variation` --- -## Mood → Style Guide +## 8. Cinematic / Title Sequence — Saul Bass, Pablo Ferro, Kyle Cooper + +**What it teaches:** Type and image can be choreographed like film. Reveal is narrative. Darkness is a material — what emerges from it carries weight. Silence between cuts can be as loud as the cuts themselves. -| If the content feels... | Use... | -| ---------------------------------- | --------------- | -| Data-driven, analytical, technical | Swiss Pulse | -| Premium, enterprise, luxury | Velvet Standard | -| Raw, punk, aggressive, rebellious | Deconstructed | -| Hype, loud, high-energy launch | Maximalist Type | -| AI, ML, speculative, futuristic | Data Drift | -| Human, warm, personal, wellness | Soft Signal | -| Cultural, fun, consumer, festive | Folk Frequency | -| Dark, dramatic, intense, cinematic | Shadow Cut | +**Where it resonates:** Dramatic reveals, security and finance products where stakes matter, launch teasers that need a build. Brands with deep neutrals and a single restrained accent. Sites with reduced color palettes and high contrast between hero and background. + +**Pitfalls when borrowing it:** "Cinematic" is the most overused word in this whole pipeline. Agents reach for darkness + slow reveal + bass impact as if those three ingredients automatically produce cinema. Cinema requires _structure_ — the buildup has to earn the payoff. A 15-second video with three "cinematic reveals" has zero cinematic moments; it has three hero shots competing for attention. + +**Tags:** `#reveal-as-narrative #emerging-from-dark #structured-buildup #restrained-accent #weight-of-pause` --- -## Creating Custom Styles +## Choosing a lens + +There's no table here. The earlier version had a "if your brand suggests X, use style Y" mapping that produced exactly the prescriptive output this file is now trying to avoid. + +The work is: + +1. **Describe the brand in your own words first.** Three to five sentences from the captured DESIGN.md. What does it actually look like? Where is the personality? What's it confident about? +2. **Then ask:** does any tradition above resonate with what I just described? If yes, name it and say _what specifically_ resonates — not the whole tradition, the specific lesson. +3. **If nothing resonates,** that's a real outcome. Plenty of brands sit between or outside these eight. Note that, and proceed from the brand alone. + +Examples of good resonance notes for a storyboard: + +- "This brand's homepage shows Swiss-tradition grid discipline in its type hierarchy. The video should honor that — no decorative motion on text, content drives the structure, no ornament. _Lesson borrowed:_ grid as design. _Values stay from this brand:_ the actual colors, the actual font, the actual cadence." +- "There's some Sagmeister warmth here in the photography and lowercase serif. The video should lean into close framing and unhurried motion. _Lesson borrowed:_ close framing as intimacy. _Values stay from this brand:_ the actual warm-neutral palette, the actual serif weight." +- "Nothing in the catalog resonates. This brand is its own thing — saturated multi-color, dense feature grid, casual second-person copy. Skip the catalog and design from the brand directly." + +The third example is the most important. If you find yourself forcing a tradition to "fit" because the catalog exists, that's the catalog using you, not you using the catalog. + +--- -These 8 styles are starters — not constraints. Create your own: +## What this file is not -1. **Name it** after a designer, art movement, or cultural reference -2. **Write YAML tokens** — `colors` (2–5 tokens), `typography` (2–3 scales), `rounded`, `spacing`, `motion` (energy + easing + duration + atmosphere + transition) -3. **Add prose** — one paragraph describing the feel, what to do, what to avoid -4. **Token references** — use `{colors.accent}`, `{typography.headline}` in component definitions +- **Not a style picker.** There is no "for SaaS use Swiss Pulse, for fintech use Velvet Standard" mapping anywhere in this file by design. +- **Not a token source.** Don't extract colors, typography, or motion values from the descriptions above. Those values belong to specific historical works — they're not yours to reuse, and the brand you're working on has its own. If you need starting palette inspiration when no DESIGN.md exists, see the `palettes/` directory — each file has named palettes for a color category, but treat them as starting points to derive from, not as finishes. +- **Not exhaustive.** Eight entries is a starter vocabulary. Vernacular design from Mexico, Japan, India, West Africa, the Middle East, and many other traditions have produced influential motion work and aren't covered here. If you know one and it fits, use it and name it. -The pattern: **YAML tokens (what) → prose rationale (why) → components (how they combine).** +The job is to make a video that's _of this brand_, informed by your knowledge of design history. Not a video that's _of a tradition_, with this brand's logo inserted. From 6c575899802f0b65c155d7bc9c8e025706d3b2a3 Mon Sep 17 00:00:00 2001 From: ukimsanov Date: Wed, 20 May 2026 14:27:12 -0700 Subject: [PATCH 2/6] fix(skill): audit-found bugs in hyperframes core examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three concrete bugs found while auditing PR #991: 1. html-in-canvas-patterns.md (#1 in catalog, 3D Rotation with Bloom): The code example used `new THREE.EffectComposer(renderer)` UMD-style namespace access while the ESM imports right below pull them in as bare named imports. Three.js r150+ removed the UMD `examples/js/` globals, so as written the example throws `TypeError: THREE.EffectComposer is not a constructor`. Switched to the bare names matching the imports. THREE.Vector2 stays as-is — Vector2 is on the THREE namespace. 2. techniques.md (#5, Lottie Animation): The CDN path `@lottiefiles/dotlottie-web/dist/dotlottie-player.js` returns 404. `@lottiefiles/dotlottie-web` is the JavaScript SDK, not a web component — its `main` is `dist/index.cjs`. The web-component package is `@lottiefiles/dotlottie-wc` and the custom element is ``, not ``. Updated both. 3. techniques.md (5 occurrences across Lottie / lottie-web / Video / @font-face examples): asset paths used the `../capture/` pattern that PR #989's `invalid_capture_path` lint rule emits an error for. Replaced all with root-relative `capture/...`. PRs #989 and #991 are no longer self-contradictory. --- .../references/html-in-canvas-patterns.md | 11 ++++++---- skills/hyperframes/references/techniques.md | 22 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/skills/hyperframes/references/html-in-canvas-patterns.md b/skills/hyperframes/references/html-in-canvas-patterns.md index bd6fd8f79..f826b1f92 100644 --- a/skills/hyperframes/references/html-in-canvas-patterns.md +++ b/skills/hyperframes/references/html-in-canvas-patterns.md @@ -98,10 +98,13 @@ var mesh = new THREE.Mesh( ); scene3d.add(mesh); -// Post-processing: bloom for cinematic glow -var composer = new THREE.EffectComposer(renderer); -composer.addPass(new THREE.RenderPass(scene3d, camera)); -composer.addPass(new THREE.UnrealBloomPass(new THREE.Vector2(1920, 1080), 0.3, 0.4, 0.85)); +// Post-processing: bloom for cinematic glow. +// EffectComposer / RenderPass / UnrealBloomPass are ES-module named imports +// (see the import block below) — they're NOT properties of THREE in modern +// versions. Three.js r150+ removed the UMD `examples/js/` globals. +var composer = new EffectComposer(renderer); +composer.addPass(new RenderPass(scene3d, camera)); +composer.addPass(new UnrealBloomPass(new THREE.Vector2(1920, 1080), 0.3, 0.4, 0.85)); var proxy = { rotY: -0.12, zoom: 4.2 }; tl.to( diff --git a/skills/hyperframes/references/techniques.md b/skills/hyperframes/references/techniques.md index ca7f28fb6..6226b4e28 100644 --- a/skills/hyperframes/references/techniques.md +++ b/skills/hyperframes/references/techniques.md @@ -188,16 +188,22 @@ The slide distance DECAYS per word (80→12px) — mimics a camera settling. Vector animations that play inside a composition. Use for logos, character animations, icons. ```html - -. --> + + - + -``` - -The line appears slightly after the text lands (0.15s offset). It expands, then fades while growing wider — simulating dissipating energy. - ---- - -## 16. Device Mockups (Laptop + Phone) - -CSS-only laptop and phone frames with realistic chrome, shadows, and perspective. Content scrolls inside the screen. - -```html - -
-
-
-
- -
-
-
-
-
-
- -``` - -Animate the content scrolling inside the screen with `tl.to(".site-img", { y: -SCROLL_DISTANCE, duration: 5, ease: "power1.inOut" })`. Add inspector callout annotations that appear at each scroll stop. - -For **phone mockups**: same structure but with dynamic island, status bar, home indicator, and 3D perspective via `transform: perspective(2200px) rotateY(-12deg)`. - ---- - -## 17. Aurora Gradient Backgrounds - -Multi-blob radial gradients that drift slowly — creates depth and atmosphere without Canvas/WebGL complexity. - -```html -
- - -``` - -Derive colors from DESIGN.md's brand palette. 4-5 blobs at different positions create natural color mixing. Use brand accent as one blob, complementary tones for the rest. - ---- - -## 18. Floating Particles - -CSS-only particles with absolute positioning, glow shadows, and staggered float animation. Adds premium ambient texture. - -```html -
-
-
-
- -
- - -``` - ---- - -## 19. Terminal UI with Typing - -macOS-style terminal chrome (traffic light dots) with typed commands, scaffold output lines, and cursor blink. Use for developer-focused videos, CTA sections, and product demos. - -```html -
-
- - - - Terminal — zsh -
-
-
- - - | -
-
-
Creating project...
-
index.html
-
meta.json
-
Done
-
-
-
- -``` - ---- - -## 20. Moodboard / Editorial Layout - -Print-inspired compositions with paper texture, decorative rules, pinned cards at angles, and connecting gold lines. Works for brand reels and design-forward content. - -```html -
-
-
-
- BRAND NAME -
-
-
- -
Hero Section
-
- -
- -``` - -Pin cards at slightly random angles (±2-5deg). Connect related cards with thin gold SVG lines that draw in. Use paper-like warm backgrounds (#f1ece2, #fbf8f1). - ---- - ## Easing Vocabulary GSAP offers a deep easing library. Every composition should use at least 3 different easings — using `power2.out` for everything produces flat, monotonous motion. Think of easings as tone of voice: a video that only whispers is boring; one that varies between whisper, normal, and punch is engaging. From 957aa1d903bd09cfeed8538ed9dfd8898afef53e Mon Sep 17 00:00:00 2001 From: ukimsanov Date: Wed, 20 May 2026 16:05:04 -0700 Subject: [PATCH 5/6] =?UTF-8?q?chore(skill):=20audit=20cleanup=20=E2=80=94?= =?UTF-8?q?=20stale=20visual-vocabulary=20ref=20+=2020=E2=86=9213=20techni?= =?UTF-8?q?ques=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups caught by a post-restructure audit pass: - skills/hyperframes/references/transitions.md:46 had a parenthetical "(derived from visual-vocabulary.md)" pointing at a file deleted earlier in this stack. Drop the parenthetical; the surrounding sentence reads cleanly without it. - skills/hyperframes/SKILL.md:476 still said "20 visual techniques" and listed 7 entries that the techniques.md trim removed (frosted glass, impact lines, device mockups, aurora gradients, floating particles, terminal UI, moodboard layouts). Updated to the actual 13 primitive techniques + a pointer to registry/blocks/ for the pre-built UI templates that used to be conflated with techniques. --- skills/hyperframes/SKILL.md | 2 +- skills/hyperframes/references/transitions.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/hyperframes/SKILL.md b/skills/hyperframes/SKILL.md index c06bfaa6d..2ec610cb4 100644 --- a/skills/hyperframes/SKILL.md +++ b/skills/hyperframes/SKILL.md @@ -473,7 +473,7 @@ Skip on small edits (fixing a color, adjusting one duration). Run on new composi - **[references/beat-direction.md](references/beat-direction.md)** — Beat planning: concept, mood, choreography verbs, rhythm templates, transition decisions, depth layers. **Always read for multi-scene compositions.** - **[references/typography.md](references/typography.md)** — Typography: font pairing, OpenType features, dark-background adjustments, font discovery script. **Always read** — every composition has text. - **[references/motion-principles.md](references/motion-principles.md)** — Motion design principles, image motion treatment, load-bearing GSAP rules. **Always read** — every composition has motion. -- **[references/techniques.md](references/techniques.md)** — 20 visual techniques with code patterns: SVG drawing, Canvas 2D, CSS 3D, kinetic type, Lottie, video compositing, typing, variable fonts, MotionPath, velocity transitions, audio-reactive, frosted glass, clip-path reveals, WebGL shaders, impact lines, device mockups, aurora gradients, floating particles, terminal UI, moodboard layouts. Adapt the patterns — don't copy-paste. +- **[references/techniques.md](references/techniques.md)** — 13 primitive animation techniques with code patterns: SVG drawing, Canvas 2D, CSS 3D, kinetic type, Lottie, video compositing, typing, variable fonts, MotionPath, velocity transitions, audio-reactive, clip-path reveals, WebGL shaders. Adapt the patterns — don't copy-paste. (For pre-built UI templates — terminal chrome, device mockups, moodboard layouts — see `registry/blocks/`.) - **[references/html-in-canvas-patterns.md](references/html-in-canvas-patterns.md)** — HTML-in-Canvas patterns: live DOM as GPU texture via `drawElementImage` + `layoutsubtree`. Shared boilerplate + ~6 effect recipes (iPhone/MacBook mockups, liquid glass, magnetic, portal, shatter, text cursor). Use for 1–3 hero beats per video. - **[references/narration.md](references/narration.md)** — Pacing, tone, script structure, number pronunciation, opening line patterns. Read when the composition includes voiceover or TTS. - **[references/design-picker.md](references/design-picker.md)** — Create a design.md via visual picker. Read when no design.md exists and the user wants to create one. diff --git a/skills/hyperframes/references/transitions.md b/skills/hyperframes/references/transitions.md index 9e3d65b08..3737583b3 100644 --- a/skills/hyperframes/references/transitions.md +++ b/skills/hyperframes/references/transitions.md @@ -43,7 +43,7 @@ Use this table to derive what **quality** the transition should have, then look ## Narrative Position -Each position in the video has a different job to do. What transition you pick for each should come from the brand's motion character (derived from visual-vocabulary.md) and the storyboard's intent — not from a rule about "climax = boldest." +Each position in the video has a different job to do. What transition you pick for each should come from the brand's motion character and the storyboard's intent — not from a rule about "climax = boldest." - **Opening** — establishes the motion language for the entire video. Make a deliberate choice; whatever you pick here sets the viewer's expectation for everything that follows. - **Between related points** — should be almost invisible. The content is continuing; the transition shouldn't draw attention to itself. Consistency matters more than distinctiveness here. From 8acc71bf2ee83c2e33ff7052a7096f24cbccc252 Mon Sep 17 00:00:00 2001 From: ukimsanov Date: Wed, 20 May 2026 18:13:27 -0700 Subject: [PATCH 6/6] fix(skill)!: rewrite text-effects bundle from scratch (license blocker) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes Rames's blocker on PR #991. The previous text-effects bundle was vendored from `pixel-point/animate-text`, which the GitHub API confirms has no license: GET /repos/pixel-point/animate-text → license: null GET /repos/pixel-point/animate-text/license → 404 With no explicit license, default US copyright is "all rights reserved" by the upstream author. Cannot ship under Apache 2.0. This commit replaces the 48 vendored JSON files (8451 lines) with a 24-file rewrite (525 lines) using a fresh, simpler schema. The effect IDs (typewriter, soft-blur-in, etc.) are concept names — not copyrightable. The schema (`enter`/`exit`/`swap`, `target`, `durationMs`/`staggerMs`/`easing`) is standard motion-design vocabulary. Per-effect parameter values are picked from motion-design intuition for each effect's intent — not copied. ### Structure changes Before: dual directory + per-effect schema duplication assets/text-effects/effects/.json (~330 lines each, GSAP recipe + embedded spec) assets/text-effects/specs/.json (~40 lines each, portable contract — duplicates the spec section above) After: flat single-file-per-effect with one shared rendering pattern assets/text-effects/.json (~20 lines each, just the per-effect parameters) references/text-effects.md (shared GSAP rendering pattern, documented ONCE with split rules, CustomEase wiring, layout-aware exceptions) The shared rendering pattern (split by target → set initial state → stagger to enter.to → exit symmetric to enter → hard-kill at beat boundary) is in text-effects.md instead of repeated in every effect JSON. Sub-agents read the catalog once and the per-effect JSON gives them the parameters. ~94% smaller file footprint, single canonical implementation pattern. ### Per-effect schema { id, name, description, target, enter, exit, swap, notes? } target = "char" | "word" | "line" | "element" enter / exit = { durationMs, staggerMs?, staggerOrder?, easing, from, to } swap = { mode: "crossfade" | "sequential", overlapMs, microDelayMs } notes = implementation guidance (when present) layoutAware = true for the 3 effects whose line container animates separately (kinetic-center-build, short-slide-right, short-slide-down) ### Reference updates - references/text-effects.md: rewritten with our own catalog table, fresh duration / stagger values per effect, and the shared GSAP rendering pattern as one canonical section instead of per-effect recipes. - step-3-storyboard.md: path references updated from `text-effects/effects/[id].json` → `text-effects/[id].json`. Removed the upstream-specific `showcase.library_adapters.gsap` reference; sub-agents now read the per-effect params + the shared rendering pattern in text-effects.md. 24 effects total, unchanged set: 7 per-character, 8 per-word, 2 per-line, 7 whole-element. The 3 layout-aware effects keep their custom behavior documented in their JSON `notes` field and in text-effects.md's layout-aware section. --- .../assets/text-effects/blur-out-up.json | 22 + .../text-effects/bottom-up-letters.json | 22 + .../text-effects/depth-parallax-words.json | 22 + .../text-effects/effects/blur-out-up.json | 335 ------------- .../effects/bottom-up-letters.json | 348 ------------- .../effects/depth-parallax-words.json | 53 -- .../text-effects/effects/fade-through.json | 339 ------------- .../effects/focus-blur-resolve.json | 339 ------------- .../effects/kinetic-center-build.json | 469 ------------------ .../effects/line-by-line-slide.json | 335 ------------- .../text-effects/effects/mask-reveal-up.json | 339 ------------- .../effects/micro-scale-fade.json | 331 ------------ .../effects/per-character-rise.json | 347 ------------- .../effects/per-word-crossfade.json | 348 ------------- .../text-effects/effects/scale-down-fade.json | 335 ------------- .../text-effects/effects/shared-axis-x.json | 49 -- .../text-effects/effects/shared-axis-y.json | 335 ------------- .../text-effects/effects/shared-axis-z.json | 335 ------------- .../text-effects/effects/shimmer-sweep.json | 335 ------------- .../effects/short-slide-down.json | 464 ----------------- .../effects/short-slide-right.json | 330 ------------ .../text-effects/effects/soft-blur-in.json | 351 ------------- .../text-effects/effects/spring-scale-in.json | 331 ------------ .../effects/stagger-from-center.json | 50 -- .../effects/stagger-from-edges.json | 50 -- .../effects/top-down-letters.json | 348 ------------- .../text-effects/effects/typewriter.json | 331 ------------ .../assets/text-effects/fade-through.json | 20 + .../text-effects/focus-blur-resolve.json | 20 + .../text-effects/kinetic-center-build.json | 23 + .../text-effects/line-by-line-slide.json | 22 + .../assets/text-effects/mask-reveal-up.json | 22 + .../assets/text-effects/micro-scale-fade.json | 20 + .../text-effects/per-character-rise.json | 22 + .../text-effects/per-word-crossfade.json | 22 + .../assets/text-effects/scale-down-fade.json | 20 + .../assets/text-effects/shared-axis-x.json | 20 + .../assets/text-effects/shared-axis-y.json | 22 + .../assets/text-effects/shared-axis-z.json | 20 + .../assets/text-effects/shimmer-sweep.json | 20 + .../assets/text-effects/short-slide-down.json | 23 + .../text-effects/short-slide-right.json | 27 + .../assets/text-effects/soft-blur-in.json | 22 + .../text-effects/specs/blur-out-up.json | 44 -- .../text-effects/specs/bottom-up-letters.json | 57 --- .../specs/depth-parallax-words.json | 48 -- .../text-effects/specs/fade-through.json | 48 -- .../specs/focus-blur-resolve.json | 48 -- .../specs/kinetic-center-build.json | 84 ---- .../specs/line-by-line-slide.json | 40 -- .../text-effects/specs/mask-reveal-up.json | 44 -- .../text-effects/specs/micro-scale-fade.json | 40 -- .../specs/per-character-rise.json | 56 --- .../specs/per-word-crossfade.json | 57 --- .../text-effects/specs/scale-down-fade.json | 44 -- .../text-effects/specs/shared-axis-x.json | 44 -- .../text-effects/specs/shared-axis-y.json | 44 -- .../text-effects/specs/shared-axis-z.json | 44 -- .../text-effects/specs/shimmer-sweep.json | 44 -- .../text-effects/specs/short-slide-down.json | 83 ---- .../text-effects/specs/short-slide-right.json | 68 --- .../text-effects/specs/soft-blur-in.json | 60 --- .../text-effects/specs/spring-scale-in.json | 40 -- .../specs/stagger-from-center.json | 45 -- .../specs/stagger-from-edges.json | 45 -- .../text-effects/specs/top-down-letters.json | 57 --- .../assets/text-effects/specs/typewriter.json | 40 -- .../assets/text-effects/spring-scale-in.json | 22 + .../text-effects/stagger-from-center.json | 24 + .../text-effects/stagger-from-edges.json | 24 + .../assets/text-effects/top-down-letters.json | 22 + .../assets/text-effects/typewriter.json | 22 + skills/hyperframes/references/text-effects.md | 186 ++++--- .../references/step-3-storyboard.md | 6 +- 74 files changed, 644 insertions(+), 8524 deletions(-) create mode 100644 skills/hyperframes/assets/text-effects/blur-out-up.json create mode 100644 skills/hyperframes/assets/text-effects/bottom-up-letters.json create mode 100644 skills/hyperframes/assets/text-effects/depth-parallax-words.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/blur-out-up.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/bottom-up-letters.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/depth-parallax-words.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/fade-through.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/focus-blur-resolve.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/kinetic-center-build.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/line-by-line-slide.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/mask-reveal-up.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/micro-scale-fade.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/per-character-rise.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/per-word-crossfade.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/scale-down-fade.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/shared-axis-x.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/shared-axis-y.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/shared-axis-z.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/shimmer-sweep.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/short-slide-down.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/short-slide-right.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/soft-blur-in.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/spring-scale-in.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/stagger-from-center.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/stagger-from-edges.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/top-down-letters.json delete mode 100644 skills/hyperframes/assets/text-effects/effects/typewriter.json create mode 100644 skills/hyperframes/assets/text-effects/fade-through.json create mode 100644 skills/hyperframes/assets/text-effects/focus-blur-resolve.json create mode 100644 skills/hyperframes/assets/text-effects/kinetic-center-build.json create mode 100644 skills/hyperframes/assets/text-effects/line-by-line-slide.json create mode 100644 skills/hyperframes/assets/text-effects/mask-reveal-up.json create mode 100644 skills/hyperframes/assets/text-effects/micro-scale-fade.json create mode 100644 skills/hyperframes/assets/text-effects/per-character-rise.json create mode 100644 skills/hyperframes/assets/text-effects/per-word-crossfade.json create mode 100644 skills/hyperframes/assets/text-effects/scale-down-fade.json create mode 100644 skills/hyperframes/assets/text-effects/shared-axis-x.json create mode 100644 skills/hyperframes/assets/text-effects/shared-axis-y.json create mode 100644 skills/hyperframes/assets/text-effects/shared-axis-z.json create mode 100644 skills/hyperframes/assets/text-effects/shimmer-sweep.json create mode 100644 skills/hyperframes/assets/text-effects/short-slide-down.json create mode 100644 skills/hyperframes/assets/text-effects/short-slide-right.json create mode 100644 skills/hyperframes/assets/text-effects/soft-blur-in.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/blur-out-up.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/bottom-up-letters.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/depth-parallax-words.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/fade-through.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/focus-blur-resolve.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/kinetic-center-build.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/line-by-line-slide.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/mask-reveal-up.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/micro-scale-fade.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/per-character-rise.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/per-word-crossfade.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/scale-down-fade.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/shared-axis-x.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/shared-axis-y.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/shared-axis-z.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/shimmer-sweep.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/short-slide-down.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/short-slide-right.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/soft-blur-in.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/spring-scale-in.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/stagger-from-center.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/stagger-from-edges.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/top-down-letters.json delete mode 100644 skills/hyperframes/assets/text-effects/specs/typewriter.json create mode 100644 skills/hyperframes/assets/text-effects/spring-scale-in.json create mode 100644 skills/hyperframes/assets/text-effects/stagger-from-center.json create mode 100644 skills/hyperframes/assets/text-effects/stagger-from-edges.json create mode 100644 skills/hyperframes/assets/text-effects/top-down-letters.json create mode 100644 skills/hyperframes/assets/text-effects/typewriter.json diff --git a/skills/hyperframes/assets/text-effects/blur-out-up.json b/skills/hyperframes/assets/text-effects/blur-out-up.json new file mode 100644 index 000000000..33885a63b --- /dev/null +++ b/skills/hyperframes/assets/text-effects/blur-out-up.json @@ -0,0 +1,22 @@ +{ + "id": "blur-out-up", + "name": "Blur Out Up", + "description": "Words arrive clean and exit upward with increasing blur. The entrance is matter-of-fact; the exit dissolves into atmosphere. Asymmetric pairing — good when the line should feel like it lingers in the viewer's memory rather than getting decisively dismissed.", + "target": "word", + "enter": { + "durationMs": 360, + "staggerMs": 90, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 0, "y": 6 }, + "to": { "opacity": 1, "y": 0 } + }, + "exit": { + "durationMs": 520, + "staggerMs": 22, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 1, "y": 0, "filter": "blur(0px)" }, + "to": { "opacity": 0, "y": -28, "filter": "blur(12px)" } + }, + "swap": { "mode": "crossfade", "overlapMs": 200, "microDelayMs": 0 }, + "notes": "The 200ms overlap during swap is intentional — incoming text starts arriving while outgoing text is still mid-blur. Reads as a transition rather than a clean cut." +} diff --git a/skills/hyperframes/assets/text-effects/bottom-up-letters.json b/skills/hyperframes/assets/text-effects/bottom-up-letters.json new file mode 100644 index 000000000..e91386b18 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/bottom-up-letters.json @@ -0,0 +1,22 @@ +{ + "id": "bottom-up-letters", + "name": "Bottom-Up Letters", + "description": "Letters rise from below in a pronounced staircase. Each character takes more visual time than per-character-rise — the motion is larger, slower, and reads as confident punctuation rather than ambient build.", + "target": "char", + "enter": { + "durationMs": 320, + "staggerMs": 65, + "easing": "cubic-bezier(0.18, 1, 0.32, 1)", + "from": { "opacity": 0, "y": 56 }, + "to": { "opacity": 1, "y": 0 } + }, + "exit": { + "durationMs": 280, + "staggerMs": 14, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "y": 0 }, + "to": { "opacity": 0, "y": -22 } + }, + "swap": { "mode": "crossfade", "overlapMs": 80, "microDelayMs": 0 }, + "notes": "Pair with bold or display-weight headlines. The 65ms stagger creates an audible-feeling rhythm; works well when the headline lands on a beat marker in narration." +} diff --git a/skills/hyperframes/assets/text-effects/depth-parallax-words.json b/skills/hyperframes/assets/text-effects/depth-parallax-words.json new file mode 100644 index 000000000..bf8d0b4ca --- /dev/null +++ b/skills/hyperframes/assets/text-effects/depth-parallax-words.json @@ -0,0 +1,22 @@ +{ + "id": "depth-parallax-words", + "name": "Depth Parallax Words", + "description": "Per-word entrance where each word enters at a different scale and slight vertical offset, simulating depth — back words start smaller and lower, front words larger and at baseline. Reads as a 3D-feeling layered headline without needing a real Z-axis transform.", + "target": "word", + "enter": { + "durationMs": 540, + "staggerMs": 110, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 0, "y": 18, "scale": 0.82 }, + "to": { "opacity": 1, "y": 0, "scale": 1 } + }, + "exit": { + "durationMs": 360, + "staggerMs": 22, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "y": 0, "scale": 1 }, + "to": { "opacity": 0, "y": -8, "scale": 0.92 } + }, + "swap": { "mode": "crossfade", "overlapMs": 120, "microDelayMs": 0 }, + "notes": "The scale + y combination is what reads as depth. Don't drop scale below 0.7 — at that point the word looks small rather than far. Keep above 0.8 for the parallax illusion to hold." +} diff --git a/skills/hyperframes/assets/text-effects/effects/blur-out-up.json b/skills/hyperframes/assets/text-effects/effects/blur-out-up.json deleted file mode 100644 index 7222f453e..000000000 --- a/skills/hyperframes/assets/text-effects/effects/blur-out-up.json +++ /dev/null @@ -1,335 +0,0 @@ -{ - "id": "blur-out-up", - "visibility": "visible", - "portable_spec": { - "id": "blur-out-up", - "display_name": "Blur Out Up", - "description": "Words arrive clean and depart upward with increasing blur for airy exits.", - "inspiration": "Apple-style light typography where exit has more character than entry.", - "target": "per-word", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 560, - "stagger_ms": 28, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 10, - "blur_px": 6 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 480, - "stagger_ms": 24, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -14, - "blur_px": 8 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 170, - "micro_delay_ms": 35 - }, - "usage_notes": "Works best on short phrases; avoid very long lines to keep swap time tight." - }, - "showcase": { - "content": { - "sample": "Clear in, airy out.", - "samples": ["Clear in, airy out.", "Lightweight typography.", "Exit with grace."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "blur-out-up" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 35, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 560, - "source_stagger_ms": 28, - "scaled_duration_ms": 403, - "scaled_stagger_ms": 20, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)" - }, - "exit": { - "source_duration_ms": 480, - "source_stagger_ms": 24, - "scaled_duration_ms": 346, - "scaled_stagger_ms": 17, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "per-word", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/bottom-up-letters.json b/skills/hyperframes/assets/text-effects/effects/bottom-up-letters.json deleted file mode 100644 index c1d18ef40..000000000 --- a/skills/hyperframes/assets/text-effects/effects/bottom-up-letters.json +++ /dev/null @@ -1,348 +0,0 @@ -{ - "id": "bottom-up-letters", - "visibility": "visible", - "portable_spec": { - "id": "bottom-up-letters", - "display_name": "Bottom-Up Letters", - "description": "Letters rise from below in a pronounced staircase, one symbol at a time, with zero blur.", - "inspiration": "Apple-style keynote typography, sharp lower-thirds, and clean editorial word swaps.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.18, 1, 0.32, 1)", - "enter": { - "duration_ms": 400, - "stagger_ms": 88, - "easing": "cubic-bezier(0.18, 1, 0.32, 1)", - "from": { - "opacity": 0, - "y_px": 46 - }, - "to": { - "opacity": 1, - "y_px": 0 - } - }, - "exit": { - "duration_ms": 280, - "stagger_ms": 28, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "y_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -14 - } - }, - "swap": { - "mode": "sequential", - "overlap_ms": 0, - "micro_delay_ms": 35, - "scenario_spec": { - "entry_condition": "Use when short words or compact headlines should build upward letter by letter with completely crisp glyph edges.", - "switch_order": [ - "Run old text exit first so the slot clears cleanly.", - "Wait micro_delay_ms after exit.", - "Start new text enter from below with per-character stagger." - ], - "verification": [ - "Letters never blur during enter or exit.", - "The reveal clearly reads bottom-up rather than typewriter-left-to-right.", - "Spacing remains stable while characters settle." - ], - "fallback": { - "if_motion_feels_too_tall": "Reduce enter from.y_px from 46 to 36.", - "if_readability_drops": "Increase stagger_ms from 88 to 100 for even more separation." - } - } - }, - "usage_notes": "Best for short single words, labels, or compact headline swaps at 40px+. This version is intentionally more staged than per-character-rise: very large per-symbol delay, fewer simultaneous letters on screen, and a taller lift from below." - }, - "showcase": { - "content": { - "sample": "Shift", - "samples": ["Shift", "Stage", "Letter"] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "bottom-up-letters" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 35, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 400, - "source_stagger_ms": 88, - "scaled_duration_ms": 288, - "scaled_stagger_ms": 63, - "easing": "cubic-bezier(0.18, 1, 0.32, 1)" - }, - "exit": { - "source_duration_ms": 280, - "source_stagger_ms": 28, - "scaled_duration_ms": 202, - "scaled_stagger_ms": 20, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "per-character", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/depth-parallax-words.json b/skills/hyperframes/assets/text-effects/effects/depth-parallax-words.json deleted file mode 100644 index 8503f98cc..000000000 --- a/skills/hyperframes/assets/text-effects/effects/depth-parallax-words.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "id": "depth-parallax-words", - "visibility": "hidden", - "portable_spec": { - "id": "depth-parallax-words", - "display_name": "Depth Parallax Words", - "description": "Per-word depth motion with scale and vertical drift for layered readability.", - "inspiration": "Product landing pages combining depth cues with clean typography.", - "target": "per-word", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 700, - "stagger_ms": 70, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 18, - "scale": 0.92, - "blur_px": 3 - }, - "to": { - "opacity": 1, - "y_px": 0, - "scale": 1, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 500, - "stagger_ms": 45, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "scale": 1, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -10, - "scale": 1.05, - "blur_px": 2 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 180, - "micro_delay_ms": 30 - }, - "usage_notes": "Use short copy blocks and moderate stagger to avoid visual overload." - }, - "showcase": null -} diff --git a/skills/hyperframes/assets/text-effects/effects/fade-through.json b/skills/hyperframes/assets/text-effects/effects/fade-through.json deleted file mode 100644 index 956830347..000000000 --- a/skills/hyperframes/assets/text-effects/effects/fade-through.json +++ /dev/null @@ -1,339 +0,0 @@ -{ - "id": "fade-through", - "visibility": "visible", - "portable_spec": { - "id": "fade-through", - "display_name": "Fade Through", - "description": "A Material-style content transition: old fades out, new fades in with a soft delay.", - "inspiration": "Google Material fade through transitions for same-level UI changes.", - "target": "whole", - "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", - "enter": { - "duration_ms": 420, - "stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0, 0, 1)", - "from": { - "opacity": 0, - "y_px": 6, - "scale": 0.99, - "blur_px": 2 - }, - "to": { - "opacity": 1, - "y_px": 0, - "scale": 1, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 260, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 1, 1)", - "from": { - "opacity": 1, - "y_px": 0, - "scale": 1, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -4, - "scale": 1, - "blur_px": 0 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 20, - "micro_delay_ms": 60 - }, - "usage_notes": "Best for replacing content in the same layout slot without directional meaning." - }, - "showcase": { - "content": { - "sample": "Calm transitions.", - "samples": ["Calm transitions.", "Fade through content.", "Focus shifts smoothly."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "fade-through" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 60, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 420, - "source_stagger_ms": 0, - "scaled_duration_ms": 302, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0, 0, 1)" - }, - "exit": { - "source_duration_ms": 260, - "source_stagger_ms": 0, - "scaled_duration_ms": 187, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 1, 1)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "whole", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/focus-blur-resolve.json b/skills/hyperframes/assets/text-effects/effects/focus-blur-resolve.json deleted file mode 100644 index 969f8d632..000000000 --- a/skills/hyperframes/assets/text-effects/effects/focus-blur-resolve.json +++ /dev/null @@ -1,339 +0,0 @@ -{ - "id": "focus-blur-resolve", - "visibility": "visible", - "portable_spec": { - "id": "focus-blur-resolve", - "display_name": "Focus Blur Resolve", - "description": "A premium focus pull from heavy blur to crisp text, then a soft blur-out exit.", - "inspiration": "Apple-style hero transitions that resolve detail with cinematic restraint.", - "target": "whole", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 760, - "stagger_ms": 0, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 14, - "blur_px": 14, - "scale": 1.01 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0, - "scale": 1 - } - }, - "exit": { - "duration_ms": 520, - "stagger_ms": 0, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0, - "scale": 1 - }, - "to": { - "opacity": 0, - "y_px": -10, - "blur_px": 10, - "scale": 1 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 160, - "micro_delay_ms": 35 - }, - "usage_notes": "Best on large headlines where blur distance reads as intentional and premium." - }, - "showcase": { - "content": { - "sample": "Focus resolves clearly.", - "samples": ["Focus resolves clearly.", "Detail emerges.", "Then softly recedes."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "focus-blur-resolve" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 35, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 760, - "source_stagger_ms": 0, - "scaled_duration_ms": 547, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)" - }, - "exit": { - "source_duration_ms": 520, - "source_stagger_ms": 0, - "scaled_duration_ms": 374, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "whole", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/kinetic-center-build.json b/skills/hyperframes/assets/text-effects/effects/kinetic-center-build.json deleted file mode 100644 index bcef847f1..000000000 --- a/skills/hyperframes/assets/text-effects/effects/kinetic-center-build.json +++ /dev/null @@ -1,469 +0,0 @@ -{ - "id": "kinetic-center-build", - "visibility": "visible", - "portable_spec": { - "id": "kinetic-center-build", - "display_name": "Kinetic Center Build", - "description": "A word appears in the center; each new word enters from right to left with a soft blur and pushes the existing line until the full phrase locks centered.", - "inspiration": "Apple keynote kinetic editorial typography and sequential phrase builds.", - "target": "per-word", - "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "enter": { - "duration_ms": 360, - "stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "from": { - "opacity": 0, - "y_px": 6, - "scale": 0.992, - "blur_px": 3.5 - }, - "to": { - "opacity": 1, - "y_px": 0, - "scale": 1, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 260, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 0.2, 1)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -6, - "blur_px": 2.5 - } - }, - "swap": { - "mode": "sequential", - "overlap_ms": 0, - "micro_delay_ms": 220, - "scenario_spec": { - "entry_condition": "Use when a short phrase should be built word-by-word, with each new word entering from the right and physically re-centering the existing line.", - "switch_order": [ - "Show the first word in the center.", - "Bring the second word in from right to left while shifting the first word left.", - "Bring the third word in from right to left while shifting the first two words so the final phrase stays centered." - ], - "verification": [ - "Each new word visibly pushes the existing words rather than simply fading in.", - "The completed phrase ends centered and evenly spaced.", - "The motion reads as one kinetic line build, not as three isolated reveals." - ], - "fallback": { - "if_push_is_too_subtle": "Increase build.entry_offset_px from 96 to 120.", - "if_phrase_feels_too_slow": "Reduce build.push_duration_ms from 480 to 420." - } - } - }, - "build": { - "entry_direction": "from-right", - "line_alignment": "center", - "first_word_duration_ms": 340, - "push_duration_ms": 430, - "entry_offset_px": 88, - "word_gap_px": 10, - "first_word_y_px": 6, - "entry_scale": 0.992, - "entry_blur_px": 3.5, - "reflow_blur_px": 0.8, - "exit_y_px": -6, - "exit_blur_px": 2.5, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)", - "phrase_samples": [ - ["Words", "push", "left"], - ["Type", "locks", "center"], - ["Build", "the", "line"] - ] - }, - "usage_notes": "Layout-aware effect: each incoming word changes the target x-position of the whole line. Best for short three-word phrases; implementation requires measuring word widths and animating existing words to new positions. A small entry and reflow blur helps the push feel smoother without extending the timing." - }, - "showcase": { - "content": { - "sample": "Words push left.", - "phrases": [ - ["Words", "push", "left"], - ["Type", "locks", "center"], - ["Build", "the", "line"] - ] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "kinetic-center-build" - }, - "renderer": { - "id": "kinetic-center-build", - "source": "catalog-override", - "params": { - "entry_direction": "from-right", - "line_alignment": "center", - "first_word_duration_ms": 340, - "push_duration_ms": 430, - "entry_offset_px": 88, - "word_gap_px": 10, - "first_word_y_px": 6, - "entry_scale": 0.992, - "entry_blur_px": 3.5, - "reflow_blur_px": 0.8, - "exit_y_px": -6, - "exit_blur_px": 2.5, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)", - "phrase_samples": [ - ["Words", "push", "left"], - ["Type", "locks", "center"], - ["Build", "the", "line"] - ] - }, - "recipe": { - "id": "kinetic-center-build", - "summary": "Build a centered horizontal phrase word by word; each incoming word enters from the right and pushes existing words into newly centered positions.", - "required_measurements": ["offsetWidth for every word after appending the incoming word"], - "algorithm": [ - "Create a relative kinetic line container using the kinetic-line-host stage preset.", - "For each phrase word, append an absolutely centered word span.", - "Measure all child widths and compute centered x positions: totalWidth = sum(widths) + word_gap_px * (count - 1); cursor starts at -totalWidth / 2; each word position is cursor + width / 2.", - "First word enters at x=0 with first_word_y_px, entry_scale, entry_blur_px, and opacity 0, then settles to x=0/y=0/scale=1/blur=0/opacity=1.", - "For later words, animate existing words from previous x positions to next centered x positions while the incoming word starts at targetX + entry_offset_px and lands at targetX.", - "Use an intermediate keyframe around offset 0.52 for existing-word reflow blur and 0.6 for incoming-word settle blur.", - "After every push, snap all words to exact final poses to avoid accumulated engine drift.", - "Exit all words together from current centered x positions with exit_y_px and exit_blur_px, then clear the line." - ], - "frame_materialization": { - "coordinate_space": "x/y values are renderer pixel coordinates and are not multiplied by runtime.y_travel_multiplier.", - "transform": "translate(-50%, -50%) translate3d(x, y, 0) scale(scale)", - "filter": "blur(blur)", - "opacity": "unit opacity" - }, - "keyframe_recipe": { - "first_word": [ - { - "offset": 0, - "x": 0, - "y": "build.first_word_y_px", - "scale": "build.entry_scale", - "blur": "build.entry_blur_px", - "opacity": 0 - }, - { - "offset": 0.58, - "x": 0, - "y": "build.first_word_y_px * 0.35", - "scale": 0.998, - "blur": "build.entry_blur_px * 0.45", - "opacity": 0.78 - }, - { - "offset": 1, - "x": 0, - "y": 0, - "scale": 1, - "blur": 0, - "opacity": 1 - } - ], - "existing_word_push": [ - { - "offset": 0, - "x": "currentX", - "y": 0, - "scale": 1, - "blur": 0, - "opacity": 1 - }, - { - "offset": 0.52, - "x": "mix(currentX, nextX, 0.58)", - "y": 0, - "scale": 1, - "blur": "build.reflow_blur_px", - "opacity": 1 - }, - { - "offset": 1, - "x": "nextX", - "y": 0, - "scale": 1, - "blur": 0, - "opacity": 1 - } - ], - "incoming_word_push": [ - { - "offset": 0, - "x": "targetX + build.entry_offset_px", - "y": 0, - "scale": "build.entry_scale", - "blur": "build.entry_blur_px", - "opacity": 0 - }, - { - "offset": 0.6, - "x": "mix(targetX + build.entry_offset_px, targetX, 0.72)", - "y": 0, - "scale": 0.998, - "blur": "build.entry_blur_px * 0.38", - "opacity": 0.84 - }, - { - "offset": 1, - "x": "targetX", - "y": 0, - "scale": 1, - "blur": 0, - "opacity": 1 - } - ], - "exit_word": [ - { - "offset": 0, - "x": "position", - "y": 0, - "scale": 1, - "blur": 0, - "opacity": 1 - }, - { - "offset": 0.52, - "x": "position", - "y": "build.exit_y_px * 0.45", - "scale": 1, - "blur": "build.exit_blur_px * 0.55", - "opacity": 0.62 - }, - { - "offset": 1, - "x": "position", - "y": "build.exit_y_px", - "scale": 1, - "blur": "build.exit_blur_px", - "opacity": 0 - } - ] - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["build-phrase", "hold", "exit-phrase", "gap"], - "replacement_behavior": "phrase-loop", - "hold_ms": 706, - "micro_delay_ms": 0, - "gap_ms": 158 - }, - "timing": { - "first_word": { - "source_duration_ms": 340, - "scaled_duration_ms": 245, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" - }, - "push": { - "source_duration_ms": 430, - "scaled_duration_ms": 310, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" - }, - "exit": { - "source_duration_ms": 260, - "scaled_duration_ms": 187, - "easing": "cubic-bezier(0.4, 0, 0.2, 1)" - }, - "hold_ms": 706, - "gap_ms": 158 - }, - "stage": { - "preset": "kinetic-line-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - }, - "kinetic_container": { - "requirement": "Use a relative-positioned inline host large enough for the phrase; exact dimensions belong to the consuming UI.", - "position": "relative", - "coordinate_origin": "center" - }, - "kinetic_word": { - "backface_visibility": "hidden", - "left": "50%", - "position": "absolute", - "top": "50%", - "white_space": "nowrap", - "absolute_centered": true, - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "kinetic-center-build", - "target": "per-word", - "stagger_mode": "normal", - "coordinate_space": "renderer-pixels", - "y_travel_multiplier": 1, - "y_travel_multiplier_note": "runtime.y_travel_multiplier is not applied to kinetic build coordinates; x/y values in build params are final transform pixels.", - "transform_order": "translate(-50%, -50%) translate3d(x_px, y_px, 0) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "follow renderer recipe algorithm" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Measure word widths after appending each incoming word.", - "Compute centered x positions from measured widths and word_gap_px.", - "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", - "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow x is mix(currentX, nextX, 0.58) at offset 0.52; incoming-word settle x is mix(startX, targetX, 0.72) at offset 0.6.", - "Exit uses a three-keyframe path with offset 0.52 at y = exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Measure word widths after appending each incoming word.", - "Compute centered x positions from measured widths and word_gap_px.", - "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", - "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow x is mix(currentX, nextX, 0.58) at offset 0.52; incoming-word settle x is mix(startX, targetX, 0.72) at offset 0.6.", - "Exit uses a three-keyframe path with offset 0.52 at y = exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Measure word widths after appending each incoming word.", - "Compute centered x positions from measured widths and word_gap_px.", - "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", - "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow x is mix(currentX, nextX, 0.58) at offset 0.52; incoming-word settle x is mix(startX, targetX, 0.72) at offset 0.6.", - "Exit uses a three-keyframe path with offset 0.52 at y = exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "All engines", - "notes": [ - "Do not apply runtime.y_travel_multiplier to kinetic build x/y coordinates; buildKineticFrame uses the build params as final transform pixels.", - "Use explicit offset keyframes for the intermediate reflow frames, then snap final styles after each push to avoid layout drift." - ] - } - ], - "reproduction_notes": [ - "On the site this effect is layout-aware. Measure word widths, compute centered x positions for the whole phrase, and animate existing words to their next positions while the incoming word enters from the right.", - "For site parity, scale duration and stagger timing by 0.72. Keep kinetic build x/y params as raw renderer pixel coordinates; runtime.y_travel_multiplier applies to generic/title frame conversion, not to buildKineticFrame coordinates.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/line-by-line-slide.json b/skills/hyperframes/assets/text-effects/effects/line-by-line-slide.json deleted file mode 100644 index 4a1edd4c4..000000000 --- a/skills/hyperframes/assets/text-effects/effects/line-by-line-slide.json +++ /dev/null @@ -1,335 +0,0 @@ -{ - "id": "line-by-line-slide", - "visibility": "visible", - "portable_spec": { - "id": "line-by-line-slide", - "display_name": "Line-by-Line Slide", - "description": "Each line enters from the left with a staggered slide and exits to the right for a flowing paragraph reveal.", - "inspiration": "Apple landing page subheads and section headers that breathe line by line.", - "target": "per-line", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 900, - "stagger_ms": 120, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "x_px": -48 - }, - "to": { - "opacity": 1, - "x_px": 0 - } - }, - "exit": { - "duration_ms": 600, - "stagger_ms": 80, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "x_px": 0 - }, - "to": { - "opacity": 0, - "x_px": 48 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 20 - }, - "usage_notes": "Great for 2-line or 3-line headings. This variant keeps swap non-overlapping to avoid content intersections. Reduce x-distance for narrow layouts to keep motion tight on mobile." - }, - "showcase": { - "content": { - "sample": "Think different.\nDo more.", - "samples": [ - "Think different.\nDo more.", - "Built for speed.\nMade to last.", - "Clear ideas.\nClean motion." - ] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "line-by-line-slide" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 20, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 900, - "source_stagger_ms": 120, - "scaled_duration_ms": 648, - "scaled_stagger_ms": 86, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)" - }, - "exit": { - "source_duration_ms": 600, - "source_stagger_ms": 80, - "scaled_duration_ms": 432, - "scaled_stagger_ms": 58, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "per-line", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/mask-reveal-up.json b/skills/hyperframes/assets/text-effects/effects/mask-reveal-up.json deleted file mode 100644 index 65b2f9e34..000000000 --- a/skills/hyperframes/assets/text-effects/effects/mask-reveal-up.json +++ /dev/null @@ -1,339 +0,0 @@ -{ - "id": "mask-reveal-up", - "visibility": "visible", - "portable_spec": { - "id": "mask-reveal-up", - "display_name": "Mask Reveal Up", - "description": "Lines reveal upward with a soft masked feel and compact stagger.", - "inspiration": "Apple section transitions where multiline copy rises in with control.", - "target": "per-line", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 760, - "stagger_ms": 90, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 30, - "blur_px": 6 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 520, - "stagger_ms": 70, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -22, - "blur_px": 6 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 210, - "micro_delay_ms": 35 - }, - "usage_notes": "Best for two-line and three-line headings where line order should stay readable." - }, - "showcase": { - "content": { - "sample": "Designed to move.\nBuilt to focus.", - "samples": [ - "Designed to move.\nBuilt to focus.", - "Quiet motion.\nStrong hierarchy.", - "Premium feel.\nEvery frame." - ] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "mask-reveal-up" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 35, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 760, - "source_stagger_ms": 90, - "scaled_duration_ms": 547, - "scaled_stagger_ms": 65, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)" - }, - "exit": { - "source_duration_ms": 520, - "source_stagger_ms": 70, - "scaled_duration_ms": 374, - "scaled_stagger_ms": 50, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "per-line", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/micro-scale-fade.json b/skills/hyperframes/assets/text-effects/effects/micro-scale-fade.json deleted file mode 100644 index a27d7fa30..000000000 --- a/skills/hyperframes/assets/text-effects/effects/micro-scale-fade.json +++ /dev/null @@ -1,331 +0,0 @@ -{ - "id": "micro-scale-fade", - "visibility": "visible", - "portable_spec": { - "id": "micro-scale-fade", - "display_name": "Micro Scale Fade", - "description": "A calm, tiny scale pop used as subtle premium polish for labels and headings.", - "inspiration": "Apple system status copy, secondary UI labels, and lightweight onboarding micro-animations.", - "target": "whole", - "signature_easing": "cubic-bezier(0.32, 0.72, 0, 1)", - "enter": { - "duration_ms": 600, - "stagger_ms": 0, - "easing": "cubic-bezier(0.32, 0.72, 0, 1)", - "from": { - "opacity": 0, - "scale": 0.96 - }, - "to": { - "opacity": 1, - "scale": 1 - } - }, - "exit": { - "duration_ms": 400, - "stagger_ms": 0, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "scale": 1 - }, - "to": { - "opacity": 0, - "scale": 0.96 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 20 - }, - "usage_notes": "Use this for single words or short titles. This variant keeps swap non-overlapping to avoid content intersections. For paragraphs, switch target to per-word to avoid perceivable lag." - }, - "showcase": { - "content": { - "sample": "Welcome to motion.", - "samples": ["Welcome to motion.", "Small details matter.", "Quietly premium."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "micro-scale-fade" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 20, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 600, - "source_stagger_ms": 0, - "scaled_duration_ms": 432, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.32, 0.72, 0, 1)" - }, - "exit": { - "source_duration_ms": 400, - "source_stagger_ms": 0, - "scaled_duration_ms": 288, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "whole", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/per-character-rise.json b/skills/hyperframes/assets/text-effects/effects/per-character-rise.json deleted file mode 100644 index c5cd35513..000000000 --- a/skills/hyperframes/assets/text-effects/effects/per-character-rise.json +++ /dev/null @@ -1,347 +0,0 @@ -{ - "id": "per-character-rise", - "visibility": "visible", - "portable_spec": { - "id": "per-character-rise", - "display_name": "Per-Character Rise", - "description": "Letters slide up from below with no blur — crisp, deliberate, kinetic. Apple's clean tvOS-style reveal.", - "inspiration": "Apple tvOS, Fitness+ intros, iPadOS home screen title appearances.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "enter": { - "duration_ms": 700, - "stagger_ms": 24, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "from": { - "opacity": 0, - "y_px": 32 - }, - "to": { - "opacity": 1, - "y_px": 0 - } - }, - "exit": { - "duration_ms": 420, - "stagger_ms": 14, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "y_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -24 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 210, - "scenario_spec": { - "entry_condition": "Use for headline replacement where each character must remain crisp and readable throughout the switch.", - "switch_order": [ - "Start old text exit at t=0ms.", - "Start new text enter at t=exit_total_ms-overlap_ms.", - "Use a single active headline layer after enter starts to avoid stacked glyph artifacts." - ], - "verification": [ - "Characters never blur during swap.", - "No visible pause appears between exit and enter phases.", - "Swap keeps staircase rhythm from stagger settings." - ], - "fallback": { - "if_glyphs_collide": "Lower overlap_ms to 140.", - "if_motion_feels_slow": "Reduce enter stagger_ms from 24 to 18." - } - } - }, - "usage_notes": "Works on 40px+ headlines. Zero blur keeps it sharp — that's the key distinction from soft-blur-in. Stagger 24ms gives it quicker momentum; don't go below 16ms or it flattens." - }, - "showcase": { - "content": { - "sample": "One more thing.", - "samples": ["One more thing.", "Fast and fluid.", "Sharp by design."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "per-character-rise" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 0, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 700, - "source_stagger_ms": 24, - "scaled_duration_ms": 504, - "scaled_stagger_ms": 17, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" - }, - "exit": { - "source_duration_ms": 420, - "source_stagger_ms": 14, - "scaled_duration_ms": 302, - "scaled_stagger_ms": 10, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "per-character", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/per-word-crossfade.json b/skills/hyperframes/assets/text-effects/effects/per-word-crossfade.json deleted file mode 100644 index 5e1341a06..000000000 --- a/skills/hyperframes/assets/text-effects/effects/per-word-crossfade.json +++ /dev/null @@ -1,348 +0,0 @@ -{ - "id": "per-word-crossfade", - "visibility": "visible", - "portable_spec": { - "id": "per-word-crossfade", - "display_name": "Per-Word Crossfade", - "description": "Words gently fade into place one after another, with a short vertical drift for a calm keynote rhythm.", - "inspiration": "Apple product announcements and section title transitions where words are readable but still alive.", - "target": "per-word", - "signature_easing": "cubic-bezier(0.16, 1, 0.3, 1)", - "enter": { - "duration_ms": 700, - "stagger_ms": 70, - "easing": "cubic-bezier(0.16, 1, 0.3, 1)", - "from": { - "opacity": 0, - "y_px": 8 - }, - "to": { - "opacity": 1, - "y_px": 0 - } - }, - "exit": { - "duration_ms": 500, - "stagger_ms": 40, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "y_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -6 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 170, - "micro_delay_ms": 70, - "scenario_spec": { - "entry_condition": "Use when phrase-level content changes and word readability is more important than per-character flair.", - "switch_order": [ - "Start old text exit at t=0ms.", - "Start new text enter at t=exit_total_ms-overlap_ms+micro_delay_ms.", - "Advance word groups in the same stagger direction for old and new text." - ], - "verification": [ - "Word boundaries stay readable during overlap.", - "No two identical word positions stay stacked for more than one stagger step.", - "Swap cadence stays calm and editorial, without abrupt jumps." - ], - "fallback": { - "if_words_stack_visibly": "Increase micro_delay_ms to 90.", - "if_total_swap_is_too_long": "Reduce enter stagger_ms to 55 and overlap_ms to 120." - } - } - }, - "usage_notes": "Best for medium phrases and headings; for long copy prefer per-word only up to 16–18 words to keep total stagger time readable. micro_delay_ms helps prevent old/new words from visibly stacking during swaps." - }, - "showcase": { - "content": { - "sample": "Beautifully, unmistakably simple.", - "samples": ["Beautifully simple.", "Designed for focus.", "Built for people."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "per-word-crossfade" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 70, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 700, - "source_stagger_ms": 70, - "scaled_duration_ms": 504, - "scaled_stagger_ms": 50, - "easing": "cubic-bezier(0.16, 1, 0.3, 1)" - }, - "exit": { - "source_duration_ms": 500, - "source_stagger_ms": 40, - "scaled_duration_ms": 360, - "scaled_stagger_ms": 29, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "per-word", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/scale-down-fade.json b/skills/hyperframes/assets/text-effects/effects/scale-down-fade.json deleted file mode 100644 index 4b05a2344..000000000 --- a/skills/hyperframes/assets/text-effects/effects/scale-down-fade.json +++ /dev/null @@ -1,335 +0,0 @@ -{ - "id": "scale-down-fade", - "visibility": "visible", - "portable_spec": { - "id": "scale-down-fade", - "display_name": "Scale Down Fade", - "description": "Subtle premium settle-in with a restrained scale-down fade on exit.", - "inspiration": "Apple product copy transitions where motion remains quiet and precise.", - "target": "whole", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 520, - "stagger_ms": 0, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 8, - "scale": 1.04 - }, - "to": { - "opacity": 1, - "y_px": 0, - "scale": 1 - } - }, - "exit": { - "duration_ms": 380, - "stagger_ms": 0, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "scale": 1 - }, - "to": { - "opacity": 0, - "y_px": -8, - "scale": 0.94 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 130, - "micro_delay_ms": 20 - }, - "usage_notes": "Safe default for product UIs where copy should feel polished but not animated." - }, - "showcase": { - "content": { - "sample": "Quietly refined.", - "samples": ["Quietly refined.", "Polished transitions.", "A soft close."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "scale-down-fade" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 20, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 520, - "source_stagger_ms": 0, - "scaled_duration_ms": 374, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)" - }, - "exit": { - "source_duration_ms": 380, - "source_stagger_ms": 0, - "scaled_duration_ms": 274, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "whole", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/shared-axis-x.json b/skills/hyperframes/assets/text-effects/effects/shared-axis-x.json deleted file mode 100644 index c96f8c179..000000000 --- a/skills/hyperframes/assets/text-effects/effects/shared-axis-x.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "id": "shared-axis-x", - "visibility": "hidden", - "portable_spec": { - "id": "shared-axis-x", - "display_name": "Shared Axis X", - "description": "Horizontal shared-axis transition for sibling destinations with continuity.", - "inspiration": "Google Material shared axis (X) transitions.", - "target": "whole", - "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", - "enter": { - "duration_ms": 500, - "stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0, 0, 1)", - "from": { - "opacity": 0, - "x_px": 24, - "scale": 0.98 - }, - "to": { - "opacity": 1, - "x_px": 0, - "scale": 1 - } - }, - "exit": { - "duration_ms": 360, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 1, 1)", - "from": { - "opacity": 1, - "x_px": 0, - "scale": 1 - }, - "to": { - "opacity": 0, - "x_px": -20, - "scale": 0.98 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 120, - "micro_delay_ms": 20 - }, - "usage_notes": "Use when moving between same-level views where horizontal direction conveys progress." - }, - "showcase": null -} diff --git a/skills/hyperframes/assets/text-effects/effects/shared-axis-y.json b/skills/hyperframes/assets/text-effects/effects/shared-axis-y.json deleted file mode 100644 index e9705d282..000000000 --- a/skills/hyperframes/assets/text-effects/effects/shared-axis-y.json +++ /dev/null @@ -1,335 +0,0 @@ -{ - "id": "shared-axis-y", - "visibility": "visible", - "portable_spec": { - "id": "shared-axis-y", - "display_name": "Word Cut Staircase", - "description": "Per-word hard-cut transition with staircase timing for sharp editorial swaps.", - "inspiration": "Hard-cut typography timing with stepped word sequencing.", - "target": "per-word", - "signature_easing": "steps(1, end)", - "enter": { - "duration_ms": 180, - "stagger_ms": 78, - "easing": "steps(1, end)", - "from": { - "opacity": 0, - "y_px": 0, - "scale": 1 - }, - "to": { - "opacity": 1, - "y_px": 0, - "scale": 1 - } - }, - "exit": { - "duration_ms": 140, - "stagger_ms": 78, - "easing": "steps(1, end)", - "from": { - "opacity": 1, - "y_px": 0, - "scale": 1 - }, - "to": { - "opacity": 0, - "y_px": 0, - "scale": 1 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 28 - }, - "usage_notes": "Use for bold word-by-word hard cuts. No overlap keeps phrase swaps visually clean." - }, - "showcase": { - "content": { - "sample": "Layered navigation.", - "samples": ["Layered navigation.", "Hierarchy made clear.", "Depth with restraint."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "shared-axis-y" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 28, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 180, - "source_stagger_ms": 78, - "scaled_duration_ms": 140, - "scaled_stagger_ms": 56, - "easing": "steps(1, end)" - }, - "exit": { - "source_duration_ms": 140, - "source_stagger_ms": 78, - "scaled_duration_ms": 140, - "scaled_stagger_ms": 56, - "easing": "steps(1, end)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "per-word", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/shared-axis-z.json b/skills/hyperframes/assets/text-effects/effects/shared-axis-z.json deleted file mode 100644 index 2bce463de..000000000 --- a/skills/hyperframes/assets/text-effects/effects/shared-axis-z.json +++ /dev/null @@ -1,335 +0,0 @@ -{ - "id": "shared-axis-z", - "visibility": "visible", - "portable_spec": { - "id": "shared-axis-z", - "display_name": "Shared Axis Z", - "description": "Scale-based shared-axis transition for focus shifts and context depth.", - "inspiration": "Google Material shared axis (Z), adapted for typography swaps.", - "target": "whole", - "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", - "enter": { - "duration_ms": 520, - "stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0, 0, 1)", - "from": { - "opacity": 0, - "scale": 0.9, - "blur_px": 2 - }, - "to": { - "opacity": 1, - "scale": 1, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 360, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 1, 1)", - "from": { - "opacity": 1, - "scale": 1, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "scale": 1.06, - "blur_px": 1 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 100, - "micro_delay_ms": 20 - }, - "usage_notes": "Use for emphasizing focus transitions where scale communicates depth." - }, - "showcase": { - "content": { - "sample": "Zooming between states.", - "samples": ["Zooming between states.", "Elevate and settle.", "Scale with purpose."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "shared-axis-z" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 20, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 520, - "source_stagger_ms": 0, - "scaled_duration_ms": 374, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0, 0, 1)" - }, - "exit": { - "source_duration_ms": 360, - "source_stagger_ms": 0, - "scaled_duration_ms": 259, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 1, 1)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "whole", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/shimmer-sweep.json b/skills/hyperframes/assets/text-effects/effects/shimmer-sweep.json deleted file mode 100644 index 35dcd6029..000000000 --- a/skills/hyperframes/assets/text-effects/effects/shimmer-sweep.json +++ /dev/null @@ -1,335 +0,0 @@ -{ - "id": "shimmer-sweep", - "visibility": "visible", - "portable_spec": { - "id": "shimmer-sweep", - "display_name": "Shimmer Sweep", - "description": "A subtle sweep across a clean headline, blending in while gliding from left to center.", - "inspiration": "Premium hero copy transitions where a short soft push is used before settle.", - "target": "whole", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 850, - "stagger_ms": 0, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "x_px": -22, - "blur_px": 8 - }, - "to": { - "opacity": 1, - "x_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 650, - "stagger_ms": 0, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "x_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "x_px": 22, - "blur_px": 8 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 36 - }, - "usage_notes": "Use as a premium micro-transition for title swaps and copy refreshes. This variant avoids overlap between outgoing and incoming text." - }, - "showcase": { - "content": { - "sample": "Shiny details.", - "samples": ["Shiny details.", "Glide with intent.", "Soft and precise."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "shimmer-sweep" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 36, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 850, - "source_stagger_ms": 0, - "scaled_duration_ms": 612, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)" - }, - "exit": { - "source_duration_ms": 650, - "source_stagger_ms": 0, - "scaled_duration_ms": 468, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "whole", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/short-slide-down.json b/skills/hyperframes/assets/text-effects/effects/short-slide-down.json deleted file mode 100644 index 96db9f38e..000000000 --- a/skills/hyperframes/assets/text-effects/effects/short-slide-down.json +++ /dev/null @@ -1,464 +0,0 @@ -{ - "id": "short-slide-down", - "visibility": "visible", - "portable_spec": { - "id": "short-slide-down", - "display_name": "Short Slide Down", - "description": "Each new word drops in from above into its own line and pushes the existing stack downward until a centered three-line composition locks in place.", - "inspiration": "Keynote-style editorial headings where motion is present but tightly restrained.", - "target": "per-word", - "custom_renderer": "kinetic-top-build", - "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "enter": { - "duration_ms": 520, - "stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "from": { - "opacity": 0, - "y_px": -24, - "blur_px": 2.4, - "scale": 0.992 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0, - "scale": 1 - } - }, - "exit": { - "duration_ms": 320, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 0.2, 1)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0, - "scale": 1 - }, - "to": { - "opacity": 0, - "y_px": 10, - "blur_px": 1.2, - "scale": 1 - } - }, - "build": { - "first_word_duration_ms": 360, - "push_duration_ms": 500, - "exit_duration_ms": 320, - "hold_ms": 1100, - "between_phrases_ms": 180, - "entry_offset_y_px": -28, - "line_gap_px": 12, - "first_word_y_px": -14, - "entry_scale": 0.992, - "entry_blur_px": 2.4, - "reflow_blur_px": 0.7, - "exit_y_px": 10, - "exit_blur_px": 1.2, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)" - }, - "swap": { - "mode": "sequential", - "overlap_ms": 0, - "micro_delay_ms": 70, - "scenario_spec": { - "entry_condition": "Use when three short words should build into a vertical stack, with each new word dropping from above and physically re-centering the composition.", - "switch_order": [ - "Show the first word in the center with a short top-down drop.", - "Bring the second word into a lower line while shifting the first word upward into the stack.", - "Bring the third word into the bottom line while shifting the first two words upward so the final three-line stack stays centered." - ], - "verification": [ - "Each new word visibly pushes the existing words rather than simply fading in.", - "The completed phrase ends as three centered lines with even vertical spacing.", - "The motion reads as one kinetic stacked build with a top-down entry direction." - ], - "fallback": { - "if_drop_is_too_subtle": "Increase build.entry_offset_y_px from -28 to -36.", - "if_phrase_feels_too_slow": "Reduce build.push_duration_ms from 500 to 460." - } - } - }, - "usage_notes": "Best on short three-word headings where each word can live on its own line. Keep the vertical drop compact so the motion still feels editorial, and let the stacking displacement carry most of the energy. For longer phrases, reduce entry_offset_y_px or switch to a softer shared-slide pattern." - }, - "showcase": { - "content": { - "sample": "Build from above.", - "phrases": [ - ["Drop", "into", "place"], - ["Words", "settle", "lower"], - ["Build", "from", "above"] - ] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "short-slide-down" - }, - "renderer": { - "id": "kinetic-top-build", - "source": "spec", - "params": { - "first_word_duration_ms": 360, - "push_duration_ms": 500, - "exit_duration_ms": 320, - "hold_ms": 1100, - "between_phrases_ms": 180, - "entry_offset_y_px": -28, - "line_gap_px": 12, - "first_word_y_px": -14, - "entry_scale": 0.992, - "entry_blur_px": 2.4, - "reflow_blur_px": 0.7, - "exit_y_px": 10, - "exit_blur_px": 1.2, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)" - }, - "recipe": { - "id": "kinetic-top-build", - "summary": "Build a centered vertical stack word by word; each incoming word drops from above and pushes existing words into newly centered y positions.", - "required_measurements": ["offsetHeight for every word after appending the incoming word"], - "algorithm": [ - "Create a relative kinetic stack container using the kinetic-stack-host stage preset.", - "For each phrase word, append an absolutely centered word span.", - "Measure all child heights and compute centered y positions: totalHeight = sum(heights) + line_gap_px * (count - 1); cursor starts at -totalHeight / 2; each word position is cursor + height / 2.", - "First word enters at y=first_word_y_px with entry_scale, entry_blur_px, and opacity 0, then settles to y=0/scale=1/blur=0/opacity=1.", - "For later words, animate existing words from previous y positions to next centered y positions while the incoming word starts at targetY + entry_offset_y_px and lands at targetY.", - "Use an intermediate keyframe around offset 0.52 for existing-word reflow blur and 0.6 for incoming-word settle blur.", - "After every push, snap all words to exact final poses to avoid accumulated engine drift.", - "Exit all words together from current centered y positions with exit_y_px and exit_blur_px, then clear the stack." - ], - "frame_materialization": { - "coordinate_space": "x/y values are renderer pixel coordinates and are not multiplied by runtime.y_travel_multiplier.", - "transform": "translate(-50%, -50%) translate3d(0, y, 0) scale(scale)", - "filter": "blur(blur)", - "opacity": "unit opacity" - }, - "keyframe_recipe": { - "first_word": [ - { - "offset": 0, - "x": 0, - "y": "build.first_word_y_px", - "scale": "build.entry_scale", - "blur": "build.entry_blur_px", - "opacity": 0 - }, - { - "offset": 0.58, - "x": 0, - "y": "build.first_word_y_px * 0.35", - "scale": 0.998, - "blur": "build.entry_blur_px * 0.45", - "opacity": 0.78 - }, - { - "offset": 1, - "x": 0, - "y": 0, - "scale": 1, - "blur": 0, - "opacity": 1 - } - ], - "existing_word_push": [ - { - "offset": 0, - "x": 0, - "y": "currentY", - "scale": 1, - "blur": 0, - "opacity": 1 - }, - { - "offset": 0.52, - "x": 0, - "y": "mix(currentY, nextY, 0.58)", - "scale": 1, - "blur": "build.reflow_blur_px", - "opacity": 1 - }, - { - "offset": 1, - "x": 0, - "y": "nextY", - "scale": 1, - "blur": 0, - "opacity": 1 - } - ], - "incoming_word_push": [ - { - "offset": 0, - "x": 0, - "y": "targetY + build.entry_offset_y_px", - "scale": "build.entry_scale", - "blur": "build.entry_blur_px", - "opacity": 0 - }, - { - "offset": 0.6, - "x": 0, - "y": "mix(targetY + build.entry_offset_y_px, targetY, 0.72)", - "scale": 0.998, - "blur": "build.entry_blur_px * 0.38", - "opacity": 0.84 - }, - { - "offset": 1, - "x": 0, - "y": "targetY", - "scale": 1, - "blur": 0, - "opacity": 1 - } - ], - "exit_word": [ - { - "offset": 0, - "x": 0, - "y": "position", - "scale": 1, - "blur": 0, - "opacity": 1 - }, - { - "offset": 0.52, - "x": 0, - "y": "position + build.exit_y_px * 0.45", - "scale": 1, - "blur": "build.exit_blur_px * 0.55", - "opacity": 0.62 - }, - { - "offset": 1, - "x": 0, - "y": "position + build.exit_y_px", - "scale": 1, - "blur": "build.exit_blur_px", - "opacity": 0 - } - ] - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["build-phrase", "hold", "exit-phrase", "gap"], - "replacement_behavior": "phrase-loop", - "hold_ms": 792, - "micro_delay_ms": 0, - "gap_ms": 130 - }, - "timing": { - "first_word": { - "source_duration_ms": 360, - "scaled_duration_ms": 259, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" - }, - "push": { - "source_duration_ms": 500, - "scaled_duration_ms": 360, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" - }, - "exit": { - "source_duration_ms": 320, - "scaled_duration_ms": 230, - "easing": "cubic-bezier(0.4, 0, 0.2, 1)" - }, - "hold_ms": 792, - "gap_ms": 130 - }, - "stage": { - "preset": "kinetic-stack-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - }, - "kinetic_container": { - "requirement": "Use a relative-positioned block host large enough for the stack; exact dimensions belong to the consuming UI.", - "position": "relative", - "coordinate_origin": "center" - }, - "kinetic_word": { - "backface_visibility": "hidden", - "left": "50%", - "position": "absolute", - "top": "50%", - "white_space": "nowrap", - "absolute_centered": true, - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "kinetic-top-build", - "target": "per-word", - "stagger_mode": "normal", - "coordinate_space": "renderer-pixels", - "y_travel_multiplier": 1, - "y_travel_multiplier_note": "runtime.y_travel_multiplier is not applied to kinetic build coordinates; x/y values in build params are final transform pixels.", - "transform_order": "translate(-50%, -50%) translate3d(0, y_px, 0) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "follow renderer recipe algorithm" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Measure word heights after appending each incoming word.", - "Compute centered y positions from measured heights and line_gap_px.", - "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", - "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow y is mix(currentY, nextY, 0.58) at offset 0.52; incoming-word settle y is mix(startY, targetY, 0.72) at offset 0.6.", - "Exit uses a three-keyframe path with offset 0.52 at y = position + exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Measure word heights after appending each incoming word.", - "Compute centered y positions from measured heights and line_gap_px.", - "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", - "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow y is mix(currentY, nextY, 0.58) at offset 0.52; incoming-word settle y is mix(startY, targetY, 0.72) at offset 0.6.", - "Exit uses a three-keyframe path with offset 0.52 at y = position + exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Measure word heights after appending each incoming word.", - "Compute centered y positions from measured heights and line_gap_px.", - "Use raw renderer-pixel build x/y values; do not apply y_travel_multiplier to kinetic coordinates.", - "Use renderer.recipe.keyframe_recipe exactly: existing-word reflow y is mix(currentY, nextY, 0.58) at offset 0.52; incoming-word settle y is mix(startY, targetY, 0.72) at offset 0.6.", - "Exit uses a three-keyframe path with offset 0.52 at y = position + exit_y_px * 0.45 and opacity 0.62, not a two-keyframe fade." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "All engines", - "notes": [ - "Do not apply runtime.y_travel_multiplier to kinetic build x/y coordinates; buildKineticFrame uses the build params as final transform pixels.", - "Use explicit offset keyframes for the intermediate reflow frames, then snap final styles after each push to avoid layout drift." - ] - } - ], - "reproduction_notes": [ - "On the site this effect builds a centered vertical stack. Measure line heights, compute centered y positions for the stack, and animate existing words upward as the incoming word drops into the next line.", - "For site parity, scale duration and stagger timing by 0.72. Keep kinetic build x/y params as raw renderer pixel coordinates; runtime.y_travel_multiplier applies to generic/title frame conversion, not to buildKineticFrame coordinates.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/short-slide-right.json b/skills/hyperframes/assets/text-effects/effects/short-slide-right.json deleted file mode 100644 index 1755ac24f..000000000 --- a/skills/hyperframes/assets/text-effects/effects/short-slide-right.json +++ /dev/null @@ -1,330 +0,0 @@ -{ - "id": "short-slide-right", - "visibility": "visible", - "portable_spec": { - "id": "short-slide-right", - "display_name": "Short Slide Right", - "description": "The whole phrase glides in from the left as one compact move, while the words themselves are revealed in sequence only through opacity.", - "inspiration": "Keynote-style editorial headings where motion is present but tightly restrained.", - "target": "per-word", - "custom_renderer": "shared-slide-opacity-stage", - "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "enter": { - "duration_ms": 520, - "stagger_ms": 92, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "from": { - "opacity": 1, - "x_px": -24, - "blur_px": 1.2 - }, - "to": { - "opacity": 1, - "x_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 320, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 0.2, 1)", - "from": { - "opacity": 1, - "x_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "x_px": 12, - "blur_px": 1 - } - }, - "build": { - "word_opacity_duration_ms": 210, - "word_opacity_from": 0, - "word_opacity_to": 1 - }, - "swap": { - "mode": "sequential", - "overlap_ms": 0, - "micro_delay_ms": 70, - "scenario_spec": { - "entry_condition": "Use when the heading should feel like one shared horizontal motion, but the words should reveal progressively.", - "switch_order": [ - "Start the whole phrase from one shared left offset.", - "Animate the phrase transform once, with no per-word positional delay.", - "Reveal each word with only opacity stagger so the ordering reads clearly." - ], - "verification": [ - "The phrase position starts and ends in sync for all words.", - "Only opacity is staggered across the words.", - "The amplitude stays compact enough to feel controlled, not swishy." - ], - "fallback": { - "if_motion_feels_too_wide": "Reduce enter.from.x_px from -24 to -18.", - "if_reveal_reads_too_fast": "Increase enter.stagger_ms from 92 to 108.", - "if_words_feel_too_ghosted": "Increase build.word_opacity_duration_ms from 210 to 240." - } - } - }, - "usage_notes": "Best on three-word headings where word order matters. Keep the horizontal travel compact and shared; the phrase should read as one move, with staging communicated only by opacity. For longer phrases, reduce stagger_ms or shorten the opacity duration so the cascade does not drag." - }, - "showcase": { - "content": { - "sample": "Move with intent.", - "samples": ["Move with intent.", "Words glide across.", "Build the rhythm."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "short-slide-right" - }, - "renderer": { - "id": "shared-slide-opacity-stage", - "source": "spec", - "params": { - "word_opacity_duration_ms": 210, - "word_opacity_from": 0, - "word_opacity_to": 1 - }, - "recipe": { - "id": "shared-slide-opacity-stage", - "summary": "Move the full phrase as one title-level transform while staggering only word opacity.", - "required_dom": [ - "one h3.text-animation-title for the full phrase transform", - "word spans are nested inside the title and only receive opacity animation" - ], - "algorithm": [ - "Split text as per-word by default.", - "Apply titleFrame(enter.from) to the h3 and word_opacity_from to each word span.", - "Start the h3 transform animation and every word opacity animation in the same tick; do not wait for the title transform to finish before starting word opacity.", - "Animate the h3 once from enter.from to enter.to using scaled enter duration.", - "Animate every word opacity from word_opacity_from to word_opacity_to with index * scaled enter.stagger_ms delay.", - "Hold, then animate only the h3 from exit.from to exit.to, clear the stage, wait gap_ms, advance to the next phrase, and repeat." - ], - "frame_materialization": { - "title_transform": "translate3d(x_px, y_px * runtime.y_travel_multiplier, 0) scale(scale)", - "title_filter": "blur(blur_px)", - "word_animation_properties": ["opacity"] - }, - "initial_state": { - "before_enter": [ - "Set the title element to titleFrame(enter.from).", - "Set every non-space word span opacity to build.word_opacity_from before starting any enter tween.", - "Whitespace spans should preserve layout but do not receive opacity tweens." - ], - "before_exit": ["Set the title element to titleFrame(exit.from)."] - }, - "verification": [ - "A GSAP implementation must call gsap.set(wordNodes, { opacity: word_opacity_from }) or assign equivalent inline styles before gsap.to(wordNodes, { opacity: word_opacity_to, ... }).", - "A Motion implementation must initialize every word span opacity to word_opacity_from before animate(... opacity: [word_opacity_from, word_opacity_to] ...).", - "A loop implementation must preserve exit and gap timing; an enter-only reveal is not an exact reproduction." - ] - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 0, - "gap_ms": 320 - }, - "timing": { - "enter_title": { - "source_duration_ms": 520, - "source_stagger_ms": 92, - "scaled_duration_ms": 374, - "scaled_stagger_ms": 66, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" - }, - "enter_word_opacity": { - "source_duration_ms": 210, - "scaled_duration_ms": 151, - "delay_step_ms": 66, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)" - }, - "exit_title": { - "source_duration_ms": 320, - "source_stagger_ms": 0, - "scaled_duration_ms": 230, - "scaled_stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 0.2, 1)" - }, - "total_formulas": { - "enter_total_ms": "enter_title.scaled_duration_ms", - "exit_total_ms": "exit_title.scaled_duration_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "shared-slide-opacity-stage", - "target": "per-word", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "follow renderer recipe algorithm" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Animate the title-level transform and every word opacity animation concurrently.", - "Before starting enter animations, set every non-space word span to build.word_opacity_from; otherwise the opacity reveal will be invisible.", - "For GSAP, call gsap.set(wordNodes, { opacity: build.word_opacity_from }) before one batched gsap.to(wordNodes, { opacity: build.word_opacity_to, stagger, ... }) tween; do not create one opacity tween per word unless the delays are non-uniform.", - "For Motion, assign style.opacity = build.word_opacity_from before animate(wordNode, { opacity: [from, to] }, ...).", - "Use enter_title for the phrase transform and enter_word_opacity for word fades.", - "Exit animates only the title-level frame, then clears/replaces content according to playback.", - "Do not ship an enter-only reveal when exact playback is requested; include hold, exit, gap, phrase advance, cancellation, and final-frame snapping." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Animate the title-level transform and every word opacity animation concurrently.", - "Before starting enter animations, set every non-space word span to build.word_opacity_from; otherwise the opacity reveal will be invisible.", - "For GSAP, call gsap.set(wordNodes, { opacity: build.word_opacity_from }) before one batched gsap.to(wordNodes, { opacity: build.word_opacity_to, stagger, ... }) tween; do not create one opacity tween per word unless the delays are non-uniform.", - "For Motion, assign style.opacity = build.word_opacity_from before animate(wordNode, { opacity: [from, to] }, ...).", - "Use enter_title for the phrase transform and enter_word_opacity for word fades.", - "Exit animates only the title-level frame, then clears/replaces content according to playback.", - "Do not ship an enter-only reveal when exact playback is requested; include hold, exit, gap, phrase advance, cancellation, and final-frame snapping." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Animate the title-level transform and every word opacity animation concurrently.", - "Before starting enter animations, set every non-space word span to build.word_opacity_from; otherwise the opacity reveal will be invisible.", - "For GSAP, call gsap.set(wordNodes, { opacity: build.word_opacity_from }) before one batched gsap.to(wordNodes, { opacity: build.word_opacity_to, stagger, ... }) tween; do not create one opacity tween per word unless the delays are non-uniform.", - "For Motion, assign style.opacity = build.word_opacity_from before animate(wordNode, { opacity: [from, to] }, ...).", - "Use enter_title for the phrase transform and enter_word_opacity for word fades.", - "Exit animates only the title-level frame, then clears/replaces content according to playback.", - "Do not ship an enter-only reveal when exact playback is requested; include hold, exit, gap, phrase advance, cancellation, and final-frame snapping." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - } - ], - "reproduction_notes": [ - "On the site this effect moves the full phrase as one shared horizontal transform. Preserve a single phrase-level translation and reveal word order only through opacity timing.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/soft-blur-in.json b/skills/hyperframes/assets/text-effects/effects/soft-blur-in.json deleted file mode 100644 index 03bf7a2fb..000000000 --- a/skills/hyperframes/assets/text-effects/effects/soft-blur-in.json +++ /dev/null @@ -1,351 +0,0 @@ -{ - "id": "soft-blur-in", - "visibility": "visible", - "portable_spec": { - "id": "soft-blur-in", - "display_name": "Soft Blur", - "description": "Per-character fade-in with a gentle blur and upward motion. Apple's signature hero-title reveal.", - "inspiration": "Apple keynote intros; iPhone, Mac, and Vision Pro product page headlines; macOS system UI reveals.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 900, - "stagger_ms": 25, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 16, - "blur_px": 12 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 600, - "stagger_ms": 15, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -16, - "blur_px": 12 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 300, - "scenario_spec": { - "entry_condition": "Use when text is replaced in the same layout slot and both strings remain visually stable in one block.", - "switch_order": [ - "Start old text exit at t=0ms.", - "Start new text enter at t=exit_total_ms-overlap_ms.", - "Keep both text layers mounted only during the overlap window." - ], - "verification": [ - "No hard-cut frame appears between old and new text.", - "Blur stays readable during overlap on desktop and mobile.", - "Total swap duration remains below 1300ms for default sample length." - ], - "fallback": { - "if_overlap_looks_heavy": "Reduce overlap_ms to 180 and exit blur_px to 8.", - "if_copy_is_long": "Switch target to per-word and reduce enter stagger_ms to 15." - } - } - }, - "usage_notes": "Works best on hero titles 48px+ against solid backgrounds. On body text (<24px), reduce blur_px to 6 and stagger_ms to 15. Avoid on very long strings (>40 chars) — total stagger becomes too long; in that case switch target to 'per-word'." - }, - "showcase": { - "content": { - "sample": "Think different.", - "samples": ["Think different.", "Built to flow.", "Motion with intent."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "soft-blur-in" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 0, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 900, - "source_stagger_ms": 25, - "scaled_duration_ms": 648, - "scaled_stagger_ms": 18, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)" - }, - "exit": { - "source_duration_ms": 600, - "source_stagger_ms": 15, - "scaled_duration_ms": 432, - "scaled_stagger_ms": 11, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "per-character", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/spring-scale-in.json b/skills/hyperframes/assets/text-effects/effects/spring-scale-in.json deleted file mode 100644 index 103558e32..000000000 --- a/skills/hyperframes/assets/text-effects/effects/spring-scale-in.json +++ /dev/null @@ -1,331 +0,0 @@ -{ - "id": "spring-scale-in", - "visibility": "visible", - "portable_spec": { - "id": "spring-scale-in", - "display_name": "Spring Scale In", - "description": "Words pop in with a soft overshoot scale, like a physical spring settling into place.", - "inspiration": "iOS app icons bouncing into the home screen, macOS Dock, widget appearances, Vision Pro floating UI pops.", - "target": "per-word", - "signature_easing": "cubic-bezier(0.34, 1.56, 0.64, 1)", - "enter": { - "duration_ms": 360, - "stagger_ms": 95, - "easing": "cubic-bezier(0.34, 1.56, 0.64, 1)", - "from": { - "opacity": 0, - "scale": 0.7 - }, - "to": { - "opacity": 1, - "scale": 1 - } - }, - "exit": { - "duration_ms": 200, - "stagger_ms": 80, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "scale": 1 - }, - "to": { - "opacity": 0, - "scale": 0.8 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 35 - }, - "usage_notes": "The overshoot comes from cubic-bezier y2 > 1 (1.56). Per-word is the sweet spot - per-character at this easing feels too bouncy. Stagger is intentionally high here to create a visible staircase effect. This variant uses no overlap on swap to avoid content crossing during transitions." - }, - "showcase": { - "content": { - "sample": "Fast. Crisp. Fluid.", - "samples": ["Fast. Crisp. Fluid.", "Pop into place.", "Smooth by default."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "spring-scale-in" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 35, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 360, - "source_stagger_ms": 95, - "scaled_duration_ms": 259, - "scaled_stagger_ms": 68, - "easing": "cubic-bezier(0.34, 1.56, 0.64, 1)" - }, - "exit": { - "source_duration_ms": 200, - "source_stagger_ms": 80, - "scaled_duration_ms": 144, - "scaled_stagger_ms": 58, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "per-word", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/stagger-from-center.json b/skills/hyperframes/assets/text-effects/effects/stagger-from-center.json deleted file mode 100644 index 6c17b86f7..000000000 --- a/skills/hyperframes/assets/text-effects/effects/stagger-from-center.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "id": "stagger-from-center", - "visibility": "hidden", - "portable_spec": { - "id": "stagger-from-center", - "display_name": "Stagger from Center", - "description": "Characters reveal from the center outward to emphasize the keyword core.", - "inspiration": "Product hero typography where center-weighted emphasis drives attention.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "stagger_mode": "center-out", - "enter": { - "duration_ms": 620, - "stagger_ms": 22, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 12, - "blur_px": 3 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 420, - "stagger_ms": 16, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -8, - "blur_px": 3 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 150, - "micro_delay_ms": 20 - }, - "usage_notes": "Use on short words or compact titles; long text reduces the center-emphasis effect." - }, - "showcase": null -} diff --git a/skills/hyperframes/assets/text-effects/effects/stagger-from-edges.json b/skills/hyperframes/assets/text-effects/effects/stagger-from-edges.json deleted file mode 100644 index 9c5ee8de1..000000000 --- a/skills/hyperframes/assets/text-effects/effects/stagger-from-edges.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "id": "stagger-from-edges", - "visibility": "hidden", - "portable_spec": { - "id": "stagger-from-edges", - "display_name": "Stagger from Edges", - "description": "Characters start from both edges and converge toward the center.", - "inspiration": "Directional typography reveals used in modern product hero systems.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "stagger_mode": "edges-in", - "enter": { - "duration_ms": 620, - "stagger_ms": 22, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 12, - "blur_px": 3 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 420, - "stagger_ms": 16, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -8, - "blur_px": 3 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 150, - "micro_delay_ms": 20 - }, - "usage_notes": "Effective for medium word lengths where edge-to-center motion remains readable." - }, - "showcase": null -} diff --git a/skills/hyperframes/assets/text-effects/effects/top-down-letters.json b/skills/hyperframes/assets/text-effects/effects/top-down-letters.json deleted file mode 100644 index 8b4eef36d..000000000 --- a/skills/hyperframes/assets/text-effects/effects/top-down-letters.json +++ /dev/null @@ -1,348 +0,0 @@ -{ - "id": "top-down-letters", - "visibility": "visible", - "portable_spec": { - "id": "top-down-letters", - "display_name": "Top-Down Letters", - "description": "Letters descend from above in a pronounced staircase, one symbol at a time, with zero blur.", - "inspiration": "Apple-style keynote typography, crisp editorial headers, and controlled top-down word reveals.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.18, 1, 0.32, 1)", - "enter": { - "duration_ms": 400, - "stagger_ms": 88, - "easing": "cubic-bezier(0.18, 1, 0.32, 1)", - "from": { - "opacity": 0, - "y_px": -46 - }, - "to": { - "opacity": 1, - "y_px": 0 - } - }, - "exit": { - "duration_ms": 280, - "stagger_ms": 28, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "y_px": 0 - }, - "to": { - "opacity": 0, - "y_px": 14 - } - }, - "swap": { - "mode": "sequential", - "overlap_ms": 0, - "micro_delay_ms": 35, - "scenario_spec": { - "entry_condition": "Use when short words or compact headlines should build downward letter by letter with completely crisp glyph edges.", - "switch_order": [ - "Run old text exit first so the slot clears cleanly.", - "Wait micro_delay_ms after exit.", - "Start new text enter from above with per-character stagger." - ], - "verification": [ - "Letters never blur during enter or exit.", - "The reveal clearly reads top-down rather than typewriter-left-to-right.", - "Spacing remains stable while characters settle." - ], - "fallback": { - "if_motion_feels_too_tall": "Reduce enter from.y_px from -46 to -36.", - "if_readability_drops": "Increase stagger_ms from 88 to 100 for even more separation." - } - } - }, - "usage_notes": "Best for short single words, labels, or compact headline swaps at 40px+. This is the top-down counterpart to bottom-up-letters: very large per-symbol delay, fewer simultaneous letters on screen, and a tall drop from above." - }, - "showcase": { - "content": { - "sample": "Signal", - "samples": ["Signal", "Header", "Vector"] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "top-down-letters" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 35, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 400, - "source_stagger_ms": 88, - "scaled_duration_ms": 288, - "scaled_stagger_ms": 63, - "easing": "cubic-bezier(0.18, 1, 0.32, 1)" - }, - "exit": { - "source_duration_ms": 280, - "source_stagger_ms": 28, - "scaled_duration_ms": 202, - "scaled_stagger_ms": 20, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "per-character", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/effects/typewriter.json b/skills/hyperframes/assets/text-effects/effects/typewriter.json deleted file mode 100644 index 41639bb04..000000000 --- a/skills/hyperframes/assets/text-effects/effects/typewriter.json +++ /dev/null @@ -1,331 +0,0 @@ -{ - "id": "typewriter", - "visibility": "visible", - "portable_spec": { - "id": "typewriter", - "display_name": "Typewriter", - "description": "Per-character stepped reveal with a minimal editorial typing rhythm.", - "inspiration": "System-like text build patterns in Apple presentation and utility UI.", - "target": "per-character", - "signature_easing": "steps(1, end)", - "enter": { - "duration_ms": 240, - "stagger_ms": 46, - "easing": "steps(1, end)", - "from": { - "opacity": 0, - "y_px": 0 - }, - "to": { - "opacity": 1, - "y_px": 0 - } - }, - "exit": { - "duration_ms": 260, - "stagger_ms": 10, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "y_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -4 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 85 - }, - "usage_notes": "Good for short copy. Keep line length moderate so stepping stays intentional." - }, - "showcase": { - "content": { - "sample": "Precision in motion.", - "samples": ["Precision in motion.", "Write. Pause. Continue."] - }, - "content_usage": { - "default_policy": "When applying an effect to an existing heading or text section, preserve the section text. Do not replace user/application copy with showcase sample text unless the user explicitly asks to reproduce the demo copy.", - "showcase_samples": "showcase.content.sample and samples are reference/demo copy used by the generated website examples and useful fallback copy for isolated demos.", - "loop_policy": "If the existing section supplies multiple phrases, loop those phrases. If it supplies one phrase, animate that phrase with the same enter/exit playback or use explicitly provided alternate phrases." - }, - "sample_source": { - "asset": "assets/samples.json", - "key": "typewriter" - }, - "renderer": { - "id": "generic-stagger", - "source": "default", - "params": {}, - "recipe": { - "id": "generic-stagger", - "summary": "Split text by target, animate each animated unit from enter.from to enter.to, hold, animate current units from exit.from to exit.to, then replace content.", - "required_dom": [ - "one h3.text-animation-title per phrase", - "one span.text-animation-unit per split part", - "animate only non-space parts for per-word targets", - "span.text-animation-unit.line uses display:block for per-line targets" - ], - "split_rules": { - "whole": "single animated unit containing the full text", - "per-character": "Array.from(text), preserving punctuation and spaces as animated visual units", - "per-word": "regex /(\\S+|\\s+)/g; create spans for words and whitespace, but animate only non-whitespace spans", - "per-line": "split on explicit \"\\n\"; each line is an animated block span" - }, - "stagger_rank_algorithms": { - "normal": "rank equals DOM unit index", - "reverse": "rank 0 starts at last animated unit and proceeds backward", - "center-out": "sort animated indices by absolute distance from center, ties by lower index", - "edges-in": "alternate left edge, right edge, then move inward" - }, - "frame_materialization": { - "transform_order": "translate3d(x_px, y_px * runtime.y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "filter": "blur(blur_px)", - "opacity_default": 1, - "scale_default": 1, - "letter_spacing": "for per-character targets, split letter_spacing_em across marginLeft/marginRight halves on glyphs; otherwise assign letterSpacing directly", - "fill": "final frame must remain applied after each phase completes" - }, - "loop_algorithm": [ - "Wait initial_delay_ms before starting the first enter.", - "Create current phrase, apply enter.from to every animated unit, append it, then animate enter.", - "After the first enter completes, wait hold_ms.", - "Loop from the visible phrase: animate current units through exit.", - "Create next phrase off-DOM and apply enter.from.", - "After the exit completes, wait micro_delay_ms.", - "Replace the stage contents with the next phrase and animate enter.", - "After the next enter completes, wait gap_ms.", - "Continue the loop by exiting the currently visible phrase; do not run another enter for a phrase that is already visible." - ], - "canonical_loop_pseudocode": [ - "current = createPhrase(firstText); append(current); await enter(current);", - "while active:", - " await sleep(hold_ms);", - " await exit(current);", - " next = createPhrase(nextText); applyEnterFrom(next);", - " await sleep(micro_delay_ms);", - " replaceStage(next);", - " current = next;", - " await enter(current);", - " await sleep(gap_ms);", - "Do not put await enter(current) at the top of the while loop; that double-enters the phrase that just entered before gap_ms." - ], - "loop_invariants": [ - "The initial phrase enters exactly once before the loop body.", - "Every later phrase enters exactly once immediately after replacement.", - "If implementation awaits an animation or tween promise, do not also sleep for that phase total; use either await completion or sleep(total), not both.", - "Do not implement an enter-only demo when exact playback is requested; preserve exit, replacement, micro-delay, gap, cancellation, and final-frame snapping." - ], - "current_site_swap_support": { - "uses_micro_delay_ms": true, - "uses_overlap_ms": false, - "branches_on_swap_mode": false, - "note": "The portable swap block may describe broader intent; the current site showcase uses the playback recipe here as the exact behavior." - } - } - }, - "runtime": { - "preset": "website-default", - "speed_multiplier": 0.72, - "hold_ms": 550, - "gap_ms": 320, - "y_travel_multiplier": 0.58, - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - } - }, - "playback": { - "kind": "loop", - "cycle": ["enter", "hold", "exit", "micro-delay", "gap"], - "replacement_behavior": "exit-before-enter", - "hold_ms": 550, - "micro_delay_ms": 85, - "gap_ms": 320 - }, - "timing": { - "enter": { - "source_duration_ms": 240, - "source_stagger_ms": 46, - "scaled_duration_ms": 173, - "scaled_stagger_ms": 33, - "easing": "steps(1, end)" - }, - "exit": { - "source_duration_ms": 260, - "source_stagger_ms": 10, - "scaled_duration_ms": 187, - "scaled_stagger_ms": 7, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)" - }, - "total_formulas": { - "enter_total_ms": "enter.scaled_duration_ms + max(0, animated_unit_count - 1) * enter.scaled_stagger_ms", - "exit_total_ms": "exit.scaled_duration_ms + max(0, animated_unit_count - 1) * exit.scaled_stagger_ms" - } - }, - "stage": { - "preset": "default-text-host", - "purpose": "Animation-only host requirements. Typography, color, card chrome, padding, and responsive sizing are intentionally excluded so the skill stays portable.", - "container": { - "requirement": "Provide a host element for the animated title.", - "perspective_px": 900, - "perspective_note": "Needed when effects use z_px, rotate_x_deg, or rotate_y_deg. Host layout and size are application-owned." - }, - "title": { - "requirement": "Animate the phrase container when the renderer recipe uses title frames.", - "display": "inline-block", - "transform_style": "preserve-3d", - "layout_note": "Do not force flex-direction: column on the title globally; line breaks come from span.text-animation-unit.line using display:block." - }, - "unit": { - "backface_visibility": "hidden", - "display": "inline-block", - "line_display": "block", - "transform_origin": "50% 55%", - "white_space": "pre", - "will_change": ["transform", "opacity", "filter"] - } - }, - "rendering_contract": { - "renderer": "generic-stagger", - "target": "per-character", - "stagger_mode": "normal", - "y_travel_multiplier": 0.58, - "transform_order": "translate3d(x_px, y_px * y_travel_multiplier, z_px) rotateX(rotate_x_deg) rotateY(rotate_y_deg) rotate(rotate_deg) scale(scale)", - "fill_behavior": "retain final frame after each phase", - "initial_delay_ms": { - "mode": "random-range", - "min": 0, - "max": 400 - }, - "content_replacement": "current phrase is cleared and replaced only after exit_total_ms + micro_delay_ms" - }, - "library_selection": { - "supported_adapters": ["waapi", "motion", "gsap"], - "aliases": { - "web animations api": "waapi", - "waapi": "waapi", - "motion": "motion", - "motion.dev": "motion", - "motion react": "motion", - "framer motion": "motion", - "gsap": "gsap", - "greensock": "gsap" - }, - "rule": "If the user names a target animation library, use only the matching adapter for that effect. Do not silently substitute Motion for GSAP, GSAP for Motion, or WAAPI for either library. If a requested library is unsupported, state that limitation before implementing.", - "verification": "For generated code, verify imports and animation calls match the selected adapter: Motion should import/use animate from motion/react and not Element.animate/gsap, GSAP should import/use gsap and CustomEase and not Motion/Element.animate, and WAAPI should use Element.animate without a third-party animation import." - }, - "library_adapters": { - "waapi": { - "target_library": "Web Animations API", - "install": "none; native browser Element.animate", - "import_statement": null, - "time_unit": "milliseconds", - "start_animation": "element.animate(keyframes, { delay: delay_ms, duration: duration_ms, easing, fill: \"forwards\" })", - "keyframe_shape": "Use CSS-style Keyframe[] objects with transform, filter, opacity, letterSpacing, and optional offset fields.", - "easing": "Pass CSS easing strings directly, including cubic-bezier(...) and steps(...).", - "completion": "await animation.finished, then assign the final keyframe styles before replacing content.", - "cancellation": "cancel active Animation objects and clear pending timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "motion": { - "target_library": "Motion for React / motion.dev", - "install": "pnpm add motion", - "import_statement": "import { animate, cubicBezier, steps } from \"motion/react\";", - "time_unit": "seconds for delay and duration options", - "start_animation": "animate(element, propertyKeyframes, { delay: delay_ms / 1000, duration: duration_ms / 1000, ease, times })", - "keyframe_shape": "Convert Keyframe[] into property arrays, for example { opacity: [0, 1], transform: [\"...\", \"...\"], filter: [\"...\", \"...\"] }. Convert keyframe offset values into the times array.", - "verification": [ - "When offsets are present, pass times in the Motion options object, not inside the propertyKeyframes object.", - "The Motion times array length must match each animated property array length for that tween.", - "Motion TypeScript may reject CSS transform/filter property arrays; use a local typed helper/cast at the animate boundary instead of changing the keyframe shape.", - "Exact reproduction must include exit/replacement playback, not only initial enter tweens." - ], - "easing": "Convert cubic-bezier(a,b,c,d) to cubicBezier(a,b,c,d). Convert steps(n,start|end) to steps(n, \"start\"|\"end\"). Map CSS ease-in/ease-out/ease-in-out to Motion easeIn/easeOut/easeInOut.", - "completion": "Use controls.then(...) or await the returned controls in an async loop, then assign final styles before content replacement.", - "cancellation": "call controls.stop?.() and controls.cancel?.() for active Motion animations when available, and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - }, - "gsap": { - "target_library": "GSAP", - "install": "pnpm add gsap", - "import_statement": "import { gsap } from \"gsap\"; import { CustomEase } from \"gsap/CustomEase\"; gsap.registerPlugin(CustomEase);", - "time_unit": "seconds for delay and duration options", - "start_animation": "gsap.set(element, firstKeyframe); gsap.to(element, { keyframes: remainingKeyframesWithSegmentDurations, delay: delay_ms / 1000, ease, overwrite: \"auto\" })", - "keyframe_shape": "Use GSAP property objects with transform, filter, opacity, letterSpacing. For offset keyframes, convert adjacent offset gaps into absolute per-keyframe segment durations in seconds.", - "verification": [ - "Initialize first-frame styles with gsap.set before starting a tween.", - "Do not pass both per-keyframe segment durations and a top-level gsap.to duration; that retimes the tween and makes the GSAP reproduction feel slower than the spec.", - "For renderer keyframe_recipe offsets, use GSAP keyframes with equivalent segment durations or a timeline that preserves the same absolute offsets.", - "For generic-stagger loops, do not enter the same visible phrase twice; after gap, the next action is exit of the current phrase." - ], - "easing": "Convert cubic-bezier(a,b,c,d) with CustomEase.create(...). Use \"none\" for linear. Convert steps(n,end) to GSAP steps(n).", - "completion": "Wrap tweens/timelines in a Promise resolved by onComplete, then assign final styles before replacing content.", - "cancellation": "kill active tweens/timelines and clear timers on teardown.", - "renderer_notes": [ - "Create split units from target and animate only the animated units.", - "Delay each unit by stagger rank * scaled_stagger_ms.", - "Use materialized transform/filter/opacity keyframes from rendering_contract.transform_order.", - "Implement the complete playback loop from renderer.recipe.loop_algorithm: initial enter once, hold, exit current, micro-delay, replace next, enter next, gap, then exit that visible phrase.", - "Do not restart enter on a phrase that is already visible after gap; the next cycle starts with exit for the current phrase.", - "When awaiting animation completion promises, wait hold_ms/micro_delay_ms/gap_ms only; do not also sleep enter_total_ms or exit_total_ms.", - "Reject the code shape `while (...) { await enter(current); ... await enter(next); await sleep(gap); }`; it double-enters the visible phrase. Use renderer.recipe.canonical_loop_pseudocode instead." - ] - } - }, - "engine_notes": [ - { - "engine": "WAAPI", - "notes": [ - "Use Element.animate(keyframes, { delay, duration, easing, fill: \"forwards\" }).", - "For multi-keyframe effects, keep offsets on the keyframes and apply easing at the animation options level to match the site runtime." - ] - }, - { - "engine": "Motion", - "notes": [ - "Use imperative animate(element, keyframes, options) when reproducing the site loops.", - "Convert CSS cubic-bezier strings to cubicBezier(x1, y1, x2, y2), convert steps(n, start|end) to steps(n, direction), and pass explicit times for keyframe offsets." - ] - }, - { - "engine": "GSAP", - "notes": [ - "Register CustomEase for CSS cubic-bezier curves; map linear to ease \"none\" and steps(n, end) to GSAP steps(n).", - "For multi-keyframe effects, convert offset gaps into per-keyframe segment durations in seconds and keep one tween-level ease. Do not also pass a top-level duration when segment durations are present." - ] - }, - { - "engine": "CSS", - "notes": [ - "CSS keyframes are viable for simple generic-stagger effects if every unit gets the same keyframes and computed delay.", - "CSS alone is usually not sufficient for the site loop unless JavaScript handles content replacement timing." - ] - } - ], - "reproduction_notes": [ - "On the site this effect uses the generic stagger renderer. Apply the portable enter and exit frames per animated unit, preserving the declared target split and stagger ordering.", - "For site parity, scale duration and stagger timing by 0.72 and scale vertical travel by 0.58. These runtime transforms materially affect the perceived pace and distance.", - "For exact animation reproduction, follow `showcase.playback`, `showcase.timing`, `showcase.rendering_contract`, and `showcase.stage` over assumptions inferred from the portable contract alone. Presentation styling such as font size, font weight, color, padding, and card chrome is intentionally application-owned." - ] - } -} diff --git a/skills/hyperframes/assets/text-effects/fade-through.json b/skills/hyperframes/assets/text-effects/fade-through.json new file mode 100644 index 000000000..5cf187d92 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/fade-through.json @@ -0,0 +1,20 @@ +{ + "id": "fade-through", + "name": "Fade Through", + "description": "Material-style cross-dissolve where the old content fades out completely before the new content fades in. Different from a normal crossfade — there's a brief moment where neither is visible. Reads as a contextual swap rather than a continuous flow.", + "target": "element", + "enter": { + "durationMs": 280, + "easing": "cubic-bezier(0.2, 0, 0, 1)", + "from": { "opacity": 0 }, + "to": { "opacity": 1 } + }, + "exit": { + "durationMs": 200, + "easing": "cubic-bezier(0.4, 0, 1, 1)", + "from": { "opacity": 1 }, + "to": { "opacity": 0 } + }, + "swap": { "mode": "sequential", "overlapMs": -60, "microDelayMs": 0 }, + "notes": "The -60ms overlap is intentional: outgoing element finishes fading out, then 60ms of empty space, then incoming starts. The empty moment is what distinguishes this from a crossfade. Implementer: timeline puts the new content's enter AFTER the previous content's exit completes." +} diff --git a/skills/hyperframes/assets/text-effects/focus-blur-resolve.json b/skills/hyperframes/assets/text-effects/focus-blur-resolve.json new file mode 100644 index 000000000..b0b301ff4 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/focus-blur-resolve.json @@ -0,0 +1,20 @@ +{ + "id": "focus-blur-resolve", + "name": "Focus Blur Resolve", + "description": "Heavy blur resolves to sharp clarity on entrance, returns to soft blur on exit. Reads as a camera-focus pull — the element doesn't move; it just comes into focus. Cinematic, attention-pulling.", + "target": "element", + "enter": { + "durationMs": 580, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 0, "filter": "blur(16px)" }, + "to": { "opacity": 1, "filter": "blur(0px)" } + }, + "exit": { + "durationMs": 420, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "filter": "blur(0px)" }, + "to": { "opacity": 0, "filter": "blur(6px)" } + }, + "swap": { "mode": "crossfade", "overlapMs": 160, "microDelayMs": 0 }, + "notes": "The 16px entrance blur is heavy enough to feel like out-of-focus rather than mild softness. Don't push above 24px — at that point the text is unreadable for too long. Don't drop below 8px — at that point the effect reads as 'gently fades in,' not 'focuses.'" +} diff --git a/skills/hyperframes/assets/text-effects/kinetic-center-build.json b/skills/hyperframes/assets/text-effects/kinetic-center-build.json new file mode 100644 index 000000000..77bf13875 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/kinetic-center-build.json @@ -0,0 +1,23 @@ +{ + "id": "kinetic-center-build", + "name": "Kinetic Center Build", + "description": "Each word locks to its center position as the phrase builds right-to-left with a soft blur. Layout-aware — the next word arrives at the previous word's left edge while the line stays centered in frame.", + "target": "word", + "enter": { + "durationMs": 480, + "staggerMs": 220, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 0, "x": 24, "filter": "blur(6px)" }, + "to": { "opacity": 1, "x": 0, "filter": "blur(0px)" } + }, + "exit": { + "durationMs": 420, + "staggerMs": 40, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "y": 0, "filter": "blur(0px)" }, + "to": { "opacity": 0, "y": -10, "filter": "blur(6px)" } + }, + "swap": { "mode": "crossfade", "overlapMs": 160, "microDelayMs": 0 }, + "layoutAware": true, + "notes": "Layout-aware: the implementer must shift each word's x-position so the whole phrase stays horizontally centered as it grows. Without this, words appended to the right push the line off-center. Computed positions, not just stagger timing — read the layout-aware section of text-effects.md." +} diff --git a/skills/hyperframes/assets/text-effects/line-by-line-slide.json b/skills/hyperframes/assets/text-effects/line-by-line-slide.json new file mode 100644 index 000000000..a00e62242 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/line-by-line-slide.json @@ -0,0 +1,22 @@ +{ + "id": "line-by-line-slide", + "name": "Line By Line Slide", + "description": "Each line slides in from the left, exits to the right. Reads as flowing paragraph rhythm — best for multi-line body copy or quotes where the lines build sequentially. Different from mask-reveal-up in that the motion is horizontal, not masked.", + "target": "line", + "enter": { + "durationMs": 640, + "staggerMs": 110, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 0, "x": -40 }, + "to": { "opacity": 1, "x": 0 } + }, + "exit": { + "durationMs": 480, + "staggerMs": 60, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "x": 0 }, + "to": { "opacity": 0, "x": 32 } + }, + "swap": { "mode": "crossfade", "overlapMs": 160, "microDelayMs": 0 }, + "notes": "Best for 2–4 lines max. Beyond that the cumulative stagger gets noticeably long (110ms × 5 lines = 550ms before the last line starts moving). For long body copy, prefer mask-reveal-up or shorten the stagger." +} diff --git a/skills/hyperframes/assets/text-effects/mask-reveal-up.json b/skills/hyperframes/assets/text-effects/mask-reveal-up.json new file mode 100644 index 000000000..36c5b68cb --- /dev/null +++ b/skills/hyperframes/assets/text-effects/mask-reveal-up.json @@ -0,0 +1,22 @@ +{ + "id": "mask-reveal-up", + "name": "Mask Reveal Up", + "description": "Each line of text reveals as a `clip-path` mask wipes upward — the text moves up into a fixed viewport. Reads as contained, intentional, and slightly magazine-like; the masked feel separates this from a plain fade.", + "target": "line", + "enter": { + "durationMs": 580, + "staggerMs": 90, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "y": 36, "clipPath": "inset(100% 0% 0% 0%)" }, + "to": { "y": 0, "clipPath": "inset(0% 0% 0% 0%)" } + }, + "exit": { + "durationMs": 420, + "staggerMs": 40, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "y": 0, "clipPath": "inset(0% 0% 0% 0%)" }, + "to": { "y": -12, "clipPath": "inset(0% 0% 100% 0%)" } + }, + "swap": { "mode": "crossfade", "overlapMs": 120, "microDelayMs": 0 }, + "notes": "Each `.line` element needs `overflow: hidden` on its parent so the clip-path mask reads as a window. Without that the y-translation just shows the text moving below its baseline — the masking effect is lost." +} diff --git a/skills/hyperframes/assets/text-effects/micro-scale-fade.json b/skills/hyperframes/assets/text-effects/micro-scale-fade.json new file mode 100644 index 000000000..26ef27008 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/micro-scale-fade.json @@ -0,0 +1,20 @@ +{ + "id": "micro-scale-fade", + "name": "Micro Scale Fade", + "description": "Whole-element entrance with a tiny scale change (0.96 → 1.00) and a fade. Barely perceptible — reads as polish rather than animation. Use when you want the element to settle in without drawing attention to the motion itself.", + "target": "element", + "enter": { + "durationMs": 460, + "easing": "cubic-bezier(0.32, 0.72, 0, 1)", + "from": { "opacity": 0, "scale": 0.96 }, + "to": { "opacity": 1, "scale": 1 } + }, + "exit": { + "durationMs": 320, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "scale": 1 }, + "to": { "opacity": 0, "scale": 0.98 } + }, + "swap": { "mode": "crossfade", "overlapMs": 140, "microDelayMs": 0 }, + "notes": "Don't push scale beyond 0.92 or below 0.94 — at that range the motion stops reading as polish and starts reading as 'something is shrinking.' The whole point is to feel deliberate without feeling animated." +} diff --git a/skills/hyperframes/assets/text-effects/per-character-rise.json b/skills/hyperframes/assets/text-effects/per-character-rise.json new file mode 100644 index 000000000..1c2373383 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/per-character-rise.json @@ -0,0 +1,22 @@ +{ + "id": "per-character-rise", + "name": "Per-Character Rise", + "description": "Letters slide up from below baseline with no blur, settling one after another. Crisp and deliberate — the structural answer to soft-blur-in.", + "target": "char", + "enter": { + "durationMs": 520, + "staggerMs": 18, + "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "from": { "opacity": 0, "y": 24 }, + "to": { "opacity": 1, "y": 0 } + }, + "exit": { + "durationMs": 320, + "staggerMs": 8, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "y": 0 }, + "to": { "opacity": 0, "y": -10 } + }, + "swap": { "mode": "crossfade", "overlapMs": 100, "microDelayMs": 0 }, + "notes": "Works well across most weights. The 24px rise distance gives obvious motion without crossing into theatrical territory." +} diff --git a/skills/hyperframes/assets/text-effects/per-word-crossfade.json b/skills/hyperframes/assets/text-effects/per-word-crossfade.json new file mode 100644 index 000000000..d2b9bdd4e --- /dev/null +++ b/skills/hyperframes/assets/text-effects/per-word-crossfade.json @@ -0,0 +1,22 @@ +{ + "id": "per-word-crossfade", + "name": "Per-Word Crossfade", + "description": "Words fade in one at a time with a short vertical drift. The rhythm is calm and sequential — best for sub-headlines, body copy that needs gentle pacing, or value-prop lines where each word should land before the next.", + "target": "word", + "enter": { + "durationMs": 460, + "staggerMs": 140, + "easing": "cubic-bezier(0.16, 1, 0.3, 1)", + "from": { "opacity": 0, "y": 10 }, + "to": { "opacity": 1, "y": 0 } + }, + "exit": { + "durationMs": 320, + "staggerMs": 32, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "y": 0 }, + "to": { "opacity": 0, "y": -6 } + }, + "swap": { "mode": "crossfade", "overlapMs": 140, "microDelayMs": 0 }, + "notes": "140ms stagger is the sweet spot for short headlines (3-7 words). For longer lines (8+ words) drop stagger to 80-100ms to keep the total entrance under 1.5 seconds." +} diff --git a/skills/hyperframes/assets/text-effects/scale-down-fade.json b/skills/hyperframes/assets/text-effects/scale-down-fade.json new file mode 100644 index 000000000..d0d0e4649 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/scale-down-fade.json @@ -0,0 +1,20 @@ +{ + "id": "scale-down-fade", + "name": "Scale Down Fade", + "description": "Content settles with a slight scale-down on entrance (1.04 → 1.00) and the same on exit. Restrained and premium — the scale gives the element a subtle 'arriving' and 'departing' feel without being theatrical.", + "target": "element", + "enter": { + "durationMs": 380, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 0, "scale": 1.04 }, + "to": { "opacity": 1, "scale": 1 } + }, + "exit": { + "durationMs": 380, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 1, "scale": 1 }, + "to": { "opacity": 0, "scale": 0.96 } + }, + "swap": { "mode": "crossfade", "overlapMs": 140, "microDelayMs": 0 }, + "notes": "Symmetric durations (380ms in, 380ms out) keep the entrance and exit feeling like the same motion played in reverse. Asymmetric durations work for other effects but read as unsettled here." +} diff --git a/skills/hyperframes/assets/text-effects/shared-axis-x.json b/skills/hyperframes/assets/text-effects/shared-axis-x.json new file mode 100644 index 000000000..fca936a24 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/shared-axis-x.json @@ -0,0 +1,20 @@ +{ + "id": "shared-axis-x", + "name": "Shared Axis X", + "description": "Horizontal sibling transition — outgoing content slides left and fades, incoming content arrives from the right with a fade. Reads as 'next page' or 'sequential destination,' similar to Material's shared-axis-X pattern.", + "target": "element", + "enter": { + "durationMs": 380, + "easing": "cubic-bezier(0.2, 0, 0, 1)", + "from": { "opacity": 0, "x": 40 }, + "to": { "opacity": 1, "x": 0 } + }, + "exit": { + "durationMs": 380, + "easing": "cubic-bezier(0.4, 0, 1, 1)", + "from": { "opacity": 1, "x": 0 }, + "to": { "opacity": 0, "x": -40 } + }, + "swap": { "mode": "crossfade", "overlapMs": 180, "microDelayMs": 0 }, + "notes": "Use shared-axis-x for sequential moves (next/previous), shared-axis-y for hierarchical (up/down), shared-axis-z for contextual swaps (zoom in/out). Symmetric ±40px translates." +} diff --git a/skills/hyperframes/assets/text-effects/shared-axis-y.json b/skills/hyperframes/assets/text-effects/shared-axis-y.json new file mode 100644 index 000000000..78886c0c5 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/shared-axis-y.json @@ -0,0 +1,22 @@ +{ + "id": "shared-axis-y", + "name": "Shared Axis Y", + "description": "Hard-cut word-by-word with staircase timing along the Y axis. No interpolation — each word snaps to its position. Sharp and editorial; pairs with mono or condensed display weights.", + "target": "word", + "enter": { + "durationMs": 160, + "staggerMs": 60, + "easing": "steps(1, end)", + "from": { "opacity": 0, "y": 18 }, + "to": { "opacity": 1, "y": 0 } + }, + "exit": { + "durationMs": 160, + "staggerMs": 30, + "easing": "steps(1, end)", + "from": { "opacity": 1, "y": 0 }, + "to": { "opacity": 0, "y": -18 } + }, + "swap": { "mode": "crossfade", "overlapMs": 0, "microDelayMs": 60 }, + "notes": "The steps easing makes this read as a series of discrete cuts, not motion. Don't expect smoothness — that's the point. Use sparingly; one or two beats per video at most." +} diff --git a/skills/hyperframes/assets/text-effects/shared-axis-z.json b/skills/hyperframes/assets/text-effects/shared-axis-z.json new file mode 100644 index 000000000..25dedf3cc --- /dev/null +++ b/skills/hyperframes/assets/text-effects/shared-axis-z.json @@ -0,0 +1,20 @@ +{ + "id": "shared-axis-z", + "name": "Shared Axis Z", + "description": "Scale-based depth transition along the Z axis. One context fades out small (recedes), the next fades in at full scale (arrives). Reads as a contextual shift — best for swapping between hierarchically related sections.", + "target": "element", + "enter": { + "durationMs": 360, + "easing": "cubic-bezier(0.2, 0, 0, 1)", + "from": { "opacity": 0, "scale": 1.08 }, + "to": { "opacity": 1, "scale": 1 } + }, + "exit": { + "durationMs": 360, + "easing": "cubic-bezier(0.4, 0, 1, 1)", + "from": { "opacity": 1, "scale": 1 }, + "to": { "opacity": 0, "scale": 0.92 } + }, + "swap": { "mode": "crossfade", "overlapMs": 180, "microDelayMs": 0 }, + "notes": "Outgoing scales DOWN (recedes), incoming scales DOWN from above (arrives from depth). The 0.92/1.08 deltas are intentionally subtle — pushing them beyond 0.85/1.15 makes the effect feel theatrical rather than spatial." +} diff --git a/skills/hyperframes/assets/text-effects/shimmer-sweep.json b/skills/hyperframes/assets/text-effects/shimmer-sweep.json new file mode 100644 index 000000000..5b42dc008 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/shimmer-sweep.json @@ -0,0 +1,20 @@ +{ + "id": "shimmer-sweep", + "name": "Shimmer Sweep", + "description": "A subtle horizontal light sweep glides across the element from left to right, leaving the element fully visible after the sweep passes. The text itself doesn't move — only the highlight band does. Reads as premium / luxury polish.", + "target": "element", + "enter": { + "durationMs": 680, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 0, "backgroundPosition": "-100% 0%" }, + "to": { "opacity": 1, "backgroundPosition": "100% 0%" } + }, + "exit": { + "durationMs": 360, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1 }, + "to": { "opacity": 0 } + }, + "swap": { "mode": "crossfade", "overlapMs": 100, "microDelayMs": 0 }, + "notes": "Implementer: apply a linear-gradient background to the text (or its container) with `background-clip: text` so the shimmer reveals the text rather than a separate highlight bar. The shimmer band runs from -100% to 100% of background-position-x." +} diff --git a/skills/hyperframes/assets/text-effects/short-slide-down.json b/skills/hyperframes/assets/text-effects/short-slide-down.json new file mode 100644 index 000000000..380e8e60a --- /dev/null +++ b/skills/hyperframes/assets/text-effects/short-slide-down.json @@ -0,0 +1,23 @@ +{ + "id": "short-slide-down", + "name": "Short Slide Down", + "description": "Each word drops in from above and pushes the line stack downward — the line never moves; instead each new word arrives at the top of the stack and shoves what's already there. Useful for stacked vertical text where reveal order is meaningful.", + "target": "word", + "enter": { + "durationMs": 420, + "staggerMs": 160, + "easing": "cubic-bezier(0.18, 1, 0.32, 1)", + "from": { "opacity": 0, "y": -36 }, + "to": { "opacity": 1, "y": 0 } + }, + "exit": { + "durationMs": 320, + "staggerMs": 30, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "y": 0 }, + "to": { "opacity": 0, "y": 14 } + }, + "swap": { "mode": "crossfade", "overlapMs": 100, "microDelayMs": 0 }, + "layoutAware": true, + "notes": "Layout-aware: words must be stacked vertically (one per line), not horizontal. Implementer wraps each word in a block-display container so they push the next word down as they enter." +} diff --git a/skills/hyperframes/assets/text-effects/short-slide-right.json b/skills/hyperframes/assets/text-effects/short-slide-right.json new file mode 100644 index 000000000..357ad0ce2 --- /dev/null +++ b/skills/hyperframes/assets/text-effects/short-slide-right.json @@ -0,0 +1,27 @@ +{ + "id": "short-slide-right", + "name": "Short Slide Right", + "description": "The whole phrase glides in from the left as one shared move; individual words reveal only by opacity, not by sliding independently. The line settles to its centered position as words light up sequentially.", + "target": "word", + "enter": { + "durationMs": 560, + "staggerMs": 120, + "easing": "cubic-bezier(0.16, 1, 0.3, 1)", + "lineFrom": { "x": -48 }, + "lineTo": { "x": 0 }, + "from": { "opacity": 0 }, + "to": { "opacity": 1 } + }, + "exit": { + "durationMs": 360, + "staggerMs": 24, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "lineFrom": { "x": 0 }, + "lineTo": { "x": -16 }, + "from": { "opacity": 1 }, + "to": { "opacity": 0 } + }, + "swap": { "mode": "crossfade", "overlapMs": 120, "microDelayMs": 0 }, + "layoutAware": true, + "notes": "Layout-aware: animate the line container's x-position separately from each word's opacity. Words DO NOT slide individually — they only fade. Two GSAP tweens compose this effect: one on the line wrapper, one staggered across the word spans." +} diff --git a/skills/hyperframes/assets/text-effects/soft-blur-in.json b/skills/hyperframes/assets/text-effects/soft-blur-in.json new file mode 100644 index 000000000..372bfc16e --- /dev/null +++ b/skills/hyperframes/assets/text-effects/soft-blur-in.json @@ -0,0 +1,22 @@ +{ + "id": "soft-blur-in", + "name": "Soft Blur In", + "description": "Each character fades in with a gentle upward drift and a brief blur trail. Premium and atmospheric — best for hero headlines where the focal text should feel like it materializes rather than appears.", + "target": "char", + "enter": { + "durationMs": 700, + "staggerMs": 20, + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 0, "y": 14, "filter": "blur(8px)" }, + "to": { "opacity": 1, "y": 0, "filter": "blur(0px)" } + }, + "exit": { + "durationMs": 380, + "staggerMs": 8, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "y": 0, "filter": "blur(0px)" }, + "to": { "opacity": 0, "y": -6, "filter": "blur(4px)" } + }, + "swap": { "mode": "crossfade", "overlapMs": 120, "microDelayMs": 0 }, + "notes": "Pair with serif or display weights — the blur softens the silhouette and reads as polished. Avoid with mono fonts; the blur halo fights with the rigid grid." +} diff --git a/skills/hyperframes/assets/text-effects/specs/blur-out-up.json b/skills/hyperframes/assets/text-effects/specs/blur-out-up.json deleted file mode 100644 index c2cdf79ac..000000000 --- a/skills/hyperframes/assets/text-effects/specs/blur-out-up.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "blur-out-up", - "display_name": "Blur Out Up", - "description": "Words arrive clean and depart upward with increasing blur for airy exits.", - "inspiration": "Apple-style light typography where exit has more character than entry.", - "target": "per-word", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 560, - "stagger_ms": 28, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 10, - "blur_px": 6 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 480, - "stagger_ms": 24, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -14, - "blur_px": 8 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 170, - "micro_delay_ms": 35 - }, - "usage_notes": "Works best on short phrases; avoid very long lines to keep swap time tight." -} diff --git a/skills/hyperframes/assets/text-effects/specs/bottom-up-letters.json b/skills/hyperframes/assets/text-effects/specs/bottom-up-letters.json deleted file mode 100644 index 1241f8656..000000000 --- a/skills/hyperframes/assets/text-effects/specs/bottom-up-letters.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "id": "bottom-up-letters", - "display_name": "Bottom-Up Letters", - "description": "Letters rise from below in a pronounced staircase, one symbol at a time, with zero blur.", - "inspiration": "Apple-style keynote typography, sharp lower-thirds, and clean editorial word swaps.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.18, 1, 0.32, 1)", - "enter": { - "duration_ms": 400, - "stagger_ms": 88, - "easing": "cubic-bezier(0.18, 1, 0.32, 1)", - "from": { - "opacity": 0, - "y_px": 46 - }, - "to": { - "opacity": 1, - "y_px": 0 - } - }, - "exit": { - "duration_ms": 280, - "stagger_ms": 28, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "y_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -14 - } - }, - "swap": { - "mode": "sequential", - "overlap_ms": 0, - "micro_delay_ms": 35, - "scenario_spec": { - "entry_condition": "Use when short words or compact headlines should build upward letter by letter with completely crisp glyph edges.", - "switch_order": [ - "Run old text exit first so the slot clears cleanly.", - "Wait micro_delay_ms after exit.", - "Start new text enter from below with per-character stagger." - ], - "verification": [ - "Letters never blur during enter or exit.", - "The reveal clearly reads bottom-up rather than typewriter-left-to-right.", - "Spacing remains stable while characters settle." - ], - "fallback": { - "if_motion_feels_too_tall": "Reduce enter from.y_px from 46 to 36.", - "if_readability_drops": "Increase stagger_ms from 88 to 100 for even more separation." - } - } - }, - "usage_notes": "Best for short single words, labels, or compact headline swaps at 40px+. This version is intentionally more staged than per-character-rise: very large per-symbol delay, fewer simultaneous letters on screen, and a taller lift from below." -} diff --git a/skills/hyperframes/assets/text-effects/specs/depth-parallax-words.json b/skills/hyperframes/assets/text-effects/specs/depth-parallax-words.json deleted file mode 100644 index 2755d6937..000000000 --- a/skills/hyperframes/assets/text-effects/specs/depth-parallax-words.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "id": "depth-parallax-words", - "display_name": "Depth Parallax Words", - "description": "Per-word depth motion with scale and vertical drift for layered readability.", - "inspiration": "Product landing pages combining depth cues with clean typography.", - "target": "per-word", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 700, - "stagger_ms": 70, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 18, - "scale": 0.92, - "blur_px": 3 - }, - "to": { - "opacity": 1, - "y_px": 0, - "scale": 1, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 500, - "stagger_ms": 45, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "scale": 1, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -10, - "scale": 1.05, - "blur_px": 2 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 180, - "micro_delay_ms": 30 - }, - "usage_notes": "Use short copy blocks and moderate stagger to avoid visual overload." -} diff --git a/skills/hyperframes/assets/text-effects/specs/fade-through.json b/skills/hyperframes/assets/text-effects/specs/fade-through.json deleted file mode 100644 index f5035f59f..000000000 --- a/skills/hyperframes/assets/text-effects/specs/fade-through.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "id": "fade-through", - "display_name": "Fade Through", - "description": "A Material-style content transition: old fades out, new fades in with a soft delay.", - "inspiration": "Google Material fade through transitions for same-level UI changes.", - "target": "whole", - "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", - "enter": { - "duration_ms": 420, - "stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0, 0, 1)", - "from": { - "opacity": 0, - "y_px": 6, - "scale": 0.99, - "blur_px": 2 - }, - "to": { - "opacity": 1, - "y_px": 0, - "scale": 1, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 260, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 1, 1)", - "from": { - "opacity": 1, - "y_px": 0, - "scale": 1, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -4, - "scale": 1, - "blur_px": 0 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 20, - "micro_delay_ms": 60 - }, - "usage_notes": "Best for replacing content in the same layout slot without directional meaning." -} diff --git a/skills/hyperframes/assets/text-effects/specs/focus-blur-resolve.json b/skills/hyperframes/assets/text-effects/specs/focus-blur-resolve.json deleted file mode 100644 index d92202348..000000000 --- a/skills/hyperframes/assets/text-effects/specs/focus-blur-resolve.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "id": "focus-blur-resolve", - "display_name": "Focus Blur Resolve", - "description": "A premium focus pull from heavy blur to crisp text, then a soft blur-out exit.", - "inspiration": "Apple-style hero transitions that resolve detail with cinematic restraint.", - "target": "whole", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 760, - "stagger_ms": 0, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 14, - "blur_px": 14, - "scale": 1.01 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0, - "scale": 1 - } - }, - "exit": { - "duration_ms": 520, - "stagger_ms": 0, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0, - "scale": 1 - }, - "to": { - "opacity": 0, - "y_px": -10, - "blur_px": 10, - "scale": 1 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 160, - "micro_delay_ms": 35 - }, - "usage_notes": "Best on large headlines where blur distance reads as intentional and premium." -} diff --git a/skills/hyperframes/assets/text-effects/specs/kinetic-center-build.json b/skills/hyperframes/assets/text-effects/specs/kinetic-center-build.json deleted file mode 100644 index 6b2f02df4..000000000 --- a/skills/hyperframes/assets/text-effects/specs/kinetic-center-build.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "id": "kinetic-center-build", - "display_name": "Kinetic Center Build", - "description": "A word appears in the center; each new word enters from right to left with a soft blur and pushes the existing line until the full phrase locks centered.", - "inspiration": "Apple keynote kinetic editorial typography and sequential phrase builds.", - "target": "per-word", - "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "enter": { - "duration_ms": 360, - "stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "from": { - "opacity": 0, - "y_px": 6, - "scale": 0.992, - "blur_px": 3.5 - }, - "to": { - "opacity": 1, - "y_px": 0, - "scale": 1, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 260, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 0.2, 1)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -6, - "blur_px": 2.5 - } - }, - "swap": { - "mode": "sequential", - "overlap_ms": 0, - "micro_delay_ms": 220, - "scenario_spec": { - "entry_condition": "Use when a short phrase should be built word-by-word, with each new word entering from the right and physically re-centering the existing line.", - "switch_order": [ - "Show the first word in the center.", - "Bring the second word in from right to left while shifting the first word left.", - "Bring the third word in from right to left while shifting the first two words so the final phrase stays centered." - ], - "verification": [ - "Each new word visibly pushes the existing words rather than simply fading in.", - "The completed phrase ends centered and evenly spaced.", - "The motion reads as one kinetic line build, not as three isolated reveals." - ], - "fallback": { - "if_push_is_too_subtle": "Increase build.entry_offset_px from 96 to 120.", - "if_phrase_feels_too_slow": "Reduce build.push_duration_ms from 480 to 420." - } - } - }, - "build": { - "entry_direction": "from-right", - "line_alignment": "center", - "first_word_duration_ms": 340, - "push_duration_ms": 430, - "entry_offset_px": 88, - "word_gap_px": 10, - "first_word_y_px": 6, - "entry_scale": 0.992, - "entry_blur_px": 3.5, - "reflow_blur_px": 0.8, - "exit_y_px": -6, - "exit_blur_px": 2.5, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)", - "phrase_samples": [ - ["Words", "push", "left"], - ["Type", "locks", "center"], - ["Build", "the", "line"] - ] - }, - "usage_notes": "Layout-aware effect: each incoming word changes the target x-position of the whole line. Best for short three-word phrases; implementation requires measuring word widths and animating existing words to new positions. A small entry and reflow blur helps the push feel smoother without extending the timing." -} diff --git a/skills/hyperframes/assets/text-effects/specs/line-by-line-slide.json b/skills/hyperframes/assets/text-effects/specs/line-by-line-slide.json deleted file mode 100644 index 500bee8eb..000000000 --- a/skills/hyperframes/assets/text-effects/specs/line-by-line-slide.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "line-by-line-slide", - "display_name": "Line-by-Line Slide", - "description": "Each line enters from the left with a staggered slide and exits to the right for a flowing paragraph reveal.", - "inspiration": "Apple landing page subheads and section headers that breathe line by line.", - "target": "per-line", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 900, - "stagger_ms": 120, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "x_px": -48 - }, - "to": { - "opacity": 1, - "x_px": 0 - } - }, - "exit": { - "duration_ms": 600, - "stagger_ms": 80, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "x_px": 0 - }, - "to": { - "opacity": 0, - "x_px": 48 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 20 - }, - "usage_notes": "Great for 2-line or 3-line headings. This variant keeps swap non-overlapping to avoid content intersections. Reduce x-distance for narrow layouts to keep motion tight on mobile." -} diff --git a/skills/hyperframes/assets/text-effects/specs/mask-reveal-up.json b/skills/hyperframes/assets/text-effects/specs/mask-reveal-up.json deleted file mode 100644 index da8017a66..000000000 --- a/skills/hyperframes/assets/text-effects/specs/mask-reveal-up.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "mask-reveal-up", - "display_name": "Mask Reveal Up", - "description": "Lines reveal upward with a soft masked feel and compact stagger.", - "inspiration": "Apple section transitions where multiline copy rises in with control.", - "target": "per-line", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 760, - "stagger_ms": 90, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 30, - "blur_px": 6 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 520, - "stagger_ms": 70, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -22, - "blur_px": 6 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 210, - "micro_delay_ms": 35 - }, - "usage_notes": "Best for two-line and three-line headings where line order should stay readable." -} diff --git a/skills/hyperframes/assets/text-effects/specs/micro-scale-fade.json b/skills/hyperframes/assets/text-effects/specs/micro-scale-fade.json deleted file mode 100644 index 009a0492a..000000000 --- a/skills/hyperframes/assets/text-effects/specs/micro-scale-fade.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "micro-scale-fade", - "display_name": "Micro Scale Fade", - "description": "A calm, tiny scale pop used as subtle premium polish for labels and headings.", - "inspiration": "Apple system status copy, secondary UI labels, and lightweight onboarding micro-animations.", - "target": "whole", - "signature_easing": "cubic-bezier(0.32, 0.72, 0, 1)", - "enter": { - "duration_ms": 600, - "stagger_ms": 0, - "easing": "cubic-bezier(0.32, 0.72, 0, 1)", - "from": { - "opacity": 0, - "scale": 0.96 - }, - "to": { - "opacity": 1, - "scale": 1 - } - }, - "exit": { - "duration_ms": 400, - "stagger_ms": 0, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "scale": 1 - }, - "to": { - "opacity": 0, - "scale": 0.96 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 20 - }, - "usage_notes": "Use this for single words or short titles. This variant keeps swap non-overlapping to avoid content intersections. For paragraphs, switch target to per-word to avoid perceivable lag." -} diff --git a/skills/hyperframes/assets/text-effects/specs/per-character-rise.json b/skills/hyperframes/assets/text-effects/specs/per-character-rise.json deleted file mode 100644 index eb4bbf905..000000000 --- a/skills/hyperframes/assets/text-effects/specs/per-character-rise.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "id": "per-character-rise", - "display_name": "Per-Character Rise", - "description": "Letters slide up from below with no blur — crisp, deliberate, kinetic. Apple's clean tvOS-style reveal.", - "inspiration": "Apple tvOS, Fitness+ intros, iPadOS home screen title appearances.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "enter": { - "duration_ms": 700, - "stagger_ms": 24, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "from": { - "opacity": 0, - "y_px": 32 - }, - "to": { - "opacity": 1, - "y_px": 0 - } - }, - "exit": { - "duration_ms": 420, - "stagger_ms": 14, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "y_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -24 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 210, - "scenario_spec": { - "entry_condition": "Use for headline replacement where each character must remain crisp and readable throughout the switch.", - "switch_order": [ - "Start old text exit at t=0ms.", - "Start new text enter at t=exit_total_ms-overlap_ms.", - "Use a single active headline layer after enter starts to avoid stacked glyph artifacts." - ], - "verification": [ - "Characters never blur during swap.", - "No visible pause appears between exit and enter phases.", - "Swap keeps staircase rhythm from stagger settings." - ], - "fallback": { - "if_glyphs_collide": "Lower overlap_ms to 140.", - "if_motion_feels_slow": "Reduce enter stagger_ms from 24 to 18." - } - } - }, - "usage_notes": "Works on 40px+ headlines. Zero blur keeps it sharp — that's the key distinction from soft-blur-in. Stagger 24ms gives it quicker momentum; don't go below 16ms or it flattens." -} diff --git a/skills/hyperframes/assets/text-effects/specs/per-word-crossfade.json b/skills/hyperframes/assets/text-effects/specs/per-word-crossfade.json deleted file mode 100644 index 020151aa0..000000000 --- a/skills/hyperframes/assets/text-effects/specs/per-word-crossfade.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "id": "per-word-crossfade", - "display_name": "Per-Word Crossfade", - "description": "Words gently fade into place one after another, with a short vertical drift for a calm keynote rhythm.", - "inspiration": "Apple product announcements and section title transitions where words are readable but still alive.", - "target": "per-word", - "signature_easing": "cubic-bezier(0.16, 1, 0.3, 1)", - "enter": { - "duration_ms": 700, - "stagger_ms": 70, - "easing": "cubic-bezier(0.16, 1, 0.3, 1)", - "from": { - "opacity": 0, - "y_px": 8 - }, - "to": { - "opacity": 1, - "y_px": 0 - } - }, - "exit": { - "duration_ms": 500, - "stagger_ms": 40, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "y_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -6 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 170, - "micro_delay_ms": 70, - "scenario_spec": { - "entry_condition": "Use when phrase-level content changes and word readability is more important than per-character flair.", - "switch_order": [ - "Start old text exit at t=0ms.", - "Start new text enter at t=exit_total_ms-overlap_ms+micro_delay_ms.", - "Advance word groups in the same stagger direction for old and new text." - ], - "verification": [ - "Word boundaries stay readable during overlap.", - "No two identical word positions stay stacked for more than one stagger step.", - "Swap cadence stays calm and editorial, without abrupt jumps." - ], - "fallback": { - "if_words_stack_visibly": "Increase micro_delay_ms to 90.", - "if_total_swap_is_too_long": "Reduce enter stagger_ms to 55 and overlap_ms to 120." - } - } - }, - "usage_notes": "Best for medium phrases and headings; for long copy prefer per-word only up to 16–18 words to keep total stagger time readable. micro_delay_ms helps prevent old/new words from visibly stacking during swaps." -} diff --git a/skills/hyperframes/assets/text-effects/specs/scale-down-fade.json b/skills/hyperframes/assets/text-effects/specs/scale-down-fade.json deleted file mode 100644 index 2eae5ab03..000000000 --- a/skills/hyperframes/assets/text-effects/specs/scale-down-fade.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "scale-down-fade", - "display_name": "Scale Down Fade", - "description": "Subtle premium settle-in with a restrained scale-down fade on exit.", - "inspiration": "Apple product copy transitions where motion remains quiet and precise.", - "target": "whole", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 520, - "stagger_ms": 0, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 8, - "scale": 1.04 - }, - "to": { - "opacity": 1, - "y_px": 0, - "scale": 1 - } - }, - "exit": { - "duration_ms": 380, - "stagger_ms": 0, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "scale": 1 - }, - "to": { - "opacity": 0, - "y_px": -8, - "scale": 0.94 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 130, - "micro_delay_ms": 20 - }, - "usage_notes": "Safe default for product UIs where copy should feel polished but not animated." -} diff --git a/skills/hyperframes/assets/text-effects/specs/shared-axis-x.json b/skills/hyperframes/assets/text-effects/specs/shared-axis-x.json deleted file mode 100644 index 7ed127236..000000000 --- a/skills/hyperframes/assets/text-effects/specs/shared-axis-x.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "shared-axis-x", - "display_name": "Shared Axis X", - "description": "Horizontal shared-axis transition for sibling destinations with continuity.", - "inspiration": "Google Material shared axis (X) transitions.", - "target": "whole", - "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", - "enter": { - "duration_ms": 500, - "stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0, 0, 1)", - "from": { - "opacity": 0, - "x_px": 24, - "scale": 0.98 - }, - "to": { - "opacity": 1, - "x_px": 0, - "scale": 1 - } - }, - "exit": { - "duration_ms": 360, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 1, 1)", - "from": { - "opacity": 1, - "x_px": 0, - "scale": 1 - }, - "to": { - "opacity": 0, - "x_px": -20, - "scale": 0.98 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 120, - "micro_delay_ms": 20 - }, - "usage_notes": "Use when moving between same-level views where horizontal direction conveys progress." -} diff --git a/skills/hyperframes/assets/text-effects/specs/shared-axis-y.json b/skills/hyperframes/assets/text-effects/specs/shared-axis-y.json deleted file mode 100644 index 662b8cc8f..000000000 --- a/skills/hyperframes/assets/text-effects/specs/shared-axis-y.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "shared-axis-y", - "display_name": "Word Cut Staircase", - "description": "Per-word hard-cut transition with staircase timing for sharp editorial swaps.", - "inspiration": "Hard-cut typography timing with stepped word sequencing.", - "target": "per-word", - "signature_easing": "steps(1, end)", - "enter": { - "duration_ms": 180, - "stagger_ms": 78, - "easing": "steps(1, end)", - "from": { - "opacity": 0, - "y_px": 0, - "scale": 1 - }, - "to": { - "opacity": 1, - "y_px": 0, - "scale": 1 - } - }, - "exit": { - "duration_ms": 140, - "stagger_ms": 78, - "easing": "steps(1, end)", - "from": { - "opacity": 1, - "y_px": 0, - "scale": 1 - }, - "to": { - "opacity": 0, - "y_px": 0, - "scale": 1 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 28 - }, - "usage_notes": "Use for bold word-by-word hard cuts. No overlap keeps phrase swaps visually clean." -} diff --git a/skills/hyperframes/assets/text-effects/specs/shared-axis-z.json b/skills/hyperframes/assets/text-effects/specs/shared-axis-z.json deleted file mode 100644 index fc1a23f3e..000000000 --- a/skills/hyperframes/assets/text-effects/specs/shared-axis-z.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "shared-axis-z", - "display_name": "Shared Axis Z", - "description": "Scale-based shared-axis transition for focus shifts and context depth.", - "inspiration": "Google Material shared axis (Z), adapted for typography swaps.", - "target": "whole", - "signature_easing": "cubic-bezier(0.2, 0, 0, 1)", - "enter": { - "duration_ms": 520, - "stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0, 0, 1)", - "from": { - "opacity": 0, - "scale": 0.9, - "blur_px": 2 - }, - "to": { - "opacity": 1, - "scale": 1, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 360, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 1, 1)", - "from": { - "opacity": 1, - "scale": 1, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "scale": 1.06, - "blur_px": 1 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 100, - "micro_delay_ms": 20 - }, - "usage_notes": "Use for emphasizing focus transitions where scale communicates depth." -} diff --git a/skills/hyperframes/assets/text-effects/specs/shimmer-sweep.json b/skills/hyperframes/assets/text-effects/specs/shimmer-sweep.json deleted file mode 100644 index 6dea8f43e..000000000 --- a/skills/hyperframes/assets/text-effects/specs/shimmer-sweep.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "shimmer-sweep", - "display_name": "Shimmer Sweep", - "description": "A subtle sweep across a clean headline, blending in while gliding from left to center.", - "inspiration": "Premium hero copy transitions where a short soft push is used before settle.", - "target": "whole", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 850, - "stagger_ms": 0, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "x_px": -22, - "blur_px": 8 - }, - "to": { - "opacity": 1, - "x_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 650, - "stagger_ms": 0, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "x_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "x_px": 22, - "blur_px": 8 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 36 - }, - "usage_notes": "Use as a premium micro-transition for title swaps and copy refreshes. This variant avoids overlap between outgoing and incoming text." -} diff --git a/skills/hyperframes/assets/text-effects/specs/short-slide-down.json b/skills/hyperframes/assets/text-effects/specs/short-slide-down.json deleted file mode 100644 index ef2900f7f..000000000 --- a/skills/hyperframes/assets/text-effects/specs/short-slide-down.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "id": "short-slide-down", - "display_name": "Short Slide Down", - "description": "Each new word drops in from above into its own line and pushes the existing stack downward until a centered three-line composition locks in place.", - "inspiration": "Keynote-style editorial headings where motion is present but tightly restrained.", - "target": "per-word", - "custom_renderer": "kinetic-top-build", - "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "enter": { - "duration_ms": 520, - "stagger_ms": 0, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "from": { - "opacity": 0, - "y_px": -24, - "blur_px": 2.4, - "scale": 0.992 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0, - "scale": 1 - } - }, - "exit": { - "duration_ms": 320, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 0.2, 1)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0, - "scale": 1 - }, - "to": { - "opacity": 0, - "y_px": 10, - "blur_px": 1.2, - "scale": 1 - } - }, - "build": { - "first_word_duration_ms": 360, - "push_duration_ms": 500, - "exit_duration_ms": 320, - "hold_ms": 1100, - "between_phrases_ms": 180, - "entry_offset_y_px": -28, - "line_gap_px": 12, - "first_word_y_px": -14, - "entry_scale": 0.992, - "entry_blur_px": 2.4, - "reflow_blur_px": 0.7, - "exit_y_px": 10, - "exit_blur_px": 1.2, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "exit_easing": "cubic-bezier(0.4, 0, 0.2, 1)" - }, - "swap": { - "mode": "sequential", - "overlap_ms": 0, - "micro_delay_ms": 70, - "scenario_spec": { - "entry_condition": "Use when three short words should build into a vertical stack, with each new word dropping from above and physically re-centering the composition.", - "switch_order": [ - "Show the first word in the center with a short top-down drop.", - "Bring the second word into a lower line while shifting the first word upward into the stack.", - "Bring the third word into the bottom line while shifting the first two words upward so the final three-line stack stays centered." - ], - "verification": [ - "Each new word visibly pushes the existing words rather than simply fading in.", - "The completed phrase ends as three centered lines with even vertical spacing.", - "The motion reads as one kinetic stacked build with a top-down entry direction." - ], - "fallback": { - "if_drop_is_too_subtle": "Increase build.entry_offset_y_px from -28 to -36.", - "if_phrase_feels_too_slow": "Reduce build.push_duration_ms from 500 to 460." - } - } - }, - "usage_notes": "Best on short three-word headings where each word can live on its own line. Keep the vertical drop compact so the motion still feels editorial, and let the stacking displacement carry most of the energy. For longer phrases, reduce entry_offset_y_px or switch to a softer shared-slide pattern." -} diff --git a/skills/hyperframes/assets/text-effects/specs/short-slide-right.json b/skills/hyperframes/assets/text-effects/specs/short-slide-right.json deleted file mode 100644 index 5b91954c2..000000000 --- a/skills/hyperframes/assets/text-effects/specs/short-slide-right.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "id": "short-slide-right", - "display_name": "Short Slide Right", - "description": "The whole phrase glides in from the left as one compact move, while the words themselves are revealed in sequence only through opacity.", - "inspiration": "Keynote-style editorial headings where motion is present but tightly restrained.", - "target": "per-word", - "custom_renderer": "shared-slide-opacity-stage", - "signature_easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "enter": { - "duration_ms": 520, - "stagger_ms": 92, - "easing": "cubic-bezier(0.2, 0.8, 0.2, 1)", - "from": { - "opacity": 1, - "x_px": -24, - "blur_px": 1.2 - }, - "to": { - "opacity": 1, - "x_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 320, - "stagger_ms": 0, - "easing": "cubic-bezier(0.4, 0, 0.2, 1)", - "from": { - "opacity": 1, - "x_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "x_px": 12, - "blur_px": 1 - } - }, - "build": { - "word_opacity_duration_ms": 210, - "word_opacity_from": 0, - "word_opacity_to": 1 - }, - "swap": { - "mode": "sequential", - "overlap_ms": 0, - "micro_delay_ms": 70, - "scenario_spec": { - "entry_condition": "Use when the heading should feel like one shared horizontal motion, but the words should reveal progressively.", - "switch_order": [ - "Start the whole phrase from one shared left offset.", - "Animate the phrase transform once, with no per-word positional delay.", - "Reveal each word with only opacity stagger so the ordering reads clearly." - ], - "verification": [ - "The phrase position starts and ends in sync for all words.", - "Only opacity is staggered across the words.", - "The amplitude stays compact enough to feel controlled, not swishy." - ], - "fallback": { - "if_motion_feels_too_wide": "Reduce enter.from.x_px from -24 to -18.", - "if_reveal_reads_too_fast": "Increase enter.stagger_ms from 92 to 108.", - "if_words_feel_too_ghosted": "Increase build.word_opacity_duration_ms from 210 to 240." - } - } - }, - "usage_notes": "Best on three-word headings where word order matters. Keep the horizontal travel compact and shared; the phrase should read as one move, with staging communicated only by opacity. For longer phrases, reduce stagger_ms or shorten the opacity duration so the cascade does not drag." -} diff --git a/skills/hyperframes/assets/text-effects/specs/soft-blur-in.json b/skills/hyperframes/assets/text-effects/specs/soft-blur-in.json deleted file mode 100644 index 8e9143191..000000000 --- a/skills/hyperframes/assets/text-effects/specs/soft-blur-in.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "id": "soft-blur-in", - "display_name": "Soft Blur", - "description": "Per-character fade-in with a gentle blur and upward motion. Apple's signature hero-title reveal.", - "inspiration": "Apple keynote intros; iPhone, Mac, and Vision Pro product page headlines; macOS system UI reveals.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "enter": { - "duration_ms": 900, - "stagger_ms": 25, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 16, - "blur_px": 12 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 600, - "stagger_ms": 15, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -16, - "blur_px": 12 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 300, - "scenario_spec": { - "entry_condition": "Use when text is replaced in the same layout slot and both strings remain visually stable in one block.", - "switch_order": [ - "Start old text exit at t=0ms.", - "Start new text enter at t=exit_total_ms-overlap_ms.", - "Keep both text layers mounted only during the overlap window." - ], - "verification": [ - "No hard-cut frame appears between old and new text.", - "Blur stays readable during overlap on desktop and mobile.", - "Total swap duration remains below 1300ms for default sample length." - ], - "fallback": { - "if_overlap_looks_heavy": "Reduce overlap_ms to 180 and exit blur_px to 8.", - "if_copy_is_long": "Switch target to per-word and reduce enter stagger_ms to 15." - } - } - }, - "usage_notes": "Works best on hero titles 48px+ against solid backgrounds. On body text (<24px), reduce blur_px to 6 and stagger_ms to 15. Avoid on very long strings (>40 chars) — total stagger becomes too long; in that case switch target to 'per-word'." -} diff --git a/skills/hyperframes/assets/text-effects/specs/spring-scale-in.json b/skills/hyperframes/assets/text-effects/specs/spring-scale-in.json deleted file mode 100644 index 11ed9d668..000000000 --- a/skills/hyperframes/assets/text-effects/specs/spring-scale-in.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "spring-scale-in", - "display_name": "Spring Scale In", - "description": "Words pop in with a soft overshoot scale, like a physical spring settling into place.", - "inspiration": "iOS app icons bouncing into the home screen, macOS Dock, widget appearances, Vision Pro floating UI pops.", - "target": "per-word", - "signature_easing": "cubic-bezier(0.34, 1.56, 0.64, 1)", - "enter": { - "duration_ms": 360, - "stagger_ms": 95, - "easing": "cubic-bezier(0.34, 1.56, 0.64, 1)", - "from": { - "opacity": 0, - "scale": 0.7 - }, - "to": { - "opacity": 1, - "scale": 1 - } - }, - "exit": { - "duration_ms": 200, - "stagger_ms": 80, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "scale": 1 - }, - "to": { - "opacity": 0, - "scale": 0.8 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 35 - }, - "usage_notes": "The overshoot comes from cubic-bezier y2 > 1 (1.56). Per-word is the sweet spot - per-character at this easing feels too bouncy. Stagger is intentionally high here to create a visible staircase effect. This variant uses no overlap on swap to avoid content crossing during transitions." -} diff --git a/skills/hyperframes/assets/text-effects/specs/stagger-from-center.json b/skills/hyperframes/assets/text-effects/specs/stagger-from-center.json deleted file mode 100644 index 6f07e6d9e..000000000 --- a/skills/hyperframes/assets/text-effects/specs/stagger-from-center.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "id": "stagger-from-center", - "display_name": "Stagger from Center", - "description": "Characters reveal from the center outward to emphasize the keyword core.", - "inspiration": "Product hero typography where center-weighted emphasis drives attention.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "stagger_mode": "center-out", - "enter": { - "duration_ms": 620, - "stagger_ms": 22, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 12, - "blur_px": 3 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 420, - "stagger_ms": 16, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -8, - "blur_px": 3 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 150, - "micro_delay_ms": 20 - }, - "usage_notes": "Use on short words or compact titles; long text reduces the center-emphasis effect." -} diff --git a/skills/hyperframes/assets/text-effects/specs/stagger-from-edges.json b/skills/hyperframes/assets/text-effects/specs/stagger-from-edges.json deleted file mode 100644 index b67437ac7..000000000 --- a/skills/hyperframes/assets/text-effects/specs/stagger-from-edges.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "id": "stagger-from-edges", - "display_name": "Stagger from Edges", - "description": "Characters start from both edges and converge toward the center.", - "inspiration": "Directional typography reveals used in modern product hero systems.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "stagger_mode": "edges-in", - "enter": { - "duration_ms": 620, - "stagger_ms": 22, - "easing": "cubic-bezier(0.22, 1, 0.36, 1)", - "from": { - "opacity": 0, - "y_px": 12, - "blur_px": 3 - }, - "to": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - } - }, - "exit": { - "duration_ms": 420, - "stagger_ms": 16, - "easing": "cubic-bezier(0.64, 0, 0.78, 0)", - "from": { - "opacity": 1, - "y_px": 0, - "blur_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -8, - "blur_px": 3 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 150, - "micro_delay_ms": 20 - }, - "usage_notes": "Effective for medium word lengths where edge-to-center motion remains readable." -} diff --git a/skills/hyperframes/assets/text-effects/specs/top-down-letters.json b/skills/hyperframes/assets/text-effects/specs/top-down-letters.json deleted file mode 100644 index e255459da..000000000 --- a/skills/hyperframes/assets/text-effects/specs/top-down-letters.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "id": "top-down-letters", - "display_name": "Top-Down Letters", - "description": "Letters descend from above in a pronounced staircase, one symbol at a time, with zero blur.", - "inspiration": "Apple-style keynote typography, crisp editorial headers, and controlled top-down word reveals.", - "target": "per-character", - "signature_easing": "cubic-bezier(0.18, 1, 0.32, 1)", - "enter": { - "duration_ms": 400, - "stagger_ms": 88, - "easing": "cubic-bezier(0.18, 1, 0.32, 1)", - "from": { - "opacity": 0, - "y_px": -46 - }, - "to": { - "opacity": 1, - "y_px": 0 - } - }, - "exit": { - "duration_ms": 280, - "stagger_ms": 28, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "y_px": 0 - }, - "to": { - "opacity": 0, - "y_px": 14 - } - }, - "swap": { - "mode": "sequential", - "overlap_ms": 0, - "micro_delay_ms": 35, - "scenario_spec": { - "entry_condition": "Use when short words or compact headlines should build downward letter by letter with completely crisp glyph edges.", - "switch_order": [ - "Run old text exit first so the slot clears cleanly.", - "Wait micro_delay_ms after exit.", - "Start new text enter from above with per-character stagger." - ], - "verification": [ - "Letters never blur during enter or exit.", - "The reveal clearly reads top-down rather than typewriter-left-to-right.", - "Spacing remains stable while characters settle." - ], - "fallback": { - "if_motion_feels_too_tall": "Reduce enter from.y_px from -46 to -36.", - "if_readability_drops": "Increase stagger_ms from 88 to 100 for even more separation." - } - } - }, - "usage_notes": "Best for short single words, labels, or compact headline swaps at 40px+. This is the top-down counterpart to bottom-up-letters: very large per-symbol delay, fewer simultaneous letters on screen, and a tall drop from above." -} diff --git a/skills/hyperframes/assets/text-effects/specs/typewriter.json b/skills/hyperframes/assets/text-effects/specs/typewriter.json deleted file mode 100644 index df5070f80..000000000 --- a/skills/hyperframes/assets/text-effects/specs/typewriter.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "typewriter", - "display_name": "Typewriter", - "description": "Per-character stepped reveal with a minimal editorial typing rhythm.", - "inspiration": "System-like text build patterns in Apple presentation and utility UI.", - "target": "per-character", - "signature_easing": "steps(1, end)", - "enter": { - "duration_ms": 240, - "stagger_ms": 46, - "easing": "steps(1, end)", - "from": { - "opacity": 0, - "y_px": 0 - }, - "to": { - "opacity": 1, - "y_px": 0 - } - }, - "exit": { - "duration_ms": 260, - "stagger_ms": 10, - "easing": "cubic-bezier(0.7, 0, 0.84, 0)", - "from": { - "opacity": 1, - "y_px": 0 - }, - "to": { - "opacity": 0, - "y_px": -4 - } - }, - "swap": { - "mode": "crossfade", - "overlap_ms": 0, - "micro_delay_ms": 85 - }, - "usage_notes": "Good for short copy. Keep line length moderate so stepping stays intentional." -} diff --git a/skills/hyperframes/assets/text-effects/spring-scale-in.json b/skills/hyperframes/assets/text-effects/spring-scale-in.json new file mode 100644 index 000000000..cca6a50de --- /dev/null +++ b/skills/hyperframes/assets/text-effects/spring-scale-in.json @@ -0,0 +1,22 @@ +{ + "id": "spring-scale-in", + "name": "Spring Scale In", + "description": "Words pop in with a spring overshoot — scale runs 0.4 → 1.06 → 1.0. Physical and slightly playful; the overshoot reads as bouncy without crossing into cartoonish.", + "target": "word", + "enter": { + "durationMs": 280, + "staggerMs": 80, + "easing": "cubic-bezier(0.34, 1.56, 0.64, 1)", + "from": { "opacity": 0, "scale": 0.4 }, + "to": { "opacity": 1, "scale": 1 } + }, + "exit": { + "durationMs": 240, + "staggerMs": 20, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "scale": 1 }, + "to": { "opacity": 0, "scale": 0.8 } + }, + "swap": { "mode": "crossfade", "overlapMs": 80, "microDelayMs": 0 }, + "notes": "The spring easing's overshoot (1.56) lands at ~6% past the target before settling. Pair with playful brands or product launches — avoid for clinical / enterprise contexts." +} diff --git a/skills/hyperframes/assets/text-effects/stagger-from-center.json b/skills/hyperframes/assets/text-effects/stagger-from-center.json new file mode 100644 index 000000000..12ddfc72a --- /dev/null +++ b/skills/hyperframes/assets/text-effects/stagger-from-center.json @@ -0,0 +1,24 @@ +{ + "id": "stagger-from-center", + "name": "Stagger From Center", + "description": "Characters reveal outward from the center of the headline — middle glyph appears first, edges last. Emphasizes the keyword's core; useful for short slogans where the central word carries the punch.", + "target": "char", + "enter": { + "durationMs": 480, + "staggerMs": 32, + "staggerOrder": "center-out", + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 0, "y": 12, "scale": 0.92 }, + "to": { "opacity": 1, "y": 0, "scale": 1 } + }, + "exit": { + "durationMs": 340, + "staggerMs": 14, + "staggerOrder": "center-out", + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "y": 0, "scale": 1 }, + "to": { "opacity": 0, "y": -8, "scale": 0.94 } + }, + "swap": { "mode": "crossfade", "overlapMs": 100, "microDelayMs": 0 }, + "notes": "Implementer-only: stagger order is center-out, NOT DOM order. Compute each character's rank as `|index - centerIndex|`, ties resolved by lower index first." +} diff --git a/skills/hyperframes/assets/text-effects/stagger-from-edges.json b/skills/hyperframes/assets/text-effects/stagger-from-edges.json new file mode 100644 index 000000000..6e3b1a70b --- /dev/null +++ b/skills/hyperframes/assets/text-effects/stagger-from-edges.json @@ -0,0 +1,24 @@ +{ + "id": "stagger-from-edges", + "name": "Stagger From Edges", + "description": "Mirror of stagger-from-center — edge characters appear first, center last. The headline assembles inward toward its keyword core; emphasizes that the middle word is the destination.", + "target": "char", + "enter": { + "durationMs": 480, + "staggerMs": 32, + "staggerOrder": "edges-in", + "easing": "cubic-bezier(0.22, 1, 0.36, 1)", + "from": { "opacity": 0, "y": 12, "scale": 0.92 }, + "to": { "opacity": 1, "y": 0, "scale": 1 } + }, + "exit": { + "durationMs": 340, + "staggerMs": 14, + "staggerOrder": "edges-in", + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "y": 0, "scale": 1 }, + "to": { "opacity": 0, "y": -8, "scale": 0.94 } + }, + "swap": { "mode": "crossfade", "overlapMs": 100, "microDelayMs": 0 }, + "notes": "Implementer-only: stagger order is edges-in. Compute each character's rank as `(text.length - 1) / 2 - |index - centerIndex|`, ties resolved by higher index first." +} diff --git a/skills/hyperframes/assets/text-effects/top-down-letters.json b/skills/hyperframes/assets/text-effects/top-down-letters.json new file mode 100644 index 000000000..4b23e770f --- /dev/null +++ b/skills/hyperframes/assets/text-effects/top-down-letters.json @@ -0,0 +1,22 @@ +{ + "id": "top-down-letters", + "name": "Top-Down Letters", + "description": "Mirror of bottom-up-letters — characters descend from above the baseline rather than rising from below. Use when the layout wants downward energy or when the headline is positioned below another anchored element.", + "target": "char", + "enter": { + "durationMs": 320, + "staggerMs": 65, + "easing": "cubic-bezier(0.18, 1, 0.32, 1)", + "from": { "opacity": 0, "y": -56 }, + "to": { "opacity": 1, "y": 0 } + }, + "exit": { + "durationMs": 280, + "staggerMs": 14, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "y": 0 }, + "to": { "opacity": 0, "y": 22 } + }, + "swap": { "mode": "crossfade", "overlapMs": 80, "microDelayMs": 0 }, + "notes": "Choose between bottom-up and top-down based on layout pull, not by default. A headline anchored to the bottom of frame benefits from top-down energy; one anchored at top benefits from bottom-up." +} diff --git a/skills/hyperframes/assets/text-effects/typewriter.json b/skills/hyperframes/assets/text-effects/typewriter.json new file mode 100644 index 000000000..4c333557a --- /dev/null +++ b/skills/hyperframes/assets/text-effects/typewriter.json @@ -0,0 +1,22 @@ +{ + "id": "typewriter", + "name": "Typewriter", + "description": "Per-character stepped reveal that types each glyph in sequence. No interpolation — characters pop into existence at discrete intervals. Reads as mechanical, editorial, or terminal-style.", + "target": "char", + "enter": { + "durationMs": 200, + "staggerMs": 50, + "easing": "steps(1, end)", + "from": { "opacity": 0 }, + "to": { "opacity": 1 } + }, + "exit": { + "durationMs": 240, + "staggerMs": 12, + "easing": "cubic-bezier(0.7, 0, 0.84, 0)", + "from": { "opacity": 1, "y": 0 }, + "to": { "opacity": 0, "y": -4 } + }, + "swap": { "mode": "crossfade", "overlapMs": 0, "microDelayMs": 80 }, + "notes": "Pair with a blinking caret element if you want the full terminal feel. The 50ms stagger lands around 1200 chars/min — fast but legible. Increase to 80-100ms if you want a deliberate typing pace." +} diff --git a/skills/hyperframes/references/text-effects.md b/skills/hyperframes/references/text-effects.md index 0d5595e5f..20d579864 100644 --- a/skills/hyperframes/references/text-effects.md +++ b/skills/hyperframes/references/text-effects.md @@ -2,104 +2,150 @@ 24 named text animation effects, bundled into HyperFrames. No separate install needed. -**Spec files:** +**One JSON file per effect** at `skills/hyperframes/assets/text-effects/.json`. Each file is the portable contract for that effect — the per-element parameters (durations, staggers, easings, from/to keyframes, swap behavior). The shared GSAP rendering pattern (how to split text, how to stagger, how to wire to a timeline) lives once in this file, below the catalog. -- `skills/hyperframes/assets/text-effects/effects/.json` — exact GSAP implementation recipe -- `skills/hyperframes/assets/text-effects/specs/.json` — portable motion contract - -**How to use:** pick an effect that fits the brand, mood, and content. Read its `.json` file. Use `showcase.library_adapters.gsap` for exact easing strings, durations, stagger values, and keyframe data. +**How to use:** pick an effect that fits the brand, mood, and content. Read its `.json` to get parameters. Read the shared rendering pattern below to implement. --- ## Catalog -### Per-Character - -| ID | Enter duration | Stagger | Ease | Character | -| --------------------- | -------------- | ------- | ----------------------------- | -------------------------------------------------------------------------------- | -| `soft-blur-in` | 648ms | 18ms | `cubic-bezier(0.22,1,0.36,1)` | Each letter fades in with a gentle upward drift and blur. Smooth, airy, premium. | -| `per-character-rise` | 504ms | 17ms | `cubic-bezier(0.2,0.8,0.2,1)` | Letters slide up from below, no blur. Crisp, deliberate, kinetic. | -| `typewriter` | 173ms | 33ms | `steps(1,end)` | Per-character stepped reveal. Discrete, mechanical, editorial. | -| `bottom-up-letters` | 288ms | 63ms | `cubic-bezier(0.18,1,0.32,1)` | Letters rise from below in a pronounced staircase, one symbol at a time. | -| `top-down-letters` | 288ms | 63ms | `cubic-bezier(0.18,1,0.32,1)` | Same staircase but descending from above. | -| `stagger-from-center` | — | — | — | Characters reveal outward from the center. Emphasizes the keyword core. | -| `stagger-from-edges` | — | — | — | Characters converge inward from both edges toward the center. | - -### Per-Word - -| ID | Enter duration | Stagger | Ease | Character | -| ---------------------- | -------------- | ------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `per-word-crossfade` | 504ms | 50ms | `cubic-bezier(0.16,1,0.3,1)` | Words gently fade in with a short vertical drift. Calm, sequential. | -| `spring-scale-in` | 259ms | 68ms | `cubic-bezier(0.34,1.56,0.64,1)` | Words pop in with a spring overshoot. Physical, bouncy, playful. | -| `shared-axis-y` | 140ms | 56ms | `steps(1,end)` | Hard-cut word-by-word with staircase timing. Sharp, editorial. | -| `blur-out-up` | 403ms | 20ms | `cubic-bezier(0.22,1,0.36,1)` | Words arrive clean and exit upward with increasing blur. Airy exit. | -| `kinetic-center-build` | custom | — | custom | Each word locks center as phrase builds right-to-left with soft blur. Layout-aware renderer — read `showcase.rendering_contract`. | -| `short-slide-right` | custom | — | custom | Whole phrase glides in from the left as one move; words reveal only by opacity. Layout-aware — read `showcase.rendering_contract`. | -| `short-slide-down` | custom | — | custom | Each word drops from above and pushes the stack down until centered. Layout-aware — read `showcase.rendering_contract`. | -| `depth-parallax-words` | — | — | — | Per-word depth motion with scale and vertical drift. Layered readability. | - -### Per-Line - -| ID | Enter duration | Stagger | Ease | Character | -| -------------------- | -------------- | ------- | ----------------------------- | ------------------------------------------------------------------ | -| `mask-reveal-up` | 547ms | 65ms | `cubic-bezier(0.22,1,0.36,1)` | Lines clip-reveal upward. Contained, intentional, masked feel. | -| `line-by-line-slide` | 648ms | 86ms | `cubic-bezier(0.22,1,0.36,1)` | Lines slide in from left, exit to right. Flowing paragraph rhythm. | - -### Whole Element - -| ID | Enter duration | Ease | Character | -| -------------------- | -------------- | ----------------------------- | ------------------------------------------------------------------------------ | -| `micro-scale-fade` | 432ms | `cubic-bezier(0.32,0.72,0,1)` | Tiny scale pop and fade. Barely perceptible — premium polish. | -| `shimmer-sweep` | 612ms | `cubic-bezier(0.22,1,0.36,1)` | Subtle horizontal sweep glides from left to center. | -| `fade-through` | 302ms | `cubic-bezier(0.2,0,0,1)` | Old content fades out, new fades in with a soft delay. Material-style swap. | -| `shared-axis-z` | 374ms | `cubic-bezier(0.2,0,0,1)` | Scale-based depth transition. One context fades out small, next fades in full. | -| `scale-down-fade` | 374ms | `cubic-bezier(0.22,1,0.36,1)` | Content settles with a slight scale-down on exit. Restrained, premium. | -| `focus-blur-resolve` | 547ms | `cubic-bezier(0.22,1,0.36,1)` | Heavy blur resolves to sharp clarity on enter, returns to soft blur on exit. | -| `shared-axis-x` | — | — | Horizontal sibling transition for sequential destinations. | +### Per-character + +| ID | Enter duration | Stagger | Character | +| --------------------- | -------------- | ------- | -------------------------------------------------------------------------------------------- | +| `soft-blur-in` | 700ms | 20ms | Letters fade in with a gentle upward drift and brief blur trail. Premium, atmospheric. | +| `per-character-rise` | 520ms | 18ms | Letters slide up from below baseline, no blur. Crisp, deliberate, kinetic. | +| `typewriter` | 200ms | 50ms | Stepped reveal — characters pop into existence at discrete intervals. Mechanical, editorial. | +| `bottom-up-letters` | 320ms | 65ms | Pronounced staircase rise from below. Confident, audible-feeling rhythm. | +| `top-down-letters` | 320ms | 65ms | Mirror of bottom-up — characters descend from above. | +| `stagger-from-center` | 480ms | 32ms | Middle character reveals first, edges last. Emphasizes the keyword core. | +| `stagger-from-edges` | 480ms | 32ms | Edge characters reveal first, center last. Assembles inward toward the keyword. | + +### Per-word + +| ID | Enter duration | Stagger | Character | +| ---------------------- | -------------- | ------- | ----------------------------------------------------------------------------------------------- | +| `per-word-crossfade` | 460ms | 140ms | Words fade in sequentially with short vertical drift. Calm, paced. | +| `spring-scale-in` | 280ms | 80ms | Words pop in with spring overshoot (1.06 then settle). Bouncy without being cartoonish. | +| `shared-axis-y` | 160ms | 60ms | Hard-cut word-by-word along Y axis. Sharp, editorial. | +| `blur-out-up` | 360ms | 90ms | Clean entrance, blurry rising exit. Lingers in memory rather than dismisses. | +| `kinetic-center-build` | 480ms | 220ms | Phrase builds right-to-left, line stays centered as it grows. **Layout-aware.** | +| `short-slide-right` | 560ms | 120ms | Whole line glides in from the left as one move; words reveal only by opacity. **Layout-aware.** | +| `short-slide-down` | 420ms | 160ms | Each word drops in from above and pushes the stack down. **Layout-aware.** | +| `depth-parallax-words` | 540ms | 110ms | Words enter at varied scales (0.82 → 1.0) reading as layered depth. | + +### Per-line + +| ID | Enter duration | Stagger | Character | +| -------------------- | -------------- | ------- | --------------------------------------------------------------------------- | +| `mask-reveal-up` | 580ms | 90ms | Lines clip-reveal upward through a masked viewport. Contained, masked feel. | +| `line-by-line-slide` | 640ms | 110ms | Lines slide in from left, exit to right. Flowing paragraph rhythm. | + +### Whole element + +| ID | Enter duration | Character | +| -------------------- | -------------- | ---------------------------------------------------------------------------- | +| `micro-scale-fade` | 460ms | Tiny scale (0.96 → 1.0) + fade. Barely perceptible, reads as polish. | +| `shimmer-sweep` | 680ms | Horizontal light sweep glides left-to-right. Premium, luxury polish. | +| `fade-through` | 280ms | Material-style sequential dissolve — old fades out fully, then new fades in. | +| `shared-axis-z` | 360ms | Scale-based depth swap. Outgoing recedes, incoming arrives from depth. | +| `scale-down-fade` | 380ms | Symmetric scale (1.04 → 1.0) on entrance and exit. Restrained, premium. | +| `focus-blur-resolve` | 580ms | Heavy 16px blur resolves to sharp clarity. Reads as a camera focus pull. | +| `shared-axis-x` | 380ms | Horizontal sibling transition for sequential destinations (next/prev). | --- -## Implementation in GSAP +## Shared rendering pattern (GSAP) -**Step 1:** Read the effect JSON — `skills/hyperframes/assets/text-effects/effects/.json` +All non-layout-aware effects render the same way. Implement once, then per-effect just feed in the parameters from the JSON. -**Step 2:** In `showcase.library_adapters.gsap` find: +**1. Register CustomEase** (most effects use cubic-bezier strings): -- `import_statement` — which GSAP plugins to register (`CustomEase` is almost always needed) -- `easing_conversion` — exact easing string or `CustomEase.create()` call -- `start_animation` pattern — how to initialize the tween +```js +gsap.registerPlugin(CustomEase); +``` -**Step 3:** Get timing from `showcase.timing.enter`: +**2. Split the text by `target`:** -- `scaled_duration_ms` → convert to seconds for GSAP (`/ 1000`) -- `scaled_stagger_ms` → stagger value in seconds -- `easing` → register as CustomEase +| `target` | Split rule | +| --------- | ------------------------------------------------------------------------------------------------ | +| `char` | `Array.from(text)` — preserve every character including spaces and punctuation as animated units | +| `word` | Regex `/(\S+\|\s+)/g` — span-wrap words and whitespace; animate the non-whitespace spans only | +| `line` | Split on `"\n"` — each line is a block-display span | +| `element` | No split — the whole element is the animated unit | -**Step 4:** Split text yourself. Span-wrap each character/word/line before the timeline starts. Apply `gsap.set()` to set initial state, then `tl.to()` for the enter animation with stagger. +Wrap each animated unit in a span (e.g., `.text-anim-unit`). For `char` and `word` targets use `display: inline-block`; for `line` use `display: block`. -**For layout-aware effects** (`kinetic-center-build`, `short-slide-right`, `short-slide-down`): read `showcase.rendering_contract` and `showcase.renderer` in the effect JSON. These have custom layout algorithms that manage DOM position — not just stagger timing. +**3. Set the initial state with `gsap.set()`** from the effect's `enter.from`: -**Register CustomEase before using cubic-bezier strings:** +```js +gsap.set(units, { opacity: 0, y: 14, filter: "blur(8px)" }); // values from .enter.from +``` + +**4. Animate to `enter.to` with a stagger using the effect's easing and duration:** ```js -gsap.registerPlugin(CustomEase); -const ease = CustomEase.create("custom", "cubic-bezier(0.22, 1, 0.36, 1)"); +const ease = CustomEase.create("custom-in", "cubic-bezier(0.22, 1, 0.36, 1)"); // from enter.easing +tl.to( + units, + { + opacity: 1, + y: 0, + filter: "blur(0px)", // values from enter.to + duration: enterDurationMs / 1000, // 700ms → 0.7 + ease, + stagger: enterStaggerMs / 1000, // 20ms → 0.02 + }, + 0, +); +``` + +**5. For exit**, do the same with `exit.from` / `exit.to` / `exit.durationMs` / `exit.easing`. Place it on the timeline at `beatEnd - exit.durationMs - (units.length * exit.staggerMs) / 1000`. + +**6. Hard-kill at beat boundaries** (so a later tween or sibling resurrect doesn't bring the element back): + +```js +tl.set(units, { opacity: 0, visibility: "hidden" }, beatEnd + 0.01); ``` +### Custom stagger order + +Two effects use ordered staggers (not DOM order): + +| Effect | `staggerOrder` value | Algorithm | +| --------------------- | -------------------- | ----------------------------------------------------------------------------------- | +| `stagger-from-center` | `center-out` | Rank = `\|index - centerIndex\|`. Ties: lower index first. | +| `stagger-from-edges` | `edges-in` | Rank = `(text.length - 1) / 2 - \|index - centerIndex\|`. Ties: higher index first. | + +GSAP's `stagger: { each, from: "center" }` handles `center-out` natively. For `edges-in`, write the function form: `stagger: { each, from: (i, target, list) => /* edges-in rank */ }`. + +### Layout-aware effects (3) + +Three effects in the catalog (`kinetic-center-build`, `short-slide-right`, `short-slide-down`) animate the LINE container's position separately from each word's reveal. Their JSON includes `layoutAware: true` plus optional `lineFrom` / `lineTo` keyframes for the container. + +Implementation pattern: + +1. Wrap all word spans in a `.text-anim-line` container. +2. Animate the line container with `lineFrom` → `lineTo` (or, for `kinetic-center-build`, with a per-word x-offset to keep the line centered as it grows). +3. Independently stagger the word spans' opacity (and any per-word transform from `enter.from` / `enter.to`). + +Each layout-aware effect's `notes` field in its JSON tells you which line-level transform to apply. Don't infer. + --- -## In the Storyboard +## In the storyboard -Every text element in every beat must name an effect by ID. Not "headline fades in" — read the catalog, pick what fits the brand/mood/beat, and name the specific effect. +Every text element in every beat names an effect by ID. Not "headline fades in" — read the catalog, pick the effect that fits the brand and beat, and name the specific ID. -Format (these are format placeholders — the effect you choose should fit this specific brand and beat, not default to any particular ID): +Format: ```markdown **Text Animations:** -- [element, e.g. "main headline"]: `[effect-id]` — skills/hyperframes/assets/text-effects/effects/[id].json -- [element, e.g. "eyebrow label"]: `[effect-id]` — skills/hyperframes/assets/text-effects/effects/[id].json -- [element, e.g. "body copy 3 lines"]: `[effect-id]` — skills/hyperframes/assets/text-effects/effects/[id].json +- [element, e.g. "main headline"]: `[effect-id]` — skills/hyperframes/assets/text-effects/[id].json +- [element, e.g. "eyebrow label"]: `[effect-id]` — skills/hyperframes/assets/text-effects/[id].json +- [element, e.g. "body copy 3 lines"]: `[effect-id]` — skills/hyperframes/assets/text-effects/[id].json ``` -Sub-agents read the named JSON file and implement from `showcase.library_adapters.gsap` — no creative invention needed. +Sub-agents read the named JSON and implement using the shared rendering pattern above. No creative invention needed — just parameter substitution. diff --git a/skills/website-to-hyperframes/references/step-3-storyboard.md b/skills/website-to-hyperframes/references/step-3-storyboard.md index 2d516962a..314d3cce0 100644 --- a/skills/website-to-hyperframes/references/step-3-storyboard.md +++ b/skills/website-to-hyperframes/references/step-3-storyboard.md @@ -322,10 +322,10 @@ Every text element in this beat must name a specific effect from `skills/hyperfr Format (FORMAT EXAMPLES of structure, not prescriptions — pick based on brand/mood/context): -- `[element — e.g. "main headline"]`: `[effect-id]` — `skills/hyperframes/assets/text-effects/effects/[id].json` -- `[element — e.g. "eyebrow label"]`: `[effect-id]` — `skills/hyperframes/assets/text-effects/effects/[id].json` +- `[element — e.g. "main headline"]`: `[effect-id]` — `skills/hyperframes/assets/text-effects/[id].json` +- `[element — e.g. "eyebrow label"]`: `[effect-id]` — `skills/hyperframes/assets/text-effects/[id].json` -The sub-agent reads the named JSON file and implements from `showcase.library_adapters.gsap`. No creative decisions at build time. +The sub-agent reads the named JSON file for the per-effect parameters (durations, staggers, easings, from/to keyframes) and implements using the shared GSAP rendering pattern in `text-effects.md`. No creative decisions at build time — just parameter substitution. ### Beat Timing