etro 0.9.0 → 0.10.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 (68) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/CONTRIBUTING.md +25 -34
  3. package/README.md +9 -17
  4. package/dist/custom-array.d.ts +10 -0
  5. package/dist/effect/base.d.ts +10 -1
  6. package/dist/effect/shader.d.ts +11 -1
  7. package/dist/effect/stack.d.ts +6 -2
  8. package/dist/etro-cjs.js +1182 -592
  9. package/dist/etro-iife.js +1182 -592
  10. package/dist/event.d.ts +10 -5
  11. package/dist/layer/audio-source.d.ts +9 -4
  12. package/dist/layer/audio.d.ts +15 -2
  13. package/dist/layer/base.d.ts +49 -3
  14. package/dist/layer/image.d.ts +15 -1
  15. package/dist/layer/text.d.ts +6 -3
  16. package/dist/layer/video.d.ts +13 -1
  17. package/dist/layer/visual-source.d.ts +18 -3
  18. package/dist/layer/visual.d.ts +11 -7
  19. package/dist/movie/effects.d.ts +6 -0
  20. package/dist/movie/index.d.ts +1 -0
  21. package/dist/movie/layers.d.ts +6 -0
  22. package/dist/movie/movie.d.ts +260 -0
  23. package/dist/object.d.ts +9 -2
  24. package/dist/util.d.ts +4 -10
  25. package/eslint.conf.js +4 -2
  26. package/eslint.test-conf.js +1 -2
  27. package/karma.conf.js +10 -14
  28. package/package.json +23 -22
  29. package/scripts/{gen-effect-samples.html → effect/gen-effect-samples.html} +24 -0
  30. package/scripts/{save-effect-samples.js → effect/save-effect-samples.js} +1 -1
  31. package/src/custom-array.ts +43 -0
  32. package/src/effect/base.ts +23 -22
  33. package/src/effect/gaussian-blur.ts +11 -6
  34. package/src/effect/pixelate.ts +3 -3
  35. package/src/effect/shader.ts +33 -27
  36. package/src/effect/stack.ts +43 -30
  37. package/src/effect/transform.ts +16 -9
  38. package/src/event.ts +111 -21
  39. package/src/layer/audio-source.ts +60 -20
  40. package/src/layer/audio.ts +25 -3
  41. package/src/layer/base.ts +79 -25
  42. package/src/layer/image.ts +26 -2
  43. package/src/layer/text.ts +11 -4
  44. package/src/layer/video.ts +31 -4
  45. package/src/layer/visual-source.ts +70 -8
  46. package/src/layer/visual.ts +57 -35
  47. package/src/movie/effects.ts +26 -0
  48. package/src/movie/index.ts +1 -0
  49. package/src/movie/layers.ts +26 -0
  50. package/src/movie/movie.ts +855 -0
  51. package/src/object.ts +9 -2
  52. package/src/util.ts +68 -89
  53. package/tsconfig.json +3 -1
  54. package/dist/movie.d.ts +0 -201
  55. package/examples/application/readme-screenshot.html +0 -85
  56. package/examples/application/video-player.html +0 -130
  57. package/examples/application/webcam.html +0 -28
  58. package/examples/introduction/audio.html +0 -64
  59. package/examples/introduction/effects.html +0 -79
  60. package/examples/introduction/export.html +0 -83
  61. package/examples/introduction/functions.html +0 -37
  62. package/examples/introduction/hello-world1.html +0 -37
  63. package/examples/introduction/hello-world2.html +0 -32
  64. package/examples/introduction/keyframes.html +0 -79
  65. package/examples/introduction/media.html +0 -63
  66. package/examples/introduction/text.html +0 -31
  67. package/private-todo.txt +0 -70
  68. package/src/movie.ts +0 -742
