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.
@@ -89,6 +89,22 @@ var etro = (function () {
89
89
  * @module util
90
90
  */
91
91
 
92
+ /**
93
+ * Gets the first matching property descriptor in the prototype chain, or undefined.
94
+ * @param {Object} obj
95
+ * @param {string|Symbol} name
96
+ */
97
+ function getPropertyDescriptor (obj, name) {
98
+ do {
99
+ const propDesc = Object.getOwnPropertyDescriptor(obj, name);
100
+ if (propDesc) {
101
+ return propDesc
102
+ }
103
+ obj = Object.getPrototypeOf(obj);
104
+ } while (obj)
105
+ return undefined
106
+ }
107
+
92
108
  /**
93
109
  * Merges `options` with `defaultOptions`, and then copies the properties with the keys in `defaultOptions`
94
110
  * from the merged object to `destObj`.
@@ -112,7 +128,9 @@ var etro = (function () {
112
128
 
113
129
  // copy options
114
130
  for (const option in options) {
115
- if (!(option in destObj)) {
131
+ const propDesc = getPropertyDescriptor(destObj, option);
132
+ // Update the property as long as the property has not been set (unless if it has a setter)
133
+ if (!propDesc || propDesc.set) {
116
134
  destObj[option] = options[option];
117
135
  }
118
136
  }
@@ -1064,21 +1082,15 @@ var etro = (function () {
1064
1082
  * @param {number} [options.clipY=0] - image source y
1065
1083
  * @param {number} [options.clipWidth=undefined] - image source width, or <code>undefined</code> to fill the entire layer
1066
1084
  * @param {number} [options.clipHeight=undefined] - image source height, or <code>undefined</code> to fill the entire layer
1067
- * @param {number} [options.imageX=0] - offset of the image relative to the layer
1068
- * @param {number} [options.imageY=0] - offset of the image relative to the layer
1069
1085
  */
1070
1086
  constructor (startTime, duration, image, options = {}) {
1071
1087
  super(startTime, duration, options); // wait to set width & height
1072
1088
  applyOptions(options, this);
1073
- // clipX... => how much to show of this.image
1074
- // imageX... => how to project this.image onto the canvas
1075
1089
  this._image = image;
1076
1090
 
1077
1091
  const load = () => {
1078
- this.width = this.imageWidth = this.width || this.image.width;
1079
- this.height = this.imageHeight = this.height || this.image.height;
1080
- this.clipWidth = this.clipWidth || image.width;
1081
- this.clipHeight = this.clipHeight || image.height;
1092
+ this.width = this.width || this.clipWidth || this.image.width;
1093
+ this.height = this.height || this.clipHeight || this.image.height;
1082
1094
  };
1083
1095
  if (image.complete) {
1084
1096
  load();
@@ -1089,13 +1101,21 @@ var etro = (function () {
1089
1101
 
1090
1102
  doRender (reltime) {
1091
1103
  super.doRender(reltime); // clear/fill background
1104
+
1105
+ const w = val(this, 'width', reltime);
1106
+ const h = val(this, 'height', reltime);
1107
+
1108
+ let cw = val(this, 'clipWidth', reltime);
1109
+ if (cw === undefined) cw = w;
1110
+ let ch = val(this, 'clipHeight', reltime);
1111
+ if (ch === undefined) ch = h;
1112
+
1092
1113
  this.cctx.drawImage(
1093
1114
  this.image,
1094
1115
  val(this, 'clipX', reltime), val(this, 'clipY', reltime),
1095
- val(this, 'clipWidth', reltime), val(this, 'clipHeight', reltime),
1096
- // this.imageX and this.imageY are relative to layer
1097
- val(this, 'imageX', reltime), val(this, 'imageY', reltime),
1098
- val(this, 'imageWidth', reltime), val(this, 'imageHeight', reltime)
1116
+ cw, ch,
1117
+ 0, 0,
1118
+ w, h
1099
1119
  );
1100
1120
  }
1101
1121
 
@@ -1132,19 +1152,7 @@ var etro = (function () {
1132
1152
  * @type number
1133
1153
  * @desc Image source height, or <code>undefined</code> to fill the entire layer
1134
1154
  */
1135
- clipHeight: undefined,
1136
- /**
1137
- * @name module:layer.Image#imageX
1138
- * @type number
1139
- * @desc Offset of the image relative to the layer
1140
- */
1141
- imageX: 0,
1142
- /**
1143
- * @name module:layer.Image#imageX
1144
- * @type number
1145
- * @desc Offset of the image relative to the layer
1146
- */
1147
- imageY: 0
1155
+ clipHeight: undefined
1148
1156
  }
1149
1157
  }
1150
1158
  }
@@ -1183,7 +1191,8 @@ var etro = (function () {
1183
1191
  if ((options.duration || (media.duration - this.mediaStartTime)) < 0) {
1184
1192
  throw new Error('Invalid options.duration or options.mediaStartTime')
1185
1193
  }
1186
- this.duration = options.duration || (media.duration - this.mediaStartTime);
1194
+ this._unstretchedDuration = options.duration || (media.duration - this.mediaStartTime);
1195
+ this.duration = this._unstretchedDuration / (this.playbackRate);
1187
1196
  // onload will use `this`, and can't bind itself because it's before super()
1188
1197
  onload && onload.bind(this)(media, options);
1189
1198
  };
@@ -1192,7 +1201,7 @@ var etro = (function () {
1192
1201
  load();
1193
1202
  } else {
1194
1203
  // when this frame's data is available
1195
- media.addEventListener('canplay', load);
1204
+ media.addEventListener('loadedmetadata', load);
1196
1205
  }
1197
1206
  media.addEventListener('durationchange', () => {
1198
1207
  this.duration = options.duration || (media.duration - this.mediaStartTime);
@@ -1200,9 +1209,12 @@ var etro = (function () {
1200
1209
 
1201
1210
  // TODO: on unattach?
1202
1211
  subscribe(this, 'movie.audiodestinationupdate', event => {
1203
- // reset destination
1204
- this.source.disconnect();
1205
- this.source.connect(event.destination);
1212
+ // Connect to new destination if immeidately connected to the existing
1213
+ // destination.
1214
+ if (this._connectedToDestination) {
1215
+ this.source.disconnect(this.movie.actx.destination);
1216
+ this.source.connect(event.destination);
1217
+ }
1206
1218
  });
1207
1219
  }
1208
1220
 
@@ -1218,6 +1230,24 @@ var etro = (function () {
1218
1230
  });
1219
1231
  // connect to audiocontext
1220
1232
  this._source = movie.actx.createMediaElementSource(this.media);
1233
+
1234
+ // Spy on connect and disconnect to remember if it connected to
1235
+ // actx.destination (for Movie#record).
1236
+ const oldConnect = this._source.connect.bind(this.source);
1237
+ this._source.connect = (destination, outputIndex, inputIndex) => {
1238
+ this._connectedToDestination = destination === movie.actx.destination;
1239
+ return oldConnect(destination, outputIndex, inputIndex)
1240
+ };
1241
+ const oldDisconnect = this._source.disconnect.bind(this.source);
1242
+ this._source.disconnect = (destination, output, input) => {
1243
+ if (this.connectedToDestination &&
1244
+ destination === movie.actx.destination) {
1245
+ this._connectedToDestination = false;
1246
+ }
1247
+ return oldDisconnect(destination, output, input)
1248
+ };
1249
+
1250
+ // Connect to actx.destination by default (can be rewired by user)
1221
1251
  this.source.connect(movie.actx.destination);
1222
1252
  }
1223
1253
 
@@ -1255,6 +1285,17 @@ var etro = (function () {
1255
1285
  return this._source
1256
1286
  }
1257
1287
 
1288
+ get playbackRate () {
1289
+ return this._playbackRate
1290
+ }
1291
+
1292
+ set playbackRate (value) {
1293
+ this._playbackRate = value;
1294
+ if (this._unstretchedDuration !== undefined) {
1295
+ this.duration = this._unstretchedDuration / value;
1296
+ }
1297
+ }
1298
+
1258
1299
  get startTime () {
1259
1300
  return this._startTime
1260
1301
  }
@@ -1342,21 +1383,15 @@ var etro = (function () {
1342
1383
  * @param {numer} [options.duration=media.duration-options.mediaStartTime]
1343
1384
  * @param {number} [options.clipX=0] - video source x
1344
1385
  * @param {number} [options.clipY=0] - video source y
1345
- * @param {number} [options.clipWidth=0] - video destination width
1346
- * @param {number} [options.clipHeight=0] - video destination height
1347
- * @param {number} [options.mediaX=0] - video offset relative to the layer
1348
- * @param {number} [options.mediaY=0] - video offset relative to the layer
1386
+ * @param {number} [options.clipWidth] - video destination width
1387
+ * @param {number} [options.clipHeight] - video destination height
1349
1388
  */
1350
1389
  constructor (startTime, media, options = {}) {
1351
1390
  // fill in the zeros once loaded
1352
1391
  super(startTime, media, function () {
1353
- this.width = this.mediaWidth = options.width || media.videoWidth;
1354
- this.height = this.mediaHeight = options.height || media.videoHeight;
1355
- this.clipWidth = options.clipWidth || media.videoWidth;
1356
- this.clipHeight = options.clipHeight || media.videoHeight;
1392
+ this.width = options.width || options.clipWidth || media.videoWidth;
1393
+ this.height = options.height || options.clipHeight || media.videoHeight;
1357
1394
  }, options);
1358
- // clipX... => how much to show of this.media
1359
- // mediaX... => how to project this.media onto the canvas
1360
1395
  applyOptions(options, this);
1361
1396
  if (this.duration === undefined) {
1362
1397
  this.duration = media.duration - this.mediaStartTime;
@@ -1365,11 +1400,28 @@ var etro = (function () {
1365
1400
 
1366
1401
  doRender (reltime) {
1367
1402
  super.doRender();
1403
+
1404
+ // Determine layer width & height.
1405
+ // When properties can use custom logic to return a value,
1406
+ // this will look a lot cleaner.
1407
+ let w = val(this, 'width', reltime);
1408
+ let h = val(this, 'height', reltime) || this._movie.height;
1409
+ // fall back to movie dimensions (only if user sets this.width = null)
1410
+ if (w === undefined) w = this._movie.width;
1411
+ if (h === undefined) h = this._movie.height;
1412
+
1413
+ let cw = val(this, 'clipWidth', reltime);
1414
+ let ch = val(this, 'clipHeight', reltime);
1415
+ // fall back to layer dimensions
1416
+ if (cw === undefined) cw = w;
1417
+ if (ch === undefined) ch = h;
1418
+
1368
1419
  this.cctx.drawImage(this.media,
1369
1420
  val(this, 'clipX', reltime), val(this, 'clipY', reltime),
1370
- val(this, 'clipWidth', reltime), val(this, 'clipHeight', reltime),
1371
- val(this, 'mediaX', reltime), val(this, 'mediaY', reltime), // relative to layer
1372
- val(this, 'mediaWidth', reltime), val(this, 'mediaHeight', reltime));
1421
+ cw, ch,
1422
+ 0, 0,
1423
+ w, h
1424
+ );
1373
1425
  }
1374
1426
 
1375
1427
  getDefaultOptions () {
@@ -1388,29 +1440,17 @@ var etro = (function () {
1388
1440
  */
1389
1441
  clipY: 0,
1390
1442
  /**
1391
- * @name module:layer.Video#mediaX
1392
- * @type number
1393
- * @desc Video offset relative to layer
1394
- */
1395
- mediaX: 0,
1396
- /**
1397
- * @name module:layer.Video#mediaY
1398
- * @type number
1399
- * @desc Video offset relative to layer
1400
- */
1401
- mediaY: 0,
1402
- /**
1403
- * @name module:layer.Video#mediaWidth
1443
+ * @name module:layer.Video#clipWidth
1404
1444
  * @type number
1405
- * @desc Video destination width
1445
+ * @desc Video source width, or <code>undefined</code> to fill the entire layer
1406
1446
  */
1407
- mediaWidth: undefined,
1447
+ clipWidth: undefined,
1408
1448
  /**
1409
- * @name module:layer.Video#mediaHeight
1449
+ * @name module:layer.Video#clipHeight
1410
1450
  * @type number
1411
- * @desc Video destination height
1451
+ * @desc Video source height, or <code>undefined</code> to fill the entire layer
1412
1452
  */
1413
- mediaHeight: undefined
1453
+ clipHeight: undefined
1414
1454
  }
1415
1455
  }
1416
1456
  }
@@ -1623,7 +1663,7 @@ var etro = (function () {
1623
1663
 
1624
1664
  if (!this._renderingFrame) {
1625
1665
  // Not rendering (and not playing), so play
1626
- this._render(undefined, resolve);
1666
+ this._render(true, undefined, resolve);
1627
1667
  }
1628
1668
  // Stop rendering frame if currently doing so, because playing has higher priority.
1629
1669
  this._renderingFrame = false; // this will effect the next _render call
@@ -1643,11 +1683,12 @@ var etro = (function () {
1643
1683
  * @param {boolean} [options.video=true] - whether to include video in recording
1644
1684
  * @param {boolean} [options.audio=true] - whether to include audio in recording
1645
1685
  * @param {object} [options.mediaRecorderOptions=undefined] - options to pass to the <code>MediaRecorder</code>
1686
+ * @param {string} [options.type='video/webm'] - MIME type for exported video
1646
1687
  * constructor
1647
1688
  * @return {Promise} resolves when done recording, rejects when internal media recorder errors
1648
1689
  */
1649
1690
  record (framerate, options = {}) {
1650
- if (options.video === options.audio === false) {
1691
+ if (options.video === false && options.audio === false) {
1651
1692
  throw new Error('Both video and audio cannot be disabled')
1652
1693
  }
1653
1694
 
@@ -1700,7 +1741,11 @@ var etro = (function () {
1700
1741
  this._mediaRecorder = null;
1701
1742
  // construct super-blob
1702
1743
  // this is the exported video as a blob!
1703
- resolve(new Blob(recordedChunks, { type: 'video/webm' }/*, {"type" : "audio/ogg; codecs=opus"} */));
1744
+ resolve(
1745
+ new Blob(recordedChunks, {
1746
+ type: options.type || 'video/webm'
1747
+ }/*, {"type" : "audio/ogg; codecs=opus"} */)
1748
+ );
1704
1749
  };
1705
1750
  mediaRecorder.onerror = reject;
1706
1751
 
@@ -1742,7 +1787,7 @@ var etro = (function () {
1742
1787
  * @param {function} [done=undefined] - called when done playing or when the current frame is loaded
1743
1788
  * @private
1744
1789
  */
1745
- _render (timestamp = performance.now(), done = undefined) {
1790
+ _render (repeat, timestamp = performance.now(), done = undefined) {
1746
1791
  clearCachedValues(this);
1747
1792
 
1748
1793
  if (!this.rendering) {
@@ -1786,14 +1831,14 @@ var etro = (function () {
1786
1831
 
1787
1832
  // if instant didn't load, repeatedly frame-render until frame is loaded
1788
1833
  // if the expression below is false, don't publish an event, just silently stop render loop
1789
- if (this._renderingFrame && frameFullyLoaded) {
1834
+ if (!repeat || (this._renderingFrame && frameFullyLoaded)) {
1790
1835
  this._renderingFrame = false;
1791
1836
  done && done();
1792
1837
  return
1793
1838
  }
1794
1839
 
1795
1840
  window.requestAnimationFrame(timestamp => {
1796
- this._render(timestamp);
1841
+ this._render(repeat, timestamp);
1797
1842
  }); // TODO: research performance cost
1798
1843
  }
1799
1844
 
@@ -1828,7 +1873,7 @@ var etro = (function () {
1828
1873
  const layer = this.layers[i];
1829
1874
  const reltime = this.currentTime - layer.startTime;
1830
1875
  // Cancel operation if layer disabled or outside layer time interval
1831
- if (!layer.enabled ||
1876
+ if (!val(layer, 'enabled', reltime) ||
1832
1877
  // > or >= ?
1833
1878
  this.currentTime < layer.startTime || this.currentTime > layer.startTime + layer.duration) {
1834
1879
  // outside time interval
@@ -1842,7 +1887,7 @@ var etro = (function () {
1842
1887
  continue
1843
1888
  }
1844
1889
  // if only rendering this frame, we are not "starting" the layer
1845
- if (!layer.active && layer.enabled && !this._renderingFrame) {
1890
+ if (!layer.active && val(layer, 'enabled', reltime) && !this._renderingFrame) {
1846
1891
  // TODO: make an `activate()` method?
1847
1892
  // console.log("start");
1848
1893
  layer.start(reltime);
@@ -1881,13 +1926,9 @@ var etro = (function () {
1881
1926
  * @return {Promise} - resolves when the frame is loaded
1882
1927
  */
1883
1928
  refresh () {
1884
- if (this.rendering) {
1885
- throw new Error('Cannot refresh frame while already rendering')
1886
- }
1887
-
1888
1929
  return new Promise((resolve, reject) => {
1889
1930
  this._renderingFrame = true;
1890
- this._render(undefined, resolve);
1931
+ this._render(false, undefined, resolve);
1891
1932
  })
1892
1933
  }
1893
1934
 
@@ -2349,7 +2390,6 @@ var etro = (function () {
2349
2390
  } */
2350
2391
 
2351
2392
  apply (target, reltime) {
2352
- const gl = this._gl;
2353
2393
  this._checkDimensions(target);
2354
2394
  this._refreshGl();
2355
2395
 
@@ -2357,7 +2397,7 @@ var etro = (function () {
2357
2397
  this._enableTexCoordAttrib();
2358
2398
  this._prepareTextures(target, reltime);
2359
2399
 
2360
- gl.useProgram(this._program);
2400
+ this._gl.useProgram(this._program);
2361
2401
 
2362
2402
  this._prepareUniforms(target, reltime);
2363
2403
 
@@ -2635,6 +2675,8 @@ var etro = (function () {
2635
2675
  // worry about mipmaps)
2636
2676
  const w = target instanceof HTMLVideoElement ? target.videoWidth : target.width;
2637
2677
  const h = target instanceof HTMLVideoElement ? target.videoHeight : target.height;
2678
+ gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, minFilter);
2679
+ gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, magFilter);
2638
2680
  if ((w && isPowerOf2(w)) && (h && isPowerOf2(h))) {
2639
2681
  // Yes, it's a power of 2. All wrap modes are valid. Generate mips.
2640
2682
  gl.texParameteri(target, gl.TEXTURE_WRAP_S, wrapS);
@@ -2648,8 +2690,6 @@ var etro = (function () {
2648
2690
  }
2649
2691
  gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2650
2692
  gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2651
- gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, minFilter);
2652
- gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, magFilter);
2653
2693
  }
2654
2694
 
2655
2695
  return tex
@@ -3340,11 +3380,15 @@ var etro = (function () {
3340
3380
  }
3341
3381
 
3342
3382
  apply (target, reltime) {
3343
- const ctx = target.cctx; const canvas = target.canvas;
3344
- const x = val(this, 'x', reltime); const y = val(this.y, target, reltime);
3345
- const radiusX = val(this, 'radiusX', reltime); const radiusY = val(this.radiusY, target, reltime);
3383
+ const ctx = target.cctx;
3384
+ const canvas = target.canvas;
3385
+ const x = val(this, 'x', reltime);
3386
+ const y = val(this, 'y', reltime);
3387
+ const radiusX = val(this, 'radiusX', reltime);
3388
+ const radiusY = val(this, 'radiusY', reltime);
3346
3389
  const rotation = val(this, 'rotation', reltime);
3347
- const startAngle = val(this, 'startAngle', reltime); const endAngle = val(this.endAngle, target, reltime);
3390
+ const startAngle = val(this, 'startAngle', reltime);
3391
+ const endAngle = val(this, 'endAngle', reltime);
3348
3392
  const anticlockwise = val(this, 'anticlockwise', reltime);
3349
3393
  this._tmpCanvas.width = target.canvas.width;
3350
3394
  this._tmpCanvas.height = target.canvas.height;
@@ -8,7 +8,9 @@
8
8
  // TODO: test audio output on a device that actually has drivers
9
9
  import etro from '../../src/index.js'
10
10
  let movie
11
- window.addEventListener('load', () => {
11
+ const btn = document.querySelector('button')
12
+ btn.addEventListener('click', () => {
13
+ btn.disabled = true
12
14
  const canvas = document.createElement('canvas')
13
15
  canvas.width = 600
14
16
  canvas.height = 400
@@ -36,7 +38,7 @@
36
38
  }))
37
39
  .addLayer(new etro.layer.Video(3, video, {
38
40
  // trim video to only include 3 seconds starting 2 minutes into the video in the video
39
- mediaStartTime: 120,
41
+ mediaStartTime: 5,
40
42
  duration: 3
41
43
  }))
42
44
  .addLayer(new etro.layer.Audio(6, document.querySelector('#input audio'), {
@@ -49,11 +51,15 @@
49
51
  .then(blob => {
50
52
  const video = document.querySelector('#output video')
51
53
  video.src = URL.createObjectURL(blob)
52
- document.querySelector('p').innerHTML = 'Done'
53
54
  })
54
55
  .catch(error => {
55
56
  throw error
56
57
  })
58
+
59
+ window.addEventListener('unload', () => {
60
+ const video = document.querySelector('#output video')
61
+ URL.revokeObjectURL(video.src)
62
+ })
57
63
  }
58
64
  </script>
59
65
  <div id="input">
@@ -65,6 +71,6 @@
65
71
  <video controls></video><br>
66
72
  </div>
67
73
 
68
- <p>Recording</p>
74
+ <button>Record</button>
69
75
  </body>
70
76
  </html>
package/karma.conf.js CHANGED
@@ -13,7 +13,7 @@ module.exports = function (config) {
13
13
 
14
14
  // list of files / patterns to load in the browser
15
15
  files: [
16
- 'dist/etro.js',
16
+ 'dist/etro-iife.js',
17
17
  'spec/*.spec.js',
18
18
  { pattern: 'spec/assets/**/*', included: false }
19
19
  ],
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "etro",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "A flexible video-editing library for the browser",
5
+ "main": "dist/etro-cjs.js",
5
6
  "browser": "src/index.js",
6
7
  "directories": {
7
8
  "doc": "docs",
@@ -11,6 +12,7 @@
11
12
  "dependencies": {},
12
13
  "devDependencies": {
13
14
  "docdash": "^1.1.1",
15
+ "ecstatic": ">=4.1.3",
14
16
  "eslint": "^6.5.1",
15
17
  "eslint-config-standard": "^14.1.0",
16
18
  "eslint-plugin-html": "^6.0.0",
@@ -19,11 +21,11 @@
19
21
  "eslint-plugin-promise": "^4.2.1",
20
22
  "eslint-plugin-standard": "^4.0.1",
21
23
  "ev": "0.0.7",
22
- "http-server": "^0.11.1",
24
+ "http-server": "^0.12.3",
23
25
  "jasmine": "^3.4.0",
24
26
  "jsdoc": "^3.6.3",
25
27
  "jsdoc-export-default-interop": "^0.3.1",
26
- "karma": "^4.3.0",
28
+ "karma": "^5.0.5",
27
29
  "karma-chrome-launcher": "^3.1.0",
28
30
  "karma-jasmine": "^2.0.1",
29
31
  "puppeteer": "^2.0.0",
package/rollup.config.js CHANGED
@@ -5,7 +5,12 @@ export default [
5
5
  // iife bundle
6
6
  {
7
7
  input: 'src/index.js',
8
- output: { file: 'dist/etro.js', format: 'iife', name: 'etro' },
8
+ output: { file: 'dist/etro-iife.js', format: 'iife', name: 'etro' },
9
+ plugins: [resolve()]
10
+ },
11
+ {
12
+ input: 'src/index.js',
13
+ output: { file: 'dist/etro-cjs.js', format: 'cjs' },
9
14
  plugins: [resolve()]
10
15
  }
11
16
  // // es6 module bundle
@@ -2,7 +2,7 @@
2
2
  <!DOCTYPE html>
3
3
  <html>
4
4
  <body>
5
- <script src="../dist/etro.js"></script>
5
+ <script src="../dist/etro-iife.js"></script>
6
6
  <script>
7
7
  /**
8
8
  * Prepares a canvas for saving
@@ -96,6 +96,28 @@ describe('Layers', function () {
96
96
  })
97
97
  })
98
98
 
99
+ describe('Media', function () {
100
+ let layer
101
+ // Media is an abstract mixin, so make a concrete subclass here.
102
+ const CustomMedia = etro.layer.MediaMixin(etro.layer.Base)
103
+ const source = new Audio()
104
+
105
+ beforeAll(function (done) {
106
+ source.addEventListener('canplay', done)
107
+ source.src = '/base/spec/assets/layer/audio.wav'
108
+ })
109
+
110
+ beforeEach(function () {
111
+ layer = new CustomMedia(0, source)
112
+ })
113
+
114
+ it('should have its duration depend on its playbackRate', function () {
115
+ const oldDuration = layer.duration
116
+ layer.playbackRate = 2
117
+ expect(layer.duration).toBe(oldDuration / 2)
118
+ })
119
+ })
120
+
99
121
  // I suspect this doesn't work becuase of autoplay restrictions
100
122
  /* describe('Audio', function () {
101
123
  let layer
@@ -62,6 +62,14 @@ describe('Movie', function () {
62
62
  throw e
63
63
  })
64
64
  })
65
+
66
+ it('can record with custom MIME type', function (done) {
67
+ movie.record(60, { type: 'video/mp4' })
68
+ .then(video => {
69
+ expect(video.type).toBe('video/mp4')
70
+ done()
71
+ })
72
+ })
65
73
  })
66
74
 
67
75
  describe('events ->', function () {
package/src/effect.js CHANGED
@@ -250,7 +250,6 @@ export class Shader extends Base {
250
250
  } */
251
251
 
252
252
  apply (target, reltime) {
253
- const gl = this._gl
254
253
  this._checkDimensions(target)
255
254
  this._refreshGl()
256
255
 
@@ -258,7 +257,7 @@ export class Shader extends Base {
258
257
  this._enableTexCoordAttrib()
259
258
  this._prepareTextures(target, reltime)
260
259
 
261
- gl.useProgram(this._program)
260
+ this._gl.useProgram(this._program)
262
261
 
263
262
  this._prepareUniforms(target, reltime)
264
263
 
@@ -536,6 +535,8 @@ Shader._loadTexture = (gl, source, options = {}) => {
536
535
  // worry about mipmaps)
537
536
  const w = target instanceof HTMLVideoElement ? target.videoWidth : target.width
538
537
  const h = target instanceof HTMLVideoElement ? target.videoHeight : target.height
538
+ gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, minFilter)
539
+ gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, magFilter)
539
540
  if ((w && isPowerOf2(w)) && (h && isPowerOf2(h))) {
540
541
  // Yes, it's a power of 2. All wrap modes are valid. Generate mips.
541
542
  gl.texParameteri(target, gl.TEXTURE_WRAP_S, wrapS)
@@ -549,8 +550,6 @@ Shader._loadTexture = (gl, source, options = {}) => {
549
550
  }
550
551
  gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
551
552
  gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
552
- gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, minFilter)
553
- gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, magFilter)
554
553
  }
555
554
 
556
555
  return tex
@@ -1241,11 +1240,15 @@ export class EllipticalMask extends Base {
1241
1240
  }
1242
1241
 
1243
1242
  apply (target, reltime) {
1244
- const ctx = target.cctx; const canvas = target.canvas
1245
- const x = val(this, 'x', reltime); const y = val(this.y, target, reltime)
1246
- const radiusX = val(this, 'radiusX', reltime); const radiusY = val(this.radiusY, target, reltime)
1243
+ const ctx = target.cctx
1244
+ const canvas = target.canvas
1245
+ const x = val(this, 'x', reltime)
1246
+ const y = val(this, 'y', reltime)
1247
+ const radiusX = val(this, 'radiusX', reltime)
1248
+ const radiusY = val(this, 'radiusY', reltime)
1247
1249
  const rotation = val(this, 'rotation', reltime)
1248
- const startAngle = val(this, 'startAngle', reltime); const endAngle = val(this.endAngle, target, reltime)
1250
+ const startAngle = val(this, 'startAngle', reltime)
1251
+ const endAngle = val(this, 'endAngle', reltime)
1249
1252
  const anticlockwise = val(this, 'anticlockwise', reltime)
1250
1253
  this._tmpCanvas.width = target.canvas.width
1251
1254
  this._tmpCanvas.height = target.canvas.height