Skip to content

Commit

Permalink
feat: 🎸 add play mode (#42)
Browse files Browse the repository at this point in the history
* feat: 🎸 add play mode

* chore: 🤖 fix typo in README
  • Loading branch information
theashraf authored Nov 20, 2023
1 parent 0e48249 commit 600aed1
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 41 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-horses-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lottiefiles/dotlottie-web': minor
---

feat: 🎸 add play mode
11 changes: 9 additions & 2 deletions apps/dotlottie-web-example/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const app = document.getElementById('app') as HTMLDivElement;

app.innerHTML = `
<div class="grid">
<canvas data-src="https://lottie.host/1cf72a35-6d88-4d9a-9961-f1bb88087f2c/miJIHiyH4Q.lottie" width="200px" height="200px"></canvas>
<canvas data-src="https://lottie.host/1cf72a35-6d88-4d9a-9961-f1bb88087f2c/miJIHiyH4Q.lottie" width="200px" height="200px"></canvas>
<canvas data-src="https://lottie.host/647eb023-6040-4b60-a275-e2546994dd7f/zDCfp5lhLe.json" width="200px" height="200px"></canvas>
<canvas data-src="https://lottie.host/a7421582-4733-49e5-9f77-e8d4cd792239/WZQjpo4uZR.lottie" width="200px" height="200px"></canvas>
<canvas data-src="https://lottie.host/e2a24b6f-df7f-4fc5-94ea-30f0846f85dc/1RLOR2g0m3.lottie" width="200px" height="200px"></canvas>
Expand Down Expand Up @@ -83,6 +83,7 @@ fetch('/hamster.lottie')
data,
loop: true,
autoplay: true,
mode: 'bounce',
});

const playPauseButton = document.getElementById('playPause') as HTMLButtonElement;
Expand Down Expand Up @@ -114,11 +115,17 @@ fetch('/hamster.lottie')
frameSlider.value = '0';
});

frameSlider.addEventListener('mousedown', () => {
dotLottie.pause();
});

frameSlider.addEventListener('input', () => {
const frame = frameSlider.valueAsNumber;

dotLottie.pause();
dotLottie.setFrame(frame);
});

frameSlider.addEventListener('mouseup', () => {
dotLottie.play();
});

Expand Down
20 changes: 11 additions & 9 deletions packages/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,20 @@ const dotLottie = new DotLottie({
| `src` | string | | undefined | URL to the animation data (`.json` or `.lottie`). |
| `speed` | number | | 1 | Animation playback speed. 1 is regular speed. |
| `data` | string \| ArrayBuffer | | undefined | Animation data provided either as a Lottie JSON string or as an ArrayBuffer for .lottie animations. |
| `mode` | string | | "normal" | Animation play mode. Accepts "normal", "reverse", "bounce", "bounce-reverse". |

### Properties

| Property | Type | Description |
| -------------- | ------- | ---------------------------------------------------------------------- |
| `currentFrame` | number | Represents the animation's currently displayed frame number. |
| `duration` | number | Specifies the animation's total playback time in milliseconds. |
| `totalFrames` | number | Denotes the total count of individual frames within the animation. |
| `loop` | boolean | Indicates if the animation is set to play in a continuous loop. |
| `speed` | number | Represents the playback speed factor; e.g., 2 would mean double speed. |
| `loopCount` | number | Tracks how many times the animation has completed its loop. |
| `playing` | boolean | Reflects whether the animation is in active playback or not |
| Property | Type | Description |
| -------------- | ------- | ------------------------------------------------------------------------------------------- |
| `currentFrame` | number | Represents the animation's currently displayed frame number. |
| `duration` | number | Specifies the animation's total playback time in milliseconds. |
| `totalFrames` | number | Denotes the total count of individual frames within the animation. |
| `loop` | boolean | Indicates if the animation is set to play in a continuous loop. |
| `speed` | number | Represents the playback speed factor; e.g., 2 would mean double speed. |
| `loopCount` | number | Tracks how many times the animation has completed its loop. |
| `playing` | boolean | Reflects whether the animation is in active playback or not |
| `direction` | string | Reflects the current playback direction; e.g., 1 would mean forward, -1 would mean reverse. |

### Methods

Expand Down
160 changes: 130 additions & 30 deletions packages/web/src/dotlottie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { getAnimationJSONFromDotLottie, loadAnimationJSONFromURL } from './utils

const MS_TO_SEC_FACTOR = 1000;

export type Mode = 'normal' | 'reverse' | 'bounce' | 'bounce-reverse';

export interface Options {
/**
* Boolean indicating if the animation should start playing automatically.
Expand All @@ -33,6 +35,10 @@ export interface Options {
* Boolean indicating if the animation should loop.
*/
loop?: boolean;
/**
* The playback mode of the animation.
*/
mode?: Mode;
/**
* The speed of the animation.
*/
Expand Down Expand Up @@ -70,14 +76,27 @@ export class DotLottie {

private _autoplay = false;

private _mode: Mode = 'normal';

private _direction = 1;

private _bounceCount = 0;

private _animationFrameId?: number;

public constructor(config: Options) {
this._animationLoop = this._animationLoop.bind(this);

this._canvas = config.canvas;
this._context = this._canvas.getContext('2d');
if (!this._context) {
throw new Error('2D context not supported or canvas already initialized with another context type.');
}

this._loop = config.loop ?? false;
this._speed = config.speed ?? 1;
this._autoplay = config.autoplay ?? false;
this._mode = config.mode ?? 'normal';

WasmLoader.load()
.then((module) => {
Expand All @@ -98,6 +117,16 @@ export class DotLottie {
}

// #region Getters and Setters

/**
* Gets the current direction of the animation.
*
* @returns The current direction of the animation.
*/
public get direction(): number {
return this._direction;
}

/**
* Gets the current frame number.
*
Expand Down Expand Up @@ -160,8 +189,9 @@ export class DotLottie {
public get playing(): boolean {
return this._playing;
}
// #endregion
// #endregion Getters and Setters

// #region Private Methods
/**
* Loads and initializes the animation from a given URL.
*
Expand Down Expand Up @@ -221,6 +251,8 @@ export class DotLottie {
error: error as Error,
});
});
} else {
console.error('Unsupported data type for animation data. Expected a string or ArrayBuffer.');

Check warning on line 255 in packages/web/src/dotlottie.ts

View workflow job for this annotation

GitHub Actions / validate

Unexpected console statement
}
}

Expand Down Expand Up @@ -259,36 +291,69 @@ export class DotLottie {
* @returns Boolean indicating if update was successful.
*/
private _update(): boolean {
if (!this._playing) return false;
// animation is not loaded yet
if (this._duration === 0) return false;

const timeElapsed = (performance.now() / MS_TO_SEC_FACTOR - this._beginTime) * this._speed;
let frameProgress = (timeElapsed / this._duration) * this._totalFrames;

if (this._mode === 'normal') {
this._currentFrame = frameProgress;
} else if (this._mode === 'reverse') {
this._currentFrame = this._totalFrames - frameProgress - 1;
} else if (this._mode === 'bounce') {
if (this._direction === -1) {
frameProgress = this._totalFrames - frameProgress - 1;
}
this._currentFrame = frameProgress;
} else {
// bounce-reverse mode
if (this._direction === -1) {
frameProgress = this._totalFrames - frameProgress - 1;
}
this._currentFrame = frameProgress;
if (this._bounceCount === 0) {
this._direction = -1;
}
}

this._currentFrame =
(((performance.now() / MS_TO_SEC_FACTOR - this._beginTime) * this._speed) / this._duration) * this._totalFrames;
// ensure the frame is within the valid range
this._currentFrame = Math.max(0, Math.min(this._currentFrame, this._totalFrames - 1));

if (this._currentFrame >= this._totalFrames) {
if (this._loop) {
this._currentFrame = 0;
// handle animation looping or completion
if (this._currentFrame >= this._totalFrames - 1 || this._currentFrame <= 0) {
if (this._loop || this._mode === 'bounce' || this._mode === 'bounce-reverse') {
this._beginTime = performance.now() / MS_TO_SEC_FACTOR;
this._loopCount += 1;

this._eventManager.dispatch({
type: 'loop',
loopCount: this._loopCount,
});

return true;
if (this._mode === 'bounce' || this._mode === 'bounce-reverse') {
this._direction *= -1;
this._bounceCount += 1;

if (this._bounceCount >= 2) {
this._bounceCount = 0;
if (!this._loop) {
this._playing = false;
this._bounceCount = 0;
this._direction = 1;
this._eventManager.dispatch({ type: 'complete' });

return false;
}
this._loopCount += 1;
this._eventManager.dispatch({ type: 'loop', loopCount: this._loopCount });
}
} else {
this._loopCount += 1;
this._eventManager.dispatch({ type: 'loop', loopCount: this._loopCount });
}
} else {
this._playing = false;

this._eventManager.dispatch({
type: 'complete',
});
this._eventManager.dispatch({ type: 'complete' });

return false;
}
}

this._currentFrame = Math.max(0, Math.min(this._currentFrame, this._totalFrames - 1));

if (this._renderer?.frame(this._currentFrame)) {
this._eventManager.dispatch({
type: 'frame',
Expand All @@ -305,10 +370,33 @@ export class DotLottie {
* Loop that handles the animation playback.
*/
private _animationLoop(): void {
if (this._update()) {
if (this._playing && this._update()) {
this._render();
window.requestAnimationFrame(this._animationLoop);
this._startAnimationLoop();
}
}

/**
* Stops the animation loop.
*
* This is used to ensure that the animation loop is only stopped once.
*/
public _stopAnimationLoop(): void {
if (this._animationFrameId) {
window.cancelAnimationFrame(this._animationFrameId);
}
}

/**
* Starts the animation loop.
*
* This is used to ensure that the animation loop is only started once.
*/
public _startAnimationLoop(): void {
if (this._animationFrameId) {
window.cancelAnimationFrame(this._animationFrameId);
}
this._animationFrameId = window.requestAnimationFrame(this._animationLoop);
}
// #endregion

Expand All @@ -327,26 +415,33 @@ export class DotLottie {
return;
}

const progress = this._currentFrame / this._totalFrames;
const currentProgress = this._currentFrame / this._totalFrames;

if (this._direction === -1) {
this._beginTime = performance.now() / MS_TO_SEC_FACTOR - this._duration * (1 - currentProgress);
} else {
this._beginTime = performance.now() / MS_TO_SEC_FACTOR - this._duration * currentProgress;
}

this._beginTime = performance.now() / 1000 - progress * this._duration;
if (!this._playing) {
this._playing = true;
this._animationLoop();

this._eventManager.dispatch({
type: 'play',
});

this._startAnimationLoop();
}
}

/**
* Stops the animation playback and resets the current frame.
*/
public stop(): void {
if (!this._playing && this._currentFrame === 0) return;

this._playing = false;
this._loopCount = 0;
this._direction = 1;
this._currentFrame = 0;
this._bounceCount = 0;
this._beginTime = 0;
this.setFrame(0);
this._eventManager.dispatch({
type: 'stop',
Expand All @@ -371,6 +466,11 @@ export class DotLottie {
* @param speed - Speed multiplier for playback.
*/
public setSpeed(speed: number): void {
if (speed <= 0) {
console.error('Speed must be a positive number.');

Check warning on line 470 in packages/web/src/dotlottie.ts

View workflow job for this annotation

GitHub Actions / validate

Unexpected console statement

return;
}
this._speed = speed;
}

Expand All @@ -388,7 +488,6 @@ export class DotLottie {
*/
public setFrame(frame: number): void {
if (frame < 0 || frame >= this._totalFrames) {
// eslint-disable-next-line no-console
console.error(`Invalid frame number provided: ${frame}. Valid range is between 0 and ${this._totalFrames - 1}.`);

return;
Expand Down Expand Up @@ -437,6 +536,7 @@ export class DotLottie {
*
*/
public destroy(): void {
this._stopAnimationLoop();
this._eventManager.removeAllEventListeners();
this._context = null;
this._renderer = null;
Expand Down

0 comments on commit 600aed1

Please sign in to comment.