etro 0.9.0 → 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 (68) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/CONTRIBUTING.md +25 -34
  3. package/README.md +9 -17
  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 +1182 -592
  9. package/dist/etro-iife.js +1182 -592
  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 +6 -3
  16. package/dist/layer/video.d.ts +13 -1
  17. package/dist/layer/visual-source.d.ts +18 -3
  18. package/dist/layer/visual.d.ts +11 -7
  19. package/dist/movie/effects.d.ts +6 -0
  20. package/dist/movie/index.d.ts +1 -0
  21. package/dist/movie/layers.d.ts +6 -0
  22. package/dist/movie/movie.d.ts +260 -0
  23. package/dist/object.d.ts +9 -2
  24. package/dist/util.d.ts +4 -10
  25. package/eslint.conf.js +4 -2
  26. package/eslint.test-conf.js +1 -2
  27. package/karma.conf.js +10 -14
  28. package/package.json +23 -22
  29. package/scripts/{gen-effect-samples.html → effect/gen-effect-samples.html} +24 -0
  30. package/scripts/{save-effect-samples.js → effect/save-effect-samples.js} +1 -1
  31. package/src/custom-array.ts +43 -0
  32. package/src/effect/base.ts +23 -22
  33. package/src/effect/gaussian-blur.ts +11 -6
  34. package/src/effect/pixelate.ts +3 -3
  35. package/src/effect/shader.ts +33 -27
  36. package/src/effect/stack.ts +43 -30
  37. package/src/effect/transform.ts +16 -9
  38. package/src/event.ts +111 -21
  39. package/src/layer/audio-source.ts +60 -20
  40. package/src/layer/audio.ts +25 -3
  41. package/src/layer/base.ts +79 -25
  42. package/src/layer/image.ts +26 -2
  43. package/src/layer/text.ts +11 -4
  44. package/src/layer/video.ts +31 -4
  45. package/src/layer/visual-source.ts +70 -8
  46. package/src/layer/visual.ts +57 -35
  47. package/src/movie/effects.ts +26 -0
  48. package/src/movie/index.ts +1 -0
  49. package/src/movie/layers.ts +26 -0
  50. package/src/movie/movie.ts +855 -0
  51. package/src/object.ts +9 -2
  52. package/src/util.ts +68 -89
  53. package/tsconfig.json +3 -1
  54. package/dist/movie.d.ts +0 -201
  55. package/examples/application/readme-screenshot.html +0 -85
  56. package/examples/application/video-player.html +0 -130
  57. package/examples/application/webcam.html +0 -28
  58. package/examples/introduction/audio.html +0 -64
  59. package/examples/introduction/effects.html +0 -79
  60. package/examples/introduction/export.html +0 -83
  61. package/examples/introduction/functions.html +0 -37
  62. package/examples/introduction/hello-world1.html +0 -37
  63. package/examples/introduction/hello-world2.html +0 -32
  64. package/examples/introduction/keyframes.html +0 -79
  65. package/examples/introduction/media.html +0 -63
  66. package/examples/introduction/text.html +0 -31
  67. package/private-todo.txt +0 -70
  68. package/src/movie.ts +0 -742
package/src/layer/base.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import EtroObject from '../object'
2
- import { publish, subscribe } from '../event'
3
- import { watchPublic, applyOptions } from '../util'
2
+ import { applyOptions } from '../util'
4
3
  import { Movie } from '../movie'
5
4
 
