motionloom 0.1.0

MotionLoom DSL parser and renderer for video effects, scene graphs, motion graphics, and world graphs.
Documentation
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProcessCategory {
    TransformCamera,
    ColorTone,
    StylizeLook,
    BlurSharpenDetail,
    KeyingMatteMask,
    LightAtmosphere,
    DistortionWarp,
    Composite,
    Transition,
    Testing,
}

impl ProcessCategory {
    pub const fn label(self) -> &'static str {
        match self {
            Self::TransformCamera => "Transform & Camera",
            Self::ColorTone => "Color & Tone",
            Self::StylizeLook => "Stylize & Look",
            Self::BlurSharpenDetail => "Blur, Sharpen & Detail",
            Self::KeyingMatteMask => "Keying, Matte & Mask",
            Self::LightAtmosphere => "Light & Atmosphere",
            Self::DistortionWarp => "Distortion & Warp",
            Self::Composite => "Composite",
            Self::Transition => "Transition",
            Self::Testing => "Testing",
        }
    }

    pub const fn folder(self) -> &'static str {
        match self {
            Self::TransformCamera => "transform_camera",
            Self::ColorTone => "color_tone",
            Self::StylizeLook => "stylize_look",
            Self::BlurSharpenDetail => "blur_sharpen_detail",
            Self::KeyingMatteMask => "keying_matte_mask",
            Self::LightAtmosphere => "light_atmosphere",
            Self::DistortionWarp => "distortion_warp",
            Self::Composite => "composite",
            Self::Transition => "transition",
            Self::Testing => "testing",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProcessEffectDefinition {
    pub id: &'static str,
    pub display_name: &'static str,
    pub category: ProcessCategory,
    pub kernel: &'static str,
    pub summary: &'static str,
}

pub const PROCESS_CATEGORIES: [ProcessCategory; 10] = [
    ProcessCategory::TransformCamera,
    ProcessCategory::ColorTone,
    ProcessCategory::StylizeLook,
    ProcessCategory::BlurSharpenDetail,
    ProcessCategory::KeyingMatteMask,
    ProcessCategory::LightAtmosphere,
    ProcessCategory::DistortionWarp,
    ProcessCategory::Composite,
    ProcessCategory::Transition,
    ProcessCategory::Testing,
];

pub const PROCESS_EFFECTS: [ProcessEffectDefinition; 16] = [
    ProcessEffectDefinition {
        id: "transform_camera.affine_uv",
        display_name: "Affine UV Transform",
        category: ProcessCategory::TransformCamera,
        kernel: "transform_camera_affine.wgsl",
        summary: "Scale/rotate/translate UV for camera-style pan+zoom motion.",
    },
    ProcessEffectDefinition {
        id: "color_tone.exposure_contrast",
        display_name: "Exposure + Contrast",
        category: ProcessCategory::ColorTone,
        kernel: "color_core.wgsl",
        summary: "Unified color core: exposure/contrast/sat/curves/LUT + look and utility ops.",
    },
    ProcessEffectDefinition {
        id: "stylize_look.posterize",
        display_name: "Posterize",
        category: ProcessCategory::StylizeLook,
        kernel: "stylize_look_posterize.wgsl",
        summary: "Quantize RGB into a fixed number of tonal levels.",
    },
    ProcessEffectDefinition {
        id: "blur_sharpen_detail.gaussian",
        display_name: "Blur/Sharpen Operator",
        category: ProcessCategory::BlurSharpenDetail,
        kernel: "blur_sharpen_detail_gaussian.wgsl",
        summary: "Mode-driven kernel: gaussian_5tap_blur | gaussian_5tap_h | gaussian_5tap_v | box | unsharp.",
    },
    ProcessEffectDefinition {
        id: "keying_matte_mask.luma_key",
        display_name: "Luma Key Matte",
        category: ProcessCategory::KeyingMatteMask,
        kernel: "keying_matte_mask_luma_key.wgsl",
        summary: "Build matte alpha from image luminance with soft edge.",
    },
    ProcessEffectDefinition {
        id: "light_atmosphere.bloom_prefilter",
        display_name: "Bloom Prefilter",
        category: ProcessCategory::LightAtmosphere,
        kernel: "light_atmosphere_bloom_prefilter.wgsl",
        summary: "Extract bright-energy signal before blur and merge.",
    },
    ProcessEffectDefinition {
        id: "light_atmosphere.bloom",
        display_name: "Bloom / Glow Composite",
        category: ProcessCategory::LightAtmosphere,
        kernel: "color_core.wgsl",
        summary: "Full-scene post-process glow: bright-pass, blur, then additive composite.",
    },
    ProcessEffectDefinition {
        id: "light_atmosphere.glow_stack",
        display_name: "Glow Stack",
        category: ProcessCategory::LightAtmosphere,
        kernel: "color_core.wgsl",
        summary: "Cinematic multi-radius glow stack with threshold, tint, and intensity controls.",
    },
    ProcessEffectDefinition {
        id: "color_tone.tone_map",
        display_name: "Tone Map",
        category: ProcessCategory::ColorTone,
        kernel: "color_core.wgsl",
        summary: "Filmic tone mapping with exposure, contrast, shoulder, gamma, and saturation.",
    },
    ProcessEffectDefinition {
        id: "light_atmosphere.light_sweep",
        display_name: "Light Sweep",
        category: ProcessCategory::LightAtmosphere,
        kernel: "color_core.wgsl",
        summary: "Animated directional sweep highlight for text, logos, and energy reveals.",
    },
    ProcessEffectDefinition {
        id: "stylize_look.texture_overlay",
        display_name: "Texture Overlay",
        category: ProcessCategory::StylizeLook,
        kernel: "color_core.wgsl",
        summary: "Procedural paper, canvas, impasto, brushed paint, grain, noise, and scanline texture overlay for tactile surfaces.",
    },
    ProcessEffectDefinition {
        id: "distortion_warp.magnify_lens",
        display_name: "Magnify Lens",
        category: ProcessCategory::DistortionWarp,
        kernel: "color_core.wgsl",
        summary: "Circular pixel-space lens magnification with distortion, feathering, and glass highlight.",
    },
    ProcessEffectDefinition {
        id: "distortion_warp.heat_haze_uv",
        display_name: "Heat Haze UV Warp",
        category: ProcessCategory::DistortionWarp,
        kernel: "distortion_warp_heat_haze.wgsl",
        summary: "Noise-driven UV offset for shimmer/heat refraction looks.",
    },
    ProcessEffectDefinition {
        id: "composite.opacity",
        display_name: "Opacity Composite",
        category: ProcessCategory::Composite,
        kernel: "composite_core.wgsl",
        summary: "Composite operator for layer opacity and related blend staging.",
    },
    ProcessEffectDefinition {
        id: "transition.fade_in",
        display_name: "Transition Core",
        category: ProcessCategory::Transition,
        kernel: "transition_core.wgsl",
        summary: "Transition family entry (fade_in/fade_out/dip/dissolve via effect).",
    },
    ProcessEffectDefinition {
        id: "testing.effect_for_testing_run",
        display_name: "Testing Kernel",
        category: ProcessCategory::Testing,
        kernel: "effect_for_testing_run.wgsl",
        summary: "Testing-only kernel slot for quick validation runs.",
    },
];

pub fn process_effects() -> &'static [ProcessEffectDefinition] {
    &PROCESS_EFFECTS
}

