etro 0.8.0 → 0.8.3

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 (97) hide show
  1. package/.github/workflows/nodejs.yml +3 -1
  2. package/.github/workflows/shipjs-trigger.yml +29 -0
  3. package/CHANGELOG.md +36 -13
  4. package/CODE_OF_CONDUCT.md +5 -5
  5. package/CONTRIBUTING.md +22 -72
  6. package/README.md +2 -2
  7. package/dist/effect/base.d.ts +14 -1
  8. package/dist/etro-cjs.js +189 -230
  9. package/dist/etro-iife.js +189 -230
  10. package/dist/layer/base.d.ts +13 -0
  11. package/eslint.conf.js +2 -1
  12. package/eslint.test-conf.js +1 -0
  13. package/examples/application/readme-screenshot.html +4 -8
  14. package/examples/application/video-player.html +3 -4
  15. package/examples/introduction/effects.html +23 -4
  16. package/karma.conf.js +4 -2
  17. package/package.json +8 -4
  18. package/scripts/gen-effect-samples.html +0 -3
  19. package/ship.config.js +80 -0
  20. package/src/effect/base.ts +29 -10
  21. package/src/effect/gaussian-blur.ts +10 -10
  22. package/src/effect/pixelate.ts +1 -2
  23. package/src/effect/shader.ts +18 -22
  24. package/src/effect/stack.ts +8 -4
  25. package/src/effect/transform.ts +13 -14
  26. package/src/event.ts +8 -14
  27. package/src/layer/audio-source.ts +16 -14
  28. package/src/layer/audio.ts +1 -2
  29. package/src/layer/base.ts +26 -7
  30. package/src/layer/visual.ts +11 -6
  31. package/src/movie.ts +70 -64
  32. package/src/util.ts +50 -57
  33. package/docs/effect.js.html +0 -1215
  34. package/docs/event.js.html +0 -145
  35. package/docs/index.html +0 -81
  36. package/docs/index.js.html +0 -92
  37. package/docs/layer.js.html +0 -888
  38. package/docs/module-effect-GaussianBlurComponent.html +0 -345
  39. package/docs/module-effect.Brightness.html +0 -339
  40. package/docs/module-effect.Channels.html +0 -319
  41. package/docs/module-effect.ChromaKey.html +0 -611
  42. package/docs/module-effect.Contrast.html +0 -339
  43. package/docs/module-effect.EllipticalMask.html +0 -200
  44. package/docs/module-effect.GaussianBlur.html +0 -202
  45. package/docs/module-effect.GaussianBlurHorizontal.html +0 -242
  46. package/docs/module-effect.GaussianBlurVertical.html +0 -242
  47. package/docs/module-effect.Pixelate.html +0 -330
  48. package/docs/module-effect.Shader.html +0 -1227
  49. package/docs/module-effect.Stack.html +0 -406
  50. package/docs/module-effect.Transform.Matrix.html +0 -193
  51. package/docs/module-effect.Transform.html +0 -1174
  52. package/docs/module-effect.html +0 -148
  53. package/docs/module-event.html +0 -473
  54. package/docs/module-index.html +0 -186
  55. package/docs/module-layer-Media.html +0 -1116
  56. package/docs/module-layer-MediaMixin.html +0 -164
  57. package/docs/module-layer.Audio.html +0 -1188
  58. package/docs/module-layer.Base.html +0 -629
  59. package/docs/module-layer.Image.html +0 -1421
  60. package/docs/module-layer.Text.html +0 -1731
  61. package/docs/module-layer.Video.html +0 -1938
  62. package/docs/module-layer.Visual.html +0 -1698
  63. package/docs/module-layer.html +0 -137
  64. package/docs/module-movie.html +0 -3118
  65. package/docs/module-util.Color.html +0 -702
  66. package/docs/module-util.Font.html +0 -395
  67. package/docs/module-util.html +0 -845
  68. package/docs/movie.js.html +0 -689
  69. package/docs/scripts/collapse.js +0 -20
  70. package/docs/scripts/linenumber.js +0 -25
  71. package/docs/scripts/nav.js +0 -12
  72. package/docs/scripts/polyfill.js +0 -4
  73. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
  74. package/docs/scripts/prettify/lang-css.js +0 -2
  75. package/docs/scripts/prettify/prettify.js +0 -28
  76. package/docs/scripts/search.js +0 -83
  77. package/docs/styles/jsdoc.css +0 -671
  78. package/docs/styles/prettify.css +0 -79
  79. package/docs/util.js.html +0 -503
  80. package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
  81. package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
  82. package/spec/assets/effect/grayscale.png +0 -0
  83. package/spec/assets/effect/original.png +0 -0
  84. package/spec/assets/effect/pixelate.png +0 -0
  85. package/spec/assets/effect/transform/multiply.png +0 -0
  86. package/spec/assets/effect/transform/rotate.png +0 -0
  87. package/spec/assets/effect/transform/scale-fraction.png +0 -0
  88. package/spec/assets/effect/transform/scale.png +0 -0
  89. package/spec/assets/effect/transform/translate-fraction.png +0 -0
  90. package/spec/assets/effect/transform/translate.png +0 -0
  91. package/spec/assets/layer/audio.wav +0 -0
  92. package/spec/assets/layer/image.jpg +0 -0
  93. package/spec/effect.spec.js +0 -421
  94. package/spec/event.spec.js +0 -39
  95. package/spec/layer.spec.js +0 -307
  96. package/spec/movie.spec.js +0 -346
  97. package/spec/util.spec.js +0 -294
