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/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.imageWidth = this.width || this.image.width
495
- this.height = this.imageHeight = this.height || this.image.height
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
- val(this, 'clipWidth', reltime), val(this, 'clipHeight', reltime),
512
- // this.imageX and this.imageY are relative to layer
513
- val(this, 'imageX', reltime), val(this, 'imageY', reltime),
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.duration = options.duration || (media.duration - this.mediaStartTime)
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('canplay', load)
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
- // reset destination
620
- this.source.disconnect()
621
- this.source.connect(event.destination)
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=0] - video destination width
763
- * @param {number} [options.clipHeight=0] - video destination height
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 = this.mediaWidth = options.width || media.videoWidth
771
- this.height = this.mediaHeight = options.height || media.videoHeight
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
- val(this, 'clipWidth', reltime), val(this, 'clipHeight', reltime),
788
- val(this, 'mediaX', reltime), val(this, 'mediaY', reltime), // relative to layer
789
- val(this, 'mediaWidth', reltime), val(this, 'mediaHeight', reltime))
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#mediaX
842
+ * @name module:layer.Video#clipWidth
809
843
  * @type number
810
- * @desc Video offset relative to layer
844
+ * @desc Video source width, or <code>undefined</code> to fill the entire layer
811
845
  */
812
- mediaX: 0,
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#mediaHeight
848
+ * @name module:layer.Video#clipHeight
827
849
  * @type number
828
- * @desc Video destination height
850
+ * @desc Video source height, or <code>undefined</code> to fill the entire layer
829
851
  */
830
- mediaHeight: undefined
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(new Blob(recordedChunks, { type: 'video/webm' }/*, {"type" : "audio/ogg; codecs=opus"} */))
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.enabled ||
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.enabled && !this._renderingFrame) {
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
- if (!(option in destObj)) {
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
  }