wavyte 0.2.1

Programmatic video composition and rendering engine in Rust (CPU backend, ffmpeg MP4 encoding)
Documentation
mod cpu {
    use std::collections::BTreeMap;

    use wavyte::{
        Anim, Asset, BackendKind, BlendMode, Canvas, Clip, ClipProps, Composition, FrameIndex,
        FrameRange, PathAsset, PreparedAssetStore, RenderSettings, Track, Transform2D,
        create_backend, render_frame,
    };

    fn mix64(mut z: u64) -> u64 {
        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
        z ^ (z >> 31)
    }

    fn digest_u64(bytes: &[u8]) -> u64 {
        let mut state = 0x9E37_79B9_7F4A_7C15u64;
        for chunk in bytes.chunks(8) {
            let mut v = 0u64;
            for (i, &b) in chunk.iter().enumerate() {
                v |= (b as u64) << (i * 8);
            }
            state = mix64(state ^ v);
        }
        state
    }

    fn store_for(comp: &Composition) -> PreparedAssetStore {
        PreparedAssetStore::prepare(comp, ".").unwrap()
    }

    fn simple_path_comp() -> Composition {
        let mut assets = BTreeMap::new();
        assets.insert(
            "p0".to_string(),
            Asset::Path(PathAsset {
                svg_path_d: "M10,10 L54,10 L54,54 L10,54 Z".to_string(),
            }),
        );

        Composition {
            fps: wavyte::Fps::new(30, 1).unwrap(),
            canvas: Canvas {
                width: 64,
                height: 64,
            },
            duration: FrameIndex(1),
            assets,
            tracks: vec![Track {
                name: "main".to_string(),
                z_base: 0,
                layout_mode: wavyte::LayoutMode::Absolute,
                layout_gap_px: 0.0,
                layout_padding: wavyte::Edges::default(),
                layout_align_x: wavyte::LayoutAlignX::Start,
                layout_align_y: wavyte::LayoutAlignY::Start,
                layout_grid_columns: 2,
                clips: vec![Clip {
                    id: "c0".to_string(),
                    asset: "p0".to_string(),
                    range: FrameRange::new(FrameIndex(0), FrameIndex(1)).unwrap(),
                    props: ClipProps {
                        transform: Anim::constant(Transform2D::default()),
                        opacity: Anim::constant(1.0),
                        blend: BlendMode::Normal,
                    },
                    z_offset: 0,
                    effects: vec![],
                    transition_in: None,
                    transition_out: None,
                }],
            }],
            seed: 1,
        }
    }

    fn two_layer_path_comp() -> Composition {
        let mut assets = BTreeMap::new();
        assets.insert(
            "p0".to_string(),
            Asset::Path(PathAsset {
                svg_path_d: "M0,0 L64,0 L64,64 L0,64 Z".to_string(),
            }),
        );
        assets.insert(
            "p1".to_string(),
            Asset::Path(PathAsset {
                svg_path_d: "M16,16 L48,16 L48,48 L16,48 Z".to_string(),
            }),
        );

        Composition {
            fps: wavyte::Fps::new(30, 1).unwrap(),
            canvas: Canvas {
                width: 64,
                height: 64,
            },
            duration: FrameIndex(1),
            assets,
            tracks: vec![
                Track {
                    name: "bg".to_string(),
                    z_base: 0,
                    layout_mode: wavyte::LayoutMode::Absolute,
                    layout_gap_px: 0.0,
                    layout_padding: wavyte::Edges::default(),
                    layout_align_x: wavyte::LayoutAlignX::Start,
                    layout_align_y: wavyte::LayoutAlignY::Start,
                    layout_grid_columns: 2,
                    clips: vec![Clip {
                        id: "c0".to_string(),
                        asset: "p0".to_string(),
                        range: FrameRange::new(FrameIndex(0), FrameIndex(1)).unwrap(),
                        props: ClipProps {
                            transform: Anim::constant(Transform2D::default()),
                            opacity: Anim::constant(1.0),
                            blend: BlendMode::Normal,
                        },
                        z_offset: 0,
                        effects: vec![],
                        transition_in: None,
                        transition_out: None,
                    }],
                },
                Track {
                    name: "fg".to_string(),
                    z_base: 1,
                    layout_mode: wavyte::LayoutMode::Absolute,
                    layout_gap_px: 0.0,
                    layout_padding: wavyte::Edges::default(),
                    layout_align_x: wavyte::LayoutAlignX::Start,
                    layout_align_y: wavyte::LayoutAlignY::Start,
                    layout_grid_columns: 2,
                    clips: vec![Clip {
                        id: "c1".to_string(),
                        asset: "p1".to_string(),
                        range: FrameRange::new(FrameIndex(0), FrameIndex(1)).unwrap(),
                        props: ClipProps {
                            transform: Anim::constant(Transform2D::default()),
                            opacity: Anim::constant(1.0),
                            blend: BlendMode::Normal,
                        },
                        z_offset: 0,
                        effects: vec![],
                        transition_in: None,
                        transition_out: None,
                    }],
                },
            ],
            seed: 1,
        }
    }

    #[test]
    fn cpu_render_is_deterministic_and_nonempty() {
        let comp = simple_path_comp();

        let settings = RenderSettings {
            clear_rgba: Some([0, 0, 0, 255]),
        };
        let mut backend = create_backend(BackendKind::Cpu, &settings).unwrap();
        let assets = store_for(&comp);

        let a = render_frame(&comp, FrameIndex(0), backend.as_mut(), &assets).unwrap();
        let b = render_frame(&comp, FrameIndex(0), backend.as_mut(), &assets).unwrap();

        assert_eq!(a.width, 64);
        assert_eq!(a.height, 64);
        assert!(a.premultiplied);
        assert_eq!(digest_u64(&a.data), digest_u64(&b.data));
        assert!(a.data.iter().any(|&x| x != 0));
    }

    #[test]
    fn cpu_render_two_layers_is_nonempty() {
        let comp = two_layer_path_comp();

        let settings = RenderSettings {
            clear_rgba: Some([0, 0, 0, 255]),
        };
        let mut backend = create_backend(BackendKind::Cpu, &settings).unwrap();
        let assets = store_for(&comp);

        let frame = render_frame(&comp, FrameIndex(0), backend.as_mut(), &assets).unwrap();
        assert_eq!(frame.width, 64);
        assert_eq!(frame.height, 64);
        assert!(frame.premultiplied);
        assert!(frame.data.iter().any(|&x| x != 0));
    }
}