etro 0.8.5 → 0.9.1

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 (47) hide show
  1. package/.github/workflows/nodejs.yml +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/CONTRIBUTING.md +13 -26
  4. package/README.md +8 -15
  5. package/dist/effect/base.d.ts +5 -5
  6. package/dist/effect/visual.d.ts +11 -0
  7. package/dist/etro-cjs.js +84 -53
  8. package/dist/etro-iife.js +84 -53
  9. package/dist/layer/image.d.ts +2 -2
  10. package/dist/layer/text.d.ts +3 -3
  11. package/dist/layer/visual-source.d.ts +18 -3
  12. package/dist/layer/visual.d.ts +5 -5
  13. package/dist/movie.d.ts +13 -4
  14. package/dist/object.d.ts +5 -1
  15. package/dist/util.d.ts +2 -0
  16. package/eslint.conf.js +2 -0
  17. package/eslint.test-conf.js +1 -2
  18. package/karma.conf.js +17 -8
  19. package/package.json +19 -19
  20. package/scripts/gen-effect-samples.html +24 -0
  21. package/scripts/save-effect-samples.js +1 -1
  22. package/src/effect/base.ts +6 -6
  23. package/src/effect/stack.ts +2 -2
  24. package/src/effect/transform.ts +2 -2
  25. package/src/effect/visual.ts +16 -1
  26. package/src/layer/audio-source.ts +4 -1
  27. package/src/layer/base.ts +3 -2
  28. package/src/layer/image.ts +3 -3
  29. package/src/layer/text.ts +4 -4
  30. package/src/layer/visual-source.ts +27 -7
  31. package/src/layer/visual.ts +7 -7
  32. package/src/movie.ts +55 -37
  33. package/src/object.ts +5 -1
  34. package/src/util.ts +2 -0
  35. package/tsconfig.json +3 -1
  36. package/examples/application/readme-screenshot.html +0 -85
  37. package/examples/application/video-player.html +0 -130
  38. package/examples/application/webcam.html +0 -28
  39. package/examples/introduction/audio.html +0 -64
  40. package/examples/introduction/effects.html +0 -79
  41. package/examples/introduction/export.html +0 -83
  42. package/examples/introduction/functions.html +0 -37
  43. package/examples/introduction/hello-world1.html +0 -37
  44. package/examples/introduction/hello-world2.html +0 -32
  45. package/examples/introduction/keyframes.html +0 -79
  46. package/examples/introduction/media.html +0 -63
  47. package/examples/introduction/text.html +0 -31
@@ -10,7 +10,7 @@ jobs:
10
10
 
11
11
  strategy:
12
12
  matrix:
13
- node-version: [10.x, 12.x]
13
+ node-version: [15.x, 16.x, 17.x]
14
14
 
15
15
  steps:
16
16
  - uses: actions/checkout@v1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