6
5
  interface BaseOptions {
@@ -30,6 +29,7 @@ class Base implements EtroObject {
30
29
  private _occurrenceCount: number
31
30
  private _startTime: number
32
31
  private _duration: number
32
+ private _currentTime: number
33
33
  private _movie: Movie
34
34
 
35
35
  /**
@@ -42,67 +42,82 @@ class Base implements EtroObject {
42
42
  * movie's timeline
43
43
  */
44
44
  constructor (options: BaseOptions) {
45
+ if (options.duration === null || options.duration === undefined) {
46
+ throw new Error('Property "duration" is required in BaseOptions')
47
+ }
48
+
49
+ if (options.startTime === null || options.startTime === undefined) {
50
+ throw new Error('Property "startTime" is required in BaseOptions')
51
+ }
52
+
45
53
  // Set startTime and duration properties manually, because they are
46
54
  // readonly. applyOptions ignores readonly properties.
47
55
  this._startTime = options.startTime
48
56
  this._duration = options.duration
49
57
 
50
- // Proxy that will be returned by constructor (for sending 'modified'
51
- // events).
52
- const newThis = watchPublic(this) as Base
53
- // Don't send updates when initializing, so use this instead of newThis
54
58
  applyOptions(options, this)
55
59
 
56
60
  // Whether this layer is currently being rendered
57
61
  this.active = false
58
62
  this.enabled = true
59
63
 
60
- this._occurrenceCount = 0 // no occurances in parent
64
+ this._occurrenceCount = 0 // no occurrences in parent
61
65
  this._movie = null
62
-
63
- // Propogate up to target
64
- subscribe(newThis, 'layer.change', event => {
65
- const typeOfChange = event.type.substring(event.type.lastIndexOf('.') + 1)
66
- const type = `movie.change.layer.${typeOfChange}`
67
- publish(newThis._movie, type, { ...event, target: newThis._movie, type })
68
- })
69
-
70
- return newThis
71
66
  }
72
67
 
68
+ /**
69
+ * Wait until this layer is ready to render
70
+ */
71
+ async whenReady (): Promise<void> {} // eslint-disable-line @typescript-eslint/no-empty-function
72
+
73
73
  /**
74
74
  * Attaches this layer to `movie` if not already attached.
75
75
  * @ignore
76
76
  */
77
77
  tryAttach (movie: Movie): void {
78
- if (this._occurrenceCount === 0)
78
+ if (this._occurrenceCount === 0) {
79
79
  this.attach(movie)
80
+ }
80
81
 
81
82
  this._occurrenceCount++
82
83
  }
83
84
 
85
+ /**
86
+ * Attaches this layer to `movie`
87
+ *
88
+ * Called when the layer is added to a movie's `layers` array.
89
+ *
90
+ * @param movie The movie to attach to
91
+ */
84
92
  attach (movie: Movie): void {
85
93
  this._movie = movie
86
94
  }
87
95
 
88
96
  /**
89
- * Dettaches this layer from its movie if the number of times `tryDetach` has
97
+ * Detaches this layer from its movie if the number of times `tryDetach` has
90
98
  * been called (including this call) equals the number of times `tryAttach`
91
99
  * has been called.
92
100
  *
93
101
  * @ignore
94
102
  */
95
103
  tryDetach (): void {
96
- if (this.movie === null)
104
+ if (this.movie === null) {
97
105
  throw new Error('No movie to detach from')
106
+ }
98
107
 
99
108
  this._occurrenceCount--
100
109
  // If this layer occurs in another place in a `layers` array, do not unset
101
110
  // _movie. (For calling `unshift` on the `layers` proxy)
102
- if (this._occurrenceCount === 0)
111
+ if (this._occurrenceCount === 0) {
103
112
  this.detach()
113
+ }
104
114
  }
105
115
 
116
+ /**
117
+ * Detaches this layer from its movie
118
+ *
119
+ * Called when the layer is removed from a movie's `layers` array.
120
+ */
106
121
  detach (): void {
107
122
  this._movie = null
108
123
  }
@@ -112,15 +127,43 @@ class Base implements EtroObject {
112
127
  */
113
128
  start (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
114
129
 
130
+ /**
131
+ * Update {@link currentTime} when seeking
132
+ *
133
+ * This method is called when the movie seeks to a new time at the request of
134
+ * the user. {@link progress} is called when the movie's `currentTime` is
135
+ * updated due to playback.
136
+ *
137
+ * @param time - The new time in the layer
138
+ */
139
+ seek (time: number): void {
140
+ this._currentTime = time
141
+ }
142
+
143
+ /**
144
+ * Update {@link currentTime} due to playback
145
+ *
146
+ * This method is called when the movie's `currentTime` is updated due to
147
+ * playback. {@link seek} is called when the movie seeks to a new time at the
148
+ * request of the user.
149
+ *
150
+ * @param time - The new time in the layer
151
+ */
152
+ progress (time: number): void {
153
+ this._currentTime = time
154
+ }
155
+
115
156
  /**
116
157
  * Called when the movie renders and the layer is active
117
158
  */
118
159
  render (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
119
160
 
120
161
  /**
121
- * Called when the layer is deactivated
162
+ * Called when the layer is deactivated
122
163
  */
123
- stop (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
164
+ stop (): void {
165
+ this._currentTime = undefined
166
+ }
124
167
 
125
168
  // TODO: is this needed?
126
169
  get parent (): Movie {
@@ -128,6 +171,7 @@ class Base implements EtroObject {
128
171
  }
129
172
 
130
173
  /**
174
+ * The time in the movie at which this layer starts (in seconds)
131
175
  */
132
176
  get startTime (): number {
133
177
  return this._startTime
@@ -138,14 +182,14 @@ class Base implements EtroObject {
138
182
  }
139
183
 
140
184
  /**
141
- * The current time of the movie relative to this layer
185
+ * The current time of the movie relative to this layer (in seconds)
142
186
  */
143
187
  get currentTime (): number {
144
- return this._movie ? this._movie.currentTime - this.startTime
145
- : undefined
188
+ return this._currentTime
146
189
  }
147
190
 
148
191
  /**
192
+ * The duration of this layer (in seconds)
149
193
  */
150
194
  get duration (): number {
151
195
  return this._duration
@@ -155,10 +199,20 @@ class Base implements EtroObject {
155
199
  this._duration = val
156
200
  }
157
201
 
202
+ /**
203
+ * `true` if this layer is ready to be rendered, `false` otherwise
204
+ */
205
+ get ready (): boolean {
206
+ return true
207
+ }
208
+
158
209
  get movie (): Movie {
159
210
  return this._movie
160
211
  }
161
212
 
213
+ /**
214
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
215
+ */
162
216
  getDefaultOptions (): BaseOptions {
163
217
  return {
164
218
  startTime: undefined, // required
@@ -1,8 +1,32 @@
1
1
  import { Visual } from './visual'
2
2
  import { VisualSourceMixin, VisualSourceOptions } from './visual-source'
3
3
 
4
- type ImageOptions = VisualSourceOptions
4
+ interface ImageOptions extends Omit<VisualSourceOptions, 'source'> {
5
+ /**
6
+ * The raw html `<img>` element
7
+ */
8
+ source: string | HTMLImageElement
9
+ }
5
10
 
6
- class Image extends VisualSourceMixin(Visual) {}
11
+ /**
12
+ * Layer for an HTML image element
13
+ * @extends VisualSource
14
+ */
15
+ class Image extends VisualSourceMixin(Visual) {
16
+ /**
17
+ * The raw html `<img>` element
18
+ */
19
+ source: HTMLImageElement
20
+
21
+ constructor (options: ImageOptions) {
22
+ if (typeof (options.source) === 'string') {
23
+ const img = document.createElement('img')
24
+ img.src = options.source
25
+ options.source = img
26
+ }
27
+
28
+ super(options as VisualSourceOptions)
29
+ }
30
+ }
7
31
 
8
32
  export { Image, ImageOptions }
package/src/layer/text.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { Dynamic, val, applyOptions } from '../util'
1
+ import { Dynamic, val, applyOptions, Color, parseColor } from '../util'
2
2
  import { Visual, VisualOptions } from './visual'
3
3
 
4
4
  interface TextOptions extends VisualOptions {
5
5
  text: Dynamic<string>
6
6
  font?: Dynamic<string>
7
- color?: Dynamic<string>
7
+ color?: Dynamic<Color>
8
8
  /** The text's horizontal offset from the layer */
9
9
  textX?: Dynamic<number>
10
10
  /** The text's vertical offset from the layer */
@@ -29,7 +29,7 @@ interface TextOptions extends VisualOptions {
29
29
  class Text extends Visual {
30
30
  text: Dynamic<string>
31
31
  font: Dynamic<string>
32
- color: Dynamic<string>
32
+ color: Dynamic<Color>
33
33
  /** The text's horizontal offset from the layer */
34
34
  textX: Dynamic<number>
35
35
  /** The text's vertical offset from the layer */
@@ -61,6 +61,10 @@ class Text extends Visual {
61
61
  // TODO: is textX necessary? it seems inconsistent, because you can't define
62
62
  // width/height directly for a text layer
63
63
  constructor (options: TextOptions) {
64
+ if (!options.text) {
65
+ throw new Error('Property "text" is required in TextOptions')
66
+ }
67
+
64
68
  // Default to no (transparent) background
65
69
  super({ background: null, ...options })
66
70
  applyOptions(options, this)
@@ -118,13 +122,16 @@ class Text extends Visual {
118
122
  return metrics;
119
123
  } */
120
124
 
125
+ /**
126
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
127
+ */
121
128
  getDefaultOptions (): TextOptions {
122
129
  return {
123
130
  ...Visual.prototype.getDefaultOptions(),
124
131
  background: null,
125
132
  text: undefined, // required
126
133
  font: '10px sans-serif',
127
- color: '#fff',
134
+ color: parseColor('#fff'),
128
135
  textX: 0,
129
136
  textY: 0,
130
137
  maxWidth: null,
@@ -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 }
@@ -1,11 +1,27 @@
1
1
  import { Dynamic, val, applyOptions } from '../util'
2
- import { Base, BaseOptions } from './base'
3
2
  import { Visual, VisualOptions } from './visual'
4
3
 
5
4
  type Constructor<T> = new (...args: unknown[]) => T
6
5
 
7
- interface VisualSource extends Base {
6
+ interface VisualSource extends Visual {
8
7
  readonly source: HTMLImageElement | HTMLVideoElement
8
+
9
+ /** What part of {@link source} to render */
10
+ sourceX: Dynamic<number>
11
+ /** What part of {@link source} to render */
12
+ sourceY: Dynamic<number>
13
+ /** What part of {@link source} to render, or undefined for the entire width */
14
+ sourceWidth: Dynamic<number>
15
+ /** What part of {@link source} to render, or undefined for the entire height */
16
+ sourceHeight: Dynamic<number>
17
+ /** Where to render {@link source} onto the layer */
18
+ destX: Dynamic<number>
19
+ /** Where to render {@link source} onto the layer */
20
+ destY: Dynamic<number>
21
+ /** Where to render {@link source} onto the layer, or undefined to fill the layer's width */
22
+ destWidth: Dynamic<number>
23
+ /** Where to render {@link source} onto the layer, or undefined to fill the layer's height */
24
+ destHeight: Dynamic<number>
9
25
  }
10
26
 
11
27
  interface VisualSourceOptions extends VisualOptions {
@@ -32,7 +48,7 @@ interface VisualSourceOptions extends VisualOptions {
32
48
  * A layer that gets its image data from an HTML image or video element
33
49
  * @mixin VisualSourceMixin
34
50
  */
35
- function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Constructor<Visual>): Constructor<VisualSource> {
51
+ function VisualSourceMixin<OptionsSuperclass extends VisualOptions> (superclass: Constructor<Visual>): Constructor<VisualSource> {
36
52
  type MixedVisualSourceOptions = OptionsSuperclass & VisualSourceOptions
37
53
 
38
54
  class MixedVisualSource extends superclass {
@@ -59,10 +75,40 @@ function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: C
59
75
  destHeight: Dynamic<number>
60
76
 
61
77
  constructor (options: MixedVisualSourceOptions) {
78
+ if (!options.source) {
79
+ throw new Error('Property "source" is required in options')
80
+ }
81
+
62
82
  super(options)
63
83
  applyOptions(options, this)
64
84
  }
65
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
+
66
112
  doRender () {
67
113
  // Clear/fill background
68
114
  super.doRender()
@@ -70,7 +116,7 @@ function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: C
70
116
  /*
71
117
  * Source dimensions crop the image. Dest dimensions set the size that
72
118
  * the image will be rendered at *on the layer*. Note that this is
73
- * different than the layer dimensions (`this.width` and `this.height`).
119
+ * different from the layer dimensions (`this.width` and `this.height`).
74
120
  * The main reason this distinction exists is so that an image layer can
75
121
  * be rotated without being cropped (see iss #46).
76
122
  */
@@ -84,6 +130,18 @@ function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: C
84
130
  )
85
131
  }
86
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
+ */
87
145
  getDefaultOptions (): MixedVisualSourceOptions {
88
146
  return {
89
147
  ...superclass.prototype.getDefaultOptions(),
@@ -125,22 +183,26 @@ function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: C
125
183
  // instead. (TODO: fact check)
126
184
  /* eslint-disable eqeqeq */
127
185
  return destWidth != undefined
128
- ? destWidth : val(this, 'sourceWidth', this.currentTime)
186
+ ? destWidth
187
+ : val(this, 'sourceWidth', this.currentTime)
129
188
  },
130
189
  destHeight: function (destHeight) {
131
190
  /* eslint-disable eqeqeq */
132
191
  return destHeight != undefined
133
- ? destHeight : val(this, 'sourceHeight', this.currentTime)
192
+ ? destHeight
193
+ : val(this, 'sourceHeight', this.currentTime)
134
194
  },
135
195
  width: function (width) {
136
196
  /* eslint-disable eqeqeq */
137
197
  return width != undefined
138
- ? width : val(this, 'destWidth', this.currentTime)
198
+ ? width
199
+ : val(this, 'destWidth', this.currentTime)
139
200
  },
140
201
  height: function (height) {
141
202
  /* eslint-disable eqeqeq */
142
203
  return height != undefined
143
- ? height : val(this, 'destHeight', this.currentTime)
204
+ ? height
205
+ : val(this, 'destHeight', this.currentTime)
144
206
  }
145
207
  }
146
208
 
@@ -1,15 +1,43 @@
1
- import { Dynamic, val, applyOptions } from '../util'
1
+ import { CustomArray, CustomArrayListener } from '../custom-array'
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>
8
36
  width?: Dynamic<number>
9
37
  height?: Dynamic<number>
10
- background?: Dynamic<string>
38
+ background?: Dynamic<Color>
11
39
  border?: Dynamic<{
12
- color: string
40
+ color: Color
13
41
  thickness?: number
14
42
  }>
15
43
 
@@ -22,9 +50,9 @@ class Visual extends Base {
22
50
  y: Dynamic<number>
23
51
  width: Dynamic<number>
24
52
  height: Dynamic<number>
25
- background: Dynamic<string>
53
+ background: Dynamic<Color>
26
54
  border: Dynamic<{
27
- color: string
55
+ color: Color
28
56
  thickness: number
29
57
  }>
30
58
 
@@ -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(),
@@ -169,13 +191,13 @@ class Visual extends Base {
169
191
  height: null,
170
192
  /**
171
193
  * @name module:layer.Visual#background
172
- * @desc The CSS color code for the background, or <code>null</code> for
194
+ * @desc The color code for the background, or <code>null</code> for
173
195
  * transparency
174
196
  */
175
197
  background: null,
176
198
  /**
177
199
  * @name module:layer.Visual#border
178
- * @desc The CSS border style, or <code>null</code> for no border
200
+ * @desc The border style, or <code>null</code> for no border
179
201
  */
180
202
  border: null,
181
203
  /**
@@ -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'