diff --git a/.changeset/orange-years-ring.md b/.changeset/orange-years-ring.md
new file mode 100644
index 00000000..f67de163
--- /dev/null
+++ b/.changeset/orange-years-ring.md
@@ -0,0 +1,5 @@
+---
+'@lottiefiles/dotlottie-web': patch
+---
+
+fix: 🐛 prevent `stop` event from triggering if playback is already stopped.
diff --git a/.changeset/slow-tools-bake.md b/.changeset/slow-tools-bake.md
new file mode 100644
index 00000000..75fd90a9
--- /dev/null
+++ b/.changeset/slow-tools-bake.md
@@ -0,0 +1,5 @@
+---
+'@lottiefiles/dotlottie-web': minor
+---
+
+refactor: rename default mode to `forward`
diff --git a/.changeset/soft-kids-chew.md b/.changeset/soft-kids-chew.md
new file mode 100644
index 00000000..9b1a8c5b
--- /dev/null
+++ b/.changeset/soft-kids-chew.md
@@ -0,0 +1,5 @@
+---
+'@lottiefiles/dotlottie-web': minor
+---
+
+feat: 🎸 `isPlaying`, `isPaused`, `isStopped` properties
diff --git a/.changeset/wise-cats-yell.md b/.changeset/wise-cats-yell.md
new file mode 100644
index 00000000..4ad5de45
--- /dev/null
+++ b/.changeset/wise-cats-yell.md
@@ -0,0 +1,5 @@
+---
+'@lottiefiles/dotlottie-web': minor
+---
+
+feat: 🎸 add `setSegments` method & `segments` config.
diff --git a/apps/dotlottie-web-example/src/main.ts b/apps/dotlottie-web-example/src/main.ts
index 2206d4cf..2b80924d 100644
--- a/apps/dotlottie-web-example/src/main.ts
+++ b/apps/dotlottie-web-example/src/main.ts
@@ -35,26 +35,39 @@ app.innerHTML = `
`;
@@ -96,6 +109,8 @@ fetch('/hamster.lottie')
loop: true,
autoplay: true,
mode: 'bounce',
+ segments: [10, 90],
+ speed: 1,
backgroundColor: 'purple',
});
@@ -110,13 +125,28 @@ fetch('/hamster.lottie')
const reloadButton = document.getElementById('reload') as HTMLButtonElement;
const jumpButton = document.getElementById('jump') as HTMLButtonElement;
const modeSelect = document.getElementById('mode') as HTMLSelectElement;
+ const startFrameInput = document.getElementById('startFrame') as HTMLInputElement;
+ const endFrameInput = document.getElementById('endFrame') as HTMLInputElement;
+ const setSegmentsButton = document.getElementById('setSegments') as HTMLButtonElement;
+
+ setSegmentsButton.addEventListener('click', () => {
+ const startFrame = parseInt(startFrameInput.value, 10);
+ const endFrame = parseInt(endFrameInput.value, 10);
+
+ dotLottie.setSegments(startFrame, endFrame);
+ });
modeSelect.addEventListener('change', () => {
dotLottie.setMode(modeSelect.value.toString() as Mode);
});
jumpButton.addEventListener('click', () => {
- const midFrame = dotLottie.totalFrames / 2;
+ if (!dotLottie.segments) return;
+
+ const startFrame = parseInt(dotLottie.segments[0].toString(), 10);
+ const endFrame = parseInt(dotLottie.segments[1].toString(), 10);
+
+ const midFrame = (endFrame - startFrame) / 2 + startFrame;
dotLottie.setFrame(midFrame);
});
@@ -137,7 +167,7 @@ fetch('/hamster.lottie')
});
playPauseButton.addEventListener('click', () => {
- if (dotLottie.playing) {
+ if (dotLottie.isPlaying) {
dotLottie.pause();
} else {
dotLottie.play();
diff --git a/apps/dotlottie-web-example/src/styles.css b/apps/dotlottie-web-example/src/styles.css
index 3ce7726c..93859c66 100644
--- a/apps/dotlottie-web-example/src/styles.css
+++ b/apps/dotlottie-web-example/src/styles.css
@@ -60,3 +60,27 @@ canvas {
width: 200px;
cursor: pointer;
}
+
+.segments-control {
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ margin: 20px 0;
+ padding: 10px;
+ background-color: #f0f0f0;
+ border-radius: 8px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+}
+
+.segments-control label {
+ margin-right: 10px;
+ font-weight: bold;
+}
+
+.segments-control input[type='number'] {
+ padding: 5px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ width: 60px;
+ margin: 0 10px;
+}
diff --git a/packages/web/README.md b/packages/web/README.md
index fc5933f5..3b833e7f 100644
--- a/packages/web/README.md
+++ b/packages/web/README.md
@@ -124,31 +124,36 @@ For this behavior to work correctly, the canvas element must be styled using CSS
The `DotLottie` constructor accepts a config object with the following properties:
-| Property name | Type | Required | Default | Description |
-| ----------------- | --------------------- | :------: | --------- | --------------------------------------------------------------------------------------------------- |
-| `autoplay` | boolean | | false | Auto-starts the animation on load. |
-| `loop` | boolean | | false | Determines if the animation should loop. |
-| `canvas` | HTMLCanvasElement | ✔️ | undefined | Canvas element for animation rendering. |
-| `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". |
-| `backgroundColor` | string | | undefined | Background color of the canvas. e.g., "#000000", "rgba(0, 0, 0, 0.5)" or "transparent". |
+| Property name | Type | Required | Default | Description |
+| ----------------- | --------------------- | :------: | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
+| `autoplay` | boolean | | false | Auto-starts the animation on load. |
+| `loop` | boolean | | false | Determines if the animation should loop. |
+| `canvas` | HTMLCanvasElement | ✔️ | undefined | Canvas element for animation rendering. |
+| `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". |
+| `backgroundColor` | string | | undefined | Background color of the canvas. e.g., "#000000", "rgba(0, 0, 0, 0.5)" or "transparent". |
+| `segments` | \[number, number] | | \[0, totalFrames - 1] | Animation segments. Accepts an array of two numbers, where the first number is the start frame and the second number is the end frame. |
### Properties
`DotLottie` instances expose the following 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 |
-| `direction` | string | Reflects the current playback direction; e.g., 1 would mean forward, -1 would mean reverse. |
+| 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. |
+| `direction` | string | Reflects the current playback direction; e.g., 1 would mean forward, -1 would mean reverse. |
+| `mode` | string | Reflects the current playback mode. |
+| `isPaused` | boolean | Reflects whether the animation is paused or not. |
+| `isStopped` | boolean | Reflects whether the animation is stopped or not. |
+| `isPlaying` | boolean | Reflects whether the animation is playing or not. |
+| `segments` | number | Reflects the frames range of the animations. where segments\[0] is the start frame and segments\[1] is the end frame. |
### Methods
@@ -167,6 +172,7 @@ The `DotLottie` constructor accepts a config object with the following propertie
| `destroy()` | Destroys the renderer instance and unregisters all event listeners. This method should be called when the canvas is removed from the DOM to prevent memory leaks. |
| `load(config: Config)` | Loads a new configuration or a new animation. |
| `setMode(mode: string)` | Sets the animation play mode. |
+| `setSegments(startFrame: number, endFrame: number)` | Sets the start and end frame of the animation. |
### Static Methods
diff --git a/packages/web/src/dotlottie.ts b/packages/web/src/dotlottie.ts
index 9708beb2..22e05845 100644
--- a/packages/web/src/dotlottie.ts
+++ b/packages/web/src/dotlottie.ts
@@ -13,7 +13,9 @@ import { getAnimationJSONFromDotLottie, loadAnimationJSONFromURL, debounce } fro
const MS_TO_SEC_FACTOR = 1000;
-export type Mode = 'normal' | 'reverse' | 'bounce' | 'bounce-reverse';
+export type Mode = 'forward' | 'reverse' | 'bounce' | 'bounce-reverse';
+
+type PlaybackState = 'playing' | 'paused' | 'stopped';
export interface Config {
/**
@@ -32,6 +34,8 @@ export interface Config {
* The animation data.
* string: The JSON string of the animation data.
* ArrayBuffer: The ArrayBuffer of the .lottie file.
+ *
+ * If the data is an ArrayBuffer, the JSON string will be extracted from the .lottie file.
*/
data?: string | ArrayBuffer;
/**
@@ -41,14 +45,30 @@ export interface Config {
/**
* The playback mode of the animation.
*
+ * forward: The animation will play from start to end.
+ * reverse: The animation will play from end to start.
+ * bounce: The animation will play from start to end and then from end to start.
+ * bounce-reverse: The animation will play from end to start and then from start to end.
+ *
*/
mode?: Mode;
+ /**
+ * The frame boundaries of the animation.
+ *
+ * The animation will only play between the given start and end frames.
+ *
+ * e.g. [0, 10] will play the first 10 frames of the animation only.
+ *
+ */
+ segments?: [number, number];
/**
* The speed of the animation.
*/
speed?: number;
/**
* The source URL of the animation.
+ *
+ * If the data is provided, the src will be ignored.
*/
src?: string;
}
@@ -62,8 +82,6 @@ export class DotLottie {
private _renderer: Renderer | null = null;
- private _playing = false;
-
private _beginTime = 0;
private _elapsedTime = 0;
@@ -82,7 +100,7 @@ export class DotLottie {
private _autoplay = false;
- private _mode: Mode = 'normal';
+ private _mode: Mode = 'forward';
private _direction = 1;
@@ -90,6 +108,10 @@ export class DotLottie {
private _animationFrameId?: number;
+ private _segments: [number, number] | null = null;
+
+ private _playbackState: PlaybackState = 'stopped';
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly
private _shouldAutoResizeCanvas = false;
@@ -107,7 +129,8 @@ export class DotLottie {
this._loop = config.loop ?? false;
this._speed = config.speed ?? 1;
this._autoplay = config.autoplay ?? false;
- this._mode = config.mode ?? 'normal';
+ this._mode = config.mode ?? 'forward';
+ this._segments = config.segments ?? null;
if (config.backgroundColor) {
this._canvas.style.backgroundColor = config.backgroundColor;
@@ -119,7 +142,7 @@ export class DotLottie {
this._canvasResizeObserver = new ResizeObserver(
debounce(() => {
this._resizeAnimationToCanvas();
- if (!this._playing) {
+ if (!this.isPlaying) {
this._render();
}
}, 100),
@@ -212,12 +235,24 @@ export class DotLottie {
}
/**
- * Gets the playing status of the animation.
+ * Gets the segments of the animation if any are set.
+ * Default is 0 to total frames. but if segments are set, it will be the start and end frames.
*
- * @returns The playing status of the animation.
*/
- public get playing(): boolean {
- return this._playing;
+ public get segments(): [number, number] | null {
+ return this._segments;
+ }
+
+ public get isPlaying(): boolean {
+ return this._playbackState === 'playing';
+ }
+
+ public get isPaused(): boolean {
+ return this._playbackState === 'paused';
+ }
+
+ public get isStopped(): boolean {
+ return this._playbackState === 'stopped';
}
// #endregion Getters and Setters
@@ -296,6 +331,10 @@ export class DotLottie {
if (this._renderer) {
this._totalFrames = this._renderer.totalFrames();
this._duration = this._renderer.duration();
+ this._segments = [
+ Math.max(0, this._segments?.[0] ?? 0),
+ Math.min(this._totalFrames - 1, this.segments?.[1] ?? this._totalFrames - 1),
+ ];
}
}
@@ -303,16 +342,22 @@ export class DotLottie {
* Renders the animation frame on the canvas.
*/
private _render(): void {
- if (!this._context) return;
+ if (!this._context || !this._renderer) {
+ return;
+ }
- this._renderer?.resize(this._canvas.width, this._canvas.height);
+ this._renderer.resize(this._canvas.width, this._canvas.height);
- if (this._renderer?.update()) {
+ if (this._renderer.update()) {
const buffer = this._renderer.render();
- const clampedBuffer = Uint8ClampedArray.from(buffer);
- if (clampedBuffer.length === 0) return;
+ if (buffer.length === 0) {
+ console.warn('Empty buffer received from renderer.');
+
+ return;
+ }
+ const clampedBuffer = new Uint8ClampedArray(buffer);
const imageData = new ImageData(clampedBuffer, this._canvas.width, this._canvas.height);
this._context.putImageData(imageData, 0, 0);
@@ -321,90 +366,94 @@ export class DotLottie {
/**
* Updates the current frame and animation state.
- * @returns Boolean indicating if update was successful.
+ * @returns Boolean indicating if the frame was updated.
*/
private _update(): boolean {
// animation is not loaded yet
- if (this._duration === 0) return false;
-
- this._elapsedTime = (performance.now() / MS_TO_SEC_FACTOR - this._beginTime) * this._speed;
- let frameProgress = (this._elapsedTime / 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;
- }
- }
-
- // ensure the frame is within the valid range
- this._currentFrame = Math.max(0, Math.min(this._currentFrame, this._totalFrames - 1));
+ if (this._duration === 0 || this._totalFrames === 0) return false;
- // 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._elapsedTime = 0;
+ const effectiveStartFrame = this._getEffectiveStartFrame();
+ const effectiveEndFrame = this._getEffectiveEndFrame();
+ const effectiveDuration = this._getEffectiveDuration();
+ const frameDuration = effectiveDuration / (effectiveEndFrame - effectiveStartFrame);
- if (this._mode === 'bounce' || this._mode === 'bounce-reverse') {
- this._direction *= -1;
- this._bounceCount += 1;
+ this._elapsedTime = (Date.now() / MS_TO_SEC_FACTOR - this._beginTime) * this._speed;
+ const frameProgress = this._elapsedTime / frameDuration;
- 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 });
+ // determine the current frame based on the animation mode and progress
+ if (this._mode === 'forward' || this._mode === 'reverse') {
+ this._currentFrame =
+ this._mode === 'forward' ? effectiveStartFrame + frameProgress : effectiveEndFrame - frameProgress;
+ } else {
+ // handle bounce or bounce-reverse mode
+ // eslint-disable-next-line no-lonely-if
+ if (this._direction === 1) {
+ this._currentFrame = effectiveStartFrame + frameProgress;
+ if (this._currentFrame >= effectiveEndFrame) {
+ this._currentFrame = effectiveEndFrame;
+ this._direction = -1;
+ this._beginTime = Date.now() / MS_TO_SEC_FACTOR;
}
} else {
- this._playing = false;
- this._eventManager.dispatch({ type: 'complete' });
-
- return false;
+ this._currentFrame = effectiveEndFrame - frameProgress;
+ if (this._currentFrame <= effectiveStartFrame) {
+ this._currentFrame = effectiveStartFrame;
+ this._direction = 1;
+ this._beginTime = Date.now() / MS_TO_SEC_FACTOR;
+ }
}
}
+ // clamp the current frame within the effective range and round it
+ this._currentFrame =
+ Math.round(Math.max(effectiveStartFrame, Math.min(this._currentFrame, effectiveEndFrame)) * 100) / 100;
+
+ let shouldUpdate = false;
+
if (this._renderer?.frame(this._currentFrame)) {
this._eventManager.dispatch({
type: 'frame',
currentFrame: this._currentFrame,
});
- return true;
+ shouldUpdate = true;
}
- return false;
+ // check if the animation should loop or complete
+ if (this._mode === 'forward' || this._mode === 'reverse') {
+ if (this._currentFrame >= effectiveEndFrame || this._currentFrame <= effectiveStartFrame) {
+ this._handleLoopOrCompletion();
+ }
+ } else if (this._currentFrame <= effectiveStartFrame || this._currentFrame >= effectiveEndFrame) {
+ this._bounceCount += 1;
+ if (this._bounceCount % 2 === 0) {
+ this._bounceCount = 0;
+ this._handleLoopOrCompletion();
+ }
+ }
+
+ return shouldUpdate;
+ }
+
+ /**
+ * Handles the loop or completion logic for the animation.
+ */
+ private _handleLoopOrCompletion(): void {
+ if (this._loop) {
+ this._loopCount += 1;
+ this._eventManager.dispatch({ type: 'loop', loopCount: this._loopCount });
+ this._beginTime = Date.now() / MS_TO_SEC_FACTOR;
+ } else {
+ this._playbackState = 'stopped';
+ this._eventManager.dispatch({ type: 'complete' });
+ }
}
/**
* Loop that handles the animation playback.
*/
private _animationLoop(): void {
- if (this._playing && this._update()) {
+ if (this.isPlaying && this._update()) {
this._render();
this._startAnimationLoop();
}
@@ -459,6 +508,52 @@ export class DotLottie {
}
this._animationFrameId = window.requestAnimationFrame(this._animationLoop);
}
+
+ public _getEffectiveStartFrame(): number {
+ return this._segments ? this._segments[0] : 0;
+ }
+
+ public _getEffectiveEndFrame(): number {
+ return this._segments ? this._segments[1] : this._totalFrames - 1;
+ }
+
+ public _getEffectiveTotalFrames(): number {
+ return this._segments ? this._segments[1] - this._segments[0] : this._totalFrames;
+ }
+
+ public _getEffectiveDuration(): number {
+ return this._segments
+ ? this._duration * ((this._segments[1] - this._segments[0]) / this._totalFrames)
+ : this._duration;
+ }
+
+ /**
+ * Synchronizes the animation timing based on the current frame, direction, and speed settings.
+ * This method calculates the appropriate begin time for the animation loop, ensuring that the
+ * animation's playback is consistent with the specified parameters.
+ *
+ * @param frame - The current frame number from which the animation timing will be synchronized.
+ * This frame number is used to calculate the correct position in the animation timeline.
+ *
+ * Usage:
+ * - This function should be called whenever there is a change in the frame, speed, or direction
+ * of the animation to maintain the correct timing.
+ * - It is used internally in methods like `play`, `setFrame`, and `setMode` to ensure that the
+ * animation's playback remains smooth and accurate.
+ */
+ private _synchronizeAnimationTiming(frame: number): void {
+ const effectiveDuration = this._getEffectiveDuration();
+ const effectiveTotalFrames = this._getEffectiveTotalFrames();
+ const frameDuration = effectiveDuration / effectiveTotalFrames;
+ let frameTime = (frame - this._getEffectiveStartFrame()) * frameDuration;
+
+ if (this._direction === -1) {
+ frameTime = effectiveDuration - frameTime;
+ }
+
+ this._beginTime = Date.now() / MS_TO_SEC_FACTOR - frameTime / this._speed;
+ }
+
// #endregion
// #region Public Methods
@@ -468,32 +563,29 @@ export class DotLottie {
*/
public play(): void {
if (this._totalFrames === 0) {
- this._eventManager.dispatch({
- type: 'loadError',
- error: new Error('Unable to play animation.'),
- });
+ console.error('Animation is not loaded yet.');
return;
}
- if (!this._playing) {
- // calculate the elapsed time based on the current frame and direction
- const frameDuration = this._duration / this._totalFrames;
- let frameTime = this._currentFrame * frameDuration;
-
- // adjust frame time based on the current direction
- if (this._direction === -1) {
- frameTime = this._duration - frameTime;
- }
+ const effectiveStartFrame = this._getEffectiveStartFrame();
+ const effectiveEndFrame = this._getEffectiveEndFrame();
- // set the begin time based on the current frame position
- this._beginTime = performance.now() / MS_TO_SEC_FACTOR - frameTime / this._speed;
+ // reset begin time and loop count if starting from the beginning
+ // eslint-disable-next-line no-negated-condition
+ if (this._playbackState !== 'paused') {
+ this._currentFrame =
+ this._mode === 'reverse' || this._mode === 'bounce-reverse' ? effectiveEndFrame : effectiveStartFrame;
+ this._beginTime = Date.now() / MS_TO_SEC_FACTOR;
+ } else {
+ this._synchronizeAnimationTiming(this._currentFrame);
+ }
- this._playing = true;
+ if (!this.isPlaying) {
+ this._playbackState = 'playing';
this._eventManager.dispatch({
type: 'play',
});
-
this._startAnimationLoop();
}
}
@@ -502,14 +594,23 @@ export class DotLottie {
* Stops the animation playback and resets the current frame.
*/
public stop(): void {
+ if (this.isStopped) return;
+
this._stopAnimationLoop();
- this._playing = false;
- this._loopCount = 0;
- this._direction = 1;
- this._currentFrame = 0;
+ this._playbackState = 'stopped';
this._bounceCount = 0;
- this._beginTime = 0;
- this.setFrame(0);
+
+ if (this._mode === 'reverse' || this._mode === 'bounce-reverse') {
+ this._currentFrame = this._getEffectiveEndFrame();
+ this._direction = -1;
+ } else {
+ this._currentFrame = this._getEffectiveStartFrame();
+ this._direction = 1;
+ }
+
+ this.setFrame(this._currentFrame);
+ this._render();
+
this._eventManager.dispatch({
type: 'stop',
});
@@ -519,9 +620,11 @@ export class DotLottie {
* Pauses the animation playback.
*/
public pause(): void {
- if (!this._playing) return;
+ if (this.isPaused) return;
+
+ this._stopAnimationLoop();
- this._playing = false;
+ this._playbackState = 'paused';
this._eventManager.dispatch({
type: 'pause',
@@ -539,9 +642,11 @@ export class DotLottie {
return;
}
- if (this._playing) {
+ if (this._speed === speed) return;
+
+ if (this.isPlaying) {
// recalculate the begin time based on the new speed to maintain the current position
- const currentTime = performance.now() / MS_TO_SEC_FACTOR;
+ const currentTime = Date.now() / MS_TO_SEC_FACTOR;
this._beginTime = currentTime - this._elapsedTime / speed;
}
@@ -562,34 +667,31 @@ export class DotLottie {
* @param frame - Frame number to set.
*/
public setFrame(frame: number): void {
- if (frame < 0 || frame >= this._totalFrames) {
- console.error(`Invalid frame number provided: ${frame}. Valid range is between 0 and ${this._totalFrames - 1}.`);
+ const effectiveStartFrame = this._getEffectiveStartFrame();
+ const effectiveEndFrame = this._getEffectiveEndFrame();
+
+ // validate the frame number within the effective frame range
+ if (frame < effectiveStartFrame || frame > effectiveEndFrame) {
+ console.error(
+ `Invalid frame number: ${frame}. It should be between ${effectiveStartFrame} and ${effectiveEndFrame}.`,
+ );
return;
}
this._currentFrame = frame;
- // if the animation is playing, adjust the begin time to maintain continuity
- if (this._playing) {
- const frameDuration = this._duration / this._totalFrames;
- let frameTime = frame * frameDuration;
-
- // adjust frame time based on the current direction
- if (this._direction === -1) {
- frameTime = this._duration - frameTime;
- }
-
- this._beginTime = performance.now() / MS_TO_SEC_FACTOR - frameTime / this._speed;
+ if (this.isPlaying) {
+ this._synchronizeAnimationTiming(frame);
}
- this._renderer?.frame(this._currentFrame);
- this._render();
-
- this._eventManager.dispatch({
- type: 'frame',
- currentFrame: this._currentFrame,
- });
+ if (this._renderer?.frame(this._currentFrame)) {
+ this._render();
+ this._eventManager.dispatch({
+ type: 'frame',
+ currentFrame: this._currentFrame,
+ });
+ }
}
/**
@@ -606,38 +708,46 @@ export class DotLottie {
this._bounceCount = 0;
this._direction = mode.includes('reverse') ? -1 : 1;
- const frameDuration = this._duration / this._totalFrames;
- let frameTime = this._currentFrame * frameDuration;
-
- // Adjust frame time based on the current direction
- if (this._direction === -1) {
- frameTime = this._duration - frameTime;
+ if (this.isPlaying) {
+ this._synchronizeAnimationTiming(this._currentFrame);
}
-
- // Set the begin time based on the current frame position
- this._beginTime = performance.now() / MS_TO_SEC_FACTOR - frameTime / this._speed;
}
public load(config: Omit): void {
- this._playing = false;
+ if (!this._renderer || !this._context) {
+ return;
+ }
+
+ if (!config.src && !config.data) {
+ console.error('Either "src" or "data" must be provided.');
+
+ return;
+ }
+
this._stopAnimationLoop();
+ this._playbackState = 'stopped';
this._loop = config.loop ?? false;
this._speed = config.speed ?? 1;
this._autoplay = config.autoplay ?? false;
+ this._mode = config.mode ?? 'forward';
+ this._segments = config.segments ?? null;
this._loopCount = 0;
- this._currentFrame = 0;
+ this._bounceCount = 0;
+ this._direction = this._mode.includes('reverse') ? -1 : 1;
+
+ // Set the initial frame based on the mode and segments
+ const effectiveStartFrame = this._getEffectiveStartFrame();
+ const effectiveEndFrame = this._getEffectiveEndFrame();
+
+ this._currentFrame =
+ this._mode === 'reverse' || this._mode === 'bounce-reverse' ? effectiveEndFrame : effectiveStartFrame;
+
+ // Reset other properties
this._beginTime = 0;
this._totalFrames = 0;
this._duration = 0;
- this._bounceCount = 0;
- this._direction = 1;
- this._mode = config.mode ?? 'normal';
- this._canvas.style.backgroundColor = '';
-
- if (config.backgroundColor) {
- this._canvas.style.backgroundColor = config.backgroundColor;
- }
+ this._canvas.style.backgroundColor = config.backgroundColor ?? '';
if (config.src) {
this._loadAnimationFromURL(config.src);
@@ -646,6 +756,41 @@ export class DotLottie {
}
}
+ public setSegments(startFrame: number, endFrame: number): void {
+ if (!this._renderer) {
+ console.error('Animation not initialized.');
+
+ return;
+ }
+
+ // Validate the frame range
+ if (startFrame < 0 || endFrame >= this._totalFrames || startFrame > endFrame) {
+ console.error('Invalid frame range.');
+
+ return;
+ }
+
+ this._segments = [startFrame, endFrame];
+
+ if (this._currentFrame < startFrame || this._currentFrame > endFrame) {
+ this._currentFrame = this._direction === 1 ? startFrame : endFrame;
+
+ // render the current frame
+ if (this._renderer.frame(this._currentFrame)) {
+ this._render();
+ this._eventManager.dispatch({
+ type: 'frame',
+ currentFrame: this._currentFrame,
+ });
+ }
+ }
+
+ // If playing, adjust the animation timing
+ if (this.isPlaying) {
+ this._synchronizeAnimationTiming(this._currentFrame);
+ }
+ }
+
/**
* Registers an event listener for a specific event type.
*
@@ -684,6 +829,7 @@ export class DotLottie {
this._canvasResizeObserver?.disconnect();
this._context = null;
this._renderer = null;
+ this._playbackState = 'stopped';
}
// #endregion