etro 0.9.1 → 0.10.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CONTRIBUTING.md +23 -20
  3. package/README.md +3 -2
  4. package/dist/custom-array.d.ts +10 -0
  5. package/dist/effect/base.d.ts +10 -1
  6. package/dist/effect/shader.d.ts +11 -1
  7. package/dist/effect/stack.d.ts +6 -2
  8. package/dist/etro-cjs.js +1156 -575
  9. package/dist/etro-iife.js +1156 -575
  10. package/dist/event.d.ts +10 -5
  11. package/dist/layer/audio-source.d.ts +9 -4
  12. package/dist/layer/audio.d.ts +15 -2
  13. package/dist/layer/base.d.ts +49 -3
  14. package/dist/layer/image.d.ts +15 -1
  15. package/dist/layer/text.d.ts +3 -0
  16. package/dist/layer/video.d.ts +13 -1
  17. package/dist/layer/visual.d.ts +6 -2
  18. package/dist/movie/effects.d.ts +6 -0
  19. package/dist/movie/index.d.ts +1 -0
  20. package/dist/movie/layers.d.ts +6 -0
  21. package/dist/movie/movie.d.ts +260 -0
  22. package/dist/object.d.ts +9 -6
  23. package/dist/util.d.ts +4 -10
  24. package/eslint.conf.js +2 -2
  25. package/karma.conf.js +4 -7
  26. package/package.json +8 -7
  27. package/src/custom-array.ts +43 -0
  28. package/src/effect/base.ts +23 -22
  29. package/src/effect/gaussian-blur.ts +11 -6
  30. package/src/effect/pixelate.ts +3 -3
  31. package/src/effect/shader.ts +33 -27
  32. package/src/effect/stack.ts +43 -30
  33. package/src/effect/transform.ts +14 -7
  34. package/src/event.ts +111 -21
  35. package/src/layer/audio-source.ts +60 -20
  36. package/src/layer/audio.ts +25 -3
  37. package/src/layer/base.ts +79 -26
  38. package/src/layer/image.ts +26 -2
  39. package/src/layer/text.ts +7 -0
  40. package/src/layer/video.ts +31 -4
  41. package/src/layer/visual-source.ts +43 -1
  42. package/src/layer/visual.ts +50 -28
  43. package/src/movie/effects.ts +26 -0
  44. package/src/movie/index.ts +1 -0
  45. package/src/movie/layers.ts +26 -0
  46. package/src/movie/movie.ts +855 -0
  47. package/src/object.ts +9 -6
  48. package/src/util.ts +68 -89
  49. package/dist/movie.d.ts +0 -201
  50. package/src/movie.ts +0 -744
  51. /package/scripts/{gen-effect-samples.html → effect/gen-effect-samples.html} +0 -0
  52. /package/scripts/{save-effect-samples.js → effect/save-effect-samples.js} +0 -0
@@ -2,14 +2,41 @@ import { Visual } from './visual'
2
2
  import { VisualSourceOptions, VisualSourceMixin } from './visual-source'
3
3
  import { AudioSourceOptions, AudioSourceMixin } from './audio-source'
4
4
 
5
- type VideoOptions = VisualSourceOptions & AudioSourceOptions
5
+ interface VideoOptions extends Omit<AudioSourceOptions & VisualSourceOptions, 'duration'|'source'> {
6
+ duration?: number
7
+
8
+ /**
9
+ * The raw html `<video>` element
10
+ */
11
+ source: string | HTMLVideoElement
12
+ }
6
13
 
7
- // Use mixins instead of `extend`ing two classes (which isn't supported by
8
- // JavaScript).
9
14
  /**
15
+ * Layer for an HTML video element
10
16
  * @extends AudioSource
11
17
  * @extends VisualSource
12
18
  */
13
- class Video extends AudioSourceMixin(VisualSourceMixin(Visual)) {}
19
+ class Video extends AudioSourceMixin(VisualSourceMixin(Visual)) {
20
+ /**
21
+ * The raw html `<video>` element
22
+ */
23
+ source: HTMLVideoElement
24
+
25
+ constructor (options: VideoOptions) {
26
+ if (typeof (options.source) === 'string') {
27
+ const video = document.createElement('video')
28
+ video.src = options.source
29
+ options.source = video
30
+ }
31
+
32
+ super({
33
+ ...options,
34
+
35
+ // Set a default duration so that the super constructor doesn't throw an
36
+ // error
37
+ duration: options.duration ?? 0
38
+ } as (AudioSourceOptions & VisualSourceOptions))
39
+ }
40
+ }
14
41
 
15
42
  export { Video, VideoOptions }
