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.
Files changed (106) hide show
  1. package/.github/workflows/nodejs.yml +1 -1
  2. package/CHANGELOG.md +45 -1
  3. package/CONTRIBUTING.md +23 -19
  4. package/README.md +81 -26
  5. package/dist/effect/base.d.ts +38 -0
  6. package/dist/effect/brightness.d.ts +16 -0
  7. package/dist/effect/channels.d.ts +23 -0
  8. package/dist/effect/chroma-key.d.ts +23 -0
  9. package/dist/effect/contrast.d.ts +15 -0
  10. package/dist/effect/elliptical-mask.d.ts +31 -0
  11. package/dist/effect/gaussian-blur.d.ts +60 -0
  12. package/dist/effect/grayscale.d.ts +7 -0
  13. package/dist/effect/index.d.ts +15 -0
  14. package/dist/effect/pixelate.d.ts +18 -0
  15. package/dist/effect/shader.d.ts +99 -0
  16. package/dist/effect/stack.d.ts +23 -0
  17. package/dist/effect/transform.d.ts +73 -0
  18. package/dist/etro-cjs.js +9337 -3331
  19. package/dist/etro-iife.js +9279 -3273
  20. package/dist/etro.d.ts +7 -0
  21. package/dist/event.d.ts +35 -0
  22. package/dist/index.d.ts +6 -0
  23. package/dist/layer/audio-source.d.ts +24 -0
  24. package/dist/layer/audio.d.ts +14 -0
  25. package/dist/layer/base.d.ts +69 -0
  26. package/dist/layer/image.d.ts +6 -0
  27. package/dist/layer/index.d.ts +11 -0
  28. package/dist/layer/text.d.ts +60 -0
  29. package/dist/layer/video.d.ts +11 -0
  30. package/dist/layer/visual-source.d.ts +32 -0
  31. package/dist/layer/visual.d.ts +58 -0
  32. package/dist/movie.d.ts +192 -0
  33. package/dist/object.d.ts +12 -0
  34. package/dist/util.d.ts +125 -0
  35. package/eslint.conf.js +0 -8
  36. package/eslint.example-conf.js +9 -0
  37. package/eslint.typescript-conf.js +5 -0
  38. package/examples/application/readme-screenshot.html +12 -9
  39. package/examples/application/video-player.html +7 -7
  40. package/examples/application/webcam.html +6 -6
  41. package/examples/introduction/audio.html +30 -18
  42. package/examples/introduction/effects.html +14 -10
  43. package/examples/introduction/export.html +32 -25
  44. package/examples/introduction/functions.html +6 -4
  45. package/examples/introduction/hello-world1.html +9 -5
  46. package/examples/introduction/hello-world2.html +5 -5
  47. package/examples/introduction/keyframes.html +35 -23
  48. package/examples/introduction/media.html +26 -18
  49. package/examples/introduction/text.html +9 -5
  50. package/karma.conf.js +1 -1
  51. package/package.json +29 -13
  52. package/rollup.config.js +15 -4
  53. package/scripts/gen-effect-samples.html +29 -25
  54. package/scripts/save-effect-samples.js +14 -15
  55. package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
  56. package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
  57. package/spec/assets/effect/grayscale.png +0 -0
  58. package/spec/assets/effect/original.png +0 -0
  59. package/spec/assets/effect/pixelate.png +0 -0
  60. package/spec/assets/effect/transform/multiply.png +0 -0
  61. package/spec/assets/effect/transform/rotate.png +0 -0
  62. package/spec/assets/effect/transform/scale-fraction.png +0 -0
  63. package/spec/assets/effect/transform/scale.png +0 -0
  64. package/spec/assets/effect/transform/translate-fraction.png +0 -0
  65. package/spec/assets/effect/transform/translate.png +0 -0
  66. package/spec/effect.spec.js +126 -57
  67. package/spec/event.spec.js +14 -0
  68. package/spec/layer.spec.js +175 -18
  69. package/spec/movie.spec.js +191 -7
  70. package/spec/util.spec.js +14 -5
  71. package/src/effect/base.ts +96 -0
  72. package/src/effect/brightness.ts +43 -0
  73. package/src/effect/channels.ts +50 -0
  74. package/src/effect/chroma-key.ts +82 -0
  75. package/src/effect/contrast.ts +42 -0
  76. package/src/effect/elliptical-mask.ts +75 -0
  77. package/src/effect/gaussian-blur.ts +232 -0
  78. package/src/effect/grayscale.ts +34 -0
  79. package/src/effect/index.ts +22 -0
  80. package/src/effect/pixelate.ts +59 -0
  81. package/src/effect/shader.ts +561 -0
  82. package/src/effect/stack.ts +74 -0
  83. package/src/effect/transform.ts +194 -0
  84. package/src/etro.ts +26 -0
  85. package/src/event.ts +118 -0
  86. package/src/index.ts +8 -0
  87. package/src/layer/audio-source.ts +217 -0
  88. package/src/layer/audio.ts +35 -0
  89. package/src/layer/base.ts +156 -0
  90. package/src/layer/image.ts +8 -0
  91. package/src/layer/index.ts +13 -0
  92. package/src/layer/text.ts +138 -0
  93. package/src/layer/video.ts +15 -0
  94. package/src/layer/visual-source.ts +150 -0
  95. package/src/layer/visual.ts +198 -0
  96. package/src/movie.ts +709 -0
  97. package/src/object.ts +14 -0
  98. package/src/util.ts +473 -0
  99. package/tsconfig.json +8 -0
  100. package/screenshots/2019-08-17_0.png +0 -0
  101. package/src/effect.js +0 -1268
  102. package/src/event.js +0 -78
  103. package/src/index.js +0 -23
  104. package/src/layer.js +0 -897
  105. package/src/movie.js +0 -637
  106. package/src/util.js +0 -505
@@ -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
- movie = new etro.Movie(canvas)
11
- movie.addLayer(new etro.layer.Visual(0, 0.5))
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, { type: 'video/mp4' })
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: { 0: 0, 4: 1 },
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: { 0: 'start', 4: 'end' },
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(elem.prop[0])
84
+ expect(etro.val(elem, 'prop', 0)).toBe('start')
85
85
  etro.clearCachedValues(elem.movie)
86
- expect(etro.val(elem, 'prop', 3)).toBe(elem.prop[0])
86
+ expect(etro.val(elem, 'prop', 3)).toBe('start')
87
87
  etro.clearCachedValues(elem.movie)
88
- expect(etro.val(elem, 'prop', 4)).toBe(elem.prop[4])
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
+ }