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