From 04ccf6f906a2a36751b851e89ff450e5324f18eb Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 8 Oct 2026 23:15:17 +0700 Subject: [PATCH] Timeline: keyframes without a dark outline (#284) Each half of a keyframe glyph had a 1 px near-black border, so keys that overlap read as a tangle of borders. Keys are now flat, as in After Effects: each half is filled and edged in the key's own colour (the hairline closes the seam between the halves), so overlapping keys read as one shape. The Light theme's keyframe grey is darker so unselected keys still stand out on its light time graph without the border. Test: keyframes_are_drawn_without_a_dark_outline (icons). Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/icons.rs | 41 ++++++++++++++++++++++++--- crates/ui-egui/src/panels/timeline.rs | 2 +- crates/ui-egui/src/theme.rs | 2 ++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index de2c2b9d..1068acab 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -641,13 +641,24 @@ pub fn keyframe( left: effectcraft_engine::keyframe::KeyHalf, right: effectcraft_engine::keyframe::KeyHalf, fill: Color32, - outline: Color32, ) { + painter.extend(keyframe_shapes(center, size, left, right, fill)); +} + +/// The halves of a [`keyframe`] glyph. Flat, without a dark outline (as in After Effects), so +/// keys that overlap read as one shape instead of a tangle of borders (#284); the hairline in +/// the fill colour closes the seam between the halves. +fn keyframe_shapes( + center: Pos2, + size: f32, + left: effectcraft_engine::keyframe::KeyHalf, + right: effectcraft_engine::keyframe::KeyHalf, + fill: Color32, +) -> Vec { use effectcraft_engine::keyframe::KeyHalf as H; let r = size / 2.0; - let half = |pts: Vec| { - painter.add(PathShape::convex_polygon(pts, fill, Stroke::new(1.0, outline))); - }; + let mut out = Vec::with_capacity(2); + let mut half = |pts: Vec| out.push(PathShape::convex_polygon(pts, fill, Stroke::new(0.5, fill)).into()); let c = center; for (side, h) in [(-1.0f32, left), (1.0, right)] { match h { @@ -678,4 +689,26 @@ pub fn keyframe( } } } + out +} + +#[cfg(test)] +mod tests { + use effectcraft_engine::keyframe::KeyHalf as H; + + /// #284: keyframes have no dark border (overlapping keys looked messy): each half is filled + /// and edged in the key's own colour. + #[test] + fn keyframes_are_drawn_without_a_dark_outline() { + let fill = egui::Color32::from_gray(0xc0); + for (l, r) in [(H::Linear, H::Linear), (H::Bezier, H::Hold), (H::Auto, H::Linear)] { + let shapes = super::keyframe_shapes(egui::pos2(10.0, 10.0), 11.0, l, r, fill); + assert_eq!(shapes.len(), 2); + for s in shapes { + let egui::Shape::Path(p) = s else { panic!("a path: {s:?}") }; + assert_eq!(p.fill, fill); + assert!(p.stroke.width <= 0.5 && p.stroke.color == egui::epaint::ColorMode::Solid(fill), "{:?}", p.stroke); + } + } + } } diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 457a9396..bdedd202 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -2255,7 +2255,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { // Roving keys: a small dot (their time follows their neighbours). gp.circle_filled(c, 2.5, kcol); } else { - icons::keyframe(&gp, c, 11.0, icon.left, icon.right, kcol, Color32::from_black_alpha(200)); + icons::keyframe(&gp, c, 11.0, icon.left, icon.right, kcol); } let kr = Rect::from_center_size(c, vec2(12.0, 14.0)); let kresp = ui.interact(kr, egui::Id::new(("key", uid, k.time.0)), Sense::click_and_drag()); diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 1a9e1a1a..bfe4de75 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -187,6 +187,8 @@ impl Tokens { tl_ruler_bg: Color32::from_rgb(0xe2, 0xe2, 0xe2), tl_ruler_text: Color32::from_rgb(0x50, 0x50, 0x50), pasteboard: Color32::from_rgb(0xa8, 0xa8, 0xa8), + // Keyframes have no outline: dark enough to stand out on the light time graph. + keyframe: Color32::from_rgb(0x6c, 0x6c, 0x6c), ..dark }, }