motionloom 0.1.0

MotionLoom DSL parser and renderer for video effects, scene graphs, motion graphics, and world graphs.
Documentation
use crate::scene::model::{PrecomposeNode, SceneLayerNode, SceneSequenceNode};
use crate::scene::render::{MotionLoomSceneRenderError, eval_scene_number};

pub(crate) fn scene_sequence_local_time(
    sequence: &SceneSequenceNode,
    start_override_ms: Option<i64>,
    scene_time_sec: f32,
) -> Option<(f32, f32)> {
    let base_ms = start_override_ms.unwrap_or(0);
    let start_ms = base_ms + sequence.from_ms as i64;
    let duration_ms = sequence.duration_ms.max(1) as i64;
    let now_ms = (scene_time_sec.max(0.0) * 1000.0).round() as i64;
    if now_ms < start_ms {
        return None;
    }
    let elapsed_ms = now_ms - start_ms;
    if elapsed_ms > duration_ms {
        if sequence.out.trim().eq_ignore_ascii_case("hold") {
            return Some((1.0, duration_ms as f32 / 1000.0));
        }
        return None;
    }
    let local_norm = (elapsed_ms as f32 / duration_ms as f32).clamp(0.0, 1.0);
    Some((local_norm, elapsed_ms as f32 / 1000.0))
}

pub(crate) fn scene_layer_source_time(
    layer: &SceneLayerNode,
    precompose: &PrecomposeNode,
    parent_time_norm: f32,
    parent_time_sec: f32,
) -> Result<Option<(f32, f32)>, MotionLoomSceneRenderError> {
    let playback_rate = eval_scene_number(&layer.playback_rate, parent_time_norm, parent_time_sec)?;
    let source_mode = layer.source_time.trim().to_ascii_lowercase();
    let base_sec = match source_mode.as_str() {
        "" | "local" | "relative" | "parent" | "global" | "scene" | "absolute" => parent_time_sec,
        _ => parent_time_sec,
    };
    let mut source_sec = (base_sec - layer.time_offset_ms as f32 / 1000.0) * playback_rate;
    let out = layer.out.trim().to_ascii_lowercase().replace('_', "-");

    if let Some(duration_ms) = precompose.duration_ms {
        let duration_sec = (duration_ms as f32 / 1000.0).max(0.000_001);
        if source_sec < 0.0 || source_sec > duration_sec {
            match out.as_str() {
                "loop" | "repeat" => {
                    source_sec = source_sec.rem_euclid(duration_sec);
                }
                "none" | "hidden" | "transparent" => return Ok(None),
                _ => {
                    source_sec = source_sec.clamp(0.0, duration_sec);
                }
            }
        }
        return Ok(Some((
            (source_sec / duration_sec).clamp(0.0, 1.0),
            source_sec,
        )));
    }

    if source_sec < 0.0 && matches!(out.as_str(), "none" | "hidden" | "transparent") {
        return Ok(None);
    }
    Ok(Some((parent_time_norm, source_sec.max(0.0))))
}