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/graphics/src/gradient/stops.rs | 151 +++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 rust/crates/graphics/src/gradient/stops.rs (limited to 'rust/crates/graphics/src/gradient/stops.rs') diff --git a/rust/crates/graphics/src/gradient/stops.rs b/rust/crates/graphics/src/gradient/stops.rs new file mode 100644 index 00000000..e719ae96 --- /dev/null +++ b/rust/crates/graphics/src/gradient/stops.rs @@ -0,0 +1,151 @@ +use super::ast::{Color, ColorStop, Distance}; +use crate::color::{is_transparent, parse_color_to_rgb}; + +#[derive(Debug, Clone, PartialEq)] +pub struct NormalizedColorStop { + pub color: String, + pub offset: f64, +} + +pub fn color_to_string(color: &Color) -> String { + match color { + Color::Hex(value) => format!("#{value}"), + Color::Literal(value) => value.clone(), + Color::Rgb(values) => format!("rgb({})", values.join(",")), + Color::Rgba(values) => format!("rgba({})", values.join(",")), + Color::Hsl(values) => format!("hsl({})", values.join(",")), + Color::Hsla(values) => format!("hsla({})", values.join(",")), + Color::Var(value) => format!("var({value})"), + } +} + +fn clamp01(value: f64) -> f64 { + value.clamp(0.0, 1.0) +} + +fn resolve_stop_offset(stop: &ColorStop, gradient_length: f64) -> Option { + match stop.length.as_ref()? { + Distance::Percent(value) => value.parse::().ok().map(|v| v / 100.0), + Distance::Px(value) => value.parse::().ok().map(|v| v / gradient_length), + Distance::Em(value) => value + .parse::() + .ok() + .map(|v| (v * 16.0) / gradient_length), + _ => None, + } +} + +fn fix_transparent_stops(stops: &[(String, Option)]) -> Vec<(String, Option)> { + let mut result = stops.to_vec(); + for i in 0..result.len() { + if !is_transparent(&result[i].0) { + continue; + } + let mut donor = None; + for j in (0..i).rev() { + if !is_transparent(&result[j].0) { + donor = parse_color_to_rgb(&result[j].0); + if donor.is_some() { + break; + } + } + } + if donor.is_none() { + for item in result.iter().skip(i + 1) { + if !is_transparent(&item.0) { + donor = parse_color_to_rgb(&item.0); + if donor.is_some() { + break; + } + } + } + } + if let Some(donor) = donor { + result[i].0 = format!("rgba({},{},{},0)", donor.r, donor.g, donor.b); + } + } + result +} + +pub fn normalize_color_stops( + color_stops: &[ColorStop], + gradient_length: f64, +) -> Vec { + let mapped: Vec<(String, Option)> = color_stops + .iter() + .map(|stop| { + ( + color_to_string(&stop.color), + resolve_stop_offset(stop, gradient_length), + ) + }) + .collect(); + + let fixed = fix_transparent_stops(&mapped); + let mut resolved = fixed; + let known_indices: Vec = resolved + .iter() + .enumerate() + .filter_map(|(index, stop)| stop.1.map(|_| index)) + .collect(); + + if known_indices.is_empty() { + let step = if resolved.len() > 1 { + 1.0 / (resolved.len() - 1) as f64 + } else { + 1.0 + }; + for (index, stop) in resolved.iter_mut().enumerate() { + stop.1 = Some(step * index as f64); + } + return resolved + .into_iter() + .map(|(color, offset)| NormalizedColorStop { + color, + offset: clamp01(offset.unwrap_or(0.0)), + }) + .collect(); + } + + let first_known = known_indices[0]; + if resolved[first_known].1.is_none() { + resolved[first_known].1 = Some(0.0); + } + for index in 0..first_known { + let next_offset = resolved[first_known].1.unwrap_or(0.0); + resolved[index].1 = Some((next_offset * index as f64) / first_known as f64); + } + + for window in known_indices.windows(2) { + let start_index = window[0]; + let end_index = window[1]; + let start_offset = resolved[start_index].1.unwrap_or(0.0); + let end_offset = resolved[end_index].1.unwrap_or(start_offset); + let gap = end_index - start_index; + if gap <= 1 { + continue; + } + let step = (end_offset - start_offset) / gap as f64; + for index in 1..gap { + resolved[start_index + index].1 = Some(start_offset + step * index as f64); + } + } + + let last_known = known_indices[known_indices.len() - 1]; + let last_offset = resolved[last_known].1.unwrap_or(1.0); + if last_known < resolved.len() - 1 { + let gap = resolved.len() - 1 - last_known; + let step = (1.0 - last_offset) / gap as f64; + for index in 1..=gap { + resolved[last_known + index].1 = Some(last_offset + step * index as f64); + } + } + + resolved + .into_iter() + .map(|(color, offset)| NormalizedColorStop { + color, + offset: clamp01(offset.unwrap_or(0.0)), + }) + .collect() +} -- cgit v1.3.1