@@ -27,6 +27,9 @@ export type Vec2 = { x: number; y: number }
2727 * ```
2828 */
2929export type Vec3 = { x : number ; y : number ; z : number }
30+
31+
32+ export type ColorHex = `#${string } `
3033/**
3134 * Supported variable value types for animation.
3235 *
@@ -37,9 +40,9 @@ export type Vec3 = { x: number; y: number; z: number }
3740 * const value: VariableType = { x: 0, y: 0 }
3841 * ```
3942 */
40- export type VariableType = number | Vec2 | Vec3
43+ export type VariableType = number | Vec2 | Vec3 | ColorHex
4144
42- type VariableKind = "number" | "vec2" | "vec3"
45+ type VariableKind = "number" | "vec2" | "vec3" | "color"
4346
4447type Segment < T > = {
4548 start : number
@@ -171,8 +174,47 @@ const installAnimationApi = () => {
171174const toFrames = ( value : number ) => Math . max ( 0 , Math . round ( value ) )
172175const isDev = typeof import . meta !== "undefined" && Boolean ( ( import . meta as any ) . env ?. DEV )
173176
177+ type ParsedColor = { r : number ; g : number ; b : number ; a : number ; hasAlpha : boolean }
178+
179+ const clampChannel = ( value : number ) => Math . min ( 255 , Math . max ( 0 , value ) )
180+
181+ const expandShortHex = ( hex : string ) => hex . split ( "" ) . map ( ( ch ) => ch + ch ) . join ( "" )
182+
183+ const parseColorHex = ( value : string ) : ParsedColor | null => {
184+ if ( ! value . startsWith ( "#" ) ) return null
185+ let raw = value . slice ( 1 )
186+ let hasAlpha = false
187+
188+ if ( raw . length === 3 || raw . length === 4 ) {
189+ hasAlpha = raw . length === 4
190+ raw = expandShortHex ( raw )
191+ }
192+
193+ if ( raw . length === 8 ) {
194+ hasAlpha = true
195+ } else if ( raw . length !== 6 ) {
196+ return null
197+ }
198+
199+ if ( ! / ^ [ 0 - 9 a - f A - F ] + $ / . test ( raw ) ) return null
200+
201+ const r = Number . parseInt ( raw . slice ( 0 , 2 ) , 16 )
202+ const g = Number . parseInt ( raw . slice ( 2 , 4 ) , 16 )
203+ const b = Number . parseInt ( raw . slice ( 4 , 6 ) , 16 )
204+ const a = hasAlpha ? Number . parseInt ( raw . slice ( 6 , 8 ) , 16 ) : 255
205+ return { r, g, b, a, hasAlpha }
206+ }
207+
208+ const formatColorHex = ( r : number , g : number , b : number , a : number | null ) => {
209+ const toHex = ( value : number ) => clampChannel ( value ) . toString ( 16 ) . padStart ( 2 , "0" ) . toUpperCase ( )
210+ return `#${ toHex ( r ) } ${ toHex ( g ) } ${ toHex ( b ) } ${ a == null ? "" : toHex ( a ) } `
211+ }
212+
174213const getKind = ( value : unknown ) : VariableKind | null => {
175214 if ( typeof value === "number" ) return "number"
215+ if ( typeof value === "string" ) {
216+ if ( parseColorHex ( value ) ) return "color"
217+ }
176218 if ( value && typeof value === "object" ) {
177219 const obj = value as Partial < Vec3 >
178220 if ( typeof obj . x === "number" && typeof obj . y === "number" ) {
@@ -193,6 +235,20 @@ const lerpVec3 = (from: Vec3, to: Vec3, t: number) => ({
193235 y : from . y + ( to . y - from . y ) * t ,
194236 z : from . z + ( to . z - from . z ) * t ,
195237} )
238+ const lerpColor = ( from : ColorHex , to : ColorHex , t : number ) => {
239+ const fromColor = parseColorHex ( from )
240+ const toColor = parseColorHex ( to )
241+ if ( ! fromColor || ! toColor ) {
242+ return to
243+ }
244+ const mix = ( a : number , b : number ) => a + ( b - a ) * t
245+ const r = clampChannel ( Math . round ( mix ( fromColor . r , toColor . r ) ) )
246+ const g = clampChannel ( Math . round ( mix ( fromColor . g , toColor . g ) ) )
247+ const b = clampChannel ( Math . round ( mix ( fromColor . b , toColor . b ) ) )
248+ const a = clampChannel ( Math . round ( mix ( fromColor . a , toColor . a ) ) )
249+ const useAlpha = fromColor . hasAlpha || toColor . hasAlpha
250+ return formatColorHex ( r , g , b , useAlpha ? a : null )
251+ }
196252
197253const lerpForKind = ( kind : VariableKind ) : Lerp < VariableType > => {
198254 switch ( kind ) {
@@ -202,6 +258,8 @@ const lerpForKind = (kind: VariableKind): Lerp<VariableType> => {
202258 return lerpVec2 as Lerp < VariableType >
203259 case "vec3" :
204260 return lerpVec3 as Lerp < VariableType >
261+ case "color" :
262+ return lerpColor as Lerp < VariableType >
205263 }
206264}
207265
@@ -286,6 +344,7 @@ export class AnimationHandle {
286344export function useVariable ( initial : number ) : Variable < number >
287345export function useVariable ( initial : Vec2 ) : Variable < Vec2 >
288346export function useVariable ( initial : Vec3 ) : Variable < Vec3 >
347+ export function useVariable ( initial : ColorHex ) : Variable < ColorHex >
289348export function useVariable < T extends VariableType > ( initial : T ) : Variable < T > {
290349 const stateRef = useRef < VariableStateBase | null > ( null )
291350 if ( ! stateRef . current ) {
0 commit comments