diff --git a/.changeset/three-horses-dream.md b/.changeset/three-horses-dream.md
new file mode 100644
index 00000000..2aa027de
--- /dev/null
+++ b/.changeset/three-horses-dream.md
@@ -0,0 +1,5 @@
+---
+'@lottiefiles/dotlottie-web': minor
+---
+
+feat: 🎸 add play mode
diff --git a/apps/dotlottie-web-example/src/main.ts b/apps/dotlottie-web-example/src/main.ts
index c496f0b0..8defccaf 100644
--- a/apps/dotlottie-web-example/src/main.ts
+++ b/apps/dotlottie-web-example/src/main.ts
@@ -13,7 +13,7 @@ const app = document.getElementById('app') as HTMLDivElement;
app.innerHTML = `
-
+
@@ -83,6 +83,7 @@ fetch('/hamster.lottie')
data,
loop: true,
autoplay: true,
+ mode: 'bounce',
});
const playPauseButton = document.getElementById('playPause') as HTMLButtonElement;
@@ -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();
});
diff --git a/packages/web/README.md b/packages/web/README.md
index c7bfd600..0ca8aafa 100644
--- a/packages/web/README.md
+++ b/packages/web/README.md
@@ -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
diff --git a/packages/web/src/dotlottie.ts b/packages/web/src/dotlottie.ts
index d6e1a13d..f55b51db 100644
--- a/packages/web/src/dotlottie.ts
+++ b/packages/web/src/dotlottie.ts
@@ -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.
@@ -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.
*/
@@ -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) => {
@@ -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.
*
@@ -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.
*
@@ -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.');
}
}
@@ -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',
@@ -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
@@ -327,16 +415,21 @@ 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();
}
}
@@ -344,9 +437,11 @@ export class DotLottie {
* 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',
@@ -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.');
+
+ return;
+ }
this._speed = speed;
}
@@ -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;
@@ -437,6 +536,7 @@ export class DotLottie {
*
*/
public destroy(): void {
+ this._stopAnimationLoop();
this._eventManager.removeAllEventListeners();
this._context = null;
this._renderer = null;