Repository navigation
fix(preview, properties, export): timeline-zero effect cache, text-template properties tabs, and audio speed export filter chain - #546
Merged
theunavailableguy merged 19 commits intoNov 11, 2026
Conversation
- Render all 3 category grids (in/out/combo) always mounted, toggled via CSS (contents/hidden) so state is preserved across tab switches - Add enabled prop to CategoryGrid and useMediaAnimationCategory so each category only fetches when its tab is first visited - Track visitedTabs in MediaAnimationSection; in tab pre-seeded so it fetches on mount, out and combo fetch on first visit only - Fix activePresetId and handleSelectPreset to be per-category instead of derived from activeTab — fixes combo showing in preset data - Add client-side category filter in useMediaAnimationCategory to drop items whose raw.category does not match the requested category - Key allPresets map by id@category to prevent cross-category stomping - Fix button-in-button violation in PresetCard: outer wrapper changed from button to div with role=button and keyboard handler
…ly play
useMediaAnimationCatalogSnapshot was a plain function reading allPresets
once at mount time. When a user clicked a preset before the async fetch
completed, catalogSnapshot.presets was still empty, compileClipMotionKeyframes
received an empty catalog, found no preset, and wrote visualKeyframes: {}
to the clip — causing no animation during playback.
Fix: introduce a catalogListeners subscriber set. The fetch hook calls
notifyCatalogListeners() after writing new presets into allPresets.
useMediaAnimationCatalogSnapshot is now a real hook backed by
useSyncExternalStore so MediaAnimationSection re-renders with the
populated catalog the moment the fetch completes.
… keyframes Three changes to make playback animate immediately after selecting a preset: 1. downloadManifest() now calls notifyCatalogListeners() after the manifest lands in the cache. This causes useMediaAnimationCatalogSnapshot to re-render with manifest-merged tracks, so the next compile sees real keyframe data. 2. handleDownload() always recompiles the clip after a successful download, removing the isApplied guard that prevented recompile when the preset had been selected before the manifest arrived. 3. handleSelectPreset() auto-triggers handleDownload() when the selected preset has no cached manifest yet. The select applies the motion config immediately; the download fires in the background and recompiles visualKeyframes with full tracks the moment it completes — no manual download button click required.
Image clips with media animations (visualKeyframes) went black during playback because registerNativeImageAsset is async. The rasterizeImages method awaited every registration promise, blocking the frame render. buildNativeVideoProjectRequest then returned null (image raster not yet registered), causing the entire frame to be skipped — black canvas. The clip rendered correctly when paused because there was no real-time pressure on the await. Text and stickers already had nonBlockingText / nonBlockingStickers paths that return the cached snapshot with updated animated transforms while registration completes in the background. Images had no equivalent. Changes: - Add nonBlockingImages option to NativeRasterBridgeOptions - Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im-ren- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Add im- Ad visible every frame without re-uploading the texture - First-frame entr- First-frame entr- First-frame entr- First-frame entr- First-frame iately on clip entry - Store snapshot in imageSnapshotsByLayerId on each blocking resolution so the non-blocking path can use it on subsequent so the non-blocking path can use it on subsequent so the non-bloPass so the non-blockingsPl so the non-blocking path can use it on subsequent so the non-blockingsting nonBlockingText/nonBlockingStickers flags
Tasks 1-9 complete: - Task 1: Add PerfLogKind entries (media-animation-catalog-fetch, -download, -cache-hydration, -rollup) + MediaAnimationTelemetry class with 4 static fire-and-forget methods (recordCatalogFetch, recordManifestDownload, recordCacheHydration, recordAnimationEval) - Task 2: Instrument cache hydration in mediaAnimationCache.ts with performance.now() timing; hydrateManifests() now returns error count - Task 3: Instrument catalog fetch in useMediaAnimationCategory() hook; refactored from .then()/.catch() to async/await; capture httpStatus - Task 4: Instrument ensureDownloaded() cache-hit + network fetch spans with fetchDurationMs + writeDurationMs breakdown and error reporting - Task 5: MediaAnimationRollupCollector with hasRecordedData()/flush()/clear(); flush() computes p95 with sort; singleton mediaAnimationRollup exported - Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: InstronR- Task 6: Instrumeed- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Instrumn pe- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: InstrumEV- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Iatio- Task 6: Instrument compile- Task 6: Instlat- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: t:- Task 6: Instrument compile- Task 6: Instrument compile- Task 6: Instests__/: 37/37 passed
…on telemetry - Use selectedClip.kind (ClipKind) instead of selectedClip.type which does not exist on Clip and always produced 'unknown' (two call sites in MediaAnimationSection.tsx) - Wrap performance.now() - t0 in Math.round() on both the success and error paths in useMediaAnimationCategory, consistent with all other timing calls in the telemetry stack (mediaAnimationCatalog.ts)
…y demand (Bug 13) Problem: 100% frame drop on first play (droppedFramesRatio=1, 169/169 dropped) because Rust WorkPlanner only populated lookahead while already in Play mode. Three compounding delays: configureNativePlaybackRender IPC not established, serial audio IPC chain, cold HEVC decode (50–500ms). Fix (four sites in NativeProgramPreview.tsx): 1. primeNativeLookahead() — new closure-local function (same scope as prefetchUpcomingNativeVideo) that calls queueNativeFrame fire-and-forget for LOOKAHEAD_PRIME_FRAMES=6 frames while still paused. Uses 'video:prime:' cache-key namespace to avoid colliding with existing prefetch entries. Emits playback-trace telemetry via perfLogService. 2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused-settle trigger — pr tr2. paused-settle trigger — prim2. paused-settle trigger — prim2. paused- de2. paused-settle trigger — prim2. paused-settle trigger — prim2. pauseder 2. paused-settle trigger — prim2. paused-settle trigger — prim2. pausedam2. paused-settle trigger — prim2. paused-settle trsure2. paused-settle trigger — prim2. paused-sditionally (not deferred until persistentNativePausedEligible is satisfi deferred until persistentNativePausedEligible is satisfi deferred erifi deferred until persistentNativePausedEligible is satisfi Pre deferred until persistentNativePausedEligible is satisfi deferre3 regression tests)
…ss testable - mediaAnimationCatalog.ts: replace (p as any).phase with p.phase at both filter sites in getCatalogSnapshot() — RemoteTextAnimationPreset already types phase: MotionBehaviorCategory, no cast needed (review blocker) - ProgramPreview.renderLoop.test.ts: add videoLayersByFrame option to makePrimingHarness so the scene.visualLayers guard in primeNativeLookahead is exercised rather than hardcoded to true (review finding #3)
The updateSource() path was missing a markNativeAudioUnavailable() call for the 'genuinely no audio clips' branch. When updateSource() fires after initialize() (via the pendingSource drain), it would overwrite the flag that initialize() correctly set — leaving hasNativeClockPosition false and permanently blocking nativeAudioClockReady for silent projects. The result: pressing play on a project with no audio clips produced 1 frame attempt then froze — the render loop never entered persistentNativePlaybackEligible because nativeAudioClockReady stayed false, so animations never ran. initialize() already had the correct guard; this aligns updateSource() to match. The allClipsHavePaths / resolving branch (paths not yet hydrated) is unchanged — that path correctly skips the call since audio will join later.
…yback freeze - Invalidate evaluation cache when effects, filters, adjustments, colorGrade, or animations change in computeClipVersion - Guard lookahead pre-decode outcome in present_native_frame_internal when timeline has no video layers (image/text only) - Match raster layers by asset_id in NativeRenderSession and guard is_text with prefix check - Reset lastNativePlaybackRequestKey on transition to playing so t=0 frame demand is dispatched - Add !this._nativeAudioUnavailable guard to CPAL init timeout in PlaybackClock.time - Add comprehensive deterministic reproduction matrix and regression tests
… ingestion parity - compile 'scale' property symmetrically to width and height keyframes to prevent aspect-ratio distortion - clear legacy entranceAnimation and exitAnimation on modern motion preset selection to prevent split-brain collisions - make subcategory normalization case-insensitive in animationCatalog - add comprehensive textAnimationPipeline integration suite covering ingestion, composition, scaling, t=0 evaluation, and GPU quad scaling
…Section architecture - Extract preset grid into standalone PhaseGrid component with PresetCard parity - Keep all three tabs (in/out/loop) permanently mounted using CSS contents/hidden toggling - Implement visitedTabs lazy-fetch gating with useAnimationPhase(phase, enabled) - Remove legacy subcategories in favor of All vs Favorites filter matching media animations - Add TextAnimationCacheManager for disk persistence under BaseDirectory.AppCache - Support thumbnail, WebM hover preview video, and download indicator states - Add comprehensive behavioral unit tests for lazy gating and lifecycle persistence
…n when downloading - In PresetCard, invoke onSelect() unconditionally when card is clicked, and call onDownload() to fetch manifest in background if not yet cached - Pass motionOverride to handleDownload so keyframe compilation dispatches updates to timeline clip as soon as manifest arrives - Ensure clicking an un-downloaded preset immediately sets inPreset on the selected clip instead of requiring a second click
Fixes: Applying a stagger-based preset (e.g. 'Flip In') rotated the entire text bounding box as a rigid rectangle instead of flipping individual characters one by one — breaking parity with Clypra Studio. Root cause: motionPresets.ts compiled ALL preset tracks into clip-level visualKeyframes (6 channels: x, y, width, height, rotation, opacity). This discarded stagger config entirely and mapped 'rotation' to the whole-layer rotation channel. Fix (5-part): 1. glyphAnimator.ts (NEW): Port calculateStaggerProgress + evaluatePresetTransform math from clypra-studio's animationEvaluator.ts. Identical stagger math ensures visual output matches Studio exactly. 2. EvaluatedTextLayer (types.ts): Add optional motion, motionInProgress, and motionOutProgress fields forwarded from evaluator to rasterizer. 3. evaluator.ts: Compute normalized in/out progress [0,1] from clip.motion and attach to textLayer alongside the motion config. 4. textRasterizer.ts: When a stagger-based preset is active, render each character individually with its own translate/rotate/scale/skewX/blur/ tracking transforms evaluated per glyph via glyphAnimator. 5. motionPresets.ts: Guard stagger presets with isGlyphAnimatedPreset() so they are never compiled into clip-level visualKeyframes. 6. nativeTextPreview.ts: Include rounded motionInProgress/motionOutProgress in buildNativeTextKeyObject so animated frames get unique cache keys. Tests: 27 new regression tests in glyphAnimator.test.ts covering stagger math, easing, track interpolation, and the core per-glyph divergence (different glyphs at same overall progress must get different transforms). verify:fast: 4/4 passed (TypeScript clean, architecture invariants, 156 preview render loop tests, telemetry audit).
…mation sticky footer - Remove unpolished inline duration slider from MotionPresetSection - Extend PropertiesPanel sticky footer duration control to support text clips - Synchronize active animation sub-tab (in/out/loop) between preset section and panel footer - Target inDuration or outDuration based on active category and preset state - Add pointer drag-scrubbing with setPointerCapture to duration slider track - Compile responsive visualKeyframes via text animation catalog for text clips - Add automated regression tests in PropertiesPanelTextAnimation.test.tsx and update MotionPresetSection.test.tsx
…io speed export filter chain - Fix PropertyPanelHeader to include text-template clip kind in TEXT_TABS resolution (CLY-BUG-TEXT-001) - Add regression test suite for PropertyPanelHeader across all clip kinds - Implement chained atempo filter complex generation in audio_filter.rs for export speed remapping - Fix nativeTimelineExport eligibility check to prevent preservePitch from disqualifying cut-only video - Add reset buttons for volume and fades in AudioSection and AudioBasicTab, wiring AudioFXSection - Update types, tests, and otioImporter for export audio clip speed configuration
theunavailableguy
approved these changes
Nov 11, 2026
theunavailableguy
deleted the
fix/timeline-zero-effect-playback-freeze
branch
November 11, 2026 02:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves timeline-zero effect cache invalidation, text-template properties tab rendering, and audio speed export filter parity:
computeClipVersiontracks all visual effects, filters, adjustments, colorGrade, chromaKey, animations, and conform to prevent stale cache hits on t=0.playback_lookahead_misscheck so image and text timelines composite without stalling.PlaybackClock.PropertyPanelHeader.tsxto includetext-templateclip kind inTEXT_TABSresolution. Text templates now reliably display Text and Animation tabs and mountTextStyleSectionandTextTemplateOverlay.atempofilter complex generation in Rustaudio_filter.rsfor non-unity playback speeds andasetratefor pitch shifts.nativeTimelineExport.tseligibility check so defaultpreservePitchdoes not spuriously disqualify cut-only videos from native export.AudioSectionandAudioBasicTab, wiringAudioFXSection.Type
bugfix)Evidence Bundle (mandatory for preview/playback/audio/text/sticker changes)
Test Output
Subsystem Contract Suites
replayBehavioralContract.test.ts(38/38 passed)audioPreviewSyncContract.test.ts(27/27 passed)previewStateResetContract.test.ts(29/29 passed)textEditPreviewCycle.test.ts(26/26 passed)Total: 120/120 passed in 4.46s.
Negative Control (bug fixes only)
PropertyPanelHeader.test.tsxto pre-fix code → Failed:expected "vi.fn()" to be called with ['text'], received [null]Mutation Canary
npm run verify:fastpassed with 200 baseline render loop tests)Architecture-First Delivery
PropertyPanelHeader.tsx: tab resolution omittedtext-template.audio_filter.rs: FFmpeg audio filter graph lacked speed/tempo filter chains.nativeTimelineExport.ts:audio.speed.preservePitchchecked as unsupported setting on cut-only clips.historyStore.dispatch/ command pattern.PropertyPanelHeader.test.tsx(6 tests), updatednativeTimelineExport.test.tsandaudio_filter.rstest suite.