diff --git a/crates/ui-egui/src/panels/gradient_editor.rs b/crates/ui-egui/src/panels/gradient_editor.rs index fe10f4ddb..e132a42d1 100644 --- a/crates/ui-egui/src/panels/gradient_editor.rs +++ b/crates/ui-egui/src/panels/gradient_editor.rs @@ -241,7 +241,7 @@ fn commit(app: &mut EffectcraftApp, ctx: &egui::Context) { } } -fn to_color32(c: [f32; 4]) -> Color32 { +pub(super) fn to_color32(c: [f32; 4]) -> Color32 { let b = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; Color32::from_rgba_unmultiplied(b(c[0]), b(c[1]), b(c[2]), b(c[3])) } diff --git a/crates/ui-egui/src/panels/scriptui_view.rs b/crates/ui-egui/src/panels/scriptui_view.rs index 242626272..d12309b02 100644 --- a/crates/ui-egui/src/panels/scriptui_view.rs +++ b/crates/ui-egui/src/panels/scriptui_view.rs @@ -261,20 +261,22 @@ fn draw_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, origin: egui::Pos2, } } +/// A draw-list colour (`None` is the theme colour). Script colours are display (sRGB) values +/// in 0..1, as in After Effects, not linear light: `[43/255, 43/255, 43/255, 1]` is RGB 43. +fn draw_color(c: &Option<[f32; 4]>, theme: Color32) -> Color32 { + c.map_or(theme, super::gradient_editor::to_color32) +} + /// Paint an onDraw draw list in `rect` (control coordinates → screen). fn paint_draw(ui: &egui::Ui, rect: Rect, ops: &[DrawOp], t: &Tokens, services: &dyn Services) { let p = ui.painter().with_clip_rect(rect.intersect(ui.clip_rect())); - let col = |c: &Option<[f32; 4]>, theme: Color32| match c { - Some(c) => egui::Rgba::from_rgba_unmultiplied(c[0], c[1], c[2], c[3]).into(), - None => theme, - }; let at = |x: f64, y: f64| rect.min + vec2(x as f32, y as f32); for op in ops { match op { DrawOp::Fill { color, path } => { // Any path shape (concave, self-intersecting, with holes): non-zero winding // triangles, not egui's convex-only polygon fill. - let c = col(color, t.field_bg); + let c = draw_color(color, t.field_bg); let mut mesh = egui::Mesh::default(); for tri in PathSeg::fill_triangles(path, 48, false) { let base = mesh.vertices.len() as u32; @@ -288,14 +290,14 @@ fn paint_draw(ui: &egui::Ui, rect: Rect, ops: &[DrawOp], t: &Tokens, services: & } } DrawOp::Stroke { color, width, path } => { - let s = Stroke::new(*width as f32, col(color, t.text)); + let s = Stroke::new(*width as f32, draw_color(color, t.text)); for (pts, closed) in PathSeg::polylines(path, 48) { let pts: Vec = pts.iter().map(|q| at(q[0], q[1])).collect(); p.add(if closed { egui::Shape::closed_line(pts, s) } else { egui::Shape::line(pts, s) }); } } DrawOp::Text { text, color, x, y, size, .. } => { - p.text(at(*x, *y), Align2::LEFT_TOP, text, Tokens::ui(*size as f32), col(color, t.text)); + p.text(at(*x, *y), Align2::LEFT_TOP, text, Tokens::ui(*size as f32), draw_color(color, t.text)); } DrawOp::Image { image, x, y, w, h } => { let tex = image.as_ref().and_then(|i| image_texture(ui.ctx(), i, services)); @@ -595,3 +597,47 @@ pub fn sync_panels(app: &mut EffectcraftApp) { } } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Script colours are sRGB, not linear: `#2B2B2B` drew as RGB 114 when read as linear light. + #[test] + fn draw_colours_are_srgb() { + let v = 43.0 / 255.0; + assert_eq!(draw_color(&Some([v, v, v, 1.0]), Color32::RED), Color32::from_rgb(43, 43, 43)); + let orange = [242.0 / 255.0, 140.0 / 255.0, 40.0 / 255.0, 1.0]; + assert_eq!(draw_color(&Some(orange), Color32::RED), Color32::from_rgb(242, 140, 40)); + assert_eq!(draw_color(&None, Color32::RED), Color32::RED, "no colour is the theme colour"); + } + + /// fillPath, strokePath and drawString all paint the script's colour as sRGB. + #[test] + fn paint_draw_paints_srgb_colours() { + let v = 43.0 / 255.0; + let color = Some([v, v, v, 1.0]); + let path = vec![PathSeg::R { x: 0.0, y: 0.0, w: 10.0, h: 10.0 }]; + let ops = [ + DrawOp::Fill { color, path: path.clone() }, + DrawOp::Stroke { color, width: 1.0, path }, + DrawOp::Text { text: "Hi".into(), color, x: 0.0, y: 0.0, size: 12.0, style: String::new() }, + ]; + let t = Tokens::for_kind(crate::theme::ThemeKind::Dark); + let mut out = egui::Context::default().run_ui(egui::RawInput::default(), |ui| { + paint_draw(ui, Rect::from_min_size(pos2(0.0, 0.0), vec2(50.0, 50.0)), &ops, &t, &effectcraft_engine::FsServices); + }); + out.textures_delta.clear(); + let want = Color32::from_rgb(43, 43, 43); + let (mut fill, mut stroke, mut text) = (false, false, false); + for s in out.shapes { + match s.shape { + egui::Shape::Mesh(m) => fill |= m.vertices.iter().all(|v| v.color == want), + egui::Shape::Path(p) => stroke |= matches!(p.stroke.color, egui::epaint::ColorMode::Solid(c) if c == want), + egui::Shape::Text(t) => text |= t.fallback_color == want, + _ => {} + } + } + assert!(fill && stroke && text, "fill {fill}, stroke {stroke}, text {text}"); + } +}