@omnimedia/omnitool 1.1.0-77 → 1.1.0-79

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.
Files changed (62) hide show
  1. package/README.md +111 -7
  2. package/package.json +1 -1
  3. package/s/demo/routines/timeline-setup.ts +37 -22
  4. package/s/driver/fns/schematic.ts +2 -0
  5. package/s/driver/parts/compositor.ts +6 -0
  6. package/s/timeline/index.ts +2 -0
  7. package/s/timeline/parts/animations.ts +24 -0
  8. package/s/timeline/parts/filters.ts +3 -34
  9. package/s/timeline/parts/item.ts +24 -1
  10. package/s/timeline/renderers/parts/handy.ts +53 -36
  11. package/s/timeline/renderers/parts/samplers/visual/parts/sample.ts +9 -8
  12. package/s/timeline/renderers/parts/samplers/visual/parts/sequence.ts +4 -4
  13. package/s/timeline/sugar/helpers.ts +77 -5
  14. package/s/timeline/sugar/o.ts +97 -4
  15. package/s/timeline/types.ts +26 -3
  16. package/s/timeline/utils/anim.ts +71 -0
  17. package/s/timeline/utils/terps.ts +81 -0
  18. package/x/demo/demo.bundle.min.js +104 -104
  19. package/x/demo/demo.bundle.min.js.map +4 -4
  20. package/x/demo/routines/timeline-setup.js +16 -4
  21. package/x/demo/routines/timeline-setup.js.map +1 -1
  22. package/x/driver/fns/schematic.d.ts +2 -0
  23. package/x/driver/parts/compositor.js +5 -0
  24. package/x/driver/parts/compositor.js.map +1 -1
  25. package/x/index.html +2 -2
  26. package/x/tests.bundle.min.js +106 -106
  27. package/x/tests.bundle.min.js.map +4 -4
  28. package/x/tests.html +1 -1
  29. package/x/timeline/index.d.ts +2 -0
  30. package/x/timeline/index.js +2 -0
  31. package/x/timeline/index.js.map +1 -1
  32. package/x/timeline/parts/animations.d.ts +25 -0
  33. package/x/timeline/parts/animations.js +12 -0
  34. package/x/timeline/parts/animations.js.map +1 -0
  35. package/x/timeline/parts/filters.d.ts +3 -8
  36. package/x/timeline/parts/filters.js +1 -16
  37. package/x/timeline/parts/filters.js.map +1 -1
  38. package/x/timeline/parts/item.d.ts +24 -5
  39. package/x/timeline/parts/item.js +5 -3
  40. package/x/timeline/parts/item.js.map +1 -1
  41. package/x/timeline/renderers/parts/handy.d.ts +13 -7
  42. package/x/timeline/renderers/parts/handy.js +22 -20
  43. package/x/timeline/renderers/parts/handy.js.map +1 -1
  44. package/x/timeline/renderers/parts/samplers/visual/parts/sample.d.ts +3 -2
  45. package/x/timeline/renderers/parts/samplers/visual/parts/sample.js +6 -5
  46. package/x/timeline/renderers/parts/samplers/visual/parts/sample.js.map +1 -1
  47. package/x/timeline/renderers/parts/samplers/visual/parts/sequence.d.ts +3 -2
  48. package/x/timeline/renderers/parts/samplers/visual/parts/sequence.js +2 -2
  49. package/x/timeline/renderers/parts/samplers/visual/parts/sequence.js.map +1 -1
  50. package/x/timeline/sugar/helpers.d.ts +21 -4
  51. package/x/timeline/sugar/helpers.js +45 -2
  52. package/x/timeline/sugar/helpers.js.map +1 -1
  53. package/x/timeline/sugar/o.d.ts +18 -1
  54. package/x/timeline/sugar/o.js +72 -2
  55. package/x/timeline/sugar/o.js.map +1 -1
  56. package/x/timeline/types.d.ts +12 -2
  57. package/x/timeline/utils/anim.d.ts +5 -0
  58. package/x/timeline/utils/anim.js +44 -0
  59. package/x/timeline/utils/anim.js.map +1 -0
  60. package/x/timeline/utils/terps.d.ts +11 -0
  61. package/x/timeline/utils/terps.js +57 -0
  62. package/x/timeline/utils/terps.js.map +1 -0
