This commit is contained in:
th committed 2026-04-19 17:53:36 +02:00
commit c6863f2bfc
34 files changed
+6909

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
import type { AnimatableProps, Easing, Keyframe, Layer } from '@/types/scene'
const easings: Record<Easing, (t: number) => number> = {
linear: (t) => t,
easeIn: (t) => t * t,
easeOut: (t) => 1 - (1 - t) ** 2,
easeInOut: (t) => (t < 0.5 ? 2 * t * t : 1 - ((-2 * t + 2) ** 2) / 2),
}
function merge(base: AnimatableProps, partial: Partial<AnimatableProps>): AnimatableProps {
return { ...base, ...partial }
}
function lerp(a: number, b: number, t: number) {
return a + (b - a) * t
}
function parseHex(hex: string): [number, number, number] | null {
const h = hex.trim().replace('#', '')
const full = h.length === 3 ? h.split('').map((c) => c + c).join('') : h
if (full.length < 6) return null
const n = parseInt(full.slice(0, 6), 16)
if (Number.isNaN(n)) return null
return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
}
function formatHex(r: number, g: number, b: number): string {
const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v)))
const h = (v: number) => clamp(v).toString(16).padStart(2, '0')
return `#${h(r)}${h(g)}${h(b)}`
}
export function lerpColor(a: string, b: string, t: number): string {
const pa = parseHex(a)
const pb = parseHex(b)
if (!pa || !pb) return t < 0.5 ? a : b
return formatHex(lerp(pa[0], pb[0], t), lerp(pa[1], pb[1], t), lerp(pa[2], pb[2], t))
}
function lerpProps(a: AnimatableProps, b: AnimatableProps, t: number): AnimatableProps {
const out: AnimatableProps = {
x: lerp(a.x, b.x, t),
y: lerp(a.y, b.y, t),
scaleX: lerp(a.scaleX, b.scaleX, t),
scaleY: lerp(a.scaleY, b.scaleY, t),
rotation: lerp(a.rotation, b.rotation, t),
opacity: lerp(a.opacity, b.opacity, t),
}
if (a.fill && b.fill) out.fill = lerpColor(a.fill, b.fill, t)
else if (b.fill) out.fill = b.fill
else if (a.fill) out.fill = a.fill
return out
}
export function sortedKeyframes(layer: Layer): Keyframe[] {
return [...layer.keyframes].sort((a, b) => a.time - b.time)
}
export function sampleLayer(layer: Layer, time: number): AnimatableProps {
const kfs = sortedKeyframes(layer)
if (!kfs.length) return { ...layer.base }
if (time <= kfs[0].time) return merge(layer.base, kfs[0].props)
const last = kfs[kfs.length - 1]
if (time >= last.time) return merge(layer.base, last.props)
for (let i = 0; i < kfs.length - 1; i++) {
const a = kfs[i]
const b = kfs[i + 1]
if (time >= a.time && time <= b.time) {
const raw = (time - a.time) / (b.time - a.time)
const t = easings[b.easing](raw)
return lerpProps(merge(layer.base, a.props), merge(layer.base, b.props), t)
}
}
return { ...layer.base }
}