Skip to content

Commit

Permalink
feat(node-support): 🎸 Enable playing dotLottie animations in non-brow…
Browse files Browse the repository at this point in the history
…ser environments (#70)

* feat: 🎸 add resize method and renderConfig

* chore: 🤖 add changelog

* feat: 🎸 ability to run animations on non-browser environment

* chore: 🤖 update tsup target to neutral

* chore: 🤖 dotLottie to gif node example

* chore: 🤖 update pnpm-lock file

* chore: 🤖 add changelog

* chore: 🤖 update changelog

* chore: 🤖 remove unused utils bounce func

* chore: 🤖 apply suggested changes

* chore: 🤖 improve example

* chore: 🤖 update README
  • Loading branch information
theashraf authored Dec 6, 2023
1 parent 2e72aab commit 7bad1ec
Show file tree
Hide file tree
Showing 16 changed files with 499 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/large-masks-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lottiefiles/dotlottie-web': minor
---

feat(node-support): 🎸 Enable playing dotLottie animations in non-browser environments
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
*.json linguist-language=JSON-with-Comments

*.wasm binary
*.gif binary
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Discover how to implement and utilize the dotlottie-web packages with our exampl
Available examples:

* [dotlottie-web-example](apps/dotlottie-web-example/src/main.ts): A basic typescript example app of how to use `@lottiefiles/dotlottie-web` to render a Lottie or dotLottie animation in the browser.
* [dotlottie-web-node-example](apps/dotlottie-web-node-example/index.ts): This example demonstrates how to use the `@lottiefiles/dotlottie-web` in a Node.js environment. It showcases controlling animation playback, rendering frame by frame, and converting a dotLottie animation into a GIF file. for more information, see the [README](apps/dotlottie-web-node-example/README.md).

#### Running Examples

Expand Down
1 change: 1 addition & 0 deletions apps/dotlottie-web-node-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
output
39 changes: 39 additions & 0 deletions apps/dotlottie-web-node-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# DotLottie Web Node.js Example

## Overview

This example demonstrates how to use the DotLottie Web package in a Node.js environment. It showcases controlling animation playback, rendering frame by frame, and converting a Lottie animation into a GIF file.

## How to Run

To run the example with default settings, execute:

```bash
pnpm start
```

The output GIF will be saved to `./output/animation.gif`.

![Alt Text](./demo.gif)

To customize settings and explore different options, use the command line arguments as follows:

```bash
pnpm start --width [width] --height [height] --fps [fps] --repeat [repeat] --quality [quality] --input [input file or URL] --speed [speed]
```

### Arguments

* `--width`: Width of the output GIF (default: 200)
* `--height`: Height of the output GIF (default: 200)
* `--fps`: Frames per second for the output GIF (default: 60)
* `--repeat`: Number of times the GIF should repeat (default: 0 for infinite)
* `--input`: Path or URL to the Lottie animation file (default: example animation)
* `--speed`: Speed multiplier for the animation (default: 1)
* `--quality`: Quality of the output GIF ('high', 'mid', or 'low'; default: 'mid')

### Example Command

```bash
pnpm start --width 200 --height 200 --fps 30 --repeat 0 --quality high --input https://lottie.host/aaccfd1e-487e-4e9a-9d20-c57299089cfc/iVNpuLw0co.lottie --speed 1.5
```
Binary file added apps/dotlottie-web-node-example/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
131 changes: 131 additions & 0 deletions apps/dotlottie-web-node-example/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Copyright 2023 Design Barn Inc.
*/

import fs from 'node:fs';
import path from 'node:path';

import type { Config } from '@lottiefiles/dotlottie-web';
import { DotLottie } from '@lottiefiles/dotlottie-web';
import { createCanvas } from '@napi-rs/canvas';
import GIFEncoder from 'gif-encoder';
import minimist from 'minimist';

const wasmBase64 = fs.readFileSync('./node_modules/@lottiefiles/dotlottie-web/dist/renderer.wasm').toString('base64');
const wasmDataUri = `data:application/octet-stream;base64,${wasmBase64}`;

// This is only required for testing the local version of the renderer
DotLottie.setWasmUrl(wasmDataUri);

const rawArgs = minimist(process.argv.slice(2));

interface Args {
fps: number;
height: number;
input: string;
quality: 'high' | 'mid' | 'low';
repeat: number;
speed: number;
width: number;
}

const qualityMap = {
high: 1,
low: 20,
mid: 10,
};

const args: Args = {
width: Number(rawArgs['width']) || 200,
height: Number(rawArgs['height']) || 200,
fps: Number(rawArgs['fps']) || 60,
repeat: Number(rawArgs['repeat']) || 0,
input: rawArgs['input'] || 'https://lottie.host/195549aa-ad39-4c51-80ee-a8899c2264ee/cWdgpn8n7B.lottie',
speed: Number(rawArgs['speed']) || 1,
quality: rawArgs['quality'] || 'mid',
};

if (!args.input) {
console.error('No input file provided. Use --input to specify the Lottie animation file.');
process.exit(1);
}

const canvas = createCanvas(args.width, args.height);
const ctx = canvas.getContext('2d');

const dotLottieConfig: Config = {
speed: args.speed,
canvas: canvas as unknown as HTMLCanvasElement,
};

if (args.input.startsWith('http://') || args.input.startsWith('https://')) {
dotLottieConfig.src = args.input;
} else {
const filePath = path.resolve(args.input);

if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`);
process.exit(1);
}

dotLottieConfig.data = fs.readFileSync(filePath, 'utf-8');
}

const gif = new GIFEncoder(args.width, args.height);

const dotLottie = new DotLottie(dotLottieConfig);

const outputPath = path.resolve('./output');

dotLottie.addEventListener('load', () => {
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath);
}

const file = fs.createWriteStream(path.resolve(outputPath, 'animation.gif'));

gif.pipe(file);

gif.setRepeat(args.repeat);
gif.setFrameRate(args.fps);
gif.setQuality(qualityMap[args.quality]);

gif.writeHeader();

const frameRateAdjustedForSpeed = args.fps / dotLottie.speed;
const delay = 1 / frameRateAdjustedForSpeed;

console.info(
`
Recording GIF with the following settings:
- Width: ${args.width}
- Height: ${args.height}
- FPS: ${args.fps}
- Repeat: ${args.repeat}
- Animation Speed: ${args.speed}
- Quality: ${args.quality}
- Delay: ${delay}
- Total Frames: ${dotLottie.totalFrames}
- Duration: ${dotLottie.duration}
- Output Path: ${outputPath}
`,
);

for (let timeElapsed = 0; timeElapsed <= dotLottie.duration; timeElapsed += delay) {
const frameNo = (dotLottie.totalFrames - 1) * (timeElapsed / dotLottie.duration);

dotLottie.setFrame(frameNo);
}
});

dotLottie.addEventListener('frame', (event) => {
const frame = ctx.getImageData(0, 0, args.width, args.height).data;

if (Math.round(event.currentFrame) >= dotLottie.totalFrames - 1) {
console.log('Finished recording GIF');
gif.finish();
} else {
gif.addFrame(frame);
gif._read(1);
}
});
22 changes: 22 additions & 0 deletions apps/dotlottie-web-node-example/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@lottiefiles/dotlottie-web-node-example",
"version": "0.0.0",
"type": "module",
"license": "MIT",
"scripts": {
"build": "tsc --noEmit",
"start": "tsx index.ts"
},
"dependencies": {
"@lottiefiles/dotlottie-web": "workspace:*",
"@napi-rs/canvas": "^0.1.44",
"gif-encoder": "^0.7.2",
"minimist": "^1.2.8"
},
"devDependencies": {
"@types/gif-encoder": "^0.7.4",
"@types/minimist": "^1.2.5",
"@types/node": "^20.10.2",
"tsx": "^4.6.2"
}
}
12 changes: 12 additions & 0 deletions apps/dotlottie-web-node-example/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
// Extend from the build config
"extends": "../../tsconfig.build.json",

// Compiler options
"compilerOptions": {
// Source root directory
"rootDir": ".",

"resolveJsonModule": true
}
}
1 change: 1 addition & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@dotlottie/dotlottie-js": "^0.6.0"
},
"devDependencies": {
"@types/node": "^20.10.2",
"cross-env": "7.0.3",
"tsup": "7.2.0",
"typescript": "5.0.4"
Expand Down
64 changes: 64 additions & 0 deletions packages/web/src/animation-frame-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright 2023 Design Barn Inc.
*/

/* eslint-disable max-classes-per-file */

import { IS_BROWSER } from './constants';

interface AnimationFrameStrategy {
cancelAnimationFrame(id: number): void;
requestAnimationFrame(callback: (time: number) => void): number;
}

class WebAnimationFrameStrategy implements AnimationFrameStrategy {
public requestAnimationFrame(callback: (time: number) => void): number {
return window.requestAnimationFrame(callback);
}

public cancelAnimationFrame(id: number): void {
window.cancelAnimationFrame(id);
}
}

class NodeAnimationFrameStrategy implements AnimationFrameStrategy {
private _lastHandleId: number = 0;

private _lastImmediate: NodeJS.Immediate | null = null;

public requestAnimationFrame(callback: (time: number) => void): number {
if (this._lastHandleId >= Number.MAX_SAFE_INTEGER) {
this._lastHandleId = 0;
}

this._lastHandleId += 1;

this._lastImmediate = setImmediate(() => {
callback(Date.now());
});

return this._lastHandleId;
}

public cancelAnimationFrame(_id: number): void {
if (this._lastImmediate) {
clearImmediate(this._lastImmediate);
}
}
}

export class AnimationFrameManager {
private readonly _strategy: AnimationFrameStrategy;

public constructor() {
this._strategy = IS_BROWSER ? new WebAnimationFrameStrategy() : new NodeAnimationFrameStrategy();
}

public requestAnimationFrame(callback: (time: number) => void): number {
return this._strategy.requestAnimationFrame(callback);
}

public cancelAnimationFrame(id: number): void {
this._strategy.cancelAnimationFrame(id);
}
}
6 changes: 6 additions & 0 deletions packages/web/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Copyright 2023 Design Barn Inc.
*/

export const IS_BROWSER = typeof window === 'object';
export const MS_TO_SEC_FACTOR = 1000;
Loading

0 comments on commit 7bad1ec

Please sign in to comment.