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.
Files changed (157) hide show
  1. package/.github/workflows/nodejs.yml +4 -2
  2. package/CHANGELOG.md +85 -4
  3. package/CODE_OF_CONDUCT.md +5 -5
  4. package/CONTRIBUTING.md +33 -79
  5. package/README.md +93 -26
  6. package/dist/effect/base.d.ts +51 -0
  7. package/dist/effect/brightness.d.ts +16 -0
  8. package/dist/effect/channels.d.ts +23 -0
  9. package/dist/effect/chroma-key.d.ts +23 -0
  10. package/dist/effect/contrast.d.ts +15 -0
  11. package/dist/effect/elliptical-mask.d.ts +31 -0
  12. package/dist/effect/gaussian-blur.d.ts +60 -0
  13. package/dist/effect/grayscale.d.ts +7 -0
  14. package/dist/effect/index.d.ts +15 -0
  15. package/dist/effect/pixelate.d.ts +18 -0
  16. package/dist/effect/shader.d.ts +99 -0
  17. package/dist/effect/stack.d.ts +23 -0
  18. package/dist/effect/transform.d.ts +73 -0
  19. package/dist/etro-cjs.js +9387 -0
  20. package/dist/etro-iife.js +9390 -0
  21. package/dist/etro.d.ts +7 -0
  22. package/dist/event.d.ts +35 -0
  23. package/dist/index.d.ts +6 -0
  24. package/dist/layer/audio-source.d.ts +24 -0
  25. package/dist/layer/audio.d.ts +14 -0
  26. package/dist/layer/base.d.ts +82 -0
  27. package/dist/layer/image.d.ts +6 -0
  28. package/dist/layer/index.d.ts +11 -0
  29. package/dist/layer/text.d.ts +60 -0
  30. package/dist/layer/video.d.ts +11 -0
  31. package/dist/layer/visual-source.d.ts +32 -0
  32. package/dist/layer/visual.d.ts +58 -0
  33. package/dist/movie.d.ts +192 -0
  34. package/dist/object.d.ts +12 -0
  35. package/dist/util.d.ts +125 -0
  36. package/eslint.conf.js +2 -9
  37. package/eslint.example-conf.js +9 -0
  38. package/eslint.test-conf.js +1 -0
  39. package/eslint.typescript-conf.js +5 -0
  40. package/examples/application/readme-screenshot.html +16 -17
  41. package/examples/application/video-player.html +10 -11
  42. package/examples/application/webcam.html +6 -6
  43. package/examples/introduction/audio.html +30 -18
  44. package/examples/introduction/effects.html +37 -14
  45. package/examples/introduction/export.html +40 -27
  46. package/examples/introduction/functions.html +6 -4
  47. package/examples/introduction/hello-world1.html +9 -5
  48. package/examples/introduction/hello-world2.html +5 -5
  49. package/examples/introduction/keyframes.html +35 -23
  50. package/examples/introduction/media.html +26 -18
  51. package/examples/introduction/text.html +9 -5
  52. package/karma.conf.js +6 -4
  53. package/package.json +34 -13
  54. package/rollup.config.js +19 -3
  55. package/scripts/gen-effect-samples.html +27 -26
  56. package/scripts/save-effect-samples.js +14 -15
  57. package/src/effect/base.ts +115 -0
  58. package/src/effect/brightness.ts +43 -0
  59. package/src/effect/channels.ts +50 -0
  60. package/src/effect/chroma-key.ts +82 -0
  61. package/src/effect/contrast.ts +42 -0
  62. package/src/effect/elliptical-mask.ts +75 -0
  63. package/src/effect/gaussian-blur.ts +232 -0
  64. package/src/effect/grayscale.ts +34 -0
  65. package/src/effect/index.ts +22 -0
  66. package/src/effect/pixelate.ts +58 -0
  67. package/src/effect/shader.ts +557 -0
  68. package/src/effect/stack.ts +77 -0
  69. package/src/effect/transform.ts +193 -0
  70. package/src/etro.ts +26 -0
  71. package/src/event.ts +112 -0
  72. package/src/index.ts +8 -0
  73. package/src/layer/audio-source.ts +219 -0
  74. package/src/layer/audio.ts +34 -0
  75. package/src/layer/base.ts +175 -0
  76. package/src/layer/image.ts +8 -0
  77. package/src/layer/index.ts +13 -0
  78. package/src/layer/text.ts +138 -0
  79. package/src/layer/video.ts +15 -0
  80. package/src/layer/visual-source.ts +150 -0
  81. package/src/layer/visual.ts +197 -0
  82. package/src/movie.ts +701 -0
  83. package/src/object.ts +14 -0
  84. package/src/util.ts +466 -0
  85. package/tsconfig.json +8 -0
  86. package/dist/etro.js +0 -3397
  87. package/docs/effect.js.html +0 -1215
  88. package/docs/event.js.html +0 -145
  89. package/docs/index.html +0 -81
  90. package/docs/index.js.html +0 -92
  91. package/docs/layer.js.html +0 -888
  92. package/docs/module-effect-GaussianBlurComponent.html +0 -345
  93. package/docs/module-effect.Brightness.html +0 -339
  94. package/docs/module-effect.Channels.html +0 -319
  95. package/docs/module-effect.ChromaKey.html +0 -611
  96. package/docs/module-effect.Contrast.html +0 -339
  97. package/docs/module-effect.EllipticalMask.html +0 -200
  98. package/docs/module-effect.GaussianBlur.html +0 -202
  99. package/docs/module-effect.GaussianBlurHorizontal.html +0 -242
  100. package/docs/module-effect.GaussianBlurVertical.html +0 -242
  101. package/docs/module-effect.Pixelate.html +0 -330
  102. package/docs/module-effect.Shader.html +0 -1227
  103. package/docs/module-effect.Stack.html +0 -406
  104. package/docs/module-effect.Transform.Matrix.html +0 -193
  105. package/docs/module-effect.Transform.html +0 -1174
  106. package/docs/module-effect.html +0 -148
  107. package/docs/module-event.html +0 -473
  108. package/docs/module-index.html +0 -186
  109. package/docs/module-layer-Media.html +0 -1116
  110. package/docs/module-layer-MediaMixin.html +0 -164
  111. package/docs/module-layer.Audio.html +0 -1188
  112. package/docs/module-layer.Base.html +0 -629
  113. package/docs/module-layer.Image.html +0 -1421
  114. package/docs/module-layer.Text.html +0 -1731
  115. package/docs/module-layer.Video.html +0 -1938
  116. package/docs/module-layer.Visual.html +0 -1698
  117. package/docs/module-layer.html +0 -137
  118. package/docs/module-movie.html +0 -3118
  119. package/docs/module-util.Color.html +0 -702
  120. package/docs/module-util.Font.html +0 -395
  121. package/docs/module-util.html +0 -845
  122. package/docs/movie.js.html +0 -689
  123. package/docs/scripts/collapse.js +0 -20
  124. package/docs/scripts/linenumber.js +0 -25
  125. package/docs/scripts/nav.js +0 -12
  126. package/docs/scripts/polyfill.js +0 -4
  127. package/docs/scripts/prettify/Apache-License-2.0.txt +0 -202
  128. package/docs/scripts/prettify/lang-css.js +0 -2
  129. package/docs/scripts/prettify/prettify.js +0 -28
  130. package/docs/scripts/search.js +0 -83
  131. package/docs/styles/jsdoc.css +0 -671
  132. package/docs/styles/prettify.css +0 -79
  133. package/docs/util.js.html +0 -503
  134. package/screenshots/2019-08-17_0.png +0 -0
  135. package/spec/assets/effect/gaussian-blur-horizontal.png +0 -0
  136. package/spec/assets/effect/gaussian-blur-vertical.png +0 -0
  137. package/spec/assets/effect/original.png +0 -0
  138. package/spec/assets/effect/pixelate.png +0 -0
  139. package/spec/assets/effect/transform/multiply.png +0 -0
  140. package/spec/assets/effect/transform/rotate.png +0 -0
  141. package/spec/assets/effect/transform/scale-fraction.png +0 -0
  142. package/spec/assets/effect/transform/scale.png +0 -0
  143. package/spec/assets/effect/transform/translate-fraction.png +0 -0
  144. package/spec/assets/effect/transform/translate.png +0 -0
  145. package/spec/assets/layer/audio.wav +0 -0
  146. package/spec/assets/layer/image.jpg +0 -0
  147. package/spec/effect.spec.js +0 -352
  148. package/spec/event.spec.js +0 -25
  149. package/spec/layer.spec.js +0 -128
  150. package/spec/movie.spec.js +0 -154
  151. package/spec/util.spec.js +0 -285
  152. package/src/effect.js +0 -1265
  153. package/src/event.js +0 -78
  154. package/src/index.js +0 -23
  155. package/src/layer.js +0 -875
  156. package/src/movie.js +0 -636
  157. 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,8 @@
1
+ import { Visual, VisualOptions } from './visual'
2
+ import { VisualSourceMixin } from './visual-source'
3
+
4
+ type ImageOptions = VisualOptions
5
+
6
+ class Image extends VisualSourceMixin(Visual) {}
7
+
8
+ export { Image, ImageOptions }
@@ -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 }