@@ -14,14 +14,139 @@ export const DEFAULT_SHIMMER_COLORS = [
1414 '#ff7700' ,
1515]
1616
17+ const clamp = ( value : number , min : number , max : number ) : number =>
18+ Math . min ( max , Math . max ( min , value ) )
19+
20+ const normalizeHex = ( hex : string ) : string | null => {
21+ const trimmed = hex . trim ( )
22+ const withoutHash = trimmed . startsWith ( '#' ) ? trimmed . slice ( 1 ) : trimmed
23+ if ( withoutHash . length === 3 ) {
24+ return withoutHash
25+ . split ( '' )
26+ . map ( ( char ) => char + char )
27+ . join ( '' )
28+ }
29+ if ( withoutHash . length === 6 ) {
30+ return withoutHash
31+ }
32+ return null
33+ }
34+
35+ const hexToRgb = ( hex : string ) : { r : number ; g : number ; b : number } | null => {
36+ const normalized = normalizeHex ( hex )
37+ if ( ! normalized ) return null
38+ const r = parseInt ( normalized . slice ( 0 , 2 ) , 16 ) / 255
39+ const g = parseInt ( normalized . slice ( 2 , 4 ) , 16 ) / 255
40+ const b = parseInt ( normalized . slice ( 4 , 6 ) , 16 ) / 255
41+ if ( Number . isNaN ( r ) || Number . isNaN ( g ) || Number . isNaN ( b ) ) return null
42+ return { r, g, b }
43+ }
44+
45+ const rgbToHsl = (
46+ r : number ,
47+ g : number ,
48+ b : number ,
49+ ) : { h : number ; s : number ; l : number } => {
50+ const max = Math . max ( r , g , b )
51+ const min = Math . min ( r , g , b )
52+ const l = ( max + min ) / 2
53+
54+ let h = 0
55+ let s = 0
56+
57+ if ( max !== min ) {
58+ const d = max - min
59+ s = l > 0.5 ? d / ( 2 - max - min ) : d / ( max + min )
60+ switch ( max ) {
61+ case r :
62+ h = ( g - b ) / d + ( g < b ? 6 : 0 )
63+ break
64+ case g :
65+ h = ( b - r ) / d + 2
66+ break
67+ default :
68+ h = ( r - g ) / d + 4
69+ }
70+ h /= 6
71+ }
72+
73+ return { h, s, l }
74+ }
75+
76+ const hueToRgb = ( p : number , q : number , t : number ) : number => {
77+ let temp = t
78+ if ( temp < 0 ) temp += 1
79+ if ( temp > 1 ) temp -= 1
80+ if ( temp < 1 / 6 ) return p + ( q - p ) * 6 * temp
81+ if ( temp < 1 / 2 ) return q
82+ if ( temp < 2 / 3 ) return p + ( q - p ) * ( 2 / 3 - temp ) * 6
83+ return p
84+ }
85+
86+ const hslToRgb = (
87+ h : number ,
88+ s : number ,
89+ l : number ,
90+ ) : { r : number ; g : number ; b : number } => {
91+ if ( s === 0 ) {
92+ return { r : l , g : l , b : l }
93+ }
94+
95+ const q = l < 0.5 ? l * ( 1 + s ) : l + s - l * s
96+ const p = 2 * l - q
97+
98+ return {
99+ r : hueToRgb ( p , q , h + 1 / 3 ) ,
100+ g : hueToRgb ( p , q , h ) ,
101+ b : hueToRgb ( p , q , h - 1 / 3 ) ,
102+ }
103+ }
104+
105+ const rgbToHex = ( r : number , g : number , b : number ) : string => {
106+ const toHex = ( value : number ) =>
107+ Math . round ( clamp ( value , 0 , 1 ) * 255 )
108+ . toString ( 16 )
109+ . padStart ( 2 , '0' )
110+ return `#${ toHex ( r ) } ${ toHex ( g ) } ${ toHex ( b ) } `
111+ }
112+
113+ const generatePaletteFromPrimary = (
114+ primaryColor : string ,
115+ size : number ,
116+ ) : string [ ] => {
117+ const baseRgb = hexToRgb ( primaryColor )
118+ if ( ! baseRgb ) {
119+ return DEFAULT_SHIMMER_COLORS
120+ }
121+
122+ const { h, s, l } = rgbToHsl ( baseRgb . r , baseRgb . g , baseRgb . b )
123+ const palette : string [ ] = [ ]
124+ const paletteSize = Math . max ( 6 , Math . min ( 24 , size ) )
125+ const lightnessRange = 0.22
126+
127+ for ( let i = 0 ; i < paletteSize ; i ++ ) {
128+ const ratio = paletteSize === 1 ? 0.5 : i / ( paletteSize - 1 )
129+ const offset = ( 0.5 - ratio ) * 2 * lightnessRange
130+ const adjustedLightness = clamp ( l + offset , 0.08 , 0.92 )
131+ const saturationScale = 0.88 + 0.18 * Math . cos ( ratio * Math . PI )
132+ const adjustedSaturation = clamp ( s * saturationScale , 0.05 , 1 )
133+ const { r, g, b } = hslToRgb ( h , adjustedSaturation , adjustedLightness )
134+ palette . push ( rgbToHex ( r , g , b ) )
135+ }
136+
137+ return palette
138+ }
139+
17140export const ShimmerText = ( {
18141 text,
19- interval = 250 ,
20- colors = DEFAULT_SHIMMER_COLORS ,
142+ interval = 180 ,
143+ colors,
144+ primaryColor,
21145} : {
22146 text : string
23147 interval ?: number
24148 colors ?: string [ ]
149+ primaryColor ?: string
25150} ) => {
26151 const [ pulse , setPulse ] = useState < number > ( 0 )
27152 const chars = text . split ( '' )
@@ -36,19 +161,40 @@ export const ShimmerText = ({
36161 } , [ interval , numChars ] )
37162
38163 const generateColors = ( length : number , colorPalette : string [ ] ) : string [ ] => {
164+ if ( length === 0 ) return [ ]
165+ if ( colorPalette . length === 0 ) {
166+ return Array . from ( { length } , ( ) => '#ffffff' )
167+ }
168+ if ( colorPalette . length === 1 ) {
169+ return Array . from ( { length } , ( ) => colorPalette [ 0 ] )
170+ }
39171 const generatedColors : string [ ] = [ ]
40172 for ( let i = 0 ; i < length ; i ++ ) {
41- const ratio = i / ( length - 1 )
42- const colorIndex = Math . floor ( ratio * ( colorPalette . length - 1 ) )
173+ const ratio = length === 1 ? 0 : i / ( length - 1 )
174+ const colorIndex = Math . min (
175+ colorPalette . length - 1 ,
176+ Math . floor ( ratio * ( colorPalette . length - 1 ) ) ,
177+ )
43178 generatedColors . push ( colorPalette [ colorIndex ] )
44179 }
45180 return generatedColors
46181 }
47182
183+ const palette = useMemo ( ( ) => {
184+ if ( colors && colors . length > 0 ) {
185+ return colors
186+ }
187+ if ( primaryColor ) {
188+ const paletteSize = Math . max ( 8 , Math . min ( 20 , Math . ceil ( numChars * 1.5 ) ) )
189+ return generatePaletteFromPrimary ( primaryColor , paletteSize )
190+ }
191+ return DEFAULT_SHIMMER_COLORS
192+ } , [ colors , primaryColor , numChars ] )
193+
48194 const generateAttributes = ( length : number ) : number [ ] => {
49195 const attributes : number [ ] = [ ]
50196 for ( let i = 0 ; i < length ; i ++ ) {
51- const ratio = i / ( length - 1 )
197+ const ratio = length <= 1 ? 0 : i / ( length - 1 )
52198 if ( ratio < 0.23 ) {
53199 attributes . push ( TextAttributes . BOLD )
54200 } else if ( ratio < 0.69 ) {
@@ -61,8 +207,8 @@ export const ShimmerText = ({
61207 }
62208
63209 const generatedColors = useMemo (
64- ( ) => generateColors ( numChars , colors ) ,
65- [ numChars , colors ] ,
210+ ( ) => generateColors ( numChars , palette ) ,
211+ [ numChars , palette ] ,
66212 )
67213 const attributes = useMemo ( ( ) => generateAttributes ( numChars ) , [ numChars ] )
68214
0 commit comments