mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-07 07:09:15 +00:00
sync/hyperframes-codegen-58de81bf
709
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
536346c5d1 |
feat(core): block capture motion blur on html-in-canvas defects (#5971)
html-in-canvas captures a few things wrongly, and a wrong blurred frame is worse than a sharp one. Add a defect list the runtime checks before it wraps a composition: a WebGL or WebGPU canvas (captured frozen), backface-hidden faces in a 3D context (drawn mirrored), an iframe (reloaded by the wrap and not recoverable), a transform or opacity below 1 on the root (ignored by the capture) and a canvas that is already a capture canvas (nesting hangs the renderer). Any hit turns blur off for the whole composition and the runtime says which one. The backface scan reads rules nested inside a style rule (CSS nesting), sheets pulled in with @import and adopted style sheets. Shadow DOM stays a documented limit. A face whose backface-visibility: hidden arrives through an animation counts too: the scan also takes the elements of running CSS or WAAPI animations that key it, and elements naming a @keyframes rule that does, as candidates. Support Chrome 157's html-in-canvas API alongside the older one: a canvas marked content="drawable" with a drawable attribute on captured children replaces layoutsubtree. Blur, the VFX runtime and Studio's browser check detect both through the new @hyperframes/core/html-in-canvas subpath. Folded review fixes: R14, R9 (the iframe defect entry). Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
1f8ead7bde |
feat(producer,studio): wire capture motion blur into renders and Studio (#5969)
Carry the runtime's blur through the render pipeline and the Studio render panel. - engine: installPageMotionBlur sets the page-side off switch and the render's device scale before any author script runs; CaptureOptions.pageMotionBlur lets a caller stand the page blur down. - producer: a composition that asks for data-motion-blur on the root the runtime would pick forces screenshot capture, because html-in-canvas needs a real paint; the probe session carries the request's pageMotionBlur like every other session. - studio-server and Studio: the render panel sends previewMotionBlur, the render route turns "off" into pageMotionBlur once, and the Studio dev server and the CLI server both receive the final value. - mover scan: a large element that is see-through while it moves (a sweep, a light leak) is no mover, including a large image or canvas with alpha; the scan skips elements moved by a 3D individual transform property. - cli snapshot injects video frames before the seek settles, matching the engine's order. Folded review fixes: R1, R2, R3 (scan half), R7, R16, R18, T3 (render adapters). Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
688229314f |
feat(skills)!: publish seven knowledge skills aligned with Studio and retire the workflow skills (#5651)
* refactor(skills): fold six domain skills into hyperframes and media-use guides
hyperframes-core, -animation, -keyframes, -creative and -studio move to
skills/hyperframes/{core,animation,keyframes,creative,studio}/, and
hyperframes-audio moves to skills/media-use/mix/. Each moved SKILL.md becomes
a frontmatter-free GUIDE.md so installers that scan recursively for SKILL.md
no longer see them as skills; the published list goes from 21 to 15.
Relative links, code paths and skill-name mentions are rewritten to the new
locations, the catalog surfaces, marketplace core-skills list, fallback core
set, module-copy targets and skills-manifest.json follow, and
`hyperframes skills update` now removes the retired skills (RETIRED_SKILLS)
when they are still installed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): slim the core entries and give media-use a HeyGen and a free path
The four entries every run opens are rewritten to what an agent cannot infer:
hyperframes 21.8 KB to 7.7 KB, hyperframes-cli 24.8 KB to 6.9 KB,
hyperframes-registry 9.9 KB to 4.3 KB, media-use 9.7 KB to 4.9 KB. Detail moved
whole into references (routing close calls and creator edits, CLI pin upgrades,
catalog search tiers, CLI conventions and feedback reports).
- hyperframes: state table, routing, the render contract, a terminal work loop
(check, snapshot, preview before render), the desktop-app hand-off, and a
table of the folded guides.
- media-use: one table of HeyGen versus free local tools per need; HeyGen
sign-in, credits and the CLI's video-agent, avatar, clone, lipsync, translate
and clipping commands; every free path works with no account.
- scripts/check-core-skills.mjs holds entries and descriptions to budget and
checks every taught `npx hyperframes` command and path; runs in lint.
- Core entries are exempt from the formatter: its table padding cost ~1.5 KB
per entry on every run.
- Content tests follow the text to its new home.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): plugin runs skip skills update; media-use explains HeyGen sign-in and TTS setup
The smoke eval showed a presenter request never reaching media-use and the
free voiceover failing on a missing kokoro-onnx; plugin runs also ran the
skills update the plugin rules forbid.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(skills): drop a committed Python bytecode file and ignore bytecode
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): align the entry with the Studio skills and keep the workflows' measured facts
The entry drops routing and the intent interview and carries the contract, the
editable-project rules and the work loop. Studio's format, runtimes, motion,
look and captions references come over for terminal use, frame presets move to
assets/, media-use gains Studio's mixing and local-command references, and
film.md and overlays.md keep what the workflow skills measured.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills)!: retire the workflow skills and folded guides; publish seven knowledge skills
The skill pack now publishes exactly seven skills - hyperframes,
hyperframes-cli, hyperframes-registry, media-use, figma, slideshow and
remotion-to-hyperframes - carrying knowledge, not process. The eight
workflow skills, the folded domain guides and the routing/interview
references are deleted; their measured facts already live in the
hyperframes references (format, runtimes, motion, look, captions, film,
overlays) and media-use's mixing reference.
- bake-basemap.mjs and locate.mjs move to skills/hyperframes/scripts/
(bake-basemap gains --help); extract-audio-data.py moves to
media-use/audio/scripts/ for the caption motion reference.
- skills update: the eight workflows join RETIRED_SKILLS so existing
installs prune them (by lock attribution only, since their names are
generic); naming a retired skill reports where its knowledge went and
succeeds instead of failing as unknown.
- Capture prompts point at the hyperframes skill's references/film.md;
the live-map warning names the new bake-basemap path.
- Content tests that pinned knowledge are retargeted to the kept
references; tests of deleted workflows, scripts and process go.
- Catalog surfaces, templates and docs describe the seven skills.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): resolve the local.md captions link and allow capture's output files in citations
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): regenerate the skills manifest from tracked files only
The committed media-use hash included an ignored __pycache__ bytecode file present
in the local checkout, so CI's clean checkout hashed differently.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(docs): restore generated catalog pages and docs navigation to main
Catalog PRs commit item sources only; CI regenerates the pages from the edited
registry sources. docs/docs.json is catalog navigation, so its Workflows group
rename is left for a non-catalog PR.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* build(cli): raise the CLI size budget for the skill references it bundles
The CLI bundles skills/hyperframes, which now carries the folded domain guides and
frame presets (155 KB to 1.08 MB in git). Budgets are the CI-measured size plus 5%:
packed 7526634, unpacked 22593213.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test: keep repo-sweep guards meaningful after the workflow skills left
The last shell ffprobe caller and most root index.html examples lived in retired
skills. The shell detector is now pinned on a literal command line, and the
structure-rule sweep needs more than 5 roots (11 remain) rather than 30.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): slim figma, slideshow and remotion to the Studio shape; restore the storyboard board
slideshow 33.5 KB to 4.9, figma 18.2 to 6.2, remotion-to-hyperframes 11.0 to
4.9, each keeping the facts only it holds (the figma motion helpers, verify gate,
storyboard decoding and usage beacons; present and hand-off; the Remotion lint,
SSIM check and API map). The board page format from the storyboard recipe comes
back as hyperframes/references/storyboard.md: the desktop app's Storyboard tab
reads boards/storyboard.html by its frame, title and time classes. The entry and
the init template now say to snapshot the moments an edit changed before
replying, which the terminal eval found skipped in 22 of 38 runs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): manifest without local bytecode; Studio's edit prompt names kept files
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): render when the person asks for the file or for no stops, not only after approval
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): keep the voiceover carve in media-use; presets stage their own fonts
Review follow-ups on #5651:
- carve.mjs and its tests move to skills/media-use/audio/scripts; the audio
docs, mixing.md and two Studio comments point there.
- code-editorial's FRAME.md says to copy its bundled WOFF2s and add the
@font-face blocks, instead of promising the retired build-frame.mjs.
- media-use states the host rule before the HeyGen sign-in steps.
- storyboard-format marks transition_in as a note the agent builds itself.
- Two stale citations (creator-editing-recipes.md, sub-compositions.md).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(skills): regenerate the manifest without local bytecode
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): the CLI render step follows step 4's rule, not approval alone
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(ci): run the core-skills check from lint again after the main merge
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): films that tell a story start from three storylines; maps use real imagery
Opus 5.5 arm D read motion.md and applied it, but its launch film was a
feature list (pain, three feature cards, proof, logo): film.md left story
to the model. And the map case never opened film.md, so it drew Natural
Earth outlines instead of baking satellite tiles.
- references/story.md: the generic half of Studio's moment-film story and
craft (one thing, three storylines with world and device, the hook,
beats, worlds, copy, brand, tells of a generated film). The person picks
a storyline, or the agent takes its recommendation when asked for the
file or for no stops.
- The entry's work loop names the storyline step for films over ~15 s; the
references table routes stories to story.md and maps to film.md.
- film.md points at story.md and gives maps and real places a section.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): story.md follows the entry: no voiceover unless asked
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): bake-basemap starts from a globe and stops waiting out its idle timeout
The Opus 5.5 map run spent 13 of its 38 minutes on parallel bakes that
stalled and 15 more recording 300 frames, after rewriting the script because
it could only start on a flat map.
- PROJECTION=globe with a seeded starfield and atmosphere, and FROM/PAN to
turn the globe to CENTER before the dive.
- Each frame polls for loaded tiles instead of waiting for an "idle" event
that had often already fired: 30 frames went from 198-219 s to 10-13 s
locally with byte-identical frames.
- Progress prints seconds a frame and minutes left; film.md shows the globe
call and says to run one bake at a time.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): render in the foreground; wait on a detached render's log, never pgrep -f
Both Opus 5.5 explainer runs started a detached render, then waited with
`until ! pgrep -f "hyperframes render"`, which matches its own loop: each
render took 88-117 s and the wait sat for the tool's full 10 minutes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): route each kind of film; storylines carry look and sound; add review and sound
The entry routes by request (launch, explainer, music, data, vertical, 3D,
character) to a self-contained file under references/types/, and a film
over ~15 s now gets music and sounds on its visible actions unless the
person asked for silence. Each storyline names its look and its sound, so
picking one picks both; STORYBOARD.md frames carry `reads` and `sound`.
review.md is the whole-film pass: the check warnings, the review sheets,
seven questions to look for, P0/P1/P2 notes and REVIEW.md. motion.md gains
eases by role, six title entrances, a reading-time rule and nine measured
transitions; look.md a design plan before the first scene, with presets
kept for decks and named picks. story.md adds the second default that
generated design falls into, with what to do instead.
The blank template and format.md no longer teach the fade-and-slide-up
entrance on a dark canvas.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(cli): check warns on unread lines, dead stretches, small type, empty frame 0 and silence
A coding agent cannot watch its film, so check now measures what a viewer
notices over time. A new `craft` section holds warnings only (exit 0
unless --strict), sampled on the dense 8 fps grid the overlap pass
already seeks, skipped for --range checks:
- line_unread: a reading-size line (>= 30 px tall at 1080) on screen
under 0.4 s for 1-2 words, else under 3/4 of 0.5 s + 0.3 s a word;
the film's last line needs 2 s. Short lines are re-measured between
samples to ~0.01 s before judging. Captions-track and
data-layout-ignore text are exempt.
- dead_stretch: more than 3 s where the keepsMoving liveness signature
does not change, outside the final 3 s and any data-hold element;
quiet when sweep_static fires or a whole-composition keepsMoving
assertion owns the limit.
- text_below_floor: text under 2.6% of the short side (28 px on 1080)
held 0.5 s, one finding per file listing every element.
- first_frame_empty: frame 0 paints only the background.
- film_silent: over 15 s with no unmuted audio or audible video.
Each message names the fix and the skill reference to read. keepsMoving
now also leaves the film's last 3 s (a still end card) out of its
static runs, so the two agree.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(cli): snapshot --review captures a beat sheet, seam strips and a phone sheet
snapshot --review picks the frames that show a film over time instead of
evenly spaced ones, in the same single browser session:
- review-beats.jpg: one settled frame per scene at its start + 0.3 s,
plus one per timed data-beat="name" element, labelled time and id.
Scenes are the composition's top-level timed clips and sub-composition
hosts, read like `timeline` reads rows (captions and audio excluded)
and timed by the runtime's start resolver over the live page.
- review-seam-NN.jpg: frames at -0.2, -0.1, 0, +0.1, +0.2 s around
every cut (each scene start, and any scene end no scene starts at).
- review-phone.jpg: the beat frames at 360 px wide.
--composition scopes the review to one sub-composition; with fewer than
two scenes the beats spread evenly and a note says so. snapshot also
gains --json: one envelope with every frame's path and time and, under
--review, the scenes, beats and seams behind the sheets; progress moves
to stderr.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): craft warnings stay out of --strict; a hold gets a real next action, not a caret
Craft findings (unread lines, dead stretches, small type, an empty first
frame, silence) are judgment calls a film may break on purpose, so
--strict keeps gating defects only. The check reference no longer
suggests a blinking caret to keep a still frame moving.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): review spreads beats when every scene starts together; map credits skip the type floor
A map film whose basemap and overlay both start at 0 s got one beat
frame; the even spread now keys on distinct scene starts. film.md tells
agents to mark the imagery credit data-layout-ignore and back it, since
the new type floor flagged it on the first real run.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): review frames each scene at its middle too, not only 0.3 s in
On the first real explainer, 0.3 s into a scene showed a bare colour
field before its title entered. Each scene longer than about a second
and a half now adds a frame at its middle, so the sheet shows what the
scene builds.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): a film is built from a numeric SPEC.md, with defaults banned by name and a look test
Studied in full: flxrnc's Motion Lab (vague vs directed prompt), 0xMovez's
course, EianLu's riso skill, agentchengfeng's style samples and the two
Opus 5.5 guides. Their common point: adjectives are where the default look
lives, so the agent now writes SPEC.md before any scene (GOAL, OUTPUT,
TIME, LOOK, MOTION, SHOTS, BANNED, QA), with a number for every adjective.
spec.md bans the defaults by name, unless the brand or reference uses
them (a dark void with small things floating, glow and particles, the
cream-serif-mono-label look, fade-and-slide-up, corner brackets), and
grows that list after the first render. Each storyline gets a 3 s test of
its hero shot before the pick; a look is written as the rules of one
medium. Composition numbers: the hero fills 40% of the frame, one hero
move per shot, the shot's information in within 2.5 s then a held poster.
motion.md adds springs by damping ratio, shorter exits, overlap, stagger
and the beat grid; review.md reviews as a harsh director and grows BANNED;
vertical.md takes the riso skill's measured safe band; explainer.md the
shapes of information.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): story before look; every object reads as itself; posters only for info cards
The first spec-driven C02 swapped the dark default for a flat Swiss
poster, and the story went with it: two black bars nobody read as mirrors,
slogans instead of cause and effect, a storyline picked for its picture.
The spec now serves the story: a storyline is picked for the story a
stranger would follow, the look test asks first whether a stranger
understands what they see, and SPEC.md gains an OBJECTS line giving each
thing the material and details that make it read without a label. The
2.5 s held-poster rule is limited to information cards; in a story film
every shot causes the next. Explainers start from a person or situation
the viewer knows. Review names every object as a stranger would and
treats one that needs explaining as a P0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): storylines are three films in words, each with a device and a form
The three storylines are pitched as text and the person picks from them;
nothing is built for a storyline before the pick, so the 3 s hero-shot
look test is gone. Ideas pair a device with a form, five or more first,
keeping the three that differ most, and the one stop states the rest in
an assumption line that names every paid item beside its free option.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci: list main's new embedded-captions hf-cli test among the retired workflow files
The merge with main removed it with the rest of the retired skill, and the
deletion guard rejects any removal it has not been told about.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(core): keep the audio preset comment within core's size budget
The comment's new citation to media-use's mixing reference made the published
package 14 bytes larger than its budget, which sits at main's exact size.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): craft keeps its own 8 fps grid; comments back within the checks
Review on #5651 found that the merge moved craft onto the overlap pass's
times, which main caps at 180, so a brief line in a long film fell between
samples and line_unread missed it. Craft now builds its own grid at 8 fps up to
600 samples, as before the merge, and a 60 s film with a line shown for 0.2 s
pins it.
The Comments check passes again: REVIEW.md and STORYLINES.md join the files a
skill writes into a project, the comment lines this branch added come back
under each file's share at the merge-base, and the audio preset comment drops
a word the check reads as history.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
||
|
|
5975dd7579 |
feat(studio): the desktop-app button says Install the desktop app and never dead-ends (#5854)
* feat(studio): the header's desktop-app button says Install the desktop app and never dead-ends The button now reads "Install the desktop app" beside Framey drawn as the app draws it. Where the app is installed, a press opens this project in it; a folder preview serves from a bare index.html gets its hyperframes.json first, so the app no longer refuses it. Without the app, the button links straight to hyperframes.dev's download, and a failed open lands there too. Platforms with no build of the app show no button. Telemetry now records eligibility, one impression, the click and the press outcome, with OS, app installed and download available. * feat(studio): the desktop-app button records eligibility, impression, click and outcome * feat(studio): a desktop-app press carries its page id to the download and the app hand-off * chore(studio): describe Framey's drawing without internal names * feat(studio): the desktop-app button opens a guide that walks download, install and open Without the app, the header button opens a card where Framey walks three steps: the download opens in a new tab, the install is found by asking the preview server while the card is open, and the project opens through the existing hand-off. A failed hand-off shows the folder to copy and Try again. With the app ready the press opens the project directly. Each step change is recorded through trackStudioEvent; a download is never counted as an install. * fix(studio): the desktop-app guide counts one install per page The app removed and found again on the same page recorded install_detected twice; a page latch keeps it to one, as the telemetry contract says. |
||
|
|
2b9009ab52 |
fix(studio): keep a preview reload waiting while a slow project boots (#5823)
* fix(studio): keep a preview reload waiting while a slow project boots Studio gives a reload up to three 15 s budgets, but the player failed it at 8 s and Studio's own 5 s safety net stopped listening for the runtime. A host can now own the load deadline (no-load-timeout); script and runtime errors still fail at once. * fix(player): send the 8 s load deadline through the probe's one error path |
||
|
|
ba08acb1f5 |
fix(studio): keep the shortcuts panel above the player controls and inside the window (#5457)
* fix(studio): keep the shortcuts panel above the player controls and inside the window The panel was drawn inline next to its button at z-50, so a host layer above that (the desktop's playback bar, z-55) covered its Playback rows, and the preview pane's overflow clip cut its top off in a short window. It now opens through Studio's Popover on the shared popup layer: portaled out of the button's boxes, held above the button, and capped to the room there with a scrolling body. * fix(studio): the shortcuts button keeps its tooltip description for screen readers |
||
|
|
9825614d91 |
fix(studio): Studio's own text reads without em or en dashes (#5414)
* fix(studio): text in Studio reads without em or en dashes * fix(studio): the dash check also catches HTML entities, and peak reads Unavailable * test(studio): the dash check test covers every entity form * fix(studio): messages Studio shows from lint, sdk, core and the renderer drop dashes too * fix(producer): the gate check stops early on the engine's new fallback wording |
||
|
|
91f9c9eb04 |
feat(studio): export can encode on the GPU, like render --gpu (#5397)
* feat(studio): export can encode on the GPU, like render --gpu * test(studio): the 4K export assertion carries the GPU flag * refactor(studio): the GPU flag needs no comment beyond its name * fix(studio-server): only MP4 exports encode on the GPU, and render errors report it * fix(studio): the disabled GPU option dims like the other export controls |
||
|
|
d93b769763 |
fix(studio): a stalled font or asset no longer leaves the preview empty or drops reloads (#5370)
* fix(studio): a stalled font or asset no longer leaves the preview empty or drops reloads * fix(studio): a page's own late load no longer counts as replaced content * refactor(studio): one announcement owns when a preview page has loaded * fix(studio): let hosts with their own player announce a new preview page * fix(studio): a late ready from the replaced preview page no longer claims the new page * refactor(studio): keep the Player doc comment on Player |
||
|
|
5562c280da |
fix(core): stop small previews dropping layers on heavy compositions (#5268)
* fix(player): render the preview at its shown size so heavy compositions stop dropping layers
* test(player): check a small preview rasters its will-change layers at the shown size
* fix(player): accept device-pixel snapping in the zoom probe
* fix(player): probe the initial frame and preserve editing coordinates
* fix(player): defer every initial source reload until setup
* fix(studio): convert measured frame coordinates once
* test(studio): bank reduced preview opening work
* fix(core): keep centered layers exact when the preview is zoomed
GSAP infers a -50% CSS translate from the rendered pixel matrix. At a
fractional preview zoom that matrix is snapped, so a centered layer lost
its percentage and shifted by half its height after reload, render and
redo. The runtime now restores the authored percentage before GSAP's
first transform parse. Also restores the original thumbnail ceilings.
* test(studio): bank the two resize cases that now pass edit accuracy
resize-scale-px-r30-{root,nested}-z50-after measured 0.53 px tracking on
main and 0.49 px here. Also drops a comment line the runtime name covers.
* fix(core): keep stylesheet-centered layers exact under preview zoom
The centering repair now also reads percentages from a stylesheet
transform (the common translate(-50%, -50%) pattern), runs for every
property that makes GSAP parse a transform (transformOrigin first, too),
resolves against the border box, and leaves elements without a layout
box alone instead of throwing. The browser check now runs zoomed.
* fix(core): restore only centering, the one percentage GSAP infers
GSAP folds every other CSS percentage translate into pixels, and
compositions rely on that (a translateX(-100%) title tweened to x: 0
slides in). The repair now gives back only a -50% that zoom snapping
lost, and treats a size that is not in px (a hidden element sized in
percent) as no box.
* fix(core): restore centering only where GSAP would infer it
A pixel part in the translate means GSAP never infers -50%, so the
repair leaves it alone, and a layer without a layout box is left to
GSAP whatever size its computed style reports (that differs by Chrome
build for hidden percent-sized layers).
* fix(player): fit the preview before the composition starts loading
The zoom was applied on the first resize callback, after the source was
assigned, so a fast-loading composition could run its scripts and lay
out unzoomed and then reflow. Fitting on connect, before any source,
makes the first layout use the preview zoom.
* test(player): pin the first fit before the composition source
* revert(player): drop the zoom fit so the preview keeps the export's layout
CSS zoom shaped text at the zoomed size, so paragraph heights in the
preview differed from the render by up to a line. The transform fit
stays; the centering adapter and zoom-only bench changes go with it.
* fix(core): raster a small preview at the size it is shown
Chromium never rasters a will-change: transform layer below device
scale, so a preview shown at a quarter size rastered every hinted layer
at full size and could run out of tile memory, dropping layers. While
the player shows the composition below 1x, the runtime swaps the hint
for an identity filter, which keeps its stacking context and
containing block; preserve-3d containers keep the hint.
* fix(core): keep a small preview's raster swap out of filters edits can save
The identity filter that replaced will-change: transform showed up in computed filter, so
Studio's blur slider saved it, GSAP filter tweens and clearProps picked it up, and it cut
backdrop-filter off from what is behind it. A far perspective keeps the same stacking context
and containing block, is never composited for flat content, and nothing reads or writes it.
Also: the runtime guard lint (realm-safe element checks), a class change that drops the hint
undoes the swap, and the browser check measures raster scale with the swap off and on at one
shown size and compares frosted glass and 3D pixels.
* fix(core): drop preview raster hints only where nothing depends on them
The perspective that stood in for will-change: transform could still reach a saved project:
Studio records perspective keyframes from GSAP's computed value, and copying a generated
element serialised its inline styles. It also went stale when a parent class or a later
stylesheet changed the element.
Now the runtime writes no inline style. It marks a hinted layer with a preview attribute
and one injected rule sets its will-change to auto. Only layers whose hint changes nothing
are marked: positioned, with no fixed descendants, and no z-indexed or blending descendants
unless the layer has its own z-index. Every class or tree change re-checks the whole page.
Studio strips the mark from copied markup, as it does the other preview marks.
* fix(core): keep raster hints that pseudo-elements, other hints or inline styles need
A layer's ::before or ::after can sit behind it, blend with it or be fixed to it, so those count
like children. A layer that also hints opacity or filter keeps every hint, since those make their
own boundaries. Inline style changes, which scripts make every frame, are watched cheaply: they
only give the hint back to the layers holding an element that now depends on it, or to a layer
that turned static or changed its z-index. Text changes no longer trigger a full pass; a new
stylesheet still does.
* fix(core): re-read a marked layer's own hints and follow any attribute or late sheet
A marked layer now has its mark lifted while its hints are read, so a class or inline value
that adds opacity or filter gives every hint back. Any attribute change re-checks its subtree,
so attribute selectors are followed, and a linked stylesheet re-runs the pass once it loads.
* perf(core): only lift a raster mark on the element that changed
Lifting a mark to read a layer's own hints repaints it, and a change on a scene container lifted
every marked layer under it (about 6 ms and a re-raster per event). Now only the changed element
is lifted, and a full pass clears all marks at once so the page restyles a single time. Tests
cover leaving marked layers alone, ignoring our own marks, and the linked-sheet load pass.
* fix(core): decide raster marks before writing them in a full pass
A full pass re-marked layers one at a time, so each read restyled the page again. Every mark is
now decided first and written after. The browser check holds off the player's own scale
messages while it turns the swap off and on, since a late resize tick could turn it back on.
* test(player): hold the player's own scale while the raster check turns the swap off
Turning the swap off restyles the page, and the player answers by resending its real scale,
which turned the swap back on within milliseconds, so the off half compared the swapped page
with itself. The check now holds the player's sends on the host page, asserts the swap is off
before the native shots, and removes the layers that keep their hint before either trace.
* test(player): give the raster check's 3D probe a solid fill
Stripes a few pixels wide alias differently at each raster scale, so the probe measured texture
resampling rather than the 3D shape it is there for. A solid fill keeps the shape and position.
* fix(studio): keep the preview raster mark out of stacking and saved text edits
Studio's stacking model read a marked layer's will-change as auto, so a timeline drag could
write a z-index that does nothing in the render; a marked element now still counts as a stacking
context. A text edit copied every live attribute but style, so it could save the mark; the
preview-only attributes are now one list, used by both the copy and the text-edit paths.
* fix(core): keep a marked video through a scene swap
The runtime keeps a video across a scene swap only when its live attributes match the fresh
import, and the preview raster mark made them differ, so a hinted video in a small preview was
rebuilt on every edit. The comparison now ignores every preview-only attribute, which also
fixes the same rebuild for data-hf-upcoming. The shared list is renamed PREVIEW_ONLY_ATTRS, and
the lazy-image strip in copied markup gets a test.
|
||
|
|
6b341b0d84 |
fix(studio): a resized corner stays on the pointer when the size rounds to whole pixels (#5213)
* fix(studio): a resized corner stays on the pointer when the size rounds to whole pixels Resized sizes stay on whole CSS pixels (GSAP centres a -50% translate from the integer offset size), so the centre-anchored resize left the grabbed corner up to a third of a pixel times the element's scale off the pointer: 0.53 px on a 1.5x scaled layer. The centre pin now takes half the rounding remainder along the element's rendered edges, so the grabbed corner lands where the wanted size puts it while the written size stays whole. * fix(studio): a scale resize saves the box exactly where it was dropped finalizeScaleResizeCommit skipped its position correction whenever the box sat within 0.5 px of the drop point on each axis, so a sub-pixel drop (now the norm, with the grabbed corner on the pointer) saved up to half a pixel off. It now skips only when the correction rounds to nothing at the precision the file is written in. * fix(studio): a scale resize lands centred on the box that was dropped finalizeScaleResizeCommit lined the committed box up with the drop by its top-left corner. When one scale cannot reproduce the dragged width and height (the draft rounds each to whole pixels), that put the whole gap on the far edges, up to half a pixel. The resize is centre-anchored, so it now lines up centres and the gap splits evenly. * chore(studio): keep the complexity note on the resize intercept, not the new helper * test(studio): bank the two scaled resize cases that now pass the edit accuracy bench * fix(studio): a scale resize leaves animated positions alone and mirrored corners track * test(studio): bank only the two newly passing scaled resize cases * fix(studio): a mirrored or parent-rotated layer's resize corner tracks the pointer * fix(studio): a resize corner on a skewed layer picks its side from the box's own edges * fix(studio): resizing a layer that renders with no area moves nothing |
||
|
|
a1803448b8 |
fix(studio): clip strips resize on the next frame, ending ResizeObserver loop errors (#5187)
* fix(studio): clip strips resize on the next frame, ending ResizeObserver loop errors * test(studio): verify strip resize frame scheduling deterministically * fix(studio): a menu set to render inline opens beside its trigger * fix(studio): a popover set to render inline opens beside its trigger * fix(studio): a remounted clip strip keeps its own size instead of a queued one * test(studio): zoom thumbnail tests run resize frames on fake timers * test(studio): zoom preview test runs the resize frame it guards |
||
|
|
7a84d29df1 |
fix(studio): let small picks move when dragged by their middle (#5206)
* fix(studio): let small picks move when dragged by their middle * fix(studio): keep the grabbed corner under the cursor while resizing * fix(studio): let picks a few pixels across resize from their corner * docs(studio): say which picks the resize guard still lets resize |
||
|
|
621ddd4693 |
fix(studio): stop file names with %, apostrophes or $ from breaking Studio (#5156)
* fix(studio): serve files whose name has a bare percent sign A browser sends src="100%.png" with the % unescaped, and decoding the whole path threw, so Studio answered 500. Only well-formed escapes are decoded now. * fix(cli): serve files with a bare percent sign from the check and snapshot server One shared decode rule for request paths; the image thumbnail route's malformed-escape 400 could no longer fire, so a name that is not a file is a 404. * fix(cli): name a failed file with a bare percent sign in check * fix(studio): keep apostrophes and dollar signs in file names when editing attributes An attribute value is matched by its own quote kind, so src="it's.png" is replaced instead of duplicated, and every insert uses a function replacer, so $1 or $& in a file name is written as is. * fix(studio): rename references spelled with %20 or & and read quoted media sources WIP: not yet tested; see the resume note. * fix(studio): match whole attribute names when editing media sources * fix(studio): compare decoded names and escape replacement references * fix(studio): preserve file references across URL and string consumers * docs: clarify raw file references and URL fields * fix(studio): keep raw path identity distinct from URL spelling * fix: share attribute grammar and decode media URLs at their readers * fix: preserve local filename identity in HTML URL output * fix: decode entity references before rename candidate matching * style: format rename entity handling and regressions * fix: preserve platform file identity when renaming references * fix(core): decode CSS escapes before rebasing asset URLs * fix(core): consume CSS continuations before URL encoding * fix(core): preserve CSS URL identity and escape rebased suffixes * fix(core): express CSS suffix controls with Unicode categories * fix(core): preserve URL backslash identity while rebasing * test(core): respect platform backslash filename semantics * fix(studio): a dropped file with %, # or ? in its name loads, and an encoded reference renames * fix(studio): read an authored src as a URL so dropped files with spaces, %, # or ? load * fix(studio): media routes find files named with ? or #, and the probe reads a src as a URL * fix(studio-server): one rule for request file paths, so loudness finds files named with %, # or ? * chore: drop the design note from the change * fix(core): rename replaced clips whose src is URL-encoded |
||
|
|
042ec2ebd0 |
fix(studio): restore overscan and publish the zoom viewport (#5151)
* fix(studio): restore the timeline overscan budget * test(studio): align window witnesses with the restored budget * fix(studio): publish the viewport when a zoom anchor lands * fix(studio): publish the viewport when a zoom-out ends at its laid-out scale An eased zoom-out that ends at the scale already laid out only moves the scroll position, so the timeline drew from the old position until the next scroll event. The zoom input now publishes that scroll through the same callback the zoom anchor uses. * fix(studio): a pan at the laid-out scale publishes its scroll before paint The same-scale ending of a zoom ran outside React, so its scroll publish rendered on the next task, after the browser painted the old window. It now flushes like the scale change beside it, and its test pans at the laid-out scale instead of changing it. * test(studio): a same-scale pan must render its scroll before paint Reads the render window outside act, so unwrapping the flush fails it, and records each published scroll at call time instead of reading the live element afterwards. |
||
|
|
b66e8a195e |
perf(studio): preview decodes hard-to-play videos at the size they are shown (#5184)
* perf(studio): preview decodes hard-to-play videos at the size they are shown Studio's copy of an HEVC or ProRes clip was made at the source's full size, so a 4K clip was decoded at 4K inside a preview a few hundred pixels wide, and back-to-back 4K clips flashed the background at their cuts. The player now reports how large it shows the composition, the runtime asks for a copy sized to the shown video (on a ladder of sizes, never above the source), and Studio stops pre-warming source-size copies. CLI play, static serving, publish and renders keep the source-size copy. * fix(studio): sized preview copies follow zoom, hover previews and the newest size Studio reports the preview's on-screen scale when a zoom settles and for composition hover previews, which no player measures. The runtime loads only the newest size asked for, regrows a first copy made while the frame grew, and never replaces a copy with one smaller on either side. The player's scale is measured against the frame's own layout width. * fix(studio): keep comment shares and file sizes within the repo ratchets * test(studio-server): end the sizing test's ffprobe options before its input * fix(studio): hover previews report their size once their runtime is ready |
||
|
|
1e711b087d |
feat(studio): a host can show the hover box and give the stage its starting size (#5116)
* test(studio): pin a host's hover box and a starting stage size * feat(studio): a host can show the hover box and give the stage its starting size * fix(studio): keep the comment share flat and let a measured size win over the hint |
||
|
|
17c699db4a |
fix(studio): replacing an image's fill on an img changes its picture (#5170)
* fix(studio): replace image fill through its source attribute * fix(studio): preserve literal percent image sources |
||
|
|
dbb8222c8d |
fix(studio): catch up the preview after missed file changes (#5126)
* fix(studio): recover previews after missed file changes * fix(studio): keep queued reloads with the current owner * fix(studio): keep a queued outside change when switching compositions * test(cli): the project watcher closes its parent folder's watch too * fix(studio): keep the project-folder change out of the shared signature check * fix(studio): keep the edited file's draft and reload every session after a reconnect A reconnect or replaced folder is a project-wide change: every open SDK session reloads, a failed save is recorded against the file the draft belongs to, and two queued changes to different files merge into one project-wide reload. * fix(studio): refresh the preview cache when the .hyperframes folder is replaced * fix(cli): report the files of a folder moved into the project Replacing a populated folder such as .hyperframes/ now reaches the signature cache through its files, so the shared signature rule stays as on main and Studio's own writes inside .hyperframes/ still invalidate nothing. * refactor(studio): name the merged file-change payload and share the test sender * fix(studio): keep a failed draft's recovery copy until that file itself saves * style(studio): rewrap the merged payload line after the rename * fix(studio): deliver a held change's full reload when its block clears A clean save, Use external and Keep Studio now all deliver the merged scope a blocked change owes, and a moved-in .hyperframes folder reports its manifests so a dropped one still clears the cached signature. * fix(studio): keep restored drafts out of the reload scope and clear the lint gates * fix(studio): retain held reload scope before React renders |
||
|
|
eee0c5101c |
perf(studio): smooth timeline zoom on long projects (#5109)
* 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. |
||
|
|
dc244d765c |
fix(studio): drags after a resize, undo or reload land where they were pressed (#5088)
* 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 |
||
|
|
8b5b012e5c | fix(studio): dragging a Layers row no longer highlights the row names (#5104) | ||
|
|
4905b4dbca |
fix(studio): make clip peak warnings plain and hoverable (#5099)
* fix(studio): make clip peak warnings plain and hoverable * fix(studio): expose peak detail on clip focus above waveforms * fix(studio): preserve shared badge styling and track test identity |
||
|
|
0d5e395cd3 |
fix(studio): keyframed animations keep their ease through every Studio edit (#5051)
* 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 |
||
|
|
98fe011699 |
fix(studio): undo takes back the last edit, even right after a resize or a nudge (#5048)
* 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 |
||
|
|
f7d6734761 |
fix(studio): Layers rows show a kind icon, and only a group row has a caret (#5067)
* 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 |
||
|
|
5c7f11ab6d |
fix(studio): plain elements keep their place and size when keyframed (#5045)
* test(studio): bench a plain element animated through auto-record and Add keyframe * fix(studio): seed a first tween from the offset a plain move left in CSS translate * test(studio): bench resize then drag on a GSAP tween, root and nested * fix(studio): a resize under auto-record keys size at the playhead, held at all times * fix(studio): keyframe tweens Studio writes end on their last key, so a seek matches playback * test(studio): keep the plain-to-keyframes moves inside the nested frame * fix(studio): a resize moves its anchor from the drag stamp it read at release * fix(studio): an undo shown in place is no longer replaced by an older reload * refactor: split the trailing keyframe trim and drop an unused GSAP reader * test(studio): settle the resize-then-drag bench steps so a two-save resize is one undo * fix(studio): a preview reload waits for an undo shown before its write lands * fix(studio): Add keyframe reads a subtracted calc() translate with its sign * refactor: tidy the keyframe hold, trim and resize code after review * fix(parsers): the keyframe trim leaves looping, chained and relatively placed tweens alone * fix(parsers): a size hold pins only a lone key, so an edit never changes frames left alone * fix(studio): every undo applied in place counts as a preview change, script-only ones too * fix(studio): a resize keys size only when a move or size is keyframed, not a fade * test(studio): a bench step whose save never lands fails the case * fix(parsers): holds and trims change only the tweens an edit touches * test(studio): bank the plain-to-keyframes and resize-then-drag bench cases |
||
|
|
618fd7684b |
feat(studio): a host app can mount the Layers panel outside Studio (#5049)
* feat(studio): a host app can mount the Layers panel outside Studio LayersPanel is exported from @hyperframes/studio with an optional host prop carrying the preview iframe, active composition, toast, timeline elements and playing state. Studio's own mount still reads its contexts. * fix(studio): a layer reorder no longer moves timeline rows a host cannot save Without a TimelineEditProvider the z-order lane mirror changed the clip's row in the store only, unsaved. It now skips the mirror there. LayersPanelHost is built from Studio's context types. * chore(studio): keep the lane mirror's comment share flat |
||
|
|
db12b3e022 |
fix(studio): place GSAP step lists and duration-less tweens where GSAP plays them (#5024)
* fix(studio): place GSAP step lists and duration-less tweens where GSAP plays them * refactor(studio): drop narrated comments and unnest the extend clamp * fix(studio): match keyframes in time, resolve partial step timing, start readout from prior tween * fix(studio): time step-list keyframes from the parts' last end and format * refactor(studio): trim comments to the package's comment share * fix(parsers): apply timeline defaults as GSAP does to step lists and motion paths * fix(studio): keep GSAP's ease for motion paths with keyframes and read finished tweens * refactor(studio): read a live motion path in its own step * refactor(parsers): leave step-list timing to its one owner and skip timeline defaults there * refactor(studio): move the live motion-path read into its own module * test(parsers): pin an unresolved length for a step list stretched by a variable duration * fix(studio): time keyframes linearly under an outer ease that overshoots or steps * fix(studio): time a converted expression-duration tween from its live tween, and pin no rewrite |
||
|
|
cd82ce044d |
perf(studio): drag and resize any number of selected clips, saved in one write per file (#4937)
* feat(studio): hand-move and hand-resize any number of selected clips * test(studio): bench dragging and resizing every clip, frame cost and drop time * test(studio): give the bench browser launch the run timeout * test(studio): bench request timeline offsets * fix(studio): joining or leaving a multi-clip drag restyles a clip instead of remounting it * fix(studio): restore the linked-selection import and mark the drag bench complex * test(studio): find a clip's track row through the per-clip wrapper * test(studio): resolve track rows through the per-clip wrapper in the remaining cases * perf(studio): write a group edit's GSAP changes as one request per file * test(studio): cover the batched GSAP write and keep the owned-chain cases * test(studio): bench profile of the drag's pointer moves * perf(studio): sync a group edit's root duration once per file, not once per clip * feat(studio): a timeline move registers a still-saving revert so Cmd+Z right after it need not wait * test(studio): the reverted move stays reverted when its save lands * test(studio): bench prints the heaviest call chain on request * perf(studio): preflight a many-clip selection against one index of the live tweens * test(studio): share one timeline across the indexed lookups * test(studio): bench failure output skips resource 404 noise * fix(studio): the live-tween index skips timeline entries that are not objects * fix(studio): the live-tween index skips a null timeline entry * test(studio): bench reports the first drag frame apart from the steady ones * test(studio): bench lists inclusive time per function on request * test(studio): bench reports when the last write of an action landed * test(studio): bench reports medians of save, frame and first-frame times * refactor(studio): move the live-tween index out of gsapRuntimeKeyframes to keep it under the line cap * fix(studio): wire the live-tween index imports and drop an unused test binding * style(studio): format the files CI flagged * fix(studio): keep the bench and the scale helper out of fallow's findings * refactor(studio): one exact tween-index lookup, finish a batch file once, reuse the duration sync * fix(studio): the tween index lists a tween once per key * fix(studio): restore the batch-finish and named-parameter helpers * fix(studio): undo reverts an edit still saving before painting an older predicted step * style(studio): format the batch duration sync * chore(studio): keep the new comments within the block and share limits * test(studio): settle the pending edit even when the undo-order test fails * perf(studio-server): retime every clip in a GSAP batch with one parse and one hold sync * test(studio-server): a batch of clip retimes writes what single requests in turn write * style: format the retime fold and its tests * perf(parsers): clip matchers in one retime share a DOM query cache * perf(parsers): look up every clip id in one DOM walk when retiming * perf(studio): re-resolving the same selected node keeps its source check * perf(studio): a drop re-renders only the selection that actually changed * test(studio): report main-thread task time of a bulk drop * perf(studio): a group drop updates the timeline store once, not once per clip * fix(studio): re-reading index.html keeps sub-composition keyframes * refactor(studio): one owner for clip-retime no-op rules and selection options * fix(studio): reuse a source check only for the same target, and re-check on a new cache version * fix(studio): undo confirms a reverted save only once its history claim counted * refactor: split a retime's application out of the one-parse fold; share test fixtures * perf(studio): drop the per-clip wrapper node so timeline scrolling stays within budget * refactor(studio): name the drag passenger style after the passenger it styles * fix(studio): undo keeps a move undone when its history claim landed before the key * fix(studio): undo a timeline move and its tween shift in one step * refactor(studio): one owner of a timeline gesture's undo window * refactor(studio): move single-clip move and resize into their own hook * fix(studio): a reload or second tab never joins another page's undo step * fix(studio): a delete and its ripple stay one undo step however late the ripple lands * fix(studio-server): date a held undo step by its last write, not by the edit that ends it |
||
|
|
cd0d04e4e9 | fix(studio): dragging a keyframe's path node keeps the other keyframes where they were (#4946) | ||
|
|
1bdb5edd93 |
perf(studio): long clips mount only the picture tiles in view (#4993)
* perf(studio): long clips mount only the picture tiles in view A clip's picture strip mounted one tile per frame width, so a 10-minute clip at full zoom mounted 18,000 tiles. The strip-size hook now also reports the strip's on-screen span (refreshed on scroll, resize and clip moves), and the video, image and composition strips mount only the tiles inside it. * refactor(studio): read the strip span outside the state updater * fix(studio): a dragged long clip keeps its picture across the screen The strip refreshed its span only on scroll, resize or a style change on its own clip, but a drag moves the ghost through a wrapper, so a long clip's ghost went blank past about 600 px of drag. Each strip now marks its unmounted ends and re-measures when either nears the screen, whatever moved it, and commits before paint. The tile row is one shared ThumbnailTiles for the video, image and composition strips. * fix(studio): warn of a tile gap inside the timeline scroller too * fix(studio): clip strips re-measure together, one layout per frame With row virtualization off the timeline mounts every clip, and each strip had its own scroll listener and frame that read layout and then committed. A scroll jump changed every strip's span, so a 1,000-clip timeline laid out about once per clip per frame (the viewport gate's 1,000-row arm went to ~3 s frames). All strips now share one scroll listener, observers and frame: every position is read first, then every update commits in one batch. * perf(studio): a scroll re-renders only the clip strips crossing the screen edge A strip's span is clamped to its own measured width, and each strip keeps its current size outside React so an unchanged size never calls setState. A clip wholly on or wholly off screen then does no work on a scroll; only strips straddling the edge re-render. * perf(studio): a scroll re-measures only clip strips near the screen The shared frame read every mounted strip's position, so a timeline of 1,000 unvirtualized clips forced about 1,000 style-and-layout passes per scroll frame. An IntersectionObserver now tracks which strips are within 256 px of the screen; a scroll re-measures only those, and a strip is measured as it comes near. A clip wholly off screen mounts no tile. * test(studio): share the strip test setup through the root harness * test(studio): a strip is measured as it comes near the screen * fix(studio): a scroll jump measures the clips it brings on screen in the same frame Each frame moves every strip's last measured box by its scroller's offset change and reads only the boxes that land within 256 px of the screen. A jump therefore measures exactly the strips it brings into view, with no blank frame, at the cost of a short scroll; strips far away get their span from the moved box without a layout read. * perf(studio): a strip off either side of the screen keeps one empty span * perf(studio): a clip strip far above or below the screen mounts no tiles * perf(studio): a scroll frame leaves off-screen clip strips untouched * perf(studio): short clips keep every tile and do no work on a scroll A strip up to 4,096 px wide keeps all its tiles, as before this change, and a scroll frame skips it; only long clips mount the tiles in view. Short clips gain nothing from virtualizing and paid a re-render each time they crossed the screen's edge. * fix(studio): a scroll is never applied twice to a clip strip's position Each strip remembers the scroll offset its box was read at, so a strip read between a scroll and that scroll's frame is not shifted again. The empty-span guards are removed: a near strip always starts inside itself and a far one already gets nothing. * fix(studio): a clip on screen keeps its pictures after a move and a scroll A strip showing tiles is re-read on every scroll frame instead of trusting its last box shifted by the scroll, which a move without a scroll (rows removed above it, a dock resize) leaves stale. Tests now pin the vertical shift, the 256 px band, the 4,096 px short-clip limit and the tile padding. |
||
|
|
b45818f69d |
fix(studio): the motion path draws on every load, not only when the layer's node is the frame's own (#4957)
* fix(studio): the motion path draws on every load, not only when the layer's node is the frame's own useMotionPathData found the selected layer with `instanceof` against the preview frame's HTMLElement. In the Studio preview the body's nodes often carry the editor window's prototypes (see core's runtime/domRealm.ts), so the check failed, the path never got its anchor and drew nothing. Core's realm-free isHtmlElement owns that question; both motion-path call sites now use it. * fix(studio): every preview node check asks what a node is, not which window built it Studio checked preview nodes against their document window's own constructors in seek reapply, studio motion, timeline z-order commits, the manual offset drag and the z-order menu. On loads where body nodes carry the editor window's prototypes those checks fail silently, as the motion path did. They now use core's realm-free predicates, and a source test fails on a new window-bound check. * fix(studio): the webmcp preview helper asks what a node is, and both realm tests stub the frame * fix(studio): the last preview node checks ask what a node is, and the guard sees any constructor read The selector walk read ancestor ids only from editor-realm nodes and the audio group observer never saw a group the preview added. Both use core's predicates now. The guard flags any DOM interface read off an object, the spellings it missed included, and the window gates the old constructor needed are gone. * test(studio): cover the main seek filter across windows and split member reads * fix(studio): a press on the motion path's line inside the selected layer moves the layer * fix(studio): a press inside the selected layer moves it, and path nodes sit where the layer is * refactor(studio): one helper places a path point, for keyframes and the start * fix(studio): a handle over a path node takes its press; nodes sit right on centred layers * fix(studio): reading a layer's path anchor leaves its CSS translate alone * chore(studio): keep the motion path home file under the comment share * fix(studio): a new motion path ends where you click on a CSS-centred layer * fix(studio): a path made on a transform-centred layer ends where you click * style(studio): keep a motion path test comment within 120 columns |
||
|
|
49333a882d |
fix(studio): undo shows a resize, rotate or GSAP edit undone at once while it saves (#5022)
* fix(studio): undo shows a resize, rotate or GSAP edit undone at once while it saves * test(studio): pin the painted-back draw and redraw rules * test(studio): the resize intercept also gets the draw that keeps an undone edit undone * fix(studio): a scale resize reads its drop point on the box as dropped, even after undo * fix(studio): a group move and a resize settle keep an undone edit undone * fix(studio): undo paints back a GSAP move saved as a left/top offset * fix(studio): undo still steps a GSAP edit whose first write saved before a later one failed * fix(studio): the resize pixel check measures a box undo painted back as the edit drew it * fix(studio): a GSAP move undone mid-save keeps its drag base, so its save adds the drag once * refactor(studio): trim the scale-resize finalize comments to what the code cannot say |
||
|
|
2b82e39cd6 |
feat(studio): make edit with framey stand out in the header (#5019)
Wenbo's feedback: the button read like the ghost controls beside it. It now wears an accent tint and border, a larger Framey and a permanent New badge, and pulses three times as Studio opens (none under reduced motion). |
||
|
|
6a27a34f21 |
feat(cli,studio): hand off on macOS now, and open the app on Windows and Linux (#4999)
* feat(studio,cli): add an Edit with Framey button and `hyperframes open` for HyperFrames Studio
Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app.
- Studio header: an "Edit with Framey" button with Framey, the app's cursor mascot (the eye
follows the pointer, a wiggle on hover, honours reduced motion). Until the app opens
handed-over projects (HANDOFF_READY), a press shows a "Meet Framey" card with the download.
The button shows only where the preview server answers GET /api/open-in-desktop on macOS,
so the desktop app's own embedded Studio never shows it.
- `hyperframes open [dir]`: hands the project folder to the app with `open -b` (released app,
then Canary) and points to the download when neither is installed. When run by Claude Code
or Codex it leaves .hyperframes/agent-handoff.json (CLAUDE_CODE_SESSION_ID / CODEX_THREAD_ID)
so the app's first chat can pick up that conversation.
- render and preview print one line about the app: the download until HANDOFF_READY, then
`hyperframes open` when the app is installed. Nothing for a draft render, a batch row, or a
run inside the app (HYPERFRAMES_DESKTOP_PROJECT).
* fix(studio,cli): gate `hyperframes open` too, guard the open-in-desktop POST, keep one gate
Review of #4951 at
|
||
|
|
bd0dc8c3ae |
feat(studio,cli): add an Edit with Framey button and hyperframes open for HyperFrames Studio (#4951)
* feat(studio,cli): add an Edit with Framey button and `hyperframes open` for HyperFrames Studio
Point people from the CLI and the Studio preview to HyperFrames Studio, the desktop app.
- Studio header: an "Edit with Framey" button with Framey, the app's cursor mascot (the eye
follows the pointer, a wiggle on hover, honours reduced motion). Until the app opens
handed-over projects (HANDOFF_READY), a press shows a "Meet Framey" card with the download.
The button shows only where the preview server answers GET /api/open-in-desktop on macOS,
so the desktop app's own embedded Studio never shows it.
- `hyperframes open [dir]`: hands the project folder to the app with `open -b` (released app,
then Canary) and points to the download when neither is installed. When run by Claude Code
or Codex it leaves .hyperframes/agent-handoff.json (CLAUDE_CODE_SESSION_ID / CODEX_THREAD_ID)
so the app's first chat can pick up that conversation.
- render and preview print one line about the app: the download until HANDOFF_READY, then
`hyperframes open` when the app is installed. Nothing for a draft render, a batch row, or a
run inside the app (HYPERFRAMES_DESKTOP_PROJECT).
* fix(studio,cli): gate `hyperframes open` too, guard the open-in-desktop POST, keep one gate
Review of #4951 at
|
||
|
|
7faf2cc6c3 |
fix(studio): dragging a keyframed layer from its middle moves the layer, not a path node (#4941)
* fix(studio): a move on a keyframe of an eased tween changes that keyframe in place GSAP eases a keyframe run as a whole with keyframes.ease || ease. The playhead writer now edits an existing keyframe under that ease and writes the ease back as the tween's own; adding a keyframe in eased form stays refused, since the ease moves where its time lands. * fix(studio): dragging a keyframed layer from its middle moves the layer, not a path node The motion path drew an axis the tween does not animate at 0, though GSAP folds the layer's CSS translate into it, so each node sat beside the layer and its grab ring took presses aimed at the layer. The path now uses GSAP's live position, the same base the layer drag reads. A press on the node GSAP renders the layer at, or inside the layer's box but off another node's drawn dot, starts the layer's own move and write. * test(studio): stub elementsFromPoint, which happy-dom does not have * refactor(studio): name the press check for what it answers, and format * chore(studio): cut the helper comments to the package comment share * fix(studio): a node press goes to the layer only when the layer takes it The hand-off to the layer's box falls back to the node drag when the box starts no gesture or takes no pointer, and a selected node's larger dot stays the node's. * chore(studio): name the keyframe run ease instead of commenting it * refactor(studio): share the keyframe-at-percentage check in the playhead writer * fix(parsers): a keyframed tween keeps GSAP's ease, never a default or a quoted variable GSAP never gives a tween with keyframes its timeline's default ease, so the parser no longer does. A keyframes-level ease or easeEach the file names by variable is kept as that variable, and every ease the writer emits goes through valueToCode, so ease: E is written back as E instead of the string "__raw:E". * fix(studio): edit an eased keyframe run only at its ends, and a refused press is not a node drag GSAP renders a run eased as a whole at ease(t), so only its 0% and 100% keyframes sit at their clock time; the writer refuses the others as before. A layer press the box answers with its blocked notice counts as handled, so the node does not drag after it. * fix(studio): a layer press refused at setup leaves the box live, and variable eases stay pinned startGesture paused the box's position updates and playback before the drag member setup could refuse; a press handed off from a path node then left the box frozen, since its release lands outside the box. It now pauses only once the setup succeeds. Tests pin that adding, converting and unrolling write an ease the file names by variable as that variable. * fix(studio): pause playback before drag setup and pin variable eases on convert and unroll * fix(studio): pause playback before a nudge snapshots timelines, and pin the order * test(studio): a failed nudge pause test cleans up, and a nudge with nothing to move leaves playback alone |
||
|
|
b04081b376 |
fix(studio): the host can name Split's key on the toolbar and bind the clip menu's link keys (#4994)
* fix(studio): the host can name Split's key on the toolbar and bind the clip menu's link keys * refactor(studio): early return for typed keys; export test checks identity |
||
|
|
dda0e757b1 |
fix(studio): the host can name the Split key shown in the clip menu (#4985)
* fix(studio): let the host name Split's key in the clip menu * fix(studio): editor shell and timeline pane forward the Split key label * fix(studio): drop two added doc comments the comment ratchet refuses |
||
|
|
98763899d6 |
fix(studio): an audio track can be muted again after it is unmuted (#4977)
* fix(studio): an audio track can be muted again after it is unmuted The audio track header showed its toggle only while the track was muted, so a track unmuted once could never be muted again. The toggle now shows on every audio-only track as a speaker (Mute track N / Unmute track N), decided by the same rule the undo entry already used. * refactor(studio): one owner for the track toggle words, drop comments the mute fix made false * fix(studio): an audio clip or group can be muted and unmuted from the property panel The panel offered its hide toggle on audio only while already hidden, and a selected <hf-audio-group> had no toggle at all (it has no timeline row). The panel now offers Mute/Unmute both ways; a group writes data-hidden through the group attribute writer. Track rows, the panel and their undo entries share one verb and icon helper. * style(studio): format the panel test and keep the comment share flat * fix(studio): the panel's mute label follows the write, without a reselect * fix(studio): after a panel mute, refresh what is selected now, not the muted item * perf(studio): read the selection ref from the actions context so the inspector does not re-render on hover * refactor(studio): move the panel's mute toggle into usePanelHiddenToggle |
||
|
|
239ea1aecd |
feat(studio): a host can put its own controls before the timeline's thumbnails toggle (#4973)
* feat(studio): a host can put its own controls before the timeline's thumbnails toggle * fix(studio): keep the toolbar under the comment ratchet |
||
|
|
9106772593 |
fix(studio): a rotation saves through its press route; a host hook reloads only the host's way (#4961)
* fix(studio): a rotation saves through the route its press chose * fix(studio): a host hook reloads its preview only the way the host says |
||
|
|
e27cae0261 | fix(studio): a resize saves through the route its press chose, as a move does (#4943) | ||
|
|
7dea1e581f |
fix(studio): playhead keyframe edits keep their ease, undo names its edit and never waits on videos (#4889)
* fix(studio): an edit on a GSAP-animated layer writes its value at the playhead into the file's tween
* fix(studio): undo pressed while an edit saves undoes that edit, not one started after the key
* fix(studio): an edit stays saved when the history reply after it cannot be read
* feat(studio): export ColorField and GradientField; a session style commit takes a map
* refactor(studio): name the map form instead of commenting it
* feat(studio): a host can move layers by exact film px, one undo per call
handleDomGroupMoveBy(moves) on useDomEditSession and DomEditValue moves each
selection by its delta in composition px, for Desktop's align and distribute.
It reuses the group drag's own path: a drag member per element (press-time
route, measured offset map), the delta carried to screen px through the
preview frames, then handleGsapAwareGroupPathOffsetCommit, so one shared
coalesce key gives one undo entry and GSAP members take the GSAP write.
A member that can't move refuses the call before anything is written; the
refusal rejects with its reason and shows no Studio toast (the group commit
takes refusalToast: false; telemetry stays). Zero deltas are skipped.
Tests: a pre-offset element and one under a half-scale parent move by exactly
{10,0} and {0,-5} under one group key; a half-scale parent with a 30 deg
rotated ancestor lands within the 0.001 px a translate is written with; a
GSAP member goes through the GSAP write; three refusal kinds write nothing,
leave the boxes where they were, show no toast and leave no gesture mark,
while a canvas group drag still toasts; the session hands the call to the
group commit with refusalToast: false. Each mutation (no frame mapping, no
zero skip, no restore, no teardown, no capability check, toast on refusal)
fails at least one of them.
* fix(studio): an outside change during a drag waits for the drop to show
While a gesture mark is live in the preview, refreshPlayer takes the shadow load instead of
swapping scenes in place. A swap replaced the node under the pointer mid-drag; the shadow load
is then held by the existing gesture hold and, if the drop saved, reloaded fresh by the stale
rule, so this adds no second "released" signal.
Test: an outside change during a live drag on a preview that can swap scenes leaves the dragged
node in place, shows nothing under the pointer, and after the drop loads fresh with the dropped
position. It fails on the parent without the condition ("swapped under the pointer").
Cost case, measured in a real headless preview on the edit-accuracy bench's per-frame teleport
check. Every GSAP instant patch was forced to miss and fall back to reloadPreview during its own
commit; move and rotate of a held element (gsap.set and tl.set holds), px and xPercent
placement, r0 and r30, root and nested, at 100%:
- The gesture mark was live when the fallback asked for the reload, but had always ended by the
time refreshPlayer ran (refreshKey is consumed in an effect after the commit settles). So the
new condition never fired there: root previews offer no scene swap and load shadow on both
builds, and nested previews swap on both builds.
- Worst post-release frame offset, 32 cases on this build and 32 on the parent: 0.00 px over
76-106 frames each (release to settle). No frame was off by more than 0.5 px.
- Sensitivity: a 3 px nudge injected 50 ms after the fallback reads back as 3.00 px.
* fix(studio): an edit no longer reopens the live-updates stream, and thumbnails ask one at a time
* fix(studio): an undo no longer waits behind paused preview videos for a connection
* feat(studio): a host whose API needs same-origin cookies can have Studio send them
* docs(studio-server): a host on same-origin cookies sets the Studio credentials opt-out
* test(studio): an undo is not held up by the preview's paused videos
* fix(studio): a resize inside one gesture writes a mixed tween once, in place, never beside it
* chore(studio): keep the carried fixes inside the comment ratchet; telemetry posts through studioApiFetch
* test(studio): a drag on a mixed tween is one write under the gesture key
* fix(studio): freeze-frame, loudness and peak-map requests go through studioApiFetch too
* fix(studio): an undo with no project open reloads in full instead of skipping nested files
* refactor(studio): drop the soft reload's reparse seeding, the held-element sweep covers it
* test(studio): keep the undo-restore browser test off the network and inside its timeout
* test(studio): nested composition reads expect the cookie-free request studioApiFetch sends
* fix(studio): an edit at the playhead rewrites its tween in place, so later tweens keep their times
* fix(studio): a rotate on a tween that turns another axis holds its angle and never turns early
* fix(studio): plan every group member before the first write so one refusal moves none
A group drag or nudge preflighted each member against the move refusal rule only. The value-at-playhead planner ran later, in the commit loop, after earlier members were already flushed to disk, so a tween with extras, eased keyframes or an unknown ease left the group half moved.
The group preflight now runs the commit's own route and keyframe plan for every member with its real offset, so any refusal refuses the whole group with nothing written and one toast attributed to the refusing member. The plan is not reused in the commit: earlier members' writes change the file under it, so each member re-plans against the fresh tween, from the same planValueEdit.
* chore(studio): the fetch lint rule also catches window.fetch, globalThis.fetch and self.fetch
* docs(studio-server): set the credentials flag before Studio loads; a mounting host injects it
* test(studio): the undo socket test needs six pinned videos under Vite and checks each undo restores
* fix(studio): Studio leaves cookies out of its requests only when served from loopback
* fix(studio): undo takes back the first edit, and the first save never waits on media
* fix(studio): undo of a move whose save failed stays undone and steps back no older edit
* fix(studio): a style set by its JavaScript name saves under its CSS name
* test(studio): the undo socket test holds round 0 to a full restore too
* fix(studio): the host revert keeps returning what shows the edit again
* fix(studio): an entrance rewritten at the playhead still shows its start state before it starts
* fix(studio): undo takes back the edit pending at the key even after a later edit claims
* chore(studio): lift the late-write quarantine now that undo never waits on videos; format
* test(studio): pin an entrance's start render on the written tween, not a happy-dom GSAP run
* revert(studio): leave entrances as on main, immediateRender does not render a keyframed start
This reverts commit
|
||
|
|
ce08f2042e |
fix(studio): a hovered menu row fills its row, and a host can hide the clip menu tools (#4921)
* fix(studio): a hovered menu row fills its row, with no band around it * refactor(studio): one shared menu style owns every context menu's panel, rows and dividers * fix(studio): menus clip their rows to the panel's corners and cover every hand-built panel * fix(studio): asset menu Delete shows red, trim menu owner comments, test submenu placement * docs(studio): shorten a submenu comment * feat(studio): a host can hide the clip menu tools without losing the edits fades need * test(studio): the clip menu tools fixture is splittable * test(studio): the default clip menu shows Voice, the control for hiding it * docs(studio): shorten the menu owner comment * docs(studio): drop comment lines the ratchet counts * docs(studio): the menu owner carries one comment * style(studio): format the files the repo formatter listed * test(studio): the menu style scan names files with forward slashes on Windows too |
||
|
|
70dde41b5c |
perf(studio): keep timeline bulk actions under a second, hand-move at most 3 clips (#4932)
* perf(studio): keep timeline bulk actions under a second, hand-move at most 3 clips Dragging or keyboard-moving a selection of 4 or more clips is refused with a toast pointing at the prompt box; 1 to 3 clips move as before. Root causes of the multi-second waits on large projects: - every selected clip fired its own source-existence probe; they now leave as one request per file - a timeline delete sent one request per clip, each re-parsing the file; it is now one request that parses once - each click on a clip from another file re-fetched GSAP data for every composition; covered files are kept and unchanged files answer 304 - any root index.html write re-captured every composition thumbnail; only a root head edit does now, since a scene page borrows nothing else from it Adds timeline-bulk-actions.mjs, a browser benchmark of select-all, move 3 and delete-all on a real project. * chore(studio): trim the bulk benchmark header to the comment limit * refactor(studio): tidy the bulk-action fixes after review Root head memo is keyed by the root file's content so every subscriber of one write gets one answer, and a thumbnail request can no longer overwrite it. The GSAP read reuses the shared content version as its ETag. * fix(studio): keep a head edit's thumbnail refresh when a later write replaces it File changes coalesce to the newest while a drain is in flight, so the affected-composition lists are merged instead of replaced. A failed GSAP read stays unloaded and is retried, and the GSAP read's ETag is salted with the parser version. * feat(studio): cap hand-resizing a multi-selection at 3 clips like moving Dragging a resize edge with 4 or more clips selected is refused with the same toast as a move; the rule is one constant and one helper shared by pointer move, pointer resize and keyboard pickup. * fix(studio): keep a head edit's thumbnail refresh through a replaced conflict A newer blocked change replaces the held one, so it now inherits the thumbnails the held one owed. * fix(studio): keep a held head edit's thumbnail refresh when a later write saves cleanly The clean-save path and the failed-cleanup path now merge the held blocked change's affected compositions, and the blocked-state merge runs inside the state update so two conflicts in one tick cannot lose the first. * chore(studio): keep useTimelineClipDrag under the 600-line cap * feat(studio): cap by the clips an edit would change, and cut the added comments The 3-clip rule counts the clips a move or trim would write to: a locked clip swept into a move stays put, and a trim some member cannot take edits only the grabbed clip. Comment share is back at or under the base in every changed file. * fix(studio): clear fallow audit and comment length on the multi-select cap * fix(studio): keep bench script comments under the block limit * fix(studio): keep the failed file's payload when another file's snapshot delete fails |
||
|
|
ada4e0d289 |
fix(studio): show each timeline row its own layer instead of a flat bar (#4917)
* fix(studio): show each timeline row its own layer instead of a grey bar A row got a frame only if it spanned under 92% of the film and its id did not look like a backdrop; every other row drew a plain grey bar. Every element row now gets its frame, and the capture fades the row element's siblings first, so the frame shows that layer and not the whole film. Studio's dev server and the CLI share one capture helper; Studio's own copy is gone. * fix(studio): read timeline rows from any realm so each keeps its selector * refactor(studio-server): always isolate the element in a thumbnail clip * fix(cli): undo a thumbnail's isolation before its page takes the next capture * chore(core): generate the dom-realm export and trim thumbnail clip comments * fix(studio-server): restore sibling opacity after a clip, hash selectors in cache keys * fix(studio-server): keep faded siblings in a page-owned list so restore is exact * feat(studio): draw a text layer row as its own live text instead of a captured picture * refactor(studio): move timeline text reading into its own module * fix(studio): keep text rows readable and current, and leave painted layers as pictures * fix(studio): refresh a canvas-edited text row by its own identity and source file * fix(studio): refresh a canvas-edited text row known only by its data-hf-id |
||
|
|
852782bbb0 |
feat(studio): track completed preview editing gestures (#4916)
* feat(studio): track completed preview editing gestures * fix(studio): preserve gesture receipts and suppress duplicate usage * fix(studio): count semantic keyframe writes once per gesture * fix(studio): isolate recording state during pending saves * fix(studio): count marquee selection changes from owner receipts * fix(studio): retain accurate empty marquee receipts * refactor(studio): simplify recording transitions and selection tests * refactor(studio): share recording timing and marquee fixtures * fix(studio): preserve recording targets and batch attribution * fix(studio): retain per-member telemetry suppression * style(studio): format batch receipt attribution * test(studio): match writer receipts and animation metadata * docs(studio): clarify paused position save rejection * fix(studio): replace telemetry route names with route identity * style(studio): format route identity regression * Use random tab-local route identities in Studio telemetry * Preserve route identities inherited through session storage * Separate route storage validation from token creation |
||
|
|
5c52f72399 |
fix(studio): parse modern CSS colors without a new dependency (#3931)
* fix(studio): parse modern CSS colors without a new dependency * refactor(studio): one colour parser for the text toolbar, stricter rgb numbers The inline text toolbar kept its own canvas fallback for named colours. parseCssColor now resolves them, so the toolbar uses it alone. rgb() channels must be well-formed numbers; that replaces the NaN check after parsing and rejects rgb(1. 2 3) as CSS does. * refactor(studio): drop comment lines the comment ratchet refuses in colorValue * fix(studio): linear-time rgb() patterns and no new colour literal in the parser The rgb() number pattern now has one way to match each digit run, which clears CodeQL's slow-regex alerts. The canvas reset uses transparent so the Studio colour-literal ratchet stays at its baseline. * fix(studio): legacy colours resolve in browsers without relative colour syntax parseBrowserColor reads the colour itself from the canvas first and accepts a #rrggbb or rgba() result, then tries color(from ...). Named, hsl() and percent rgb() colours resolve again in the text toolbar on Chrome before 119 and Safari 17. * test(studio): a colour the canvas rejects parses to null, not transparent black * fix(studio): an inserted gradient stop takes the colour the gradient paints there The stop colour is color-mix() in oklab, or srgb when both stops are legacy, resolved by the browser, so it matches the painted midpoint including premultiplied alpha; the channel blend stays as the fallback. The parser tries color(from ...) before the legacy read, keeping alpha precise, and calls CSS.supports inside its try. A Chrome-147-shaped fake canvas covers the modern path in unit tests. * test(studio): keep the fake browser canvas inside its test file * fix(studio): mix an inserted gradient stop in the space the whole gradient uses * fix(studio): let an unresolved var() stop keep a legacy gradient in sRGB * fix(studio): only a bare var() stop leaves the gradient space to its neighbours * fix(studio): pick light-dark() text colour from what the element paints * refactor(studio): drop the neighbour clause the whole-gradient check already covers * fix(studio): resolve the picker colour from the element that paints the selected text * fix(studio): read the picker colour from the character the swatch colour came from --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |