summaryrefslogtreecommitdiff
path: root/rust/crates/mask-defs/src/paint.rs
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-28 20:14:51 +0200
committercashmere <cashmere@autistici.org>2026-07-28 20:14:51 +0200
commit32438ce25f69de99d2523b98237f62cfa90890d7 (patch)
tree3a712c347dec0889bdea3328b4c817c2d9392b27 /rust/crates/mask-defs/src/paint.rs
parentf5a82152394fde4d8f740f664ab7737b82861618 (diff)
downloadopencut-32438ce25f69de99d2523b98237f62cfa90890d7.tar.gz
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)
Diffstat (limited to 'rust/crates/mask-defs/src/paint.rs')
-rw-r--r--rust/crates/mask-defs/src/paint.rs133
1 files changed, 133 insertions, 0 deletions
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<Path>, 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<Color, MaskRenderError> {
+ 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<Path>,
+ 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();
+ }
+ }
+}