etro 0.7.0 → 0.8.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/.github/workflows/nodejs.yml +1 -1
- package/CHANGELOG.md +45 -1
- package/CONTRIBUTING.md +23 -19
- package/README.md +81 -26
- package/dist/effect/base.d.ts +38 -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 +9337 -3331
- package/dist/etro-iife.js +9279 -3273
- 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 +69 -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 +0 -8
- package/eslint.example-conf.js +9 -0
- package/eslint.typescript-conf.js +5 -0
- package/examples/application/readme-screenshot.html +12 -9
- package/examples/application/video-player.html +7 -7
- package/examples/application/webcam.html +6 -6
- package/examples/introduction/audio.html +30 -18
- package/examples/introduction/effects.html +14 -10
- package/examples/introduction/export.html +32 -25
- 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 +1 -1
- package/package.json +29 -13
- package/rollup.config.js +15 -4
- package/scripts/gen-effect-samples.html +29 -25
- package/scripts/save-effect-samples.js +14 -15
- package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
- package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
- package/spec/assets/effect/grayscale.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/effect.spec.js +126 -57
- package/spec/event.spec.js +14 -0
- package/spec/layer.spec.js +175 -18
- package/spec/movie.spec.js +191 -7
- package/spec/util.spec.js +14 -5
- package/src/effect/base.ts +96 -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 +59 -0
- package/src/effect/shader.ts +561 -0
- package/src/effect/stack.ts +74 -0
- package/src/effect/transform.ts +194 -0
- package/src/etro.ts +26 -0
- package/src/event.ts +118 -0
- package/src/index.ts +8 -0
- package/src/layer/audio-source.ts +217 -0
- package/src/layer/audio.ts +35 -0
- package/src/layer/base.ts +156 -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 +198 -0
- package/src/movie.ts +709 -0
- package/src/object.ts +14 -0
- package/src/util.ts +473 -0
- package/tsconfig.json +8 -0
- package/screenshots/2019-08-17_0.png +0 -0
- package/src/effect.js +0 -1268
- package/src/event.js +0 -78
- package/src/index.js +0 -23
- package/src/layer.js +0 -897
- package/src/movie.js +0 -637
- package/src/util.js +0 -505
package/spec/movie.spec.js
CHANGED
|
@@ -6,9 +6,21 @@ describe('Movie', function () {
|
|
|
6
6
|
document.body.removeChild(canvas)
|
|
7
7
|
}
|
|
8
8
|
canvas = document.createElement('canvas')
|
|
9
|
+
// Resolutions lower than 20x20 rreslt in empty blobs.
|
|
10
|
+
canvas.width = 20
|
|
11
|
+
canvas.height = 20
|
|
9
12
|
document.body.appendChild(canvas)
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
/*
|
|
14
|
+
* Because `autoRefresh` defaults to true, any operation that could effect
|
|
15
|
+
* the current frame causes a refresh. Thus, we have to wait to the test
|
|
16
|
+
* until the movie is done refreshing, to catch all possible errors.
|
|
17
|
+
* However, errors that take place while refreshing will only cause the test
|
|
18
|
+
* to timeout, without the actual error being shown in the terminal. The
|
|
19
|
+
* current best way to debug this situation would be to open the test in
|
|
20
|
+
* 'Chrome' instead of 'ChromeHeadless' (see karma.conf.js).
|
|
21
|
+
*/
|
|
22
|
+
movie = new etro.Movie({ canvas, background: 'blue' })
|
|
23
|
+
movie.addLayer(new etro.layer.Visual({ startTime: 0, duration: 0.8 }))
|
|
12
24
|
})
|
|
13
25
|
|
|
14
26
|
describe('identity ->', function () {
|
|
@@ -17,6 +29,119 @@ describe('Movie', function () {
|
|
|
17
29
|
})
|
|
18
30
|
})
|
|
19
31
|
|
|
32
|
+
describe('layers ->', function () {
|
|
33
|
+
it('should call `attach` when a layer is added', function (done) {
|
|
34
|
+
const layer = new etro.layer.Base({ startTime: 0, duration: 1 })
|
|
35
|
+
spyOn(layer, 'attach')
|
|
36
|
+
// Manually attach layer to movie, because `attach` is stubbed.
|
|
37
|
+
// Otherwise, auto-refresh will cause errors.
|
|
38
|
+
layer._movie = movie
|
|
39
|
+
|
|
40
|
+
// Adding a layer will cause the movie to refresh. Wait until the movie's
|
|
41
|
+
// done refreshing to end the test (in case errors arise there!)
|
|
42
|
+
etro.event.subscribe(movie, 'movie.loadeddata', done)
|
|
43
|
+
// Add layer
|
|
44
|
+
movie.layers.push(layer)
|
|
45
|
+
expect(layer.attach).toHaveBeenCalled()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should call `detach` when a layer is removed', function (done) {
|
|
49
|
+
spyOn(movie.layers[0], 'detach')
|
|
50
|
+
// Wait to end the test until the movie's done refreshing.
|
|
51
|
+
etro.event.subscribe(movie, 'movie.loadeddata', done)
|
|
52
|
+
const layer = movie.layers.shift()
|
|
53
|
+
expect(layer.detach).toHaveBeenCalled()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should call `detach` when a layer is replaced', function (done) {
|
|
57
|
+
const layer = movie.layers[0]
|
|
58
|
+
spyOn(layer, 'detach')
|
|
59
|
+
// Wait to end the test until the movie's done refreshing.
|
|
60
|
+
etro.event.subscribe(movie, 'movie.loadeddata', done)
|
|
61
|
+
movie.layers[0] = new etro.layer.Base({ startTime: 0, duration: 1 })
|
|
62
|
+
expect(layer.detach).toHaveBeenCalled()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should implement common array methods', function (done) {
|
|
66
|
+
const dummy = () => new etro.layer.Base({ startTime: 0, duration: 1 })
|
|
67
|
+
const calls = {
|
|
68
|
+
concat: [[dummy()]],
|
|
69
|
+
every: [layer => true],
|
|
70
|
+
includes: [dummy()],
|
|
71
|
+
pop: [],
|
|
72
|
+
push: [dummy()],
|
|
73
|
+
unshift: [dummy()]
|
|
74
|
+
}
|
|
75
|
+
// Wait to end the test until the movie's done refreshing.
|
|
76
|
+
etro.event.subscribe(movie, 'movie.loadeddata', done)
|
|
77
|
+
for (const method in calls) {
|
|
78
|
+
const args = calls[method]
|
|
79
|
+
const copy = [...movie.layers]
|
|
80
|
+
const expectedResult = Array.prototype[method].apply(copy, args)
|
|
81
|
+
const actualResult = movie.layers[method](...args)
|
|
82
|
+
expect(actualResult).toEqual(expectedResult)
|
|
83
|
+
expect(movie.layers).toEqual(copy)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('effects ->', function () {
|
|
89
|
+
it('should call `attach` when an effect is added', function (done) {
|
|
90
|
+
const effect = new etro.effect.Base()
|
|
91
|
+
spyOn(effect, 'attach')
|
|
92
|
+
// Wait to end the test until the movie's done refreshing.
|
|
93
|
+
etro.event.subscribe(movie, 'movie.loadeddata', done)
|
|
94
|
+
|
|
95
|
+
movie.effects.push(effect)
|
|
96
|
+
expect(effect.attach).toHaveBeenCalled()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should call `detach` when an effect is removed', function (done) {
|
|
100
|
+
const effect = new etro.effect.Base()
|
|
101
|
+
movie.effects.push(effect)
|
|
102
|
+
spyOn(effect, 'detach')
|
|
103
|
+
// Wait to end the test until the movie's done refreshing.
|
|
104
|
+
etro.event.subscribe(movie, 'movie.loadeddata', done)
|
|
105
|
+
|
|
106
|
+
movie.effects.pop()
|
|
107
|
+
expect(effect.detach).toHaveBeenCalled()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should call `detach` when an effect is replaced', function (done) {
|
|
111
|
+
const effect = new etro.effect.Base()
|
|
112
|
+
movie.effects.push(effect)
|
|
113
|
+
spyOn(effect, 'detach')
|
|
114
|
+
// Wait to end the test until the movie's done refreshing.
|
|
115
|
+
etro.event.subscribe(movie, 'movie.loadeddata', done)
|
|
116
|
+
|
|
117
|
+
movie.effects[0] = new etro.effect.Base()
|
|
118
|
+
expect(effect.detach).toHaveBeenCalled()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should implement common array methods', function (done) {
|
|
122
|
+
const dummy = () => new etro.effect.Base()
|
|
123
|
+
const calls = {
|
|
124
|
+
concat: [[dummy()]],
|
|
125
|
+
every: [layer => true],
|
|
126
|
+
includes: [dummy()],
|
|
127
|
+
pop: [],
|
|
128
|
+
push: [dummy()],
|
|
129
|
+
unshift: [dummy()]
|
|
130
|
+
}
|
|
131
|
+
// Wait to end the test until the movie's done refreshing.
|
|
132
|
+
etro.event.subscribe(movie, 'movie.loadeddata', done)
|
|
133
|
+
|
|
134
|
+
for (const method in calls) {
|
|
135
|
+
const args = calls[method]
|
|
136
|
+
const copy = [...movie.effects]
|
|
137
|
+
const expectedResult = Array.prototype[method].apply(copy, args)
|
|
138
|
+
const actualResult = movie.effects[method](...args)
|
|
139
|
+
expect(actualResult).toEqual(expectedResult)
|
|
140
|
+
expect(movie.effects).toEqual(copy)
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
20
145
|
describe('operations ->', function () {
|
|
21
146
|
it('should not be paused after playing', function () {
|
|
22
147
|
movie.play()
|
|
@@ -43,17 +168,32 @@ describe('Movie', function () {
|
|
|
43
168
|
})
|
|
44
169
|
|
|
45
170
|
it('should be `recording` when recording', function () {
|
|
46
|
-
movie.record(10)
|
|
171
|
+
movie.record({ frameRate: 10 })
|
|
47
172
|
expect(movie.recording).toBe(true)
|
|
48
173
|
})
|
|
49
174
|
|
|
50
175
|
it('should not be paused when recording', function () {
|
|
51
|
-
movie.record(10)
|
|
176
|
+
movie.record({ frameRate: 10 })
|
|
52
177
|
expect(movie.paused).toBe(false)
|
|
53
178
|
})
|
|
54
179
|
|
|
180
|
+
it('should end recording at the right time when `duration` is supplied', function (done) {
|
|
181
|
+
movie.record({ frameRate: 10, duration: 0.4 })
|
|
182
|
+
.then(_ => {
|
|
183
|
+
// Expect movie.currentTime to be a little larger than 0.4 (the last render might land after 0.4)
|
|
184
|
+
expect(movie.currentTime).toBeGreaterThanOrEqual(0.4)
|
|
185
|
+
expect(movie.currentTime).toBeLessThan(0.4 + 0.08)
|
|
186
|
+
done()
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should reach the end when recording with no `duration`', function (done) {
|
|
191
|
+
etro.event.subscribe(movie, 'movie.ended', done)
|
|
192
|
+
movie.record({ frameRate: 10 })
|
|
193
|
+
})
|
|
194
|
+
|
|
55
195
|
it('should return blob after recording', function (done) {
|
|
56
|
-
movie.record(60)
|
|
196
|
+
movie.record({ frameRate: 60 })
|
|
57
197
|
.then(video => {
|
|
58
198
|
expect(video.size).toBeGreaterThan(0)
|
|
59
199
|
done()
|
|
@@ -64,12 +204,56 @@ describe('Movie', function () {
|
|
|
64
204
|
})
|
|
65
205
|
|
|
66
206
|
it('can record with custom MIME type', function (done) {
|
|
67
|
-
movie.record(60,
|
|
207
|
+
movie.record({ frameRate: 60, type: 'video/mp4' })
|
|
68
208
|
.then(video => {
|
|
69
209
|
expect(video.type).toBe('video/mp4')
|
|
70
210
|
done()
|
|
71
211
|
})
|
|
72
212
|
})
|
|
213
|
+
|
|
214
|
+
it('should produce correct image data when recording', function (done) {
|
|
215
|
+
movie.record({ frameRate: 10 })
|
|
216
|
+
.then(video => {
|
|
217
|
+
// Render the first frame of the video to a canvas and make sure the
|
|
218
|
+
// image data is correct.
|
|
219
|
+
|
|
220
|
+
// Load blob into html video element
|
|
221
|
+
const v = document.createElement('video')
|
|
222
|
+
v.src = URL.createObjectURL(video)
|
|
223
|
+
// Since it's a blob, we need to force-load all frames for it to
|
|
224
|
+
// render properly, using this hack:
|
|
225
|
+
v.currentTime = Number.MAX_SAFE_INTEGER
|
|
226
|
+
v.ontimeupdate = () => {
|
|
227
|
+
// Now the video is loaded. Create temporary canvas and render first
|
|
228
|
+
// frame onto it.
|
|
229
|
+
const ctx = document
|
|
230
|
+
.createElement('canvas')
|
|
231
|
+
.getContext('2d')
|
|
232
|
+
ctx.canvas.width = v.videoWidth
|
|
233
|
+
ctx.canvas.height = v.videoHeight
|
|
234
|
+
ctx.drawImage(v, 0, 0)
|
|
235
|
+
// Expect all opaque blue pixels
|
|
236
|
+
const expectedImageData = Array(v.videoWidth * v.videoHeight)
|
|
237
|
+
.fill([0, 0, 255, 255])
|
|
238
|
+
.flat(1)
|
|
239
|
+
const actualImageData = Array.from(
|
|
240
|
+
ctx.getImageData(0, 0, v.videoWidth, v.videoHeight).data
|
|
241
|
+
)
|
|
242
|
+
const maxDiff = actualImageData
|
|
243
|
+
// Calculate diff image data
|
|
244
|
+
.map((x, i) => x - expectedImageData[i])
|
|
245
|
+
// Find max pixel component diff
|
|
246
|
+
.reduce((x, max) => Math.max(x, max))
|
|
247
|
+
|
|
248
|
+
// Now, there is going to be variance due to encoding problems.
|
|
249
|
+
// Accept an error of 5 for each color component (5 is somewhat
|
|
250
|
+
// arbitrary but it works).
|
|
251
|
+
expect(maxDiff).toBeLessThanOrEqual(5)
|
|
252
|
+
URL.revokeObjectURL(v.src)
|
|
253
|
+
done()
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
})
|
|
73
257
|
})
|
|
74
258
|
|
|
75
259
|
describe('events ->', function () {
|
|
@@ -100,7 +284,7 @@ describe('Movie', function () {
|
|
|
100
284
|
etro.event.subscribe(movie, 'movie.record', function () {
|
|
101
285
|
timesFired++
|
|
102
286
|
})
|
|
103
|
-
movie.record().then(function () {
|
|
287
|
+
movie.record({ frameRate: 1 }).then(function () {
|
|
104
288
|
expect(timesFired).toBe(1)
|
|
105
289
|
})
|
|
106
290
|
})
|
package/spec/util.spec.js
CHANGED
|
@@ -65,7 +65,7 @@ describe('Util', function () {
|
|
|
65
65
|
|
|
66
66
|
it('should interpolate keyframes', function () {
|
|
67
67
|
const elem = {
|
|
68
|
-
prop:
|
|
68
|
+
prop: new etro.KeyFrame([0, 0], [4, 1]),
|
|
69
69
|
movie: {}, // _movie is unique, so it won't depend on existing cache
|
|
70
70
|
propertyFilters: {}
|
|
71
71
|
}
|
|
@@ -77,18 +77,27 @@ describe('Util', function () {
|
|
|
77
77
|
|
|
78
78
|
it('should work with noninterpolated keyframes', function () {
|
|
79
79
|
const elem = {
|
|
80
|
-
prop:
|
|
80
|
+
prop: new etro.KeyFrame([0, 'start'], [4, 'end']),
|
|
81
81
|
movie: {}, // _movie is unique, so it won't depend on existing cache
|
|
82
82
|
propertyFilters: {}
|
|
83
83
|
}
|
|
84
|
-
expect(etro.val(elem, 'prop', 0)).toBe(
|
|
84
|
+
expect(etro.val(elem, 'prop', 0)).toBe('start')
|
|
85
85
|
etro.clearCachedValues(elem.movie)
|
|
86
|
-
expect(etro.val(elem, 'prop', 3)).toBe(
|
|
86
|
+
expect(etro.val(elem, 'prop', 3)).toBe('start')
|
|
87
87
|
etro.clearCachedValues(elem.movie)
|
|
88
|
-
expect(etro.val(elem, 'prop', 4)).toBe(
|
|
88
|
+
expect(etro.val(elem, 'prop', 4)).toBe('end')
|
|
89
89
|
etro.clearCachedValues(elem.movie)
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
+
it('should use individual interpolation methods', function () {
|
|
93
|
+
const elem = {
|
|
94
|
+
prop: new etro.KeyFrame([0, 0, etro.cosineInterp], [1, 4]),
|
|
95
|
+
movie: {},
|
|
96
|
+
propertyFilters: {}
|
|
97
|
+
}
|
|
98
|
+
expect(etro.val(elem, 'prop', 0.5)).toBe(etro.cosineInterp(0, 4, 0.5))
|
|
99
|
+
})
|
|
100
|
+
|
|
92
101
|
it('should call property filters', function () {
|
|
93
102
|
const elem = {
|
|
94
103
|
prop: 'value',
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { watchPublic } from '../util'
|
|
2
|
+
import { publish, subscribe } from '../event'
|
|
3
|
+
import { Movie } from '../movie'
|
|
4
|
+
import { Visual } from '../layer/index'
|
|
5
|
+
import BaseObject from '../object'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Modifies the visual contents of a layer.
|
|
9
|
+
*/
|
|
10
|
+
export class Base implements BaseObject {
|
|
11
|
+
type: string
|
|
12
|
+
publicExcludes: string[]
|
|
13
|
+
propertyFilters: Record<string, <T>(value: T) => T>
|
|
14
|
+
|
|
15
|
+
enabled: boolean
|
|
16
|
+
|
|
17
|
+
private _target: Movie | Visual
|
|
18
|
+
/**
|
|
19
|
+
* The number of times this effect has been attached to a target minus the
|
|
20
|
+
* number of times it's been detached. (Used for the target's array proxy with
|
|
21
|
+
* `unshift`)
|
|
22
|
+
*/
|
|
23
|
+
private _occurrenceCount: number
|
|
24
|
+
|
|
25
|
+
constructor () {
|
|
26
|
+
const newThis = watchPublic(this) as Base // proxy that will be returned by constructor
|
|
27
|
+
|
|
28
|
+
newThis.enabled = true
|
|
29
|
+
newThis._occurrenceCount = 0
|
|
30
|
+
newThis._target = null
|
|
31
|
+
|
|
32
|
+
// Propogate up to target
|
|
33
|
+
subscribe(newThis, 'effect.change.modify', event => {
|
|
34
|
+
if (!newThis._target) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
const type = `${newThis._target.type}.change.effect.modify`
|
|
38
|
+
publish(newThis._target, type, { ...event, target: newThis._target, source: newThis, type })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return newThis
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
attach (target: Movie | Visual): void {
|
|
45
|
+
this._occurrenceCount++
|
|
46
|
+
this._target = target
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
detach (): void {
|
|
50
|
+
if (this._target === null) {
|
|
51
|
+
throw new Error('No movie to detach from')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this._occurrenceCount--
|
|
55
|
+
// If this effect occurs in another place in the containing array, do not
|
|
56
|
+
// unset _target. (For calling `unshift` on the `layers` proxy)
|
|
57
|
+
if (this._occurrenceCount === 0) {
|
|
58
|
+
this._target = null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// subclasses must implement apply
|
|
63
|
+
/**
|
|
64
|
+
* Apply this effect to a target at the given time
|
|
65
|
+
*
|
|
66
|
+
* @param target
|
|
67
|
+
* @param reltime - the movie's current time relative to the layer
|
|
68
|
+
* (will soon be replaced with an instance getter)
|
|
69
|
+
* @abstract
|
|
70
|
+
*/
|
|
71
|
+
apply (target: Movie | Visual, reltime: number): void {} // eslint-disable-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The current time of the target
|
|
75
|
+
*/
|
|
76
|
+
get currentTime (): number {
|
|
77
|
+
return this._target ? this._target.currentTime : undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get parent (): Movie | Visual {
|
|
81
|
+
return this._target
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get movie (): Movie {
|
|
85
|
+
return this._target ? this._target.movie : undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getDefaultOptions (): Record<string, unknown> {
|
|
89
|
+
return {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// id for events (independent of instance, but easy to access when on prototype
|
|
93
|
+
// chain)
|
|
94
|
+
Base.prototype.type = 'effect'
|
|
95
|
+
Base.prototype.publicExcludes = []
|
|
96
|
+
Base.prototype.propertyFilters = {}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Dynamic } from '../util'
|
|
2
|
+
import { Shader } from './shader'
|
|
3
|
+
|
|
4
|
+
export interface BrightnessOptions {
|
|
5
|
+
brightness?: Dynamic<number>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Changes the brightness
|
|
10
|
+
*/
|
|
11
|
+
export class Brightness extends Shader {
|
|
12
|
+
brightness: Dynamic<number>
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param [brightness=0] - the value to add to each pixel's color
|
|
16
|
+
* channels (between -255 and 255)
|
|
17
|
+
*/
|
|
18
|
+
constructor (options: BrightnessOptions = {}) {
|
|
19
|
+
super({
|
|
20
|
+
fragmentSource: `
|
|
21
|
+
precision mediump float;
|
|
22
|
+
|
|
23
|
+
uniform sampler2D u_Source;
|
|
24
|
+
uniform float u_Brightness;
|
|
25
|
+
|
|
26
|
+
varying highp vec2 v_TextureCoord;
|
|
27
|
+
|
|
28
|
+
void main() {
|
|
29
|
+
vec4 color = texture2D(u_Source, v_TextureCoord);
|
|
30
|
+
vec3 rgb = clamp(color.rgb + u_Brightness / 255.0, 0.0, 1.0);
|
|
31
|
+
gl_FragColor = vec4(rgb, color.a);
|
|
32
|
+
}
|
|
33
|
+
`,
|
|
34
|
+
uniforms: {
|
|
35
|
+
brightness: '1f'
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
/**
|
|
39
|
+
* The value to add to each pixel's color channels (between -255 and 255)
|
|
40
|
+
*/
|
|
41
|
+
this.brightness = options.brightness || 0
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Dynamic } from '../util'
|
|
2
|
+
import { Shader } from './shader'
|
|
3
|
+
|
|
4
|
+
export interface ChannelsOptions {
|
|
5
|
+
factors?: Dynamic<{
|
|
6
|
+
r?: number,
|
|
7
|
+
g?: number,
|
|
8
|
+
b?: number
|
|
9
|
+
}>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Multiplies each channel by a different factor
|
|
14
|
+
*/
|
|
15
|
+
export class Channels extends Shader {
|
|
16
|
+
factors: Dynamic<{
|
|
17
|
+
r?: number,
|
|
18
|
+
b?: number,
|
|
19
|
+
g?: number
|
|
20
|
+
}>
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param factors - channel factors, each defaulting to 1
|
|
24
|
+
*/
|
|
25
|
+
constructor (options: ChannelsOptions = {}) {
|
|
26
|
+
super({
|
|
27
|
+
fragmentSource: `
|
|
28
|
+
precision mediump float;
|
|
29
|
+
|
|
30
|
+
uniform sampler2D u_Source;
|
|
31
|
+
uniform vec4 u_Factors;
|
|
32
|
+
|
|
33
|
+
varying highp vec2 v_TextureCoord;
|
|
34
|
+
|
|
35
|
+
void main() {
|
|
36
|
+
vec4 color = texture2D(u_Source, v_TextureCoord);
|
|
37
|
+
gl_FragColor = clamp(u_Factors * color, 0.0, 1.0);
|
|
38
|
+
}
|
|
39
|
+
`,
|
|
40
|
+
uniforms: {
|
|
41
|
+
factors: { type: '4fv', defaultFloatComponent: 1 }
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Channel factors, each defaulting to 1
|
|
47
|
+
*/
|
|
48
|
+
this.factors = options.factors || {}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Dynamic, Color } from '../util'
|
|
2
|
+
import { Shader } from './shader'
|
|
3
|
+
|
|
4
|
+
export interface ChromaKeyOptions {
|
|
5
|
+
target?: Dynamic<Color>
|
|
6
|
+
threshold?: Dynamic<number>
|
|
7
|
+
interpolate?: Dynamic<boolean>
|
|
8
|
+
// smoothingSharpness
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reduces alpha for pixels which are close to a specified target color
|
|
13
|
+
*/
|
|
14
|
+
export class ChromaKey extends Shader {
|
|
15
|
+
target: Dynamic<Color>
|
|
16
|
+
threshold: Dynamic<number>
|
|
17
|
+
interpolate: Dynamic<boolean>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param [target={r: 0, g: 0, b: 0, a: 1}] - the color to remove
|
|
21
|
+
* @param [threshold=0] - how much error to allow
|
|
22
|
+
* @param [interpolate=false] - <code>true</code> to interpolate
|
|
23
|
+
* the alpha channel, or <code>false</code> value for no smoothing (i.e. an
|
|
24
|
+
* alpha of either 0 or 255)
|
|
25
|
+
*/
|
|
26
|
+
// TODO: Use <code>smoothingSharpness</code>
|
|
27
|
+
constructor (options: ChromaKeyOptions = {}) {
|
|
28
|
+
super({
|
|
29
|
+
fragmentSource: `
|
|
30
|
+
precision mediump float;
|
|
31
|
+
|
|
32
|
+
uniform sampler2D u_Source;
|
|
33
|
+
uniform vec3 u_Target;
|
|
34
|
+
uniform float u_Threshold;
|
|
35
|
+
uniform bool u_Interpolate;
|
|
36
|
+
|
|
37
|
+
varying highp vec2 v_TextureCoord;
|
|
38
|
+
|
|
39
|
+
void main() {
|
|
40
|
+
vec4 color = texture2D(u_Source, v_TextureCoord);
|
|
41
|
+
float alpha = color.a;
|
|
42
|
+
vec3 dist = abs(color.rgb - u_Target / 255.0);
|
|
43
|
+
if (!u_Interpolate) {
|
|
44
|
+
// Standard way that most video editors probably use (all-or-nothing method)
|
|
45
|
+
float thresh = u_Threshold / 255.0;
|
|
46
|
+
bool transparent = dist.r <= thresh && dist.g <= thresh && dist.b <= thresh;
|
|
47
|
+
if (transparent)
|
|
48
|
+
alpha = 0.0;
|
|
49
|
+
} else {
|
|
50
|
+
/*
|
|
51
|
+
better way IMHO:
|
|
52
|
+
Take the average of the absolute differences between the pixel and the target for each channel
|
|
53
|
+
*/
|
|
54
|
+
float transparency = (dist.r + dist.g + dist.b) / 3.0;
|
|
55
|
+
// TODO: custom or variety of interpolation methods
|
|
56
|
+
alpha = transparency;
|
|
57
|
+
}
|
|
58
|
+
gl_FragColor = vec4(color.rgb, alpha);
|
|
59
|
+
}
|
|
60
|
+
`,
|
|
61
|
+
uniforms: {
|
|
62
|
+
target: '3fv',
|
|
63
|
+
threshold: '1f',
|
|
64
|
+
interpolate: '1i'
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
/**
|
|
68
|
+
* The color to remove
|
|
69
|
+
*/
|
|
70
|
+
this.target = options.target || new Color(0, 0, 0)
|
|
71
|
+
/**
|
|
72
|
+
* How much error to allow
|
|
73
|
+
*/
|
|
74
|
+
this.threshold = options.threshold || 0
|
|
75
|
+
/**
|
|
76
|
+
* <code>true<code> to interpolate the alpha channel, or <code>false<code>
|
|
77
|
+
* for no smoothing (i.e. 255 or 0 alpha)
|
|
78
|
+
*/
|
|
79
|
+
this.interpolate = options.interpolate || false
|
|
80
|
+
// this.smoothingSharpness = smoothingSharpness;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Dynamic } from '../util'
|
|
2
|
+
import { Shader } from './shader'
|
|
3
|
+
|
|
4
|
+
export interface ContrastOptions {
|
|
5
|
+
contrast?: Dynamic<number>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Changes the contrast by multiplying the RGB channels by a constant
|
|
10
|
+
*/
|
|
11
|
+
export class Contrast extends Shader {
|
|
12
|
+
contrast: Dynamic<number>
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param [contrast=1] - the contrast multiplier
|
|
16
|
+
*/
|
|
17
|
+
constructor (options: ContrastOptions = {}) {
|
|
18
|
+
super({
|
|
19
|
+
fragmentSource: `
|
|
20
|
+
precision mediump float;
|
|
21
|
+
|
|
22
|
+
uniform sampler2D u_Source;
|
|
23
|
+
uniform float u_Contrast;
|
|
24
|
+
|
|
25
|
+
varying highp vec2 v_TextureCoord;
|
|
26
|
+
|
|
27
|
+
void main() {
|
|
28
|
+
vec4 color = texture2D(u_Source, v_TextureCoord);
|
|
29
|
+
vec3 rgb = clamp(u_Contrast * (color.rgb - 0.5) + 0.5, 0.0, 1.0);
|
|
30
|
+
gl_FragColor = vec4(rgb, color.a);
|
|
31
|
+
}
|
|
32
|
+
`,
|
|
33
|
+
uniforms: {
|
|
34
|
+
contrast: '1f'
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
/**
|
|
38
|
+
* The contrast multiplier
|
|
39
|
+
*/
|
|
40
|
+
this.contrast = options.contrast || 1
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Movie } from '../movie'
|
|
2
|
+
import { Dynamic, val } from '../util'
|
|
3
|
+
import { Base } from './base'
|
|
4
|
+
import { Visual } from '../layer/index'
|
|
5
|
+
|
|
6
|
+
export class EllipticalMaskOptions {
|
|
7
|
+
x: Dynamic<number>
|
|
8
|
+
y: Dynamic<number>
|
|
9
|
+
radiusX: Dynamic<number>
|
|
10
|
+
radiusY: Dynamic<number>
|
|
11
|
+
rotation?: Dynamic<number>
|
|
12
|
+
startAngle?: Dynamic<number>
|
|
13
|
+
endAngle?: Dynamic<number>
|
|
14
|
+
anticlockwise?: Dynamic<boolean>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Preserves an ellipse of the layer and clears the rest
|
|
19
|
+
*/
|
|
20
|
+
// TODO: Parent layer mask effects will make more complex masks easier
|
|
21
|
+
export class EllipticalMask extends Base {
|
|
22
|
+
x: Dynamic<number>
|
|
23
|
+
y: Dynamic<number>
|
|
24
|
+
radiusX: Dynamic<number>
|
|
25
|
+
radiusY: Dynamic<number>
|
|
26
|
+
rotation: Dynamic<number>
|
|
27
|
+
startAngle: Dynamic<number>
|
|
28
|
+
endAngle: Dynamic<number>
|
|
29
|
+
anticlockwise: Dynamic<boolean>
|
|
30
|
+
|
|
31
|
+
private _tmpCanvas
|
|
32
|
+
private _tmpCtx
|
|
33
|
+
|
|
34
|
+
constructor (options: EllipticalMaskOptions) {
|
|
35
|
+
super()
|
|
36
|
+
this.x = options.x
|
|
37
|
+
this.y = options.y
|
|
38
|
+
this.radiusX = options.radiusX
|
|
39
|
+
this.radiusY = options.radiusY
|
|
40
|
+
this.rotation = options.rotation || 0
|
|
41
|
+
this.startAngle = options.startAngle || 0
|
|
42
|
+
this.endAngle = options.endAngle !== undefined ? options.endAngle : 2 * Math.PI
|
|
43
|
+
this.anticlockwise = options.anticlockwise || false
|
|
44
|
+
// for saving image data before clearing
|
|
45
|
+
this._tmpCanvas = document.createElement('canvas')
|
|
46
|
+
this._tmpCtx = this._tmpCanvas.getContext('2d')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
apply (target: Movie | Visual, reltime: number): void {
|
|
50
|
+
const ctx = target.cctx
|
|
51
|
+
const canvas = target.canvas
|
|
52
|
+
const x = val(this, 'x', reltime)
|
|
53
|
+
const y = val(this, 'y', reltime)
|
|
54
|
+
const radiusX = val(this, 'radiusX', reltime)
|
|
55
|
+
const radiusY = val(this, 'radiusY', reltime)
|
|
56
|
+
const rotation = val(this, 'rotation', reltime)
|
|
57
|
+
const startAngle = val(this, 'startAngle', reltime)
|
|
58
|
+
const endAngle = val(this, 'endAngle', reltime)
|
|
59
|
+
const anticlockwise = val(this, 'anticlockwise', reltime)
|
|
60
|
+
this._tmpCanvas.width = target.canvas.width
|
|
61
|
+
this._tmpCanvas.height = target.canvas.height
|
|
62
|
+
this._tmpCtx.drawImage(canvas, 0, 0)
|
|
63
|
+
|
|
64
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
65
|
+
ctx.save() // idk how to preserve clipping state without save/restore
|
|
66
|
+
// create elliptical path and clip
|
|
67
|
+
ctx.beginPath()
|
|
68
|
+
ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)
|
|
69
|
+
ctx.closePath()
|
|
70
|
+
ctx.clip()
|
|
71
|
+
// render image with clipping state
|
|
72
|
+
ctx.drawImage(this._tmpCanvas, 0, 0)
|
|
73
|
+
ctx.restore()
|
|
74
|
+
}
|
|
75
|
+
}
|