Skip to content

Commit 908c142

Browse files
committed
add RGBA useAnimation support
1 parent 5c255c6 commit 908c142

4 files changed

Lines changed: 71 additions & 6 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "frame-script",
33
"private": true,
4-
"version": "0.0.3",
4+
"version": "0.0.6",
55
"type": "module",
66
"scripts": {
77
"dev:vite": "vite --config vite.studio.config.ts",

project/project.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ export const PROJECT_SETTINGS: ProjectSettings = {
1616

1717
const HelloScene = () => {
1818
const progress = useVariable(0)
19+
const color = useVariable("#FFFFFF")
1920

2021
useAnimation(async (context) => {
21-
await context.move(progress).to(1, seconds(3), BEZIER_SMOOTH)
22+
await context.parallel([
23+
context.move(progress).to(1, seconds(3), BEZIER_SMOOTH),
24+
context.move(color).to("#75a9bd", seconds(3), BEZIER_SMOOTH),
25+
])
2226
await context.sleep(seconds(1))
2327
await context.move(progress).to(0, seconds(3), BEZIER_SMOOTH)
2428
}, [])
@@ -30,6 +34,8 @@ const HelloScene = () => {
3034
fontUrl="assets/NotoSerifCJKJP-Medium.ttf"
3135
strokeWidth={2}
3236
progress={progress}
37+
strokeColor={color.use()}
38+
fillColor={color.use()}
3339
/>
3440
</FillFrame>
3541
)

src/lib/animation.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export type Vec2 = { x: number; y: number }
2727
* ```
2828
*/
2929
export 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

4447
type Segment<T> = {
4548
start: number
@@ -171,8 +174,47 @@ const installAnimationApi = () => {
171174
const toFrames = (value: number) => Math.max(0, Math.round(value))
172175
const 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-9a-fA-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+
174213
const 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

197253
const 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 {
286344
export function useVariable(initial: number): Variable<number>
287345
export function useVariable(initial: Vec2): Variable<Vec2>
288346
export function useVariable(initial: Vec3): Variable<Vec3>
347+
export function useVariable(initial: ColorHex): Variable<ColorHex>
289348
export function useVariable<T extends VariableType>(initial: T): Variable<T> {
290349
const stateRef = useRef<VariableStateBase | null>(null)
291350
if (!stateRef.current) {

0 commit comments

Comments
 (0)