Emoji Background: Render on canvas (#4151)

This commit is contained in:
Alexander Zinchuk 2024-01-12 13:00:37 +01:00
parent e9e06e93e0
commit b866ec3dc0
5 changed files with 133 additions and 68 deletions

View File

@ -24,6 +24,10 @@
}
}
&--background-icons {
margin: -0.1875rem -0.375rem -0.1875rem -0.1875rem;
}
.custom-shape & {
max-width: 15rem;
margin: 0;
@ -183,10 +187,6 @@
object-fit: cover;
}
&--background-icons {
color: var(--accent-color);
}
&.inside-input {
flex-grow: 1;
margin: 0;

View File

@ -1,7 +1,5 @@
.root {
--custom-emoji-border-radius: 0.25rem;
--custom-emoji-size: 1.25rem;
color: var(--accent-color);
position: absolute;
top: 0;
right: 0;
@ -9,7 +7,3 @@
left: 0;
pointer-events: none;
}
.emoji {
position: absolute;
}

View File

@ -1,9 +1,20 @@
import React, { memo } from '../../../lib/teact/teact';
import React, {
memo, useEffect, useRef, useState,
} from '../../../lib/teact/teact';
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import { getStickerPreviewHash } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import buildStyle from '../../../util/buildStyle';
import { preloadImage } from '../../../util/files';
import { REM } from '../helpers/mediaDimensions';
import CustomEmoji from '../CustomEmoji';
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia';
import useResizeObserver from '../../../hooks/useResizeObserver';
import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio';
import useCustomEmoji from '../hooks/useCustomEmoji';
import styles from './EmojiIconBackground.module.scss';
@ -16,37 +27,35 @@ type IconPosition = {
const ICON_POSITIONS: IconPosition[] = [
{
inline: 5, block: 15, opacity: 0.35, scale: 1,
inline: 22, block: 38, opacity: 0.35, scale: 0.75,
},
{
inline: 10, block: 45, opacity: 0.3, scale: 0.9,
inline: 32, block: 12, opacity: 0.3, scale: 1,
},
{
inline: 20, block: 75, opacity: 0.3, scale: 0.75,
inline: 60, block: 22, opacity: 0.25, scale: 0.75,
},
{
inline: 40, block: 20, opacity: 0.25, scale: 0.8,
inline: 75, block: 44, opacity: 0.25, scale: 1,
},
{
inline: 60, block: 50, opacity: 0.25, scale: 0.85,
inline: 75, block: 2, opacity: 0.2, scale: 0.625,
},
{
inline: 55, block: -5, opacity: 0.20, scale: 0.75,
inline: 95, block: 18, opacity: 0.2, scale: 1,
},
{
inline: 80, block: 15, opacity: 0.15, scale: 0.95,
inline: 115, block: 38, opacity: 0.2, scale: 0.625,
},
{
inline: 100, block: 70, opacity: 0.15, scale: 0.9,
},
{
inline: 120, block: 25, opacity: 0.10, scale: 0.65,
},
{
inline: 140, block: 0, opacity: 0.10, scale: 0.75,
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;
@ -56,29 +65,104 @@ 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 = useLang();
const { customEmoji } = useCustomEmoji(emojiDocumentId);
const previewMediaHash = customEmoji ? getStickerPreviewHash(customEmoji.id) : undefined;
const previewUrl = useMedia(previewMediaHash);
const customColor = useDynamicColorListener(containerRef);
useEffect(() => {
if (!previewUrl) return;
preloadImage(previewUrl).then(setEmojiImage);
}, [previewUrl]);
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();
});
useEffect(() => {
updateCanvas();
}, [emojiImage, lang.isRtl, customColor]);
const updateCanvasSize = useLastCallback((parentWidth: number, parentHeight: number) => {
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();
});
const handleResize = useLastCallback((entry: ResizeObserverEntry) => {
const { width, height } = entry.contentRect;
requestMutation(() => {
updateCanvasSize(width, height);
});
});
useResizeObserver(containerRef, handleResize);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const { width, height } = container.getBoundingClientRect();
requestMutation(() => {
updateCanvasSize(width, height);
});
}, [dpr]);
return (
<div className={buildClassName(styles.root, className)}>
{ICON_POSITIONS.map((position) => {
const {
inline, block, opacity, scale,
} = position;
const style = buildStyle(
`inset-inline-end: ${inline}px`,
`inset-block-start: ${block}px`,
`opacity: ${opacity}`,
`transform: scale(${scale})`,
);
return (
<CustomEmoji
documentId={emojiDocumentId}
className={styles.emoji}
noPlay
style={style}
/>
);
})}
<div className={buildClassName(styles.root, className)} ref={containerRef}>
<canvas ref={canvasRef} />
</div>
);
};

View File

@ -10,6 +10,10 @@
position: relative;
overflow: hidden;
&--background-icons {
margin: -0.375rem;
}
&.in-preview {
border-radius: 0.25rem;
background-color: var(--color-primary-tint);
@ -59,10 +63,6 @@
}
}
&--background-icons {
color: var(--accent-color);
}
&-text {
display: flex;
flex-direction: column;
@ -137,19 +137,6 @@
}
}
&:not(.with-square-photo):not(.with-quick-button) {
.site-name,
.site-title,
.site-description {
&:last-child::after {
content: "";
width: var(--meta-safe-area-size);
height: 0.75rem;
float: right;
}
}
}
.site-name,
.site-description,
.site-title {

View File

@ -140,7 +140,7 @@ const WebPage: FC<OwnProps> = ({
<div
className={className}
data-initial={(siteName || displayUrl)[0]}
dir="auto"
dir={lang.isRtl ? 'rtl' : 'auto'}
>
<div className={buildClassName('WebPage--content', isStory && 'is-story')}>
{isStory && (