etro 0.6.0 → 0.7.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.
- package/CHANGELOG.md +33 -4
- package/CONTRIBUTING.md +3 -3
- package/README.md +23 -11
- package/dist/etro-cjs.js +3438 -0
- package/dist/{etro.js → etro-iife.js} +127 -83
- package/examples/introduction/export.html +10 -4
- package/karma.conf.js +1 -1
- package/package.json +5 -3
- package/rollup.config.js +6 -1
- package/scripts/gen-effect-samples.html +1 -1
- package/spec/layer.spec.js +22 -0
- package/spec/movie.spec.js +8 -0
- package/src/effect.js +11 -8
- package/src/layer.js +83 -61
- package/src/movie.js +14 -13
- package/src/util.js +19 -1
package/src/layer.js
CHANGED
|
@@ -480,21 +480,15 @@ export class Image extends Visual {
|
|
|
480
480
|
* @param {number} [options.clipY=0] - image source y
|
|
481
481
|
* @param {number} [options.clipWidth=undefined] - image source width, or <code>undefined</code> to fill the entire layer
|
|
482
482
|
* @param {number} [options.clipHeight=undefined] - image source height, or <code>undefined</code> to fill the entire layer
|
|
483
|
-
* @param {number} [options.imageX=0] - offset of the image relative to the layer
|
|
484
|
-
* @param {number} [options.imageY=0] - offset of the image relative to the layer
|
|
485
483
|
*/
|
|
486
484
|
constructor (startTime, duration, image, options = {}) {
|
|
487
485
|
super(startTime, duration, options) // wait to set width & height
|
|
488
486
|
applyOptions(options, this)
|
|
489
|
-
// clipX... => how much to show of this.image
|
|
490
|
-
// imageX... => how to project this.image onto the canvas
|
|
491
487
|
this._image = image
|
|
492
488
|
|
|
493
489
|
const load = () => {
|
|
494
|
-
this.width = this.
|
|
495
|
-
this.height = this.
|
|
496
|
-
this.clipWidth = this.clipWidth || image.width
|
|
497
|
-
this.clipHeight = this.clipHeight || image.height
|
|
490
|
+
this.width = this.width || this.clipWidth || this.image.width
|
|
491
|
+
this.height = this.height || this.clipHeight || this.image.height
|
|
498
492
|
}
|
|
499
493
|
if (image.complete) {
|
|
500
494
|
load()
|
|
@@ -505,13 +499,21 @@ export class Image extends Visual {
|
|
|
505
499
|
|
|
506
500
|
doRender (reltime) {
|
|
507
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
|
+
|
|
508
511
|
this.cctx.drawImage(
|
|
509
512
|
this.image,
|
|
510
513
|
val(this, 'clipX', reltime), val(this, 'clipY', reltime),
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
val(this, 'imageWidth', reltime), val(this, 'imageHeight', reltime)
|
|
514
|
+
cw, ch,
|
|
515
|
+
0, 0,
|
|
516
|
+
w, h
|
|
515
517
|
)
|
|
516
518
|
}
|
|
517
519
|
|
|
@@ -548,19 +550,7 @@ export class Image extends Visual {
|
|
|
548
550
|
* @type number
|
|
549
551
|
* @desc Image source height, or <code>undefined</code> to fill the entire layer
|
|
550
552
|
*/
|
|
551
|
-
clipHeight: undefined
|
|
552
|
-
/**
|
|
553
|
-
* @name module:layer.Image#imageX
|
|
554
|
-
* @type number
|
|
555
|
-
* @desc Offset of the image relative to the layer
|
|
556
|
-
*/
|
|
557
|
-
imageX: 0,
|
|
558
|
-
/**
|
|
559
|
-
* @name module:layer.Image#imageX
|
|
560
|
-
* @type number
|
|
561
|
-
* @desc Offset of the image relative to the layer
|
|
562
|
-
*/
|
|
563
|
-
imageY: 0
|
|
553
|
+
clipHeight: undefined
|
|
564
554
|
}
|
|
565
555
|
}
|
|
566
556
|
}
|
|
@@ -599,7 +589,8 @@ export const MediaMixin = superclass => {
|
|
|
599
589
|
if ((options.duration || (media.duration - this.mediaStartTime)) < 0) {
|
|
600
590
|
throw new Error('Invalid options.duration or options.mediaStartTime')
|
|
601
591
|
}
|
|
602
|
-
this.
|
|
592
|
+
this._unstretchedDuration = options.duration || (media.duration - this.mediaStartTime)
|
|
593
|
+
this.duration = this._unstretchedDuration / (this.playbackRate)
|
|
603
594
|
// onload will use `this`, and can't bind itself because it's before super()
|
|
604
595
|
onload && onload.bind(this)(media, options)
|
|
605
596
|
}
|
|
@@ -608,7 +599,7 @@ export const MediaMixin = superclass => {
|
|
|
608
599
|
load()
|
|
609
600
|
} else {
|
|
610
601
|
// when this frame's data is available
|
|
611
|
-
media.addEventListener('
|
|
602
|
+
media.addEventListener('loadedmetadata', load)
|
|
612
603
|
}
|
|
613
604
|
media.addEventListener('durationchange', () => {
|
|
614
605
|
this.duration = options.duration || (media.duration - this.mediaStartTime)
|
|
@@ -616,9 +607,12 @@ export const MediaMixin = superclass => {
|
|
|
616
607
|
|
|
617
608
|
// TODO: on unattach?
|
|
618
609
|
subscribe(this, 'movie.audiodestinationupdate', event => {
|
|
619
|
-
//
|
|
620
|
-
|
|
621
|
-
this.
|
|
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
|
+
}
|
|
622
616
|
})
|
|
623
617
|
}
|
|
624
618
|
|
|
@@ -634,6 +628,24 @@ export const MediaMixin = superclass => {
|
|
|
634
628
|
})
|
|
635
629
|
// connect to audiocontext
|
|
636
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)
|
|
637
649
|
this.source.connect(movie.actx.destination)
|
|
638
650
|
}
|
|
639
651
|
|
|
@@ -671,6 +683,17 @@ export const MediaMixin = superclass => {
|
|
|
671
683
|
return this._source
|
|
672
684
|
}
|
|
673
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
|
+
|
|
674
697
|
get startTime () {
|
|
675
698
|
return this._startTime
|
|
676
699
|
}
|
|
@@ -759,21 +782,15 @@ export class Video extends MediaMixin(Visual) {
|
|
|
759
782
|
* @param {numer} [options.duration=media.duration-options.mediaStartTime]
|
|
760
783
|
* @param {number} [options.clipX=0] - video source x
|
|
761
784
|
* @param {number} [options.clipY=0] - video source y
|
|
762
|
-
* @param {number} [options.clipWidth
|
|
763
|
-
* @param {number} [options.clipHeight
|
|
764
|
-
* @param {number} [options.mediaX=0] - video offset relative to the layer
|
|
765
|
-
* @param {number} [options.mediaY=0] - video offset relative to the layer
|
|
785
|
+
* @param {number} [options.clipWidth] - video destination width
|
|
786
|
+
* @param {number} [options.clipHeight] - video destination height
|
|
766
787
|
*/
|
|
767
788
|
constructor (startTime, media, options = {}) {
|
|
768
789
|
// fill in the zeros once loaded
|
|
769
790
|
super(startTime, media, function () {
|
|
770
|
-
this.width =
|
|
771
|
-
this.height =
|
|
772
|
-
this.clipWidth = options.clipWidth || media.videoWidth
|
|
773
|
-
this.clipHeight = options.clipHeight || media.videoHeight
|
|
791
|
+
this.width = options.width || options.clipWidth || media.videoWidth
|
|
792
|
+
this.height = options.height || options.clipHeight || media.videoHeight
|
|
774
793
|
}, options)
|
|
775
|
-
// clipX... => how much to show of this.media
|
|
776
|
-
// mediaX... => how to project this.media onto the canvas
|
|
777
794
|
applyOptions(options, this)
|
|
778
795
|
if (this.duration === undefined) {
|
|
779
796
|
this.duration = media.duration - this.mediaStartTime
|
|
@@ -782,11 +799,28 @@ export class Video extends MediaMixin(Visual) {
|
|
|
782
799
|
|
|
783
800
|
doRender (reltime) {
|
|
784
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
|
+
|
|
785
818
|
this.cctx.drawImage(this.media,
|
|
786
819
|
val(this, 'clipX', reltime), val(this, 'clipY', reltime),
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
820
|
+
cw, ch,
|
|
821
|
+
0, 0,
|
|
822
|
+
w, h
|
|
823
|
+
)
|
|
790
824
|
}
|
|
791
825
|
|
|
792
826
|
getDefaultOptions () {
|
|
@@ -805,29 +839,17 @@ export class Video extends MediaMixin(Visual) {
|
|
|
805
839
|
*/
|
|
806
840
|
clipY: 0,
|
|
807
841
|
/**
|
|
808
|
-
* @name module:layer.Video#
|
|
842
|
+
* @name module:layer.Video#clipWidth
|
|
809
843
|
* @type number
|
|
810
|
-
* @desc Video
|
|
844
|
+
* @desc Video source width, or <code>undefined</code> to fill the entire layer
|
|
811
845
|
*/
|
|
812
|
-
|
|
813
|
-
/**
|
|
814
|
-
* @name module:layer.Video#mediaY
|
|
815
|
-
* @type number
|
|
816
|
-
* @desc Video offset relative to layer
|
|
817
|
-
*/
|
|
818
|
-
mediaY: 0,
|
|
819
|
-
/**
|
|
820
|
-
* @name module:layer.Video#mediaWidth
|
|
821
|
-
* @type number
|
|
822
|
-
* @desc Video destination width
|
|
823
|
-
*/
|
|
824
|
-
mediaWidth: undefined,
|
|
846
|
+
clipWidth: undefined,
|
|
825
847
|
/**
|
|
826
|
-
* @name module:layer.Video#
|
|
848
|
+
* @name module:layer.Video#clipHeight
|
|
827
849
|
* @type number
|
|
828
|
-
* @desc Video
|
|
850
|
+
* @desc Video source height, or <code>undefined</code> to fill the entire layer
|
|
829
851
|
*/
|
|
830
|
-
|
|
852
|
+
clipHeight: undefined
|
|
831
853
|
}
|
|
832
854
|
}
|
|
833
855
|
}
|
package/src/movie.js
CHANGED
|
@@ -158,7 +158,7 @@ export default class Movie {
|
|
|
158
158
|
|
|
159
159
|
if (!this._renderingFrame) {
|
|
160
160
|
// Not rendering (and not playing), so play
|
|
161
|
-
this._render(undefined, resolve)
|
|
161
|
+
this._render(true, undefined, resolve)
|
|
162
162
|
}
|
|
163
163
|
// Stop rendering frame if currently doing so, because playing has higher priority.
|
|
164
164
|
this._renderingFrame = false // this will effect the next _render call
|
|
@@ -178,11 +178,12 @@ export default class Movie {
|
|
|
178
178
|
* @param {boolean} [options.video=true] - whether to include video in recording
|
|
179
179
|
* @param {boolean} [options.audio=true] - whether to include audio in recording
|
|
180
180
|
* @param {object} [options.mediaRecorderOptions=undefined] - options to pass to the <code>MediaRecorder</code>
|
|
181
|
+
* @param {string} [options.type='video/webm'] - MIME type for exported video
|
|
181
182
|
* constructor
|
|
182
183
|
* @return {Promise} resolves when done recording, rejects when internal media recorder errors
|
|
183
184
|
*/
|
|
184
185
|
record (framerate, options = {}) {
|
|
185
|
-
if (options.video === options.audio === false) {
|
|
186
|
+
if (options.video === false && options.audio === false) {
|
|
186
187
|
throw new Error('Both video and audio cannot be disabled')
|
|
187
188
|
}
|
|
188
189
|
|
|
@@ -235,7 +236,11 @@ export default class Movie {
|
|
|
235
236
|
this._mediaRecorder = null
|
|
236
237
|
// construct super-blob
|
|
237
238
|
// this is the exported video as a blob!
|
|
238
|
-
resolve(
|
|
239
|
+
resolve(
|
|
240
|
+
new Blob(recordedChunks, {
|
|
241
|
+
type: options.type || 'video/webm'
|
|
242
|
+
}/*, {"type" : "audio/ogg; codecs=opus"} */)
|
|
243
|
+
)
|
|
239
244
|
}
|
|
240
245
|
mediaRecorder.onerror = reject
|
|
241
246
|
|
|
@@ -277,7 +282,7 @@ export default class Movie {
|
|
|
277
282
|
* @param {function} [done=undefined] - called when done playing or when the current frame is loaded
|
|
278
283
|
* @private
|
|
279
284
|
*/
|
|
280
|
-
_render (timestamp = performance.now(), done = undefined) {
|
|
285
|
+
_render (repeat, timestamp = performance.now(), done = undefined) {
|
|
281
286
|
clearCachedValues(this)
|
|
282
287
|
|
|
283
288
|
if (!this.rendering) {
|
|
@@ -321,14 +326,14 @@ export default class Movie {
|
|
|
321
326
|
|
|
322
327
|
// if instant didn't load, repeatedly frame-render until frame is loaded
|
|
323
328
|
// if the expression below is false, don't publish an event, just silently stop render loop
|
|
324
|
-
if (this._renderingFrame && frameFullyLoaded) {
|
|
329
|
+
if (!repeat || (this._renderingFrame && frameFullyLoaded)) {
|
|
325
330
|
this._renderingFrame = false
|
|
326
331
|
done && done()
|
|
327
332
|
return
|
|
328
333
|
}
|
|
329
334
|
|
|
330
335
|
window.requestAnimationFrame(timestamp => {
|
|
331
|
-
this._render(timestamp)
|
|
336
|
+
this._render(repeat, timestamp)
|
|
332
337
|
}) // TODO: research performance cost
|
|
333
338
|
}
|
|
334
339
|
|
|
@@ -363,7 +368,7 @@ export default class Movie {
|
|
|
363
368
|
const layer = this.layers[i]
|
|
364
369
|
const reltime = this.currentTime - layer.startTime
|
|
365
370
|
// Cancel operation if layer disabled or outside layer time interval
|
|
366
|
-
if (!layer
|
|
371
|
+
if (!val(layer, 'enabled', reltime) ||
|
|
367
372
|
// > or >= ?
|
|
368
373
|
this.currentTime < layer.startTime || this.currentTime > layer.startTime + layer.duration) {
|
|
369
374
|
// outside time interval
|
|
@@ -377,7 +382,7 @@ export default class Movie {
|
|
|
377
382
|
continue
|
|
378
383
|
}
|
|
379
384
|
// if only rendering this frame, we are not "starting" the layer
|
|
380
|
-
if (!layer.active && layer
|
|
385
|
+
if (!layer.active && val(layer, 'enabled', reltime) && !this._renderingFrame) {
|
|
381
386
|
// TODO: make an `activate()` method?
|
|
382
387
|
// console.log("start");
|
|
383
388
|
layer.start(reltime)
|
|
@@ -416,13 +421,9 @@ export default class Movie {
|
|
|
416
421
|
* @return {Promise} - resolves when the frame is loaded
|
|
417
422
|
*/
|
|
418
423
|
refresh () {
|
|
419
|
-
if (this.rendering) {
|
|
420
|
-
throw new Error('Cannot refresh frame while already rendering')
|
|
421
|
-
}
|
|
422
|
-
|
|
423
424
|
return new Promise((resolve, reject) => {
|
|
424
425
|
this._renderingFrame = true
|
|
425
|
-
this._render(undefined, resolve)
|
|
426
|
+
this._render(false, undefined, resolve)
|
|
426
427
|
})
|
|
427
428
|
}
|
|
428
429
|
|
package/src/util.js
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
import { publish } from './event.js'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Gets the first matching property descriptor in the prototype chain, or undefined.
|
|
9
|
+
* @param {Object} obj
|
|
10
|
+
* @param {string|Symbol} name
|
|
11
|
+
*/
|
|
12
|
+
function getPropertyDescriptor (obj, name) {
|
|
13
|
+
do {
|
|
14
|
+
const propDesc = Object.getOwnPropertyDescriptor(obj, name)
|
|
15
|
+
if (propDesc) {
|
|
16
|
+
return propDesc
|
|
17
|
+
}
|
|
18
|
+
obj = Object.getPrototypeOf(obj)
|
|
19
|
+
} while (obj)
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
/**
|
|
8
24
|
* Merges `options` with `defaultOptions`, and then copies the properties with the keys in `defaultOptions`
|
|
9
25
|
* from the merged object to `destObj`.
|
|
@@ -27,7 +43,9 @@ export function applyOptions (options, destObj) {
|
|
|
27
43
|
|
|
28
44
|
// copy options
|
|
29
45
|
for (const option in options) {
|
|
30
|
-
|
|
46
|
+
const propDesc = getPropertyDescriptor(destObj, option)
|
|
47
|
+
// Update the property as long as the property has not been set (unless if it has a setter)
|
|
48
|
+
if (!propDesc || propDesc.set) {
|
|
31
49
|
destObj[option] = options[option]
|
|
32
50
|
}
|
|
33
51
|
}
|