Skip to content

feat(layers): move, scale, turn and fade layers between keyframes - #884

Merged
hm21 merged 2 commits into
stablefrom
feat/layer-keyframes
Oct 7, 2026
Merged

hm21 merged 2 commits into
stablefrom
feat/layer-keyframes

Conversation

@hm21

@hm21 hm21 commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Description

Problem. A video editor built on this package could animate a layer only in, out, or in a loop over its whole time range. It could not move a layer along a path, or grow, turn or fade it at moments the user picks, and the preview had nothing to match an export that does.

Changes.

  • Layer.keyframes (LayerKeyframe: time from the layer's start, offset, scale, rotation, opacity, curve) place a layer at points in time. Before the first keyframe it holds the first one, after the last one the last, and between two it moves along the earlier one's curve; rotation turns the whole difference. Helpers on Layer: keyframePlacementAt, keyframeIndexAt, setKeyframeAt, removeKeyframeAt, applyKeyframePlacement, transformKeyframes. Keyframes are serialized (minified key kf), copied by LayerCopyManager and compared in ==.
  • LayerKeyframe.effects: loop animations such as a wiggle, played from one keyframe to the next. Each cycle is fitted so a whole number of them fills the stretch and the layer rests on both keyframes. Layer.keyframeEffects places them on the video's timeline (LayerKeyframeEffect with start, end and cycle count), so an exporter can keep the same cycles.
  • Preview. LayerTimelineVisibility applies the keyframed placement as the difference to the laid-out one, turned the other way for a layer mirrored on one axis, then composes the animations and the keyframe effects on top, as pro_video_editor does.
  • Editing. A transform gesture, keyboard turn or zoom on a keyframed layer writes the keyframe at the playback position, adding one when there is none; LayerTimelineConfigs.keyframeTolerance sets how close counts as on a keyframe. Crops, flips, canvas resizes and image-frame moves transform the keyframes with the layer. setLayerTimeline takes keyframes.
  • Layer.opacity for every layer type (moved up from PaintLayer): applied by the layer widget and baked into captureAsPng. A keyframed layer carries its opacity in its keyframes.
  • replaceLayer(skipUpdateHistory: true) shows a layer without a history step, so a host can preview every value of a slider and record one step at the end.
  • Sharp exports. captureAsPng captures a keyframed layer at the largest size its keyframes grow it to (Layer.keyframeCaptureGrowth, at most 4x).
  • Fix. Grouping or ungrouping layers keeps their time range, animations, highlights and widget width.

Why. The divine video editor adds keyframes to overlay layers (divinevideo/divine-mobile#9845) and exports them through pro_video_editor 2.29.0 (hm21/pro_video_editor#235). This preview mirrors that renderer's keyframe and loop-window semantics, so what the user sets up is what the export shows.

Tests. Keyframe model, serialization and import/export, keyframe effects and their cycle fitting, the preview (placement, mirrored layers, legacy fade, effects only between their keyframes), gesture editing, copies, crop/flip/resize transforms and capture growth. Editing and preview were checked in the divine app on a Samsung Galaxy S26 (SM-S942B).

Related Issue: none in this repository; needed for divinevideo/divine-mobile#9845.

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore

Layer.keyframes place a layer at points in time; between them it moves
along the earlier keyframe's curve and can play loop effects. Video
editors that animate overlays over time need the preview to follow the
same keyframes their export renders. A transform gesture on a keyframed
layer changes the keyframe at the playback position.

Layer.opacity applies to every layer type and is baked into captures.
replaceLayer takes skipUpdateHistory so a host can preview a layer
without a history step, and captures grow keyframed layers so they stay
sharp at their largest keyframed size.
Review fixes for the layer keyframes:

- A crop or image rotation keeps the keyframes' rotation instead of
  resetting it to the layer's rotation from before the crop.
- A duplicated layer moves its keyframes with it, and addLayer's zoom and
  padding corrections apply to the keyframes too.
- A gesture, scroll or key that leaves a keyframed layer where it was
  adds no keyframe, which would change its easing.
- Drag selection and alignment guides measure keyframed layers where they
  are drawn at the playback position.
- A layer flipped in place turns its keyframed rotation the right way at
  once.
- The paint layer sheet hides its opacity slider for a keyframed layer,
  whose keyframes set the opacity.
- Layer opacity applies inside the hero, so it holds during sub-editor
  transitions, and a fully transparent layer lets touches through.
- The paint editor scales the keyframes of the layers it hands back.
- replaceLayer documents how to record previewed values as one undo step.
@hm21
hm21 marked this pull request as ready for review October 7, 2026 10:06
@hm21
hm21 merged commit 8305f0f into stable Oct 7, 2026
3 checks passed
hm21 added a commit to divinevideo/divine-mobile that referenced this pull request Oct 7, 2026
…29.0

The keyframe editing and export need the layer keyframes, keyframe
effects and windowed loops these releases add
(hm21/pro_image_editor#884, hm21/pro_video_editor#235).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant