@omnimedia/omnitool 1.1.0-97 → 1.1.0-99

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 +46 -13
  2. package/package.json +1 -1
  3. package/s/demo/routines/timeline-setup.ts +8 -4
  4. package/s/driver/fns/schematic.ts +2 -1
  5. package/s/driver/parts/compositor.ts +1 -1
  6. package/s/features/bg-remover/worker.bundle.ts +4 -3
  7. package/s/features/parts/expose-errors.ts +41 -0
  8. package/s/features/speech/transcribe/worker.bundle.ts +4 -3
  9. package/s/features/transition/parts/types.ts +3 -70
  10. package/s/timeline/index.ts +1 -0
  11. package/s/timeline/parts/item.ts +2 -5
  12. package/s/timeline/parts/transitions.ts +102 -0
  13. package/s/timeline/renderers/parts/samplers/visual/parts/transition.ts +1 -1
  14. package/s/timeline/sugar/helpers.ts +9 -4
  15. package/s/timeline/sugar/o.ts +16 -7
  16. package/s/timeline/sugar/omni.test.ts +7 -7
  17. package/x/demo/demo.bundle.min.js +25 -25
  18. package/x/demo/demo.bundle.min.js.map +4 -4
  19. package/x/demo/routines/timeline-setup.js +3 -2
  20. package/x/demo/routines/timeline-setup.js.map +1 -1
  21. package/x/driver/fns/schematic.d.ts +2 -1
  22. package/x/driver/parts/compositor.js +1 -1
  23. package/x/driver/parts/compositor.js.map +1 -1
  24. package/x/features/bg-remover/worker.bundle.js +4 -3
  25. package/x/features/bg-remover/worker.bundle.js.map +1 -1
  26. package/x/features/bg-remover/worker.bundle.min.js +167 -167
  27. package/x/features/bg-remover/worker.bundle.min.js.map +4 -4
  28. package/x/features/parts/expose-errors.d.ts +3 -0
  29. package/x/features/parts/expose-errors.js +32 -0
  30. package/x/features/parts/expose-errors.js.map +1 -0
  31. package/x/features/speech/transcribe/worker.bundle.js +4 -3
  32. package/x/features/speech/transcribe/worker.bundle.js.map +1 -1
  33. package/x/features/speech/transcribe/worker.bundle.min.js +167 -167
  34. package/x/features/speech/transcribe/worker.bundle.min.js.map +4 -4
  35. package/x/features/transition/parts/types.d.ts +3 -3
  36. package/x/index.html +2 -2
  37. package/x/tests.bundle.min.js +58 -58
  38. package/x/tests.bundle.min.js.map +4 -4
  39. package/x/tests.html +1 -1
  40. package/x/timeline/index.d.ts +1 -0
  41. package/x/timeline/index.js +1 -0
  42. package/x/timeline/index.js.map +1 -1
  43. package/x/timeline/parts/item.d.ts +2 -4
  44. package/x/timeline/parts/item.js +0 -4
  45. package/x/timeline/parts/item.js.map +1 -1
  46. package/x/timeline/parts/transitions.d.ts +15 -0
  47. package/x/timeline/parts/transitions.js +81 -0
  48. package/x/timeline/parts/transitions.js.map +1 -0
  49. package/x/timeline/renderers/parts/samplers/visual/parts/transition.js +1 -1
  50. package/x/timeline/renderers/parts/samplers/visual/parts/transition.js.map +1 -1
  51. package/x/timeline/sugar/helpers.d.ts +5 -3
  52. package/x/timeline/sugar/helpers.js +7 -5
  53. package/x/timeline/sugar/helpers.js.map +1 -1
  54. package/x/timeline/sugar/o.d.ts +2 -3
  55. package/x/timeline/sugar/o.js +12 -5
  56. package/x/timeline/sugar/o.js.map +1 -1
  57. package/x/timeline/sugar/omni.test.js +5 -5
  58. package/x/timeline/sugar/omni.test.js.map +1 -1
package/README.md CHANGED
@@ -36,17 +36,21 @@ const timeline = omni.timeline(o => {
36
36
  duration: 1500,
37
37
  styles: {fill: "white", fontSize: 48}
38
38
  })