pub fn process_effect_for_id(id: &str) -> Option<&'static ProcessEffectDefinition> {
    PROCESS_EFFECTS.iter().find(|fx| fx.id == id)
}

pub fn process_effects_for_category(
    category: ProcessCategory,
) -> impl Iterator<Item = &'static ProcessEffectDefinition> {
    PROCESS_EFFECTS
        .iter()
        .filter(move |fx| fx.category == category)
}

pub fn kernel_source_by_name(kernel: &str) -> Option<&'static str> {
    match kernel {
        "transform_camera_affine.wgsl" => Some(include_str!(
            "kernels/transform_camera/transform_camera_affine.wgsl"
        )),
        "color_core.wgsl" => Some(include_str!("kernels/color_tone/color_core.wgsl")),
        "color_tone_exposure_contrast.wgsl" => Some(include_str!(
            "kernels/color_tone/color_tone_exposure_contrast.wgsl"
        )),
        "stylize_look_posterize.wgsl" => Some(include_str!(
            "kernels/stylize_look/stylize_look_posterize.wgsl"
        )),
        "blur_sharpen_detail_gaussian.wgsl" => Some(include_str!(
            "kernels/blur_sharpen_detail/blur_sharpen_detail_gaussian.wgsl"
        )),
        "keying_matte_mask_luma_key.wgsl" => Some(include_str!(
            "kernels/keying_matte_mask/keying_matte_mask_luma_key.wgsl"
        )),
        "light_atmosphere_bloom_prefilter.wgsl" => Some(include_str!(
            "kernels/light_atmosphere/light_atmosphere_bloom_prefilter.wgsl"
        )),
        "distortion_warp_heat_haze.wgsl" => Some(include_str!(
            "kernels/distortion_warp/distortion_warp_heat_haze.wgsl"
        )),
        "composite_core.wgsl" => Some(include_str!("kernels/composite/composite_core.wgsl")),
        "effect_for_testing_run.wgsl" => {
            Some(include_str!("kernels/testing/effect_for_testing_run.wgsl"))
        }
        "transition_core.wgsl" => Some(include_str!("kernels/transition/transition_core.wgsl")),
        _ => None,
    }
}

pub fn is_known_process_kernel(kernel: &str) -> bool {
    kernel_source_by_name(kernel).is_some()
}

#[cfg(test)]
mod tests {
    use std::collections::HashSet;

    use super::{
        PROCESS_CATEGORIES, PROCESS_EFFECTS, is_known_process_kernel, kernel_source_by_name,
        process_effect_for_id,
    };

    #[test]
    fn every_process_category_has_one_seed_effect() {
        let covered: HashSet<_> = PROCESS_EFFECTS.iter().map(|fx| fx.category).collect();
        for category in PROCESS_CATEGORIES {
            assert!(
                covered.contains(&category),
                "missing seed effect: {category:?}"
            );
        }
    }

    #[test]
    fn every_seed_kernel_has_embedded_source() {
        for fx in PROCESS_EFFECTS {
            let src = kernel_source_by_name(fx.kernel).expect("kernel source");
            assert!(
                src.contains("fn ml_"),
                "expected helper function signature in {}",
                fx.kernel
            );
            assert!(is_known_process_kernel(fx.kernel));
            assert_eq!(
                process_effect_for_id(fx.id).map(|it| it.kernel),
                Some(fx.kernel)
            );
        }
    }
}