@kitn.ai/ui 0.19.0 → 0.20.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/LICENSE +6 -0
- package/NOTICE +54 -0
- package/dist/components/audio-visualizer/audio-visualizer.voice-fixture.d.ts +59 -0
- package/dist/components/audio-visualizer/aurora.glsl.d.ts +114 -0
- package/dist/components/audio-visualizer/index.d.ts +155 -0
- package/dist/components/audio-visualizer/shader-canvas.d.ts +130 -0
- package/dist/components/audio-visualizer/sizes.d.ts +25 -0
- package/dist/components/audio-visualizer/variant-aurora.d.ts +89 -0
- package/dist/components/audio-visualizer/variant-bar.d.ts +46 -0
- package/dist/components/audio-visualizer/variant-custom.d.ts +73 -0
- package/dist/components/audio-visualizer/variant-grid.d.ts +22 -0
- package/dist/components/audio-visualizer/variant-radial.d.ts +13 -0
- package/dist/components/audio-visualizer/variant-wave.d.ts +11 -0
- package/dist/components/audio-visualizer/wave.glsl.d.ts +16 -0
- package/dist/create-tween-hvpmSuwW.js +7 -0
- package/dist/custom-elements.json +282 -0
- package/dist/elements/agent-card.js +1 -1
- package/dist/elements/artifact.js +1 -1
- package/dist/elements/attachments.js +1 -1
- package/dist/elements/audio-visualizer.d.ts +1 -0
- package/dist/elements/audio-visualizer.js +1 -0
- package/dist/elements/autoloader.js +1 -1
- package/dist/elements/avatar.js +1 -1
- package/dist/elements/badge.js +1 -1
- package/dist/elements/button.js +1 -1
- package/dist/elements/card.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chain-of-thought.js +1 -1
- package/dist/elements/chat-scope-picker.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/chunks/{Icon-11SRdjJ4.js → Icon-Tj58ES_d.js} +1 -1
- package/dist/elements/chunks/{action-icons-CtRg17uX.js → action-icons-ns1Fnedj.js} +1 -1
- package/dist/elements/chunks/arrow-left-DQocUFvy.js +1 -0
- package/dist/elements/chunks/{attachments-CBqQRh60.js → attachments-mCB5F1Fq.js} +1 -1
- package/dist/elements/chunks/audio-visualizer-BqNy7dfm.js +1 -0
- package/dist/elements/chunks/{badge-CxklEXQm.js → badge-B4Zuajl2.js} +1 -1
- package/dist/elements/chunks/{button-BD7lUvVB.js → button-ZWNif3--.js} +1 -1
- package/dist/elements/chunks/{card-B-PRGXdj.js → card-BkHbp4BA.js} +1 -1
- package/dist/elements/chunks/{chat-thread-DIZbRIKG.js → chat-thread-CkRN6pZ7.js} +1 -1
- package/dist/elements/chunks/check-B9-gn6SS.js +1 -0
- package/dist/elements/chunks/chevron-down-BNsxc2hk.js +1 -0
- package/dist/elements/chunks/chevron-right-Hv00StBO.js +1 -0
- package/dist/elements/chunks/circle-0e3KJTDr.js +1 -0
- package/dist/elements/chunks/circle-check-BNAS0JEy.js +1 -0
- package/dist/elements/chunks/{circle-x-mtjCO_Ok.js → circle-x-B7faJFlY.js} +1 -1
- package/dist/elements/chunks/{code-block-BeWVttmU.js → code-block-BFUHn6Va.js} +1 -1
- package/dist/elements/chunks/collapsible-DM1Ckd4e.js +1 -0
- package/dist/elements/chunks/{composer-CavJww3d.js → composer-BguS0QL_.js} +2 -2
- package/dist/elements/chunks/{context-CnlsNJSJ.js → context-Cb7k4F-v.js} +1 -1
- package/dist/elements/chunks/{conversation-list-5O_KdI_I.js → conversation-list-CvoAkWhN.js} +1 -1
- package/dist/elements/chunks/create-tween-oVvAIG8r.js +7 -0
- package/dist/elements/chunks/{default-input-CB46cJJ1.js → default-input-Cqqilg9t.js} +1 -1
- package/dist/elements/chunks/define-D1din7Ld.js +1 -0
- package/dist/elements/chunks/{disclosure-BtFK3a-M.js → disclosure-CIZ5c8nO.js} +1 -1
- package/dist/elements/chunks/{download-qJv1dS3M.js → download-Dsbg3ste.js} +1 -1
- package/dist/elements/chunks/{dropdown-UhtAueYR.js → dropdown-BDd1-Zgi.js} +1 -1
- package/dist/elements/chunks/{ellipsis-B1sOOqiJ.js → ellipsis-Dw6qrss-.js} +1 -1
- package/dist/elements/chunks/{external-link-DToP_VKE.js → external-link-Vt74zAoL.js} +1 -1
- package/dist/elements/chunks/{file-text-BwUs_Pj4.js → file-text-CZrZF6_v.js} +1 -1
- package/dist/elements/chunks/{file-tree-C6RhmSnX.js → file-tree-DF4okKyo.js} +1 -1
- package/dist/elements/chunks/{folder-H_Xp0GYP.js → folder-glP5rha4.js} +1 -1
- package/dist/elements/chunks/{hover-card-Dn3yrk1l.js → hover-card-Cmm6Q53m.js} +1 -1
- package/dist/elements/chunks/{icon-BrxJQXzC.js → icon-BLb8beoi.js} +1 -1
- package/dist/elements/chunks/{info-11eT2q5H.js → info-CIsv92JK.js} +1 -1
- package/dist/elements/chunks/{input-B9oZKDfb.js → input-Dds1kFyt.js} +1 -1
- package/dist/elements/chunks/{kbd-D2fx3rWh.js → kbd-De_xbgi9.js} +1 -1
- package/dist/elements/chunks/{link-B-RNYWNw.js → link-i4r_k7z4.js} +1 -1
- package/dist/elements/chunks/{loader-BJh_aRlp.js → loader-BmQH4s3V.js} +1 -1
- package/dist/elements/chunks/{markdown-BIpjrDFv.js → markdown-Zi21WhUz.js} +7 -7
- package/dist/elements/chunks/{message-CPfqj8Gi.js → message-BqyXDN3-.js} +1 -1
- package/dist/elements/chunks/{message-feedback-8uj0aVYh.js → message-feedback-Djr3z6Yb.js} +1 -1
- package/dist/elements/chunks/message-square-vFAgbDaX.js +1 -0
- package/dist/elements/chunks/{minimize-2-C9qkiTJg.js → minimize-2--ifeCGrl.js} +1 -1
- package/dist/elements/chunks/{model-switcher-DRrMcRXH.js → model-switcher-B94YAKsM.js} +1 -1
- package/dist/elements/chunks/{overlay-C8_FUL8q.js → overlay-kD8dwRv_.js} +1 -1
- package/dist/elements/chunks/{paperclip-MA9hxLhW.js → paperclip-q21dYOSK.js} +1 -1
- package/dist/elements/chunks/{progress-bar-y5G6HDjg.js → progress-bar-BrAIIaaJ.js} +1 -1
- package/dist/elements/chunks/{prompt-suggestion-LalWvCJy.js → prompt-suggestion-Bjn2K-fR.js} +1 -1
- package/dist/elements/chunks/reasoning-DTpCGtnY.js +1 -0
- package/dist/elements/chunks/{resizable-xhYF5lCQ.js → resizable-Dp4JExql.js} +1 -1
- package/dist/elements/chunks/{rotate-cw-2ndEXjgS.js → rotate-cw-CFWWkpj2.js} +1 -1
- package/dist/elements/chunks/{scroll-area-BzOPDXrZ.js → scroll-area-Bb0kHH1j.js} +1 -1
- package/dist/elements/chunks/scroll-button-B0cH5fXf.js +1 -0
- package/dist/elements/chunks/{separator-CPvL5Xdl.js → separator-9kRgr1X9.js} +1 -1
- package/dist/elements/chunks/{settings-hmzIuNXD.js → settings-B8lmec4D.js} +1 -1
- package/dist/elements/chunks/{settings-group-BiIym17O.js → settings-group-I0hEuXiN.js} +1 -1
- package/dist/elements/chunks/{share-BbziTAzn.js → share-C3lTiYGt.js} +1 -1
- package/dist/elements/chunks/{skeleton-Bxhs1thL.js → skeleton-DSXW5Yya.js} +1 -1
- package/dist/elements/chunks/{star-BHL3Va5p.js → star-CSJtGxgv.js} +1 -1
- package/dist/elements/chunks/{store-tpnnWlxY.js → store-CHyW95TP.js} +1 -1
- package/dist/elements/chunks/{text-shimmer-1DCKivKx.js → text-shimmer-Cn9TmNpI.js} +1 -1
- package/dist/elements/chunks/{textarea-Czaowo9F.js → textarea-CUwqAX0G.js} +1 -1
- package/dist/elements/chunks/{thumbs-up-CdLqA13R.js → thumbs-up-PNVVThEF.js} +1 -1
- package/dist/elements/chunks/{toast-store-BvBquUFA.js → toast-store-DKbQn3Wn.js} +1 -1
- package/dist/elements/chunks/{tool-4P_ne8Go.js → tool-DjTRpIpl.js} +1 -1
- package/dist/elements/chunks/tooltip-CgBZCVKp.js +1 -0
- package/dist/elements/chunks/{triangle-alert-BllqxXq8.js → triangle-alert-DsHTHCPm.js} +1 -1
- package/dist/elements/chunks/{use-card-resolution-73xiddIW.js → use-card-resolution-BSu_XHjH.js} +1 -1
- package/dist/elements/chunks/variant-aurora-Dc4nQUia.js +218 -0
- package/dist/elements/chunks/variant-custom-y_3kVQhS.js +1 -0
- package/dist/elements/chunks/variant-wave-D5OR1-9Z.js +116 -0
- package/dist/elements/chunks/{video-O9rA6k7S.js → video-B7vmenoC.js} +1 -1
- package/dist/elements/chunks/x-GRUeeQFT.js +1 -0
- package/dist/elements/coachmark.js +1 -1
- package/dist/elements/code-block.js +1 -1
- package/dist/elements/command.js +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/composer.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/context-meter.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/define.d.ts +11 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/embed.js +1 -1
- package/dist/elements/empty.js +1 -1
- package/dist/elements/feedback-bar.js +1 -1
- package/dist/elements/file-tree.js +1 -1
- package/dist/elements/file-upload.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/hover-card.js +1 -1
- package/dist/elements/icon.js +1 -1
- package/dist/elements/image.js +1 -1
- package/dist/elements/input.js +1 -1
- package/dist/elements/kbd.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/loader.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/menu.js +1 -1
- package/dist/elements/message-skills.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/model-switcher.js +1 -1
- package/dist/elements/nav.js +1 -1
- package/dist/elements/notice.js +1 -1
- package/dist/elements/pane-group.js +1 -1
- package/dist/elements/pane.js +1 -1
- package/dist/elements/popover.js +1 -1
- package/dist/elements/progress-bar.js +1 -1
- package/dist/elements/prompt-dock.js +1 -1
- package/dist/elements/prompt-input.js +1 -1
- package/dist/elements/prompt-suggestions.js +1 -1
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +1 -1
- package/dist/elements/screen.js +1 -1
- package/dist/elements/scroll-area.js +1 -1
- package/dist/elements/scroll-button.js +1 -1
- package/dist/elements/search.js +1 -1
- package/dist/elements/segmented.js +1 -1
- package/dist/elements/separator.js +1 -1
- package/dist/elements/setting-item.js +1 -1
- package/dist/elements/settings-group.js +1 -1
- package/dist/elements/skeleton.js +1 -1
- package/dist/elements/slots.d.ts +12 -0
- package/dist/elements/source.js +1 -1
- package/dist/elements/status.js +1 -1
- package/dist/elements/switch.js +1 -1
- package/dist/elements/tabs.js +1 -1
- package/dist/elements/tasks.js +1 -1
- package/dist/elements/text-shimmer.js +1 -1
- package/dist/elements/thinking-bar.js +1 -1
- package/dist/elements/thread.js +1 -1
- package/dist/elements/toast.js +1 -1
- package/dist/elements/tool.js +1 -1
- package/dist/elements/tooltip.js +1 -1
- package/dist/elements/voice-input.js +1 -1
- package/dist/elements/voice-output.js +1 -1
- package/dist/elements.d.ts +38 -0
- package/dist/index.js +18 -17
- package/dist/kai.es.js +1 -1
- package/dist/llms/llms-full.txt +40 -4
- package/dist/llms/llms.txt +3 -3
- package/dist/primitives/audio-bands.d.ts +106 -0
- package/dist/primitives/create-tween.d.ts +33 -0
- package/dist/primitives/use-audio-analysis.d.ts +149 -0
- package/dist/primitives/use-sequencer.d.ts +13 -0
- package/dist/primitives/use-voice-recorder.d.ts +1 -0
- package/dist/primitives/visualizer-sequences.d.ts +39 -0
- package/dist/react/index.d.ts +41 -0
- package/dist/react.js +159 -153
- package/dist/register-impl-DQyejdfb.js +145 -0
- package/dist/test-utils/fake-clock.d.ts +45 -0
- package/dist/variant-aurora-DVtGqVH6.js +218 -0
- package/dist/variant-custom-Dif-IV4u.js +1 -0
- package/dist/variant-wave-DbfNdvSi.js +116 -0
- package/frameworks/react/index.tsx +42 -0
- package/llms-full.txt +40 -4
- package/llms.txt +3 -3
- package/package.json +6 -3
- package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +257 -0
- package/src/components/audio-visualizer/aurora.glsl.ts +330 -0
- package/src/components/audio-visualizer/index.tsx +442 -0
- package/src/components/audio-visualizer/shader-canvas.tsx +846 -0
- package/src/components/audio-visualizer/sizes.ts +54 -0
- package/src/components/audio-visualizer/variant-aurora.tsx +299 -0
- package/src/components/audio-visualizer/variant-bar.tsx +143 -0
- package/src/components/audio-visualizer/variant-custom.tsx +338 -0
- package/src/components/audio-visualizer/variant-grid.tsx +170 -0
- package/src/components/audio-visualizer/variant-radial.tsx +158 -0
- package/src/components/audio-visualizer/variant-wave.tsx +125 -0
- package/src/components/audio-visualizer/wave.glsl.ts +130 -0
- package/src/components/component-meta.json +24 -24
- package/src/elements/audio-visualizer.tsx +183 -0
- package/src/elements/compiled.css +1 -1
- package/src/elements/define.tsx +12 -1
- package/src/elements/element-manifest.json +4 -0
- package/src/elements/element-meta.json +170 -0
- package/src/elements/element-types.d.ts +38 -0
- package/src/elements/register-impl.ts +1 -0
- package/src/elements/slots.ts +35 -0
- package/src/primitives/audio-bands.ts +174 -0
- package/src/primitives/create-tween.ts +214 -0
- package/src/primitives/use-audio-analysis.ts +321 -0
- package/src/primitives/use-sequencer.ts +53 -0
- package/src/primitives/use-voice-recorder.ts +16 -4
- package/src/primitives/visualizer-sequences.ts +350 -0
- package/src/test-utils/fake-clock.ts +88 -0
- package/dist/elements/chunks/arrow-left-CifsGq8x.js +0 -1
- package/dist/elements/chunks/check-CUKS495w.js +0 -1
- package/dist/elements/chunks/chevron-down-AELmYT1j.js +0 -1
- package/dist/elements/chunks/chevron-right-C8g26oKh.js +0 -1
- package/dist/elements/chunks/circle-DGcI8Cft.js +0 -1
- package/dist/elements/chunks/circle-check-DuU0IcBQ.js +0 -1
- package/dist/elements/chunks/collapsible-DGjADCN6.js +0 -1
- package/dist/elements/chunks/define-DfTe4zwZ.js +0 -1
- package/dist/elements/chunks/message-square-CCSQ4M9g.js +0 -1
- package/dist/elements/chunks/reasoning-CW3SDdb4.js +0 -1
- package/dist/elements/chunks/scroll-button-B1x4cYqM.js +0 -1
- package/dist/elements/chunks/tooltip-CBeFqJKk.js +0 -1
- package/dist/elements/chunks/x-DeS3rQhx.js +0 -1
- package/dist/register-impl-CaaHJcea.js +0 -145
package/src/elements/define.tsx
CHANGED
|
@@ -78,6 +78,17 @@ export interface WebComponentContext<E = Record<string, unknown>> {
|
|
|
78
78
|
* convention). Methods are attached when the facade renders (on element upgrade).
|
|
79
79
|
*/
|
|
80
80
|
expose: (methods: Record<string, (...args: never[]) => unknown>) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Whether the element should currently render its dark-mode look --
|
|
83
|
+
* exactly the SAME resolved value that already drives the `.dark` class
|
|
84
|
+
* every facade's content sits inside (not a second computation of the
|
|
85
|
+
* `theme='light'|'dark'|'auto'` rule; see `createDarkMode` above). Most
|
|
86
|
+
* facades never need this directly, since the injected kit CSS already
|
|
87
|
+
* flips its custom properties under `.dark`. It exists for content that
|
|
88
|
+
* cannot read CSS at all -- e.g. a WebGL shader baking a colour choice
|
|
89
|
+
* into a GLSL uniform, which is why `kai-audio-visualizer` reads it.
|
|
90
|
+
*/
|
|
91
|
+
dark: () => boolean;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
/** camelCase prop name → kebab-case attribute (`hoverCard` → `hover-card`). */
|
|
@@ -223,7 +234,7 @@ export function defineWebComponent<P extends Record<string, unknown>, E = Record
|
|
|
223
234
|
<div classList={{ dark: isDark() }} style={{ display: 'contents', color: 'var(--color-foreground)' }}>
|
|
224
235
|
<div ref={portalNode} />
|
|
225
236
|
<ChatConfig portalMount={portalNode}>
|
|
226
|
-
{Facade(props as unknown as P, { element, dispatch, flag, expose })}
|
|
237
|
+
{Facade(props as unknown as P, { element, dispatch, flag, expose, dark: isDark })}
|
|
227
238
|
</ChatConfig>
|
|
228
239
|
</div>
|
|
229
240
|
</>
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"kai-suggestions": "prompt-suggestions",
|
|
20
20
|
"kai-file-upload": "file-upload",
|
|
21
21
|
"kai-voice-input": "voice-input",
|
|
22
|
+
"kai-audio-visualizer": "audio-visualizer",
|
|
22
23
|
"kai-loader": "loader",
|
|
23
24
|
"kai-text-shimmer": "text-shimmer",
|
|
24
25
|
"kai-image": "image",
|
|
@@ -137,6 +138,9 @@
|
|
|
137
138
|
"voice-input": [
|
|
138
139
|
"kai-voice-input"
|
|
139
140
|
],
|
|
141
|
+
"audio-visualizer": [
|
|
142
|
+
"kai-audio-visualizer"
|
|
143
|
+
],
|
|
140
144
|
"loader": [
|
|
141
145
|
"kai-loader"
|
|
142
146
|
],
|
|
@@ -456,6 +456,176 @@
|
|
|
456
456
|
}
|
|
457
457
|
]
|
|
458
458
|
},
|
|
459
|
+
{
|
|
460
|
+
"tag": "kai-audio-visualizer",
|
|
461
|
+
"className": "KaiAudioVisualizerElement",
|
|
462
|
+
"displayName": "AudioVisualizer",
|
|
463
|
+
"props": [
|
|
464
|
+
{
|
|
465
|
+
"name": "variant",
|
|
466
|
+
"type": "undefined | string",
|
|
467
|
+
"optional": true,
|
|
468
|
+
"scalar": true,
|
|
469
|
+
"description": "Look to render: `bar` (default), `grid`, `radial`, `wave`, `aurora`, `custom`. `aura` is accepted as a LiveKit-markup alias for `aurora`. Attribute: `variant`.",
|
|
470
|
+
"default": "'bar'",
|
|
471
|
+
"displayType": "undefined | string"
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
"name": "state",
|
|
475
|
+
"type": "undefined | string",
|
|
476
|
+
"optional": true,
|
|
477
|
+
"scalar": true,
|
|
478
|
+
"description": "`idle` (default), `connecting`, `listening`, `thinking`, `speaking`, `disconnected` (connection down: the dead, flat look). LiveKit's room-lifecycle state names are accepted as aliases. Attribute: `state`.",
|
|
479
|
+
"default": "'idle'",
|
|
480
|
+
"displayType": "undefined | string"
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
"name": "size",
|
|
484
|
+
"type": "undefined | string",
|
|
485
|
+
"optional": true,
|
|
486
|
+
"scalar": true,
|
|
487
|
+
"description": "`icon` | `sm` | `md` (default) | `lg` | `xl`. Attribute: `size`.",
|
|
488
|
+
"default": "'md'",
|
|
489
|
+
"displayType": "undefined | string"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
"name": "barCount",
|
|
493
|
+
"type": "undefined | number",
|
|
494
|
+
"optional": true,
|
|
495
|
+
"scalar": true,
|
|
496
|
+
"description": "Bars to draw. Bar and radial only. Attribute: `bar-count`.",
|
|
497
|
+
"displayType": "undefined | number"
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
"name": "count",
|
|
501
|
+
"type": "undefined | number",
|
|
502
|
+
"optional": true,
|
|
503
|
+
"scalar": true,
|
|
504
|
+
"description": "Grid only: rows and columns of the (always square) grid. Attribute: `count`.",
|
|
505
|
+
"displayType": "undefined | number"
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
"name": "radius",
|
|
509
|
+
"type": "undefined | number",
|
|
510
|
+
"optional": true,
|
|
511
|
+
"scalar": true,
|
|
512
|
+
"description": "Radial only: ring distance from center, in px. Attribute: `radius`.",
|
|
513
|
+
"displayType": "undefined | number"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
"name": "spread",
|
|
517
|
+
"type": "undefined | number",
|
|
518
|
+
"optional": true,
|
|
519
|
+
"scalar": true,
|
|
520
|
+
"description": "Grid only: ring distance for the connecting animation, in cells. Attribute: `spread`.",
|
|
521
|
+
"displayType": "undefined | number"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"name": "interval",
|
|
525
|
+
"type": "undefined | number",
|
|
526
|
+
"optional": true,
|
|
527
|
+
"scalar": true,
|
|
528
|
+
"description": "Grid only: ms between scripted frames. Attribute: `interval`.",
|
|
529
|
+
"displayType": "undefined | number"
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
"name": "color",
|
|
533
|
+
"type": "undefined | string",
|
|
534
|
+
"optional": true,
|
|
535
|
+
"scalar": true,
|
|
536
|
+
"description": "CSS color for the geometry, overriding the inherited `currentColor`. Attribute: `color`.",
|
|
537
|
+
"displayType": "undefined | string"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"name": "complexity",
|
|
541
|
+
"type": "undefined | number",
|
|
542
|
+
"optional": true,
|
|
543
|
+
"scalar": true,
|
|
544
|
+
"description": "Shader variants only: pattern density, 0..1. Attribute: `complexity`.",
|
|
545
|
+
"displayType": "undefined | number"
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
"name": "label",
|
|
549
|
+
"type": "undefined | string",
|
|
550
|
+
"optional": true,
|
|
551
|
+
"scalar": true,
|
|
552
|
+
"description": "Setting this makes the element an announced image (`role=\"img\"`) instead of decorative (`aria-hidden`). Attribute: `label`.",
|
|
553
|
+
"displayType": "undefined | string"
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
"name": "stream",
|
|
557
|
+
"type": "undefined | MediaStream",
|
|
558
|
+
"optional": true,
|
|
559
|
+
"scalar": false,
|
|
560
|
+
"description": "Live microphone or WebRTC audio to analyze. JS property only.",
|
|
561
|
+
"displayType": "undefined | MediaStream"
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
"name": "audioElement",
|
|
565
|
+
"type": "undefined | HTMLMediaElement",
|
|
566
|
+
"optional": true,
|
|
567
|
+
"scalar": false,
|
|
568
|
+
"description": "An `<audio>` or `<video>` element to tap for its audio. JS property only.",
|
|
569
|
+
"displayType": "undefined | HTMLMediaElement"
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"name": "bands",
|
|
573
|
+
"type": "undefined | number[]",
|
|
574
|
+
"optional": true,
|
|
575
|
+
"scalar": false,
|
|
576
|
+
"description": "Pre-computed levels, 0..1. Set this and no AudioContext is ever built, which is what keeps headless/SSR rendering and browser-speech-synthesis playback (which exposes no audio node) free of Web Audio entirely. JS property only. A new array reference is required for each update; mutating the existing array in place will not re-render.",
|
|
577
|
+
"displayType": "undefined | number[]"
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
"name": "shader",
|
|
581
|
+
"type": "undefined | { fragment: string; uniforms?: undefined | Record<string, { type: \"1f\" | \"1i\" | \"1fv\" | \"2f\" | \"3f\" | \"3fv\" | \"4f\" | \"4fv\" | \"Matrix2fv\" | \"Matrix3fv\" | \"Matrix4fv\"; value: number | number[] }> }",
|
|
582
|
+
"optional": true,
|
|
583
|
+
"scalar": false,
|
|
584
|
+
"description": "Custom fragment shader for `variant=\"custom\"`. JS property only.",
|
|
585
|
+
"displayType": "undefined | { fragment: string; uniforms?: undefined | Record<string, { type: \"1f\" | \"1i\" | \"1fv\" | \"2f\" | \"3f\" | \"3fv\" | \"4f\" | \"4fv\" | \"Matrix2fv\" | \"Matrix3fv\" | \"Matrix4fv\"; value: number | number[] }> }"
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
"name": "animateWhenNotVisible",
|
|
589
|
+
"type": "undefined | false | true",
|
|
590
|
+
"optional": true,
|
|
591
|
+
"scalar": true,
|
|
592
|
+
"description": "Shader variants only: keep animating while scrolled off screen. Off by default, which stops drawing and releases the WebGL context until the element comes back (browsers ration contexts to roughly 16 a page). Does not override `prefers-reduced-motion`. Attribute: `animate-when-not-visible`.",
|
|
593
|
+
"displayType": "undefined | false | true"
|
|
594
|
+
}
|
|
595
|
+
],
|
|
596
|
+
"events": [],
|
|
597
|
+
"methods": [],
|
|
598
|
+
"composedFrom": [
|
|
599
|
+
{
|
|
600
|
+
"name": "AudioVisualizer",
|
|
601
|
+
"group": "Components",
|
|
602
|
+
"storyId": "solid-advanced-elements-audiovisualizer--docs"
|
|
603
|
+
}
|
|
604
|
+
],
|
|
605
|
+
"tokens": [],
|
|
606
|
+
"parts": [
|
|
607
|
+
{
|
|
608
|
+
"name": "bar",
|
|
609
|
+
"doc": "A single bar in the `bar` variant, or a single spoke in the `radial` variant. Also carries `data-kai-index` and `data-kai-highlighted` (\"true\"/\"false\") for use inside the shadow root; to style the lit state from OUTSIDE, combine with the `highlighted` part below rather than an attribute selector.",
|
|
610
|
+
"recipe": "kai-audio-visualizer::part(bar) { border-radius: 2px }\nkai-audio-visualizer::part(bar highlighted) { background: var(--brand) }"
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
"name": "cell",
|
|
614
|
+
"doc": "A single dot in the `grid` variant. Also carries `data-kai-index` and `data-kai-highlighted` (\"true\"/\"false\") for use inside the shadow root; to style the lit state from OUTSIDE, combine with the `highlighted` part below rather than an attribute selector.",
|
|
615
|
+
"recipe": "kai-audio-visualizer::part(cell) { border-radius: 9999px }\nkai-audio-visualizer::part(cell highlighted) { background: var(--brand) }"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
"name": "highlighted",
|
|
619
|
+
"doc": "A second part TOKEN present on a `bar` or `cell` exactly when the sequencer or live audio has it lit, not a standalone styleable element. Combine it in the same `::part()` argument: `::part(bar highlighted)` or `::part(cell highlighted)`. This is the external equivalent of the internal `data-kai-highlighted=\"true\"` attribute, which a `::part()` selector cannot reach (an attribute selector cannot follow a pseudo-element).",
|
|
620
|
+
"recipe": "kai-audio-visualizer::part(bar highlighted) { background: var(--brand) }\nkai-audio-visualizer::part(cell highlighted) { background: var(--brand) }"
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
"name": "canvas",
|
|
624
|
+
"doc": "The WebGL canvas backing the `wave` and `aurora` variants. Restyle its size or radius, or layer a mask/filter, from outside.",
|
|
625
|
+
"recipe": "kai-audio-visualizer::part(canvas) { border-radius: 0.75rem }"
|
|
626
|
+
}
|
|
627
|
+
]
|
|
628
|
+
},
|
|
459
629
|
{
|
|
460
630
|
"tag": "kai-avatar",
|
|
461
631
|
"className": "KaiAvatarElement",
|
|
@@ -164,6 +164,43 @@ export interface KaiAttachmentsElement extends HTMLElement {
|
|
|
164
164
|
emptyText?: string;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
export interface KaiAudioVisualizerElement extends HTMLElement {
|
|
168
|
+
/** Color mode (`auto` follows prefers-color-scheme). */
|
|
169
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
170
|
+
/** Look to render: `bar` (default), `grid`, `radial`, `wave`, `aurora`, `custom`. `aura` is accepted as a LiveKit-markup alias for `aurora`. Attribute: `variant`. */
|
|
171
|
+
variant?: string;
|
|
172
|
+
/** `idle` (default), `connecting`, `listening`, `thinking`, `speaking`, `disconnected` (connection down: the dead, flat look). LiveKit's room-lifecycle state names are accepted as aliases. Attribute: `state`. */
|
|
173
|
+
state?: string;
|
|
174
|
+
/** `icon` | `sm` | `md` (default) | `lg` | `xl`. Attribute: `size`. */
|
|
175
|
+
size?: string;
|
|
176
|
+
/** Bars to draw. Bar and radial only. Attribute: `bar-count`. */
|
|
177
|
+
barCount?: number;
|
|
178
|
+
/** Grid only: rows and columns of the (always square) grid. Attribute: `count`. */
|
|
179
|
+
count?: number;
|
|
180
|
+
/** Radial only: ring distance from center, in px. Attribute: `radius`. */
|
|
181
|
+
radius?: number;
|
|
182
|
+
/** Grid only: ring distance for the connecting animation, in cells. Attribute: `spread`. */
|
|
183
|
+
spread?: number;
|
|
184
|
+
/** Grid only: ms between scripted frames. Attribute: `interval`. */
|
|
185
|
+
interval?: number;
|
|
186
|
+
/** CSS color for the geometry, overriding the inherited `currentColor`. Attribute: `color`. */
|
|
187
|
+
color?: string;
|
|
188
|
+
/** Shader variants only: pattern density, 0..1. Attribute: `complexity`. */
|
|
189
|
+
complexity?: number;
|
|
190
|
+
/** Setting this makes the element an announced image (`role="img"`) instead of decorative (`aria-hidden`). Attribute: `label`. */
|
|
191
|
+
label?: string;
|
|
192
|
+
/** Live microphone or WebRTC audio to analyze. JS property only. */
|
|
193
|
+
stream?: MediaStream;
|
|
194
|
+
/** An `<audio>` or `<video>` element to tap for its audio. JS property only. */
|
|
195
|
+
audioElement?: HTMLMediaElement;
|
|
196
|
+
/** Pre-computed levels, 0..1. Set this and no AudioContext is ever built, which is what keeps headless/SSR rendering and browser-speech-synthesis playback (which exposes no audio node) free of Web Audio entirely. JS property only. A new array reference is required for each update; mutating the existing array in place will not re-render. */
|
|
197
|
+
bands?: number[];
|
|
198
|
+
/** Custom fragment shader for `variant="custom"`. JS property only. */
|
|
199
|
+
shader?: { fragment: string; uniforms?: Record<string, { type: "1f" | "1i" | "1fv" | "2f" | "3f" | "3fv" | "4f" | "4fv" | "Matrix2fv" | "Matrix3fv" | "Matrix4fv"; value: number | number[] }> };
|
|
200
|
+
/** Shader variants only: keep animating while scrolled off screen. Off by default, which stops drawing and releases the WebGL context until the element comes back (browsers ration contexts to roughly 16 a page). Does not override `prefers-reduced-motion`. Attribute: `animate-when-not-visible`. */
|
|
201
|
+
animateWhenNotVisible?: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
167
204
|
export interface KaiAvatarElement extends HTMLElement {
|
|
168
205
|
/** Color mode (`auto` follows prefers-color-scheme). */
|
|
169
206
|
theme?: 'light' | 'dark' | 'auto';
|
|
@@ -1366,6 +1403,7 @@ declare global {
|
|
|
1366
1403
|
'kai-agent-card': KaiAgentCardElement;
|
|
1367
1404
|
'kai-artifact': KaiArtifactElement;
|
|
1368
1405
|
'kai-attachments': KaiAttachmentsElement;
|
|
1406
|
+
'kai-audio-visualizer': KaiAudioVisualizerElement;
|
|
1369
1407
|
'kai-avatar': KaiAvatarElement;
|
|
1370
1408
|
'kai-badge': KaiBadgeElement;
|
|
1371
1409
|
'kai-button': KaiButtonElement;
|
package/src/elements/slots.ts
CHANGED
|
@@ -558,6 +558,40 @@ export const THREAD_SLOTS: SlotDef[] = [
|
|
|
558
558
|
{ name: 'empty', mode: 'replace', doc: 'Custom zero-state rendered in the message area while the thread is empty; replaces the built-in default.' },
|
|
559
559
|
];
|
|
560
560
|
|
|
561
|
+
/** Styleable `::part`s of `<kai-audio-visualizer>`. Every DOM variant (`bar`,
|
|
562
|
+
* `grid`, `radial`) shares the `bar`/`cell` markup pattern; the shader variants
|
|
563
|
+
* (`wave`, `aurora`) share the `canvas` part. Lit items ALSO carry the
|
|
564
|
+
* `highlighted` part token, so `::part(bar highlighted)`/`::part(cell
|
|
565
|
+
* highlighted)` selects only the lit ones. `::part(bar)[data-kai-highlighted=
|
|
566
|
+
* "true"]` looks plausible but is invalid CSS: a CSS attribute selector
|
|
567
|
+
* cannot follow a pseudo-element (`CSS.supports(selector(...))` returns
|
|
568
|
+
* `false` for it in Chromium, and `insertRule` throws). `data-kai-index` /
|
|
569
|
+
* `data-kai-highlighted` still exist on the element for consumers reading
|
|
570
|
+
* them from INSIDE the shadow root or via the Solid render-prop; they are
|
|
571
|
+
* just not reachable from an external `::part()` selector. */
|
|
572
|
+
export const AUDIO_VISUALIZER_PARTS: PartDef[] = [
|
|
573
|
+
{
|
|
574
|
+
name: 'bar',
|
|
575
|
+
doc: 'A single bar in the `bar` variant, or a single spoke in the `radial` variant. Also carries `data-kai-index` and `data-kai-highlighted` ("true"/"false") for use inside the shadow root; to style the lit state from OUTSIDE, combine with the `highlighted` part below rather than an attribute selector.',
|
|
576
|
+
recipe: 'kai-audio-visualizer::part(bar) { border-radius: 2px }\nkai-audio-visualizer::part(bar highlighted) { background: var(--brand) }',
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
name: 'cell',
|
|
580
|
+
doc: 'A single dot in the `grid` variant. Also carries `data-kai-index` and `data-kai-highlighted` ("true"/"false") for use inside the shadow root; to style the lit state from OUTSIDE, combine with the `highlighted` part below rather than an attribute selector.',
|
|
581
|
+
recipe: 'kai-audio-visualizer::part(cell) { border-radius: 9999px }\nkai-audio-visualizer::part(cell highlighted) { background: var(--brand) }',
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: 'highlighted',
|
|
585
|
+
doc: 'A second part TOKEN present on a `bar` or `cell` exactly when the sequencer or live audio has it lit, not a standalone styleable element. Combine it in the same `::part()` argument: `::part(bar highlighted)` or `::part(cell highlighted)`. This is the external equivalent of the internal `data-kai-highlighted="true"` attribute, which a `::part()` selector cannot reach (an attribute selector cannot follow a pseudo-element).',
|
|
586
|
+
recipe: 'kai-audio-visualizer::part(bar highlighted) { background: var(--brand) }\nkai-audio-visualizer::part(cell highlighted) { background: var(--brand) }',
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
name: 'canvas',
|
|
590
|
+
doc: 'The WebGL canvas backing the `wave` and `aurora` variants. Restyle its size or radius, or layer a mask/filter, from outside.',
|
|
591
|
+
recipe: 'kai-audio-visualizer::part(canvas) { border-radius: 0.75rem }',
|
|
592
|
+
},
|
|
593
|
+
];
|
|
594
|
+
|
|
561
595
|
export const ELEMENT_COMPOSITION: Record<string, ElementComposition> = {
|
|
562
596
|
'kai-chat': { slots: CHAT_SLOTS, parts: CHAT_PARTS },
|
|
563
597
|
'kai-command': { parts: COMMAND_PARTS },
|
|
@@ -597,6 +631,7 @@ export const ELEMENT_COMPOSITION: Record<string, ElementComposition> = {
|
|
|
597
631
|
'kai-search': { parts: SEARCH_PARTS },
|
|
598
632
|
'kai-kbd': { parts: KBD_PARTS },
|
|
599
633
|
'kai-editable-label': { parts: EDITABLE_LABEL_PARTS },
|
|
634
|
+
'kai-audio-visualizer': { parts: AUDIO_VISUALIZER_PARTS },
|
|
600
635
|
};
|
|
601
636
|
|
|
602
637
|
/**
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reductions from AnalyserNode output to the numbers a visualizer draws.
|
|
3
|
+
*
|
|
4
|
+
* Ported from livekit/components-js `packages/react/src/hooks/useTrackVolume.ts`
|
|
5
|
+
* (Apache License 2.0). Both the dB normalization curve AND the band split
|
|
6
|
+
* are carried over so our output matches theirs frame for frame: the split
|
|
7
|
+
* is upstream's linear-proportional chunking (their PR #1265). An interim
|
|
8
|
+
* revision here spaced the buckets geometrically to serve a wide raw-mic
|
|
9
|
+
* window; both reverted together -- see `reduceToBands` below and the
|
|
10
|
+
* DEFAULTS docstring in use-audio-analysis.ts for the full story.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** dB floor of the normalization curve. Below this reads as silence. */
|
|
14
|
+
const MIN_DB = -100;
|
|
15
|
+
/** dB ceiling. Above this saturates. */
|
|
16
|
+
const MAX_DB = -10;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Map one `getFloatFrequencyData` decibel value to 0..1.
|
|
20
|
+
*
|
|
21
|
+
* The `sqrt` at the end is a perceptual curve, not a normalization step: it
|
|
22
|
+
* lifts quiet detail so low-level audio still moves the bars visibly.
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeDb(value: number): number {
|
|
25
|
+
if (value === -Infinity) return 0;
|
|
26
|
+
const clamped = Math.max(MIN_DB, Math.min(MAX_DB, value));
|
|
27
|
+
return Math.sqrt(1 - (clamped * -1) / 100);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Average a slice of the frequency spectrum into `bands` buckets.
|
|
32
|
+
*
|
|
33
|
+
* `loPass` / `hiPass` are BIN INDICES relative to `fftSize`, not frequencies.
|
|
34
|
+
* Upstream's naming is misleading; the behavior is a plain array slice.
|
|
35
|
+
*
|
|
36
|
+
* Bucket edges are LINEAR-PROPORTIONAL across `[loPass, hiPass]` -- upstream
|
|
37
|
+
* LiveKit's own distribution (`useMultibandTrackVolume`, useTrackVolume.ts
|
|
38
|
+
* lines 141-153, rewritten to proportional chunking by their PR #1265): each
|
|
39
|
+
* band averages an equal consecutive run of bins, `floor(i * total / bands)`
|
|
40
|
+
* to `floor((i + 1) * total / bands)`, and a run that comes out empty reads
|
|
41
|
+
* 0 (their `chunkLength === 0` branch). With the default window's 100 bins
|
|
42
|
+
* and 5 bands, that is 20 bins averaged per band -- the averaging itself is
|
|
43
|
+
* part of upstream's idle stillness, diluting any single flickering bin
|
|
44
|
+
* 20:1 (noise-floor diagnosis, table B).
|
|
45
|
+
*
|
|
46
|
+
* An interim revision spaced these buckets geometrically so that a wide
|
|
47
|
+
* raw-microphone window (bins 4-120) kept its upper buckets alive. That
|
|
48
|
+
* concentrated the noisiest few low bins into band 0 undiluted, which the
|
|
49
|
+
* centre-out mirror below then promoted to the CENTRE element -- the
|
|
50
|
+
* measured core of the "white noise when the mic is on" defect (diagnosis
|
|
51
|
+
* table B: geometric band 0 idled at 0.52 on real room tone where this
|
|
52
|
+
* split reads 0.32 over the same wide window, and exactly 0 over the
|
|
53
|
+
* default one). Reverted to upstream's split when the default window
|
|
54
|
+
* reverted; the mirror stays, it is orthogonal.
|
|
55
|
+
*/
|
|
56
|
+
export function reduceToBands(
|
|
57
|
+
freq: Float32Array,
|
|
58
|
+
bands: number,
|
|
59
|
+
loPass: number,
|
|
60
|
+
hiPass: number,
|
|
61
|
+
): number[] {
|
|
62
|
+
// `slice` clamps both ends into the buffer and yields an empty window when
|
|
63
|
+
// hiPass <= loPass -- every bucket then reads 0 below, no special case.
|
|
64
|
+
const window = freq.slice(Math.max(0, loPass), Math.max(0, hiPass));
|
|
65
|
+
const total = window.length;
|
|
66
|
+
const out: number[] = [];
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < bands; i++) {
|
|
69
|
+
const start = Math.floor((i * total) / bands);
|
|
70
|
+
const end = Math.floor(((i + 1) * total) / bands);
|
|
71
|
+
if (end <= start) {
|
|
72
|
+
out.push(0);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
let sum = 0;
|
|
76
|
+
for (let j = start; j < end; j++) sum += normalizeDb(window[j] as number);
|
|
77
|
+
out.push(sum / (end - start));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Root-mean-square of `getByteFrequencyData`, scaled to 0..1. */
|
|
84
|
+
export function reduceToVolume(bytes: Uint8Array): number {
|
|
85
|
+
if (bytes.length === 0) return 0;
|
|
86
|
+
let sum = 0;
|
|
87
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
88
|
+
const a = bytes[i] as number;
|
|
89
|
+
sum += a * a;
|
|
90
|
+
}
|
|
91
|
+
return Math.sqrt(sum / bytes.length) / 255;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resize a caller-supplied band array to exactly `count` entries. Extra values
|
|
96
|
+
* are dropped; a short array repeats its last value (0 when empty) so a
|
|
97
|
+
* consumer passing the wrong length gets a sane picture instead of holes.
|
|
98
|
+
*/
|
|
99
|
+
export function normalizeVolumeBands(bands: number[], count: number): number[] {
|
|
100
|
+
if (bands.length === count) return bands;
|
|
101
|
+
if (bands.length > count) return bands.slice(0, count);
|
|
102
|
+
const last = bands[bands.length - 1] ?? 0;
|
|
103
|
+
return [...bands, ...new Array(count - bands.length).fill(last)];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Maps a smaller, ordered set of "half" band values (index 0 = loudest,
|
|
108
|
+
* typically lowest frequency) onto `count` positions laid out in a straight
|
|
109
|
+
* line -- a row of bars, or a row of grid columns -- centre-outward and
|
|
110
|
+
* mirrored: band 0 lands on the centre (an odd `count`) or the centre PAIR
|
|
111
|
+
* (an even one, both positions sharing band 0), band 1 on the pair either
|
|
112
|
+
* side of that, and so on outward to the two ends.
|
|
113
|
+
*
|
|
114
|
+
* Every scripted state in this component is already centre-oriented
|
|
115
|
+
* (`listening` blinks the centre bar, `connecting` sweeps inward from both
|
|
116
|
+
* ends, `thinking` sweeps the middle row) -- `speaking` ramping left to
|
|
117
|
+
* right, because it fed the analyser's raw band order straight across, was
|
|
118
|
+
* the one state inconsistent with that. This closes that gap.
|
|
119
|
+
*
|
|
120
|
+
* `halfBands` should have exactly `Math.ceil(count / 2)` entries -- the
|
|
121
|
+
* caller is responsible for requesting that many bands from
|
|
122
|
+
* `useAudioAnalysis`, matching counts exactly rather than leaning on
|
|
123
|
+
* `normalizeVolumeBands`'s pad-by-repeating-the-last-value, which would
|
|
124
|
+
* produce a subtly wrong (not obviously broken) shape here. A shorter
|
|
125
|
+
* `halfBands` still degrades sanely: the outermost positions repeat the last
|
|
126
|
+
* value available rather than reading `undefined`.
|
|
127
|
+
*/
|
|
128
|
+
export function mirrorBandsCenterOut(halfBands: number[], count: number): number[] {
|
|
129
|
+
const center = (count - 1) / 2;
|
|
130
|
+
const out: number[] = new Array(count);
|
|
131
|
+
for (let i = 0; i < count; i++) {
|
|
132
|
+
const ring = Math.floor(Math.abs(i - center));
|
|
133
|
+
out[i] = halfBands[Math.min(ring, halfBands.length - 1)] ?? 0;
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Maps a smaller, ordered set of "half" band values onto `count` positions
|
|
140
|
+
* arranged around a RING (radial's spokes), mirrored left-right across a
|
|
141
|
+
* single vertical axis rather than fanned from a linear centre: index 0 and
|
|
142
|
+
* index `count / 2` (for an even `count`; radial warns when `count` is not
|
|
143
|
+
* divisible by 4, so this is the common case) are each their own fixed
|
|
144
|
+
* point of the reflection and both land band 0, and every other index pairs
|
|
145
|
+
* with its mirror partner at `count - i`, sharing a band.
|
|
146
|
+
*
|
|
147
|
+
* Radial's own geometry (`variant-radial.tsx`) places index 0 at the
|
|
148
|
+
* BOTTOM of the ring (`rotate(0) translateY(radius)`, and CSS rotation is
|
|
149
|
+
* clockwise for a positive angle), so band 0 (usually the loudest) reads at
|
|
150
|
+
* the bottom, fading toward the top -- not band 0 at both the top AND the
|
|
151
|
+
* bottom, which would need a second, horizontal mirror axis on top of this
|
|
152
|
+
* one. Chosen over that fuller symmetry because it reuses the exact same
|
|
153
|
+
* `Math.ceil(count / 2)` band request as the linear mirror above (one rule
|
|
154
|
+
* for every variant, not a radial-specific band count), and because the
|
|
155
|
+
* task's request was "spikes vary in length, not moving as one" -- this
|
|
156
|
+
* already delivers that. If bottom-loud/top-quiet reads wrong in practice,
|
|
157
|
+
* swapping to true 4-fold symmetry is a self-contained change here, not
|
|
158
|
+
* elsewhere.
|
|
159
|
+
*
|
|
160
|
+
* For an even `count`, the antipodal index (`count / 2`) needs one more
|
|
161
|
+
* distinct band than the linear mirror does (rings run 0..`count / 2`, not
|
|
162
|
+
* 0..`count / 2 - 1`) -- with only `Math.ceil(count / 2)` bands available,
|
|
163
|
+
* that one index clamps to the same value as its nearest neighbour rather
|
|
164
|
+
* than getting a unique band. A minor, deliberate simplification for the
|
|
165
|
+
* same one-rule-for-every-variant reason above.
|
|
166
|
+
*/
|
|
167
|
+
export function mirrorBandsAroundRing(halfBands: number[], count: number): number[] {
|
|
168
|
+
const out: number[] = new Array(count);
|
|
169
|
+
for (let i = 0; i < count; i++) {
|
|
170
|
+
const ring = Math.min(i, count - i);
|
|
171
|
+
out[i] = halfBands[Math.min(ring, halfBands.length - 1)] ?? 0;
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|