diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index ce4b26173..d666db089 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -12,9 +12,11 @@ import buildStyle from '../../util/buildStyle'; import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck'; import useBackgroundMode from '../../hooks/useBackgroundMode'; import useOnChange from '../../hooks/useOnChange'; +import generateIdFor from '../../util/generateIdFor'; export type OwnProps = { ref?: RefObject; + id?: string; className?: string; style?: string; tgsUrl?: string; @@ -41,6 +43,8 @@ let RLottie: RLottieClass; // Time for the main interface to completely load const LOTTIE_LOAD_DELAY = 3000; +const ID_STORE = {}; + async function ensureLottie() { if (!lottiePromise) { lottiePromise = import('../../lib/rlottie/RLottie') as unknown as Promise; @@ -54,6 +58,7 @@ setTimeout(ensureLottie, LOTTIE_LOAD_DELAY); const AnimatedSticker: FC = ({ ref, + id, className, style, tgsUrl, @@ -97,8 +102,12 @@ const AnimatedSticker: FC = ({ return; } - const newAnimation = new RLottie( + const fullId = `${id || generateIdFor(ID_STORE, true)}_${size}${color ? `_${color.join(',')}` : ''}`; + + const newAnimation = RLottie.init( containerRef.current, + onLoad, + fullId, tgsUrl, { noLoop, @@ -107,7 +116,6 @@ const AnimatedSticker: FC = ({ isLowPriority, }, color, - onLoad, onEnded, onLoop, ); @@ -130,7 +138,7 @@ const AnimatedSticker: FC = ({ }); }); } - }, [color, animation, tgsUrl, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop]); + }, [color, animation, tgsUrl, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop, id]); useEffect(() => { if (!animation) return; @@ -139,9 +147,11 @@ const AnimatedSticker: FC = ({ }, [color, animation]); useEffect(() => { + const container = containerRef.current!; + return () => { if (animation) { - animation.destroy(); + animation.removeContainer(container); } }; }, [animation]); @@ -151,7 +161,7 @@ const AnimatedSticker: FC = ({ if (playSegmentRef.current) { animation.playSegment(playSegmentRef.current); } else { - animation.play(shouldRestart); + animation.play(shouldRestart, containerRef.current!); } } }, [animation]); @@ -161,7 +171,7 @@ const AnimatedSticker: FC = ({ return; } - animation.pause(); + animation.pause(containerRef.current!); }, [animation]); const freezeAnimation = useCallback(() => { @@ -229,7 +239,7 @@ const AnimatedSticker: FC = ({ }, [playAnimation, animation, tgsUrl]); useHeavyAnimationCheck(freezeAnimation, unfreezeAnimation, forceOnHeavyAnimation); - // Pausing frame may not happen in background + // Pausing frame may not happen in background, // so we need to make sure it happens right after focusing, // then we can play again. useBackgroundMode(freezeAnimation, unfreezeAnimationOnRaf); diff --git a/src/components/common/StickerView.tsx b/src/components/common/StickerView.tsx index 31b13323e..f462a6932 100644 --- a/src/components/common/StickerView.tsx +++ b/src/components/common/StickerView.tsx @@ -93,8 +93,8 @@ const StickerView: FC = ({ const [isPlayerReady, markPlayerReady] = useFlag(Boolean(isLottie && fullMediaData)); const isFullMediaReady = fullMediaData && (isStatic || isPlayerReady); + const thumbClassNames = useMediaTransition(thumbData && !isFullMediaReady); const fullMediaClassNames = useMediaTransition(isFullMediaReady); - const thumbClassNames = useMediaTransition(!isFullMediaReady); // Preload preview for Message Input and local message useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster); @@ -108,8 +108,9 @@ const StickerView: FC = ({ /> {isLottie ? ( (); const workers = new Array(MAX_WORKERS).fill(undefined).map( () => new WorkerConnector(new Worker(new URL('./rlottie.worker.ts', import.meta.url))), @@ -36,9 +36,15 @@ let lastWorkerIndex = -1; class RLottie { // Config - private imgSize!: number; + private containers = new Map(); - private key = generateIdFor(KEY_STORE); + private imgSize!: number; private msPerFrame = 1000 / 60; @@ -56,12 +62,6 @@ class RLottie { private chunksCount?: number; - // Container - - private canvas = document.createElement('canvas'); - - private ctx = this.canvas.getContext('2d')!; - // State private isAnimating = false; @@ -70,10 +70,10 @@ class RLottie { private isEnded = false; - private isOnLoadFired = false; - private isDestroyed = false; + private isRendererInited = false; + private approxFrameIndex = 0; private prevFrameIndex = -1; @@ -86,25 +86,59 @@ class RLottie { private lastRenderAt?: number; + static init(...args: ConstructorParameters) { + const [container, onLoad, id] = args; + let instance = instancesById.get(id); + + if (!instance) { + // eslint-disable-next-line prefer-rest-params + instance = new RLottie(...args); + instancesById.set(id, instance); + } else { + instance.addContainer(container, onLoad); + } + + return instance; + } + constructor( - private container: HTMLDivElement, + container: HTMLDivElement, + onLoad: NoneToVoidFunction | undefined, + private id: string, private tgsUrl: string, private params: Params = {}, private customColor?: [number, number, number], - private onLoad?: () => void, private onEnded?: (isDestroyed?: boolean) => void, private onLoop?: () => void, ) { - this.initContainer(); + this.addContainer(container, onLoad); this.initConfig(); this.initRenderer(); } + public removeContainer(container: HTMLDivElement) { + this.containers.get(container)!.canvas.remove(); + this.containers.delete(container); + + if (!this.containers.size) { + this.destroy(); + } + } + isPlaying() { return this.isAnimating || this.isWaiting; } - play(forceRestart = false) { + play(forceRestart = false, container?: HTMLDivElement) { + if (container) { + this.containers.get(container)!.isPaused = false; + + const areAllContainersPlaying = Array.from(this.containers.values()).every(({ isPaused }) => !isPaused); + if (!areAllContainersPlaying) { + return; + } + } + if (this.isEnded && forceRestart) { this.approxFrameIndex = Math.floor(0); } @@ -114,7 +148,16 @@ class RLottie { this.doPlay(); } - pause() { + pause(container?: HTMLDivElement) { + if (container) { + this.containers.get(container)!.isPaused = true; + + const areAllContainersPaused = Array.from(this.containers.values()).every(({ isPaused }) => isPaused); + if (!areAllContainersPaused) { + return; + } + } + if (this.isWaiting) { this.stopFrameIndex = this.approxFrameIndex; } else { @@ -140,15 +183,8 @@ class RLottie { this.params.noLoop = noLoop; } - destroy() { - this.isDestroyed = true; - this.pause(); - this.destroyRenderer(); - this.destroyContainer(); - } - - private initContainer() { - if (!(this.container.parentNode instanceof HTMLElement)) { + private addContainer(container: HTMLDivElement, onLoad?: NoneToVoidFunction) { + if (!(container.parentNode instanceof HTMLElement)) { throw new Error('[RLottie] Container is not mounted'); } @@ -156,9 +192,9 @@ class RLottie { if (!size) { size = ( - this.container.offsetWidth - || parseInt(this.container.style.width, 10) - || this.container.parentNode.offsetWidth + container.offsetWidth + || parseInt(container.style.width, 10) + || container.parentNode.offsetWidth ); if (!size) { @@ -166,19 +202,39 @@ class RLottie { } } - this.canvas.style.width = `${size}px`; - this.canvas.style.height = `${size}px`; + const canvas = document.createElement('canvas'); + canvas.dataset.id = this.id; + const ctx = canvas.getContext('2d')!; + + canvas.style.width = `${size}px`; + canvas.style.height = `${size}px`; const { isLowPriority, quality = isLowPriority ? LOW_PRIORITY_QUALITY : HIGH_PRIORITY_QUALITY } = this.params; // Reduced quality only looks acceptable on high DPR screens const imgSize = Math.round(size * Math.max(DPR * quality, 1)); - this.canvas.width = imgSize; - this.canvas.height = imgSize; + canvas.width = imgSize; + canvas.height = imgSize; - this.container.appendChild(this.canvas); + container.appendChild(canvas); - this.imgSize = imgSize; + if (!this.imgSize) { + this.imgSize = imgSize; + } + + this.containers.set(container, { canvas, ctx, onLoad }); + + if (this.isRendererInited) { + this.doPlay(); + } + } + + private destroy() { + this.isDestroyed = true; + this.pause(); + this.destroyRenderer(); + + instancesById.delete(this.id); } private initConfig() { @@ -188,26 +244,8 @@ class RLottie { this.chunkSize = CHUNK_SIZE; } - private destroyContainer() { - this.canvas.remove(); - } - setColor(newColor: [number, number, number] | undefined) { this.customColor = newColor; - - // TODO Remove? - if (this.customColor) { - const imageData = this.ctx.getImageData(0, 0, this.imgSize, this.imgSize); - const arr = imageData.data; - for (let i = 0; i < arr.length; i += 4) { - /* eslint-disable prefer-destructuring */ - arr[i] = this.customColor[0]; - arr[i + 1] = this.customColor[1]; - arr[i + 2] = this.customColor[2]; - /* eslint-enable prefer-destructuring */ - } - this.ctx.putImageData(imageData, 0, 0); - } } private initRenderer() { @@ -216,7 +254,7 @@ class RLottie { workers[this.workerIndex].request({ name: 'init', args: [ - this.key, + this.id, this.tgsUrl, this.imgSize, this.params.isLowPriority, @@ -228,11 +266,12 @@ class RLottie { private destroyRenderer() { workers[this.workerIndex].request({ name: 'destroy', - args: [this.key], + args: [this.id], }); } private onRendererInit(reduceFactor: number, msPerFrame: number, framesCount: number) { + this.isRendererInited = true; this.reduceFactor = reduceFactor; this.msPerFrame = msPerFrame; this.framesCount = framesCount; @@ -251,7 +290,7 @@ class RLottie { workers[this.workerIndex].request({ name: 'changeData', args: [ - this.key, + this.id, this.tgsUrl, this.params.isLowPriority, this.onChangeData.bind(this), @@ -297,7 +336,7 @@ class RLottie { } // Paused from outside - if (!this.isAnimating && this.isOnLoadFired) { + if (!this.isAnimating && Array.from(this.containers.values())[0]!.isOnLoadFired) { return false; } @@ -335,12 +374,23 @@ class RLottie { } } const imageData = new ImageData(arr, this.imgSize, this.imgSize); - this.ctx.putImageData(imageData, 0, 0); - if (this.onLoad && !this.isOnLoadFired) { - this.isOnLoadFired = true; - this.onLoad(); - } + this.containers.forEach((containerData) => { + const { + ctx, onLoad, isOnLoadFired, isPaused, + } = containerData; + + if (isPaused) { + return; + } + + ctx.putImageData(imageData, 0, 0); + + if (onLoad && !isOnLoadFired) { + containerData.isOnLoadFired = true; + onLoad(); + } + }); this.prevFrameIndex = frameIndex; } @@ -439,7 +489,7 @@ class RLottie { workers[this.workerIndex].request({ name: 'renderFrames', - args: [this.key, fromIndex, toIndex, this.onFrameLoad.bind(this)], + args: [this.id, fromIndex, toIndex, this.onFrameLoad.bind(this)], }); } diff --git a/src/util/generateIdFor.ts b/src/util/generateIdFor.ts index 2dda752e1..15f9deaec 100644 --- a/src/util/generateIdFor.ts +++ b/src/util/generateIdFor.ts @@ -1,11 +1,13 @@ -const generateIdFor = (store: AnyLiteral) => { +export default function generateIdFor(store: AnyLiteral, withAutoUpdate = false) { let id; do { id = String(Math.random()).replace('0.', 'id'); - } while (store.hasOwnProperty(id)); + } while (store[id]); + + if (withAutoUpdate) { + store[id] = true; + } return id; -}; - -export default generateIdFor; +}