package/README.md CHANGED
@@ -81,7 +81,7 @@ const timeline = timeline(
81
81
 
82
82
  ## 🎛 Filters
83
83
 
84
- Inline filter application:
84
+ Filter application:
85
85
 
86
86
  ```ts
87
87
  const timeline = omni.timeline(o =>
@@ -115,14 +115,12 @@ Filter metadata for UI:
115
115
 
116
116
  ```ts
117
117
  import {
118
- filterTypes,
119
- filterSchemas,
120
- getFilterDefaultParams
118
+ filters
121
119
  } from "@omnimedia/omnitool"
122
120
 
123
- const available = Object.entries(filterTypes)
124
- const schema = filterSchemas.BlurFilter
125
- const defaults = getFilterDefaultParams("BlurFilter")
121
+ const available = Object.entries(filters)
122
+ const blur = filters.blur
123
+ const schema = blur.schema
126
124
  ```
127
125
 
128
126
  ## 🧭 Spatial Transforms
@@ -148,6 +146,89 @@ const timeline = omni.timeline(o => {
148
146
  })
149
147
  ```
150
148
 
149
+ Animated spatial transforms:
150
+
151
+ ```ts
152
+ const timeline = omni.timeline(o => {
153
+ const slideIn = o.animatedSpatial(
154
+ o.anim.transform("linear", [
155
+ [0, o.transform({position: [-400, 0]})],
156
+ [1000, o.transform({position: [0, 0]})],
157
+ ])
158
+ )
159
+
160
+ const title = o.text("Lower third", {
161
+ duration: 2000,
162
+ styles: {fill: "white", fontSize: 36}
163
+ })
164
+ o.set(title.id, {spatialId: slideIn.id})
165
+
166
+ return o.stack(
167
+ o.video(clip, {duration: 4000}),
168
+ title
169
+ )
170
+ })
171
+ ```
172
+
173
+ Animation application:
174
+
175
+ ```ts
176
+ const timeline = omni.timeline(o => {
177
+ const title = o.animate.opacity(
178
+ o.text("Lower third", {
179
+ duration: 2000,
180
+ styles: {fill: "white", fontSize: 36},
181
+ }),
182
+ "easeIn",
183
+ [
184
+ [0, 0],
185
+ [700, 1],
186
+ ]
187
+ )
188
+
189
+ return o.stack(
190
+ o.video(clip, {duration: 4000}),
191
+ title
192
+ )
193
+ })
194
+ ```
195
+
196
+ Reusable animation:
197
+
198
+ ```ts
199
+ const timeline = omni.timeline(o => {
200
+ const fadeIn = o.animate.opacity.make("easeIn", [
201
+ [0, 0],
202
+ [700, 1],
203
+ ])
204
+
205
+ const title = o.text("Lower third", {
206
+ duration: 2000,
207
+ styles: {fill: "white", fontSize: 36},
208
+ })
209
+ o.set(title.id, {animationId: fadeIn.id})
210
+
211
+ return o.stack(
212
+ o.video(clip, {duration: 4000}),
213
+ title
214
+ )
215
+ })
216
+ ```
217
+
218
+ Animation registry:
219
+
220
+ ```ts
221
+ import {animations} from "@omnimedia/omnitool"
222
+
223
+ Object.entries(animations).forEach(([property, meta]) => {
224
+ console.log(property, meta.value)
225
+ // transform transform
226
+ // opacity scalar
227
+ })
228
+ ```
229
+
230
+ This is useful for UI code that needs to list keyframeable properties and choose the right editor shape for each one.
231
+
151
232
  Worker URL notes:
152
233
  - `Driver.setup()` defaults to `/node_modules/@omnimedia/omnitool/x/driver/driver.worker.bundle.min.js`.
153
234
  - If you serve the worker from a different location, pass `workerUrl`:
@@ -230,4 +311,27 @@ omnitool ai "make a 15s promo for tea"
230
311
 
231
312
  - smooth seeking
232
313
  - keyframes
314
+ - custom filters, likely via driver-side registration with timeline sugar such as:
315
+
316
+ ```ts
317
+ // Register custom filter
318
+ driver.registerFilter({
319
+ type: "vhs",
320
+ make: params => new Filter(/* ... */),
321
+ schema: {
322
+ intensity: {type: "number", min: 0, max: 1, default: 0.5},
323
+ scanlines: {type: "boolean", default: true},
324
+ },
325
+ })
326
+
327
+ // Use custom filter
328
+ const timeline = omni.timeline(o =>
329
+ o.filter.custom(
330
+ "vhs",
331
+ o.video(clip, {duration: 3000}),
332
+ {intensity: 0.8}
333
+ )
334
+ )
335
+ ```
336
+
233
337
  - server-side, not just browsers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnimedia/omnitool",
3
- "version": "1.1.0-77",
3
+ "version": "1.1.0-79",
4
4
  "description": "open source video processing tools",
5
5
  "license": "MIT",
6
6
  "author": "Przemysław Gałęzki",
@@ -6,30 +6,45 @@ export async function TimelineSchemaTest(driver: Driver, file: File) {
6
6
  const omni = new Omni(driver)
7
7
  const {videoA} = await omni.load({videoA: Datafile.make(file)})
8
8
  const timeline = omni.timeline(o => {
9
- const text = o.text("content", {duration: 1000})
10
- const style = o.textStyle({fill: "green", fontSize: 100})
11
- const spatial = o.spatial(
12
- o.transform({
13
- position: [240, 160],
14
- scale: [1.4, 1.4],
15
- rotation: 0.08,
16
- }),
17
- [0.15, 0.1, 0.05, 0.2],
18
- )
9
+ const text = o.text("content", {duration: 3000})
10
+ const fade = o.animate.opacity.make("easeIn", [
11
+ [0, 0],
12
+ [700, 1],
13
+ [2200, 1],
14
+ [3000, 0.35],
15
+ ])
16
+ const style = o.textStyle({fill: "green", fontSize: 100})
17
+ const videoSpatial = o.spatial(
18
+ o.transform({
19
+ position: [240, 160],
20
+ scale: [1.4, 1.4],
21
+ rotation: 0.08,
22
+ }),
23
+ [0.15, 0.1, 0.05, 0.2],
24
+ )
25
+ const textSpatial = o.animatedSpatial(
26
+ o.anim.transform("linear", [
27
+ [0, o.transform({position: [-320, 80], scale: [0.7, 0.7]})],
28
+ [1000, o.transform({position: [120, 80], scale: [1, 1]})],
29
+ [2000, o.transform({position: [200, 40], scale: [1.35, 1.35], rotation: 8})],
30
+ [3000, o.transform({position: [320, 0], scale: [1.15, 1.15], rotation: 0})],
31
+ ]),
32
+ )
19
33
 
20
- const video = o.video(videoA, {duration: 3000, start: 1000})
21
- o.set<Item.Text>(text.id, {styleId: style.id})
22
- o.set<Item.Video>(video.id, {spatialId: spatial.id})
34
+ const video = o.video(videoA, {duration: 3000, start: 1000})
35
+ o.set<Item.Text>(text.id, {styleId: style.id, spatialId: textSpatial.id, animationId: fade.id})
36
+ o.set<Item.Video>(video.id, {spatialId: videoSpatial.id})
23
37
 
24
- return o.sequence(
25
- o.stack(
26
- text,
27
- video,
28
- o.audio(videoA, {duration: 1000})
29
- ),
30
- o.gap(500),
31
- o.video(videoA, {duration: 7000, start: 5000})
32
- )})
38
+ return o.sequence(
39
+ o.stack(
40
+ text,
41
+ video,
42
+ o.audio(videoA, {duration: 1000})
43
+ ),
44
+ o.gap(500),
45
+ o.video(videoA, {duration: 7000, start: 5000})
46
+ )
47
+ })
33
48
 
