@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.
- package/dist/astro-voice-pill/VoicePill.astro +159 -0
- package/dist/astro-voice-pill/index.ts +27 -0
- package/dist/astro-waveform/Waveform.astro +279 -0
- package/dist/astro-waveform/index.ts +29 -0
- package/dist/index.ts +2 -0
- package/dist/slide-viewer/slide-viewer.styles.ts +3 -1
- package/dist/status-indicator/status-indicator.ts +8 -5
- package/dist/version-selector/version-selector.styles.ts +3 -1
- package/dist/voice-pill/index.ts +31 -0
- package/dist/voice-pill/voice-pill.styles.ts +78 -0
- package/dist/voice-pill/voice-pill.ts +235 -0
- package/dist/waveform/index.ts +39 -0
- package/dist/waveform/waveform-renderer.ts +181 -0
- package/dist/waveform/waveform.styles.ts +19 -0
- package/dist/waveform/waveform.ts +262 -0
- package/package.json +3 -1
|
@@ -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.
|
|
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:*",
|