lecodes-sdk 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/global.d.ts +18 -4
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +24 -0
- package/dist/types/gl/Light.d.ts +25 -7
- package/dist/types/gl/Lightmap.d.ts +90 -60
- package/dist/types/gl/Material.d.ts +28 -20
- package/dist/types/gl/Model.d.ts +7 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +40 -1
- package/dist/types/gl/Scene.d.ts +20 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
- package/dist/types/gl/animation/IK.d.ts +86 -30
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
- package/dist/types/gl/terrain/Terrain.d.ts +4 -2
- package/dist/types/inject.d.ts +8 -2
- package/dist/types/scene/defineScene.d.ts +44 -32
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/bridges.d.ts +226 -65
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/index.ts +2 -0
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +393 -348
- package/src/gl/Light.ts +46 -16
- package/src/gl/Lightmap.ts +439 -275
- package/src/gl/Material.ts +59 -47
- package/src/gl/Model.ts +167 -156
- package/src/gl/Node.ts +39 -0
- package/src/gl/Particles.ts +61 -2
- package/src/gl/Scene.ts +34 -1
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +482 -459
- package/src/gl/animation/IK.ts +173 -152
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/physics/Ragdoll.ts +451 -272
- package/src/gl/terrain/Terrain.ts +4 -2
- package/src/inject.ts +12 -2
- package/src/scene/defineScene.ts +72 -62
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/src/animate/animate.ts +0 -238
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// `Timeline` (docs/timeline-plan.md §2.4): choreography over the keyframe core. Tracks on any
|
|
2
|
+
// number of targets (UI elements, 3D / 2D nodes, lights, cameras) at absolute positions, JS calls
|
|
3
|
+
// at times, labels, holds — one clock, one handle. Also the `animateTo` / `animateFrom` bag
|
|
4
|
+
// builder every target class delegates to (`tweenBag`).
|
|
5
|
+
|
|
6
|
+
import { TweenAnimation, type Animation } from "./Animation"
|
|
7
|
+
import { makeValueTrack, type AnimateOptions, type AnimateValue } from "./animateValue"
|
|
8
|
+
import { CLOCK_GAME, CLOCK_UI, bagProps, iterationsOf, makeTrack, type Track, type TweenMeta, type TweenSpec, type TweenTarget } from "./spec"
|
|
9
|
+
|
|
10
|
+
/** Vector-valued props: an array value is ONE keyframe unless it is an array of vectors. */
|
|
11
|
+
const VECTOR_PROPS = new Set(["position", "scale", "quaternion", "eulerAngles", "color"])
|
|
12
|
+
|
|
13
|
+
const clockOf = (clock: TweenMeta["clock"], fallback: number): number =>
|
|
14
|
+
clock === "game" ? CLOCK_GAME : clock === "ui" ? CLOCK_UI : fallback
|
|
15
|
+
|
|
16
|
+
/** @internal Build the tracks of one options bag on one target. `fromCurrent` = animateTo shape. */
|
|
17
|
+
export const bagTracks = (target: TweenTarget, bag: Record<string, unknown>, atMs: number, fromCurrent: boolean): Track[] => {
|
|
18
|
+
const meta = bag as TweenMeta
|
|
19
|
+
const tracks: Track[] = []
|
|
20
|
+
for (const [prop, value] of bagProps(bag)) {
|
|
21
|
+
const track = makeTrack(target, prop, value, meta, atMs, fromCurrent, VECTOR_PROPS.has(prop))
|
|
22
|
+
if (track) tracks.push(track)
|
|
23
|
+
}
|
|
24
|
+
return tracks
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @internal The `animateTo` / `animateFrom` implementation: one bag → one animation, played now. */
|
|
28
|
+
export const tweenBag = (target: TweenTarget, bag: Record<string, unknown>, fromCurrent: boolean): Animation => {
|
|
29
|
+
const meta = bag as TweenMeta
|
|
30
|
+
const tracks = bagTracks(target, bag, 0, fromCurrent)
|
|
31
|
+
let durationMs = 0
|
|
32
|
+
for (const t of tracks) durationMs = Math.max(durationMs, t.atMs + t.durMs)
|
|
33
|
+
const spec: TweenSpec = {
|
|
34
|
+
clock: clockOf(meta.clock, target._tweenClock),
|
|
35
|
+
durationMs, delayMs: 0,
|
|
36
|
+
iterations: iterationsOf(meta.loop),
|
|
37
|
+
pingPong: meta.loopMode !== "restart",
|
|
38
|
+
rate: 1, tracks, calls: [],
|
|
39
|
+
}
|
|
40
|
+
return new TweenAnimation(spec).play()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type TimelineOptions = {
|
|
44
|
+
/** `'ui'` (default) = wall time; `'game'` follows `Time.scale` and pauses with the game. */
|
|
45
|
+
clock?: "ui" | "game",
|
|
46
|
+
/** Repeat the whole timeline: `true` = forever, a number = cycles. */
|
|
47
|
+
loop?: boolean | number,
|
|
48
|
+
loopMode?: "restart" | "ping-pong",
|
|
49
|
+
/** Wait before the first play, ms. */
|
|
50
|
+
delay?: number,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Where a track goes: ms, a label, or `[label, offsetMs]`. Default = the current end (sequencing). */
|
|
54
|
+
export type TimelinePosition = number | string | [string, number]
|
|
55
|
+
|
|
56
|
+
export type TimelineAddOptions = {
|
|
57
|
+
at?: TimelinePosition,
|
|
58
|
+
/** With several targets: each starts this many ms after the previous one. */
|
|
59
|
+
stagger?: number,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A choreography: tracks on many targets at absolute times, JS calls, labels and holds, driven by
|
|
63
|
+
* one clock. Build it once, `play()` on every entrance — `play()` restarts from t = 0 and the pose
|
|
64
|
+
* at any time is fully defined by the tracks (the first keyframe of a track holds before it
|
|
65
|
+
* starts, the last one after it ends). */
|
|
66
|
+
export class TimelineImpl extends TweenAnimation {
|
|
67
|
+
private _labels = new Map<string, number>()
|
|
68
|
+
private _end = 0
|
|
69
|
+
|
|
70
|
+
constructor(options: TimelineOptions = {}) {
|
|
71
|
+
super({
|
|
72
|
+
clock: clockOf(options.clock, CLOCK_UI),
|
|
73
|
+
durationMs: 0,
|
|
74
|
+
delayMs: options.delay ?? 0,
|
|
75
|
+
iterations: iterationsOf(options.loop),
|
|
76
|
+
pingPong: options.loopMode !== "restart",
|
|
77
|
+
rate: 1, tracks: [], calls: [],
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private _at(at: TimelinePosition | undefined): number {
|
|
82
|
+
if (at === undefined) return this._end
|
|
83
|
+
if (typeof at === "number") return at
|
|
84
|
+
const name = Array.isArray(at) ? at[0] : at
|
|
85
|
+
const offset = Array.isArray(at) ? at[1] : 0
|
|
86
|
+
const base = this._labels.get(name)
|
|
87
|
+
if (base === undefined) throw new Error(`Timeline: unknown label "${name}"`)
|
|
88
|
+
return base + offset
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private _grow(endMs: number): void {
|
|
92
|
+
if (endMs > this._end) this._end = endMs
|
|
93
|
+
this._spec.durationMs = this._end
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Animate `props` on one target or several (staggered), like `animateTo` — arrays are keyframes,
|
|
97
|
+
* `duration` / `easing` / `times` / `commit` apply. The bag's `delay` shifts the track after `at`. */
|
|
98
|
+
add(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
|
|
99
|
+
const at = this._at(options.at)
|
|
100
|
+
const targets = Array.isArray(target) ? target : [target]
|
|
101
|
+
const stagger = options.stagger ?? 0
|
|
102
|
+
targets.forEach((t, i) => {
|
|
103
|
+
const tracks = bagTracks(t, props, at + i * stagger, true)
|
|
104
|
+
for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
|
|
105
|
+
})
|
|
106
|
+
return this
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Like `add` with the `animateFrom` shape: the target's own state is the implicit last keyframe. */
|
|
110
|
+
addFrom(target: TweenTarget | TweenTarget[], props: Record<string, unknown> & TweenMeta, options: TimelineAddOptions = {}): this {
|
|
111
|
+
const at = this._at(options.at)
|
|
112
|
+
const targets = Array.isArray(target) ? target : [target]
|
|
113
|
+
const stagger = options.stagger ?? 0
|
|
114
|
+
targets.forEach((t, i) => {
|
|
115
|
+
const tracks = bagTracks(t, props, at + i * stagger, false)
|
|
116
|
+
for (const tr of tracks) { this._spec.tracks.push(tr); this._grow(tr.atMs + tr.durMs) }
|
|
117
|
+
})
|
|
118
|
+
return this
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A free-value track (see the `animate()` global): the host evaluates it and calls `onUpdate`
|
|
122
|
+
* every frame with the value. */
|
|
123
|
+
animate<T extends AnimateValue>(options: AnimateOptions<T>, at?: TimelinePosition): this {
|
|
124
|
+
const { track, deliver, slot } = makeValueTrack(options, this._at(at))
|
|
125
|
+
this._values.set(slot, deliver)
|
|
126
|
+
this._spec.tracks.push(track)
|
|
127
|
+
this._grow(track.atMs + track.durMs)
|
|
128
|
+
return this
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Call `fn` when playback crosses `at` (forward or backward), in time order, always before
|
|
132
|
+
* finish, never from a previous run. */
|
|
133
|
+
call(at: TimelinePosition, fn: () => void): this {
|
|
134
|
+
const t = this._at(at)
|
|
135
|
+
this._spec.calls.push(t)
|
|
136
|
+
this._calls.push(fn)
|
|
137
|
+
this._grow(t)
|
|
138
|
+
return this
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Name a time — for `at`, `seek` and callers (`tl.labels.landed`). Default = the current end. */
|
|
142
|
+
label(name: string, at?: TimelinePosition): this {
|
|
143
|
+
this._labels.set(name, this._at(at))
|
|
144
|
+
return this
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Named times, ms. */
|
|
148
|
+
get labels(): Record<string, number> {
|
|
149
|
+
const o: Record<string, number> = {}
|
|
150
|
+
for (const [k, v] of this._labels) o[k] = v
|
|
151
|
+
return o
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Extend the timeline with stillness after its current end. */
|
|
155
|
+
hold(ms: number): this {
|
|
156
|
+
this._grow(this._end + Math.max(0, ms))
|
|
157
|
+
return this
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The current end, ms (tracks, calls and holds). */
|
|
161
|
+
get end(): number { return this._end }
|
|
162
|
+
|
|
163
|
+
/** `seek` also accepts a label. */
|
|
164
|
+
override seek(at: number | string): this {
|
|
165
|
+
return super.seek(typeof at === "string" ? this._at(at) : at)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The timeline handle type (see {@link TimelineImpl} for the members). */
|
|
170
|
+
export type Timeline = TimelineImpl
|
|
171
|
+
|
|
172
|
+
/** Create a timeline — `Timeline()` / `Timeline({ clock: 'game', loop: true })`. */
|
|
173
|
+
export function Timeline(options?: TimelineOptions): Timeline {
|
|
174
|
+
return new TimelineImpl(options)
|
|
175
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// `animate()` — "animate a VALUE, I apply it" (docs/timeline-plan.md): the free-value tween for
|
|
2
|
+
// anything without a native writer (a material parameter, a volume, a number the Canvas draws).
|
|
3
|
+
// Since 2026-09-14 it runs on the keyframe core like everything else: a VALUE-domain track the host
|
|
4
|
+
// evaluates natively and hands back to JS once per frame, the same easing vocabulary, keyframes,
|
|
5
|
+
// loops and clocks, and the same `Animation` handle. `Timeline.animate` puts one into a timeline.
|
|
6
|
+
|
|
7
|
+
import { TweenAnimation, type Animation } from "./Animation"
|
|
8
|
+
import { Mat4 } from "../../math/mat4"
|
|
9
|
+
import type { Vec2Like, Vec3Like } from "../../math/vec"
|
|
10
|
+
import type { QuatLike } from "../../math/quat"
|
|
11
|
+
import type { Mat4Like } from "../../math/mat4"
|
|
12
|
+
import type { ColorInput } from "../../core/color"
|
|
13
|
+
import {
|
|
14
|
+
CLOCK_GAME, CLOCK_UI, DOM_VALUE, KIND_FLOAT, KIND_MAT4, DEFAULT_DURATION_MS, easingFor, iterationsOf, lanesOf, valueValue,
|
|
15
|
+
type Keyframe, type Track, type TweenChannel, type TweenMeta, type TweenSpec, type TweenTarget,
|
|
16
|
+
} from "./spec"
|
|
17
|
+
|
|
18
|
+
/** What `animate()` tweens: a number, a color (string / packed / rgb(a) array — delivered as rgba
|
|
19
|
+
* 0..1), a 2 / 3-vector, a quaternion (slerp), or a 4x4 matrix (position / rotation / scale). */
|
|
20
|
+
export type AnimateValue = number | string | Vec2Like | Vec3Like | QuatLike | Mat4Like | ColorInput
|
|
21
|
+
|
|
22
|
+
/** `onUpdate` receives a number for a number; every other kind arrives as the SAME `Float32Array`
|
|
23
|
+
* every frame (overwritten in place — copy it if you keep it). A matrix is the 16 floats. */
|
|
24
|
+
export type AnimateOut<T> = T extends number ? number : Float32Array
|
|
25
|
+
|
|
26
|
+
export type AnimateOptions<T extends AnimateValue> = Omit<TweenMeta, "commit" | "layer"> & {
|
|
27
|
+
/** Start value (required unless `values` is given). */
|
|
28
|
+
from?: T,
|
|
29
|
+
/** End value. */
|
|
30
|
+
to?: T,
|
|
31
|
+
/** Keyframes instead of from/to (offsets via `times`). */
|
|
32
|
+
values?: T[],
|
|
33
|
+
onUpdate(value: AnimateOut<T>): void,
|
|
34
|
+
/** After the last frame of a run that reached its end (not on cancel). */
|
|
35
|
+
onComplete?(): void,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @internal Deliverers turn the host's lanes into the `onUpdate` argument without allocating. */
|
|
39
|
+
const deliverer = (kind: number, lanes: number, onUpdate: (v: any) => void): ((l: Float32Array) => void) => {
|
|
40
|
+
if (kind === KIND_FLOAT) return (l) => onUpdate(l[0])
|
|
41
|
+
if (kind === KIND_MAT4) {
|
|
42
|
+
const out = new Float32Array(16)
|
|
43
|
+
return (l) => {
|
|
44
|
+
const m = Mat4.compose([l[0]!, l[1]!, l[2]!], [l[3]!, l[4]!, l[5]!, l[6]!], [l[7]!, l[8]!, l[9]!]).m
|
|
45
|
+
for (let i = 0; i < 16; i++) out[i] = m[i]!
|
|
46
|
+
onUpdate(out)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const out = new Float32Array(lanes)
|
|
50
|
+
return (l) => { for (let i = 0; i < lanes; i++) out[i] = l[i]!; onUpdate(out) }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** VALUE slots are unique per world: the host addresses a value track by (animation, slot), and the
|
|
54
|
+
* evaluator must never see two animations naming the same slot (a channel is owned by the newest
|
|
55
|
+
* animation that names it — values have no shared state to own). */
|
|
56
|
+
let nextSlot = 0
|
|
57
|
+
|
|
58
|
+
/** @internal Build a VALUE track from animate options. Returns the track, its deliverer and its slot. */
|
|
59
|
+
export const makeValueTrack = (o: AnimateOptions<any>, atMs: number): { track: Track, deliver: (l: Float32Array) => void, slot: number } => {
|
|
60
|
+
const slot = ++nextSlot
|
|
61
|
+
const raw: unknown[] = o.values ?? (o.from !== undefined && o.to !== undefined ? [o.from, o.to] : [])
|
|
62
|
+
if (raw.length < 1) throw new Error("animate: give `from` + `to`, or `values`")
|
|
63
|
+
const norm = raw.map((v) => valueValue(v))
|
|
64
|
+
if (norm.some((n) => n === null)) throw new Error("animate: a value is not a number, color, vector, quaternion or matrix")
|
|
65
|
+
const kind = norm[0]!.kind
|
|
66
|
+
if (norm.some((n) => n!.kind !== kind)) throw new Error("animate: every value must be the same kind")
|
|
67
|
+
const lanes = lanesOf(kind)
|
|
68
|
+
const easing = easingFor(o.easing, "value")
|
|
69
|
+
const keys: Keyframe[] = norm.map((n, i) => ({
|
|
70
|
+
t: o.times && o.times.length === norm.length ? o.times[i]! : norm.length === 1 ? 1 : i / (norm.length - 1),
|
|
71
|
+
easing,
|
|
72
|
+
value: { lanes: (n as { lanes: number[] }).lanes },
|
|
73
|
+
raw: raw[i],
|
|
74
|
+
}))
|
|
75
|
+
const channel: TweenChannel = { domain: DOM_VALUE, id: () => slot, value: valueValue }
|
|
76
|
+
const target: TweenTarget = { _tweenClock: CLOCK_UI, _tweenChannel: () => channel }
|
|
77
|
+
const track: Track = {
|
|
78
|
+
target, prop: "value", channel, kind, lanes,
|
|
79
|
+
atMs: atMs + (o.delay ?? 0), durMs: Math.max(0, o.duration ?? DEFAULT_DURATION_MS),
|
|
80
|
+
commit: false, keys,
|
|
81
|
+
}
|
|
82
|
+
return { track, deliver: deliverer(kind, lanes, o.onUpdate), slot }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Tween a free value and apply it yourself in `onUpdate` — the primitive for anything without a
|
|
86
|
+
* native channel (material params, volumes, numbers a Canvas draws). Same easing / keyframes /
|
|
87
|
+
* `loop` / `clock` as `animateTo`; returns the {@link Animation} handle. Default clock: `'ui'`. */
|
|
88
|
+
export const animate = <T extends AnimateValue>(options: AnimateOptions<T>): Animation => {
|
|
89
|
+
const { track, deliver, slot } = makeValueTrack(options, 0)
|
|
90
|
+
const spec: TweenSpec = {
|
|
91
|
+
clock: options.clock === "game" ? CLOCK_GAME : CLOCK_UI,
|
|
92
|
+
durationMs: track.atMs + track.durMs, delayMs: 0,
|
|
93
|
+
iterations: iterationsOf(options.loop), pingPong: options.loopMode !== "restart",
|
|
94
|
+
rate: 1, tracks: [track], calls: [],
|
|
95
|
+
}
|
|
96
|
+
const a = new TweenAnimation(spec)
|
|
97
|
+
a._values.set(slot, deliver)
|
|
98
|
+
if (options.onComplete) a.onFinish((done) => { if (done) options.onComplete!() })
|
|
99
|
+
return a.play()
|
|
100
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Easing vocabulary of the keyframe animation core (docs/timeline-plan.md §2.1). Every form the
|
|
2
|
+
// options bag accepts is normalized here into an `EasingSpec` the native evaluator (creator-tween)
|
|
3
|
+
// understands: linear, a NAMED curve (evaluated natively by id — the formulas below are the twin,
|
|
4
|
+
// used for the old-host path and tests), a cubic bezier, `steps(n)`, or a sampled TABLE for an
|
|
5
|
+
// arbitrary `Easing` function (65 points, linear between them — the only way a JS function can
|
|
6
|
+
// cross the bridge).
|
|
7
|
+
|
|
8
|
+
import type { Easing } from "../easings"
|
|
9
|
+
|
|
10
|
+
export const EASE_LINEAR = 0
|
|
11
|
+
export const EASE_NAMED = 1
|
|
12
|
+
export const EASE_BEZIER = 2
|
|
13
|
+
export const EASE_STEPS = 3
|
|
14
|
+
export const EASE_TABLE = 4
|
|
15
|
+
|
|
16
|
+
/** Samples per table (t = i / 64). Interpolation error for the smooth built-ins is < 1e-3. */
|
|
17
|
+
export const EASE_TABLE_SIZE = 65
|
|
18
|
+
|
|
19
|
+
export type EasingSpec = {
|
|
20
|
+
kind: number,
|
|
21
|
+
/** kind-specific parameters: NAMED [id], BEZIER [x1, y1, x2, y2], STEPS [n, jumpAtStart], TABLE [tableIndex] */
|
|
22
|
+
p: [number, number, number, number],
|
|
23
|
+
/** TABLE only — the samples, appended to the blob's table section by the builder. */
|
|
24
|
+
table?: Float32Array,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A curve in the options bag: a name, `"cubic-bezier(x1,y1,x2,y2)"`, `"steps(n)"`, a bezier tuple,
|
|
28
|
+
* or any `Easing` function. */
|
|
29
|
+
export type EasingInput = string | readonly [number, number, number, number] | Easing
|
|
30
|
+
|
|
31
|
+
// ---- named curves (ids are the native contract — append, never renumber) ------------------------
|
|
32
|
+
|
|
33
|
+
const c1 = 1.70158
|
|
34
|
+
const c2 = c1 * 1.525
|
|
35
|
+
const c3 = c1 + 1
|
|
36
|
+
const c4 = (2 * Math.PI) / 3
|
|
37
|
+
const c5 = (2 * Math.PI) / 4.5
|
|
38
|
+
|
|
39
|
+
const outBounce = (x: number): number => {
|
|
40
|
+
const n1 = 7.5625, d1 = 2.75
|
|
41
|
+
if (x < 1 / d1) return n1 * x * x
|
|
42
|
+
if (x < 2 / d1) return n1 * (x -= 1.5 / d1) * x + 0.75
|
|
43
|
+
if (x < 2.5 / d1) return n1 * (x -= 2.25 / d1) * x + 0.9375
|
|
44
|
+
return n1 * (x -= 2.625 / d1) * x + 0.984375
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** id → (name, formula). The formulas are the standard easings.net set; creator-tween's
|
|
48
|
+
* `ctweenEaseNamed` implements the same list by id. */
|
|
49
|
+
export const NAMED_EASINGS: ReadonlyArray<readonly [string, Easing]> = [
|
|
50
|
+
["linear", (x) => x], // 0
|
|
51
|
+
["inSine", (x) => 1 - Math.cos((x * Math.PI) / 2)], // 1
|
|
52
|
+
["outSine", (x) => Math.sin((x * Math.PI) / 2)], // 2
|
|
53
|
+
["inOutSine", (x) => -(Math.cos(Math.PI * x) - 1) / 2], // 3
|
|
54
|
+
["inQuad", (x) => x * x], // 4
|
|
55
|
+
["outQuad", (x) => 1 - (1 - x) * (1 - x)], // 5
|
|
56
|
+
["inOutQuad", (x) => (x < 0.5 ? 2 * x * x : 1 - (-2 * x + 2) ** 2 / 2)], // 6
|
|
57
|
+
["inCubic", (x) => x * x * x], // 7
|
|
58
|
+
["outCubic", (x) => 1 - (1 - x) ** 3], // 8
|
|
59
|
+
["inOutCubic", (x) => (x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2)], // 9
|
|
60
|
+
["inQuart", (x) => x ** 4], // 10
|
|
61
|
+
["outQuart", (x) => 1 - (1 - x) ** 4], // 11
|
|
62
|
+
["inOutQuart", (x) => (x < 0.5 ? 8 * x ** 4 : 1 - (-2 * x + 2) ** 4 / 2)], // 12
|
|
63
|
+
["inExpo", (x) => (x === 0 ? 0 : 2 ** (10 * x - 10))], // 13
|
|
64
|
+
["outExpo", (x) => (x === 1 ? 1 : 1 - 2 ** (-10 * x))], // 14
|
|
65
|
+
["inOutExpo", (x) => (x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? 2 ** (20 * x - 10) / 2 : (2 - 2 ** (-20 * x + 10)) / 2)], // 15
|
|
66
|
+
["inCirc", (x) => 1 - Math.sqrt(1 - x * x)], // 16
|
|
67
|
+
["outCirc", (x) => Math.sqrt(1 - (x - 1) ** 2)], // 17
|
|
68
|
+
["inOutCirc", (x) => (x < 0.5 ? (1 - Math.sqrt(1 - (2 * x) ** 2)) / 2 : (Math.sqrt(1 - (-2 * x + 2) ** 2) + 1) / 2)], // 18
|
|
69
|
+
["inBack", (x) => c3 * x * x * x - c1 * x * x], // 19
|
|
70
|
+
["outBack", (x) => 1 + c3 * (x - 1) ** 3 + c1 * (x - 1) ** 2], // 20
|
|
71
|
+
["inOutBack", (x) => (x < 0.5 ? ((2 * x) ** 2 * ((c2 + 1) * 2 * x - c2)) / 2 : ((2 * x - 2) ** 2 * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2)], // 21
|
|
72
|
+
["inElastic", (x) => (x === 0 ? 0 : x === 1 ? 1 : -(2 ** (10 * x - 10)) * Math.sin((x * 10 - 10.75) * c4))], // 22
|
|
73
|
+
["outElastic", (x) => (x === 0 ? 0 : x === 1 ? 1 : 2 ** (-10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1)], // 23
|
|
74
|
+
["inOutElastic", (x) => (x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? -(2 ** (20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 : (2 ** (-20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1)], // 24
|
|
75
|
+
["inBounce", (x) => 1 - outBounce(1 - x)], // 25
|
|
76
|
+
["outBounce", outBounce], // 26
|
|
77
|
+
["inOutBounce", (x) => (x < 0.5 ? (1 - outBounce(1 - 2 * x)) / 2 : (1 + outBounce(2 * x - 1)) / 2)], // 27
|
|
78
|
+
["smoothstep", (x) => x * x * (3 - 2 * x)], // 28 — the $class / onPressed default
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
/** Short spellings → canonical names. `in`/`out`/`inOut` are the quadratics, matching the SDK's
|
|
82
|
+
* `easeIn`/`easeOut`/`easeInOut` functions. */
|
|
83
|
+
const ALIASES: Record<string, string> = {
|
|
84
|
+
in: "inQuad", out: "outQuad", inOut: "inOutQuad",
|
|
85
|
+
easeIn: "inQuad", easeOut: "outQuad", easeInOut: "inOutQuad",
|
|
86
|
+
"ease-in": "inQuad", "ease-out": "outQuad", "ease-in-out": "inOutQuad",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const namedId = (name: string): number => {
|
|
90
|
+
const canon = ALIASES[name] ?? name
|
|
91
|
+
for (let i = 0; i < NAMED_EASINGS.length; i++) if (NAMED_EASINGS[i]![0] === canon) return i
|
|
92
|
+
return -1
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The CSS `ease` curve — what a bare `"ease"` means. */
|
|
96
|
+
const CSS_EASE: [number, number, number, number] = [0.25, 0.1, 0.25, 1]
|
|
97
|
+
|
|
98
|
+
const LINEAR: EasingSpec = { kind: EASE_LINEAR, p: [0, 0, 0, 0] }
|
|
99
|
+
|
|
100
|
+
/** Sample an `Easing` function into a table. Endpoints are forced exact so a curve that is
|
|
101
|
+
* numerically 0.9999 at 1 still lands on the keyframe. */
|
|
102
|
+
export const sampleEasing = (fn: Easing): Float32Array => {
|
|
103
|
+
const t = new Float32Array(EASE_TABLE_SIZE)
|
|
104
|
+
for (let i = 0; i < EASE_TABLE_SIZE; i++) t[i] = fn(i / (EASE_TABLE_SIZE - 1))
|
|
105
|
+
t[0] = 0
|
|
106
|
+
t[EASE_TABLE_SIZE - 1] = 1
|
|
107
|
+
return t
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Normalize one easing input. Unknown strings fall back to linear with a console warning (a typo
|
|
111
|
+
* should be visible, not a silent snap). */
|
|
112
|
+
export const parseEasing = (input: EasingInput | undefined): EasingSpec => {
|
|
113
|
+
if (input === undefined) return LINEAR
|
|
114
|
+
if (typeof input === "function") return { kind: EASE_TABLE, p: [0, 0, 0, 0], table: sampleEasing(input) }
|
|
115
|
+
if (Array.isArray(input)) {
|
|
116
|
+
const [x1, y1, x2, y2] = input as readonly number[]
|
|
117
|
+
return { kind: EASE_BEZIER, p: [x1!, y1!, x2!, y2!] }
|
|
118
|
+
}
|
|
119
|
+
const s = (input as string).trim()
|
|
120
|
+
if (s === "linear") return LINEAR
|
|
121
|
+
if (s === "ease") return { kind: EASE_BEZIER, p: CSS_EASE }
|
|
122
|
+
if (s === "steps") return { kind: EASE_STEPS, p: [1, 0, 0, 0] }
|
|
123
|
+
let m = /^steps\(\s*(\d+)\s*(?:,\s*(start|end|jump-start|jump-end)\s*)?\)$/.exec(s)
|
|
124
|
+
if (m) return { kind: EASE_STEPS, p: [Math.max(1, parseInt(m[1]!, 10)), m[2] === "start" || m[2] === "jump-start" ? 1 : 0, 0, 0] }
|
|
125
|
+
m = /^cubic-bezier\(\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*\)$/.exec(s)
|
|
126
|
+
if (m) return { kind: EASE_BEZIER, p: [parseFloat(m[1]!), parseFloat(m[2]!), parseFloat(m[3]!), parseFloat(m[4]!)] }
|
|
127
|
+
const id = namedId(s)
|
|
128
|
+
if (id === 0) return LINEAR
|
|
129
|
+
if (id > 0) return { kind: EASE_NAMED, p: [id, 0, 0, 0] }
|
|
130
|
+
console.warn(`[tween] unknown easing "${s}" — using linear`)
|
|
131
|
+
return LINEAR
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---- evaluation twin (old-host path + tests) ---------------------------------------------------
|
|
135
|
+
|
|
136
|
+
const bezierY = (x: number, x1: number, y1: number, x2: number, y2: number): number => {
|
|
137
|
+
// Newton on the x polynomial, then y at that t — the same method creator-tween uses.
|
|
138
|
+
const ax = 1 - 3 * x2 + 3 * x1, bx = 3 * x2 - 6 * x1, cx = 3 * x1
|
|
139
|
+
const ay = 1 - 3 * y2 + 3 * y1, by = 3 * y2 - 6 * y1, cy = 3 * y1
|
|
140
|
+
let t = x
|
|
141
|
+
for (let i = 0; i < 8; i++) {
|
|
142
|
+
const xt = ((ax * t + bx) * t + cx) * t - x
|
|
143
|
+
const dx = (3 * ax * t + 2 * bx) * t + cx
|
|
144
|
+
if (Math.abs(xt) < 1e-6) break
|
|
145
|
+
if (Math.abs(dx) < 1e-6) break
|
|
146
|
+
t -= xt / dx
|
|
147
|
+
}
|
|
148
|
+
t = Math.min(1, Math.max(0, t))
|
|
149
|
+
return ((ay * t + by) * t + cy) * t
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Evaluate an `EasingSpec` at `u` ∈ [0, 1] — the JS twin of `ctweenEase`. */
|
|
153
|
+
export const evalEasing = (e: EasingSpec, u: number): number => {
|
|
154
|
+
if (u <= 0) return e.kind === EASE_STEPS && e.p[1] === 1 ? 1 / e.p[0] : 0
|
|
155
|
+
if (u >= 1) return 1
|
|
156
|
+
switch (e.kind) {
|
|
157
|
+
case EASE_NAMED: return NAMED_EASINGS[e.p[0]]
|
|
158
|
+
case EASE_BEZIER: return bezierY(u, e.p[0], e.p[1], e.p[2], e.p[3])
|
|
159
|
+
case EASE_STEPS: {
|
|
160
|
+
const n = e.p[0]
|
|
161
|
+
return e.p[1] === 1 ? Math.min(1, Math.ceil(u * n) / n) : Math.floor(u * n) / n
|
|
162
|
+
}
|
|
163
|
+
case EASE_TABLE: {
|
|
164
|
+
const t = e.table!
|
|
165
|
+
const f = u * (EASE_TABLE_SIZE - 1)
|
|
166
|
+
const i = Math.floor(f)
|
|
167
|
+
const a = t[i]!, b = t[Math.min(i + 1, EASE_TABLE_SIZE - 1)]!
|
|
168
|
+
return a + (b - a) * (f - i)
|
|
169
|
+
}
|
|
170
|
+
default: return u
|
|
171
|
+
}
|
|
172
|
+
}
|