etro 0.6.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 (103) hide show
  1. package/.env +2 -0
  2. package/.github/workflows/nodejs.yml +27 -0
  3. package/CHANGELOG.md +109 -0
  4. package/CODE_OF_CONDUCT.md +77 -0
  5. package/CONTRIBUTING.md +155 -0
  6. package/LICENSE +674 -0
  7. package/README.md +57 -0
  8. package/dist/etro.js +3397 -0
  9. package/docs/effect.js.html +1215 -0
  10. package/docs/event.js.html +145 -0
  11. package/docs/index.html +81 -0
  12. package/docs/index.js.html +92 -0
  13. package/docs/layer.js.html +888 -0
  14. package/docs/module-effect-GaussianBlurComponent.html +345 -0
  15. package/docs/module-effect.Brightness.html +339 -0
  16. package/docs/module-effect.Channels.html +319 -0
  17. package/docs/module-effect.ChromaKey.html +611 -0
  18. package/docs/module-effect.Contrast.html +339 -0
  19. package/docs/module-effect.EllipticalMask.html +200 -0
  20. package/docs/module-effect.GaussianBlur.html +202 -0
  21. package/docs/module-effect.GaussianBlurHorizontal.html +242 -0
  22. package/docs/module-effect.GaussianBlurVertical.html +242 -0
  23. package/docs/module-effect.Pixelate.html +330 -0
  24. package/docs/module-effect.Shader.html +1227 -0
  25. package/docs/module-effect.Stack.html +406 -0
  26. package/docs/module-effect.Transform.Matrix.html +193 -0
  27. package/docs/module-effect.Transform.html +1174 -0
  28. package/docs/module-effect.html +148 -0
  29. package/docs/module-event.html +473 -0
  30. package/docs/module-index.html +186 -0
  31. package/docs/module-layer-Media.html +1116 -0
  32. package/docs/module-layer-MediaMixin.html +164 -0
  33. package/docs/module-layer.Audio.html +1188 -0
  34. package/docs/module-layer.Base.html +629 -0
  35. package/docs/module-layer.Image.html +1421 -0
  36. package/docs/module-layer.Text.html +1731 -0
  37. package/docs/module-layer.Video.html +1938 -0
  38. package/docs/module-layer.Visual.html +1698 -0
  39. package/docs/module-layer.html +137 -0
  40. package/docs/module-movie.html +3118 -0
  41. package/docs/module-util.Color.html +702 -0
  42. package/docs/module-util.Font.html +395 -0
  43. package/docs/module-util.html +845 -0
  44. package/docs/movie.js.html +689 -0
  45. package/docs/scripts/collapse.js +20 -0
  46. package/docs/scripts/linenumber.js +25 -0
  47. package/docs/scripts/nav.js +12 -0
  48. package/docs/scripts/polyfill.js +4 -0
  49. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  50. package/docs/scripts/prettify/lang-css.js +2 -0
  51. package/docs/scripts/prettify/prettify.js +28 -0
  52. package/docs/scripts/search.js +83 -0
  53. package/docs/styles/jsdoc.css +671 -0
  54. package/docs/styles/prettify.css +79 -0
  55. package/docs/util.js.html +503 -0
  56. package/eslint.conf.js +28 -0
  57. package/eslint.test-conf.js +4 -0
  58. package/examples/application/readme-screenshot.html +86 -0
  59. package/examples/application/video-player.html +131 -0
  60. package/examples/application/webcam.html +28 -0
  61. package/examples/introduction/audio.html +52 -0
  62. package/examples/introduction/effects.html +56 -0
  63. package/examples/introduction/export.html +70 -0
  64. package/examples/introduction/functions.html +35 -0
  65. package/examples/introduction/hello-world1.html +33 -0
  66. package/examples/introduction/hello-world2.html +32 -0
  67. package/examples/introduction/keyframes.html +67 -0
  68. package/examples/introduction/media.html +55 -0
  69. package/examples/introduction/text.html +27 -0
  70. package/jsdoc.conf.json +3 -0
  71. package/karma.conf.js +60 -0
  72. package/package.json +63 -0
  73. package/private-todo.txt +70 -0
  74. package/rename-file.sh +18 -0
  75. package/rename-versions.sh +14 -0
  76. package/rename.sh +22 -0
  77. package/rollup.config.js +31 -0
  78. package/screenshots/2019-08-17_0.png +0 -0
  79. package/scripts/gen-effect-samples.html +99 -0
  80. package/scripts/save-effect-samples.js +43 -0
  81. package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
  82. package/spec/assets/effect/gaussian-blur-vertical.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 +352 -0
  94. package/spec/event.spec.js +25 -0
  95. package/spec/layer.spec.js +128 -0
  96. package/spec/movie.spec.js +154 -0
  97. package/spec/util.spec.js +285 -0
  98. package/src/effect.js +1265 -0
  99. package/src/event.js +78 -0
  100. package/src/index.js +23 -0
  101. package/src/layer.js +875 -0
  102. package/src/movie.js +636 -0
  103. package/src/util.js +487 -0
