@px-petals/roku.motion-web 0.0.0-stage → 1.0.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/LICENSE +201 -0
- package/audio/audio.cues.js +27 -0
- package/audio/audio.js +259 -0
- package/bands/bands.js +390 -0
- package/bundle/bundle.fonts.js +55 -0
- package/bundle/bundle.js +75 -0
- package/focus/focus.direction.js +46 -0
- package/focus/focus.dom.js +158 -0
- package/focus/focus.js +277 -0
- package/focus/focus.tree.js +146 -0
- package/hit/hit.geometry.js +113 -0
- package/hit/hit.js +129 -0
- package/list/list.flex.js +181 -0
- package/list/list.items.js +80 -0
- package/list/list.js +507 -0
- package/lottie/lottie.js +124 -0
- package/lottie/lottie.layers.js +363 -0
- package/lottie/lottie.paint.js +208 -0
- package/lottie/lottie.paths.js +459 -0
- package/lottie/lottie.props.js +255 -0
- package/lottie/lottie.shapes.js +258 -0
- package/mixer/mixer.js +284 -0
- package/mixer/mixer.mix.js +90 -0
- package/mixer/mixer.stage.js +431 -0
- package/motion/motion.convert.js +373 -0
- package/motion/motion.flow.js +403 -0
- package/motion/motion.keys.js +150 -0
- package/motion/motion.machine.js +367 -0
- package/motion/motion.nested.js +345 -0
- package/motion/motion.values.js +364 -0
- package/motion.js +979 -0
- package/package.json +53 -4
- package/player/overlay/overlay.js +325 -0
- package/player/overlay/overlay.layout.js +208 -0
- package/player/player.fit.js +101 -0
- package/player/player.js +401 -0
- package/player/player.raster.js +127 -0
- package/pointer/pointer.js +292 -0
- package/readme.md +270 -0
- package/README.md +0 -3
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stage: what is playing, per layer, as poses for the mixer. It knows no
|
|
3
|
+
* state machine, only what the machine tells it: a layer enters a state,
|
|
4
|
+
* with a cross-fade or without. It keeps each state's clock and says, for a
|
|
5
|
+
* set of input values, which animations show at what time and weight.
|
|
6
|
+
*
|
|
7
|
+
* - A layer holds a list of runs, oldest first. A run is a state being
|
|
8
|
+
* played. Entering a state with a cross-fade adds a run whose weight rises
|
|
9
|
+
* from 0 to 1 over the fade, while the outgoing run plays on; entering one
|
|
10
|
+
* without replaces them. A run that reaches full weight drops the runs
|
|
11
|
+
* before it, and a run entered during a fade stacks on the last, so a
|
|
12
|
+
* cross-fade interrupted by another continues from where it was.
|
|
13
|
+
* - A blend state is a run with several animations: a 1D blend weights the
|
|
14
|
+
* two animations either side of its input's value, a direct blend weights
|
|
15
|
+
* each by its own input (0 to 100).
|
|
16
|
+
* - A state's `speed` scales its clock, and its `mode` plays its timelines
|
|
17
|
+
* backwards (`Reverse`, `ReverseBounce`). A ping-pong timeline is already
|
|
18
|
+
* written out there and back, so `Bounce` plays it forward.
|
|
19
|
+
* - Layers apply in order, so a later layer overrides what it keys.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { convertValue } from '../motion/motion.convert.js'
|
|
23
|
+
|
|
24
|
+
/** @import { Pose } from './mixer.js' */
|
|
25
|
+
|
|
26
|
+
/** Runs one layer keeps: an endless chain of interrupted fades is cut here */
|
|
27
|
+
const MAX_RUNS = 4
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One state being played.
|
|
31
|
+
* @typedef {object} Run
|
|
32
|
+
* @property {any} state
|
|
33
|
+
* @property {number} time seconds since it was entered, unwrapped
|
|
34
|
+
* @property {number} ramp its weight, 0 to 1
|
|
35
|
+
* @property {number[] | null} ease the curve the ramp follows from `fade`'s share of `fadeFor`: a cubic's `[x1, y1, x2, y2]`; null for linear
|
|
36
|
+
* @property {number} fade seconds the ramp has taken
|
|
37
|
+
* @property {number} fadeFor seconds the ramp takes
|
|
38
|
+
* @property {boolean} finished a state that does not loop has reached its end
|
|
39
|
+
* @property {number} from where the last step began, in seconds of the state's clock; -1 before the first, so frame 0 sounds
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {Record<string, { frames: number, fps: number }>} animations a bundle's animations
|
|
44
|
+
*/
|
|
45
|
+
export function createStage(animations) {
|
|
46
|
+
/** @type {{ runs: Run[] }[]} */
|
|
47
|
+
let layers = []
|
|
48
|
+
let loopsAll = false
|
|
49
|
+
let frames = 0
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* An animation's length in seconds.
|
|
53
|
+
* @param {string} id
|
|
54
|
+
*/
|
|
55
|
+
function lengthOf(id) {
|
|
56
|
+
const entry = animations[id]
|
|
57
|
+
return entry && entry.fps > 0 ? entry.frames / entry.fps : 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A state's timeline: its animation's, or a blend's longest.
|
|
62
|
+
* @param {any} state
|
|
63
|
+
*/
|
|
64
|
+
function stateLength(state) {
|
|
65
|
+
if (!state.blend) return lengthOf(state.animation)
|
|
66
|
+
let longest = 0
|
|
67
|
+
for (let n = 0; n < state.blend.animations.length; n++) longest = Math.max(longest, lengthOf(state.blend.animations[n].animation))
|
|
68
|
+
return longest
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A state loops when it says so, or the host asked for every state to.
|
|
73
|
+
* @param {any} state
|
|
74
|
+
*/
|
|
75
|
+
function loops(state) {
|
|
76
|
+
return loopsAll || state.loop === true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {number} layer
|
|
81
|
+
* @param {any} state a machine state: an animation, or a blend of several
|
|
82
|
+
* @param {number} fadeMs how long the layer's current state fades into it
|
|
83
|
+
* @param {number[]} [ease] the fade's cubic curve; linear without
|
|
84
|
+
*/
|
|
85
|
+
function enter(layer, state, fadeMs, ease) {
|
|
86
|
+
if (!layers[layer]) layers[layer] = { runs: [] }
|
|
87
|
+
const target = layers[layer]
|
|
88
|
+
|
|
89
|
+
/** @type {Run} */
|
|
90
|
+
const run = { state, time: 0, ramp: 1, ease: ease && ease.length === 4 ? ease : null, fade: 0, fadeFor: 0, finished: false, from: -1 }
|
|
91
|
+
if (fadeMs > 0 && target.runs.length) {
|
|
92
|
+
run.ramp = 0
|
|
93
|
+
run.fadeFor = fadeMs / 1000
|
|
94
|
+
target.runs.push(run)
|
|
95
|
+
if (target.runs.length > MAX_RUNS) target.runs.shift()
|
|
96
|
+
} else {
|
|
97
|
+
target.runs = [ run ]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Plays time forward.
|
|
103
|
+
*
|
|
104
|
+
* @param {number} seconds
|
|
105
|
+
* @returns {number[]} the layers whose state, which does not loop, reached its end
|
|
106
|
+
*/
|
|
107
|
+
function advance(seconds) {
|
|
108
|
+
frames += 1
|
|
109
|
+
/** @type {number[]} */
|
|
110
|
+
const done = []
|
|
111
|
+
for (let n = 0; n < layers.length; n++) {
|
|
112
|
+
const layer = layers[n]
|
|
113
|
+
if (!layer) continue
|
|
114
|
+
|
|
115
|
+
for (let r = 0; r < layer.runs.length; r++) {
|
|
116
|
+
const run = layer.runs[r]
|
|
117
|
+
run.from = run.time === 0 && run.from === -1 ? -1 : run.time
|
|
118
|
+
run.time += seconds * speedOf(run.state)
|
|
119
|
+
if (run.ramp < 1) {
|
|
120
|
+
run.fade += seconds
|
|
121
|
+
run.ramp = run.fadeFor > 0 ? eased(Math.min(1, run.fade / run.fadeFor), run.ease) : 1
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// the newest run at full weight is all that shows: the ones before it go
|
|
126
|
+
for (let r = layer.runs.length - 1; r > 0; r--) {
|
|
127
|
+
if (layer.runs[r].ramp >= 1) {
|
|
128
|
+
layer.runs = layer.runs.slice(r)
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const newest = layer.runs[layer.runs.length - 1]
|
|
134
|
+
if (!newest.finished && !loops(newest.state) && newest.time >= stateLength(newest.state)) {
|
|
135
|
+
newest.finished = true
|
|
136
|
+
done.push(n)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return done
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* How fast a state plays its timeline, as its `speed` says (1 without).
|
|
144
|
+
* @param {any} state
|
|
145
|
+
*/
|
|
146
|
+
function speedOf(state) {
|
|
147
|
+
return typeof state.speed === 'number' && state.speed > 0 ? state.speed : 1
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* An animation's time at a run's time: wrapped if it loops, held a frame
|
|
152
|
+
* before its end if it doesn't (the last frame, as the plain player holds it).
|
|
153
|
+
*
|
|
154
|
+
* @param {number} time
|
|
155
|
+
* @param {string} id
|
|
156
|
+
* @param {boolean} isLooping
|
|
157
|
+
* @param {string} [mode] the state's `Reverse` or `ReverseBounce`; forward without
|
|
158
|
+
* @returns {number}
|
|
159
|
+
*/
|
|
160
|
+
function timeIn(time, id, isLooping, mode) {
|
|
161
|
+
const length = lengthOf(id)
|
|
162
|
+
if (length <= 0) return 0
|
|
163
|
+
// a ping-pong timeline is written out forward then back, so bouncing is playing it through, and bouncing in reverse is playing it backwards
|
|
164
|
+
if (mode === 'Reverse' || mode === 'ReverseBounce') return isLooping ? length - (time % length) : Math.max(0, length - time)
|
|
165
|
+
if (isLooping) return time % length
|
|
166
|
+
const entry = animations[id]
|
|
167
|
+
return time < length ? time : Math.max(0, length - 1 / entry.fps)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* What shows now, in the order the mixer applies it.
|
|
172
|
+
*
|
|
173
|
+
* @param {Record<string, any>} values the machine's inputs, which blends read
|
|
174
|
+
* @returns {Pose[]}
|
|
175
|
+
*/
|
|
176
|
+
function poses(values) {
|
|
177
|
+
/** @type {Pose[]} */
|
|
178
|
+
const result = []
|
|
179
|
+
for (let n = 0; n < layers.length; n++) {
|
|
180
|
+
const layer = layers[n]
|
|
181
|
+
if (!layer) continue
|
|
182
|
+
for (let r = 0; r < layer.runs.length; r++) {
|
|
183
|
+
const run = layer.runs[r]
|
|
184
|
+
if (run.state.blend) addBlend(result, run, values)
|
|
185
|
+
else result.push({ animation: run.state.animation, time: timeIn(run.time, run.state.animation, loops(run.state), run.state.mode), weight: run.ramp })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {Pose[]} result
|
|
193
|
+
* @param {Run} run
|
|
194
|
+
* @param {Record<string, any>} values
|
|
195
|
+
*/
|
|
196
|
+
function addBlend(result, run, values) {
|
|
197
|
+
const blend = run.state.blend
|
|
198
|
+
const weights = blend.type === 'Direct' ? directWeights(blend, values) : oneDimensionWeights(blend, values)
|
|
199
|
+
for (let n = 0; n < weights.length; n++) {
|
|
200
|
+
const entry = blend.animations[weights[n].index]
|
|
201
|
+
const weight = weights[n].weight * run.ramp
|
|
202
|
+
if (weight <= 0) continue
|
|
203
|
+
result.push({ animation: entry.animation, time: timeIn(run.time, entry.animation, loopsAll || entry.loop === true, run.state.mode), weight })
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The stretch of each playing animation's own timeline the last step passed
|
|
209
|
+
* over, in seconds, for its audio cues: more than one where it looped, and
|
|
210
|
+
* where a state plays backwards, the stretch of the timeline as it is laid out.
|
|
211
|
+
*
|
|
212
|
+
* @param {Record<string, any>} values
|
|
213
|
+
* @returns {{ animation: string, from: number, to: number }[]}
|
|
214
|
+
*/
|
|
215
|
+
function sweeps(values) {
|
|
216
|
+
/** @type {{ animation: string, from: number, to: number }[]} */
|
|
217
|
+
const result = []
|
|
218
|
+
for (let n = 0; n < layers.length; n++) {
|
|
219
|
+
const layer = layers[n]
|
|
220
|
+
if (!layer) continue
|
|
221
|
+
for (let r = 0; r < layer.runs.length; r++) {
|
|
222
|
+
const run = layer.runs[r]
|
|
223
|
+
const state = run.state
|
|
224
|
+
/** @type {{ id: string, isLooping: boolean }[]} */
|
|
225
|
+
const shown = []
|
|
226
|
+
if (!state.blend) shown.push({ id: state.animation, isLooping: loops(state) })
|
|
227
|
+
else {
|
|
228
|
+
const weights = state.blend.type === 'Direct' ? directWeights(state.blend, values) : oneDimensionWeights(state.blend, values)
|
|
229
|
+
for (let w = 0; w < weights.length; w++) {
|
|
230
|
+
const entry = state.blend.animations[weights[w].index]
|
|
231
|
+
if (weights[w].weight * run.ramp > 0) shown.push({ id: entry.animation, isLooping: loopsAll || entry.loop === true })
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (let s = 0; s < shown.length; s++) {
|
|
235
|
+
const length = lengthOf(shown[s].id)
|
|
236
|
+
const parts = segments(run.from, run.time, length, shown[s].isLooping)
|
|
237
|
+
for (let p = 0; p < parts.length; p++) {
|
|
238
|
+
const isReversed = state.mode === 'Reverse' || state.mode === 'ReverseBounce'
|
|
239
|
+
result.push(isReversed ? { animation: shown[s].id, from: length - parts[p][1], to: length - parts[p][0] } : { animation: shown[s].id, from: parts[p][0], to: parts[p][1] })
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return result
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The newest run of the first layer: what text overlays follow. */
|
|
248
|
+
function lead() {
|
|
249
|
+
const layer = layers[0]
|
|
250
|
+
if (!layer || !layer.runs.length) return null
|
|
251
|
+
const run = layer.runs[layer.runs.length - 1]
|
|
252
|
+
if (!run.state.blend) return { animation: run.state.animation, time: timeIn(run.time, run.state.animation, loops(run.state), run.state.mode) }
|
|
253
|
+
const first = run.state.blend.animations[0]
|
|
254
|
+
return first ? { animation: first.animation, time: timeIn(run.time, first.animation, loopsAll || first.loop === true, run.state.mode) } : null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
enter,
|
|
259
|
+
advance,
|
|
260
|
+
poses,
|
|
261
|
+
lead,
|
|
262
|
+
sweeps,
|
|
263
|
+
|
|
264
|
+
/** @returns {number} how many times time has moved on: the mixer's frame */
|
|
265
|
+
get frame() {
|
|
266
|
+
return frames
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @param {number} layer
|
|
271
|
+
* @returns {number} ms the layer's current state has played, unwrapped
|
|
272
|
+
*/
|
|
273
|
+
elapsedMs(layer) {
|
|
274
|
+
const target = layers[layer]
|
|
275
|
+
return target && target.runs.length ? target.runs[target.runs.length - 1].time * 1000 : 0
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/** Whether nothing moves: every state has run out, and no fade is left. */
|
|
279
|
+
isIdle() {
|
|
280
|
+
for (let n = 0; n < layers.length; n++) {
|
|
281
|
+
const layer = layers[n]
|
|
282
|
+
if (!layer) continue
|
|
283
|
+
const newest = layer.runs[layer.runs.length - 1]
|
|
284
|
+
if (layer.runs.length > 1 || !newest.finished) return false
|
|
285
|
+
}
|
|
286
|
+
return true
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/** Starts every layer's current state over, fades done. */
|
|
290
|
+
rewind() {
|
|
291
|
+
for (let n = 0; n < layers.length; n++) {
|
|
292
|
+
const layer = layers[n]
|
|
293
|
+
if (!layer || !layer.runs.length) continue
|
|
294
|
+
const newest = layer.runs[layer.runs.length - 1]
|
|
295
|
+
layer.runs = [ { state: newest.state, time: 0, ramp: 1, ease: null, fade: 0, fadeFor: 0, finished: false, from: -1 } ]
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
/** @param {boolean} on every state loops, as the `loop` control asks */
|
|
300
|
+
setLoops(on) {
|
|
301
|
+
loopsAll = on
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
/** Forgets every layer. */
|
|
305
|
+
clear() {
|
|
306
|
+
layers = []
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The stretches of a timeline that a step from `from` to `to` (a state's
|
|
313
|
+
* unwrapped clock) passes over: one, or one per time it wrapped, each after
|
|
314
|
+
* the first from before frame 0 so that frame sounds again. A timeline that
|
|
315
|
+
* does not loop stops at its end.
|
|
316
|
+
*
|
|
317
|
+
* @param {number} from
|
|
318
|
+
* @param {number} to
|
|
319
|
+
* @param {number} length the timeline's, in seconds
|
|
320
|
+
* @param {boolean} isLooping
|
|
321
|
+
* @returns {[number, number][]}
|
|
322
|
+
*/
|
|
323
|
+
export function segments(from, to, length, isLooping) {
|
|
324
|
+
if (length <= 0 || to <= from) return []
|
|
325
|
+
if (!isLooping) return from < length ? [ [ from, Math.min(to, length) ] ] : []
|
|
326
|
+
|
|
327
|
+
/** @type {[number, number][]} */
|
|
328
|
+
const parts = []
|
|
329
|
+
const first = Math.floor(Math.max(from, 0) / length)
|
|
330
|
+
let start = from
|
|
331
|
+
for (let wrap = first; wrap < first + 8; wrap++) {
|
|
332
|
+
const base = wrap * length
|
|
333
|
+
parts.push([ start < 0 ? start : start - base, Math.min(to, base + length) - base ])
|
|
334
|
+
if (to <= base + length) break
|
|
335
|
+
start = -1
|
|
336
|
+
}
|
|
337
|
+
return parts
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* How far a cross-fade is along its curve: a cubic Bezier from (0, 0) to
|
|
342
|
+
* (1, 1) with control points (x1, y1) and (x2, y2), read at time `t`, as
|
|
343
|
+
* Rive's cubic interpolator reads it. Without a curve, `t` itself.
|
|
344
|
+
*
|
|
345
|
+
* @param {number} t 0 to 1, how much of the fade's time has passed
|
|
346
|
+
* @param {number[]|null} curve [x1, y1, x2, y2]
|
|
347
|
+
* @returns {number}
|
|
348
|
+
*/
|
|
349
|
+
export function eased(t, curve) {
|
|
350
|
+
if (!curve || t <= 0 || t >= 1) return t
|
|
351
|
+
const x1 = curve[0]
|
|
352
|
+
const x2 = curve[2]
|
|
353
|
+
// solve x(s) = t for the curve's parameter s, by bisection: x is rising for the control points a designer can draw
|
|
354
|
+
let low = 0
|
|
355
|
+
let high = 1
|
|
356
|
+
let s = t
|
|
357
|
+
for (let n = 0; n < 24; n++) {
|
|
358
|
+
s = (low + high) / 2
|
|
359
|
+
const x = bezier(s, x1, x2)
|
|
360
|
+
if (x < t) low = s
|
|
361
|
+
else high = s
|
|
362
|
+
}
|
|
363
|
+
return bezier(s, curve[1], curve[3])
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* One axis of a cubic Bezier from 0 to 1.
|
|
368
|
+
*
|
|
369
|
+
* @param {number} s
|
|
370
|
+
* @param {number} p1
|
|
371
|
+
* @param {number} p2
|
|
372
|
+
*/
|
|
373
|
+
function bezier(s, p1, p2) {
|
|
374
|
+
const u = 1 - s
|
|
375
|
+
return 3 * u * u * s * p1 + 3 * u * s * s * p2 + s * s * s
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* A 1D blend: the animation at the input's value, or the two either side of
|
|
380
|
+
* it, weighted by how far along it is (the first by what is left, as Rive
|
|
381
|
+
* applies them; they lerp onto what the property held, so the pair settles
|
|
382
|
+
* a little off a plain average). A value outside the range
|
|
383
|
+
* plays the animation at that end.
|
|
384
|
+
*
|
|
385
|
+
* @param {any} blend
|
|
386
|
+
* @param {Record<string, any>} values
|
|
387
|
+
* @returns {{ index: number, weight: number }[]}
|
|
388
|
+
*/
|
|
389
|
+
export function oneDimensionWeights(blend, values) {
|
|
390
|
+
const raw = Number(convertValue(blend.convert, values[blend.input]))
|
|
391
|
+
const x = raw === raw ? raw : 0
|
|
392
|
+
|
|
393
|
+
// by value, as the animations were authored: the order may not be sorted
|
|
394
|
+
const order = []
|
|
395
|
+
for (let n = 0; n < blend.animations.length; n++) order.push(n)
|
|
396
|
+
order.sort((a, b) => blend.animations[a].value - blend.animations[b].value)
|
|
397
|
+
if (!order.length) return []
|
|
398
|
+
|
|
399
|
+
const first = order[0]
|
|
400
|
+
const last = order[order.length - 1]
|
|
401
|
+
if (x <= blend.animations[first].value) return [ { index: first, weight: 1 } ]
|
|
402
|
+
if (x >= blend.animations[last].value) return [ { index: last, weight: 1 } ]
|
|
403
|
+
|
|
404
|
+
for (let n = 0; n < order.length - 1; n++) {
|
|
405
|
+
const from = blend.animations[order[n]]
|
|
406
|
+
const to = blend.animations[order[n + 1]]
|
|
407
|
+
if (x > to.value) continue
|
|
408
|
+
const span = to.value - from.value
|
|
409
|
+
return [ { index: order[n], weight: 1 - (span > 0 ? (x - from.value) / span : 1) }, { index: order[n + 1], weight: span > 0 ? (x - from.value) / span : 1 } ]
|
|
410
|
+
}
|
|
411
|
+
return [ { index: last, weight: 1 } ]
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* A direct blend: each animation weighted by its own input, 0 to 100, or by
|
|
416
|
+
* the fixed mix the file gives it.
|
|
417
|
+
*
|
|
418
|
+
* @param {any} blend
|
|
419
|
+
* @param {Record<string, any>} values
|
|
420
|
+
* @returns {{ index: number, weight: number }[]}
|
|
421
|
+
*/
|
|
422
|
+
export function directWeights(blend, values) {
|
|
423
|
+
const weights = []
|
|
424
|
+
for (let n = 0; n < blend.animations.length; n++) {
|
|
425
|
+
const entry = blend.animations[n]
|
|
426
|
+
const raw = typeof entry.input === 'string' ? Number(convertValue(entry.convert, values[entry.input])) : entry.mix === undefined ? 100 : entry.mix
|
|
427
|
+
const percent = raw === raw ? raw : 0
|
|
428
|
+
weights.push({ index: n, weight: Math.min(1, Math.max(0, percent / 100)) })
|
|
429
|
+
}
|
|
430
|
+
return weights
|
|
431
|
+
}
|