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/src/event.ts CHANGED
@@ -4,6 +4,32 @@
4
4
 
5
5
  import EtroObject from './object'
6
6
 
7
+ class DeprecatedEvent {
8
+ replacement: string
9
+ message: string
10
+
11
+ constructor (replacement: string, message: string = undefined) {
12
+ this.replacement = replacement
13
+ this.message = message
14
+ }
15
+
16
+ toString () {
17
+ let str = ''
18
+
19
+ if (this.replacement) {
20
+ str += `Use ${this.replacement} instead.`
21
+ }
22
+
23
+ if (this.message) {
24
+ str += ` ${this.message}`
25
+ }
26
+
27
+ return str
28
+ }
29
+ }
30
+
31
+ const deprecatedEvents: Record<string, DeprecatedEvent> = {}
32
+
7
33
  export interface Event {
8
34
  target: EtroObject
9
35
  type: string
@@ -21,12 +47,15 @@ class TypeId {
21
47
  }
22
48
 
23
49
  contains (other) {
24
- if (other._parts.length > this._parts.length)
50
+ if (other._parts.length > this._parts.length) {
25
51
  return false
52
+ }
26
53
 
27
- for (let i = 0; i < other._parts.length; i++)
28
- if (other._parts[i] !== this._parts[i])
54
+ for (let i = 0; i < other._parts.length; i++) {
55
+ if (other._parts[i] !== this._parts[i]) {
29
56
  return false
57
+ }
58
+ }
30
59
 
31
60
  return true
32
61
  }
@@ -36,27 +65,60 @@ class TypeId {
36
65
  }
37
66
  }
38
67
 
68
+ export function deprecate (type: string, newType: string, message: string = undefined): void {
69
+ deprecatedEvents[type] = new DeprecatedEvent(newType, message)
70
+ }
71
+
72
+ function subscribeOnce (target: EtroObject, type: string, listener: <T extends Event>(T) => void): void {
73
+ const wrapped = event => {
74
+ unsubscribe(target, wrapped)
75
+ listener(event)
76
+ }
77
+ subscribe(target, type, wrapped)
78
+ }
79
+
80
+ function subscribeMany (target: EtroObject, type: string, listener: <T extends Event>(T) => void): void {
81
+ if (!listeners.has(target)) {
82
+ listeners.set(target, [])
83
+ }
84
+
85
+ listeners.get(target).push(
86
+ { type: new TypeId(type), listener }
87
+ )
88
+ }
89
+
39
90
  /**
40
- * Listen for an event or category of events
91
+ * Listen for an event or category of events.
41
92
  *
42
- * @param target - a etro object
93
+ * @param target - an etro object
43
94
  * @param type - the id of the type (can contain subtypes, such as
44
95
  * "type.subtype")
45
96
  * @param listener
97
+ * @param options - options
98
+ * @param options.once - if true, the listener will only be called once
46
99
  */
