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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "etro",
3
- "version": "0.8.5",
3
+ "version": "0.9.1",
4
4
  "description": "An extendable video-editing framework for the browser and Node",
5
5
  "browser": "dist/etro-cjs.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,39 +10,39 @@
10
10
  "test": "spec"
11
11
  },
12
12
  "devDependencies": {
13
- "@types/dom-mediacapture-record": "^1.0.7",
14
- "@typescript-eslint/eslint-plugin": "^4.15.2",
15
- "@typescript-eslint/parser": "^4.15.2",
13
+ "@rollup/plugin-eslint": "^8.0.2",
14
+ "@types/jest": "^29.0.0",
15
+ "@typescript-eslint/eslint-plugin": "^5.30.7",
16
+ "@typescript-eslint/parser": "^5.30.7",
16
17
  "docdash": "^1.1.1",
17
18
  "ecstatic": ">=4.1.3",
18
- "eslint": "^6.5.1",
19
- "eslint-config-standard": "^14.1.0",
20
- "eslint-plugin-html": "^6.0.0",
21
- "eslint-plugin-import": "^2.18.2",
22
- "eslint-plugin-node": "^10.0.0",
23
- "eslint-plugin-promise": "^4.2.1",
24
- "eslint-plugin-standard": "^4.0.1",
19
+ "eslint": "^8.20.0",
20
+ "eslint-config-standard": "^17.0.0",
21
+ "eslint-plugin-html": "^7.0.0",
22
+ "eslint-plugin-import": "^2.26.0",
23
+ "eslint-plugin-node": "^11.1.0",
24
+ "eslint-plugin-promise": "^6.0.0",
25
+ "eslint-plugin-standard": "^5.0.0",
25
26
  "ev": "0.0.7",
26
- "http-server": "^0.12.3",
27
+ "http-server": "^14.1.1",
27
28
  "jasmine": "^3.4.0",
29
+ "jasmine-ts": "^0.4.0",
28
30
  "karma": "^6.1.1",
29
- "karma-es6-shim": "^1.0.0",
30
31
  "karma-firefox-launcher": "^2.1.2",
31
32
  "karma-jasmine": "^2.0.1",
32
- "karma-requirejs": "^1.1.0",
33
33
  "karma-super-dots-reporter": "^0.2.0",
34
+ "karma-typescript": "^5.5.3",
34
35
  "keep-a-changelog": "^0.10.4",
35
36
  "puppeteer": "^2.0.0",
36
37
  "resemblejs": "^4.1.0",
37
38
  "rollup": "^1.19.4",
38
39
  "rollup-plugin-cleaner": "^1.0.0",
39
- "rollup-plugin-eslint": "^7.0.0",
40
40
  "rollup-plugin-node-resolve": "^5.2.0",
41
41
  "rollup-plugin-typescript2": "^0.29.0",
42
42
  "rollup-plugin-uglify-es": "^0.0.1",
43
- "shipjs": "0.23.3",
44
- "typedoc": "^0.22.11",
45
- "typescript": "^4.1.3"
43
+ "shipjs": "^0.24.4",
44
+ "typedoc": "^0.23.8",
45
+ "typescript": "^4.7.4"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "rollup -c",
@@ -51,7 +51,7 @@
51
51
  "effects": "node scripts/save-effect-samples.js",
52
52
  "lint": "npm run --silent lint:main && npm run --silent lint:test && npm run --silent lint:examples",
53
53
  "lint:main": "eslint -c eslint.typescript-conf.js --ext .ts src",
54
- "lint:test": "eslint -c eslint.test-conf.js spec",
54
+ "lint:test": "eslint -c eslint.test-conf.js --ext .ts spec",
55
55
  "lint:examples": "eslint -c eslint.example-conf.js --ext .html examples",
56
56
  "start": "http-server",
57
57
  "test": "karma start",
@@ -76,10 +76,34 @@
76
76
  save(original, 'original.png')
77
77
 
78
78
  const samples = {
79
+ 'brightness.png': new etro.effect.Brightness({
80
+ brightness: -100
81
+ }),
82
+ 'contrast.png': new etro.effect.Contrast({
83
+ contrast: 0.5
84
+ }),
85
+ 'channels.png': new etro.effect.Channels({
86
+ factors: {
87
+ r: 0.25,
88
+ g: 0.5,
89
+ b: 0.75
90
+ }
91
+ }),
92
+ 'chroma-key.png': new etro.effect.ChromaKey({
93
+ target: etro.parseColor('green'),
94
+ threshold: 100
95
+ }),
79
96
  'gaussian-blur-horizontal.png': new etro.effect.GaussianBlurHorizontal({ radius: 5 }),
80
97
  'gaussian-blur-vertical.png': new etro.effect.GaussianBlurVertical({ radius: 5 }),
81
98
  'grayscale.png': new etro.effect.Grayscale(),
82
99
  'pixelate.png': new etro.effect.Pixelate({ pixelSize: 3 }),
100
+ 'shader.png': new etro.effect.Shader(),
101
+ 'stack.png': new etro.effect.Stack({
102
+ effects: [
103
+ new etro.effect.Brightness({ brightness: -100 }),
104
+ new etro.effect.Contrast({ contrast: 0.5 })
105
+ ]
106
+ }),
83
107
  'transform/translate.png': new etro.effect.Transform({
84
108
  matrix: new etro.effect.Transform.Matrix().translate(-3, 5)
85
109
  }),
@@ -39,7 +39,7 @@ function createDirs(filePath) {
39
39
  // remove prefix and save to png
40
40
  const buffer = Buffer.from(item.data.replace(/^data:image\/png;base64,/, ''), 'base64')
41
41
  console.log(`writing ${item.path} ...`)
42
- const path = projectDir + '/spec/assets/effect/' + item.path
42
+ const path = projectDir + '/spec/integration/assets/effect/' + item.path
43
43
  createDirs(path)
44
44
  fs.writeFileSync(path, buffer)
45
45
  })
@@ -1,7 +1,7 @@
1
1
  import { watchPublic } from '../util'
2
2
  import { publish, subscribe } from '../event'
3
3
  import { Movie } from '../movie'
4
- import { Visual } from '../layer/index'
4
+ import { Base as BaseLayer } from '../layer/index'
5
5
  import BaseObject from '../object'
6
6
 
7
7
  /**
@@ -14,7 +14,7 @@ export class Base implements BaseObject {
14
14
 
15
15
  enabled: boolean
16
16
 
17
- private _target: Movie | Visual
17
+ private _target: Movie | BaseLayer
18
18
  /**
19
19
  * The number of times this effect has been attached to a target minus the
20
20
  * number of times it's been detached. (Used for the target's array proxy with
@@ -45,14 +45,14 @@ export class Base implements BaseObject {
45
45
  * Attaches this effect to `target` if not already attached.
46
46
  * @ignore
47
47
  */
48
- tryAttach (target: Movie | Visual): void {
48
+ tryAttach (target: Movie | BaseLayer): void {
49
49
  if (this._occurrenceCount === 0)
50
50
  this.attach(target)
51
51
 
52
52
  this._occurrenceCount++
53
53
  }
54
54
 
55
- attach (movie: Movie | Visual): void {
55
+ attach (movie: Movie | BaseLayer): void {
56
56
  this._target = movie
57
57
  }
58
58
 
@@ -87,7 +87,7 @@ export class Base implements BaseObject {
87
87
  * (will soon be replaced with an instance getter)
88
88
  * @abstract
89
89
  */
90
- apply (target: Movie | Visual, reltime: number): void {} // eslint-disable-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
90
+ apply (target: Movie | BaseLayer, reltime: number): void {} // eslint-disable-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
91
91
 
92
92
  /**
93
93
  * The current time of the target
@@ -96,7 +96,7 @@ export class Base implements BaseObject {
96
96
  return this._target ? this._target.currentTime : undefined
97
97
  }
98
98
 
99
- get parent (): Movie | Visual {
99
+ get parent (): Movie | BaseLayer {
100
100
  return this._target
101
101
  }
102
102
 
@@ -21,13 +21,13 @@ export class Stack extends Visual {
21
21
  this._effectsBack = []
22
22
  // TODO: Throw 'change' events in handlers
23
23
  this.effects = new Proxy(this._effectsBack, {
24
- deleteProperty: function (target: Visual[], property: number | string): boolean {
24
+ deleteProperty: function (target: Visual[], property: string | symbol): boolean {
25
25
  const value = target[property]
26
26
  value.detach() // Detach effect from movie
27
27
  delete target[property]
28
28
  return true
29
29
  },
30
- set: function (target: Visual[], property: number | string, value: Visual): boolean {
30
+ set: function (target: Visual[], property: string | symbol, value: Visual): boolean {
31
31
  // TODO: make sure type check works
32
32
  if (!isNaN(Number(property))) { // if property is a number (index)
33
33
  if (target[property])
@@ -15,9 +15,9 @@ export interface TransformOptions {
15
15
  */
16
16
  class Transform extends Visual {
17
17
  /** Matrix that determines how to transform the target */
18
- matrix: Dynamic<Transform.Matrix>
18
+ matrix: Dynamic<Transform.Matrix> // eslint-disable-line no-use-before-define
19
19
 
20
- private _tmpMatrix: Transform.Matrix
20
+ private _tmpMatrix: Transform.Matrix // eslint-disable-line no-use-before-define
21
21
  private _tmpCanvas: HTMLCanvasElement
22
22
  private _tmpCtx: CanvasRenderingContext2D
23
23
 
@@ -1,6 +1,21 @@
1
+ import { Movie } from '../movie'
2
+ import { Visual as VisualLayer } from '../layer/index'
1
3
  import { Base } from './base'
2
4
 
3
5
  /**
4
6
  * Modifies the visual contents of a layer.
5
7
  */
6
- export class Visual extends Base {}
8
+ export class Visual extends Base {
9
+ // subclasses must implement apply
10
+ /**
11
+ * Apply this effect to a target at the given time
12
+ *
13
+ * @param target
14
+ * @param reltime - the movie's current time relative to the layer
15
+ * (will soon be replaced with an instance getter)
16
+ * @abstract
17
+ */
18
+ apply (target: Movie | VisualLayer, reltime: number): void { // eslint-disable-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
19
+ super.apply(target, reltime)
20
+ }
21
+ }
@@ -134,7 +134,10 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
134
134
  }
135
135
 
136
136
  detach () {
137
- this.audioNode.disconnect(this.movie.actx.destination)
137
+ // Cache dest before super.detach() unsets this.movie
138
+ const dest = this.movie.actx.destination
139
+ super.detach()
140
+ this.audioNode.disconnect(dest)
138
141
  }
139
142
 
140
143
  start () {
package/src/layer/base.ts CHANGED
@@ -141,7 +141,8 @@ class Base implements EtroObject {
141
141
  * The current time of the movie relative to this layer
142
142
  */
143
143
  get currentTime (): number {
144
- return this._movie ? this._movie.currentTime - this.startTime
144
+ return this._movie
145
+ ? this._movie.currentTime - this.startTime
145
146
  : undefined
146
147
  }
147
148
 
@@ -169,7 +170,7 @@ class Base implements EtroObject {
169
170
  // id for events (independent of instance, but easy to access when on prototype
170
171
  // chain)
171
172
  Base.prototype.type = 'layer'
172
- Base.prototype.publicExcludes = []
173
+ Base.prototype.publicExcludes = ['active']
173
174
  Base.prototype.propertyFilters = {}
174
175
 
175
176
  export { Base, BaseOptions }
@@ -1,7 +1,7 @@
1
- import { Visual, VisualOptions } from './visual'
2
- import { VisualSourceMixin } from './visual-source'
1
+ import { Visual } from './visual'
2
+ import { VisualSourceMixin, VisualSourceOptions } from './visual-source'
3
3
 
4
- type ImageOptions = VisualOptions
4
+ type ImageOptions = VisualSourceOptions
5
5
 
6
6
  class Image extends VisualSourceMixin(Visual) {}
7
7
 
package/src/layer/text.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { Dynamic, val, applyOptions } from '../util'
1
+ import { Dynamic, val, applyOptions, Color, parseColor } from '../util'
2
2
  import { Visual, VisualOptions } from './visual'
3
3
 
4
4
  interface TextOptions extends VisualOptions {
5
5
  text: Dynamic<string>
6
6
  font?: Dynamic<string>
7
- color?: Dynamic<string>
7
+ color?: Dynamic<Color>
8
8
  /** The text's horizontal offset from the layer */
9
9
  textX?: Dynamic<number>
10
10
  /** The text's vertical offset from the layer */
@@ -29,7 +29,7 @@ interface TextOptions extends VisualOptions {
29
29
  class Text extends Visual {
30
30
  text: Dynamic<string>
31
31
  font: Dynamic<string>
32
- color: Dynamic<string>
32
+ color: Dynamic<Color>
33
33
  /** The text's horizontal offset from the layer */
34
34
  textX: Dynamic<number>
35
35
  /** The text's vertical offset from the layer */
@@ -124,7 +124,7 @@ class Text extends Visual {
124
124
  background: null,
125
125
  text: undefined, // required
126
126
  font: '10px sans-serif',
127
- color: '#fff',
127
+ color: parseColor('#fff'),
128
128
  textX: 0,
129
129
  textY: 0,
130
130
  maxWidth: null,
@@ -1,11 +1,27 @@
1
1
  import { Dynamic, val, applyOptions } from '../util'
2
- import { Base, BaseOptions } from './base'
3
2
  import { Visual, VisualOptions } from './visual'
4
3
 
5
4
  type Constructor<T> = new (...args: unknown[]) => T
6
5
 
7
- interface VisualSource extends Base {
6
+ interface VisualSource extends Visual {
8
7
  readonly source: HTMLImageElement | HTMLVideoElement
8
+
9
+ /** What part of {@link source} to render */
10
+ sourceX: Dynamic<number>
11
+ /** What part of {@link source} to render */
12
+ sourceY: Dynamic<number>
13
+ /** What part of {@link source} to render, or undefined for the entire width */
14
+ sourceWidth: Dynamic<number>
15
+ /** What part of {@link source} to render, or undefined for the entire height */
16
+ sourceHeight: Dynamic<number>
17
+ /** Where to render {@link source} onto the layer */
18
+ destX: Dynamic<number>
19
+ /** Where to render {@link source} onto the layer */
20
+ destY: Dynamic<number>
21
+ /** Where to render {@link source} onto the layer, or undefined to fill the layer's width */
22
+ destWidth: Dynamic<number>
23
+ /** Where to render {@link source} onto the layer, or undefined to fill the layer's height */
24
+ destHeight: Dynamic<number>
9
25
  }
10
26
 
11
27
  interface VisualSourceOptions extends VisualOptions {
@@ -32,7 +48,7 @@ interface VisualSourceOptions extends VisualOptions {
32
48
  * A layer that gets its image data from an HTML image or video element
33
49
  * @mixin VisualSourceMixin
34
50
  */
35
- function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Constructor<Visual>): Constructor<VisualSource> {
51
+ function VisualSourceMixin<OptionsSuperclass extends VisualOptions> (superclass: Constructor<Visual>): Constructor<VisualSource> {
36
52
  type MixedVisualSourceOptions = OptionsSuperclass & VisualSourceOptions
37
53
 
38
54
  class MixedVisualSource extends superclass {
@@ -125,22 +141,26 @@ function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: C
125
141
  // instead. (TODO: fact check)
126
142
  /* eslint-disable eqeqeq */
127
143
  return destWidth != undefined
128
- ? destWidth : val(this, 'sourceWidth', this.currentTime)
144
+ ? destWidth
145
+ : val(this, 'sourceWidth', this.currentTime)
129
146
  },
130
147
  destHeight: function (destHeight) {
131
148
  /* eslint-disable eqeqeq */
132
149
  return destHeight != undefined
133
- ? destHeight : val(this, 'sourceHeight', this.currentTime)
150
+ ? destHeight
151
+ : val(this, 'sourceHeight', this.currentTime)
134
152
  },
135
153
  width: function (width) {
136
154
  /* eslint-disable eqeqeq */
137
155
  return width != undefined
138
- ? width : val(this, 'destWidth', this.currentTime)
156
+ ? width
157
+ : val(this, 'destWidth', this.currentTime)
139
158
  },
140
159
  height: function (height) {
141
160
  /* eslint-disable eqeqeq */
142
161
  return height != undefined
143
- ? height : val(this, 'destHeight', this.currentTime)
162
+ ? height
163
+ : val(this, 'destHeight', this.currentTime)
144
164
  }
145
165
  }
146
166
 
@@ -1,4 +1,4 @@
1
- import { Dynamic, val, applyOptions } from '../util'
1
+ import { Dynamic, val, applyOptions, Color } from '../util'
2
2
  import { Base, BaseOptions } from './base'
3
3
  import { Visual as VisualEffect } from '../effect/visual'
4
4
 
@@ -7,9 +7,9 @@ interface VisualOptions extends BaseOptions {
7
7
  y?: Dynamic<number>
8
8
  width?: Dynamic<number>
9
9
  height?: Dynamic<number>
10
- background?: Dynamic<string>
10
+ background?: Dynamic<Color>
11
11
  border?: Dynamic<{
12
- color: string
12
+ color: Color
13
13
  thickness?: number
14
14
  }>
15
15
 
@@ -22,9 +22,9 @@ class Visual extends Base {
22
22
  y: Dynamic<number>
23
23
  width: Dynamic<number>
24
24
  height: Dynamic<number>
25
- background: Dynamic<string>
25
+ background: Dynamic<Color>
26
26
  border: Dynamic<{
27
- color: string
27
+ color: Color
28
28
  thickness: number
29
29
  }>
30
30
 
@@ -169,13 +169,13 @@ class Visual extends Base {
169
169
  height: null,
170
170
  /**
171
171
  * @name module:layer.Visual#background
172
- * @desc The CSS color code for the background, or <code>null</code> for
172
+ * @desc The color code for the background, or <code>null</code> for
173
173
  * transparency
174
174
  */
175
175
  background: null,
176
176
  /**
177
177
  * @name module:layer.Visual#border
178
- * @desc The CSS border style, or <code>null</code> for no border
178
+ * @desc The border style, or <code>null</code> for no border
179
179
  */
180
180
  border: null,
181
181
  /**
package/src/movie.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { subscribe, publish } from './event'
6
- import { Dynamic, val, clearCachedValues, applyOptions, watchPublic } from './util'
6
+ import { Dynamic, val, clearCachedValues, applyOptions, watchPublic, Color, parseColor } from './util'
7
7
  import { Base as BaseLayer, Audio as AudioLayer, Video as VideoLayer, Visual } from './layer/index' // `Media` mixins
8
8
  import { AudioSource } from './layer/audio-source' // not exported from ./layer/index
9
9
  import { Base as BaseEffect } from './effect/index'
@@ -26,7 +26,7 @@ export class MovieOptions {
26
26
  /** @deprecated Use <code>actx</code> instead */
27
27
  audioContext?: AudioContext
28
28
  /** The background color of the movie as a cSS string */
29
- background?: Dynamic<string>
29
+ background?: Dynamic<Color>
30
30
  repeat?: boolean
31
31
  /** Call `refresh` when the user changes a property on the movie or any of its layers or effects */
32
32
  autoRefresh?: boolean
@@ -37,21 +37,28 @@ export class MovieOptions {
37
37
  *
38
38
  * Implements a pub/sub system.
39
39
  */
40
- // TODO: Implement event "durationchange", and more
41
- // TODO: Add width and height options
42
40
  // TODO: Make record option to make recording video output to the user while
43
41
  // it's recording
44
42
  // TODO: rename renderingFrame -> refreshing
45
43
  export class Movie {
46
44
  type: string
45
+ /**
46
+ * @deprecated Auto-refresh will be removed in the future (see issue #130).
47
+ */
47
48
  publicExcludes: string[]
48
49
  propertyFilters: Record<string, <T>(value: T) => T>
49
50
 
50
51
  repeat: boolean
51
- /** Call `refresh` when the user changes a property on the movie or any of its layers or effects */
52
+ /**
53
+ * Call `refresh` when the user changes a property on the movie or any of its
54
+ * layers or effects
55
+ *
56
+ * @deprecated Auto-refresh will be removed in the future. If you want to
57
+ * refresh the canvas, call `refresh`. See issue #130.
58
+ */
52
59
  autoRefresh: boolean
53
60
  /** The background color of the movie as a cSS string */
54
- background: Dynamic<string>
61
+ background: Dynamic<Color>
55
62
  /** The audio context to which audio output is sent during playback */
56
63
  readonly actx: AudioContext
57
64
  // Readonly because it's a proxy (so it can't be overwritten).
@@ -59,7 +66,7 @@ export class Movie {
59
66
  // Readonly because it's a proxy (so it can't be overwritten).
60
67
  readonly layers: BaseLayer[]
61
68
 
62
- private _canvas: HTMLCanvasElement;
69
+ private _canvas: HTMLCanvasElement
63
70
  private _cctx: CanvasRenderingContext2D
64
71
  private _effectsBack: BaseEffect[]
65
72
  private _layersBack: BaseLayer[]
@@ -378,20 +385,34 @@ export class Movie {
378
385
  return
379
386
  }
380
387
 
381
- this._updateCurrentTime(timestamp)
382
- const recordingEnd = this.recording ? this._recordEndTime : this.duration
383
- const recordingEnded = this.currentTime > recordingEnd
384
- if (recordingEnded)
385
- publish(this, 'movie.recordended', { movie: this })
388
+ const end = this.recording ? this._recordEndTime : this.duration
389
+
390
+ this._updateCurrentTime(timestamp, end)
391
+
392
+ // TODO: Is calling duration every frame bad for performance? (remember,
393
+ // it's calling Array.reduce)
394
+ if (this.currentTime === end) {
395
+ if (this.recording)
396
+ publish(this, 'movie.recordended', { movie: this })
397
+
398
+ if (this.currentTime === this.duration)
399
+ publish(this, 'movie.ended', { movie: this, repeat: this.repeat })
400
+
401
+ // TODO: only reset currentTime if repeating
402
+ if (this.repeat) {
403
+ // Don't use setter, which publishes 'movie.seek'. Instead, update the
404
+ // value and publish a 'movie.timeupdate' event.
405
+ this._currentTime = 0
406
+ publish(this, 'movie.timeupdate', { movie: this })
407
+ }
386
408
 
387
- // Bad for performance? (remember, it's calling Array.reduce)
388
- const end = this.duration
389
- const ended = this.currentTime > end
390
- if (ended) {
391
409
  this._lastPlayed = performance.now()
392
410
  this._lastPlayedOffset = 0 // this.currentTime
393
411
  this._renderingFrame = false
394
- if (!this.repeat || this.recording) {
412
+
413
+ // Stop playback or recording if done (except if it's playing and repeat
414
+ // is true)
415
+ if (!(!this.recording && this.repeat)) {
395
416
  this._paused = true
396
417
  this._ended = true
397
418
  // Deactivate all layers
@@ -400,27 +421,18 @@ export class Movie {
400
421
  const layer = this.layers[i]
401
422
  // A layer that has been deleted before layers.length has been updated
402
423
  // (see the layers proxy in the constructor).
403
- if (!layer)
424
+ if (!layer || !layer.active)
404
425
  continue
405
426
 
406
427
  layer.stop()
407
428
  layer.active = false
408
429
  }
409
- }
410
-
411
- publish(this, 'movie.ended', { movie: this, repeat: this.repeat })
412
-
413
- // TODO: only reset currentTime if repeating
414
- this._currentTime = 0 // don't use setter
415
- publish(this, 'movie.timeupdate', { movie: this })
416
- }
417
430
 
418
- // Stop playback or recording if done
419
- if (recordingEnded || (ended && !this.repeat)) {
420
- if (done)
421
- done()
431
+ if (done)
432
+ done()
422
433
 
423
- return
434
+ return
435
+ }
424
436
  }
425
437
 
426
438
  // Do render
@@ -448,16 +460,22 @@ export class Movie {
448
460
  }) // TODO: research performance cost
449
461
  }
450
462
 
451
- private _updateCurrentTime (timestamp) {
463
+ private _updateCurrentTime (timestampMs: number, end: number) {
452
464
  // If we're only instant-rendering (current frame only), it doens't matter
453
465
  // if it's paused or not.
454
466
  if (!this._renderingFrame) {
455
467
  // if ((timestamp - this._lastUpdate) >= this._updateInterval) {
456
- const sinceLastPlayed = (timestamp - this._lastPlayed) / 1000
457
- this._currentTime = this._lastPlayedOffset + sinceLastPlayed // don't use setter
458
- publish(this, 'movie.timeupdate', { movie: this })
468
+ const sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000
469
+ const currentTime = this._lastPlayedOffset + sinceLastPlayed // don't use setter
470
+ if (this.currentTime !== currentTime) {
471
+ this._currentTime = currentTime
472
+ publish(this, 'movie.timeupdate', { movie: this })
473
+ }
459
474
  // this._lastUpdate = timestamp;
460
475
  // }
476
+
477
+ if (this.currentTime > end)
478
+ this.currentTime = end
461
479
  }
462
480
  }
463
481
 
@@ -703,9 +721,9 @@ export class Movie {
703
721
  canvas: undefined, // required
704
722
  /**
705
723
  * @name module:movie#background
706
- * @desc The css color for the background, or <code>null</code> for transparency
724
+ * @desc The color for the background, or <code>null</code> for transparency
707
725
  */
708
- background: '#000',
726
+ background: parseColor('#000'),
709
727
  /**
710
728
  * @name module:movie#repeat
711
729
  */
package/src/object.ts CHANGED
@@ -4,7 +4,11 @@ import { Movie } from './movie'
4
4
  export default interface EtroObject {
5
5
  /** Used in etro internals */
6
6
  type: string
7
- /** Which properties to not watch for changes, for `Movie#autoRefresh` */
7
+ /**
8
+ * Which properties to not watch for changes, for `Movie#autoRefresh`
9
+ *
10
+ * @deprecated `Movie#autoRefresh` is deprecated
11
+ */
8
12
  publicExcludes: string[]
9
13
  /** Map of property name to function to run on result of `val` */
10
14
  propertyFilters: Record<string, <T>(value: T) => T>
package/src/util.ts CHANGED
@@ -415,6 +415,8 @@ export function mapPixels (
415
415
  * <p>Must be called before any watchable properties are set, and only once in
416
416
  * the prototype chain.
417
417
  *
418
+ * @deprecated Will be removed in the future (see issue #130)
419
+ *
418
420
  * @param target - object to watch
419
421
  */
420
422
  export function watchPublic (target: EtroObject): EtroObject {
package/tsconfig.json CHANGED
@@ -4,5 +4,7 @@
4
4
  "target": "es5",
5
5
  "declaration": true
6
6
  },
7
- "include": ["./src/**/*"]
7
+ "include": [
8
+ "./src/**/*"
9
+ ]
8
10
  }