Scale 1→1.06 over first 45%, then 1.06→1.08 while fading out
over remaining 55%. Phase 2 triggers at 900ms mid-fade.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Background fades to black (0.5s), card scales up 6% and holds,
then card fades out after background is dark. No dramatic zoom.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Calculates offset from card center to viewport center and sets
transform-origin so the zoom always converges on the middle of
the screen regardless of card position.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace two-class transition with one keyframe animation.
scale(1)→scale(4) over 0.9s, opacity fades after 60%.
cubic-bezier(0.4,0,0,1) for smooth acceleration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Card scales to 3.5x (well beyond viewport) over 0.8s with
cubic-bezier easing. Background fades to black over 0.5s.
Card fades out at 0.4s delay. Phase 2 slides in over 0.6s.
Total transition ~900ms for cinematic feel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Click a template card: background fades to black, card scales up
and fades out over 450ms, then Phase 2 fades in with subtle slide-up.
Smooth handoff between browsing and configuring.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace moodboard HTML with iframe to template-picker.html. Tab says
'1. Template'. Clicking a template sends postMessage to parent which
triggers goToTune() — no page navigation, no flash. Template picker
and fine-tune live on the same page.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When pick-design.html is opened from the template picker bridge,
rewrite the Direction tab to show '← Template' and navigate back
to template-picker.html instead of showing the empty moodboard view.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When bridging from template picker, replace the static preview_frames
with a live iframe of the selected template. Includes slide cycling,
deck-stage fallback CSS, opacity reset for animation start states,
and scroll-mode detection for flex-scroll templates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Guard mood-title, mood-grid, pal-bar, mood-next-btn references so
pick-design.html works when Phase 1 is replaced by template picker.
Add handleTemplatePick bridge that reads sessionStorage/postMessage
and auto-advances to Phase 2 with selected template metadata.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Inject fallback CSS for <deck-stage> since its JS gets stripped.
Makes it a 1920x1080 block container with absolute slides toggled
by .active class. Use visibility instead of display:none for slide
switching to work with both div and section slides.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Templates set opacity:0 and transform offsets for entrance animations.
Since we strip all scripts, those animations never fire and content
stays invisible. Reset opacity:0 → 1 and stuck transforms after load.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Templates use three different slide architectures:
- .slide.active toggle (16 templates)
- horizontal flex-scroll (8 templates: signal, studio, etc.)
- <deck-stage> web component (10 templates)
Auto-detect system via getSlides(), scroll-based navigation for
flex layouts, component-aware queries for deck-stage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All text pools (headlines, body, stats, labels, smalls) now read from
PROMPT_TEXT instead of hardcoded arrays. Four placeholders:
__PALETTES_JSON__, __PROMPT_TEXT_JSON__, __TEMPLATES_JSON__, __PROMPT_DESC__
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy prototype from rawblock and replace the four hardcoded data values
(PALETTES, PROMPT_TEXT, ALL_TEMPLATES, header subtitle) with placeholder
tokens (__PALETTES_JSON__, __PROMPT_TEXT_JSON__, __TEMPLATES_JSON__,
__PROMPT_DESC__) so build-template-picker.py can inject prompt-specific
data at skill-invocation time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The exported design.html used performance.now() in a rAF loop, which is
not seekable by the HyperFrames three adapter. Replace with the
event-driven renderAt pattern that listens for hf-seek and reads
window.__hfThreeTime for deterministic frame capture.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restructure design-picker.md to force creative thinking BEFORE code:
1. "What is unique about THIS prompt?" — visual vocabulary, emotion as
layout, surface context, anti-competition
2. Verbalized sampling with reasoning paths — from subject, emotion,
audience, anti-pattern, unusual format
3. Rate concepts HIGH/MID/LOW, keep 3+ LOW boards
4. Anti-mode-collapse check
5. THEN generate data
The creative process is now a HARD-GATE before the data format section.
Previously agents skipped straight to architecture HTML and produced
identical layouts across different projects.
Also: mandatory token usage rule strengthened — never hardcode colors,
fonts, spacing, or radii in preview frames.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- design-picker.md: tokens are mandatory in ALL preview frame HTML — never
hardcode colors, fonts, spacing, or radii. The preview must update live
when Phase 2 options change.
- Explicit guidance to follow video-composition.md, house-style.md, and
motion-principles.md when generating preview frames
- Fix easing option not calling renderPreview()
- Fix max-width:100% clamping scene frames in showcase
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2 right panel is now a scrollable design system showcase:
- Overview, Palette swatches, Typography specimens, Scene grid,
Motion easing preview, Elevation cards, Radius chips
- All sections themed via CSS custom properties from selected palette
- Fix max-width: 100% clamping scene frames (was preventing scale)
Architecture preview frames guidance:
- Agents must follow video-composition.md, house-style.md, and
motion-principles.md when generating preview frame HTML
- 4 frames map to beat structure: hook, proof, action, close
- Use real content from prompt, not placeholders
- Frames are static but should look paused mid-animation
- Strip old 15-element checklist and "every token must be used"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove hard element counts and "never/always/every" mandates that forced
visual clutter and prevented minimal compositions:
- video-composition.md: replace "8-10 elements per scene" with density-as-
creative-choice; soften "fill the frame" to "use the frame"; replace
"two focal points minimum" with focal hierarchy; allow solid backgrounds
and deliberate stillness
- house-style.md: remove "2-5 decoratives per scene" count and "1 decorative
is under-dressed"; decoratives are a tool, not a requirement
- prompt-expansion.md: remove "required 2-5 from house-style" and element
padding; density matches the emotional beat
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace single vertical-column example with 3 structurally different
examples (split horizontal, single dominant, equal grid)
- Add structural diversity check: no two architectures share same wireframe
- List 8 layout shapes to draw from (centered, split, bottom-anchored,
grid, single-dominant, list, diagonal, timeline)
- Strip "every token must be used" — tokens only where concept needs them
- Canvas backgrounds now config-driven objects (type + params) instead of
hardcoded string types — same primitives, contextual parameters
- Added text renderer primitive for falling characters
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brief step: 6 questions (audience, emotion, brand assets, light/dark, surface,
key takeaway) asked one at a time, each answer informing the next. Skip what
the prompt already answers.
Verbalized sampling: generate 8-10 mood boards, label each as high/mid/low
mode, keep 2+ rare boards. Different creative modes (formal, emotional,
unconventional, speculative). Anti-mode-collapse check ensures no two boards
share the same template. Unlikely-but-valid board generation.
Design picker template:
- 3x3 grid layout (9 boards)
- 3-frame carousel per card (title, data, CTA scenes)
- Procedural canvas backgrounds (particles, voronoi, perlin heatmap,
ring-pulse, scanline, digits, sine-mesh, ripple, blobs)
- Backgrounds as selectable option in Phase 2
- Sticky canvas in Phase 2 preview
- No labels on Phase 1 cards — design speaks for itself
- Density matches concept, not a 15-element checklist
- Full implementation descriptions in design.md output
Prompt expansion: verbalized sampling for choreography — 3 approaches per
scene (content-first, emotion-first, unconventional), pick the best.
Video composition: added proximity/grouping rule.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(studio): add smooth preview zoom with pinch/Ctrl+scroll
- Scale iframe content from inside (contentDocument.documentElement) instead
of scaling the parent div, avoiding compositor re-rasterization on every
zoom frame — critical for smooth zoom on high-refresh displays (240Hz)
- Document-level capture-phase wheel handler bypasses the DomEditOverlay
- Center-based zoom (no pan drift from pointer-anchored formulas)
- Transient HUD shows zoom % briefly, no persistent UI controls
- Double-click preview area to reset zoom to fit
- Drag-to-pan when zoomed past 100%
- Momentum scroll suppression after pinch gesture (400ms cooldown)
- Delta clamping (MAX_DELTA=10) prevents overshooting on fast gestures
- toDomPrecision rounds transform values to 4 decimals (matches tldraw)
- Zoom state persisted to localStorage with 200ms debounce
- Exposes --preview-zoom CSS custom property for overlay coordinate mapping
- Fix infinite render loop in NLELayout (onIframeRef → refreshPreviewDocumentVersion)
* fix(studio): use CSS zoom instead of transform scale for preview zoom
CSS transform: scale() on a div containing an iframe causes compositor
cross-layer sync issues that produce visible frame tearing on high-refresh
displays (240Hz ProMotion). CSS zoom property changes the actual rendered
size without compositor layer synchronization, eliminating the jumping.
- Replace transform: scale(Z) with zoom: Z on the stage div
- Keep transform: translate() for panning (compositor-friendly, no iframe)
- Overlays work correctly since getBoundingClientRect() includes zoom
- Remove will-change, transition hacks, pointer-events toggles
* fix(ci): use apt-get for ffmpeg in preview-regression workflow
The FedericoCarboni/setup-ffmpeg action downloads from an external URL
that has been persistently unreachable, causing CI failures. Switch to
apt-get install which uses Ubuntu's package repos (same as ci.yml and
player-perf.yml).
* fix(studio): clear zoom timers on NLEPreview unmount
settleTimerRef, hudTimerRef, and retiringTimerRef could fire after
component unmount. Add cleanup effect to prevent stale callbacks.
* feat(studio): persist sidebar, timeline, and playback speed across reloads
Wire up studioUiPreferences for the three remaining UI states requested
in #752: left sidebar collapsed, timeline visibility, and playback rate.
All three now survive page reloads using the same localStorage key as
preview zoom.
The mountEditor callback had content in its dependency array and was
used as a React ref callback. Every content change (keystroke) gave
mountEditor a new identity, causing React to destroy and recreate
the entire CodeMirror editor — losing cursor position, undo history,
and focus.
Remove content from the dependency array and use a separate useEffect
to push external content updates to the existing editor via
dispatch(). The editor is now only recreated when filePath, language,
or readOnly change.
Co-authored-by: Test User <test@example.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(studio): add Layers panel as new inspector tab
Adds a dedicated Layers tab alongside Design and Renders in the right
panel inspector. The panel shows the full composition element tree with
collapsible hierarchy — clicking a layer selects it without navigating
away from the tree view.
Closes#783
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(studio): add visual element previews to Layers panel
Each layer row now shows a small color/content preview thumbnail:
- Text elements show a snippet of their content in the actual font color
- Container elements show their background color as a colored swatch
- Image elements show a tiny thumbnail of the image
- Media elements show an icon indicator
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(studio): add hover-to-highlight and auto-seek to Layers panel
Replace tiny preview thumbnails with hover highlighting — hovering a
layer row highlights the element in the preview canvas. Clicking a layer
auto-seeks the playhead to that element's start time in the timeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(studio): layers panel tab order, autoseek, and renders toolbar overflow
- Reorder inspector tabs to Design → Layers → Renders
- Fix autoseek: walk DOM ancestors when selected element has no direct
timeline match, so clicking a child like S2 Heading correctly seeks
to the start of its parent scene
- Fix Renders toolbar overflow: add flex-wrap to the export controls
header so selects and the Export button wrap instead of clipping
* fix(studio): seek to midpoint of element duration in layers panel autoseek
* fix(studio): layers panel seek now drives adapter.seek via requestSeek signal
setCurrentTime() only updated the store — adapter.seek() and liveTime.notify()
were never called so the iframe never moved. Add requestedSeekTime to the player
store; useTimelinePlayer subscribes and calls the real seek() path when it fires.
* feat(studio): hover over a layer auto-seeks to element midpoint (300ms debounce)
* feat(studio): add collapsible sections to Design panel
Section component now supports collapse/expand with a chevron toggle.
Text, Layout, and Fill sections stay expanded by default. Less-used
sections (Flex, Radius, Stroke, Effects, Clip, Transparency) start
collapsed to reduce scrolling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(studio): remove stale selectedTimelineElement usages dropped in rebase
* fix(studio): stabilize resetErrors in useConsoleErrorCapture to break render loop
resetErrors was a new function object on every render. handlePreviewIframeRef
had it as a dep, so it also changed every render. NLELayout's useEffect watching
onIframeRef would re-fire, calling setPreviewIframe again, which re-ran
useConsoleErrorCapture with the new iframe — infinite loop.
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Part of Phase 2 of the distributed rendering plan (determinism hardening).
See DISTRIBUTED-RENDERING-PLAN.md §7.1 and §17.2 (gating table).
Adds two optional fields to EncoderOptions:
lockGopForChunkConcat?: boolean // default false
gopSize?: number // required when lockGopForChunkConcat=true
When the flag is true on the SW libx264 / libx265 paths, buildEncoderArgs
emits closed-GOP / forced-keyframe args so the resulting chunk file can be
losslessly concatenated (`ffmpeg -f concat -c copy`) with sibling chunks:
-g <gopSize>
-keyint_min <gopSize>
-sc_threshold 0
-force_key_frames "expr:eq(mod(n,<gopSize>),0)"
-x264-params "...:scenecut=0:open-gop=0:repeat-headers=1"
-x265-params "keyint=<gopSize>:min-keyint=<gopSize>:scenecut=0:open-gop=0:repeat-headers=1"
-bf 0 (added for h265 too when locked)
GPU encoders, vp9, and prores ignore the flag (their concat-copy story is
separate — see plan §7.2 / §8).
In-process behavior is unchanged: the default (false) path emits no new
args. New unit tests pin both branches in packages/engine/src/services/
chunkEncoder.test.ts.
This is part of a stack of 10 PRs; this is PR 1 of 10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #755 added the typegpu-adapter regression test scaffolding (meta.json,
src/index.html, output/compiled.html) but left the output.mp4 golden
baseline ungenerated:
> Note: output.mp4 baseline needs to be generated in CI — the local …
Every \`regression-shards (fast)\` run since #755 merged has failed with
\`Snapshot not found: /app/packages/producer/tests/typegpu-adapter/output/output.mp4.
Run with --update to create it.\`
Generated via the canonical Docker path per CLAUDE.md:
bun run --cwd packages/producer docker:test \\
--update --suite typegpu-adapter
Stored via Git LFS (already configured for
\`packages/producer/tests/*/output/output.mp4\` in \`.gitattributes\`).
The Windows install failures (`ENOENT: failed copying files from cache to
destination for package @types/node` / `esbuild`) are caused by bun creating
workspace-scoped nested installs under
`node_modules/@hyperframes/<pkg>/node_modules/...`. Those nested paths only
exist because each workspace package pinned a different `@types/node` /
`esbuild` major:
- root: `@types/node ^25.0.10`, core: `^24.10.13`, cli/engine/producer: `^22`
- core/cli: `esbuild ^0.25.x`, producer: `^0.27.2`
Each major-version gap forces bun to install a workspace-scoped copy in a
deep `node_modules/@hyperframes/<pkg>/node_modules/<dep>/node_modules/...`
tree that bun can't reliably materialize on Windows GHA runners. Aligning
versions lets bun dedup to a single root-hoisted install per dep, and the
nested workspace block disappears from `bun.lock` entirely.
## Alignment
- `@types/node` → `^25.0.10` across root, core, cli, engine, producer
- `esbuild` → `^0.25.12` across cli, core, producer
- `tsx` → `^4.21.0` across producer (matches root + core)
## Source-level v25 compat (already in this PR)
@types/node v25 declares `File` as an interface (not a class) and exposes a
conditional global where `FormData.entries()` narrows to `[string, string]`
when an `onmessage` global is in scope. `packages/core/src/studio-api/routes/files.ts`'s
`value instanceof File` check was relying on the v24 class declaration —
already cast the iterator to `Iterable<[string, FileLike | string]>` in the
prior commit.
Two more v25 source fixes here:
- `packages/cli/src/commands/init.ts`
- `packages/cli/src/whisper/normalize.ts`
`Dirent.path` was removed in @types/node v25 (deprecated alias for
`parentPath` since Node 20.12). Drop the `?? e.path` fallback.
## Verification
Both install layouts now build clean end-to-end:
- `bun install` (isolated, default): full build green, 853 core tests pass,
typecheck green across all 7 packages
- `bun install --linker=hoisted` (Windows CI): same result
- `bun.lock` no longer contains any `@hyperframes/<pkg>/<dep>` nested
workspace entries — 70+ lines of nested install blocks gone
Pushing further to actually get Windows render verification green, not just
work around it.
## What's wrong on Windows
Bun 1.3's default `isolated` linker creates nested workspace junctions under
`packages/*/node_modules/` on Windows GHA runners. Those junctions don't
materialize reliably — Node's `realpathSync` returns `EPERM` on stat, and
ESM resolution returns `ERR_MODULE_NOT_FOUND`. Every Windows build since
PR #748 has tripped this in one of three places:
- `packages/producer/build.mjs` importing `esbuild`
- `packages/producer/scripts/generate-font-data.ts` reading `@fontsource/*`
- `packages/producer` running `tsc` to emit `.d.ts`s
Long-running bun bugs: oven-sh/bun#23615, #18354, #10146.
## Fix
**1. `--linker=hoisted` for the Windows install step** (workflow change,
Windows only). Hoisted layout puts deps as real directories at the workspace
root + workspace package node_modules. No junctions, no Windows-specific
path quirks. Linux CI keeps the default isolated linker; the lockfile is
linker-agnostic so `--frozen-lockfile` is still valid.
**2. Source-level FormData narrowing in `packages/core/src/studio-api/routes/files.ts`**
(needed because the hoisted layout exposes a `@types/node@25` typecheck
issue that the isolated layout hides). With v25 + an `onmessage` global in
scope, the ambient `FormData.entries()` infers `[string, string]` instead of
`[string, File | string]`, so the `value instanceof File` check breaks at
`TS2358`. Cast the iterator to a `[string, FileLike | string]` shape and
narrow via `typeof value === "string"`. Identical runtime behavior; works
under both v24 (isolated layout, what Linux CI sees) and v25 (hoisted, what
Windows CI sees with this change).
## Verification
- `bun install --frozen-lockfile` (isolated, default): full build green
- `bun install --frozen-lockfile --linker=hoisted`: full build green, core
typecheck passes, `@hyperframes/core` 853 tests pass
- Format/lint clean on both layouts