etro 0.6.0 → 0.8.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.
- package/.github/workflows/nodejs.yml +4 -2
- package/CHANGELOG.md +85 -4
- package/CODE_OF_CONDUCT.md +5 -5
- package/CONTRIBUTING.md +33 -79
- package/README.md +93 -26
- package/dist/effect/base.d.ts +51 -0
- package/dist/effect/brightness.d.ts +16 -0
- package/dist/effect/channels.d.ts +23 -0
- package/dist/effect/chroma-key.d.ts +23 -0
- package/dist/effect/contrast.d.ts +15 -0
- package/dist/effect/elliptical-mask.d.ts +31 -0
- package/dist/effect/gaussian-blur.d.ts +60 -0
- package/dist/effect/grayscale.d.ts +7 -0
- package/dist/effect/index.d.ts +15 -0
- package/dist/effect/pixelate.d.ts +18 -0
- package/dist/effect/shader.d.ts +99 -0
- package/dist/effect/stack.d.ts +23 -0
- package/dist/effect/transform.d.ts +73 -0
- package/dist/etro-cjs.js +9387 -0
- package/dist/etro-iife.js +9390 -0
- package/dist/etro.d.ts +7 -0
- package/dist/event.d.ts +35 -0
- package/dist/index.d.ts +6 -0
- package/dist/layer/audio-source.d.ts +24 -0
- package/dist/layer/audio.d.ts +14 -0
- package/dist/layer/base.d.ts +82 -0
- package/dist/layer/image.d.ts +6 -0
- package/dist/layer/index.d.ts +11 -0
- package/dist/layer/text.d.ts +60 -0
- package/dist/layer/video.d.ts +11 -0
- package/dist/layer/visual-source.d.ts +32 -0
- package/dist/layer/visual.d.ts +58 -0
- package/dist/movie.d.ts +192 -0
- package/dist/object.d.ts +12 -0
- package/dist/util.d.ts +125 -0
- package/eslint.conf.js +2 -9
- package/eslint.example-conf.js +9 -0
- package/eslint.test-conf.js +1 -0
- package/eslint.typescript-conf.js +5 -0
- package/examples/application/readme-screenshot.html +16 -17
- package/examples/application/video-player.html +10 -11
- package/examples/application/webcam.html +6 -6
- package/examples/introduction/audio.html +30 -18
- package/examples/introduction/effects.html +37 -14
- package/examples/introduction/export.html +40 -27
- package/examples/introduction/functions.html +6 -4
- package/examples/introduction/hello-world1.html +9 -5
- package/examples/introduction/hello-world2.html +5 -5
- package/examples/introduction/keyframes.html +35 -23
- package/examples/introduction/media.html +26 -18
- package/examples/introduction/text.html +9 -5
- package/karma.conf.js +6 -4
- package/package.json +34 -13
- package/rollup.config.js +19 -3
- package/scripts/gen-effect-samples.html +27 -26
- package/scripts/save-effect-samples.js +14 -15
- package/src/effect/base.ts +115 -0
- package/src/effect/brightness.ts +43 -0
- package/src/effect/channels.ts +50 -0
- package/src/effect/chroma-key.ts +82 -0
- package/src/effect/contrast.ts +42 -0
- package/src/effect/elliptical-mask.ts +75 -0
- package/src/effect/gaussian-blur.ts +232 -0
- package/src/effect/grayscale.ts +34 -0
- package/src/effect/index.ts +22 -0
- package/src/effect/pixelate.ts +58 -0
- package/src/effect/shader.ts +557 -0
- package/src/effect/stack.ts +77 -0
- package/src/effect/transform.ts +193 -0
- package/src/etro.ts +26 -0
- package/src/event.ts +112 -0
- package/src/index.ts +8 -0
- package/src/layer/audio-source.ts +219 -0
- package/src/layer/audio.ts +34 -0
- package/src/layer/base.ts +175 -0
- package/src/layer/image.ts +8 -0
- package/src/layer/index.ts +13 -0
- package/src/layer/text.ts +138 -0
- package/src/layer/video.ts +15 -0
- package/src/layer/visual-source.ts +150 -0
- package/src/layer/visual.ts +197 -0
- package/src/movie.ts +701 -0
- package/src/object.ts +14 -0
- package/src/util.ts +466 -0
- package/tsconfig.json +8 -0
- package/dist/etro.js +0 -3397
- package/docs/effect.js.html +0 -1215
- package/docs/event.js.html +0 -145
- package/docs/index.html +0 -81
- package/docs/index.js.html +0 -92
- package/docs/layer.js.html +0 -888
- package/docs/module-effect-GaussianBlurComponent.html +0 -345
- package/docs/module-effect.Brightness.html +0 -339
- package/docs/module-effect.Channels.html +0 -319
- package/docs/module-effect.ChromaKey.html +0 -611
- package/docs/module-effect.Contrast.html +0 -339
- package/docs/module-effect.EllipticalMask.html +0 -200
- package/docs/module-effect.GaussianBlur.html +0 -202
- package/docs/module-effect.GaussianBlurHorizontal.html +0 -242
- package/docs/module-effect.GaussianBlurVertical.html +0 -242
- package/docs/module-effect.Pixelate.html +0 -330
- package/docs/module-effect.Shader.html +0 -1227
- package/docs/module-effect.Stack.html +0 -406
- package/docs/module-effect.Transform.Matrix.html +0 -193
- package/docs/module-effect.Transform.html +0 -1174
- package/docs/module-effect.html +0 -148
- package/docs/module-event.html +0 -473
- package/docs/module-index.html +0 -186
- package/docs/module-layer-Media.html +0 -1116
- package/docs/module-layer-MediaMixin.html +0 -164
- package/docs/module-layer.Audio.html +0 -1188
- package/docs/module-layer.Base.html +0 -629
- package/docs/module-layer.Image.html +0 -1421
- package/docs/module-layer.Text.html +0 -1731
- package/docs/module-layer.Video.html +0 -1938
- package/docs/module-layer.Visual.html +0 -1698
- package/docs/module-layer.html +0 -137
- package/docs/module-movie.html +0 -3118
- package/docs/module-util.Color.html +0 -702
- package/docs/module-util.Font.html +0 -395
- package/docs/module-util.html +0 -845
- package/docs/movie.js.html +0 -689
- package/docs/scripts/collapse.js +0 -20
- package/docs/scripts/linenumber.js +0 -25
- package/docs/scripts/nav.js +0 -12
- package/docs/scripts/polyfill.js +0 -4
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
- package/docs/scripts/prettify/lang-css.js +0 -2
- package/docs/scripts/prettify/prettify.js +0 -28
- package/docs/scripts/search.js +0 -83
- package/docs/styles/jsdoc.css +0 -671
- package/docs/styles/prettify.css +0 -79
- package/docs/util.js.html +0 -503
- package/screenshots/2019-08-17_0.png +0 -0
- package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
- package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
- package/spec/assets/effect/original.png +0 -0
- package/spec/assets/effect/pixelate.png +0 -0
- package/spec/assets/effect/transform/multiply.png +0 -0
- package/spec/assets/effect/transform/rotate.png +0 -0
- package/spec/assets/effect/transform/scale-fraction.png +0 -0
- package/spec/assets/effect/transform/scale.png +0 -0
- package/spec/assets/effect/transform/translate-fraction.png +0 -0
- package/spec/assets/effect/transform/translate.png +0 -0
- package/spec/assets/layer/audio.wav +0 -0
- package/spec/assets/layer/image.jpg +0 -0
- package/spec/effect.spec.js +0 -352
- package/spec/event.spec.js +0 -25
- package/spec/layer.spec.js +0 -128
- package/spec/movie.spec.js +0 -154
- package/spec/util.spec.js +0 -285
- package/src/effect.js +0 -1265
- package/src/event.js +0 -78
- package/src/index.js +0 -23
- package/src/layer.js +0 -875
- package/src/movie.js +0 -636
- package/src/util.js +0 -487
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import EtroObject from '../object'
|
|
2
|
+
import { publish, subscribe } from '../event'
|
|
3
|
+
import { watchPublic, applyOptions } from '../util'
|
|
4
|
+
import { Movie } from '../movie'
|
|
5
|
+
|
|
6
|
+
interface BaseOptions {
|
|
7
|
+
/** The time in the movie at which this layer starts */
|
|
8
|
+
startTime: number
|
|
9
|
+
duration: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A layer outputs content for the movie
|
|
14
|
+
*/
|
|
15
|
+
class Base implements EtroObject {
|
|
16
|
+
type: string
|
|
17
|
+
publicExcludes: string[]
|
|
18
|
+
propertyFilters: Record<string, <T>(value: T) => T>
|
|
19
|
+
enabled: boolean
|
|
20
|
+
/**
|
|
21
|
+
* If the attached movie's playback position is in this layer
|
|
22
|
+
*/
|
|
23
|
+
active: boolean
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The number of times this layer has been attached to a movie minus the
|
|
27
|
+
* number of times it's been detached. (Used for the movie's array proxy with
|
|
28
|
+
* `unshift`)
|
|
29
|
+
*/
|
|
30
|
+
private _occurrenceCount: number
|
|
31
|
+
private _startTime: number
|
|
32
|
+
private _duration: number
|
|
33
|
+
private _movie: Movie
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a new empty layer
|
|
37
|
+
*
|
|
38
|
+
* @param options
|
|
39
|
+
* @param options.startTime - when to start the layer on the movie's
|
|
40
|
+
* timeline
|
|
41
|
+
* @param options.duration - how long the layer should last on the
|
|
42
|
+
* movie's timeline
|
|
43
|
+
*/
|
|
44
|
+
constructor (options: BaseOptions) {
|
|
45
|
+
// Set startTime and duration properties manually, because they are
|
|
46
|
+
// readonly. applyOptions ignores readonly properties.
|
|
47
|
+
this._startTime = options.startTime
|
|
48
|
+
this._duration = options.duration
|
|
49
|
+
|
|
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
|
+
applyOptions(options, this)
|
|
55
|
+
|
|
56
|
+
// Whether this layer is currently being rendered
|
|
57
|
+
this.active = false
|
|
58
|
+
this.enabled = true
|
|
59
|
+
|
|
60
|
+
this._occurrenceCount = 0 // no occurances in parent
|
|
61
|
+
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
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Attaches this layer to `movie` if not already attached.
|
|
75
|
+
* @ignore
|
|
76
|
+
*/
|
|
77
|
+
tryAttach (movie: Movie): void {
|
|
78
|
+
if (this._occurrenceCount === 0)
|
|
79
|
+
this.attach(movie)
|
|
80
|
+
|
|
81
|
+
this._occurrenceCount++
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
attach (movie: Movie): void {
|
|
85
|
+
this._movie = movie
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Dettaches this layer from its movie if the number of times `tryDetach` has
|
|
90
|
+
* been called (including this call) equals the number of times `tryAttach`
|
|
91
|
+
* has been called.
|
|
92
|
+
*
|
|
93
|
+
* @ignore
|
|
94
|
+
*/
|
|
95
|
+
tryDetach (): void {
|
|
96
|
+
if (this.movie === null)
|
|
97
|
+
throw new Error('No movie to detach from')
|
|
98
|
+
|
|
99
|
+
this._occurrenceCount--
|
|
100
|
+
// If this layer occurs in another place in a `layers` array, do not unset
|
|
101
|
+
// _movie. (For calling `unshift` on the `layers` proxy)
|
|
102
|
+
if (this._occurrenceCount === 0)
|
|
103
|
+
this.detach()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
detach (): void {
|
|
107
|
+
this._movie = null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Called when the layer is activated
|
|
112
|
+
*/
|
|
113
|
+
start (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Called when the movie renders and the layer is active
|
|
117
|
+
*/
|
|
118
|
+
render (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Called when the layer is deactivated
|
|
122
|
+
*/
|
|
123
|
+
stop (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
|
|
124
|
+
|
|
125
|
+
// TODO: is this needed?
|
|
126
|
+
get parent (): Movie {
|
|
127
|
+
return this._movie
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
*/
|
|
132
|
+
get startTime (): number {
|
|
133
|
+
return this._startTime
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
set startTime (val: number) {
|
|
137
|
+
this._startTime = val
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The current time of the movie relative to this layer
|
|
142
|
+
*/
|
|
143
|
+
get currentTime (): number {
|
|
144
|
+
return this._movie ? this._movie.currentTime - this.startTime
|
|
145
|
+
: undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
*/
|
|
150
|
+
get duration (): number {
|
|
151
|
+
return this._duration
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
set duration (val: number) {
|
|
155
|
+
this._duration = val
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get movie (): Movie {
|
|
159
|
+
return this._movie
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getDefaultOptions (): BaseOptions {
|
|
163
|
+
return {
|
|
164
|
+
startTime: undefined, // required
|
|
165
|
+
duration: undefined // required
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// id for events (independent of instance, but easy to access when on prototype
|
|
170
|
+
// chain)
|
|
171
|
+
Base.prototype.type = 'layer'
|
|
172
|
+
Base.prototype.publicExcludes = []
|
|
173
|
+
Base.prototype.propertyFilters = {}
|
|
174
|
+
|
|
175
|
+
export { Base, BaseOptions }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module layer
|
|
3
|
+
*/
|
|
4
|
+
// TODO: Add aligning options, like horizontal and vertical align modes
|
|
5
|
+
|
|
6
|
+
export * from './audio-source'
|
|
7
|
+
export * from './audio'
|
|
8
|
+
export * from './base'
|
|
9
|
+
export * from './image'
|
|
10
|
+
export * from './text'
|
|
11
|
+
export * from './video'
|
|
12
|
+
export * from './visual-source'
|
|
13
|
+
export * from './visual'
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Dynamic, val, applyOptions } from '../util'
|
|
2
|
+
import { Visual, VisualOptions } from './visual'
|
|
3
|
+
|
|
4
|
+
interface TextOptions extends VisualOptions {
|
|
5
|
+
text: Dynamic<string>
|
|
6
|
+
font?: Dynamic<string>
|
|
7
|
+
color?: Dynamic<string>
|
|
8
|
+
/** The text's horizontal offset from the layer */
|
|
9
|
+
textX?: Dynamic<number>
|
|
10
|
+
/** The text's vertical offset from the layer */
|
|
11
|
+
textY?: Dynamic<number>
|
|
12
|
+
maxWidth?: Dynamic<number>
|
|
13
|
+
/**
|
|
14
|
+
* @desc The horizontal alignment
|
|
15
|
+
* @see [`CanvasRenderingContext2D#textAlign<`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textAlign)
|
|
16
|
+
*/
|
|
17
|
+
textAlign?: Dynamic<string>
|
|
18
|
+
/**
|
|
19
|
+
* @desc The vertical alignment
|
|
20
|
+
* @see [`CanvasRenderingContext2D#textBaseline`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
|
|
21
|
+
*/
|
|
22
|
+
textBaseline?: Dynamic<string>
|
|
23
|
+
/**
|
|
24
|
+
* @see [`CanvasRenderingContext2D#direction`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
|
|
25
|
+
*/
|
|
26
|
+
textDirection?: Dynamic<string>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class Text extends Visual {
|
|
30
|
+
text: Dynamic<string>
|
|
31
|
+
font: Dynamic<string>
|
|
32
|
+
color: Dynamic<string>
|
|
33
|
+
/** The text's horizontal offset from the layer */
|
|
34
|
+
textX: Dynamic<number>
|
|
35
|
+
/** The text's vertical offset from the layer */
|
|
36
|
+
textY: Dynamic<number>
|
|
37
|
+
maxWidth: Dynamic<number>
|
|
38
|
+
/**
|
|
39
|
+
* @desc The horizontal alignment
|
|
40
|
+
* @see [`CanvasRenderingContext2D#textAlign<`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textAlign)
|
|
41
|
+
*/
|
|
42
|
+
textAlign: Dynamic<string>
|
|
43
|
+
/**
|
|
44
|
+
* @desc The vertical alignment
|
|
45
|
+
* @see [`CanvasRenderingContext2D#textBaseline`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
|
|
46
|
+
*/
|
|
47
|
+
textBaseline: Dynamic<string>
|
|
48
|
+
/**
|
|
49
|
+
* @see [`CanvasRenderingContext2D#direction`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
|
|
50
|
+
*/
|
|
51
|
+
textDirection: Dynamic<string>
|
|
52
|
+
|
|
53
|
+
private _prevText: string
|
|
54
|
+
private _prevFont: string
|
|
55
|
+
private _prevMaxWidth: number
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a new text layer
|
|
59
|
+
*/
|
|
60
|
+
// TODO: add padding options
|
|
61
|
+
// TODO: is textX necessary? it seems inconsistent, because you can't define
|
|
62
|
+
// width/height directly for a text layer
|
|
63
|
+
constructor (options: TextOptions) {
|
|
64
|
+
// Default to no (transparent) background
|
|
65
|
+
super({ background: null, ...options })
|
|
66
|
+
applyOptions(options, this)
|
|
67
|
+
|
|
68
|
+
// this._prevText = undefined;
|
|
69
|
+
// // because the canvas context rounds font size, but we need to be more accurate
|
|
70
|
+
// // rn, this doesn't make a difference, because we can only measure metrics by integer font sizes
|
|
71
|
+
// this._lastFont = undefined;
|
|
72
|
+
// this._prevMaxWidth = undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
doRender (): void {
|
|
76
|
+
super.doRender()
|
|
77
|
+
const text = val(this, 'text', this.currentTime); const font = val(this, 'font', this.currentTime)
|
|
78
|
+
const maxWidth = this.maxWidth ? val(this, 'maxWidth', this.currentTime) : undefined
|
|
79
|
+
// // properties that affect metrics
|
|
80
|
+
// if (this._prevText !== text || this._prevFont !== font || this._prevMaxWidth !== maxWidth)
|
|
81
|
+
// this._updateMetrics(text, font, maxWidth);
|
|
82
|
+
|
|
83
|
+
this.cctx.font = font
|
|
84
|
+
this.cctx.fillStyle = val(this, 'color', this.currentTime)
|
|
85
|
+
this.cctx.textAlign = val(this, 'textAlign', this.currentTime)
|
|
86
|
+
this.cctx.textBaseline = val(this, 'textBaseline', this.currentTime)
|
|
87
|
+
this.cctx.direction = val(this, 'textDirection', this.currentTime)
|
|
88
|
+
this.cctx.fillText(
|
|
89
|
+
text, val(this, 'textX', this.currentTime), val(this, 'textY', this.currentTime),
|
|
90
|
+
maxWidth
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
this._prevText = text
|
|
94
|
+
this._prevFont = font
|
|
95
|
+
this._prevMaxWidth = maxWidth
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// _updateMetrics(text, font, maxWidth) {
|
|
99
|
+
// // TODO calculate / measure for non-integer font.size values
|
|
100
|
+
// let metrics = Text._measureText(text, font, maxWidth);
|
|
101
|
+
// // TODO: allow user-specified/overwritten width/height
|
|
102
|
+
// this.width = /*this.width || */metrics.width;
|
|
103
|
+
// this.height = /*this.height || */metrics.height;
|
|
104
|
+
// }
|
|
105
|
+
|
|
106
|
+
// TODO: implement setters and getters that update dimensions!
|
|
107
|
+
|
|
108
|
+
/* static _measureText(text, font, maxWidth) {
|
|
109
|
+
// TODO: fix too much bottom padding
|
|
110
|
+
const s = document.createElement("span");
|
|
111
|
+
s.textContent = text;
|
|
112
|
+
s.style.font = font;
|
|
113
|
+
s.style.padding = "0";
|
|
114
|
+
if (maxWidth) s.style.maxWidth = maxWidth;
|
|
115
|
+
document.body.appendChild(s);
|
|
116
|
+
const metrics = {width: s.offsetWidth, height: s.offsetHeight};
|
|
117
|
+
document.body.removeChild(s);
|
|
118
|
+
return metrics;
|
|
119
|
+
} */
|
|
120
|
+
|
|
121
|
+
getDefaultOptions (): TextOptions {
|
|
122
|
+
return {
|
|
123
|
+
...Visual.prototype.getDefaultOptions(),
|
|
124
|
+
background: null,
|
|
125
|
+
text: undefined, // required
|
|
126
|
+
font: '10px sans-serif',
|
|
127
|
+
color: '#fff',
|
|
128
|
+
textX: 0,
|
|
129
|
+
textY: 0,
|
|
130
|
+
maxWidth: null,
|
|
131
|
+
textAlign: 'start',
|
|
132
|
+
textBaseline: 'top',
|
|
133
|
+
textDirection: 'ltr'
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { Text, TextOptions }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Visual } from './visual'
|
|
2
|
+
import { VisualSourceOptions, VisualSourceMixin } from './visual-source'
|
|
3
|
+
import { AudioSourceOptions, AudioSourceMixin } from './audio-source'
|
|
4
|
+
|
|
5
|
+
type VideoOptions = VisualSourceOptions & AudioSourceOptions
|
|
6
|
+
|
|
7
|
+
// Use mixins instead of `extend`ing two classes (which isn't supported by
|
|
8
|
+
// JavaScript).
|
|
9
|
+
/**
|
|
10
|
+
* @extends AudioSource
|
|
11
|
+
* @extends VisualSource
|
|
12
|
+
*/
|
|
13
|
+
class Video extends AudioSourceMixin(VisualSourceMixin(Visual)) {}
|
|
14
|
+
|
|
15
|
+
export { Video, VideoOptions }
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Dynamic, val, applyOptions } from '../util'
|
|
2
|
+
import { Base, BaseOptions } from './base'
|
|
3
|
+
import { Visual, VisualOptions } from './visual'
|
|
4
|
+
|
|
5
|
+
type Constructor<T> = new (...args: unknown[]) => T
|
|
6
|
+
|
|
7
|
+
interface VisualSource extends Base {
|
|
8
|
+
readonly source: HTMLImageElement | HTMLVideoElement
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface VisualSourceOptions extends VisualOptions {
|
|
12
|
+
source: HTMLImageElement | HTMLVideoElement
|
|
13
|
+
/** What part of {@link source} to render */
|
|
14
|
+
sourceX?: Dynamic<number>
|
|
15
|
+
/** What part of {@link source} to render */
|
|
16
|
+
sourceY?: Dynamic<number>
|
|
17
|
+
/** What part of {@link source} to render, or undefined for the entire width */
|
|
18
|
+
sourceWidth?: Dynamic<number>
|
|
19
|
+
/** What part of {@link source} to render, or undefined for the entire height */
|
|
20
|
+
sourceHeight?: Dynamic<number>
|
|
21
|
+
/** Where to render {@link source} onto the layer */
|
|
22
|
+
destX?: Dynamic<number>
|
|
23
|
+
/** Where to render {@link source} onto the layer */
|
|
24
|
+
destY?: Dynamic<number>
|
|
25
|
+
/** Where to render {@link source} onto the layer, or undefined to fill the layer's width */
|
|
26
|
+
destWidth?: Dynamic<number>
|
|
27
|
+
/** Where to render {@link source} onto the layer, or undefined to fill the layer's height */
|
|
28
|
+
destHeight?: Dynamic<number>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A layer that gets its image data from an HTML image or video element
|
|
33
|
+
* @mixin VisualSourceMixin
|
|
34
|
+
*/
|
|
35
|
+
function VisualSourceMixin<OptionsSuperclass extends BaseOptions> (superclass: Constructor<Visual>): Constructor<VisualSource> {
|
|
36
|
+
type MixedVisualSourceOptions = OptionsSuperclass & VisualSourceOptions
|
|
37
|
+
|
|
38
|
+
class MixedVisualSource extends superclass {
|
|
39
|
+
/**
|
|
40
|
+
* The raw html media element
|
|
41
|
+
*/
|
|
42
|
+
readonly source: HTMLImageElement | HTMLVideoElement
|
|
43
|
+
|
|
44
|
+
/** What part of {@link source} to render */
|
|
45
|
+
sourceX: Dynamic<number>
|
|
46
|
+
/** What part of {@link source} to render */
|
|
47
|
+
sourceY: Dynamic<number>
|
|
48
|
+
/** What part of {@link source} to render, or undefined for the entire width */
|
|
49
|
+
sourceWidth: Dynamic<number>
|
|
50
|
+
/** What part of {@link source} to render, or undefined for the entire height */
|
|
51
|
+
sourceHeight: Dynamic<number>
|
|
52
|
+
/** Where to render {@link source} onto the layer */
|
|
53
|
+
destX: Dynamic<number>
|
|
54
|
+
/** Where to render {@link source} onto the layer */
|
|
55
|
+
destY: Dynamic<number>
|
|
56
|
+
/** Where to render {@link source} onto the layer, or undefined to fill the layer's width */
|
|
57
|
+
destWidth: Dynamic<number>
|
|
58
|
+
/** Where to render {@link source} onto the layer, or undefined to fill the layer's height */
|
|
59
|
+
destHeight: Dynamic<number>
|
|
60
|
+
|
|
61
|
+
constructor (options: MixedVisualSourceOptions) {
|
|
62
|
+
super(options)
|
|
63
|
+
applyOptions(options, this)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
doRender () {
|
|
67
|
+
// Clear/fill background
|
|
68
|
+
super.doRender()
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
* Source dimensions crop the image. Dest dimensions set the size that
|
|
72
|
+
* the image will be rendered at *on the layer*. Note that this is
|
|
73
|
+
* different than the layer dimensions (`this.width` and `this.height`).
|
|
74
|
+
* The main reason this distinction exists is so that an image layer can
|
|
75
|
+
* be rotated without being cropped (see iss #46).
|
|
76
|
+
*/
|
|
77
|
+
this.cctx.drawImage(
|
|
78
|
+
this.source,
|
|
79
|
+
val(this, 'sourceX', this.currentTime), val(this, 'sourceY', this.currentTime),
|
|
80
|
+
val(this, 'sourceWidth', this.currentTime), val(this, 'sourceHeight', this.currentTime),
|
|
81
|
+
// `destX` and `destY` are relative to the layer
|
|
82
|
+
val(this, 'destX', this.currentTime), val(this, 'destY', this.currentTime),
|
|
83
|
+
val(this, 'destWidth', this.currentTime), val(this, 'destHeight', this.currentTime)
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getDefaultOptions (): MixedVisualSourceOptions {
|
|
88
|
+
return {
|
|
89
|
+
...superclass.prototype.getDefaultOptions(),
|
|
90
|
+
source: undefined, // required
|
|
91
|
+
sourceX: 0,
|
|
92
|
+
sourceY: 0,
|
|
93
|
+
sourceWidth: undefined,
|
|
94
|
+
sourceHeight: undefined,
|
|
95
|
+
destX: 0,
|
|
96
|
+
destY: 0,
|
|
97
|
+
destWidth: undefined,
|
|
98
|
+
destHeight: undefined
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
MixedVisualSource.prototype.propertyFilters = {
|
|
103
|
+
...Visual.prototype.propertyFilters,
|
|
104
|
+
|
|
105
|
+
/*
|
|
106
|
+
* If no layer width was provided, fall back to the dest width.
|
|
107
|
+
* If no dest width was provided, fall back to the source width.
|
|
108
|
+
* If no source width was provided, fall back to `source.width`.
|
|
109
|
+
*/
|
|
110
|
+
sourceWidth: function (sourceWidth) {
|
|
111
|
+
// != instead of !== to account for `null`
|
|
112
|
+
const width = this.source instanceof HTMLImageElement
|
|
113
|
+
? this.source.width
|
|
114
|
+
: this.source.videoWidth
|
|
115
|
+
return sourceWidth != undefined ? sourceWidth : width // eslint-disable-line eqeqeq
|
|
116
|
+
},
|
|
117
|
+
sourceHeight: function (sourceHeight) {
|
|
118
|
+
const height = this.source instanceof HTMLImageElement
|
|
119
|
+
? this.source.height
|
|
120
|
+
: this.source.videoHeight
|
|
121
|
+
return sourceHeight != undefined ? sourceHeight : height // eslint-disable-line eqeqeq
|
|
122
|
+
},
|
|
123
|
+
destWidth: function (destWidth) {
|
|
124
|
+
// I believe reltime is redundant, as element#currentTime can be used
|
|
125
|
+
// instead. (TODO: fact check)
|
|
126
|
+
/* eslint-disable eqeqeq */
|
|
127
|
+
return destWidth != undefined
|
|
128
|
+
? destWidth : val(this, 'sourceWidth', this.currentTime)
|
|
129
|
+
},
|
|
130
|
+
destHeight: function (destHeight) {
|
|
131
|
+
/* eslint-disable eqeqeq */
|
|
132
|
+
return destHeight != undefined
|
|
133
|
+
? destHeight : val(this, 'sourceHeight', this.currentTime)
|
|
134
|
+
},
|
|
135
|
+
width: function (width) {
|
|
136
|
+
/* eslint-disable eqeqeq */
|
|
137
|
+
return width != undefined
|
|
138
|
+
? width : val(this, 'destWidth', this.currentTime)
|
|
139
|
+
},
|
|
140
|
+
height: function (height) {
|
|
141
|
+
/* eslint-disable eqeqeq */
|
|
142
|
+
return height != undefined
|
|
143
|
+
? height : val(this, 'destHeight', this.currentTime)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return MixedVisualSource
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { VisualSource, VisualSourceOptions, VisualSourceMixin }
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Dynamic, val, applyOptions } from '../util'
|
|
2
|
+
import { Base, BaseOptions } from './base'
|
|
3
|
+
import { Base as BaseEffect } from '../effect/base'
|
|
4
|
+
|
|
5
|
+
interface VisualOptions extends BaseOptions {
|
|
6
|
+
x?: Dynamic<number>
|
|
7
|
+
y?: Dynamic<number>
|
|
8
|
+
width?: Dynamic<number>
|
|
9
|
+
height?: Dynamic<number>
|
|
10
|
+
background?: Dynamic<string>
|
|
11
|
+
border?: Dynamic<{
|
|
12
|
+
color: string
|
|
13
|
+
thickness?: number
|
|
14
|
+
}>
|
|
15
|
+
|
|
16
|
+
opacity?: Dynamic<number>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Any layer that renders to a canvas */
|
|
20
|
+
class Visual extends Base {
|
|
21
|
+
x: Dynamic<number>
|
|
22
|
+
y: Dynamic<number>
|
|
23
|
+
width: Dynamic<number>
|
|
24
|
+
height: Dynamic<number>
|
|
25
|
+
background: Dynamic<string>
|
|
26
|
+
border: Dynamic<{
|
|
27
|
+
color: string
|
|
28
|
+
thickness: number
|
|
29
|
+
}>
|
|
30
|
+
|
|
31
|
+
opacity: Dynamic<number>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The layer's rendering canvas
|
|
35
|
+
*/
|
|
36
|
+
readonly canvas: HTMLCanvasElement
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The context of {@link Visual#canvas}
|
|
40
|
+
*/
|
|
41
|
+
readonly cctx: CanvasRenderingContext2D
|
|
42
|
+
|
|
43
|
+
// readonly because it's a proxy
|
|
44
|
+
readonly effects: BaseEffect[]
|
|
45
|
+
|
|
46
|
+
private _effectsBack: BaseEffect[]
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a visual layer
|
|
50
|
+
*/
|
|
51
|
+
constructor (options: VisualOptions) {
|
|
52
|
+
super(options)
|
|
53
|
+
// Only validate extra if not subclassed, because if subclcass, there will
|
|
54
|
+
// be extraneous options.
|
|
55
|
+
applyOptions(options, this)
|
|
56
|
+
|
|
57
|
+
this.canvas = document.createElement('canvas')
|
|
58
|
+
this.cctx = this.canvas.getContext('2d')
|
|
59
|
+
|
|
60
|
+
this._effectsBack = []
|
|
61
|
+
this.effects = new Proxy(this._effectsBack, {
|
|
62
|
+
deleteProperty: (target, property) => {
|
|
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
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Render visual output
|
|
84
|
+
*/
|
|
85
|
+
render (): void {
|
|
86
|
+
this.beginRender()
|
|
87
|
+
this.doRender()
|
|
88
|
+
this.endRender()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
beginRender (): void {
|
|
92
|
+
this.canvas.width = val(this, 'width', this.currentTime)
|
|
93
|
+
this.canvas.height = val(this, 'height', this.currentTime)
|
|
94
|
+
this.cctx.globalAlpha = val(this, 'opacity', this.currentTime)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
doRender (): void {
|
|
98
|
+
/*
|
|
99
|
+
* If this.width or this.height is null, that means "take all available
|
|
100
|
+
* screen space", so set it to this._move.width or this._movie.height,
|
|
101
|
+
* respectively canvas.width & canvas.height are already interpolated
|
|
102
|
+
*/
|
|
103
|
+
if (this.background) {
|
|
104
|
+
this.cctx.fillStyle = val(this, 'background', this.currentTime)
|
|
105
|
+
// (0, 0) relative to layer
|
|
106
|
+
this.cctx.fillRect(0, 0, this.canvas.width, this.canvas.height)
|
|
107
|
+
}
|
|
108
|
+
const border = val(this, 'border', this.currentTime)
|
|
109
|
+
if (border && border.color) {
|
|
110
|
+
this.cctx.strokeStyle = border.color
|
|
111
|
+
// This is optional.. TODO: integrate this with defaultOptions
|
|
112
|
+
this.cctx.lineWidth = border.thickness || 1
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
endRender (): void {
|
|
117
|
+
const w = val(this, 'width', this.currentTime) || val(this.movie, 'width', this.movie.currentTime)
|
|
118
|
+
const h = val(this, 'height', this.currentTime) || val(this.movie, 'height', this.movie.currentTime)
|
|
119
|
+
if (w * h > 0)
|
|
120
|
+
this._applyEffects()
|
|
121
|
+
|
|
122
|
+
// else InvalidStateError for drawing zero-area image in some effects, right?
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_applyEffects (): void {
|
|
126
|
+
for (let i = 0; i < this.effects.length; i++) {
|
|
127
|
+
const effect = this.effects[i]
|
|
128
|
+
if (effect.enabled)
|
|
129
|
+
// Pass relative time
|
|
130
|
+
effect.apply(this, this.movie.currentTime - this.startTime)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Convienence method for <code>effects.push()</code>
|
|
136
|
+
* @param effect
|
|
137
|
+
* @return the layer (for chaining)
|
|
138
|
+
*/
|
|
139
|
+
addEffect (effect: BaseEffect): Visual {
|
|
140
|
+
this.effects.push(effect); return this
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getDefaultOptions (): VisualOptions {
|
|
144
|
+
return {
|
|
145
|
+
...Base.prototype.getDefaultOptions(),
|
|
146
|
+
/**
|
|
147
|
+
* @name module:layer.Visual#x
|
|
148
|
+
* @desc The offset of the layer relative to the movie
|
|
149
|
+
*/
|
|
150
|
+
x: 0,
|
|
151
|
+
/**
|
|
152
|
+
* @name module:layer.Visual#y
|
|
153
|
+
* @desc The offset of the layer relative to the movie
|
|
154
|
+
*/
|
|
155
|
+
y: 0,
|
|
156
|
+
/**
|
|
157
|
+
* @name module:layer.Visual#width
|
|
158
|
+
*/
|
|
159
|
+
width: null,
|
|
160
|
+
/**
|
|
161
|
+
* @name module:layer.Visual#height
|
|
162
|
+
*/
|
|
163
|
+
height: null,
|
|
164
|
+
/**
|
|
165
|
+
* @name module:layer.Visual#background
|
|
166
|
+
* @desc The CSS color code for the background, or <code>null</code> for
|
|
167
|
+
* transparency
|
|
168
|
+
*/
|
|
169
|
+
background: null,
|
|
170
|
+
/**
|
|
171
|
+
* @name module:layer.Visual#border
|
|
172
|
+
* @desc The CSS border style, or <code>null</code> for no border
|
|
173
|
+
*/
|
|
174
|
+
border: null,
|
|
175
|
+
/**
|
|
176
|
+
* @name module:layer.Visual#opacity
|
|
177
|
+
*/
|
|
178
|
+
opacity: 1
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
Visual.prototype.publicExcludes = Base.prototype.publicExcludes.concat(['canvas', 'cctx', 'effects'])
|
|
183
|
+
Visual.prototype.propertyFilters = {
|
|
184
|
+
...Base.prototype.propertyFilters,
|
|
185
|
+
/*
|
|
186
|
+
* If this.width or this.height is null, that means "take all available screen
|
|
187
|
+
* space", so set it to this._move.width or this._movie.height, respectively
|
|
188
|
+
*/
|
|
189
|
+
width: function (width) {
|
|
190
|
+
return width != undefined ? width : this._movie.width // eslint-disable-line eqeqeq
|
|
191
|
+
},
|
|
192
|
+
height: function (height) {
|
|
193
|
+
return height != undefined ? height : this._movie.height // eslint-disable-line eqeqeq
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export { Visual, VisualOptions }
|