diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21cfdc4..96ff329 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ env: # This should be limited to packages that are intended for publishing. RUST_MIN_VER_PKGS: "-p interpoli" # List of features that depend on the standard library and will be excluded from no_std checks. - FEATURES_DEPENDING_ON_STD: "std,default" + FEATURES_DEPENDING_ON_STD: "std,default,vello" # Rationale diff --git a/Cargo.lock b/Cargo.lock index cc69262..61bd227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,136 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bytemuck" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", +] + +[[package]] +name = "font-types" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34fd7136aca682873d859ef34494ab1a7d3f57ecd485ed40eb6437ee8c85aa29" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "interpoli" version = "0.1.0" @@ -21,6 +151,7 @@ dependencies = [ "keyframe", "kurbo", "peniko", + "vello", ] [[package]] @@ -45,18 +176,60 @@ dependencies = [ "smallvec", ] +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + [[package]] name = "mint" version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" +[[package]] +name = "naga" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e536ae46fcab0876853bd4a632ede5df4b1c2527a58f6c5a4150fe86be858231" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "num-traits", + "rustc-hash", + "termcolor", + "thiserror", + "unicode-xid", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -67,6 +240,29 @@ dependencies = [ "libm", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "peniko" version = "0.1.1" @@ -77,8 +273,268 @@ dependencies = [ "smallvec", ] +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "read-fonts" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b8af39d1f23869711ad4cea5e7835a20daa987f80232f7f2a2374d648ca64d" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "redox_syscall" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "skrifa" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab45fb68b53576a43d4fc0e9ec8ea64e29a4d2cc7f44506964cb75f288222e9" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "svg_fmt" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20e16a0f46cf5fd675563ef54f26e83e20f2366bcf027bcb3cc3ed2b98aaf2ca" + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unicode-xid" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" + +[[package]] +name = "vello" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "861c12258ed7e72762765e2c88a07bb528040ec4e5f87514d65b19b29a7cccf0" +dependencies = [ + "bytemuck", + "futures-intrusive", + "log", + "peniko", + "raw-window-handle", + "skrifa", + "static_assertions", + "thiserror", + "vello_encoding", + "vello_shaders", +] + +[[package]] +name = "vello_encoding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d73777327877fa824a45c7195f850390dd3f91feb15f47d331db1fc01abf6d" +dependencies = [ + "bytemuck", + "guillotiere", + "peniko", + "skrifa", + "smallvec", +] + +[[package]] +name = "vello_shaders" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13ab6bcb2b079c3cf57e964d1ba0b1f08901284be1c7f5cba34d3e0e08154bce" +dependencies = [ + "bytemuck", + "naga", + "thiserror", + "vello_encoding", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index 23d7d29..bc283a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ default = ["std"] std = ["kurbo/std", "peniko/std"] libm = ["kurbo/libm", "peniko/libm"] mint = ["keyframe/mint_types", "kurbo/mint"] +vello = ["dep:vello"] [package.metadata.docs.rs] all-features = true @@ -28,6 +29,7 @@ targets = [] keyframe = { version = "1.1.1", default-features = false } kurbo = { version = "0.11", default-features = false } peniko = { version = "0.1.1", default-features = false } +vello = { version = "0.2.0", default-features = false, optional = true } [lints] rust.unsafe_code = "forbid" diff --git a/src/composition.rs b/src/composition.rs new file mode 100644 index 0000000..95a318b --- /dev/null +++ b/src/composition.rs @@ -0,0 +1,157 @@ +// Copyright 2024 the Interpoli Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use alloc::{string::String, vec::Vec}; +use core::ops::Range; +use kurbo::{PathEl, Shape as _}; + +#[cfg(feature = "std")] +use std::collections::HashMap; + +use crate::{animated, Brush, Repeater, Stroke, Transform, Value}; + +/// Model of a Lottie file. +#[derive(Clone, Default, Debug)] +pub struct Composition { + /// Frames in which the animation is active. + pub frames: Range, + /// Frames per second. + pub frame_rate: f64, + /// Width of the animation. + pub width: usize, + /// Height of the animation. + pub height: usize, + /// Precomposed layers that may be instanced. + #[cfg(feature = "std")] + pub assets: HashMap>, + /// Collection of layers. + pub layers: Vec, +} + +#[derive(Clone, Debug)] +pub enum Geometry { + Fixed(Vec), + Rect(animated::Rect), + Ellipse(animated::Ellipse), + Spline(animated::Spline), +} + +impl Geometry { + pub fn evaluate(&self, frame: f64, path: &mut Vec) { + match self { + Self::Fixed(value) => { + path.extend_from_slice(value); + } + Self::Rect(value) => { + path.extend(value.evaluate(frame).path_elements(0.1)); + } + Self::Ellipse(value) => { + path.extend(value.evaluate(frame).path_elements(0.1)); + } + Self::Spline(value) => { + value.evaluate(frame, path); + } + } + } +} + +#[derive(Clone, Debug)] +pub struct Draw { + /// Parameters for a stroked draw operation. + pub stroke: Option, + /// Brush for the draw operation. + pub brush: Brush, + /// Opacity of the draw operation. + pub opacity: Value, +} + +/// Elements of a shape layer. +#[derive(Clone, Debug)] +pub enum Shape { + /// Group of shapes with an optional transform. + Group(Vec, Option), + /// Geometry element. + Geometry(Geometry), + /// Fill or stroke element. + Draw(Draw), + /// Repeater element. + Repeater(Repeater), +} + +/// Transform and opacity for a shape group. +#[derive(Clone, Debug)] +pub struct GroupTransform { + pub transform: Transform, + pub opacity: Value, +} + +/// Layer in an animation. +#[derive(Clone, Debug, Default)] +pub struct Layer { + /// Name of the layer. + pub name: String, + /// Index of the transform parent layer. + pub parent: Option, + /// Transform for the entire layer. + pub transform: Transform, + /// Opacity for the entire layer. + pub opacity: Value, + /// Width of the layer. + pub width: f64, + /// Height of the layer. + pub height: f64, + /// Blend mode for the layer. + pub blend_mode: Option, + /// Range of frames in which the layer is active. + pub frames: Range, + /// Frame time stretch factor. + pub stretch: f64, + /// Starting frame for the layer (only applied to instances). + pub start_frame: f64, + /// List of masks applied to the content. + pub masks: Vec, + /// True if the layer is used as a mask. + pub is_mask: bool, + /// Mask blend mode and layer. + pub mask_layer: Option<(peniko::BlendMode, usize)>, + /// Content of the layer. + pub content: Content, +} + +/// Matte layer mode. +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +pub enum Matte { + #[default] + Normal, + // TODO: Use these + // Alpha, + // InvertAlpha, + // Luma, + // InvertLuma, +} + +/// Mask for a layer. +#[derive(Clone, Debug)] +pub struct Mask { + /// Blend mode for the mask. + pub mode: peniko::BlendMode, + /// Geometry that defines the shape of the mask. + pub geometry: Geometry, + /// Opacity of the mask. + pub opacity: Value, +} + +/// Content of a layer. +#[derive(Clone, Default, Debug)] +pub enum Content { + /// Empty layer. + #[default] + None, + /// Asset instance with the specified name and time remapping. + Instance { + name: String, + time_remap: Option>, + }, + /// Collection of shapes. + Shape(Vec), +} diff --git a/src/lib.rs b/src/lib.rs index 3b35094..eaaa6d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,17 +8,26 @@ extern crate alloc; -use alloc::vec::Vec; -use kurbo::{Affine, PathEl, Shape as _}; +use kurbo::Affine; +mod composition; mod spline; mod value; +#[cfg(feature = "vello")] +mod render; + pub mod animated; pub mod fixed; +pub use composition::{ + Composition, Content, Draw, Geometry, GroupTransform, Layer, Mask, Matte, Shape, +}; pub use value::{Animated, Easing, EasingHandle, Time, Tween, Value, ValueRef}; +#[cfg(feature = "vello")] +pub use render::Renderer; + macro_rules! simple_value { ($name:ident) => { #[allow(clippy::large_enum_variant)] @@ -77,30 +86,3 @@ impl Default for Transform { Self::Fixed(Affine::IDENTITY) } } - -#[derive(Clone, Debug)] -pub enum Geometry { - Fixed(Vec), - Rect(animated::Rect), - Ellipse(animated::Ellipse), - Spline(animated::Spline), -} - -impl Geometry { - pub fn evaluate(&self, frame: f64, path: &mut Vec) { - match self { - Self::Fixed(value) => { - path.extend_from_slice(value); - } - Self::Rect(value) => { - path.extend(value.evaluate(frame).path_elements(0.1)); - } - Self::Ellipse(value) => { - path.extend(value.evaluate(frame).path_elements(0.1)); - } - Self::Spline(value) => { - value.evaluate(frame, path); - } - } - } -} diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..6ef784c --- /dev/null +++ b/src/render.rs @@ -0,0 +1,381 @@ +// Copyright 2024 the Interpoli Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +#![allow(clippy::shadow_unrelated)] + +use crate::{fixed, Composition, Content, Draw, Geometry, GroupTransform, Layer, Shape}; +use kurbo::{Affine, PathEl, Rect}; +use peniko::{Fill, Mix}; +use std::ops::Range; + +/// Renders a composition into a scene. +#[allow(missing_debug_implementations)] +#[derive(Default)] +pub struct Renderer { + batch: Batch, + mask_elements: Vec, +} + +impl Renderer { + /// Creates a new renderer. + pub fn new() -> Self { + Self::default() + } + + /// Renders the animation at a given frame to a new scene. + pub fn render( + &mut self, + animation: &Composition, + frame: f64, + transform: Affine, + alpha: f64, + ) -> vello::Scene { + let mut scene = vello::Scene::new(); + self.append(animation, frame, transform, alpha, &mut scene); + scene + } + + /// Renders and appends the animation at a given frame to the provided scene. + pub fn append( + &mut self, + animation: &Composition, + frame: f64, + transform: Affine, + alpha: f64, + scene: &mut vello::Scene, + ) { + self.batch.clear(); + scene.push_layer( + Mix::Clip, + 1.0, + transform, + &Rect::new(0.0, 0.0, animation.width as _, animation.height as _), + ); + for layer in animation.layers.iter().rev() { + if layer.is_mask { + continue; + } + self.render_layer( + animation, + &animation.layers, + layer, + transform, + alpha, + frame, + scene, + ); + } + scene.pop_layer(); + } + + #[allow(clippy::too_many_arguments)] + fn render_layer( + &mut self, + animation: &Composition, + layer_set: &[Layer], + layer: &Layer, + transform: Affine, + alpha: f64, + frame: f64, + scene: &mut vello::Scene, + ) { + if !layer.frames.contains(&frame) { + return; + } + let parent_transform = transform; + let transform = self.compute_transform(layer_set, layer, parent_transform, frame); + let full_rect = Rect::new(0.0, 0.0, animation.width as f64, animation.height as f64); + if let Some((mode, mask_index)) = layer.mask_layer { + // todo: re-enable masking when it is more understood (and/or if + // it's currently supported in vello?) Extra layer to + // isolate blending for the mask + scene.push_layer(Mix::Normal, 1.0, parent_transform, &full_rect); + if let Some(mask) = layer_set.get(mask_index) { + self.render_layer( + animation, + layer_set, + mask, + parent_transform, + alpha, + frame, + scene, + ); + } + scene.push_layer(mode, 1.0, parent_transform, &full_rect); + } + let alpha = alpha * layer.opacity.evaluate(frame) / 100.0; + for mask in &layer.masks { + let alpha = mask.opacity.evaluate(frame) / 100.0; + mask.geometry.evaluate(frame, &mut self.mask_elements); + scene.push_layer( + Mix::Clip, + alpha as f32, + transform, + &self.mask_elements.as_slice(), + ); + self.mask_elements.clear(); + } + match &layer.content { + Content::None => {} + Content::Instance { + name, + time_remap: _, + } => { + // TODO: Use time_remap + // let frame = time_remap + // .as_ref() + // .map(|tm| tm.evaluate(frame)) + // .unwrap_or(frame); + if let Some(asset_layers) = animation.assets.get(name) { + let frame = frame / layer.stretch; + let frame_delta = -layer.start_frame / layer.stretch; + for asset_layer in asset_layers.iter().rev() { + if asset_layer.is_mask { + continue; + } + self.render_layer( + animation, + asset_layers, + asset_layer, + transform, + alpha, + frame + frame_delta, + scene, + ); + } + } + } + Content::Shape(shapes) => { + self.render_shapes(shapes, transform, alpha, frame); + self.batch.render(scene); + self.batch.clear(); + } + } + for _ in 0..layer.masks.len() + (layer.mask_layer.is_some() as usize * 2) { + scene.pop_layer(); + } + } + + fn render_shapes(&mut self, shapes: &[Shape], transform: Affine, alpha: f64, frame: f64) { + // Keep track of our local top of the geometry stack. Any subsequent + // draws are bounded by this. + let geometry_start = self.batch.geometries.len(); + // Also keep track of top of draw stack for repeater evaluation. + let draw_start = self.batch.draws.len(); + // Top to bottom, collect geometries and draws. + for shape in shapes { + match shape { + Shape::Group(shapes, group_transform) => { + let (group_transform, group_alpha) = + if let Some(GroupTransform { transform, opacity }) = group_transform { + ( + transform.evaluate(frame).into_owned(), + opacity.evaluate(frame) / 100.0, + ) + } else { + (Affine::IDENTITY, 1.0) + }; + self.render_shapes( + shapes, + transform * group_transform, + alpha * group_alpha, + frame, + ); + } + Shape::Geometry(geometry) => { + self.batch.push_geometry(geometry, transform, frame); + } + Shape::Draw(draw) => { + self.batch.push_draw(draw, alpha, geometry_start, frame); + } + Shape::Repeater(repeater) => { + let repeater = repeater.evaluate(frame); + self.batch + .repeat(repeater.as_ref(), geometry_start, draw_start); + } + } + } + } + + /// Computes the transform for a single layer. This currently chases the + /// full transform chain each time. If it becomes a bottleneck, we can + /// implement caching. + fn compute_transform( + &self, + layer_set: &[Layer], + layer: &Layer, + global_transform: Affine, + frame: f64, + ) -> Affine { + let mut transform = layer.transform.evaluate(frame).into_owned(); + let mut parent_index = layer.parent; + let mut count = 0_usize; + while let Some(index) = parent_index { + // We don't check for cycles at import time, so this heuristic + // prevents infinite loops. + if count >= layer_set.len() { + break; + } + if let Some(parent) = layer_set.get(index) { + parent_index = parent.parent; + transform = parent.transform.evaluate(frame).into_owned() * transform; + count += 1; + } else { + break; + } + } + global_transform * transform + } +} + +#[derive(Clone, Debug)] +struct DrawData { + stroke: Option, + brush: fixed::Brush, + alpha: f64, + /// Range into `ShapeBatch::geometries` + geometry: Range, +} + +impl DrawData { + fn new(draw: &Draw, alpha: f64, geometry: Range, frame: f64) -> Self { + Self { + stroke: draw + .stroke + .as_ref() + .map(|stroke| stroke.evaluate(frame).into_owned()), + brush: draw.brush.evaluate(1.0, frame).into_owned(), + alpha: alpha * draw.opacity.evaluate(frame) / 100.0, + geometry, + } + } +} + +#[derive(Clone, Debug)] +struct GeometryData { + /// Range into `ShapeBatch::elements` + elements: Range, + transform: Affine, +} + +#[derive(Default)] +struct Batch { + elements: Vec, + geometries: Vec, + draws: Vec, + repeat_geometries: Vec, + repeat_draws: Vec, + /// Length of geometries at time of most recent draw. This is + /// used to prevent merging into already used geometries. + drawn_geometry: usize, +} + +impl Batch { + fn push_geometry(&mut self, geometry: &Geometry, transform: Affine, frame: f64) { + // Merge with the previous geometry if possible. There are two + // conditions: + // 1. The previous geometry has not yet been referenced by a draw + // 2. The geometries have the same transform + if self.drawn_geometry < self.geometries.len() + && self.geometries.last().map(|last| last.transform) == Some(transform) + { + geometry.evaluate(frame, &mut self.elements); + self.geometries.last_mut().unwrap().elements.end = self.elements.len(); + } else { + let start = self.elements.len(); + geometry.evaluate(frame, &mut self.elements); + let end = self.elements.len(); + self.geometries.push(GeometryData { + elements: start..end, + transform, + }); + } + } + + fn push_draw(&mut self, draw: &Draw, alpha: f64, geometry_start: usize, frame: f64) { + self.draws.push(DrawData::new( + draw, + alpha, + geometry_start..self.geometries.len(), + frame, + )); + self.drawn_geometry = self.geometries.len(); + } + + fn repeat(&mut self, repeater: &fixed::Repeater, geometry_start: usize, draw_start: usize) { + // First move the relevant ranges of geometries and draws into side + // buffers + self.repeat_geometries + .extend(self.geometries.drain(geometry_start..)); + self.repeat_draws.extend(self.draws.drain(draw_start..)); + // Next, repeat the geometries and apply the offset transform + for geometry in self.repeat_geometries.iter() { + for i in 0..repeater.copies { + let transform = repeater.transform(i); + let mut geometry = geometry.clone(); + geometry.transform *= transform; + self.geometries.push(geometry); + } + } + // Finally, repeat the draws, taking into account opacity and the + // modified newly repeated geometry ranges + let start_alpha = repeater.start_opacity / 100.0; + let end_alpha = repeater.end_opacity / 100.0; + let delta_alpha = if repeater.copies > 1 { + // See note in Skottie: AE does not cover the full opacity range + (end_alpha - start_alpha) / repeater.copies as f64 + } else { + 0.0 + }; + for i in 0..repeater.copies { + let alpha = start_alpha + delta_alpha * i as f64; + if alpha <= 0.0 { + continue; + } + for mut draw in self.repeat_draws.iter().cloned() { + draw.alpha *= alpha; + let count = draw.geometry.end - draw.geometry.start; + draw.geometry.start = + geometry_start + (draw.geometry.start - geometry_start) * repeater.copies; + draw.geometry.end = draw.geometry.start + count * repeater.copies; + self.draws.push(draw); + } + } + // Clear the side buffers + self.repeat_geometries.clear(); + self.repeat_draws.clear(); + // Prevent merging until new geometries are pushed + self.drawn_geometry = self.geometries.len(); + } + + fn render(&self, scene: &mut vello::Scene) { + // Process all draws in reverse + for draw in self.draws.iter().rev() { + // Some nastiness to avoid cloning the brush if unnecessary + let modified_brush = if draw.alpha != 1.0 { + Some(fixed::brush_with_alpha(&draw.brush, draw.alpha)) + } else { + None + }; + let brush = modified_brush.as_ref().unwrap_or(&draw.brush); + for geometry in self.geometries[draw.geometry.clone()].iter() { + let path = &self.elements[geometry.elements.clone()]; + let transform = geometry.transform; + if let Some(stroke) = draw.stroke.as_ref() { + scene.stroke(stroke, transform, brush, None, &path); + } else { + scene.fill(Fill::NonZero, transform, brush, None, &path); + } + } + } + } + + fn clear(&mut self) { + self.elements.clear(); + self.geometries.clear(); + self.draws.clear(); + self.repeat_geometries.clear(); + self.repeat_draws.clear(); + self.drawn_geometry = 0; + } +}