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