etro 0.9.0 → 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 +50 -0
- package/CONTRIBUTING.md +25 -34
- package/README.md +9 -17
- 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 +1182 -592
- package/dist/etro-iife.js +1182 -592
- 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 +6 -3
- package/dist/layer/video.d.ts +13 -1
- package/dist/layer/visual-source.d.ts +18 -3
- package/dist/layer/visual.d.ts +11 -7
- 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 -2
- package/dist/util.d.ts +4 -10
- package/eslint.conf.js +4 -2
- package/eslint.test-conf.js +1 -2
- package/karma.conf.js +10 -14
- package/package.json +23 -22
- package/scripts/{gen-effect-samples.html → effect/gen-effect-samples.html} +24 -0
- package/scripts/{save-effect-samples.js → effect/save-effect-samples.js} +1 -1
- 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 +16 -9
- 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 -25
- package/src/layer/image.ts +26 -2
- package/src/layer/text.ts +11 -4
- package/src/layer/video.ts +31 -4
- package/src/layer/visual-source.ts +70 -8
- package/src/layer/visual.ts +57 -35
- 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 -2
- package/src/util.ts +68 -89
- package/tsconfig.json +3 -1
- package/dist/movie.d.ts +0 -201
- package/examples/application/readme-screenshot.html +0 -85
- package/examples/application/video-player.html +0 -130
- package/examples/application/webcam.html +0 -28
- package/examples/introduction/audio.html +0 -64
- package/examples/introduction/effects.html +0 -79
- package/examples/introduction/export.html +0 -83
- package/examples/introduction/functions.html +0 -37
- package/examples/introduction/hello-world1.html +0 -37
- package/examples/introduction/hello-world2.html +0 -32
- package/examples/introduction/keyframes.html +0 -79
- package/examples/introduction/media.html +0 -63
- package/examples/introduction/text.html +0 -31
- package/private-todo.txt +0 -70
- package/src/movie.ts +0 -742
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,14 +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
|
-
: undefined
|
|
188
|
+
return this._currentTime
|
|
146
189
|
}
|
|
147
190
|
|
|
148
191
|
/**
|
|
192
|
+
* The duration of this layer (in seconds)
|
|
149
193
|
*/
|
|
150
194
|
get duration (): number {
|
|
151
195
|
return this._duration
|
|
@@ -155,10 +199,20 @@ class Base implements EtroObject {
|
|
|
155
199
|
this._duration = val
|
|
156
200
|
}
|
|
157
201
|
|
|
202
|
+
/**
|
|
203
|
+
* `true` if this layer is ready to be rendered, `false` otherwise
|
|
204
|
+
*/
|
|
205
|
+
get ready (): boolean {
|
|
206
|
+
return true
|
|
207
|
+
}
|
|
208
|
+
|
|
158
209
|
get movie (): Movie {
|
|
159
210
|
return this._movie
|
|
160
211
|
}
|
|
161
212
|
|
|
213
|
+
/**
|
|
214
|
+
* @deprecated See {@link https://github.com/etro-js/etro/issues/131}
|
|
215
|
+
*/
|
|
162
216
|
getDefaultOptions (): BaseOptions {
|
|
163
217
|
return {
|
|
164
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
|
@@ -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<
|
|
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<
|
|
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 */
|
|
@@ -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,13 +122,16 @@ 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(),
|
|
124
131
|
background: null,
|
|
125
132
|
text: undefined, // required
|
|
126
133
|
font: '10px sans-serif',
|
|
127
|
-
color: '#fff',
|
|
134
|
+
color: parseColor('#fff'),
|
|
128
135
|
textX: 0,
|
|
129
136
|
textY: 0,
|
|
130
137
|
maxWidth: null,
|
package/src/layer/video.ts
CHANGED
|
@@ -2,14 +2,41 @@ import { Visual } from './visual'
|
|
|
2
2
|
import { VisualSourceOptions, VisualSourceMixin } from './visual-source'
|
|
3
3
|
import { AudioSourceOptions, AudioSourceMixin } from './audio-source'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
interface VideoOptions extends Omit<AudioSourceOptions & VisualSourceOptions, 'duration'|'source'> {
|
|
6
|
+
duration?: number
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The raw html `<video>` element
|
|
10
|
+
*/
|
|
11
|
+
source: string | HTMLVideoElement
|
|
12
|
+
}
|
|
6
13
|
|
|
7
|
-
// Use mixins instead of `extend`ing two classes (which isn't supported by
|
|
8
|
-
// JavaScript).
|
|
9
14
|
/**
|
|
15
|
+
* Layer for an HTML video element
|
|
10
16
|
* @extends AudioSource
|
|
11
17
|
* @extends VisualSource
|
|
12
18
|
*/
|
|
13
|
-
class Video extends AudioSourceMixin(VisualSourceMixin(Visual)) {
|
|
19
|
+
class Video extends AudioSourceMixin(VisualSourceMixin(Visual)) {
|
|
20
|
+
/**
|
|
21
|
+
* The raw html `<video>` element
|
|
22
|
+
*/
|
|
23
|
+
source: HTMLVideoElement
|
|
24
|
+
|
|
25
|
+
constructor (options: VideoOptions) {
|
|
26
|
+
if (typeof (options.source) === 'string') {
|
|
27
|
+
const video = document.createElement('video')
|
|
28
|
+
video.src = options.source
|
|
29
|
+
options.source = video
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
super({
|
|
33
|
+
...options,
|
|
34
|
+
|
|
35
|
+
// Set a default duration so that the super constructor doesn't throw an
|
|
36
|
+
// error
|
|
37
|
+
duration: options.duration ?? 0
|
|
38
|
+
} as (AudioSourceOptions & VisualSourceOptions))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
14
41
|
|
|
15
42
|
export { Video, VideoOptions }
|
|
@@ -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
|
|
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
|
|
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 {
|
|
@@ -59,10 +75,40 @@ function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: C
|
|
|
59
75
|
destHeight: Dynamic<number>
|
|
60
76
|
|
|
61
77
|
constructor (options: MixedVisualSourceOptions) {
|
|
78
|
+
if (!options.source) {
|
|
79
|
+
throw new Error('Property "source" is required in options')
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
super(options)
|
|
63
83
|
applyOptions(options, this)
|
|
64
84
|
}
|
|
65
85
|
|
|
86
|
+
async whenReady (): Promise<void> {
|
|
87
|
+
await super.whenReady()
|
|
88
|
+
|
|
89
|
+
await new Promise<void>(resolve => {
|
|
90
|
+
if (this.source instanceof HTMLImageElement) {
|
|
91
|
+
// The source is an image; wait for it to load
|
|
92
|
+
if (this.source.complete) {
|
|
93
|
+
resolve()
|
|
94
|
+
} else {
|
|
95
|
+
this.source.addEventListener('load', () => {
|
|
96
|
+
resolve()
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// The source is a video; wait for the first frame to load
|
|
101
|
+
if (this.source.readyState === 4) {
|
|
102
|
+
resolve()
|
|
103
|
+
} else {
|
|
104
|
+
this.source.addEventListener('canplaythrough', () => {
|
|
105
|
+
resolve()
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
66
112
|
doRender () {
|
|
67
113
|
// Clear/fill background
|
|
68
114
|
super.doRender()
|
|
@@ -70,7 +116,7 @@ function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: C
|
|
|
70
116
|
/*
|
|
71
117
|
* Source dimensions crop the image. Dest dimensions set the size that
|
|
72
118
|
* the image will be rendered at *on the layer*. Note that this is
|
|
73
|
-
* different
|
|
119
|
+
* different from the layer dimensions (`this.width` and `this.height`).
|
|
74
120
|
* The main reason this distinction exists is so that an image layer can
|
|
75
121
|
* be rotated without being cropped (see iss #46).
|
|
76
122
|
*/
|
|
@@ -84,6 +130,18 @@ function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: C
|
|
|
84
130
|
)
|
|
85
131
|
}
|
|
86
132
|
|
|
133
|
+
get ready (): boolean {
|
|
134
|
+
// Typescript doesn't support `super.ready` when targeting es5
|
|
135
|
+
const superReady = Object.getOwnPropertyDescriptor(superclass.prototype, 'ready').get.call(this)
|
|
136
|
+
const sourceReady = this.source instanceof HTMLImageElement
|
|
137
|
+
? this.source.complete
|
|
138
|
+
: this.source.readyState === 4
|
|
139
|
+
return superReady && sourceReady
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @deprecated See {@link https://github.com/etro-js/etro/issues/131}
|
|
144
|
+
*/
|
|
87
145
|
getDefaultOptions (): MixedVisualSourceOptions {
|
|
88
146
|
return {
|
|
89
147
|
...superclass.prototype.getDefaultOptions(),
|
|
@@ -125,22 +183,26 @@ function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: C
|
|
|
125
183
|
// instead. (TODO: fact check)
|
|
126
184
|
/* eslint-disable eqeqeq */
|
|
127
185
|
return destWidth != undefined
|
|
128
|
-
? destWidth
|
|
186
|
+
? destWidth
|
|
187
|
+
: val(this, 'sourceWidth', this.currentTime)
|
|
129
188
|
},
|
|
130
189
|
destHeight: function (destHeight) {
|
|
131
190
|
/* eslint-disable eqeqeq */
|
|
132
191
|
return destHeight != undefined
|
|
133
|
-
? destHeight
|
|
192
|
+
? destHeight
|
|
193
|
+
: val(this, 'sourceHeight', this.currentTime)
|
|
134
194
|
},
|
|
135
195
|
width: function (width) {
|
|
136
196
|
/* eslint-disable eqeqeq */
|
|
137
197
|
return width != undefined
|
|
138
|
-
? width
|
|
198
|
+
? width
|
|
199
|
+
: val(this, 'destWidth', this.currentTime)
|
|
139
200
|
},
|
|
140
201
|
height: function (height) {
|
|
141
202
|
/* eslint-disable eqeqeq */
|
|
142
203
|
return height != undefined
|
|
143
|
-
? height
|
|
204
|
+
? height
|
|
205
|
+
: val(this, 'destHeight', this.currentTime)
|
|
144
206
|
}
|
|
145
207
|
}
|
|
146
208
|
|
package/src/layer/visual.ts
CHANGED
|
@@ -1,15 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CustomArray, CustomArrayListener } from '../custom-array'
|
|
2
|
+
import { Dynamic, val, applyOptions, Color } from '../util'
|
|
2
3
|
import { Base, BaseOptions } from './base'
|
|
3
4
|
import { Visual as VisualEffect } from '../effect/visual'
|
|
4
5
|
|
|
6
|
+
// eslint-disable-next-line no-use-before-define
|
|
7
|
+
class VisualEffectsListener extends CustomArrayListener<VisualEffect> {
|
|
8
|
+
// eslint-disable-next-line no-use-before-define
|
|
9
|
+
private _layer: Visual
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line no-use-before-define
|
|
12
|
+
constructor (layer: Visual) {
|
|
13
|
+
super()
|
|
14
|
+
this._layer = layer
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onAdd (effect: VisualEffect) {
|
|
18
|
+
effect.tryAttach(this._layer)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
onRemove (effect: VisualEffect) {
|
|
22
|
+
effect.tryDetach()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class VisualEffects extends CustomArray<VisualEffect> {
|
|
27
|
+
// eslint-disable-next-line no-use-before-define
|
|
28
|
+
constructor (target: VisualEffect[], layer: Visual) {
|
|
29
|
+
super(target, new VisualEffectsListener(layer))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
5
33
|
interface VisualOptions extends BaseOptions {
|
|
6
34
|
x?: Dynamic<number>
|
|
7
35
|
y?: Dynamic<number>
|
|
8
36
|
width?: Dynamic<number>
|
|
9
37
|
height?: Dynamic<number>
|
|
10
|
-
background?: Dynamic<
|
|
38
|
+
background?: Dynamic<Color>
|
|
11
39
|
border?: Dynamic<{
|
|
12
|
-
color:
|
|
40
|
+
color: Color
|
|
13
41
|
thickness?: number
|
|
14
42
|
}>
|
|
15
43
|
|
|
@@ -22,9 +50,9 @@ class Visual extends Base {
|
|
|
22
50
|
y: Dynamic<number>
|
|
23
51
|
width: Dynamic<number>
|
|
24
52
|
height: Dynamic<number>
|
|
25
|
-
background: Dynamic<
|
|
53
|
+
background: Dynamic<Color>
|
|
26
54
|
border: Dynamic<{
|
|
27
|
-
color:
|
|
55
|
+
color: Color
|
|
28
56
|
thickness: number
|
|
29
57
|
}>
|
|
30
58
|
|
|
@@ -43,40 +71,22 @@ class Visual extends Base {
|
|
|
43
71
|
// readonly because it's a proxy
|
|
44
72
|
readonly effects: VisualEffect[]
|
|
45
73
|
|
|
46
|
-
private _effectsBack: VisualEffect[]
|
|
47
|
-
|
|
48
74
|
/**
|
|
49
75
|
* Creates a visual layer
|
|
50
76
|
*/
|
|
51
77
|
constructor (options: VisualOptions) {
|
|
52
78
|
super(options)
|
|
53
|
-
|
|
54
|
-
// be extraneous options.
|
|
79
|
+
|
|
55
80
|
applyOptions(options, this)
|
|
56
81
|
|
|
57
82
|
this.canvas = document.createElement('canvas')
|
|
58
83
|
this.cctx = this.canvas.getContext('2d')
|
|
84
|
+
this.effects = new VisualEffects([], this)
|
|
85
|
+
}
|
|
59
86
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const value = target[property]
|
|
64
|
-
value.detach()
|
|
65
|
-
delete target[property]
|
|
66
|
-
return true
|
|
67
|
-
},
|
|
68
|
-
set: (target, property, value) => {
|
|
69
|
-
if (!isNaN(Number(property))) {
|
|
70
|
-
// The property is a number (index)
|
|
71
|
-
if (target[property])
|
|
72
|
-
target[property].detach()
|
|
73
|
-
|
|
74
|
-
value.attach(this)
|
|
75
|
-
}
|
|
76
|
-
target[property] = value
|
|
77
|
-
return true
|
|
78
|
-
}
|
|
79
|
-
})
|
|
87
|
+
async whenReady (): Promise<void> {
|
|
88
|
+
await super.whenReady()
|
|
89
|
+
await Promise.all(this.effects.map(effect => effect.whenReady()))
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
/**
|
|
@@ -86,8 +96,9 @@ class Visual extends Base {
|
|
|
86
96
|
// Prevent empty canvas errors if the width or height is 0
|
|
87
97
|
const width = val(this, 'width', this.currentTime)
|
|
88
98
|
const height = val(this, 'height', this.currentTime)
|
|
89
|
-
if (width === 0 || height === 0)
|
|
99
|
+
if (width === 0 || height === 0) {
|
|
90
100
|
return
|
|
101
|
+
}
|
|
91
102
|
|
|
92
103
|
this.beginRender()
|
|
93
104
|
this.doRender()
|
|
@@ -122,8 +133,9 @@ class Visual extends Base {
|
|
|
122
133
|
endRender (): void {
|
|
123
134
|
const w = val(this, 'width', this.currentTime) || val(this.movie, 'width', this.movie.currentTime)
|
|
124
135
|
const h = val(this, 'height', this.currentTime) || val(this.movie, 'height', this.movie.currentTime)
|
|
125
|
-
if (w * h > 0)
|
|
136
|
+
if (w * h > 0) {
|
|
126
137
|
this._applyEffects()
|
|
138
|
+
}
|
|
127
139
|
|
|
128
140
|
// else InvalidStateError for drawing zero-area image in some effects, right?
|
|
129
141
|
}
|
|
@@ -131,14 +143,15 @@ class Visual extends Base {
|
|
|
131
143
|
_applyEffects (): void {
|
|
132
144
|
for (let i = 0; i < this.effects.length; i++) {
|
|
133
145
|
const effect = this.effects[i]
|
|
134
|
-
if (effect && effect.enabled)
|
|
146
|
+
if (effect && effect.enabled) {
|
|
135
147
|
// Pass relative time
|
|
136
148
|
effect.apply(this, this.movie.currentTime - this.startTime)
|
|
149
|
+
}
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
/**
|
|
141
|
-
*
|
|
154
|
+
* Convenience method for <code>effects.push()</code>
|
|
142
155
|
* @param effect
|
|
143
156
|
* @return the layer (for chaining)
|
|
144
157
|
*/
|
|
@@ -146,6 +159,15 @@ class Visual extends Base {
|
|
|
146
159
|
this.effects.push(effect); return this
|
|
147
160
|
}
|
|
148
161
|
|
|
162
|
+
get ready (): boolean {
|
|
163
|
+
// Typescript doesn't support `super.ready` when targeting es5
|
|
164
|
+
const superReady = Object.getOwnPropertyDescriptor(Base.prototype, 'ready').get.call(this)
|
|
165
|
+
return superReady && this.effects.every(effect => effect.ready)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @deprecated See {@link https://github.com/etro-js/etro/issues/131}
|
|
170
|
+
*/
|
|
149
171
|
getDefaultOptions (): VisualOptions {
|
|
150
172
|
return {
|
|
151
173
|
...Base.prototype.getDefaultOptions(),
|
|
@@ -169,13 +191,13 @@ class Visual extends Base {
|
|
|
169
191
|
height: null,
|
|
170
192
|
/**
|
|
171
193
|
* @name module:layer.Visual#background
|
|
172
|
-
* @desc The
|
|
194
|
+
* @desc The color code for the background, or <code>null</code> for
|
|
173
195
|
* transparency
|
|
174
196
|
*/
|
|
175
197
|
background: null,
|
|
176
198
|
/**
|
|
177
199
|
* @name module:layer.Visual#border
|
|
178
|
-
* @desc The
|
|
200
|
+
* @desc The border style, or <code>null</code> for no border
|
|
179
201
|
*/
|
|
180
202
|
border: null,
|
|
181
203
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CustomArray, CustomArrayListener } from '../custom-array'
|
|
2
|
+
import { Visual as VisualEffect } from '../effect/index'
|
|
3
|
+
import { Movie } from './movie'
|
|
4
|
+
|
|
5
|
+
class MovieEffectsListener extends CustomArrayListener<VisualEffect> {
|
|
6
|
+
private _movie: Movie
|
|
7
|
+
|
|
8
|
+
constructor (movie: Movie) {
|
|
9
|
+
super()
|
|
10
|
+
this._movie = movie
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
onAdd (effect: VisualEffect) {
|
|
14
|
+
effect.tryAttach(this._movie)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onRemove (effect: VisualEffect) {
|
|
18
|
+
effect.tryDetach()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class MovieEffects extends CustomArray<VisualEffect> {
|
|
23
|
+
constructor (target: VisualEffect[], movie: Movie) {
|
|
24
|
+
super(target, new MovieEffectsListener(movie))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './movie'
|