@refraction-ui/astro 0.4.13 → 0.5.1

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.
@@ -0,0 +1,262 @@
1
+ import type { AccessibilityProps } from '../shared/index.ts'
2
+
3
+ export type WaveformVariant = 'bars' | 'line' | 'rings'
4
+ export type WaveformSampleInput = Float32Array | number[]
5
+ export type WaveformSource = AnalyserNode | MediaStream | WaveformSampleInput
6
+
7
+ export interface WaveformProps {
8
+ source?: WaveformSource
9
+ samples?: WaveformSampleInput
10
+ intensity?: number
11
+ amplitude?: number
12
+ variant?: WaveformVariant
13
+ height?: number | string
14
+ width?: number | string
15
+ barCount?: number
16
+ smoothing?: number
17
+ color?: string
18
+ paused?: boolean
19
+ }
20
+
21
+ export interface NormalizedWaveformConfig {
22
+ variant: WaveformVariant
23
+ intensity: number
24
+ amplitude: number
25
+ height: number | string
26
+ width: number | string
27
+ barCount: number
28
+ smoothing: number
29
+ color: string
30
+ paused: boolean
31
+ }
32
+
33
+ export interface WaveformAPI {
34
+ config: NormalizedWaveformConfig
35
+ samples: Float32Array
36
+ ariaProps: Partial<AccessibilityProps>
37
+ dataAttributes: Record<'data-variant' | 'data-paused', string>
38
+ getSamples: (barCount?: number) => Float32Array
39
+ }
40
+
41
+ export const DEFAULT_WAVEFORM_BAR_COUNT = 48
42
+ export const DEFAULT_WAVEFORM_COLOR = 'var(--accent-2)'
43
+ export const DEFAULT_WAVEFORM_HEIGHT = 80
44
+ export const DEFAULT_WAVEFORM_INTENSITY = 1
45
+ export const DEFAULT_WAVEFORM_AMPLITUDE = 1
46
+ export const DEFAULT_WAVEFORM_SMOOTHING = 0.8
47
+
48
+ const MAX_BAR_COUNT = 1024
49
+ const MAX_INTENSITY = 1
50
+
51
+ export function normalizeBarCount(value: number | undefined): number {
52
+ if (value == null || !Number.isFinite(value)) return DEFAULT_WAVEFORM_BAR_COUNT
53
+
54
+ return Math.min(MAX_BAR_COUNT, Math.max(1, Math.round(value)))
55
+ }
56
+
57
+ export function normalizeSmoothing(value: number | undefined): number {
58
+ if (value == null || !Number.isFinite(value)) return DEFAULT_WAVEFORM_SMOOTHING
59
+
60
+ return Math.min(0.99, Math.max(0, value))
61
+ }
62
+
63
+ export function normalizeIntensity(value: number | undefined): number {
64
+ if (value == null || !Number.isFinite(value)) return DEFAULT_WAVEFORM_INTENSITY
65
+
66
+ return Math.min(MAX_INTENSITY, Math.max(0, value))
67
+ }
68
+
69
+ export function normalizeAmplitude(value: number | undefined): number {
70
+ if (value == null || !Number.isFinite(value)) return DEFAULT_WAVEFORM_AMPLITUDE
71
+
72
+ return Math.min(1, Math.max(0, value))
73
+ }
74
+
75
+ export function normalizeWaveformConfig(props: WaveformProps = {}): NormalizedWaveformConfig {
76
+ return {
77
+ variant: props.variant ?? 'bars',
78
+ intensity: normalizeIntensity(props.intensity),
79
+ amplitude: normalizeAmplitude(props.amplitude),
80
+ height: props.height ?? DEFAULT_WAVEFORM_HEIGHT,
81
+ width: props.width ?? '100%',
82
+ barCount: normalizeBarCount(props.barCount),
83
+ smoothing: normalizeSmoothing(props.smoothing),
84
+ color: props.color ?? DEFAULT_WAVEFORM_COLOR,
85
+ paused: props.paused ?? false,
86
+ }
87
+ }
88
+
89
+ export function isWaveformSampleInput(value: unknown): value is WaveformSampleInput {
90
+ return value instanceof Float32Array || Array.isArray(value)
91
+ }
92
+
93
+ export function createSilentSamples(count = DEFAULT_WAVEFORM_BAR_COUNT): Float32Array {
94
+ return new Float32Array(normalizeBarCount(count))
95
+ }
96
+
97
+ export function createIntensitySamples(
98
+ intensity = DEFAULT_WAVEFORM_INTENSITY,
99
+ count = DEFAULT_WAVEFORM_BAR_COUNT,
100
+ phase = 0,
101
+ ): Float32Array {
102
+ const normalizedIntensity = normalizeIntensity(intensity)
103
+ const normalizedCount = normalizeBarCount(count)
104
+ const samples = new Float32Array(normalizedCount)
105
+
106
+ if (normalizedIntensity === 0) return samples
107
+
108
+ const pulse = 0.72 + Math.sin(phase * 1.8) * 0.18
109
+
110
+ for (let i = 0; i < normalizedCount; i += 1) {
111
+ const progress = normalizedCount <= 1 ? 0.5 : i / (normalizedCount - 1)
112
+ const envelope = 0.35 + Math.sin(progress * Math.PI) * 0.65
113
+ const carrier = Math.sin(progress * Math.PI * 6 + phase * 2.8)
114
+ samples[i] = carrier * envelope * normalizedIntensity * pulse
115
+ }
116
+
117
+ return samples
118
+ }
119
+
120
+ export function normalizeWaveformSamples(
121
+ input: WaveformSampleInput | undefined,
122
+ targetCount?: number,
123
+ ): Float32Array {
124
+ const sourceLength = input?.length ?? 0
125
+
126
+ if (sourceLength === 0) {
127
+ return targetCount == null ? new Float32Array() : createSilentSamples(targetCount)
128
+ }
129
+
130
+ const normalized = new Float32Array(sourceLength)
131
+ let maxAbs = 0
132
+
133
+ for (let i = 0; i < sourceLength; i += 1) {
134
+ const value = Number(input?.[i])
135
+ const sample = Number.isFinite(value) ? value : 0
136
+ normalized[i] = sample
137
+ maxAbs = Math.max(maxAbs, Math.abs(sample))
138
+ }
139
+
140
+ const divisor = maxAbs > 1 ? maxAbs : 1
141
+
142
+ for (let i = 0; i < normalized.length; i += 1) {
143
+ normalized[i] = clamp(normalized[i] / divisor, -1, 1)
144
+ }
145
+
146
+ if (targetCount == null || targetCount === normalized.length) {
147
+ return normalized
148
+ }
149
+
150
+ return resampleWaveformSamples(normalized, targetCount)
151
+ }
152
+
153
+ export function resampleWaveformSamples(
154
+ samples: WaveformSampleInput,
155
+ targetCount: number,
156
+ ): Float32Array {
157
+ const count = normalizeBarCount(targetCount)
158
+ const sourceLength = samples.length
159
+
160
+ if (sourceLength === 0) return createSilentSamples(count)
161
+ if (sourceLength === count) return Float32Array.from(samples)
162
+
163
+ const output = new Float32Array(count)
164
+
165
+ for (let i = 0; i < count; i += 1) {
166
+ const start = (i * sourceLength) / count
167
+ const end = ((i + 1) * sourceLength) / count
168
+ const firstIndex = Math.floor(start)
169
+ const lastIndex = Math.max(firstIndex + 1, Math.ceil(end))
170
+ let total = 0
171
+ let values = 0
172
+
173
+ for (let j = firstIndex; j < lastIndex && j < sourceLength; j += 1) {
174
+ total += Number(samples[j])
175
+ values += 1
176
+ }
177
+
178
+ output[i] = values > 0 ? total / values : Number(samples[Math.min(firstIndex, sourceLength - 1)])
179
+ }
180
+
181
+ return output
182
+ }
183
+
184
+ export function smoothWaveformSamples(
185
+ previous: WaveformSampleInput | undefined,
186
+ next: WaveformSampleInput,
187
+ smoothing = DEFAULT_WAVEFORM_SMOOTHING,
188
+ ): Float32Array {
189
+ if (previous == null || previous.length === 0) {
190
+ return Float32Array.from(next)
191
+ }
192
+
193
+ const amount = normalizeSmoothing(smoothing)
194
+ const output = new Float32Array(next.length)
195
+
196
+ for (let i = 0; i < next.length; i += 1) {
197
+ const previousValue = Number(previous[i] ?? next[i])
198
+ output[i] = previousValue * amount + Number(next[i]) * (1 - amount)
199
+ }
200
+
201
+ return output
202
+ }
203
+
204
+ export function scaleWaveformSamples(
205
+ samples: WaveformSampleInput,
206
+ intensity = DEFAULT_WAVEFORM_INTENSITY,
207
+ ): Float32Array {
208
+ const amount = normalizeIntensity(intensity)
209
+ const output = new Float32Array(samples.length)
210
+
211
+ for (let i = 0; i < samples.length; i += 1) {
212
+ output[i] = clamp(Number(samples[i]) * amount, -1, 1)
213
+ }
214
+
215
+ return output
216
+ }
217
+
218
+ export function getWaveformPeak(samples: WaveformSampleInput): number {
219
+ let peak = 0
220
+
221
+ for (let i = 0; i < samples.length; i += 1) {
222
+ peak = Math.max(peak, Math.abs(Number(samples[i]) || 0))
223
+ }
224
+
225
+ return Math.min(1, peak)
226
+ }
227
+
228
+ export function toCssDimension(value: number | string | undefined): string | undefined {
229
+ if (value == null) return undefined
230
+
231
+ return typeof value === 'number' ? `${value}px` : value
232
+ }
233
+
234
+ export function createWaveform(props: WaveformProps = {}): WaveformAPI {
235
+ const config = normalizeWaveformConfig(props)
236
+ const inputSamples = props.samples ?? (isWaveformSampleInput(props.source) ? props.source : undefined)
237
+ const samples = inputSamples == null
238
+ ? createIntensitySamples(config.intensity, config.barCount)
239
+ : normalizeWaveformSamples(inputSamples, config.barCount)
240
+
241
+ return {
242
+ config,
243
+ samples,
244
+ ariaProps: {
245
+ role: 'img',
246
+ 'aria-label': 'Audio waveform',
247
+ },
248
+ dataAttributes: {
249
+ 'data-variant': config.variant,
250
+ 'data-paused': String(config.paused),
251
+ },
252
+ getSamples(barCount = config.barCount) {
253
+ return inputSamples == null
254
+ ? createIntensitySamples(config.intensity, barCount)
255
+ : normalizeWaveformSamples(inputSamples, barCount)
256
+ },
257
+ }
258
+ }
259
+
260
+ function clamp(value: number, min: number, max: number): number {
261
+ return Math.min(max, Math.max(min, value))
262
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@refraction-ui/astro",
3
- "version": "0.4.13",
3
+ "version": "0.5.1",
4
4
  "description": "All Refraction UI Astro components in one package",
5
5
  "type": "module",
6
6
  "exports": {
@@ -77,6 +77,8 @@
77
77
  "@refraction-ui/astro-tooltip": "workspace:*",
78
78
  "@refraction-ui/astro-version-selector": "workspace:*",
79
79
  "@refraction-ui/astro-video-player": "workspace:*",
80
+ "@refraction-ui/astro-voice-pill": "workspace:*",
81
+ "@refraction-ui/astro-waveform": "workspace:*",
80
82
  "@refraction-ui/shared": "workspace:*",
81
83
  "@refraction-ui/astro-table-of-contents": "workspace:*",
82
84
  "@refraction-ui/astro-carousel": "workspace:*",