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/effect-defs/src/definitions/blur.rs | 147 ++++++++++++++++++++++++ rust/crates/effect-defs/src/definitions/mod.rs | 12 ++ rust/crates/effect-defs/src/types.rs | 3 + 3 files changed, 162 insertions(+) create mode 100644 rust/crates/effect-defs/src/definitions/blur.rs create mode 100644 rust/crates/effect-defs/src/definitions/mod.rs (limited to 'rust/crates/effect-defs/src') diff --git a/rust/crates/effect-defs/src/definitions/blur.rs b/rust/crates/effect-defs/src/definitions/blur.rs new file mode 100644 index 00000000..8ad0dd55 --- /dev/null +++ b/rust/crates/effect-defs/src/definitions/blur.rs @@ -0,0 +1,147 @@ +use std::collections::HashMap; + +use effects::{EffectPass, UniformValue}; +use scene::params::{ParamValue, ParamValues}; + +use crate::math::{js_max, js_min, js_parse_float}; +use crate::params::{NumberParamDefinition, ParamDefinition}; +use crate::types::{EffectDefinition, EffectPassTemplate, EffectRendererConfig, PassContext}; + +pub const GAUSSIAN_BLUR_SHADER: &str = "gaussian-blur"; + +const MAX_SINGLE_PASS_SIGMA: f64 = 10.0; +const MAX_STEP: f64 = 4.0; +const MAX_EFFECTIVE_SIGMA: f64 = MAX_SINGLE_PASS_SIGMA * MAX_STEP; +const MAX_ITERATIONS: f64 = 8.0; + +pub const INTENSITY_TO_SIGMA_DIVISOR: f64 = 5.0; + +pub fn build_gaussian_blur_passes(sigma_x: f64, sigma_y: f64) -> Vec { + let max_sigma = js_max(sigma_x, sigma_y); + if max_sigma < 0.001 { + return Vec::new(); + } + + let iterations = js_min( + MAX_ITERATIONS, + js_max( + 1.0, + ((max_sigma * max_sigma) / (MAX_EFFECTIVE_SIGMA * MAX_EFFECTIVE_SIGMA)).ceil(), + ), + ); + let iteration_count = iterations.max(0.0) as usize; + let per_pass_sigma_x = sigma_x / iterations.sqrt(); + let per_pass_sigma_y = sigma_y / iterations.sqrt(); + let step_x = js_max(1.0, per_pass_sigma_x / MAX_SINGLE_PASS_SIGMA); + let step_y = js_max(1.0, per_pass_sigma_y / MAX_SINGLE_PASS_SIGMA); + + let mut passes = Vec::with_capacity(iteration_count * 2); + for _ in 0..iteration_count { + passes.push(blur_pass(per_pass_sigma_x, step_x, [1.0, 0.0])); + passes.push(blur_pass(per_pass_sigma_y, step_y, [0.0, 1.0])); + } + passes +} + +pub fn intensity_to_sigma(intensity: f64, resolution: f64, reference: f64) -> f64 { + (intensity / INTENSITY_TO_SIGMA_DIVISOR) * (resolution / reference) +} + +pub fn parse_intensity(effect_params: &ParamValues) -> f64 { + match effect_params.get("intensity") { + Some(ParamValue::Number(value)) => *value, + Some(ParamValue::String(value)) => js_parse_float(value), + Some(ParamValue::Bool(true)) => js_parse_float("true"), + Some(ParamValue::Bool(false)) => js_parse_float("false"), + None => js_parse_float("undefined"), + } +} + +pub fn blur_effect_definition() -> EffectDefinition { + EffectDefinition { + effect_type: "blur", + name: "Blur", + keywords: &["blur", "soft", "defocus"], + params: vec![ParamDefinition::Number(NumberParamDefinition::new( + "intensity", + "Intensity", + 15.0, + 0.0, + Some(100.0), + 1.0, + ))], + renderer: EffectRendererConfig { + passes: vec![ + EffectPassTemplate { + shader: GAUSSIAN_BLUR_SHADER, + uniforms: horizontal_template_uniforms, + }, + EffectPassTemplate { + shader: GAUSSIAN_BLUR_SHADER, + uniforms: vertical_template_uniforms, + }, + ], + build_passes: Some(build_passes_from_params), + }, + } +} + +fn blur_pass(sigma: f64, step: f64, direction: [f32; 2]) -> EffectPass { + EffectPass { + shader: GAUSSIAN_BLUR_SHADER.to_string(), + uniforms: HashMap::from([ + ("u_sigma".to_string(), UniformValue::Number(sigma as f32)), + ("u_step".to_string(), UniformValue::Number(step as f32)), + ( + "u_direction".to_string(), + UniformValue::Vector(direction.to_vec()), + ), + ]), + } +} + +fn horizontal_template_uniforms(context: &PassContext) -> HashMap { + let sigma = js_max( + intensity_to_sigma( + parse_intensity(context.effect_params), + context.width as f64, + 1920.0, + ), + 0.001, + ); + HashMap::from([ + ("u_sigma".to_string(), UniformValue::Number(sigma as f32)), + ("u_step".to_string(), UniformValue::Number(1.0)), + ( + "u_direction".to_string(), + UniformValue::Vector(vec![1.0, 0.0]), + ), + ]) +} + +fn vertical_template_uniforms(context: &PassContext) -> HashMap { + let sigma = js_max( + intensity_to_sigma( + parse_intensity(context.effect_params), + context.height as f64, + 1080.0, + ), + 0.001, + ); + HashMap::from([ + ("u_sigma".to_string(), UniformValue::Number(sigma as f32)), + ("u_step".to_string(), UniformValue::Number(1.0)), + ( + "u_direction".to_string(), + UniformValue::Vector(vec![0.0, 1.0]), + ), + ]) +} + +fn build_passes_from_params(context: &PassContext) -> Vec { + let intensity = parse_intensity(context.effect_params); + build_gaussian_blur_passes( + intensity_to_sigma(intensity, context.width as f64, 1920.0), + intensity_to_sigma(intensity, context.height as f64, 1080.0), + ) +} diff --git a/rust/crates/effect-defs/src/definitions/mod.rs b/rust/crates/effect-defs/src/definitions/mod.rs new file mode 100644 index 00000000..59f2575c --- /dev/null +++ b/rust/crates/effect-defs/src/definitions/mod.rs @@ -0,0 +1,12 @@ +pub mod blur; + +use crate::registry::EffectsRegistry; + +pub fn register_default_effects(registry: &mut EffectsRegistry) { + for definition in [blur::blur_effect_definition()] { + if registry.has(definition.effect_type) { + continue; + } + registry.register(definition); + } +} diff --git a/rust/crates/effect-defs/src/types.rs b/rust/crates/effect-defs/src/types.rs index c9815fd7..2b1e79d2 100644 --- a/rust/crates/effect-defs/src/types.rs +++ b/rust/crates/effect-defs/src/types.rs @@ -11,16 +11,19 @@ pub struct PassContext<'a> { pub height: u32, } +#[derive(Debug)] pub struct EffectPassTemplate { pub shader: &'static str, pub uniforms: fn(&PassContext) -> HashMap, } +#[derive(Debug)] pub struct EffectRendererConfig { pub passes: Vec, pub build_passes: Option Vec>, } +#[derive(Debug)] pub struct EffectDefinition { pub effect_type: &'static str, pub name: &'static str, -- cgit v1.3.1