39
- const xfade = o.transition.crossfade(500)
39
+ const xfade = o.transition.fade(500)
40
40
  const softened = o.filter.blur.make({strength: 8, quality: 4})
41
41
 
42
- return o.sequence(
42
+ const visual = o.sequence(
43
43
  o.stack(
44
44
  o.video(clip, {start: 0, duration: 3000, filterIds: [softened.id]}),
45
45
  caption
46
46
  ),
47
- o.gap(400),
48
47
  xfade,
49
48
  o.video(clip, {start: 5000, duration: 2500}),
49
+ o.gap(400)
50
+ )
51
+
52
+ return o.stack(
53
+ visual,
50
54
  o.audio(clip, {start: 5000, duration: 2500})
51
55
  )
52
56
  })
@@ -57,7 +61,7 @@ Declarative helper style (no explicit `o` in timeline declarations):
57
61
  ```ts
58
62
  import {
59
63
  Driver, Omni, Datafile,
60
- timeline, stack, video, audio, text, gap, transition, filter
64
+ timeline, sequence, stack, video, audio, text, gap, transition, filter
61
65
  } from "@omnimedia/omnitool"
62
66
 
63
67
  const driver = await Driver.setup()
@@ -66,16 +70,20 @@ const {clip} = await omni.load({clip: Datafile.make(file)})
66
70
 
67
71
  const timeline = timeline(
68
72
  stack(
69
- filter.blur(
70
- video(clip, {start: 0, duration: 3000}),
71
- {strength: 8, quality: 4}
73
+ sequence(
74
+ stack(
75
+ filter.blur(
76
+ video(clip, {start: 0, duration: 3000}),
77
+ {strength: 8, quality: 4}
78
+ ),
79
+ text("Hello world", {duration: 1500}),
80
+ ),
81
+ transition.fade(500),
82
+ video(clip, {start: 5000, duration: 2500}),
83
+ gap(400),
72
84
  ),
73
- text("Hello world", {duration: 1500}),
74
- ),
75
- gap(400),
76
- transition.crossfade(500),
77
- video(clip, {start: 5000, duration: 2500}),
78
- audio(clip, {start: 5000, duration: 2500})
85
+ audio(clip, {start: 5000, duration: 2500})
86
+ )
79
87
  )
80
88
  ```
81
89
 
@@ -458,6 +466,31 @@ const opacity = resolveScalarAnimation(localTime, opacityAnimation)
458
466
  `localTime` is time relative to the item being resolved.
459
467
  `clamp` is the default and currently only extrapolation mode, holding the first or last keyframe value outside the authored range.
460
468
 
469
+ ## 🔀 Transitions
470
+
471
+ Transitions are sequence items placed between two visual items.
472
+
473
+ ```ts
474
+ const timeline = omni.timeline(o =>
475
+ o.sequence(
476
+ o.video(firstClip, {duration: 3000}),
477
+ o.transition.fade(700),
478
+ o.video(secondClip, {duration: 3000}),
479
+ )
480
+ )
481
+ ```
482
+
483
+ Use the exported transition registry to build UI:
484
+
485
+ ```ts
486
+ import {transitions} from "@omnimedia/omnitool"
487
+
488
+ const options = Object.entries(transitions).map(([key, transition]) => ({
489
+ key,
490
+ label: transition.label,
491
+ }))
492
+ ```
493
+
461
494
  Worker URL notes:
462
495
  - `Driver.setup()` defaults to `/node_modules/@omnimedia/omnitool/x/driver/driver.worker.bundle.min.js`.
463
496
  - If you serve the worker from a different location, pass `workerUrl`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnimedia/omnitool",
3
- "version": "1.1.0-97",
3
+ "version": "1.1.0-99",
4
4
  "description": "open source video processing tools",
5
5
  "license": "MIT",
6
6
  "author": "Przemysław Gałęzki",