34
49
  return {timeline, omni}
35
50
  }
@@ -88,6 +88,7 @@ export type TextLayer = {
88
88
  content: string
89
89
  style?: TextStyleOptions
90
90
  matrix?: Mat6
91
+ alpha?: number
91
92
  crop?: Crop
92
93
  filters?: FilterSpec[]
93
94
  }
@@ -97,6 +98,7 @@ export type ImageLayer = {
97
98
  kind: 'image'
98
99
  frame: VideoFrame
99
100
  matrix?: Mat6
101
+ alpha?: number
100
102
  crop?: Crop
101
103
  filters?: FilterSpec[]
102
104
  }
@@ -111,6 +111,7 @@ export class Compositor {
111
111
  ) {
112
112
  const sprite = this.#findOrCreate<Text>(layer)!
113
113
  this.#applyTransform(sprite, layer.matrix)
114
+ this.#applyAlpha(sprite, layer.alpha)
114
115
  this.#applyCrop(sprite, layer.crop)
115
116
  this.#applyFilters(sprite, layer.filters)
116
117
  parent.addChild(sprite)
@@ -132,6 +133,7 @@ export class Compositor {
132
133
  const texture = Texture.from(layer.frame)
133
134
  sprite.texture = texture
134
135
  this.#applyTransform(sprite, layer.matrix)
136
+ this.#applyAlpha(sprite, layer.alpha)
135
137
  this.#applyCrop(sprite, layer.crop)
136
138
  this.#applyFilters(sprite, layer.filters)
137
139
  parent.addChild(sprite)
@@ -166,6 +168,10 @@ export class Compositor {
166
168
  target.setFromMatrix(mx)
167
169
  }
168
170
 
171
+ #applyAlpha(target: Container, alpha?: number) {
172
+ target.alpha = alpha ?? 1
173
+ }
174
+
169
175
  #applyCrop(target: Container, crop?: Crop) {
170
176
  const existing = this.#cropMasks.get(target)
171
177
  if (existing) {
@@ -1,11 +1,13 @@
1
1
 
2
2
  export * from "./parts/basics.js"
3
+ export * from "./parts/animations.js"
3
4
  export * from "./parts/filters.js"
4
5
  export * from "./parts/item.js"
5
6
  export * from "./parts/media.js"
6
7
  export * from "./parts/resource-pool.js"
7
8
  export * from "./parts/resource.js"
8
9
  export * from "./parts/filmstrip.js"
10
+ export * from "./types.js"
9
11
 
10
12
  export * from "./parts/waveform/waveform.js"
11
13
  export * from "./parts/waveform/parts/types.js"
@@ -0,0 +1,24 @@
1
+ export type AnimationValue = "scalar" | "transform"
2
+
3
+ export type AnimationDefinition = {
4
+ value: AnimationValue
5
+ }
6
+
7
+ export const spatialAnimations = {
8
+ transform: {value: "transform"},
9
+ } as const satisfies Record<string, AnimationDefinition>
10
+
11
+ export const visualAnimations = {
12
+ opacity: {value: "scalar"},
13
+ } as const satisfies Record<string, AnimationDefinition>
14
+
15
+ // const audioAnimations = {}
16
+
17
+ export const animations = {
18
+ ...spatialAnimations,
19
+ ...visualAnimations,
20
+ } as const
21
+
22
+ export type SpatialAnimationProperty = keyof typeof spatialAnimations
23
+ export type VisualAnimationProperty = keyof typeof visualAnimations
24
+ export type AnimationProperty = keyof typeof animations
@@ -127,7 +127,7 @@ const defineFilter = <TParams>() =>
127
127
  schema: SchemaFromOptions<TParams> = {},
128
128
  ): FilterDefinition<TType, TParams> => ({type, schema})
129
129
 
130
- export const filterTypes = {
130
+ export const filters = {
131
131
  adjustment: defineFilter<PixiFilters.AdjustmentFilterOptions>()("AdjustmentFilter", {
132
132
  gamma: num(0, 5, 1),
133
133
  saturation: num(0, 5, 1),
@@ -430,7 +430,7 @@ export const filterTypes = {
430
430
  }),
431
431
  } as const
432
432
 
433
- type FilterDefinitions = typeof filterTypes
433
+ type FilterDefinitions = typeof filters
434
434
  type FilterDefinitionParams<T> =
435
435
  T extends FilterDefinition<string, infer TParams>
436
436
  ? TParams
@@ -443,42 +443,11 @@ export type FilterOptions = {
443
443
 
444
444
  export type FilterType = FilterDefinitions[keyof FilterDefinitions]["type"]
445
445
  export type FilterParams<T extends FilterType = FilterType> = FilterOptions[T]
446
- export type FilterSchemas = {
447
- [TName in keyof FilterDefinitions as FilterDefinitions[TName]["type"]]:
448
- FilterDefinitions[TName]["schema"]
449
- }
450
-
451
- export const filterSchemas = Object.fromEntries(
452
- Object.values(filterTypes).map(filter => [filter.type, filter.schema]),
453
- ) as FilterSchemas
454
-
455
- const getDefaultValue =(config: FilterPropertyConfig): unknown => {
456
- switch(config.type) {
457
- case "number":
458
- case "boolean":
459
- case "color":
460
- case "choice":
461
- return config.default
462
-
463
- case "object":
464
- return Object.fromEntries(
465
- Object.entries(config.properties).map(([key, value]) => [key, getDefaultValue(value)]),
466
- )
467
-
468
- case "array":
469
- return config.items.map(getDefaultValue)
470
- }
471
- }
472
-
473
- export const getFilterDefaultParams = <TFilter extends FilterType>(type: TFilter): FilterParams<TFilter> =>
474
- Object.fromEntries(
475
- Object.entries(filterSchemas[type]).map(([key, value]) => [key, getDefaultValue(value)]),
476
- ) as FilterParams<TFilter>
477
446
 
478
447
  export interface FilterAction<TFilter extends FilterType> {
479
448
  <T extends FilterableItem>(item: T, params?: FilterParams<TFilter>): T
480
449
  make(params?: FilterParams<TFilter>): Item.Filter<TFilter>
481
450
  }
482
451
  export type FilterActions = {
483
- [TName in keyof typeof filterTypes]: FilterAction<(typeof filterTypes)[TName]["type"]>
452
+ [TName in keyof typeof filters]: FilterAction<(typeof filters)[TName]["type"]>
484
453
  }
@@ -2,9 +2,9 @@
2
2
  import {TextStyleOptions} from "pixi.js"
3
3
 
4
4
  import {Id, Hash} from "./basics.js"
5
- import {Transform} from "../types.js"
6
5
  import {Ms} from "../../units/ms.js"
7
6
  import type {FilterParams, FilterType} from "./filters.js"
7
+ import {Anim, TrackTransform, Transform, VisualAnimations} from "../types.js"
8
8
 
9
9
  export type Crop = [top: number, right: number, bottom: number, left: number]
10
10
 
@@ -16,6 +16,8 @@ export enum Kind {
16
16
  Text,
17
17
  Gap,
18
18
  Spatial,
19
+ AnimatedSpatial,
20
+ Animation,
19
21
  Transition,
20
22
  TextStyle,
21
23
  Filter
@@ -40,6 +42,21 @@ export namespace Item {
40
42
  enabled: boolean
41
43
  }
42
44
 
45
+ export type AnimatedSpatial = {
46
+ id: Id
47
+ kind: Kind.AnimatedSpatial
48
+ anim: Anim<TrackTransform>
49
+ crop?: Crop
50
+ enabled: boolean
51
+ }
52
+
53
+ export type Animation = {
54
+ id: Id
55
+ kind: Kind.Animation
56
+ anims: VisualAnimations
57
+ enabled: boolean
58
+ }
59
+
43
60
  export type Filter<T extends FilterType = FilterType> = {
44
61
  id: Id
45
62
  kind: Kind.Filter
@@ -77,6 +94,7 @@ export namespace Item {
77
94
  start: number
78
95
  duration: number
79
96
  spatialId?: Id
97
+ animationId?: Id
80
98
  filterIds?: Id[]
81
99
  }
82
100
 
@@ -95,6 +113,7 @@ export namespace Item {
95
113
  content: string
96
114
  duration: number
97
115
  spatialId?: Id
116
+ animationId?: Id
98
117
  styleId?: Id
99
118
  filterIds?: Id[]
100
119
  }
@@ -115,6 +134,8 @@ export namespace Item {
115
134
  | Gap
116
135
  | Transition
117
136
  | Spatial
137
+ | AnimatedSpatial
138
+ | Animation
118
139
  | TextStyle
119
140
  | Filter
120
141
  )
@@ -123,6 +144,8 @@ export namespace Item {
123
144
  export type ContainerItem = Item.Sequence | Item.Stack
124
145
  export type NonContainerItem = Exclude<Item.Any, ContainerItem>
125
146
  export type FilterableItem = Item.Sequence | Item.Stack | Item.Video | Item.Text
147
+ export type SpatialItem = Item.Spatial | Item.AnimatedSpatial
148
+ export type VisualAnimatableItem = Item.Video | Item.Text
126
149
 
127
150
  export type PlayableItem = Item.Any & {
128
151
  start: Ms
@@ -1,26 +1,28 @@
1
1
 
2
2
  import {ms, Ms} from '../../../units/ms.js'
3
3
  import {Id, TimelineFile} from '../../parts/basics.js'
4
+ import { SampleContext } from './samplers/visual/parts/types.js'
4
5
  import {I6, Mat6, mul6, transformToMat6} from '../../utils/matrix.js'
5
- import {ContainerItem, Item, Kind, PlayableItem} from '../../parts/item.js'
6
+ import {resolveScalarAnimation, resolveSpatialTransform} from '../../utils/anim.js'
7
+ import {ContainerItem, Item, Kind, PlayableItem, SpatialItem} from '../../parts/item.js'
6
8
 
7
9
  function isPlayableItem(item: Item.Any): item is PlayableItem {
8
10
  return 'duration' in item
9
11
  }
10
12
 
11
13
  type WalkAtCallbacks = {
12
- sequence: (x: Item.Sequence, localTime: Ms, ancestors: ContainerItem[]) => void
13
- stack: (x: Item.Stack, localTime: Ms, ancestors: ContainerItem[]) => void
14
- video: (x: Item.Video, localTime: Ms, ancestors: ContainerItem[]) => void
15
- text: (x: Item.Text, localTime: Ms, ancestors: ContainerItem[]) => void
16
- audio: (x: Item.Audio, localTime: Ms, ancestors: ContainerItem[]) => void
14
+ sequence: (x: Item.Sequence, localTime: Ms, ancestors: AncestorAt[]) => void
15
+ stack: (x: Item.Stack, localTime: Ms, ancestors: AncestorAt[]) => void
16
+ video: (x: Item.Video, localTime: Ms, ancestors: AncestorAt[]) => void
17
+ text: (x: Item.Text, localTime: Ms, ancestors: AncestorAt[]) => void
18
+ audio: (x: Item.Audio, localTime: Ms, ancestors: AncestorAt[]) => void
17
19
  }
18
20
 
19
21
  type WalkCallbacks = {
20
- sequence?: (x: Item.Sequence, matrix: Mat6, ancestors: ContainerItem[]) => void
21
- stack?: (x: Item.Stack, matrix: Mat6, ancestors: ContainerItem[]) => void
22
- video?: (x: Item.Video, matrix: Mat6, ancestors: ContainerItem[]) => void
23
- text?: (x: Item.Text, matrix: Mat6, ancestors: ContainerItem[]) => void
22
+ sequence?: (x: Item.Sequence, matrix: Mat6, ancestors: AncestorAt[]) => void
23
+ stack?: (x: Item.Stack, matrix: Mat6, ancestors: AncestorAt[]) => void
24
+ video?: (x: Item.Video, matrix: Mat6, ancestors: AncestorAt[]) => void
25
+ text?: (x: Item.Text, matrix: Mat6, ancestors: AncestorAt[]) => void
24
26
  audio?: (x: Item.Audio) => void
25
27
  }
26
28
 
@@ -29,10 +31,15 @@ interface Props {
29
31
  timecode: Ms
30
32
  }
31
33
 
34
+ export interface AncestorAt {
35
+ item: ContainerItem
36
+ localTime: Ms
37
+ }
38
+
32
39
  interface At {
33
40
  item: Item.Any
34
41
  localTime: Ms
35
- ancestors: ContainerItem[]
42
+ ancestors: AncestorAt[]
36
43
  }
37
44
 
38
45
  export function itemsAt(p: Props): At[] {
@@ -72,27 +79,29 @@ export function itemsFrom(p: FromProps): At[] {
72
79
 
73
80
  export function computeWorldMatrix(
74
81
  items: Map<Id, Item.Any>,
75
- ancestors: ContainerItem[],
76
- item: Item.Any
82
+ ancestors: AncestorAt[],
83
+ item: Item.Any,
84
+ localTime: Ms,
77
85
  ): Mat6 {
78
86
  let world = I6
79
87
 
80
88
  for (const ancestor of ancestors) {
81
- world = applySpatialIfAny(items, ancestor, world)
89
+ world = applySpatialIfAny(items, ancestor.item, world, ancestor.localTime)
82
90
  }
83
91
 
84
- return applySpatialIfAny(items, item, world)
92
+ return applySpatialIfAny(items, item, world, localTime)
85
93
  }
86
94
 
87
95
  function applySpatialIfAny(
88
96
  items: Map<Id, Item.Any>,
89
97
  item: Item.Any,
90
- parentMatrix: Mat6
98
+ parentMatrix: Mat6,
99
+ time: Ms,
91
100
  ) {
92
101
  if ("spatialId" in item && item.spatialId) {
93
- const spatial = items.get(item.spatialId) as Item.Spatial | undefined
102
+ const spatial = items.get(item.spatialId) as SpatialItem | undefined
94
103
  if (spatial?.enabled) {
95
- const local = transformToMat6(spatial.transform)
104
+ const local = transformToMat6(resolveSpatialTransform(spatial, time))
96
105
  return mul6(local, parentMatrix)
97
106
  }
98
107
  }
@@ -103,27 +112,20 @@ export function walk(
103
112
  id: Id,
104
113
  items: Map<Id, Item.Any>,
105
114
  parentMatrix: Mat6,
115
+ localTime: Ms,
106
116
  callbacks: WalkCallbacks,
107
- ancestors: ContainerItem[] = []
117
+ ancestors: AncestorAt[] = []
108
118
  ) {
109
119
  const item = items.get(id)
110
120
  if (!item) return
111
121
 
112
- let currentMatrix = parentMatrix
113
-
114
- if ("spatialId" in item && item.spatialId) {
115
- const spatial = items.get(item.spatialId) as Item.Spatial
116
- if (spatial.enabled) {
117
- const local = transformToMat6(spatial.transform)
118
- currentMatrix = mul6(local, currentMatrix)
119
- }
120
- }
122
+ const currentMatrix = applySpatialIfAny(items, item, parentMatrix, localTime)
121
123
 
122
124
  switch (item.kind) {
123
125
  case Kind.Stack:
124
126
  callbacks.stack?.(item, currentMatrix, ancestors)
125
127
  for (const childId of item.childrenIds) {
126
- walk(childId, items, currentMatrix, callbacks, [...ancestors, item])
128
+ walk(childId, items, currentMatrix, localTime, callbacks, [...ancestors, {item, localTime}])
127
129
  }
128
130
  break
129
131
 
@@ -143,8 +145,9 @@ export function walk(
143
145
  childId,
144
146
  items,
145
147
  currentMatrix,
148
+ localTime,
146
149
  callbacks,
147
- [...ancestors, item]
150
+ [...ancestors, {item, localTime}]
148
151
  )
149
152
  }
150
153
 
@@ -171,7 +174,7 @@ function walkAt(
171
174
  items: Map<Id, Item.Any>,
172
175
  time: Ms,
173
176
  callbacks: WalkAtCallbacks,
174
- ancestors: ContainerItem[] = []
177
+ ancestors: AncestorAt[] = []
175
178
  ) {
176
179
  const item = items.get(id)
177
180
  if (!item) return
@@ -180,7 +183,7 @@ function walkAt(
180
183
  case Kind.Stack:
181
184
  callbacks.stack(item, time, ancestors)
182
185
  for (const childId of item.childrenIds) {
183
- walkAt(childId, items, time, callbacks, [...ancestors, item])
186
+ walkAt(childId, items, time, callbacks, [...ancestors, {item, localTime: time}])
184
187
  }
185
188
  break
186
189
 
@@ -205,7 +208,7 @@ function walkAt(
205
208
  items,
206
209
  localTime,
207
210
  callbacks,
208
- [...ancestors, item]
211
+ [...ancestors, {item, localTime: time}]
209
212
  )
210
213
  break
211
214
  }
@@ -235,7 +238,7 @@ function walkFrom(
235
238
  items: Map<Id, Item.Any>,
236
239
  from: Ms,
237
240
  callbacks: WalkAtCallbacks,
238
- ancestors: ContainerItem[] = []
241
+ ancestors: AncestorAt[] = []
239
242
  ) {
240
243
  const item = items.get(id)
241
244
  if (!item) return
@@ -244,7 +247,7 @@ function walkFrom(
244
247
  case Kind.Stack:
245
248
  callbacks.stack(item, from, ancestors)
246
249
  for (const childId of item.childrenIds) {
247
- walkFrom(childId, items, from, callbacks, [...ancestors, item])
250
+ walkFrom(childId, items, from, callbacks, [...ancestors, {item, localTime: from}])
248
251
  }
249
252
  break
250
253
 
@@ -274,7 +277,7 @@ function walkFrom(
274
277
  items,
275
278
  localTime,
276
279
  callbacks,
277
- [...ancestors, item]
280
+ [...ancestors, {item, localTime: from}]
278
281
  )
279
282
 
280
283
  offset = end
@@ -358,3 +361,17 @@ export function computeItemDuration(
358
361
  }
359
362
  }
360
363
 
364
+ export function computeOpacity(
365
+ ctx: SampleContext,
366
+ item: Item.Any,
367
+ time: Ms,
368
+ ) {
369
+ if (!("animationId" in item) || item.animationId === undefined)
370
+ return 1
371
+
372
+ const animation = ctx.items.get(item.animationId) as Item.Animation | undefined
373
+ return animation?.enabled && animation.anims.opacity
374
+ ? resolveScalarAnimation(time, animation.anims.opacity)
375
+ : 1
376
+ }
377
+