From 32438ce25f69de99d2523b98237f62cfa90890d7 Mon Sep 17 00:00:00 2001 From: cashmere Date: Tue, 28 Jul 2026 20:14:51 +0200 Subject: feat: port domain crates to Rust (session 1 complete) All green: cargo test --workspace (~300 tests, 0 failures). - timeline (45): placement, resize/ripple, split, snap, retime, audio separation, scene helpers - animation (73): bezier solver, channel sampling, keyframe ops, split/clamp/clone, param definitions - commands (9): Command trait, BatchCommand, history, 19 concrete commands, freeze-frame port 1:1 - audio (22): audible collection, range mixing, mastering limiter, rms/peak buckets - text (19): cosmic-text layout/measure/raster, backgrounds, decorations - graphics (52): full CSS gradient parser, tiny-skia raster, 4 shape definitions - effect-defs (16): blur definition, registry, pass resolution - mask-defs (19): all 9 mask renderers via tiny-skia - renderer (6): node model + scene builder port - desktop: GPUI projects screen wired to ProjectStore - scene: factory (web defaults: v32, Main Track, 1920x1080, 30fps) --- rust/crates/mask-defs/src/paint.rs | 133 +++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 rust/crates/mask-defs/src/paint.rs (limited to 'rust/crates/mask-defs/src/paint.rs') diff --git a/rust/crates/mask-defs/src/paint.rs b/rust/crates/mask-defs/src/paint.rs new file mode 100644 index 00000000..0b122132 --- /dev/null +++ b/rust/crates/mask-defs/src/paint.rs @@ -0,0 +1,133 @@ +use scene::StrokeAlign; +use tiny_skia::{Color, Paint, Path, Pixmap, PremultipliedColorU8, Stroke, Transform}; + +use crate::MaskRenderError; + +pub fn white_paint() -> Paint<'static> { + Paint { + shader: tiny_skia::Shader::SolidColor(Color::from_rgba8(255, 255, 255, 255)), + anti_alias: true, + ..Paint::default() + } +} + +pub fn fill_white(path: &Option, pixmap: &mut Pixmap) { + if let Some(path) = path { + pixmap.fill_path( + path, + &white_paint(), + tiny_skia::FillRule::Winding, + Transform::default(), + None, + ); + } +} + +pub fn parse_stroke_color(value: &str) -> Result { + let hex = value + .trim() + .strip_prefix('#') + .ok_or_else(|| MaskRenderError::InvalidStrokeColor(value.to_string()))?; + let expand = |c: u8| (c << 4) | c; + let (r, g, b, a) = match hex.len() { + 3 => { + let v = u32::from_str_radix(hex, 16) + .map_err(|_| MaskRenderError::InvalidStrokeColor(value.to_string()))?; + ( + expand(((v >> 8) & 0xF) as u8), + expand(((v >> 4) & 0xF) as u8), + expand((v & 0xF) as u8), + 255, + ) + } + 4 => { + let v = u32::from_str_radix(hex, 16) + .map_err(|_| MaskRenderError::InvalidStrokeColor(value.to_string()))?; + ( + expand(((v >> 12) & 0xF) as u8), + expand(((v >> 8) & 0xF) as u8), + expand(((v >> 4) & 0xF) as u8), + expand((v & 0xF) as u8), + ) + } + 6 => { + let v = u32::from_str_radix(hex, 16) + .map_err(|_| MaskRenderError::InvalidStrokeColor(value.to_string()))?; + (((v >> 16) & 0xFF) as u8, ((v >> 8) & 0xFF) as u8, (v & 0xFF) as u8, 255) + } + 8 => { + let v = u32::from_str_radix(hex, 16) + .map_err(|_| MaskRenderError::InvalidStrokeColor(value.to_string()))?; + ( + ((v >> 24) & 0xFF) as u8, + ((v >> 16) & 0xFF) as u8, + ((v >> 8) & 0xFF) as u8, + (v & 0xFF) as u8, + ) + } + _ => return Err(MaskRenderError::InvalidStrokeColor(value.to_string())), + }; + Ok(Color::from_rgba8(r, g, b, a)) +} + +pub fn canvas_stroke(stroke_width: f64) -> Stroke { + Stroke { + width: stroke_width as f32, + miter_limit: 10.0, + line_cap: tiny_skia::LineCap::Butt, + line_join: tiny_skia::LineJoin::Miter, + ..Stroke::default() + } +} + +pub fn round_stroke(stroke_width: f64) -> Stroke { + Stroke { + width: stroke_width as f32, + line_cap: tiny_skia::LineCap::Round, + line_join: tiny_skia::LineJoin::Round, + ..Stroke::default() + } +} + +pub fn stroke_path_with( + path: &Option, + stroke: &Stroke, + color: Color, + pixmap: &mut Pixmap, +) { + if let Some(path) = path { + let paint = Paint { + shader: tiny_skia::Shader::SolidColor(color), + anti_alias: true, + ..Paint::default() + }; + pixmap.stroke_path(path, &paint, stroke, Transform::default(), None); + } +} + +pub fn apply_stroke_align(stroke: &mut Pixmap, fill: &Pixmap, align: StrokeAlign) { + let keep_inside = match align { + StrokeAlign::Center => return, + StrokeAlign::Inside => true, + StrokeAlign::Outside => false, + }; + for (dst, src) in stroke.pixels_mut().iter_mut().zip(fill.pixels().iter()) { + let coverage = if keep_inside { + src.alpha() as u16 + } else { + 255 - src.alpha() as u16 + }; + let scale = |channel: u8| ((channel as u16 * coverage + 127) / 255) as u8; + let scaled = PremultipliedColorU8::from_rgba( + scale(dst.red()), + scale(dst.green()), + scale(dst.blue()), + scale(dst.alpha()), + ); + if let Some(scaled) = scaled { + *dst = scaled; + } else { + *dst = PremultipliedColorU8::from_rgba(0, 0, 0, 0).unwrap(); + } + } +} -- cgit v1.3.1