@@ -49,7 +49,7 @@ export async function TimelineSchemaTest(driver: Driver, file: File) {
49
49
  o.set<Item.Text>(text.id, {styleId: style.id, spatialId: textSpatial.id, animationIds: [fade.id, textMotion.id]})
50
50
  o.set<Item.Video>(video.id, {spatialId: videoSpatial.id})
51
51
 
52
- return o.sequence(
52
+ const visual = o.sequence(
53
53
  o.stack(
54
54
  text,
55
55
  o.captions(video, transcript, {
@@ -65,12 +65,16 @@ export async function TimelineSchemaTest(driver: Driver, file: File) {
65
65
  wordWrapWidth: 1280,
66
66
  stroke: {color: "#000000", width: 6},
67
67
  },
68
- }),
69
- o.audio(videoA, {start: 3000})
68
+ })
70
69
  ),
71
- o.gap(500),
70
+ o.transition.circle(1500),
72
71
  o.video(videoA, {duration: 7000, start: 5000})
73
72
  )
73
+
74
+ return o.stack(
75
+ visual,
76
+ o.audio(videoA, {start: 3000, duration: video.duration})
77
+ )
74
78
  })
75
79
 
76
80
  return {timeline, omni}
@@ -7,6 +7,7 @@ import {Id} from "../../timeline/index.js"
7
7
  import {Crop} from "../../timeline/parts/item.js"
8
8
  import {Mat6} from "../../timeline/utils/matrix.js"
9
9
  import {FilterParams, FilterType} from "../../timeline/parts/filters.js"
10
+ import {TransitionName} from "../../timeline/parts/transitions.js"
10
11
 
