Animated Sticker: Use single instance for same stickers
This commit is contained in:
parent
1fda24ab92
commit
4c97d37439
@ -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<HTMLDivElement>;
|
||||
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<RLottieClass>;
|
||||
@ -54,6 +58,7 @@ setTimeout(ensureLottie, LOTTIE_LOAD_DELAY);
|
||||
|
||||
const AnimatedSticker: FC<OwnProps> = ({
|
||||
ref,
|
||||
id,
|
||||
className,
|
||||
style,
|
||||
tgsUrl,
|
||||
@ -97,8 +102,12 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
isLowPriority,
|
||||
},
|
||||
color,
|
||||
onLoad,
|
||||
onEnded,
|
||||
onLoop,
|
||||
);
|
||||
@ -130,7 +138,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [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<OwnProps> = ({
|
||||
}, [color, animation]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current!;
|
||||
|
||||
return () => {
|
||||
if (animation) {
|
||||
animation.destroy();
|
||||
animation.removeContainer(container);
|
||||
}
|
||||
};
|
||||
}, [animation]);
|
||||
@ -151,7 +161,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
if (playSegmentRef.current) {
|
||||
animation.playSegment(playSegmentRef.current);
|
||||
} else {
|
||||
animation.play(shouldRestart);
|
||||
animation.play(shouldRestart, containerRef.current!);
|
||||
}
|
||||
}
|
||||
}, [animation]);
|
||||
@ -161,7 +171,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
animation.pause();
|
||||
animation.pause(containerRef.current!);
|
||||
}, [animation]);
|
||||
|
||||
const freezeAnimation = useCallback(() => {
|
||||
@ -229,7 +239,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
}, [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);
|
||||
|
||||
@ -93,8 +93,8 @@ const StickerView: FC<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
/>
|
||||
{isLottie ? (
|
||||
<AnimatedSticker
|
||||
key={customColor?.join(',')}
|
||||
id={id}
|
||||
size={size}
|
||||
key={fullMediaData}
|
||||
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames)}
|
||||
tgsUrl={fullMediaData}
|
||||
play={shouldPlay}
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
import WorkerConnector from '../../util/WorkerConnector';
|
||||
import { animate } from '../../util/animation';
|
||||
import cycleRestrict from '../../util/cycleRestrict';
|
||||
import generateIdFor from '../../util/generateIdFor';
|
||||
|
||||
interface Params {
|
||||
noLoop?: boolean;
|
||||
@ -26,7 +25,8 @@ const HIGH_PRIORITY_QUALITY = IS_SINGLE_COLUMN_LAYOUT ? 0.75 : 1;
|
||||
const LOW_PRIORITY_QUALITY = IS_ANDROID ? 0.5 : 0.75;
|
||||
const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4;
|
||||
const LOW_PRIORITY_CACHE_MODULO = 0;
|
||||
const KEY_STORE = {};
|
||||
|
||||
const instancesById = new Map<string, RLottie>();
|
||||
|
||||
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<HTMLDivElement, {
|
||||
canvas: HTMLCanvasElement;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
onLoad?: NoneToVoidFunction;
|
||||
isOnLoadFired?: boolean;
|
||||
isPaused?: boolean;
|
||||
}>();
|
||||
|
||||
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<typeof RLottie>) {
|
||||
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)],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user