@@ -75,10 +75,40 @@ function VisualSourceMixin<OptionsSuperclass extends VisualOptions> (superclass:
75
75
  destHeight: Dynamic<number>
76
76
 
77
77
  constructor (options: MixedVisualSourceOptions) {
78
+ if (!options.source) {
79
+ throw new Error('Property "source" is required in options')
80
+ }
81
+
78
82
  super(options)
79
83
  applyOptions(options, this)
80
84
  }
81
85
 
86
+ async whenReady (): Promise<void> {
87
+ await super.whenReady()
88
+
89
+ await new Promise<void>(resolve => {
90
+ if (this.source instanceof HTMLImageElement) {
91
+ // The source is an image; wait for it to load
92
+ if (this.source.complete) {
93
+ resolve()
94
+ } else {
95
+ this.source.addEventListener('load', () => {
96
+ resolve()
97
+ })
98
+ }
99
+ } else {
100
+ // The source is a video; wait for the first frame to load
101
+ if (this.source.readyState === 4) {
102
+ resolve()
103
+ } else {
104
+ this.source.addEventListener('canplaythrough', () => {
105
+ resolve()
106
+ })
107
+ }
108
+ }
109
+ })
110
+ }
111
+
82
112
  doRender () {
83
113
  // Clear/fill background
84
114
  super.doRender()
@@ -86,7 +116,7 @@ function VisualSourceMixin<OptionsSuperclass extends VisualOptions> (superclass:
86
116
  /*
87
117
  * Source dimensions crop the image. Dest dimensions set the size that
88
118
  * the image will be rendered at *on the layer*. Note that this is
89
- * different than the layer dimensions (`this.width` and `this.height`).
119
+ * different from the layer dimensions (`this.width` and `this.height`).
90
120
  * The main reason this distinction exists is so that an image layer can
91
121
  * be rotated without being cropped (see iss #46).
92
122
  */
@@ -100,6 +130,18 @@ function VisualSourceMixin<OptionsSuperclass extends VisualOptions> (superclass:
100
130
  )
101
131
  }
102
132
 
133
+ get ready (): boolean {
134
+ // Typescript doesn't support `super.ready` when targeting es5
135
+ const superReady = Object.getOwnPropertyDescriptor(superclass.prototype, 'ready').get.call(this)
136
+ const sourceReady = this.source instanceof HTMLImageElement
137
+ ? this.source.complete
138
+ : this.source.readyState === 4
139
+ return superReady && sourceReady
140
+ }
141
+
142
+ /**
143
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
144
+ */
103
145
  getDefaultOptions (): MixedVisualSourceOptions {
104
146
  return {
105
147
  ...superclass.prototype.getDefaultOptions(),
@@ -1,7 +1,35 @@
1
+ import { CustomArray, CustomArrayListener } from '../custom-array'
1
2
  import { Dynamic, val, applyOptions, Color } from '../util'
2
3
  import { Base, BaseOptions } from './base'
3
4
  import { Visual as VisualEffect } from '../effect/visual'
4
5
 
6
+ // eslint-disable-next-line no-use-before-define
7
+ class VisualEffectsListener extends CustomArrayListener<VisualEffect> {
8
+ // eslint-disable-next-line no-use-before-define
9
+ private _layer: Visual
10
+
11
+ // eslint-disable-next-line no-use-before-define
12
+ constructor (layer: Visual) {
13
+ super()
14
+ this._layer = layer
15
+ }
16
+
17
+ onAdd (effect: VisualEffect) {
18
+ effect.tryAttach(this._layer)
19
+ }
20
+
21
+ onRemove (effect: VisualEffect) {
22
+ effect.tryDetach()
23
+ }
24
+ }
25
+
26
+ class VisualEffects extends CustomArray<VisualEffect> {
27
+ // eslint-disable-next-line no-use-before-define
28
+ constructor (target: VisualEffect[], layer: Visual) {
29
+ super(target, new VisualEffectsListener(layer))
30
+ }
31
+ }
32
+
5
33
  interface VisualOptions extends BaseOptions {
6
34
  x?: Dynamic<number>
7
35
  y?: Dynamic<number>
@@ -43,40 +71,22 @@ class Visual extends Base {
43
71
  // readonly because it's a proxy
44
72
  readonly effects: VisualEffect[]
45
73
 
46
- private _effectsBack: VisualEffect[]
47
-
48
74
  /**
49
75
  * Creates a visual layer
50
76
  */
51
77
  constructor (options: VisualOptions) {
52
78
  super(options)
53
- // Only validate extra if not subclassed, because if subclcass, there will
54
- // be extraneous options.
79
+
55
80
  applyOptions(options, this)
56
81
 
57
82
  this.canvas = document.createElement('canvas')
58
83
  this.cctx = this.canvas.getContext('2d')
84
+ this.effects = new VisualEffects([], this)
85
+ }
59
86
 
60
- this._effectsBack = []
61
- this.effects = new Proxy(this._effectsBack, {
62
- deleteProperty: (target, property) => {
63
- const value = target[property]
64
- value.detach()
65
- delete target[property]
66
- return true
67
- },
68
- set: (target, property, value) => {
69
- if (!isNaN(Number(property))) {
70
- // The property is a number (index)
71
- if (target[property])
72
- target[property].detach()
73
-
74
- value.attach(this)
75
- }
76
- target[property] = value
77
- return true
78
- }
79
- })
87
+ async whenReady (): Promise<void> {
88
+ await super.whenReady()
89
+ await Promise.all(this.effects.map(effect => effect.whenReady()))
80
90
  }
81
91
 
82
92
  /**
@@ -86,8 +96,9 @@ class Visual extends Base {
86
96
  // Prevent empty canvas errors if the width or height is 0
87
97
  const width = val(this, 'width', this.currentTime)
88
98
  const height = val(this, 'height', this.currentTime)
89
- if (width === 0 || height === 0)
99
+ if (width === 0 || height === 0) {
90
100
  return
101
+ }
91
102
 
92
103
  this.beginRender()
93
104
  this.doRender()
@@ -122,8 +133,9 @@ class Visual extends Base {
122
133
  endRender (): void {
123
134
  const w = val(this, 'width', this.currentTime) || val(this.movie, 'width', this.movie.currentTime)
124
135
  const h = val(this, 'height', this.currentTime) || val(this.movie, 'height', this.movie.currentTime)
125
- if (w * h > 0)
136
+ if (w * h > 0) {
126
137
  this._applyEffects()
138
+ }
127
139
 
128
140
  // else InvalidStateError for drawing zero-area image in some effects, right?
129
141
  }
@@ -131,14 +143,15 @@ class Visual extends Base {
131
143
  _applyEffects (): void {
132
144
  for (let i = 0; i < this.effects.length; i++) {
133
145
  const effect = this.effects[i]
134
- if (effect && effect.enabled)
146
+ if (effect && effect.enabled) {
135
147
  // Pass relative time
136
148
  effect.apply(this, this.movie.currentTime - this.startTime)
149
+ }
137
150
  }
138
151
  }
139
152
 
140
153
  /**
141
- * Convienence method for <code>effects.push()</code>
154
+ * Convenience method for <code>effects.push()</code>
142
155
  * @param effect
143
156
  * @return the layer (for chaining)
144
157
  */
@@ -146,6 +159,15 @@ class Visual extends Base {
146
159
  this.effects.push(effect); return this
147
160
  }
148
161
 
162
+ get ready (): boolean {
163
+ // Typescript doesn't support `super.ready` when targeting es5
164
+ const superReady = Object.getOwnPropertyDescriptor(Base.prototype, 'ready').get.call(this)
165
+ return superReady && this.effects.every(effect => effect.ready)
166
+ }
167
+
168
+ /**
169
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
170
+ */
149
171
  getDefaultOptions (): VisualOptions {
150
172
  return {
151
173
  ...Base.prototype.getDefaultOptions(),
@@ -0,0 +1,26 @@
1
+ import { CustomArray, CustomArrayListener } from '../custom-array'
2
+ import { Visual as VisualEffect } from '../effect/index'
3
+ import { Movie } from './movie'
4
+
5
+ class MovieEffectsListener extends CustomArrayListener<VisualEffect> {
6
+ private _movie: Movie
7
+
8
+ constructor (movie: Movie) {
9
+ super()
10
+ this._movie = movie
11
+ }
12
+
13
+ onAdd (effect: VisualEffect) {
14
+ effect.tryAttach(this._movie)
15
+ }
16
+
17
+ onRemove (effect: VisualEffect) {
18
+ effect.tryDetach()
19
+ }
20
+ }
21
+
22
+ export class MovieEffects extends CustomArray<VisualEffect> {
23
+ constructor (target: VisualEffect[], movie: Movie) {
24
+ super(target, new MovieEffectsListener(movie))
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ export * from './movie'
@@ -0,0 +1,26 @@
1
+ import { CustomArray, CustomArrayListener } from '../custom-array'
2
+ import { Base as BaseLayer } from '../layer/index'
3
+ import { Movie } from './movie'
4
+
5
+ class MovieLayersListener extends CustomArrayListener<BaseLayer> {
6
+ private _movie: Movie
7
+
8
+ constructor (movie: Movie) {
9
+ super()
10
+ this._movie = movie
11
+ }
12
+
13
+ onAdd (layer: BaseLayer) {
14
+ layer.tryAttach(this._movie)
15
+ }
16
+
17
+ onRemove (layer: BaseLayer) {
18
+ layer.tryDetach()
19
+ }
20
+ }
21
+
22
+ export class MovieLayers extends CustomArray<BaseLayer> {
23
+ constructor (target: BaseLayer[], movie: Movie) {
24
+ super(target, new MovieLayersListener(movie))
25
+ }
26
+ }