etro 0.8.0 → 0.8.1

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 (95) hide show
  1. package/.github/workflows/nodejs.yml +3 -1
  2. package/CHANGELOG.md +9 -1
  3. package/CODE_OF_CONDUCT.md +5 -5
  4. package/CONTRIBUTING.md +22 -72
  5. package/README.md +1 -1
  6. package/dist/effect/base.d.ts +14 -1
  7. package/dist/etro-cjs.js +156 -213
  8. package/dist/etro-iife.js +156 -213
  9. package/dist/layer/base.d.ts +13 -0
  10. package/eslint.conf.js +2 -1
  11. package/eslint.test-conf.js +1 -0
  12. package/examples/application/readme-screenshot.html +4 -8
  13. package/examples/application/video-player.html +3 -4
  14. package/examples/introduction/effects.html +23 -4
  15. package/karma.conf.js +4 -2
  16. package/package.json +4 -1
  17. package/scripts/gen-effect-samples.html +0 -3
  18. package/src/effect/base.ts +29 -10
  19. package/src/effect/gaussian-blur.ts +10 -10
  20. package/src/effect/pixelate.ts +1 -2
  21. package/src/effect/shader.ts +18 -22
  22. package/src/effect/stack.ts +5 -2
  23. package/src/effect/transform.ts +13 -14
  24. package/src/event.ts +8 -14
  25. package/src/layer/audio-source.ts +16 -14
  26. package/src/layer/audio.ts +1 -2
  27. package/src/layer/base.ts +26 -7
  28. package/src/layer/visual.ts +5 -6
  29. package/src/movie.ts +41 -49
  30. package/src/util.ts +50 -57
  31. package/docs/effect.js.html +0 -1215
  32. package/docs/event.js.html +0 -145
  33. package/docs/index.html +0 -81
  34. package/docs/index.js.html +0 -92
  35. package/docs/layer.js.html +0 -888
  36. package/docs/module-effect-GaussianBlurComponent.html +0 -345
  37. package/docs/module-effect.Brightness.html +0 -339
  38. package/docs/module-effect.Channels.html +0 -319
  39. package/docs/module-effect.ChromaKey.html +0 -611
  40. package/docs/module-effect.Contrast.html +0 -339
  41. package/docs/module-effect.EllipticalMask.html +0 -200
  42. package/docs/module-effect.GaussianBlur.html +0 -202
  43. package/docs/module-effect.GaussianBlurHorizontal.html +0 -242
  44. package/docs/module-effect.GaussianBlurVertical.html +0 -242
  45. package/docs/module-effect.Pixelate.html +0 -330
  46. package/docs/module-effect.Shader.html +0 -1227
  47. package/docs/module-effect.Stack.html +0 -406
  48. package/docs/module-effect.Transform.Matrix.html +0 -193
  49. package/docs/module-effect.Transform.html +0 -1174
  50. package/docs/module-effect.html +0 -148
  51. package/docs/module-event.html +0 -473
  52. package/docs/module-index.html +0 -186
  53. package/docs/module-layer-Media.html +0 -1116
  54. package/docs/module-layer-MediaMixin.html +0 -164
  55. package/docs/module-layer.Audio.html +0 -1188
  56. package/docs/module-layer.Base.html +0 -629
  57. package/docs/module-layer.Image.html +0 -1421
  58. package/docs/module-layer.Text.html +0 -1731
  59. package/docs/module-layer.Video.html +0 -1938
  60. package/docs/module-layer.Visual.html +0 -1698
  61. package/docs/module-layer.html +0 -137
  62. package/docs/module-movie.html +0 -3118
  63. package/docs/module-util.Color.html +0 -702
  64. package/docs/module-util.Font.html +0 -395
  65. package/docs/module-util.html +0 -845
  66. package/docs/movie.js.html +0 -689
  67. package/docs/scripts/collapse.js +0 -20
  68. package/docs/scripts/linenumber.js +0 -25
  69. package/docs/scripts/nav.js +0 -12
  70. package/docs/scripts/polyfill.js +0 -4
  71. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
  72. package/docs/scripts/prettify/lang-css.js +0 -2
  73. package/docs/scripts/prettify/prettify.js +0 -28
  74. package/docs/scripts/search.js +0 -83
  75. package/docs/styles/jsdoc.css +0 -671
  76. package/docs/styles/prettify.css +0 -79
  77. package/docs/util.js.html +0 -503
  78. package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
  79. package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
  80. package/spec/assets/effect/grayscale.png +0 -0
  81. package/spec/assets/effect/original.png +0 -0
  82. package/spec/assets/effect/pixelate.png +0 -0
  83. package/spec/assets/effect/transform/multiply.png +0 -0
  84. package/spec/assets/effect/transform/rotate.png +0 -0
  85. package/spec/assets/effect/transform/scale-fraction.png +0 -0
  86. package/spec/assets/effect/transform/scale.png +0 -0
  87. package/spec/assets/effect/transform/translate-fraction.png +0 -0
  88. package/spec/assets/effect/transform/translate.png +0 -0
  89. package/spec/assets/layer/audio.wav +0 -0
  90. package/spec/assets/layer/image.jpg +0 -0
  91. package/spec/effect.spec.js +0 -421
  92. package/spec/event.spec.js +0 -39
  93. package/spec/layer.spec.js +0 -307
  94. package/spec/movie.spec.js +0 -346
  95. package/spec/util.spec.js +0 -294