package/src/movie.js ADDED
@@ -0,0 +1,636 @@
1
+ /**
2
+ * @module movie
3
+ */
4
+
5
+ import { subscribe, publish } from './event.js'
6
+ import { val, clearCachedValues, applyOptions, watchPublic } from './util.js'
7
+ import { Audio as AudioLayer, Video as VideoLayer } from './layer.js' // `Media` mixins
8
+
9
+ /**
10
+ * Contains all layers and movie information<br>
11
+ * Implements a sub/pub system (adapted from https://gist.github.com/lizzie/4993046)
12
+ *
13
+ * @todo Implement event "durationchange", and more
14
+ * @todo Add width and height options
15
+ * @todo Make record option to make recording video output to the user while it's recording
16
+ * @todo rename renderingFrame -> refreshing
17
+ */
18
+ export default class Movie {
19
+ /**
20
+ * Creates a new <code>Movie</code> instance (project)
21
+ *
22
+ * @param {HTMLCanvasElement} canvas - the canvas to display image data on
23
+ * @param {object} [options] - various optional arguments
24
+ * @param {BaseAudioContext} [options.audioContext=new AudioContext()]
25
+ * @param {string} [options.background="#000"] - the background color of the movijse,
26
+ * or <code>null</code> for a transparent background
27
+ * @param {boolean} [options.repeat=false] - whether to loop playbackjs
28
+ * @param {boolean} [options.autoRefresh=true] - whether to call `.refresh()` on init and when relevant layers
29
+ * are added/removed
30
+ */
31
+ constructor (canvas, options = {}) {
32
+ // TODO: move into multiple methods!
33
+ // Rename audioContext -> _actx
34
+ if ('audioContext' in options) {
35
+ options._actx = options.audioContext
36
+ }
37
+ delete options.audioContext // TODO: move up a line :P
38
+
39
+ const newThis = watchPublic(this) // proxy that will be returned by constructor
40
+ // Don't send updates when initializing, so use this instead of newThis:
41
+ // output canvas
42
+ this._canvas = canvas
43
+ // output canvas context
44
+ this._cctx = canvas.getContext('2d') // TODO: make private?
45
+ applyOptions(options, this)
46
+
47
+ // proxy arrays
48
+ const that = newThis
49
+
50
+ this._effectsBack = []
51
+ this._effects = new Proxy(newThis._effectsBack, {
52
+ apply: function (target, thisArg, argumentsList) {
53
+ return thisArg[target].apply(newThis, argumentsList)
54
+ },
55
+ deleteProperty: function (target, property) {
56
+ // Refresh screen when effect is removed, if the movie isn't playing already.
57
+ const value = target[property]
58
+ publish(that, 'movie.change.effect.remove', { effect: value })
59
+ value.detach()
60
+ delete target[property]
61
+ return true
62
+ },
63
+ set: function (target, property, value) {
64
+ if (!isNaN(property)) { // if property is an number (index)
65
+ if (target[property]) {
66
+ delete target[property] // call deleteProperty
67
+ }
68
+ value.attach(that) // Attach effect to movie (first)
69
+ // Refresh screen when effect is set, if the movie isn't playing already.
70
+ publish(that, 'movie.change.effect.add', { effect: value })
71
+ }
72
+ target[property] = value
73
+ return true
74
+ }
75
+ })
76
+
77
+ this._layersBack = []
78
+ this._layers = new Proxy(newThis._layersBack, {
79
+ apply: function (target, thisArg, argumentsList) {
80
+ return thisArg[target].apply(newThis, argumentsList)
81
+ },
82
+ deleteProperty: function (target, property) {
83
+ const oldDuration = this.duration
84
+ const value = target[property]
85
+ value.detach(that)
86
+ const current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration
87
+ if (current) {
88
+ publish(that, 'movie.change.layer.remove', { layer: value })
89
+ }
90
+ publish(that, 'movie.change.duration', { oldDuration })
91
+ delete target[property]
92
+ return true
93
+ },
94
+ set: function (target, property, value) {
95
+ const oldDuration = this.duration
96
+ target[property] = value
97
+ if (!isNaN(property)) { // if property is an number (index)
98
+ value.attach(that) // Attach layer to movie (first)
99
+ // Refresh screen when a relevant layer is added or removed
100
+ const current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration
101
+ if (current) {
102
+ publish(that, 'movie.change.layer.add', { layer: value })
103
+ }
104
+ publish(that, 'movie.change.duration', { oldDuration })
105
+ }
106
+ return true
107
+ }
108
+ })
109
+ this._paused = true
110
+ this._ended = false
111
+ // to prevent multiple frame-rendering loops at the same time (see `render`)
112
+ this._renderingFrame = false // only applicable when rendering
113
+ this._currentTime = 0
114
+
115
+ this._mediaRecorder = null // for recording
116
+
117
+ // NOTE: -1 works well in inequalities
118
+ this._lastPlayed = -1 // the last time `play` was called
119
+ this._lastPlayedOffset = -1 // what was `currentTime` when `play` was called
120
+ // newThis._updateInterval = 0.1; // time in seconds between each "timeupdate" event
121
+ // newThis._lastUpdate = -1;
122
+
123
+ if (newThis.autoRefresh) {
124
+ newThis.refresh() // render single frame on init
125
+ }
126
+
127
+ // Subscribe to own event "change" (child events propogate up)
128
+ subscribe(newThis, 'movie.change', () => {
129
+ if (newThis.autoRefresh && !newThis.rendering) {
130
+ newThis.refresh()
131
+ }
132
+ })
133
+
134
+ // Subscribe to own event "ended"
135
+ subscribe(newThis, 'movie.ended', () => {
136
+ if (newThis.recording) {
137
+ newThis._mediaRecorder.requestData() // I shouldn't have to call newThis right? err
138
+ newThis._mediaRecorder.stop()
139
+ }
140
+ })
141
+
142
+ return newThis
143
+ }
144
+
145
+ /**
146
+ * Plays the movie
147
+ * @return {Promise} fulfilled when done playing, never fails
148
+ */
149
+ play () {
150
+ return new Promise((resolve, reject) => {
151
+ if (!this.paused) {
152
+ throw new Error('Already playing')
153
+ }
154
+
155
+ this._paused = this._ended = false
156
+ this._lastPlayed = performance.now()
157
+ this._lastPlayedOffset = this.currentTime
158
+
159
+ if (!this._renderingFrame) {
160
+ // Not rendering (and not playing), so play
161
+ this._render(undefined, resolve)
162
+ }
163
+ // Stop rendering frame if currently doing so, because playing has higher priority.
164
+ this._renderingFrame = false // this will effect the next _render call
165
+
166
+ publish(this, 'movie.play', {})
167
+ })
168
+ }
169
+
170
+ // TEST: *support recording that plays back with audio!*
171
+ // TODO: figure out a way to record faster than playing (i.e. not in real time)
172
+ // TODO: improve recording performance to increase frame rate?
173
+ /**
174
+ * Plays the movie in the background and records it
175
+ *
176
+ * @param {number} framerate
177
+ * @param {object} [options]
178
+ * @param {boolean} [options.video=true] - whether to include video in recording
179
+ * @param {boolean} [options.audio=true] - whether to include audio in recording
180
+ * @param {object} [options.mediaRecorderOptions=undefined] - options to pass to the <code>MediaRecorder</code>
181
+ * constructor
182
+ * @return {Promise} resolves when done recording, rejects when internal media recorder errors
183
+ */
184
+ record (framerate, options = {}) {
185
+ if (options.video === options.audio === false) {
186
+ throw new Error('Both video and audio cannot be disabled')
187
+ }
188
+
189
+ if (!this.paused) {
190
+ throw new Error('Cannot record movie while already playing or recording')
191
+ }
192
+ return new Promise((resolve, reject) => {
193
+ // https://developers.google.com/web/updates/2016/01/mediarecorder
194
+ const canvasCache = this.canvas
195
+ // record on a temporary canvas context
196
+ this._canvas = document.createElement('canvas')
197
+ this.canvas.width = canvasCache.width
198
+ this.canvas.height = canvasCache.height
199
+ this._cctx = this.canvas.getContext('2d')
200
+
201
+ const recordedChunks = [] // frame blobs
202
+ // combine image + audio, or just pick one
203
+ let tracks = []
204
+ if (options.video !== false) {
205
+ const visualStream = this.canvas.captureStream(framerate)
206
+ tracks = tracks.concat(visualStream.getTracks())
207
+ }
208
+ // Check if there's a layer that's an instance of a Media mixin (Audio or Video)
209
+ const hasMediaTracks = this.layers.some(layer => layer instanceof AudioLayer || layer instanceof VideoLayer)
210
+ // If no media tracks present, don't include an audio stream, because Chrome doesn't record silence
211
+ // when an audio stream is present.
212
+ if (hasMediaTracks && options.audio !== false) {
213
+ const audioDestination = this.actx.createMediaStreamDestination()
214
+ const audioStream = audioDestination.stream
215
+ tracks = tracks.concat(audioStream.getTracks())
216
+ this.publishToLayers('movie.audiodestinationupdate', { movie: this, destination: audioDestination })
217
+ }
218
+ const stream = new MediaStream(tracks)
219
+ const mediaRecorder = new MediaRecorder(stream, options.mediaRecorderOptions)
220
+ // TODO: publish to movie, not layers
221
+ mediaRecorder.ondataavailable = event => {
222
+ // if (this._paused) reject(new Error("Recording was interrupted"));
223
+ if (event.data.size > 0) {
224
+ recordedChunks.push(event.data)
225
+ }
226
+ }
227
+ mediaRecorder.onstop = () => {
228
+ this._ended = true
229
+ this._canvas = canvasCache
230
+ this._cctx = this.canvas.getContext('2d')
231
+ this.publishToLayers(
232
+ 'movie.audiodestinationupdate',
233
+ { movie: this, destination: this.actx.destination }
234
+ )
235
+ this._mediaRecorder = null
236
+ // construct super-blob
237
+ // this is the exported video as a blob!
238
+ resolve(new Blob(recordedChunks, { type: 'video/webm' }/*, {"type" : "audio/ogg; codecs=opus"} */))
239
+ }
240
+ mediaRecorder.onerror = reject
241
+
242
+ mediaRecorder.start()
243
+ this._mediaRecorder = mediaRecorder
244
+ this.play()
245
+ publish(this, 'movie.record', { options })
246
+ })
247
+ }
248
+
249
+ /**
250
+ * Stops the movie, without reseting the playback position
251
+ * @return {Movie} the movie (for chaining)
252
+ */
253
+ pause () {
254
+ this._paused = true
255
+ // disable all layers
256
+ for (let i = 0; i < this.layers.length; i++) {
257
+ const layer = this.layers[i]
258
+ layer.stop(this.currentTime - layer.startTime)
259
+ layer._active = false
260
+ }
261
+ publish(this, 'movie.pause', {})
262
+ return this
263
+ }
264
+
265
+ /**
266
+ * Stops playback and resets the playback position
267
+ * @return {Movie} the movie (for chaining)
268
+ */
269
+ stop () {
270
+ this.pause()
271
+ this.currentTime = 0 // use setter?
272
+ return this
273
+ }
274
+
275
+ /**
276
+ * @param {number} [timestamp=performance.now()]
277
+ * @param {function} [done=undefined] - called when done playing or when the current frame is loaded
278
+ * @private
279
+ */
280
+ _render (timestamp = performance.now(), done = undefined) {
281
+ clearCachedValues(this)
282
+
283
+ if (!this.rendering) {
284
+ // (!this.paused || this._renderingFrame) is true (it's playing or it's rendering a single frame)
285
+ done && done()
286
+ return
287
+ }
288
+
289
+ this._updateCurrentTime(timestamp)
290
+ // bad for performance? (remember, it's calling Array.reduce)
291
+ const end = this.duration
292
+ const ended = this.currentTime >= end
293
+ if (ended) {
294
+ publish(this, 'movie.ended', { movie: this, repeat: this.repeat })
295
+ this._currentTime = 0 // don't use setter
296
+ publish(this, 'movie.timeupdate', { movie: this })
297
+ this._lastPlayed = performance.now()
298
+ this._lastPlayedOffset = 0 // this.currentTime
299
+ this._renderingFrame = false
300
+ if (!this.repeat || this.recording) {
301
+ this._ended = true
302
+ // disable all layers
303
+ for (let i = 0; i < this.layers.length; i++) {
304
+ const layer = this.layers[i]
305
+ layer.stop(this.currentTime - layer.startTime)
306
+ layer._active = false
307
+ }
308
+ }
309
+ done && done()
310
+ return
311
+ }
312
+
313
+ // do render
314
+ this._renderBackground(timestamp)
315
+ const frameFullyLoaded = this._renderLayers(timestamp)
316
+ this._applyEffects()
317
+
318
+ if (frameFullyLoaded) {
319
+ publish(this, 'movie.loadeddata', { movie: this })
320
+ }
321
+
322
+ // if instant didn't load, repeatedly frame-render until frame is loaded
323
+ // if the expression below is false, don't publish an event, just silently stop render loop
324
+ if (this._renderingFrame && frameFullyLoaded) {
325
+ this._renderingFrame = false
326
+ done && done()
327
+ return
328
+ }
329
+
330
+ window.requestAnimationFrame(timestamp => {
331
+ this._render(timestamp)
332
+ }) // TODO: research performance cost
333
+ }
334
+
335
+ _updateCurrentTime (timestamp) {
336
+ // if we're only instant-rendering (current frame only), it doens't matter if it's paused or not
337
+ if (!this._renderingFrame) {
338
+ // if ((timestamp - this._lastUpdate) >= this._updateInterval) {
339
+ const sinceLastPlayed = (timestamp - this._lastPlayed) / 1000
340
+ this._currentTime = this._lastPlayedOffset + sinceLastPlayed // don't use setter
341
+ publish(this, 'movie.timeupdate', { movie: this })
342
+ // this._lastUpdate = timestamp;
343
+ // }
344
+ }
345
+ }
346
+
347
+ _renderBackground (timestamp) {
348
+ this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
349
+ if (this.background) { // TODO: check valued result
350
+ this.cctx.fillStyle = val(this, 'background', timestamp)
351
+ this.cctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
352
+ }
353
+ }
354
+
355
+ /**
356
+ * @return {boolean} whether or not video frames are loaded
357
+ * @param {number} [timestamp=performance.now()]
358
+ * @private
359
+ */
360
+ _renderLayers (timestamp) {
361
+ let frameFullyLoaded = true
362
+ for (let i = 0; i < this.layers.length; i++) {
363
+ const layer = this.layers[i]
364
+ const reltime = this.currentTime - layer.startTime
365
+ // Cancel operation if layer disabled or outside layer time interval
366
+ if (!layer.enabled ||
367
+ // > or >= ?
368
+ this.currentTime < layer.startTime || this.currentTime > layer.startTime + layer.duration) {
369
+ // outside time interval
370
+ // if only rendering this frame (instant==true), we are not "starting" the layer
371
+ if (layer.active && !this._renderingFrame) {
372
+ // TODO: make a `deactivate()` method?
373
+ // console.log("stop");
374
+ layer.stop(reltime)
375
+ layer._active = false
376
+ }
377
+ continue
378
+ }
379
+ // if only rendering this frame, we are not "starting" the layer
380
+ if (!layer.active && layer.enabled && !this._renderingFrame) {
381
+ // TODO: make an `activate()` method?
382
+ // console.log("start");
383
+ layer.start(reltime)
384
+ layer._active = true
385
+ }
386
+
387
+ if (layer.media) {
388
+ frameFullyLoaded = frameFullyLoaded && layer.media.readyState >= 2
389
+ } // frame loaded
390
+ layer.render(reltime) // pass relative time for convenience
391
+
392
+ // if the layer has visual component
393
+ if (layer.canvas) {
394
+ // layer.canvas.width and layer.canvas.height should already be interpolated
395
+ // if the layer has an area (else InvalidStateError from canvas)
396
+ if (layer.canvas.width * layer.canvas.height > 0) {
397
+ this.cctx.drawImage(layer.canvas,
398
+ val(layer, 'x', reltime), val(layer, 'y', reltime), layer.canvas.width, layer.canvas.height
399
+ )
400
+ }
401
+ }
402
+ }
403
+
404
+ return frameFullyLoaded
405
+ }
406
+
407
+ _applyEffects () {
408
+ for (let i = 0; i < this.effects.length; i++) {
409
+ const effect = this.effects[i]
410
+ effect.apply(this, this.currentTime)
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Refreshes the screen (only use this if auto-refresh is disabled)
416
+ * @return {Promise} - resolves when the frame is loaded
417
+ */
418
+ refresh () {
419
+ if (this.rendering) {
420
+ throw new Error('Cannot refresh frame while already rendering')
421
+ }
422
+
423
+ return new Promise((resolve, reject) => {
424
+ this._renderingFrame = true
425
+ this._render(undefined, resolve)
426
+ })
427
+ }
428
+
429
+ /**
430
+ * Convienence method
431
+ * @todo Make private
432
+ */
433
+ publishToLayers (type, event) {
434
+ for (let i = 0; i < this.layers.length; i++) {
435
+ publish(this.layers[i], type, event)
436
+ }
437
+ }
438
+
439
+ /**
440
+ * If the movie is playing, recording or refreshing
441
+ * @type boolean
442
+ */
443
+ get rendering () {
444
+ return !this.paused || this._renderingFrame
445
+ }
446
+
447
+ /**
448
+ * If the movie is refreshing current frame
449
+ * @type boolean
450
+ */
451
+ get renderingFrame () {
452
+ return this._renderingFrame
453
+ }
454
+
455
+ /**
456
+ * If the movie is recording
457
+ * @type boolean
458
+ */
459
+ get recording () {
460
+ return !!this._mediaRecorder
461
+ }
462
+
463
+ /**
464
+ * The combined duration of all layers
465
+ * @type number
466
+ */
467
+ get duration () { // TODO: dirty flag?
468
+ return this.layers.reduce((end, layer) => Math.max(layer.startTime + layer.duration, end), 0)
469
+ }
470
+
471
+ /**
472
+ * @type layer.Base[]
473
+ */
474
+ get layers () {
475
+ return this._layers
476
+ }
477
+
478
+ // (proxy)
479
+ /**
480
+ * Convienence method for <code>layers.push()</code>
481
+ * @param {BaseLayer} layer
482
+ * @return {Movie} the movie (for chaining)
483
+ */
484
+ addLayer (layer) {
485
+ this.layers.push(layer); return this
486
+ }
487
+
488
+ /**
489
+ * @type effect.Base[]
490
+ */
491
+ get effects () {
492
+ return this._effects // private (because it's a proxy)
493
+ }
494
+
495
+ /**
496
+ * Convienence method for <code>effects.push()</code>
497
+ * @param {BaseEffect} effect
498
+ * @return {Movie} the movie (for chaining)
499
+ */
500
+ addEffect (effect) {
501
+ this.effects.push(effect); return this
502
+ }
503
+
504
+ /**
505
+ * @type boolean
506
+ */
507
+ get paused () {
508
+ return this._paused
509
+ }
510
+
511
+ /**
512
+ * If the playback position is at the end of the movie
513
+ * @type boolean
514
+ */
515
+ get ended () {
516
+ return this._ended
517
+ }
518
+
519
+ /**
520
+ * The current playback position
521
+ * @type number
522
+ */
523
+ get currentTime () {
524
+ return this._currentTime
525
+ }
526
+
527
+ /**
528
+ * Sets the current playback position. This is a more powerful version of `set currentTime`.
529
+ *
530
+ * @param {number} time - the new cursor's time value in seconds
531
+ * @param {boolean} [refresh=true] - whether to render a single frame to match new time or not
532
+ * @return {Promise} resolves when the current frame is rendered if <code>refresh</code> is true,
533
+ * otherwise resolves immediately
534
+ *
535
+ * @todo Refresh ionly f auto-refreshing is enabled
536
+ */
537
+ setCurrentTime (time, refresh = true) {
538
+ return new Promise((resolve, reject) => {
539
+ this._currentTime = time
540
+ publish(this, 'movie.seek', {})
541
+ if (refresh) {
542
+ // pass promise callbacks to `refresh`
543
+ this.refresh().then(resolve).catch(reject)
544
+ } else {
545
+ resolve()
546
+ }
547
+ })
548
+ }
549
+
550
+ set currentTime (time) {
551
+ this._currentTime = time
552
+ publish(this, 'movie.seek', {})
553
+ this.refresh() // render single frame to match new time
554
+ }
555
+
556
+ /**
557
+ * The rendering canvas
558
+ * @type HTMLCanvasElement
559
+ */
560
+ get canvas () {
561
+ return this._canvas
562
+ }
563
+
564
+ /**
565
+ * The rendering canvas's context
566
+ * @type CanvasRenderingContext2D
567
+ */
568
+ get cctx () {
569
+ return this._cctx
570
+ }
571
+
572
+ /**
573
+ * The audio context to which audio is played
574
+ * @type BaseAudioContext
575
+ */
576
+ get actx () {
577
+ return this._actx
578
+ }
579
+
580
+ /**
581
+ * The width of the rendering canvas
582
+ * @type number
583
+ */
584
+ get width () {
585
+ return this.canvas.width
586
+ }
587
+
588
+ /**
589
+ * The height of the rendering canvas
590
+ * @type number
591
+ */
592
+ get height () {
593
+ return this.canvas.height
594
+ }
595
+
596
+ set width (width) {
597
+ this.canvas.width = width
598
+ }
599
+
600
+ set height (height) {
601
+ this.canvas.height = height
602
+ }
603
+
604
+ get movie () {
605
+ return this
606
+ }
607
+
608
+ getDefaultOptions () {
609
+ return {
610
+ _actx: new AudioContext(),
611
+ /**
612
+ * @name module:movie#background
613
+ * @type string
614
+ * @desc The css color for the background, or <code>null</code> for transparency
615
+ */
616
+ background: '#000',
617
+ /**
618
+ * @name module:movie#repeat
619
+ * @type boolean
620
+ */
621
+ repeat: false,
622
+ /**
623
+ * @name module:movie#autoRefresh
624
+ * @type boolean
625
+ * @desc Whether to refresh when changes are made that would effect the current frame
626
+ */
627
+ autoRefresh: true
628
+ }
629
+ }
630
+ }
631
+
632
+ // id for events (independent of instance, but easy to access when on prototype chain)
633
+ Movie.prototype.type = 'movie'
634
+ // TODO: refactor so we don't need to explicitly exclude some of these
635
+ Movie.prototype.publicExcludes = ['canvas', 'cctx', 'actx', 'layers', 'effects']
636
+ Movie.prototype.propertyFilters = {}