etro 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/CONTRIBUTING.md +25 -34
  3. package/README.md +9 -17
  4. package/dist/custom-array.d.ts +10 -0
  5. package/dist/effect/base.d.ts +10 -1
  6. package/dist/effect/shader.d.ts +11 -1
  7. package/dist/effect/stack.d.ts +6 -2
  8. package/dist/etro-cjs.js +1182 -592
  9. package/dist/etro-iife.js +1182 -592
  10. package/dist/event.d.ts +10 -5
  11. package/dist/layer/audio-source.d.ts +9 -4
  12. package/dist/layer/audio.d.ts +15 -2
  13. package/dist/layer/base.d.ts +49 -3
  14. package/dist/layer/image.d.ts +15 -1
  15. package/dist/layer/text.d.ts +6 -3
  16. package/dist/layer/video.d.ts +13 -1
  17. package/dist/layer/visual-source.d.ts +18 -3
  18. package/dist/layer/visual.d.ts +11 -7
  19. package/dist/movie/effects.d.ts +6 -0
  20. package/dist/movie/index.d.ts +1 -0
  21. package/dist/movie/layers.d.ts +6 -0
  22. package/dist/movie/movie.d.ts +260 -0
  23. package/dist/object.d.ts +9 -2
  24. package/dist/util.d.ts +4 -10
  25. package/eslint.conf.js +4 -2
  26. package/eslint.test-conf.js +1 -2
  27. package/karma.conf.js +10 -14
  28. package/package.json +23 -22
  29. package/scripts/{gen-effect-samples.html → effect/gen-effect-samples.html} +24 -0
  30. package/scripts/{save-effect-samples.js → effect/save-effect-samples.js} +1 -1
  31. package/src/custom-array.ts +43 -0
  32. package/src/effect/base.ts +23 -22
  33. package/src/effect/gaussian-blur.ts +11 -6
  34. package/src/effect/pixelate.ts +3 -3
  35. package/src/effect/shader.ts +33 -27
  36. package/src/effect/stack.ts +43 -30
  37. package/src/effect/transform.ts +16 -9
  38. package/src/event.ts +111 -21
  39. package/src/layer/audio-source.ts +60 -20
  40. package/src/layer/audio.ts +25 -3
  41. package/src/layer/base.ts +79 -25
  42. package/src/layer/image.ts +26 -2
  43. package/src/layer/text.ts +11 -4
  44. package/src/layer/video.ts +31 -4
  45. package/src/layer/visual-source.ts +70 -8
  46. package/src/layer/visual.ts +57 -35
  47. package/src/movie/effects.ts +26 -0
  48. package/src/movie/index.ts +1 -0
  49. package/src/movie/layers.ts +26 -0
  50. package/src/movie/movie.ts +855 -0
  51. package/src/object.ts +9 -2
  52. package/src/util.ts +68 -89
  53. package/tsconfig.json +3 -1
  54. package/dist/movie.d.ts +0 -201
  55. package/examples/application/readme-screenshot.html +0 -85
  56. package/examples/application/video-player.html +0 -130
  57. package/examples/application/webcam.html +0 -28
  58. package/examples/introduction/audio.html +0 -64
  59. package/examples/introduction/effects.html +0 -79
  60. package/examples/introduction/export.html +0 -83
  61. package/examples/introduction/functions.html +0 -37
  62. package/examples/introduction/hello-world1.html +0 -37
  63. package/examples/introduction/hello-world2.html +0 -32
  64. package/examples/introduction/keyframes.html +0 -79
  65. package/examples/introduction/media.html +0 -63
  66. package/examples/introduction/text.html +0 -31
  67. package/private-todo.txt +0 -70
  68. package/src/movie.ts +0 -742
