From dfe7b80a6be8361b495372e3840bf885ec51af6e Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sun, 13 Nov 2022 17:06:12 +0400 Subject: [PATCH] Properly handle shared canvas resize --- src/components/common/AnimatedSticker.tsx | 8 ++- src/components/common/CustomEmoji.module.scss | 2 +- src/hooks/useSharedCanvasCoords.ts | 40 +++++++++-- src/lib/rlottie/RLottie.ts | 66 +++++++++++++++---- 4 files changed, 94 insertions(+), 22 deletions(-) diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index 59bd48bf6..38ed54427 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -209,11 +209,17 @@ const AnimatedSticker: FC = ({ }, [unfreezeAnimation]); useOnChange(([prevNoLoop]) => { - if (noLoop !== undefined && noLoop !== prevNoLoop) { + if (prevNoLoop !== undefined && noLoop !== prevNoLoop) { animation?.setNoLoop(noLoop); } }, [noLoop, animation]); + useOnChange(([prevSharedCanvasCoords]) => { + if (prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords) { + animation?.setSharedCanvasCoords(containerId, sharedCanvasCoords); + } + }, [sharedCanvasCoords, containerId, animation]); + useEffect(() => { if (!animation) { return; diff --git a/src/components/common/CustomEmoji.module.scss b/src/components/common/CustomEmoji.module.scss index cbb961395..48aeb85b2 100644 --- a/src/components/common/CustomEmoji.module.scss +++ b/src/components/common/CustomEmoji.module.scss @@ -20,6 +20,7 @@ .thumb { width: 100%; height: 100%; + pointer-events: none; } .media { @@ -42,7 +43,6 @@ left: 0; width: 100%; height: 100%; - z-index: 1; user-select: auto !important; } diff --git a/src/hooks/useSharedCanvasCoords.ts b/src/hooks/useSharedCanvasCoords.ts index 2699e7ce9..93fabeac8 100644 --- a/src/hooks/useSharedCanvasCoords.ts +++ b/src/hooks/useSharedCanvasCoords.ts @@ -1,4 +1,8 @@ -import { useEffect, useMemo, useState } from '../lib/teact/teact'; +import { + useCallback, useEffect, useMemo, useState, +} from '../lib/teact/teact'; +import { throttle } from '../util/schedulers'; +import { round } from '../util/math'; export default function useSharedCanvasCoords( containerRef: React.RefObject, @@ -7,20 +11,42 @@ export default function useSharedCanvasCoords( const [x, setX] = useState(); const [y, setY] = useState(); - useEffect(() => { - if (!sharedCanvasRef?.current) { + const recalculate = useCallback(() => { + const container = containerRef.current; + const canvas = sharedCanvasRef?.current; + if (!container || !canvas) { return; } - const container = containerRef.current!; const target = container.classList.contains('sticker-set-cover') ? container : container.querySelector('img')!; const targetBounds = target.getBoundingClientRect(); - const canvasBounds = sharedCanvasRef!.current!.getBoundingClientRect(); + const canvasBounds = canvas.getBoundingClientRect(); // Factor coords are used to support rendering while being rescaled (e.g. message appearance animation) - setX((targetBounds.left - canvasBounds.left) / canvasBounds.width); - setY((targetBounds.top - canvasBounds.top) / canvasBounds.height); + setX(round((targetBounds.left - canvasBounds.left) / canvasBounds.width, 4)); + setY(round((targetBounds.top - canvasBounds.top) / canvasBounds.height, 4)); }, [containerRef, sharedCanvasRef]); + useEffect(() => { + if (!('ResizeObserver' in window) || !sharedCanvasRef?.current) { + return undefined; + } + + const observer = new ResizeObserver(throttle(([entry]) => { + // During animation + if (!(entry.target as HTMLCanvasElement).offsetParent) { + return; + } + + recalculate(); + }, 300, false)); + + observer.observe(sharedCanvasRef.current); + + return () => { + observer.disconnect(); + }; + }, [recalculate, sharedCanvasRef]); + return useMemo(() => (x !== undefined && y !== undefined ? { x, y } : undefined), [x, y]); } diff --git a/src/lib/rlottie/RLottie.ts b/src/lib/rlottie/RLottie.ts index f9c4b6477..ea6859779 100644 --- a/src/lib/rlottie/RLottie.ts +++ b/src/lib/rlottie/RLottie.ts @@ -7,6 +7,7 @@ import { import WorkerConnector from '../../util/WorkerConnector'; import { animate } from '../../util/animation'; import cycleRestrict from '../../util/cycleRestrict'; +import { fastRaf } from '../../util/schedulers'; interface Params { noLoop?: boolean; @@ -120,9 +121,14 @@ class RLottie { } public removeContainer(containerId: string) { - const containerData = this.containers.get(containerId)!; - if (!containerData.isSharedCanvas) { - this.containers.get(containerId)!.canvas.remove(); + const { + canvas, ctx, isSharedCanvas, coords, + } = this.containers.get(containerId)!; + + if (isSharedCanvas) { + ctx.clearRect(coords!.x, coords!.y, this.imgSize, this.imgSize); + } else { + canvas.remove(); } this.containers.delete(containerId); @@ -167,9 +173,8 @@ class RLottie { } if (!this.params.isLowPriority) { - const currentFrameIndex = Math.floor(this.approxFrameIndex); this.frames = this.frames.map((frame, i) => { - if (i === currentFrameIndex) { + if (i === this.prevFrameIndex) { return frame; } else { if (frame && frame !== WAITING) { @@ -193,10 +198,41 @@ class RLottie { this.speed = speed; } - setNoLoop(noLoop: boolean) { + setNoLoop(noLoop?: boolean) { this.params.noLoop = noLoop; } + setSharedCanvasCoords(containerId: string, newCoords: Params['coords']) { + const containerInfo = this.containers.get(containerId)!; + const { + canvas, ctx, isPaused, coords, + } = containerInfo; + + if (!canvas.dataset.isJustCleaned || canvas.dataset.isJustCleaned === 'false') { + const { isLowPriority, quality = isLowPriority ? LOW_PRIORITY_QUALITY : HIGH_PRIORITY_QUALITY } = this.params; + const sizeFactor = Math.max(DPR * quality, 1); + ensureCanvasSize(canvas, sizeFactor); + ctx.clearRect(0, 0, canvas.width, canvas.height); + canvas.dataset.isJustCleaned = 'true'; + fastRaf(() => { + canvas.dataset.isJustCleaned = 'false'; + }); + } + + containerInfo.coords = { + x: Math.round((newCoords?.x || 0) * canvas.width), + y: Math.round((newCoords?.y || 0) * canvas.height), + }; + + if (isPaused || !this.isPlaying()) { + const frame = this.getFrame(this.prevFrameIndex) || this.getFrame(Math.round(this.approxFrameIndex)); + + if (frame && frame !== WAITING) { + ctx.drawImage(frame, coords!.x, coords!.y); + } + } + } + private addContainer( containerId: string, container: HTMLDivElement | HTMLCanvasElement, @@ -251,14 +287,9 @@ class RLottie { const canvas = container; const ctx = canvas.getContext('2d')!; - imgSize = Math.round(this.params.size! * sizeFactor); + ensureCanvasSize(canvas, sizeFactor); - const expectedWidth = Math.round(canvas.offsetWidth * sizeFactor); - const expectedHeight = Math.round(canvas.offsetHeight * sizeFactor); - if (canvas.width !== expectedWidth || canvas.height !== expectedHeight) { - canvas.width = expectedWidth; - canvas.height = expectedHeight; - } + imgSize = Math.round(this.params.size! * sizeFactor); this.containers.set(containerId, { canvas, @@ -537,4 +568,13 @@ class RLottie { } } +function ensureCanvasSize(canvas: HTMLCanvasElement, sizeFactor: number) { + const expectedWidth = Math.round(canvas.offsetWidth * sizeFactor); + const expectedHeight = Math.round(canvas.offsetHeight * sizeFactor); + if (canvas.width !== expectedWidth || canvas.height !== expectedHeight) { + canvas.width = expectedWidth; + canvas.height = expectedHeight; + } +} + export default RLottie;