motionloom 0.1.0

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

use super::Affine2;

#[derive(Debug, Clone)]
pub(crate) struct EvaluatedDeformGrid {
    pub(crate) cols: usize,
    pub(crate) rows: usize,
    pub(crate) from: Vec<Point2>,
    pub(crate) to: Vec<Point2>,
}

pub(crate) fn eval_group_deform_grid(
    group: &GroupNode,
    time_norm: f32,
    time_sec: f32,
) -> Result<Option<EvaluatedDeformGrid>, MotionLoomSceneRenderError> {
    let Some(size_raw) = group.deform_grid.as_deref() else {
        return Ok(None);
    };
    let size_raw = size_raw.trim();
    if size_raw.is_empty() || size_raw.eq_ignore_ascii_case("none") {
        return Ok(None);
    }

    let amount = eval_scene_number(&group.deform_amount, time_norm, time_sec)?.clamp(0.0, 1.0);
    if amount <= 0.0001 {
        return Ok(None);
    }

    let (cols, rows) = parse_deform_grid_size(size_raw)?;
    let expected = cols * rows;
    let grid_from_raw = group.grid_from.as_deref().ok_or_else(|| {
        invalid_deform_grid(size_raw, "deformGrid requires gridFrom=\"x,y ...\".")
    })?;
    let grid_to_raw = group
        .grid_to
        .as_deref()
        .ok_or_else(|| invalid_deform_grid(size_raw, "deformGrid requires gridTo=\"x,y ...\"."))?;
    let from = parse_deform_grid_points(grid_from_raw, cols, rows, "gridFrom")?;
    let target = parse_deform_grid_points(grid_to_raw, cols, rows, "gridTo")?;
    if from.len() != expected || target.len() != expected {
        return Err(invalid_deform_grid(
            size_raw,
            format!("expected {expected} control points."),
        ));
    }

    let to = from
        .iter()
        .zip(target.iter())
        .map(|(from, target)| from.lerp(*target, amount))
        .collect();

    Ok(Some(EvaluatedDeformGrid {
        cols,
        rows,
        from,
        to,
    }))
}

pub(crate) fn transform_deform_grid(
    grid: &EvaluatedDeformGrid,
    transform: Affine2,
) -> EvaluatedDeformGrid {
    EvaluatedDeformGrid {
        cols: grid.cols,
        rows: grid.rows,
        from: grid
            .from
            .iter()
            .map(|point| transform_point2(transform, *point))
            .collect(),
        to: grid
            .to
            .iter()
            .map(|point| transform_point2(transform, *point))
            .collect(),
    }
}

fn transform_point2(transform: Affine2, point: Point2) -> Point2 {
    let (x, y) = transform.transform_point(point.x, point.y);
    Point2::new(x, y)
}

pub(crate) fn transform_and_deform_point(
    transform: Affine2,
    point: Point2,
    deform: Option<&EvaluatedDeformGrid>,
) -> Point2 {
    let transformed = transform_point2(transform, point);
    deform
        .map(|grid| warp_point_with_deform_grid(transformed, grid))
        .unwrap_or(transformed)
}

pub(crate) fn transform_and_deform_subpaths(
    subpaths: &[Vec<Point2>],
    transform: Affine2,
    deform: &EvaluatedDeformGrid,
) -> Vec<Vec<Point2>> {
    subpaths
        .iter()
        .map(|subpath| {
            subpath
                .iter()
                .map(|point| transform_and_deform_point(transform, *point, Some(deform)))
                .collect()
        })
        .collect()
}

fn warp_point_with_deform_grid(point: Point2, grid: &EvaluatedDeformGrid) -> Point2 {
    for row in 0..grid.rows - 1 {
        for col in 0..grid.cols - 1 {
            let i00 = row * grid.cols + col;
            let i10 = i00 + 1;
            let i01 = (row + 1) * grid.cols + col;
            let i11 = i01 + 1;
            if let Some(warped) = warp_point_with_deform_triangle(
                point,
                [grid.from[i00], grid.from[i10], grid.from[i11]],
                [grid.to[i00], grid.to[i10], grid.to[i11]],
            ) {
                return warped;
            }
            if let Some(warped) = warp_point_with_deform_triangle(
                point,
                [grid.from[i00], grid.from[i11], grid.from[i01]],
                [grid.to[i00], grid.to[i11], grid.to[i01]],
            ) {
                return warped;
            }
        }
    }
    point
}

