etro 0.7.0 → 0.8.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 (106) hide show
  1. package/.github/workflows/nodejs.yml +1 -1
  2. package/CHANGELOG.md +45 -1
  3. package/CONTRIBUTING.md +23 -19
  4. package/README.md +81 -26
  5. package/dist/effect/base.d.ts +38 -0
  6. package/dist/effect/brightness.d.ts +16 -0
  7. package/dist/effect/channels.d.ts +23 -0
  8. package/dist/effect/chroma-key.d.ts +23 -0
  9. package/dist/effect/contrast.d.ts +15 -0
  10. package/dist/effect/elliptical-mask.d.ts +31 -0
  11. package/dist/effect/gaussian-blur.d.ts +60 -0
  12. package/dist/effect/grayscale.d.ts +7 -0
  13. package/dist/effect/index.d.ts +15 -0
  14. package/dist/effect/pixelate.d.ts +18 -0
  15. package/dist/effect/shader.d.ts +99 -0
  16. package/dist/effect/stack.d.ts +23 -0
  17. package/dist/effect/transform.d.ts +73 -0
  18. package/dist/etro-cjs.js +9337 -3331
  19. package/dist/etro-iife.js +9279 -3273
  20. package/dist/etro.d.ts +7 -0
  21. package/dist/event.d.ts +35 -0
  22. package/dist/index.d.ts +6 -0
  23. package/dist/layer/audio-source.d.ts +24 -0
  24. package/dist/layer/audio.d.ts +14 -0
  25. package/dist/layer/base.d.ts +69 -0
  26. package/dist/layer/image.d.ts +6 -0
  27. package/dist/layer/index.d.ts +11 -0
  28. package/dist/layer/text.d.ts +60 -0
  29. package/dist/layer/video.d.ts +11 -0
  30. package/dist/layer/visual-source.d.ts +32 -0
  31. package/dist/layer/visual.d.ts +58 -0
  32. package/dist/movie.d.ts +192 -0
  33. package/dist/object.d.ts +12 -0
  34. package/dist/util.d.ts +125 -0
  35. package/eslint.conf.js +0 -8
  36. package/eslint.example-conf.js +9 -0
  37. package/eslint.typescript-conf.js +5 -0
  38. package/examples/application/readme-screenshot.html +12 -9
  39. package/examples/application/video-player.html +7 -7
  40. package/examples/application/webcam.html +6 -6
  41. package/examples/introduction/audio.html +30 -18
  42. package/examples/introduction/effects.html +14 -10
  43. package/examples/introduction/export.html +32 -25
  44. package/examples/introduction/functions.html +6 -4
  45. package/examples/introduction/hello-world1.html +9 -5
  46. package/examples/introduction/hello-world2.html +5 -5
  47. package/examples/introduction/keyframes.html +35 -23
  48. package/examples/introduction/media.html +26 -18
  49. package/examples/introduction/text.html +9 -5
  50. package/karma.conf.js +1 -1
  51. package/package.json +29 -13
  52. package/rollup.config.js +15 -4
  53. package/scripts/gen-effect-samples.html +29 -25
  54. package/scripts/save-effect-samples.js +14 -15
  55. package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
  56. package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
  57. package/spec/assets/effect/grayscale.png +0 -0
  58. package/spec/assets/effect/original.png +0 -0
  59. package/spec/assets/effect/pixelate.png +0 -0
  60. package/spec/assets/effect/transform/multiply.png +0 -0
  61. package/spec/assets/effect/transform/rotate.png +0 -0
  62. package/spec/assets/effect/transform/scale-fraction.png +0 -0
  63. package/spec/assets/effect/transform/scale.png +0 -0
  64. package/spec/assets/effect/transform/translate-fraction.png +0 -0
  65. package/spec/assets/effect/transform/translate.png +0 -0
  66. package/spec/effect.spec.js +126 -57
  67. package/spec/event.spec.js +14 -0
  68. package/spec/layer.spec.js +175 -18
  69. package/spec/movie.spec.js +191 -7
  70. package/spec/util.spec.js +14 -5
  71. package/src/effect/base.ts +96 -0
  72. package/src/effect/brightness.ts +43 -0
  73. package/src/effect/channels.ts +50 -0
  74. package/src/effect/chroma-key.ts +82 -0
  75. package/src/effect/contrast.ts +42 -0
  76. package/src/effect/elliptical-mask.ts +75 -0
  77. package/src/effect/gaussian-blur.ts +232 -0
  78. package/src/effect/grayscale.ts +34 -0
  79. package/src/effect/index.ts +22 -0
  80. package/src/effect/pixelate.ts +59 -0
  81. package/src/effect/shader.ts +561 -0
  82. package/src/effect/stack.ts +74 -0
  83. package/src/effect/transform.ts +194 -0
  84. package/src/etro.ts +26 -0
  85. package/src/event.ts +118 -0
  86. package/src/index.ts +8 -0
  87. package/src/layer/audio-source.ts +217 -0
  88. package/src/layer/audio.ts +35 -0
  89. package/src/layer/base.ts +156 -0
  90. package/src/layer/image.ts +8 -0
  91. package/src/layer/index.ts +13 -0
  92. package/src/layer/text.ts +138 -0
  93. package/src/layer/video.ts +15 -0
  94. package/src/layer/visual-source.ts +150 -0
  95. package/src/layer/visual.ts +198 -0
  96. package/src/movie.ts +709 -0
  97. package/src/object.ts +14 -0
  98. package/src/util.ts +473 -0
  99. package/tsconfig.json +8 -0
  100. package/screenshots/2019-08-17_0.png +0 -0
  101. package/src/effect.js +0 -1268
  102. package/src/event.js +0 -78
  103. package/src/index.js +0 -23
  104. package/src/layer.js +0 -897
  105. package/src/movie.js +0 -637
  106. package/src/util.js +0 -505
