Repository navigation
feat: keyframes for text and image overlays (engine + GPUI) - #2846
YoavLevavi wants to merge 4 commits into
Conversation
Text and image segments gain optional keyframes for position, size and opacity (text: position and opacity), in seconds from the segment start, eased between keys. The renderer samples them per frame. In the GPUI editor the text and image panels get a Keyframes section: add a key at the playhead, jump to or remove keys. Once a property is keyed, dragging on the canvas or moving the opacity slider edits the key under the playhead. Keys show as marks on the timeline boxes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
@YoavLevavi is attempting to deploy a commit to the Cap Software Inc Team on Vercel. A member of the Team first needs to authorize it. |
|
Thanks @YoavLevavi, this is a substantial feature, and the engine side is clean: |
- Text and image segments get their own TrackSegmentOps whose trim_start_to shifts the keys by the change in start (as masks do), so a start trim or the tail half of a split keeps the animation where it was in output time. Split and trim tests for both types. - The canvas selection, snap targets and the image box filter use opacity_at(time), so an image keyed up from a base opacity of 0 can be selected and dragged. - keyframes is skipped when empty, so existing projects save unchanged (attribute order keeps the generated TypeScript field optional); bindings regenerated and the image serde test is back to its original shape. - The keyframe chips and button use an svg diamond and x icon instead of text glyphs. Merges origin/main. Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Thanks @richiemcilroy, all three are in 3782cd2 (also merged main), plus the optional one:
@greptileai review |
| impl_keyed_overlay_segment!(TextSegment); | ||
| impl_track_segment!(cap_project::StyleSegment, lane: track); | ||
| impl_track_segment!(cap_project::ImageSegment, lane: track); | ||
| impl_keyed_overlay_segment!(cap_project::ImageSegment); |
There was a problem hiding this comment.
Deleted clips leave keys behind
Deleting a clip does not retime keys on text or images that cross the removed interval. Both still use ripple_delete_track, which changes only their bounds. For an overlay spanning 10–20 seconds, deleting 12–14 seconds shortens it to 10–18, but a key originally at 18 stays there instead of moving to 16. Add key-aware ripple deletion for both overlay types.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/editor_edits.rs
Line: 683-685
Comment:
**Deleted clips leave keys behind**
Deleting a clip does not retime keys on text or images that cross the removed interval. Both still use `ripple_delete_track`, which changes only their bounds. For an overlay spanning 10–20 seconds, deleting 12–14 seconds shortens it to 10–18, but a key originally at 18 stays there instead of moving to 16. Add key-aware ripple deletion for both overlay types.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| #[serde(skip_serializing_if = "OverlayKeyframes::is_empty", default)] | ||
| pub keyframes: OverlayKeyframes, |
There was a problem hiding this comment.
Faster clips cut animations short
set_clip_segment_timescale retimes text and image bounds but leaves their new keys unchanged. Speeding a 10-second clip to 2× shortens a matching overlay to 5 seconds, but its final opacity key remains at relative time 10. The overlay disappears before reaching that value. Map each overlay key through the same change in time, as the mask path already does.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/project/src/configuration.rs
Line: 1438-1439
Comment:
**Faster clips cut animations short**
`set_clip_segment_timescale` retimes text and image bounds but leaves their new keys unchanged. Speeding a 10-second clip to 2× shortens a matching overlay to 5 seconds, but its final opacity key remains at relative time 10. The overlay disappears before reaching that value. Map each overlay key through the same change in time, as the mask path already does.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| relative_time: f64, | ||
| ) -> (XY<f64>, XY<f64>, f64) { | ||
| ( | ||
| sample_vector_keys(&self.position, relative_time).unwrap_or(center), |
There was a problem hiding this comment.
Centering ignores animated positions
Once position keys exist, sample ignores changes to the base center. The existing “Center on canvas” button still writes only segment.center, so it stops moving an image after the user adds a keyframe. Switching keyed text to TextLayout::Fullscreen has the same problem. Route these position changes through the keys, or explicitly reset the position animation.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/project/src/configuration.rs
Line: 1307
Comment:
**Centering ignores animated positions**
Once position keys exist, `sample` ignores changes to the base `center`. The existing “Center on canvas” button still writes only `segment.center`, so it stops moving an image after the user adds a keyframe. Switching keyed text to `TextLayout::Fullscreen` has the same problem. Route these position changes through the keys, or explicitly reset the position animation.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| if segment.keyframes.size.is_empty() { | ||
| segment.size = size; | ||
| } else { | ||
| segment.keyframes.set_size(at, size); |
There was a problem hiding this comment.
Moving changes the size animation
Moving or nudging an image inserts a size key even when its size did not change. With size keys at 0 and 10 seconds, moving the image at 5 seconds inserts a third size key. Because each span eases separately, this changes the size animation before and after the move. Write a size key only when the gesture changes size.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/editor_canvas.rs
Line: 1260-1263
Comment:
**Moving changes the size animation**
Moving or nudging an image inserts a size key even when its size did not change. With size keys at 0 and 10 seconds, moving the image at 5 seconds inserts a third size key. Because each span eases separately, this changes the size animation before and after the move. Write a size key only when the gesture changes size.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| if segment.keyframes.position.is_empty() { | ||
| segment.center = center; | ||
| } else { | ||
| segment.keyframes.set_position(at, center); |
There was a problem hiding this comment.
Keys added by dragging or nudging do not appear on the timeline. write_overlay_rect calls project_changed_live, which does not rebuild the stored TimelineModel; ending the drag only saves. The new marks read key times copied into that model, so they remain stale until another edit rebuilds it. Refresh those times when the canvas edit finishes.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/editor_canvas.rs
Line: 1258
Comment:
**New keys lack timeline marks**
Keys added by dragging or nudging do not appear on the timeline. `write_overlay_rect` calls `project_changed_live`, which does not rebuild the stored `TimelineModel`; ending the drag only saves. The new marks read key times copied into that model, so they remain stale until another edit rebuilds it. Refresh those times when the canvas edit finishes.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| let mut sorted: Vec<&T> = keys.iter().filter(|k| key_time(k).is_finite()).collect(); | ||
| sorted.sort_by(|a, b| key_time(a).total_cmp(&key_time(b))); |
There was a problem hiding this comment.
keyframe_span allocates and sorts a new list every time a property is sampled. A full sample repeats this for position, size and opacity on every frame, and opacity_at repeats all three just to read opacity. This adds avoidable work as overlays and keys grow. Find the surrounding keys without allocating, or keep a sorted list ready for sampling.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/project/src/configuration.rs
Line: 1315-1316
Comment:
**Every frame sorts keys again**
`keyframe_span` allocates and sorts a new list every time a property is sampled. A full `sample` repeats this for position, size and opacity on every frame, and `opacity_at` repeats all three just to read opacity. This adds avoidable work as overlays and keys grow. Find the surrounding keys without allocating, or keep a sorted list ready for sampling.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| for time in keyframes.iter() { | ||
| let x = ((time / duration).clamp(0., 1.) as f32) * width; |
There was a problem hiding this comment.
Trimming and splitting deliberately retain keys outside the segment to preserve the animation. This clamp draws negative times as start marks and later times as end marks, showing keys where none exist. Skip marks outside the visible segment while keeping those keys for sampling.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/editor_timeline.rs
Line: 2808-2809
Comment:
**Hidden keys appear at edges**
Trimming and splitting deliberately retain keys outside the segment to preserve the animation. This clamp draws negative times as start marks and later times as end marks, showing keys where none exist. Skip marks outside the visible segment while keeping those keys for sampling.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| /// The Keyframes section of the text and image panels: add a key at the | ||
| /// playhead, then any move, resize or opacity change at another time adds | ||
| /// or edits a key there. Each key is a chip: click to jump, × to remove. |
There was a problem hiding this comment.
This new comment narrates how the controls work. AGENTS.md explicitly bans comments that merely describe the code and allows them only for non-obvious context. Remove this description. The same pattern appears above KeyframeTarget and KEYFRAME_MARK, and elsewhere in the added comments. This repository requirement must be satisfied before merging.
Context Used: AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/editor_panels.rs
Line: 5687-5689
Comment:
**Comments repeat the code**
This new comment narrates how the controls work. `AGENTS.md` explicitly bans comments that merely describe the code and allows them only for non-obvious context. Remove this description. The same pattern appears above `KeyframeTarget` and `KEYFRAME_MARK`, and elsewhere in the added comments. This repository requirement must be satisfied before merging.
**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…fresh marks after canvas edits Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
All four fixed in a06f4de:
@greptileai review |
Keyframes for text and image overlays: animate position, size and opacity over a segment.
Engine (shared crates)
OverlayKeyframes { position, size, opacity }onTextSegmentandImageSegment(#[serde(default)], so existing projects load unchanged). Times are seconds from the segment start; values ease between keys and hold outside them.prepare_textsand the image layer sample them per frame. Segment helpers:opacity_at,set_opacity_at,add_keyframe_at, plus upsert and remove onOverlayKeyframes.GPUI editor
Testing:
cargo test -p cap-project --lib(sampling, editing, segment helpers),cargo test -p cap-rendering --lib text image,cargo test --bin cap-gpui.🤖 Generated with Claude Code
The PR still needs fixes because deleting clips and changing clip speed leave overlay keys at the wrong times.
Findings
Fix with agent prompt
Summary
Adds position, size, and opacity keys for text and image overlays, with controls in the GPUI editor.
Reviews (2) · Last reviewed commit: "Scan keyframes without sorting, mark onl..." · Reviewed by Greptile