fn warp_point_with_deform_triangle(
    point: Point2,
    src: [Point2; 3],
    dst: [Point2; 3],
) -> Option<Point2> {
    let denom = triangle_barycentric_denominator(src);
    let (w0, w1, w2) = triangle_barycentric(point, src, denom)?;
    if w0 < -0.001 || w1 < -0.001 || w2 < -0.001 {
        return None;
    }
    Some(Point2::new(
        dst[0].x * w0 + dst[1].x * w1 + dst[2].x * w2,
        dst[0].y * w0 + dst[1].y * w1 + dst[2].y * w2,
    ))
}

fn parse_deform_grid_size(size: &str) -> Result<(usize, usize), MotionLoomSceneRenderError> {
    let normalized = size.trim().to_ascii_lowercase().replace(' ', "");
    let Some((cols_raw, rows_raw)) = normalized.split_once('x') else {
        return Err(invalid_deform_grid(
            size,
            "deformGrid must use the form \"colsxrows\", for example \"3x3\".",
        ));
    };
    let cols = cols_raw
        .parse::<usize>()
        .map_err(|_| invalid_deform_grid(size, format!("invalid column count: {cols_raw}")))?;
    let rows = rows_raw
        .parse::<usize>()
        .map_err(|_| invalid_deform_grid(size, format!("invalid row count: {rows_raw}")))?;
    if cols < 2 || rows < 2 || cols > 16 || rows > 16 {
        return Err(invalid_deform_grid(
            size,
            "deformGrid supports 2..16 columns and 2..16 rows.",
        ));
    }
    Ok((cols, rows))
}

fn parse_deform_grid_points(
    value: &str,
    cols: usize,
    rows: usize,
    label: &str,
) -> Result<Vec<Point2>, MotionLoomSceneRenderError> {
    let mut points = Vec::new();
    let row_chunks: Vec<&str> = if value.contains(';') {
        value.split(';').collect()
    } else {
        vec![value]
    };
    if row_chunks.len() != 1 && row_chunks.len() != rows {
        return Err(invalid_deform_grid(
            value,
            format!("{label} expected {rows} rows separated by ';'."),
        ));
    }

    for (row_index, row) in row_chunks.iter().enumerate() {
        let row_points = row
            .split_whitespace()
            .map(|raw| parse_deform_grid_point(raw, value))
            .collect::<Result<Vec<_>, _>>()?;
        if row_chunks.len() != 1 && row_points.len() != cols {
            return Err(invalid_deform_grid(
                value,
                format!(
                    "{label} row {} expected {cols} points, got {}.",
                    row_index + 1,
                    row_points.len()
                ),
            ));
        }
        points.extend(row_points);
    }

    let expected = cols * rows;
    if points.len() != expected {
        return Err(invalid_deform_grid(
            value,
            format!("{label} expected {expected} points, got {}.", points.len()),
        ));
    }
    Ok(points)
}

fn parse_deform_grid_point(raw: &str, source: &str) -> Result<Point2, MotionLoomSceneRenderError> {
    let Some((x_raw, y_raw)) = raw.split_once(',') else {
        return Err(invalid_deform_grid(
            source,
            format!("control point must be \"x,y\": {raw}"),
        ));
    };
    let x = x_raw
        .trim()
        .parse::<f32>()
        .map_err(|_| invalid_deform_grid(source, format!("invalid x value: {x_raw}")))?;
    let y = y_raw
        .trim()
        .parse::<f32>()
        .map_err(|_| invalid_deform_grid(source, format!("invalid y value: {y_raw}")))?;
    if !x.is_finite() || !y.is_finite() {
        return Err(invalid_deform_grid(
            source,
            format!("control point must be finite: {raw}"),
        ));
    }
    Ok(Point2::new(x, y))
}

pub(crate) fn triangle_barycentric_denominator(tri: [Point2; 3]) -> f32 {
    (tri[1].y - tri[2].y) * (tri[0].x - tri[2].x) + (tri[2].x - tri[1].x) * (tri[0].y - tri[2].y)
}

pub(crate) fn triangle_barycentric(
    point: Point2,
    tri: [Point2; 3],
    denom: f32,
) -> Option<(f32, f32, f32)> {
    if denom.abs() <= 0.00001 {
        return None;
    }
    let w0 = ((tri[1].y - tri[2].y) * (point.x - tri[2].x)
        + (tri[2].x - tri[1].x) * (point.y - tri[2].y))
        / denom;
    let w1 = ((tri[2].y - tri[0].y) * (point.x - tri[2].x)
        + (tri[0].x - tri[2].x) * (point.y - tri[2].y))
        / denom;
    let w2 = 1.0 - w0 - w1;
    Some((w0, w1, w2))
}

fn invalid_deform_grid(value: &str, message: impl Into<String>) -> MotionLoomSceneRenderError {
    MotionLoomSceneRenderError::InvalidDeformGrid {
        value: value.to_string(),
        message: message.into(),
    }
}