189 lines
5.3 KiB
TypeScript
189 lines
5.3 KiB
TypeScript
import {
|
|
memo, useEffect, useMemo, useRef, useSignal, useState,
|
|
} from '../../../lib/teact/teact';
|
|
|
|
import type { ApiSticker } from '../../../api/types';
|
|
|
|
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
|
import { getStickerMediaHash } from '../../../global/helpers';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import buildStyle from '../../../util/buildStyle';
|
|
import { preloadImage } from '../../../util/files';
|
|
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useMedia from '../../../hooks/useMedia';
|
|
import useResizeObserver from '../../../hooks/useResizeObserver';
|
|
import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio';
|
|
|
|
import styles from './RadialPatternBackground.module.scss';
|
|
|
|
type OwnProps = {
|
|
backgroundColors: string[];
|
|
patternColor?: string;
|
|
patternIcon?: ApiSticker;
|
|
className?: string;
|
|
clearBottomSector?: boolean;
|
|
patternSize?: number;
|
|
};
|
|
|
|
const RINGS = 3;
|
|
const BASE_RING_ITEM_COUNT = 8;
|
|
const RING_INCREMENT = 0.5;
|
|
const CENTER_EMPTINESS = 0.05;
|
|
const MAX_RADIUS = 0.4;
|
|
const BASE_ICON_SIZE = 20;
|
|
|
|
const MIN_SIZE = 250;
|
|
|
|
const RadialPatternBackground = ({
|
|
backgroundColors,
|
|
patternColor,
|
|
patternIcon,
|
|
clearBottomSector,
|
|
className,
|
|
patternSize = 1,
|
|
}: OwnProps) => {
|
|
const containerRef = useRef<HTMLDivElement>();
|
|
const canvasRef = useRef<HTMLCanvasElement>();
|
|
|
|
const [getContainerSize, setContainerSize] = useSignal({ width: 0, height: 0 });
|
|
|
|
const dpr = useDevicePixelRatio();
|
|
|
|
const [emojiImage, setEmojiImage] = useState<HTMLImageElement | undefined>();
|
|
|
|
const previewMediaHash = patternIcon && getStickerMediaHash(patternIcon, 'preview');
|
|
const previewUrl = useMedia(previewMediaHash);
|
|
|
|
useEffect(() => {
|
|
if (!previewUrl) return;
|
|
preloadImage(previewUrl).then(setEmojiImage);
|
|
}, [previewUrl]);
|
|
|
|
const patternPositions = useMemo(() => {
|
|
const coordinates: { x: number; y: number; sizeFactor: number }[] = [];
|
|
for (let ring = 1; ring <= RINGS; ring++) {
|
|
const ringItemCount = Math.floor(BASE_RING_ITEM_COUNT * (1 + (ring - 1) * RING_INCREMENT));
|
|
const ringProgress = ring / RINGS;
|
|
const ringRadius = CENTER_EMPTINESS + (MAX_RADIUS - CENTER_EMPTINESS) * ringProgress;
|
|
|
|
const angleShift = ring % 2 === 0 ? Math.PI / ringItemCount : 0;
|
|
|
|
for (let i = 0; i < ringItemCount; i++) {
|
|
const angle = (i / ringItemCount) * Math.PI * 2 + angleShift;
|
|
|
|
if (clearBottomSector && angle > Math.PI * 0.45 && angle < Math.PI * 0.55) {
|
|
continue;
|
|
}
|
|
|
|
// Slightly oval
|
|
const xOffset = ringRadius * 1.71 * Math.cos(angle);
|
|
const yOffset = ringRadius * Math.sin(angle);
|
|
|
|
const sizeFactor = 1.4 - ringProgress * Math.random();
|
|
|
|
coordinates.push({
|
|
x: xOffset,
|
|
y: yOffset,
|
|
sizeFactor,
|
|
});
|
|
}
|
|
}
|
|
return coordinates;
|
|
}, [clearBottomSector]);
|
|
|
|
useResizeObserver(containerRef, (entry) => {
|
|
setContainerSize({
|
|
width: entry.contentRect.width,
|
|
height: entry.contentRect.height,
|
|
});
|
|
});
|
|
|
|
useEffect(() => {
|
|
const container = containerRef.current;
|
|
if (container) {
|
|
setContainerSize({
|
|
width: container.clientWidth,
|
|
height: container.clientHeight,
|
|
});
|
|
}
|
|
}, [setContainerSize]);
|
|
|
|
const draw = useLastCallback(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas || !emojiImage) return;
|
|
const ctx = canvas.getContext('2d')!;
|
|
const { width, height } = canvas;
|
|
if (!width || !height) return;
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
ctx.save();
|
|
patternPositions.forEach(({
|
|
x, y, sizeFactor,
|
|
}) => {
|
|
const renderX = x * patternSize * Math.max(width, MIN_SIZE * dpr) + width / 2;
|
|
const renderY = y * patternSize * Math.max(height, MIN_SIZE * dpr) + height / 2;
|
|
|
|
const size = BASE_ICON_SIZE * dpr * patternSize * sizeFactor;
|
|
|
|
ctx.drawImage(emojiImage, renderX - size / 2, renderY - size / 2, size, size);
|
|
});
|
|
ctx.restore();
|
|
|
|
if (patternColor) {
|
|
ctx.save();
|
|
ctx.fillStyle = patternColor;
|
|
ctx.globalCompositeOperation = 'source-atop';
|
|
ctx.fillRect(0, 0, width, height);
|
|
ctx.restore();
|
|
}
|
|
|
|
const radialGradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, width / 2);
|
|
radialGradient.addColorStop(0, '#FFFFFF77');
|
|
radialGradient.addColorStop(1, '#FFFFFF');
|
|
|
|
// Alpha mask
|
|
ctx.save();
|
|
ctx.globalCompositeOperation = 'destination-out';
|
|
ctx.fillStyle = radialGradient;
|
|
ctx.fillRect(0, 0, width, height);
|
|
ctx.restore();
|
|
});
|
|
|
|
useEffect(() => {
|
|
draw();
|
|
}, [emojiImage]);
|
|
|
|
useEffect(() => {
|
|
const { width, height } = getContainerSize();
|
|
const canvas = canvasRef.current!;
|
|
if (!width || !height) {
|
|
return;
|
|
}
|
|
|
|
const maxSide = Math.max(width, height);
|
|
requestMutation(() => {
|
|
canvas.width = maxSide * dpr;
|
|
canvas.height = maxSide * dpr;
|
|
|
|
draw();
|
|
});
|
|
}, [getContainerSize, dpr]);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={buildClassName(styles.root, className)}
|
|
style={buildStyle(
|
|
`--_bg-1: ${backgroundColors[0]}`,
|
|
`--_bg-2: ${backgroundColors[1] || backgroundColors[0]}`,
|
|
)}
|
|
>
|
|
<canvas className={styles.canvas} ref={canvasRef} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(RadialPatternBackground);
|