175 lines
5.0 KiB
TypeScript
175 lines
5.0 KiB
TypeScript
import React, {
|
|
memo, useEffect, useRef, useState,
|
|
} from '../../../lib/teact/teact';
|
|
|
|
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
|
import { getStickerMediaHash } from '../../../global/helpers';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { preloadImage } from '../../../util/files';
|
|
import { REM } from '../helpers/mediaDimensions';
|
|
|
|
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
|
|
import { useThrottleForHeavyAnimation } from '../../../hooks/useHeavyAnimation';
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useMedia from '../../../hooks/useMedia';
|
|
import useOldLang from '../../../hooks/useOldLang';
|
|
import useResizeObserver from '../../../hooks/useResizeObserver';
|
|
import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio';
|
|
import useCustomEmoji from '../hooks/useCustomEmoji';
|
|
|
|
import styles from './EmojiIconBackground.module.scss';
|
|
|
|
type IconPosition = {
|
|
inline: number;
|
|
block: number;
|
|
opacity: number;
|
|
scale: number;
|
|
};
|
|
|
|
const ICON_POSITIONS: IconPosition[] = [
|
|
{
|
|
inline: 22, block: 38, opacity: 0.35, scale: 0.75,
|
|
},
|
|
{
|
|
inline: 32, block: 12, opacity: 0.3, scale: 1,
|
|
},
|
|
{
|
|
inline: 60, block: 22, opacity: 0.25, scale: 0.75,
|
|
},
|
|
{
|
|
inline: 75, block: 44, opacity: 0.25, scale: 1,
|
|
},
|
|
{
|
|
inline: 75, block: 2, opacity: 0.2, scale: 0.625,
|
|
},
|
|
{
|
|
inline: 95, block: 18, opacity: 0.2, scale: 1,
|
|
},
|
|
{
|
|
inline: 115, block: 38, opacity: 0.2, scale: 0.625,
|
|
},
|
|
{
|
|
inline: 125, block: 12, opacity: 0.1, scale: 0.75,
|
|
},
|
|
];
|
|
|
|
const EMOJI_SIZE = REM;
|
|
const LOTTIE_TINT_OPACITY = 'ff';
|
|
const NON_LOTTIE_TINT_OPACITY = 'bb';
|
|
|
|
type OwnProps = {
|
|
emojiDocumentId: string;
|
|
className?: string;
|
|
};
|
|
|
|
const EmojiIconBackground = ({
|
|
emojiDocumentId,
|
|
className,
|
|
}: OwnProps) => {
|
|
// eslint-disable-next-line no-null/no-null
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
// eslint-disable-next-line no-null/no-null
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [emojiImage, setEmojiImage] = useState<HTMLImageElement | undefined>();
|
|
|
|
const dpr = useDevicePixelRatio();
|
|
|
|
const lang = useOldLang();
|
|
|
|
const { customEmoji } = useCustomEmoji(emojiDocumentId);
|
|
const previewMediaHash = customEmoji ? getStickerMediaHash(customEmoji, 'preview') : undefined;
|
|
const previewUrl = useMedia(previewMediaHash);
|
|
|
|
const customColor = useDynamicColorListener(containerRef);
|
|
|
|
const preloadAfterHeavyAnimation = useThrottleForHeavyAnimation(() => {
|
|
if (!previewUrl) return;
|
|
preloadImage(previewUrl).then(setEmojiImage);
|
|
}, [previewUrl]);
|
|
|
|
useEffect(() => {
|
|
preloadAfterHeavyAnimation();
|
|
}, [preloadAfterHeavyAnimation]);
|
|
|
|
const updateCanvas = useLastCallback(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas || !emojiImage || !customColor) return;
|
|
|
|
const context = canvas.getContext('2d')!;
|
|
const { width, height } = canvas;
|
|
|
|
context.clearRect(0, 0, width, height);
|
|
|
|
ICON_POSITIONS.forEach(({
|
|
inline, block, opacity, scale,
|
|
}) => {
|
|
const x = (lang.isRtl ? inline : width / dpr - inline) * dpr;
|
|
const y = block * dpr;
|
|
|
|
const emojiSize = EMOJI_SIZE * dpr;
|
|
|
|
context.save();
|
|
context.globalAlpha = opacity;
|
|
context.translate(x, y);
|
|
context.scale(scale, scale);
|
|
context.drawImage(emojiImage, -emojiSize / 2, -emojiSize / 2, emojiSize, emojiSize);
|
|
context.restore();
|
|
});
|
|
|
|
const tintColor = `${customColor}${customEmoji!.isLottie ? LOTTIE_TINT_OPACITY : NON_LOTTIE_TINT_OPACITY}`;
|
|
|
|
context.save();
|
|
context.fillStyle = tintColor;
|
|
context.globalCompositeOperation = 'source-atop';
|
|
context.fillRect(0, 0, width, height);
|
|
context.restore();
|
|
});
|
|
|
|
const updateCanvasAfterHeavyAnimation = useThrottleForHeavyAnimation(updateCanvas, [updateCanvas]);
|
|
|
|
useEffect(() => {
|
|
updateCanvasAfterHeavyAnimation();
|
|
}, [emojiImage, lang.isRtl, customColor, updateCanvasAfterHeavyAnimation]);
|
|
|
|
const updateCanvasSize = useThrottleForHeavyAnimation((parentWidth: number, parentHeight: number) => {
|
|
requestMutation(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
canvas.width = parentWidth * dpr;
|
|
canvas.height = parentHeight * dpr;
|
|
|
|
canvas.style.width = `${parentWidth}px`;
|
|
canvas.style.height = `${parentHeight}px`;
|
|
|
|
updateCanvas();
|
|
});
|
|
}, [dpr]);
|
|
|
|
const handleResize = useThrottleForHeavyAnimation((entry: ResizeObserverEntry) => {
|
|
const { width, height } = entry.contentRect;
|
|
|
|
updateCanvasSize(width, height);
|
|
}, [updateCanvasSize]);
|
|
|
|
useResizeObserver(containerRef, handleResize);
|
|
|
|
useEffect(() => {
|
|
const container = containerRef.current;
|
|
if (!container) return;
|
|
|
|
const { width, height } = container.getBoundingClientRect();
|
|
|
|
updateCanvasSize(width, height);
|
|
}, [updateCanvasSize]);
|
|
|
|
return (
|
|
<div className={buildClassName(styles.root, className)} ref={containerRef}>
|
|
<canvas ref={canvasRef} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(EmojiIconBackground);
|