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/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 }
@@ -74,8 +74,8 @@ export class Movie {
74
74
  private _recording = false
75
75
  private _currentStream: MediaStream
76
76
  private _endTime: number
77
- private _lastPlayed: number
78
- private _lastPlayedOffset: number
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._lastPlayed = performance.now()
147
- this._lastPlayedOffset = this.currentTime
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(true, undefined, resolve)
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 (repeat, timestamp = performance.now(), done = undefined) {
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(repeat, undefined, done)
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 sinceLastPlayed = (timestampMs - this._lastPlayed) / 1000
517
- const currentTime = this._lastPlayedOffset + sinceLastPlayed
518
- if (this.currentTime !== currentTime) {
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 = 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
- private _renderBackground (timestamp) {
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
- * @param [timestamp=performance.now()]
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(false, undefined, resolve)
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: dirty flag?
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
  */