@@ -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
@@ -116,19 +116,18 @@ class Visual extends Base {
116
116
  endRender (): void {
117
117
  const w = val(this, 'width', this.currentTime) || val(this.movie, 'width', this.movie.currentTime)
118
118
  const h = val(this, 'height', this.currentTime) || val(this.movie, 'height', this.movie.currentTime)
119
- if (w * h > 0) {
119
+ if (w * h > 0)
120
120
  this._applyEffects()
121
- }
121
+
122
122
  // else InvalidStateError for drawing zero-area image in some effects, right?
123
123
  }
124
124
 
125
125
  _applyEffects (): void {
126
126
  for (let i = 0; i < this.effects.length; i++) {
127
127
  const effect = this.effects[i]
128
- if (effect.enabled) {
128
+ if (effect.enabled)
129
129
  // Pass relative time
130
130
  effect.apply(this, this.movie.currentTime - this.startTime)
131
- }
132
131
  }
133
132
  }
134
133
 
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,12 @@ 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
+
260
258
  return new Promise((resolve, reject) => {
261
259
  const canvasCache = this.canvas
262
260
  // Record on a temporary canvas context
@@ -290,9 +288,8 @@ export class Movie {
290
288
  const mediaRecorder = new MediaRecorder(stream, options.mediaRecorderOptions)
291
289
  mediaRecorder.ondataavailable = event => {
292
290
  // if (this._paused) reject(new Error("Recording was interrupted"));
293
- if (event.data.size > 0) {
291
+ if (event.data.size > 0)
294
292
  recordedChunks.push(event.data)
295
- }
296
293
  }
297
294
  // TODO: publish to movie, not layers
298
295
  mediaRecorder.onstop = () => {
@@ -357,18 +354,17 @@ export class Movie {
357
354
  if (!this.rendering) {
358
355
  // (!this.paused || this._renderingFrame) is true so it's playing or it's
359
356
  // rendering a single frame.
360
- if (done) {
357
+ if (done)
361
358
  done()
362
- }
359
+
363
360
  return
364
361
  }
365
362
 
366
363
  this._updateCurrentTime(timestamp)
367
364
  const recordingEnd = this.recording ? this._recordEndTime : this.duration
368
365
  const recordingEnded = this.currentTime > recordingEnd
369
- if (recordingEnded) {
366
+ if (recordingEnded)
370
367
  publish(this, 'movie.recordended', { movie: this })
371
- }
372
368
 
373
369
  // Bad for performance? (remember, it's calling Array.reduce)
374
370
  const end = this.duration
@@ -388,9 +384,9 @@ export class Movie {
388
384
  const layer = this.layers[i]
389
385
  // A layer that has been deleted before layers.length has been updated
390
386
  // (see the layers proxy in the constructor).
391
- if (!layer) {
387
+ if (!layer)
392
388
  continue
393
- }
389
+
394
390
  layer.stop()
395
391
  layer.active = false
396
392
  }
@@ -399,9 +395,9 @@ export class Movie {
399
395
 
400
396
  // Stop playback or recording if done
401
397
  if (recordingEnded || (ended && !this.repeat)) {
402
- if (done) {
398
+ if (done)
403
399
  done()
404
- }
400
+
405
401
  return
406
402
  }
407
403
 
@@ -410,9 +406,8 @@ export class Movie {
410
406
  const frameFullyLoaded = this._renderLayers()
411
407
  this._applyEffects()
412
408
 
413
- if (frameFullyLoaded) {
409
+ if (frameFullyLoaded)
414
410
  publish(this, 'movie.loadeddata', { movie: this })
415
- }
416
411
 
417
412
  // If didn't load in this instant, repeatedly frame-render until frame is
418
413
  // loaded.
@@ -420,9 +415,9 @@ export class Movie {
420
415
  // stop render loop.
421
416
  if (!repeat || (this._renderingFrame && frameFullyLoaded)) {
422
417
  this._renderingFrame = false
423
- if (done) {
418
+ if (done)
424
419
  done()
425
- }
420
+
426
421
  return
427
422
  }
428
423
 
@@ -464,9 +459,9 @@ export class Movie {
464
459
  const layer = this.layers[i]
465
460
  // A layer that has been deleted before layers.length has been updated
466
461
  // (see the layers proxy in the constructor).
467
- if (!layer) {
462
+ if (!layer)
468
463
  continue
469
- }
464
+
470
465
  const reltime = this.currentTime - layer.startTime
471
466
  // Cancel operation if layer disabled or outside layer time interval
472
467
  if (!val(layer, 'enabled', reltime) ||
@@ -489,9 +484,9 @@ export class Movie {
489
484
  }
490
485
 
491
486
  // if the layer has an input file
492
- if ('source' in layer) {
487
+ if ('source' in layer)
493
488
  frameFullyLoaded = frameFullyLoaded && (layer as unknown as AudioSource).source.readyState >= 2
494
- }
489
+
495
490
  layer.render()
496
491
 
497
492
  // if the layer has visual component
@@ -499,11 +494,10 @@ export class Movie {
499
494
  const canvas = (layer as Visual).canvas
500
495
  // layer.canvas.width and layer.canvas.height should already be interpolated
501
496
  // if the layer has an area (else InvalidStateError from canvas)
502
- if (canvas.width * canvas.height > 0) {
497
+ if (canvas.width * canvas.height > 0)
503
498
  this.cctx.drawImage(canvas,
504
499
  val(layer, 'x', reltime), val(layer, 'y', reltime), canvas.width, canvas.height
505
500
  )
506
- }
507
501
  }
508
502
  }
509
503
 
@@ -515,9 +509,9 @@ export class Movie {
515
509
  const effect = this.effects[i]
516
510
  // An effect that has been deleted before effects.length has been updated
517
511
  // (see the effectsproxy in the constructor).
518
- if (!effect) {
512
+ if (!effect)
519
513
  continue
520
- }
514
+
521
515
  effect.apply(this, this.currentTime)
522
516
  }
523
517
  }
@@ -537,9 +531,8 @@ export class Movie {
537
531
  * Convienence method
538
532
  */
539
533
  private _publishToLayers (type, event) {
540
- for (let i = 0; i < this.layers.length; i++) {
534
+ for (let i = 0; i < this.layers.length; i++)
541
535
  publish(this.layers[i], type, event)
542
- }
543
536
  }
544
537
 
545
538
  /**
@@ -631,12 +624,11 @@ export class Movie {
631
624
  return new Promise((resolve, reject) => {
632
625
  this._currentTime = time
633
626
  publish(this, 'movie.seek', {})
634
- if (refresh) {
627
+ if (refresh)
635
628
  // Pass promise callbacks to `refresh`
636
629
  this.refresh().then(resolve).catch(reject)
637
- } else {
630
+ else
638
631
  resolve()
639
- }
640
632
  })
641
633
  }
642
634