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
|
@@ -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
|
-
|
|
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.
|
|
1079
|
-
this.height = this.
|
|
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
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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.
|
|
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('
|
|
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
|
-
//
|
|
1204
|
-
|
|
1205
|
-
this.
|
|
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
|
|
1346
|
-
* @param {number} [options.clipHeight
|
|
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 =
|
|
1354
|
-
this.height =
|
|
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
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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#
|
|
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
|
|
1445
|
+
* @desc Video source width, or <code>undefined</code> to fill the entire layer
|
|
1406
1446
|
*/
|
|
1407
|
-
|
|
1447
|
+
clipWidth: undefined,
|
|
1408
1448
|
/**
|
|
1409
|
-
* @name module:layer.Video#
|
|
1449
|
+
* @name module:layer.Video#clipHeight
|
|
1410
1450
|
* @type number
|
|
1411
|
-
* @desc Video
|
|
1451
|
+
* @desc Video source height, or <code>undefined</code> to fill the entire layer
|
|
1412
1452
|
*/
|
|
1413
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
|
3344
|
-
const
|
|
3345
|
-
const
|
|
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);
|
|
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
|
-
|
|
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:
|
|
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
|
-
<
|
|
74
|
+
<button>Record</button>
|
|
69
75
|
</body>
|
|
70
76
|
</html>
|
package/karma.conf.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "etro",
|
|
3
|
-
"version": "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.
|
|
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": "^
|
|
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
|
package/spec/layer.spec.js
CHANGED
|
@@ -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
|
package/spec/movie.spec.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1245
|
-
const
|
|
1246
|
-
const
|
|
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)
|
|
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
|