+ ## [0.9.1] - 2022-09-18
9
+ ### Fixed
10
+ - Update color types from `string` to `Color` ([#135](https://github.com/etro-js/etro/pull/135)).
11
+ - `Image` and `Video` classes now include missing properties.
12
+ - `Movie#currentTime` no longer exceeds the stop time.
13
+
14
+ ## [0.9.0] - 2022-07-17
15
+ ### Changed
16
+ - Methods in the `Base` effect now accept `Base` layers instead of `Visual` layers.
17
+
18
+ ### Deprecated
19
+ - `autoRefresh` option ([#130](https://github.com/etro-js/etro/issues/130)).
20
+ - `publicExcludes` ([#130](https://github.com/etro-js/etro/issues/130)).
21
+ - All `change` events ([#130](https://github.com/etro-js/etro/issues/130)).
22
+
23
+ ### Fixed
24
+ - Layers no longer trigger infinite loops when their active states change ([#127](https://github.com/etro-js/etro/issues/127)).
25
+ - Add missing `VisualSource` options to `Image` layer ([#128](https://github.com/etro-js/etro/pull/128)).
26
+ - Layers are now stopped when recording ends.
27
+ - `stop()` is no longer called on inactive layers.
28
+ - Movies no longer publish `'movie.ended'` when done recording.
29
+ - `Audio` and `Video` layers not detaching properly.
30
+ - When done playing or recording, movies only reset their time if they're in repeat mode.
31
+ - The `timeupdate` event is no longer fired when `currentTime` remains the same (due to `performance.now()` rounding).
32
+
8
33
  ## [0.8.5] - 2022-03-06
9
34
  ### Deprecated
10
35
  - `vd.effect.Base` - All visual effects now inherit from `vd.effect.Visual` instead.
@@ -208,6 +233,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
208
233
  - Gaussian blur
209
234
  - Transform
210
235
 
236
+ [0.9.1]: https://github.com/etro-js/etro/compare/v0.9.0...v0.9.1
237
+ [0.9.0]: https://github.com/etro-js/etro/compare/v0.8.5...v0.9.0
211
238
  [0.8.5]: https://github.com/etro-js/etro/compare/v0.8.4...v0.8.5
212
239
  [0.8.4]: https://github.com/etro-js/etro/compare/v0.8.3...v0.8.4
213
240
  [0.8.3]: https://github.com/etro-js/etro/compare/v0.8.2...v0.8.3
package/CONTRIBUTING.md CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  ## Introduction
4
4
 
5
- Thank you for considering contributing to Etro! There are many ways you can contribute to Etro, like creating issues for features or bugs, improving the docs or wiki, or writing the actual code for the library. This page covers how to make changes to the repository files (either code or docs).
5
+ Thank you for considering contributing to Etro! There are many ways you can contribute to Etro, like creating issues for features or bugs, improving the docs or wiki, or writing the code for the library. This page covers how to make changes to the repository files (either code or jsdocs).
6
6
 
7
- > Etro has a [Taiga Project](https://tree.taiga.io/project/etro-js-etro/epics) for managing issues and a [GitHub Discussion page](https://github.com/etro-js/etro/discussions) for questions, ideas and casual discussion
7
+ [Join our Discord](https://discord.gg/myrBsQ8Cht)
8
8
 
9
9
  ## Setting up your local environment
10
10
 
11
11
  #### Step 0: Dependencies
12
12
 
13
- - You will need Git, Node, NPM (at least 7.x) and Chrome (for headless unit testing) installed
13
+ - You will need Git, Node, NPM (at least 7.x) and Firefox (for headless functional testing) installed.
14
14
 
15
15
  #### Step 1: Fork
16
16
 
@@ -27,9 +27,9 @@ Thank you for considering contributing to Etro! There are many ways you can cont
27
27
 
28
28
  #### Step 2: Code
29
29
 
30
- - Make some changes.
31
- - If you are writing code, the linter uses [StandardJS](https://standardjs.com/rules.html) for style conventions.
32
- - When you're ready to submit a piece of code, first run
30
+ - Make some changes
31
+ - If you are writing code, the linter uses [StandardJS](https://standardjs.com/rules.html) for style conventions
32
+ - When you're ready to submit, first run
33
33
 
34
34
  ```
35
35
  npm run lint
@@ -40,12 +40,11 @@ Thank you for considering contributing to Etro! There are many ways you can cont
40
40
  to lint the code, generate the [dist](dist) files and run unit tests on them. It may be helpful to put these commands in a pre-commit hook.
41
41
 
42
42
  - Commit your changes
43
- - Please avoid squashing all your commits into one; we try to keep atomic commits.
44
43
  - Please follow these commit message guidelines:
45
- - Optionally, prefix each commit message with [an appropriate emoji](https://gitmoji.dev)
44
+ - Optionally, prefix each commit message with [an appropriate emoji](https://gitmoji.dev), such as `:bug:` for fixes.
46
45
  - Write in the imperative tense
47
46
  - Wrap lines after 72 characters (for Vim add `filetype indent plugin on` to ~/.vimrc, it's enabled by default in Atom).
48
- - Example:
47
+ - Format:
49
48
 
50
49
  ```
51
50
  :emoji: One-liner
@@ -57,19 +56,18 @@ Thank you for considering contributing to Etro! There are many ways you can cont
57
56
 
58
57
  #### Step 3: Push
59
58
 
60
- - First, rebase (don't merge) to integrate your work with the main repository
59
+ - First, rebase (please avoid merging) to integrate your work with any new changes in the main repository
61
60
 
62
61
  ```
63
62
  git fetch upstream
64
63
  git rebase upstream/master
65
64
  ```
66
65
 
67
- - Push to your fork
66
+ - Push to the fork
68
67
 
69
68
  #### Step 4: Pull request
70
69
 
71
- - Open a pull request from your the branch in your fork to the main repository
72
- - In the PR title, include **fixes ###** for bugs and **resolves ###** for feature requests
70
+ - Open a pull request from the branch in your fork to the main repository
73
71
  - If you changed any core functionality, make sure you explain your motives for those changes
74
72
 
75
73
  #### Step 5: Feedback
@@ -80,20 +78,9 @@ Thank you for considering contributing to Etro! There are many ways you can cont
80
78
 
81
79
  ### Etro Overview
82
80
 
83
- If you are new to the core elements of etro, you should probably read [the overview guide](https://etrojs.dev/docs/overview).
81
+ Check out [the overview guide](https://etrojs.dev/docs/overview) for usage information
84
82
 
85
- ### API Structure
86
-
87
- * `etro.Movie` - the movie
88
- * `etro.layer.*` - all layers
89
- * `etro.effect.*` - all (visual) effects
90
- - `etro.event.publish` - emit an event
91
- - `etro.event.subscribe` - add an event listener
92
- - `etro.*` - other utility classes and methods (see **src/util.ts**)
93
-
94
- ### Etro concepts
95
-
96
- #### Pub/sub system
83
+ ### Pub/sub system
97
84
 
98
85
  Events emitted by Etro objects use a [pub/sub system](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern). To emit an event, use `event.publish(target, type, event)`. For instance,
99
86
 
package/README.md CHANGED
@@ -3,14 +3,12 @@
3
3
  [![](https://img.shields.io/npm/v/etro)](https://www.npmjs.com/package/etro)
4
4
  [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fetro-js%2Fetro%2Fbadge&style=flat)](https://actions-badge.atrox.dev/etro-js/etro/goto)
5
5
 
6
- > Etro was previously known as Vidar, but it had to be renamed to avoid
7
- > confusion with an existing software product.
6
+ Etro is a typescript framework for programmatically editing videos. It lets you
7
+ composite layers and add filters (effects). Etro comes shipped with text, video,
8
+ audio and image layers, along with a bunch of GLSL effects. You can also define
9
+ your own layers and effects with javascript and GLSL.
8
10
 
9
- Etro is a typescript framework for programmatically editing videos. Similar to
10
- GUI-based video-editing software, it lets you composite layers and add effects.
11
- Etro comes shipped with text, video, audio and image layers, along with a bunch
12
- of GLSL effects. You can also define your own layers and effects with javascript
13
- and GLSL.
11
+ [Join our Discord](https://discord.gg/myrBsQ8Cht)
14
12
 
15
13
  ## Features
16
14
 
@@ -72,16 +70,11 @@ To use Etro in Node, see the [wrapper](https://github.com/etro-js/etro-node):
72
70
 
73
71
  ## Running the Examples
74
72
 
75
- First, download the assets for the examples:
76
-
77
- ```
78
- npm run assets
79
- ```
80
-
81
- Then, start the development server (only used for convience while developing;
82
- you don't need a server to use Etro):
73
+ Start the development server (only used for convience while developing; you
74
+ don't need a server to use Etro):
83
75
 
84
76
  ```
77
+ npm i
85
78
  npm start
86
79
  ```
87
80
 
@@ -1,5 +1,5 @@
1
1
  import { Movie } from '../movie';
2
- import { Visual } from '../layer/index';
2
+ import { Base as BaseLayer } from '../layer/index';
3
3
  import BaseObject from '../object';
4
4
  /**
5
5
  * @deprecated All visual effects now inherit from `Visual` instead
@@ -21,8 +21,8 @@ export declare class Base implements BaseObject {
21
21
  * Attaches this effect to `target` if not already attached.
22
22
  * @ignore
23
23
  */
24
- tryAttach(target: Movie | Visual): void;
25
- attach(movie: Movie | Visual): void;
24
+ tryAttach(target: Movie | BaseLayer): void;
25
+ attach(movie: Movie | BaseLayer): void;
26
26
  /**
27
27
  * Dettaches this effect from its target if the number of times `tryDetach`
28
28
  * has been called (including this call) equals the number of times
@@ -40,12 +40,12 @@ export declare class Base implements BaseObject {
40
40
  * (will soon be replaced with an instance getter)
41
41
  * @abstract
42
42
  */
43
- apply(target: Movie | Visual, reltime: number): void;
43
+ apply(target: Movie | BaseLayer, reltime: number): void;
44
44
  /**
45
45
  * The current time of the target
46
46
  */
47
47
  get currentTime(): number;
48
- get parent(): Movie | Visual;
48
+ get parent(): Movie | BaseLayer;
49
49
  get movie(): Movie;
50
50
  getDefaultOptions(): Record<string, unknown>;
51
51
  }
@@ -1,6 +1,17 @@
1
+ import { Movie } from '../movie';
2
+ import { Visual as VisualLayer } from '../layer/index';
1
3
  import { Base } from './base';
2
4
  /**
3
5
  * Modifies the visual contents of a layer.
4
6
  */
5
7
  export declare class Visual extends Base {
8
+ /**
9
+ * Apply this effect to a target at the given time
10
+ *
11
+ * @param target
12
+ * @param reltime - the movie's current time relative to the layer
13
+ * (will soon be replaced with an instance getter)
14
+ * @abstract
15
+ */
16
+ apply(target: Movie | VisualLayer, reltime: number): void;
6
17
  }
package/dist/etro-cjs.js CHANGED
@@ -125,6 +125,7 @@ function publish(target, type, event) {
125
125
  var listeners = new WeakMap();
126
126
 
127
127
  var event = /*#__PURE__*/Object.freeze({
128
+ __proto__: null,
128
129
  subscribe: subscribe,
129
130
  unsubscribe: unsubscribe,
130
131
  publish: publish
@@ -374,7 +375,7 @@ var Color = /** @class */ (function () {
374
375
  * Converts to a CSS color
375
376
  */
376
377
  Color.prototype.toString = function () {
377
- return "rgba(" + this.r + ", " + this.g + ", " + this.b + ", " + this.a + ")";
378
+ return "rgba(".concat(this.r, ", ").concat(this.g, ", ").concat(this.b, ", ").concat(this.a, ")");
378
379
  };
379
380
  return Color;
380
381
  }());
@@ -433,7 +434,7 @@ var Font = /** @class */ (function () {
433
434
  s += this.weight + ' ';
434
435
  if (this.stretch !== 'normal')
435
436
  s += this.stretch + ' ';
436
- s += "" + this.size + this.sizeUnit + " ";
437
+ s += "".concat(this.size).concat(this.sizeUnit, " ");
437
438
  if (this.lineHeight !== 'normal')
438
439
  s += this.lineHeight + ' ';
439
440
  s += this.family;
@@ -450,7 +451,7 @@ var parseFontEl = document.createElement('div');
450
451
  */
451
452
  function parseFont(str) {
452
453
  // Assign css string to html element
453
- parseFontEl.setAttribute('style', "font: " + str);
454
+ parseFontEl.setAttribute('style', "font: ".concat(str));
454
455
  var _a = parseFontEl.style, fontSize = _a.fontSize, fontFamily = _a.fontFamily, fontStyle = _a.fontStyle, fontVariant = _a.fontVariant, fontWeight = _a.fontWeight, lineHeight = _a.lineHeight;
455
456
  parseFontEl.removeAttribute('style');
456
457
  var size = parseFloat(fontSize);
@@ -485,6 +486,8 @@ function mapPixels(mapper, canvas, ctx, x, y, width, height, flush) {
485
486
  * <p>Must be called before any watchable properties are set, and only once in
486
487
  * the prototype chain.
487
488
  *
489
+ * @deprecated Will be removed in the future (see issue #130)
490
+ *
488
491
  * @param target - object to watch
489
492
  */
490
493
  function watchPublic(target) {
@@ -493,7 +496,7 @@ function watchPublic(target) {
493
496
  };
494
497
  var callback = function (prop, val, receiver) {
495
498
  // Public API property updated, emit 'modify' event.
496
- publish(proxy, target.type + ".change.modify", { property: getPath(receiver, prop), newValue: val });
499
+ publish(proxy, "".concat(target.type, ".change.modify"), { property: getPath(receiver, prop), newValue: val });
497
500
  };
498
501
  var canWatch = function (receiver, prop) { return !prop.startsWith('_') &&
499
502
  (receiver.publicExcludes === undefined || !receiver.publicExcludes.includes(prop)); };
@@ -618,7 +621,10 @@ function AudioSourceMixin(superclass) {
618
621
  this.audioNode.connect(movie.actx.destination);
619
622
  };
620
623
  MixedAudioSource.prototype.detach = function () {
621
- this.audioNode.disconnect(this.movie.actx.destination);
624
+ // Cache dest before super.detach() unsets this.movie
625
+ var dest = this.movie.actx.destination;
626
+ _super.prototype.detach.call(this);
627
+ this.audioNode.disconnect(dest);
622
628
  };
623
629
  MixedAudioSource.prototype.start = function () {
624
630
  this.source.currentTime = this.currentTime + this.sourceStartTime;
@@ -727,7 +733,7 @@ var Base = /** @class */ (function () {
727
733
  // Propogate up to target
728
734
  subscribe(newThis, 'layer.change', function (event) {
729
735
  var typeOfChange = event.type.substring(event.type.lastIndexOf('.') + 1);
730
- var type = "movie.change.layer." + typeOfChange;
736
+ var type = "movie.change.layer.".concat(typeOfChange);
731
737
  publish(newThis._movie, type, __assign(__assign({}, event), { target: newThis._movie, type: type }));
732
738
  });
733
739
  return newThis;
@@ -800,7 +806,8 @@ var Base = /** @class */ (function () {
800
806
  * The current time of the movie relative to this layer
801
807
  */
802
808
  get: function () {
803
- return this._movie ? this._movie.currentTime - this.startTime
809
+ return this._movie
810
+ ? this._movie.currentTime - this.startTime
804
811
  : undefined;
805
812
  },
806
813
  enumerable: false,
@@ -836,7 +843,7 @@ var Base = /** @class */ (function () {
836
843
  // id for events (independent of instance, but easy to access when on prototype
837
844
  // chain)
838
845
  Base.prototype.type = 'layer';
839
- Base.prototype.publicExcludes = [];
846
+ Base.prototype.publicExcludes = ['active'];
840
847
  Base.prototype.propertyFilters = {};
841
848
 
842
849
  // TODO: rename to something more consistent with the naming convention of Visual and VisualSourceMixin
@@ -981,13 +988,13 @@ var Visual = /** @class */ (function (_super) {
981
988
  height: null,
982
989
  /**
983
990
  * @name module:layer.Visual#background
984
- * @desc The CSS color code for the background, or <code>null</code> for
991
+ * @desc The color code for the background, or <code>null</code> for
985
992
  * transparency
986
993
  */
987
994
  background: null,
988
995
  /**
989
996
  * @name module:layer.Visual#border
990
- * @desc The CSS border style, or <code>null</code> for no border
997
+ * @desc The border style, or <code>null</code> for no border
991
998
  */
992
999
  border: null,
993
1000
  /**
@@ -1062,19 +1069,23 @@ function VisualSourceMixin(superclass) {
1062
1069
  // instead. (TODO: fact check)
1063
1070
  /* eslint-disable eqeqeq */
1064
1071
  return destWidth != undefined
1065
- ? destWidth : val(this, 'sourceWidth', this.currentTime);
1072
+ ? destWidth
1073
+ : val(this, 'sourceWidth', this.currentTime);
1066
1074
  }, destHeight: function (destHeight) {
1067
1075
  /* eslint-disable eqeqeq */
1068
1076
  return destHeight != undefined
1069
- ? destHeight : val(this, 'sourceHeight', this.currentTime);
1077
+ ? destHeight
1078
+ : val(this, 'sourceHeight', this.currentTime);
1070
1079
  }, width: function (width) {
1071
1080
  /* eslint-disable eqeqeq */
1072
1081
  return width != undefined
1073
- ? width : val(this, 'destWidth', this.currentTime);
1082
+ ? width
1083
+ : val(this, 'destWidth', this.currentTime);
1074
1084
  }, height: function (height) {
1075
1085
  /* eslint-disable eqeqeq */
1076
1086
  return height != undefined
1077
- ? height : val(this, 'destHeight', this.currentTime);
1087
+ ? height
1088
+ : val(this, 'destHeight', this.currentTime);
1078
1089
  } });
1079
1090
  return MixedVisualSource;
1080
1091
  }
@@ -1146,7 +1157,7 @@ var Text = /** @class */ (function (_super) {
1146
1157
  return metrics;
1147
1158
  } */
1148
1159
  Text.prototype.getDefaultOptions = function () {
1149
- return __assign(__assign({}, Visual.prototype.getDefaultOptions()), { background: null, text: undefined, font: '10px sans-serif', color: '#fff', textX: 0, textY: 0, maxWidth: null, textAlign: 'start', textBaseline: 'top', textDirection: 'ltr' });
1160
+ 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' });
1150
1161
  };
1151
1162
  return Text;
1152
1163
  }(Visual));
@@ -1170,6 +1181,7 @@ var Video = /** @class */ (function (_super) {
1170
1181
  */
1171
1182
 
1172
1183
  var index = /*#__PURE__*/Object.freeze({
1184
+ __proto__: null,
1173
1185
  AudioSourceMixin: AudioSourceMixin,
1174
1186
  Audio: Audio,
1175
1187
  Base: Base,
@@ -1193,7 +1205,7 @@ var Base$1 = /** @class */ (function () {
1193
1205
  subscribe(newThis, 'effect.change.modify', function (event) {
1194
1206
  if (!newThis._target)
1195
1207
  return;
1196
- var type = newThis._target.type + ".change.effect.modify";
1208
+ var type = "".concat(newThis._target.type, ".change.effect.modify");
1197
1209
  publish(newThis._target, type, __assign(__assign({}, event), { target: newThis._target, source: newThis, type: type }));
1198
1210
  });
1199
1211
  return newThis;
@@ -1282,6 +1294,18 @@ var Visual$1 = /** @class */ (function (_super) {
1282
1294
  function Visual() {
1283
1295
  return _super !== null && _super.apply(this, arguments) || this;
1284
1296
  }
1297
+ // subclasses must implement apply
1298
+ /**
1299
+ * Apply this effect to a target at the given time
1300
+ *
1301
+ * @param target
1302
+ * @param reltime - the movie's current time relative to the layer
1303
+ * (will soon be replaced with an instance getter)
1304
+ * @abstract
1305
+ */
1306
+ Visual.prototype.apply = function (target, reltime) {
1307
+ _super.prototype.apply.call(this, target, reltime);
1308
+ };
1285
1309
  return Visual;
1286
1310
  }(Base$1));
1287
1311
 
@@ -1341,7 +1365,7 @@ var Shader = /** @class */ (function (_super) {
1341
1365
  * object.
1342
1366
  */
1343
1367
  if (userUniforms[name_1])
1344
- throw new Error("Texture - uniform naming conflict: " + name_1 + "!");
1368
+ throw new Error("Texture - uniform naming conflict: ".concat(name_1, "!"));
1345
1369
  // Add this as a "user uniform".
1346
1370
  userUniforms[name_1] = '1i'; // texture pointer
1347
1371
  }
@@ -1550,7 +1574,7 @@ var Shader = /** @class */ (function (_super) {
1550
1574
  value.g !== undefined ? value.g : def,
1551
1575
  value.b !== undefined ? value.b : def
1552
1576
  ];
1553
- throw new Error("Invalid type: " + outputType + " or value: " + value);
1577
+ throw new Error("Invalid type: ".concat(outputType, " or value: ").concat(value));
1554
1578
  }
1555
1579
  if (outputType === '4fv') {
1556
1580
  if (Array.isArray(value) && value.length === 4)
@@ -1563,7 +1587,7 @@ var Shader = /** @class */ (function (_super) {
1563
1587
  value.b !== undefined ? value.b : def,
1564
1588
  value.a !== undefined ? value.a : def
1565
1589
  ];
1566
- throw new Error("Invalid type: " + outputType + " or value: " + value);
1590
+ throw new Error("Invalid type: ".concat(outputType, " or value: ").concat(value));
1567
1591
  }
1568
1592
  return value;
1569
1593
  };
@@ -2040,7 +2064,7 @@ var GaussianBlurComponent = /** @class */ (function (_super) {
2040
2064
  };
2041
2065
  GaussianBlurComponent._genPascalRow = function (index) {
2042
2066
  if (index < 0)
2043
- throw new Error("Invalid index " + index);
2067
+ throw new Error("Invalid index ".concat(index));
2044
2068
  var currRow = [1];
2045
2069
  for (var i = 1; i < index; i++) {
2046
2070
  var nextRow = [];
@@ -2312,16 +2336,17 @@ var Transform = /** @class */ (function (_super) {
2312
2336
  */
2313
2337
 
2314
2338
  var index$1 = /*#__PURE__*/Object.freeze({
2315
- GaussianBlur: GaussianBlur,
2316
- GaussianBlurHorizontal: GaussianBlurHorizontal,
2317
- GaussianBlurVertical: GaussianBlurVertical,
2339
+ __proto__: null,
2318
2340
  Base: Base$1,
2341
+ Brightness: Brightness,
2319
2342
  Channels: Channels,
2320
2343
  ChromaKey: ChromaKey,
2321
2344
  Contrast: Contrast,
2322
2345
  EllipticalMaskOptions: EllipticalMaskOptions,
2323
2346
  EllipticalMask: EllipticalMask,
2324
- Brightness: Brightness,
2347
+ GaussianBlur: GaussianBlur,
2348
+ GaussianBlurHorizontal: GaussianBlurHorizontal,
2349
+ GaussianBlurVertical: GaussianBlurVertical,
2325
2350
  Grayscale: Grayscale,
2326
2351
  Pixelate: Pixelate,
2327
2352
  Shader: Shader,
@@ -2343,8 +2368,6 @@ var MovieOptions = /** @class */ (function () {
2343
2368
  *
2344
2369
  * Implements a pub/sub system.
2345
2370
  */
2346
- // TODO: Implement event "durationchange", and more
2347
- // TODO: Add width and height options
2348
2371
  // TODO: Make record option to make recording video output to the user while
2349
2372
  // it's recording
2350
2373
  // TODO: rename renderingFrame -> refreshing
@@ -2614,19 +2637,28 @@ var Movie = /** @class */ (function () {
2614
2637
  done();
2615
2638
  return;
2616
2639
  }
2617
- this._updateCurrentTime(timestamp);
2618
- var recordingEnd = this.recording ? this._recordEndTime : this.duration;
2619
- var recordingEnded = this.currentTime > recordingEnd;
2620
- if (recordingEnded)
2621
- publish(this, 'movie.recordended', { movie: this });
2622
- // Bad for performance? (remember, it's calling Array.reduce)
2623
- var end = this.duration;
2624
- var ended = this.currentTime > end;
2625
- if (ended) {
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.
2653
+ this._currentTime = 0;
2654
+ publish(this, 'movie.timeupdate', { movie: this });
2655
+ }
2626
2656
  this._lastPlayed = performance.now();
2627
2657
  this._lastPlayedOffset = 0; // this.currentTime
2628
2658
  this._renderingFrame = false;
2629
- if (!this.repeat || this.recording) {
2659
+ // Stop playback or recording if done (except if it's playing and repeat
2660
+ // is true)
2661
+ if (!(!this.recording && this.repeat)) {
2630
2662
  this._paused = true;
2631
2663
  this._ended = true;
2632
2664
  // Deactivate all layers
@@ -2635,22 +2667,15 @@ var Movie = /** @class */ (function () {
2635
2667
  var layer = this.layers[i];
2636
2668
  // A layer that has been deleted before layers.length has been updated
2637
2669
  // (see the layers proxy in the constructor).
2638
- if (!layer)
2670
+ if (!layer || !layer.active)
2639
2671
  continue;
2640
2672
  layer.stop();
2641
2673
  layer.active = false;
2642
2674
  }
2675
+ if (done)
2676
+ done();
2677
+ return;
2643
2678
  }
2644
- publish(this, 'movie.ended', { movie: this, repeat: this.repeat });
2645
- // TODO: only reset currentTime if repeating
2646
- this._currentTime = 0; // don't use setter
2647
- publish(this, 'movie.timeupdate', { movie: this });
2648
- }
2649
- // Stop playback or recording if done
2650
- if (recordingEnded || (ended && !this.repeat)) {
2651
- if (done)
2652
- done();
2653
- return;
2654
2679
  }
2655
2680
  // Do render
2656
2681
  this._renderBackground(timestamp);
@@ -2672,16 +2697,21 @@ var Movie = /** @class */ (function () {
2672
2697
  _this._render(repeat, undefined, done);
2673
2698
  }); // TODO: research performance cost
2674
2699
  };
2675
- Movie.prototype._updateCurrentTime = function (timestamp) {
2700
+ Movie.prototype._updateCurrentTime = function (timestampMs, end) {
2676
2701
  // If we're only instant-rendering (current frame only), it doens't matter
2677
2702
  // if it's paused or not.
2678
2703
  if (!this._renderingFrame) {
2679
2704
  // if ((timestamp - this._lastUpdate) >= this._updateInterval) {
2680
- var sinceLastPlayed = (timestamp - this._lastPlayed) / 1000;
2681
- this._currentTime = this._lastPlayedOffset + sinceLastPlayed; // don't use setter
2682
- publish(this, 'movie.timeupdate', { movie: this });
2705
+ var sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000;
2706
+ var currentTime = this._lastPlayedOffset + sinceLastPlayed; // don't use setter
2707
+ if (this.currentTime !== currentTime) {
2708
+ this._currentTime = currentTime;
2709
+ publish(this, 'movie.timeupdate', { movie: this });
2710
+ }
2683
2711
  // this._lastUpdate = timestamp;
2684
2712
  // }
2713
+ if (this.currentTime > end)
2714
+ this.currentTime = end;
2685
2715
  }
2686
2716
  };
2687
2717
  Movie.prototype._renderBackground = function (timestamp) {
@@ -2948,9 +2978,9 @@ var Movie = /** @class */ (function () {
2948
2978
  canvas: undefined,
2949
2979
  /**
2950
2980
  * @name module:movie#background
2951
- * @desc The css color for the background, or <code>null</code> for transparency
2981
+ * @desc The color for the background, or <code>null</code> for transparency
2952
2982
  */
2953
- background: '#000',
2983
+ background: parseColor('#000'),
2954
2984
  /**
2955
2985
  * @name module:movie#repeat
2956
2986
  */
@@ -2978,6 +3008,7 @@ Movie.prototype.propertyFilters = {};
2978
3008
  */
2979
3009
 
2980
3010
  var etro = /*#__PURE__*/Object.freeze({
3011
+ __proto__: null,
2981
3012
  layer: index,
2982
3013
  effect: index$1,
2983
3014
  event: event,