package/src/movie.js DELETED
@@ -1,637 +0,0 @@
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(true, 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
- * @param {string} [options.type='video/webm'] - MIME type for exported video
182
- * constructor
183
- * @return {Promise} resolves when done recording, rejects when internal media recorder errors
184
- */
185
- record (framerate, options = {}) {
186
- if (options.video === false && options.audio === false) {
187
- throw new Error('Both video and audio cannot be disabled')
188
- }
189
-
190
- if (!this.paused) {
191
- throw new Error('Cannot record movie while already playing or recording')
192
- }
193
- return new Promise((resolve, reject) => {
194
- // https://developers.google.com/web/updates/2016/01/mediarecorder
195
- const canvasCache = this.canvas
196
- // record on a temporary canvas context
197
- this._canvas = document.createElement('canvas')
198
- this.canvas.width = canvasCache.width
199
- this.canvas.height = canvasCache.height
200
- this._cctx = this.canvas.getContext('2d')
201
-
202
- const recordedChunks = [] // frame blobs
203
- // combine image + audio, or just pick one
204
- let tracks = []
205
- if (options.video !== false) {
206
- const visualStream = this.canvas.captureStream(framerate)
207
- tracks = tracks.concat(visualStream.getTracks())
208
- }
209
- // Check if there's a layer that's an instance of a Media mixin (Audio or Video)
210
- const hasMediaTracks = this.layers.some(layer => layer instanceof AudioLayer || layer instanceof VideoLayer)
211
- // If no media tracks present, don't include an audio stream, because Chrome doesn't record silence
212
- // when an audio stream is present.
213
- if (hasMediaTracks && options.audio !== false) {
214
- const audioDestination = this.actx.createMediaStreamDestination()
215
- const audioStream = audioDestination.stream
216
- tracks = tracks.concat(audioStream.getTracks())
217
- this.publishToLayers('movie.audiodestinationupdate', { movie: this, destination: audioDestination })
218
- }
219
- const stream = new MediaStream(tracks)
220
- const mediaRecorder = new MediaRecorder(stream, options.mediaRecorderOptions)
221
- // TODO: publish to movie, not layers
222
- mediaRecorder.ondataavailable = event => {
223
- // if (this._paused) reject(new Error("Recording was interrupted"));
224
- if (event.data.size > 0) {
225
- recordedChunks.push(event.data)
226
- }
227
- }
228
- mediaRecorder.onstop = () => {
229
- this._ended = true
230
- this._canvas = canvasCache
231
- this._cctx = this.canvas.getContext('2d')
232
- this.publishToLayers(
233
- 'movie.audiodestinationupdate',
234
- { movie: this, destination: this.actx.destination }
235
- )
236
- this._mediaRecorder = null
237
- // construct super-blob
238
- // this is the exported video as a blob!
239
- resolve(
240
- new Blob(recordedChunks, {
241
- type: options.type || 'video/webm'
242
- }/*, {"type" : "audio/ogg; codecs=opus"} */)
243
- )
244
- }
245
- mediaRecorder.onerror = reject
246
-
247
- mediaRecorder.start()
248
- this._mediaRecorder = mediaRecorder
249
- this.play()
250
- publish(this, 'movie.record', { options })
251
- })
252
- }
253
-
254
- /**
255
- * Stops the movie, without reseting the playback position
256
- * @return {Movie} the movie (for chaining)
257
- */
258
- pause () {
259
- this._paused = true
260
- // disable all layers
261
- for (let i = 0; i < this.layers.length; i++) {
262
- const layer = this.layers[i]
263
- layer.stop(this.currentTime - layer.startTime)
264
- layer._active = false
265
- }
266
- publish(this, 'movie.pause', {})
267
- return this
268
- }
269
-
270
- /**
271
- * Stops playback and resets the playback position
272
- * @return {Movie} the movie (for chaining)
273
- */
274
- stop () {
275
- this.pause()
276
- this.currentTime = 0 // use setter?
277
- return this
278
- }
279
-
280
- /**
281
- * @param {number} [timestamp=performance.now()]
282
- * @param {function} [done=undefined] - called when done playing or when the current frame is loaded
283
- * @private
284
- */
285
- _render (repeat, timestamp = performance.now(), done = undefined) {
286
- clearCachedValues(this)
287
-
288
- if (!this.rendering) {
289
- // (!this.paused || this._renderingFrame) is true (it's playing or it's rendering a single frame)
290
- done && done()
291
- return
292
- }
293
-
294
- this._updateCurrentTime(timestamp)
295
- // bad for performance? (remember, it's calling Array.reduce)
296
- const end = this.duration
297
- const ended = this.currentTime >= end
298
- if (ended) {
299
- publish(this, 'movie.ended', { movie: this, repeat: this.repeat })
300
- this._currentTime = 0 // don't use setter
301
- publish(this, 'movie.timeupdate', { movie: this })
302
- this._lastPlayed = performance.now()
303
- this._lastPlayedOffset = 0 // this.currentTime
304
- this._renderingFrame = false
305
- if (!this.repeat || this.recording) {
306
- this._ended = true
307
- // disable all layers
308
- for (let i = 0; i < this.layers.length; i++) {
309
- const layer = this.layers[i]
310
- layer.stop(this.currentTime - layer.startTime)
311
- layer._active = false
312
- }
313
- }
314
- done && done()
315
- return
316
- }
317
-
318
- // do render
319
- this._renderBackground(timestamp)
320
- const frameFullyLoaded = this._renderLayers(timestamp)
321
- this._applyEffects()
322
-
323
- if (frameFullyLoaded) {
324
- publish(this, 'movie.loadeddata', { movie: this })
325
- }
326
-
327
- // if instant didn't load, repeatedly frame-render until frame is loaded
328
- // if the expression below is false, don't publish an event, just silently stop render loop
329
- if (!repeat || (this._renderingFrame && frameFullyLoaded)) {
330
- this._renderingFrame = false
331
- done && done()
332
- return
333
- }
334
-
335
- window.requestAnimationFrame(timestamp => {
336
- this._render(repeat, timestamp)
337
- }) // TODO: research performance cost
338
- }
339
-
340
- _updateCurrentTime (timestamp) {
341
- // if we're only instant-rendering (current frame only), it doens't matter if it's paused or not
342
- if (!this._renderingFrame) {
343
- // if ((timestamp - this._lastUpdate) >= this._updateInterval) {
344
- const sinceLastPlayed = (timestamp - this._lastPlayed) / 1000
345
- this._currentTime = this._lastPlayedOffset + sinceLastPlayed // don't use setter
346
- publish(this, 'movie.timeupdate', { movie: this })
347
- // this._lastUpdate = timestamp;
348
- // }
349
- }
350
- }
351
-
352
- _renderBackground (timestamp) {
353
- this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
354
- if (this.background) { // TODO: check valued result
355
- this.cctx.fillStyle = val(this, 'background', timestamp)
356
- this.cctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
357
- }
358
- }
359
-
360
- /**
361
- * @return {boolean} whether or not video frames are loaded
362
- * @param {number} [timestamp=performance.now()]
363
- * @private
364
- */
365
- _renderLayers (timestamp) {
366
- let frameFullyLoaded = true
367
- for (let i = 0; i < this.layers.length; i++) {
368
- const layer = this.layers[i]
369
- const reltime = this.currentTime - layer.startTime
370
- // Cancel operation if layer disabled or outside layer time interval
371
- if (!val(layer, 'enabled', reltime) ||
372
- // > or >= ?
373
- this.currentTime < layer.startTime || this.currentTime > layer.startTime + layer.duration) {
374
- // outside time interval
375
- // if only rendering this frame (instant==true), we are not "starting" the layer
376
- if (layer.active && !this._renderingFrame) {
377
- // TODO: make a `deactivate()` method?
378
- // console.log("stop");
379
- layer.stop(reltime)
380
- layer._active = false
381
- }
382
- continue
383
- }
384
- // if only rendering this frame, we are not "starting" the layer
385
- if (!layer.active && val(layer, 'enabled', reltime) && !this._renderingFrame) {
386
- // TODO: make an `activate()` method?
387
- // console.log("start");
388
- layer.start(reltime)
389
- layer._active = true
390
- }
391
-
392
- if (layer.media) {
393
- frameFullyLoaded = frameFullyLoaded && layer.media.readyState >= 2
394
- } // frame loaded
395
- layer.render(reltime) // pass relative time for convenience
396
-
397
- // if the layer has visual component
398
- if (layer.canvas) {
399
- // layer.canvas.width and layer.canvas.height should already be interpolated
400
- // if the layer has an area (else InvalidStateError from canvas)
401
- if (layer.canvas.width * layer.canvas.height > 0) {
402
- this.cctx.drawImage(layer.canvas,
403
- val(layer, 'x', reltime), val(layer, 'y', reltime), layer.canvas.width, layer.canvas.height
404
- )
405
- }
406
- }
407
- }
408
-
409
- return frameFullyLoaded
410
- }
411
-
412
- _applyEffects () {
413
- for (let i = 0; i < this.effects.length; i++) {
414
- const effect = this.effects[i]
415
- effect.apply(this, this.currentTime)
416
- }
417
- }
418
-
419
- /**
420
- * Refreshes the screen (only use this if auto-refresh is disabled)
421
- * @return {Promise} - resolves when the frame is loaded
422
- */
423
- refresh () {
424
- return new Promise((resolve, reject) => {
425
- this._renderingFrame = true
426
- this._render(false, undefined, resolve)
427
- })
428
- }
429
-
430
- /**
431
- * Convienence method
432
- * @todo Make private
433
- */
434
- publishToLayers (type, event) {
435
- for (let i = 0; i < this.layers.length; i++) {
436
- publish(this.layers[i], type, event)
437
- }
438
- }
439
-
440
- /**
441
- * If the movie is playing, recording or refreshing
442
- * @type boolean
443
- */
444
- get rendering () {
445
- return !this.paused || this._renderingFrame
446
- }
447
-
448
- /**
449
- * If the movie is refreshing current frame
450
- * @type boolean
451
- */
452
- get renderingFrame () {
453
- return this._renderingFrame
454
- }
455
-
456
- /**
457
- * If the movie is recording
458
- * @type boolean
459
- */
460
- get recording () {
461
- return !!this._mediaRecorder
462
- }
463
-
464
- /**
465
- * The combined duration of all layers
466
- * @type number
467
- */
468
- get duration () { // TODO: dirty flag?
469
- return this.layers.reduce((end, layer) => Math.max(layer.startTime + layer.duration, end), 0)
470
- }
471
-
472
- /**
473
- * @type layer.Base[]
474
- */
475
- get layers () {
476
- return this._layers
477
- }
478
-
479
- // (proxy)
480
- /**
481
- * Convienence method for <code>layers.push()</code>
482
- * @param {BaseLayer} layer
483
- * @return {Movie} the movie (for chaining)
484
- */
485
- addLayer (layer) {
486
- this.layers.push(layer); return this
487
- }
488
-
489
- /**
490
- * @type effect.Base[]
491
- */
492
- get effects () {
493
- return this._effects // private (because it's a proxy)
494
- }
495
-
496
- /**
497
- * Convienence method for <code>effects.push()</code>
498
- * @param {BaseEffect} effect
499
- * @return {Movie} the movie (for chaining)
500
- */
501
- addEffect (effect) {
502
- this.effects.push(effect); return this
503
- }
504
-
505
- /**
506
- * @type boolean
507
- */
508
- get paused () {
509
- return this._paused
510
- }
511
-
512
- /**
513
- * If the playback position is at the end of the movie
514
- * @type boolean
515
- */
516
- get ended () {
517
- return this._ended
518
- }
519
-
520
- /**
521
- * The current playback position
522
- * @type number
523
- */
524
- get currentTime () {
525
- return this._currentTime
526
- }
527
-
528
- /**
529
- * Sets the current playback position. This is a more powerful version of `set currentTime`.
530
- *
531
- * @param {number} time - the new cursor's time value in seconds
532
- * @param {boolean} [refresh=true] - whether to render a single frame to match new time or not
533
- * @return {Promise} resolves when the current frame is rendered if <code>refresh</code> is true,
534
- * otherwise resolves immediately
535
- *
536
- * @todo Refresh ionly f auto-refreshing is enabled
537
- */
538
- setCurrentTime (time, refresh = true) {
539
- return new Promise((resolve, reject) => {
540
- this._currentTime = time
541
- publish(this, 'movie.seek', {})
542
- if (refresh) {
543
- // pass promise callbacks to `refresh`
544
- this.refresh().then(resolve).catch(reject)
545
- } else {
546
- resolve()
547
- }
548
- })
549
- }
550
-
551
- set currentTime (time) {
552
- this._currentTime = time
553
- publish(this, 'movie.seek', {})
554
- this.refresh() // render single frame to match new time
555
- }
556
-
557
- /**
558
- * The rendering canvas
559
- * @type HTMLCanvasElement
560
- */
561
- get canvas () {
562
- return this._canvas
563
- }
564
-
565
- /**
566
- * The rendering canvas's context
567
- * @type CanvasRenderingContext2D
568
- */
569
- get cctx () {
570
- return this._cctx
571
- }
572
-
573
- /**
574
- * The audio context to which audio is played
575
- * @type BaseAudioContext
576
- */
577
- get actx () {
578
- return this._actx
579
- }
580
-
581
- /**
582
- * The width of the rendering canvas
583
- * @type number
584
- */
585
- get width () {
586
- return this.canvas.width
587
- }
588
-
589
- /**
590
- * The height of the rendering canvas
591
- * @type number
592
- */
593
- get height () {
594
- return this.canvas.height
595
- }
596
-
597
- set width (width) {
598
- this.canvas.width = width
599
- }
600
-
601
- set height (height) {
602
- this.canvas.height = height
603
- }
604
-
605
- get movie () {
606
- return this
607
- }
608
-
609
- getDefaultOptions () {
610
- return {
611
- _actx: new AudioContext(),
612
- /**
613
- * @name module:movie#background
614
- * @type string
615
- * @desc The css color for the background, or <code>null</code> for transparency
616
- */
617
- background: '#000',
618
- /**
619
- * @name module:movie#repeat
620
- * @type boolean
621
- */
622
- repeat: false,
623
- /**
624
- * @name module:movie#autoRefresh
625
- * @type boolean
626
- * @desc Whether to refresh when changes are made that would effect the current frame
627
- */
628
- autoRefresh: true
629
- }
630
- }
631
- }
632
-
633
- // id for events (independent of instance, but easy to access when on prototype chain)
634
- Movie.prototype.type = 'movie'
635
- // TODO: refactor so we don't need to explicitly exclude some of these
636
- Movie.prototype.publicExcludes = ['canvas', 'cctx', 'actx', 'layers', 'effects']
637
- Movie.prototype.propertyFilters = {}