Skip to content

Commit

Permalink
Merge branch 'feat/dotlottie-support' of github.com:LottieFiles/dotlo…
Browse files Browse the repository at this point in the history
…ttie-thorvg into feat/dotlottie-support
  • Loading branch information
samuelOsborne committed Jan 17, 2024
2 parents 3192bcd + d1efbb2 commit b59e8af
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 47 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/check-pr.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
name: Check PR

on:
pull_request:
types: [opened]
branches: [main]

jobs:
check-pr:
if: github.head_ref != 'release'
runs-on: macos-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.9.1
with:
access_token: ${{ github.token }}
- uses: actions/checkout@v4
- uses: Homebrew/actions/setup-homebrew@master
- uses: maxim-lobanov/setup-xcode@v1
Expand Down
44 changes: 43 additions & 1 deletion dotlottie-ffi/emscripten_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,51 @@
using namespace emscripten;
using namespace dotlottie_player;

val getBuffer(DotLottiePlayer &player)
val buffer(DotLottiePlayer &player)
{
auto buffer_ptr = player.buffer_ptr();
auto buffer_len = player.buffer_len();
return val(typed_memory_view(buffer_len, reinterpret_cast<uint8_t *>(buffer_ptr)));
}

EMSCRIPTEN_BINDINGS(DotLottiePlayer)
{
enum_<Mode>("Mode")
.value("Forward", Mode::FORWARD)
.value("Reverse", Mode::REVERSE)
.value("Bounce", Mode::BOUNCE)
.value("ReverseBounce", Mode::REVERSE_BOUNCE);

value_object<Config>("Config")
.field("autoplay", &Config::autoplay)
.field("loop_animation", &Config::loop_animation)
.field("mode", &Config::mode)
.field("speed", &Config::speed)
.field("use_frame_interpolation", &Config::use_frame_interpolation);

class_<DotLottiePlayer>("DotLottiePlayer")
.constructor(&DotLottiePlayer::init, allow_raw_pointers())
.function("buffer_len", &DotLottiePlayer::buffer_len)
.function("buffer_ptr", &DotLottiePlayer::buffer_ptr)
.function("buffer", &buffer)
.function("clear", &DotLottiePlayer::clear)
.function("config", &DotLottiePlayer::config)
.function("current_frame", &DotLottiePlayer::current_frame)
.function("duration", &DotLottiePlayer::duration)
.function("is_loaded", &DotLottiePlayer::is_loaded)
.function("is_paused", &DotLottiePlayer::is_paused)
.function("is_playing", &DotLottiePlayer::is_playing)
.function("is_stopped", &DotLottiePlayer::is_stopped)
.function("load_animation_data", &DotLottiePlayer::load_animation_data, allow_raw_pointers())
.function("load_animation_path", &DotLottiePlayer::load_animation_path, allow_raw_pointers())
.function("loop_count", &DotLottiePlayer::loop_count)
.function("pause", &DotLottiePlayer::pause)
.function("play", &DotLottiePlayer::play)
.function("render", &DotLottiePlayer::render)
.function("request_frame", &DotLottiePlayer::request_frame)
.function("resize", &DotLottiePlayer::resize)
.function("set_config", &DotLottiePlayer::set_config)
.function("set_frame", &DotLottiePlayer::set_frame)
.function("stop", &DotLottiePlayer::stop)
.function("total_frames", &DotLottiePlayer::total_frames);
}
3 changes: 2 additions & 1 deletion dotlottie-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ name = "dotlottie_player_core"
[dependencies]
dotlottie_fms = { path = "../dotlottie-fms" }
thiserror = "1.0.48"
wasm-bindgen = "0.2.84"
# "emscripten-no-leading-underscore" branch fix this issue -> https://github.com/sebcrozet/instant/issues/35
instant = {git = "https://github.com/hoodmane/instant", branch = "emscripten-no-leading-underscore", features = ["inaccurate"]}

[build-dependencies]
bindgen = "0.69.1"
Expand Down
24 changes: 10 additions & 14 deletions dotlottie-rs/src/dotlottie_player.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::{sync::RwLock, time::SystemTime};
use instant::Instant;
use std::sync::RwLock;

