etro 0.10.1 → 0.12.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 +17 -4
- package/.github/workflows/shipjs-trigger.yml +4 -1
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +10 -0
- package/.husky/prepare-commit-msg +11 -0
- package/CHANGELOG.md +24 -0
- package/CONTRIBUTING.md +18 -43
- package/README.md +1 -2
- package/commitlint.config.ts +39 -0
- package/dist/etro-cjs.js +83 -47
- package/dist/etro-iife.js +83 -47
- package/dist/layer/text.d.ts +16 -1
- package/dist/movie/movie.d.ts +20 -3
- package/karma.conf.js +10 -3
- package/package.json +14 -6
- package/scripts/effect/save-effect-samples.js +1 -1
- package/ship.config.js +12 -1
- package/src/layer/audio-source.ts +17 -28
- package/src/layer/text.ts +48 -2
- package/src/movie/movie.ts +38 -24
package/src/layer/text.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Dynamic, val, applyOptions, Color, parseColor } from '../util'
|
|
2
2
|
import { Visual, VisualOptions } from './visual'
|
|
3
3
|
|
|
4
|
+
enum TextStrokePosition {
|
|
5
|
+
Inside,
|
|
6
|
+
Center,
|
|
7
|
+
Outside,
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
interface TextOptions extends VisualOptions {
|
|
5
11
|
text: Dynamic<string>
|
|
6
12
|
font?: Dynamic<string>
|
|
@@ -24,6 +30,12 @@ interface TextOptions extends VisualOptions {
|
|
|
24
30
|
* @see [`CanvasRenderingContext2D#direction`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
|
|
25
31
|
*/
|
|
26
32
|
textDirection?: Dynamic<string>
|
|
33
|
+
|
|
34
|
+
textStroke?: Dynamic<{
|
|
35
|
+
color: Color,
|
|
36
|
+
position?: TextStrokePosition
|
|
37
|
+
thickness?: number
|
|
38
|
+
}>
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
class Text extends Visual {
|
|
@@ -49,6 +61,11 @@ class Text extends Visual {
|
|
|
49
61
|
* @see [`CanvasRenderingContext2D#direction`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline)
|
|
50
62
|
*/
|
|
51
63
|
textDirection: Dynamic<string>
|
|
64
|
+
textStroke?: Dynamic<{
|
|
65
|
+
color: Color,
|
|
66
|
+
position?: TextStrokePosition
|
|
67
|
+
thickness?: number
|
|
68
|
+
}>
|
|
52
69
|
|
|
53
70
|
private _prevText: string
|
|
54
71
|
private _prevFont: string
|
|
@@ -94,6 +111,34 @@ class Text extends Visual {
|
|
|
94
111
|
maxWidth
|
|
95
112
|
)
|
|
96
113
|
|
|
114
|
+
const textStroke = val(this, 'textStroke', this.currentTime)
|
|
115
|
+
if (textStroke) {
|
|
116
|
+
this.cctx.strokeStyle = textStroke.color
|
|
117
|
+
this.cctx.lineWidth = textStroke.thickness ?? 1
|
|
118
|
+
const position = textStroke.position ?? 'outer'
|
|
119
|
+
// Save the globalCompositeOperation, we have to revert it after stroking the text.
|
|
120
|
+
const globalCompositionOperation = this.cctx.globalCompositeOperation
|
|
121
|
+
switch (position) {
|
|
122
|
+
case TextStrokePosition.Inside:
|
|
123
|
+
this.cctx.globalCompositeOperation = 'source-atop'
|
|
124
|
+
this.cctx.lineWidth *= 2
|
|
125
|
+
break
|
|
126
|
+
case TextStrokePosition.Center:
|
|
127
|
+
break
|
|
128
|
+
case TextStrokePosition.Outside:
|
|
129
|
+
this.cctx.globalCompositeOperation = 'destination-over'
|
|
130
|
+
this.cctx.lineWidth *= 2
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
this.cctx.strokeText(
|
|
134
|
+
text,
|
|
135
|
+
val(this, 'textX', this.currentTime),
|
|
136
|
+
val(this, 'textY', this.currentTime),
|
|
137
|
+
maxWidth
|
|
138
|
+
)
|
|
139
|
+
this.cctx.globalCompositeOperation = globalCompositionOperation
|
|
140
|
+
}
|
|
141
|
+
|
|
97
142
|
this._prevText = text
|
|
98
143
|
this._prevFont = font
|
|
99
144
|
this._prevMaxWidth = maxWidth
|
|
@@ -137,9 +182,10 @@ class Text extends Visual {
|
|
|
137
182
|
maxWidth: null,
|
|
138
183
|
textAlign: 'start',
|
|
139
184
|
textBaseline: 'top',
|
|
140
|
-
textDirection: 'ltr'
|
|
185
|
+
textDirection: 'ltr',
|
|
186
|
+
textStroke: null
|
|
141
187
|
}
|
|
142
188
|
}
|
|
143
189
|
}
|
|
144
190
|
|
|
145
|
-
export { Text, TextOptions }
|
|
191
|
+
export { Text, TextOptions, TextStrokePosition }
|
package/src/movie/movie.ts
CHANGED
|
@@ -74,8 +74,8 @@ export class Movie {
|
|
|
74
74
|
private _recording = false
|
|
75
75
|
private _currentStream: MediaStream
|
|
76
76
|
private _endTime: number
|
|
77
|
-
|
|
78
|
-
private
|
|
77
|
+
/** The timestamp last frame in seconds */
|
|
78
|
+
private _lastRealTime: number
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Creates a new movie.
|
|
@@ -111,11 +111,6 @@ export class Movie {
|
|
|
111
111
|
// `render`). It's only valid while rendering.
|
|
112
112
|
this._renderingFrame = false
|
|
113
113
|
this.currentTime = 0
|
|
114
|
-
|
|
115
|
-
// The last time `play` was called, -1 works well in comparisons
|
|
116
|
-
this._lastPlayed = -1
|
|
117
|
-
// What `currentTime` was when `play` was called
|
|
118
|
-
this._lastPlayedOffset = -1
|
|
119
114
|
}
|
|
120
115
|
|
|
121
116
|
private async _whenReady (): Promise<void> {
|
|
@@ -130,11 +125,13 @@ export class Movie {
|
|
|
130
125
|
*
|
|
131
126
|
* @param [options]
|
|
132
127
|
* @param [options.onStart] Called when the movie starts playing
|
|
128
|
+
* @param [options.duration] The duration of the movie to play in seconds
|
|
133
129
|
*
|
|
134
130
|
* @return Fulfilled when the movie is done playing, never fails
|
|
135
131
|
*/
|
|
136
132
|
async play (options: {
|
|
137
133
|
onStart?: () => void,
|
|
134
|
+
duration?: number,
|
|
138
135
|
} = {}): Promise<void> {
|
|
139
136
|
await this._whenReady()
|
|
140
137
|
|
|
@@ -143,8 +140,8 @@ export class Movie {
|
|
|
143
140
|
}
|
|
144
141
|
|
|
145
142
|
this._paused = this._ended = false
|
|
146
|
-
this.
|
|
147
|
-
this.
|
|
143
|
+
this._lastRealTime = performance.now()
|
|
144
|
+
this._endTime = options.duration ? this.currentTime + options.duration : this.duration
|
|
148
145
|
|
|
149
146
|
options.onStart?.()
|
|
150
147
|
|
|
@@ -155,13 +152,16 @@ export class Movie {
|
|
|
155
152
|
await new Promise<void>(resolve => {
|
|
156
153
|
if (!this.renderingFrame) {
|
|
157
154
|
// Not rendering (and not playing), so play.
|
|
158
|
-
this._render(
|
|
155
|
+
this._render(undefined, resolve)
|
|
159
156
|
}
|
|
160
157
|
|
|
161
158
|
// Stop rendering frame if currently doing so, because playing has higher
|
|
162
159
|
// priority. This will affect the next _render call.
|
|
163
160
|
this._renderingFrame = false
|
|
164
161
|
})
|
|
162
|
+
|
|
163
|
+
// After we're done playing, clear the last timestamp
|
|
164
|
+
this._lastRealTime = undefined
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
@@ -244,12 +244,12 @@ export class Movie {
|
|
|
244
244
|
this._currentStream = new MediaStream(tracks)
|
|
245
245
|
|
|
246
246
|
// Play the movie
|
|
247
|
-
this._endTime = options.duration ? this.currentTime + options.duration : this.duration
|
|
248
247
|
await this.play({
|
|
249
248
|
onStart: () => {
|
|
250
249
|
// Call the user's onStart callback
|
|
251
250
|
options.onStart(this._currentStream)
|
|
252
|
-
}
|
|
251
|
+
},
|
|
252
|
+
duration: options.duration
|
|
253
253
|
})
|
|
254
254
|
|
|
255
255
|
// Clear the stream after the movie is done playing
|
|
@@ -396,11 +396,13 @@ export class Movie {
|
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
/**
|
|
399
|
+
* Processes one frame of the movie and draws it to the canvas
|
|
400
|
+
*
|
|
399
401
|
* @param [timestamp=performance.now()]
|
|
400
402
|
* @param [done=undefined] - Called when done playing or when the current
|
|
401
403
|
* frame is loaded
|
|
402
404
|
*/
|
|
403
|
-
private _render (
|
|
405
|
+
private _render (timestamp = performance.now(), done = undefined) {
|
|
404
406
|
clearCachedValues(this)
|
|
405
407
|
|
|
406
408
|
if (!this.rendering) {
|
|
@@ -444,8 +446,6 @@ export class Movie {
|
|
|
444
446
|
this._currentTime = 0
|
|
445
447
|
publish(this, 'movie.timeupdate', { movie: this })
|
|
446
448
|
|
|
447
|
-
this._lastPlayed = performance.now()
|
|
448
|
-
this._lastPlayedOffset = 0 // this.currentTime
|
|
449
449
|
this._renderingFrame = false
|
|
450
450
|
|
|
451
451
|
// Stop playback or recording if done (except if it's playing and repeat
|
|
@@ -505,7 +505,7 @@ export class Movie {
|
|
|
505
505
|
|
|
506
506
|
// TODO: Is making a new arrow function every frame bad for performance?
|
|
507
507
|
window.requestAnimationFrame(() => {
|
|
508
|
-
this._render(
|
|
508
|
+
this._render(undefined, done)
|
|
509
509
|
})
|
|
510
510
|
}
|
|
511
511
|
|
|
@@ -513,11 +513,12 @@ export class Movie {
|
|
|
513
513
|
// If we're only frame-rendering (current frame only), it doesn't matter if
|
|
514
514
|
// it's paused or not.
|
|
515
515
|
if (!this._renderingFrame) {
|
|
516
|
-
const
|
|
517
|
-
const
|
|
518
|
-
|
|
516
|
+
const timestamp = timestampMs / 1000
|
|
517
|
+
const delta = timestamp - this._lastRealTime
|
|
518
|
+
this._lastRealTime = timestamp
|
|
519
|
+
if (delta > 0) {
|
|
519
520
|
// Update the current time (don't use setter)
|
|
520
|
-
this._currentTime
|
|
521
|
+
this._currentTime += delta
|
|
521
522
|
|
|
522
523
|
// For backwards compatibility, publish a 'movie.timeupdate' event.
|
|
523
524
|
publish(this, 'movie.timeupdate', { movie: this })
|
|
@@ -529,7 +530,12 @@ export class Movie {
|
|
|
529
530
|
}
|
|
530
531
|
}
|
|
531
532
|
|
|
532
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Draws the movie's background to the canvas
|
|
535
|
+
*
|
|
536
|
+
* @param timestamp The current high-resolution timestamp in milliseconds
|
|
537
|
+
*/
|
|
538
|
+
private _renderBackground (timestamp: number) {
|
|
533
539
|
this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
534
540
|
|
|
535
541
|
// Evaluate background color (since it's a dynamic property)
|
|
@@ -541,7 +547,7 @@ export class Movie {
|
|
|
541
547
|
}
|
|
542
548
|
|
|
543
549
|
/**
|
|
544
|
-
*
|
|
550
|
+
* Ticks all layers and renders them to the canvas
|
|
545
551
|
*/
|
|
546
552
|
private _renderLayers () {
|
|
547
553
|
for (let i = 0; i < this.layers.length; i++) {
|
|
@@ -595,6 +601,12 @@ export class Movie {
|
|
|
595
601
|
}
|
|
596
602
|
}
|
|
597
603
|
|
|
604
|
+
/**
|
|
605
|
+
* Applies all of the movie's effects to the canvas
|
|
606
|
+
*
|
|
607
|
+
* Note: This method only applies the movie's effects, not the layers'
|
|
608
|
+
* effects.
|
|
609
|
+
*/
|
|
598
610
|
private _applyEffects () {
|
|
599
611
|
for (let i = 0; i < this.effects.length; i++) {
|
|
600
612
|
const effect = this.effects[i]
|
|
@@ -624,7 +636,7 @@ export class Movie {
|
|
|
624
636
|
|
|
625
637
|
return new Promise(resolve => {
|
|
626
638
|
this._renderingFrame = true
|
|
627
|
-
this._render(
|
|
639
|
+
this._render(undefined, resolve)
|
|
628
640
|
})
|
|
629
641
|
}
|
|
630
642
|
|
|
@@ -665,13 +677,14 @@ export class Movie {
|
|
|
665
677
|
*
|
|
666
678
|
* Calculated from the end time of the last layer
|
|
667
679
|
*/
|
|
668
|
-
// TODO:
|
|
680
|
+
// TODO: cache
|
|
669
681
|
get duration (): number {
|
|
670
682
|
return this.layers.reduce((end, layer) => Math.max(layer.startTime + layer.duration, end), 0)
|
|
671
683
|
}
|
|
672
684
|
|
|
673
685
|
/**
|
|
674
686
|
* Convenience method for `layers.push()`
|
|
687
|
+
*
|
|
675
688
|
* @param layer
|
|
676
689
|
* @return The movie
|
|
677
690
|
*/
|
|
@@ -681,6 +694,7 @@ export class Movie {
|
|
|
681
694
|
|
|
682
695
|
/**
|
|
683
696
|
* Convenience method for `effects.push()`
|
|
697
|
+
*
|
|
684
698
|
* @param effect
|
|
685
699
|
* @return the movie
|
|
686
700
|
*/
|