etro 0.9.1 → 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 (52) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CONTRIBUTING.md +23 -20
  3. package/README.md +3 -2
  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 +1156 -575
  9. package/dist/etro-iife.js +1156 -575
  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 +3 -0
  16. package/dist/layer/video.d.ts +13 -1
  17. package/dist/layer/visual.d.ts +6 -2
  18. package/dist/movie/effects.d.ts +6 -0
  19. package/dist/movie/index.d.ts +1 -0
  20. package/dist/movie/layers.d.ts +6 -0
  21. package/dist/movie/movie.d.ts +260 -0
  22. package/dist/object.d.ts +9 -6
  23. package/dist/util.d.ts +4 -10
  24. package/eslint.conf.js +2 -2
  25. package/karma.conf.js +4 -7
  26. package/package.json +8 -7
  27. package/src/custom-array.ts +43 -0
  28. package/src/effect/base.ts +23 -22
  29. package/src/effect/gaussian-blur.ts +11 -6
  30. package/src/effect/pixelate.ts +3 -3
  31. package/src/effect/shader.ts +33 -27
  32. package/src/effect/stack.ts +43 -30
  33. package/src/effect/transform.ts +14 -7
  34. package/src/event.ts +111 -21
  35. package/src/layer/audio-source.ts +60 -20
  36. package/src/layer/audio.ts +25 -3
  37. package/src/layer/base.ts +79 -26
  38. package/src/layer/image.ts +26 -2
  39. package/src/layer/text.ts +7 -0
  40. package/src/layer/video.ts +31 -4
  41. package/src/layer/visual-source.ts +43 -1
  42. package/src/layer/visual.ts +50 -28
  43. package/src/movie/effects.ts +26 -0
  44. package/src/movie/index.ts +1 -0
  45. package/src/movie/layers.ts +26 -0
  46. package/src/movie/movie.ts +855 -0
  47. package/src/object.ts +9 -6
  48. package/src/util.ts +68 -89
  49. package/dist/movie.d.ts +0 -201
  50. package/src/movie.ts +0 -744
  51. /package/scripts/{gen-effect-samples.html → effect/gen-effect-samples.html} +0 -0
  52. /package/scripts/{save-effect-samples.js → effect/save-effect-samples.js} +0 -0
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,10 +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({
128
240
  __proto__: null,
241
+ deprecate: deprecate,
129
242
  subscribe: subscribe,
130
243
  unsubscribe: unsubscribe,
131
244
  publish: publish
@@ -143,8 +256,9 @@ var event = /*#__PURE__*/Object.freeze({
143
256
  function getPropertyDescriptor(obj, name) {
144
257
  do {
145
258
  var propDesc = Object.getOwnPropertyDescriptor(obj, name);
146
- if (propDesc)
259
+ if (propDesc) {
147
260
  return propDesc;
261
+ }
148
262
  obj = Object.getPrototypeOf(obj);
149
263
  } while (obj);
150
264
  return undefined;
@@ -153,36 +267,45 @@ function getPropertyDescriptor(obj, name) {
153
267
  * Merges `options` with `defaultOptions`, and then copies the properties with
154
268
  * the keys in `defaultOptions` from the merged object to `destObj`.
155
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
+ *
156
274
  * @return
157
275
  */
158
276
  // TODO: Make methods like getDefaultOptions private
159
277
  function applyOptions(options, destObj) {
160
278
  var defaultOptions = destObj.getDefaultOptions();
161
279
  // Validate; make sure `keys` doesn't have any extraneous items
162
- for (var option in options)
280
+ for (var option in options) {
163
281
  // eslint-disable-next-line no-prototype-builtins
164
- if (!defaultOptions.hasOwnProperty(option))
282
+ if (!defaultOptions.hasOwnProperty(option)) {
165
283
  throw new Error("Invalid option: '" + option + "'");
284
+ }
285
+ }
166
286
  // Merge options and defaultOptions
167
287
  options = __assign(__assign({}, defaultOptions), options);
168
288
  // Copy options
169
289
  for (var option in options) {
170
290
  var propDesc = getPropertyDescriptor(destObj, option);
171
291
  // Update the property as long as the property has not been set (unless if it has a setter)
172
- if (!propDesc || propDesc.set)
292
+ if (!propDesc || propDesc.set) {
173
293
  destObj[option] = options[option];
294
+ }
174
295
  }
175
296
  }
176
297
  // This must be cleared at the start of each frame
177
298
  var valCache = new WeakMap();
178
299
  function cacheValue(element, path, value) {
179
300
  // Initiate movie cache
180
- if (!valCache.has(element.movie))
301
+ if (!valCache.has(element.movie)) {
181
302
  valCache.set(element.movie, new WeakMap());
303
+ }
182
304
  var movieCache = valCache.get(element.movie);
183
- // Iniitate element cache
184
- if (!movieCache.has(element))
305
+ // Initiate element cache
306
+ if (!movieCache.has(element)) {
185
307
  movieCache.set(element, {});
308
+ }
186
309
  var elementCache = movieCache.get(element);
187
310
  // Cache the value
188
311
  elementCache[path] = value;
@@ -222,13 +345,16 @@ var KeyFrame = /** @class */ (function () {
222
345
  return this;
223
346
  };
224
347
  KeyFrame.prototype.evaluate = function (time) {
225
- if (this.value.length === 0)
348
+ if (this.value.length === 0) {
226
349
  throw new Error('Empty keyframe');
227
- if (time === undefined)
350
+ }
351
+ if (time === undefined) {
228
352
  throw new Error('|time| is undefined or null');
353
+ }
229
354
  var firstTime = this.value[0][0];
230
- if (time < firstTime)
355
+ if (time < firstTime) {
231
356
  throw new Error('No keyframe point before |time|');
357
+ }
232
358
  // I think reduce are slow to do per-frame (or more)?
233
359
  for (var i = 0; i < this.value.length; i++) {
234
360
  var startTime = this.value[i][0];
@@ -237,7 +363,7 @@ var KeyFrame = /** @class */ (function () {
237
363
  if (i + 1 < this.value.length) {
238
364
  var endTime = this.value[i + 1][0];
239
365
  var endValue = this.value[i + 1][1];
240
- if (startTime <= time && time < endTime)
366
+ if (startTime <= time && time < endTime) {
241
367
  // No need for endValue if it is flat interpolation
242
368
  // TODO: support custom interpolation for 'other' types?
243
369
  if (!(typeof startValue === 'number' || typeof endValue === 'object')) {
@@ -253,6 +379,7 @@ var KeyFrame = /** @class */ (function () {
253
379
  endValue, // eslint-disable-line @typescript-eslint/ban-types
254
380
  percentProgress, this.interpolationKeys);
255
381
  }
382
+ }
256
383
  }
257
384
  else {
258
385
  // Repeat last value forever
@@ -278,23 +405,28 @@ var KeyFrame = /** @class */ (function () {
278
405
  // TODO: Is this function efficient?
279
406
  // TODO: Update doc @params to allow for keyframes
280
407
  function val(element, path, time) {
281
- if (hasCachedValue(element, path))
408
+ if (hasCachedValue(element, path)) {
282
409
  return getCachedValue(element, path);
410
+ }
283
411
  // Get property of element at path
284
412
  var pathParts = path.split('.');
285
413
  var property = element[pathParts.shift()];
286
- while (pathParts.length > 0)
414
+ while (pathParts.length > 0) {
287
415
  property = property[pathParts.shift()];
416
+ }
288
417
  // Property filter function
289
418
  var process = element.propertyFilters[path];
290
419
  var value;
291
- if (property instanceof KeyFrame)
420
+ if (property instanceof KeyFrame) {
292
421
  value = property.evaluate(time);
293
- else if (typeof property === 'function')
294
- value = property(element, time); // TODO? add more args
295
- else
422
+ }
423
+ else if (typeof property === 'function') {
424
+ value = property(element, time);
425
+ }
426
+ else {
296
427
  // Simple value
297
428
  value = property;
429
+ }
298
430
  return cacheValue(element, path, process ? process.call(element, value) : value);
299
431
  }
300
432
  /* export function floorInterp(x1, x2, t, objectKeys) {
@@ -305,15 +437,18 @@ function val(element, path, time) {
305
437
  }, Object.create(Object.getPrototypeOf(x1)));
306
438
  } */
307
439
  function linearInterp(x1, x2, t, objectKeys) {
308
- if (typeof x1 !== typeof x2)
440
+ if (typeof x1 !== typeof x2) {
309
441
  throw new Error('Type mismatch');
310
- if (typeof x1 !== 'number' && typeof x1 !== 'object')
442
+ }
443
+ if (typeof x1 !== 'number' && typeof x1 !== 'object') {
311
444
  // Flat interpolation (floor)
312
445
  return x1;
446
+ }
313
447
  if (typeof x1 === 'object') { // to work with objects (including arrays)
314
448
  // TODO: make this code DRY
315
- if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2))
449
+ if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2)) {
316
450
  throw new Error('Prototype mismatch');
451
+ }
317
452
  // Preserve prototype of objects
318
453
  var int = Object.create(Object.getPrototypeOf(x1));
319
454
  // Take the intersection of properties
@@ -321,8 +456,9 @@ function linearInterp(x1, x2, t, objectKeys) {
321
456
  for (var i = 0; i < keys.length; i++) {
322
457
  var key = keys[i];
323
458
  // eslint-disable-next-line no-prototype-builtins
324
- if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key))
459
+ if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key)) {
325
460
  continue;
461
+ }
326
462
  int[key] = linearInterp(x1[key], x2[key], t);
327
463
  }
328
464
  return int;
@@ -330,14 +466,17 @@ function linearInterp(x1, x2, t, objectKeys) {
330
466
  return (1 - t) * x1 + t * x2;
331
467
  }
332
468
  function cosineInterp(x1, x2, t, objectKeys) {
333
- if (typeof x1 !== typeof x2)
469
+ if (typeof x1 !== typeof x2) {
334
470
  throw new Error('Type mismatch');
335
- if (typeof x1 !== 'number' && typeof x1 !== 'object')
471
+ }
472
+ if (typeof x1 !== 'number' && typeof x1 !== 'object') {
336
473
  // Flat interpolation (floor)
337
474
  return x1;
475
+ }
338
476
  if (typeof x1 === 'object' && typeof x2 === 'object') { // to work with objects (including arrays)
339
- if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2))
477
+ if (Object.getPrototypeOf(x1) !== Object.getPrototypeOf(x2)) {
340
478
  throw new Error('Prototype mismatch');
479
+ }
341
480
  // Preserve prototype of objects
342
481
  var int = Object.create(Object.getPrototypeOf(x1));
343
482
  // Take the intersection of properties
@@ -345,8 +484,9 @@ function cosineInterp(x1, x2, t, objectKeys) {
345
484
  for (var i = 0; i < keys.length; i++) {
346
485
  var key = keys[i];
347
486
  // eslint-disable-next-line no-prototype-builtins
348
- if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key))
487
+ if (!x1.hasOwnProperty(key) || !x2.hasOwnProperty(key)) {
349
488
  continue;
489
+ }
350
490
  int[key] = cosineInterp(x1[key], x2[key], t);
351
491
  }
352
492
  return int;
@@ -426,17 +566,22 @@ var Font = /** @class */ (function () {
426
566
  */
427
567
  Font.prototype.toString = function () {
428
568
  var s = '';
429
- if (this.style !== 'normal')
569
+ if (this.style !== 'normal') {
430
570
  s += this.style + ' ';
431
- if (this.variant !== 'normal')
571
+ }
572
+ if (this.variant !== 'normal') {
432
573
  s += this.variant + ' ';
433
- if (this.weight !== 'normal')
574
+ }
575
+ if (this.weight !== 'normal') {
434
576
  s += this.weight + ' ';
435
- if (this.stretch !== 'normal')
577
+ }
578
+ if (this.stretch !== 'normal') {
436
579
  s += this.stretch + ' ';
580
+ }
437
581
  s += "".concat(this.size).concat(this.sizeUnit, " ");
438
- if (this.lineHeight !== 'normal')
582
+ if (this.lineHeight !== 'normal') {
439
583
  s += this.lineHeight + ' ';
584
+ }
440
585
  s += this.family;
441
586
  return s;
442
587
  };
@@ -476,61 +621,12 @@ function mapPixels(mapper, canvas, ctx, x, y, width, height, flush) {
476
621
  width = width || canvas.width;
477
622
  height = height || canvas.height;
478
623
  var frame = ctx.getImageData(x, y, width, height);
479
- 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) {
480
625
  mapper(frame.data, i);
481
- if (flush)
626
+ }
627
+ if (flush) {
482
628
  ctx.putImageData(frame, x, y);
483
- }
484
- /**
485
- * <p>Emits "change" event when public properties updated, recursively.
486
- * <p>Must be called before any watchable properties are set, and only once in
487
- * the prototype chain.
488
- *
489
- * @deprecated Will be removed in the future (see issue #130)
490
- *
491
- * @param target - object to watch
492
- */
493
- function watchPublic(target) {
494
- var getPath = function (receiver, prop) {
495
- return (receiver === proxy ? '' : (paths.get(receiver) + '.')) + prop;
496
- };
497
- var callback = function (prop, val, receiver) {
498
- // Public API property updated, emit 'modify' event.
499
- publish(proxy, "".concat(target.type, ".change.modify"), { property: getPath(receiver, prop), newValue: val });
500
- };
501
- var canWatch = function (receiver, prop) { return !prop.startsWith('_') &&
502
- (receiver.publicExcludes === undefined || !receiver.publicExcludes.includes(prop)); };
503
- // The path to each child property (each is a unique proxy)
504
- var paths = new WeakMap();
505
- var handler = {
506
- set: function (obj, prop, val, receiver) {
507
- // Recurse
508
- if (typeof val === 'object' && val !== null && !paths.has(val) && canWatch(receiver, prop)) {
509
- val = new Proxy(val, handler);
510
- paths.set(val, getPath(receiver, prop));
511
- }
512
- // Set property or attribute
513
- // Search prototype chain for the closest setter
514
- var objProto = obj;
515
- while ((objProto = Object.getPrototypeOf(objProto))) {
516
- var propDesc = Object.getOwnPropertyDescriptor(objProto, prop);
517
- if (propDesc && propDesc.set) {
518
- // Call setter, supplying proxy as this (fixes event bugs)
519
- propDesc.set.call(receiver, val);
520
- break;
521
- }
522
- }
523
- if (!objProto)
524
- // Couldn't find setter; set value on instance
525
- obj[prop] = val;
526
- // Check if the property isn't blacklisted in publicExcludes.
527
- if (canWatch(receiver, prop))
528
- callback(prop, val, receiver);
529
- return true;
530
- }
531
- };
532
- var proxy = new Proxy(target, handler);
533
- return proxy;
629
+ }
534
630
  }
535
631
 
536
632
  /**
@@ -556,45 +652,70 @@ function AudioSourceMixin(superclass) {
556
652
  */
557
653
  function MixedAudioSource(options) {
558
654
  var _this = this;
655
+ var _a;
656
+ if (!options.source) {
657
+ throw new Error('Property "source" is required in options');
658
+ }
559
659
  var onload = options.onload;
560
660
  // Don't set as instance property
561
661
  delete options.onload;
562
- _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;
563
666
  _this._initialized = false;
564
667
  _this._sourceStartTime = options.sourceStartTime || 0;
565
668
  applyOptions(options, _this);
566
669
  var load = function () {
567
670
  // TODO: && ?
568
- if ((options.duration || (_this.source.duration - _this.sourceStartTime)) < 0)
671
+ if ((options.duration || (_this.source.duration - _this.sourceStartTime)) < 0) {
569
672
  throw new Error('Invalid options.duration or options.sourceStartTime');
673
+ }
570
674
  _this._unstretchedDuration = options.duration || (_this.source.duration - _this.sourceStartTime);
571
675
  _this.duration = _this._unstretchedDuration / (_this.playbackRate);
572
676
  // onload will use `this`, and can't bind itself because it's before
573
677
  // super()
574
678
  onload && onload.bind(_this)(_this.source, options);
575
679
  };
576
- if (_this.source.readyState >= 2)
680
+ if (_this.source.readyState >= 2) {
577
681
  // this frame's data is available now
578
682
  load();
579
- else
683
+ }
684
+ else {
580
685
  // when this frame's data is available
581
686
  _this.source.addEventListener('loadedmetadata', load);
687
+ }
582
688
  _this.source.addEventListener('durationchange', function () {
583
689
  _this.duration = options.duration || (_this.source.duration - _this.sourceStartTime);
584
690
  });
585
691
  return _this;
586
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
+ };
587
713
  MixedAudioSource.prototype.attach = function (movie) {
588
714
  var _this = this;
589
715
  _super.prototype.attach.call(this, movie);
590
- subscribe(movie, 'movie.seek', function () {
591
- if (_this.currentTime < 0 || _this.currentTime >= _this.duration)
592
- return;
593
- _this.source.currentTime = _this.currentTime + _this.sourceStartTime;
594
- });
595
716
  // TODO: on unattach?
596
- subscribe(movie, 'movie.audiodestinationupdate', function (event) {
597
- // 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
598
719
  // destination.
599
720
  if (_this._connectedToDestination) {
600
721
  _this.audioNode.disconnect(movie.actx.destination);
@@ -613,8 +734,9 @@ function AudioSourceMixin(superclass) {
613
734
  var oldDisconnect = this._audioNode.disconnect.bind(this.audioNode);
614
735
  this._audioNode.disconnect = function (destination, output, input) {
615
736
  if (_this._connectedToDestination &&
616
- destination === movie.actx.destination)
737
+ destination === movie.actx.destination) {
617
738
  _this._connectedToDestination = false;
739
+ }
618
740
  return oldDisconnect(destination, output, input);
619
741
  };
620
742
  // Connect to actx.destination by default (can be rewired by user)
@@ -630,6 +752,10 @@ function AudioSourceMixin(superclass) {
630
752
  this.source.currentTime = this.currentTime + this.sourceStartTime;
631
753
  this.source.play();
632
754
  };
755
+ MixedAudioSource.prototype.seek = function (time) {
756
+ _super.prototype.seek.call(this, time);
757
+ this.source.currentTime = this.currentTime + this.sourceStartTime;
758
+ };
633
759
  MixedAudioSource.prototype.render = function () {
634
760
  _super.prototype.render.call(this);
635
761
  // TODO: implement Issue: Create built-in audio node to support built-in
@@ -639,6 +765,7 @@ function AudioSourceMixin(superclass) {
639
765
  this.source.playbackRate = val(this, 'playbackRate', this.currentTime);
640
766
  };
641
767
  MixedAudioSource.prototype.stop = function () {
768
+ _super.prototype.stop.call(this);
642
769
  this.source.pause();
643
770
  };
644
771
  Object.defineProperty(MixedAudioSource.prototype, "audioNode", {
@@ -657,8 +784,9 @@ function AudioSourceMixin(superclass) {
657
784
  },
658
785
  set: function (value) {
659
786
  this._playbackRate = value;
660
- if (this._unstretchedDuration !== undefined)
787
+ if (this._unstretchedDuration !== undefined) {
661
788
  this.duration = this._unstretchedDuration / value;
789
+ }
662
790
  },
663
791
  enumerable: false,
664
792
  configurable: true
@@ -694,6 +822,18 @@ function AudioSourceMixin(superclass) {
694
822
  enumerable: false,
695
823
  configurable: true
696
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
+ */
697
837
  MixedAudioSource.prototype.getDefaultOptions = function () {
698
838
  return __assign(__assign({}, superclass.prototype.getDefaultOptions()), { source: undefined, sourceStartTime: 0, duration: undefined, muted: false, volume: 1, playbackRate: 1 });
699
839
  };
@@ -716,56 +856,74 @@ var Base = /** @class */ (function () {
716
856
  * movie's timeline
717
857
  */
718
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
+ }
719
865
  // Set startTime and duration properties manually, because they are
720
866
  // readonly. applyOptions ignores readonly properties.
721
867
  this._startTime = options.startTime;
722
868
  this._duration = options.duration;
723
- // Proxy that will be returned by constructor (for sending 'modified'
724
- // events).
725
- var newThis = watchPublic(this);
726
- // Don't send updates when initializing, so use this instead of newThis
727
869
  applyOptions(options, this);
728
870
  // Whether this layer is currently being rendered
729
871
  this.active = false;
730
872
  this.enabled = true;
731
- this._occurrenceCount = 0; // no occurances in parent
873
+ this._occurrenceCount = 0; // no occurrences in parent
732
874
  this._movie = null;
733
- // Propogate up to target
734
- subscribe(newThis, 'layer.change', function (event) {
735
- var typeOfChange = event.type.substring(event.type.lastIndexOf('.') + 1);
736
- var type = "movie.change.layer.".concat(typeOfChange);
737
- publish(newThis._movie, type, __assign(__assign({}, event), { target: newThis._movie, type: type }));
738
- });
739
- return newThis;
740
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
741
884
  /**
742
885
  * Attaches this layer to `movie` if not already attached.
743
886
  * @ignore
744
887
  */
745
888
  Base.prototype.tryAttach = function (movie) {
746
- if (this._occurrenceCount === 0)
889
+ if (this._occurrenceCount === 0) {
747
890
  this.attach(movie);
891
+ }
748
892
  this._occurrenceCount++;
749
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
+ */
750
901
  Base.prototype.attach = function (movie) {
751
902
  this._movie = movie;
752
903
  };
753
904
  /**
754
- * 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
755
906
  * been called (including this call) equals the number of times `tryAttach`
756
907
  * has been called.
757
908
  *
758
909
  * @ignore
759
910
  */
760
911
  Base.prototype.tryDetach = function () {
761
- if (this.movie === null)
912
+ if (this.movie === null) {
762
913
  throw new Error('No movie to detach from');
914
+ }
763
915
  this._occurrenceCount--;
764
916
  // If this layer occurs in another place in a `layers` array, do not unset
765
917
  // _movie. (For calling `unshift` on the `layers` proxy)
766
- if (this._occurrenceCount === 0)
918
+ if (this._occurrenceCount === 0) {
767
919
  this.detach();
920
+ }
768
921
  };
922
+ /**
923
+ * Detaches this layer from its movie
924
+ *
925
+ * Called when the layer is removed from a movie's `layers` array.
926
+ */
769
927
  Base.prototype.detach = function () {
770
928
  this._movie = null;
771
929
  };
@@ -773,14 +931,40 @@ var Base = /** @class */ (function () {
773
931
  * Called when the layer is activated
774
932
  */
775
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
+ };
776
958
  /**
777
959
  * Called when the movie renders and the layer is active
778
960
  */
779
961
  Base.prototype.render = function () { }; // eslint-disable-line @typescript-eslint/no-empty-function
780
962
  /**
781
- * Called when the layer is deactivated
963
+ * Called when the layer is deactivated
782
964
  */
783
- Base.prototype.stop = function () { }; // eslint-disable-line @typescript-eslint/no-empty-function
965
+ Base.prototype.stop = function () {
966
+ this._currentTime = undefined;
967
+ };
784
968
  Object.defineProperty(Base.prototype, "parent", {
785
969
  // TODO: is this needed?
786
970
  get: function () {
@@ -791,6 +975,7 @@ var Base = /** @class */ (function () {
791
975
  });
792
976
  Object.defineProperty(Base.prototype, "startTime", {
793
977
  /**
978
+ * The time in the movie at which this layer starts (in seconds)
794
979
  */
795
980
  get: function () {
796
981
  return this._startTime;
@@ -803,18 +988,17 @@ var Base = /** @class */ (function () {
803
988
  });
804
989
  Object.defineProperty(Base.prototype, "currentTime", {
805
990
  /**
806
- * The current time of the movie relative to this layer
991
+ * The current time of the movie relative to this layer (in seconds)
807
992
  */
808
993
  get: function () {
809
- return this._movie
810
- ? this._movie.currentTime - this.startTime
811
- : undefined;
994
+ return this._currentTime;
812
995
  },
813
996
  enumerable: false,
814
997
  configurable: true
815
998
  });
816
999
  Object.defineProperty(Base.prototype, "duration", {
817
1000
  /**
1001
+ * The duration of this layer (in seconds)
818
1002
  */
819
1003
  get: function () {
820
1004
  return this._duration;
@@ -825,6 +1009,16 @@ var Base = /** @class */ (function () {
825
1009
  enumerable: false,
826
1010
  configurable: true
827
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
+ });
828
1022
  Object.defineProperty(Base.prototype, "movie", {
829
1023
  get: function () {
830
1024
  return this._movie;
@@ -832,6 +1026,9 @@ var Base = /** @class */ (function () {
832
1026
  enumerable: false,
833
1027
  configurable: true
834
1028
  });
1029
+ /**
1030
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1031
+ */
835
1032
  Base.prototype.getDefaultOptions = function () {
836
1033
  return {
837
1034
  startTime: undefined,
@@ -848,6 +1045,7 @@ Base.prototype.propertyFilters = {};
848
1045
 
849
1046
  // TODO: rename to something more consistent with the naming convention of Visual and VisualSourceMixin
850
1047
  /**
1048
+ * Layer for an HTML audio element
851
1049
  * @extends AudioSource
852
1050
  */
853
1051
  var Audio = /** @class */ (function (_super) {
@@ -856,11 +1054,21 @@ var Audio = /** @class */ (function (_super) {
856
1054
  * Creates an audio layer
857
1055
  */
858
1056
  function Audio(options) {
859
- var _this = _super.call(this, options) || this;
860
- 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) {
861
1065
  _this.duration = (_this).source.duration - _this.sourceStartTime;
1066
+ }
862
1067
  return _this;
863
1068
  }
1069
+ /**
1070
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1071
+ */
864
1072
  Audio.prototype.getDefaultOptions = function () {
865
1073
  return __assign(__assign({}, Object.getPrototypeOf(this).getDefaultOptions()), {
866
1074
  /**
@@ -872,40 +1080,101 @@ var Audio = /** @class */ (function (_super) {
872
1080
  return Audio;
873
1081
  }(AudioSourceMixin(Base)));
874
1082
 
875
- /** Any layer that renders to a canvas */
876
- var Visual = /** @class */ (function (_super) {
877
- __extends(Visual, _super);
878
- /**
879
- * Creates a visual layer
880
- */
881
- function Visual(options) {
882
- var _this = _super.call(this, options) || this;
883
- // Only validate extra if not subclassed, because if subclcass, there will
884
- // be extraneous options.
885
- applyOptions(options, _this);
886
- _this.canvas = document.createElement('canvas');
887
- _this.cctx = _this.canvas.getContext('2d');
888
- _this._effectsBack = [];
889
- _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, {
890
1101
  deleteProperty: function (target, property) {
891
1102
  var value = target[property];
892
- value.detach();
893
1103
  delete target[property];
1104
+ listener.onRemove(value);
894
1105
  return true;
895
1106
  },
896
1107
  set: function (target, property, value) {
1108
+ var oldValue = target[property];
1109
+ target[property] = value;
1110
+ // Check if property is a number (index)
897
1111
  if (!isNaN(Number(property))) {
898
- // The property is a number (index)
899
- if (target[property])
900
- target[property].detach();
901
- value.attach(_this);
1112
+ if (oldValue !== undefined) {
1113
+ listener.onRemove(oldValue);
1114
+ }
1115
+ listener.onAdd(value);
902
1116
  }
903
- target[property] = value;
904
1117
  return true;
905
1118
  }
906
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;
907
1131
  return _this;
908
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
+ };
909
1178
  /**
910
1179
  * Render visual output
911
1180
  */
@@ -913,8 +1182,9 @@ var Visual = /** @class */ (function (_super) {
913
1182
  // Prevent empty canvas errors if the width or height is 0
914
1183
  var width = val(this, 'width', this.currentTime);
915
1184
  var height = val(this, 'height', this.currentTime);
916
- if (width === 0 || height === 0)
1185
+ if (width === 0 || height === 0) {
917
1186
  return;
1187
+ }
918
1188
  this.beginRender();
919
1189
  this.doRender();
920
1190
  this.endRender();
@@ -945,20 +1215,22 @@ var Visual = /** @class */ (function (_super) {
945
1215
  Visual.prototype.endRender = function () {
946
1216
  var w = val(this, 'width', this.currentTime) || val(this.movie, 'width', this.movie.currentTime);
947
1217
  var h = val(this, 'height', this.currentTime) || val(this.movie, 'height', this.movie.currentTime);
948
- if (w * h > 0)
1218
+ if (w * h > 0) {
949
1219
  this._applyEffects();
1220
+ }
950
1221
  // else InvalidStateError for drawing zero-area image in some effects, right?
951
1222
  };
952
1223
  Visual.prototype._applyEffects = function () {
953
1224
  for (var i = 0; i < this.effects.length; i++) {
954
1225
  var effect = this.effects[i];
955
- if (effect && effect.enabled)
1226
+ if (effect && effect.enabled) {
956
1227
  // Pass relative time
957
1228
  effect.apply(this, this.movie.currentTime - this.startTime);
1229
+ }
958
1230
  }
959
1231
  };
960
1232
  /**
961
- * Convienence method for <code>effects.push()</code>
1233
+ * Convenience method for <code>effects.push()</code>
962
1234
  * @param effect
963
1235
  * @return the layer (for chaining)
964
1236
  */
@@ -966,6 +1238,18 @@ var Visual = /** @class */ (function (_super) {
966
1238
  this.effects.push(effect);
967
1239
  return this;
968
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
+ */
969
1253
  Visual.prototype.getDefaultOptions = function () {
970
1254
  return __assign(__assign({}, Base.prototype.getDefaultOptions()), {
971
1255
  /**
@@ -1024,17 +1308,60 @@ function VisualSourceMixin(superclass) {
1024
1308
  var MixedVisualSource = /** @class */ (function (_super) {
1025
1309
  __extends(MixedVisualSource, _super);
1026
1310
  function MixedVisualSource(options) {
1027
- 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;
1028
1316
  applyOptions(options, _this);
1029
1317
  return _this;
1030
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
+ };
1031
1358
  MixedVisualSource.prototype.doRender = function () {
1032
1359
  // Clear/fill background
1033
1360
  _super.prototype.doRender.call(this);
1034
1361
  /*
1035
1362
  * Source dimensions crop the image. Dest dimensions set the size that
1036
1363
  * the image will be rendered at *on the layer*. Note that this is
1037
- * different than the layer dimensions (`this.width` and `this.height`).
1364
+ * different from the layer dimensions (`this.width` and `this.height`).
1038
1365
  * The main reason this distinction exists is so that an image layer can
1039
1366
  * be rotated without being cropped (see iss #46).
1040
1367
  */
@@ -1042,6 +1369,21 @@ function VisualSourceMixin(superclass) {
1042
1369
  // `destX` and `destY` are relative to the layer
1043
1370
  val(this, 'destX', this.currentTime), val(this, 'destY', this.currentTime), val(this, 'destWidth', this.currentTime), val(this, 'destHeight', this.currentTime));
1044
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
+ */
1045
1387
  MixedVisualSource.prototype.getDefaultOptions = function () {
1046
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 });
1047
1389
  };
@@ -1090,10 +1432,19 @@ function VisualSourceMixin(superclass) {
1090
1432
  return MixedVisualSource;
1091
1433
  }
1092
1434
 
1435
+ /**
1436
+ * Layer for an HTML image element
1437
+ * @extends VisualSource
1438
+ */
1093
1439
  var Image = /** @class */ (function (_super) {
1094
1440
  __extends(Image, _super);
1095
- function Image() {
1096
- 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;
1097
1448
  }
1098
1449
  return Image;
1099
1450
  }(VisualSourceMixin(Visual)));
@@ -1107,9 +1458,12 @@ var Text = /** @class */ (function (_super) {
1107
1458
  // TODO: is textX necessary? it seems inconsistent, because you can't define
1108
1459
  // width/height directly for a text layer
1109
1460
  function Text(options) {
1110
- var _this =
1461
+ var _this = this;
1462
+ if (!options.text) {
1463
+ throw new Error('Property "text" is required in TextOptions');
1464
+ }
1111
1465
  // Default to no (transparent) background
1112
- _super.call(this, __assign({ background: null }, options)) || this;
1466
+ _this = _super.call(this, __assign({ background: null }, options)) || this;
1113
1467
  applyOptions(options, _this);
1114
1468
  return _this;
1115
1469
  // this._prevText = undefined;
@@ -1156,22 +1510,33 @@ var Text = /** @class */ (function (_super) {
1156
1510
  document.body.removeChild(s);
1157
1511
  return metrics;
1158
1512
  } */
1513
+ /**
1514
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1515
+ */
1159
1516
  Text.prototype.getDefaultOptions = function () {
1160
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' });
1161
1518
  };
1162
1519
  return Text;
1163
1520
  }(Visual));
1164
1521
 
1165
- // Use mixins instead of `extend`ing two classes (which isn't supported by
1166
- // JavaScript).
1167
1522
  /**
1523
+ * Layer for an HTML video element
1168
1524
  * @extends AudioSource
1169
1525
  * @extends VisualSource
1170
1526
  */
1171
1527
  var Video = /** @class */ (function (_super) {
1172
1528
  __extends(Video, _super);
1173
- function Video() {
1174
- 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;
1175
1540
  }
1176
1541
  return Video;
1177
1542
  }(AudioSourceMixin(VisualSourceMixin(Visual))));
@@ -1197,46 +1562,48 @@ var index = /*#__PURE__*/Object.freeze({
1197
1562
  */
1198
1563
  var Base$1 = /** @class */ (function () {
1199
1564
  function Base() {
1200
- var newThis = watchPublic(this); // proxy that will be returned by constructor
1201
- newThis.enabled = true;
1202
- newThis._occurrenceCount = 0;
1203
- newThis._target = null;
1204
- // Propogate up to target
1205
- subscribe(newThis, 'effect.change.modify', function (event) {
1206
- if (!newThis._target)
1207
- return;
1208
- var type = "".concat(newThis._target.type, ".change.effect.modify");
1209
- publish(newThis._target, type, __assign(__assign({}, event), { target: newThis._target, source: newThis, type: type }));
1210
- });
1211
- return newThis;
1565
+ this.enabled = true;
1566
+ this._occurrenceCount = 0;
1567
+ this._target = null;
1212
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
1213
1577
  /**
1214
1578
  * Attaches this effect to `target` if not already attached.
1215
1579
  * @ignore
1216
1580
  */
1217
1581
  Base.prototype.tryAttach = function (target) {
1218
- if (this._occurrenceCount === 0)
1582
+ if (this._occurrenceCount === 0) {
1219
1583
  this.attach(target);
1584
+ }
1220
1585
  this._occurrenceCount++;
1221
1586
  };
1222
1587
  Base.prototype.attach = function (movie) {
1223
1588
  this._target = movie;
1224
1589
  };
1225
1590
  /**
1226
- * 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`
1227
1592
  * has been called (including this call) equals the number of times
1228
1593
  * `tryAttach` has been called.
1229
1594
  *
1230
1595
  * @ignore
1231
1596
  */
1232
1597
  Base.prototype.tryDetach = function () {
1233
- if (this._target === null)
1598
+ if (this._target === null) {
1234
1599
  throw new Error('No movie to detach from');
1600
+ }
1235
1601
  this._occurrenceCount--;
1236
1602
  // If this effect occurs in another place in the containing array, do not
1237
1603
  // unset _target. (For calling `unshift` on the `layers` proxy)
1238
- if (this._occurrenceCount === 0)
1604
+ if (this._occurrenceCount === 0) {
1239
1605
  this.detach();
1606
+ }
1240
1607
  };
1241
1608
  Base.prototype.detach = function () {
1242
1609
  this._target = null;
@@ -1261,6 +1628,14 @@ var Base$1 = /** @class */ (function () {
1261
1628
  enumerable: false,
1262
1629
  configurable: true
1263
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
+ });
1264
1639
  Object.defineProperty(Base.prototype, "parent", {
1265
1640
  get: function () {
1266
1641
  return this._target;
@@ -1275,6 +1650,9 @@ var Base$1 = /** @class */ (function () {
1275
1650
  enumerable: false,
1276
1651
  configurable: true
1277
1652
  });
1653
+ /**
1654
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
1655
+ */
1278
1656
  Base.prototype.getDefaultOptions = function () {
1279
1657
  return {};
1280
1658
  };
@@ -1342,16 +1720,18 @@ var Shader = /** @class */ (function (_super) {
1342
1720
  Shader.prototype._initGl = function () {
1343
1721
  this._canvas = document.createElement('canvas');
1344
1722
  var gl = this._canvas.getContext('webgl');
1345
- if (gl === null)
1723
+ if (gl === null) {
1346
1724
  throw new Error('Unable to initialize WebGL. Your browser or machine may not support it.');
1725
+ }
1347
1726
  this._gl = gl;
1348
1727
  return gl;
1349
1728
  };
1350
1729
  Shader.prototype._initTextures = function (userUniforms, userTextures, sourceTextureOptions) {
1351
1730
  var gl = this._gl;
1352
1731
  var maxTextures = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
1353
- if (userTextures.length > maxTextures)
1732
+ if (userTextures.length > maxTextures) {
1354
1733
  console.warn('Too many textures!');
1734
+ }
1355
1735
  this._userTextures = {};
1356
1736
  for (var name_1 in userTextures) {
1357
1737
  var userOptions = userTextures[name_1];
@@ -1364,8 +1744,9 @@ var Shader = /** @class */ (function (_super) {
1364
1744
  * textures, without having to define multiple properties in the effect
1365
1745
  * object.
1366
1746
  */
1367
- if (userUniforms[name_1])
1747
+ if (userUniforms[name_1]) {
1368
1748
  throw new Error("Texture - uniform naming conflict: ".concat(name_1, "!"));
1749
+ }
1369
1750
  // Add this as a "user uniform".
1370
1751
  userUniforms[name_1] = '1i'; // texture pointer
1371
1752
  }
@@ -1400,18 +1781,6 @@ var Shader = /** @class */ (function (_super) {
1400
1781
  this._uniformLocations[unprefixed] = gl.getUniformLocation(this._program, prefixed);
1401
1782
  }
1402
1783
  };
1403
- // Not needed, right?
1404
- /* watchWebGLOptions() {
1405
- const pubChange = () => {
1406
- this.publish("change", {});
1407
- };
1408
- for (let name in this._userTextures) {
1409
- watch(this, name, pubChange);
1410
- }
1411
- for (let name in this._userUniforms) {
1412
- watch(this, name, pubChange);
1413
- }
1414
- } */
1415
1784
  Shader.prototype.apply = function (target, reltime) {
1416
1785
  this._checkDimensions(target);
1417
1786
  this._refreshGl();
@@ -1498,16 +1867,22 @@ var Shader = /** @class */ (function (_super) {
1498
1867
  i++;
1499
1868
  }
1500
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
+ */
1501
1875
  Shader.prototype._prepareUniforms = function (target, reltime) {
1502
1876
  var gl = this._gl;
1503
- // Set the shader uniforms.
1504
1877
  // Tell the shader we bound the texture to texture unit 0.
1505
1878
  // All base (Shader class) uniforms are optional.
1506
- if (this._uniformLocations.source)
1879
+ if (this._uniformLocations.source) {
1507
1880
  gl.uniform1i(this._uniformLocations.source, 0);
1881
+ }
1508
1882
  // All base (Shader class) uniforms are optional.
1509
- if (this._uniformLocations.size)
1883
+ if (this._uniformLocations.size) {
1510
1884
  gl.uniform2iv(this._uniformLocations.size, [target.canvas.width, target.canvas.height]);
1885
+ }
1511
1886
  for (var unprefixed in this._userUniforms) {
1512
1887
  var options = this._userUniforms[unprefixed];
1513
1888
  var value = val(this, unprefixed, reltime);
@@ -1531,11 +1906,13 @@ var Shader = /** @class */ (function (_super) {
1531
1906
  /**
1532
1907
  * Converts a value of a standard type for javascript to a standard type for
1533
1908
  * GLSL
1909
+ *
1534
1910
  * @param value - the raw value to prepare
1535
1911
  * @param outputType - the WebGL type of |value|; example:
1536
1912
  * <code>1f</code> for a float
1537
1913
  * @param reltime - current time, relative to the target
1538
- * @param [options] - Optional config
1914
+ * @param [options]
1915
+ * @returns the prepared value
1539
1916
  */
1540
1917
  Shader.prototype._prepareValue = function (value, outputType, reltime, options) {
1541
1918
  if (options === void 0) { options = {}; }
@@ -1558,35 +1935,40 @@ var Shader = /** @class */ (function (_super) {
1558
1935
  var i = 0;
1559
1936
  for (var name_4 in this._userTextures) {
1560
1937
  var testValue = val(this, name_4, reltime);
1561
- if (value === testValue)
1938
+ if (value === testValue) {
1562
1939
  value = Shader.INTERNAL_TEXTURE_UNITS + i; // after the internal texture units
1940
+ }
1563
1941
  i++;
1564
1942
  }
1565
1943
  }
1566
1944
  if (outputType === '3fv') {
1567
1945
  // allow 4-component vectors; TODO: why?
1568
- if (Array.isArray(value) && (value.length === 3 || value.length === 4))
1946
+ if (Array.isArray(value) && (value.length === 3 || value.length === 4)) {
1569
1947
  return value;
1948
+ }
1570
1949
  // kind of loose so this can be changed if needed
1571
- if (typeof value === 'object')
1950
+ if (typeof value === 'object') {
1572
1951
  return [
1573
1952
  value.r !== undefined ? value.r : def,
1574
1953
  value.g !== undefined ? value.g : def,
1575
1954
  value.b !== undefined ? value.b : def
1576
1955
  ];
1956
+ }
1577
1957
  throw new Error("Invalid type: ".concat(outputType, " or value: ").concat(value));
1578
1958
  }
1579
1959
  if (outputType === '4fv') {
1580
- if (Array.isArray(value) && value.length === 4)
1960
+ if (Array.isArray(value) && value.length === 4) {
1581
1961
  return value;
1962
+ }
1582
1963
  // kind of loose so this can be changed if needed
1583
- if (typeof value === 'object')
1964
+ if (typeof value === 'object') {
1584
1965
  return [
1585
1966
  value.r !== undefined ? value.r : def,
1586
1967
  value.g !== undefined ? value.g : def,
1587
1968
  value.b !== undefined ? value.b : def,
1588
1969
  value.a !== undefined ? value.a : def
1589
1970
  ];
1971
+ }
1590
1972
  throw new Error("Invalid type: ".concat(outputType, " or value: ").concat(value));
1591
1973
  }
1592
1974
  return value;
@@ -1680,8 +2062,9 @@ var Shader = /** @class */ (function (_super) {
1680
2062
  else {
1681
2063
  // No, it's not a power of 2. Turn off mips and set
1682
2064
  // wrapping to clamp to edge
1683
- if (wrapS !== gl.CLAMP_TO_EDGE || wrapT !== gl.CLAMP_TO_EDGE)
2065
+ if (wrapS !== gl.CLAMP_TO_EDGE || wrapT !== gl.CLAMP_TO_EDGE) {
1684
2066
  console.warn('Wrap mode is not CLAMP_TO_EDGE for a non-power-of-two texture. Defaulting to CLAMP_TO_EDGE');
2067
+ }
1685
2068
  gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1686
2069
  gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1687
2070
  }
@@ -1733,7 +2116,6 @@ var Shader = /** @class */ (function (_super) {
1733
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 ";
1734
2117
  return Shader;
1735
2118
  }(Visual$1));
1736
- // Shader.prototype.getpublicExcludes = () =>
1737
2119
  var isPowerOf2 = function (value) { return (value && (value - 1)) === 0; };
1738
2120
 
1739
2121
  /**
@@ -1908,6 +2290,35 @@ var EllipticalMask = /** @class */ (function (_super) {
1908
2290
  return EllipticalMask;
1909
2291
  }(Visual$1));
1910
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));
1911
2322
  /**
1912
2323
  * A sequence of effects to apply, treated as one effect. This can be useful
1913
2324
  * for defining reused effect sequences as one effect.
@@ -1916,48 +2327,28 @@ var Stack = /** @class */ (function (_super) {
1916
2327
  __extends(Stack, _super);
1917
2328
  function Stack(options) {
1918
2329
  var _this = _super.call(this) || this;
1919
- _this._effectsBack = [];
1920
- // TODO: Throw 'change' events in handlers
1921
- _this.effects = new Proxy(_this._effectsBack, {
1922
- deleteProperty: function (target, property) {
1923
- var value = target[property];
1924
- value.detach(); // Detach effect from movie
1925
- delete target[property];
1926
- return true;
1927
- },
1928
- set: function (target, property, value) {
1929
- // TODO: make sure type check works
1930
- if (!isNaN(Number(property))) { // if property is a number (index)
1931
- if (target[property])
1932
- target[property].detach(); // Detach old effect from movie
1933
- value.attach(this._target); // Attach effect to movie
1934
- }
1935
- target[property] = value;
1936
- return true;
1937
- }
1938
- });
2330
+ _this.effects = new StackEffects(options.effects, _this);
1939
2331
  options.effects.forEach(function (effect) { return _this.effects.push(effect); });
1940
2332
  return _this;
1941
- // TODO: Propogate 'change' events from children up
1942
2333
  }
1943
2334
  Stack.prototype.attach = function (movie) {
1944
2335
  _super.prototype.attach.call(this, movie);
1945
2336
  this.effects.filter(function (effect) { return !!effect; }).forEach(function (effect) {
1946
- effect.detach();
1947
- effect.attach(movie);
2337
+ effect.tryAttach(movie);
1948
2338
  });
1949
2339
  };
1950
2340
  Stack.prototype.detach = function () {
1951
2341
  _super.prototype.detach.call(this);
1952
2342
  this.effects.filter(function (effect) { return !!effect; }).forEach(function (effect) {
1953
- effect.detach();
2343
+ effect.tryDetach();
1954
2344
  });
1955
2345
  };
1956
2346
  Stack.prototype.apply = function (target, reltime) {
1957
2347
  for (var i = 0; i < this.effects.length; i++) {
1958
2348
  var effect = this.effects[i];
1959
- if (!effect)
2349
+ if (!effect) {
1960
2350
  continue;
2351
+ }
1961
2352
  effect.apply(target, reltime);
1962
2353
  }
1963
2354
  };
@@ -1976,7 +2367,7 @@ var Stack = /** @class */ (function (_super) {
1976
2367
  * Applies a Gaussian blur
1977
2368
  */
1978
2369
  // TODO: Improve performance
1979
- // 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
1980
2371
  // standard deviation
1981
2372
  var GaussianBlur = /** @class */ (function (_super) {
1982
2373
  __extends(GaussianBlur, _super);
@@ -2021,9 +2412,10 @@ var GaussianBlurComponent = /** @class */ (function (_super) {
2021
2412
  }
2022
2413
  GaussianBlurComponent.prototype.apply = function (target, reltime) {
2023
2414
  var radiusVal = val(this, 'radius', reltime);
2024
- if (radiusVal !== this._radiusCache)
2415
+ if (radiusVal !== this._radiusCache) {
2025
2416
  // Regenerate gaussian distribution canvas.
2026
2417
  this.shape = GaussianBlurComponent._render1DKernel(GaussianBlurComponent._gen1DKernel(radiusVal));
2418
+ }
2027
2419
  this._radiusCache = radiusVal;
2028
2420
  _super.prototype.apply.call(this, target, reltime);
2029
2421
  };
@@ -2056,23 +2448,27 @@ var GaussianBlurComponent = /** @class */ (function (_super) {
2056
2448
  var pascal = GaussianBlurComponent._genPascalRow(2 * radius + 1);
2057
2449
  // don't use `reduce` and `map` (overhead?)
2058
2450
  var sum = 0;
2059
- for (var i = 0; i < pascal.length; i++)
2451
+ for (var i = 0; i < pascal.length; i++) {
2060
2452
  sum += pascal[i];
2061
- for (var i = 0; i < pascal.length; i++)
2453
+ }
2454
+ for (var i = 0; i < pascal.length; i++) {
2062
2455
  pascal[i] /= sum;
2456
+ }
2063
2457
  return pascal;
2064
2458
  };
2065
2459
  GaussianBlurComponent._genPascalRow = function (index) {
2066
- if (index < 0)
2460
+ if (index < 0) {
2067
2461
  throw new Error("Invalid index ".concat(index));
2462
+ }
2068
2463
  var currRow = [1];
2069
2464
  for (var i = 1; i < index; i++) {
2070
2465
  var nextRow = [];
2071
2466
  nextRow.length = currRow.length + 1;
2072
2467
  // edges are always 1's
2073
2468
  nextRow[0] = nextRow[nextRow.length - 1] = 1;
2074
- for (var j = 1; j < nextRow.length - 1; j++)
2469
+ for (var j = 1; j < nextRow.length - 1; j++) {
2075
2470
  nextRow[j] = currRow[j - 1] + currRow[j];
2471
+ }
2076
2472
  currRow = nextRow;
2077
2473
  }
2078
2474
  return currRow;
@@ -2143,15 +2539,14 @@ var Pixelate = /** @class */ (function (_super) {
2143
2539
  pixelSize: '1i'
2144
2540
  }
2145
2541
  }) || this;
2146
- /**
2147
- */
2148
2542
  _this.pixelSize = options.pixelSize || 1;
2149
2543
  return _this;
2150
2544
  }
2151
2545
  Pixelate.prototype.apply = function (target, reltime) {
2152
2546
  var ps = val(this, 'pixelSize', reltime);
2153
- if (ps % 1 !== 0 || ps < 0)
2547
+ if (ps % 1 !== 0 || ps < 0) {
2154
2548
  throw new Error('Pixel size must be a nonnegative integer');
2549
+ }
2155
2550
  _super.prototype.apply.call(this, target, reltime);
2156
2551
  };
2157
2552
  return Pixelate;
@@ -2180,10 +2575,12 @@ var Transform = /** @class */ (function (_super) {
2180
2575
  return _this;
2181
2576
  }
2182
2577
  Transform.prototype.apply = function (target, reltime) {
2183
- if (target.canvas.width !== this._tmpCanvas.width)
2578
+ if (target.canvas.width !== this._tmpCanvas.width) {
2184
2579
  this._tmpCanvas.width = target.canvas.width;
2185
- if (target.canvas.height !== this._tmpCanvas.height)
2580
+ }
2581
+ if (target.canvas.height !== this._tmpCanvas.height) {
2186
2582
  this._tmpCanvas.height = target.canvas.height;
2583
+ }
2187
2584
  // Use data, since that's the underlying storage
2188
2585
  this._tmpMatrix.data = val(this, 'matrix.data', reltime);
2189
2586
  this._tmpCtx.setTransform(this._tmpMatrix.a, this._tmpMatrix.b, this._tmpMatrix.c, this._tmpMatrix.d, this._tmpMatrix.e, this._tmpMatrix.f);
@@ -2209,8 +2606,9 @@ var Transform = /** @class */ (function (_super) {
2209
2606
  ];
2210
2607
  }
2211
2608
  Matrix.prototype.identity = function () {
2212
- for (var i = 0; i < this.data.length; i++)
2609
+ for (var i = 0; i < this.data.length; i++) {
2213
2610
  this.data[i] = Matrix.IDENTITY.data[i];
2611
+ }
2214
2612
  return this;
2215
2613
  };
2216
2614
  /**
@@ -2219,8 +2617,9 @@ var Transform = /** @class */ (function (_super) {
2219
2617
  * @param [val]
2220
2618
  */
2221
2619
  Matrix.prototype.cell = function (x, y, val) {
2222
- if (val !== undefined)
2620
+ if (val !== undefined) {
2223
2621
  this.data[3 * y + x] = val;
2622
+ }
2224
2623
  return this.data[3 * y + x];
2225
2624
  };
2226
2625
  Object.defineProperty(Matrix.prototype, "a", {
@@ -2272,16 +2671,19 @@ var Transform = /** @class */ (function (_super) {
2272
2671
  */
2273
2672
  Matrix.prototype.multiply = function (other) {
2274
2673
  // copy to temporary matrix to avoid modifying `this` while reading from it
2275
- for (var x = 0; x < 3; x++)
2674
+ for (var x = 0; x < 3; x++) {
2276
2675
  for (var y = 0; y < 3; y++) {
2277
2676
  var sum = 0;
2278
- for (var i = 0; i < 3; i++)
2677
+ for (var i = 0; i < 3; i++) {
2279
2678
  sum += this.cell(x, i) * other.cell(i, y);
2679
+ }
2280
2680
  Matrix._TMP_MATRIX.cell(x, y, sum);
2281
2681
  }
2682
+ }
2282
2683
  // copy data from TMP_MATRIX to this
2283
- for (var i = 0; i < Matrix._TMP_MATRIX.data.length; i++)
2684
+ for (var i = 0; i < Matrix._TMP_MATRIX.data.length; i++) {
2284
2685
  this.data[i] = Matrix._TMP_MATRIX.data[i];
2686
+ }
2285
2687
  return this;
2286
2688
  };
2287
2689
  /**
@@ -2355,6 +2757,52 @@ var index$1 = /*#__PURE__*/Object.freeze({
2355
2757
  Visual: Visual$1
2356
2758
  });
2357
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
+
2358
2806
  /**
2359
2807
  * @module movie
2360
2808
  */
@@ -2368,15 +2816,13 @@ var MovieOptions = /** @class */ (function () {
2368
2816
  *
2369
2817
  * Implements a pub/sub system.
2370
2818
  */
2371
- // TODO: Make record option to make recording video output to the user while
2372
- // it's recording
2373
2819
  // TODO: rename renderingFrame -> refreshing
2374
2820
  var Movie = /** @class */ (function () {
2375
2821
  /**
2376
2822
  * Creates a new movie.
2377
2823
  */
2378
2824
  function Movie(options) {
2379
- // TODO: move into multiple methods!
2825
+ this._recording = false;
2380
2826
  // Set actx option manually, because it's readonly.
2381
2827
  this.actx = options.actx ||
2382
2828
  options.audioContext ||
@@ -2384,236 +2830,294 @@ var Movie = /** @class */ (function () {
2384
2830
  // eslint-disable-next-line new-cap
2385
2831
  new window.webkitAudioContext();
2386
2832
  delete options.actx;
2387
- // Proxy that will be returned by constructor
2388
- 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
+ }
2389
2837
  // Set canvas option manually, because it's readonly.
2390
- this._canvas = options.canvas;
2838
+ this._canvas = this._visibleCanvas = options.canvas;
2391
2839
  delete options.canvas;
2392
- // Don't send updates when initializing, so use this instead of newThis:
2393
2840
  this._cctx = this.canvas.getContext('2d'); // TODO: make private?
2841
+ // Set options on the movie
2394
2842
  applyOptions(options, this);
2395
- var that = newThis;
2396
- this._effectsBack = [];
2397
- this.effects = new Proxy(newThis._effectsBack, {
2398
- deleteProperty: function (target, property) {
2399
- // Refresh screen when effect is removed, if the movie isn't playing
2400
- // already.
2401
- var value = target[property];
2402
- value.tryDetach();
2403
- delete target[property];
2404
- publish(that, 'movie.change.effect.remove', { effect: value });
2405
- return true;
2406
- },
2407
- set: function (target, property, value) {
2408
- // Check if property is an number (an index)
2409
- if (!isNaN(Number(property))) {
2410
- if (target[property]) {
2411
- publish(that, 'movie.change.effect.remove', {
2412
- effect: target[property]
2413
- });
2414
- target[property].tryDetach();
2415
- }
2416
- // Attach effect to movie
2417
- value.tryAttach(that);
2418
- target[property] = value;
2419
- // Refresh screen when effect is set, if the movie isn't playing
2420
- // already.
2421
- publish(that, 'movie.change.effect.add', { effect: value });
2422
- }
2423
- else {
2424
- target[property] = value;
2425
- }
2426
- return true;
2427
- }
2428
- });
2429
- this._layersBack = [];
2430
- this.layers = new Proxy(newThis._layersBack, {
2431
- deleteProperty: function (target, property) {
2432
- var oldDuration = this.duration;
2433
- var value = target[property];
2434
- value.tryDetach(that);
2435
- delete target[property];
2436
- var current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration;
2437
- if (current)
2438
- publish(that, 'movie.change.layer.remove', { layer: value });
2439
- publish(that, 'movie.change.duration', { oldDuration: oldDuration });
2440
- return true;
2441
- },
2442
- set: function (target, property, value) {
2443
- var oldDuration = this.duration;
2444
- // Check if property is an number (an index)
2445
- if (!isNaN(Number(property))) {
2446
- if (target[property]) {
2447
- publish(that, 'movie.change.layer.remove', {
2448
- layer: target[property]
2449
- });
2450
- target[property].tryDetach();
2451
- }
2452
- // Attach layer to movie
2453
- value.tryAttach(that);
2454
- target[property] = value;
2455
- // Refresh screen when a relevant layer is added or removed
2456
- var current = that.currentTime >= value.startTime && that.currentTime < value.startTime + value.duration;
2457
- if (current)
2458
- publish(that, 'movie.change.layer.add', { layer: value });
2459
- publish(that, 'movie.change.duration', { oldDuration: oldDuration });
2460
- }
2461
- else {
2462
- target[property] = value;
2463
- }
2464
- return true;
2465
- }
2466
- });
2843
+ this.effects = new MovieEffects([], this);
2844
+ this.layers = new MovieLayers([], this);
2467
2845
  this._paused = true;
2468
2846
  this._ended = false;
2469
- // This variable helps prevent multiple frame-rendering loops at the same
2470
- // 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.
2471
2849
  this._renderingFrame = false;
2472
2850
  this.currentTime = 0;
2473
- // For recording
2474
- this._mediaRecorder = null;
2475
- // -1 works well in inequalities
2476
- // The last time `play` was called
2851
+ // The last time `play` was called, -1 works well in comparisons
2477
2852
  this._lastPlayed = -1;
2478
- // What was `currentTime` when `play` was called
2853
+ // What `currentTime` was when `play` was called
2479
2854
  this._lastPlayedOffset = -1;
2480
- // newThis._updateInterval = 0.1; // time in seconds between each "timeupdate" event
2481
- // newThis._lastUpdate = -1;
2482
- if (newThis.autoRefresh)
2483
- newThis.refresh(); // render single frame on creation
2484
- // Subscribe to own event "change" (child events propogate up)
2485
- subscribe(newThis, 'movie.change', function () {
2486
- if (newThis.autoRefresh && !newThis.rendering)
2487
- newThis.refresh();
2488
- });
2489
- // Subscribe to own event "ended"
2490
- subscribe(newThis, 'movie.recordended', function () {
2491
- if (newThis.recording) {
2492
- newThis._mediaRecorder.requestData();
2493
- newThis._mediaRecorder.stop();
2494
- }
2495
- });
2496
- return newThis;
2497
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
+ };
2498
2871
  /**
2499
2872
  * Plays the movie
2500
- * @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
2501
2878
  */
2502
- Movie.prototype.play = function () {
2503
- var _this = this;
2504
- return new Promise(function (resolve) {
2505
- if (!_this.paused)
2506
- throw new Error('Already playing');
2507
- _this._paused = _this._ended = false;
2508
- _this._lastPlayed = performance.now();
2509
- _this._lastPlayedOffset = _this.currentTime;
2510
- if (!_this.renderingFrame)
2511
- // Not rendering (and not playing), so play.
2512
- _this._render(true, undefined, resolve);
2513
- // Stop rendering frame if currently doing so, because playing has higher
2514
- // priority. This will effect the next _render call.
2515
- _this._renderingFrame = false;
2516
- 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
+ });
2517
3004
  });
2518
3005
  };
2519
3006
  /**
2520
3007
  * Plays the movie in the background and records it
2521
3008
  *
2522
3009
  * @param options
2523
- * @param frameRate
3010
+ * @param [options.frameRate] - Video frame rate
2524
3011
  * @param [options.video=true] - whether to include video in recording
2525
3012
  * @param [options.audio=true] - whether to include audio in recording
2526
- * @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
2527
3015
  * @param [options.type='video/webm'] - MIME type for exported video
2528
- * constructor
2529
- * @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
2530
3018
  */
2531
- // TEST: *support recording that plays back with audio!*
2532
- // TODO: figure out how to do offline recording (faster than realtime).
2533
- // TODO: improve recording performance to increase frame rate?
3019
+ // TODO: Improve recording performance to increase frame rate
2534
3020
  Movie.prototype.record = function (options) {
2535
- var _this = this;
2536
- if (options.video === false && options.audio === false)
2537
- throw new Error('Both video and audio cannot be disabled');
2538
- if (!this.paused)
2539
- throw new Error('Cannot record movie while already playing or recording');
2540
- var mimeType = options.type || 'video/webm';
2541
- if (MediaRecorder && MediaRecorder.isTypeSupported && !MediaRecorder.isTypeSupported(mimeType))
2542
- throw new Error('Please pass a valid MIME type for the exported video');
2543
- return new Promise(function (resolve, reject) {
2544
- var canvasCache = _this.canvas;
2545
- // Record on a temporary canvas context
2546
- _this._canvas = document.createElement('canvas');
2547
- _this.canvas.width = canvasCache.width;
2548
- _this.canvas.height = canvasCache.height;
2549
- _this._cctx = _this.canvas.getContext('2d');
2550
- // frame blobs
2551
- var recordedChunks = [];
2552
- // Combine image + audio, or just pick one
2553
- var tracks = [];
2554
- if (options.video !== false) {
2555
- var visualStream = _this.canvas.captureStream(options.frameRate);
2556
- tracks = tracks.concat(visualStream.getTracks());
2557
- }
2558
- // Check if there's a layer that's an instance of an AudioSourceMixin
2559
- // (Audio or Video)
2560
- var hasMediaTracks = _this.layers.some(function (layer) { return layer instanceof Audio || layer instanceof Video; });
2561
- // If no media tracks present, don't include an audio stream, because
2562
- // Chrome doesn't record silence when an audio stream is present.
2563
- if (hasMediaTracks && options.audio !== false) {
2564
- var audioDestination = _this.actx.createMediaStreamDestination();
2565
- var audioStream = audioDestination.stream;
2566
- tracks = tracks.concat(audioStream.getTracks());
2567
- publish(_this, 'movie.audiodestinationupdate', { movie: _this, destination: audioDestination });
2568
- }
2569
- var stream = new MediaStream(tracks);
2570
- var mediaRecorderOptions = __assign(__assign({}, (options.mediaRecorderOptions || {})), { mimeType: mimeType });
2571
- var mediaRecorder = new MediaRecorder(stream, mediaRecorderOptions);
2572
- mediaRecorder.ondataavailable = function (event) {
2573
- // if (this._paused) reject(new Error("Recording was interrupted"));
2574
- if (event.data.size > 0)
2575
- recordedChunks.push(event.data);
2576
- };
2577
- // TODO: publish to movie, not layers
2578
- mediaRecorder.onstop = function () {
2579
- _this._paused = true;
2580
- _this._ended = true;
2581
- _this._canvas = canvasCache;
2582
- _this._cctx = _this.canvas.getContext('2d');
2583
- publish(_this, 'movie.audiodestinationupdate', { movie: _this, destination: _this.actx.destination });
2584
- _this._mediaRecorder = null;
2585
- // Construct the exported video out of all the frame blobs.
2586
- resolve(new Blob(recordedChunks, {
2587
- type: mimeType
2588
- }));
2589
- };
2590
- mediaRecorder.onerror = reject;
2591
- mediaRecorder.start();
2592
- _this._mediaRecorder = mediaRecorder;
2593
- _this._recordEndTime = options.duration ? _this.currentTime + options.duration : _this.duration;
2594
- _this.play();
2595
- 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
+ });
2596
3094
  });
2597
3095
  };
2598
3096
  /**
2599
- * Stops the movie, without reseting the playback position
2600
- * @return the movie (for chaining)
3097
+ * Stops the movie without resetting the playback position
3098
+ * @return The movie
2601
3099
  */
2602
3100
  Movie.prototype.pause = function () {
3101
+ // Update state
2603
3102
  this._paused = true;
2604
3103
  // Deactivate all layers
2605
- for (var i = 0; i < this.layers.length; i++)
3104
+ for (var i = 0; i < this.layers.length; i++) {
2606
3105
  if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
2607
3106
  var layer = this.layers[i];
2608
- layer.stop();
2609
- layer.active = false;
3107
+ if (layer.active) {
3108
+ layer.stop();
3109
+ layer.active = false;
3110
+ }
2610
3111
  }
3112
+ }
3113
+ // For backwards compatibility, notify event listeners that the movie has
3114
+ // paused
2611
3115
  publish(this, 'movie.pause', {});
2612
3116
  return this;
2613
3117
  };
2614
3118
  /**
2615
3119
  * Stops playback and resets the playback position
2616
- * @return the movie (for chaining)
3120
+ * @return The movie
2617
3121
  */
2618
3122
  Movie.prototype.stop = function () {
2619
3123
  this.pause();
@@ -2622,8 +3126,8 @@ var Movie = /** @class */ (function () {
2622
3126
  };
2623
3127
  /**
2624
3128
  * @param [timestamp=performance.now()]
2625
- * @param [done=undefined] - called when done playing or when the current frame is loaded
2626
- * @private
3129
+ * @param [done=undefined] - Called when done playing or when the current
3130
+ * frame is loaded
2627
3131
  */
2628
3132
  Movie.prototype._render = function (repeat, timestamp, done) {
2629
3133
  var _this = this;
@@ -2631,179 +3135,207 @@ var Movie = /** @class */ (function () {
2631
3135
  if (done === void 0) { done = undefined; }
2632
3136
  clearCachedValues(this);
2633
3137
  if (!this.rendering) {
2634
- // (!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
2635
3139
  // rendering a single frame.
2636
- if (done)
3140
+ if (done) {
2637
3141
  done();
3142
+ }
2638
3143
  return;
2639
3144
  }
2640
- var end = this.recording ? this._recordEndTime : this.duration;
2641
- this._updateCurrentTime(timestamp, end);
2642
- // TODO: Is calling duration every frame bad for performance? (remember,
2643
- // it's calling Array.reduce)
2644
- if (this.currentTime === end) {
2645
- if (this.recording)
2646
- publish(this, 'movie.recordended', { movie: this });
2647
- if (this.currentTime === this.duration)
2648
- publish(this, 'movie.ended', { movie: this, repeat: this.repeat });
2649
- // TODO: only reset currentTime if repeating
2650
- if (this.repeat) {
2651
- // Don't use setter, which publishes 'movie.seek'. Instead, update the
2652
- // 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.
2653
3167
  this._currentTime = 0;
2654
3168
  publish(this, 'movie.timeupdate', { movie: this });
2655
- }
2656
- this._lastPlayed = performance.now();
2657
- this._lastPlayedOffset = 0; // this.currentTime
2658
- this._renderingFrame = false;
2659
- // Stop playback or recording if done (except if it's playing and repeat
2660
- // is true)
2661
- if (!(!this.recording && this.repeat)) {
2662
- this._paused = true;
2663
- this._ended = true;
2664
- // Deactivate all layers
2665
- for (var i = 0; i < this.layers.length; i++)
2666
- if (Object.prototype.hasOwnProperty.call(this.layers, i)) {
2667
- var layer = this.layers[i];
2668
- // A layer that has been deleted before layers.length has been updated
2669
- // (see the layers proxy in the constructor).
2670
- if (!layer || !layer.active)
2671
- continue;
2672
- layer.stop();
2673
- 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();
2674
3193
  }
2675
- if (done)
2676
- done();
2677
- return;
3194
+ return;
3195
+ }
2678
3196
  }
3197
+ // Do render
3198
+ this._renderBackground(timestamp);
3199
+ this._renderLayers();
3200
+ this._applyEffects();
2679
3201
  }
2680
- // Do render
2681
- this._renderBackground(timestamp);
2682
- var frameFullyLoaded = this._renderLayers();
2683
- this._applyEffects();
2684
- if (frameFullyLoaded)
2685
- publish(this, 'movie.loadeddata', { movie: this });
2686
- // If didn't load in this instant, repeatedly frame-render until frame is
2687
- // loaded.
2688
- // If the expression below is false, don't publish an event, just silently
2689
- // stop render loop.
2690
- 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) {
2691
3214
  this._renderingFrame = false;
2692
- if (done)
3215
+ if (done) {
2693
3216
  done();
3217
+ }
2694
3218
  return;
2695
3219
  }
3220
+ // TODO: Is making a new arrow function every frame bad for performance?
2696
3221
  window.requestAnimationFrame(function () {
2697
3222
  _this._render(repeat, undefined, done);
2698
- }); // TODO: research performance cost
3223
+ });
2699
3224
  };
2700
3225
  Movie.prototype._updateCurrentTime = function (timestampMs, end) {
2701
- // If we're only instant-rendering (current frame only), it doens't matter
2702
- // if it's paused or not.
3226
+ // If we're only frame-rendering (current frame only), it doesn't matter if
3227
+ // it's paused or not.
2703
3228
  if (!this._renderingFrame) {
2704
- // if ((timestamp - this._lastUpdate) >= this._updateInterval) {
2705
3229
  var sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000;
2706
- var currentTime = this._lastPlayedOffset + sinceLastPlayed; // don't use setter
3230
+ var currentTime = this._lastPlayedOffset + sinceLastPlayed;
2707
3231
  if (this.currentTime !== currentTime) {
3232
+ // Update the current time (don't use setter)
2708
3233
  this._currentTime = currentTime;
3234
+ // For backwards compatibility, publish a 'movie.timeupdate' event.
2709
3235
  publish(this, 'movie.timeupdate', { movie: this });
2710
3236
  }
2711
- // this._lastUpdate = timestamp;
2712
- // }
2713
- if (this.currentTime > end)
2714
- this.currentTime = end;
3237
+ if (this.currentTime > end) {
3238
+ this._currentTime = end;
3239
+ }
2715
3240
  }
2716
3241
  };
2717
3242
  Movie.prototype._renderBackground = function (timestamp) {
2718
3243
  this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
3244
+ // Evaluate background color (since it's a dynamic property)
2719
3245
  var background = val(this, 'background', timestamp);
2720
- if (background) { // TODO: check val'd result
3246
+ if (background) {
2721
3247
  this.cctx.fillStyle = background;
2722
3248
  this.cctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
2723
3249
  }
2724
3250
  };
2725
3251
  /**
2726
- * @return whether or not video frames are loaded
2727
3252
  * @param [timestamp=performance.now()]
2728
- * @private
2729
3253
  */
2730
3254
  Movie.prototype._renderLayers = function () {
2731
- var frameFullyLoaded = true;
2732
3255
  for (var i = 0; i < this.layers.length; i++) {
2733
- if (!Object.prototype.hasOwnProperty.call(this.layers, i))
3256
+ if (!Object.prototype.hasOwnProperty.call(this.layers, i)) {
2734
3257
  continue;
3258
+ }
2735
3259
  var layer = this.layers[i];
2736
3260
  // A layer that has been deleted before layers.length has been updated
2737
3261
  // (see the layers proxy in the constructor).
2738
- if (!layer)
3262
+ if (!layer) {
2739
3263
  continue;
2740
- var reltime = this.currentTime - layer.startTime;
3264
+ }
2741
3265
  // Cancel operation if layer disabled or outside layer time interval
3266
+ var reltime = this.currentTime - layer.startTime;
2742
3267
  if (!val(layer, 'enabled', reltime) ||
2743
3268
  // TODO > or >= ?
2744
3269
  this.currentTime < layer.startTime || this.currentTime > layer.startTime + layer.duration) {
2745
3270
  // Layer is not active.
2746
3271
  // If only rendering this frame, we are not "starting" the layer.
2747
3272
  if (layer.active && !this._renderingFrame) {
2748
- // TODO: make a `deactivate()` method?
2749
3273
  layer.stop();
2750
3274
  layer.active = false;
2751
3275
  }
2752
3276
  continue;
2753
3277
  }
3278
+ // If we are playing (not refreshing), update the layer's progress
3279
+ if (!this._renderingFrame) {
3280
+ layer.progress(reltime);
3281
+ }
2754
3282
  // If only rendering this frame, we are not "starting" the layer
2755
3283
  if (!layer.active && val(layer, 'enabled', reltime) && !this._renderingFrame) {
2756
- // TODO: make an `activate()` method?
2757
3284
  layer.start();
2758
3285
  layer.active = true;
2759
3286
  }
2760
- // if the layer has an input file
2761
- if ('source' in layer)
2762
- frameFullyLoaded = frameFullyLoaded && layer.source.readyState >= 2;
2763
3287
  layer.render();
2764
3288
  // if the layer has visual component
2765
3289
  if (layer instanceof Visual) {
2766
3290
  var canvas = layer.canvas;
2767
- // layer.canvas.width and layer.canvas.height should already be interpolated
2768
- // if the layer has an area (else InvalidStateError from canvas)
2769
- if (canvas.width * canvas.height > 0)
3291
+ if (canvas.width * canvas.height > 0) {
2770
3292
  this.cctx.drawImage(canvas, val(layer, 'x', reltime), val(layer, 'y', reltime), canvas.width, canvas.height);
3293
+ }
2771
3294
  }
2772
3295
  }
2773
- return frameFullyLoaded;
2774
3296
  };
2775
3297
  Movie.prototype._applyEffects = function () {
2776
3298
  for (var i = 0; i < this.effects.length; i++) {
2777
3299
  var effect = this.effects[i];
2778
3300
  // An effect that has been deleted before effects.length has been updated
2779
3301
  // (see the effectsproxy in the constructor).
2780
- if (!effect)
3302
+ if (!effect) {
2781
3303
  continue;
3304
+ }
2782
3305
  effect.apply(this, this.currentTime);
2783
3306
  }
2784
3307
  };
2785
3308
  /**
2786
- * Refreshes the screen (only use this if auto-refresh is disabled)
2787
- * @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
2788
3314
  */
2789
3315
  Movie.prototype.refresh = function () {
2790
3316
  var _this = this;
3317
+ // Refreshing while playing can interrupt playback
3318
+ if (!this.paused) {
3319
+ throw new Error('Already playing');
3320
+ }
2791
3321
  return new Promise(function (resolve) {
2792
3322
  _this._renderingFrame = true;
2793
3323
  _this._render(false, undefined, resolve);
2794
3324
  });
2795
3325
  };
2796
3326
  /**
2797
- * Convienence method
3327
+ * Convienence method (TODO: remove)
2798
3328
  */
2799
3329
  Movie.prototype._publishToLayers = function (type, event) {
2800
- for (var i = 0; i < this.layers.length; i++)
2801
- 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)) {
2802
3332
  publish(this.layers[i], type, event);
3333
+ }
3334
+ }
2803
3335
  };
2804
3336
  Object.defineProperty(Movie.prototype, "rendering", {
2805
3337
  /**
2806
- * If the movie is playing, recording or refreshing
3338
+ * `true` if the movie is playing, recording or refreshing
2807
3339
  */
2808
3340
  get: function () {
2809
3341
  return !this.paused || this._renderingFrame;
@@ -2813,7 +3345,7 @@ var Movie = /** @class */ (function () {
2813
3345
  });
2814
3346
  Object.defineProperty(Movie.prototype, "renderingFrame", {
2815
3347
  /**
2816
- * If the movie is refreshing current frame
3348
+ * `true` if the movie is refreshing the current frame
2817
3349
  */
2818
3350
  get: function () {
2819
3351
  return this._renderingFrame;
@@ -2823,17 +3355,19 @@ var Movie = /** @class */ (function () {
2823
3355
  });
2824
3356
  Object.defineProperty(Movie.prototype, "recording", {
2825
3357
  /**
2826
- * If the movie is recording
3358
+ * `true` if the movie is recording
2827
3359
  */
2828
3360
  get: function () {
2829
- return !!this._mediaRecorder;
3361
+ return this._recording;
2830
3362
  },
2831
3363
  enumerable: false,
2832
3364
  configurable: true
2833
3365
  });
2834
3366
  Object.defineProperty(Movie.prototype, "duration", {
2835
3367
  /**
2836
- * 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
2837
3371
  */
2838
3372
  // TODO: dirty flag?
2839
3373
  get: function () {
@@ -2843,16 +3377,16 @@ var Movie = /** @class */ (function () {
2843
3377
  configurable: true
2844
3378
  });
2845
3379
  /**
2846
- * Convienence method for <code>layers.push()</code>
3380
+ * Convenience method for `layers.push()`
2847
3381
  * @param layer
2848
- * @return the movie
3382
+ * @return The movie
2849
3383
  */
2850
3384
  Movie.prototype.addLayer = function (layer) {
2851
3385
  this.layers.push(layer);
2852
3386
  return this;
2853
3387
  };
2854
3388
  /**
2855
- * Convienence method for <code>effects.push()</code>
3389
+ * Convenience method for `effects.push()`
2856
3390
  * @param effect
2857
3391
  * @return the movie
2858
3392
  */
@@ -2862,6 +3396,7 @@ var Movie = /** @class */ (function () {
2862
3396
  };
2863
3397
  Object.defineProperty(Movie.prototype, "paused", {
2864
3398
  /**
3399
+ * `true` if the movie is paused
2865
3400
  */
2866
3401
  get: function () {
2867
3402
  return this._paused;
@@ -2871,7 +3406,7 @@ var Movie = /** @class */ (function () {
2871
3406
  });
2872
3407
  Object.defineProperty(Movie.prototype, "ended", {
2873
3408
  /**
2874
- * If the playback position is at the end of the movie
3409
+ * `true` if the playback position is at the end of the movie
2875
3410
  */
2876
3411
  get: function () {
2877
3412
  return this._ended;
@@ -2879,50 +3414,89 @@ var Movie = /** @class */ (function () {
2879
3414
  enumerable: false,
2880
3415
  configurable: true
2881
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
+ };
2882
3440
  Object.defineProperty(Movie.prototype, "currentTime", {
2883
3441
  /**
2884
- * The current playback position
3442
+ * The current playback position in seconds
2885
3443
  */
2886
3444
  get: function () {
2887
3445
  return this._currentTime;
2888
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
+ */
2889
3454
  set: function (time) {
2890
- this._currentTime = time;
2891
- publish(this, 'movie.seek', {});
2892
- // Render single frame to match new time
2893
- if (this.autoRefresh)
2894
- this.refresh();
3455
+ this.seek(time);
2895
3456
  },
2896
3457
  enumerable: false,
2897
3458
  configurable: true
2898
3459
  });
2899
3460
  /**
2900
- * Sets the current playback position. This is a more powerful version of
2901
- * `set currentTime`.
3461
+ * Skips to the provided playback position, updating {@link currentTime}.
2902
3462
  *
2903
- * @param time - the new cursor's time value in seconds
2904
- * @param [refresh=true] - whether to render a single frame
2905
- * @return resolves when the current frame is rendered if
2906
- * <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.
2907
3467
  *
3468
+ * @deprecated Call {@link seek} and {@link refresh} separately
2908
3469
  */
2909
- // TODO: Refresh if only auto-refreshing is enabled
3470
+ // TODO: Refresh only if auto-refreshing is enabled
2910
3471
  Movie.prototype.setCurrentTime = function (time, refresh) {
2911
3472
  var _this = this;
2912
3473
  if (refresh === void 0) { refresh = true; }
2913
3474
  return new Promise(function (resolve, reject) {
2914
- _this._currentTime = time;
2915
- publish(_this, 'movie.seek', {});
2916
- if (refresh)
3475
+ _this.seek(time);
3476
+ if (refresh) {
2917
3477
  // Pass promise callbacks to `refresh`
2918
3478
  _this.refresh().then(resolve).catch(reject);
2919
- else
3479
+ }
3480
+ else {
2920
3481
  resolve();
3482
+ }
2921
3483
  });
2922
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
+ });
2923
3497
  Object.defineProperty(Movie.prototype, "canvas", {
2924
3498
  /**
2925
- * The rendering canvas
3499
+ * The HTML canvas element used for rendering
2926
3500
  */
2927
3501
  get: function () {
2928
3502
  return this._canvas;
@@ -2932,7 +3506,7 @@ var Movie = /** @class */ (function () {
2932
3506
  });
2933
3507
  Object.defineProperty(Movie.prototype, "cctx", {
2934
3508
  /**
2935
- * The rendering canvas's context
3509
+ * The canvas context used for rendering
2936
3510
  */
2937
3511
  get: function () {
2938
3512
  return this._cctx;
@@ -2942,7 +3516,7 @@ var Movie = /** @class */ (function () {
2942
3516
  });
2943
3517
  Object.defineProperty(Movie.prototype, "width", {
2944
3518
  /**
2945
- * The width of the rendering canvas
3519
+ * The width of the output canvas
2946
3520
  */
2947
3521
  get: function () {
2948
3522
  return this.canvas.width;
@@ -2955,7 +3529,7 @@ var Movie = /** @class */ (function () {
2955
3529
  });
2956
3530
  Object.defineProperty(Movie.prototype, "height", {
2957
3531
  /**
2958
- * The height of the rendering canvas
3532
+ * The height of the output canvas
2959
3533
  */
2960
3534
  get: function () {
2961
3535
  return this.canvas.height;
@@ -2967,12 +3541,18 @@ var Movie = /** @class */ (function () {
2967
3541
  configurable: true
2968
3542
  });
2969
3543
  Object.defineProperty(Movie.prototype, "movie", {
3544
+ /**
3545
+ * @return The movie
3546
+ */
2970
3547
  get: function () {
2971
3548
  return this;
2972
3549
  },
2973
3550
  enumerable: false,
2974
3551
  configurable: true
2975
3552
  });
3553
+ /**
3554
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
3555
+ */
2976
3556
  Movie.prototype.getDefaultOptions = function () {
2977
3557
  return {
2978
3558
  canvas: undefined,
@@ -2984,21 +3564,23 @@ var Movie = /** @class */ (function () {
2984
3564
  /**
2985
3565
  * @name module:movie#repeat
2986
3566
  */
2987
- repeat: false,
2988
- /**
2989
- * @name module:movie#autoRefresh
2990
- * @desc Whether to refresh when changes are made that would effect the current frame
2991
- */
2992
- autoRefresh: true
3567
+ repeat: false
2993
3568
  };
2994
3569
  };
2995
3570
  return Movie;
2996
3571
  }());
2997
- // id for events (independent of instance, but easy to access when on prototype chain)
3572
+ // Id for events
2998
3573
  Movie.prototype.type = 'movie';
2999
- // TODO: refactor so we don't need to explicitly exclude some of these
3000
- Movie.prototype.publicExcludes = ['canvas', 'cctx', 'actx', 'layers', 'effects'];
3001
- 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.');
3002
3584
 
3003
3585
  /*
3004
3586
  * Typedoc can't handle default exports. To let users import default export and
@@ -3024,8 +3606,7 @@ var etro = /*#__PURE__*/Object.freeze({
3024
3606
  parseColor: parseColor,
3025
3607
  Font: Font,
3026
3608
  parseFont: parseFont,
3027
- mapPixels: mapPixels,
3028
- watchPublic: watchPublic
3609
+ mapPixels: mapPixels
3029
3610
  });
3030
3611
 
3031
3612
  /**