use dotlottie_fms::DotLottieManager;

Expand Down Expand Up @@ -31,7 +32,7 @@ struct DotLottieRuntime {
renderer: LottieRenderer,
playback_state: PlaybackState,
is_loaded: bool,
start_time: SystemTime,
start_time: Instant,
loop_count: u32,
config: Config,
dotlottie_manager: DotLottieManager,
Expand All @@ -43,7 +44,7 @@ impl DotLottieRuntime {
renderer: LottieRenderer::new(),
playback_state: PlaybackState::Stopped,
is_loaded: false,
start_time: SystemTime::now(),
start_time: Instant::now(),
loop_count: 0,
config,
dotlottie_manager: DotLottieManager::new(None),
Expand Down Expand Up @@ -78,7 +79,7 @@ impl DotLottieRuntime {
pub fn play(&mut self) -> bool {
if self.is_loaded && !self.is_playing() {
self.playback_state = PlaybackState::Playing;
self.start_time = SystemTime::now();
self.start_time = Instant::now();

true
} else {
Expand Down Expand Up @@ -166,17 +167,12 @@ impl DotLottieRuntime {
return self.current_frame();
}

let current_time = SystemTime::now();
let elapsed_time = self.start_time.elapsed().as_secs_f32();

let elapsed_time = match current_time.duration_since(self.start_time) {
Ok(n) => n.as_millis(),
Err(_) => 0,
} as f32;

let duration = (self.duration() * 1000.0) / self.config.speed as f32;
let duration = self.duration() / self.config.speed;
let total_frames = self.total_frames() - 1.0;

let raw_next_frame = elapsed_time / duration * total_frames;
let raw_next_frame = (elapsed_time / duration) * total_frames;

let next_frame = if self.config.use_frame_interpolation {
raw_next_frame
Expand All @@ -195,7 +191,7 @@ impl DotLottieRuntime {
if next_frame >= total_frames {
if self.config.loop_animation {
self.loop_count += 1;
self.start_time = SystemTime::now();
self.start_time = Instant::now();
0.0
} else {
total_frames
Expand All @@ -208,7 +204,7 @@ impl DotLottieRuntime {
if next_frame <= 0.0 {
if self.config.loop_animation {
self.loop_count += 1;
self.start_time = SystemTime::now();
self.start_time = Instant::now();
total_frames
} else {
0.0
Expand Down
79 changes: 79 additions & 0 deletions node-example.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import createDotLottiePlayerModule from "./release/wasm/DotLottiePlayer.mjs";
import fs from "fs";
import path from "path";

const wasmBinary = fs.readFileSync(
path.join("./release/wasm/DotLottiePlayer.wasm")
);

const Module = await createDotLottiePlayerModule({
wasmBinary,
});

const WIDTH = 200;
const HEIGHT = 200;

const dotLottie = new Module.DotLottiePlayer({
autoplay: false,
loop_animation: false,
mode: Module.Mode.values[1],
speed: 1,
use_frame_interpolation: false,
});

const data = await fetch(
"https://lottie.host/647eb023-6040-4b60-a275-e2546994dd7f/zDCfp5lhLe.json"
).then((res) => res.text());

const loaded = dotLottie.load_animation_data(data, WIDTH, HEIGHT);

if (!loaded) {
console.log("failed to load animation data");
}

dotLottie.set_frame(10.0);
const rendered = dotLottie.render();

if (!rendered) {
console.log("failed to render");
}

const frameBuffer = dotLottie.buffer();

const bmpBuffer = createBMP(WIDTH, WIDTH, frameBuffer);

fs.writeFileSync("./output.bmp", bmpBuffer);

// This is for demonstration purposes only. to avoid adding a dependency
function createBMP(width, height, frameBuffer) {
// Each pixel in BMP is 4 bytes (BGRA)
const bmpDataSize = width * height * 4;
const headerSize = 54;
const fileSize = bmpDataSize + headerSize;
const bmpBuffer = Buffer.alloc(fileSize);

// Bitmap file header
bmpBuffer.write("BM", 0); // Signature
bmpBuffer.writeInt32LE(fileSize, 2); // File size
bmpBuffer.writeInt32LE(headerSize, 10); // Pixel data offset

// DIB header
bmpBuffer.writeInt32LE(40, 14); // DIB header size
bmpBuffer.writeInt32LE(width, 18); // Width
bmpBuffer.writeInt32LE(-height, 22); // Height (negative for top-down bitmap)
bmpBuffer.writeInt16LE(1, 26); // Color planes
bmpBuffer.writeInt16LE(32, 28); // Bits per pixel
bmpBuffer.writeInt32LE(0, 30); // Compression (0 for none)

// Convert RGBA to BGRA and write pixel data
for (let i = 0; i < width * height; i++) {
const rgbaIndex = i * 4;
const bgraIndex = headerSize + i * 4;
bmpBuffer[bgraIndex + 0] = frameBuffer[rgbaIndex + 2]; // Blue
bmpBuffer[bgraIndex + 1] = frameBuffer[rgbaIndex + 1]; // Green
bmpBuffer[bgraIndex + 2] = frameBuffer[rgbaIndex + 0]; // Red
bmpBuffer[bgraIndex + 3] = frameBuffer[rgbaIndex + 3]; // Alpha
}

return bmpBuffer;
}
59 changes: 29 additions & 30 deletions web-example.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
<title>WASM test</title>
</head>
<body>
<canvas width="500" height="500"></canvas>
<div style="max-width: 500px">
<canvas style="width: 100%"></canvas>
</div>
<script type="module">
import createModule from "./release/wasm/DotLottiePlayer.js";
import createDotLottiePlayerModule from "./release/wasm/DotLottiePlayer.js";

const module = await createModule({
const Module = await createDotLottiePlayerModule({
locateFile: (path, prefix) => {
if (path.endsWith(".wasm")) {
return `./release/wasm/${path}`;
Expand All @@ -19,49 +21,46 @@
},
});

const player = new module.DotLottiePlayer();
const dotLottie = new Module.DotLottiePlayer({
autoplay: true,
loop_animation: true,
mode: Module.Mode.values[1],
speed: 1,
use_frame_interpolation: true,
});

const data = await fetch(
"https://lottie.host/647eb023-6040-4b60-a275-e2546994dd7f/zDCfp5lhLe.json"
).then((res) => res.text());
const canvas = document.querySelector("canvas");

const width = canvas.width;
const height = canvas.height;
const { width: client_width, height: client_height } =
canvas.getBoundingClientRect();

const loaded = player.loadAnimation(data, width, height);
const width = client_width * (window.devicePixelRatio || 1);
const height = client_height * (window.devicePixelRatio || 1);

const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(width, height);
canvas.width = width;
canvas.height = height;

const duration = player.getDuration(); // duration in seconds
const totalFrames = player.getTotalFrame();
const fps = totalFrames / duration;
const timePerFrame = 1000 / fps; // time per frame in milliseconds
const loaded = dotLottie.load_animation_data(data, width, height);

let lastFrameTime = 0;

let currentFrame = 0;
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(width, height);

const animationLoop = (timestamp) => {
if (!lastFrameTime || timestamp - lastFrameTime >= timePerFrame) {
player.frame(currentFrame);
const buffer = player.getBuffer();
function animationLoop() {
const next_frame_number = dotLottie.request_frame();
dotLottie.set_frame(next_frame_number);
const rendered = dotLottie.render();

imageData.data.set(buffer);
if (rendered) {
const frame_buffer = dotLottie.buffer();
imageData.data.set(frame_buffer);
ctx.putImageData(imageData, 0, 0);

currentFrame++;

if (currentFrame >= totalFrames) {
currentFrame = 0;
}

lastFrameTime = timestamp;
}

requestAnimationFrame(animationLoop);
};
}

requestAnimationFrame(animationLoop);
</script>
Expand Down

0 comments on commit b59e8af

Please sign in to comment.