Emoji Background: Render on canvas (#4151)
This commit is contained in:
parent
e9e06e93e0
commit
b866ec3dc0
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 && (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user