47
- export function subscribe (target: EtroObject, type: string, listener: <T extends Event>(T) => void): void {
48
- if (!listeners.has(target))
49
- listeners.set(target, [])
100
+ export function subscribe (
101
+ target: EtroObject,
102
+ type: string,
103
+ listener: <T extends Event>(T) => void,
104
+ options: { once?: boolean } = {}
105
+ ): void {
106
+ // Check if this event is deprecated.
107
+ if (Object.keys(deprecatedEvents).includes(type)) {
108
+ console.warn(`Event ${type} is deprecated. ${deprecatedEvents[type]}`)
109
+ }
50
110
 
51
- listeners.get(target).push(
52
- { type: new TypeId(type), listener }
53
- )
111
+ if (options.once) {
112
+ subscribeOnce(target, type, listener)
113
+ } else {
114
+ subscribeMany(target, type, listener)
115
+ }
54
116
  }
55
117
 
56
118
  /**
57
119
  * Remove an event listener
58
120
  *
59
- * @param target - a etro object
121
+ * @param target - an etro object
60
122
  * @param type - the id of the type (can contain subtypes, such as
61
123
  * "type.subtype")
62
124
  * @param listener
@@ -64,8 +126,9 @@ export function subscribe (target: EtroObject, type: string, listener: <T extend
64
126
  export function unsubscribe (target: EtroObject, listener: <T extends Event>(T) => void): void {
65
127
  // Make sure `listener` has been added with `subscribe`.
66
128
  if (!listeners.has(target) ||
67
- !listeners.get(target).map(pair => pair.listener).includes(listener))
129
+ !listeners.get(target).map(pair => pair.listener).includes(listener)) {
68
130
  throw new Error('No matching event listener to remove')
131
+ }
69
132
 
70
133
  const removed = listeners.get(target)
71
134
  .filter(pair => pair.listener !== listener)
@@ -73,29 +136,31 @@ export function unsubscribe (target: EtroObject, listener: <T extends Event>(T)
73
136
  }
74
137
 
75
138
  /**
76
- * Emits an event to all listeners
139
+ * Publish an event to all listeners without checking if it is deprecated.
77
140
  *
78
- * @param target - a etro object
79
- * @param type - the id of the type (can contain subtypes, such as
80
- * "type.subtype")
81
- * @param event - any additional event data
141
+ * @param target
142
+ * @param type
143
+ * @param event
144
+ * @returns
82
145
  */
83
- export function publish (target: EtroObject, type: string, event: Record<string, unknown>): Event {
146
+ function _publish (target: EtroObject, type: string, event: Record<string, unknown>): Event {
84
147
  (event as unknown as Event).target = target; // could be a proxy
85
148
  (event as unknown as Event).type = type
86
149
 
87
150
  const t = new TypeId(type)
88
151
 
89
- if (!listeners.has(target))
152
+ if (!listeners.has(target)) {
90
153
  // No event fired
91
154
  return null
155
+ }
92
156
 
93
157
  // Call event listeners for this event.
94
158
  const listenersForType = []
95
159
  for (let i = 0; i < listeners.get(target).length; i++) {
96
160
  const item = listeners.get(target)[i]
97
- if (t.contains(item.type))
161
+ if (t.contains(item.type)) {
98
162
  listenersForType.push(item.listener)
163
+ }
99
164
  }
100
165
 
101
166
  for (let i = 0; i < listenersForType.length; i++) {
@@ -106,6 +171,31 @@ export function publish (target: EtroObject, type: string, event: Record<string,
106
171
  return event as unknown as Event
107
172
  }
108
173
 
174
+ /**
175
+ * Emits an event to all listeners
176
+ *
177
+ * @param target - an etro object
178
+ * @param type - the id of the type (can contain subtypes, such as
179
+ * "type.subtype")
180
+ * @param event - any additional event data
181
+ */
182
+ export function publish (target: EtroObject, type: string, event: Record<string, unknown>): Event {
183
+ // Check if this event is deprecated only if it can be replaced.
184
+ if (Object.keys(deprecatedEvents).includes(type) && deprecatedEvents[type].replacement) {
185
+ throw new Error(`Event ${type} is deprecated. ${deprecatedEvents[type]}`)
186
+ }
187
+
188
+ // Check for deprecated events that this event replaces.
189
+ for (const deprecated in deprecatedEvents) {
190
+ const deprecatedEvent = deprecatedEvents[deprecated]
191
+ if (type === deprecatedEvent.replacement) {
192
+ _publish(target, deprecated, { ...event })
193
+ }
194
+ }
195
+
196
+ return _publish(target, type, event)
197
+ }
198
+
109
199
  const listeners: WeakMap<EtroObject, {
110
200
  type: TypeId,
111
201
  listener: (Event) => void
@@ -6,19 +6,24 @@ import { Base, BaseOptions } from './base'
6
6
  type Constructor<T> = new (...args: unknown[]) => T
7
7
 
8
8
  interface AudioSource extends Base {
9
- readonly source: HTMLMediaElement
9
+ /** HTML media element (an audio or video element) */
10
+ readonly source: HTMLAudioElement
11
+ /** Audio source node for the media */
10
12
  readonly audioNode: AudioNode
11
13
  playbackRate: number
12
- /** The audio source node for the media */
14
+ /** Seconds to skip ahead by */
13
15
  sourceStartTime: number
14
16
  }
15
17
 
16
- interface AudioSourceOptions extends BaseOptions {
18
+ interface AudioSourceOptions extends Omit<BaseOptions, 'duration'> {
19
+ duration?: number
20
+ /** HTML media element (an audio or video element) */
17
21
  source: HTMLMediaElement
22
+ /** Seconds to skip ahead by */
18
23
  sourceStartTime?: number
19
24
  muted?: boolean
20
25
  volume?: number
21
- playbackRate: number
26
+ playbackRate?: number
22
27
  onload?: (source: HTMLMediaElement, options: AudioSourceOptions) => void
23
28
  }
24
29
 
@@ -59,18 +64,31 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
59
64
  * @param [options.playbackRate=1]
60
65
  */
61
66
  constructor (options: MixedAudioSourceOptions) {
67
+ if (!options.source) {
68
+ throw new Error('Property "source" is required in options')
69
+ }
70
+
62
71
  const onload = options.onload
63
72
  // Don't set as instance property
64
73
  delete options.onload
65
- super(options)
74
+
75
+ super({
76
+ ...options,
77
+
78
+ // Set a default duration so that the super constructor doesn't throw an
79
+ // error
80
+ duration: options.duration ?? 0
81
+ })
82
+
66
83
  this._initialized = false
67
84
  this._sourceStartTime = options.sourceStartTime || 0
68
85
  applyOptions(options, this)
69
86
 
70
87
  const load = () => {
71
88
  // TODO: && ?
72
- if ((options.duration || (this.source.duration - this.sourceStartTime)) < 0)
89
+ if ((options.duration || (this.source.duration - this.sourceStartTime)) < 0) {
73
90
  throw new Error('Invalid options.duration or options.sourceStartTime')
91
+ }
74
92
 
75
93
  this._unstretchedDuration = options.duration || (this.source.duration - this.sourceStartTime)
76
94
  this.duration = this._unstretchedDuration / (this.playbackRate)
@@ -78,31 +96,34 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
78
96
  // super()
79
97
  onload && onload.bind(this)(this.source, options)
80
98
  }
81
- if (this.source.readyState >= 2)
99
+ if (this.source.readyState >= 2) {
82
100
  // this frame's data is available now
83
101
  load()
84
- else
102
+ } else {
85
103
  // when this frame's data is available
86
104
  this.source.addEventListener('loadedmetadata', load)
105
+ }
87
106
 
88
107
  this.source.addEventListener('durationchange', () => {
89
108
  this.duration = options.duration || (this.source.duration - this.sourceStartTime)
90
109
  })
91
110
  }
92
111
 
112
+ async whenReady (): Promise<void> {
113
+ await super.whenReady()
114
+ if (this.source.readyState < 4) {
115
+ await new Promise(resolve => {
116
+ this.source.addEventListener('canplaythrough', resolve)
117
+ })
118
+ }
119
+ }
120
+
93
121
  attach (movie: Movie) {
94
122
  super.attach(movie)
95
123
 
96
- subscribe(movie, 'movie.seek', () => {
97
- if (this.currentTime < 0 || this.currentTime >= this.duration)
98
- return
99
-
100
- this.source.currentTime = this.currentTime + this.sourceStartTime
101
- })
102
-
103
124
  // TODO: on unattach?
104
- subscribe(movie, 'movie.audiodestinationupdate', event => {
105
- // Connect to new destination if immeidately connected to the existing
125
+ subscribe(movie, 'audiodestinationupdate', event => {
126
+ // Connect to new destination if immediately connected to the existing
106
127
  // destination.
107
128
  if (this._connectedToDestination) {
108
129
  this.audioNode.disconnect(movie.actx.destination)
@@ -123,8 +144,9 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
123
144
  const oldDisconnect = this._audioNode.disconnect.bind(this.audioNode)
124
145
  this._audioNode.disconnect = <T extends AudioDestinationNode>(destination?: T | number, output?: number, input?: number): AudioNode => {
125
146
  if (this._connectedToDestination &&
126
- destination === movie.actx.destination)
147
+ destination === movie.actx.destination) {
127
148
  this._connectedToDestination = false
149
+ }
128
150
 
129
151
  return oldDisconnect(destination, output, input)
130
152
  }
@@ -145,6 +167,12 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
145
167
  this.source.play()
146
168
  }
147
169
 
170
+ seek (time: number): void {
171
+ super.seek(time)
172
+
173
+ this.source.currentTime = this.currentTime + this.sourceStartTime
174
+ }
175
+
148
176
  render () {
149
177
  super.render()
150
178
  // TODO: implement Issue: Create built-in audio node to support built-in
@@ -155,6 +183,8 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
155
183
  }
156
184
 
157
185
  stop () {
186
+ super.stop()
187
+
158
188
  this.source.pause()
159
189
  }
160
190
 
@@ -171,8 +201,9 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
171
201
 
172
202
  set playbackRate (value) {
173
203
  this._playbackRate = value
174
- if (this._unstretchedDuration !== undefined)
204
+ if (this._unstretchedDuration !== undefined) {
175
205
  this.duration = this._unstretchedDuration / value
206
+ }
176
207
  }
177
208
 
178
209
  get startTime () {
@@ -202,7 +233,16 @@ function AudioSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Co
202
233
  return this._sourceStartTime
203
234
  }
204
235
 
205
- getDefaultOptions (): MixedAudioSourceOptions {
236
+ get ready (): boolean {
237
+ // Typescript doesn't support `super.ready` when targeting es5
238
+ const superReady = Object.getOwnPropertyDescriptor(superclass.prototype, 'ready').get.call(this)
239
+ return superReady && this.source.readyState === 4
240
+ }
241
+
242
+ /**
243
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
244
+ */
245
+ getDefaultOptions () {
206
246
  return {
207
247
  ...superclass.prototype.getDefaultOptions(),
208
248
  source: undefined, // required
@@ -3,22 +3,44 @@
3
3
  import { Base, BaseOptions } from './base'
4
4
  import { AudioSourceMixin, AudioSourceOptions } from './audio-source'
5
5
 
6
- type AudioOptions = AudioSourceOptions
6
+ interface AudioOptions extends Omit<AudioSourceOptions, 'source'> {
7
+ /**
8
+ * The raw html `<audio>` element
9
+ */
10
+ source: string | HTMLAudioElement
11
+ }
7
12
 
8
13
  /**
14
+ * Layer for an HTML audio element
9
15
  * @extends AudioSource
10
16
  */
11
17
  class Audio extends AudioSourceMixin<BaseOptions>(Base) {
18
+ /**
19
+ * The raw html `<audio>` element
20
+ */
21
+ source: HTMLAudioElement
22
+
12
23
  /**
13
24
  * Creates an audio layer
14
25
  */
15
26
  constructor (options: AudioOptions) {
27
+ if (typeof options.source === 'string') {
28
+ const audio = document.createElement('audio')
29
+ audio.src = options.source
30
+ options.source = audio
31
+ }
32
+
16
33
  super(options)
17
- if (this.duration === undefined)
34
+
35
+ if (this.duration === undefined) {
18
36
  this.duration = (this).source.duration - this.sourceStartTime
37
+ }
19
38
  }
20
39
 
21
- getDefaultOptions (): AudioOptions {
40
+ /**
41
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
42
+ */
43
+ getDefaultOptions () {
22
44
  return {
23
45
  ...Object.getPrototypeOf(this).getDefaultOptions(),
24
46
  /**
package/src/layer/base.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import EtroObject from '../object'
2
- import { publish, subscribe } from '../event'
3
- import { watchPublic, applyOptions } from '../util'
2
+ import { applyOptions } from '../util'
4
3
  import { Movie } from '../movie'
5
4
 
6
5
  interface BaseOptions {
@@ -30,6 +29,7 @@ class Base implements EtroObject {
30
29
  private _occurrenceCount: number
31
30
  private _startTime: number
32
31
  private _duration: number
32
+ private _currentTime: number
33
33
  private _movie: Movie
34
34
 
35
35
  /**
@@ -42,67 +42,82 @@ class Base implements EtroObject {
42
42
  * movie's timeline
43
43
  */
44
44
  constructor (options: BaseOptions) {
45
+ if (options.duration === null || options.duration === undefined) {
46
+ throw new Error('Property "duration" is required in BaseOptions')
47
+ }
48
+
49
+ if (options.startTime === null || options.startTime === undefined) {
50
+ throw new Error('Property "startTime" is required in BaseOptions')
51
+ }
52
+
45
53
  // Set startTime and duration properties manually, because they are
46
54
  // readonly. applyOptions ignores readonly properties.
47
55
  this._startTime = options.startTime
48
56
  this._duration = options.duration
49
57
 
50
- // Proxy that will be returned by constructor (for sending 'modified'
51
- // events).
52
- const newThis = watchPublic(this) as Base
53
- // Don't send updates when initializing, so use this instead of newThis
54
58
  applyOptions(options, this)
55
59
 
56
60
  // Whether this layer is currently being rendered
57
61
  this.active = false
58
62
  this.enabled = true
59
63
 
60
- this._occurrenceCount = 0 // no occurances in parent
64
+ this._occurrenceCount = 0 // no occurrences in parent
61
65
  this._movie = null
62
-
63
- // Propogate up to target
64
- subscribe(newThis, 'layer.change', event => {
65
- const typeOfChange = event.type.substring(event.type.lastIndexOf('.') + 1)
66
- const type = `movie.change.layer.${typeOfChange}`
67
- publish(newThis._movie, type, { ...event, target: newThis._movie, type })
68
- })
69
-
70
- return newThis
71
66
  }
72
67
 
68
+ /**
69
+ * Wait until this layer is ready to render
70
+ */
71
+ async whenReady (): Promise<void> {} // eslint-disable-line @typescript-eslint/no-empty-function
72
+
73
73
  /**
74
74
  * Attaches this layer to `movie` if not already attached.
75
75
  * @ignore
76
76
  */
77
77
  tryAttach (movie: Movie): void {
78
- if (this._occurrenceCount === 0)
78
+ if (this._occurrenceCount === 0) {
79
79
  this.attach(movie)
80
+ }
80
81
 
81
82
  this._occurrenceCount++
82
83
  }
83
84
 
85
+ /**
86
+ * Attaches this layer to `movie`
87
+ *
88
+ * Called when the layer is added to a movie's `layers` array.
89
+ *
90
+ * @param movie The movie to attach to
91
+ */
84
92
  attach (movie: Movie): void {
85
93
  this._movie = movie
86
94
  }
87
95
 
88
96
  /**
89
- * Dettaches this layer from its movie if the number of times `tryDetach` has
97
+ * Detaches this layer from its movie if the number of times `tryDetach` has
90
98
  * been called (including this call) equals the number of times `tryAttach`
91
99
  * has been called.
92
100
  *
93
101
  * @ignore
94
102
  */
95
103
  tryDetach (): void {
96
- if (this.movie === null)
104
+ if (this.movie === null) {
97
105
  throw new Error('No movie to detach from')
106
+ }
98
107
 
99
108
  this._occurrenceCount--
100
109
  // If this layer occurs in another place in a `layers` array, do not unset
101
110
  // _movie. (For calling `unshift` on the `layers` proxy)
102
- if (this._occurrenceCount === 0)
111
+ if (this._occurrenceCount === 0) {
103
112
  this.detach()
113
+ }
104
114
  }
105
115
 
116
+ /**
117
+ * Detaches this layer from its movie
118
+ *
119
+ * Called when the layer is removed from a movie's `layers` array.
120
+ */
106
121
  detach (): void {
107
122
  this._movie = null
108
123
  }
@@ -112,15 +127,43 @@ class Base implements EtroObject {
112
127
  */
113
128
  start (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
114
129
 
130
+ /**
131
+ * Update {@link currentTime} when seeking
132
+ *
133
+ * This method is called when the movie seeks to a new time at the request of
134
+ * the user. {@link progress} is called when the movie's `currentTime` is
135
+ * updated due to playback.
136
+ *
137
+ * @param time - The new time in the layer
138
+ */
139
+ seek (time: number): void {
140
+ this._currentTime = time
141
+ }
142
+
143
+ /**
144
+ * Update {@link currentTime} due to playback
145
+ *
146
+ * This method is called when the movie's `currentTime` is updated due to
147
+ * playback. {@link seek} is called when the movie seeks to a new time at the
148
+ * request of the user.
149
+ *
150
+ * @param time - The new time in the layer
151
+ */
152
+ progress (time: number): void {
153
+ this._currentTime = time
154
+ }
155
+
115
156
  /**
116
157
  * Called when the movie renders and the layer is active
117
158
  */
118
159
  render (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
119
160
 
120
161
  /**
121
- * Called when the layer is deactivated
162
+ * Called when the layer is deactivated
122
163
  */
123
- stop (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
164
+ stop (): void {
165
+ this._currentTime = undefined
166
+ }
124
167
 
125
168
  // TODO: is this needed?
126
169
  get parent (): Movie {
@@ -128,6 +171,7 @@ class Base implements EtroObject {
128
171
  }
129
172
 
130
173
  /**
174
+ * The time in the movie at which this layer starts (in seconds)
131
175
  */
132
176
  get startTime (): number {
133
177
  return this._startTime
@@ -138,15 +182,14 @@ class Base implements EtroObject {
138
182
  }
139
183
 
140
184
  /**
141
- * The current time of the movie relative to this layer
185
+ * The current time of the movie relative to this layer (in seconds)
142
186
  */
143
187
  get currentTime (): number {
144
- return this._movie
145
- ? this._movie.currentTime - this.startTime
146
- : undefined
188
+ return this._currentTime
147
189
  }
148
190
 
149
191
  /**
192
+ * The duration of this layer (in seconds)
150
193
  */
151
194
  get duration (): number {
152
195
  return this._duration
@@ -156,10 +199,20 @@ class Base implements EtroObject {
156
199
  this._duration = val
157
200
  }
158
201
 
202
+ /**
203
+ * `true` if this layer is ready to be rendered, `false` otherwise
204
+ */
205
+ get ready (): boolean {
206
+ return true
207
+ }
208
+
159
209
  get movie (): Movie {
160
210
  return this._movie
161
211
  }
162
212
 
213
+ /**
214
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
215
+ */
163
216
  getDefaultOptions (): BaseOptions {
164
217
  return {
165
218
  startTime: undefined, // required
@@ -1,8 +1,32 @@
1
1
  import { Visual } from './visual'
2
2
  import { VisualSourceMixin, VisualSourceOptions } from './visual-source'
3
3
 
4
- type ImageOptions = VisualSourceOptions
4
+ interface ImageOptions extends Omit<VisualSourceOptions, 'source'> {
5
+ /**
6
+ * The raw html `<img>` element
7
+ */
8
+ source: string | HTMLImageElement
9
+ }
5
10
 
6
- class Image extends VisualSourceMixin(Visual) {}
11
+ /**
12
+ * Layer for an HTML image element
13
+ * @extends VisualSource
14
+ */
15
+ class Image extends VisualSourceMixin(Visual) {
16
+ /**
17
+ * The raw html `<img>` element
18
+ */
19
+ source: HTMLImageElement
20
+
21
+ constructor (options: ImageOptions) {
22
+ if (typeof (options.source) === 'string') {
23
+ const img = document.createElement('img')
24
+ img.src = options.source
25
+ options.source = img
26
+ }
27
+
28
+ super(options as VisualSourceOptions)
29
+ }
30
+ }
7
31
 
8
32
  export { Image, ImageOptions }
package/src/layer/text.ts CHANGED
@@ -61,6 +61,10 @@ class Text extends Visual {
61
61
  // TODO: is textX necessary? it seems inconsistent, because you can't define
62
62
  // width/height directly for a text layer
63
63
  constructor (options: TextOptions) {
64
+ if (!options.text) {
65
+ throw new Error('Property "text" is required in TextOptions')
66
+ }
67
+
64
68
  // Default to no (transparent) background
65
69
  super({ background: null, ...options })
66
70
  applyOptions(options, this)
@@ -118,6 +122,9 @@ class Text extends Visual {
118
122
  return metrics;
119
123
  } */
120
124
 
125
+ /**
126
+ * @deprecated See {@link https://github.com/etro-js/etro/issues/131}
127
+ */
121
128
  getDefaultOptions (): TextOptions {
122
129
  return {
123
130
  ...Visual.prototype.getDefaultOptions(),