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.
- package/CHANGELOG.md +43 -0
- package/CONTRIBUTING.md +23 -20
- package/README.md +3 -2
- package/dist/custom-array.d.ts +10 -0
- package/dist/effect/base.d.ts +10 -1
- package/dist/effect/shader.d.ts +11 -1
- package/dist/effect/stack.d.ts +6 -2
- package/dist/etro-cjs.js +1156 -575
- package/dist/etro-iife.js +1156 -575
- package/dist/event.d.ts +10 -5
- package/dist/layer/audio-source.d.ts +9 -4
- package/dist/layer/audio.d.ts +15 -2
- package/dist/layer/base.d.ts +49 -3
- package/dist/layer/image.d.ts +15 -1
- package/dist/layer/text.d.ts +3 -0
- package/dist/layer/video.d.ts +13 -1
- package/dist/layer/visual.d.ts +6 -2
- package/dist/movie/effects.d.ts +6 -0
- package/dist/movie/index.d.ts +1 -0
- package/dist/movie/layers.d.ts +6 -0
- package/dist/movie/movie.d.ts +260 -0
- package/dist/object.d.ts +9 -6
- package/dist/util.d.ts +4 -10
- package/eslint.conf.js +2 -2
- package/karma.conf.js +4 -7
- package/package.json +8 -7
- package/src/custom-array.ts +43 -0
- package/src/effect/base.ts +23 -22
- package/src/effect/gaussian-blur.ts +11 -6
- package/src/effect/pixelate.ts +3 -3
- package/src/effect/shader.ts +33 -27
- package/src/effect/stack.ts +43 -30
- package/src/effect/transform.ts +14 -7
- package/src/event.ts +111 -21
- package/src/layer/audio-source.ts +60 -20
- package/src/layer/audio.ts +25 -3
- package/src/layer/base.ts +79 -26
- package/src/layer/image.ts +26 -2
- package/src/layer/text.ts +7 -0
- package/src/layer/video.ts +31 -4
- package/src/layer/visual-source.ts +43 -1
- package/src/layer/visual.ts +50 -28
- package/src/movie/effects.ts +26 -0
- package/src/movie/index.ts +1 -0
- package/src/movie/layers.ts +26 -0
- package/src/movie/movie.ts +855 -0
- package/src/object.ts +9 -6
- package/src/util.ts +68 -89
- package/dist/movie.d.ts +0 -201
- package/src/movie.ts +0 -744
- /package/scripts/{gen-effect-samples.html → effect/gen-effect-samples.html} +0 -0
- /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 -
|
|
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 (
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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 -
|
|
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
|
-
*
|
|
139
|
+
* Publish an event to all listeners without checking if it is deprecated.
|
|
77
140
|
*
|
|
78
|
-
* @param target
|
|
79
|
-
* @param type
|
|
80
|
-
*
|
|
81
|
-
* @
|
|
141
|
+
* @param target
|
|
142
|
+
* @param type
|
|
143
|
+
* @param event
|
|
144
|
+
* @returns
|
|
82
145
|
*/
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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, '
|
|
105
|
-
// Connect to new destination if
|
|
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
|
-
|
|
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
|
package/src/layer/audio.ts
CHANGED
|
@@ -3,22 +3,44 @@
|
|
|
3
3
|
import { Base, BaseOptions } from './base'
|
|
4
4
|
import { AudioSourceMixin, AudioSourceOptions } from './audio-source'
|
|
5
5
|
|
|
6
|
-
|
|
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
|
-
|
|
34
|
+
|
|
35
|
+
if (this.duration === undefined) {
|
|
18
36
|
this.duration = (this).source.duration - this.sourceStartTime
|
|
37
|
+
}
|
|
19
38
|
}
|
|
20
39
|
|
|
21
|
-
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
162
|
+
* Called when the layer is deactivated
|
|
122
163
|
*/
|
|
123
|
-
stop (): void {
|
|
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.
|
|
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
|
package/src/layer/image.ts
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import { Visual } from './visual'
|
|
2
2
|
import { VisualSourceMixin, VisualSourceOptions } from './visual-source'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
interface ImageOptions extends Omit<VisualSourceOptions, 'source'> {
|
|
5
|
+
/**
|
|
6
|
+
* The raw html `<img>` element
|
|
7
|
+
*/
|
|
8
|
+
source: string | HTMLImageElement
|
|
9
|
+
}
|
|
5
10
|
|
|
6
|
-
|
|
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(),
|