Custom Emoji: Support adaptive static and video packs (#3118)

This commit is contained in:
Alexander Zinchuk 2023-05-15 10:57:14 +02:00
parent cebbda28cf
commit a71a3bc53b
20 changed files with 290 additions and 110 deletions

View File

@ -128,6 +128,7 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet {
accessHash: String(accessHash),
title,
hasThumbnail: Boolean(thumbs?.length || thumbDocumentId),
thumbCustomEmojiId: thumbDocumentId?.toString(),
count,
shortName,
};

View File

@ -57,6 +57,7 @@ export interface ApiStickerSet {
accessHash: string;
title: string;
hasThumbnail?: boolean;
thumbCustomEmojiId?: string;
count: number;
stickers?: ApiSticker[];
packs?: Record<string, ApiSticker[]>;

View File

@ -51,11 +51,11 @@ const OutlinedMicrophoneIcon: FC<OwnProps> = ({
// eslint-disable-next-line
}, [isMuted, shouldRaiseHand, isRaiseHand]);
const microphoneColor: [number, number, number] | undefined = useMemo(() => {
return noColor ? [0xff, 0xff, 0xff] : (
isRaiseHand ? [0x4d, 0xa6, 0xe0]
: (shouldRaiseHand || isMutedByMe ? [0xFF, 0x70, 0x6F] : (
isSpeaking ? [0x57, 0xBC, 0x6C] : [0x84, 0x8D, 0x94]
const microphoneColor: string | undefined = useMemo(() => {
return noColor ? '#ffffff' : (
isRaiseHand ? '#4da6e0'
: (shouldRaiseHand || isMutedByMe ? '#ff706f' : (
isSpeaking ? '#57bc6c' : '#848d94'
))
);
}, [noColor, isRaiseHand, shouldRaiseHand, isMutedByMe, isSpeaking]);

View File

@ -11,6 +11,7 @@ import React, {
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import generateIdFor from '../../util/generateIdFor';
import { hexToRgb } from '../../util/switchTheme';
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck';
import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../hooks/usePriorityPlaybackCheck';
@ -19,6 +20,8 @@ import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
import { useStateRef } from '../../hooks/useStateRef';
import useSharedIntersectionObserver from '../../hooks/useSharedIntersectionObserver';
import useThrottledCallback from '../../hooks/useThrottledCallback';
import useColorFilter from '../../hooks/stickers/useColorFilter';
import useSyncEffect from '../../hooks/useSyncEffect';
export type OwnProps = {
ref?: RefObject<HTMLDivElement>;
@ -32,7 +35,7 @@ export type OwnProps = {
noLoop?: boolean;
size: number;
quality?: number;
color?: [number, number, number];
color?: string;
isLowPriority?: boolean;
forceOnHeavyAnimation?: boolean;
sharedCanvas?: HTMLCanvasElement;
@ -80,10 +83,24 @@ const AnimatedSticker: FC<OwnProps> = ({
const animationRef = useRef<RLottieInstance>();
const isFirstRender = useRef(true);
const shouldUseColorFilter = !sharedCanvas && color;
const colorFilter = useColorFilter(shouldUseColorFilter ? color : undefined);
const playKey = play || playSegment;
const playRef = useStateRef(play);
const playSegmentRef = useStateRef(playSegment);
const rgbColor = useRef<[number, number, number] | undefined>();
useSyncEffect(() => {
if (color && !shouldUseColorFilter) {
const { r, g, b } = hexToRgb(color);
rgbColor.current = [r, g, b];
} else {
rgbColor.current = undefined;
}
}, [color, shouldUseColorFilter]);
const isUnmountedRef = useRef(false);
useEffect(() => {
return () => {
@ -118,7 +135,7 @@ const AnimatedSticker: FC<OwnProps> = ({
coords: sharedCanvasCoords,
},
viewId,
color,
rgbColor.current,
onLoad,
onEnded,
onLoop,
@ -131,7 +148,7 @@ const AnimatedSticker: FC<OwnProps> = ({
setAnimation(newAnimation);
animationRef.current = newAnimation;
}, [
color, isLowPriority, noLoop, onEnded, onLoad, onLoop, quality,
isLowPriority, noLoop, onEnded, onLoad, onLoop, quality,
renderId, sharedCanvas, sharedCanvasCoords, size, speed, tgsUrl, viewId,
]);
@ -149,7 +166,7 @@ const AnimatedSticker: FC<OwnProps> = ({
useEffect(() => {
if (!animation) return;
animation.setColor(color);
animation.setColor(rgbColor.current);
}, [color, animation]);
useEffect(() => {
@ -239,6 +256,7 @@ const AnimatedSticker: FC<OwnProps> = ({
style={buildStyle(
size !== undefined && `width: ${size}px; height: ${size}px;`,
onClick && 'cursor: pointer',
colorFilter,
style,
)}
onClick={onClick}

View File

@ -12,7 +12,7 @@ import safePlay from '../../util/safePlay';
import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors';
import useCustomEmoji from './hooks/useCustomEmoji';
import useDynamicColorListener from '../../hooks/useDynamicColorListener';
import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListener';
import StickerView from './StickerView';
@ -77,7 +77,7 @@ const CustomEmoji: FC<OwnProps> = ({
const [shouldLoop, setShouldLoop] = useState(true);
const hasCustomColor = customEmoji?.shouldUseTextColor;
const { rgbColor: customColor } = useDynamicColorListener(containerRef, !hasCustomColor);
const customColor = useDynamicColorListener(containerRef, !hasCustomColor);
const handleVideoEnded = useCallback((e) => {
if (!loopLimit) return;
@ -132,7 +132,6 @@ const CustomEmoji: FC<OwnProps> = ({
isSmall={!isBig}
size={size}
noPlay={noPlay || !canPlay}
customColor={customColor}
thumbClassName={styles.thumb}
fullMediaClassName={styles.media}
shouldLoop={shouldLoop}
@ -146,6 +145,7 @@ const CustomEmoji: FC<OwnProps> = ({
withTranslucentThumb={withTranslucentThumb}
onVideoEnded={handleVideoEnded}
onAnimatedStickerLoop={handleStickerLoop}
customColor={customColor}
/>
)}
</div>

View File

@ -6,4 +6,8 @@
> h3 {
margin-bottom: 0;
}
:global(.custom-emoji) {
color: var(--color-primary);
}
}

View File

@ -16,7 +16,7 @@ import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useMenuPosition from '../../hooks/useMenuPosition';
import useDynamicColorListener from '../../hooks/useDynamicColorListener';
import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListener';
import StickerView from './StickerView';
import Button from '../ui/Button';
@ -93,7 +93,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
const menuRef = useRef<HTMLDivElement>(null);
const lang = useLang();
const hasCustomColor = sticker.shouldUseTextColor;
const { rgbColor: customColor } = useDynamicColorListener(ref, !hasCustomColor);
const customColor = useDynamicColorListener(ref, !hasCustomColor);
const {
id, isCustomEmoji, hasEffect: isPremium, stickerSetInfo,

View File

@ -1,4 +1,6 @@
import React, { memo, useMemo } from '../../lib/teact/teact';
import React, {
memo, useMemo,
} from '../../lib/teact/teact';
import { getGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact';
@ -19,6 +21,7 @@ import useMediaTransition from '../../hooks/useMediaTransition';
import useFlag from '../../hooks/useFlag';
import useCoordsInSharedCanvas from '../../hooks/useCoordsInSharedCanvas';
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck';
import useColorFilter from '../../hooks/stickers/useColorFilter';
import AnimatedSticker from './AnimatedSticker';
import OptimizedVideo from '../ui/OptimizedVideo';
@ -33,7 +36,7 @@ type OwnProps = {
fullMediaClassName?: string;
isSmall?: boolean;
size?: number;
customColor?: [number, number, number];
customColor?: string;
loopLimit?: number;
shouldLoop?: boolean;
shouldPreloadPreview?: boolean;
@ -86,6 +89,8 @@ const StickerView: FC<OwnProps> = ({
const isStatic = !isLottie && !isVideo;
const previewMediaHash = getStickerPreviewHash(sticker.id);
const filterStyle = useColorFilter(customColor);
const isIntersectingForLoading = useIsIntersecting(containerRef, observeIntersectionForLoading);
const shouldLoad = isIntersectingForLoading && !noLoad;
const isIntersectingForPlaying = (
@ -128,7 +133,10 @@ const StickerView: FC<OwnProps> = ({
const randomIdPrefix = useMemo(() => generateIdFor(ID_STORE, true), []);
const renderId = [
(withSharedAnimation ? SHARED_PREFIX : randomIdPrefix), id, size, customColor?.join(','),
(withSharedAnimation ? SHARED_PREFIX : randomIdPrefix),
id,
size,
(withSharedAnimation ? customColor : undefined),
].filter(Boolean).join('_');
return (
@ -159,7 +167,6 @@ const StickerView: FC<OwnProps> = ({
)}
tgsUrl={fullMediaData}
play={shouldPlay}
color={customColor}
noLoop={!shouldLoop}
forceOnHeavyAnimation={forceOnHeavyAnimation}
isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
@ -168,6 +175,7 @@ const StickerView: FC<OwnProps> = ({
onLoad={markPlayerReady}
onLoop={onAnimatedStickerLoop}
onEnded={onAnimatedStickerLoop}
color={customColor}
/>
) : isVideo ? (
<OptimizedVideo
@ -180,12 +188,14 @@ const StickerView: FC<OwnProps> = ({
disablePictureInPicture
onReady={markPlayerReady}
onEnded={onVideoEnded}
style={filterStyle}
/>
) : (
<img
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames, 'sticker-media')}
src={fullMediaData}
alt={emoji}
style={filterStyle}
draggable={false}
/>
))}

View File

@ -26,7 +26,7 @@
width: calc(100vw - 1rem);
}
:global(.custom-emoji) {
:global(.sticker-set-cover), :global(.custom-emoji) {
color: var(--color-primary);
}
}

View File

@ -462,6 +462,10 @@
.custom-emoji {
margin: 0;
vertical-align: text-top;
&.colorable {
filter: var(--input-custom-emoji-filter);
}
}
// Workaround to preserve correct input height

View File

@ -16,6 +16,9 @@ import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useMedia from '../../../hooks/useMedia';
import useMediaTransition from '../../../hooks/useMediaTransition';
import useCoordsInSharedCanvas from '../../../hooks/useCoordsInSharedCanvas';
import useCustomEmoji from '../../common/hooks/useCustomEmoji';
import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener';
import useColorFilter from '../../../hooks/stickers/useColorFilter';
import AnimatedSticker from '../../common/AnimatedSticker';
import OptimizedVideo from '../../ui/OptimizedVideo';
@ -41,7 +44,14 @@ const StickerSetCover: FC<OwnProps> = ({
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const { hasThumbnail, isLottie, isVideos: isVideo } = stickerSet;
const {
hasThumbnail, thumbCustomEmojiId, isLottie, isVideos: isVideo,
} = stickerSet;
const { customEmoji } = useCustomEmoji(thumbCustomEmojiId);
const hasCustomColor = customEmoji?.shouldUseTextColor;
const customColor = useDynamicColorListener(containerRef, !hasCustomColor);
const colorFilter = useColorFilter(customColor);
const isIntersecting = useIsIntersecting(containerRef, observeIntersection);
const shouldPlay = isIntersecting && !noPlay;
@ -86,12 +96,14 @@ const StickerSetCover: FC<OwnProps> = ({
className={buildClassName(styles.video, transitionClassNames)}
src={mediaData}
canPlay={shouldPlay}
style={colorFilter}
loop
disablePictureInPicture
/>
) : (
<img
src={mediaData || staticMediaData}
style={colorFilter}
className={buildClassName(styles.image, transitionClassNames)}
alt=""
/>

View File

@ -161,7 +161,7 @@
}
}
.StickerButton.custom-emoji {
.StickerButton.custom-emoji, .sticker-set-cover {
color: var(--color-text);
}

View File

@ -5,14 +5,19 @@ import { getGlobal } from '../../../../global';
import { EMOJI_SIZES } from '../../../../config';
import { REM } from '../../../common/helpers/mediaDimensions';
import { getInputCustomEmojiParams } from '../../../../util/customEmojiManager';
import buildClassName from '../../../../util/buildClassName';
export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]';
export function buildCustomEmojiHtml(emoji: ApiSticker) {
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(emoji);
const className = buildClassName(
'custom-emoji', 'emoji', 'emoji-small', isPlaceholder && 'placeholder', emoji.shouldUseTextColor && 'colorable',
);
return `<img
class="custom-emoji emoji emoji-small ${isPlaceholder ? 'placeholder' : ''}"
class="${className}"
draggable="false"
alt="${emoji.emoji}"
data-document-id="${emoji.id}"
@ -25,8 +30,17 @@ export function buildCustomEmojiHtml(emoji: ApiSticker) {
export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessageEntityCustomEmoji) {
const customEmoji = getGlobal().customEmojis.byId[entity.documentId];
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(customEmoji);
const className = buildClassName(
'custom-emoji',
'emoji',
'emoji-small',
isPlaceholder && 'placeholder',
customEmoji?.shouldUseTextColor && 'colorable',
);
return `<img
class="custom-emoji emoji emoji-small ${isPlaceholder ? 'placeholder' : ''}"
class="${className}"
draggable="false"
alt="${rawText}"
data-document-id="${entity.documentId}"

View File

@ -1,7 +1,8 @@
import {
useCallback, useEffect, useRef,
} from '../../../../lib/teact/teact';
import { requestMeasure } from '../../../../lib/fasterdom/fasterdom';
import { requestMeasure, requestMutation } from '../../../../lib/fasterdom/fasterdom';
import { ensureRLottie } from '../../../../lib/rlottie/RLottie.async';
import type { ApiSticker } from '../../../../api/types';
import type { Signal } from '../../../../util/signals';
@ -16,13 +17,14 @@ import {
import { round } from '../../../../util/math';
import AbsoluteVideo from '../../../../util/AbsoluteVideo';
import { REM } from '../../../common/helpers/mediaDimensions';
import { hexToRgb } from '../../../../util/switchTheme';
import useColorFilter from '../../../../hooks/stickers/useColorFilter';
import useResizeObserver from '../../../../hooks/useResizeObserver';
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
import useThrottledCallback from '../../../../hooks/useThrottledCallback';
import useDynamicColorListener from '../../../../hooks/useDynamicColorListener';
import useDynamicColorListener from '../../../../hooks/stickers/useDynamicColorListener';
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
import { ensureRLottie } from '../../../../lib/rlottie/RLottie.async';
const SIZE = 1.25 * REM;
const THROTTLE_MS = 300;
@ -44,7 +46,8 @@ export default function useInputCustomEmojis(
canPlayAnimatedEmojis: boolean,
isActive?: boolean,
) {
const { rgbColor: textColor } = useDynamicColorListener(inputRef);
const customColor = useDynamicColorListener(inputRef);
const colorFilter = useColorFilter(customColor, true);
const playersById = useRef<Map<string, CustomEmojiPlayer>>(new Map());
const clearPlayers = useCallback((ids: string[]) => {
@ -59,6 +62,11 @@ export default function useInputCustomEmojis(
const synchronizeElements = useCallback(() => {
if (!inputRef.current || !sharedCanvasRef.current || !sharedCanvasHqRef.current) return;
requestMutation(() => {
document.documentElement.style.setProperty('--input-custom-emoji-filter', colorFilter || 'none');
});
const global = getGlobal();
const playerIdsToClear = new Set(playersById.current.keys());
const customEmojis = Array.from(inputRef.current.querySelectorAll<HTMLElement>('.custom-emoji'));
@ -67,7 +75,7 @@ export default function useInputCustomEmojis(
if (!element.dataset.uniqueId) {
return;
}
const playerId = `${prefixId}${element.dataset.uniqueId}${textColor?.join(',') || ''}`;
const playerId = `${prefixId}${element.dataset.uniqueId}${customColor || ''}`;
const documentId = element.dataset.documentId!;
playerIdsToClear.delete(playerId);
@ -77,50 +85,53 @@ export default function useInputCustomEmojis(
return;
}
const canvasBounds = sharedCanvasRef.current!.getBoundingClientRect();
const elementBounds = element.getBoundingClientRect();
const x = round((elementBounds.left - canvasBounds.left) / canvasBounds.width, 4);
const y = round((elementBounds.top - canvasBounds.top) / canvasBounds.height, 4);
requestMeasure(() => {
const canvasBounds = sharedCanvasRef.current!.getBoundingClientRect();
const elementBounds = element.getBoundingClientRect();
const x = round((elementBounds.left - canvasBounds.left) / canvasBounds.width, 4);
const y = round((elementBounds.top - canvasBounds.top) / canvasBounds.height, 4);
if (playersById.current.has(playerId)) {
const player = playersById.current.get(playerId)!;
player.updatePosition(x, y);
return;
}
const customEmoji = global.customEmojis.byId[documentId];
if (!customEmoji) {
return;
}
const isHq = customEmoji?.stickerSetInfo && selectIsAlwaysHighPriorityEmoji(global, customEmoji.stickerSetInfo);
const renderId = [
prefixId, documentId, textColor?.join(','),
].filter(Boolean).join('_');
createPlayer({
customEmoji,
sharedCanvasRef,
sharedCanvasHqRef,
absoluteContainerRef,
renderId,
viewId: playerId,
mediaUrl,
isHq,
position: { x, y },
textColor,
}).then((animation) => {
if (canPlayAnimatedEmojis) {
animation.play();
if (playersById.current.has(playerId)) {
const player = playersById.current.get(playerId)!;
player.updatePosition(x, y);
return;
}
playersById.current.set(playerId, animation);
const customEmoji = global.customEmojis.byId[documentId];
if (!customEmoji) {
return;
}
const isHq = customEmoji?.stickerSetInfo && selectIsAlwaysHighPriorityEmoji(global, customEmoji.stickerSetInfo);
const renderId = [
prefixId, documentId, customColor,
].filter(Boolean).join('_');
createPlayer({
customEmoji,
sharedCanvasRef,
sharedCanvasHqRef,
absoluteContainerRef,
renderId,
viewId: playerId,
mediaUrl,
isHq,
position: { x, y },
textColor: customColor,
colorFilter,
}).then((animation) => {
if (canPlayAnimatedEmojis) {
animation.play();
}
playersById.current.set(playerId, animation);
});
});
});
clearPlayers(Array.from(playerIdsToClear));
}, [
inputRef, sharedCanvasRef, sharedCanvasHqRef, clearPlayers, prefixId, textColor, absoluteContainerRef,
canPlayAnimatedEmojis,
inputRef, sharedCanvasRef, sharedCanvasHqRef, clearPlayers, prefixId, customColor, absoluteContainerRef,
canPlayAnimatedEmojis, colorFilter,
]);
useEffect(() => {
@ -143,11 +154,11 @@ export default function useInputCustomEmojis(
});
}, [getHtml, synchronizeElements, inputRef, clearPlayers, sharedCanvasRef, isActive]);
useEffectWithPrevDeps(([prevTextColor]) => {
if (textColor !== prevTextColor) {
useEffectWithPrevDeps(([prevCustomColor]) => {
if (customColor !== prevCustomColor) {
synchronizeElements();
}
}, [textColor, synchronizeElements]);
}, [customColor, synchronizeElements]);
const throttledSynchronizeElements = useThrottledCallback(
synchronizeElements,
@ -194,6 +205,7 @@ async function createPlayer({
position,
isHq,
textColor,
colorFilter,
}: {
customEmoji: ApiSticker;
sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
@ -204,9 +216,11 @@ async function createPlayer({
mediaUrl: string;
position: { x: number; y: number };
isHq?: boolean;
textColor?: [number, number, number];
textColor?: string;
colorFilter?: string;
}): Promise<CustomEmojiPlayer> {
if (customEmoji.isLottie) {
const color = customEmoji.shouldUseTextColor && textColor ? hexToRgb(textColor) : undefined;
const RLottie = await ensureRLottie();
const lottie = RLottie.init(
mediaUrl,
@ -218,7 +232,7 @@ async function createPlayer({
isLowPriority: !isHq,
},
viewId,
customEmoji.shouldUseTextColor ? textColor : undefined,
color ? [color.r, color.g, color.b] : undefined,
);
return {
@ -232,7 +246,12 @@ async function createPlayer({
}
if (customEmoji.isVideo) {
const absoluteVideo = new AbsoluteVideo(mediaUrl, absoluteContainerRef.current!, { size: SIZE, position });
const style = colorFilter ? `filter: ${colorFilter};` : undefined;
const absoluteVideo = new AbsoluteVideo(
mediaUrl,
absoluteContainerRef.current!,
{ size: SIZE, position, style },
);
return {
play: () => absoluteVideo.play(),
pause: () => absoluteVideo.pause(),

View File

@ -147,7 +147,7 @@ const RightSearch: FC<OwnProps & StateProps> = ({
/>
<div className="info">
<div className="search-result-message-top">
<FullNameTitle peer={(senderUser || senderChat)!} />
<FullNameTitle peer={(senderUser || senderChat)!} withEmojiStatus />
<LastMessageMeta message={message} />
</div>
<div className="subtitle" dir="auto">

View File

@ -0,0 +1,94 @@
import { useEffect } from '../../lib/teact/teact';
import { hexToRgb } from '../../util/switchTheme';
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
const SVG_MAP = new Map<string, SvgColorFilter>();
class SvgColorFilter {
public filterId: string;
public element: SVGSVGElement;
private referenceCount = 0;
constructor(public color: string) {
this.filterId = `color-filter-${color.slice(1)}`;
this.element = document.createElementNS(SVG_NAMESPACE, 'svg');
this.element.width.baseVal.valueAsString = '0px';
this.element.height.baseVal.valueAsString = '0px';
const defs = document.createElementNS(SVG_NAMESPACE, 'defs');
this.element.appendChild(defs);
const filter = document.createElementNS(SVG_NAMESPACE, 'filter');
filter.id = this.filterId;
filter.setAttribute('color-interpolation-filters', 'sRGB');
defs.appendChild(filter);
const feColorMatrix = document.createElementNS(SVG_NAMESPACE, 'feColorMatrix');
feColorMatrix.setAttribute('type', 'matrix');
const rgbColor = hexToRgb(color);
feColorMatrix.setAttribute(
'values',
`0 0 0 0 ${rgbColor.r / 255} 0 0 0 0 ${rgbColor.g / 255} 0 0 0 0 ${rgbColor.b / 255} 0 0 0 1 0`,
);
filter.appendChild(feColorMatrix);
document.body.appendChild(this.element);
}
public getFilterId() {
this.referenceCount += 1;
return this.filterId;
}
public removeReference() {
this.referenceCount -= 1;
if (this.referenceCount === 0) {
this.element.remove();
}
}
public isUsed() {
return this.referenceCount > 0;
}
}
export default function useColorFilter(color?: string, asValue?: boolean) {
useEffect(() => {
if (!color) return undefined;
return () => {
const colorFilter = SVG_MAP.get(color);
if (colorFilter) {
colorFilter.removeReference();
if (!colorFilter.isUsed()) {
SVG_MAP.delete(colorFilter.color);
}
}
};
}, [color]);
if (!color) return undefined;
if (SVG_MAP.has(color)) {
const svg = SVG_MAP.get(color)!;
return prepareStyle(svg.getFilterId(), asValue);
}
const svg = new SvgColorFilter(color);
SVG_MAP.set(color, svg);
return prepareStyle(svg.getFilterId(), asValue);
}
function prepareStyle(filterId: string, asValue?: boolean) {
if (asValue) {
return `url(#${filterId})`;
}
return `filter: url(#${filterId});`;
}

View File

@ -1,20 +1,17 @@
import {
useCallback, useEffect, useLayoutEffect, useRef, useState,
} from '../lib/teact/teact';
import { hexToRgb } from '../util/switchTheme';
import { getPropertyHexColor } from '../util/themeStyle';
import useResizeObserver from './useResizeObserver';
import useSyncEffect from './useSyncEffect';
useCallback, useEffect, useLayoutEffect, useState,
} from '../../lib/teact/teact';
import { getPropertyHexColor } from '../../util/themeStyle';
import useResizeObserver from '../useResizeObserver';
// Transition required to detect `color` property change.
// Duration parameter describes a delay between color change and color state update.
// Small values may cause large amount of re-renders.
const TRANSITION_PROPERTY = 'color';
const TRANSITION_STYLE = `60ms ${TRANSITION_PROPERTY} linear`;
const TRANSITION_STYLE = `50ms ${TRANSITION_PROPERTY} linear`;
export default function useDynamicColorListener(ref: React.RefObject<HTMLElement>, isDisabled?: boolean) {
const [hexColor, setHexColor] = useState<string | undefined>();
const rgbColorRef = useRef<[number, number, number] | undefined>();
const updateColor = useCallback(() => {
if (!ref.current || isDisabled) {
@ -30,17 +27,6 @@ export default function useDynamicColorListener(ref: React.RefObject<HTMLElement
// We will receive `resize` event when parent is shown again.
useResizeObserver(ref, updateColor, isDisabled);
// Update RGB color only when hex color changes
useSyncEffect(() => {
if (!hexColor) {
rgbColorRef.current = undefined;
return;
}
const { r, g, b } = hexToRgb(hexColor);
rgbColorRef.current = [r, g, b];
}, [hexColor]);
useLayoutEffect(() => {
const el = ref.current;
if (!el || isDisabled) {
@ -78,8 +64,5 @@ export default function useDynamicColorListener(ref: React.RefObject<HTMLElement
};
}, [isDisabled, ref, updateColor]);
return {
hexColor,
rgbColor: rgbColorRef.current,
};
return hexColor;
}

View File

@ -236,9 +236,7 @@ class RLottie {
const frame = this.getFrame(this.prevFrameIndex) || this.getFrame(Math.round(this.approxFrameIndex));
if (frame && frame !== WAITING) {
requestMutation(() => {
ctx.drawImage(frame, containerInfo.coords!.x, containerInfo.coords!.y);
});
ctx.drawImage(frame, containerInfo.coords!.x, containerInfo.coords!.y);
}
}

View File

@ -1,9 +1,11 @@
import { requestMeasure, requestMutation } from '../lib/fasterdom/fasterdom';
import safePlay from './safePlay';
type AbsoluteVideoOptions = {
position: { x: number; y: number };
noLoop?: boolean;
size: number;
style?: string;
};
export default class AbsoluteVideo {
@ -20,6 +22,9 @@ export default class AbsoluteVideo {
this.video.src = videoUrl;
this.video.disablePictureInPicture = true;
this.video.muted = true;
if (options.style) {
this.video.setAttribute('style', options.style);
}
this.video.style.position = 'absolute';
this.video.load();
@ -27,8 +32,11 @@ export default class AbsoluteVideo {
this.video.loop = true;
}
this.container.appendChild(this.video);
this.recalculatePositionAndSize();
requestMutation(() => {
this.container.appendChild(this.video!);
this.recalculatePositionAndSize();
});
}
public play() {
@ -60,12 +68,17 @@ export default class AbsoluteVideo {
}
private recalculatePositionAndSize() {
if (!this.video) return;
const { size, position: { x, y } } = this.options;
const { width, height } = this.container.getBoundingClientRect();
this.video.style.left = `${Math.round(x * width)}px`;
this.video.style.top = `${Math.round(y * height)}px`;
this.video.style.width = `${size}px`;
this.video.style.height = `${size}px`;
requestMeasure(() => {
if (!this.video) return;
const video = this.video;
const { width, height } = this.container.getBoundingClientRect();
requestMutation(() => {
video.style.left = `${Math.round(x * width)}px`;
video.style.top = `${Math.round(y * height)}px`;
video.style.width = `${size}px`;
video.style.height = `${size}px`;
});
});
}
}

View File

@ -1,4 +1,5 @@
import { addCallback } from '../lib/teact/teactn';
import { requestMutation } from '../lib/fasterdom/fasterdom';
import { getGlobal } from '../global';
import { ApiMediaFormat } from '../api/types';
@ -76,12 +77,20 @@ function processDomForCustomEmoji() {
}
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(customEmoji);
if (!isPlaceholder) {
emoji.src = src;
emoji.classList.remove('placeholder');
if (uniqueId) emoji.dataset.uniqueId = uniqueId;
if (customEmoji.shouldUseTextColor && !emoji.classList.contains('colorable')) {
requestMutation(() => {
emoji.classList.add('colorable');
});
}
callInputRenderHandlers(customEmoji.id);
if (!isPlaceholder) {
requestMutation(() => {
emoji.src = src;
emoji.classList.remove('placeholder');
if (uniqueId) emoji.dataset.uniqueId = uniqueId;
callInputRenderHandlers(customEmoji.id);
});
}
});
}