* feat(core): support deterministic async frame sources
* fix(runtime): retain queued draws and disable unsafe dedup
* docs(core): mark frame-source hosts with data-no-timeline
A composition that follows the frame-source contract registers no GSAP
timeline, so lint reported missing_timeline_registry and every render
waited 45 s for timeline registration (17.6 s -> 1 min 47.9 s for the
14 s imported film sample). Document the existing data-no-timeline
opt-out for the host and a timeline-less root.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(runtime): avoid transport redraws after render seeks
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ci): skills sync publishes past a version the registry holds unpublished
* fix(ci): skills sync fails unless a republished skill is the registry's latest
* fix(ci): skills sync republishes one patch past a hidden version and checks it as owner
* test(ci): skills sync fails the job on an error and stops after a clean sync
Two mutants survived review: dropping the line that sets the exit code (the job would print its ::error:: lines and still pass) and dropping the clean-sync return. One test runs the script with a failing fake clawhub on PATH and expects exit 1; another pins that a clean sync makes no further call.
* test(ci): the exit-code test can only reach its fake clawhub
The child's PATH was the fake's folder plus the inherited PATH, so on a machine where the fake cannot execute, a real logged-in clawhub further down would run sync for real. PATH is now the fake's folder alone.
* fix(ci): skills sync skips only hyperframes-creative's hidden 1.0.13
Republishing one patch past any taken version would publish past every future hidden version with no one looking. Only the acknowledged case (hyperframes-creative, 1.0.13 taken) is published at 1.0.14; any other taken version fails the sync as before.
* test(ci): the skip allowlist matches the skill, not just the version
A taken 1.0.13 on any other skill must fail the sync too; without this case, accepting 1.0.13 for every skill passed all tests.
* refactor(ci): skills sync picks the republish version in its own helper
The allowlist check took republishTaken to cyclomatic 5, which the Fallow audit scores at its 30.0 CRAP threshold; deciding the version in versionPastSkippable keeps each function under it. Behaviour unchanged.
* feat(engine): render WebGPU compositions on SwiftShader behind an opt-in
PRODUCER_ALLOW_SOFTWARE_WEBGPU=true lets a host without a GPU render a
data-requires-webgpu composition instead of refusing it. SwiftShader can
draw canvas WebGPU only through its Vulkan backend with GPU compositing
on, so an opted-in software launch adds the Vulkan feature and
--use-vulkan=swiftshader, keeps compositing on, and the adapter check
accepts the fallback adapter. Off by default; every other launch is
unchanged.
* test(engine): compare the opted-out launch without a cast
* test(engine): pin which adapter a capture session accepts for WebGPU
* test(engine): assert the adapter check's arguments with the call matcher
* docs(engine): fit the software WebGPU note in three lines
* test(producer): unit tests never reach the network
* test(producer): the unit lane refuses sockets to other hosts, not only fetch
* test(producer): refuse outside hosts at the lookup, not by parsing socket calls
* test(producer): keep dns.lookup promisify-compatible under the no-network guard
* perf(studio): coalesce timeline zoom to one write per frame and zoom to a range
* perf(studio): coalesce timeline zoom to one write per frame and zoom to a range
Every zoom input goes through one owner that writes the store once per frame.
zoomTimelineToRange eases to a time range; the zoom buttons use it.
* feat(studio): zoom-to-range reports done or cancelled and is not a person zoom
* perf(studio): place clips in percent of a one-second layer so zoom restyles nothing
A zoom step rewrites one width per row; clip thumbnails start decoding at rest.
* perf(studio): a zoom step re-renders only clips whose drawing it changes
Clip handlers read the lane callbacks when pressed, so a skipped clip never acts on a stale zoom.
* refactor(studio): move at-rest waveform and thumbnail work to its own change
* perf(studio): draw waveforms and decode thumbnails when the timeline rests
Waveforms draw only the visible span of a clip.
A zoom or scroll step stretches what is drawn until the timeline rests.
* test(studio): fade handles measure the clip only while they show
* test(studio): waveforms draw their visible span and redraw at rest
* feat(studio): export currentTimelineRange, the inverse of zoomTimelineToRange
* perf(studio): zoom gestures scale the drawn timeline and lay it out once at rest
No layout or render per gesture frame; past 0.67x or 2x the preview is laid out and continues.
* perf(studio): rebase a zoom preview only past 0.5x or 4x
Clips stay mounted half a viewport each side, so a zoom-out preview to 0.5x shows no gaps.
* fix(studio): keep the zoom readout, edges and Fit right during a preview
The slider and its percent follow a zoom while it is previewed. A zoom in
from below Fit no longer jumps when it lands: the preview now uses the
width the timeline will really have. A zoom-out lays out as soon as it
would show time with no clips mounted, so edges never show empty strips.
Fit, a press or a scroll on the timeline end a pending zoom first. An
older abort no longer cancels a newer zoom-to-range, and an instant one
lands on the next frame instead of inside its caller. Range selection and
gap tints scale with the preview.
* fix(studio): keep a pending zoom when the timeline re-registers its view
The timeline registers its scroll view again whenever clips are added or
removed, which used to drop a zoom still waiting to be laid out. Now only
a real unmount drops it, and unmounting one timeline leaves a newer
timeline's registration alone.
* refactor(studio): share the zoom geometry helpers the timeline already has
Zoom scales come from getTimelinePixelsPerSecond, the range margins from one
helper, and a zoom step starts from the same playhead anchor every zoom uses.
The clip minimum width is named and shared with the drag ghost.
* fix(studio): keep a zoom preview through scrolls and past the content end
A scroll during a zoom preview, including the one a laid-out zoom makes
itself, used to lay the next preview out at once, so a pinch or eased zoom
fell back to a full layout every frame. A scroll now only rescales what it
mounted. Zooming out also lays out before the preview shows past where the
ruler and beat lines are drawn, not only past the mounted clips.
* fix(studio): keep a moved audio clip's waveform drawn where it shows
A waveform now draws the stretch of its clip that the shared strip tracker
reports near the screen, the tracker the filmstrip already uses. It follows
scrolls, resizes and moves (a drag or ripple that shifts the clip), so a
moved clip no longer shows a blank stretch, and a zoom redraws it once.
* perf(studio): lay a zoom-out step out once, at its start
An eased zoom-out (a Zoom out click, or zooming out to a range) used to
lay out partway, once the shrinking preview reached time with nothing
mounted, and again at the end. It now lays the target out first and eases
by scaling that layout down from the old view, so each click lays out
once. Each eased frame also draws its preview in that frame.
* fix(studio): keep previewing a zoom at the content's end and over new rows
The preview now lays out only once it would show past the window the
timeline draws clips, ruler ticks and beat lines in, with two pixels of
slack, so rounding at the content's end no longer lays out every frame.
Rows mounted into the timeline during a preview are scaled before they
paint.
* fix(studio): redraw a waveform when its clip's width changes or it moves
A waveform drawn whole kept its old bars, stretched, after a zoom changed
its clip's width; it now redraws on any resize. A long clip also marks
its undrawn ends for the strip tracker, so a move without a scroll (a
ripple, a drop, an undo) re-measures it instead of leaving a blank
stretch.
* perf(studio): skip the layout when a zoom preview ends on its own scale
An eased zoom-out lays its target out first, so its preview ends on the
scale already laid out. Landing it now only sets the scroll, instead of
writing the same zoom again and counting the person's zoom twice.
* fix(studio): measure filmstrips and waveforms after a zoom preview ends
The strip tracker measured clip boxes while a zoom preview scaled them,
so after a Zoom out click a long clip's filmstrip or waveform could stay
blank across part of the screen. Reads during a preview now wait, and the
tracker re-measures every strip once the preview ends. A zoom-out lays its
target out in its first frame rather than inside the caller, and only when
the new view holds the old one.
* perf(studio): watch a waveform's undrawn ends only when it has some
A clip drawn whole has no undrawn ends, so it no longer mounts the two
empty markers the strip tracker watches; with hundreds of short audio
clips that saved an observer entry each.
* fix(studio): measure strips each time a zoom preview is laid out
A zoom-out lays its target out partway into the ease, after which the
strip tracker kept the stretch it measured before the zoom, so a long
filmstrip or waveform went partly blank near the end of the ease. Every
layout of a preview now re-measures the strips.
* docs(studio): keep the zoom-to-range doc comment within four lines
* docs(studio): keep the zoom files' comment share at or under main's
The comment ratchet failed on ten files this branch touches. Comments are
cut or folded into one line; no code changes.
* test(studio): launch real-Chrome tests like the e2e launcher, bounded by the test budget
* test(studio): share the real-Chrome page setup between the two runtime tests
* test(studio): keep the real-Chrome test helper with the other test helpers
* fix(core): the preview loads the next scene's images before it starts
* fix(cli): check stops reporting image loads that its own scrubbing cancelled
* fix: preload only a coming clip's own images, and judge aborted images by whether they still decode
* fix(cli): count an aborted image as broken only when its decode fails
* style(cli): keep the aborted-image comment within the line limit
* test(cli): pin that a reload still pending at the cap is not reported
* test(cli): run the cap test on whatever timers are pending, and always restore real timers
* perf(studio): parse the saved file once to sync the composition length after a timeline edit
* perf(studio): keep empty duration updates free of parsing
* test(studio): pin one parse for rootless moves and the delete length readout
* test(studio): check the delete's length readout before its save lands
* fix(core): keep nested scenes in place during a drag when the root has no timeline
* fix(core): reuse the missing-root composite so a late root timeline still binds
* fix(core): count a nested scene in its composite length so a rebind keeps it at the playhead
* fix(core): rebuild a held composite whose length went stale so the player length stays right
* test(core): reuse the no-root-timeline loader for the stale-length case
* feat(registry): gooey split block with metaball goo and directional motion blur
A dot splits into a three-dot menu through a gooey neck, spins into a
swirl mark under shutter motion blur, breaks back into three dots and
merges into one. Timing, spacing and eases are measured off a reference
recording frame by frame.
The motion-blur snippet gains an opt-in `sharp: 0` option that hides the
crisp frame-time copy while an element moves, leaving only the shutter
average. The default is unchanged; the inlined copies carry the same text.
* feat(registry): grow gooey-split blades out of swelling dots
* refactor(registry): tidy gooey-split pose code and document sharp 0 limits
* fix(registry): hide a sharp 0 motion-blur target by marker, not its visibility
Hiding through the element's own visibility fought timelines that
write it (autoAlpha): a target faded in after attach stayed hidden at
rest. A marker attribute and one !important rule now do the hiding, the
default path writes nothing, and a declarative sharp other than 0 or 1
is refused like any other bad option.
* fix(registry): smear a motion-blur target that was hidden when blur attached
Copies froze the target's computed visibility at attach, so a target
entering with autoAlpha never smeared (and vanished entirely under
sharp 0). The group now carries the target's current visibility each
frame, read with the sharp 0 marker lifted, and a copy keeps a
descendant's visibility only where it differs from its parent's.
* test(registry): pin a copy keeping a descendant's own visibility
* fix(registry): keep a child's own hiding in the smear of a target hidden at attach
* refactor(registry): restore a lifted visibility in one call and lift any hidden form
* fix(registry): restore a lifted visibility even when copying styles throws
* fix(registry): let motion-blur copies hide with a clip that has ended
The smear group set an explicit visibility on every moving frame, and
copies used to freeze an explicit visible, so an ancestor clip hiding
after the last update could not hide them: copies from a 0-4 s
sub-composition stayed on screen at 16.4 s. The group now sets a
visibility only where the element's differs from its parent's.
Also regenerates skills-manifest.json for the motion-blur skill doc.
* feat(cli): stream heard words on the transcribe --json progress stream
* test(cli): share the streaming transcribe helper
* fix(cli): keep install and download stoppable and whisper errors short while streaming
* fix(core): renders keep a fromTo's from-only values from the frame it starts on
* refactor(core): prime a tween at its start in one helper
* fix(core): a keyframed fromTo also keeps its from-only values at its start
* fix(core): each staggered target keeps its from-only values from the frame it starts on
* test(studio): bench moves pressed right after Add keyframe
* fix(studio): an undo that reloads the preview keeps the selection, so a drag after it moves
* fix(studio): a drag started while a resize saves keeps its animations paused
* fix(parsers): an edit pins no position on a keyframed tween it did not touch
* fix(studio): a move pressed while the preview changes lands on the new preview, at the time pressed
* test(studio): bench waiting presses, a held box resized twice, and reloads at the last playhead
* refactor(studio): tidy the waiting press, bound its wait, and freeze edit stamps after early exits
* test(studio): load the property panel once so its first test stops timing out
* fix(studio): presses queued on a held box keep their release, hover and newest outline
* test(studio): judge bench undo by the history groups Studio claims, not by file writes
* refactor(studio): one waiting-box lookup in the bench and one raf-pause write per release
Behaviour-identical tidy of the waiting-press work: the bench reads the waiting
box through one helper, releaseActiveGesture sets the raf pause once, clearPointerState
writes the false it always computed, and previewGestureStarts calls the opts setters
directly instead of through aliases named like the gesture hook's gated setters.
* fix(studio): a queued press lands on the element it pressed and survives a window switch
* fix(studio): undo cancels a waiting press, a stuck save stops holding presses, scene swaps hold them
* refactor(studio): keep the complexity waiver on clearPointerState itself
* fix(studio): undo of a queued press leaves nothing waiting, and hosts keep their own undo
* fix(studio): a waiting press keeps the box still after a blur, and undo reports what it took back
* style(studio): bring the PR's comments within the comment limits
* fix(studio): bank the four bench cases this fixes and meet the comment, complexity and duplication gates
* test(studio): pin that a waiting press is never a pending edit, and name the new duplication waivers
* fix(cli): let parakeet-mlx detect the language and name the Parakeet install under auto
* docs(cli): say what --language does under each engine
* docs(cli): whisper transcribes as the --language, it does not filter
* chore(cli): drop a comment the install-hint condition already says
* fix(studio): a timeline jump no longer starts thumbnail loads it is about to cancel
* fix(studio): pump inline after the release batch, and pin the synchronous slot free
* fix(studio): an offline preview says GSAP could not load instead of an unhandled Event
* fix(studio): the preview error list survives a second capture attach without duplicates
* fix(studio): the console capture keeps listening after a long early error list
* refactor(studio): the console capture owns every reset of its findings
* test(studio): bundle the real-Chrome undo test with esbuild so its setup fits on Windows
* test(studio): stub only the Node built-ins the undo test bundle reaches
* feat(cli): catch up on what the desktop app did since a project was handed over
The desktop app records each chat turn in .hyperframes/app-history.jsonl.
`hyperframes catch-up [dir]` prints the turns since the hand-off or the last
catch-up (what the person asked, what Framey did, which files changed) plus
the project files changed since, by the app or by hand, then marks them seen.
`hyperframes open` and Studio's Edit with Framey mark the hand-off moment.
Any command run on a project with unseen turns ends with a line naming
catch-up, so an agent back in the terminal hears about it even when it
forgets. /hyperframes and /hyperframes-cli tell the agent to catch up as
soon as the person writes again.
* feat(cli): hand a Grok terminal session over to the desktop app
Grok gives every tool command its session as GROK_SESSION_ID (grok 1.0.4),
so `hyperframes open` and Edit with Framey now name a Grok session in
.hyperframes/agent-handoff.json, after Claude Code and Codex. The desktop
app reads its turns from Grok's own session files.
* feat(cli): tell a new project's agent to catch up after the desktop app
Scaffolded projects' CLAUDE.md and AGENTS.md now say to run
`npx hyperframes catch-up` before the next change once the project was
opened in the desktop app. Claude Code, Codex and Grok read these files on
their own, so a resumed session catches up even when it never loaded a
HyperFrames skill: a resumed Claude Code session that had not, asked to
change the title, ran catch-up first.
catch-up also prints each reply on one line.
* fix(cli): read and write .hyperframes records without following links, mark seen after showing
Review (Jerrai, Rames):
- One helper (projectRecords.ts) now reads and writes every record in
.hyperframes/. A linked folder is refused. A read opens once with
O_NOFOLLOW and checks the open file is a plain file, which clears the
CodeQL race. A write goes to a temp file renamed over the record, so a
link is replaced, never written through. agent-handoff.json, the seen
record and the history all go through it.
- catch-up marks a turn seen only after showing it, and only up to the
newest turn shown, so a turn written just after the read shows next
time. Changed files have their own "checked" time.
- Turns show only once the project was handed over from this machine, so
a history file shipped in a cloned repo is never shown as the person's
words. The end-of-command check skips reading when the history is
older than the seen record.
- The skills mention catch-up only when the CLI printed a line naming it,
so an older CLI never meets an unknown command.
- The notice test uses basename, which fixes Windows.
* feat(cli): add the catch-up line to an older project's instructions on hand-off
A project scaffolded before catch-up existed has no line telling its agent
to catch up, and a resumed session that never loaded a HyperFrames skill
then edits without knowing what happened in the app. When `hyperframes open`
or Edit with Framey hands such a project over, its scaffolded CLAUDE.md and
AGENTS.md (they start "# HyperFrames Composition Project") gain the
template's line, in the template's place. A file of the person's own, a
link, a missing file, or one that has the line is left alone. The file is
rewritten through a temp file with its mode kept. A test keeps the line the
same as the template's.
* fix(cli): keep catch-up's cursor in the person's home, never reset it, read without links on Windows
Review (Rames, Jerrai):
- The seen record moved from the project to ~/.hyperframes/catch-up/,
keyed by the project's real path. A seen file a cloned repo commits
can no longer make its history read as the person's words.
- A second hand-off (`open` again, or another Edit with Framey click)
keeps the cursor, so turns the app recorded meanwhile still show.
- Reads lstat the path, open it once, and refuse a file whose dev/ino
differs from what lstat saw. That holds where O_NOFOLLOW doesn't
exist, fixing the Windows test. The note writer reads the same way,
which clears the new CodeQL alert.
- Writes use core's replaceFileAtomically, which retries a rename that
Windows briefly refuses.
- catch-up names every changed file, and the cursor is the newest `at`
shown, not the last line's.
- `open --json` carries `catchUp` after a hand-off.
- The bin scripts are back to mode 644.
* fix(cli): open before checking for CodeQL, never show a turn dated ahead
Review (Rames, Jerrai):
- readPlainFile opens the file first, then requires lstat to find the same
plain file (dev/ino) at the path, and reads only through the fd. That is
just as safe, and it clears CodeQL's check-then-use alert.
- catch-up skips turns dated after the moment it reads. So a cloned history
can't show a far-future turn as the person's words or park the cursor
past real ones.
- The seen key uses core's realpath (realpathSync.native), so letter case
can't split a project in two. ~/.hyperframes/catch-up is made 0700, and
a hand-off that can't note itself says so.
- Tests stub HOME with vi.stubEnv and restore it.
* fix(studio-server): keep the desktop app's hand-off and chat history out of the file routes
The file and preview routes reach any path inside the project. The desktop
app keeps two files there that hold the person's agent session and their
chat: .hyperframes/agent-handoff.json and .hyperframes/app-history.jsonl.
No route now reads, writes, renames onto, duplicates or uploads over them,
by any spelling or through a link. The rest of .hyperframes/ (Studio's
motion file, prepared GIF assets) is served as before.
* fix(studio-server): keep the desktop app's run log out of the file routes too
The app records every chat run, the person's words included, in
.hyperframes/agent-runs.jsonl. Only the app's main process reads it.
* fix(studio-server): refuse everything under .hyperframes/ but Studio's own files
Review (Jerrai, Rames): a list of three names missed the read marks the
desktop app and the CLI also keep there, and renaming the folder reached
them under a new name. The check is now an allow-list: no route reaches
.hyperframes/ or anything in it, by any spelling or through a link, except
Studio's studio-motion.json, studio-manual-edits.json and prepared-assets/.
Studio's own internal files (backups, preview documents, history id) are
read by path inside the server, not through a request path.
The project's file tree leaves out the same paths. The upload test now
sends a file uploads accept elsewhere, so the folder is the only reason
it is refused.
* fix(studio-server): serve the app's request pictures, close the renders and rename paths
Review (Rames, Jerrai):
- Desktop loads each request's picture from .hyperframes/requests/ through
the preview route, so that folder joins the allow-list. An allowed
folder's own path is allowed too.
- The render-file route resolves against renders/, so a renders/ linked
into .hyperframes/ reached the records. It now also checks the file
against the project.
- A rename's reference update walked every JSON/MD file, records
included, and could rewrite them. It now skips private paths.
- The allow-list reuses STUDIO_MOTION_PATH and STUDIO_MANUAL_EDITS_PATH.
* chore(studio-server): keep render.ts's comment share at the base
* fix(studio-server): list, view and download only renders the render-file route would serve
Review (Jerrai, Rames): the render list found files in renders/ with join
and statSync, which follow links, and registered them for /render/:id/view
and /download, which read them with no check. A project shipping
renders/leak.mp4 -> ../.hyperframes/app-history.jsonl had its chat served.
The list now keeps an entry only when resolveWithinProject accepts it and
it isn't private, and registers the resolved path. View and download serve
only a plain file, so a link swapped in after the list is refused. This
predates the PR; it's the same shipped-link case.
Dockview keeps inactive "always"-rendered panel overlays in the DOM with
visibility:hidden and no position, so each one falls to its static position
below the dock at full dock height. That made the document ~2x the viewport
tall (1954px in a 997px window). body overflow:hidden only blocks user
scrolling, so the scrollIntoView that runs when a property group opens
scrolled the viewport 124px and cut off the header and panel tabs.
overflow:clip on #root (not hidden, which is itself programmatically
scrollable) removes the overflow: scrollHeight drops to the viewport height
and scrollIntoView on a hidden overlay no longer moves anything.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): namespace SVG ids in runtime-mounted compositions
* fix(core): keep SVG selectors and captured ids working after loaded scenes rename them
* chore(core): generated artifacts carry a plain do-not-edit banner
* test(catalog): record the font subsets the new scene-script installer requests
* test(catalog): keep earlier mirror recordings, as re-records only add
* test(studio): keep the undo socket gate's sockets pinned past Chrome's idle media suspend
* test(studio): the socket gate retries its video scan and says only what its pins prove
* docs(cli): map the pinned Sherpa runtime loading contract
* fix(cli): use a healthy pinned Sherpa copy beside the CLI
* chore(cli): keep runtime loader comments within the ratchet
* docs(cli): wrap the runtime contract palette declarations
* chore(cli): keep the Sherpa contract note out of the repo
* fix(catalog): 3D previews load their scripts under the docs host's script policy
* fix(catalog): preserve script text through HTML parsing
* docs(catalog): describe the shared script bundle
* test: pin the guards that keep script-placed scenes and unparseable scripts out of the in-place move
* test: make the script-placed scene check able to fail and pin its page order
* fix(parsers): dragging a keyframe keeps the keyframes' ease
* fix(parsers): splitting a keyframed tween into groups keeps its eases
* fix(studio): keyframed tweens keep their eases when Studio rewrites them
Extending a keyframed tween to the playhead and merging a gesture recording into it now carry the tween's eases and each keyframe's own ease. The parsers no longer report a tween-level easeEach, which GSAP ignores, as the keyframes' ease.
* fix(studio): a gesture merged into a tween writes rounded keyframe percentages
* fix(studio): a gesture merged into an eased tween plays at the times it was recorded
Merged keyframes are placed through the tween's run ease, as the extend path already does, and recorded constant-speed segments stay linear.
* fix(studio): the ease card describes a keyframed tween by its keyframes and eases
The summary line read 'no properties yet using a none curve' for every keyframed tween. It now names the keyframed properties, the keyframe count, the segment ease and the run ease that GSAP plays.
* fix(studio): a gesture merged over a strong ease-out keeps its keyframes and the ones before it
The merge keeps or replaces old keyframes by when they play, and rounds percentages only when the run ease does not warp time.
* fix(studio): the ease card labels an unset segment with the ease GSAP plays
* fix(studio): the ease card header shows the segment ease its rows play
The header shows the one ease every segment plays, or Mixed, and a run ease other than none gets its own line.
* fix(parsers): keyframe percentages are written as decimal keys the parsers read back
A percentage below 1e-6 printed as '1.28e-7%', which both parsers reject. One plainPercentKey owner now writes every percentage key in both writers and the unroller.
* fix(studio): the ease card's segment rows show rounded percentages
The three adapter skill had no guidance for camera cuts made inside one
canvas. A visibility window written as its own literal near a cut shows
the next shot's objects for the last frames of the previous shot, a
two-frame pop-in that a still per shot does not reveal. The skill now
says to declare each cut once, read it in both the shot selector and
every visibility window, and snapshot the last frame before each cut.
It also explains why a MeshStandardMaterial lit by a single key light
renders as a flat black cutout when the camera faces that light, and to
add a fill such as a HemisphereLight driven at a fixed ratio to the key.
Signed: Miga
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* fix(studio): a size edit right after a resize is its own undo step
* fix(studio): a nudge still saving is recorded before the next edit
* fix(studio): published types name cn by its package, not a node_modules path
* fix(studio): drop baseUrl so declarations never name a node_modules path
* fix(studio): an edit started inside the save of another edit never flushes
* fix(studio): an edit made while a nudge is still saving writes after it
* fix(studio): a later edit never overtakes one still waiting on a nudge save
* fix(studio): undoing a gsap.set clears what it applied from the preview
* fix(studio): an edit never waits on itself behind a nudge save
* fix(studio): a second nudge burst saves when its commit runs behind the first
* fix(studio): undoing a W on an animated box clears the width it set live
* fix(studio): any GSAP edit that drops a live-applied set clears it from the preview
* fix(studio): a soft reload clears exactly the set props a live patch applied
* fix(studio): name width and height as set-patch props
* fix(studio): Layers rows show a kind icon, and only a group's row has a caret
Rows show what an element is (image, video, audio, vector, group, text or shape) instead of the tag's first letters. The caret column sits only on rows with children, a group's children indent under its icon, and rows are inset with rounded corners so the selection reads centred.
* style(studio): name the Layers indent and phrasing tags instead of commenting them
* refactor(studio): Layers text and host checks reuse the editor's own rules
* fix(studio): a Layers child's icon starts where its parent's label starts
* refactor(studio): Layers text icon uses the inline editor's text check
* fix(studio): a Layers row with children shows the group icon, and its icon keeps the row's hint
* fix(studio): Layers text with styled spans keeps the Text icon
* fix(engine): a render fails when a composition script throws before its timeline binds
* fix(engine): an unrelated page error no longer fails a render whose timeline registers late
* fix(engine): only the composition's own scripts fail a render, local or distributed
* style(engine): format the script failure change
* fix(engine): a third-party browser rejection no longer fails a render
* refactor(engine): one page URL for navigation and error attribution
* fix(engine): a rejection after a hash change or in an iframe no longer fails a render
* fix(engine): a widget's error no longer fails a render once its script is inlined
* fix(core): name injected and inlined scripts without building code from library input
* fix(engine): a third-party embed's eval or inserted script no longer fails a render
* fix(producer): a widget's inline handler or a sub-composition no longer misreports a render
* refactor(engine): read each script response once and keep comments within 120 characters
* fix(engine): a scene that throws after registering its timeline no longer cuts a render short
* fix(engine): a composition script failure names the error and its real cause
* fix(engine): a failed VFX chain fails the render at once instead of shipping a missing layer
* fix(core): a VFX layer that is empty on one frame renders empty instead of failing or going stale
* fix(core): a VFX layer that grows from zero size renders instead of staying missing
* fix(core): a VFX layer hidden or late while empty renders once it has a size
* test(producer): cover a VFX layer that is 0×0 under a host that starts later
* test(producer): pin both VFX 0×0 marks, with a GSAP-shaped wipe and a real late host
* test(producer): a late VFX host with a GSAP wipe catches a walk that skips hidden hosts
* fix(core): a sub-composition follows its host clip when the host moves without a reload
* perf(studio): move live tweens in place for a sub-composition host's clip too
* fix(core): keep sub-compositions nested where a fresh load nests them across rebinds and re-runs
* fix(studio): match the preview's re-printed script by its tokens and check every other timeline
* fix: compare preview scripts by syntax tree and keep scenes a re-run script places
* fix(parsers): keep a tagged template's raw text and a __proto__ key in the script compare
* fix(parsers): keep only a tagged template's raw text in the script compare
* perf(studio): read each preview script once across repeat timeline drags
* style(studio): format the preview script read test
* perf(studio): keep only script shapes and refresh them on reuse