11
12
  export type DriverSchematic = AsSchematic<{
12
13
 
@@ -106,7 +107,7 @@ export type ImageLayer = {
106
107
  export type TransitionLayer = {
107
108
  id: Id
108
109
  kind: 'transition'
109
- name: string
110
+ name: TransitionName
110
111
  progress: number
111
112
  from: VideoFrame
112
113
  to: VideoFrame
@@ -154,7 +154,7 @@ export class Compositor {
154
154
  ) {
155
155
  const transition = this.#transitions.get(name) ??
156
156
  (this.#transitions.set(name, makeTransition({
157
- name: "circle",
157
+ name,
158
158
  renderer: this.pixi.renderer
159
159
  })),
160
160
  this.#transitions.get(name)!
@@ -6,6 +6,7 @@ import {BackgroundRemovalPipeline} from "@huggingface/transformers"
6
6
  import {PipelineSpec} from "../parts/types.js"
7
7
  import {BgRemoverSchematic} from "./types.js"
8
8
  import {loadPipe} from "../parts/load-pipe.js"
9
+ import {exposeErrors} from "../parts/expose-errors.js"
9
10
 
10
11
  const deferred = defer<{spec: PipelineSpec, pipe: BackgroundRemovalPipeline}>()
11
12
  const makePrepare = (host: Host<BgRemoverSchematic>) => once(async(spec: PipelineSpec) => {
@@ -25,8 +26,8 @@ const ctx = canvas.getContext("2d")
25
26
  await Comrade.worker<BgRemoverSchematic>(shell => {
26
27
  const prepare = makePrepare(shell.host)
27
28
  return {
28
- prepare,
29
- async remove(request) {
29
+ prepare: exposeErrors(prepare),
30
+ remove: exposeErrors(async(request) => {
30
31
  const {pipe} = await deferred.promise
31
32
 
32
33
  canvas.width = request.displayWidth
@@ -44,7 +45,7 @@ await Comrade.worker<BgRemoverSchematic>(shell => {
44
45
  request.close()
45
46
  shell.transfer = [frame]
46
47
  return frame
47
- }
48
+ })
48
49
  }
49
50
  })
50
51
 
@@ -0,0 +1,41 @@
1
+ import {ExposedError} from "@e280/renraku"
2
+
3
+ export function exposeError(error: unknown) {
4
+ if (error instanceof ExposedError)
5
+ return error
6
+
7
+ const exposed = new ExposedError(errorToString(error))
8
+
9
+ if (error instanceof Error)
10
+ exposed.stack = error.stack
11
+
12
+ return exposed
13
+ }
14
+
15
+ export function exposeErrors<Args extends unknown[], Result>(
16
+ fn: (...args: Args) => Result | Promise<Result>
17
+ ) {
18
+ return async(...args: Args) => {
19
+ try {
20
+ return await fn(...args)
21
+ }
22
+ catch (error) {
23
+ throw exposeError(error)
24
+ }
25
+ }
26
+ }
27
+
28
+ function errorToString(error: unknown) {
29
+ if (error instanceof Error)
30
+ return error.message
31
+
32
+ if (typeof error === "string")
33
+ return error
34
+
35
+ try {
36
+ return JSON.stringify(error) ?? String(error)
37
+ }
38
+ catch {
39
+ return String(error)
40
+ }
41
+ }
@@ -5,6 +5,7 @@ import {AutomaticSpeechRecognitionPipeline, Pipeline} from "@huggingface/transfo
5
5
 
6
6
  import {loadPipe} from "../../parts/load-pipe.js"
7
7
  import {transcribe} from "./parts/transcribe.js"
8
+ import {exposeErrors} from "../../parts/expose-errors.js"
8
9
  import {TranscriberSchematic, TranscriberSpec} from "./types.js"
9
10
 
10
11
  const deferred = defer<{pipe: Pipeline, spec: TranscriberSpec}>()
@@ -23,8 +24,8 @@ const makePrepare = (host: Host<TranscriberSchematic>) => once(async(spec: Trans
23
24
  await Comrade.worker<TranscriberSchematic>(shell => {
24
25
  const prepare = makePrepare(shell.host)
25
26
  return {
26
- prepare,
27
- async transcribe(request) {
27
+ prepare: exposeErrors(prepare),
28
+ transcribe: exposeErrors(async(request) => {
28
29
  const {pipe, spec} = await deferred.promise
29
30
  return transcribe({
30
31
  pipe,
@@ -35,7 +36,7 @@ await Comrade.worker<TranscriberSchematic>(shell => {
35
36
  onTranscription: transcription => shell.host.deliverTranscription(transcription),
36
37
  },
37
38
  })
38
- }
39
+ })
39
40
  }
40
41
  })
41
42
 
@@ -1,7 +1,8 @@
1
1
  import {Renderer} from "pixi.js"
2
+ import {TransitionName} from "../../../timeline/parts/transitions.js"
2
3
 
3
4
  export interface TransitionOptions {
4
- name: Transition
5
+ name: TransitionName
5
6
  renderer: Renderer
6
7
  }
7
8
 
@@ -18,77 +19,9 @@ export interface GLTransition {
18
19
  createdAt: string
19
20
  glsl: string
20
21
  license: string
21
- name: Transition
22
+ name: TransitionName
22
23
  updatedAt: string
23
24
  defaultParams: any
24
25
  paramsTypes: any
25
26
  }
26
27
 
27
- export type Transition =
28
- | "Bounce"
29
- | "BowTieHorizontal"
30
- | "BowTieVertical"
31
- | "ButterflyWaveScrawler"
32
- | "CircleCrop"
33
- | "ColourDistance"
34
- | "CrazyParametricFun"
35
- | "CrossZoom"
36
- | "Directional"
37
- | "DoomScreenTransition"
38
- | "Dreamy"
39
- | "DreamyZoom"
40
- | "GlitchDisplace"
41
- | "GlitchMemories"
42
- | "GridFlip"
43
- | "InvertedPageCurl"
44
- | "LinearBlur"
45
- | "Mosaic"
46
- | "PolkaDotsCurtain"
47
- | "Radial"
48
- | "SimpleZoom"
49
- | "StereoViewer"
50
- | "Swirl"
51
- | "WaterDrop"
52
- | "ZoomInCircles"
53
- | "angular"
54
- | "burn"
55
- | "cannabisleaf"
56
- | "circle"
57
- | "circleopen"
58
- | "colorphase"
59
- | "crosshatch"
60
- | "crosswarp"
61
- | "cube"
62
- | "directionalwarp"
63
- | "directionalwipe"
64
- | "displacement"
65
- | "doorway"
66
- | "fade"
67
- | "fadecolor"
68
- | "fadegrayscale"
69
- | "flyeye"
70
- | "heart"
71
- | "hexagonalize"
72
- | "kaleidoscope"
73
- | "luma"
74
- | "luminance_melt"
75
- | "morph"
76
- | "multiply_blend"
77
- | "perlin"
78
- | "pinwheel"
79
- | "pixelize"
80
- | "polar_function"
81
- | "randomsquares"
82
- | "ripple"
83
- | "rotate_scale_fade"
84
- | "squareswire"
85
- | "squeeze"
86
- | "swap"
87
- | "undulatingBurnOut"
88
- | "wind"
89
- | "windowblinds"
90
- | "windowslice"
91
- | "wipeDown"
92
- | "wipeLeft"
93
- | "wipeRight"
94
- | "wipeUp"
@@ -4,6 +4,7 @@ export * from "./parts/basics.js"
4
4
  export * from "./parts/filters.js"
5
5
  export * from "./parts/item.js"
6
6
  export * from "./parts/media.js"
7
+ export * from "./parts/transitions.js"
7
8
  export * from "./parts/resource-pool.js"
8
9
  export * from "./parts/resource.js"
9
10
  export * from "./parts/filmstrip.js"
@@ -3,6 +3,7 @@ import {TextStyleOptions} from "pixi.js"
3
3
 
4
4
  import {Id, Hash} from "./basics.js"
5
5
  import {Ms} from "../../units/ms.js"
6
+ import type {TransitionName} from "./transitions.js"
6
7
  import {Transform, VisualAnimations} from "../types.js"
7
8
  import type {FilterParams, FilterType} from "./filters.js"
8
9
  import type {Transcription} from "../../features/speech/transcribe/types.js"
@@ -25,10 +26,6 @@ export enum Kind {
25
26
  Image
26
27
  }
27
28
 
28
- export enum Effect {
29
- Crossfade,
30
- }
31
-
32
29
  export namespace Item {
33
30
  export type TextStyle = {
34
31
  id: Id
@@ -142,7 +139,7 @@ export namespace Item {
142
139
  export type Transition = {
143
140
  id: Id
144
141
  kind: Kind.Transition
145
- effect: Effect.Crossfade
142
+ name: TransitionName
146
143
  duration: number
147
144
  }
148
145
 
@@ -0,0 +1,102 @@
1
+ import type {Item} from "./item.js"
2
+
3
+ export const transitionNames = [
4
+ "Bounce",
5
+ "BowTieHorizontal",
6
+ "BowTieVertical",
7
+ "ButterflyWaveScrawler",
8
+ "CircleCrop",
9
+ "ColourDistance",
10
+ "CrazyParametricFun",
11
+ "CrossZoom",
12
+ "Directional",
13
+ "DoomScreenTransition",
14
+ "Dreamy",
15
+ "DreamyZoom",
16
+ "GlitchDisplace",
17
+ "GlitchMemories",
18
+ "GridFlip",
19
+ "InvertedPageCurl",
20
+ "LinearBlur",
21
+ "Mosaic",
22
+ "PolkaDotsCurtain",
23
+ "Radial",
24
+ "SimpleZoom",
25
+ "StereoViewer",
26
+ "Swirl",
27
+ "WaterDrop",
28
+ "ZoomInCircles",
29
+ "angular",
30
+ "burn",
31
+ "cannabisleaf",
32
+ "circle",
33
+ "circleopen",
34
+ "colorphase",
35
+ "crosshatch",
36
+ "crosswarp",
37
+ "cube",
38
+ "directionalwarp",
39
+ "directionalwipe",
40
+ "displacement",
41
+ "doorway",
42
+ "fade",
43
+ "fadecolor",
44
+ "fadegrayscale",
45
+ "flyeye",
46
+ "heart",
47
+ "hexagonalize",
48
+ "kaleidoscope",
49
+ "luma",
50
+ "luminance_melt",
51
+ "morph",
52
+ "multiply_blend",
53
+ "perlin",
54
+ "pinwheel",
55
+ "pixelize",
56
+ "polar_function",
57
+ "randomsquares",
58
+ "ripple",
59
+ "rotate_scale_fade",
60
+ "squareswire",
61
+ "squeeze",
62
+ "swap",
63
+ "undulatingBurnOut",
64
+ "wind",
65
+ "windowblinds",
66
+ "windowslice",
67
+ "wipeDown",
68
+ "wipeLeft",
69
+ "wipeRight",
70
+ "wipeUp",
71
+ ] as const
72
+
73
+ export type TransitionName = typeof transitionNames[number]
74
+
75
+ export type Transition = {
76
+ name: TransitionName
77
+ label: string
78
+ }
79
+
80
+ export interface TransitionAction {
81
+ (duration: number): Item.Transition
82
+ }
83
+
84
+ export type TransitionActions = {
85
+ [TName in TransitionName]: TransitionAction
86
+ }
87
+
88
+ export const transitionRegistry = Object.fromEntries(
89
+ transitionNames.map(name => [name, {
90
+ name,
91
+ label: labelizeTransitionName(name),
92
+ }])
93
+ ) as Record<TransitionName, Transition>
94
+
95
+ export const transitions = transitionRegistry
96
+
97
+ function labelizeTransitionName(name: string) {
98
+ return name
99
+ .replace(/_/g, " ")
100
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
101
+ .replace(/\b\w/g, letter => letter.toUpperCase())
102
+ }
@@ -20,7 +20,7 @@ export async function sampleTransition(
20
20
  return f1 && f2 ? [{
21
21
  id: item.id,
22
22
  kind: "transition",
23
- name: "circle",
23
+ name: item.name,
24
24
  progress,
25
25
  from: f1,
26
26
  to: f2
@@ -5,6 +5,7 @@ import {O} from "./o.js"
5
5
  import {Media} from "../parts/media.js"
6
6
  import {TimelineFile} from "../parts/basics.js"
7
7
  import {FilterAction} from "../parts/filters.js"
8
+ import {TransitionName, transitions} from "../parts/transitions.js"
8
9
  import {filters, FilterParams, FilterType} from "../parts/filters.js"
9
10
  import {CaptionOptions, CaptionSourceItem} from "../parts/captions.js"
10
11
  import {Transcription} from "../../features/speech/transcribe/types.js"
@@ -20,6 +21,9 @@ type BuildVisualAnimateActions = {
20
21
  type BuildPresetAnimateActions = {
21
22
  [TKey in AnimationPreset]: BuildPresetAnimateAction
22
23
  }
24
+ type BuildTransitionActions = {
25
+ [TKey in TransitionName]: (duration: number) => Build<Item.Transition>
26
+ }
23
27
 
24
28
  function createTimeline(): TimelineFile {
25
29
  return {
@@ -226,9 +230,10 @@ export function textStyle(style: TextStyleOptions): Build<Item.TextStyle> {
226
230
  return o => o.textStyle(style)
227
231
  }
228
232
 
229
- export const transition = {
230
- crossfade(duration: number): Build<Item.Transition> {
231
- return o => o.transition.crossfade(duration)
232
- }
233
+ function makeTransitionActions(): BuildTransitionActions {
234
+ const entries = Object.keys(transitions)
235
+ .map(key => [key, (duration: number) => (o: O) => o.transition[key as TransitionName](duration)])
236
+ return Object.fromEntries(entries) as BuildTransitionActions
233
237
  }
234
238
 
239
+ export const transition = makeTransitionActions()
@@ -6,8 +6,9 @@ import {Id, TimelineFile} from "../parts/basics.js"
6
6
  import {FilterAction, FilterActions} from "../parts/filters.js"
7
7
  import {filters, FilterParams, FilterType} from "../parts/filters.js"
8
8
  import {Transcription} from "../../features/speech/transcribe/types.js"
9
- import {Crop, Effect, FilterableItem, Item, Kind, VisualAnimatableItem} from "../parts/item.js"
9
+ import {Crop, FilterableItem, Item, Kind, VisualAnimatableItem} from "../parts/item.js"
10
10
  import {animationPresets, makeAnimationPresets, visualAnimations} from "../parts/animations/registry.js"
11
+ import {TransitionAction, TransitionActions, transitions, TransitionName} from "../parts/transitions.js"
11
12
  import {AnimationPreset, PresetAnimateAction, PresetAnimateActions, PresetAnimation, PresetOptions} from "../parts/animations/types.js"
12
13
  import {CaptionAction, CaptionActions, captionDuration, CaptionOptions, CaptionPreset, captionPresets, CaptionSourceItem} from "../parts/captions.js"
13
14
  import {Anim, AnimateAction, Interpolation, Keyframes, ScalarAnimation, TrackTransform, Transform, TransformAnimation, TransformOptions, Vec2, VisualAnimationInput, VisualAnimationValue, VisualAnimations} from "../types.js"
@@ -395,19 +396,27 @@ export class O {
395
396
  return item
396
397
  }
397
398
 
398
- transition = {
399
- crossfade: (duration: number): Item.Transition => {
400
- const item = {
399
+ #makeTransition = (key: TransitionName): TransitionAction => {
400
+ return (duration: number): Item.Transition => {
401
+ const item: Item.Transition = {
401
402
  id: this.getId(),
402
403
  kind: Kind.Transition,
403
- effect: Effect.Crossfade,
404
+ name: transitions[key].name,
404
405
  duration,
405
- } as Item.Transition
406
+ }
406
407
  this.register(item)
407
408
  return item
408
- },
409
+ }
409
410
  }
410
411
 
412
+ #makeTransitions = (): TransitionActions => {
413
+ const entries = Object.keys(transitions)
414
+ .map(key => [key, this.#makeTransition(key as TransitionName)])
415
+ return Object.fromEntries(entries) as TransitionActions
416
+ }
417
+
418
+ transition = this.#makeTransitions()
419
+
411
420
  transform = (options?: TransformOptions): Transform => {
412
421
  const position: Vec2 = [
413
422
  options?.position?.[0] ?? 0,
@@ -104,7 +104,7 @@ export default Science.suite({
104
104
  const transitionDuration = 1000
105
105
  const o = new O({timeline: omni.timeline(o => o.sequence(
106
106
  o.video(videoA),
107
- o.transition.crossfade(transitionDuration),
107
+ o.transition.fade(transitionDuration),
108
108
  o.video(videoA)
109
109
  ))})
110
110
  const timelineDuration = computeItemDuration(o.timeline.rootId, o.timeline)
@@ -117,7 +117,7 @@ export default Science.suite({
117
117
  const transitionDuration = 1000
118
118
  const o = new O({timeline: omni.timeline(o => o.stack(
119
119
  o.video(videoA),
120
- o.transition.crossfade(transitionDuration),
120
+ o.transition.fade(transitionDuration),
121
121
  o.video(videoA)
122
122
  ))})
123
123
  const timelineDuration = computeItemDuration(o.timeline.rootId, o.timeline)
@@ -127,10 +127,10 @@ export default Science.suite({
127
127
  "ignore invalid transition": test(async () => {
128
128
  const {omni, videoA} = await setupTest()
129
129
  const o = new O({timeline: omni.timeline(o => o.sequence(
130
- o.transition.crossfade(1000),
130
+ o.transition.fade(1000),
131
131
  o.video(videoA),
132
132
  o.video(videoA),
133
- o.transition.crossfade(1000),
133
+ o.transition.fade(1000),
134
134
  ))})
135
135
  const timelineDuration = computeItemDuration(o.timeline.rootId, o.timeline)
136
136
  expect(timelineDuration).is(videoA.duration * 2)
@@ -141,7 +141,7 @@ export default Science.suite({
141
141
  const duration = 3000
142
142
  const o = new O({timeline: omni.timeline(o => o.sequence(
143
143
  o.video(videoA, {duration}),
144
- o.transition.crossfade(duration * 2),
144
+ o.transition.fade(duration * 2),
145
145
  o.video(videoA, {duration})
146
146
  ))})
147
147
  const timelineDuration = computeItemDuration(o.timeline.rootId, o.timeline)
@@ -153,9 +153,9 @@ export default Science.suite({
153
153
  const transitionDuration = 1000
154
154
  const o = new O({timeline: omni.timeline(o => o.sequence(
155
155
  o.video(videoA),
156
- o.transition.crossfade(transitionDuration),
156
+ o.transition.fade(transitionDuration),
157
157
  o.video(videoA),
158
- o.transition.crossfade(transitionDuration),
158
+ o.transition.fade(transitionDuration),
159
159
  o.video(videoA)
160
160
  ))})
161
161
  const timelineDuration = computeItemDuration(o.timeline.rootId, o.timeline)