@seams/voice-capture 0.68.0 → 0.69.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/README.md +56 -2
- package/dist/browser/card.js +267 -18
- package/dist/esm/card/index.js +1 -1
- package/dist/esm/card/phraseCard.js +2 -2
- package/dist/esm/card/prompt.js +329 -19
- package/dist/esm/card/ribbon.js +106 -5
- package/dist/esm/card/rowMotion.js +3 -3
- package/dist/esm/card/surface.js +121 -6
- package/dist/esm/card/visual.js +2 -2
- package/dist/esm/chunks/{visual-Di3ojTED.js → visual-DU1OVwCA.js} +35 -3
- package/dist/types/card/index.d.ts +2 -2
- package/dist/types/card/prompt.d.ts +13 -1
- package/dist/types/card/ribbon.d.ts +19 -0
- package/dist/types/card/rowMotion.d.ts +3 -3
- package/dist/types/card/surface.d.ts +63 -0
- package/dist/types/card/visual.d.ts +6 -1
- package/package.json +1 -1
package/dist/esm/card/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PhraseCard } from "./phraseCard.js";
|
|
2
|
-
import { t as VoiceVisual } from "../chunks/visual-
|
|
2
|
+
import { t as VoiceVisual } from "../chunks/visual-DU1OVwCA.js";
|
|
3
3
|
import { VoiceRibbon } from "./ribbon.js";
|
|
4
4
|
import { VoicePrompt, leaveSurface } from "./prompt.js";
|
|
5
5
|
import { openPhraseCard } from "./surface.js";
|
|
@@ -229,7 +229,7 @@ var PhraseCard = class {
|
|
|
229
229
|
duration: RIBBON_MOVE_MS,
|
|
230
230
|
easing: EASE_MOVE
|
|
231
231
|
});
|
|
232
|
-
for (const [index, element] of changed.entries()) fadeIn(element, this.motion.matches,
|
|
232
|
+
for (const [index, element] of changed.entries()) fadeIn(element, this.motion.matches, 0 + index * 20);
|
|
233
233
|
}
|
|
234
234
|
markedText(text) {
|
|
235
235
|
if (typeof text === "string") return [text];
|
|
@@ -444,7 +444,7 @@ function fadeIn(element, reduced, delay) {
|
|
|
444
444
|
transform: "none"
|
|
445
445
|
}], {
|
|
446
446
|
delay: reduced ? 0 : delay,
|
|
447
|
-
duration: reduced ? 150 :
|
|
447
|
+
duration: reduced ? 150 : 140,
|
|
448
448
|
easing: ROW_EASE,
|
|
449
449
|
fill: "both"
|
|
450
450
|
});
|
package/dist/esm/card/prompt.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { styleNonce } from "../config.js";
|
|
2
2
|
import { useSystemVoiceMicrophone } from "../microphoneInput.js";
|
|
3
3
|
import { ROW_EASE } from "./rowMotion.js";
|
|
4
|
-
import { t as VoiceVisual } from "../chunks/visual-
|
|
4
|
+
import { t as VoiceVisual } from "../chunks/visual-DU1OVwCA.js";
|
|
5
5
|
//#region src/card/prompt.ts
|
|
6
6
|
const CSS = `
|
|
7
7
|
.seams-voiceid-activation-surface { width: min(100%, 26.25rem); margin: 0 auto; background: transparent; }
|
|
@@ -22,7 +22,9 @@ const CSS = `
|
|
|
22
22
|
margin: 0 auto;
|
|
23
23
|
padding: 3.5rem 1.5rem 1.5rem;
|
|
24
24
|
display: grid;
|
|
25
|
-
|
|
25
|
+
/* Named rows. The context row is empty (0 high) unless the card shows the text that its sentence
|
|
26
|
+
speaks for. A host that sets its own rows keeps these names. */
|
|
27
|
+
grid-template-rows: [context] auto [title] auto [phrase] minmax(6rem, auto) [line] 5rem [status] minmax(3.375rem, auto) [action] minmax(5rem, auto);
|
|
26
28
|
border: 1px solid var(--seams-colors-borderPrimary, #303b41);
|
|
27
29
|
border-radius: var(--seams-shape-card, 16px);
|
|
28
30
|
background: var(--seams-colors-colorBackground, #192024);
|
|
@@ -54,12 +56,14 @@ const CSS = `
|
|
|
54
56
|
}
|
|
55
57
|
/* A card that holds focus itself (until its first control is ready, or after a confirm). */
|
|
56
58
|
.seams-voiceid-prompt[tabindex="-1"]:focus { outline: none; }
|
|
57
|
-
/* Each line owns its row. The download overlays rows
|
|
58
|
-
|
|
59
|
-
.seams-voiceid-prompt > .seams-voiceid-
|
|
60
|
-
.seams-voiceid-prompt >
|
|
61
|
-
.seams-voiceid-prompt > .seams-voiceid-
|
|
62
|
-
.seams-voiceid-
|
|
59
|
+
/* Each line owns its row. The download overlays the rows line, status and action; an auto-placed
|
|
60
|
+
line would move to new rows. */
|
|
61
|
+
.seams-voiceid-prompt > .seams-voiceid-context { grid-row: context; grid-column: 1; }
|
|
62
|
+
.seams-voiceid-prompt > h2 { grid-row: title; grid-column: 1; }
|
|
63
|
+
.seams-voiceid-prompt > .seams-voiceid-phrase { grid-row: phrase; grid-column: 1; }
|
|
64
|
+
.seams-voiceid-prompt > .seams-voiceid-ribbon { grid-row: line; grid-column: 1; }
|
|
65
|
+
.seams-voiceid-prompt > .seams-voiceid-status { grid-row: status; grid-column: 1; }
|
|
66
|
+
.seams-voiceid-progress-slot { grid-row: action; grid-column: 1; min-width: 0; align-self: center; }
|
|
63
67
|
.seams-voiceid-progress-slot:empty { visibility: hidden; }
|
|
64
68
|
.seams-voiceid-activation-surface .seams-voiceid-record-slot { transition: opacity 200ms cubic-bezier(0.2, 0, 0, 1) 80ms, visibility 0s linear 0s; }
|
|
65
69
|
.seams-voiceid-activation-surface[data-preparation="loading"] .seams-voiceid-record-slot { opacity: 0; visibility: hidden; transition: none; }
|
|
@@ -255,6 +259,23 @@ const CSS = `
|
|
|
255
259
|
overflow: visible;
|
|
256
260
|
}
|
|
257
261
|
.seams-voiceid-ribbon .seams-voiceid-ribbon-check { stroke: var(--seams-colors-colorBackground, #192024); }
|
|
262
|
+
/* The theme can colour the voice line (with its ring and its seal) and its three echo lines,
|
|
263
|
+
and set their widths. With none of these values, the line has the card's text colour. */
|
|
264
|
+
.seams-voiceid-ribbon { color: var(--seams-colors-voiceLine, currentColor); }
|
|
265
|
+
.seams-voiceid-ribbon .seams-voiceid-ribbon-line { stroke-width: var(--seams-shape-voiceLineWidth, 1px); }
|
|
266
|
+
.seams-voiceid-ribbon .seams-voiceid-ribbon-echo {
|
|
267
|
+
stroke-width: var(--seams-shape-voiceEchoWidth, 0.65px);
|
|
268
|
+
color: var(--seams-colors-voiceEcho, var(--seams-colors-voiceLine, currentColor));
|
|
269
|
+
}
|
|
270
|
+
.seams-voiceid-ribbon .seams-voiceid-ribbon-echo[data-layer="2"] {
|
|
271
|
+
color: var(--seams-colors-voiceEchoDeep, var(--seams-colors-voiceEcho, var(--seams-colors-voiceLine, currentColor)));
|
|
272
|
+
}
|
|
273
|
+
/* The pixel style (voiceLine: 'pixels'): dots in X blue tones by default, the lightest in the
|
|
274
|
+
middle row. The theme colours the three bands: voiceEcho (the middle row), voiceLine (rows
|
|
275
|
+
1 and 2 from it) and voiceEchoDeep (rows 3 and 4). */
|
|
276
|
+
.seams-voiceid-ribbon-pixel { fill: var(--seams-colors-voiceEchoDeep, #1a6fb0); }
|
|
277
|
+
.seams-voiceid-ribbon-pixel[data-band="1"] { fill: var(--seams-colors-voiceLine, #1d9bf0); }
|
|
278
|
+
.seams-voiceid-ribbon-pixel[data-band="0"] { fill: var(--seams-colors-voiceEcho, #8ecdf8); }
|
|
258
279
|
.seams-voiceid-prompt .seams-voiceid-phrase.seams-voiceid-success-headline {
|
|
259
280
|
display: grid;
|
|
260
281
|
align-content: end;
|
|
@@ -302,8 +323,10 @@ const CSS = `
|
|
|
302
323
|
right: auto;
|
|
303
324
|
left: 0.75rem;
|
|
304
325
|
}
|
|
326
|
+
/* Minimize sits beside Close, in the trailing corner: one button width and a small gap in. */
|
|
327
|
+
.seams-voiceid-prompt .seams-voiceid-close.seams-voiceid-minimize { right: 3.375rem; }
|
|
305
328
|
.seams-voiceid-prompt .seams-voiceid-record-slot {
|
|
306
|
-
grid-row:
|
|
329
|
+
grid-row: action;
|
|
307
330
|
grid-column: 1;
|
|
308
331
|
align-self: center;
|
|
309
332
|
width: 100%;
|
|
@@ -385,7 +408,7 @@ const CSS = `
|
|
|
385
408
|
.seams-voiceid-prompt:has(.seams-voiceid-model-download:not([data-state="ready"])) > .seams-voiceid-phrase { opacity: 0.35; }
|
|
386
409
|
.seams-voiceid-prompt:has(.seams-voiceid-model-download[data-state="failed"]) > .seams-voiceid-record-slot { display: none; }
|
|
387
410
|
.seams-voiceid-model-download progress {
|
|
388
|
-
grid-row:
|
|
411
|
+
grid-row: line;
|
|
389
412
|
justify-self: center;
|
|
390
413
|
appearance: none;
|
|
391
414
|
display: block;
|
|
@@ -403,7 +426,7 @@ const CSS = `
|
|
|
403
426
|
a looping animation would compete with it on a software-rendered page. */
|
|
404
427
|
.seams-voiceid-model-download progress::-moz-progress-bar { background: var(--seams-colors-textPrimary, #eff2f3); border-radius: 999px; }
|
|
405
428
|
.seams-voiceid-model-download p {
|
|
406
|
-
grid-row:
|
|
429
|
+
grid-row: status;
|
|
407
430
|
align-self: start;
|
|
408
431
|
margin: 0;
|
|
409
432
|
font-size: 0.75rem;
|
|
@@ -411,9 +434,9 @@ const CSS = `
|
|
|
411
434
|
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
412
435
|
font-variant-numeric: tabular-nums;
|
|
413
436
|
}
|
|
414
|
-
.seams-voiceid-model-download button { grid-row:
|
|
437
|
+
.seams-voiceid-model-download button { grid-row: action; }
|
|
415
438
|
/* A phrase card shows its check and its result in the same card. */
|
|
416
|
-
.seams-voiceid-prompt > .seams-voiceid-status-line { grid-
|
|
439
|
+
.seams-voiceid-prompt > .seams-voiceid-status-line { grid-row: line; grid-column: 1; }
|
|
417
440
|
.seams-voiceid-status-line {
|
|
418
441
|
transition: opacity 120ms ease;
|
|
419
442
|
display: block;
|
|
@@ -423,7 +446,9 @@ const CSS = `
|
|
|
423
446
|
margin: auto;
|
|
424
447
|
overflow: visible;
|
|
425
448
|
opacity: 0;
|
|
426
|
-
|
|
449
|
+
/* The theme can colour the check's line and the seal that it closes into (the disc behind the
|
|
450
|
+
tick): voiceSeal, else the voice line's colour, else the card's text colour. */
|
|
451
|
+
color: var(--seams-colors-voiceSeal, var(--seams-colors-voiceLine, var(--seams-colors-textPrimary, #eff2f3)));
|
|
427
452
|
}
|
|
428
453
|
.seams-voiceid-ribbon { transition: opacity 120ms ease; }
|
|
429
454
|
.seams-voiceid-prompt[data-phrase-line="status"] > .seams-voiceid-ribbon { opacity: 0; }
|
|
@@ -463,12 +488,29 @@ const CSS = `
|
|
|
463
488
|
.seams-voiceid-prompt .seams-voiceid-phrase.seams-voiceid-phrase-dim { color: var(--seams-colors-textMuted, #6f7f87); }
|
|
464
489
|
.seams-voiceid-prompt[data-phrase-view] > h2 { min-height: 1.19rem; }
|
|
465
490
|
.seams-voiceid-prompt[data-phrase-view="check"] {
|
|
466
|
-
grid-template-rows: auto minmax(6rem, auto) 5rem minmax(3.375rem, auto) 0;
|
|
491
|
+
grid-template-rows: [context] auto [title] auto [phrase] minmax(6rem, auto) [line] 5rem [status] minmax(3.375rem, auto) [action] 0;
|
|
467
492
|
}
|
|
468
493
|
.seams-voiceid-prompt[data-phrase-view="check"] .seams-voiceid-record-slot { display: none; }
|
|
494
|
+
/* A card that records by itself (autoStart) has no Record row: its rows sit in the middle. */
|
|
495
|
+
.seams-voiceid-prompt[data-auto-start] {
|
|
496
|
+
grid-template-rows: [context] auto [title] auto [phrase] minmax(6rem, auto) [line] 5rem [status] minmax(3.375rem, auto) [action] 0;
|
|
497
|
+
}
|
|
498
|
+
.seams-voiceid-prompt[data-auto-start] > .seams-voiceid-record-slot { display: none; }
|
|
499
|
+
/* The result reads from the top as one group: the seal, the headline under it, the caption and
|
|
500
|
+
the detail, then the button. Every row has its content's height, so the card has no empty
|
|
501
|
+
band, and the line row glides to its new place (phraseCard.ts transition). An empty label
|
|
502
|
+
takes no room. */
|
|
503
|
+
.seams-voiceid-prompt[data-phrase-view="result"] {
|
|
504
|
+
grid-template-rows: [context] auto [title] auto [line] 4.5rem [phrase] auto [status] auto [action] auto;
|
|
505
|
+
}
|
|
506
|
+
.seams-voiceid-prompt[data-phrase-view="result"] > h2:empty { display: none; }
|
|
507
|
+
.seams-voiceid-prompt[data-phrase-view="result"] > .seams-voiceid-phrase { margin-top: 0.5rem; }
|
|
508
|
+
.seams-voiceid-prompt[data-phrase-view="result"] > .seams-voiceid-status { min-height: 0; margin-top: 0.375rem; }
|
|
509
|
+
.seams-voiceid-prompt[data-phrase-view="result"] > .seams-voiceid-record-slot { margin-top: 1.5rem; }
|
|
469
510
|
.seams-voiceid-prompt[data-phrase-view] .seams-voiceid-microphone { display: none; }
|
|
470
511
|
.seams-voiceid-prompt .seams-voiceid-close { transition: opacity 150ms ease, visibility 0s linear 0s; }
|
|
471
|
-
|
|
512
|
+
/* The check hides Back and Close, as the caller works; Minimize stays. */
|
|
513
|
+
.seams-voiceid-prompt[data-phrase-view="check"] .seams-voiceid-close:not(.seams-voiceid-minimize) {
|
|
472
514
|
opacity: 0;
|
|
473
515
|
visibility: hidden;
|
|
474
516
|
transition: opacity 150ms ease, visibility 0s linear 150ms;
|
|
@@ -505,6 +547,213 @@ const CSS = `
|
|
|
505
547
|
@media (prefers-reduced-motion: reduce) {
|
|
506
548
|
.seams-voiceid-status-line rect { animation: none !important; transition: none; }
|
|
507
549
|
}
|
|
550
|
+
/* The note layout: a card with a context, in the shape of a form. The context (a note) sits
|
|
551
|
+
in a soft box under its label, the sentence to read sits centred in a blue box under its own
|
|
552
|
+
label, and one button right under it changes with the card: Record or the voice line while
|
|
553
|
+
it records, the check, then the result's action. The status sits at the foot, under the
|
|
554
|
+
button. */
|
|
555
|
+
.seams-voiceid-prompt[data-context] {
|
|
556
|
+
grid-template-rows: [context] auto [title] auto [phrase] auto [line] 4.5rem [action] 0 [status] auto;
|
|
557
|
+
text-align: start;
|
|
558
|
+
/* The height of the button: 4.5rem as the voice line, with either line style, and 3.5rem as
|
|
559
|
+
Record or a result's action. The microphone's circle follows it. */
|
|
560
|
+
--seams-voiceid-button-height: 4.5rem;
|
|
561
|
+
/* The fill of what the person says: a tint of the main button's colour (voiceFill). */
|
|
562
|
+
--seams-voiceid-tint: 16%;
|
|
563
|
+
--seams-voiceid-fill: var(--seams-colors-voiceFill, color-mix(in srgb, var(--seams-colors-buttonBackground, #1d9bf0) var(--seams-voiceid-tint), var(--seams-colors-colorBackground, #192024)));
|
|
564
|
+
}
|
|
565
|
+
/* A form is as wide as an app's dialog for a note: 560 px. */
|
|
566
|
+
.seams-voiceid-activation-surface:has(> .seams-voiceid-prompt[data-context]),
|
|
567
|
+
.seams-voiceid-prompt[data-context] {
|
|
568
|
+
width: min(100%, 35rem);
|
|
569
|
+
}
|
|
570
|
+
[data-theme="light"] .seams-voiceid-prompt[data-context] { --seams-voiceid-tint: 14%; }
|
|
571
|
+
[data-theme="dark"] .seams-voiceid-prompt[data-context] { --seams-voiceid-tint: 24%; }
|
|
572
|
+
/* A button (Record, Try again, a result's action) takes the place of the voice line. */
|
|
573
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="check"]):has(> .seams-voiceid-record-slot > .seams-voiceid-primary:not([hidden])) {
|
|
574
|
+
--seams-voiceid-button-height: 3.5rem;
|
|
575
|
+
grid-template-rows: [context] auto [title] auto [phrase] auto [line] 0 [action] auto [status] auto;
|
|
576
|
+
}
|
|
577
|
+
/* A result's caption and detail read before its button. */
|
|
578
|
+
.seams-voiceid-prompt[data-context][data-phrase-view="result"]:not([data-phrase-view="check"]):has(> .seams-voiceid-record-slot > .seams-voiceid-primary:not([hidden])) {
|
|
579
|
+
grid-template-rows: [context] auto [title] auto [phrase] auto [status] auto [line] 0 [action] auto;
|
|
580
|
+
}
|
|
581
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="check"]):has(> .seams-voiceid-record-slot > .seams-voiceid-primary:not([hidden])) > :is(.seams-voiceid-ribbon, .seams-voiceid-status-line) {
|
|
582
|
+
visibility: hidden;
|
|
583
|
+
}
|
|
584
|
+
.seams-voiceid-prompt[data-context] > .seams-voiceid-glow { display: none; }
|
|
585
|
+
/* A line across the card under the context: above it is what the person posts, below it is
|
|
586
|
+
the voice signature. */
|
|
587
|
+
.seams-voiceid-prompt .seams-voiceid-context {
|
|
588
|
+
margin: 0 -1.5rem 1.25rem;
|
|
589
|
+
padding: 0 1.5rem 1.25rem;
|
|
590
|
+
border: 0;
|
|
591
|
+
border-bottom: 1px solid var(--seams-colors-borderPrimary, #303b41);
|
|
592
|
+
}
|
|
593
|
+
.seams-voiceid-prompt .seams-voiceid-context-label {
|
|
594
|
+
display: block;
|
|
595
|
+
margin-bottom: 0.375rem;
|
|
596
|
+
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
597
|
+
font-size: 0.9375rem;
|
|
598
|
+
line-height: 1.3;
|
|
599
|
+
}
|
|
600
|
+
.seams-voiceid-prompt .seams-voiceid-context > blockquote {
|
|
601
|
+
max-height: 8.5rem;
|
|
602
|
+
margin: 0;
|
|
603
|
+
padding: 0.75rem 1rem;
|
|
604
|
+
overflow-y: auto;
|
|
605
|
+
border-radius: 1rem;
|
|
606
|
+
background: var(--seams-colors-surface, #202b31);
|
|
607
|
+
color: var(--seams-colors-textPrimary, #eff2f3);
|
|
608
|
+
font-size: 1rem;
|
|
609
|
+
line-height: 1.45;
|
|
610
|
+
white-space: pre-line;
|
|
611
|
+
overflow-wrap: anywhere;
|
|
612
|
+
}
|
|
613
|
+
/* A result has no label: its empty title takes no room. */
|
|
614
|
+
.seams-voiceid-prompt[data-context] > h2:empty { display: none; }
|
|
615
|
+
.seams-voiceid-prompt[data-context] > h2 {
|
|
616
|
+
margin: 0 0 0.625rem;
|
|
617
|
+
color: var(--seams-colors-textPrimary, #eff2f3);
|
|
618
|
+
font-size: 1rem;
|
|
619
|
+
font-weight: 700;
|
|
620
|
+
line-height: 1.3;
|
|
621
|
+
text-align: center;
|
|
622
|
+
}
|
|
623
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-phrase {
|
|
624
|
+
box-sizing: border-box;
|
|
625
|
+
width: 100%;
|
|
626
|
+
max-width: none;
|
|
627
|
+
/* A thin gap to the button: the sentence and the voice belong together. */
|
|
628
|
+
margin: 0 0 0.5rem;
|
|
629
|
+
padding: 0.875rem 1.25rem;
|
|
630
|
+
border-radius: 1rem;
|
|
631
|
+
/* Blue, apart from the grey box of the context: this is what the person says. */
|
|
632
|
+
background: var(--seams-voiceid-fill);
|
|
633
|
+
font-size: 1.25rem;
|
|
634
|
+
font-weight: 600;
|
|
635
|
+
letter-spacing: normal;
|
|
636
|
+
line-height: 1.35;
|
|
637
|
+
text-align: center;
|
|
638
|
+
}
|
|
639
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-phrase.seams-voiceid-success-headline {
|
|
640
|
+
display: block;
|
|
641
|
+
padding: 0;
|
|
642
|
+
background: none;
|
|
643
|
+
font-size: 1.25rem;
|
|
644
|
+
font-weight: 700;
|
|
645
|
+
}
|
|
646
|
+
/* The status sits at the foot of the card, under the button: "Listening", or the check. */
|
|
647
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status {
|
|
648
|
+
min-height: 1.125rem;
|
|
649
|
+
margin: 0;
|
|
650
|
+
padding: 0.625rem 0 0;
|
|
651
|
+
text-align: center;
|
|
652
|
+
}
|
|
653
|
+
.seams-voiceid-prompt[data-context][data-phrase-view="result"] .seams-voiceid-status { padding: 0.25rem 0 0.75rem; }
|
|
654
|
+
/* Under the button, the check's caption and detail share one line at the size of "Listening",
|
|
655
|
+
so the card keeps its height when the check starts. A long line ends with an ellipsis. */
|
|
656
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="result"]) .seams-voiceid-status.seams-voiceid-two-lines {
|
|
657
|
+
overflow: hidden;
|
|
658
|
+
white-space: nowrap;
|
|
659
|
+
text-overflow: ellipsis;
|
|
660
|
+
}
|
|
661
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="result"]) .seams-voiceid-status.seams-voiceid-two-lines > span {
|
|
662
|
+
display: inline;
|
|
663
|
+
}
|
|
664
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="result"]) .seams-voiceid-status .seams-voiceid-caption {
|
|
665
|
+
margin: 0;
|
|
666
|
+
font-size: inherit;
|
|
667
|
+
}
|
|
668
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="result"]) .seams-voiceid-status .seams-voiceid-caption-detail:not(:empty)::before {
|
|
669
|
+
content: ' · ';
|
|
670
|
+
}
|
|
671
|
+
/* A result's link, such as the transaction in a block explorer: short, in the main button's
|
|
672
|
+
colour, underlined only on hover. */
|
|
673
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status a {
|
|
674
|
+
color: var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3));
|
|
675
|
+
font-weight: 500;
|
|
676
|
+
text-decoration: none;
|
|
677
|
+
}
|
|
678
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status a:hover,
|
|
679
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status a:focus-visible { text-decoration: underline; }
|
|
680
|
+
/* The voice line and the check, in the main button's colours, with either line style. The line
|
|
681
|
+
keeps its drawing size; the taller button gives it room above and below. */
|
|
682
|
+
.seams-voiceid-prompt[data-context] > :is(.seams-voiceid-ribbon, .seams-voiceid-status-line) {
|
|
683
|
+
box-sizing: border-box;
|
|
684
|
+
width: 100%;
|
|
685
|
+
height: 4.5rem;
|
|
686
|
+
padding-block: 0.5rem;
|
|
687
|
+
overflow: hidden;
|
|
688
|
+
border-radius: 9999px;
|
|
689
|
+
background: var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3));
|
|
690
|
+
color: var(--seams-colors-textButton, var(--seams-colors-colorBackground, #192024));
|
|
691
|
+
}
|
|
692
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-ribbon .seams-voiceid-ribbon-echo { color: currentColor; }
|
|
693
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status-line rect {
|
|
694
|
+
fill: color-mix(in srgb, currentColor 35%, transparent);
|
|
695
|
+
}
|
|
696
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status-line rect[data-state="done"],
|
|
697
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status-line rect[data-state="run"] { fill: currentColor; }
|
|
698
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status-check {
|
|
699
|
+
stroke: var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3));
|
|
700
|
+
}
|
|
701
|
+
.seams-voiceid-prompt[data-context][data-voice-line="pixels"] {
|
|
702
|
+
--seams-voiceid-pixel-background: var(--seams-colors-voicePixelBackground, var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3)));
|
|
703
|
+
--seams-voiceid-pixel-ink: var(--seams-colors-textButton, var(--seams-colors-colorBackground, #192024));
|
|
704
|
+
}
|
|
705
|
+
.seams-voiceid-prompt[data-context][data-voice-line="pixels"] > :is(.seams-voiceid-ribbon, .seams-voiceid-status-line) {
|
|
706
|
+
background: var(--seams-voiceid-pixel-background);
|
|
707
|
+
color: var(--seams-voiceid-pixel-ink);
|
|
708
|
+
}
|
|
709
|
+
.seams-voiceid-prompt[data-context][data-voice-line="pixels"] .seams-voiceid-status-check {
|
|
710
|
+
stroke: var(--seams-voiceid-pixel-background);
|
|
711
|
+
}
|
|
712
|
+
/* On the button, the dots are the button's text colour: full in the middle row, then mixed a
|
|
713
|
+
little and more with the button's colour (white, light and light blue on X blue). */
|
|
714
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-ribbon-pixel[data-band="0"] { fill: var(--seams-voiceid-pixel-ink); }
|
|
715
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-ribbon-pixel[data-band="1"] {
|
|
716
|
+
fill: color-mix(in srgb, var(--seams-voiceid-pixel-ink) 80%, var(--seams-voiceid-pixel-background));
|
|
717
|
+
}
|
|
718
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-ribbon-pixel[data-band="2"] {
|
|
719
|
+
fill: color-mix(in srgb, var(--seams-voiceid-pixel-ink) 55%, var(--seams-voiceid-pixel-background));
|
|
720
|
+
}
|
|
721
|
+
/* The microphone at the left of the button, as on an app's own voice button: on Record, on the
|
|
722
|
+
voice line while the card records, and on the check. A result's button has none. Its circle
|
|
723
|
+
keeps the same gap to the button's top, left and bottom, so it follows the button's round end.
|
|
724
|
+
The line leaves the icon its room. */
|
|
725
|
+
.seams-voiceid-prompt[data-context] > .seams-voiceid-line-icon {
|
|
726
|
+
--seams-voiceid-icon-gap: 0.375rem;
|
|
727
|
+
grid-row: line / span 2;
|
|
728
|
+
grid-column: 1;
|
|
729
|
+
align-self: center;
|
|
730
|
+
justify-self: start;
|
|
731
|
+
z-index: 2;
|
|
732
|
+
display: grid;
|
|
733
|
+
place-items: center;
|
|
734
|
+
width: calc(var(--seams-voiceid-button-height) - 2 * var(--seams-voiceid-icon-gap));
|
|
735
|
+
height: calc(var(--seams-voiceid-button-height) - 2 * var(--seams-voiceid-icon-gap));
|
|
736
|
+
margin-inline-start: var(--seams-voiceid-icon-gap);
|
|
737
|
+
border-radius: 50%;
|
|
738
|
+
transition: width 180ms ease, height 180ms ease;
|
|
739
|
+
background: var(--seams-colors-textButton, var(--seams-colors-colorBackground, #192024));
|
|
740
|
+
color: var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3));
|
|
741
|
+
pointer-events: none;
|
|
742
|
+
}
|
|
743
|
+
.seams-voiceid-line-icon > svg { width: 46%; height: 46%; }
|
|
744
|
+
.seams-voiceid-prompt[data-context][data-phrase-view="result"] > .seams-voiceid-line-icon { display: none; }
|
|
745
|
+
.seams-voiceid-prompt[data-context] > :is(.seams-voiceid-ribbon, .seams-voiceid-status-line) {
|
|
746
|
+
box-sizing: border-box;
|
|
747
|
+
padding-inline: calc(var(--seams-voiceid-button-height) + 0.25rem) 1rem;
|
|
748
|
+
}
|
|
749
|
+
/* The status line's own space below it is the gap above the button, in every view. */
|
|
750
|
+
.seams-voiceid-prompt[data-context] > .seams-voiceid-record-slot { margin-top: 0; }
|
|
751
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-primary {
|
|
752
|
+
min-height: 3.5rem;
|
|
753
|
+
border-radius: 9999px;
|
|
754
|
+
font-size: 1.0625rem;
|
|
755
|
+
font-weight: 700;
|
|
756
|
+
}
|
|
508
757
|
@media (hover: hover) and (pointer: fine) {
|
|
509
758
|
.seams-voiceid-close:hover { background: var(--seams-colors-surface, #202b31); }
|
|
510
759
|
.seams-voiceid-primary:hover:not(:disabled) { opacity: 0.9; }
|
|
@@ -595,6 +844,29 @@ var VoicePrompt = class {
|
|
|
595
844
|
this.element.append(this.visual.glow, this.cancel, this.title, this.phrase, this.visual.element, this.status, slot, this.progress);
|
|
596
845
|
this.sizes.observe(this.element);
|
|
597
846
|
}
|
|
847
|
+
/** Draws the voice as wave lines (the default) or as columns of pixels. */
|
|
848
|
+
useVoiceLine(style) {
|
|
849
|
+
this.element.dataset.voiceLine = style;
|
|
850
|
+
this.visual.setLineStyle(style);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Shows the text that the sentence speaks for, such as the note that a spoken summary signs,
|
|
854
|
+
* above the sentence. It stays for the check and the result.
|
|
855
|
+
*/
|
|
856
|
+
showContext(label, text) {
|
|
857
|
+
const document = this.element.ownerDocument;
|
|
858
|
+
const context = document.createElement("figure");
|
|
859
|
+
context.className = "seams-voiceid-context";
|
|
860
|
+
const caption = document.createElement("figcaption");
|
|
861
|
+
caption.className = "seams-voiceid-context-label";
|
|
862
|
+
caption.textContent = label;
|
|
863
|
+
const body = document.createElement("blockquote");
|
|
864
|
+
body.textContent = text;
|
|
865
|
+
context.append(caption, body);
|
|
866
|
+
this.title.before(context);
|
|
867
|
+
this.element.append(microphoneIcon(document));
|
|
868
|
+
this.element.dataset.context = "";
|
|
869
|
+
}
|
|
598
870
|
/**
|
|
599
871
|
* In an app's dialog with a step before this card, Back in the leading corner returns to that
|
|
600
872
|
* step. With `close`, Close stays in the trailing corner and ends the whole dialog; without it,
|
|
@@ -614,6 +886,16 @@ var VoicePrompt = class {
|
|
|
614
886
|
return this.cancel;
|
|
615
887
|
}
|
|
616
888
|
/**
|
|
889
|
+
* Minimize in the trailing corner, beside Close. It stays in the check, where Close is hidden,
|
|
890
|
+
* so the person can hide the card while the check goes on. The result is the control.
|
|
891
|
+
*/
|
|
892
|
+
showMinimize() {
|
|
893
|
+
const minimize = cornerButton(this.element.ownerDocument, "Minimize", MINIMIZE_PATH);
|
|
894
|
+
minimize.classList.add("seams-voiceid-minimize");
|
|
895
|
+
this.cancel.before(minimize);
|
|
896
|
+
return minimize;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
617
899
|
* Moves the rows to a new state. The rows whose content changes fade out together, take their
|
|
618
900
|
* new content and fade in one after another; the other rows stay still. A change that comes
|
|
619
901
|
* during a fade joins it, so no state in between is drawn.
|
|
@@ -1018,8 +1300,8 @@ function fadeRowsIn(rows) {
|
|
|
1018
1300
|
opacity: to,
|
|
1019
1301
|
transform: "none"
|
|
1020
1302
|
}], {
|
|
1021
|
-
delay: reduced ? 0 :
|
|
1022
|
-
duration: reduced ? 150 :
|
|
1303
|
+
delay: reduced ? 0 : 0 + index * 20,
|
|
1304
|
+
duration: reduced ? 150 : 140,
|
|
1023
1305
|
easing: ROW_EASE,
|
|
1024
1306
|
fill: "backwards",
|
|
1025
1307
|
id: ROW_MOTION
|
|
@@ -1029,7 +1311,7 @@ function fadeRowsIn(rows) {
|
|
|
1029
1311
|
function ignoreCancel() {}
|
|
1030
1312
|
function continueFade({ row, opacity }) {
|
|
1031
1313
|
row.animate([{ opacity }, { opacity: 1 }], {
|
|
1032
|
-
duration:
|
|
1314
|
+
duration: 140 * (1 - opacity),
|
|
1033
1315
|
easing: ROW_EASE,
|
|
1034
1316
|
id: ROW_MOTION
|
|
1035
1317
|
});
|
|
@@ -1062,6 +1344,34 @@ function leaveSurface(element) {
|
|
|
1062
1344
|
}
|
|
1063
1345
|
const CLOSE_PATH = "M6 6l12 12M18 6L6 18";
|
|
1064
1346
|
const BACK_PATH = "m15 18-6-6 6-6";
|
|
1347
|
+
const MINIMIZE_PATH = "M6 12h12";
|
|
1348
|
+
/** A microphone: its head, its stand and its foot. */
|
|
1349
|
+
const MICROPHONE_PATHS = [
|
|
1350
|
+
"M9 5a3 3 0 0 1 6 0v5a3 3 0 0 1-6 0z",
|
|
1351
|
+
"M5 10a7 7 0 0 0 14 0",
|
|
1352
|
+
"M8 21h8",
|
|
1353
|
+
"M12 17v4"
|
|
1354
|
+
];
|
|
1355
|
+
/** The microphone at the left of a form card's button, in its own circle. */
|
|
1356
|
+
function microphoneIcon(document) {
|
|
1357
|
+
const circle = document.createElement("span");
|
|
1358
|
+
circle.className = "seams-voiceid-line-icon";
|
|
1359
|
+
circle.setAttribute("aria-hidden", "true");
|
|
1360
|
+
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1361
|
+
icon.setAttribute("viewBox", "0 0 24 24");
|
|
1362
|
+
for (const d of MICROPHONE_PATHS) {
|
|
1363
|
+
const part = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1364
|
+
part.setAttribute("d", d);
|
|
1365
|
+
part.setAttribute("fill", "none");
|
|
1366
|
+
part.setAttribute("stroke", "currentColor");
|
|
1367
|
+
part.setAttribute("stroke-width", "1.8");
|
|
1368
|
+
part.setAttribute("stroke-linecap", "round");
|
|
1369
|
+
part.setAttribute("stroke-linejoin", "round");
|
|
1370
|
+
icon.append(part);
|
|
1371
|
+
}
|
|
1372
|
+
circle.append(icon);
|
|
1373
|
+
return circle;
|
|
1374
|
+
}
|
|
1065
1375
|
/** A control in a corner of the card, with an icon and no text. */
|
|
1066
1376
|
function cornerButton(document, label, path) {
|
|
1067
1377
|
const button = document.createElement("button");
|
package/dist/esm/card/ribbon.js
CHANGED
|
@@ -31,6 +31,19 @@ const LINE_REST_OPACITY = .55;
|
|
|
31
31
|
const SUCCESS_MS = 1140;
|
|
32
32
|
const SEAL_RADIUS = 22.5;
|
|
33
33
|
const CHECK_PATH = "M161 26.8 151.4 36.3 147.1 32";
|
|
34
|
+
const PIXEL_COLUMNS = 44;
|
|
35
|
+
/** The rows above the middle row, and the rows below it. */
|
|
36
|
+
const PIXEL_ROWS = 4;
|
|
37
|
+
const PIXEL_PITCH = 7;
|
|
38
|
+
/** Rows sit closer than columns, so the tallest column keeps a gap to the edge of a button. */
|
|
39
|
+
const PIXEL_ROW_PITCH = 6;
|
|
40
|
+
const PIXEL_SIZE = 4.5;
|
|
41
|
+
const PIXEL_STEP_MS = 70;
|
|
42
|
+
/** A column shows between this share of the level and all of it. */
|
|
43
|
+
const PIXEL_SPREAD = .3;
|
|
44
|
+
const PIXEL_IDLE_LEVEL = .18;
|
|
45
|
+
const PIXEL_IDLE_SWING = .12;
|
|
46
|
+
const PIXEL_IDLE_PERIOD_MS = 2400;
|
|
34
47
|
const easeMove = cubicBezier(.65, 0, .35, 1);
|
|
35
48
|
const easeArc = cubicBezier(.4, 0, .2, 1);
|
|
36
49
|
const easeSeal = cubicBezier(.34, 1.56, .64, 1);
|
|
@@ -57,6 +70,14 @@ var VoiceRibbon = class {
|
|
|
57
70
|
target = 0;
|
|
58
71
|
level = 0;
|
|
59
72
|
stopped = false;
|
|
73
|
+
lineStyle = "wave";
|
|
74
|
+
/** The pixel style's dots by column, top row first. Made when the style is first chosen. */
|
|
75
|
+
pixels = null;
|
|
76
|
+
/** The level of each column, the newest at the right end. */
|
|
77
|
+
pixelLevels = new Array(PIXEL_COLUMNS).fill(0);
|
|
78
|
+
/** The rows that each column shows now, above and below the middle row. */
|
|
79
|
+
pixelHeights = new Array(PIXEL_COLUMNS).fill(-1);
|
|
80
|
+
pixelClock = 0;
|
|
60
81
|
constructor(document, checking = "ring") {
|
|
61
82
|
this.checking = checking;
|
|
62
83
|
this.element = document.createElementNS(SVG_NAMESPACE, "svg");
|
|
@@ -94,10 +115,61 @@ var VoiceRibbon = class {
|
|
|
94
115
|
this.bottom
|
|
95
116
|
].entries()) segment.dataset.seamsProgressSegment = String(index);
|
|
96
117
|
}
|
|
97
|
-
for (let layer = 3; layer >= 0; layer -= 1)
|
|
118
|
+
for (let layer = 3; layer >= 0; layer -= 1) {
|
|
119
|
+
const line = this.append(document, "path", layer === 0 ? "1" : "0.65");
|
|
120
|
+
line.classList.add(layer === 0 ? "seams-voiceid-ribbon-line" : "seams-voiceid-ribbon-echo");
|
|
121
|
+
if (layer > 0) line.dataset.layer = String(layer);
|
|
122
|
+
this.lines.push(line);
|
|
123
|
+
}
|
|
98
124
|
this.paint(performance.now());
|
|
99
125
|
}
|
|
100
126
|
readLevel = () => this.level;
|
|
127
|
+
/** Draws the voice as wave lines (the default) or as columns of pixels. */
|
|
128
|
+
setLineStyle(style) {
|
|
129
|
+
this.lineStyle = style;
|
|
130
|
+
this.element.dataset.lineStyle = style;
|
|
131
|
+
if (style === "pixels" && !this.pixels) this.pixels = this.makePixels();
|
|
132
|
+
this.pixelHeights.fill(-1);
|
|
133
|
+
this.paint(performance.now());
|
|
134
|
+
this.wake();
|
|
135
|
+
}
|
|
136
|
+
makePixels() {
|
|
137
|
+
const document = this.element.ownerDocument;
|
|
138
|
+
const group = document.createElementNS(SVG_NAMESPACE, "g");
|
|
139
|
+
group.classList.add("seams-voiceid-ribbon-pixels");
|
|
140
|
+
group.setAttribute("opacity", "0");
|
|
141
|
+
const left = (WIDTH - ((PIXEL_COLUMNS - 1) * PIXEL_PITCH + PIXEL_SIZE)) / 2;
|
|
142
|
+
const columns = [];
|
|
143
|
+
for (let column = 0; column < PIXEL_COLUMNS; column += 1) {
|
|
144
|
+
const dots = [];
|
|
145
|
+
for (let row = -PIXEL_ROWS; row <= PIXEL_ROWS; row += 1) {
|
|
146
|
+
const dot = document.createElementNS(SVG_NAMESPACE, "rect");
|
|
147
|
+
dot.classList.add("seams-voiceid-ribbon-pixel");
|
|
148
|
+
const distance = Math.abs(row);
|
|
149
|
+
dot.dataset.band = distance === 0 ? "0" : distance <= 2 ? "1" : "2";
|
|
150
|
+
dot.setAttribute("x", (left + column * PIXEL_PITCH).toFixed(2));
|
|
151
|
+
dot.setAttribute("y", (MID - PIXEL_SIZE / 2 + row * PIXEL_ROW_PITCH).toFixed(2));
|
|
152
|
+
dot.setAttribute("width", String(PIXEL_SIZE));
|
|
153
|
+
dot.setAttribute("height", String(PIXEL_SIZE));
|
|
154
|
+
dot.setAttribute("rx", "1");
|
|
155
|
+
dot.setAttribute("opacity", "0");
|
|
156
|
+
group.append(dot);
|
|
157
|
+
dots.push(dot);
|
|
158
|
+
}
|
|
159
|
+
columns.push(dots);
|
|
160
|
+
}
|
|
161
|
+
this.element.prepend(group);
|
|
162
|
+
return {
|
|
163
|
+
group,
|
|
164
|
+
columns
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** The voice line's width now: 1, or the theme's `voiceLineWidth`. */
|
|
168
|
+
lineWidth() {
|
|
169
|
+
const line = this.lines[this.lines.length - 1];
|
|
170
|
+
const width = line ? Number.parseFloat(getComputedStyle(line).strokeWidth) : NaN;
|
|
171
|
+
return Number.isFinite(width) && width > 0 ? width : 1;
|
|
172
|
+
}
|
|
101
173
|
setPhase(phase) {
|
|
102
174
|
if (phase === "processing" && this.stage.kind === "line") {
|
|
103
175
|
const amplitude = Math.max(this.level, this.idleAmplitude(performance.now()));
|
|
@@ -177,13 +249,23 @@ var VoiceRibbon = class {
|
|
|
177
249
|
this.previousTime = time;
|
|
178
250
|
this.level += (this.target - this.level) * (1 - Math.exp(-dt / (this.target > this.level ? 55 : 200)));
|
|
179
251
|
if (!this.motion.matches) this.position += dt * (.0012 + this.level * .003);
|
|
252
|
+
if (this.lineStyle === "pixels") this.stepPixels(dt);
|
|
180
253
|
this.paint(time);
|
|
181
|
-
if (!(!this.element.isConnected || this.stage.kind === "sealed" || this.stage.kind === "line" && this.phase === "idle" && this.level <= .001)) this.frame = requestAnimationFrame(this.draw);
|
|
254
|
+
if (!(!this.element.isConnected || this.stage.kind === "sealed" || this.stage.kind === "line" && this.phase === "idle" && this.level <= .001 && this.pixelLevels.every((level) => level <= .001))) this.frame = requestAnimationFrame(this.draw);
|
|
182
255
|
};
|
|
183
256
|
idleAmplitude(time) {
|
|
184
257
|
if (this.phase !== "listening" || this.motion.matches) return 0;
|
|
185
258
|
return IDLE_AMPLITUDE * (.72 + .28 * Math.sin(time / IDLE_PERIOD_MS * TAU));
|
|
186
259
|
}
|
|
260
|
+
/** Adds the level of each step at the right end; the older columns move left. */
|
|
261
|
+
stepPixels(dt) {
|
|
262
|
+
this.pixelClock += dt;
|
|
263
|
+
while (this.pixelClock >= PIXEL_STEP_MS) {
|
|
264
|
+
this.pixelClock -= PIXEL_STEP_MS;
|
|
265
|
+
this.pixelLevels.shift();
|
|
266
|
+
this.pixelLevels.push(this.level * (PIXEL_SPREAD + (1 - PIXEL_SPREAD) * Math.random()));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
187
269
|
paint(time) {
|
|
188
270
|
for (const part of [
|
|
189
271
|
this.ripple,
|
|
@@ -195,9 +277,11 @@ var VoiceRibbon = class {
|
|
|
195
277
|
this.check
|
|
196
278
|
]) part.setAttribute("opacity", "0");
|
|
197
279
|
for (const line of this.lines) line.setAttribute("opacity", "0");
|
|
280
|
+
this.pixels?.group.setAttribute("opacity", "0");
|
|
198
281
|
switch (this.stage.kind) {
|
|
199
282
|
case "line":
|
|
200
|
-
|
|
283
|
+
if (this.lineStyle === "pixels") this.paintPixels(time);
|
|
284
|
+
else this.paintLine(Math.max(this.level, this.idleAmplitude(time)));
|
|
201
285
|
return;
|
|
202
286
|
case "capture":
|
|
203
287
|
this.paintCapture(time, this.stage);
|
|
@@ -226,6 +310,23 @@ var VoiceRibbon = class {
|
|
|
226
310
|
this.lines[index].setAttribute("opacity", String(layer === 0 ? LINE_REST_OPACITY + amplitude * .4 : .1 + amplitude * .1));
|
|
227
311
|
}
|
|
228
312
|
}
|
|
313
|
+
/** Each column shows the middle dot and as many rows above and below it as its level. */
|
|
314
|
+
paintPixels(time) {
|
|
315
|
+
if (!this.pixels) return;
|
|
316
|
+
this.pixels.group.setAttribute("opacity", "1");
|
|
317
|
+
const live = this.phase === "listening" && !this.motion.matches;
|
|
318
|
+
for (const [column, dots] of this.pixels.columns.entries()) {
|
|
319
|
+
let level = this.pixelLevels[column] ?? 0;
|
|
320
|
+
if (live) {
|
|
321
|
+
const wave = Math.sin(time / PIXEL_IDLE_PERIOD_MS * TAU - column * .55);
|
|
322
|
+
level = Math.max(level, PIXEL_IDLE_LEVEL + PIXEL_IDLE_SWING * wave);
|
|
323
|
+
}
|
|
324
|
+
const height = Math.min(PIXEL_ROWS, Math.round(level * PIXEL_ROWS * 1.3));
|
|
325
|
+
if (height === this.pixelHeights[column]) continue;
|
|
326
|
+
this.pixelHeights[column] = height;
|
|
327
|
+
for (const [index, dot] of dots.entries()) dot.setAttribute("opacity", Math.abs(index - PIXEL_ROWS) <= height ? "1" : "0");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
229
330
|
paintSegments(time, stage) {
|
|
230
331
|
const elapsed = time - stage.startedAt;
|
|
231
332
|
if (!this.motion.matches && elapsed < SPLIT_FLATTEN_MS) {
|
|
@@ -242,7 +343,7 @@ var VoiceRibbon = class {
|
|
|
242
343
|
].entries()) {
|
|
243
344
|
const start = index * (width + gap);
|
|
244
345
|
segment.setAttribute("d", `M${start.toFixed(2)} ${MID}H${(start + width).toFixed(2)}`);
|
|
245
|
-
segment.setAttribute("stroke-width",
|
|
346
|
+
segment.setAttribute("stroke-width", String(this.lineWidth()));
|
|
246
347
|
segment.setAttribute("opacity", this.segmentOpacity(split, index));
|
|
247
348
|
}
|
|
248
349
|
}
|
|
@@ -268,7 +369,7 @@ var VoiceRibbon = class {
|
|
|
268
369
|
const lineOpacity = LINE_REST_OPACITY + amplitude * .4;
|
|
269
370
|
for (const [half, turned] of [[this.top, false], [this.bottom, true]]) {
|
|
270
371
|
half.setAttribute("d", this.halfPath(tail, head, turned, amplitude, waveScale));
|
|
271
|
-
half.setAttribute("stroke-width", lerp(
|
|
372
|
+
half.setAttribute("stroke-width", lerp(this.lineWidth(), Math.max(2, this.lineWidth()), settled).toFixed(2));
|
|
272
373
|
half.setAttribute("opacity", lerp(lineOpacity, 1, settled).toFixed(3));
|
|
273
374
|
}
|
|
274
375
|
this.middle.setAttribute("d", this.linePath(0, amplitude, BEND_START, WIDTH - BEND_START, waveScale));
|