Skip to content

Commit

Permalink
fix(react): cannot transfer control from a canvas more than once
Browse files Browse the repository at this point in the history
  • Loading branch information
theashraf committed Dec 14, 2024
1 parent 003e7b1 commit 4662d29
Show file tree
Hide file tree
Showing 17 changed files with 4,484 additions and 878 deletions.
66 changes: 55 additions & 11 deletions apps/dotlottie-next-example/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { DotLottie, DotLottieWorker } from '@lottiefiles/dotlottie-react';
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
import { Inter } from 'next/font/google';
import Head from 'next/head';
import { useState } from 'react';

import styles from '@/styles/Home.module.css';

Expand All @@ -9,6 +11,9 @@ const inter = Inter({ subsets: ['latin'] });
const src = 'https://lottie.host/e641272e-039b-4612-96de-138acfbede6e/bc0sW78EeR.lottie';

export default function Home(): JSX.Element {
const [dotLottie, setDotLottie] = useState<DotLottie | DotLottieWorker | null>(null);
const [showDotLottie, setShowDotLottie] = useState(false);

return (
<>
<Head>
Expand All @@ -18,17 +23,56 @@ export default function Home(): JSX.Element {
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={`${styles.main} ${inter.className}`}>
<DotLottieReact
style={{
minWidth: '100px',
}}
src={src}
loop
autoplay
renderConfig={{
autoResize: true,
}}
/>
{showDotLottie && (
<DotLottieReact
dotLottieRefCallback={setDotLottie}
style={{
minWidth: '100px',
}}
src={src}
loop
autoplay
renderConfig={{
autoResize: true,
}}
/>
)}
<div>
<button
onClick={(): void => {
setShowDotLottie(!showDotLottie);
}}
>
{showDotLottie ? 'Hide' : 'Show'}
</button>
<button
onClick={(): void => {
if (dotLottie) {
dotLottie.play();
}
}}
>
Play
</button>
<button
onClick={(): void => {
if (dotLottie) {
dotLottie.pause();
}
}}
>
Pause
</button>
<button
onClick={(): void => {
if (dotLottie) {
dotLottie.stop();
}
}}
>
Stop
</button>
</div>
</main>
</>
);
Expand Down
16 changes: 13 additions & 3 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"lint": "eslint --fix .",
"stats:eslint": "cross-env TIMING=1 eslint .",
"stats:ts": "tsc -p tsconfig.build.json --extendedDiagnostics",
"test": "vitest run --browser.headless",
"test:coverage": "vitest run --browser.headless --coverage",
"test:watch": "vitest",
"type-check": "tsc --noEmit"
},
"peerDependencies": {
Expand All @@ -47,11 +50,18 @@
"@lottiefiles/dotlottie-web": "workspace:*"
},
"devDependencies": {
"@types/react": "^19.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^18",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/browser": "2.1.0-beta.5",
"@vitest/coverage-istanbul": "2.1.0-beta.5",
"cross-env": "7.0.3",
"react": "^19.0.0",
"playwright": "1.45.2",
"react": "^18",
"tsup": "8.3.5",
"typescript": "5.0.4"
"typescript": "5.0.4",
"vitest": "2.1.0-beta.5",
"vitest-browser-react": "^0.0.4"
},
"publishConfig": {
"access": "public",
Expand Down
8 changes: 8 additions & 0 deletions packages/react/setup-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// eslint-disable-next-line import/no-unassigned-import
import 'vitest-browser-react';
import { setWasmUrl } from './src';

// eslint-disable-next-line node/no-unsupported-features/node-builtins
const wasmUrl = new URL('../web/src/core/dotlottie-player.wasm?url', import.meta.url).href;

setWasmUrl(wasmUrl);
248 changes: 248 additions & 0 deletions packages/react/src/base-dotlottie-react.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/* eslint-disable no-warning-comments */
'use client';

import type { Config, DotLottie, DotLottieWorker } from '@lottiefiles/dotlottie-web';
import { useState, useEffect, useCallback, useRef, type ComponentProps, type RefCallback } from 'react';
import type { JSX } from 'react';

export type BaseDotLottieProps<T extends DotLottie | DotLottieWorker> = Omit<Config, 'canvas'> &
ComponentProps<'canvas'> & {
animationId?: string;
/**
* A function that creates a `DotLottie` or `DotLottieWorker` instance.
*/
createDotLottie: (config: T extends DotLottieWorker ? Config & { workerId?: string } : Config) => T;
/**
* A callback function that receives the `DotLottie` or `DotLottieWorker` instance.
*
* @example
* ```tsx
* const [dotLottie, setDotLottie] = useState<DotLottie | null>(null);
*
* <DotLottieReact
* dotLottieRefCallback={setDotLottie}
* />
* ```
*/
dotLottieRefCallback?: RefCallback<T | null>;
/**
* @deprecated The `playOnHover` property is deprecated.
* Instead, use the `onMouseEnter` and `onMouseLeave` events to control animation playback.
* Utilize the `dotLottieRefCallback` to access the `DotLottie` instance and invoke the `play` and `pause` methods.
*
* Example usage:
* ```tsx
* const [dotLottie, setDotLottie] = useState<DotLottie | null>(null);
*
* <DotLottieReact
* dotLottieRefCallback={setDotLottie}
* onMouseEnter={() => dotLottie?.play()}
* onMouseLeave={() => dotLottie?.pause()}
* />
* ```
*/
playOnHover?: boolean;
themeData?: string;
workerId?: T extends DotLottieWorker ? string : undefined;
};

export const BaseDotLottieReact = <T extends DotLottie | DotLottieWorker>({
animationId,
autoplay,
backgroundColor,
className,
createDotLottie,
data,
dotLottieRefCallback,
loop,
mode,
playOnHover,
renderConfig,
segment,
speed,
src,
style,
themeData,
themeId,
useFrameInterpolation,
workerId,
...props
}: BaseDotLottieProps<T> & {
createDotLottie: (config: T extends DotLottieWorker ? Config & { workerId?: string } : Config) => T;
}): JSX.Element => {
const [dotLottie, setDotLottie] = useState<T | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const dotLottieRef = useRef<T | null>(null);
const dotLottieRefCallbackRef = useRef<RefCallback<T | null> | undefined>(dotLottieRefCallback);

const config: Omit<Config, 'canvas'> & {
workerId?: T extends DotLottieWorker ? string : undefined;
} = {
speed,
mode,
loop,
useFrameInterpolation,
segment,
backgroundColor,
autoplay,
themeId,
workerId,
src,
data,
renderConfig,
};

const configRef = useRef<Omit<BaseDotLottieProps<T>, 'createDotLottie' | 'dotLottieRefCallback'> | undefined>(config);

dotLottieRefCallbackRef.current = dotLottieRefCallback;
dotLottieRef.current = dotLottie;
configRef.current = config;

useEffect(() => {
if (typeof dotLottieRefCallbackRef.current === 'function' && dotLottie) {
dotLottieRefCallbackRef.current(dotLottie);
}
}, [dotLottie]);

const setCanvasRef = useCallback((canvas: HTMLCanvasElement | null) => {
canvasRef.current = canvas;

if (canvas) {
const dotLottieInstance = createDotLottie({
...configRef.current,
canvas,
});

setDotLottie(dotLottieInstance as T);
} else {
dotLottie?.destroy();
setDotLottie(null);
}
}, []);

useEffect(() => {
const handlePlayOnHover = (event: MouseEvent): void => {
if (!playOnHover) return;

if (event.type === 'mouseenter') {
dotLottieRef.current?.play();
}

if (event.type === 'mouseleave') {
dotLottieRef.current?.pause();
}
};

canvasRef.current?.addEventListener('mouseenter', handlePlayOnHover);
canvasRef.current?.addEventListener('mouseleave', handlePlayOnHover);

return () => {
canvasRef.current?.removeEventListener('mouseenter', handlePlayOnHover);
canvasRef.current?.removeEventListener('mouseleave', handlePlayOnHover);
};
}, [playOnHover]);

useEffect(() => {
return () => {
if (dotLottie) {
dotLottie.destroy();
setDotLottie(null);
}
};
}, [dotLottie]);

useEffect(() => {
dotLottieRef.current?.setSpeed(speed ?? 1);
}, [speed]);

useEffect(() => {
dotLottieRef.current?.setMode(mode ?? 'forward');
}, [mode]);

useEffect(() => {
dotLottieRef.current?.setLoop(loop ?? false);
}, [loop]);

useEffect(() => {
dotLottieRef.current?.setUseFrameInterpolation(useFrameInterpolation ?? true);
}, [useFrameInterpolation]);

useEffect(() => {
if (typeof segment?.[0] === 'number' && typeof segment[1] === 'number') {
dotLottieRef.current?.setSegment(segment[0], segment[1]);
} else {
// TODO: implement it for worker
// dotLottieRef.current?.resetSegment();
}
}, [segment]);

useEffect(() => {
dotLottieRef.current?.setBackgroundColor(backgroundColor ?? '');
}, [backgroundColor]);

useEffect(() => {
dotLottieRef.current?.setRenderConfig(renderConfig ?? {});
}, [JSON.stringify(renderConfig)]);

useEffect(() => {
if (typeof data !== 'string' && typeof data !== 'object') return;

dotLottieRef.current?.load({
data,
...configRef.current,
});
}, [data]);

useEffect(() => {
if (typeof src !== 'string') return;

dotLottieRef.current?.load({
src,
...configRef.current,
});
}, [src]);

useEffect(() => {
dotLottieRef.current?.setMarker(props.marker ?? '');
}, [props.marker]);

useEffect(() => {
dotLottieRef.current?.loadAnimation(animationId ?? '');
}, [animationId]);

useEffect(() => {
if (typeof themeId === 'string') {
dotLottieRef.current?.setTheme(themeId);
} else {
// TODO: implement it for worker
// dotLottieRef.current?.resetTheme();
}
}, [themeId]);

useEffect(() => {
dotLottieRef.current?.setThemeData(themeData ?? '');
}, [themeData]);

return (
<div
className={className}
{...(!className && {
style: {
width: '100%',
height: '100%',
lineHeight: 0,
...style,
},
})}
>
<canvas
ref={setCanvasRef}
style={{
width: '100%',
height: '100%',
}}
{...props}
/>
</div>
);
};
Loading

0 comments on commit 4662d29

Please sign in to comment.