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
@@ -0,0 +1,156 @@
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
+ attach (movie: Movie): void {
74
+ this._occurrenceCount++
75
+ this._movie = movie
76
+ }
77
+
78
+ detach (): void {
79
+ if (this.movie === null) {
80
+ throw new Error('No movie to detach from')
81
+ }
82
+
83
+ this._occurrenceCount--
84
+ // If this layer occurs in another place in a `layers` array, do not unset
85
+ // _movie. (For calling `unshift` on the `layers` proxy)
86
+ if (this._occurrenceCount === 0) {
87
+ this._movie = null
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Called when the layer is activated
93
+ */
94
+ start (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
95
+
96
+ /**
97
+ * Called when the movie renders and the layer is active
98
+ */
99
+ render (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
100
+
101
+ /**
102
+ * Called when the layer is deactivated
103
+ */
104
+ stop (): void {} // eslint-disable-line @typescript-eslint/no-empty-function
105
+
106
+ // TODO: is this needed?
107
+ get parent (): Movie {
108
+ return this._movie
109
+ }
110
+
111
+ /**
112
+ */
113
+ get startTime (): number {
114
+ return this._startTime
115
+ }
116
+
117
+ set startTime (val: number) {
118
+ this._startTime = val
119
+ }
120
+
121
+ /**
122
+ * The current time of the movie relative to this layer
123
+ */
124
+ get currentTime (): number {
125
+ return this._movie ? this._movie.currentTime - this.startTime
126
+ : undefined
127
+ }
128
+
129
+ /**
130
+ */
131
+ get duration (): number {
132
+ return this._duration
133
+ }
134
+
135
+ set duration (val: number) {
136
+ this._duration = val
137
+ }
138
+
139
+ get movie (): Movie {
140
+ return this._movie
141
+ }
142
+
143
+ getDefaultOptions (): BaseOptions {
144
+ return {
145
+ startTime: undefined, // required
146
+ duration: undefined // required
147
+ }
148
+ }
149
+ }
150
+ // id for events (independent of instance, but easy to access when on prototype
151
+ // chain)
152
+ Base.prototype.type = 'layer'
153
+ Base.prototype.publicExcludes = []
154
+ Base.prototype.propertyFilters = {}
155
+
156
+ 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,198 @@
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
+ /**
136
+ * Convienence method for <code>effects.push()</code>
137
+ * @param effect
138
+ * @return the layer (for chaining)
139
+ */
140
+ addEffect (effect: BaseEffect): Visual {
141
+ this.effects.push(effect); return this
142
+ }
143
+
144
+ getDefaultOptions (): VisualOptions {
145
+ return {
146
+ ...Base.prototype.getDefaultOptions(),
147
+ /**
148
+ * @name module:layer.Visual#x
149
+ * @desc The offset of the layer relative to the movie
150
+ */
151
+ x: 0,
152
+ /**
153
+ * @name module:layer.Visual#y
154
+ * @desc The offset of the layer relative to the movie
155
+ */
156
+ y: 0,
157
+ /**
158
+ * @name module:layer.Visual#width
159
+ */
160
+ width: null,
161
+ /**
162
+ * @name module:layer.Visual#height
163
+ */
164
+ height: null,
165
+ /**
166
+ * @name module:layer.Visual#background
167
+ * @desc The CSS color code for the background, or <code>null</code> for
168
+ * transparency
169
+ */
170
+ background: null,
171
+ /**
172
+ * @name module:layer.Visual#border
173
+ * @desc The CSS border style, or <code>null</code> for no border
174
+ */
175
+ border: null,
176
+ /**
177
+ * @name module:layer.Visual#opacity
178
+ */
179
+ opacity: 1
180
+ }
181
+ }
182
+ }
183
+ Visual.prototype.publicExcludes = Base.prototype.publicExcludes.concat(['canvas', 'cctx', 'effects'])
184
+ Visual.prototype.propertyFilters = {
185
+ ...Base.prototype.propertyFilters,
186
+ /*
187
+ * If this.width or this.height is null, that means "take all available screen
188
+ * space", so set it to this._move.width or this._movie.height, respectively
189
+ */
190
+ width: function (width) {
191
+ return width != undefined ? width : this._movie.width // eslint-disable-line eqeqeq
192
+ },
193
+ height: function (height) {
194
+ return height != undefined ? height : this._movie.height // eslint-disable-line eqeqeq
195
+ }
196
+ }
197
+
198
+ export { Visual, VisualOptions }