@omnimedia/omnitool 1.1.0-78 → 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 (58) hide show
  1. package/README.md +107 -1
  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/item.ts +24 -1
  9. package/s/timeline/renderers/parts/handy.ts +53 -36
  10. package/s/timeline/renderers/parts/samplers/visual/parts/sample.ts +9 -8
  11. package/s/timeline/renderers/parts/samplers/visual/parts/sequence.ts +4 -4
  12. package/s/timeline/sugar/helpers.ts +74 -2
  13. package/s/timeline/sugar/o.ts +95 -2
  14. package/s/timeline/types.ts +26 -3
  15. package/s/timeline/utils/anim.ts +71 -0
  16. package/s/timeline/utils/terps.ts +81 -0
  17. package/x/demo/demo.bundle.min.js +104 -104
  18. package/x/demo/demo.bundle.min.js.map +4 -4
  19. package/x/demo/routines/timeline-setup.js +16 -4
  20. package/x/demo/routines/timeline-setup.js.map +1 -1
  21. package/x/driver/fns/schematic.d.ts +2 -0
  22. package/x/driver/parts/compositor.js +5 -0
  23. package/x/driver/parts/compositor.js.map +1 -1
  24. package/x/index.html +2 -2
  25. package/x/tests.bundle.min.js +105 -105
  26. package/x/tests.bundle.min.js.map +4 -4
  27. package/x/tests.html +1 -1
  28. package/x/timeline/index.d.ts +2 -0
  29. package/x/timeline/index.js +2 -0
  30. package/x/timeline/index.js.map +1 -1
  31. package/x/timeline/parts/animations.d.ts +25 -0
  32. package/x/timeline/parts/animations.js +12 -0
  33. package/x/timeline/parts/animations.js.map +1 -0
  34. package/x/timeline/parts/item.d.ts +24 -5
  35. package/x/timeline/parts/item.js +5 -3
  36. package/x/timeline/parts/item.js.map +1 -1
  37. package/x/timeline/renderers/parts/handy.d.ts +13 -7
  38. package/x/timeline/renderers/parts/handy.js +22 -20
  39. package/x/timeline/renderers/parts/handy.js.map +1 -1
  40. package/x/timeline/renderers/parts/samplers/visual/parts/sample.d.ts +3 -2
  41. package/x/timeline/renderers/parts/samplers/visual/parts/sample.js +6 -5
  42. package/x/timeline/renderers/parts/samplers/visual/parts/sample.js.map +1 -1
  43. package/x/timeline/renderers/parts/samplers/visual/parts/sequence.d.ts +3 -2
  44. package/x/timeline/renderers/parts/samplers/visual/parts/sequence.js +2 -2
  45. package/x/timeline/renderers/parts/samplers/visual/parts/sequence.js.map +1 -1
  46. package/x/timeline/sugar/helpers.d.ts +19 -2
  47. package/x/timeline/sugar/helpers.js +43 -0
  48. package/x/timeline/sugar/helpers.js.map +1 -1
  49. package/x/timeline/sugar/o.d.ts +18 -1
  50. package/x/timeline/sugar/o.js +71 -1
  51. package/x/timeline/sugar/o.js.map +1 -1
  52. package/x/timeline/types.d.ts +12 -2
  53. package/x/timeline/utils/anim.d.ts +5 -0
  54. package/x/timeline/utils/anim.js +44 -0
  55. package/x/timeline/utils/anim.js.map +1 -0
  56. package/x/timeline/utils/terps.d.ts +11 -0
  57. package/x/timeline/utils/terps.js +57 -0
  58. 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 =>
@@ -146,6 +146,89 @@ const timeline = omni.timeline(o => {
146
146
  })
