From 595d2f4b4cd0483a1241e3d9c2426f8341ac0839 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sat, 11 Oct 2025 19:07:24 +0200 Subject: [PATCH] Radial Pattern Background: Various improvements --- src/components/common/gift/SavedGift.tsx | 3 + .../common/profile/ProfileInfo.module.scss | 7 +- src/components/common/profile/ProfileInfo.tsx | 12 +- .../RadialPatternBackground.module.scss | 14 +- .../profile/RadialPatternBackground.tsx | 163 +++++++++++------- .../middle/message/actions/StarGiftUnique.tsx | 3 + 6 files changed, 128 insertions(+), 74 deletions(-) diff --git a/src/components/common/gift/SavedGift.tsx b/src/components/common/gift/SavedGift.tsx index 7ff16a23b..bad9253d7 100644 --- a/src/components/common/gift/SavedGift.tsx +++ b/src/components/common/gift/SavedGift.tsx @@ -140,6 +140,9 @@ const SavedGift = ({ backgroundColors={backdropColors} patternColor={patternColor} patternIcon={pattern.sticker} + patternSize={14} + ringsCount={1} + ovalFactor={1} /> ); }, [backdrop, pattern]); diff --git a/src/components/common/profile/ProfileInfo.module.scss b/src/components/common/profile/ProfileInfo.module.scss index 5fe1d3705..e4a9c369b 100644 --- a/src/components/common/profile/ProfileInfo.module.scss +++ b/src/components/common/profile/ProfileInfo.module.scss @@ -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 { diff --git a/src/components/common/profile/ProfileInfo.tsx b/src/components/common/profile/ProfileInfo.tsx index f74f10a9e..bac0d0c30 100644 --- a/src/components/common/profile/ProfileInfo.tsx +++ b/src/components/common/profile/ProfileInfo.tsx @@ -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 = ({ )} {pinnedGifts && ( diff --git a/src/components/common/profile/RadialPatternBackground.module.scss b/src/components/common/profile/RadialPatternBackground.module.scss index 9c430468a..67a9cd379 100644 --- a/src/components/common/profile/RadialPatternBackground.module.scss +++ b/src/components/common/profile/RadialPatternBackground.module.scss @@ -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)); + } } } diff --git a/src/components/common/profile/RadialPatternBackground.tsx b/src/components/common/profile/RadialPatternBackground.tsx index 4ba99b468..4ddd658d3 100644 --- a/src/components/common/profile/RadialPatternBackground.tsx +++ b/src/components/common/profile/RadialPatternBackground.tsx @@ -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(); const canvasRef = useRef(); @@ -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 (