* feat(shader-transitions): add playSeamTransitionLoop to preview a seam on a loop
Plays a catalog transition's real WebGL effect between two already-decoded
frames on a caller-supplied canvas, looping 0->1->0 every second. The
caller supplies the two seam frames itself, so this takes two
CanvasImageSource frames directly rather than resolving any DOM scene.
Reuses the existing webgl.ts primitives hyper-shader.ts's full composition
engine already relies on; no GSAP timeline, no scene capture, no
multi-transition scheduling here, just setup + a render loop.
shaderName is the catalog block's own name. 12 of the 14 shader-tagged
catalog blocks match their shaders/registry.ts key verbatim; two are
aliased internally (domain-warp-dissolve->domain-warp,
chromatic-radial-split->chromatic-split) since those registry keys are
also engine/producer's canonical TRANSITIONS[...] names and weren't
renamed to match the catalog. Every other catalog block (including the
CSS/motion-only ones) has no WebGL implementation and throws, so a caller
can fall back to its own render for those.
Re-exported from @hyperframes/studio (new workspace dependency on
shader-transitions) so a consumer of studio and core needs no deep import.
* fix(shader-transitions): keep the WebGL context alive across stop()
- stop() called WEBGL_lose_context.loseContext(), which permanently kills
the canvas's WebGL context -- a later playSeamTransitionLoop() call on
the same canvas (a popover reusing its element across hovers) would pass
the null-context guard with the same dead context object and silently
render nothing. Delete the program, textures and buffer this call
created instead, matching the granular-release idiom hyper-shader.ts
already uses for its own reusable contexts.
- ready never resolved if stop() ran before the first frame (a hover that
ends within one frame, well under 16ms), hanging any caller awaiting it
forever along with everything it closed over. stop() now resolves ready
too; resolving an already-settled promise is a no-op for the normal path.
Also: resolveReady() only needs calling once, on the first frame, not every tick.
* test(shader-transitions): extract shared running-loop setup, dedup per fallow
* fix(shader-transitions): pass accent colors, fix leaks and validation
- renderShader's colors argument was undefined, so u_accent/
u_accent_dark/u_accent_bright stayed at WebGL's zero default. 7 of the 14
shader blocks (domain-warp, ridged-burn, sdf-iris, ripple-waves,
gravitational-lens, thermal-distortion, light-leak) use those uniforms
for their glow/burn colour, so the preview silently rendered without it
for half the catalog -- the "plays the real effect" claim was false for
those seven. Exported webgl.ts's DEFAULT_ACCENT_COLORS (hyper-shader.ts's
own fallback, previously only inlined twice there) and pass it through.
- Each linkProgram() call leaked its compiled vertex and fragment shader
objects (created, attached, linked, never deleted) -- a real WebGL
browser never frees them on its own, and this call is on a hover-repeat
path via a reused canvas. Fixed in webgl.ts's linkProgram itself, which
every caller in this package routes through, not just this new one.
- loopMs was never validated: 0 divided to NaN progress, negative values
ran the shader backwards, Infinity froze it at 0 -- all silently, no
caller-visible error. Now throws synchronously for a non-finite or
non-positive value, matching the function's other synchronous-throw
contract (unknown shader, no WebGL context).
- getFragSource() and the catalog-alias lookup both did an unguarded
plain-object index (shaders[name], CATALOG_SHADER_ALIASES[shaderName]),
which returns Object.prototype members for names like "constructor"
instead of undefined -- an unusual shaderName could reach the shader
compiler with a garbled error or an unintended match. Both now use
Object.hasOwn.
Tests: second half of the loop (progress descending, not climbing past 1),
both catalog aliases (only one was covered before), all four invalid-loopMs
shapes, the accent-color uniforms actually being set, and webgl.test.ts's
new deleteShader assertion. All mutation-verified non-vacuous.
* test(shader-transitions): dedup the two timing tests per fallow
* fix(shader-transitions): apply default accent colors and validate loop options
renderShader now defaults colors to DEFAULT_ACCENT_COLORS instead of the
caller deciding, so playSeamTransitionLoop no longer renders 7 of 14 shader
blocks with a zeroed-out accent. Validates width/height alongside loopMs,
switches the catalog alias map to a Map so a bogus name never resolves an
Object.prototype member, and fixes linkProgram to only delete the shader it
created itself.
* fix(shader-transitions): free GL resources when seam-loop setup throws
A bad fromSource/toSource reaching uploadTextureSource previously left
whatever quadBuf/program/textures had already been allocated dangling,
since playSeamTransitionLoop throws before returning a handle a caller
could call stop() on. Trims three over-limit JSDoc blocks and drops a
redundant prototype-pollution rationale now that getFragSource guards it.
* chore(shader-transitions): trim seam-loop comments to the comment ratchet
* test(shader-transitions): unknown-shader throw covers names on Object.prototype
* fix(studio): ship component styles as their own stylesheet for embedders
Studio's component rules (timeline clips, loaders, range inputs, toasts and
the rest) lived only in the app sheet, so a page that embeds Studio's
components with theme.css got unstyled clips and dark-on-dark labels. They
move unchanged into components.css, exported as
@hyperframes/studio/components.css; studio.css keeps the app shell and
imports it, so Studio's compiled CSS has the same rules.
* test(studio): read moved component rules from components.css
* test(studio): catch class rules nested in the app sheet's layers and media blocks
A host mounting Studio's header reuses HyperframesLogo and buttonSizes for
matching chrome, and imports the compiled stylesheet as a fixed-name package
export instead of a hash that changes on every rebuild.
* feat(studio): shared Button, IconButton, Tooltip and Tabs primitives
Four primitives that consume design tokens only, plus the configured class
merger they all share.
- ui/cn.ts: one `cn` instance whose config knows the two class families
theme.css invents, the `text-step-*` type scale and the `ctl` control
heights, so a caller's class replaces a primitive's instead of stacking
with it and letting stylesheet order decide.
- Button and IconButton: native buttons on the three control heights, the
radius scale, the semantic palette and the motion tokens. Four classes
that resolved to nothing (rounded-button, shadow-btn-primary,
bg-accent-red, ease-standard) are gone, which is why the Renders Export
now has a radius.
- Tooltip: Base UI's tooltip, so the portal, the viewport flip and the
collision shift stop being hand-rolled. role="tooltip", the id wiring and
aria-describedby stay explicit here because the library does not add them.
- Tabs: Base UI's tabs, with roving tabindex, arrow keys, Home and End, and
a data-tab-id the sidebar's focus restore looks up.
Every interactive look is also reachable through a data-preview-state
attribute that CSS alone reads, so a gallery can show hover, active and
focus without a pointer. A test pairs the two lists in both directions.
The class-resolution test compiles Studio's real stylesheet with every class
the primitives emit and fails on any that produces no selector. Tailwind
treats an undefined class as silence; that silence is what let four dead
classes sit in this file.
* feat(studio): export Button, IconButton, Tabs and Tooltip from the package root
* docs(studio): trim comment blocks to pass the no-essay-comments gate
* refactor(studio): use native CSS.escape in the button class-resolution test
* refactor(studio): share the ui test mount helper and flatten the Button leading slot
* fix(studio): keep the tooltip open on a disabled control
* fix(studio): give the tooltip trigger no layout box and anchor to the child
* fix(studio): anchor the tooltip through a ref so it adds no render
* fix(studio): button children lay out with the consumer gap
* test(studio): keep the no-icon label and shortcut layout assertion from the standalone fix
* docs(studio): shorten the Tooltip header comment to fit the comment gate
* test(studio): the Button vacuity guard names tokens that stay undefined
* feat(studio): dockview-backed dock engine, not yet wired into the shell
Registry, layout schema/builder, zustand store, the Dock component and
a persisted studioUiPreferences.dockLayout field. No panel moves onto
it in this change; Studio shell is untouched and looks identical.
* fix(studio): dock.css reads panel tokens as css variables so it compiles standalone
Tailwind v4 processes dock.css without the studio theme, so theme() calls failed to resolve. Use the --color-panel-* variables instead. Also drop the unused DockController.open member.
* test(studio): dock restore and schema cross-checks fail when their code is removed
* fix(studio): a reopened side panel rejoins its column's tab group instead of opening a new column
* test(studio): pin the dock reopen paths for the centre zone and a fully closed column
* feat(studio): move every design value into one theme file
Studio's design values lived in three places: a JS Tailwind preset, an
inline @theme block, and raw hex and pixel literals in markup and CSS.
theme.css is now the single owner. Tailwind v4 turns it into both the
utility classes and the CSS custom properties, so markup, plain CSS and
canvas code all read the same number.
- semantic colour palette (background levels, surfaces, borders, text
levels, accent, selection, playhead, danger, media, container), radii,
control heights, the micro type scale, font stacks, shadows, easings
and motion durations
- the stock Tailwind palette is cleared with --color-*: initial, so a
colour that is not a Studio token cannot be reached from markup; the
entries Studio's markup still names are re-declared as deprecated
aliases and removed once the sweep converts them
- motion durations ship as @utility rules that zero themselves under
prefers-reduced-motion, since Tailwind has no duration namespace
- Phosphor icons get one default size and weight, read off the theme at
the app root; icons that pass their own still win
- published as @hyperframes/studio/theme.css; the JS preset export stays
as a deprecated shim for v3 consumers, with a test that fails if the
two drift apart
* docs(studio): trim theme token comments to pass the comment-length gate
* docs(studio): shorten theme test docblock to fit the comment gate
* refactor(studio): collapse the duration utilities into one functional rule
* refactor(studio): trim theme.css comments to three lines
* test(studio): fail when a legacy palette entry is not a Tailwind default
* fix(studio): keep the palette reset out of the exported theme.css
* style(studio): format theme.css with the repo formatter
* fix(studio): declare theme.css browser-only so the packed-consumer smoke does not import it in node
* chore(studio): upgrade Tailwind CSS to v4 through the Vite plugin
Studio moves from Tailwind 3.4 to 4.3.3, loaded by @tailwindcss/vite instead
of PostCSS. The JS config becomes an @theme block in studio.css carrying the
same studio.* and panel.* colors, with explicit @source globs so the gitignored
data/projects tree is never scanned.
The official upgrade tool renamed the utilities, but it rewrites bare words
inside any string, so its edits to event names, CSS property names, filter
function names, status enum values and prose were reverted by hand.
Nothing in Studio relied on the v3 default border color, ring width or ring
color, so the tool's compatibility shim was dropped rather than kept.
* chore(studio): note the duplicated palette between theme css and preset
* fix(studio): keep the pointer cursor on buttons after the Tailwind v4 upgrade
v4 preflight dropped the pointer cursor on enabled buttons, so add it back in
one base rule. Also migrate the last outline-none to outline-hidden.
* fix(studio): keep the Renders Export button at its shipped 14px size
* chore(studio): allow the tailwind v4 config deletions in the deletion guard
* refactor(studio): remove the storyboard view and its Storyboard/Preview switch
* refactor(studio): fold the thumbnail output branches left after the storyboard mode went
* docs(studio): drop leftover storyboard references after the view was removed
* ci: allow the storyboard view deletions in the no-deletions guard
* docs(studio): wrap an over-long comment line in the caption overlay helper
* style(studio): reindent App.tsx after the view-mode wrapper was removed
* chore: prepare v0.8.46
Draft only, not tagged: bun run release:prepare 0.8.46 --no-tag.
Changelog rewritten in plain words for the one change since v0.8.45
(#4031, adaptive per-frame motion-blur sample count). Cutting the
release is a separate step on Miguel's word.
* docs(release): fix false render-time savings claim in v0.8.46 notes
Adaptive sampling never drops below the old fixed-16 default; it only
adds cost on frames that measure real spatial motion. Per Miga's review
on #4033.
- Lint no longer reports root_dimensions_mismatch for sub-compositions, the
false positive v0.8.43 shipped with; a top-level mismatch is still reported
and a viewport-meta-only mismatch gets its own wording (#4012)
- The streaming encoder accepts lockGopForChunkConcat and gopSize, emitting the
same arguments as the disk encoder. Off by default, so an existing render
produces an unchanged FFmpeg arg list (#3973)
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
- Portrait compositions render the full frame when the project was
scaffolded at a different size: html/body are sized to the composition
root at runtime; new lint warning root_dimensions_mismatch (#4005,
fixes#4001)
- render_complete telemetry adds composition_element_tags,
aroll_video_count, heygen_video_count; render_error adds error_name and
failed_stage_code (#4004)
- Catalog docs "Source" accordion titles are plain strings, so docs
search no longer shows [object Object] (#4006)
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* chore: release v0.8.42
- Studio preview works for projects whose path has non-Latin-1 characters (#3982)
- Studio render downloads use RFC 6266 headers for non-Latin-1 filenames (#3984)
- check --json reports HDR auto-promotion; render prints one warning (#3985)
- Forced --sdr renders tone-map HDR sources before extraction (#3981)
- Render/preview workers no longer survive as orphans after Ctrl-C or parent exit;
cancellation reaches final artifact promotion; HYPERFRAMES_RENDER_DETACHED=1 for
intentionally detached renders (#3974)
- HDR preflight test coverage for held tails (#3963)
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* chore: release v0.8.42 notes — name the #3982 symptom and scope
- #3982 entry: a Studio preview server crash for compositions whose file path
has non-Latin-1 characters (the project directory is not involved)
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* chore: release v0.8.42 notes — name the #3982 symptom
- #3982 entry: the uncaught ETag TypeError took the preview server down, so
call it a Studio preview server crash rather than a preview failure
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>