147
147
  ```
148
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
+
149
232
  Worker URL notes:
150
233
  - `Driver.setup()` defaults to `/node_modules/@omnimedia/omnitool/x/driver/driver.worker.bundle.min.js`.
151
234
  - If you serve the worker from a different location, pass `workerUrl`:
@@ -228,4 +311,27 @@ omnitool ai "make a 15s promo for tea"
228
311
 
229
312
  - smooth seeking
230
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
+
231
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-78",
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
@@ -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
+
@@ -2,19 +2,20 @@
2
2
  import {SampleContext} from "./types.js"
3
3
  import {sampleSequence} from "./sequence.js"
4
4
  import {Ms} from "../../../../../../units/ms.js"
5
- import {computeWorldMatrix} from "../../../handy.js"
5
+ import {Item, Kind, SpatialItem} from "../../../../../parts/item.js"
6
6
  import {FilterSpec, Layer} from "../../../../../../driver/fns/schematic.js"
7
- import {ContainerItem, Item, Kind} from "../../../../../parts/item.js"
7
+ import {AncestorAt, computeOpacity, computeWorldMatrix} from "../../../handy.js"
8
8
 
9
9
  export async function sampleVisual(
10
10
  ctx: SampleContext,
11
11
  item: Item.Any,
12
12
  time: Ms,
13
- ancestors: ContainerItem[]
13
+ ancestors: AncestorAt[]
14
14
  ): Promise<Layer[]> {
15
- const matrix = computeWorldMatrix(ctx.items, ancestors, item)
15
+ const matrix = computeWorldMatrix(ctx.items, ancestors, item, time)
16
+ const alpha = computeOpacity(ctx, item, time)
16
17
  const crop = "spatialId" in item && item.spatialId
17
- ? (ctx.items.get(item.spatialId) as Item.Spatial | undefined)?.crop
18
+ ? (ctx.items.get(item.spatialId) as SpatialItem | undefined)?.crop
18
19
  : undefined
19
20
  const filters = "filterIds" in item && item.filterIds
20
21
  ? item.filterIds
@@ -25,7 +26,7 @@ export async function sampleVisual(
25
26
 
26
27
  switch (item.kind) {
27
28
  case Kind.Stack: {
28
- const nextAnc = [...ancestors, item]
29
+ const nextAnc = [...ancestors, {item, localTime: time}]
29
30
 
30
31
  const layers = await Promise.all(
31
32
  item.childrenIds
@@ -44,7 +45,7 @@ export async function sampleVisual(
44
45
  if (time < 0 || time >= item.duration) return []
45
46
 
46
47
  const frame = await ctx.videoSampler(item, time)
47
- return frame ? [{kind: "image", frame, matrix, crop, filters, id: item.id}] : []
48
+ return frame ? [{kind: "image", frame, matrix, alpha, crop, filters, id: item.id}] : []
48
49
  }
49
50
 
50
51
  case Kind.Text: {
@@ -54,7 +55,7 @@ export async function sampleVisual(
54
55
  ? (ctx.items.get(item.styleId) as Item.TextStyle)?.style
55
56
  : undefined
56
57
 
57
- return [{id: item.id, kind: "text", content: item.content, style, matrix, crop, filters}]
58
+ return [{id: item.id, kind: "text", content: item.content, style, matrix, alpha, crop, filters}]
58
59
  }
59
60
 
60
61
  case Kind.Gap: {
@@ -3,21 +3,21 @@ import {sampleVisual} from "./sample.js"
3
3
  import {SampleContext} from "./types.js"
4
4
  import {sampleTransition} from "./transition.js"
5
5
  import {ms, Ms} from "../../../../../../units/ms.js"
6
- import {computeItemDuration} from "../../../handy.js"
6
+ import {Item, Kind} from "../../../../../parts/item.js"
7
7
  import {Layer} from "../../../../../../driver/fns/schematic.js"
8
- import {ContainerItem, Item, Kind} from "../../../../../parts/item.js"
8
+ import {AncestorAt, computeItemDuration} from "../../../handy.js"
9
9
 
10
10
  export async function sampleSequence(
11
11
  ctx: SampleContext,
12
12
  seq: Item.Sequence,
13
13
  time: Ms,
14
- ancestors: ContainerItem[]
14
+ ancestors: AncestorAt[]
15
15
  ): Promise<Layer[]> {
16
16
  const state = sampleSequenceAt(ctx, seq, time)
17
17
 
18
18
  if (!state) return []
19
19
 
20
- const nextAnc = [...ancestors, seq]
20
+ const nextAnc = [...ancestors, {item: seq, localTime: time}]
21
21
 
22
22
  if (!state.isTransitioning) {
23
23
  return sampleVisual(ctx, state.item, state.localTime, nextAnc)