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