rasterlottie 0.2.1

Pure Rust, headless Lottie rasterizer for deterministic server-side rendering
Documentation
use std::{ptr, sync::Arc};

use tiny_skia::{Pixmap, Transform as PixmapTransform};

use super::{
    assets::ImageAssetStore,
    renderer::{LayerRenderContext, RenderTransform},
    values::decode_layer_transform,
};
use crate::{Animation, Asset, Layer, RasterlottieError, timeline::sample_scalar};

pub(super) fn lookup_precomp_asset<'a>(
    animation: &'a Animation,
    layer: &Layer,
) -> Option<&'a Asset> {
    let ref_id = layer.ref_id.as_deref()?;
    animation.assets.iter().find(|asset| asset.id == ref_id)
}

pub(super) fn lookup_image_asset<'a>(
    image_assets: &'a ImageAssetStore,
    animation: &'a Animation,
    layer: &Layer,
) -> Result<Option<Arc<Pixmap>>, RasterlottieError> {
    let Some(ref_id) = layer.ref_id.as_deref() else {
        return Ok(None);
    };
    image_assets.get(animation, ref_id)
}

pub(super) fn resolve_layer_transform_chain(
    context: &LayerRenderContext<'_>,
    layer: &Layer,
    frame: f32,
) -> RenderTransform {
    span_enter!(
        tracing::Level::TRACE,
        "resolve_layer_transform_chain",
        frame = frame,
        layer = layer.name.as_str()
    );
    let cache_key = (ptr::from_ref(layer) as usize, frame.to_bits());
    if let Some(transform) = context
        .frame_cache
        .layer_transforms
        .borrow()
        .get(&cache_key)
    {
        trace!("frame_transform_cache hit");
        return *transform;
    }

    if let Some(transform) = context
        .layer_hierarchy_cache
        .and_then(|cache| cache.for_layers(context.layers))
        .and_then(|cache| cache.static_transform(layer))
    {
        trace!("frame_transform_cache static-hit");
        return transform;
    }

    trace!("frame_transform_cache miss");
    let mut matrix = PixmapTransform::identity();
    let mut opacity = 1.0;
    if let Some(lineage) = context
        .layer_hierarchy_cache
        .and_then(|cache| cache.for_layers(context.layers))
        .and_then(|cache| cache.parent_lineage(context.layers, layer))
    {
        for node in lineage {
            let transform = decode_layer_transform(
                node.transform.as_ref(),
                frame,
                context.timeline_sample_cache,
            );
            matrix = matrix.pre_concat(transform.matrix);
            if ptr::eq(node, layer) {
                opacity = transform.opacity;
            }
        }
    } else {
        let mut lineage = Vec::new();
        let mut visited = Vec::new();
        let mut current = Some(layer);

        while let Some(node) = current {
            if let Some(index) = node.index
                && visited.contains(&index)
            {
                break;
            }

            if let Some(index) = node.index {
                visited.push(index);
            }
            lineage.push(node);
            current = node.parent.and_then(|parent| {
                context
                    .layers
                    .iter()
                    .find(|candidate| candidate.index == Some(parent))
            });
        }

        lineage.reverse();
        for node in lineage {
            let transform = decode_layer_transform(
                node.transform.as_ref(),
                frame,
                context.timeline_sample_cache,
            );
            matrix = matrix.pre_concat(transform.matrix);
            if ptr::eq(node, layer) {
                opacity = transform.opacity;
            }
        }
    }

    let transform = RenderTransform { matrix, opacity };
    context
        .frame_cache
        .layer_transforms
        .borrow_mut()
        .insert(cache_key, transform);
    transform
}

pub(super) fn layer_is_visible(layer: &Layer, frame: f32) -> bool {
    !layer.hidden && frame_in_range(frame, layer.in_point, layer.out_point)
}

pub(super) fn frame_in_range(frame: f32, in_point: Option<f32>, out_point: Option<f32>) -> bool {
    if let Some(in_point) = in_point
        && frame < in_point
    {
        return false;
    }

    if let Some(out_point) = out_point
        && frame >= out_point
    {
        return false;
    }

    true
}

pub(super) fn map_layer_frame(animation: &Animation, frame: f32, layer: &Layer) -> f32 {
    let stretch = if layer.stretch > f32::EPSILON {
        layer.stretch
    } else {
        1.0
    };

    let layer_time = frame / stretch - layer.start_time;
    let Some(time_remap) = layer.time_remap.as_ref() else {
        return layer_time;
    };

    let remapped_seconds = sample_scalar(time_remap, layer_time).unwrap_or_else(|| {
        if animation.frame_rate <= f32::EPSILON {
            0.0
        } else {
            layer_time / animation.frame_rate
        }
    });

    remapped_seconds * animation.frame_rate.max(1.0)
}