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/layer.js DELETED
@@ -1,897 +0,0 @@
1
- /**
2
- * @module layer
3
- * @todo Add aligning options, like horizontal and vertical align modes
4
- */
5
-
6
- import { publish, subscribe } from './event.js'
7
- import { watchPublic, val, applyOptions } from './util.js'
8
-
9
- /**
10
- * A layer is a piece of content for the movie
11
- */
12
- export class Base {
13
- /**
14
- * Creates a new empty layer
15
- *
16
- * @param {number} startTime - when to start the layer on the movie's timeline
17
- * @param {number} duration - how long the layer should last on the movie's timeline
18
- * @param {object} [options] - no options, here for consistency
19
- */
20
- constructor (startTime, duration, options = {}) { // rn, options isn't used but I'm keeping it here
21
- const newThis = watchPublic(this) // proxy that will be returned by constructor
22
- // Don't send updates when initializing, so use this instead of newThis:
23
- applyOptions(options, this) // no options rn, but just to stick to protocol
24
-
25
- this._startTime = startTime
26
- this._duration = duration
27
-
28
- this._active = false // whether newThis layer is currently being rendered
29
- this.enabled = true
30
-
31
- this._movie = null
32
-
33
- // Propogate up to target
34
- subscribe(newThis, 'layer.change', event => {
35
- const typeOfChange = event.type.substring(event.type.lastIndexOf('.') + 1)
36
- const type = `movie.change.layer.${typeOfChange}`
37
- publish(newThis._movie, type, { ...event, target: newThis._movie, type })
38
- })
39
-
40
- return newThis
41
- }
42
-
43
- attach (movie) {
44
- this._movie = movie
45
- }
46
-
47
- detach () {
48
- this._movie = null
49
- }
50
-
51
- /**
52
- * Called when the layer is activated
53
- */
54
- start () {}
55
-
56
- /**
57
- * Called when the movie renders and the layer is active
58
- */
59
- render () {}
60
-
61
- /**
62
- * Called when the layer is deactivated
63
- */
64
- stop () {}
65
-
66
- get parent () {
67
- return this._movie
68
- }
69
-
70
- /**
71
- * If the attached movie's playback position is in this layer
72
- * @type boolean
73
- */
74
- get active () {
75
- return this._active
76
- }
77
-
78
- /**
79
- * @type number
80
- */
81
- get startTime () {
82
- return this._startTime
83
- }
84
-
85
- set startTime (val) {
86
- this._startTime = val
87
- }
88
-
89
- /**
90
- * The current time of the movie relative to this layer
91
- * @type number
92
- */
93
- get currentTime () {
94
- return this._movie ? this._movie.currentTime - this.startTime
95
- : undefined
96
- }
97
-
98
- /**
99
- * @type number
100
- */
101
- get duration () {
102
- return this._duration
103
- }
104
-
105
- set duration (val) {
106
- this._duration = val
107
- }
108
-
109
- get movie () {
110
- return this._movie
111
- }
112
-
113
- getDefaultOptions () {
114
- return {}
115
- }
116
- }
117
- // id for events (independent of instance, but easy to access when on prototype chain)
118
- Base.prototype.type = 'layer'
119
- Base.prototype.publicExcludes = []
120
- Base.prototype.propertyFilters = {}
121
-
122
- /** Any layer that renders to a canvas */
123
- export class Visual extends Base {
124
- /**
125
- * Creates a visual layer
126
- *
127
- * @param {number} startTime - when to start the layer on the movie's timeline
128
- * @param {number} duration - how long the layer should last on the movie's timeline
129
- * @param {object} [options] - various optional arguments
130
- * @param {number} [options.width=null] - the width of the entire layer
131
- * @param {number} [options.height=null] - the height of the entire layer
132
- * @param {number} [options.x=0] - the offset of the layer relative to the movie
133
- * @param {number} [options.y=0] - the offset of the layer relative to the movie
134
- * @param {string} [options.background=null] - the background color of the layer, or <code>null</code>
135
- * for a transparent background
136
- * @param {object} [options.border=null] - the layer's outline, or <code>null</code> for no outline
137
- * @param {string} [options.border.color] - the outline's color; required for a border
138
- * @param {string} [options.border.thickness=1] - the outline's weight
139
- * @param {number} [options.opacity=1] - the layer's opacity; <code>1</cod> for full opacity
140
- * and <code>0</code> for full transparency
141
- */
142
- constructor (startTime, duration, options = {}) {
143
- super(startTime, duration, options)
144
- // only validate extra if not subclassed, because if subclcass, there will be extraneous options
145
- applyOptions(options, this)
146
-
147
- this._canvas = document.createElement('canvas')
148
- this._cctx = this.canvas.getContext('2d')
149
-
150
- this._effectsBack = []
151
- const that = this
152
- this._effects = new Proxy(this._effectsBack, {
153
- apply: function (target, thisArg, argumentsList) {
154
- return thisArg[target].apply(this, argumentsList)
155
- },
156
- deleteProperty: function (target, property) {
157
- const value = target[property]
158
- value.detach()
159
- delete target[property]
160
- return true
161
- },
162
- set: function (target, property, value, receiver) {
163
- target[property] = value
164
- if (!isNaN(property)) { // if property is an number (index)
165
- value.attach(that)
166
- }
167
- return true
168
- }
169
- })
170
- }
171
-
172
- /**
173
- * Render visual output
174
- */
175
- render (reltime) {
176
- this.beginRender(reltime)
177
- this.doRender(reltime)
178
- this.endRender(reltime)
179
- }
180
-
181
- beginRender (reltime) {
182
- // if this.width or this.height is null, that means "take all available screen space", so set it to
183
- // this._move.width or this._movie.height, respectively
184
- const w = val(this, 'width', reltime) || val(this._movie, 'width', this.startTime + reltime)
185
- const h = val(this, 'height', reltime) || val(this._movie, 'height', this.startTime + reltime)
186
- this.canvas.width = w
187
- this.canvas.height = h
188
- this.cctx.globalAlpha = val(this, 'opacity', reltime)
189
- }
190
-
191
- doRender (reltime) {
192
- // if this.width or this.height is null, that means "take all available screen space", so set it to
193
- // this._move.width or this._movie.height, respectively
194
- // canvas.width & canvas.height are already interpolated
195
- if (this.background) {
196
- this.cctx.fillStyle = val(this, 'background', reltime)
197
- this.cctx.fillRect(0, 0, this.canvas.width, this.canvas.height) // (0, 0) relative to layer
198
- }
199
- if (this.border && this.border.color) {
200
- this.cctx.strokeStyle = val(this, 'border.color', reltime)
201
- this.cctx.lineWidth = val(this, 'border.thickness', reltime) || 1 // this is optional.. TODO: integrate this with defaultOptions
202
- }
203
- }
204
-
205
- endRender (reltime) {
206
- const w = val(this, 'width', reltime) || val(this._movie, 'width', this.startTime + reltime)
207
- const h = val(this, 'height', reltime) || val(this._movie, 'height', this.startTime + reltime)
208
- if (w * h > 0) {
209
- this._applyEffects()
210
- }
211
- // else InvalidStateError for drawing zero-area image in some effects, right?
212
- }
213
-
214
- _applyEffects () {
215
- for (let i = 0; i < this.effects.length; i++) {
216
- const effect = this.effects[i]
217
- if (effect.enabled) {
218
- effect.apply(this, this._movie.currentTime - this.startTime) // pass relative time
219
- }
220
- }
221
- }
222
-
223
- /**
224
- * Convienence method for <code>effects.push()</code>
225
- * @param {BaseEffect} effect
226
- * @return {module:layer.Visual} the layer (for chaining)
227
- */
228
- addEffect (effect) {
229
- this.effects.push(effect); return this
230
- }
231
-
232
- /**
233
- * The intermediate rendering canvas
234
- * @type HTMLCanvasElement
235
- */
236
- get canvas () {
237
- return this._canvas
238
- }
239
-
240
- /**
241
- * The context of {@link module:layer.Visual#canvas}
242
- * @type CanvasRenderingContext2D
243
- */
244
- get cctx () {
245
- return this._cctx
246
- }
247
-
248
- /**
249
- * @type effect.Base[]
250
- */
251
- get effects () {
252
- return this._effects // priavte (because it's a proxy)
253
- }
254
-
255
- getDefaultOptions () {
256
- return {
257
- ...Base.prototype.getDefaultOptions(),
258
- /**
259
- * @name module:layer.Visual#x
260
- * @type number
261
- * @desc The offset of the layer relative to the movie
262
- */
263
- x: 0,
264
- /**
265
- * @name module:layer.Visual#y
266
- * @type number
267
- * @desc The offset of the layer relative to the movie
268
- */
269
- y: 0,
270
- /**
271
- * @name module:layer.Visual#width
272
- * @type number
273
- */
274
- width: null,
275
- /**
276
- * @name module:layer.Visual#height
277
- * @type number
278
- */
279
- height: null,
280
- /**
281
- * @name module:layer.Visual#background
282
- * @type string
283
- * @desc The css color code for the background, or <code>null</code> for transparency
284
- */
285
- background: null,
286
- /**
287
- * @name module:layer.Visual#border
288
- * @type string
289
- * @desc The css border style, or <code>null</code> for no border
290
- */
291
- border: null,
292
- /**
293
- * @name module:layer.Visual#opacity
294
- * @type number
295
- */
296
- opacity: 1
297
- }
298
- }
299
- }
300
- Visual.prototype.publicExcludes = Base.prototype.publicExcludes.concat(['canvas', 'cctx', 'effects'])
301
- Visual.prototype.propertyFilters = {
302
- ...Base.propertyFilters,
303
- width: function (width) {
304
- return width !== undefined ? width : this._movie.width
305
- },
306
- height: function (height) {
307
- return height !== undefined ? height : this._movie.height
308
- }
309
- }
310
-
311
- export class Text extends Visual {
312
- // TODO: is textX necessary? it seems inconsistent, because you can't define width/height directly for a text layer
313
- /**
314
- * Creates a new text layer
315
- *
316
- * @param {number} startTime
317
- * @param {number} duration
318
- * @param {string} text - the text to display
319
- * @param {number} width - the width of the entire layer
320
- * @param {number} height - the height of the entire layer
321
- * @param {object} [options] - various optional arguments
322
- * @param {number} [options.x=0] - the horizontal position of the layer (relative to the movie)
323
- * @param {number} [options.y=0] - the vertical position of the layer (relative to the movie)
324
- * @param {string} [options.background=null] - the background color of the layer, or <code>null</code>
325
- * for a transparent background
326
- * @param {object} [options.border=null] - the layer's outline, or <code>null</code> for no outline
327
- * @param {string} [options.border.color] - the outline"s color; required for a border
328
- * @param {string} [options.border.thickness=1] - the outline"s weight
329
- * @param {number} [options.opacity=1] - the layer"s opacity; <code>1</cod> for full opacity
330
- * and <code>0</code> for full transparency
331
- * @param {string} [options.font="10px sans-serif"]
332
- * @param {string} [options.color="#fff"]
333
- * @param {number} [options.textX=0] - the text's horizontal offset relative to the layer
334
- * @param {number} [options.textY=0] - the text's vertical offset relative to the layer
335
- * @param {number} [options.maxWidth=null] - the maximum width of a line of text
336
- * @param {string} [options.textAlign="start"] - horizontal align
337
- * @param {string} [options.textBaseline="top"] - vertical align
338
- * @param {string} [options.textDirection="ltr"] - the text direction
339
- *
340
- * @todo add padding options
341
- */
342
- constructor (startTime, duration, text, options = {}) {
343
- // default to no (transparent) background
344
- super(startTime, duration, { background: null, ...options }) // fill in zeros in |doRender|
345
- applyOptions(options, this)
346
-
347
- /**
348
- * @type string
349
- */
350
- this.text = text
351
-
352
- // this._prevText = undefined;
353
- // // because the canvas context rounds font size, but we need to be more accurate
354
- // // rn, this doesn't make a difference, because we can only measure metrics by integer font sizes
355
- // this._lastFont = undefined;
356
- // this._prevMaxWidth = undefined;
357
- }
358
-
359
- doRender (reltime) {
360
- super.doRender(reltime)
361
- const text = val(this, 'text', reltime); const font = val(this, 'font', reltime)
362
- const maxWidth = this.maxWidth ? val(this, 'maxWidth', reltime) : undefined
363
- // // properties that affect metrics
364
- // if (this._prevText !== text || this._prevFont !== font || this._prevMaxWidth !== maxWidth)
365
- // this._updateMetrics(text, font, maxWidth);
366
-
367
- this.cctx.font = font
368
- this.cctx.fillStyle = val(this, 'color', reltime)
369
- this.cctx.textAlign = val(this, 'textAlign', reltime)
370
- this.cctx.textBaseline = val(this, 'textBaseline', reltime)
371
- this.cctx.textDirection = val(this, 'textDirection', reltime)
372
- this.cctx.fillText(
373
- text, val(this, 'textX', reltime), val(this, 'textY', reltime),
374
- maxWidth
375
- )
376
-
377
- this._prevText = text
378
- this._prevFont = font
379
- this._prevMaxWidth = maxWidth
380
- }
381
-
382
- // _updateMetrics(text, font, maxWidth) {
383
- // // TODO calculate / measure for non-integer font.size values
384
- // let metrics = Text._measureText(text, font, maxWidth);
385
- // // TODO: allow user-specified/overwritten width/height
386
- // this.width = /*this.width || */metrics.width;
387
- // this.height = /*this.height || */metrics.height;
388
- // }
389
-
390
- // TODO: implement setters and getters that update dimensions!
391
-
392
- /* static _measureText(text, font, maxWidth) {
393
- // TODO: fix too much bottom padding
394
- const s = document.createElement("span");
395
- s.textContent = text;
396
- s.style.font = font;
397
- s.style.padding = "0";
398
- if (maxWidth) s.style.maxWidth = maxWidth;
399
- document.body.appendChild(s);
400
- const metrics = {width: s.offsetWidth, height: s.offsetHeight};
401
- document.body.removeChild(s);
402
- return metrics;
403
- } */
404
-
405
- getDefaultOptions () {
406
- return {
407
- ...Visual.prototype.getDefaultOptions(),
408
- background: null,
409
- /**
410
- * @name module:layer.Text#font
411
- * @type string
412
- * @desc The css font to render with
413
- */
414
- font: '10px sans-serif',
415
- /**
416
- * @name module:layer.Text#font
417
- * @type string
418
- * @desc The css color to render with
419
- */
420
- color: '#fff',
421
- /**
422
- * @name module:layer.Text#textX
423
- * @type number
424
- * @desc Offset of the text relative to the layer
425
- */
426
- textX: 0,
427
- /**
428
- * @name module:layer.Text#textY
429
- * @type number
430
- * @desc Offset of the text relative to the layer
431
- */
432
- textY: 0,
433
- /**
434
- * @name module:layer.Text#maxWidth
435
- * @type number
436
- */
437
- maxWidth: null,
438
- /**
439
- * @name module:layer.Text#textAlign
440
- * @type string
441
- * @desc The horizontal alignment
442
- * @see [<code>CanvasRenderingContext2D#textAlign</code>]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textAlign}
443
- */
444
- textAlign: 'start',
445
- /**
446
- * @name module:layer.Text#textAlign
447
- * @type string
448
- * @desc the vertical alignment
449
- * @see [<code>CanvasRenderingContext2D#textBaseline</code>]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline}
450
- */
451
- textBaseline: 'top',
452
- /**
453
- * @name module:layer.Text#textDirection
454
- * @type string
455
- * @see [<code>CanvasRenderingContext2D#direction</code>]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline}
456
- */
457
- textDirection: 'ltr'
458
- }
459
- }
460
- }
461
-
462
- export class Image extends Visual {
463
- /**
464
- * Creates a new image layer
465
- *
466
- * @param {number} startTime
467
- * @param {number} duration
468
- * @param {HTMLImageElement} image
469
- * @param {object} [options]
470
- * @param {number} [options.x=0] - the offset of the layer relative to the movie
471
- * @param {number} [options.y=0] - the offset of the layer relative to the movie
472
- * @param {string} [options.background=null] - the background color of the layer, or <code>null</code>
473
- * for transparency
474
- * @param {object} [options.border=null] - the layer"s outline, or <code>null</code> for no outline
475
- * @param {string} [options.border.color] - the outline"s color; required for a border
476
- * @param {string} [options.border.thickness=1] - the outline"s weight
477
- * @param {number} [options.opacity=1] - the layer"s opacity; <code>1</cod> for full opacity
478
- * and <code>0</code> for full transparency
479
- * @param {number} [options.clipX=0] - image source x
480
- * @param {number} [options.clipY=0] - image source y
481
- * @param {number} [options.clipWidth=undefined] - image source width, or <code>undefined</code> to fill the entire layer
482
- * @param {number} [options.clipHeight=undefined] - image source height, or <code>undefined</code> to fill the entire layer
483
- */
484
- constructor (startTime, duration, image, options = {}) {
485
- super(startTime, duration, options) // wait to set width & height
486
- applyOptions(options, this)
487
- this._image = image
488
-
489
- const load = () => {
490
- this.width = this.width || this.clipWidth || this.image.width
491
- this.height = this.height || this.clipHeight || this.image.height
492
- }
493
- if (image.complete) {
494
- load()
495
- } else {
496
- image.addEventListener('load', load)
497
- }
498
- }
499
-
500
- doRender (reltime) {
501
- super.doRender(reltime) // clear/fill background
502
-
503
- const w = val(this, 'width', reltime)
504
- const h = val(this, 'height', reltime)
505
-
506
- let cw = val(this, 'clipWidth', reltime)
507
- if (cw === undefined) cw = w
508
- let ch = val(this, 'clipHeight', reltime)
509
- if (ch === undefined) ch = h
510
-
511
- this.cctx.drawImage(
512
- this.image,
513
- val(this, 'clipX', reltime), val(this, 'clipY', reltime),
514
- cw, ch,
515
- 0, 0,
516
- w, h
517
- )
518
- }
519
-
520
- /**
521
- * @type HTMLImageElement
522
- */
523
- get image () {
524
- return this._image
525
- }
526
-
527
- getDefaultOptions () {
528
- return {
529
- ...Visual.prototype.getDefaultOptions(),
530
- /**
531
- * @name module:layer.Image#clipX
532
- * @type number
533
- * @desc Image source x
534
- */
535
- clipX: 0,
536
- /**
537
- * @name module:layer.Image#clipY
538
- * @type number
539
- * @desc Image source y
540
- */
541
- clipY: 0,
542
- /**
543
- * @name module:layer.Image#clipWidth
544
- * @type number
545
- * @desc Image source width, or <code>undefined</code> to fill the entire layer
546
- */
547
- clipWidth: undefined,
548
- /**
549
- * @name module:layer.Image#clipHeight
550
- * @type number
551
- * @desc Image source height, or <code>undefined</code> to fill the entire layer
552
- */
553
- clipHeight: undefined
554
- }
555
- }
556
- }
557
-
558
- // https://web.archive.org/web/20190111044453/http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
559
- /**
560
- * Video or audio
561
- * @mixin MediaMixin
562
- * @todo implement playback rate
563
- */
564
- export const MediaMixin = superclass => {
565
- if (superclass !== Base && superclass !== Visual) {
566
- throw new Error('Media can only extend Base and Visual')
567
- }
568
-
569
- class Media extends superclass {
570
- /**
571
- * @param {number} startTime
572
- * @param {HTMLVideoElement} media
573
- * @param {object} [options]
574
- * @param {number} [options.mediaStartTime=0] - at what time in the audio the layer starts
575
- * @param {numer} [options.duration=media.duration-options.mediaStartTime]
576
- * @param {boolean} [options.muted=false]
577
- * @param {number} [options.volume=1]
578
- * @param {number} [options.playbackRate=1]
579
- */
580
- constructor (startTime, media, onload, options = {}) {
581
- super(startTime, 0, options) // works with both Base and Visual
582
- this._initialized = false
583
- this._media = media
584
- this._mediaStartTime = options.mediaStartTime || 0
585
- applyOptions(options, this)
586
-
587
- const load = () => {
588
- // TODO: && ?
589
- if ((options.duration || (media.duration - this.mediaStartTime)) < 0) {
590
- throw new Error('Invalid options.duration or options.mediaStartTime')
591
- }
592
- this._unstretchedDuration = options.duration || (media.duration - this.mediaStartTime)
593
- this.duration = this._unstretchedDuration / (this.playbackRate)
594
- // onload will use `this`, and can't bind itself because it's before super()
595
- onload && onload.bind(this)(media, options)
596
- }
597
- if (media.readyState >= 2) {
598
- // this frame's data is available now
599
- load()
600
- } else {
601
- // when this frame's data is available
602
- media.addEventListener('loadedmetadata', load)
603
- }
604
- media.addEventListener('durationchange', () => {
605
- this.duration = options.duration || (media.duration - this.mediaStartTime)
606
- })
607
-
608
- // TODO: on unattach?
609
- subscribe(this, 'movie.audiodestinationupdate', event => {
610
- // Connect to new destination if immeidately connected to the existing
611
- // destination.
612
- if (this._connectedToDestination) {
613
- this.source.disconnect(this.movie.actx.destination)
614
- this.source.connect(event.destination)
615
- }
616
- })
617
- }
618
-
619
- attach (movie) {
620
- super.attach(movie)
621
-
622
- subscribe(movie, 'movie.seek', e => {
623
- const time = e.movie.currentTime
624
- if (time < this.startTime || time >= this.startTime + this.duration) {
625
- return
626
- }
627
- this.media.currentTime = time - this.startTime
628
- })
629
- // connect to audiocontext
630
- this._source = movie.actx.createMediaElementSource(this.media)
631
-
632
- // Spy on connect and disconnect to remember if it connected to
633
- // actx.destination (for Movie#record).
634
- const oldConnect = this._source.connect.bind(this.source)
635
- this._source.connect = (destination, outputIndex, inputIndex) => {
636
- this._connectedToDestination = destination === movie.actx.destination
637
- return oldConnect(destination, outputIndex, inputIndex)
638
- }
639
- const oldDisconnect = this._source.disconnect.bind(this.source)
640
- this._source.disconnect = (destination, output, input) => {
641
- if (this.connectedToDestination &&
642
- destination === movie.actx.destination) {
643
- this._connectedToDestination = false
644
- }
645
- return oldDisconnect(destination, output, input)
646
- }
647
-
648
- // Connect to actx.destination by default (can be rewired by user)
649
- this.source.connect(movie.actx.destination)
650
- }
651
-
652
- start (reltime) {
653
- this.media.currentTime = reltime + this.mediaStartTime
654
- this.media.play()
655
- }
656
-
657
- render (reltime) {
658
- super.render(reltime)
659
- // even interpolate here
660
- // TODO: implement Issue: Create built-in audio node to support built-in audio nodes, as this does nothing rn
661
- this.media.muted = val(this, 'muted', reltime)
662
- this.media.volume = val(this, 'volume', reltime)
663
- this.media.playbackRate = val(this, 'playbackRate', reltime)
664
- }
665
-
666
- stop () {
667
- this.media.pause()
668
- }
669
-
670
- /**
671
- * The raw html media element
672
- * @type HTMLMediaElement
673
- */
674
- get media () {
675
- return this._media
676
- }
677
-
678
- /**
679
- * The audio source node for the media
680
- * @type MediaStreamAudioSourceNode
681
- */
682
- get source () {
683
- return this._source
684
- }
685
-
686
- get playbackRate () {
687
- return this._playbackRate
688
- }
689
-
690
- set playbackRate (value) {
691
- this._playbackRate = value
692
- if (this._unstretchedDuration !== undefined) {
693
- this.duration = this._unstretchedDuration / value
694
- }
695
- }
696
-
697
- get startTime () {
698
- return this._startTime
699
- }
700
-
701
- set startTime (val) {
702
- this._startTime = val
703
- if (this._initialized) {
704
- const mediaProgress = this._movie.currentTime - this.startTime
705
- this.media.currentTime = this.mediaStartTime + mediaProgress
706
- }
707
- }
708
-
709
- set mediaStartTime (val) {
710
- this._mediaStartTime = val
711
- if (this._initialized) {
712
- const mediaProgress = this._movie.currentTime - this.startTime
713
- this.media.currentTime = mediaProgress + this.mediaStartTime
714
- }
715
- }
716
-
717
- /**
718
- * Where in the media the layer starts at
719
- * @type number
720
- */
721
- get mediaStartTime () {
722
- return this._mediaStartTime
723
- }
724
-
725
- getDefaultOptions () {
726
- return {
727
- ...superclass.prototype.getDefaultOptions(),
728
- /**
729
- * @name module:layer~Media#mediaStartTime
730
- * @type number
731
- * @desc Where in the media the layer starts at
732
- */
733
- mediaStartTime: 0,
734
- /**
735
- * @name module:layer~Media#duration
736
- * @type number
737
- */
738
- duration: undefined, // important to include undefined keys, for applyOptions
739
- /**
740
- * @name module:layer~Media#muted
741
- * @type boolean
742
- */
743
- muted: false,
744
- /**
745
- * @name module:layer~Media#volume
746
- * @type number
747
- */
748
- volume: 1,
749
- /**
750
- * @name module:layer~Media#playbackRate
751
- * @type number
752
- * @todo <strong>Implement</strong>
753
- */
754
- playbackRate: 1
755
- }
756
- }
757
- };
758
-
759
- return Media // custom mixin class
760
- }
761
-
762
- // use mixins instead of `extend`ing two classes (which doens't work); see below class def
763
- /**
764
- * @extends module:layer~Media
765
- */
766
- export class Video extends MediaMixin(Visual) {
767
- /**
768
- * Creates a new video layer
769
- *
770
- * @param {number} startTime
771
- * @param {HTMLVideoElement} media
772
- * @param {object} [options]
773
- * @param {number} startTime
774
- * @param {HTMLVideoElement} media
775
- * @param {object} [options]
776
- * @param {number} [options.mediaStartTime=0] - at what time in the audio the layer starts
777
- * @param {numer} [options.duration=media.duration-options.mediaStartTime]
778
- * @param {boolean} [options.muted=false]
779
- * @param {number} [options.volume=1]
780
- * @param {number} [options.speed=1] - the audio's playerback rate
781
- * @param {number} [options.mediaStartTime=0] - at what time in the video the layer starts
782
- * @param {numer} [options.duration=media.duration-options.mediaStartTime]
783
- * @param {number} [options.clipX=0] - video source x
784
- * @param {number} [options.clipY=0] - video source y
785
- * @param {number} [options.clipWidth] - video destination width
786
- * @param {number} [options.clipHeight] - video destination height
787
- */
788
- constructor (startTime, media, options = {}) {
789
- // fill in the zeros once loaded
790
- super(startTime, media, function () {
791
- this.width = options.width || options.clipWidth || media.videoWidth
792
- this.height = options.height || options.clipHeight || media.videoHeight
793
- }, options)
794
- applyOptions(options, this)
795
- if (this.duration === undefined) {
796
- this.duration = media.duration - this.mediaStartTime
797
- }
798
- }
799
-
800
- doRender (reltime) {
801
- super.doRender()
802
-
803
- // Determine layer width & height.
804
- // When properties can use custom logic to return a value,
805
- // this will look a lot cleaner.
806
- let w = val(this, 'width', reltime)
807
- let h = val(this, 'height', reltime) || this._movie.height
808
- // fall back to movie dimensions (only if user sets this.width = null)
809
- if (w === undefined) w = this._movie.width
810
- if (h === undefined) h = this._movie.height
811
-
812
- let cw = val(this, 'clipWidth', reltime)
813
- let ch = val(this, 'clipHeight', reltime)
814
- // fall back to layer dimensions
815
- if (cw === undefined) cw = w
816
- if (ch === undefined) ch = h
817
-
818
- this.cctx.drawImage(this.media,
819
- val(this, 'clipX', reltime), val(this, 'clipY', reltime),
820
- cw, ch,
821
- 0, 0,
822
- w, h
823
- )
824
- }
825
-
826
- getDefaultOptions () {
827
- return {
828
- ...Object.getPrototypeOf(this).getDefaultOptions(), // let's not call MediaMixin again
829
- /**
830
- * @name module:layer.Video#clipX
831
- * @type number
832
- * @desc Video source x
833
- */
834
- clipX: 0,
835
- /**
836
- * @name module:layer.Video#clipY
837
- * @type number
838
- * @desc Video source y
839
- */
840
- clipY: 0,
841
- /**
842
- * @name module:layer.Video#clipWidth
843
- * @type number
844
- * @desc Video source width, or <code>undefined</code> to fill the entire layer
845
- */
846
- clipWidth: undefined,
847
- /**
848
- * @name module:layer.Video#clipHeight
849
- * @type number
850
- * @desc Video source height, or <code>undefined</code> to fill the entire layer
851
- */
852
- clipHeight: undefined
853
- }
854
- }
855
- }
856
-
857
- /**
858
- * @extends module:layer~Media
859
- */
860
- export class Audio extends MediaMixin(Base) {
861
- /**
862
- * Creates an audio layer
863
- *
864
- * @param {number} startTime
865
- * @param {HTMLAudioElement} media
866
- * @param {object} [options]
867
- * @param {number} startTime
868
- * @param {HTMLVideoElement} media
869
- * @param {object} [options]
870
- * @param {number} [options.mediaStartTime=0] - at what time in the audio the layer starts
871
- * @param {numer} [options.duration=media.duration-options.mediaStartTime]
872
- * @param {boolean} [options.muted=false]
873
- * @param {number} [options.volume=1]
874
- * @param {number} [options.speed=1] - the audio's playerback rate
875
- */
876
- constructor (startTime, media, options = {}) {
877
- // fill in the zero once loaded, no width or height (will raise error)
878
- super(startTime, media, null, options)
879
- applyOptions(options, this)
880
- if (this.duration === undefined) {
881
- this.duration = media.duration - this.mediaStartTime
882
- }
883
- }
884
-
885
- getDefaultOptions () {
886
- return {
887
- ...Object.getPrototypeOf(this).getDefaultOptions(), // let's not call MediaMixin again
888
- /**
889
- * @name module:layer.Audio#mediaStartTime
890
- * @type number
891
- * @desc Where in the media to start playing when the layer starts
892
- */
893
- mediaStartTime: 0,
894
- duration: undefined
895
- }
896
- }
897
- }