Radial Pattern Background: Various improvements
This commit is contained in:
parent
5e679218e2
commit
595d2f4b4c
@ -140,6 +140,9 @@ const SavedGift = ({
|
||||
backgroundColors={backdropColors}
|
||||
patternColor={patternColor}
|
||||
patternIcon={pattern.sticker}
|
||||
patternSize={14}
|
||||
ringsCount={1}
|
||||
ovalFactor={1}
|
||||
/>
|
||||
);
|
||||
}, [backdrop, pattern]);
|
||||
|
||||
@ -386,14 +386,9 @@
|
||||
|
||||
.radialPatternBackground {
|
||||
pointer-events: none;
|
||||
|
||||
// Hacky way to keep background stable during resizes
|
||||
position: absolute;
|
||||
top: 8rem;
|
||||
|
||||
inset: 0;
|
||||
aspect-ratio: 1 / 1;
|
||||
width: 140%;
|
||||
margin-top: -70%;
|
||||
}
|
||||
|
||||
.standaloneAvatar {
|
||||
|
||||
@ -45,6 +45,7 @@ import buildStyle from '../../../util/buildStyle';
|
||||
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { resolveTransitionName } from '../../../util/resolveTransitionName';
|
||||
import { REM } from '../helpers/mediaDimensions.ts';
|
||||
import renderText from '../helpers/renderText.tsx';
|
||||
|
||||
import { useVtn } from '../../../hooks/animations/useVtn';
|
||||
@ -104,9 +105,7 @@ const EMOJI_TOPIC_SIZE = 120;
|
||||
const LOAD_MORE_THRESHOLD = 3;
|
||||
const MAX_PHOTO_DASH_COUNT = 30;
|
||||
const STATUS_UPDATE_INTERVAL = 1000 * 60; // 1 min
|
||||
const PATTERN_COLOR = '#000000';
|
||||
const PATTERN_SIZE_FACTOR = 0.75;
|
||||
const PATTERN_OPACITY = 0.75;
|
||||
const PATTERN_Y_SHIFT = 8 * REM;
|
||||
|
||||
const ProfileInfo = ({
|
||||
isExpanded,
|
||||
@ -493,10 +492,11 @@ const ProfileInfo = ({
|
||||
<RadialPatternBackground
|
||||
backgroundColors={profileColorSet.bgColors}
|
||||
patternIcon={backgroundEmoji}
|
||||
patternColor={collectibleEmojiStatus?.patternColor || PATTERN_COLOR}
|
||||
patternColor={collectibleEmojiStatus?.patternColor}
|
||||
patternSize={16}
|
||||
withLinearGradient={Boolean(!collectibleEmojiStatus && profileColorSet.bgColors[1])}
|
||||
className={styles.radialPatternBackground}
|
||||
patternSize={PATTERN_SIZE_FACTOR}
|
||||
patternOpacity={collectibleEmojiStatus ? 1 : PATTERN_OPACITY}
|
||||
yPosition={PATTERN_Y_SHIFT}
|
||||
/>
|
||||
)}
|
||||
{pinnedGifts && (
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
.root {
|
||||
--_y-shift: 50%;
|
||||
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
|
||||
@ -9,9 +11,15 @@
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
background-image:
|
||||
radial-gradient(circle closest-side, #ffffff32 3rem, #ffffff00 7rem),
|
||||
radial-gradient(closest-side, var(--_bg-1), var(--_bg-2));
|
||||
background-image: radial-gradient(closest-side at 50% var(--_y-shift), var(--_bg-radial-1) 25%, var(--_bg-radial-2) 125%);
|
||||
}
|
||||
|
||||
&.withLinearGradient {
|
||||
&::before {
|
||||
background-image:
|
||||
radial-gradient(closest-side at 50% var(--_y-shift), var(--_bg-radial-1) 25%, var(--_bg-radial-2) 125%),
|
||||
linear-gradient(var(--_bg-linear-1), var(--_bg-linear-2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import {
|
||||
memo, useEffect, useMemo, useRef, useSignal, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { memo, useEffect, useLayoutEffect, useMemo, useRef, useSignal, useState } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiSticker } from '../../../api/types';
|
||||
|
||||
@ -9,7 +7,8 @@ import { getStickerMediaHash } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import { preloadImage } from '../../../util/files';
|
||||
import { clamp } from '../../../util/math';
|
||||
import { hexToRgb } from '../../../util/switchTheme.ts';
|
||||
import { REM } from '../helpers/mediaDimensions';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
@ -20,31 +19,39 @@ import styles from './RadialPatternBackground.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
backgroundColors: string[];
|
||||
patternColor?: string;
|
||||
patternIcon?: ApiSticker;
|
||||
patternColor?: string;
|
||||
patternSize?: number;
|
||||
ringsCount?: number;
|
||||
ovalFactor?: number;
|
||||
withLinearGradient?: boolean;
|
||||
className?: string;
|
||||
clearBottomSector?: boolean;
|
||||
patternSize?: number;
|
||||
patternOpacity?: number;
|
||||
yPosition?: 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 CENTER_EMPTINESS = 0.1;
|
||||
const MAX_RADIUS = 0.43;
|
||||
const MIN_SIZE = 4 * REM;
|
||||
const PATTERN_OPACITY = 0.9;
|
||||
|
||||
const MIN_SIZE = 250;
|
||||
const DEFAULT_PATTERN_SIZE = 20;
|
||||
const DEFAULT_RINGS_COUNT = 3;
|
||||
const DEFAULT_OVAL_FACTOR = 1.4;
|
||||
|
||||
const RadialPatternBackground = ({
|
||||
backgroundColors,
|
||||
patternColor,
|
||||
patternIcon,
|
||||
patternOpacity,
|
||||
patternColor,
|
||||
patternSize = DEFAULT_PATTERN_SIZE,
|
||||
ringsCount = DEFAULT_RINGS_COUNT,
|
||||
ovalFactor = DEFAULT_OVAL_FACTOR,
|
||||
withLinearGradient,
|
||||
clearBottomSector,
|
||||
className,
|
||||
patternSize = 1,
|
||||
yPosition,
|
||||
}: OwnProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const canvasRef = useRef<HTMLCanvasElement>();
|
||||
@ -65,11 +72,10 @@ const RadialPatternBackground = ({
|
||||
|
||||
const patternPositions = useMemo(() => {
|
||||
const coordinates: { x: number; y: number; sizeFactor: number }[] = [];
|
||||
for (let ring = 1; ring <= RINGS; ring++) {
|
||||
for (let ring = 1; ring <= ringsCount; ring++) {
|
||||
const ringItemCount = Math.floor(BASE_RING_ITEM_COUNT * (1 + (ring - 1) * RING_INCREMENT));
|
||||
const ringProgress = ring / RINGS;
|
||||
const ringProgress = ring / ringsCount;
|
||||
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++) {
|
||||
@ -79,11 +85,9 @@ const RadialPatternBackground = ({
|
||||
continue;
|
||||
}
|
||||
|
||||
// Slightly oval
|
||||
const xOffset = ringRadius * 1.71 * Math.cos(angle);
|
||||
const xOffset = ringRadius * Math.cos(angle) * ovalFactor;
|
||||
const yOffset = ringRadius * Math.sin(angle);
|
||||
|
||||
const sizeFactor = 1.4 - ringProgress * Math.random();
|
||||
const sizeFactor = 1.65 - ringProgress + Math.random() / ringsCount;
|
||||
|
||||
coordinates.push({
|
||||
x: xOffset,
|
||||
@ -93,7 +97,7 @@ const RadialPatternBackground = ({
|
||||
}
|
||||
}
|
||||
return coordinates;
|
||||
}, [clearBottomSector]);
|
||||
}, [clearBottomSector, ovalFactor, ringsCount]);
|
||||
|
||||
useResizeObserver(containerRef, (entry) => {
|
||||
setContainerSize({
|
||||
@ -119,59 +123,67 @@ const RadialPatternBackground = ({
|
||||
const { width, height } = canvas;
|
||||
if (!width || !height) return;
|
||||
|
||||
const centerX = width / 2;
|
||||
const centerY = yPosition !== undefined ? yPosition * dpr : height / 2;
|
||||
|
||||
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 patternImage = drawPattern(
|
||||
width, height, centerX, centerY, backgroundColors[1] ?? backgroundColors[0],
|
||||
);
|
||||
ctx.drawImage(patternImage, 0, 0, width, height);
|
||||
|
||||
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);
|
||||
|
||||
const alpha = clamp(0.6 * (patternOpacity ?? 1), 0, 1);
|
||||
|
||||
radialGradient.addColorStop(0, `rgb(255 255 255 / ${1 - alpha})`);
|
||||
radialGradient.addColorStop(1, `rgb(255 255 255 / 1)`);
|
||||
const radialGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, width / 2);
|
||||
radialGradient.addColorStop(0.75, 'rgb(255 255 255 / 0)');
|
||||
radialGradient.addColorStop(1, 'rgb(255 255 255 / 0.75)');
|
||||
|
||||
// Alpha mask
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
ctx.fillStyle = radialGradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
const drawPattern = useLastCallback((
|
||||
width: number, height: number, centerX: number, centerY: number, color: string,
|
||||
) => {
|
||||
const canvas = new OffscreenCanvas(width, height);
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
ctx.globalAlpha = PATTERN_OPACITY;
|
||||
|
||||
patternPositions.forEach(({
|
||||
x, y, sizeFactor,
|
||||
}) => {
|
||||
const renderX = x * Math.max(width, MIN_SIZE * dpr) + centerX;
|
||||
const renderY = yPosition !== undefined ? y * Math.max(width, MIN_SIZE * dpr) + centerY
|
||||
: y * Math.max(height, MIN_SIZE * dpr) + centerY;
|
||||
|
||||
const size = patternSize * dpr * sizeFactor;
|
||||
|
||||
ctx.drawImage(emojiImage!, renderX - size / 2, renderY - size / 2, size, size);
|
||||
});
|
||||
|
||||
ctx.fillStyle = adjustBrightness(color, -0.075);
|
||||
ctx.globalCompositeOperation = 'source-in';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
return canvas;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
draw();
|
||||
}, [emojiImage, patternOpacity, patternSize, patternColor, patternPositions]);
|
||||
}, [emojiImage, patternColor, patternPositions, yPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
const { width, height } = getContainerSize();
|
||||
const canvas = canvasRef.current;
|
||||
if (!width || !height || !canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxSide = Math.max(width, height);
|
||||
requestMutation(() => {
|
||||
canvas.width = maxSide * dpr;
|
||||
canvas.height = maxSide * dpr;
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
|
||||
draw();
|
||||
});
|
||||
@ -180,10 +192,10 @@ const RadialPatternBackground = ({
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={buildClassName(styles.root, className)}
|
||||
className={buildClassName(styles.root, withLinearGradient && styles.withLinearGradient, className)}
|
||||
style={buildStyle(
|
||||
`--_bg-1: ${backgroundColors[0]}`,
|
||||
`--_bg-2: ${backgroundColors[1] || backgroundColors[0]}`,
|
||||
...buildGradients(backgroundColors, withLinearGradient),
|
||||
yPosition !== undefined && `--_y-shift: ${yPosition}px`,
|
||||
)}
|
||||
>
|
||||
<canvas
|
||||
@ -196,3 +208,36 @@ const RadialPatternBackground = ({
|
||||
};
|
||||
|
||||
export default memo(RadialPatternBackground);
|
||||
|
||||
function buildGradients(colors: string[], withLinearGradient = false) {
|
||||
if (withLinearGradient) {
|
||||
return [
|
||||
`--_bg-linear-1: ${colors[0]}`,
|
||||
`--_bg-linear-2: ${colors[1]}`,
|
||||
`--_bg-radial-1: #ffffff33`,
|
||||
`--_bg-radial-2: #ffffff00`,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
`--_bg-radial-1: ${colors[1] ? colors[0] : adjustBrightness(colors[0], 0.2)}`,
|
||||
`--_bg-radial-2: ${colors[1] ?? colors[0]}`,
|
||||
];
|
||||
}
|
||||
|
||||
function adjustBrightness(hex: string, delta: number) {
|
||||
const factor = 1 + delta;
|
||||
const rgba = hexToRgb(hex);
|
||||
const darkenedRgba = [
|
||||
Math.min(255, Math.round(rgba.r * factor)),
|
||||
Math.min(Math.round(rgba.g * factor)),
|
||||
Math.min(Math.round(rgba.b * factor)),
|
||||
rgba.a ?? 1,
|
||||
] as const;
|
||||
|
||||
return rgbaToHex(...darkenedRgba);
|
||||
}
|
||||
|
||||
function rgbaToHex(r: number, g: number, b: number, a: number) {
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}${Math.round(a * 255).toString(16)}`;
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import { IS_TOUCH_ENV } from '../../../../util/browser/windowEnvironment.ts';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import buildStyle from '../../../../util/buildStyle';
|
||||
import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions.ts';
|
||||
import { renderPeerLink } from '../helpers/messageActions';
|
||||
|
||||
import useFlag from '../../../../hooks/useFlag.ts';
|
||||
@ -98,6 +99,8 @@ const StarGiftAction = ({
|
||||
backgroundColors={backgroundColors}
|
||||
patternColor={backdrop.patternColor}
|
||||
patternIcon={pattern.sticker}
|
||||
patternSize={14}
|
||||
yPosition={9.5 * REM}
|
||||
clearBottomSector
|
||||
/>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user