From f4200e5a64d08146bc14c430a31002a26b26927d Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 19 Sep 2024 20:43:27 +0200 Subject: [PATCH] [Perf] Message: Use Offscreen Canvas to detect appendix bg --- src/components/middle/message/Invoice.tsx | 5 +-- src/components/middle/message/Photo.tsx | 3 +- .../message/helpers/getCustomAppendixBg.ts | 36 +++++-------------- src/hooks/useOffscreenCanvasBlur.ts | 9 +++-- .../offscreen-canvas.worker.ts | 20 ++++++++++- src/util/launchMediaWorkers.ts | 4 +++ 6 files changed, 40 insertions(+), 37 deletions(-) diff --git a/src/components/middle/message/Invoice.tsx b/src/components/middle/message/Invoice.tsx index 23b4f7b0a..a344ebc50 100644 --- a/src/components/middle/message/Invoice.tsx +++ b/src/components/middle/message/Invoice.tsx @@ -56,6 +56,7 @@ const Invoice: FC = ({ const photoUrl = useMedia(getWebDocumentHash(photo)); const withBlurredBackground = Boolean(forcedWidth); const blurredBackgroundRef = useBlurredMediaThumbRef(photoUrl, !withBlurredBackground); + const messageId = message.id; useLayoutEffectWithPrevDeps(([prevShouldAffectAppendix]) => { if (!shouldAffectAppendix) { @@ -67,14 +68,14 @@ const Invoice: FC = ({ if (photoUrl) { const contentEl = ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!; - getCustomAppendixBg(photoUrl, false, isSelected, theme).then((appendixBg) => { + getCustomAppendixBg(photoUrl, false, messageId, isSelected, theme).then((appendixBg) => { requestMutation(() => { contentEl.style.setProperty('--appendix-bg', appendixBg); contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, ''); }); }); } - }, [shouldAffectAppendix, photoUrl, isInSelectMode, isSelected, theme]); + }, [shouldAffectAppendix, photoUrl, isInSelectMode, isSelected, theme, messageId]); const width = forcedWidth || photo?.dimensions?.width; diff --git a/src/components/middle/message/Photo.tsx b/src/components/middle/message/Photo.tsx index 3452838d0..71755a2f6 100644 --- a/src/components/middle/message/Photo.tsx +++ b/src/components/middle/message/Photo.tsx @@ -179,7 +179,8 @@ const Photo = ({ const contentEl = ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!; if (fullMediaData) { - getCustomAppendixBg(fullMediaData, Boolean(isOwn), isSelected, theme).then((appendixBg) => { + const messageId = Number(contentEl.closest('.Message')!.dataset.messageId); + getCustomAppendixBg(fullMediaData, Boolean(isOwn), messageId, isSelected, theme).then((appendixBg) => { requestMutation(() => { contentEl.style.setProperty('--appendix-bg', appendixBg); contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, ''); diff --git a/src/components/middle/message/helpers/getCustomAppendixBg.ts b/src/components/middle/message/helpers/getCustomAppendixBg.ts index d2ec35596..08ec33ad0 100644 --- a/src/components/middle/message/helpers/getCustomAppendixBg.ts +++ b/src/components/middle/message/helpers/getCustomAppendixBg.ts @@ -1,5 +1,7 @@ import type { ISettings } from '../../../../types'; +import { MAX_WORKERS, requestMediaWorker } from '../../../../util/launchMediaWorkers'; + const SELECTED_APPENDIX_COLORS = { dark: { outgoing: 'rgb(135,116,225)', @@ -12,36 +14,14 @@ const SELECTED_APPENDIX_COLORS = { }; export default function getCustomAppendixBg( - src: string, isOwn: boolean, isSelected?: boolean, theme?: ISettings['theme'], + src: string, isOwn: boolean, id: number, isSelected?: boolean, theme?: ISettings['theme'], ) { if (isSelected) { return Promise.resolve(SELECTED_APPENDIX_COLORS[theme || 'light'][isOwn ? 'outgoing' : 'incoming']); } - return getAppendixColorFromImage(src, isOwn); -} - -async function getAppendixColorFromImage(src: string, isOwn: boolean) { - const img = new Image(); - img.src = src; - img.crossOrigin = 'anonymous'; - - if (!img.width) { - await new Promise((resolve) => { - img.onload = resolve; - }); - } - - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d')!; - - canvas.width = img.width; - canvas.height = img.height; - - ctx.drawImage(img, 0, 0, img.width, img.height); - - const x = isOwn ? img.width - 1 : 0; - const y = img.height - 1; - - const pixel = Array.from(ctx.getImageData(x, y, 1, 1).data); - return `rgba(${pixel.join(',')})`; + + return requestMediaWorker({ + name: 'offscreen-canvas:getAppendixColorFromImage', + args: [src, isOwn], + }, id % MAX_WORKERS); } diff --git a/src/hooks/useOffscreenCanvasBlur.ts b/src/hooks/useOffscreenCanvasBlur.ts index 53fadc328..b25d1b097 100644 --- a/src/hooks/useOffscreenCanvasBlur.ts +++ b/src/hooks/useOffscreenCanvasBlur.ts @@ -1,7 +1,7 @@ import { useLayoutEffect, useMemo, useRef } from '../lib/teact/teact'; import cycleRestrict from '../util/cycleRestrict'; -import launchMediaWorkers, { MAX_WORKERS } from '../util/launchMediaWorkers'; +import { MAX_WORKERS, requestMediaWorker } from '../util/launchMediaWorkers'; const RADIUS = 7; @@ -25,12 +25,11 @@ export default function useOffscreenCanvasBlur( offscreenRef.current = canvas.transferControlToOffscreen(); - const { connector } = launchMediaWorkers()[workerIndex]; - connector.request({ - name: 'blurThumb', + requestMediaWorker({ + name: 'offscreen-canvas:blurThumb', args: [offscreenRef.current, thumbData, radius], transferables: [offscreenRef.current], - }); + }, workerIndex); }, [thumbData, isDisabled, radius, workerIndex]); return canvasRef; diff --git a/src/lib/offscreen-canvas/offscreen-canvas.worker.ts b/src/lib/offscreen-canvas/offscreen-canvas.worker.ts index eb0e65558..0214d8062 100644 --- a/src/lib/offscreen-canvas/offscreen-canvas.worker.ts +++ b/src/lib/offscreen-canvas/offscreen-canvas.worker.ts @@ -22,6 +22,21 @@ export async function blurThumb(canvas: OffscreenCanvas, thumbData: string, radi } } +export async function getAppendixColorFromImage(blobUrl: string, isOwn: boolean) { + const imageBitmap = await blobUrlToImageBitmap(blobUrl); + const { width, height } = imageBitmap; + const canvas = new OffscreenCanvas(width, height); + const ctx = canvas.getContext('2d')!; + + ctx.drawImage(imageBitmap, 0, 0, width, height); + + const x = isOwn ? width - 1 : 0; + const y = height - 1; + + const pixel = Array.from(ctx.getImageData(x, y, 1, 1).data); + return `rgba(${pixel.join(',')})`; +} + function dataUriToImageBitmap(dataUri: string) { const byteString = atob(dataUri.split(',')[1]); const mimeString = dataUri.split(',')[0].split(':')[1].split(';')[0]; @@ -43,7 +58,10 @@ async function blobUrlToImageBitmap(blobUrl: string) { return createImageBitmap(blob); } -const api = { blurThumb }; +const api = { + 'offscreen-canvas:blurThumb': blurThumb, + 'offscreen-canvas:getAppendixColorFromImage': getAppendixColorFromImage, +}; createWorkerInterface(api, 'media'); diff --git a/src/util/launchMediaWorkers.ts b/src/util/launchMediaWorkers.ts index 8ae7605fb..b4e3477c9 100644 --- a/src/util/launchMediaWorkers.ts +++ b/src/util/launchMediaWorkers.ts @@ -26,3 +26,7 @@ export default function launchMediaWorkers() { return instances; } + +export function requestMediaWorker(payload: Parameters['request']>[0], index: number) { + return launchMediaWorkers()[index].connector.request(payload); +}