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