1- import {
2- memo , useEffect , useMemo , useRef , useSignal , useState ,
3- } from '../../../lib/teact/teact' ;
1+ import { memo , useEffect , useLayoutEffect , useMemo , useRef , useSignal , useState } from '../../../lib/teact/teact' ;
42
53import type { ApiSticker } from '../../../api/types' ;
64
@@ -9,7 +7,8 @@ import { getStickerMediaHash } from '../../../global/helpers';
97import buildClassName from '../../../util/buildClassName' ;
108import buildStyle from '../../../util/buildStyle' ;
119import { preloadImage } from '../../../util/files' ;
12- import { clamp } from '../../../util/math' ;
10+ import { hexToRgb } from '../../../util/switchTheme.ts' ;
11+ import { REM } from '../helpers/mediaDimensions' ;
1312
1413import useLastCallback from '../../../hooks/useLastCallback' ;
1514import useMedia from '../../../hooks/useMedia' ;
@@ -20,31 +19,39 @@ import styles from './RadialPatternBackground.module.scss';
2019
2120type OwnProps = {
2221 backgroundColors : string [ ] ;
23- patternColor ?: string ;
2422 patternIcon ?: ApiSticker ;
23+ patternColor ?: string ;
24+ patternSize ?: number ;
25+ ringsCount ?: number ;
26+ ovalFactor ?: number ;
27+ withLinearGradient ?: boolean ;
2528 className ?: string ;
2629 clearBottomSector ?: boolean ;
27- patternSize ?: number ;
28- patternOpacity ?: number ;
30+ yPosition ?: number ;
2931} ;
3032
31- const RINGS = 3 ;
3233const BASE_RING_ITEM_COUNT = 8 ;
3334const RING_INCREMENT = 0.5 ;
34- const CENTER_EMPTINESS = 0.05 ;
35- const MAX_RADIUS = 0.4 ;
36- const BASE_ICON_SIZE = 20 ;
35+ const CENTER_EMPTINESS = 0.1 ;
36+ const MAX_RADIUS = 0.43 ;
37+ const MIN_SIZE = 4 * REM ;
38+ const PATTERN_OPACITY = 0.9 ;
3739
38- const MIN_SIZE = 250 ;
40+ const DEFAULT_PATTERN_SIZE = 20 ;
41+ const DEFAULT_RINGS_COUNT = 3 ;
42+ const DEFAULT_OVAL_FACTOR = 1.4 ;
3943
4044const RadialPatternBackground = ( {
4145 backgroundColors,
42- patternColor,
4346 patternIcon,
44- patternOpacity,
47+ patternColor,
48+ patternSize = DEFAULT_PATTERN_SIZE ,
49+ ringsCount = DEFAULT_RINGS_COUNT ,
50+ ovalFactor = DEFAULT_OVAL_FACTOR ,
51+ withLinearGradient,
4552 clearBottomSector,
4653 className,
47- patternSize = 1 ,
54+ yPosition ,
4855} : OwnProps ) => {
4956 const containerRef = useRef < HTMLDivElement > ( ) ;
5057 const canvasRef = useRef < HTMLCanvasElement > ( ) ;
@@ -65,11 +72,10 @@ const RadialPatternBackground = ({
6572
6673 const patternPositions = useMemo ( ( ) => {
6774 const coordinates : { x : number ; y : number ; sizeFactor : number } [ ] = [ ] ;
68- for ( let ring = 1 ; ring <= RINGS ; ring ++ ) {
75+ for ( let ring = 1 ; ring <= ringsCount ; ring ++ ) {
6976 const ringItemCount = Math . floor ( BASE_RING_ITEM_COUNT * ( 1 + ( ring - 1 ) * RING_INCREMENT ) ) ;
70- const ringProgress = ring / RINGS ;
77+ const ringProgress = ring / ringsCount ;
7178 const ringRadius = CENTER_EMPTINESS + ( MAX_RADIUS - CENTER_EMPTINESS ) * ringProgress ;
72-
7379 const angleShift = ring % 2 === 0 ? Math . PI / ringItemCount : 0 ;
7480
7581 for ( let i = 0 ; i < ringItemCount ; i ++ ) {
@@ -79,11 +85,9 @@ const RadialPatternBackground = ({
7985 continue ;
8086 }
8187
82- // Slightly oval
83- const xOffset = ringRadius * 1.71 * Math . cos ( angle ) ;
88+ const xOffset = ringRadius * Math . cos ( angle ) * ovalFactor ;
8489 const yOffset = ringRadius * Math . sin ( angle ) ;
85-
86- const sizeFactor = 1.4 - ringProgress * Math . random ( ) ;
90+ const sizeFactor = 1.65 - ringProgress + Math . random ( ) / ringsCount ;
8791
8892 coordinates . push ( {
8993 x : xOffset ,
@@ -93,7 +97,7 @@ const RadialPatternBackground = ({
9397 }
9498 }
9599 return coordinates ;
96- } , [ clearBottomSector ] ) ;
100+ } , [ clearBottomSector , ovalFactor , ringsCount ] ) ;
97101
98102 useResizeObserver ( containerRef , ( entry ) => {
99103 setContainerSize ( {
@@ -119,59 +123,67 @@ const RadialPatternBackground = ({
119123 const { width, height } = canvas ;
120124 if ( ! width || ! height ) return ;
121125
126+ const centerX = width / 2 ;
127+ const centerY = yPosition !== undefined ? yPosition * dpr : height / 2 ;
128+
122129 ctx . clearRect ( 0 , 0 , width , height ) ;
123130
124- ctx . save ( ) ;
131+ const patternImage = drawPattern (
132+ width , height , centerX , centerY , backgroundColors [ 1 ] ?? backgroundColors [ 0 ] ,
133+ ) ;
134+ ctx . drawImage ( patternImage , 0 , 0 , width , height ) ;
135+
136+ const radialGradient = ctx . createRadialGradient ( centerX , centerY , 0 , centerX , centerY , width / 2 ) ;
137+ radialGradient . addColorStop ( 0.75 , 'rgb(255 255 255 / 0)' ) ;
138+ radialGradient . addColorStop ( 1 , 'rgb(255 255 255 / 0.75)' ) ;
139+
140+ // Alpha mask
141+ ctx . globalCompositeOperation = 'destination-out' ;
142+ ctx . fillStyle = radialGradient ;
143+ ctx . fillRect ( 0 , 0 , width , height ) ;
144+ } ) ;
145+
146+ const drawPattern = useLastCallback ( (
147+ width : number , height : number , centerX : number , centerY : number , color : string ,
148+ ) => {
149+ const canvas = new OffscreenCanvas ( width , height ) ;
150+ const ctx = canvas . getContext ( '2d' ) ! ;
151+
152+ ctx . globalAlpha = PATTERN_OPACITY ;
153+
125154 patternPositions . forEach ( ( {
126155 x, y, sizeFactor,
127156 } ) => {
128- const renderX = x * patternSize * Math . max ( width , MIN_SIZE * dpr ) + width / 2 ;
129- const renderY = y * patternSize * Math . max ( height , MIN_SIZE * dpr ) + height / 2 ;
157+ const renderX = x * Math . max ( width , MIN_SIZE * dpr ) + centerX ;
158+ const renderY = yPosition !== undefined ? y * Math . max ( width , MIN_SIZE * dpr ) + centerY
159+ : y * Math . max ( height , MIN_SIZE * dpr ) + centerY ;
130160
131- const size = BASE_ICON_SIZE * dpr * patternSize * sizeFactor ;
161+ const size = patternSize * dpr * sizeFactor ;
132162
133- ctx . drawImage ( emojiImage , renderX - size / 2 , renderY - size / 2 , size , size ) ;
163+ ctx . drawImage ( emojiImage ! , renderX - size / 2 , renderY - size / 2 , size , size ) ;
134164 } ) ;
135- ctx . restore ( ) ;
136-
137- if ( patternColor ) {
138- ctx . save ( ) ;
139- ctx . fillStyle = patternColor ;
140- ctx . globalCompositeOperation = 'source-atop' ;
141- ctx . fillRect ( 0 , 0 , width , height ) ;
142- ctx . restore ( ) ;
143- }
144-
145- const radialGradient = ctx . createRadialGradient ( width / 2 , height / 2 , 0 , width / 2 , height / 2 , width / 2 ) ;
146-
147- const alpha = clamp ( 0.6 * ( patternOpacity ?? 1 ) , 0 , 1 ) ;
148165
149- radialGradient . addColorStop ( 0 , `rgb(255 255 255 / ${ 1 - alpha } )` ) ;
150- radialGradient . addColorStop ( 1 , `rgb(255 255 255 / 1)` ) ;
151-
152- // Alpha mask
153- ctx . save ( ) ;
154- ctx . globalCompositeOperation = 'destination-out' ;
155- ctx . fillStyle = radialGradient ;
166+ ctx . fillStyle = adjustBrightness ( color , - 0.075 ) ;
167+ ctx . globalCompositeOperation = 'source-in' ;
156168 ctx . fillRect ( 0 , 0 , width , height ) ;
157- ctx . restore ( ) ;
169+
170+ return canvas ;
158171 } ) ;
159172
160173 useEffect ( ( ) => {
161174 draw ( ) ;
162- } , [ emojiImage , patternOpacity , patternSize , patternColor , patternPositions ] ) ;
175+ } , [ emojiImage , patternColor , patternPositions , yPosition ] ) ;
163176
164- useEffect ( ( ) => {
177+ useLayoutEffect ( ( ) => {
165178 const { width, height } = getContainerSize ( ) ;
166179 const canvas = canvasRef . current ;
167180 if ( ! width || ! height || ! canvas ) {
168181 return ;
169182 }
170183
171- const maxSide = Math . max ( width , height ) ;
172184 requestMutation ( ( ) => {
173- canvas . width = maxSide * dpr ;
174- canvas . height = maxSide * dpr ;
185+ canvas . width = width * dpr ;
186+ canvas . height = height * dpr ;
175187
176188 draw ( ) ;
177189 } ) ;
@@ -180,10 +192,10 @@ const RadialPatternBackground = ({
180192 return (
181193 < div
182194 ref = { containerRef }
183- className = { buildClassName ( styles . root , className ) }
195+ className = { buildClassName ( styles . root , withLinearGradient && styles . withLinearGradient , className ) }
184196 style = { buildStyle (
185- `--_bg-1: ${ backgroundColors [ 0 ] } ` ,
186- `--_bg-2 : ${ backgroundColors [ 1 ] || backgroundColors [ 0 ] } ` ,
197+ ... buildGradients ( backgroundColors , withLinearGradient ) ,
198+ yPosition !== undefined && `--_y-shift : ${ yPosition } px ` ,
187199 ) }
188200 >
189201 < canvas
@@ -196,3 +208,36 @@ const RadialPatternBackground = ({
196208} ;
197209
198210export default memo ( RadialPatternBackground ) ;
211+
212+ function buildGradients ( colors : string [ ] , withLinearGradient = false ) {
213+ if ( withLinearGradient ) {
214+ return [
215+ `--_bg-linear-1: ${ colors [ 0 ] } ` ,
216+ `--_bg-linear-2: ${ colors [ 1 ] } ` ,
217+ `--_bg-radial-1: #ffffff33` ,
218+ `--_bg-radial-2: #ffffff00` ,
219+ ] ;
220+ }
221+
222+ return [
223+ `--_bg-radial-1: ${ colors [ 1 ] ? colors [ 0 ] : adjustBrightness ( colors [ 0 ] , 0.2 ) } ` ,
224+ `--_bg-radial-2: ${ colors [ 1 ] ?? colors [ 0 ] } ` ,
225+ ] ;
226+ }
227+
228+ function adjustBrightness ( hex : string , delta : number ) {
229+ const factor = 1 + delta ;
230+ const rgba = hexToRgb ( hex ) ;
231+ const darkenedRgba = [
232+ Math . min ( 255 , Math . round ( rgba . r * factor ) ) ,
233+ Math . min ( Math . round ( rgba . g * factor ) ) ,
234+ Math . min ( Math . round ( rgba . b * factor ) ) ,
235+ rgba . a ?? 1 ,
236+ ] as const ;
237+
238+ return rgbaToHex ( ...darkenedRgba ) ;
239+ }
240+
241+ function rgbaToHex ( r : number , g : number , b : number , a : number ) {
242+ return `#${ ( ( 1 << 24 ) + ( r << 16 ) + ( g << 8 ) + b ) . toString ( 16 ) . slice ( 1 ) } ${ Math . round ( a * 255 ) . toString ( 16 ) } ` ;
243+ }
0 commit comments