From def4ff0dd9a4df488ca202ef7544ef9c24eb1c3c Mon Sep 17 00:00:00 2001 From: "Spencer C. Imbleau" Date: Sat, 9 Mar 2024 16:52:37 -0500 Subject: [PATCH 01/43] fix: licenses --- LICENSE-APACHE | 26 -------------------------- LICENSE-MIT | 38 ++++++++++++++++---------------------- 2 files changed, 16 insertions(+), 48 deletions(-) diff --git a/LICENSE-APACHE b/LICENSE-APACHE index d645695..d9a10c0 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -1,4 +1,3 @@ - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -175,28 +174,3 @@ of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index 3530c97..9cf1062 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,25 +1,19 @@ -Copyright (c) 2020 Raph Levien +MIT License -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From b8c83a09b93bfe36a34464c5eda16ef96ef3946a Mon Sep 17 00:00:00 2001 From: "Spencer C. Imbleau" Date: Sat, 9 Mar 2024 16:53:05 -0500 Subject: [PATCH 02/43] docs: update README --- README.md | 88 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 6b41fbc..4328d6d 100644 --- a/README.md +++ b/README.md @@ -2,66 +2,72 @@ # Velato -**An experimental [Lottie](https://airbnb.io/lottie) animation renderer built for [Vello](https://vello.dev)** +**An integration to parse and render [Lottie](https://airbnb.io/lottie) with [Vello](https://vello.dev).** -[![Xi Zulip](https://img.shields.io/badge/Xi%20Zulip-%23gpu-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/stream/197075-gpu) +[![Linebender Zulip](https://img.shields.io/badge/Linebender-%23gpu-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/stream/197075-gpu) [![dependency status](https://deps.rs/repo/github/linebender/velato/status.svg)](https://deps.rs/repo/github/linebender/velato) [![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](#license) -[![wgpu version](https://img.shields.io/badge/wgpu-v0.15-orange.svg)](https://crates.io/crates/wgpu) -[![vello version](https://img.shields.io/badge/vello-v0.0.1-purple.svg)](https://vello.dev) - - - +[![wgpu version](https://img.shields.io/badge/wgpu-v0.19-orange.svg)](https://crates.io/crates/wgpu) +[![vello version](https://img.shields.io/badge/vello-v0.1.0-purple.svg)](https://crates.io/crates/vello) + +[![Crates.io](https://img.shields.io/crates/v/velato.svg)](https://crates.io/crates/velato) +[![Docs](https://docs.rs/velato/badge.svg)](https://docs.rs/velato) +[![Build status](https://github.com/linebender/velato/workflows/CI/badge.svg)](https://github.com/linebender/velato/actions) +> [!WARNING] +> The goal of this crate is to provide coverage of the large Lottie spec, up to what vello will support, for use in interactive graphics. We are working towards correctness, but there are several missing features. See [vello](https://github.com/linebender/vello) for more information about limitations. + +## Missing features + It is not currently feature complete, but is capable of rendering a large number of Lottie animations. +- Non-linear easings +- Position keyframe (`ti`, `to`) easing +- Time remapping (`tm`) +- Text +- Image embedding +- Advanced shapes (stroke dash, zig-zag, etc.) +- Advanced effects (motion blur, drop shadows, etc.) +- Correct color stop handling +- Split rotations +- Split positions + ## Examples -To run an example, use: -``` -cargo run -p with_winit -- -``` +### Native - -Note that at the moment, we do not provide any example animations. - +```shell +cargo run -p with_winit +``` -## Platforms +You can also load an entire folder or individual files. -The current example does not support running on WASM or Android, however Velato should do so. +```shell +cargo run -p with_winit -- examples/assets +``` -## Example usage +### Web -```rust -let lottie_data = std::fs::read("path/to/lottie.json").unwrap(); -let composition = velato::Composition::from_bytes(&lottie_data).unwrap(); -let mut renderer = velato::Renderer::new(); -let scene = vello::Scene::new(); -let mut builder = vello::SceneBuilder::for_scene(&mut scene); -let time_secs = 1.0; -let transform = kurbo::Affine::IDENTITY; -let alpha = 1.0; -renderer.render(&composition, time_secs, transform, alpha, &mut builder); -``` +Because Vello relies heavily on compute shaders, we rely on the emerging WebGPU standard to run on the web. +Until browser support becomes widespread, it will probably be necessary to use development browser versions (e.g. Chrome Canary) and explicitly enable WebGPU. -## License +This uses [`cargo-run-wasm`](https://github.com/rukai/cargo-run-wasm) to build the example for web, and host a local server for it -Licensed under either of +```shell +# Make sure the Rust toolchain supports the wasm32 target +rustup target add wasm32-unknown-unknown -- Apache License, Version 2.0 - ([LICENSE-APACHE](LICENSE-APACHE) or ) -- MIT license - ([LICENSE-MIT](LICENSE-MIT) or ) +# The binary name must also be explicitly provided as it differs from the package name +cargo run_wasm -p with_winit --bin with_winit_bin +``` -at your option. -## Contribution +There is also a web demo [available here](https://linebender.github.io/velato) on supporting web browsers. -Contributions are welcome by pull request. The [Rust code of conduct] applies. +> [!WARNING] +> The web is not currently a primary target for Vello, and WebGPU implementations are incomplete, so you might run into issues running this example. -Unless you explicitly state otherwise, any contribution intentionally submitted -for inclusion in the work by you, as defined in the Apache-2.0 license, shall be -dual licensed as above, without any additional terms or conditions. +## License -[rust code of conduct]: https://www.rust-lang.org/policies/code-of-conduct +This project is dual-licensed under both [Apache 2.0](LICENSE-APACHE) and [MIT](LICENSE-MIT) licenses. From 1375b22a6b5f5e4f73bfda19dc80cb4469c75679 Mon Sep 17 00:00:00 2001 From: "Spencer C. Imbleau" Date: Sat, 9 Mar 2024 20:07:01 -0500 Subject: [PATCH 03/43] docs: authors --- AUTHORS | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 4da9dda..b45c485 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,7 +1,20 @@ -# This is the list of Velato's significant contributors. +# This is the list of Vello's significant contributors. # # This does not necessarily list everyone who has contributed code, # especially since many employees of one corporation may be contributing. # To see the full list of contributors, see the revision history in # source control. Google LLC +Raph Levien +Chad Brokaw +Arman Uguray +Elias Naur +Daniel McNab +Spencer C. Imbleau +Bruce Mitchener +Tatsuyuki Ishi +Markus Siglreithmaier +Rose Hudson +Brian Merchant +Matt Rice +Kaur Kuut \ No newline at end of file From ce3c00733f3c4cf75aa0c562c0141cb5767bf9ef Mon Sep 17 00:00:00 2001 From: "Spencer C. Imbleau" Date: Sat, 9 Mar 2024 20:07:39 -0500 Subject: [PATCH 04/43] ci: initial CI --- .github/copyright.sh | 24 ++++ .github/workflows/cargo-release.yml | 21 ++++ .github/workflows/ci.yml | 172 ++++++++++++++++++++++++++++ .github/workflows/pages-release.yml | 69 +++++++++++ 4 files changed, 286 insertions(+) create mode 100755 .github/copyright.sh create mode 100644 .github/workflows/cargo-release.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pages-release.yml diff --git a/.github/copyright.sh b/.github/copyright.sh new file mode 100755 index 0000000..383eb2b --- /dev/null +++ b/.github/copyright.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# If there are new files with headers that can't match the conditions here, +# then the files can be ignored by an additional glob argument via the -g flag. +# For example: +# -g "!src/special_file.rs" +# -g "!src/special_directory" + +# Check all the standard Rust source files +output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Vello Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0 OR MIT$\n\n" --files-without-match --multiline -g "*.rs" -g "!src/geom.rs" .) + +if [ -n "$output" ]; then + echo -e "The following files lack the correct copyright header:\n" + echo $output + echo -e "\n\nPlease add the following header:\n" + echo "// Copyright $(date +%Y) the Vello Authors" + echo "// SPDX-License-Identifier: Apache-2.0 OR MIT" + echo -e "\n... rest of the file ...\n" + exit 1 +fi + +echo "All files have correct copyright headers." +exit 0 + diff --git a/.github/workflows/cargo-release.yml b/.github/workflows/cargo-release.yml new file mode 100644 index 0000000..0d70ddc --- /dev/null +++ b/.github/workflows/cargo-release.yml @@ -0,0 +1,21 @@ +name: Cargo Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Setup | Checkout + uses: actions/checkout@v4 + + - name: Setup | Toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cargo | Publish + run: | + cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..102cf58 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,172 @@ +env: + # We aim to always test with the latest stable Rust toolchain, however we pin to a specific + # version like 1.70. Note that we only specify MAJOR.MINOR and not PATCH so that bugfixes still + # come automatically. If the version specified here is no longer the latest stable version, + # then please feel free to submit a PR that adjusts it along with the potential clippy fixes. + RUST_STABLE_VER: "1.76" # In quotes because otherwise (e.g.) 1.70 would be interpreted as 1.7 + + +# Rationale +# +# We don't run clippy with --all-targets because then even --lib and --bins are compiled with +# dev dependencies enabled, which does not match how they would be compiled by users. +# A dev dependency might enable a feature of a regular dependency that we need, but testing +# with --all-targets would not catch that. Thus we split --lib & --bins into a separate step. + +name: CI + +on: + pull_request: + merge_group: + +jobs: + rustfmt: + runs-on: ubuntu-latest + name: cargo fmt + steps: + - uses: actions/checkout@v4 + + - name: install stable toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_STABLE_VER }} + components: rustfmt + + - name: cargo fmt + run: cargo fmt --all --check + + - name: install ripgrep + run: | + sudo apt update + sudo apt install ripgrep + + - name: check copyright headers + run: bash .github/copyright.sh + + test-stable: + runs-on: ${{ matrix.os }} + strategy: + matrix: + # We use macos-14 as that is an arm runner. These have the virtgpu support we need + os: [windows-latest, macos-14, ubuntu-latest] + include: + - os: ubuntu-latest + gpu: 'yes' + - os: macos-14 + gpu: 'yes' + - os: windows-latest + # TODO: The windows runners theoretically have CPU fallback for GPUs, but + # this failed in initial testing + gpu: 'no' + name: cargo clippy + test + steps: + - uses: actions/checkout@v4 + + - name: install stable toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_STABLE_VER }} + components: clippy + + - name: restore cache + uses: Swatinem/rust-cache@v2 + + - name: Install native dependencies + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev + + # Adapted from https://github.com/bevyengine/bevy/blob/b446374392adc70aceb92621b080d1a6cf7a7392/.github/workflows/validation-jobs.yml#L74-L79 + - name: install xvfb, llvmpipe and lavapipe + if: matrix.os == 'ubuntu-latest' + # https://launchpad.net/~kisak/+archive/ubuntu/turtle + run: | + sudo apt-get update -y -qq + sudo add-apt-repository ppa:kisak/turtle -y + sudo apt-get update + sudo apt install -y xvfb libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers + + - name: cargo clippy (no default features) + run: cargo clippy --workspace --lib --bins --no-default-features -- -D warnings + + - name: cargo clippy (no default features) (auxiliary) + run: cargo clippy --workspace --tests --benches --examples --no-default-features -- -D warnings + + - name: cargo clippy (default features) + run: cargo clippy --workspace --lib --bins -- -D warnings + + - name: cargo clippy (default features) (auxiliary) + run: cargo clippy --workspace --tests --benches --examples -- -D warnings + + - name: cargo clippy (all features) + run: cargo clippy --workspace --lib --bins --all-features -- -D warnings + + - name: cargo clippy (all features) (auxiliary) + run: cargo clippy --workspace --tests --benches --examples --all-features -- -D warnings + + # At the time of writing, we don't have any tests. Nevertheless, it's better to still run this + - name: cargo test + run: cargo test --workspace --all-features + env: + VELLO_CI_GPU_SUPPORT: ${{ matrix.gpu }} + + clippy-stable-wasm: + runs-on: ubuntu-latest + name: cargo test (wasm32) + steps: + - uses: actions/checkout@v4 + + - name: restore cache + uses: Swatinem/rust-cache@v2 + + - name: install stable toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_STABLE_VER }} + targets: wasm32-unknown-unknown + components: clippy + + - name: cargo clippy (wasm) + run: cargo clippy --all-targets --target wasm32-unknown-unknown --workspace -- -D warnings + + android-stable-check: + runs-on: ubuntu-latest + name: cargo check (aarch64-android) + steps: + - uses: actions/checkout@v4 + + - name: restore cache + uses: Swatinem/rust-cache@v2 + + - name: install stable toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_STABLE_VER }} + targets: aarch64-linux-android + + - name: install cargo apk + run: cargo install cargo-apk + + - name: cargo apk check (android) + run: cargo apk check -p with_winit --lib + env: + # This is a bit of a hack, but cargo apk doesn't seem to allow customising this + RUSTFLAGS: '-D warnings' + + docs: + name: cargo doc + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] + steps: + - uses: actions/checkout@v4 + + - name: install nightly toolchain + uses: dtolnay/rust-toolchain@nightly + + - name: restore cache + uses: Swatinem/rust-cache@v2 + + # We test documentation using nightly to match docs.rs. This prevents potential breakages + - name: cargo doc + run: cargo doc --workspace --all-features --no-deps --document-private-items -Zunstable-options -Zrustdoc-scrape-examples diff --git a/.github/workflows/pages-release.yml b/.github/workflows/pages-release.yml new file mode 100644 index 0000000..edab59c --- /dev/null +++ b/.github/workflows/pages-release.yml @@ -0,0 +1,69 @@ +name: Web Demo Update + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + release-web: + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install | Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Install | WASM Bindgen + uses: jetli/wasm-bindgen-action@v0.2.0 + with: + version: 'latest' + + - name: Build | WASM + run: cargo build -p with_winit --bin with_winit_bin --release --target wasm32-unknown-unknown + + - name: Package | WASM + run: | + mkdir public + wasm-bindgen --target web --out-dir public target/wasm32-unknown-unknown/release/with_winit_bin.wasm --no-typescript + cat << EOF > public/index.html + + Velato Web Demo + + + + + + + + + + + EOF + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: './public' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From b9c439fa4ca39f883a599b0c7fd36b222e574716 Mon Sep 17 00:00:00 2001 From: "Spencer C. Imbleau" Date: Sat, 9 Mar 2024 20:22:32 -0500 Subject: [PATCH 05/43] refactor: merge vellottie --- .gitignore | 30 +- Cargo.toml | 25 +- rustfmt.toml | 4 + src/import.rs | 549 ------------- src/import/builders.rs | 245 ++++++ src/import/converters.rs | 772 ++++++++++++++++++ src/import/defaults.rs | 55 ++ src/import/mod.rs | 88 ++ src/import/util.rs | 87 ++ src/lib.rs | 49 +- src/model/value.rs | 160 ---- src/parser/mod.rs | 2 + .../animated_properties/animated_property.rs | 39 + .../schema/animated_properties/color_value.rs | 10 + .../animated_properties/gradient_colors.rs | 20 + .../schema/animated_properties/keyframe.rs | 21 + .../animated_properties/keyframe_base.rs | 27 + .../keyframe_bezier_handle.rs | 27 + src/parser/schema/animated_properties/mod.rs | 13 + .../animated_properties/multi_dimensional.rs | 17 + .../schema/animated_properties/position.rs | 37 + .../animated_properties/position_keyframe.rs | 18 + .../animated_properties/shape_keyframe.rs | 12 + .../animated_properties/shape_property.rs | 34 + .../animated_properties/split_vector.rs | 24 + .../schema/animated_properties/value.rs | 12 + src/parser/schema/animation/composition.rs | 10 + src/parser/schema/animation/mod.rs | 5 + src/parser/schema/assets/asset.rs | 13 + src/parser/schema/assets/file_asset.rs | 21 + src/parser/schema/assets/image.rs | 51 ++ src/parser/schema/assets/mod.rs | 17 + src/parser/schema/assets/precomposition.rs | 22 + src/parser/schema/constants/blend_mode.rs | 25 + src/parser/schema/constants/composite.rs | 9 + src/parser/schema/constants/fill_rule.rs | 12 + .../schema/constants/font_path_origin.rs | 11 + src/parser/schema/constants/gradient_type.rs | 9 + src/parser/schema/constants/line_cap.rs | 13 + src/parser/schema/constants/line_join.rs | 10 + src/parser/schema/constants/mask_mode.rs | 21 + src/parser/schema/constants/matte_mode.rs | 12 + src/parser/schema/constants/merge_mode.rs | 12 + src/parser/schema/constants/mod.rs | 20 + .../schema/constants/shape_direction.rs | 11 + src/parser/schema/constants/star_type.rs | 9 + .../schema/constants/stroke_dash_type.rs | 12 + src/parser/schema/constants/text_based.rs | 10 + src/parser/schema/constants/text_caps.rs | 10 + src/parser/schema/constants/text_grouping.rs | 11 + src/parser/schema/constants/text_justify.rs | 14 + .../schema/constants/text_range_units.rs | 9 + src/parser/schema/constants/text_shape.rs | 14 + .../schema/constants/trim_multiple_shapes.rs | 11 + src/parser/schema/helpers/bezier.rs | 25 + src/parser/schema/helpers/color.rs | 9 + src/parser/schema/helpers/int_boolean.rs | 12 + src/parser/schema/helpers/marker.rs | 22 + src/parser/schema/helpers/mask.rs | 49 ++ src/parser/schema/helpers/mod.rs | 7 + src/parser/schema/helpers/transform.rs | 75 ++ src/parser/schema/helpers/visual_object.rs | 13 + src/parser/schema/layers/enumerations.rs | 23 + src/parser/schema/layers/mod.rs | 45 + src/parser/schema/layers/null.rs | 18 + src/parser/schema/layers/precomposition.rs | 35 + src/parser/schema/layers/shape.rs | 26 + src/parser/schema/layers/solid_color.rs | 31 + src/parser/schema/layers/visual.rs | 116 +++ src/parser/schema/lottie.rs | 73 ++ src/parser/schema/mod.rs | 16 + src/parser/schema/shapes/base_stroke.rs | 37 + src/parser/schema/shapes/ellipse.rs | 16 + src/parser/schema/shapes/fill.rs | 26 + src/parser/schema/shapes/gradient.rs | 36 + src/parser/schema/shapes/gradient_fill.rs | 23 + src/parser/schema/shapes/gradient_stroke.rs | 16 + src/parser/schema/shapes/group.rs | 21 + src/parser/schema/shapes/merge.rs | 17 + src/parser/schema/shapes/mod.rs | 132 +++ src/parser/schema/shapes/offset_path.rs | 24 + src/parser/schema/shapes/path.rs | 14 + src/parser/schema/shapes/polystar.rs | 38 + src/parser/schema/shapes/pucker_bloat.rs | 15 + src/parser/schema/shapes/rectangle.rs | 22 + src/parser/schema/shapes/repeater.rs | 32 + .../schema/shapes/repeater_transform.rs | 17 + src/parser/schema/shapes/shape.rs | 15 + src/parser/schema/shapes/shape_element.rs | 41 + src/parser/schema/shapes/stroke.rs | 44 + src/parser/schema/shapes/stroke_dash.rs | 20 + src/parser/schema/shapes/transform.rs | 12 + src/parser/schema/shapes/trim.rs | 29 + .../schema/styles/color_overlay_style.rs | 23 + .../schema/styles/gradient_overlay_style.rs | 57 ++ src/parser/schema/styles/layer_style.rs | 28 + src/parser/schema/styles/mod.rs | 11 + src/parser/schema/styles/outer_glow_style.rs | 39 + src/parser/schema/styles/satin_style.rs | 32 + src/runtime/mod.rs | 36 + src/{ => runtime}/model/animated.rs | 64 +- src/{ => runtime}/model/fixed.rs | 18 +- src/{ => runtime}/model/mod.rs | 17 +- src/{ => runtime}/model/spline.rs | 6 +- src/runtime/model/value.rs | 218 +++++ src/{ => runtime}/render.rs | 83 +- tests/basic.rs | 59 ++ tests/group_shape.rs | 117 +++ tests/image.rs | 49 ++ tests/layer_basic.rs | 244 ++++++ tests/precomposition.rs | 44 + 111 files changed, 4294 insertions(+), 874 deletions(-) create mode 100644 rustfmt.toml delete mode 100644 src/import.rs create mode 100644 src/import/builders.rs create mode 100644 src/import/converters.rs create mode 100644 src/import/defaults.rs create mode 100644 src/import/mod.rs create mode 100644 src/import/util.rs delete mode 100644 src/model/value.rs create mode 100644 src/parser/mod.rs create mode 100644 src/parser/schema/animated_properties/animated_property.rs create mode 100644 src/parser/schema/animated_properties/color_value.rs create mode 100644 src/parser/schema/animated_properties/gradient_colors.rs create mode 100644 src/parser/schema/animated_properties/keyframe.rs create mode 100644 src/parser/schema/animated_properties/keyframe_base.rs create mode 100644 src/parser/schema/animated_properties/keyframe_bezier_handle.rs create mode 100644 src/parser/schema/animated_properties/mod.rs create mode 100644 src/parser/schema/animated_properties/multi_dimensional.rs create mode 100644 src/parser/schema/animated_properties/position.rs create mode 100644 src/parser/schema/animated_properties/position_keyframe.rs create mode 100644 src/parser/schema/animated_properties/shape_keyframe.rs create mode 100644 src/parser/schema/animated_properties/shape_property.rs create mode 100644 src/parser/schema/animated_properties/split_vector.rs create mode 100644 src/parser/schema/animated_properties/value.rs create mode 100644 src/parser/schema/animation/composition.rs create mode 100644 src/parser/schema/animation/mod.rs create mode 100644 src/parser/schema/assets/asset.rs create mode 100644 src/parser/schema/assets/file_asset.rs create mode 100644 src/parser/schema/assets/image.rs create mode 100644 src/parser/schema/assets/mod.rs create mode 100644 src/parser/schema/assets/precomposition.rs create mode 100644 src/parser/schema/constants/blend_mode.rs create mode 100644 src/parser/schema/constants/composite.rs create mode 100644 src/parser/schema/constants/fill_rule.rs create mode 100644 src/parser/schema/constants/font_path_origin.rs create mode 100644 src/parser/schema/constants/gradient_type.rs create mode 100644 src/parser/schema/constants/line_cap.rs create mode 100644 src/parser/schema/constants/line_join.rs create mode 100644 src/parser/schema/constants/mask_mode.rs create mode 100644 src/parser/schema/constants/matte_mode.rs create mode 100644 src/parser/schema/constants/merge_mode.rs create mode 100644 src/parser/schema/constants/mod.rs create mode 100644 src/parser/schema/constants/shape_direction.rs create mode 100644 src/parser/schema/constants/star_type.rs create mode 100644 src/parser/schema/constants/stroke_dash_type.rs create mode 100644 src/parser/schema/constants/text_based.rs create mode 100644 src/parser/schema/constants/text_caps.rs create mode 100644 src/parser/schema/constants/text_grouping.rs create mode 100644 src/parser/schema/constants/text_justify.rs create mode 100644 src/parser/schema/constants/text_range_units.rs create mode 100644 src/parser/schema/constants/text_shape.rs create mode 100644 src/parser/schema/constants/trim_multiple_shapes.rs create mode 100644 src/parser/schema/helpers/bezier.rs create mode 100644 src/parser/schema/helpers/color.rs create mode 100644 src/parser/schema/helpers/int_boolean.rs create mode 100644 src/parser/schema/helpers/marker.rs create mode 100644 src/parser/schema/helpers/mask.rs create mode 100644 src/parser/schema/helpers/mod.rs create mode 100644 src/parser/schema/helpers/transform.rs create mode 100644 src/parser/schema/helpers/visual_object.rs create mode 100644 src/parser/schema/layers/enumerations.rs create mode 100644 src/parser/schema/layers/mod.rs create mode 100644 src/parser/schema/layers/null.rs create mode 100644 src/parser/schema/layers/precomposition.rs create mode 100644 src/parser/schema/layers/shape.rs create mode 100644 src/parser/schema/layers/solid_color.rs create mode 100644 src/parser/schema/layers/visual.rs create mode 100644 src/parser/schema/lottie.rs create mode 100644 src/parser/schema/mod.rs create mode 100644 src/parser/schema/shapes/base_stroke.rs create mode 100644 src/parser/schema/shapes/ellipse.rs create mode 100644 src/parser/schema/shapes/fill.rs create mode 100644 src/parser/schema/shapes/gradient.rs create mode 100644 src/parser/schema/shapes/gradient_fill.rs create mode 100644 src/parser/schema/shapes/gradient_stroke.rs create mode 100644 src/parser/schema/shapes/group.rs create mode 100644 src/parser/schema/shapes/merge.rs create mode 100644 src/parser/schema/shapes/mod.rs create mode 100644 src/parser/schema/shapes/offset_path.rs create mode 100644 src/parser/schema/shapes/path.rs create mode 100644 src/parser/schema/shapes/polystar.rs create mode 100644 src/parser/schema/shapes/pucker_bloat.rs create mode 100644 src/parser/schema/shapes/rectangle.rs create mode 100644 src/parser/schema/shapes/repeater.rs create mode 100644 src/parser/schema/shapes/repeater_transform.rs create mode 100644 src/parser/schema/shapes/shape.rs create mode 100644 src/parser/schema/shapes/shape_element.rs create mode 100644 src/parser/schema/shapes/stroke.rs create mode 100644 src/parser/schema/shapes/stroke_dash.rs create mode 100644 src/parser/schema/shapes/transform.rs create mode 100644 src/parser/schema/shapes/trim.rs create mode 100644 src/parser/schema/styles/color_overlay_style.rs create mode 100644 src/parser/schema/styles/gradient_overlay_style.rs create mode 100644 src/parser/schema/styles/layer_style.rs create mode 100644 src/parser/schema/styles/mod.rs create mode 100644 src/parser/schema/styles/outer_glow_style.rs create mode 100644 src/parser/schema/styles/satin_style.rs create mode 100644 src/runtime/mod.rs rename src/{ => runtime}/model/animated.rs (87%) rename src/{ => runtime}/model/fixed.rs (86%) rename src/{ => runtime}/model/mod.rs (95%) rename src/{ => runtime}/model/spline.rs (95%) create mode 100644 src/runtime/model/value.rs rename src/{ => runtime}/render.rs (88%) create mode 100644 tests/basic.rs create mode 100644 tests/group_shape.rs create mode 100644 tests/image.rs create mode 100644 tests/layer_basic.rs create mode 100644 tests/precomposition.rs diff --git a/.gitignore b/.gitignore index c1b02be..a9efee5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,30 @@ -/target +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock -assets/* +# Generated by Cargo +# will have compiled files and executables +target/ + +# Generated by Trunk +dist/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# Some people use VSCode +/.vscode/ + +# Some people use IntelliJ with Rust +/.idea/ +*.iml + +# Some people use pre-commit +.pre-commit-config.yaml +.pre-commit-config.yml + +# Some people have Apple +.DS_Store + +# Generated on wasm-pack failure +**/unsupported.js diff --git a/Cargo.toml b/Cargo.toml index eddf62b..9992730 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,35 @@ [workspace] resolver = "2" -members = ["demo"] +members = ["examples/with_winit", "examples/run_wasm", "examples/scenes"] [workspace.package] edition = "2021" -version = "0.0.1" -license = "MIT OR Apache-2.0" +version = "0.1.0" +license = "Apache-2.0 OR MIT" repository = "https://github.com/linebender/velato" [package] name = "velato" -description = "Lottie renderer built on vello." +description = "A Lottie integration for vello." categories = ["rendering", "graphics"] -keywords = ["2d", "vector-graphics", "animation", "lottie"] - +keywords = ["2d", "vector-graphics", "vello", "animation", "lottie"] version.workspace = true license.workspace = true edition.workspace = true repository.workspace = true [workspace.dependencies] -vello = { git = "https://github.com/linebender/vello", rev = "b0303ccf98df15a8b196272720d364a56f7304ba" } - +vello = "0.1" [dependencies] vello = { workspace = true } -bodymovin = { git = "https://github.com/vectorgameexperts/bodymovin-rs" } +keyframe = "1" +once_cell = "1" + +# For the parser +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_repr = "0.1" + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..ce1f14b --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +max_width = 100 +use_field_init_shorthand = true +newline_style = "Unix" +# TODO: imports_granularity = "Module" - Wait for this to be stable. diff --git a/src/import.rs b/src/import.rs deleted file mode 100644 index 12c2ac7..0000000 --- a/src/import.rs +++ /dev/null @@ -1,549 +0,0 @@ -// Copyright 2023 the Velato Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::model::animated::Position; - -use super::{model, model::*, Composition}; - -use vello::kurbo::{Point, Size, Vec2}; -use vello::peniko::{self, BlendMode, Color, Compose, Mix}; - -use std::collections::HashMap; - -pub fn import_composition( - source: impl AsRef<[u8]>, -) -> Result> { - let source = bodymovin::Bodymovin::from_bytes(source)?; - let mut target = Composition::default(); - target.frame_rate = source.frame_rate as f32; - target.frames = source.in_point as f32..source.out_point as f32; - target.width = source.width as u32; - target.height = source.height as u32; - let mut idmap: HashMap = HashMap::default(); - for asset in &source.assets { - match asset { - bodymovin::sources::Asset::PreComp(precomp) => { - idmap.clear(); - let mut layers = vec![]; - let mut mask_layer = None; - for layer in &precomp.layers { - let index = layers.len(); - if let Some((mut layer, id, mask_blend)) = conv_layer(layer) { - if let (Some(mask_blend), Some(mask_layer)) = - (mask_blend, mask_layer.take()) - { - layer.mask_layer = Some((mask_blend, mask_layer)); - } - if layer.is_mask { - mask_layer = Some(index); - } - idmap.insert(id, index); - layers.push(layer); - } - } - for layer in &mut layers { - if let Some(parent) = layer.parent { - layer.parent = idmap.get(&parent).copied(); - } - } - target.assets.insert(precomp.id.clone(), layers); - } - _ => {} - } - } - idmap.clear(); - let mut layers = vec![]; - let mut mask_layer = None; - for layer in &source.layers { - let index = layers.len(); - if let Some((mut layer, id, mask_blend)) = conv_layer(layer) { - if let (Some(mask_blend), Some(mask_layer)) = (mask_blend, mask_layer.take()) { - layer.mask_layer = Some((mask_blend, mask_layer)); - } - if layer.is_mask { - mask_layer = Some(index); - } - idmap.insert(id, index); - layers.push(layer); - } - } - for layer in &mut layers { - if let Some(parent) = layer.parent { - layer.parent = idmap.get(&parent).copied(); - } - } - target.layers = layers; - Ok(target) -} - -fn conv_layer(source: &bodymovin::layers::AnyLayer) -> Option<(Layer, usize, Option)> { - use bodymovin::layers::AnyLayer; - let mut layer = Layer::default(); - let params; - match source { - AnyLayer::Null(value) => { - params = setup_layer(value, &mut layer); - } - AnyLayer::PreComp(value) => { - params = setup_layer(value, &mut layer); - let name = value.mixin.ref_id.clone(); - let time_remap = conv_scalar(&value.mixin.time_remapping); - layer.content = Content::Instance { name, time_remap }; - } - AnyLayer::Shape(value) => { - params = setup_layer(value, &mut layer); - let mut shapes = vec![]; - for shape in &value.mixin.shapes { - if let Some(shape) = conv_shape(shape) { - shapes.push(shape); - } - } - layer.content = Content::Shape(shapes); - } - _ => return None, - } - let (id, matte_mode) = params; - Some((layer, id, matte_mode)) -} - -fn setup_layer( - source: &bodymovin::layers::Layer, - target: &mut Layer, -) -> (usize, Option) { - use bodymovin::helpers::MatteMode; - target.name = source.name.clone().unwrap_or_default(); - target.parent = source.parent.map(|i| i as usize); - let (transform, opacity) = conv_transform(&source.transform); - target.transform = transform; - target.opacity = opacity; - target.width = source.width.unwrap_or(0) as _; - target.height = source.height.unwrap_or(0) as _; - target.is_mask = source.is_track_matte; - let matte_mode = source.matte_mode.as_ref().map(|mode| match mode { - MatteMode::Normal => Mix::Normal.into(), - MatteMode::Alpha | MatteMode::Luma => Compose::SrcIn.into(), - MatteMode::InvertAlpha | MatteMode::InvertLuma => Compose::SrcOut.into(), - }); - target.blend_mode = conv_blend_mode(&source.blend_mode); - if target.blend_mode == Some(peniko::Mix::Normal.into()) { - target.blend_mode = None; - } - target.frames = source.in_point as f32..source.out_point as f32; - target.stretch = source.stretch as f32; - target.start_frame = source.start_time as f32; - for mask_source in &source.masks { - if let Some(geometry) = conv_shape_geometry(&mask_source.points) { - let mode = peniko::BlendMode::default(); - let opacity = conv_scalar(&mask_source.opacity); - target.masks.push(Mask { - mode, - geometry, - opacity, - }) - } - } - (source.index as usize, matte_mode) -} - -fn conv_transform(value: &bodymovin::helpers::Transform) -> (Transform, Value) { - let transform = animated::Transform { - anchor: conv_point(&value.anchor_point), - position: match &value.position { - bodymovin::properties::SplittableMultiDimensional::Uniform(position) => { - Position::Point(conv_point(position)) - } - bodymovin::properties::SplittableMultiDimensional::Split(split_vector) => { - Position::SplitComponents(( - conv_scalar(&split_vector.x_component), - conv_scalar(&split_vector.y_component), - )) - } - }, - scale: conv_vec2(&value.scale), - rotation: conv_scalar(&value.rotation), - skew: conv_scalar(&value.skew), - skew_angle: conv_scalar(&value.skew_axis), - }; - let opacity = conv_scalar(&value.opacity); - (transform.to_model(), opacity) -} - -fn conv_shape_transform(value: &bodymovin::shapes::Transform) -> GroupTransform { - let transform = animated::Transform { - anchor: conv_point(&value.anchor_point), - position: Position::Point(conv_point(&value.position)), - scale: conv_vec2(&value.scale), - rotation: conv_scalar(&value.rotation), - skew: conv_scalar(&value.skew), - skew_angle: conv_scalar(&value.skew_axis), - }; - let opacity = conv_scalar(&value.opacity); - GroupTransform { - transform: transform.to_model(), - opacity, - } -} - -fn conv_scalar(value: &bodymovin::properties::Scalar) -> Value { - use bodymovin::properties::Value::*; - match &value.value { - Fixed(value) => Value::Fixed(*value as f32), - Animated(animated) => { - let mut frames = vec![]; - let mut values = vec![]; - let mut last_value = None; - for value in animated { - if let Some(data) = value.start_value.as_ref().or(last_value.flatten()) { - frames.push(Time { - frame: value.start_time as f32, - }); - values.push(data.0 as f32); - } - last_value = Some(value.end_value.as_ref()); - } - Value::Animated(model::Animated { - times: frames, - values, - }) - } - } -} - -fn conv_multi( - value: &bodymovin::properties::MultiDimensional, - f: impl Fn(&Vec) -> T, -) -> Value { - use bodymovin::properties::Value::*; - match &value.value { - Fixed(value) => Value::Fixed(f(value)), - Animated(animated) => { - let mut frames = vec![]; - let mut values = vec![]; - let mut last_value = None; - for value in animated { - if let Some(data) = value.start_value.as_ref().or(last_value) { - frames.push(Time { - frame: value.start_time as f32, - }); - values.push(f(data)); - } - last_value = value.end_value.as_ref(); - } - Value::Animated(model::Animated { - times: frames, - values, - }) - } - } -} - -fn conv_point(value: &bodymovin::properties::MultiDimensional) -> Value { - conv_multi(value, |x| { - Point::new( - x.get(0).copied().unwrap_or(0.0), - x.get(1).copied().unwrap_or(0.0), - ) - }) -} - -fn conv_color(value: &bodymovin::properties::MultiDimensional) -> Value { - conv_multi(value, |x| { - Color::rgb( - x.get(0).copied().unwrap_or(0.0), - x.get(1).copied().unwrap_or(0.0), - x.get(2).copied().unwrap_or(0.0), - ) - }) -} - -fn conv_vec2(value: &bodymovin::properties::MultiDimensional) -> Value { - conv_multi(value, |x| { - Vec2::new( - x.get(0).copied().unwrap_or(0.0), - x.get(1).copied().unwrap_or(0.0), - ) - }) -} - -fn conv_size(value: &bodymovin::properties::MultiDimensional) -> Value { - conv_multi(value, |x| { - Size::new( - x.get(0).copied().unwrap_or(0.0), - x.get(1).copied().unwrap_or(0.0), - ) - }) -} - -fn conv_gradient_colors(value: &bodymovin::helpers::GradientColors) -> ColorStops { - use bodymovin::properties::Value::*; - let count = value.count as usize; - match &value.colors.value { - Fixed(value) => { - let mut stops = fixed::ColorStops::new(); - for chunk in value.chunks_exact(4) { - stops.push( - ( - chunk[0] as f32, - fixed::Color::rgba(chunk[1], chunk[2], chunk[3], 1.0), - ) - .into(), - ) - } - ColorStops::Fixed(stops) - } - Animated(animated) => { - let mut frames = vec![]; - let mut values = vec![]; - let mut last_value = None; - for value in animated { - if let Some(data) = value.start_value.as_ref().or(last_value.flatten()) { - frames.push(Time { - frame: value.start_time as f32, - }); - values.push(data.iter().map(|x| *x as f32).collect::>()); - } - last_value = Some(value.end_value.as_ref()); - } - ColorStops::Animated(animated::ColorStops { - frames, - values, - count, - }) - } - } -} - -fn conv_draw(value: &bodymovin::shapes::AnyShape) -> Option { - use bodymovin::helpers::{LineCap, LineJoin}; - use bodymovin::shapes::{AnyShape, GradientType}; - use peniko::{Cap, Join}; - match value { - AnyShape::Fill(value) => { - let color = conv_color(&value.color); - let brush = animated::Brush::Solid(color).to_model(); - let opacity = conv_scalar(&value.opacity); - Some(Draw { - stroke: None, - brush, - opacity, - }) - } - AnyShape::Stroke(value) => { - let stroke = animated::Stroke { - width: conv_scalar(&value.width), - join: match value.line_join { - LineJoin::Bevel => Join::Bevel, - LineJoin::Round => Join::Round, - LineJoin::Miter => Join::Miter, - }, - miter_limit: value.miter_limit.map(|x| x as f32), - cap: match value.line_cap { - LineCap::Butt => Cap::Butt, - LineCap::Round => Cap::Round, - LineCap::Square => Cap::Square, - }, - }; - let color = conv_color(&value.color); - let brush = animated::Brush::Solid(color).to_model(); - let opacity = conv_scalar(&value.opacity); - Some(Draw { - stroke: Some(stroke.to_model()), - brush, - opacity, - }) - } - AnyShape::GradientFill(value) => { - let is_radial = matches!(value.ty, GradientType::Radial); - let start_point = conv_point(&value.start_point); - let end_point = conv_point(&value.end_point); - let gradient = animated::Gradient { - is_radial, - start_point, - end_point, - stops: conv_gradient_colors(&value.gradient_colors), - }; - let brush = animated::Brush::Gradient(gradient).to_model(); - Some(Draw { - stroke: None, - brush, - opacity: Value::Fixed(100.0), - }) - } - AnyShape::GradientStroke(value) => { - let stroke = animated::Stroke { - width: conv_scalar(&value.stroke_width), - join: match value.line_join { - LineJoin::Bevel => Join::Bevel, - LineJoin::Round => Join::Round, - LineJoin::Miter => Join::Miter, - }, - miter_limit: value.miter_limit.map(|x| x as f32), - cap: match value.line_cap { - LineCap::Butt => Cap::Butt, - LineCap::Round => Cap::Round, - LineCap::Square => Cap::Square, - }, - }; - let is_radial = matches!(value.ty, GradientType::Radial); - let start_point = conv_point(&value.start_point); - let end_point = conv_point(&value.end_point); - let gradient = animated::Gradient { - is_radial, - start_point, - end_point, - stops: conv_gradient_colors(&value.gradient_colors), - }; - let brush = animated::Brush::Gradient(gradient).to_model(); - Some(Draw { - stroke: Some(stroke.to_model()), - brush, - opacity: Value::Fixed(100.0), - }) - } - _ => None, - } -} - -fn conv_shape(value: &bodymovin::shapes::AnyShape) -> Option { - use bodymovin::shapes::AnyShape; - if let Some(draw) = conv_draw(value) { - return Some(Shape::Draw(draw)); - } else if let Some(geometry) = conv_geometry(value) { - return Some(Shape::Geometry(geometry)); - } - match value { - AnyShape::Group(value) => { - let mut shapes = vec![]; - let mut group_transform = None; - for item in &value.items { - match item { - AnyShape::Transform(transform) => { - group_transform = Some(conv_shape_transform(transform)); - } - _ => { - if let Some(shape) = conv_shape(item) { - shapes.push(shape); - } - } - } - } - if !shapes.is_empty() { - Some(Shape::Group(shapes, group_transform)) - } else { - None - } - } - AnyShape::Repeater(value) => { - let repeater = animated::Repeater { - copies: conv_scalar(&value.copies), - offset: conv_scalar(&value.offset), - anchor_point: conv_point(&value.transform.anchor_point), - position: conv_point(&value.transform.position), - rotation: conv_scalar(&value.transform.rotation), - scale: conv_vec2(&value.transform.scale), - start_opacity: conv_scalar(&value.transform.start_opacity), - end_opacity: conv_scalar(&value.transform.end_opacity), - }; - Some(Shape::Repeater(repeater.to_model())) - } - _ => None, - } -} - -fn conv_geometry(value: &bodymovin::shapes::AnyShape) -> Option { - use bodymovin::shapes::AnyShape; - match value { - AnyShape::Ellipse(value) => { - let ellipse = animated::Ellipse { - is_ccw: value.direction as i32 == 3, - position: conv_point(&value.position), - size: conv_size(&value.size), - }; - Some(Geometry::Ellipse(ellipse)) - } - AnyShape::Rect(value) => { - let rect = animated::Rect { - is_ccw: value.direction as i32 == 3, - position: conv_point(&value.position), - size: conv_size(&value.size), - corner_radius: conv_scalar(&value.rounded_corners), - }; - Some(Geometry::Rect(rect)) - } - AnyShape::Shape(value) => conv_shape_geometry(&value.vertices), - _ => None, - } -} - -fn conv_shape_geometry(value: &bodymovin::properties::Shape) -> Option { - use bodymovin::properties::Value::*; - let mut is_closed = false; - match &value.value { - Fixed(value) => { - let (points, is_closed) = conv_spline(value); - let mut path = vec![]; - points.as_slice().to_path(is_closed, &mut path); - Some(Geometry::Fixed(path)) - } - Animated(animated) => { - let mut frames = vec![]; - let mut values = vec![]; - let mut last_value = None; - for value in animated { - if let Some(data) = value.start_value.as_ref().or(last_value) { - frames.push(Time { - frame: value.start_time as f32, - }); - let (points, is_frame_closed) = conv_spline(data.get(0)?); - values.push(points); - is_closed |= is_frame_closed; - } - last_value = value.end_value.as_ref(); - } - Some(Geometry::Spline(animated::Spline { - is_closed, - times: frames, - values, - })) - } - } -} - -fn conv_spline(value: &bodymovin::properties::ShapeValue) -> (Vec, bool) { - use core::iter::repeat; - let mut points = Vec::with_capacity(value.vertices.len() * 3); - let is_closed = value.closed.unwrap_or(false); - for ((v, i), o) in value - .vertices - .iter() - .zip(value.in_point.iter().chain(repeat(&(0.0, 0.0)))) - .zip(value.out_point.iter().chain(repeat(&(0.0, 0.0)))) - { - points.push((v.0, v.1).into()); - points.push((i.0, i.1).into()); - points.push((o.0, o.1).into()); - } - (points, is_closed) -} - -fn conv_blend_mode(value: &bodymovin::helpers::BlendMode) -> Option { - use bodymovin::helpers::BlendMode::*; - Some(match value { - Normal => return None, - Multiply => BlendMode::from(Mix::Multiply), - Screen => BlendMode::from(Mix::Screen), - Overlay => BlendMode::from(Mix::Overlay), - Darken => BlendMode::from(Mix::Darken), - Lighten => BlendMode::from(Mix::Lighten), - ColorDodge => BlendMode::from(Mix::ColorDodge), - ColorBurn => BlendMode::from(Mix::ColorBurn), - HardLight => BlendMode::from(Mix::HardLight), - SoftLight => BlendMode::from(Mix::SoftLight), - Difference => BlendMode::from(Mix::Difference), - Exclusion => BlendMode::from(Mix::Exclusion), - Hue => BlendMode::from(Mix::Hue), - Saturation => BlendMode::from(Mix::Saturation), - Color => BlendMode::from(Mix::Color), - Luminosity => BlendMode::from(Mix::Luminosity), - }) -} diff --git a/src/import/builders.rs b/src/import/builders.rs new file mode 100644 index 0000000..a93c0d8 --- /dev/null +++ b/src/import/builders.rs @@ -0,0 +1,245 @@ +use super::converters::{conv_blend_mode, conv_scalar, conv_shape_geometry, conv_transform}; +use super::defaults::FLOAT_VALUE_ONE_HUNDRED; +use super::NumberExt; +use crate::parser::schema::helpers::int_boolean::BoolInt; +use crate::runtime::model::Layer; +use crate::{parser, runtime}; +use parser::schema; +use vello::peniko::{self, BlendMode, Compose, Mix}; + +pub fn setup_precomp_layer( + source: &parser::schema::layers::precomposition::PrecompositionLayer, + target: &mut Layer, +) -> (usize, Option) { + target.name = source.properties.name.clone().unwrap_or_default(); + target.parent = source + .properties + .parent_index + .as_ref() + .map(|i| i.unwrap_u32() as usize); + let (transform, opacity) = conv_transform(&source.properties.transform); + target.transform = transform; + target.opacity = opacity; + target.width = source.width.unwrap_u32(); + target.height = source.height.unwrap_u32(); + target.is_mask = source + .properties + .matte_target + .as_ref() + .map_or(false, |td| *td == BoolInt::True); + + let matte_mode = source + .properties + .matte_mode + .as_ref() + .map(|mode| match mode { + schema::constants::matte_mode::MatteMode::Normal => Mix::Normal.into(), + schema::constants::matte_mode::MatteMode::Alpha + | schema::constants::matte_mode::MatteMode::Luma => Compose::SrcIn.into(), + schema::constants::matte_mode::MatteMode::InvertedAlpha + | schema::constants::matte_mode::MatteMode::InvertedLuma => Compose::SrcOut.into(), + }); + + target.blend_mode = conv_blend_mode( + source + .properties + .blend_mode + .as_ref() + .unwrap_or(&crate::parser::schema::constants::blend_mode::BlendMode::Normal), + ); + if target.blend_mode == Some(peniko::Mix::Normal.into()) { + target.blend_mode = None; + } + target.stretch = source + .properties + .time_stretch + .as_ref() + .map_or(1.0, |sr| sr.unwrap_f32()); + target.frames = + source.properties.in_point.unwrap_f32()..source.properties.out_point.unwrap_f32(); + target.start_frame = source.properties.start_time.unwrap_f32(); + + for mask_source in source + .properties + .masks_properties + .as_ref() + .unwrap_or(&Vec::default()) + { + if let Some(shape) = &mask_source.shape { + if let Some(geometry) = conv_shape_geometry(shape) { + let mode = peniko::BlendMode::default(); + let opacity = conv_scalar( + mask_source + .opacity + .as_ref() + .unwrap_or(&FLOAT_VALUE_ONE_HUNDRED), + ); + target.masks.push(runtime::model::Mask { + mode, + geometry, + opacity, + }) + } + } + } + + ( + source + .properties + .index + .as_ref() + .map_or(0, |ind| ind.unwrap_u32()) as usize, + matte_mode, + ) +} + +pub fn setup_shape_layer( + source: &parser::schema::layers::shape::ShapeLayer, + target: &mut Layer, +) -> (usize, Option) { + target.name = source.properties.name.clone().unwrap_or_default(); + target.parent = source + .properties + .parent_index + .as_ref() + .map(|i| i.unwrap_u32() as usize); + let (transform, opacity) = conv_transform(&source.properties.transform); + target.transform = transform; + target.opacity = opacity; + target.is_mask = source + .properties + .matte_target + .as_ref() + .map_or(false, |td| *td == BoolInt::True); + + let matte_mode = source + .properties + .matte_mode + .as_ref() + .map(|mode| match mode { + schema::constants::matte_mode::MatteMode::Normal => Mix::Normal.into(), + schema::constants::matte_mode::MatteMode::Alpha + | schema::constants::matte_mode::MatteMode::Luma => Compose::SrcIn.into(), + schema::constants::matte_mode::MatteMode::InvertedAlpha + | schema::constants::matte_mode::MatteMode::InvertedLuma => Compose::SrcOut.into(), + }); + + target.blend_mode = conv_blend_mode( + source + .properties + .blend_mode + .as_ref() + .unwrap_or(&crate::parser::schema::constants::blend_mode::BlendMode::Normal), + ); + if target.blend_mode == Some(peniko::Mix::Normal.into()) { + target.blend_mode = None; + } + target.stretch = source + .properties + .time_stretch + .as_ref() + .map_or(1.0, |sr| sr.unwrap_f32()); + target.frames = + source.properties.in_point.unwrap_f32()..source.properties.out_point.unwrap_f32(); + target.start_frame = source.properties.start_time.unwrap_f32(); + + for mask_source in source + .properties + .masks_properties + .as_ref() + .unwrap_or(&Vec::default()) + { + if let Some(shape) = &mask_source.shape { + if let Some(geometry) = conv_shape_geometry(shape) { + let mode = peniko::BlendMode::default(); + let opacity = conv_scalar( + mask_source + .opacity + .as_ref() + .unwrap_or(&FLOAT_VALUE_ONE_HUNDRED), + ); + target.masks.push(runtime::model::Mask { + mode, + geometry, + opacity, + }) + } + } + } + + ( + source + .properties + .index + .as_ref() + .map_or(0, |ind| ind.unwrap_u32()) as usize, + matte_mode, + ) +} + +pub fn setup_layer_base( + source: &parser::schema::layers::visual::VisualLayer, + target: &mut Layer, +) -> (usize, Option) { + target.name = source.name.clone().unwrap_or_default(); + target.parent = source + .parent_index + .as_ref() + .map(|i| i.unwrap_u32() as usize); + let (transform, opacity) = conv_transform(&source.transform); + target.transform = transform; + target.opacity = opacity; + target.is_mask = source + .matte_target + .as_ref() + .map_or(false, |td| *td == BoolInt::True); + + let matte_mode = source.matte_mode.as_ref().map(|mode| match mode { + schema::constants::matte_mode::MatteMode::Normal => Mix::Normal.into(), + schema::constants::matte_mode::MatteMode::Alpha + | schema::constants::matte_mode::MatteMode::Luma => Compose::SrcIn.into(), + schema::constants::matte_mode::MatteMode::InvertedAlpha + | schema::constants::matte_mode::MatteMode::InvertedLuma => Compose::SrcOut.into(), + }); + + target.blend_mode = conv_blend_mode( + source + .blend_mode + .as_ref() + .unwrap_or(&crate::parser::schema::constants::blend_mode::BlendMode::Normal), + ); + // TODO: Why do we do this next part? + if target.blend_mode == Some(peniko::Mix::Normal.into()) { + target.blend_mode = None; + } + target.stretch = source + .time_stretch + .as_ref() + .map_or(1.0, |sr| sr.unwrap_f32()); + target.frames = source.in_point.unwrap_f32()..source.out_point.unwrap_f32(); + target.start_frame = source.start_time.unwrap_f32(); + + for mask_source in source.masks_properties.as_ref().unwrap_or(&Vec::default()) { + if let Some(shape) = &mask_source.shape { + if let Some(geometry) = conv_shape_geometry(shape) { + let mode = peniko::BlendMode::default(); + let opacity = conv_scalar( + mask_source + .opacity + .as_ref() + .unwrap_or(&FLOAT_VALUE_ONE_HUNDRED), + ); + target.masks.push(runtime::model::Mask { + mode, + geometry, + opacity, + }) + } + } + } + + ( + source.index.as_ref().map_or(0, |ind| ind.unwrap_u32()) as usize, + matte_mode, + ) +} diff --git a/src/import/converters.rs b/src/import/converters.rs new file mode 100644 index 0000000..99a4e6d --- /dev/null +++ b/src/import/converters.rs @@ -0,0 +1,772 @@ +use super::builders::{setup_layer_base, setup_precomp_layer, setup_shape_layer}; +use super::defaults::{FLOAT_VALUE_ONE_HUNDRED, FLOAT_VALUE_ZERO, MULTIDIM_ONE, POSITION_ZERO}; +use super::NumberExt; +use crate::import::util::calc_stops; +use crate::parser::schema::animated_properties::keyframe_bezier_handle::{ + KeyframeBezierHandle, KeyframeComponent, +}; +use crate::parser::schema::animated_properties::multi_dimensional::MultiDimensional; +use crate::parser::schema::animated_properties::split_vector::SplitVector; +use crate::parser::schema::constants::gradient_type::GradientType; +use crate::parser::schema::helpers::int_boolean::BoolInt; +use crate::parser::{self}; +use crate::runtime::model::animated::{self, Position}; +use crate::runtime::model::{ + self, Content, Draw, EasingHandle, GroupTransform, Layer, SplineToPath, Time, Tweenable, Value, +}; +use crate::runtime::{self}; +use parser::schema; +use vello::kurbo::{Cap, Join, Point, Size, Vec2}; +use vello::peniko::{BlendMode, Color, Mix}; + +pub fn conv_layer( + source: &parser::schema::layers::AnyLayer, +) -> Option<(Layer, usize, Option)> { + let mut layer = Layer::default(); + + let params = match source { + parser::schema::layers::AnyLayer::Null(null_layer) => { + if let Some(true) = null_layer.properties.hidden { + return None; + } + + setup_layer_base(&null_layer.properties, &mut layer) + } + parser::schema::layers::AnyLayer::Precomposition(precomp_layer) => { + if let Some(true) = precomp_layer.properties.hidden { + return None; + } + + let params = setup_precomp_layer(precomp_layer, &mut layer); + let name = precomp_layer.precomp_id.clone(); + let time_remap = precomp_layer.time_remap.as_ref().map(conv_scalar); + layer.content = Content::Instance { name, time_remap }; + + params + } + parser::schema::layers::AnyLayer::Shape(shape_layer) => { + if let Some(true) = shape_layer.properties.hidden { + return None; + } + + let params = setup_shape_layer(shape_layer, &mut layer); + let mut shapes = vec![]; + for shape in &shape_layer.shapes { + if let Some(shape) = conv_shape(shape) { + shapes.push(shape); + } + } + layer.content = Content::Shape(shapes); + + params + } + schema::layers::AnyLayer::SolidColor(solid_color_layer) => { + if let Some(true) = solid_color_layer.properties.hidden { + return None; + } + + setup_layer_base(&solid_color_layer.properties, &mut layer) + } + }; + + let (id, matte_mode) = params; + Some((layer, id, matte_mode)) +} + +pub fn conv_transform( + value: &parser::schema::helpers::transform::Transform, +) -> (runtime::model::Transform, Value) { + let rotation_in = match &value.rotation { + Some(any_trans) => match any_trans { + parser::schema::helpers::transform::AnyTransformR::Rotation(float_value) => float_value, + // todo: need to actually handle split rotations + parser::schema::helpers::transform::AnyTransformR::SplitRotation { .. } => todo!(), + }, + None => todo!("split rotation"), + }; + + let position = match &value.position { + schema::helpers::transform::AnyTransformP::Position(position) => { + Position::Value(conv_pos_point(position)) + } + schema::helpers::transform::AnyTransformP::SplitPosition(SplitVector { x, y, .. }) => { + Position::SplitValues((conv_scalar(x), conv_scalar(y))) + } + }; + + let transform = animated::Transform { + anchor: conv_pos_point(value.anchor_point.as_ref().unwrap_or(&POSITION_ZERO)), + position, + scale: conv_vec2(value.scale.as_ref().unwrap_or(&MULTIDIM_ONE)), + rotation: conv_scalar(rotation_in), + skew: conv_scalar(value.skew.as_ref().unwrap_or(&FLOAT_VALUE_ZERO)), + skew_angle: conv_scalar(value.skew_axis.as_ref().unwrap_or(&FLOAT_VALUE_ZERO)), + }; + let opacity = conv_scalar(value.opacity.as_ref().unwrap_or(&FLOAT_VALUE_ONE_HUNDRED)); + (transform.to_model(), opacity) +} + +pub fn conv_shape_transform( + value: &parser::schema::shapes::transform::TransformShape, +) -> GroupTransform { + let rotation_in = match &value.transform.rotation { + Some(any_trans) => match any_trans { + parser::schema::helpers::transform::AnyTransformR::Rotation(float_value) => float_value, + // todo: need to actually handle split rotations + parser::schema::helpers::transform::AnyTransformR::SplitRotation { .. } => { + todo!("split rotation") + } + }, + None => &FLOAT_VALUE_ZERO, + }; + let position_in = match &value.transform.position { + schema::helpers::transform::AnyTransformP::Position(position) => position, + schema::helpers::transform::AnyTransformP::SplitPosition(_) => { + // todo: split vectors + todo!("split position"); + } + }; + + let transform = animated::Transform { + anchor: conv_pos_point( + value + .transform + .anchor_point + .as_ref() + .unwrap_or(&POSITION_ZERO), + ), + position: Position::Value(conv_pos_point(position_in)), + scale: conv_vec2(value.transform.scale.as_ref().unwrap_or(&MULTIDIM_ONE)), + rotation: conv_scalar(rotation_in), + skew: conv_scalar(value.transform.skew.as_ref().unwrap_or(&FLOAT_VALUE_ZERO)), + skew_angle: conv_scalar( + value + .transform + .skew_axis + .as_ref() + .unwrap_or(&FLOAT_VALUE_ZERO), + ), + }; + + let opacity = conv_scalar( + value + .transform + .opacity + .as_ref() + .unwrap_or(&FLOAT_VALUE_ONE_HUNDRED), + ); + + GroupTransform { + transform: transform.to_model(), + opacity, + } +} + +pub fn conv_keyframes<'a, T: Tweenable>( + keyframes: impl Iterator, + f: impl Fn(&schema::animated_properties::keyframe::Keyframe) -> T, +) -> Value { + let mut frames = vec![]; + let mut values = vec![]; + + let collect_tangents = |handle: &Option| { + let mut handles = vec![]; + let Some(handle) = handle else { return handles }; + match (&handle.x_coordinate, &handle.y_coordinate) { + (KeyframeComponent::ArrayOfValues(xarr), KeyframeComponent::ArrayOfValues(yarr)) => { + handles.extend(xarr.iter().zip(yarr).map(|(x, y)| EasingHandle { + x: x.unwrap_f64(), + y: y.unwrap_f64(), + })); + } + (KeyframeComponent::ArrayOfValues(xarr), KeyframeComponent::SingleValue(y)) => { + handles.extend(xarr.iter().map(|x| EasingHandle { + x: x.unwrap_f64(), + y: y.unwrap_f64(), + })); + } + (KeyframeComponent::SingleValue(x), KeyframeComponent::ArrayOfValues(yarr)) => { + handles.extend(yarr.iter().map(|y| EasingHandle { + x: x.unwrap_f64(), + y: y.unwrap_f64(), + })); + } + (KeyframeComponent::SingleValue(x), KeyframeComponent::SingleValue(y)) => { + handles.push(EasingHandle { + x: x.unwrap_f64(), + y: y.unwrap_f64(), + }); + } + } + handles + }; + + for keyframe in keyframes { + let hold = keyframe + .base + .hold + .as_ref() + .map(|b| b.eq(&BoolInt::True)) + .unwrap_or(false); + let in_tangents = collect_tangents(&keyframe.base.in_tangent); + let out_tangents = collect_tangents(&keyframe.base.out_tangent); + let tangents: Vec<_> = in_tangents.into_iter().zip(out_tangents).collect(); + if tangents.is_empty() { + frames.push(Time { + frame: keyframe.base.time.unwrap_f32(), + in_tangent: None, + out_tangent: None, + hold, + }); + values.push(f(keyframe)); + } else { + for (in_tangent, out_tangent) in tangents { + frames.push(Time { + frame: keyframe.base.time.unwrap_f32(), + in_tangent: Some(in_tangent), + out_tangent: Some(out_tangent), + hold, + }); + values.push(f(keyframe)); + } + } + } + Value::Animated(runtime::model::Animated { + times: frames, + values, + }) +} + +fn conv_keyframe_handle(handle: &KeyframeBezierHandle) -> EasingHandle { + let KeyframeBezierHandle { + x_coordinate, + y_coordinate, + } = handle; + let x = match x_coordinate { + KeyframeComponent::ArrayOfValues(arr) => { + assert!(arr.len() == 1, "{arr:?}"); + arr[0].as_f64().unwrap() + } + KeyframeComponent::SingleValue(v) => v.unwrap_f64(), + }; + let y = match y_coordinate { + KeyframeComponent::ArrayOfValues(arr) => { + assert!(arr.len() == 1); + arr[0].as_f64().unwrap() + } + KeyframeComponent::SingleValue(v) => v.unwrap_f64(), + }; + EasingHandle { x, y } +} + +fn conv_gradient_colors( + value: &schema::animated_properties::gradient_colors::GradientColors, +) -> runtime::model::ColorStops { + use schema::animated_properties::animated_property::AnimatedPropertyK::*; + + let count = value.count.unwrap_u32() as usize; + match &value.colors.animated_property.value { + Static(value) => runtime::model::ColorStops::Fixed({ + let mut stops = runtime::model::fixed::ColorStops::new(); + let raw = calc_stops(value, count); + for values in raw { + stops.push( + ( + values[0] as f32, + runtime::model::fixed::Color::rgba( + values[1], values[2], values[3], values[4], + ), + ) + .into(), + ); + } + stops + }), + AnimatedValue(animated) => { + let mut frames = vec![]; + let mut values: Vec> = vec![]; + for value in animated { + let hold = value + .base + .hold + .as_ref() + .map(|b| b.eq(&BoolInt::True)) + .unwrap_or(false); + frames.push(Time { + frame: value.base.time.unwrap_f32(), + in_tangent: value.base.in_tangent.as_ref().map(conv_keyframe_handle), + out_tangent: value.base.out_tangent.as_ref().map(conv_keyframe_handle), + hold, + }); + + let stops = calc_stops(&value.value, count) + .into_iter() + .flatten() + .map(|x| x as f32) + .collect::>(); + + values.push(stops); + } + runtime::model::ColorStops::Animated(animated::ColorStops { + frames, + values, + count, + }) + } + } +} + +fn conv_draw(value: &schema::shapes::AnyShape) -> Option { + use schema::constants::line_cap::LineCap; + use schema::constants::line_join::LineJoin; + use schema::shapes::AnyShape; + + match value { + AnyShape::Fill(value) => { + let color = conv_color(&value.color); + let brush = animated::Brush::Solid(color).to_model(); + let opacity = conv_scalar(value.opacity.as_ref().unwrap_or(&FLOAT_VALUE_ONE_HUNDRED)); + Some(runtime::model::Draw { + stroke: None, + brush, + opacity, + }) + } + AnyShape::Stroke(value) => { + let stroke = animated::Stroke { + width: conv_scalar(&value.stroke_width), + join: match value.line_join.as_ref().unwrap_or(&LineJoin::Bevel) { + LineJoin::Bevel => Join::Bevel, + LineJoin::Round => Join::Round, + LineJoin::Miter => Join::Miter, + }, + miter_limit: value.miter_limit.as_ref().map(|number| number.unwrap_f32()), + cap: match value.line_cap.as_ref().unwrap_or(&LineCap::Butt) { + LineCap::Butt => Cap::Butt, + LineCap::Round => Cap::Round, + LineCap::Square => Cap::Square, + }, + }; + let color = conv_color(&value.stroke_color); + let brush = animated::Brush::Solid(color).to_model(); + let opacity = conv_scalar(&value.opacity); + Some(runtime::model::Draw { + stroke: Some(stroke.to_model()), + brush, + opacity, + }) + } + AnyShape::GradientFill(value) => { + let is_radial = matches!(value.gradient.gradient_type, Some(GradientType::Radial)); + let start_point = conv_multi_point(&value.gradient.start_point); + let end_point = conv_multi_point(&value.gradient.end_point); + let gradient = animated::Gradient { + is_radial, + start_point, + end_point, + stops: conv_gradient_colors(&value.gradient.colors), + }; + let brush = animated::Brush::Gradient(gradient).to_model(); + Some(Draw { + stroke: None, + brush, + opacity: Value::Fixed(100.0), + }) + } + AnyShape::GradientStroke(value) => { + let stroke = animated::Stroke { + width: conv_scalar(&value.base_stroke.width), + join: match value + .base_stroke + .line_join + .as_ref() + .unwrap_or(&LineJoin::Round) + { + LineJoin::Bevel => Join::Bevel, + LineJoin::Round => Join::Round, + LineJoin::Miter => Join::Miter, + }, + miter_limit: value + .base_stroke + .miter_limit + .as_ref() + .map(|x| x.unwrap_f32()), + cap: match value + .base_stroke + .line_cap + .as_ref() + .unwrap_or(&LineCap::Round) + { + LineCap::Butt => Cap::Butt, + LineCap::Round => Cap::Round, + LineCap::Square => Cap::Square, + }, + }; + let is_radial = matches!( + value + .gradient + .gradient_type + .as_ref() + .unwrap_or(&GradientType::Linear), + GradientType::Radial + ); + let start_point = conv_multi_point(&value.gradient.start_point); + let end_point = conv_multi_point(&value.gradient.end_point); + let gradient = animated::Gradient { + is_radial, + start_point, + end_point, + stops: conv_gradient_colors(&value.gradient.colors), + }; + let brush = animated::Brush::Gradient(gradient).to_model(); + Some(Draw { + stroke: Some(stroke.to_model()), + brush, + opacity: Value::Fixed(100.0), + }) + } + _ => None, + } +} + +fn conv_shape(value: &parser::schema::shapes::AnyShape) -> Option { + if let Some(draw) = conv_draw(value) { + return Some(crate::runtime::model::Shape::Draw(draw)); + } else if let Some(geometry) = conv_geometry(value) { + return Some(crate::runtime::model::Shape::Geometry(geometry)); + } + + match value { + schema::shapes::AnyShape::Group(value) => { + let mut shapes = vec![]; + let mut group_transform = None; + for item in &value.shapes { + match item { + schema::shapes::AnyShape::Transform(transform) => { + group_transform = Some(conv_shape_transform(transform)); + } + _ => { + if let Some(shape) = conv_shape(item) { + shapes.push(shape); + } + } + } + } + if !shapes.is_empty() { + Some(crate::runtime::model::Shape::Group(shapes, group_transform)) + } else { + None + } + } + // todo: implement repeater shape + // shapes::Shape::Repeater(value) => { + // let repeater = animated::Repeater { + // copies: conv_scalar(&value.copies), + // offset: conv_scalar(&value.offset), + // anchor_point: conv_point(&value.transform.anchor_point), + // position: conv_point(&value.transform.position), + // rotation: conv_scalar(&value.transform.rotation), + // scale: conv_vec2(&value.transform.scale), + // start_opacity: conv_scalar(&value.transform.start_opacity), + // end_opacity: conv_scalar(&value.transform.end_opacity), + // }; + // Some(Shape::Repeater(repeater.to_model())) + // } + _ => None, + } +} + +fn conv_geometry(value: &schema::shapes::AnyShape) -> Option { + use schema::shapes::AnyShape; + match value { + AnyShape::Ellipse(value) => { + let ellipse = animated::Ellipse { + is_ccw: false, /* todo: lottie schema does not have a field + * for this (anymore?) */ + position: conv_pos_point(&value.position), + size: conv_size(&value.size), + }; + Some(crate::runtime::model::Geometry::Ellipse(ellipse)) + } + AnyShape::Rectangle(value) => { + let rect = animated::Rect { + is_ccw: false, /* todo: lottie schema does not have a field + * for this (anymore?) */ + position: conv_pos_point(&value.position), + size: conv_size(&value.size), + corner_radius: conv_scalar(&value.rounded_corner_radius), + }; + Some(crate::runtime::model::Geometry::Rect(rect)) + } + AnyShape::Path(value) => conv_shape_geometry(&value.shape_property), + // todo: generic shape + _ => None, + } +} + +pub fn conv_shape_geometry( + value: &schema::animated_properties::shape_property::ShapeProperty, +) -> Option { + use schema::animated_properties::shape_property::ShapePropertyK::*; + let mut is_closed = false; + match &value.value { + Static(value) => { + let (points, is_closed) = conv_spline(value); + let mut path = vec![]; + points.as_slice().to_path(is_closed, &mut path); + Some(runtime::model::Geometry::Fixed(path)) + } + Animated(animated) => { + let mut frames = vec![]; + let mut values = vec![]; + for value in animated { + let hold = value + .base + .hold + .as_ref() + .map(|b| b.eq(&BoolInt::True)) + .unwrap_or(false); + frames.push(Time { + frame: value.base.time.unwrap_f32(), + in_tangent: value.base.in_tangent.as_ref().map(conv_keyframe_handle), + out_tangent: value.base.out_tangent.as_ref().map(conv_keyframe_handle), + hold, + }); + let (points, is_frame_closed) = conv_spline(value.start.first()?); + values.push(points); + is_closed |= is_frame_closed; + } + Some(runtime::model::Geometry::Spline(animated::Spline { + is_closed, + times: frames, + values, + })) + } + } +} + +pub fn conv_spline(value: &schema::helpers::bezier::Bezier) -> (Vec, bool) { + use core::iter::repeat; + let mut points = Vec::with_capacity(value.vertices.len() * 3); + let is_closed = value.closed.unwrap_or(false); + + for ((v, i), o) in value + .vertices + .iter() + .zip(value.in_tangents.iter().chain(repeat(&[ + serde_json::Number::from(0), + serde_json::Number::from(0), + ]))) + .zip(value.out_tangents.iter().chain(repeat(&[ + serde_json::Number::from(0), + serde_json::Number::from(0), + ]))) + { + points.push((v[0].unwrap_f64(), v[1].unwrap_f64()).into()); + points.push((i[0].unwrap_f64(), i[1].unwrap_f64()).into()); + points.push((o[0].unwrap_f64(), o[1].unwrap_f64()).into()); + } + (points, is_closed) +} + +pub fn conv_blend_mode( + value: &crate::parser::schema::constants::blend_mode::BlendMode, +) -> Option { + use crate::parser::schema::constants::blend_mode::BlendMode::*; + + Some(match value { + Normal => return None, + Multiply => BlendMode::from(Mix::Multiply), + Screen => BlendMode::from(Mix::Screen), + Overlay => BlendMode::from(Mix::Overlay), + Darken => BlendMode::from(Mix::Darken), + Lighten => BlendMode::from(Mix::Lighten), + ColorDodge => BlendMode::from(Mix::ColorDodge), + ColorBurn => BlendMode::from(Mix::ColorBurn), + HardLight => BlendMode::from(Mix::HardLight), + SoftLight => BlendMode::from(Mix::SoftLight), + Difference => BlendMode::from(Mix::Difference), + Exclusion => BlendMode::from(Mix::Exclusion), + Hue => BlendMode::from(Mix::Hue), + Saturation => BlendMode::from(Mix::Saturation), + Color => BlendMode::from(Mix::Color), + Luminosity => BlendMode::from(Mix::Luminosity), + Add => unimplemented!(), + HardMix => unimplemented!(), + }) +} + +pub fn conv_scalar( + float_value: &parser::schema::animated_properties::value::FloatValue, +) -> Value { + use crate::parser::schema::animated_properties::animated_property::AnimatedPropertyK::*; + match &float_value.animated_property.value { + Static(number) => Value::Fixed(number.unwrap_f32()), + AnimatedValue(keyframes) => { + let mut frames = vec![]; + let mut values = vec![]; + for keyframe in keyframes { + let start_time = keyframe.base.time.unwrap_f32(); + let data = keyframe.value[0].unwrap_f32(); + let hold = keyframe + .base + .hold + .as_ref() + .map(|b| b.eq(&BoolInt::True)) + .unwrap_or(false); + frames.push(crate::runtime::model::Time { + frame: start_time, + in_tangent: keyframe.base.in_tangent.as_ref().map(conv_keyframe_handle), + out_tangent: keyframe.base.out_tangent.as_ref().map(conv_keyframe_handle), + hold, + }); + values.push(data); + // todo: end_value deprecated but should we still push it if it + // exists? + } + Value::Animated(model::Animated { + times: frames, + values, + }) + } + } +} + +pub fn conv_multi( + multidimensional: &parser::schema::animated_properties::multi_dimensional::MultiDimensional, + f: impl Fn(&Vec) -> T, +) -> Value { + use crate::parser::schema::animated_properties::animated_property::AnimatedPropertyK::*; + + match &multidimensional.animated_property.value { + Static(components) => { + let value: Vec = components + .iter() + .map(|number| number.as_f64().unwrap()) + .collect(); + Value::Fixed(f(&value)) + } + AnimatedValue(keyframes) => { + let animated = conv_keyframes(keyframes.iter(), |k| { + let data = k + .value + .iter() + .map(|number| number.as_f64().unwrap()) + .collect::>(); + f(&data) + }); + animated + } + } +} + +pub fn conv_multi_color( + color: &parser::schema::animated_properties::color_value::ColorValue, + f: impl Fn(&Vec) -> T, +) -> Value { + use crate::parser::schema::animated_properties::animated_property::AnimatedPropertyK::*; + + match &color.animated_property.value { + Static(components) => { + let value: Vec = components + .iter() + .map(|number| number.as_f64().unwrap()) + .collect(); + Value::Fixed(f(&value)) + } + AnimatedValue(keyframes) => { + let animated = conv_keyframes(keyframes.iter(), |k| { + let data = k + .value + .iter() + .map(|number| number.as_f64().unwrap()) + .collect::>(); + f(&data) + }); + animated + } + } +} + +pub fn conv_pos( + position: &parser::schema::animated_properties::position::Position, + f: impl Fn(&Vec) -> T, +) -> Value { + use crate::parser::schema::animated_properties::position::PositionValueK::*; + + match &position.value { + Static(components) => { + let value: Vec = components + .iter() + .map(|number| number.as_f64().unwrap()) + .collect(); + Value::Fixed(f(&value)) + } + Animated(pos_keyframes) => { + // TODO: Are we using PositionKeyframes here how we're supposed to? + // there are in_tangents and out_tangents in addition to the keyframes. + let animated = conv_keyframes(pos_keyframes.iter().map(|pk| &pk.keyframe), |k| { + let data = k + .value + .iter() + .map(|number| number.as_f64().unwrap()) + .collect::>(); + f(&data) + }); + animated + } + } +} + +#[allow(clippy::get_first)] +pub fn conv_pos_point(value: &schema::animated_properties::position::Position) -> Value { + conv_pos(value, |x| { + Point::new( + x.get(0).copied().unwrap_or(0.0), + x.get(1).copied().unwrap_or(0.0), + ) + }) +} + +#[allow(clippy::get_first)] +pub fn conv_multi_point( + value: &schema::animated_properties::multi_dimensional::MultiDimensional, +) -> Value { + conv_multi(value, |x| { + Point::new( + x.get(0).copied().unwrap_or(0.0), + x.get(1).copied().unwrap_or(0.0), + ) + }) +} + +#[allow(clippy::get_first)] +pub fn conv_color(value: &schema::animated_properties::color_value::ColorValue) -> Value { + conv_multi_color(value, |x| { + Color::rgb( + x.get(0).copied().unwrap_or(0.0), + x.get(1).copied().unwrap_or(0.0), + x.get(2).copied().unwrap_or(0.0), + ) + }) +} + +#[allow(clippy::get_first)] +pub fn conv_vec2(value: &MultiDimensional) -> Value { + conv_multi(value, |x| { + Vec2::new( + x.get(0).copied().unwrap_or(0.0), + x.get(1).copied().unwrap_or(0.0), + ) + }) +} + +#[allow(clippy::get_first)] +pub fn conv_size(value: &MultiDimensional) -> Value { + conv_multi(value, |x| { + Size::new( + x.get(0).copied().unwrap_or(0.0), + x.get(1).copied().unwrap_or(0.0), + ) + }) +} diff --git a/src/import/defaults.rs b/src/import/defaults.rs new file mode 100644 index 0000000..f5f8ea9 --- /dev/null +++ b/src/import/defaults.rs @@ -0,0 +1,55 @@ +use crate::parser::schema::animated_properties::animated_property::{ + AnimatedProperty, AnimatedPropertyK, +}; +use crate::parser::schema::animated_properties::multi_dimensional::MultiDimensional; +use crate::parser::schema::animated_properties::position::Position; +use crate::parser::schema::animated_properties::value::FloatValue; +use crate::parser::schema::helpers::int_boolean::BoolInt; +use crate::parser::schema::{self}; +use once_cell::sync::Lazy; + +pub static FLOAT_VALUE_ZERO: Lazy = Lazy::new(|| FloatValue { + animated_property: AnimatedProperty { + property_index: None, + animated: Some(BoolInt::False), + expression: None, + slot_id: None, + value: AnimatedPropertyK::Static(serde_json::Number::from(0)), + }, +}); + +pub static FLOAT_VALUE_ONE_HUNDRED: Lazy = Lazy::new(|| FloatValue { + animated_property: AnimatedProperty { + property_index: None, + animated: Some(BoolInt::False), + expression: None, + slot_id: None, + value: AnimatedPropertyK::Static(serde_json::Number::from(100)), + }, +}); + +pub static MULTIDIM_ONE: Lazy = Lazy::new(|| MultiDimensional { + animated_property: AnimatedProperty { + property_index: None, + animated: Some(BoolInt::False), + expression: None, + slot_id: None, + value: AnimatedPropertyK::Static(vec![ + serde_json::Number::from(1), + serde_json::Number::from(1), + serde_json::Number::from(1), + ]), + }, + length: None, +}); + +pub static POSITION_ZERO: Lazy = Lazy::new(|| Position { + property_index: None, + animated: Some(BoolInt::False), + expression: None, + length: None, + value: schema::animated_properties::position::PositionValueK::Static(vec![ + serde_json::Number::from(0), + serde_json::Number::from(0), + ]), +}); diff --git a/src/import/mod.rs b/src/import/mod.rs new file mode 100644 index 0000000..abac045 --- /dev/null +++ b/src/import/mod.rs @@ -0,0 +1,88 @@ +use self::converters::conv_layer; +use self::util::NumberExt; +use crate::parser::{schema, Lottie}; +use crate::Composition; +use std::collections::HashMap; + +mod builders; +mod converters; +mod defaults; +mod util; + +pub fn import_composition( + source: impl AsRef<[u8]>, +) -> Result> { + let source = Lottie::from_slice(source.as_ref())?; + + let mut target = Composition { + frames: source.in_point.unwrap_f32()..source.out_point.unwrap_f32(), + frame_rate: source.frame_rate.unwrap_f32(), + width: source.width.unwrap_u32(), + height: source.height.unwrap_u32(), + assets: Default::default(), + layers: Default::default(), + }; + + // Collect assets and layers + let mut idmap: HashMap = HashMap::default(); + if let Some(assets) = source.assets { + for asset in assets { + match asset { + schema::assets::AnyAsset::Precomposition(precomp) => { + idmap.clear(); + let mut layers = vec![]; + let mut mask_layer = None; + for layer in precomp.composition.layers.iter() { + let index = layers.len(); + if let Some((mut layer, id, mask_blend)) = conv_layer(layer) { + if let (Some(mask_blend), Some(mask_layer)) = + (mask_blend, mask_layer.take()) + { + layer.mask_layer = Some((mask_blend, mask_layer)); + } + if layer.is_mask { + mask_layer = Some(index); + } + idmap.insert(id, index); + layers.push(layer); + } + } + for layer in &mut layers { + if let Some(parent) = layer.parent { + layer.parent = idmap.get(&parent).copied(); + } + } + target.assets.insert(precomp.asset.id.clone(), layers); + } + asset => { + unimplemented!("asset {:?} is not yet implemented", asset) + } + } + } + } + + idmap.clear(); + let mut layers = vec![]; + let mut mask_layer = None; + for layer in &source.layers { + let index = layers.len(); + if let Some((mut layer, id, mask_blend)) = conv_layer(layer) { + if let (Some(mask_blend), Some(mask_layer)) = (mask_blend, mask_layer.take()) { + layer.mask_layer = Some((mask_blend, mask_layer)); + } + if layer.is_mask { + mask_layer = Some(index); + } + idmap.insert(id, index); + layers.push(layer); + } + } + for layer in &mut layers { + if let Some(parent) = layer.parent { + layer.parent = idmap.get(&parent).copied(); + } + } + target.layers = layers; + + Ok(target) +} diff --git a/src/import/util.rs b/src/import/util.rs new file mode 100644 index 0000000..f3b207f --- /dev/null +++ b/src/import/util.rs @@ -0,0 +1,87 @@ +use crate::runtime::model::{Easing, Tweenable}; +use serde_json::Number; + +pub trait NumberExt { + fn unwrap_f32(&self) -> f32; + fn unwrap_f64(&self) -> f64; + fn unwrap_u32(&self) -> u32; +} + +impl NumberExt for serde_json::Number { + fn unwrap_f32(&self) -> f32 { + self.as_f64().expect("Could not get float from JSON Number") as f32 + } + + fn unwrap_f64(&self) -> f64 { + self.as_f64().expect("Could not get float from JSON Number") + } + + fn unwrap_u32(&self) -> u32 { + self.as_u64() + .expect("Could not get unsigned integer from JSON Number") as u32 + } +} + +pub fn normalize_to_range(a: f32, b: f32, x: f32) -> f32 { + if a == b { + // Avoid division by zero if a and b are the same + return 0.0; + } + + // Calculate the normalized value + (x - a) / (b - a) +} + +pub fn calc_stops(value: &[Number], count: usize) -> Vec<[f64; 5]> { + let mut stops: Vec<[f64; 5]> = Vec::new(); + let mut alpha_stops: Vec<(f32, f64)> = Vec::new(); + for chunk in value.chunks_exact(4) { + stops.push([ + chunk[0].unwrap_f64(), + chunk[1].unwrap_f64(), + chunk[2].unwrap_f64(), + chunk[3].unwrap_f64(), + 1.0, + ]); + if stops.len() >= count { + // there is alpha data at the end of the list, which is a sequence + // of (offset, alpha) pairs + for chunk in value.chunks_exact(2).skip(count * 2) { + let offset = chunk[0].unwrap_f32(); + let alpha = chunk[1].unwrap_f64(); + alpha_stops.push((offset, alpha)); + } + + for stop in stops.iter_mut() { + let mut last: Option<(f32, f64)> = None; + for &(b, alpha_b) in alpha_stops.iter() { + if let Some((a, alpha_a)) = last.take() { + let x = stop[0] as f32; + let t = normalize_to_range(a, b, x); + + let alpha_interp = alpha_a.ease(&alpha_b, t, &Easing::LERP); + let alpha_interp = if (x >= a && x <= b) && (t <= 0.25) && (x <= 0.1) { + alpha_a + } else { + alpha_interp + }; // todo: this is a hack to get alpha rendering with a + // falloff similar to lottiefiles' + + let alpha_interp = if (x >= a && x <= b) && (t >= 0.75) && (x >= 0.9) { + alpha_b + } else { + alpha_interp + }; // todo: this is a hack to get alpha rendering with a + // falloff similar to lottiefiles' + + stop[4] = stop[4].min(alpha_interp); + } + last = Some((b, alpha_b)); + } + } + break; + } + } + + stops +} diff --git a/src/lib.rs b/src/lib.rs index fc68cd0..0251f16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,46 +1,5 @@ -// Copyright 2023 the Velato Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT +pub mod import; +pub mod parser; +pub mod runtime; -/// Re-export vello. -pub use vello; - -pub mod model; - -mod import; -mod render; - -pub use render::{RenderSink, Renderer}; - -use std::collections::HashMap; -use std::ops::Range; - -/// 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: f32, - /// Width of the animation. - pub width: u32, - /// Height of the animation. - pub height: u32, - /// Precomposed layers that may be instanced. - pub assets: HashMap>, - /// Collection of layers. - pub layers: Vec, -} - -impl Composition { - /// Creates a new composition from the specified buffer containing - /// the content of a Lottie file. - pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result> { - import::import_composition(bytes) - } - - /// Returns a t value for the specified time in seconds. - pub fn frame_for_time(&self, secs: f32) -> f32 { - let frame = secs * self.frame_rate; - frame % self.frames.end - } -} +pub use runtime::{Composition, Renderer}; diff --git a/src/model/value.rs b/src/model/value.rs deleted file mode 100644 index 5304efc..0000000 --- a/src/model/value.rs +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2023 the Velato Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use vello::kurbo; -use vello::peniko; - -/// Fixed or animated value. -#[derive(Clone, Debug)] -pub enum Value { - /// Fixed value. - Fixed(T), - /// Animated value. - Animated(Animated), -} - -impl Value { - /// Returns true if the value is fixed. - pub fn is_fixed(&self) -> bool { - matches!(self, Self::Fixed(_)) - } - - /// Returns the value at a specified frame. - pub fn evaluate(&self, frame: f32) -> T { - match self { - Self::Fixed(fixed) => fixed.clone(), - Self::Animated(animated) => animated.evaluate(frame), - } - } -} - -impl Default for Value { - fn default() -> Self { - Self::Fixed(T::default()) - } -} - -/// Borrowed or owned value. -#[derive(Clone, Debug)] -pub enum ValueRef<'a, T> { - Borrowed(&'a T), - Owned(T), -} - -impl<'a, T> AsRef for ValueRef<'a, T> { - fn as_ref(&self) -> &T { - match self { - Self::Borrowed(value) => value, - Self::Owned(value) => value, - } - } -} - -impl<'a, T: Clone> ValueRef<'a, T> { - pub fn to_owned(self) -> T { - match self { - Self::Borrowed(value) => value.clone(), - Self::Owned(value) => value, - } - } -} - -/// Time for a particular keyframe, represented as a frame number. -#[derive(Copy, Clone, Default, Debug)] -pub struct Time { - /// Frame number. - pub frame: f32, -} - -impl Time { - /// Returns the frame indices and interpolation weight for the given frame. - pub(crate) fn frames_and_weight(times: &[Time], frame: f32) -> Option<([usize; 2], f32)> { - if times.is_empty() { - return None; - } - use core::cmp::Ordering::*; - let ix = match times.binary_search_by(|x| { - if x.frame < frame { - Less - } else if x.frame > frame { - Greater - } else { - Equal - } - }) { - Ok(ix) => ix, - Err(ix) => ix.saturating_sub(1), - }; - let ix0 = ix.min(times.len() - 1); - let ix1 = (ix0 + 1).min(times.len() - 1); - let t0 = times[ix0].frame; - let t1 = times[ix1].frame; - let t = (frame - t0) / (t1 - t0); - Some(([ix0, ix1], t.clamp(0.0, 1.0))) - } -} - -#[derive(Clone, Debug)] -pub struct Animated { - pub times: Vec