package/src/movie.ts DELETED
@@ -1,742 +0,0 @@
1
- /**
2
- * @module movie
3
- */
4
-
5
- import { subscribe, publish } from './event'
6
- import { Dynamic, val, clearCachedValues, applyOptions, watchPublic } from './util'
7
- import { Base as BaseLayer, Audio as AudioLayer, Video as VideoLayer, Visual } from './layer/index' // `Media` mixins
8
- import { AudioSource } from './layer/audio-source' // not exported from ./layer/index
9
- import { Base as BaseEffect } from './effect/index'
10
-
11
- declare global {
12
- interface Window {
13
- webkitAudioContext: typeof AudioContext
14
- }
15
-
16
- interface HTMLCanvasElement {
17
- captureStream(frameRate?: number): MediaStream
18
- }
19
- }
20
-
21
- export class MovieOptions {
22
- /** The html canvas element to use for playback */
23
- canvas: HTMLCanvasElement
24
- /** The audio context to use for playback, defaults to a new audio context */
25
- actx?: AudioContext
26
- /** @deprecated Use <code>actx</code> instead */
27
- audioContext?: AudioContext
28
- /** The background color of the movie as a cSS string */
29
- background?: Dynamic<string>
30
- repeat?: boolean
31
- /** Call `refresh` when the user changes a property on the movie or any of its layers or effects */
32
- autoRefresh?: boolean
33
- }
34
-
35
- /**
36
- * The movie contains everything included in the render.
37
- *
38
- * Implements a pub/sub system.
39
- */
40
- // TODO: Implement event "durationchange", and more
41
- // TODO: Add width and height options
42
- // TODO: Make record option to make recording video output to the user while
43
- // it's recording
44
- // TODO: rename renderingFrame -> refreshing
45
- export class Movie {
46
- type: string
47
- /**
48
- * @deprecated Auto-refresh will be removed in the future (see issue #130).
49
- */
50
- publicExcludes: string[]
51
- propertyFilters: Record<string, <T>(value: T) => T>
52
-
53
- repeat: boolean
54
- /**
55
- * Call `refresh` when the user changes a property on the movie or any of its
56
- * layers or effects
57
- *
58
- * @deprecated Auto-refresh will be removed in the future. If you want to
59
- * refresh the canvas, call `refresh`. See issue #130.
60
- */
61
- autoRefresh: boolean
62
- /** The background color of the movie as a cSS string */
63
- background: Dynamic<string>
64
- /** The audio context to which audio output is sent during playback */
65
- readonly actx: AudioContext
66
- // Readonly because it's a proxy (so it can't be overwritten).
67
- readonly effects: BaseEffect[]
68
- // Readonly because it's a proxy (so it can't be overwritten).
69
- readonly layers: BaseLayer[]
70
-
71
- private _canvas: HTMLCanvasElement;
72
- private _cctx: CanvasRenderingContext2D
73
- private _effectsBack: BaseEffect[]
74
- private _layersBack: BaseLayer[]
75
- private _currentTime: number
76
- private _paused: boolean
77
- private _ended: boolean
78
- private _renderingFrame: boolean
79
- private _recordEndTime: number
80
- private _mediaRecorder: MediaRecorder
81
- private _lastPlayed: number
82
- private _lastPlayedOffset: number
83
-
84
- /**
85
- * Creates a new movie.
86
- */
87
- constructor (options: MovieOptions) {
88
- // TODO: move into multiple methods!
89
- // Set actx option manually, because it's readonly.
90
- this.actx = options.actx ||
91
- options.audioContext ||
92
- new AudioContext() ||
93
- // eslint-disable-next-line new-cap
94
- new window.webkitAudioContext()
95
- delete options.actx
96
-
97
- // Proxy that will be returned by constructor
98
- const newThis: Movie = watchPublic(this) as Movie
99
- // Set canvas option manually, because it's readonly.
100
- this._canvas = options.canvas
101
- delete options.canvas
102
- // Don't send updates when initializing, so use this instead of newThis:
103
- this._cctx = this.canvas.getContext('2d') // TODO: make private?
104
- applyOptions(options, this)
105
-
106
- const that: Movie = newThis
107
-
108
- this._effectsBack = []
109
- this.effects = new Proxy(newThis._effectsBack, {
110
- deleteProperty (target, property): boolean {
111
- // Refresh screen when effect is removed, if the movie isn't playing
112
- // already.
113
- const value = target[property]
114
- value.tryDetach()
115
- delete target[property]
116
- publish(that, 'movie.change.effect.remove', { effect: value })
117
- return true
118
- },
119
- set (target, property, value): boolean {
120
- // Check if property is an number (an index)
121
- if (!isNaN(Number(property))) {
122
- if (target[property]) {
123
- publish(that, 'movie.change.effect.remove', {
124
- effect: target[property]
125
- })
126
- target[property].tryDetach()
127
- }
128
- // Attach effect to movie
129
- value.tryAttach(that)
130
- target[property] = value
131
- // Refresh screen when effect is set, if the movie isn't playing
132
- // already.
133
- publish(that, 'movie.change.effect.add', { effect: value })
134
- } else {
135
- target[property] = value
136
- }
137
-
138
- return true
139
- }
140
- })
141
-
142
- this._layersBack = []
143
- this.layers = new Proxy(newThis._layersBack, {
144
- deleteProperty (target, property): boolean {
145
- const oldDuration = this.duration
146
- const value = target[property]
147
- value.tryDetach(that)
148
- delete target[property]
149
- const current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration
150
- if (current)
151
- publish(that, 'movie.change.layer.remove', { layer: value })
152
-
153
- publish(that, 'movie.change.duration', { oldDuration })
154
- return true
155
- },
156
- set (target, property, value): boolean {
157
- const oldDuration = this.duration
158
- // Check if property is an number (an index)
159
- if (!isNaN(Number(property))) {
160
- if (target[property]) {
161
- publish(that, 'movie.change.layer.remove', {
162
- layer: target[property]
163
- })
164
- target[property].tryDetach()
165
- }
166
- // Attach layer to movie
167
- value.tryAttach(that)
168
- target[property] = value
169
- // Refresh screen when a relevant layer is added or removed
170
- const current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration
171
- if (current)
172
- publish(that, 'movie.change.layer.add', { layer: value })
173
-
174
- publish(that, 'movie.change.duration', { oldDuration })
175
- } else {
176
- target[property] = value
177
- }
178
-
179
- return true
180
- }
181
- })
182
- this._paused = true
183
- this._ended = false
184
- // This variable helps prevent multiple frame-rendering loops at the same
185
- // time (see `render`). It's only applicable when rendering.
186
- this._renderingFrame = false
187
- this.currentTime = 0
188
-
189
- // For recording
190
- this._mediaRecorder = null
191
-
192
- // -1 works well in inequalities
193
- // The last time `play` was called
194
- this._lastPlayed = -1
195
- // What was `currentTime` when `play` was called
196
- this._lastPlayedOffset = -1
197
- // newThis._updateInterval = 0.1; // time in seconds between each "timeupdate" event
198
- // newThis._lastUpdate = -1;
199
-
200
- if (newThis.autoRefresh)
201
- newThis.refresh() // render single frame on creation
202
-
203
- // Subscribe to own event "change" (child events propogate up)
204
- subscribe(newThis, 'movie.change', () => {
205
- if (newThis.autoRefresh && !newThis.rendering)
206
- newThis.refresh()
207
- })
208
-
209
- // Subscribe to own event "ended"
210
- subscribe(newThis, 'movie.recordended', () => {
211
- if (newThis.recording) {
212
- newThis._mediaRecorder.requestData()
213
- newThis._mediaRecorder.stop()
214
- }
215
- })
216
-
217
- return newThis
218
- }
219
-
220
- /**
221
- * Plays the movie
222
- * @return fulfilled when the movie is done playing, never fails
223
- */
224
- play (): Promise<void> {
225
- return new Promise(resolve => {
226
- if (!this.paused)
227
- throw new Error('Already playing')
228
-
229
- this._paused = this._ended = false
230
- this._lastPlayed = performance.now()
231
- this._lastPlayedOffset = this.currentTime
232
-
233
- if (!this.renderingFrame)
234
- // Not rendering (and not playing), so play.
235
- this._render(true, undefined, resolve)
236
-
237
- // Stop rendering frame if currently doing so, because playing has higher
238
- // priority. This will effect the next _render call.
239
- this._renderingFrame = false
240
-
241
- publish(this, 'movie.play', {})
242
- })
243
- }
244
-
245
- /**
246
- * Plays the movie in the background and records it
247
- *
248
- * @param options
249
- * @param frameRate
250
- * @param [options.video=true] - whether to include video in recording
251
- * @param [options.audio=true] - whether to include audio in recording
252
- * @param [options.mediaRecorderOptions=undefined] - options to pass to the <code>MediaRecorder</code>
253
- * @param [options.type='video/webm'] - MIME type for exported video
254
- * constructor
255
- * @return resolves when done recording, rejects when internal media recorder errors
256
- */
257
- // TEST: *support recording that plays back with audio!*
258
- // TODO: figure out how to do offline recording (faster than realtime).
259
- // TODO: improve recording performance to increase frame rate?
260
- record (options: {
261
- frameRate: number,
262
- duration?: number,
263
- type?: string,
264
- video?: boolean,
265
- audio?: boolean,
266
- mediaRecorderOptions?: Record<string, unknown>
267
- }): Promise<Blob> {
268
- if (options.video === false && options.audio === false)
269
- throw new Error('Both video and audio cannot be disabled')
270
-
271
- if (!this.paused)
272
- throw new Error('Cannot record movie while already playing or recording')
273
-
274
- const mimeType = options.type || 'video/webm'
275
- if (MediaRecorder && MediaRecorder.isTypeSupported && !MediaRecorder.isTypeSupported(mimeType))
276
- throw new Error('Please pass a valid MIME type for the exported video')
277
-
278
- return new Promise((resolve, reject) => {
279
- const canvasCache = this.canvas
280
- // Record on a temporary canvas context
281
- this._canvas = document.createElement('canvas')
282
- this.canvas.width = canvasCache.width
283
- this.canvas.height = canvasCache.height
284
- this._cctx = this.canvas.getContext('2d')
285
-
286
- // frame blobs
287
- const recordedChunks = []
288
- // Combine image + audio, or just pick one
289
- let tracks = []
290
- if (options.video !== false) {
291
- const visualStream = this.canvas.captureStream(options.frameRate)
292
- tracks = tracks.concat(visualStream.getTracks())
293
- }
294
- // Check if there's a layer that's an instance of an AudioSourceMixin
295
- // (Audio or Video)
296
- const hasMediaTracks = this.layers.some(layer => layer instanceof AudioLayer || layer instanceof VideoLayer)
297
- // If no media tracks present, don't include an audio stream, because
298
- // Chrome doesn't record silence when an audio stream is present.
299
- if (hasMediaTracks && options.audio !== false) {
300
- const audioDestination = this.actx.createMediaStreamDestination()
301
- const audioStream = audioDestination.stream
302
- tracks = tracks.concat(audioStream.getTracks())
303
- publish(this, 'movie.audiodestinationupdate',
304
- { movie: this, destination: audioDestination }
305
- )
306
- }
307
- const stream = new MediaStream(tracks)
308
- const mediaRecorderOptions = {
309
- ...(options.mediaRecorderOptions || {}),
310
- mimeType
311
- }
312
- const mediaRecorder = new MediaRecorder(stream, mediaRecorderOptions)
313
- mediaRecorder.ondataavailable = event => {
314
- // if (this._paused) reject(new Error("Recording was interrupted"));
315
- if (event.data.size > 0)
316
- recordedChunks.push(event.data)
317
- }
318
- // TODO: publish to movie, not layers
319
- mediaRecorder.onstop = () => {
320
- this._paused = true
321
- this._ended = true
322
- this._canvas = canvasCache
323
- this._cctx = this.canvas.getContext('2d')
324
- publish(this, 'movie.audiodestinationupdate',
325
- { movie: this, destination: this.actx.destination }
326
- )
327
- this._mediaRecorder = null
328
- // Construct the exported video out of all the frame blobs.
329
- resolve(
330
- new Blob(recordedChunks, {
331
- type: mimeType
332
- })
333
- )
334
- }
335
- mediaRecorder.onerror = reject
336
-
337
- mediaRecorder.start()
338
- this._mediaRecorder = mediaRecorder
339
- this._recordEndTime = options.duration ? this.currentTime + options.duration : this.duration
340
- this.play()
341
- publish(this, 'movie.record', { options })
342
- })
343
- }
344
-
345
- /**
346
- * Stops the movie, without reseting the playback position
347
- * @return the movie (for chaining)
348
- */
349
- pause (): Movie {
350
- this._paused = true
351
- // Deactivate all layers
352
- for (let i = 0; i < this.layers.length; i++)
353
- if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
354
- const layer = this.layers[i]
355
- layer.stop()
356
- layer.active = false
357
- }
358
-
359
- publish(this, 'movie.pause', {})
360
- return this
361
- }
362
-
363
- /**
364
- * Stops playback and resets the playback position
365
- * @return the movie (for chaining)
366
- */
367
- stop (): Movie {
368
- this.pause()
369
- this.currentTime = 0
370
- return this
371
- }
372
-
373
- /**
374
- * @param [timestamp=performance.now()]
375
- * @param [done=undefined] - called when done playing or when the current frame is loaded
376
- * @private
377
- */
378
- private _render (repeat, timestamp = performance.now(), done = undefined) {
379
- clearCachedValues(this)
380
-
381
- if (!this.rendering) {
382
- // (!this.paused || this._renderingFrame) is true so it's playing or it's
383
- // rendering a single frame.
384
- if (done)
385
- done()
386
-
387
- return
388
- }
389
-
390
- this._updateCurrentTime(timestamp)
391
-
392
- // TODO: Is calling duration every frame bad for performance? (remember,
393
- // it's calling Array.reduce)
394
- const end = this.recording ? this._recordEndTime : this.duration
395
- if (this.currentTime > end) {
396
- if (this.recording)
397
- publish(this, 'movie.recordended', { movie: this })
398
-
399
- if (this.currentTime > this.duration)
400
- publish(this, 'movie.ended', { movie: this, repeat: this.repeat })
401
-
402
- // TODO: only reset currentTime if repeating
403
- if (this.repeat) {
404
- // Don't use setter, which publishes 'movie.seek'. Instead, update the
405
- // value and publish a 'movie.timeupdate' event.
406
- this._currentTime = 0
407
- publish(this, 'movie.timeupdate', { movie: this })
408
- }
409
-
410
- this._lastPlayed = performance.now()
411
- this._lastPlayedOffset = 0 // this.currentTime
412
- this._renderingFrame = false
413
-
414
- // Stop playback or recording if done (except if it's playing and repeat
415
- // is true)
416
- if (!(!this.recording && this.repeat)) {
417
- this._paused = true
418
- this._ended = true
419
- // Deactivate all layers
420
- for (let i = 0; i < this.layers.length; i++)
421
- if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
422
- const layer = this.layers[i]
423
- // A layer that has been deleted before layers.length has been updated
424
- // (see the layers proxy in the constructor).
425
- if (!layer || !layer.active)
426
- continue
427
-
428
- layer.stop()
429
- layer.active = false
430
- }
431
-
432
- if (done)
433
- done()
434
-
435
- return
436
- }
437
- }
438
-
439
- // Do render
440
- this._renderBackground(timestamp)
441
- const frameFullyLoaded = this._renderLayers()
442
- this._applyEffects()
443
-
444
- if (frameFullyLoaded)
445
- publish(this, 'movie.loadeddata', { movie: this })
446
-
447
- // If didn't load in this instant, repeatedly frame-render until frame is
448
- // loaded.
449
- // If the expression below is false, don't publish an event, just silently
450
- // stop render loop.
451
- if (!repeat || (this._renderingFrame && frameFullyLoaded)) {
452
- this._renderingFrame = false
453
- if (done)
454
- done()
455
-
456
- return
457
- }
458
-
459
- window.requestAnimationFrame(() => {
460
- this._render(repeat, undefined, done)
461
- }) // TODO: research performance cost
462
- }
463
-
464
- private _updateCurrentTime (timestamp) {
465
- // If we're only instant-rendering (current frame only), it doens't matter
466
- // if it's paused or not.
467
- if (!this._renderingFrame) {
468
- // if ((timestamp - this._lastUpdate) >= this._updateInterval) {
469
- const sinceLastPlayed = (timestamp - this._lastPlayed) / 1000
470
- const currentTime = this._lastPlayedOffset + sinceLastPlayed // don't use setter
471
- if (this.currentTime !== currentTime) {
472
- this._currentTime = currentTime
473
- publish(this, 'movie.timeupdate', { movie: this })
474
- }
475
- // this._lastUpdate = timestamp;
476
- // }
477
- }
478
- }
479
-
480
- private _renderBackground (timestamp) {
481
- this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
482
- const background = val(this, 'background', timestamp)
483
- if (background) { // TODO: check val'd result
484
- this.cctx.fillStyle = background
485
- this.cctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
486
- }
487
- }
488
-
489
- /**
490
- * @return whether or not video frames are loaded
491
- * @param [timestamp=performance.now()]
492
- * @private
493
- */
494
- private _renderLayers () {
495
- let frameFullyLoaded = true
496
- for (let i = 0; i < this.layers.length; i++) {
497
- if (!Object.prototype.hasOwnProperty.call(this.layers, i)) continue
498
-
499
- const layer = this.layers[i]
500
- // A layer that has been deleted before layers.length has been updated
501
- // (see the layers proxy in the constructor).
502
- if (!layer)
503
- continue
504
-
505
- const reltime = this.currentTime - layer.startTime
506
- // Cancel operation if layer disabled or outside layer time interval
507
- if (!val(layer, 'enabled', reltime) ||
508
- // TODO > or >= ?
509
- this.currentTime < layer.startTime || this.currentTime > layer.startTime + layer.duration) {
510
- // Layer is not active.
511
- // If only rendering this frame, we are not "starting" the layer.
512
- if (layer.active && !this._renderingFrame) {
513
- // TODO: make a `deactivate()` method?
514
- layer.stop()
515
- layer.active = false
516
- }
517
- continue
518
- }
519
- // If only rendering this frame, we are not "starting" the layer
520
- if (!layer.active && val(layer, 'enabled', reltime) && !this._renderingFrame) {
521
- // TODO: make an `activate()` method?
522
- layer.start()
523
- layer.active = true
524
- }
525
-
526
- // if the layer has an input file
527
- if ('source' in layer)
528
- frameFullyLoaded = frameFullyLoaded && (layer as unknown as AudioSource).source.readyState >= 2
529
-
530
- layer.render()
531
-
532
- // if the layer has visual component
533
- if (layer instanceof Visual) {
534
- const canvas = (layer as Visual).canvas
535
- // layer.canvas.width and layer.canvas.height should already be interpolated
536
- // if the layer has an area (else InvalidStateError from canvas)
537
- if (canvas.width * canvas.height > 0)
538
- this.cctx.drawImage(canvas,
539
- val(layer, 'x', reltime), val(layer, 'y', reltime), canvas.width, canvas.height
540
- )
541
- }
542
- }
543
-
544
- return frameFullyLoaded
545
- }
546
-
547
- private _applyEffects () {
548
- for (let i = 0; i < this.effects.length; i++) {
549
- const effect = this.effects[i]
550
- // An effect that has been deleted before effects.length has been updated
551
- // (see the effectsproxy in the constructor).
552
- if (!effect)
553
- continue
554
-
555
- effect.apply(this, this.currentTime)
556
- }
557
- }
558
-
559
- /**
560
- * Refreshes the screen (only use this if auto-refresh is disabled)
561
- * @return - resolves when the frame is loaded
562
- */
563
- refresh (): Promise<null> {
564
- return new Promise(resolve => {
565
- this._renderingFrame = true
566
- this._render(false, undefined, resolve)
567
- })
568
- }
569
-
570
- /**
571
- * Convienence method
572
- */
573
- private _publishToLayers (type, event) {
574
- for (let i = 0; i < this.layers.length; i++)
575
- if (Object.prototype.hasOwnProperty.call(this.layers, i))
576
- publish(this.layers[i], type, event)
577
- }
578
-
579
- /**
580
- * If the movie is playing, recording or refreshing
581
- */
582
- get rendering (): boolean {
583
- return !this.paused || this._renderingFrame
584
- }
585
-
586
- /**
587
- * If the movie is refreshing current frame
588
- */
589
- get renderingFrame (): boolean {
590
- return this._renderingFrame
591
- }
592
-
593
- /**
594
- * If the movie is recording
595
- */
596
- get recording (): boolean {
597
- return !!this._mediaRecorder
598
- }
599
-
600
- /**
601
- * The combined duration of all layers
602
- */
603
- // TODO: dirty flag?
604
- get duration (): number {
605
- return this.layers.reduce((end, layer) => Math.max(layer.startTime + layer.duration, end), 0)
606
- }
607
-
608
- /**
609
- * Convienence method for <code>layers.push()</code>
610
- * @param layer
611
- * @return the movie
612
- */
613
- addLayer (layer: BaseLayer): Movie {
614
- this.layers.push(layer); return this
615
- }
616
-
617
- /**
618
- * Convienence method for <code>effects.push()</code>
619
- * @param effect
620
- * @return the movie
621
- */
622
- addEffect (effect: BaseEffect): Movie {
623
- this.effects.push(effect); return this
624
- }
625
-
626
- /**
627
- */
628
- get paused (): boolean {
629
- return this._paused
630
- }
631
-
632
- /**
633
- * If the playback position is at the end of the movie
634
- */
635
- get ended (): boolean {
636
- return this._ended
637
- }
638
-
639
- /**
640
- * The current playback position
641
- */
642
- get currentTime (): number {
643
- return this._currentTime
644
- }
645
-
646
- set currentTime (time: number) {
647
- this._currentTime = time
648
- publish(this, 'movie.seek', {})
649
- // Render single frame to match new time
650
- if (this.autoRefresh)
651
- this.refresh()
652
- }
653
-
654
- /**
655
- * Sets the current playback position. This is a more powerful version of
656
- * `set currentTime`.
657
- *
658
- * @param time - the new cursor's time value in seconds
659
- * @param [refresh=true] - whether to render a single frame
660
- * @return resolves when the current frame is rendered if
661
- * <code>refresh</code> is true, otherwise resolves immediately
662
- *
663
- */
664
- // TODO: Refresh if only auto-refreshing is enabled
665
- setCurrentTime (time: number, refresh = true): Promise<void> {
666
- return new Promise((resolve, reject) => {
667
- this._currentTime = time
668
- publish(this, 'movie.seek', {})
669
- if (refresh)
670
- // Pass promise callbacks to `refresh`
671
- this.refresh().then(resolve).catch(reject)
672
- else
673
- resolve()
674
- })
675
- }
676
-
677
- /**
678
- * The rendering canvas
679
- */
680
- get canvas (): HTMLCanvasElement {
681
- return this._canvas
682
- }
683
-
684
- /**
685
- * The rendering canvas's context
686
- */
687
- get cctx (): CanvasRenderingContext2D {
688
- return this._cctx
689
- }
690
-
691
- /**
692
- * The width of the rendering canvas
693
- */
694
- get width (): number {
695
- return this.canvas.width
696
- }
697
-
698
- set width (width: number) {
699
- this.canvas.width = width
700
- }
701
-
702
- /**
703
- * The height of the rendering canvas
704
- */
705
- get height (): number {
706
- return this.canvas.height
707
- }
708
-
709
- set height (height: number) {
710
- this.canvas.height = height
711
- }
712
-
713
- get movie (): Movie {
714
- return this
715
- }
716
-
717
- getDefaultOptions (): MovieOptions {
718
- return {
719
- canvas: undefined, // required
720
- /**
721
- * @name module:movie#background
722
- * @desc The css color for the background, or <code>null</code> for transparency
723
- */
724
- background: '#000',
725
- /**
726
- * @name module:movie#repeat
727
- */
728
- repeat: false,
729
- /**
730
- * @name module:movie#autoRefresh
731
- * @desc Whether to refresh when changes are made that would effect the current frame
732
- */
733
- autoRefresh: true
734
- }
735
- }
736
- }
737
-
738
- // id for events (independent of instance, but easy to access when on prototype chain)
739
- Movie.prototype.type = 'movie'
740
- // TODO: refactor so we don't need to explicitly exclude some of these
741
- Movie.prototype.publicExcludes = ['canvas', 'cctx', 'actx', 'layers', 'effects']
742
- Movie.prototype.propertyFilters = {}