mocari 0.1.2

A pure Rust Live2D/Cubism-compatible runtime
Documentation
use crate::core::{
    Vector2, WarpInterpolation, rotation_deformer_transform_point, warp_deformer_transform_target,
};

const ROTATION_DERIVATIVE_STEP: f32 = 0.1;

pub(super) fn parent_rotation_angle(
    composed: &[Option<ComposedDeformer>],
    parent_index: i32,
    origin_world: Vector2,
    translation: Vector2,
) -> Option<f32> {
    let probe_y = apply_composed_parent(
        composed,
        parent_index,
        Vector2::new(translation.x(), translation.y() + ROTATION_DERIVATIVE_STEP),
    )?;
    let dx = probe_y.x() - origin_world.x();
    let dy = probe_y.y() - origin_world.y();
    Some(wrap_angle(dy.atan2(dx) - std::f32::consts::FRAC_PI_2))
}

fn wrap_angle(mut angle: f32) -> f32 {
    use std::f32::consts::PI;
    const TWO_PI: f32 = 2.0 * PI;
    while angle < -PI {
        angle += TWO_PI;
    }
    while angle > PI {
        angle -= TWO_PI;
    }
    angle
}

#[derive(Debug, Clone, PartialEq)]
pub(super) enum ComposedDeformer {
    Warp(ComposedWarp),
    Rotation(ComposedRotation),
}

#[derive(Debug, Clone, PartialEq)]
pub(super) struct ComposedWarp {
    pub(super) grid: Vec<Vector2>,
    pub(super) cols: usize,
    pub(super) rows: usize,
    pub(super) scale_accum: f32,
    pub(super) opacity_accum: f32,
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub(super) struct ComposedRotation {
    pub(super) origin: Vector2,
    pub(super) angle_degrees: f32,
    pub(super) scale: f32,
    pub(super) flip_x: bool,
    pub(super) flip_y: bool,
    pub(super) scale_accum: f32,
    pub(super) opacity_accum: f32,
}

#[derive(Debug, Clone, PartialEq)]
pub struct ComposedDeformers {
    deformers: Vec<ComposedDeformer>,
}

impl ComposedDeformers {
    pub(super) fn new(deformers: Vec<ComposedDeformer>) -> Self {
        Self { deformers }
    }

    pub(super) fn transform_vertices(
        &self,
        parent_deformer_index: i32,
        vertices: &mut [Vector2],
    ) -> Option<()> {
        if parent_deformer_index < 0 {
            return Some(());
        }
        let index = usize::try_from(parent_deformer_index).ok()?;
        for vertex in vertices {
            *vertex = apply_one(self.deformers.get(index)?, *vertex)?;
        }
        Some(())
    }

    pub(super) fn deformer_opacity(&self, parent_deformer_index: i32) -> f32 {
        if parent_deformer_index < 0 {
            return 1.0;
        }
        let index = match usize::try_from(parent_deformer_index) {
            Ok(value) => value,
            Err(_) => return 1.0,
        };
        match self.deformers.get(index) {
            Some(ComposedDeformer::Warp(warp)) => warp.opacity_accum,
            Some(ComposedDeformer::Rotation(rotation)) => rotation.opacity_accum,
            None => 1.0,
        }
    }
}

pub(super) fn apply_one(deformer: &ComposedDeformer, point: Vector2) -> Option<Vector2> {
    match deformer {
        ComposedDeformer::Warp(warp) => warp_deformer_transform_target(
            point,
            &warp.grid,
            warp.cols,
            warp.rows,
            WarpInterpolation::Quad,
        ),
        ComposedDeformer::Rotation(rotation) => Some(rotation_deformer_transform_point(
            point,
            rotation.angle_degrees,
            rotation.scale,
            rotation.origin,
            rotation.flip_x,
            rotation.flip_y,
        )),
    }
}

pub(super) fn apply_composed_parent(
    composed: &[Option<ComposedDeformer>],
    parent_index: i32,
    point: Vector2,
) -> Option<Vector2> {
    if parent_index < 0 {
        return Some(point);
    }
    let index = usize::try_from(parent_index).ok()?;
    let parent = composed.get(index)?.as_ref()?;
    apply_one(parent, point)
}

pub(super) fn parent_scale_accum(composed: &[Option<ComposedDeformer>], parent_index: i32) -> f32 {
    if parent_index < 0 {
        return 1.0;
    }
    let index = match usize::try_from(parent_index) {
        Ok(value) => value,
        Err(_) => return 1.0,
    };
    match composed.get(index).and_then(|slot| slot.as_ref()) {
        Some(ComposedDeformer::Warp(warp)) => warp.scale_accum,
        Some(ComposedDeformer::Rotation(rotation)) => rotation.scale_accum,
        None => 1.0,
    }
}

pub(super) fn parent_opacity_accum(
    composed: &[Option<ComposedDeformer>],
    parent_index: i32,
) -> f32 {
    if parent_index < 0 {
        return 1.0;
    }
    let index = match usize::try_from(parent_index) {
        Ok(value) => value,
        Err(_) => return 1.0,
    };
    match composed.get(index).and_then(|slot| slot.as_ref()) {
        Some(ComposedDeformer::Warp(warp)) => warp.opacity_accum,
        Some(ComposedDeformer::Rotation(rotation)) => rotation.opacity_accum,
        None => 1.0,
    }
}