[Perf] Use canvas for faster blur (#1100)

This commit is contained in:
Alexander Zinchuk 2021-05-24 00:28:09 +03:00
parent 1f56cc1af6
commit 65d36c7723
12 changed files with 129 additions and 74 deletions

View File

@ -20,12 +20,16 @@
grid-column-end: span 2;
}
.preview {
.thumbnail {
background-size: cover !important;
background: transparent no-repeat center;
}
.preview, video {
.thumbnail ~ video {
position: absolute;
}
.thumbnail, video {
width: 100%;
height: 100%;
object-fit: cover;

View File

@ -8,9 +8,9 @@ import buildClassName from '../../util/buildClassName';
import { ObserveFn, useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useMedia from '../../hooks/useMedia';
import useTransitionForMedia from '../../hooks/useTransitionForMedia';
import useBlur from '../../hooks/useBlur';
import useVideoCleanup from '../../hooks/useVideoCleanup';
import useBuffering from '../../hooks/useBuffering';
import useCanvasBlur from '../../hooks/useCanvasBlur';
import Spinner from '../ui/Spinner';
@ -31,15 +31,15 @@ const GifButton: FC<OwnProps> = ({
// eslint-disable-next-line no-null/no-null
const videoRef = useRef<HTMLVideoElement>(null);
const hasThumbnail = gif.thumbnail && !!gif.thumbnail.dataUri;
const localMediaHash = `gif${gif.id}`;
const isIntersecting = useIsIntersecting(ref, observeIntersection);
const loadAndPlay = isIntersecting && !isDisabled;
const previewBlobUrl = useMedia(`${localMediaHash}?size=m`, !loadAndPlay, ApiMediaFormat.BlobUrl);
const thumbDataUri = useBlur(gif.thumbnail && gif.thumbnail.dataUri, Boolean(previewBlobUrl));
const previewData = previewBlobUrl || thumbDataUri;
const thumbRef = useCanvasBlur(gif.thumbnail && gif.thumbnail.dataUri, Boolean(previewBlobUrl));
const videoData = useMedia(localMediaHash, !loadAndPlay, ApiMediaFormat.BlobUrl);
const shouldRenderVideo = Boolean(loadAndPlay && videoData);
const { transitionClassNames } = useTransitionForMedia(previewData || videoData, 'slow');
const { transitionClassNames } = useTransitionForMedia(hasThumbnail || previewBlobUrl || videoData, 'slow');
const { isBuffered, bufferingHandlers } = useBuffering(true);
const shouldRenderSpinner = loadAndPlay && !isBuffered;
@ -66,14 +66,20 @@ const GifButton: FC<OwnProps> = ({
className={className}
onClick={handleClick}
>
{previewData && !shouldRenderVideo && (
<div
className="preview"
// @ts-ignore
style={`background-image: url(${previewData});`}
{hasThumbnail && (
<canvas
ref={thumbRef}
className="thumbnail"
/>
)}
{shouldRenderVideo && (
{!hasThumbnail && previewBlobUrl && (
<img
src={previewBlobUrl}
alt=""
className="thumbnail"
/>
)}
{(shouldRenderVideo || previewBlobUrl) && (
<video
ref={videoRef}
autoPlay
@ -81,7 +87,6 @@ const GifButton: FC<OwnProps> = ({
muted
playsInline
preload="none"
poster={previewData}
// eslint-disable-next-line react/jsx-props-no-spreading
{...bufferingHandlers}
>
@ -89,7 +94,7 @@ const GifButton: FC<OwnProps> = ({
</video>
)}
{shouldRenderSpinner && (
<Spinner color={previewData ? 'white' : 'black'} />
<Spinner color={previewBlobUrl || hasThumbnail ? 'white' : 'black'} />
)}
</div>
);

View File

@ -18,7 +18,7 @@
transform: scale(1);
transition: transform .15s ease;
img {
img, canvas {
position: absolute;
left: 0;
top: 0;

View File

@ -13,7 +13,7 @@ import useMedia from '../../../hooks/useMedia';
import useMediaWithDownloadProgress from '../../../hooks/useMediaWithDownloadProgress';
import useShowTransition from '../../../hooks/useShowTransition';
import usePrevious from '../../../hooks/usePrevious';
import useBlur from '../../../hooks/useBlur';
import useCanvasBlur from '../../../hooks/useCanvasBlur';
import ProgressSpinner from '../../ui/ProgressSpinner';
@ -25,8 +25,6 @@ type OwnProps = {
onClick: (slug: string) => void;
};
const ANIMATION_DURATION = 300;
const WallpaperTile: FC<OwnProps> = ({
wallpaper,
isSelected,
@ -37,10 +35,10 @@ const WallpaperTile: FC<OwnProps> = ({
const localMediaHash = `wallpaper${document.id!}`;
const localBlobUrl = document.previewBlobUrl;
const previewBlobUrl = useMedia(`${localMediaHash}?size=m`);
const thumbDataUri = useBlur(
const thumbRef = useCanvasBlur(
document.thumbnail && document.thumbnail.dataUri,
Boolean(previewBlobUrl),
ANIMATION_DURATION,
true,
);
const {
shouldRenderThumb, shouldRenderFullMedia, transitionClassNames,
@ -88,10 +86,9 @@ const WallpaperTile: FC<OwnProps> = ({
<div className={className} onClick={handleClick}>
<div className="media-inner">
{shouldRenderThumb && (
<img
src={thumbDataUri}
<canvas
ref={thumbRef}
className="thumbnail"
alt=""
/>
)}
{shouldRenderFullMedia && (

View File

@ -16,8 +16,8 @@ import { ObserveFn, useIsIntersecting } from '../../../hooks/useIntersectionObse
import useMediaWithDownloadProgress from '../../../hooks/useMediaWithDownloadProgress';
import useTransitionForMedia from '../../../hooks/useTransitionForMedia';
import useShowTransition from '../../../hooks/useShowTransition';
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
import usePrevious from '../../../hooks/usePrevious';
import useBlurredMediaThumb from './hooks/useBlurredMediaThumb';
import buildClassName from '../../../util/buildClassName';
import getCustomAppendixBg from './helpers/getCustomAppendixBg';
import { calculateMediaDimensions } from './helpers/mediaDimensions';
@ -69,7 +69,7 @@ const Photo: FC<OwnProps> = ({
mediaData, downloadProgress,
} = useMediaWithDownloadProgress(getMessageMediaHash(message, size), !shouldDownload);
const fullMediaData = localBlobUrl || mediaData;
const thumbDataUri = useBlurredMediaThumb(message, fullMediaData);
const thumbRef = useBlurredMediaThumbRef(message, fullMediaData);
const {
isUploading, isTransferring, transferProgress,
@ -122,11 +122,6 @@ const Photo: FC<OwnProps> = ({
width === height && 'square-image',
);
const thumbClassName = buildClassName(
'thumbnail',
!thumbDataUri && 'empty',
);
const style = dimensions
? `width: ${width}px; height: ${height}px; left: ${dimensions.x}px; top: ${dimensions.y}px;`
: '';
@ -141,12 +136,11 @@ const Photo: FC<OwnProps> = ({
onClick={isUploading ? undefined : handleClick}
>
{shouldRenderThumb && (
<img
src={thumbDataUri}
className={thumbClassName}
width={width}
height={height}
alt=""
<canvas
ref={thumbRef}
className="thumbnail"
// @ts-ignore teact feature
style={`width: ${width}px; height: ${height}px`}
/>
)}
{shouldRenderFullMedia && (

View File

@ -20,8 +20,8 @@ import useBuffering from '../../../hooks/useBuffering';
import buildClassName from '../../../util/buildClassName';
import useHeavyAnimationCheckForVideo from '../../../hooks/useHeavyAnimationCheckForVideo';
import useVideoCleanup from '../../../hooks/useVideoCleanup';
import useBlurredMediaThumb from './hooks/useBlurredMediaThumb';
import usePauseOnInactive from './hooks/usePauseOnInactive';
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
import safePlay from '../../../util/safePlay';
import ProgressSpinner from '../../ui/ProgressSpinner';
@ -74,7 +74,7 @@ const RoundVideo: FC<OwnProps> = ({
getMessageMediaFormat(message, 'inline'),
lastSyncTime,
);
const thumbDataUri = useBlurredMediaThumb(message, mediaData);
const thumbRef = useBlurredMediaThumbRef(message, mediaData);
const { isBuffered, bufferingHandlers } = useBuffering();
const isTransferring = isDownloadAllowed && !isBuffered;
@ -183,12 +183,11 @@ const RoundVideo: FC<OwnProps> = ({
>
{shouldRenderThumb && (
<div className="thumbnail-wrapper">
<img
src={thumbDataUri}
<canvas
ref={thumbRef}
className="thumbnail"
width={ROUND_VIDEO_DIMENSIONS}
height={ROUND_VIDEO_DIMENSIONS}
alt=""
// @ts-ignore teact feature
style={`width: ${ROUND_VIDEO_DIMENSIONS}px; height: ${ROUND_VIDEO_DIMENSIONS}px`}
/>
</div>
)}
@ -204,7 +203,6 @@ const RoundVideo: FC<OwnProps> = ({
muted={!isActivated}
loop={!isActivated}
playsInline
poster={thumbDataUri}
onEnded={isActivated ? stopPlaying : undefined}
// eslint-disable-next-line react/jsx-props-no-spreading
{...bufferingHandlers}

View File

@ -24,9 +24,9 @@ import useTransitionForMedia from '../../../hooks/useTransitionForMedia';
import usePrevious from '../../../hooks/usePrevious';
import useBuffering from '../../../hooks/useBuffering';
import useHeavyAnimationCheckForVideo from '../../../hooks/useHeavyAnimationCheckForVideo';
import useBlurredMediaThumb from './hooks/useBlurredMediaThumb';
import useVideoCleanup from '../../../hooks/useVideoCleanup';
import usePauseOnInactive from './hooks/usePauseOnInactive';
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
import ProgressSpinner from '../../ui/ProgressSpinner';
@ -76,7 +76,7 @@ const Video: FC<OwnProps> = ({
getMessageMediaFormat(message, 'pictogram'),
lastSyncTime,
);
const thumbDataUri = useBlurredMediaThumb(message, previewBlobUrl);
const thumbRef = useBlurredMediaThumbRef(message);
const { mediaData, downloadProgress } = useMediaWithDownloadProgress(
getMessageMediaHash(message, 'inline'),
!shouldDownload,
@ -84,7 +84,6 @@ const Video: FC<OwnProps> = ({
lastSyncTime,
);
const previewMediaData = previewBlobUrl || thumbDataUri;
const fullMediaData = localBlobUrl || mediaData;
const isInline = Boolean(canPlayInline && isIntersecting && fullMediaData);
@ -132,9 +131,10 @@ const Video: FC<OwnProps> = ({
}, [isUploading, canPlayInline, fullMediaData, isPlayAllowed, onClick, onCancelUpload, message]);
const className = buildClassName('media-inner dark', !isUploading && 'interactive');
const thumbClassName = buildClassName('thumbnail', !previewMediaData && 'empty');
const videoClassName = buildClassName('full-media', transitionClassNames);
const videoStyle = previewMediaData ? `background-image: url(${previewMediaData}); background-size: cover` : '';
const videoStyle = previewBlobUrl
? `background-image: url(${previewBlobUrl}); background-size: cover`
: 'background: transparent';
const style = dimensions
? `width: ${width}px; height: ${height}px; left: ${dimensions.x}px; top: ${dimensions.y}px;`
@ -154,15 +154,25 @@ const Video: FC<OwnProps> = ({
style={style}
onClick={isUploading ? undefined : handleClick}
>
{(shouldRenderThumb || !isInline) && (
{((!isInline || shouldRenderThumb) && !previewBlobUrl)
&& (
<canvas
ref={thumbRef}
className="thumbnail"
// @ts-ignore teact feature
style={`width: ${width}px; height: ${height}px;`}
/>
)}
{previewBlobUrl && fullMediaData && (
<img
src={previewMediaData}
className={thumbClassName}
width={width}
height={height}
src={previewBlobUrl}
className="thumbnail"
// @ts-ignore teact feature
style={`width: ${width}px; height: ${height}px;`}
alt=""
/>
)}
{shouldRenderInlineVideo && (
<video
ref={videoRef}

View File

@ -1,14 +0,0 @@
import { ApiMessage } from '../../../../api/types';
import { LAYERS_TRANSITION_DURATION } from '../../../../config';
import { IS_MOBILE_SCREEN } from '../../../../util/environment';
import { getMessageMediaThumbDataUri } from '../../../../modules/helpers';
import useBlur from '../../../../hooks/useBlur';
export default function useBlurredMediaThumb(message: ApiMessage, fullMediaData?: string) {
return useBlur(
getMessageMediaThumbDataUri(message),
Boolean(fullMediaData),
IS_MOBILE_SCREEN ? LAYERS_TRANSITION_DURATION : undefined,
);
}

View File

@ -0,0 +1,13 @@
import { ApiMessage } from '../../../../api/types';
import { IS_CANVAS_FILTER_SUPPORTED, IS_MOBILE_SCREEN } from '../../../../util/environment';
import { getMessageMediaThumbDataUri } from '../../../../modules/helpers';
import useCanvasBlur from '../../../../hooks/useCanvasBlur';
export default function useBlurredMediaThumbRef(message: ApiMessage, fullMediaData?: string) {
return useCanvasBlur(
getMessageMediaThumbDataUri(message),
Boolean(fullMediaData),
IS_MOBILE_SCREEN && !IS_CANVAS_FILTER_SUPPORTED,
);
}

View File

@ -0,0 +1,53 @@
import { useEffect, useRef } from '../lib/teact/teact';
import fastBlur from '../lib/fastBlur';
import useForceUpdate from './useForceUpdate';
import { IS_CANVAS_FILTER_SUPPORTED } from '../util/environment';
const RADIUS = 2;
const ITERATIONS = 2;
export default function useCanvasBlur(dataUri?: string, isDisabled = false, withRaf?: boolean) {
// eslint-disable-next-line no-null/no-null
const canvasRef = useRef<HTMLCanvasElement>(null);
const forceUpdate = useForceUpdate();
useEffect(() => {
const canvas = canvasRef.current;
if (!dataUri || !canvas || isDisabled) {
return;
}
const img = new Image();
const processBlur = () => {
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d', { alpha: false })!;
if (IS_CANVAS_FILTER_SUPPORTED) {
ctx.filter = `blur(${RADIUS}px)`;
}
ctx.drawImage(img, -RADIUS * 2, -RADIUS * 2, canvas.width + RADIUS * 4, canvas.height + RADIUS * 4);
if (!IS_CANVAS_FILTER_SUPPORTED) {
fastBlur(ctx, 0, 0, canvas.width, canvas.height, RADIUS, ITERATIONS);
}
};
img.onload = () => {
if (withRaf) {
requestAnimationFrame(processBlur);
} else {
processBlur();
}
};
img.src = dataUri;
}, [canvasRef, dataUri, forceUpdate, isDisabled, withRaf]);
return canvasRef;
}

View File

@ -19,12 +19,6 @@
.thumbnail ~ .full-media, .media-loading {
position: absolute;
}
.thumbnail {
&.empty {
visibility: hidden;
}
}
}
.animated-close-icon {

View File

@ -47,6 +47,7 @@ export const IS_SERVICE_WORKER_SUPPORTED = 'serviceWorker' in navigator;
export const IS_PROGRESSIVE_SUPPORTED = IS_SERVICE_WORKER_SUPPORTED;
export const IS_STREAMING_SUPPORTED = 'MediaSource' in window;
export const IS_OPUS_SUPPORTED = Boolean((new Audio()).canPlayType('audio/ogg; codecs=opus'));
export const IS_CANVAS_FILTER_SUPPORTED = 'filter' in (document.createElement('canvas').getContext('2d') || {});
export const DPR = window.devicePixelRatio || 1;