package/src/event.ts CHANGED
@@ -21,15 +21,13 @@ class TypeId {
21
21
  }
22
22
 
23
23
  contains (other) {
24
- if (other._parts.length > this._parts.length) {
24
+ if (other._parts.length > this._parts.length)
25
25
  return false
26
- }
27
26
 
28
- for (let i = 0; i < other._parts.length; i++) {
29
- if (other._parts[i] !== this._parts[i]) {
27
+ for (let i = 0; i < other._parts.length; i++)
28
+ if (other._parts[i] !== this._parts[i])
30
29
  return false
31
- }
32
- }
30
+
33
31
  return true
34
32
  }
35
33
 
@@ -47,9 +45,8 @@ class TypeId {
47
45
  * @param listener
48
46
  */
49
47
  export function subscribe (target: EtroObject, type: string, listener: <T extends Event>(T) => void): void {
50
- if (!listeners.has(target)) {
48
+ if (!listeners.has(target))
51
49
  listeners.set(target, [])
52
- }
53
50
 
54
51
  listeners.get(target).push(
55
52
  { type: new TypeId(type), listener }
@@ -67,9 +64,8 @@ export function subscribe (target: EtroObject, type: string, listener: <T extend
67
64
  export function unsubscribe (target: EtroObject, listener: <T extends Event>(T) => void): void {
68
65
  // Make sure `listener` has been added with `subscribe`.
69
66
  if (!listeners.has(target) ||
70
- !listeners.get(target).map(pair => pair.listener).includes(listener)) {
67
+ !listeners.get(target).map(pair => pair.listener).includes(listener))
71
68
  throw new Error('No matching event listener to remove')
72
- }
73
69
 
74
70
  const removed = listeners.get(target)
75
71
  .filter(pair => pair.listener !== listener)
@@ -90,18 +86,16 @@ export function publish (target: EtroObject, type: string, event: Record<string,
90
86
 
91
87
  const t = new TypeId(type)
92
88
 
93
- if (!listeners.has(target)) {
89
+ if (!listeners.has(target))
94
90
  // No event fired
95
91
  return null
96
- }
97
92
 
98
93
  // Call event listeners for this event.
99
94
  const listenersForType = []
100
95
  for (let i = 0; i < listeners.get(target).length; i++) {
101
96
  const item = listeners.get(target)[i]
102
- if (t.contains(item.type)) {
97
+ if (t.contains(item.type))
103
98
  listenersForType.push(item.listener)
104
- }
105
99
  }
106
100
 
107
101
  for (let i = 0; i < listenersForType.length; i++) {
@@ -70,22 +70,22 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
70
70
 
71
71
  const load = () => {
72
72
  // TODO: && ?
73
- if ((options.duration || (this.source.duration - this.sourceStartTime)) < 0) {
73
+ if ((options.duration || (this.source.duration - this.sourceStartTime)) < 0)
74
74
  throw new Error('Invalid options.duration or options.sourceStartTime')
75
- }
75
+
76
76
  this._unstretchedDuration = options.duration || (this.source.duration - this.sourceStartTime)
77
77
  this.duration = this._unstretchedDuration / (this.playbackRate)
78
78
  // onload will use `this`, and can't bind itself because it's before
79
79
  // super()
80
80
  onload && onload.bind(this)(this.source, options)
81
81
  }
82
- if (this.source.readyState >= 2) {
82
+ if (this.source.readyState >= 2)
83
83
  // this frame's data is available now
84
84
  load()
85
- } else {
85
+ else
86
86
  // when this frame's data is available
87
87
  this.source.addEventListener('loadedmetadata', load)
88
- }
88
+
89
89
  this.source.addEventListener('durationchange', () => {
90
90
  this.duration = options.duration || (this.source.duration - this.sourceStartTime)
91
91
  })
@@ -95,11 +95,10 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
95
95
  super.attach(movie)
96
96
 
97
97
  subscribe(movie, 'movie.seek', () => {
98
- const time = movie.currentTime
99
- if (time < this.startTime || time >= this.startTime + this.duration) {
98
+ if (this.currentTime < 0 || this.currentTime >= this.duration)
100
99
  return
101
- }
102
- this.source.currentTime = time - this.startTime
100
+
101
+ this.source.currentTime = this.currentTime + this.sourceStartTime
103
102
  })
104
103
 
105
104
  // TODO: on unattach?
@@ -113,7 +112,7 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
113
112
  })
114
113
 
115
114
  // connect to audiocontext
116
- this._audioNode = movie.actx.createMediaElementSource(this.source)
115
+ this._audioNode = this.audioNode || movie.actx.createMediaElementSource(this.source)
117
116
 
118
117
  // Spy on connect and disconnect to remember if it connected to
119
118
  // actx.destination (for Movie#record).
@@ -125,9 +124,9 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
125
124
  const oldDisconnect = this._audioNode.disconnect.bind(this.audioNode)
126
125
  this._audioNode.disconnect = <T extends IAudioDestinationNode<AudioContext>>(destination?: T | number, output?: number, input?: number): AudioNode => {
127
126
  if (this._connectedToDestination &&
128
- destination === movie.actx.destination) {
127
+ destination === movie.actx.destination)
129
128
  this._connectedToDestination = false
130
- }
129
+
131
130
  return oldDisconnect(destination, output, input)
132
131
  }
133
132
 
@@ -135,6 +134,10 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
135
134
  this.audioNode.connect(movie.actx.destination)
136
135
  }
137
136
 
137
+ detach () {
138
+ this.audioNode.disconnect(this.movie.actx.destination)
139
+ }
140
+
138
141
  start () {
139
142
  this.source.currentTime = this.currentTime + this.sourceStartTime
140
143
  this.source.play()
@@ -166,9 +169,8 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
166
169
 
167
170
  set playbackRate (value) {
168
171
  this._playbackRate = value
169
- if (this._unstretchedDuration !== undefined) {
172
+ if (this._unstretchedDuration !== undefined)
170
173
  this.duration = this._unstretchedDuration / value
171
- }
172
174
  }
173
175
 
174
176
  get startTime () {
@@ -14,9 +14,8 @@ class Audio extends AudioSourceMixin<BaseOptions>(Base) {
14
14
  */
15
15
  constructor (options: AudioOptions) {
16
16
  super(options)
17
- if (this.duration === undefined) {
17
+ if (this.duration === undefined)
18
18
  this.duration = (this).source.duration - this.sourceStartTime
19
- }
20
19
  }
21
20
 
22
21
  getDefaultOptions (): AudioOptions {
package/src/layer/base.ts CHANGED
@@ -70,22 +70,41 @@ class Base implements EtroObject {
70
70
  return newThis
71
71
  }
72
72
 
73
- attach (movie: Movie): void {
73
+ /**
74
+ * Attaches this layer to `movie` if not already attached.
75
+ * @ignore
76
+ */
77
+ tryAttach (movie: Movie): void {
78
+ if (this._occurrenceCount === 0)
79
+ this.attach(movie)
80
+
74
81
  this._occurrenceCount++
82
+ }
83
+
84
+ attach (movie: Movie): void {
75
85
  this._movie = movie
76
86
  }
77
87
 
78
- detach (): void {
79
- if (this.movie === null) {
88
+ /**
89
+ * Dettaches this layer from its movie if the number of times `tryDetach` has
90
+ * been called (including this call) equals the number of times `tryAttach`
91
+ * has been called.
92
+ *
93
+ * @ignore
94
+ */
95
+ tryDetach (): void {
96
+ if (this.movie === null)
80
97
  throw new Error('No movie to detach from')
81
- }
82
98
 
83
99
  this._occurrenceCount--
84
100
  // If this layer occurs in another place in a `layers` array, do not unset
85
101
  // _movie. (For calling `unshift` on the `layers` proxy)
86
- if (this._occurrenceCount === 0) {
87
- this._movie = null
88
- }
102
+ if (this._occurrenceCount === 0)
103
+ this.detach()
104
+ }
105
+
106
+ detach (): void {
107
+ this._movie = null
89
108
  }
90
109
 
91
110
  /**
@@ -68,9 +68,9 @@ class Visual extends Base {
68
68
  set: (target, property, value) => {
69
69
  if (!isNaN(Number(property))) {
70
70
  // The property is a number (index)
71
- if (target[property]) {
71
+ if (target[property])
72
72
  target[property].detach()
73
- }
73
+
74
74
  value.attach(this)
75
75
  }
76
76
  target[property] = value
@@ -83,6 +83,12 @@ class Visual extends Base {
83
83
  * Render visual output
84
84
  */
85
85
  render (): void {
86
+ // Prevent empty canvas errors if the width or height is 0
87
+ const width = val(this, 'width', this.currentTime)
88
+ const height = val(this, 'height', this.currentTime)
89
+ if (width === 0 || height === 0)
90
+ return
91
+
86
92
  this.beginRender()
87
93
  this.doRender()
88
94
  this.endRender()
@@ -116,19 +122,18 @@ class Visual extends Base {
116
122
  endRender (): void {
117
123
  const w = val(this, 'width', this.currentTime) || val(this.movie, 'width', this.movie.currentTime)
118
124
  const h = val(this, 'height', this.currentTime) || val(this.movie, 'height', this.movie.currentTime)
119
- if (w * h > 0) {
125
+ if (w * h > 0)
120
126
  this._applyEffects()
121
- }
127
+
122
128
  // else InvalidStateError for drawing zero-area image in some effects, right?
123
129
  }
124
130
 
125
131
  _applyEffects (): void {
126
132
  for (let i = 0; i < this.effects.length; i++) {
127
133
  const effect = this.effects[i]
128
- if (effect.enabled) {
134
+ if (effect && effect.enabled)
129
135
  // Pass relative time
130
136
  effect.apply(this, this.movie.currentTime - this.startTime)
131
- }
132
137
  }
133
138
  }
134
139
 
package/src/movie.ts CHANGED
@@ -95,7 +95,7 @@ export class Movie {
95
95
  // Refresh screen when effect is removed, if the movie isn't playing
96
96
  // already.
97
97
  const value = target[property]
98
- value.detach()
98
+ value.tryDetach()
99
99
  delete target[property]
100
100
  publish(that, 'movie.change.effect.remove', { effect: value })
101
101
  return true
@@ -107,10 +107,10 @@ export class Movie {
107
107
  publish(that, 'movie.change.effect.remove', {
108
108
  effect: target[property]
109
109
  })
110
- target[property].detach()
110
+ target[property].tryDetach()
111
111
  }
112
112
  // Attach effect to movie
113
- value.attach(that)
113
+ value.tryAttach(that)
114
114
  target[property] = value
115
115
  // Refresh screen when effect is set, if the movie isn't playing
116
116
  // already.
@@ -118,6 +118,7 @@ export class Movie {
118
118
  } else {
119
119
  target[property] = value
120
120
  }
121
+
121
122
  return true
122
123
  }
123
124
  })
@@ -127,12 +128,12 @@ export class Movie {
127
128
  deleteProperty (target, property): boolean {
128
129
  const oldDuration = this.duration
129
130
  const value = target[property]
130
- value.detach(that)
131
+ value.tryDetach(that)
131
132
  delete target[property]
132
133
  const current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration
133
- if (current) {
134
+ if (current)
134
135
  publish(that, 'movie.change.layer.remove', { layer: value })
135
- }
136
+
136
137
  publish(that, 'movie.change.duration', { oldDuration })
137
138
  return true
138
139
  },
@@ -144,20 +145,21 @@ export class Movie {
144
145
  publish(that, 'movie.change.layer.remove', {
145
146
  layer: target[property]
146
147
  })
147
- target[property].detach()
148
+ target[property].tryDetach()
148
149
  }
149
150
  // Attach layer to movie
150
- value.attach(that)
151
+ value.tryAttach(that)
151
152
  target[property] = value
152
153
  // Refresh screen when a relevant layer is added or removed
153
154
  const current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration
154
- if (current) {
155
+ if (current)
155
156
  publish(that, 'movie.change.layer.add', { layer: value })
156
- }
157
+
157
158
  publish(that, 'movie.change.duration', { oldDuration })
158
159
  } else {
159
160
  target[property] = value
160
161
  }
162
+
161
163
  return true
162
164
  }
163
165
  })
@@ -179,15 +181,13 @@ export class Movie {
179
181
  // newThis._updateInterval = 0.1; // time in seconds between each "timeupdate" event
180
182
  // newThis._lastUpdate = -1;
181
183
 
182
- if (newThis.autoRefresh) {
184
+ if (newThis.autoRefresh)
183
185
  newThis.refresh() // render single frame on creation
184
- }
185
186
 
186
187
  // Subscribe to own event "change" (child events propogate up)
187
188
  subscribe(newThis, 'movie.change', () => {
188
- if (newThis.autoRefresh && !newThis.rendering) {
189
+ if (newThis.autoRefresh && !newThis.rendering)
189
190
  newThis.refresh()
190
- }
191
191
  })
192
192
 
193
193
  // Subscribe to own event "ended"
@@ -207,18 +207,17 @@ export class Movie {
207
207
  */
208
208
  play (): Promise<void> {
209
209
  return new Promise(resolve => {
210
- if (!this.paused) {
210
+ if (!this.paused)
211
211
  throw new Error('Already playing')
212
- }
213
212
 
214
213
  this._paused = this._ended = false
215
214
  this._lastPlayed = performance.now()
216
215
  this._lastPlayedOffset = this.currentTime
217
216
 
218
- if (!this.renderingFrame) {
217
+ if (!this.renderingFrame)
219
218
  // Not rendering (and not playing), so play.
220
219
  this._render(true, undefined, resolve)
221
- }
220
+
222
221
  // Stop rendering frame if currently doing so, because playing has higher
223
222
  // priority. This will effect the next _render call.
224
223
  this._renderingFrame = false
@@ -250,13 +249,16 @@ export class Movie {
250
249
  audio?: boolean,
251
250
  mediaRecorderOptions?: Record<string, unknown>
252
251
  }): Promise<Blob> {
253
- if (options.video === false && options.audio === false) {
252
+ if (options.video === false && options.audio === false)
254
253
  throw new Error('Both video and audio cannot be disabled')
255
- }
256
254
 
257
- if (!this.paused) {
255
+ if (!this.paused)
258
256
  throw new Error('Cannot record movie while already playing or recording')
259
- }
257
+
258
+ const mimeType = options.type || 'video/webm'
259
+ if (MediaRecorder && MediaRecorder.isTypeSupported && !MediaRecorder.isTypeSupported(mimeType))
260
+ throw new Error('Please pass a valid MIME type for the exported video')
261
+
260
262
  return new Promise((resolve, reject) => {
261
263
  const canvasCache = this.canvas
262
264
  // Record on a temporary canvas context
@@ -287,12 +289,15 @@ export class Movie {
287
289
  )
288
290
  }
289
291
  const stream = new MediaStream(tracks)
290
- const mediaRecorder = new MediaRecorder(stream, options.mediaRecorderOptions)
292
+ const mediaRecorderOptions = {
293
+ ...(options.mediaRecorderOptions || {}),
294
+ mimeType
295
+ }
296
+ const mediaRecorder = new MediaRecorder(stream, mediaRecorderOptions)
291
297
  mediaRecorder.ondataavailable = event => {
292
298
  // if (this._paused) reject(new Error("Recording was interrupted"));
293
- if (event.data.size > 0) {
299
+ if (event.data.size > 0)
294
300
  recordedChunks.push(event.data)
295
- }
296
301
  }
297
302
  // TODO: publish to movie, not layers
298
303
  mediaRecorder.onstop = () => {
@@ -306,7 +311,7 @@ export class Movie {
306
311
  // Construct the exported video out of all the frame blobs.
307
312
  resolve(
308
313
  new Blob(recordedChunks, {
309
- type: options.type || 'video/webm'
314
+ type: mimeType
310
315
  })
311
316
  )
312
317
  }
@@ -327,11 +332,13 @@ export class Movie {
327
332
  pause (): Movie {
328
333
  this._paused = true
329
334
  // Deactivate all layers
330
- for (let i = 0; i < this.layers.length; i++) {
331
- const layer = this.layers[i]
332
- layer.stop()
333
- layer.active = false
334
- }
335
+ for (let i = 0; i < this.layers.length; i++)
336
+ if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
337
+ const layer = this.layers[i]
338
+ layer.stop()
339
+ layer.active = false
340
+ }
341
+
335
342
  publish(this, 'movie.pause', {})
336
343
  return this
337
344
  }
@@ -357,18 +364,17 @@ export class Movie {
357
364
  if (!this.rendering) {
358
365
  // (!this.paused || this._renderingFrame) is true so it's playing or it's
359
366
  // rendering a single frame.
360
- if (done) {
367
+ if (done)
361
368
  done()
362
- }
369
+
363
370
  return
364
371
  }
365
372
 
366
373
  this._updateCurrentTime(timestamp)
367
374
  const recordingEnd = this.recording ? this._recordEndTime : this.duration
368
375
  const recordingEnded = this.currentTime > recordingEnd
369
- if (recordingEnded) {
376
+ if (recordingEnded)
370
377
  publish(this, 'movie.recordended', { movie: this })
371
- }
372
378
 
373
379
  // Bad for performance? (remember, it's calling Array.reduce)
374
380
  const end = this.duration
@@ -384,24 +390,25 @@ export class Movie {
384
390
  if (!this.repeat || this.recording) {
385
391
  this._ended = true
386
392
  // Deactivate all layers
387
- for (let i = 0; i < this.layers.length; i++) {
388
- const layer = this.layers[i]
389
- // A layer that has been deleted before layers.length has been updated
390
- // (see the layers proxy in the constructor).
391
- if (!layer) {
392
- continue
393
+ for (let i = 0; i < this.layers.length; i++)
394
+ if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
395
+ const layer = this.layers[i]
396
+ // A layer that has been deleted before layers.length has been updated
397
+ // (see the layers proxy in the constructor).
398
+ if (!layer)
399
+ continue
400
+
401
+ layer.stop()
402
+ layer.active = false
393
403
  }
394
- layer.stop()
395
- layer.active = false
396
- }
397
404
  }
398
405
  }
399
406
 
400
407
  // Stop playback or recording if done
401
408
  if (recordingEnded || (ended && !this.repeat)) {
402
- if (done) {
409
+ if (done)
403
410
  done()
404
- }
411
+
405
412
  return
406
413
  }
407
414
 
@@ -410,9 +417,8 @@ export class Movie {
410
417
  const frameFullyLoaded = this._renderLayers()
411
418
  this._applyEffects()
412
419
 
413
- if (frameFullyLoaded) {
420
+ if (frameFullyLoaded)
414
421
  publish(this, 'movie.loadeddata', { movie: this })
415
- }
416
422
 
417
423
  // If didn't load in this instant, repeatedly frame-render until frame is
418
424
  // loaded.
@@ -420,9 +426,9 @@ export class Movie {
420
426
  // stop render loop.
421
427
  if (!repeat || (this._renderingFrame && frameFullyLoaded)) {
422
428
  this._renderingFrame = false
423
- if (done) {
429
+ if (done)
424
430
  done()
425
- }
431
+
426
432
  return
427
433
  }
428
434
 
@@ -461,12 +467,14 @@ export class Movie {
461
467
  private _renderLayers () {
462
468
  let frameFullyLoaded = true
463
469
  for (let i = 0; i < this.layers.length; i++) {
470
+ if (!Object.prototype.hasOwnProperty.call(this.layers, i)) continue
471
+
464
472
  const layer = this.layers[i]
465
473
  // A layer that has been deleted before layers.length has been updated
466
474
  // (see the layers proxy in the constructor).
467
- if (!layer) {
475
+ if (!layer)
468
476
  continue
469
- }
477
+
470
478
  const reltime = this.currentTime - layer.startTime
471
479
  // Cancel operation if layer disabled or outside layer time interval
472
480
  if (!val(layer, 'enabled', reltime) ||
@@ -489,9 +497,9 @@ export class Movie {
489
497
  }
490
498
 
491
499
  // if the layer has an input file
492
- if ('source' in layer) {
500
+ if ('source' in layer)
493
501
  frameFullyLoaded = frameFullyLoaded && (layer as unknown as AudioSource).source.readyState >= 2
494
- }
502
+
495
503
  layer.render()
496
504
 
497
505
  // if the layer has visual component
@@ -499,11 +507,10 @@ export class Movie {
499
507
  const canvas = (layer as Visual).canvas
500
508
  // layer.canvas.width and layer.canvas.height should already be interpolated
501
509
  // if the layer has an area (else InvalidStateError from canvas)
502
- if (canvas.width * canvas.height > 0) {
510
+ if (canvas.width * canvas.height > 0)
503
511
  this.cctx.drawImage(canvas,
504
512
  val(layer, 'x', reltime), val(layer, 'y', reltime), canvas.width, canvas.height
505
513
  )
506
- }
507
514
  }
508
515
  }
509
516
 
@@ -515,9 +522,9 @@ export class Movie {
515
522
  const effect = this.effects[i]
516
523
  // An effect that has been deleted before effects.length has been updated
517
524
  // (see the effectsproxy in the constructor).
518
- if (!effect) {
525
+ if (!effect)
519
526
  continue
520
- }
527
+
521
528
  effect.apply(this, this.currentTime)
522
529
  }
523
530
  }
@@ -537,9 +544,9 @@ export class Movie {
537
544
  * Convienence method
538
545
  */
539
546
  private _publishToLayers (type, event) {
540
- for (let i = 0; i < this.layers.length; i++) {
541
- publish(this.layers[i], type, event)
542
- }
547
+ for (let i = 0; i < this.layers.length; i++)
548
+ if (Object.prototype.hasOwnProperty.call(this.layers, i))
549
+ publish(this.layers[i], type, event)
543
550
  }
544
551
 
545
552
  /**
@@ -631,12 +638,11 @@ export class Movie {
631
638
  return new Promise((resolve, reject) => {
632
639
  this._currentTime = time
633
640
  publish(this, 'movie.seek', {})
634
- if (refresh) {
641
+ if (refresh)
635
642
  // Pass promise callbacks to `refresh`
636
643
  this.refresh().then(resolve).catch(reject)
637
- } else {
644
+ else
638
645
  resolve()
639
- }
640
646
  })
641
647
  }
642
648