package/dist/etro-iife.js CHANGED
@@ -39,11 +39,68 @@ var etro = (function () {
39
39
  return t;
40
40
  };
41
41
  return __assign.apply(this, arguments);
42
- };
42
+ };
43
+
44
+ function __awaiter(thisArg, _arguments, P, generator) {
45
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
46
+ return new (P || (P = Promise))(function (resolve, reject) {
47
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
48
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
49
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
50
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
51
+ });
52
+ }
53
+
54
+ function __generator(thisArg, body) {
55
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
56
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
57
+ function verb(n) { return function (v) { return step([n, v]); }; }
58
+ function step(op) {
59
+ if (f) throw new TypeError("Generator is already executing.");
60
+ while (_) try {
61
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
62
+ if (y = 0, t) op = [op[0] & 2, t.value];
63
+ switch (op[0]) {
64
+ case 0: case 1: t = op; break;
65
+ case 4: _.label++; return { value: op[1], done: false };
66
+ case 5: _.label++; y = op[1]; op = [0]; continue;
67
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
68
+ default:
69
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
70
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
71
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
72
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
73
+ if (t[2]) _.ops.pop();
74
+ _.trys.pop(); continue;
75
+ }
76
+ op = body.call(thisArg, _);
77
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
78
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
79
+ }
80
+ }
43
81
 
44
82
  /**
45
83
  * @module event
46
84
  */
85
+ var DeprecatedEvent = /** @class */ (function () {
86
+ function DeprecatedEvent(replacement, message) {
87
+ if (message === void 0) { message = undefined; }
88
+ this.replacement = replacement;
89
+ this.message = message;
90
+ }
91
+ DeprecatedEvent.prototype.toString = function () {
92
+ var str = '';
93
+ if (this.replacement) {
94
+ str += "Use ".concat(this.replacement, " instead.");
95
+ }
96
+ if (this.message) {
97
+ str += " ".concat(this.message);
98
+ }
99
+ return str;
100
+ };
101
+ return DeprecatedEvent;
102
+ }());
103
+ var deprecatedEvents = {};
47
104
  /**
48
105
  * An event type
49
106
  * @private
@@ -53,11 +110,14 @@ var etro = (function () {
53
110
  this._parts = id.split('.');
54
111
  }
55
112
  TypeId.prototype.contains = function (other) {
56
- if (other._parts.length > this._parts.length)
113
+ if (other._parts.length > this._parts.length) {
57
114
  return false;
58
- for (var i = 0; i < other._parts.length; i++)
59
- if (other._parts[i] !== this._parts[i])
115
+ }
116
+ for (var i = 0; i < other._parts.length; i++) {
117
+ if (other._parts[i] !== this._parts[i]) {
60
118
  return false;
119
+ }
120
+ }
61
121
  return true;
62
122
  };
63
123
  TypeId.prototype.toString = function () {
@@ -65,23 +125,50 @@ var etro = (function () {
65
125
  };
66
126
  return TypeId;
67
127
  }());
128
+ function deprecate(type, newType, message) {
129
+ if (message === void 0) { message = undefined; }
130
+ deprecatedEvents[type] = new DeprecatedEvent(newType, message);
131
+ }
132
+ function subscribeOnce(target, type, listener) {
133
+ var wrapped = function (event) {
134
+ unsubscribe(target, wrapped);
135
+ listener(event);
136
+ };
137
+ subscribe(target, type, wrapped);
138
+ }
139
+ function subscribeMany(target, type, listener) {
140
+ if (!listeners.has(target)) {
141
+ listeners.set(target, []);
142
+ }
143
+ listeners.get(target).push({ type: new TypeId(type), listener: listener });
144
+ }
68
145
  /**
69
- * Listen for an event or category of events
146
+ * Listen for an event or category of events.
70
147
  *
71
- * @param target - a etro object
148
+ * @param target - an etro object
72
149
  * @param type - the id of the type (can contain subtypes, such as
73
150
  * "type.subtype")
74
151
  * @param listener
152
+ * @param options - options
153
+ * @param options.once - if true, the listener will only be called once
75
154
  */
76
- function subscribe(target, type, listener) {
77
- if (!listeners.has(target))
78
- listeners.set(target, []);
79
- listeners.get(target).push({ type: new TypeId(type), listener: listener });
155
+ function subscribe(target, type, listener, options) {
156
+ if (options === void 0) { options = {}; }
157
+ // Check if this event is deprecated.
158
+ if (Object.keys(deprecatedEvents).includes(type)) {
159
+ console.warn("Event ".concat(type, " is deprecated. ").concat(deprecatedEvents[type]));
160
+ }
161
+ if (options.once) {
162
+ subscribeOnce(target, type, listener);
163
+ }
164
+ else {
165
+ subscribeMany(target, type, listener);
166
+ }
80
167
  }
81
168
  /**
82
169
  * Remove an event listener
83
170
  *
84
- * @param target - a etro object
171
+ * @param target - an etro object
85
172
  * @param type - the id of the type (can contain subtypes, such as
86
173
  * "type.subtype")
87
174
  * @param listener
@@ -89,33 +176,36 @@ var etro = (function () {
89
176
  function unsubscribe(target, listener) {
90
177
  // Make sure `listener` has been added with `subscribe`.
91
178
  if (!listeners.has(target) ||
92
- !listeners.get(target).map(function (pair) { return pair.listener; }).includes(listener))
179
+ !listeners.get(target).map(function (pair) { return pair.listener; }).includes(listener)) {
93
180
  throw new Error('No matching event listener to remove');
181
+ }
94
182
  var removed = listeners.get(target)
95
183
  .filter(function (pair) { return pair.listener !== listener; });
96
184
  listeners.set(target, removed);
97
185
  }
98
186
  /**
99
- * Emits an event to all listeners
187
+ * Publish an event to all listeners without checking if it is deprecated.
100
188
  *
101
- * @param target - a etro object
102
- * @param type - the id of the type (can contain subtypes, such as
103
- * "type.subtype")
104
- * @param event - any additional event data
189
+ * @param target
190
+ * @param type
191
+ * @param event
192
+ * @returns
105
193
  */
106
- function publish(target, type, event) {
194
+ function _publish(target, type, event) {
107
195
  event.target = target; // could be a proxy
108
196
  event.type = type;
109
197
  var t = new TypeId(type);
110
- if (!listeners.has(target))
198
+ if (!listeners.has(target)) {
111
199
  // No event fired
112
200
  return null;
201
+ }
113
202
  // Call event listeners for this event.
114
203
  var listenersForType = [];
115
204
  for (var i = 0; i < listeners.get(target).length; i++) {
116
205
  var item = listeners.get(target)[i];
117
- if (t.contains(item.type))
206
+ if (t.contains(item.type)) {
118
207
  listenersForType.push(item.listener);
208
+ }
119
209
  }
120
210
  for (var i = 0; i < listenersForType.length; i++) {
121
211
  var listener = listenersForType[i];
@@ -123,9 +213,33 @@ var etro = (function () {
123
213
  }
124
214
  return event;
125
215
  }
216
+ /**
217
+ * Emits an event to all listeners
218
+ *
219
+ * @param target - an etro object
220
+ * @param type - the id of the type (can contain subtypes, such as
221
+ * "type.subtype")
222
+ * @param event - any additional event data
223
+ */
224
+ function publish(target, type, event) {
225
+ // Check if this event is deprecated only if it can be replaced.
226
+ if (Object.keys(deprecatedEvents).includes(type) && deprecatedEvents[type].replacement) {
227
+ throw new Error("Event ".concat(type, " is deprecated. ").concat(deprecatedEvents[type]));
228
+ }
229
+ // Check for deprecated events that this event replaces.
230
+ for (var deprecated in deprecatedEvents) {
231
+ var deprecatedEvent = deprecatedEvents[deprecated];
232
+ if (type === deprecatedEvent.replacement) {
233
+ _publish(target, deprecated, __assign({}, event));
234
+ }
235
+ }
236
+ return _publish(target, type, event);
237
+ }
126
238
  var listeners = new WeakMap();
127
239
 
128
240
  var event = /*#__PURE__*/Object.freeze({
241
+ __proto__: null,
242
+ deprecate: deprecate,
129
243
  subscribe: subscribe,
130
244
  unsubscribe: unsubscribe,
131
245
  publish: publish
@@ -143,8 +257,9 @@ var etro = (function () {
143
257
  function getPropertyDescriptor(obj, name) {
144
258
  do {
145
259
  var propDesc = Object.getOwnPropertyDescriptor(obj, name);
146
- if (propDesc)
260
+ if (propDesc) {
147
261
  return propDesc;
262
+ }
148
263
  obj = Object.getPrototypeOf(obj);
149
264
  } while (obj);
150
265
  return undefined;
@@ -153,36 +268,45 @@ var etro = (function () {
153
268
  * Merges `options` with `defaultOptions`, and then copies the properties with
154
269
  * the keys in `defaultOptions` from the merged object to `destObj`.
155
270
  *
271
+ * @deprecated Each option should be copied individually, and the default value
272
+ * should be set in the constructor. See
273
+ * {@link https://github.com/etro-js/etro/issues/131} for more info.
274
+ *
156
275
  * @return
157
276
  */
158
277
  // TODO: Make methods like getDefaultOptions private
159
278
  function applyOptions(options, destObj) {
160
279
  var defaultOptions = destObj.getDefaultOptions();
161
280
  // Validate; make sure `keys` doesn't have any extraneous items
162
- for (var option in options)
281
+ for (var option in options) {
163
282
  // eslint-disable-next-line no-prototype-builtins
164
- if (!defaultOptions.hasOwnProperty(option))
283
+ if (!defaultOptions.hasOwnProperty(option)) {
165
284
  throw new Error("Invalid option: '" + option + "'");
285
+ }
286
+ }
166
287
  // Merge options and defaultOptions
167
288
  options = __assign(__assign({}, defaultOptions), options);
168
289
  // Copy options
169
290
  for (var option in options) {
170
291
  var propDesc = getPropertyDescriptor(destObj, option);
171
292
  // Update the property as long as the property has not been set (unless if it has a setter)
172
- if (!propDesc || propDesc.set)
293
+ if (!propDesc || propDesc.set) {
173
294
  destObj[option] = options[option];
295
+ }
174
296
  }
175
297
  }
176
298
  // This must be cleared at the start of each frame
177
299
  var valCache = new WeakMap();
178
300
  function cacheValue(element, path, value) {
179
301
  // Initiate movie cache
180
- if (!valCache.has(element.movie))
302
+ if (!valCache.has(element.movie)) {
181
303
  valCache.set(element.movie, new WeakMap());
304
+ }
182
305
  var movieCache = valCache.get(element.movie);
183
- // Iniitate element cache
184
- if (!movieCache.has(element))
306
+ // Initiate element cache
307
+ if (!movieCache.has(element)) {
185
308
  movieCache.set(element, {});
309
+ }
186
310
  var elementCache = movieCache.get(element);
187
311
  // Cache the value
188
312
  elementCache[path] = value;
@@ -222,13 +346,16 @@ var etro = (function () {
222
346
  return this;
223
347
  };
224
348
  KeyFrame.prototype.evaluate = function (time) {
225
- if (this.value.length === 0)
349
+ if (this.value.length === 0) {
226
350
  throw new Error('Empty keyframe');
227
- if (time === undefined)
351
+ }
352
+ if (time === undefined) {
228
353
  throw new Error('|time| is undefined or null');
354
+ }
229
355
  var firstTime = this.value[0][0];
230
- if (time < firstTime)
356
+ if (time < firstTime) {
231
357
  throw new Error('No keyframe point before |time|');
358
+ }
232
359
  // I think reduce are slow to do per-frame (or more)?
233
360
  for (var i = 0; i < this.value.length; i++) {
234
361
  var startTime = this.value[i][0];
@@ -237,7 +364,7 @@ var etro = (function () {
237
364
  if (i + 1 < this.value.length) {
238
365
  var endTime = this.value[i + 1][0];
239
366
  var endValue = this.value[i + 1][1];
240
- if (startTime <= time && time < endTime)
367
+ if (startTime <= time && time < endTime) {
241
368
  // No need for endValue if it is flat interpolation
242
369
  // TODO: support custom interpolation for 'other' types?
243
370
  if (!(typeof startValue === 'number' || typeof endValue === 'object')) {
@@ -253,6 +380,7 @@ var etro = (function () {
253
380
  endValue, // eslint-disable-line @typescript-eslint/ban-types
254
381
  percentProgress, this.interpolationKeys);
255
382
  }
383
+ }
256
384
  }
257
385
  else {
258
386
  // Repeat last value forever
@@ -278,23 +406,28 @@ var etro = (function () {
278
406
  // TODO: Is this function efficient?
279
407
  // TODO: Update doc @params to allow for keyframes
280
408
  function val(element, path, time) {
281
- if (hasCachedValue(element, path))
409
+ if (hasCachedValue(element, path)) {
282
410
  return getCachedValue(element, path);
411
+ }
283
412
  // Get property of element at path
284
413
  var pathParts = path.split('.');
285
414
  var property = element[pathParts.shift()];
286
- while (pathParts.length > 0)
415
+ while (pathParts.length > 0) {
287
416
  property = property[pathParts.shift()];
417
+ }
288
418
  // Property filter function
289
419
  var process = element.propertyFilters[path];
290
420
  var value;
291
- if (property instanceof KeyFrame)
421
+ if (property instanceof KeyFrame) {
292
422
  value = property.evaluate(time);
293
- else if (typeof property === 'function')
294
- value = property(element, time); // TODO? add more args
295
- else
423
+ }
424
+ else if (typeof property === 'function') {
425
+ value = property(element, time);
426
+ }
427
+ else {
296
428
  // Simple value
297
429
  value = property;
430
+ }
298
431
  return cacheValue(element, path, process ? process.call(element, value) : value);
299
432
  }
300
433
  /* export function floorInterp(x1, x2, t, objectKeys) {
@@ -305,15 +438,18 @@ var etro = (function () {
305
438
  }, Object.create(Object.getPrototypeOf(x1)));
306
439
  } */
307
440
  function linearInterp(x1, x2, t, objectKeys) {
308
- if (typeof x1 !== typeof x2)
441
+ if (typeof x1 !== typeof x2) {
309
442
  throw new Error('Type mismatch');
310
- if (typeof x1 !== 'number' && typeof x1 !== 'object')
443
+ }
444
+ if (typeof x1 !== 'number' && typeof x1 !== 'object') {
311
445
  // Flat interpolation (floor)
312
446
  return x1;
447
+ }
313
448
  if (typeof x1 === 'object') { // to work with objects (including arrays)
314
449
  // TODO: make this code DRY
315
- if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2))
450
+ if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2)) {
316
451
  throw new Error('Prototype mismatch');
452
+ }
317
453
  // Preserve prototype of objects
318
454
  var int = Object.create(Object.getPrototypeOf(x1));
319
455
  // Take the intersection of properties
@@ -321,8 +457,9 @@ var etro = (function () {
321
457
  for (var i = 0; i < keys.length; i++) {
322
458
  var key = keys[i];
323
459
  // eslint-disable-next-line no-prototype-builtins
324
- if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key))
460
+ if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key)) {
325
461
  continue;
462
+ }
326
463
  int[key] = linearInterp(x1[key], x2[key], t);
327
464
  }
328
465
  return int;
@@ -330,14 +467,17 @@ var etro = (function () {
330
467
  return (1 - t) * x1 + t * x2;
331
468
  }
332
469
  function cosineInterp(x1, x2, t, objectKeys) {
333
- if (typeof x1 !== typeof x2)
470
+ if (typeof x1 !== typeof x2) {
334
471
  throw new Error('Type mismatch');
335
- if (typeof x1 !== 'number' && typeof x1 !== 'object')
472
+ }
473
+ if (typeof x1 !== 'number' && typeof x1 !== 'object') {
336
474
  // Flat interpolation (floor)
337
475
  return x1;
476
+ }
338
477
  if (typeof x1 === 'object' && typeof x2 === 'object') { // to work with objects (including arrays)
339
- if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2))
478
+ if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2)) {
340
479
  throw new Error('Prototype mismatch');
480
+ }
341
481
  // Preserve prototype of objects
342
482
  var int = Object.create(Object.getPrototypeOf(x1));
343
483
  // Take the intersection of properties
@@ -345,8 +485,9 @@ var etro = (function () {
345
485
  for (var i = 0; i < keys.length; i++) {
346
486
  var key = keys[i];
347
487
  // eslint-disable-next-line no-prototype-builtins
348
- if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key))
488
+ if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key)) {
349
489
  continue;
490
+ }
350
491
  int[key] = cosineInterp(x1[key], x2[key], t);
351
492
  }
352
493
  return int;
@@ -375,7 +516,7 @@ var etro = (function () {
375
516
  * Converts to a CSS color
376
517
  */
377
518
  Color.prototype.toString = function () {
378
- return "rgba(" + this.r + ", " + this.g + ", " + this.b + ", " + this.a + ")";
519
+ return "rgba(".concat(this.r, ", ").concat(this.g, ", ").concat(this.b, ", ").concat(this.a, ")");
379
520
  };
380
521
  return Color;
381
522
  }());
@@ -426,17 +567,22 @@ var etro = (function () {
426
567
  */
427
568
  Font.prototype.toString = function () {
428
569
  var s = '';
429
- if (this.style !== 'normal')
570
+ if (this.style !== 'normal') {
430
571
  s += this.style + ' ';
431
- if (this.variant !== 'normal')
572
+ }
573
+ if (this.variant !== 'normal') {
432
574
  s += this.variant + ' ';
433
- if (this.weight !== 'normal')
575
+ }
576
+ if (this.weight !== 'normal') {
434
577
  s += this.weight + ' ';
435
- if (this.stretch !== 'normal')
578
+ }
579
+ if (this.stretch !== 'normal') {
436
580
  s += this.stretch + ' ';
437
- s += "" + this.size + this.sizeUnit + " ";
438
- if (this.lineHeight !== 'normal')
581
+ }
582
+ s += "".concat(this.size).concat(this.sizeUnit, " ");
583
+ if (this.lineHeight !== 'normal') {
439
584
  s += this.lineHeight + ' ';
585
+ }
440
586
  s += this.family;
441
587
  return s;
442
588
  };
@@ -451,7 +597,7 @@ var etro = (function () {
451
597
  */
452
598
  function parseFont(str) {
453
599
  // Assign css string to html element
454
- parseFontEl.setAttribute('style', "font: " + str);
600
+ parseFontEl.setAttribute('style', "font: ".concat(str));
455
601
  var _a = parseFontEl.style, fontSize = _a.fontSize, fontFamily = _a.fontFamily, fontStyle = _a.fontStyle, fontVariant = _a.fontVariant, fontWeight = _a.fontWeight, lineHeight = _a.lineHeight;
456
602
  parseFontEl.removeAttribute('style');
457
603
  var size = parseFloat(fontSize);
@@ -476,61 +622,12 @@ var etro = (function () {
476
622
  width = width || canvas.width;
477
623
  height = height || canvas.height;
478
624
  var frame = ctx.getImageData(x, y, width, height);
479
- for (var i = 0, l = frame.data.length; i < l; i += 4)
625
+ for (var i = 0, l = frame.data.length; i < l; i += 4) {
480
626
  mapper(frame.data, i);
481
- if (flush)
627
+ }
628
+ if (flush) {
482
629
  ctx.putImageData(frame, x, y);
483
- }
484
- /**
485
- * <p>Emits "change" event when public properties updated, recursively.
486
- * <p>Must be called before any watchable properties are set, and only once in
487
- * the prototype chain.
488
- *
489
- * @deprecated Will be removed in the future (see issue #130)
490
- *
491
- * @param target - object to watch
492
- */
493
- function watchPublic(target) {
494
- var getPath = function (receiver, prop) {
495
- return (receiver === proxy ? '' : (paths.get(receiver) + '.')) + prop;
496
- };
497
- var callback = function (prop, val, receiver) {
498
- // Public API property updated, emit 'modify' event.
499
- publish(proxy, target.type + ".change.modify", { property: getPath(receiver, prop), newValue: val });
500
- };
501
- var canWatch = function (receiver, prop) { return !prop.startsWith('_') &&
502
- (receiver.publicExcludes === undefined || !receiver.publicExcludes.includes(prop)); };
503
- // The path to each child property (each is a unique proxy)
504
- var paths = new WeakMap();
505
- var handler = {
506
- set: function (obj, prop, val, receiver) {
507
- // Recurse
508
- if (typeof val === 'object' && val !== null && !paths.has(val) && canWatch(receiver, prop)) {
509
- val = new Proxy(val, handler);
510
- paths.set(val, getPath(receiver, prop));
511
- }
512
- // Set property or attribute
513
- // Search prototype chain for the closest setter
514
- var objProto = obj;
515
- while ((objProto = Object.getPrototypeOf(objProto))) {
516
- var propDesc = Object.getOwnPropertyDescriptor(objProto, prop);
517
- if (propDesc && propDesc.set) {
518
- // Call setter, supplying proxy as this (fixes event bugs)
519
- propDesc.set.call(receiver, val);
520
- break;
521
- }
522
- }
523
- if (!objProto)
524
- // Couldn't find setter; set value on instance
525
- obj[prop] = val;
526
- // Check if the property isn't blacklisted in publicExcludes.
527
- if (canWatch(receiver, prop))
528
- callback(prop, val, receiver);
529
- return true;
530
- }
531
- };
532
- var proxy = new Proxy(target, handler);
533
- return proxy;
630
+ }
534
631
  }
535
632
 
536
633
  /**
@@ -556,45 +653,70 @@ var etro = (function () {
556
653
  */
557
654
  function MixedAudioSource(options) {
558
655
  var _this = this;
656
+ var _a;
657
+ if (!options.source) {
658
+ throw new Error('Property "source" is required in options');
659
+ }
559
660
  var onload = options.onload;
560
661
  // Don't set as instance property
561
662
  delete options.onload;
562
- _this = _super.call(this, options) || this;
663
+ _this = _super.call(this, __assign(__assign({}, options), {
664
+ // Set a default duration so that the super constructor doesn't throw an
665
+ // error
666
+ duration: (_a = options.duration) !== null && _a !== void 0 ? _a : 0 })) || this;
563
667
  _this._initialized = false;
564
668
  _this._sourceStartTime = options.sourceStartTime || 0;
565
669
  applyOptions(options, _this);
566
670
  var load = function () {
567
671
  // TODO: && ?
568
- if ((options.duration || (_this.source.duration - _this.sourceStartTime)) < 0)
672
+ if ((options.duration || (_this.source.duration - _this.sourceStartTime)) < 0) {
569
673
  throw new Error('Invalid options.duration or options.sourceStartTime');
674
+ }
570
675
  _this._unstretchedDuration = options.duration || (_this.source.duration - _this.sourceStartTime);
571
676
  _this.duration = _this._unstretchedDuration / (_this.playbackRate);
572
677
  // onload will use `this`, and can't bind itself because it's before
573
678
  // super()
574
679
  onload && onload.bind(_this)(_this.source, options);
575
680
  };
576
- if (_this.source.readyState >= 2)
681
+ if (_this.source.readyState >= 2) {
577
682
  // this frame's data is available now
578
683
  load();
579
- else
684
+ }
685
+ else {
580
686
  // when this frame's data is available
581
687
  _this.source.addEventListener('loadedmetadata', load);
688
+ }
582
689
  _this.source.addEventListener('durationchange', function () {
583
690
  _this.duration = options.duration || (_this.source.duration - _this.sourceStartTime);
584
691
  });
585
692
  return _this;
586
693
  }
694
+ MixedAudioSource.prototype.whenReady = function () {
695
+ return __awaiter(this, void 0, void 0, function () {
696
+ var _this = this;
697
+ return __generator(this, function (_a) {
698
+ switch (_a.label) {
699
+ case 0: return [4 /*yield*/, _super.prototype.whenReady.call(this)];
700
+ case 1:
701
+ _a.sent();
702
+ if (!(this.source.readyState < 4)) return [3 /*break*/, 3];
703
+ return [4 /*yield*/, new Promise(function (resolve) {
704
+ _this.source.addEventListener('canplaythrough', resolve);
705
+ })];
706
+ case 2:
707
+ _a.sent();
708
+ _a.label = 3;
709
+ case 3: return [2 /*return*/];
710
+ }
711
+ });
712
+ });
713
+ };
587
714
  MixedAudioSource.prototype.attach = function (movie) {
588
715
  var _this = this;
589
716
  _super.prototype.attach.call(this, movie);
590
- subscribe(movie, 'movie.seek', function () {
591
- if (_this.currentTime < 0 || _this.currentTime >= _this.duration)
592
- return;
593
- _this.source.currentTime = _this.currentTime + _this.sourceStartTime;
594
- });
595
717
  // TODO: on unattach?
596
- subscribe(movie, 'movie.audiodestinationupdate', function (event) {
597
- // Connect to new destination if immeidately connected to the existing
718
+ subscribe(movie, 'audiodestinationupdate', function (event) {
719
+ // Connect to new destination if immediately connected to the existing
598
720
  // destination.
599
721
  if (_this._connectedToDestination) {
600
722
  _this.audioNode.disconnect(movie.actx.destination);
@@ -613,8 +735,9 @@ var etro = (function () {
613
735
  var oldDisconnect = this._audioNode.disconnect.bind(this.audioNode);
614
736
  this._audioNode.disconnect = function (destination, output, input) {
615
737
  if (_this._connectedToDestination &&
616
- destination === movie.actx.destination)
738
+ destination === movie.actx.destination) {
617
739
  _this._connectedToDestination = false;
740
+ }
618
741
  return oldDisconnect(destination, output, input);
619
742
  };
620
743
  // Connect to actx.destination by default (can be rewired by user)
@@ -630,6 +753,10 @@ var etro = (function () {
630
753
  this.source.currentTime = this.currentTime + this.sourceStartTime;
631
754
  this.source.play();
632
755
  };
756
+ MixedAudioSource.prototype.seek = function (time) {
757
+ _super.prototype.seek.call(this, time);
758
+ this.source.currentTime = this.currentTime + this.sourceStartTime;
759
+ };
633
760
  MixedAudioSource.prototype.render = function () {
634
761
  _super.prototype.render.call(this);
635
762
  // TODO: implement Issue: Create built-in audio node to support built-in
@@ -639,6 +766,7 @@ var etro = (function () {
639
766
  this.source.playbackRate = val(this, 'playbackRate', this.currentTime);
640
767
  };
641
768
  MixedAudioSource.prototype.stop = function () {
769
+ _super.prototype.stop.call(this);
642
770
  this.source.pause();
643
771
  };
644
772
  Object.defineProperty(MixedAudioSource.prototype, "audioNode", {
@@ -657,8 +785,9 @@ var etro = (function () {
657
785
  },
658
786
  set: function (value) {
659
787
  this._playbackRate = value;
660
- if (this._unstretchedDuration !== undefined)
788
+ if (this._unstretchedDuration !== undefined) {
661
789
  this.duration = this._unstretchedDuration / value;
790
+ }
662
791
  },
663
792
  enumerable: false,
664
793
  configurable: true
@@ -694,6 +823,18 @@ var etro = (function () {
694
823
  enumerable: false,
695
824
  configurable: true
696
825
  });
826
+ Object.defineProperty(MixedAudioSource.prototype, "ready", {
827
+ get: function () {
828
+ // Typescript doesn't support `super.ready` when targeting es5
829
+ var superReady = Object.getOwnPropertyDescriptor(superclass.prototype, 'ready').get.call(this);
830
+ return superReady && this.source.readyState === 4;
831
+ },
832
+ enumerable: false,
833
+ configurable: true
834
+ });
835
+ /**
836
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
837
+ */
697
838
  MixedAudioSource.prototype.getDefaultOptions = function () {
698
839
  return __assign(__assign({}, superclass.prototype.getDefaultOptions()), { source: undefined, sourceStartTime: 0, duration: undefined, muted: false, volume: 1, playbackRate: 1 });
699
840
  };
@@ -716,56 +857,74 @@ var etro = (function () {
716
857
  * movie's timeline
717
858
  */
718
859
  function Base(options) {
860
+ if (options.duration === null || options.duration === undefined) {
861
+ throw new Error('Property "duration" is required in BaseOptions');
862
+ }
863
+ if (options.startTime === null || options.startTime === undefined) {
864
+ throw new Error('Property "startTime" is required in BaseOptions');
865
+ }
719
866
  // Set startTime and duration properties manually, because they are
720
867
  // readonly. applyOptions ignores readonly properties.
721
868
  this._startTime = options.startTime;
722
869
  this._duration = options.duration;
723
- // Proxy that will be returned by constructor (for sending 'modified'
724
- // events).
725
- var newThis = watchPublic(this);
726
- // Don't send updates when initializing, so use this instead of newThis
727
870
  applyOptions(options, this);
728
871
  // Whether this layer is currently being rendered
729
872
  this.active = false;
730
873
  this.enabled = true;
731
- this._occurrenceCount = 0; // no occurances in parent
874
+ this._occurrenceCount = 0; // no occurrences in parent
732
875
  this._movie = null;
733
- // Propogate up to target
734
- subscribe(newThis, 'layer.change', function (event) {
735
- var typeOfChange = event.type.substring(event.type.lastIndexOf('.') + 1);
736
- var type = "movie.change.layer." + typeOfChange;
737
- publish(newThis._movie, type, __assign(__assign({}, event), { target: newThis._movie, type: type }));
738
- });
739
- return newThis;
740
876
  }
877
+ /**
878
+ * Wait until this layer is ready to render
879
+ */
880
+ Base.prototype.whenReady = function () {
881
+ return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
882
+ return [2 /*return*/];
883
+ }); });
884
+ }; // eslint-disable-line @typescript-eslint/no-empty-function
741
885
  /**
742
886
  * Attaches this layer to `movie` if not already attached.
743
887
  * @ignore
744
888
  */
745
889
  Base.prototype.tryAttach = function (movie) {
746
- if (this._occurrenceCount === 0)
890
+ if (this._occurrenceCount === 0) {
747
891
  this.attach(movie);
892
+ }
748
893
  this._occurrenceCount++;
749
894
  };
895
+ /**
896
+ * Attaches this layer to `movie`
897
+ *
898
+ * Called when the layer is added to a movie's `layers` array.
899
+ *
900
+ * @param movie The movie to attach to
901
+ */
750
902
  Base.prototype.attach = function (movie) {
751
903
  this._movie = movie;
752
904
  };
753
905
  /**
754
- * Dettaches this layer from its movie if the number of times `tryDetach` has
906
+ * Detaches this layer from its movie if the number of times `tryDetach` has
755
907
  * been called (including this call) equals the number of times `tryAttach`
756
908
  * has been called.
757
909
  *
758
910
  * @ignore
759
911
  */
760
912
  Base.prototype.tryDetach = function () {
761
- if (this.movie === null)
913
+ if (this.movie === null) {
762
914
  throw new Error('No movie to detach from');
915
+ }
763
916
  this._occurrenceCount--;
764
917
  // If this layer occurs in another place in a `layers` array, do not unset
765
918
  // _movie. (For calling `unshift` on the `layers` proxy)
766
- if (this._occurrenceCount === 0)
919
+ if (this._occurrenceCount === 0) {
767
920
  this.detach();
921
+ }
768
922
  };
923
+ /**
924
+ * Detaches this layer from its movie
925
+ *
926
+ * Called when the layer is removed from a movie's `layers` array.
927
+ */
769
928
  Base.prototype.detach = function () {
770
929
  this._movie = null;
771
930
  };
@@ -773,14 +932,40 @@ var etro = (function () {
773
932
  * Called when the layer is activated
774
933
  */
775
934
  Base.prototype.start = function () { }; // eslint-disable-line @typescript-eslint/no-empty-function
935
+ /**
936
+ * Update {@link currentTime} when seeking
937
+ *
938
+ * This method is called when the movie seeks to a new time at the request of
939
+ * the user. {@link progress} is called when the movie's `currentTime` is
940
+ * updated due to playback.
941
+ *
942
+ * @param time - The new time in the layer
943
+ */
944
+ Base.prototype.seek = function (time) {
945
+ this._currentTime = time;
946
+ };
947
+ /**
948
+ * Update {@link currentTime} due to playback
949
+ *
950
+ * This method is called when the movie's `currentTime` is updated due to
951
+ * playback. {@link seek} is called when the movie seeks to a new time at the
952
+ * request of the user.
953
+ *
954
+ * @param time - The new time in the layer
955
+ */
956
+ Base.prototype.progress = function (time) {
957
+ this._currentTime = time;
958
+ };
776
959
  /**
777
960
  * Called when the movie renders and the layer is active
778
961
  */
779
962
  Base.prototype.render = function () { }; // eslint-disable-line @typescript-eslint/no-empty-function
780
963
  /**
781
- * Called when the layer is deactivated
964
+ * Called when the layer is deactivated
782
965
  */
783
- Base.prototype.stop = function () { }; // eslint-disable-line @typescript-eslint/no-empty-function
966
+ Base.prototype.stop = function () {
967
+ this._currentTime = undefined;
968
+ };
784
969
  Object.defineProperty(Base.prototype, "parent", {
785
970
  // TODO: is this needed?
786
971
  get: function () {
@@ -791,6 +976,7 @@ var etro = (function () {
791
976
  });
792
977
  Object.defineProperty(Base.prototype, "startTime", {
793
978
  /**
979
+ * The time in the movie at which this layer starts (in seconds)
794
980
  */
795
981
  get: function () {
796
982
  return this._startTime;
@@ -803,17 +989,17 @@ var etro = (function () {
803
989
  });
804
990
  Object.defineProperty(Base.prototype, "currentTime", {
805
991
  /**
806
- * The current time of the movie relative to this layer
992
+ * The current time of the movie relative to this layer (in seconds)
807
993
  */
808
994
  get: function () {
809
- return this._movie ? this._movie.currentTime - this.startTime
810
- : undefined;
995
+ return this._currentTime;
811
996
  },
812
997
  enumerable: false,
813
998
  configurable: true
814
999
  });
815
1000
  Object.defineProperty(Base.prototype, "duration", {
816
1001
  /**
1002
+ * The duration of this layer (in seconds)
817
1003
  */
818
1004
  get: function () {
819
1005
  return this._duration;
@@ -824,6 +1010,16 @@ var etro = (function () {
824
1010
  enumerable: false,
825
1011
  configurable: true
826
1012
  });
1013
+ Object.defineProperty(Base.prototype, "ready", {
1014
+ /**
1015
+ * `true` if this layer is ready to be rendered, `false` otherwise
1016
+ */
1017
+ get: function () {
1018
+ return true;
1019
+ },
1020
+ enumerable: false,
1021
+ configurable: true
1022
+ });
827
1023
  Object.defineProperty(Base.prototype, "movie", {
828
1024
  get: function () {
829
1025
  return this._movie;
@@ -831,6 +1027,9 @@ var etro = (function () {
831
1027
  enumerable: false,
832
1028
  configurable: true
833
1029
  });
1030
+ /**
1031
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1032
+ */
834
1033
  Base.prototype.getDefaultOptions = function () {
835
1034
  return {
836
1035
  startTime: undefined,
@@ -847,6 +1046,7 @@ var etro = (function () {
847
1046
 
848
1047
  // TODO: rename to something more consistent with the naming convention of Visual and VisualSourceMixin
849
1048
  /**
1049
+ * Layer for an HTML audio element
850
1050
  * @extends AudioSource
851
1051
  */
852
1052
  var Audio = /** @class */ (function (_super) {
@@ -855,11 +1055,21 @@ var etro = (function () {
855
1055
  * Creates an audio layer
856
1056
  */
857
1057
  function Audio(options) {
858
- var _this = _super.call(this, options) || this;
859
- if (_this.duration === undefined)
1058
+ var _this = this;
1059
+ if (typeof options.source === 'string') {
1060
+ var audio = document.createElement('audio');
1061
+ audio.src = options.source;
1062
+ options.source = audio;
1063
+ }
1064
+ _this = _super.call(this, options) || this;
1065
+ if (_this.duration === undefined) {
860
1066
  _this.duration = (_this).source.duration - _this.sourceStartTime;
1067
+ }
861
1068
  return _this;
862
1069
  }
1070
+ /**
1071
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1072
+ */
863
1073
  Audio.prototype.getDefaultOptions = function () {
864
1074
  return __assign(__assign({}, Object.getPrototypeOf(this).getDefaultOptions()), {
865
1075
  /**
@@ -871,40 +1081,101 @@ var etro = (function () {
871
1081
  return Audio;
872
1082
  }(AudioSourceMixin(Base)));
873
1083
 
874
- /** Any layer that renders to a canvas */
875
- var Visual = /** @class */ (function (_super) {
876
- __extends(Visual, _super);
877
- /**
878
- * Creates a visual layer
879
- */
880
- function Visual(options) {
881
- var _this = _super.call(this, options) || this;
882
- // Only validate extra if not subclassed, because if subclcass, there will
883
- // be extraneous options.
884
- applyOptions(options, _this);
885
- _this.canvas = document.createElement('canvas');
886
- _this.cctx = _this.canvas.getContext('2d');
887
- _this._effectsBack = [];
888
- _this.effects = new Proxy(_this._effectsBack, {
1084
+ var CustomArrayListener = /** @class */ (function () {
1085
+ function CustomArrayListener() {
1086
+ }
1087
+ return CustomArrayListener;
1088
+ }());
1089
+ /**
1090
+ * An array that notifies a listener when items are added or removed.
1091
+ */
1092
+ var CustomArray = /** @class */ (function (_super) {
1093
+ __extends(CustomArray, _super);
1094
+ function CustomArray(target, listener) {
1095
+ var _this = _super.call(this) || this;
1096
+ for (var _i = 0, target_1 = target; _i < target_1.length; _i++) {
1097
+ var item = target_1[_i];
1098
+ listener.onAdd(item);
1099
+ }
1100
+ // Create proxy
1101
+ return new Proxy(target, {
889
1102
  deleteProperty: function (target, property) {
890
1103
  var value = target[property];
891
- value.detach();
892
1104
  delete target[property];
1105
+ listener.onRemove(value);
893
1106
  return true;
894
1107
  },
895
1108
  set: function (target, property, value) {
1109
+ var oldValue = target[property];
1110
+ target[property] = value;
1111
+ // Check if property is a number (index)
896
1112
  if (!isNaN(Number(property))) {
897
- // The property is a number (index)
898
- if (target[property])
899
- target[property].detach();
900
- value.attach(_this);
1113
+ if (oldValue !== undefined) {
1114
+ listener.onRemove(oldValue);
1115
+ }
1116
+ listener.onAdd(value);
901
1117
  }
902
- target[property] = value;
903
1118
  return true;
904
1119
  }
905
1120
  });
1121
+ }
1122
+ return CustomArray;
1123
+ }(Array));
1124
+
1125
+ // eslint-disable-next-line no-use-before-define
1126
+ var VisualEffectsListener = /** @class */ (function (_super) {
1127
+ __extends(VisualEffectsListener, _super);
1128
+ // eslint-disable-next-line no-use-before-define
1129
+ function VisualEffectsListener(layer) {
1130
+ var _this = _super.call(this) || this;
1131
+ _this._layer = layer;
906
1132
  return _this;
907
1133
  }
1134
+ VisualEffectsListener.prototype.onAdd = function (effect) {
1135
+ effect.tryAttach(this._layer);
1136
+ };
1137
+ VisualEffectsListener.prototype.onRemove = function (effect) {
1138
+ effect.tryDetach();
1139
+ };
1140
+ return VisualEffectsListener;
1141
+ }(CustomArrayListener));
1142
+ var VisualEffects = /** @class */ (function (_super) {
1143
+ __extends(VisualEffects, _super);
1144
+ // eslint-disable-next-line no-use-before-define
1145
+ function VisualEffects(target, layer) {
1146
+ return _super.call(this, target, new VisualEffectsListener(layer)) || this;
1147
+ }
1148
+ return VisualEffects;
1149
+ }(CustomArray));
1150
+ /** Any layer that renders to a canvas */
1151
+ var Visual = /** @class */ (function (_super) {
1152
+ __extends(Visual, _super);
1153
+ /**
1154
+ * Creates a visual layer
1155
+ */
1156
+ function Visual(options) {
1157
+ var _this = _super.call(this, options) || this;
1158
+ applyOptions(options, _this);
1159
+ _this.canvas = document.createElement('canvas');
1160
+ _this.cctx = _this.canvas.getContext('2d');
1161
+ _this.effects = new VisualEffects([], _this);
1162
+ return _this;
1163
+ }
1164
+ Visual.prototype.whenReady = function () {
1165
+ return __awaiter(this, void 0, void 0, function () {
1166
+ return __generator(this, function (_a) {
1167
+ switch (_a.label) {
1168
+ case 0: return [4 /*yield*/, _super.prototype.whenReady.call(this)];
1169
+ case 1:
1170
+ _a.sent();
1171
+ return [4 /*yield*/, Promise.all(this.effects.map(function (effect) { return effect.whenReady(); }))];
1172
+ case 2:
1173
+ _a.sent();
1174
+ return [2 /*return*/];
1175
+ }
1176
+ });
1177
+ });
1178
+ };
908
1179
  /**
909
1180
  * Render visual output
910
1181
  */
@@ -912,8 +1183,9 @@ var etro = (function () {
912
1183
  // Prevent empty canvas errors if the width or height is 0
913
1184
  var width = val(this, 'width', this.currentTime);
914
1185
  var height = val(this, 'height', this.currentTime);
915
- if (width === 0 || height === 0)
1186
+ if (width === 0 || height === 0) {
916
1187
  return;
1188
+ }
917
1189
  this.beginRender();
918
1190
  this.doRender();
919
1191
  this.endRender();
@@ -944,20 +1216,22 @@ var etro = (function () {
944
1216
  Visual.prototype.endRender = function () {
945
1217
  var w = val(this, 'width', this.currentTime) || val(this.movie, 'width', this.movie.currentTime);
946
1218
  var h = val(this, 'height', this.currentTime) || val(this.movie, 'height', this.movie.currentTime);
947
- if (w * h > 0)
1219
+ if (w * h > 0) {
948
1220
  this._applyEffects();
1221
+ }
949
1222
  // else InvalidStateError for drawing zero-area image in some effects, right?
950
1223
  };
951
1224
  Visual.prototype._applyEffects = function () {
952
1225
  for (var i = 0; i < this.effects.length; i++) {
953
1226
  var effect = this.effects[i];
954
- if (effect && effect.enabled)
1227
+ if (effect && effect.enabled) {
955
1228
  // Pass relative time
956
1229
  effect.apply(this, this.movie.currentTime - this.startTime);
1230
+ }
957
1231
  }
958
1232
  };
959
1233
  /**
960
- * Convienence method for <code>effects.push()</code>
1234
+ * Convenience method for <code>effects.push()</code>
961
1235
  * @param effect
962
1236
  * @return the layer (for chaining)
963
1237
  */
@@ -965,6 +1239,18 @@ var etro = (function () {
965
1239
  this.effects.push(effect);
966
1240
  return this;
967
1241
  };
1242
+ Object.defineProperty(Visual.prototype, "ready", {
1243
+ get: function () {
1244
+ // Typescript doesn't support `super.ready` when targeting es5
1245
+ var superReady = Object.getOwnPropertyDescriptor(Base.prototype, 'ready').get.call(this);
1246
+ return superReady && this.effects.every(function (effect) { return effect.ready; });
1247
+ },
1248
+ enumerable: false,
1249
+ configurable: true
1250
+ });
1251
+ /**
1252
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1253
+ */
968
1254
  Visual.prototype.getDefaultOptions = function () {
969
1255
  return __assign(__assign({}, Base.prototype.getDefaultOptions()), {
970
1256
  /**
@@ -987,13 +1273,13 @@ var etro = (function () {
987
1273
  height: null,
988
1274
  /**
989
1275
  * @name module:layer.Visual#background
990
- * @desc The CSS color code for the background, or <code>null</code> for
1276
+ * @desc The color code for the background, or <code>null</code> for
991
1277
  * transparency
992
1278
  */
993
1279
  background: null,
994
1280
  /**
995
1281
  * @name module:layer.Visual#border
996
- * @desc The CSS border style, or <code>null</code> for no border
1282
+ * @desc The border style, or <code>null</code> for no border
997
1283
  */
998
1284
  border: null,
999
1285
  /**
@@ -1023,17 +1309,60 @@ var etro = (function () {
1023
1309
  var MixedVisualSource = /** @class */ (function (_super) {
1024
1310
  __extends(MixedVisualSource, _super);
1025
1311
  function MixedVisualSource(options) {
1026
- var _this = _super.call(this, options) || this;
1312
+ var _this = this;
1313
+ if (!options.source) {
1314
+ throw new Error('Property "source" is required in options');
1315
+ }
1316
+ _this = _super.call(this, options) || this;
1027
1317
  applyOptions(options, _this);
1028
1318
  return _this;
1029
1319
  }
1320
+ MixedVisualSource.prototype.whenReady = function () {
1321
+ return __awaiter(this, void 0, void 0, function () {
1322
+ var _this = this;
1323
+ return __generator(this, function (_a) {
1324
+ switch (_a.label) {
1325
+ case 0: return [4 /*yield*/, _super.prototype.whenReady.call(this)];
1326
+ case 1:
1327
+ _a.sent();
1328
+ return [4 /*yield*/, new Promise(function (resolve) {
1329
+ if (_this.source instanceof HTMLImageElement) {
1330
+ // The source is an image; wait for it to load
1331
+ if (_this.source.complete) {
1332
+ resolve();
1333
+ }
1334
+ else {
1335
+ _this.source.addEventListener('load', function () {
1336
+ resolve();
1337
+ });
1338
+ }
1339
+ }
1340
+ else {
1341
+ // The source is a video; wait for the first frame to load
1342
+ if (_this.source.readyState === 4) {
1343
+ resolve();
1344
+ }
1345
+ else {
1346
+ _this.source.addEventListener('canplaythrough', function () {
1347
+ resolve();
1348
+ });
1349
+ }
1350
+ }
1351
+ })];
1352
+ case 2:
1353
+ _a.sent();
1354
+ return [2 /*return*/];
1355
+ }
1356
+ });
1357
+ });
1358
+ };
1030
1359
  MixedVisualSource.prototype.doRender = function () {
1031
1360
  // Clear/fill background
1032
1361
  _super.prototype.doRender.call(this);
1033
1362
  /*
1034
1363
  * Source dimensions crop the image. Dest dimensions set the size that
1035
1364
  * the image will be rendered at *on the layer*. Note that this is
1036
- * different than the layer dimensions (`this.width` and `this.height`).
1365
+ * different from the layer dimensions (`this.width` and `this.height`).
1037
1366
  * The main reason this distinction exists is so that an image layer can
1038
1367
  * be rotated without being cropped (see iss #46).
1039
1368
  */
@@ -1041,6 +1370,21 @@ var etro = (function () {
1041
1370
  // `destX` and `destY` are relative to the layer
1042
1371
  val(this, 'destX', this.currentTime), val(this, 'destY', this.currentTime), val(this, 'destWidth', this.currentTime), val(this, 'destHeight', this.currentTime));
1043
1372
  };
1373
+ Object.defineProperty(MixedVisualSource.prototype, "ready", {
1374
+ get: function () {
1375
+ // Typescript doesn't support `super.ready` when targeting es5
1376
+ var superReady = Object.getOwnPropertyDescriptor(superclass.prototype, 'ready').get.call(this);
1377
+ var sourceReady = this.source instanceof HTMLImageElement
1378
+ ? this.source.complete
1379
+ : this.source.readyState === 4;
1380
+ return superReady && sourceReady;
1381
+ },
1382
+ enumerable: false,
1383
+ configurable: true
1384
+ });
1385
+ /**
1386
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1387
+ */
1044
1388
  MixedVisualSource.prototype.getDefaultOptions = function () {
1045
1389
  return __assign(__assign({}, superclass.prototype.getDefaultOptions()), { source: undefined, sourceX: 0, sourceY: 0, sourceWidth: undefined, sourceHeight: undefined, destX: 0, destY: 0, destWidth: undefined, destHeight: undefined });
1046
1390
  };
@@ -1068,27 +1412,40 @@ var etro = (function () {
1068
1412
  // instead. (TODO: fact check)
1069
1413
  /* eslint-disable eqeqeq */
1070
1414
  return destWidth != undefined
1071
- ? destWidth : val(this, 'sourceWidth', this.currentTime);
1415
+ ? destWidth
1416
+ : val(this, 'sourceWidth', this.currentTime);
1072
1417
  }, destHeight: function (destHeight) {
1073
1418
  /* eslint-disable eqeqeq */
1074
1419
  return destHeight != undefined
1075
- ? destHeight : val(this, 'sourceHeight', this.currentTime);
1420
+ ? destHeight
1421
+ : val(this, 'sourceHeight', this.currentTime);
1076
1422
  }, width: function (width) {
1077
1423
  /* eslint-disable eqeqeq */
1078
1424
  return width != undefined
1079
- ? width : val(this, 'destWidth', this.currentTime);
1425
+ ? width
1426
+ : val(this, 'destWidth', this.currentTime);
1080
1427
  }, height: function (height) {
1081
1428
  /* eslint-disable eqeqeq */
1082
1429
  return height != undefined
1083
- ? height : val(this, 'destHeight', this.currentTime);
1430
+ ? height
1431
+ : val(this, 'destHeight', this.currentTime);
1084
1432
  } });
1085
1433
  return MixedVisualSource;
1086
1434
  }
1087
1435
 
1436
+ /**
1437
+ * Layer for an HTML image element
1438
+ * @extends VisualSource
1439
+ */
1088
1440
  var Image = /** @class */ (function (_super) {
1089
1441
  __extends(Image, _super);
1090
- function Image() {
1091
- return _super !== null && _super.apply(this, arguments) || this;
1442
+ function Image(options) {
1443
+ if (typeof (options.source) === 'string') {
1444
+ var img = document.createElement('img');
1445
+ img.src = options.source;
1446
+ options.source = img;
1447
+ }
1448
+ return _super.call(this, options) || this;
1092
1449
  }
1093
1450
  return Image;
1094
1451
  }(VisualSourceMixin(Visual)));
@@ -1102,9 +1459,12 @@ var etro = (function () {
1102
1459
  // TODO: is textX necessary? it seems inconsistent, because you can't define
1103
1460
  // width/height directly for a text layer
1104
1461
  function Text(options) {
1105
- var _this =
1462
+ var _this = this;
1463
+ if (!options.text) {
1464
+ throw new Error('Property "text" is required in TextOptions');
1465
+ }
1106
1466
  // Default to no (transparent) background
1107
- _super.call(this, __assign({ background: null }, options)) || this;
1467
+ _this = _super.call(this, __assign({ background: null }, options)) || this;
1108
1468
  applyOptions(options, _this);
1109
1469
  return _this;
1110
1470
  // this._prevText = undefined;
@@ -1151,22 +1511,33 @@ var etro = (function () {
1151
1511
  document.body.removeChild(s);
1152
1512
  return metrics;
1153
1513
  } */
1514
+ /**
1515
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1516
+ */
1154
1517
  Text.prototype.getDefaultOptions = function () {
1155
- return __assign(__assign({}, Visual.prototype.getDefaultOptions()), { background: null, text: undefined, font: '10px sans-serif', color: '#fff', textX: 0, textY: 0, maxWidth: null, textAlign: 'start', textBaseline: 'top', textDirection: 'ltr' });
1518
+ return __assign(__assign({}, Visual.prototype.getDefaultOptions()), { background: null, text: undefined, font: '10px sans-serif', color: parseColor('#fff'), textX: 0, textY: 0, maxWidth: null, textAlign: 'start', textBaseline: 'top', textDirection: 'ltr' });
1156
1519
  };
1157
1520
  return Text;
1158
1521
  }(Visual));
1159
1522
 
1160
- // Use mixins instead of `extend`ing two classes (which isn't supported by
1161
- // JavaScript).
1162
1523
  /**
1524
+ * Layer for an HTML video element
1163
1525
  * @extends AudioSource
1164
1526
  * @extends VisualSource
1165
1527
  */
1166
1528
  var Video = /** @class */ (function (_super) {
1167
1529
  __extends(Video, _super);
1168
- function Video() {
1169
- return _super !== null && _super.apply(this, arguments) || this;
1530
+ function Video(options) {
1531
+ var _a;
1532
+ if (typeof (options.source) === 'string') {
1533
+ var video = document.createElement('video');
1534
+ video.src = options.source;
1535
+ options.source = video;
1536
+ }
1537
+ return _super.call(this, __assign(__assign({}, options), {
1538
+ // Set a default duration so that the super constructor doesn't throw an
1539
+ // error
1540
+ duration: (_a = options.duration) !== null && _a !== void 0 ? _a : 0 })) || this;
1170
1541
  }
1171
1542
  return Video;
1172
1543
  }(AudioSourceMixin(VisualSourceMixin(Visual))));
@@ -1176,6 +1547,7 @@ var etro = (function () {
1176
1547
  */
1177
1548
 
1178
1549
  var index = /*#__PURE__*/Object.freeze({
1550
+ __proto__: null,
1179
1551
  AudioSourceMixin: AudioSourceMixin,
1180
1552
  Audio: Audio,
1181
1553
  Base: Base,
@@ -1191,46 +1563,48 @@ var etro = (function () {
1191
1563
  */
1192
1564
  var Base$1 = /** @class */ (function () {
1193
1565
  function Base() {
1194
- var newThis = watchPublic(this); // proxy that will be returned by constructor
1195
- newThis.enabled = true;
1196
- newThis._occurrenceCount = 0;
1197
- newThis._target = null;
1198
- // Propogate up to target
1199
- subscribe(newThis, 'effect.change.modify', function (event) {
1200
- if (!newThis._target)
1201
- return;
1202
- var type = newThis._target.type + ".change.effect.modify";
1203
- publish(newThis._target, type, __assign(__assign({}, event), { target: newThis._target, source: newThis, type: type }));
1204
- });
1205
- return newThis;
1566
+ this.enabled = true;
1567
+ this._occurrenceCount = 0;
1568
+ this._target = null;
1206
1569
  }
1570
+ /**
1571
+ * Wait until this effect is ready to be applied
1572
+ */
1573
+ Base.prototype.whenReady = function () {
1574
+ return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
1575
+ return [2 /*return*/];
1576
+ }); });
1577
+ }; // eslint-disable-line @typescript-eslint/no-empty-function
1207
1578
  /**
1208
1579
  * Attaches this effect to `target` if not already attached.
1209
1580
  * @ignore
1210
1581
  */
1211
1582
  Base.prototype.tryAttach = function (target) {
1212
- if (this._occurrenceCount === 0)
1583
+ if (this._occurrenceCount === 0) {
1213
1584
  this.attach(target);
1585
+ }
1214
1586
  this._occurrenceCount++;
1215
1587
  };
1216
1588
  Base.prototype.attach = function (movie) {
1217
1589
  this._target = movie;
1218
1590
  };
1219
1591
  /**
1220
- * Dettaches this effect from its target if the number of times `tryDetach`
1592
+ * Detaches this effect from its target if the number of times `tryDetach`
1221
1593
  * has been called (including this call) equals the number of times
1222
1594
  * `tryAttach` has been called.
1223
1595
  *
1224
1596
  * @ignore
1225
1597
  */
1226
1598
  Base.prototype.tryDetach = function () {
1227
- if (this._target === null)
1599
+ if (this._target === null) {
1228
1600
  throw new Error('No movie to detach from');
1601
+ }
1229
1602
  this._occurrenceCount--;
1230
1603
  // If this effect occurs in another place in the containing array, do not
1231
1604
  // unset _target. (For calling `unshift` on the `layers` proxy)
1232
- if (this._occurrenceCount === 0)
1605
+ if (this._occurrenceCount === 0) {
1233
1606
  this.detach();
1607
+ }
1234
1608
  };
1235
1609
  Base.prototype.detach = function () {
1236
1610
  this._target = null;
@@ -1255,6 +1629,14 @@ var etro = (function () {
1255
1629
  enumerable: false,
1256
1630
  configurable: true
1257
1631
  });
1632
+ Object.defineProperty(Base.prototype, "ready", {
1633
+ /** `true` if this effect is ready to be applied */
1634
+ get: function () {
1635
+ return true;
1636
+ },
1637
+ enumerable: false,
1638
+ configurable: true
1639
+ });
1258
1640
  Object.defineProperty(Base.prototype, "parent", {
1259
1641
  get: function () {
1260
1642
  return this._target;
@@ -1269,6 +1651,9 @@ var etro = (function () {
1269
1651
  enumerable: false,
1270
1652
  configurable: true
1271
1653
  });
1654
+ /**
1655
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1656
+ */
1272
1657
  Base.prototype.getDefaultOptions = function () {
1273
1658
  return {};
1274
1659
  };
@@ -1336,16 +1721,18 @@ var etro = (function () {
1336
1721
  Shader.prototype._initGl = function () {
1337
1722
  this._canvas = document.createElement('canvas');
1338
1723
  var gl = this._canvas.getContext('webgl');
1339
- if (gl === null)
1724
+ if (gl === null) {
1340
1725
  throw new Error('Unable to initialize WebGL. Your browser or machine may not support it.');
1726
+ }
1341
1727
  this._gl = gl;
1342
1728
  return gl;
1343
1729
  };
1344
1730
  Shader.prototype._initTextures = function (userUniforms, userTextures, sourceTextureOptions) {
1345
1731
  var gl = this._gl;
1346
1732
  var maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
1347
- if (userTextures.length > maxTextures)
1733
+ if (userTextures.length > maxTextures) {
1348
1734
  console.warn('Too many textures!');
1735
+ }
1349
1736
  this._userTextures = {};
1350
1737
  for (var name_1 in userTextures) {
1351
1738
  var userOptions = userTextures[name_1];
@@ -1358,8 +1745,9 @@ var etro = (function () {
1358
1745
  * textures, without having to define multiple properties in the effect
1359
1746
  * object.
1360
1747
  */
1361
- if (userUniforms[name_1])
1362
- throw new Error("Texture - uniform naming conflict: " + name_1 + "!");
1748
+ if (userUniforms[name_1]) {
1749
+ throw new Error("Texture - uniform naming conflict: ".concat(name_1, "!"));
1750
+ }
1363
1751
  // Add this as a "user uniform".
1364
1752
  userUniforms[name_1] = '1i'; // texture pointer
1365
1753
  }
@@ -1394,18 +1782,6 @@ var etro = (function () {
1394
1782
  this._uniformLocations[unprefixed] = gl.getUniformLocation(this._program, prefixed);
1395
1783
  }
1396
1784
  };
1397
- // Not needed, right?
1398
- /* watchWebGLOptions() {
1399
- const pubChange = () => {
1400
- this.publish("change", {});
1401
- };
1402
- for (let name in this._userTextures) {
1403
- watch(this, name, pubChange);
1404
- }
1405
- for (let name in this._userUniforms) {
1406
- watch(this, name, pubChange);
1407
- }
1408
- } */
1409
1785
  Shader.prototype.apply = function (target, reltime) {
1410
1786
  this._checkDimensions(target);
1411
1787
  this._refreshGl();
@@ -1492,16 +1868,22 @@ var etro = (function () {
1492
1868
  i++;
1493
1869
  }
1494
1870
  };
1871
+ /**
1872
+ * Set the shader's uniforms.
1873
+ * @param target The movie or layer to apply the shader to.
1874
+ * @param reltime The relative time of the movie or layer.
1875
+ */
1495
1876
  Shader.prototype._prepareUniforms = function (target, reltime) {
1496
1877
  var gl = this._gl;
1497
- // Set the shader uniforms.
1498
1878
  // Tell the shader we bound the texture to texture unit 0.
1499
1879
  // All base (Shader class) uniforms are optional.
1500
- if (this._uniformLocations.source)
1880
+ if (this._uniformLocations.source) {
1501
1881
  gl.uniform1i(this._uniformLocations.source, 0);
1882
+ }
1502
1883
  // All base (Shader class) uniforms are optional.
1503
- if (this._uniformLocations.size)
1884
+ if (this._uniformLocations.size) {
1504
1885
  gl.uniform2iv(this._uniformLocations.size, [target.canvas.width, target.canvas.height]);
1886
+ }
1505
1887
  for (var unprefixed in this._userUniforms) {
1506
1888
  var options = this._userUniforms[unprefixed];
1507
1889
  var value = val(this, unprefixed, reltime);
@@ -1525,11 +1907,13 @@ var etro = (function () {
1525
1907
  /**
1526
1908
  * Converts a value of a standard type for javascript to a standard type for
1527
1909
  * GLSL
1910
+ *
1528
1911
  * @param value - the raw value to prepare
1529
1912
  * @param outputType - the WebGL type of |value|; example:
1530
1913
  * <code>1f</code> for a float
1531
1914
  * @param reltime - current time, relative to the target
1532
- * @param [options] - Optional config
1915
+ * @param [options]
1916
+ * @returns the prepared value
1533
1917
  */
1534
1918
  Shader.prototype._prepareValue = function (value, outputType, reltime, options) {
1535
1919
  if (options === void 0) { options = {}; }
@@ -1552,36 +1936,41 @@ var etro = (function () {
1552
1936
  var i = 0;
1553
1937
  for (var name_4 in this._userTextures) {
1554
1938
  var testValue = val(this, name_4, reltime);
1555
- if (value === testValue)
1939
+ if (value === testValue) {
1556
1940
  value = Shader.INTERNAL_TEXTURE_UNITS + i; // after the internal texture units
1941
+ }
1557
1942
  i++;
1558
1943
  }
1559
1944
  }
1560
1945
  if (outputType === '3fv') {
1561
1946
  // allow 4-component vectors; TODO: why?
1562
- if (Array.isArray(value) && (value.length === 3 || value.length === 4))
1947
+ if (Array.isArray(value) && (value.length === 3 || value.length === 4)) {
1563
1948
  return value;
1949
+ }
1564
1950
  // kind of loose so this can be changed if needed
1565
- if (typeof value === 'object')
1951
+ if (typeof value === 'object') {
1566
1952
  return [
1567
1953
  value.r !== undefined ? value.r : def,
1568
1954
  value.g !== undefined ? value.g : def,
1569
1955
  value.b !== undefined ? value.b : def
1570
1956
  ];
1571
- throw new Error("Invalid type: " + outputType + " or value: " + value);
1957
+ }
1958
+ throw new Error("Invalid type: ".concat(outputType, " or value: ").concat(value));
1572
1959
  }
1573
1960
  if (outputType === '4fv') {
1574
- if (Array.isArray(value) && value.length === 4)
1961
+ if (Array.isArray(value) && value.length === 4) {
1575
1962
  return value;
1963
+ }
1576
1964
  // kind of loose so this can be changed if needed
1577
- if (typeof value === 'object')
1965
+ if (typeof value === 'object') {
1578
1966
  return [
1579
1967
  value.r !== undefined ? value.r : def,
1580
1968
  value.g !== undefined ? value.g : def,
1581
1969
  value.b !== undefined ? value.b : def,
1582
1970
  value.a !== undefined ? value.a : def
1583
1971
  ];
1584
- throw new Error("Invalid type: " + outputType + " or value: " + value);
1972
+ }
1973
+ throw new Error("Invalid type: ".concat(outputType, " or value: ").concat(value));
1585
1974
  }
1586
1975
  return value;
1587
1976
  };
@@ -1674,8 +2063,9 @@ var etro = (function () {
1674
2063
  else {
1675
2064
  // No, it's not a power of 2. Turn off mips and set
1676
2065
  // wrapping to clamp to edge
1677
- if (wrapS !== gl.CLAMP_TO_EDGE || wrapT !== gl.CLAMP_TO_EDGE)
2066
+ if (wrapS !== gl.CLAMP_TO_EDGE || wrapT !== gl.CLAMP_TO_EDGE) {
1678
2067
  console.warn('Wrap mode is not CLAMP_TO_EDGE for a non-power-of-two texture. Defaulting to CLAMP_TO_EDGE');
2068
+ }
1679
2069
  gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1680
2070
  gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1681
2071
  }
@@ -1727,7 +2117,6 @@ var etro = (function () {
1727
2117
  Shader._IDENTITY_FRAGMENT_SOURCE = "\n precision mediump float;\n\n uniform sampler2D u_Source;\n\n varying highp vec2 v_TextureCoord;\n\n void main() {\n gl_FragColor = texture2D(u_Source, v_TextureCoord);\n }\n ";
1728
2118
  return Shader;
1729
2119
  }(Visual$1));
1730
- // Shader.prototype.getpublicExcludes = () =>
1731
2120
  var isPowerOf2 = function (value) { return (value && (value - 1)) === 0; };
1732
2121
 
1733
2122
  /**
@@ -1902,6 +2291,35 @@ var etro = (function () {
1902
2291
  return EllipticalMask;
1903
2292
  }(Visual$1));
1904
2293
 
2294
+ var StackEffectsListener = /** @class */ (function (_super) {
2295
+ __extends(StackEffectsListener, _super);
2296
+ function StackEffectsListener(stack) {
2297
+ var _this = _super.call(this) || this;
2298
+ _this._stack = stack;
2299
+ return _this;
2300
+ }
2301
+ StackEffectsListener.prototype.onAdd = function (effect) {
2302
+ if (!this._stack.parent) {
2303
+ return;
2304
+ }
2305
+ effect.tryAttach(this._stack.parent);
2306
+ };
2307
+ StackEffectsListener.prototype.onRemove = function (effect) {
2308
+ if (!this._stack.parent) {
2309
+ return;
2310
+ }
2311
+ effect.tryDetach();
2312
+ };
2313
+ return StackEffectsListener;
2314
+ }(CustomArrayListener));
2315
+ var StackEffects = /** @class */ (function (_super) {
2316
+ __extends(StackEffects, _super);
2317
+ // eslint-disable-next-line no-use-before-define
2318
+ function StackEffects(target, stack) {
2319
+ return _super.call(this, target, new StackEffectsListener(stack)) || this;
2320
+ }
2321
+ return StackEffects;
2322
+ }(CustomArray));
1905
2323
  /**
1906
2324
  * A sequence of effects to apply, treated as one effect. This can be useful
1907
2325
  * for defining reused effect sequences as one effect.
@@ -1910,48 +2328,28 @@ var etro = (function () {
1910
2328
  __extends(Stack, _super);
1911
2329
  function Stack(options) {
1912
2330
  var _this = _super.call(this) || this;
1913
- _this._effectsBack = [];
1914
- // TODO: Throw 'change' events in handlers
1915
- _this.effects = new Proxy(_this._effectsBack, {
1916
- deleteProperty: function (target, property) {
1917
- var value = target[property];
1918
- value.detach(); // Detach effect from movie
1919
- delete target[property];
1920
- return true;
1921
- },
1922
- set: function (target, property, value) {
1923
- // TODO: make sure type check works
1924
- if (!isNaN(Number(property))) { // if property is a number (index)
1925
- if (target[property])
1926
- target[property].detach(); // Detach old effect from movie
1927
- value.attach(this._target); // Attach effect to movie
1928
- }
1929
- target[property] = value;
1930
- return true;
1931
- }
1932
- });
2331
+ _this.effects = new StackEffects(options.effects, _this);
1933
2332
  options.effects.forEach(function (effect) { return _this.effects.push(effect); });
1934
2333
  return _this;
1935
- // TODO: Propogate 'change' events from children up
1936
2334
  }
1937
2335
  Stack.prototype.attach = function (movie) {
1938
2336
  _super.prototype.attach.call(this, movie);
1939
2337
  this.effects.filter(function (effect) { return !!effect; }).forEach(function (effect) {
1940
- effect.detach();
1941
- effect.attach(movie);
2338
+ effect.tryAttach(movie);
1942
2339
  });
1943
2340
  };
1944
2341
  Stack.prototype.detach = function () {
1945
2342
  _super.prototype.detach.call(this);
1946
2343
  this.effects.filter(function (effect) { return !!effect; }).forEach(function (effect) {
1947
- effect.detach();
2344
+ effect.tryDetach();
1948
2345
  });
1949
2346
  };
1950
2347
  Stack.prototype.apply = function (target, reltime) {
1951
2348
  for (var i = 0; i < this.effects.length; i++) {
1952
2349
  var effect = this.effects[i];
1953
- if (!effect)
2350
+ if (!effect) {
1954
2351
  continue;
2352
+ }
1955
2353
  effect.apply(target, reltime);
1956
2354
  }
1957
2355
  };
@@ -1970,7 +2368,7 @@ var etro = (function () {
1970
2368
  * Applies a Gaussian blur
1971
2369
  */
1972
2370
  // TODO: Improve performance
1973
- // TODO: Make sure this is truly gaussian even though it doens't require a
2371
+ // TODO: Make sure this is truly gaussian even though it doesn't require a
1974
2372
  // standard deviation
1975
2373
  var GaussianBlur = /** @class */ (function (_super) {
1976
2374
  __extends(GaussianBlur, _super);
@@ -2015,9 +2413,10 @@ var etro = (function () {
2015
2413
  }
2016
2414
  GaussianBlurComponent.prototype.apply = function (target, reltime) {
2017
2415
  var radiusVal = val(this, 'radius', reltime);
2018
- if (radiusVal !== this._radiusCache)
2416
+ if (radiusVal !== this._radiusCache) {
2019
2417
  // Regenerate gaussian distribution canvas.
2020
2418
  this.shape = GaussianBlurComponent._render1DKernel(GaussianBlurComponent._gen1DKernel(radiusVal));
2419
+ }
2021
2420
  this._radiusCache = radiusVal;
2022
2421
  _super.prototype.apply.call(this, target, reltime);
2023
2422
  };
@@ -2050,23 +2449,27 @@ var etro = (function () {
2050
2449
  var pascal = GaussianBlurComponent._genPascalRow(2 * radius + 1);
2051
2450
  // don't use `reduce` and `map` (overhead?)
2052
2451
  var sum = 0;
2053
- for (var i = 0; i < pascal.length; i++)
2452
+ for (var i = 0; i < pascal.length; i++) {
2054
2453
  sum += pascal[i];
2055
- for (var i = 0; i < pascal.length; i++)
2454
+ }
2455
+ for (var i = 0; i < pascal.length; i++) {
2056
2456
  pascal[i] /= sum;
2457
+ }
2057
2458
  return pascal;
2058
2459
  };
2059
2460
  GaussianBlurComponent._genPascalRow = function (index) {
2060
- if (index < 0)
2061
- throw new Error("Invalid index " + index);
2461
+ if (index < 0) {
2462
+ throw new Error("Invalid index ".concat(index));
2463
+ }
2062
2464
  var currRow = [1];
2063
2465
  for (var i = 1; i < index; i++) {
2064
2466
  var nextRow = [];
2065
2467
  nextRow.length = currRow.length + 1;
2066
2468
  // edges are always 1's
2067
2469
  nextRow[0] = nextRow[nextRow.length - 1] = 1;
2068
- for (var j = 1; j < nextRow.length - 1; j++)
2470
+ for (var j = 1; j < nextRow.length - 1; j++) {
2069
2471
  nextRow[j] = currRow[j - 1] + currRow[j];
2472
+ }
2070
2473
  currRow = nextRow;
2071
2474
  }
2072
2475
  return currRow;
@@ -2137,15 +2540,14 @@ var etro = (function () {
2137
2540
  pixelSize: '1i'
2138
2541
  }
2139
2542
  }) || this;
2140
- /**
2141
- */
2142
2543
  _this.pixelSize = options.pixelSize || 1;
2143
2544
  return _this;
2144
2545
  }
2145
2546
  Pixelate.prototype.apply = function (target, reltime) {
2146
2547
  var ps = val(this, 'pixelSize', reltime);
2147
- if (ps % 1 !== 0 || ps < 0)
2548
+ if (ps % 1 !== 0 || ps < 0) {
2148
2549
  throw new Error('Pixel size must be a nonnegative integer');
2550
+ }
2149
2551
  _super.prototype.apply.call(this, target, reltime);
2150
2552
  };
2151
2553
  return Pixelate;
@@ -2174,10 +2576,12 @@ var etro = (function () {
2174
2576
  return _this;
2175
2577
  }
2176
2578
  Transform.prototype.apply = function (target, reltime) {
2177
- if (target.canvas.width !== this._tmpCanvas.width)
2579
+ if (target.canvas.width !== this._tmpCanvas.width) {
2178
2580
  this._tmpCanvas.width = target.canvas.width;
2179
- if (target.canvas.height !== this._tmpCanvas.height)
2581
+ }
2582
+ if (target.canvas.height !== this._tmpCanvas.height) {
2180
2583
  this._tmpCanvas.height = target.canvas.height;
2584
+ }
2181
2585
  // Use data, since that's the underlying storage
2182
2586
  this._tmpMatrix.data = val(this, 'matrix.data', reltime);
2183
2587
  this._tmpCtx.setTransform(this._tmpMatrix.a, this._tmpMatrix.b, this._tmpMatrix.c, this._tmpMatrix.d, this._tmpMatrix.e, this._tmpMatrix.f);
@@ -2203,8 +2607,9 @@ var etro = (function () {
2203
2607
  ];
2204
2608
  }
2205
2609
  Matrix.prototype.identity = function () {
2206
- for (var i = 0; i < this.data.length; i++)
2610
+ for (var i = 0; i < this.data.length; i++) {
2207
2611
  this.data[i] = Matrix.IDENTITY.data[i];
2612
+ }
2208
2613
  return this;
2209
2614
  };
2210
2615
  /**
@@ -2213,8 +2618,9 @@ var etro = (function () {
2213
2618
  * @param [val]
2214
2619
  */
2215
2620
  Matrix.prototype.cell = function (x, y, val) {
2216
- if (val !== undefined)
2621
+ if (val !== undefined) {
2217
2622
  this.data[3 * y + x] = val;
2623
+ }
2218
2624
  return this.data[3 * y + x];
2219
2625
  };
2220
2626
  Object.defineProperty(Matrix.prototype, "a", {
@@ -2266,16 +2672,19 @@ var etro = (function () {
2266
2672
  */
2267
2673
  Matrix.prototype.multiply = function (other) {
2268
2674
  // copy to temporary matrix to avoid modifying `this` while reading from it
2269
- for (var x = 0; x < 3; x++)
2675
+ for (var x = 0; x < 3; x++) {
2270
2676
  for (var y = 0; y < 3; y++) {
2271
2677
  var sum = 0;
2272
- for (var i = 0; i < 3; i++)
2678
+ for (var i = 0; i < 3; i++) {
2273
2679
  sum += this.cell(x, i) * other.cell(i, y);
2680
+ }
2274
2681
  Matrix._TMP_MATRIX.cell(x, y, sum);
2275
2682
  }
2683
+ }
2276
2684
  // copy data from TMP_MATRIX to this
2277
- for (var i = 0; i < Matrix._TMP_MATRIX.data.length; i++)
2685
+ for (var i = 0; i < Matrix._TMP_MATRIX.data.length; i++) {
2278
2686
  this.data[i] = Matrix._TMP_MATRIX.data[i];
2687
+ }
2279
2688
  return this;
2280
2689
  };
2281
2690
  /**
@@ -2330,6 +2739,7 @@ var etro = (function () {
2330
2739
  */
2331
2740
 
2332
2741
  var index$1 = /*#__PURE__*/Object.freeze({
2742
+ __proto__: null,
2333
2743
  Base: Base$1,
2334
2744
  Brightness: Brightness,
2335
2745
  Channels: Channels,
@@ -2348,6 +2758,52 @@ var etro = (function () {
2348
2758
  Visual: Visual$1
2349
2759
  });
2350
2760
 
2761
+ var MovieEffectsListener = /** @class */ (function (_super) {
2762
+ __extends(MovieEffectsListener, _super);
2763
+ function MovieEffectsListener(movie) {
2764
+ var _this = _super.call(this) || this;
2765
+ _this._movie = movie;
2766
+ return _this;
2767
+ }
2768
+ MovieEffectsListener.prototype.onAdd = function (effect) {
2769
+ effect.tryAttach(this._movie);
2770
+ };
2771
+ MovieEffectsListener.prototype.onRemove = function (effect) {
2772
+ effect.tryDetach();
2773
+ };
2774
+ return MovieEffectsListener;
2775
+ }(CustomArrayListener));
2776
+ var MovieEffects = /** @class */ (function (_super) {
2777
+ __extends(MovieEffects, _super);
2778
+ function MovieEffects(target, movie) {
2779
+ return _super.call(this, target, new MovieEffectsListener(movie)) || this;
2780
+ }
2781
+ return MovieEffects;
2782
+ }(CustomArray));
2783
+
2784
+ var MovieLayersListener = /** @class */ (function (_super) {
2785
+ __extends(MovieLayersListener, _super);
2786
+ function MovieLayersListener(movie) {
2787
+ var _this = _super.call(this) || this;
2788
+ _this._movie = movie;
2789
+ return _this;
2790
+ }
2791
+ MovieLayersListener.prototype.onAdd = function (layer) {
2792
+ layer.tryAttach(this._movie);
2793
+ };
2794
+ MovieLayersListener.prototype.onRemove = function (layer) {
2795
+ layer.tryDetach();
2796
+ };
2797
+ return MovieLayersListener;
2798
+ }(CustomArrayListener));
2799
+ var MovieLayers = /** @class */ (function (_super) {
2800
+ __extends(MovieLayers, _super);
2801
+ function MovieLayers(target, movie) {
2802
+ return _super.call(this, target, new MovieLayersListener(movie)) || this;
2803
+ }
2804
+ return MovieLayers;
2805
+ }(CustomArray));
2806
+
2351
2807
  /**
2352
2808
  * @module movie
2353
2809
  */
@@ -2361,17 +2817,13 @@ var etro = (function () {
2361
2817
  *
2362
2818
  * Implements a pub/sub system.
2363
2819
  */
2364
- // TODO: Implement event "durationchange", and more
2365
- // TODO: Add width and height options
2366
- // TODO: Make record option to make recording video output to the user while
2367
- // it's recording
2368
2820
  // TODO: rename renderingFrame -> refreshing
2369
2821
  var Movie = /** @class */ (function () {
2370
2822
  /**
2371
2823
  * Creates a new movie.
2372
2824
  */
2373
2825
  function Movie(options) {
2374
- // TODO: move into multiple methods!
2826
+ this._recording = false;
2375
2827
  // Set actx option manually, because it's readonly.
2376
2828
  this.actx = options.actx ||
2377
2829
  options.audioContext ||
@@ -2379,236 +2831,294 @@ var etro = (function () {
2379
2831
  // eslint-disable-next-line new-cap
2380
2832
  new window.webkitAudioContext();
2381
2833
  delete options.actx;
2382
- // Proxy that will be returned by constructor
2383
- var newThis = watchPublic(this);
2834
+ // Check if required file canvas is provided
2835
+ if (!options.canvas) {
2836
+ throw new Error('Required option "canvas" not provided to Movie');
2837
+ }
2384
2838
  // Set canvas option manually, because it's readonly.
2385
- this._canvas = options.canvas;
2839
+ this._canvas = this._visibleCanvas = options.canvas;
2386
2840
  delete options.canvas;
2387
- // Don't send updates when initializing, so use this instead of newThis:
2388
2841
  this._cctx = this.canvas.getContext('2d'); // TODO: make private?
2842
+ // Set options on the movie
2389
2843
  applyOptions(options, this);
2390
- var that = newThis;
2391
- this._effectsBack = [];
2392
- this.effects = new Proxy(newThis._effectsBack, {
2393
- deleteProperty: function (target, property) {
2394
- // Refresh screen when effect is removed, if the movie isn't playing
2395
- // already.
2396
- var value = target[property];
2397
- value.tryDetach();
2398
- delete target[property];
2399
- publish(that, 'movie.change.effect.remove', { effect: value });
2400
- return true;
2401
- },
2402
- set: function (target, property, value) {
2403
- // Check if property is an number (an index)
2404
- if (!isNaN(Number(property))) {
2405
- if (target[property]) {
2406
- publish(that, 'movie.change.effect.remove', {
2407
- effect: target[property]
2408
- });
2409
- target[property].tryDetach();
2410
- }
2411
- // Attach effect to movie
2412
- value.tryAttach(that);
2413
- target[property] = value;
2414
- // Refresh screen when effect is set, if the movie isn't playing
2415
- // already.
2416
- publish(that, 'movie.change.effect.add', { effect: value });
2417
- }
2418
- else {
2419
- target[property] = value;
2420
- }
2421
- return true;
2422
- }
2423
- });
2424
- this._layersBack = [];
2425
- this.layers = new Proxy(newThis._layersBack, {
2426
- deleteProperty: function (target, property) {
2427
- var oldDuration = this.duration;
2428
- var value = target[property];
2429
- value.tryDetach(that);
2430
- delete target[property];
2431
- var current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration;
2432
- if (current)
2433
- publish(that, 'movie.change.layer.remove', { layer: value });
2434
- publish(that, 'movie.change.duration', { oldDuration: oldDuration });
2435
- return true;
2436
- },
2437
- set: function (target, property, value) {
2438
- var oldDuration = this.duration;
2439
- // Check if property is an number (an index)
2440
- if (!isNaN(Number(property))) {
2441
- if (target[property]) {
2442
- publish(that, 'movie.change.layer.remove', {
2443
- layer: target[property]
2444
- });
2445
- target[property].tryDetach();
2446
- }
2447
- // Attach layer to movie
2448
- value.tryAttach(that);
2449
- target[property] = value;
2450
- // Refresh screen when a relevant layer is added or removed
2451
- var current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration;
2452
- if (current)
2453
- publish(that, 'movie.change.layer.add', { layer: value });
2454
- publish(that, 'movie.change.duration', { oldDuration: oldDuration });
2455
- }
2456
- else {
2457
- target[property] = value;
2458
- }
2459
- return true;
2460
- }
2461
- });
2844
+ this.effects = new MovieEffects([], this);
2845
+ this.layers = new MovieLayers([], this);
2462
2846
  this._paused = true;
2463
2847
  this._ended = false;
2464
- // This variable helps prevent multiple frame-rendering loops at the same
2465
- // time (see `render`). It's only applicable when rendering.
2848
+ // This lock prevents multiple refresh loops at the same time (see
2849
+ // `render`). It's only valid while rendering.
2466
2850
  this._renderingFrame = false;
2467
2851
  this.currentTime = 0;
2468
- // For recording
2469
- this._mediaRecorder = null;
2470
- // -1 works well in inequalities
2471
- // The last time `play` was called
2852
+ // The last time `play` was called, -1 works well in comparisons
2472
2853
  this._lastPlayed = -1;
2473
- // What was `currentTime` when `play` was called
2854
+ // What `currentTime` was when `play` was called
2474
2855
  this._lastPlayedOffset = -1;
2475
- // newThis._updateInterval = 0.1; // time in seconds between each "timeupdate" event
2476
- // newThis._lastUpdate = -1;
2477
- if (newThis.autoRefresh)
2478
- newThis.refresh(); // render single frame on creation
2479
- // Subscribe to own event "change" (child events propogate up)
2480
- subscribe(newThis, 'movie.change', function () {
2481
- if (newThis.autoRefresh && !newThis.rendering)
2482
- newThis.refresh();
2483
- });
2484
- // Subscribe to own event "ended"
2485
- subscribe(newThis, 'movie.recordended', function () {
2486
- if (newThis.recording) {
2487
- newThis._mediaRecorder.requestData();
2488
- newThis._mediaRecorder.stop();
2489
- }
2490
- });
2491
- return newThis;
2492
2856
  }
2857
+ Movie.prototype._whenReady = function () {
2858
+ return __awaiter(this, void 0, void 0, function () {
2859
+ return __generator(this, function (_a) {
2860
+ switch (_a.label) {
2861
+ case 0: return [4 /*yield*/, Promise.all([
2862
+ Promise.all(this.layers.map(function (layer) { return layer.whenReady(); })),
2863
+ Promise.all(this.effects.map(function (effect) { return effect.whenReady(); }))
2864
+ ])];
2865
+ case 1:
2866
+ _a.sent();
2867
+ return [2 /*return*/];
2868
+ }
2869
+ });
2870
+ });
2871
+ };
2493
2872
  /**
2494
2873
  * Plays the movie
2495
- * @return fulfilled when the movie is done playing, never fails
2874
+ *
2875
+ * @param [options]
2876
+ * @param [options.onStart] Called when the movie starts playing
2877
+ *
2878
+ * @return Fulfilled when the movie is done playing, never fails
2496
2879
  */
2497
- Movie.prototype.play = function () {
2498
- var _this = this;
2499
- return new Promise(function (resolve) {
2500
- if (!_this.paused)
2501
- throw new Error('Already playing');
2502
- _this._paused = _this._ended = false;
2503
- _this._lastPlayed = performance.now();
2504
- _this._lastPlayedOffset = _this.currentTime;
2505
- if (!_this.renderingFrame)
2506
- // Not rendering (and not playing), so play.
2507
- _this._render(true, undefined, resolve);
2508
- // Stop rendering frame if currently doing so, because playing has higher
2509
- // priority. This will effect the next _render call.
2510
- _this._renderingFrame = false;
2511
- publish(_this, 'movie.play', {});
2880
+ Movie.prototype.play = function (options) {
2881
+ var _a;
2882
+ if (options === void 0) { options = {}; }
2883
+ return __awaiter(this, void 0, void 0, function () {
2884
+ var _this = this;
2885
+ return __generator(this, function (_b) {
2886
+ switch (_b.label) {
2887
+ case 0: return [4 /*yield*/, this._whenReady()];
2888
+ case 1:
2889
+ _b.sent();
2890
+ if (!this.paused) {
2891
+ throw new Error('Already playing');
2892
+ }
2893
+ this._paused = this._ended = false;
2894
+ this._lastPlayed = performance.now();
2895
+ this._lastPlayedOffset = this.currentTime;
2896
+ (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options);
2897
+ // For backwards compatibility
2898
+ publish(this, 'movie.play', {});
2899
+ // Repeatedly render frames until the movie ends
2900
+ return [4 /*yield*/, new Promise(function (resolve) {
2901
+ if (!_this.renderingFrame) {
2902
+ // Not rendering (and not playing), so play.
2903
+ _this._render(true, undefined, resolve);
2904
+ }
2905
+ // Stop rendering frame if currently doing so, because playing has higher
2906
+ // priority. This will affect the next _render call.
2907
+ _this._renderingFrame = false;
2908
+ })];
2909
+ case 2:
2910
+ // Repeatedly render frames until the movie ends
2911
+ _b.sent();
2912
+ return [2 /*return*/];
2913
+ }
2914
+ });
2915
+ });
2916
+ };
2917
+ /**
2918
+ * Updates the rendering canvas and audio destination to the visible canvas
2919
+ * and the audio context destination.
2920
+ */
2921
+ Movie.prototype._show = function () {
2922
+ this._canvas = this._visibleCanvas;
2923
+ this._cctx = this.canvas.getContext('2d');
2924
+ publish(this, 'audiodestinationupdate', { movie: this, destination: this.actx.destination });
2925
+ };
2926
+ /**
2927
+ * Streams the movie to a MediaStream
2928
+ *
2929
+ * @param options Options for the stream
2930
+ * @param options.frameRate The frame rate of the stream's video
2931
+ * @param options.duration The duration of the stream in seconds
2932
+ * @param options.video Whether to stream video. Defaults to true.
2933
+ * @param options.audio Whether to stream audio. Defaults to true.
2934
+ * @param options.onStart Called when the stream is started
2935
+ * @return Fulfilled when the stream is done, never fails
2936
+ */
2937
+ Movie.prototype.stream = function (options) {
2938
+ return __awaiter(this, void 0, void 0, function () {
2939
+ var tracks, visualStream, hasMediaTracks, audioDestination, audioStream;
2940
+ var _this = this;
2941
+ return __generator(this, function (_a) {
2942
+ switch (_a.label) {
2943
+ case 0:
2944
+ // Validate options
2945
+ if (!options || !options.frameRate) {
2946
+ throw new Error('Required option "frameRate" not provided to Movie.stream');
2947
+ }
2948
+ if (options.video === false && options.audio === false) {
2949
+ throw new Error('Both video and audio cannot be disabled');
2950
+ }
2951
+ if (!this.paused) {
2952
+ throw new Error("Cannot stream movie while it's already playing");
2953
+ }
2954
+ // Wait until all resources are loaded
2955
+ return [4 /*yield*/, this._whenReady()
2956
+ // Create a temporary canvas to stream from
2957
+ ];
2958
+ case 1:
2959
+ // Wait until all resources are loaded
2960
+ _a.sent();
2961
+ // Create a temporary canvas to stream from
2962
+ this._canvas = document.createElement('canvas');
2963
+ this.canvas.width = this._visibleCanvas.width;
2964
+ this.canvas.height = this._visibleCanvas.height;
2965
+ this._cctx = this.canvas.getContext('2d');
2966
+ tracks = [];
2967
+ // Add video track
2968
+ if (options.video !== false) {
2969
+ visualStream = this.canvas.captureStream(options.frameRate);
2970
+ tracks = tracks.concat(visualStream.getTracks());
2971
+ }
2972
+ hasMediaTracks = this.layers.some(function (layer) { return layer instanceof Audio || layer instanceof Video; });
2973
+ // If no media tracks present, don't include an audio stream, because
2974
+ // Chrome doesn't record silence when an audio stream is present.
2975
+ if (hasMediaTracks && options.audio !== false) {
2976
+ audioDestination = this.actx.createMediaStreamDestination();
2977
+ audioStream = audioDestination.stream;
2978
+ tracks = tracks.concat(audioStream.getTracks());
2979
+ // Notify layers and any other listeners of the new audio destination
2980
+ publish(this, 'audiodestinationupdate', { movie: this, destination: audioDestination });
2981
+ }
2982
+ // Create the stream
2983
+ this._currentStream = new MediaStream(tracks);
2984
+ // Play the movie
2985
+ this._endTime = options.duration ? this.currentTime + options.duration : this.duration;
2986
+ return [4 /*yield*/, this.play({
2987
+ onStart: function () {
2988
+ // Call the user's onStart callback
2989
+ options.onStart(_this._currentStream);
2990
+ }
2991
+ })
2992
+ // Clear the stream after the movie is done playing
2993
+ ];
2994
+ case 2:
2995
+ _a.sent();
2996
+ // Clear the stream after the movie is done playing
2997
+ this._currentStream.getTracks().forEach(function (track) {
2998
+ track.stop();
2999
+ });
3000
+ this._currentStream = null;
3001
+ this._show();
3002
+ return [2 /*return*/];
3003
+ }
3004
+ });
2512
3005
  });
2513
3006
  };
2514
3007
  /**
2515
3008
  * Plays the movie in the background and records it
2516
3009
  *
2517
3010
  * @param options
2518
- * @param frameRate
3011
+ * @param [options.frameRate] - Video frame rate
2519
3012
  * @param [options.video=true] - whether to include video in recording
2520
3013
  * @param [options.audio=true] - whether to include audio in recording
2521
- * @param [options.mediaRecorderOptions=undefined] - options to pass to the <code>MediaRecorder</code>
3014
+ * @param [options.mediaRecorderOptions=undefined] - Options to pass to the
3015
+ * `MediaRecorder` constructor
2522
3016
  * @param [options.type='video/webm'] - MIME type for exported video
2523
- * constructor
2524
- * @return resolves when done recording, rejects when internal media recorder errors
3017
+ * @param [options.onStart] - Called when the recording starts
3018
+ * @return Resolves when done recording, rejects when media recorder errors
2525
3019
  */
2526
- // TEST: *support recording that plays back with audio!*
2527
- // TODO: figure out how to do offline recording (faster than realtime).
2528
- // TODO: improve recording performance to increase frame rate?
3020
+ // TODO: Improve recording performance to increase frame rate
2529
3021
  Movie.prototype.record = function (options) {
2530
- var _this = this;
2531
- if (options.video === false && options.audio === false)
2532
- throw new Error('Both video and audio cannot be disabled');
2533
- if (!this.paused)
2534
- throw new Error('Cannot record movie while already playing or recording');
2535
- var mimeType = options.type || 'video/webm';
2536
- if (MediaRecorder && MediaRecorder.isTypeSupported && !MediaRecorder.isTypeSupported(mimeType))
2537
- throw new Error('Please pass a valid MIME type for the exported video');
2538
- return new Promise(function (resolve, reject) {
2539
- var canvasCache = _this.canvas;
2540
- // Record on a temporary canvas context
2541
- _this._canvas = document.createElement('canvas');
2542
- _this.canvas.width = canvasCache.width;
2543
- _this.canvas.height = canvasCache.height;
2544
- _this._cctx = _this.canvas.getContext('2d');
2545
- // frame blobs
2546
- var recordedChunks = [];
2547
- // Combine image + audio, or just pick one
2548
- var tracks = [];
2549
- if (options.video !== false) {
2550
- var visualStream = _this.canvas.captureStream(options.frameRate);
2551
- tracks = tracks.concat(visualStream.getTracks());
2552
- }
2553
- // Check if there's a layer that's an instance of an AudioSourceMixin
2554
- // (Audio or Video)
2555
- var hasMediaTracks = _this.layers.some(function (layer) { return layer instanceof Audio || layer instanceof Video; });
2556
- // If no media tracks present, don't include an audio stream, because
2557
- // Chrome doesn't record silence when an audio stream is present.
2558
- if (hasMediaTracks && options.audio !== false) {
2559
- var audioDestination = _this.actx.createMediaStreamDestination();
2560
- var audioStream = audioDestination.stream;
2561
- tracks = tracks.concat(audioStream.getTracks());
2562
- publish(_this, 'movie.audiodestinationupdate', { movie: _this, destination: audioDestination });
2563
- }
2564
- var stream = new MediaStream(tracks);
2565
- var mediaRecorderOptions = __assign(__assign({}, (options.mediaRecorderOptions || {})), { mimeType: mimeType });
2566
- var mediaRecorder = new MediaRecorder(stream, mediaRecorderOptions);
2567
- mediaRecorder.ondataavailable = function (event) {
2568
- // if (this._paused) reject(new Error("Recording was interrupted"));
2569
- if (event.data.size > 0)
2570
- recordedChunks.push(event.data);
2571
- };
2572
- // TODO: publish to movie, not layers
2573
- mediaRecorder.onstop = function () {
2574
- _this._paused = true;
2575
- _this._ended = true;
2576
- _this._canvas = canvasCache;
2577
- _this._cctx = _this.canvas.getContext('2d');
2578
- publish(_this, 'movie.audiodestinationupdate', { movie: _this, destination: _this.actx.destination });
2579
- _this._mediaRecorder = null;
2580
- // Construct the exported video out of all the frame blobs.
2581
- resolve(new Blob(recordedChunks, {
2582
- type: mimeType
2583
- }));
2584
- };
2585
- mediaRecorder.onerror = reject;
2586
- mediaRecorder.start();
2587
- _this._mediaRecorder = mediaRecorder;
2588
- _this._recordEndTime = options.duration ? _this.currentTime + options.duration : _this.duration;
2589
- _this.play();
2590
- publish(_this, 'movie.record', { options: options });
3022
+ var _a;
3023
+ return __awaiter(this, void 0, void 0, function () {
3024
+ var mimeType, stream, recordedChunks, mediaRecorderOptions;
3025
+ var _this = this;
3026
+ return __generator(this, function (_b) {
3027
+ switch (_b.label) {
3028
+ case 0:
3029
+ // Validate options
3030
+ if (options.video === false && options.audio === false) {
3031
+ throw new Error('Both video and audio cannot be disabled');
3032
+ }
3033
+ if (!this.paused) {
3034
+ throw new Error("Cannot record movie while it's already playing");
3035
+ }
3036
+ mimeType = options.type || 'video/webm';
3037
+ if (MediaRecorder && MediaRecorder.isTypeSupported && !MediaRecorder.isTypeSupported(mimeType)) {
3038
+ throw new Error('Please pass a valid MIME type for the exported video');
3039
+ }
3040
+ return [4 /*yield*/, new Promise(function (resolve) {
3041
+ _this.stream({
3042
+ frameRate: options.frameRate,
3043
+ duration: options.duration,
3044
+ video: options.video,
3045
+ audio: options.audio,
3046
+ onStart: resolve
3047
+ }).then(function () {
3048
+ // Stop the media recorder when the movie is done playing
3049
+ _this._recorder.requestData();
3050
+ _this._recorder.stop();
3051
+ });
3052
+ })
3053
+ // The array to store the recorded chunks
3054
+ ];
3055
+ case 1:
3056
+ stream = _b.sent();
3057
+ recordedChunks = [];
3058
+ mediaRecorderOptions = __assign(__assign({}, (options.mediaRecorderOptions || {})), { mimeType: mimeType });
3059
+ this._recorder = new MediaRecorder(stream, mediaRecorderOptions);
3060
+ this._recorder.ondataavailable = function (event) {
3061
+ // if (this._paused) reject(new Error("Recording was interrupted"));
3062
+ if (event.data.size > 0) {
3063
+ recordedChunks.push(event.data);
3064
+ }
3065
+ };
3066
+ // Start recording
3067
+ this._recorder.start();
3068
+ this._recording = true;
3069
+ // Notify caller that the media recorder has started
3070
+ (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options, this._recorder);
3071
+ // For backwards compatibility
3072
+ publish(this, 'movie.record', { options: options });
3073
+ // Wait until the media recorder is done recording and processing
3074
+ return [4 /*yield*/, new Promise(function (resolve, reject) {
3075
+ _this._recorder.onstop = function () {
3076
+ resolve();
3077
+ };
3078
+ _this._recorder.onerror = reject;
3079
+ })
3080
+ // Clean up
3081
+ ];
3082
+ case 2:
3083
+ // Wait until the media recorder is done recording and processing
3084
+ _b.sent();
3085
+ // Clean up
3086
+ this._paused = true;
3087
+ this._ended = true;
3088
+ this._recording = false;
3089
+ // Construct the exported video out of all the frame blobs.
3090
+ return [2 /*return*/, new Blob(recordedChunks, {
3091
+ type: mimeType
3092
+ })];
3093
+ }
3094
+ });
2591
3095
  });
2592
3096
  };
2593
3097
  /**
2594
- * Stops the movie, without reseting the playback position
2595
- * @return the movie (for chaining)
3098
+ * Stops the movie without resetting the playback position
3099
+ * @return The movie
2596
3100
  */
2597
3101
  Movie.prototype.pause = function () {
3102
+ // Update state
2598
3103
  this._paused = true;
2599
3104
  // Deactivate all layers
2600
- for (var i = 0; i < this.layers.length; i++)
3105
+ for (var i = 0; i < this.layers.length; i++) {
2601
3106
  if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
2602
3107
  var layer = this.layers[i];
2603
- layer.stop();
2604
- layer.active = false;
3108
+ if (layer.active) {
3109
+ layer.stop();
3110
+ layer.active = false;
3111
+ }
2605
3112
  }
3113
+ }
3114
+ // For backwards compatibility, notify event listeners that the movie has
3115
+ // paused
2606
3116
  publish(this, 'movie.pause', {});
2607
3117
  return this;
2608
3118
  };
2609
3119
  /**
2610
3120
  * Stops playback and resets the playback position
2611
- * @return the movie (for chaining)
3121
+ * @return The movie
2612
3122
  */
2613
3123
  Movie.prototype.stop = function () {
2614
3124
  this.pause();
@@ -2617,8 +3127,8 @@ var etro = (function () {
2617
3127
  };
2618
3128
  /**
2619
3129
  * @param [timestamp=performance.now()]
2620
- * @param [done=undefined] - called when done playing or when the current frame is loaded
2621
- * @private
3130
+ * @param [done=undefined] - Called when done playing or when the current
3131
+ * frame is loaded
2622
3132
  */
2623
3133
  Movie.prototype._render = function (repeat, timestamp, done) {
2624
3134
  var _this = this;
@@ -2626,177 +3136,207 @@ var etro = (function () {
2626
3136
  if (done === void 0) { done = undefined; }
2627
3137
  clearCachedValues(this);
2628
3138
  if (!this.rendering) {
2629
- // (!this.paused || this._renderingFrame) is true so it's playing or it's
3139
+ // (this.paused && !this._renderingFrame) is true so it's playing or it's
2630
3140
  // rendering a single frame.
2631
- if (done)
3141
+ if (done) {
2632
3142
  done();
3143
+ }
2633
3144
  return;
2634
3145
  }
2635
- this._updateCurrentTime(timestamp);
2636
- // TODO: Is calling duration every frame bad for performance? (remember,
2637
- // it's calling Array.reduce)
2638
- var end = this.recording ? this._recordEndTime : this.duration;
2639
- if (this.currentTime > end) {
2640
- if (this.recording)
2641
- publish(this, 'movie.recordended', { movie: this });
2642
- if (this.currentTime > this.duration)
2643
- publish(this, 'movie.ended', { movie: this, repeat: this.repeat });
2644
- // TODO: only reset currentTime if repeating
2645
- if (this.repeat) {
2646
- // Don't use setter, which publishes 'movie.seek'. Instead, update the
2647
- // value and publish a 'movie.timeupdate' event.
3146
+ if (this.ready) {
3147
+ publish(this, 'movie.loadeddata', { movie: this });
3148
+ // If the movie is streaming or recording, resume the media recorder
3149
+ if (this._recording && this._recorder.state === 'paused') {
3150
+ this._recorder.resume();
3151
+ }
3152
+ // If the movie is streaming or recording, end at the specified duration.
3153
+ // Otherwise, end at the movie's duration, because play() does not
3154
+ // support playing a portion of the movie yet.
3155
+ // TODO: Is calling duration every frame bad for performance? (remember,
3156
+ // it's calling Array.reduce)
3157
+ var end = this._currentStream ? this._endTime : this.duration;
3158
+ this._updateCurrentTime(timestamp, end);
3159
+ if (this.currentTime === end) {
3160
+ if (this.recording) {
3161
+ publish(this, 'movie.recordended', { movie: this });
3162
+ }
3163
+ if (this.currentTime === this.duration) {
3164
+ publish(this, 'movie.ended', { movie: this, repeat: this.repeat });
3165
+ }
3166
+ // Don't use setter, which publishes 'seek'. Instead, update the
3167
+ // value and publish a 'imeupdate' event.
2648
3168
  this._currentTime = 0;
2649
3169
  publish(this, 'movie.timeupdate', { movie: this });
2650
- }
2651
- this._lastPlayed = performance.now();
2652
- this._lastPlayedOffset = 0; // this.currentTime
2653
- this._renderingFrame = false;
2654
- // Stop playback or recording if done (except if it's playing and repeat
2655
- // is true)
2656
- if (!(!this.recording && this.repeat)) {
2657
- this._paused = true;
2658
- this._ended = true;
2659
- // Deactivate all layers
2660
- for (var i = 0; i < this.layers.length; i++)
2661
- if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
2662
- var layer = this.layers[i];
2663
- // A layer that has been deleted before layers.length has been updated
2664
- // (see the layers proxy in the constructor).
2665
- if (!layer || !layer.active)
2666
- continue;
2667
- layer.stop();
2668
- layer.active = false;
3170
+ this._lastPlayed = performance.now();
3171
+ this._lastPlayedOffset = 0; // this.currentTime
3172
+ this._renderingFrame = false;
3173
+ // Stop playback or recording if done (except if it's playing and repeat
3174
+ // is true)
3175
+ if (!(!this.recording && this.repeat)) {
3176
+ this._paused = true;
3177
+ this._ended = true;
3178
+ // Deactivate all layers
3179
+ for (var i = 0; i < this.layers.length; i++) {
3180
+ if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
3181
+ var layer = this.layers[i];
3182
+ // A layer that has been deleted before layers.length has been updated
3183
+ // (see the layers proxy in the constructor).
3184
+ if (!layer || !layer.active) {
3185
+ continue;
3186
+ }
3187
+ layer.stop();
3188
+ layer.active = false;
3189
+ }
3190
+ }
3191
+ publish(this, 'movie.pause', {});
3192
+ if (done) {
3193
+ done();
2669
3194
  }
2670
- if (done)
2671
- done();
2672
- return;
3195
+ return;
3196
+ }
2673
3197
  }
3198
+ // Do render
3199
+ this._renderBackground(timestamp);
3200
+ this._renderLayers();
3201
+ this._applyEffects();
2674
3202
  }
2675
- // Do render
2676
- this._renderBackground(timestamp);
2677
- var frameFullyLoaded = this._renderLayers();
2678
- this._applyEffects();
2679
- if (frameFullyLoaded)
2680
- publish(this, 'movie.loadeddata', { movie: this });
2681
- // If didn't load in this instant, repeatedly frame-render until frame is
2682
- // loaded.
2683
- // If the expression below is false, don't publish an event, just silently
2684
- // stop render loop.
2685
- if (!repeat || (this._renderingFrame && frameFullyLoaded)) {
3203
+ else {
3204
+ // If we are recording, pause the media recorder until the movie is
3205
+ // ready.
3206
+ if (this.recording && this._recorder.state === 'recording') {
3207
+ this._recorder.pause();
3208
+ }
3209
+ }
3210
+ // If the frame didn't load this instant, repeatedly frame-render until it
3211
+ // is loaded.
3212
+ // If the expression below is true, don't publish an event, just silently
3213
+ // stop the render loop.
3214
+ if (this._renderingFrame && this.ready) {
2686
3215
  this._renderingFrame = false;
2687
- if (done)
3216
+ if (done) {
2688
3217
  done();
3218
+ }
2689
3219
  return;
2690
3220
  }
3221
+ // TODO: Is making a new arrow function every frame bad for performance?
2691
3222
  window.requestAnimationFrame(function () {
2692
3223
  _this._render(repeat, undefined, done);
2693
- }); // TODO: research performance cost
3224
+ });
2694
3225
  };
2695
- Movie.prototype._updateCurrentTime = function (timestamp) {
2696
- // If we're only instant-rendering (current frame only), it doens't matter
2697
- // if it's paused or not.
3226
+ Movie.prototype._updateCurrentTime = function (timestampMs, end) {
3227
+ // If we're only frame-rendering (current frame only), it doesn't matter if
3228
+ // it's paused or not.
2698
3229
  if (!this._renderingFrame) {
2699
- // if ((timestamp - this._lastUpdate) >= this._updateInterval) {
2700
- var sinceLastPlayed = (timestamp - this._lastPlayed) / 1000;
2701
- var currentTime = this._lastPlayedOffset + sinceLastPlayed; // don't use setter
3230
+ var sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000;
3231
+ var currentTime = this._lastPlayedOffset + sinceLastPlayed;
2702
3232
  if (this.currentTime !== currentTime) {
3233
+ // Update the current time (don't use setter)
2703
3234
  this._currentTime = currentTime;
3235
+ // For backwards compatibility, publish a 'movie.timeupdate' event.
2704
3236
  publish(this, 'movie.timeupdate', { movie: this });
2705
3237
  }
2706
- // this._lastUpdate = timestamp;
2707
- // }
3238
+ if (this.currentTime > end) {
3239
+ this._currentTime = end;
3240
+ }
2708
3241
  }
2709
3242
  };
2710
3243
  Movie.prototype._renderBackground = function (timestamp) {
2711
3244
  this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3245
+ // Evaluate background color (since it's a dynamic property)
2712
3246
  var background = val(this, 'background', timestamp);
2713
- if (background) { // TODO: check val'd result
3247
+ if (background) {
2714
3248
  this.cctx.fillStyle = background;
2715
3249
  this.cctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
2716
3250
  }
2717
3251
  };
2718
3252
  /**
2719
- * @return whether or not video frames are loaded
2720
3253
  * @param [timestamp=performance.now()]
2721
- * @private
2722
3254
  */
2723
3255
  Movie.prototype._renderLayers = function () {
2724
- var frameFullyLoaded = true;
2725
3256
  for (var i = 0; i < this.layers.length; i++) {
2726
- if (!Object.prototype.hasOwnProperty.call(this.layers, i))
3257
+ if (!Object.prototype.hasOwnProperty.call(this.layers, i)) {
2727
3258
  continue;
3259
+ }
2728
3260
  var layer = this.layers[i];
2729
3261
  // A layer that has been deleted before layers.length has been updated
2730
3262
  // (see the layers proxy in the constructor).
2731
- if (!layer)
3263
+ if (!layer) {
2732
3264
  continue;
2733
- var reltime = this.currentTime - layer.startTime;
3265
+ }
2734
3266
  // Cancel operation if layer disabled or outside layer time interval
3267
+ var reltime = this.currentTime - layer.startTime;
2735
3268
  if (!val(layer, 'enabled', reltime) ||
2736
3269
  // TODO > or >= ?
2737
3270
  this.currentTime < layer.startTime || this.currentTime > layer.startTime + layer.duration) {
2738
3271
  // Layer is not active.
2739
3272
  // If only rendering this frame, we are not "starting" the layer.
2740
3273
  if (layer.active && !this._renderingFrame) {
2741
- // TODO: make a `deactivate()` method?
2742
3274
  layer.stop();
2743
3275
  layer.active = false;
2744
3276
  }
2745
3277
  continue;
2746
3278
  }
3279
+ // If we are playing (not refreshing), update the layer's progress
3280
+ if (!this._renderingFrame) {
3281
+ layer.progress(reltime);
3282
+ }
2747
3283
  // If only rendering this frame, we are not "starting" the layer
2748
3284
  if (!layer.active && val(layer, 'enabled', reltime) && !this._renderingFrame) {
2749
- // TODO: make an `activate()` method?
2750
3285
  layer.start();
2751
3286
  layer.active = true;
2752
3287
  }
2753
- // if the layer has an input file
2754
- if ('source' in layer)
2755
- frameFullyLoaded = frameFullyLoaded && layer.source.readyState >= 2;
2756
3288
  layer.render();
2757
3289
  // if the layer has visual component
2758
3290
  if (layer instanceof Visual) {
2759
3291
  var canvas = layer.canvas;
2760
- // layer.canvas.width and layer.canvas.height should already be interpolated
2761
- // if the layer has an area (else InvalidStateError from canvas)
2762
- if (canvas.width * canvas.height > 0)
3292
+ if (canvas.width * canvas.height > 0) {
2763
3293
  this.cctx.drawImage(canvas, val(layer, 'x', reltime), val(layer, 'y', reltime), canvas.width, canvas.height);
3294
+ }
2764
3295
  }
2765
3296
  }
2766
- return frameFullyLoaded;
2767
3297
  };
2768
3298
  Movie.prototype._applyEffects = function () {
2769
3299
  for (var i = 0; i < this.effects.length; i++) {
2770
3300
  var effect = this.effects[i];
2771
3301
  // An effect that has been deleted before effects.length has been updated
2772
3302
  // (see the effectsproxy in the constructor).
2773
- if (!effect)
3303
+ if (!effect) {
2774
3304
  continue;
3305
+ }
2775
3306
  effect.apply(this, this.currentTime);
2776
3307
  }
2777
3308
  };
2778
3309
  /**
2779
- * Refreshes the screen (only use this if auto-refresh is disabled)
2780
- * @return - resolves when the frame is loaded
3310
+ * Refreshes the screen
3311
+ *
3312
+ * Only use this if auto-refresh is disabled
3313
+ *
3314
+ * @return - Promise that resolves when the frame is loaded
2781
3315
  */
2782
3316
  Movie.prototype.refresh = function () {
2783
3317
  var _this = this;
3318
+ // Refreshing while playing can interrupt playback
3319
+ if (!this.paused) {
3320
+ throw new Error('Already playing');
3321
+ }
2784
3322
  return new Promise(function (resolve) {
2785
3323
  _this._renderingFrame = true;
2786
3324
  _this._render(false, undefined, resolve);
2787
3325
  });
2788
3326
  };
2789
3327
  /**
2790
- * Convienence method
3328
+ * Convienence method (TODO: remove)
2791
3329
  */
2792
3330
  Movie.prototype._publishToLayers = function (type, event) {
2793
- for (var i = 0; i < this.layers.length; i++)
2794
- if (Object.prototype.hasOwnProperty.call(this.layers, i))
3331
+ for (var i = 0; i < this.layers.length; i++) {
3332
+ if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
2795
3333
  publish(this.layers[i], type, event);
3334
+ }
3335
+ }
2796
3336
  };
2797
3337
  Object.defineProperty(Movie.prototype, "rendering", {
2798
3338
  /**
2799
- * If the movie is playing, recording or refreshing
3339
+ * `true` if the movie is playing, recording or refreshing
2800
3340
  */
2801
3341
  get: function () {
2802
3342
  return !this.paused || this._renderingFrame;
@@ -2806,7 +3346,7 @@ var etro = (function () {
2806
3346
  });
2807
3347
  Object.defineProperty(Movie.prototype, "renderingFrame", {
2808
3348
  /**
2809
- * If the movie is refreshing current frame
3349
+ * `true` if the movie is refreshing the current frame
2810
3350
  */
2811
3351
  get: function () {
2812
3352
  return this._renderingFrame;
@@ -2816,17 +3356,19 @@ var etro = (function () {
2816
3356
  });
2817
3357
  Object.defineProperty(Movie.prototype, "recording", {
2818
3358
  /**
2819
- * If the movie is recording
3359
+ * `true` if the movie is recording
2820
3360
  */
2821
3361
  get: function () {
2822
- return !!this._mediaRecorder;
3362
+ return this._recording;
2823
3363
  },
2824
3364
  enumerable: false,
2825
3365
  configurable: true
2826
3366
  });
2827
3367
  Object.defineProperty(Movie.prototype, "duration", {
2828
3368
  /**
2829
- * The combined duration of all layers
3369
+ * The duration of the movie in seconds
3370
+ *
3371
+ * Calculated from the end time of the last layer
2830
3372
  */
2831
3373
  // TODO: dirty flag?
2832
3374
  get: function () {
@@ -2836,16 +3378,16 @@ var etro = (function () {
2836
3378
  configurable: true
2837
3379
  });
2838
3380
  /**
2839
- * Convienence method for <code>layers.push()</code>
3381
+ * Convenience method for `layers.push()`
2840
3382
  * @param layer
2841
- * @return the movie
3383
+ * @return The movie
2842
3384
  */
2843
3385
  Movie.prototype.addLayer = function (layer) {
2844
3386
  this.layers.push(layer);
2845
3387
  return this;
2846
3388
  };
2847
3389
  /**
2848
- * Convienence method for <code>effects.push()</code>
3390
+ * Convenience method for `effects.push()`
2849
3391
  * @param effect
2850
3392
  * @return the movie
2851
3393
  */
@@ -2855,6 +3397,7 @@ var etro = (function () {
2855
3397
  };
2856
3398
  Object.defineProperty(Movie.prototype, "paused", {
2857
3399
  /**
3400
+ * `true` if the movie is paused
2858
3401
  */
2859
3402
  get: function () {
2860
3403
  return this._paused;
@@ -2864,7 +3407,7 @@ var etro = (function () {
2864
3407
  });
2865
3408
  Object.defineProperty(Movie.prototype, "ended", {
2866
3409
  /**
2867
- * If the playback position is at the end of the movie
3410
+ * `true` if the playback position is at the end of the movie
2868
3411
  */
2869
3412
  get: function () {
2870
3413
  return this._ended;
@@ -2872,50 +3415,89 @@ var etro = (function () {
2872
3415
  enumerable: false,
2873
3416
  configurable: true
2874
3417
  });
3418
+ /**
3419
+ * Skips to the provided playback position, updating {@link currentTime}.
3420
+ *
3421
+ * @param time - The new playback position (in seconds)
3422
+ */
3423
+ Movie.prototype.seek = function (time) {
3424
+ this._currentTime = time;
3425
+ // Call `seek` on every layer
3426
+ for (var i = 0; i < this.layers.length; i++) {
3427
+ var layer = this.layers[i];
3428
+ if (layer) {
3429
+ var relativeTime = time - layer.startTime;
3430
+ if (relativeTime >= 0 && relativeTime <= layer.duration) {
3431
+ layer.seek(relativeTime);
3432
+ }
3433
+ else {
3434
+ layer.seek(undefined);
3435
+ }
3436
+ }
3437
+ }
3438
+ // For backwards compatibility, publish a `seek` event
3439
+ publish(this, 'movie.seek', {});
3440
+ };
2875
3441
  Object.defineProperty(Movie.prototype, "currentTime", {
2876
3442
  /**
2877
- * The current playback position
3443
+ * The current playback position in seconds
2878
3444
  */
2879
3445
  get: function () {
2880
3446
  return this._currentTime;
2881
3447
  },
3448
+ /**
3449
+ * Skips to the provided playback position, updating {@link currentTime}.
3450
+ *
3451
+ * @param time - The new playback position (in seconds)
3452
+ *
3453
+ * @deprecated Use `seek` instead
3454
+ */
2882
3455
  set: function (time) {
2883
- this._currentTime = time;
2884
- publish(this, 'movie.seek', {});
2885
- // Render single frame to match new time
2886
- if (this.autoRefresh)
2887
- this.refresh();
3456
+ this.seek(time);
2888
3457
  },
2889
3458
  enumerable: false,
2890
3459
  configurable: true
2891
3460
  });
2892
3461
  /**
2893
- * Sets the current playback position. This is a more powerful version of
2894
- * `set currentTime`.
3462
+ * Skips to the provided playback position, updating {@link currentTime}.
2895
3463
  *
2896
- * @param time - the new cursor's time value in seconds
2897
- * @param [refresh=true] - whether to render a single frame
2898
- * @return resolves when the current frame is rendered if
2899
- * <code>refresh</code> is true, otherwise resolves immediately
3464
+ * @param time - The new time (in seconds)
3465
+ * @param [refresh=true] - Render a single frame?
3466
+ * @return Promise that resolves when the current frame is rendered if
3467
+ * `refresh` is true; otherwise resolves immediately.
2900
3468
  *
3469
+ * @deprecated Call {@link seek} and {@link refresh} separately
2901
3470
  */
2902
- // TODO: Refresh if only auto-refreshing is enabled
3471
+ // TODO: Refresh only if auto-refreshing is enabled
2903
3472
  Movie.prototype.setCurrentTime = function (time, refresh) {
2904
3473
  var _this = this;
2905
3474
  if (refresh === void 0) { refresh = true; }
2906
3475
  return new Promise(function (resolve, reject) {
2907
- _this._currentTime = time;
2908
- publish(_this, 'movie.seek', {});
2909
- if (refresh)
3476
+ _this.seek(time);
3477
+ if (refresh) {
2910
3478
  // Pass promise callbacks to `refresh`
2911
3479
  _this.refresh().then(resolve).catch(reject);
2912
- else
3480
+ }
3481
+ else {
2913
3482
  resolve();
3483
+ }
2914
3484
  });
2915
3485
  };
3486
+ Object.defineProperty(Movie.prototype, "ready", {
3487
+ /**
3488
+ * `true` if the movie is ready for playback
3489
+ */
3490
+ get: function () {
3491
+ var layersReady = this.layers.every(function (layer) { return layer.ready; });
3492
+ var effectsReady = this.effects.every(function (effect) { return effect.ready; });
3493
+ return layersReady && effectsReady;
3494
+ },
3495
+ enumerable: false,
3496
+ configurable: true
3497
+ });
2916
3498
  Object.defineProperty(Movie.prototype, "canvas", {
2917
3499
  /**
2918
- * The rendering canvas
3500
+ * The HTML canvas element used for rendering
2919
3501
  */
2920
3502
  get: function () {
2921
3503
  return this._canvas;
@@ -2925,7 +3507,7 @@ var etro = (function () {
2925
3507
  });
2926
3508
  Object.defineProperty(Movie.prototype, "cctx", {
2927
3509
  /**
2928
- * The rendering canvas's context
3510
+ * The canvas context used for rendering
2929
3511
  */
2930
3512
  get: function () {
2931
3513
  return this._cctx;
@@ -2935,7 +3517,7 @@ var etro = (function () {
2935
3517
  });
2936
3518
  Object.defineProperty(Movie.prototype, "width", {
2937
3519
  /**
2938
- * The width of the rendering canvas
3520
+ * The width of the output canvas
2939
3521
  */
2940
3522
  get: function () {
2941
3523
  return this.canvas.width;
@@ -2948,7 +3530,7 @@ var etro = (function () {
2948
3530
  });
2949
3531
  Object.defineProperty(Movie.prototype, "height", {
2950
3532
  /**
2951
- * The height of the rendering canvas
3533
+ * The height of the output canvas
2952
3534
  */
2953
3535
  get: function () {
2954
3536
  return this.canvas.height;
@@ -2960,38 +3542,46 @@ var etro = (function () {
2960
3542
  configurable: true
2961
3543
  });
2962
3544
  Object.defineProperty(Movie.prototype, "movie", {
3545
+ /**
3546
+ * @return The movie
3547
+ */
2963
3548
  get: function () {
2964
3549
  return this;
2965
3550
  },
2966
3551
  enumerable: false,
2967
3552
  configurable: true
2968
3553
  });
3554
+ /**
3555
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
3556
+ */
2969
3557
  Movie.prototype.getDefaultOptions = function () {
2970
3558
  return {
2971
3559
  canvas: undefined,
2972
3560
  /**
2973
3561
  * @name module:movie#background
2974
- * @desc The css color for the background, or <code>null</code> for transparency
3562
+ * @desc The color for the background, or <code>null</code> for transparency
2975
3563
  */
2976
- background: '#000',
3564
+ background: parseColor('#000'),
2977
3565
  /**
2978
3566
  * @name module:movie#repeat
2979
3567
  */
2980
- repeat: false,
2981
- /**
2982
- * @name module:movie#autoRefresh
2983
- * @desc Whether to refresh when changes are made that would effect the current frame
2984
- */
2985
- autoRefresh: true
3568
+ repeat: false
2986
3569
  };
2987
3570
  };
2988
3571
  return Movie;
2989
3572
  }());
2990
- // id for events (independent of instance, but easy to access when on prototype chain)
3573
+ // Id for events
2991
3574
  Movie.prototype.type = 'movie';
2992
- // TODO: refactor so we don't need to explicitly exclude some of these
2993
- Movie.prototype.publicExcludes = ['canvas', 'cctx', 'actx', 'layers', 'effects'];
2994
- Movie.prototype.propertyFilters = {};
3575
+ Movie.prototype.propertyFilters = {};
3576
+ deprecate('movie.audiodestinationupdate', 'audiodestinationupdate');
3577
+ deprecate('movie.ended', undefined);
3578
+ deprecate('movie.loadeddata', undefined);
3579
+ deprecate('movie.pause', undefined, 'Wait for `play()`, `stream()`, or `record()` to resolve instead.');
3580
+ deprecate('movie.play', undefined, 'Provide an `onStart` callback to `play()`, `stream()`, or `record()` instead.');
3581
+ deprecate('movie.record', undefined, 'Provide an `onStart` callback to `record()` instead.');
3582
+ deprecate('movie.recordended', undefined, 'Wait for `record()` to resolve instead.');
3583
+ deprecate('movie.seek', undefined, 'Override the `seek` method on layers instead.');
3584
+ deprecate('movie.timeupdate', undefined, 'Override the `progress` method on layers instead.');
2995
3585
 
2996
3586
  /*
2997
3587
  * Typedoc can't handle default exports. To let users import default export and
@@ -3001,6 +3591,7 @@ var etro = (function () {
3001
3591
  */
3002
3592
 
3003
3593
  var etro = /*#__PURE__*/Object.freeze({
3594
+ __proto__: null,
3004
3595
  layer: index,
3005
3596
  effect: index$1,
3006
3597
  event: event,
@@ -3016,8 +3607,7 @@ var etro = (function () {
3016
3607
  parseColor: parseColor,
3017
3608
  Font: Font,
3018
3609
  parseFont: parseFont,
3019
- mapPixels: mapPixels,
3020
- watchPublic: watchPublic
3610
+ mapPixels: mapPixels
3021
3611
  });
3022
3612
 
3023
3613
  /**