@refraction-ui/astro 0.5.0 → 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.
|
@@ -12,6 +12,7 @@ import { cn } from '../shared/index.ts'
|
|
|
12
12
|
interface Props extends Omit<astroHTML.JSX.HTMLAttributes, 'color' | 'height' | 'width'> {
|
|
13
13
|
samples?: WaveformSampleInput
|
|
14
14
|
intensity?: number
|
|
15
|
+
amplitude?: number
|
|
15
16
|
variant?: WaveformVariant
|
|
16
17
|
height?: number | string
|
|
17
18
|
width?: number | string
|
|
@@ -24,6 +25,7 @@ interface Props extends Omit<astroHTML.JSX.HTMLAttributes, 'color' | 'height' |
|
|
|
24
25
|
const {
|
|
25
26
|
samples,
|
|
26
27
|
intensity,
|
|
28
|
+
amplitude,
|
|
27
29
|
variant,
|
|
28
30
|
height,
|
|
29
31
|
width,
|
|
@@ -39,6 +41,7 @@ const {
|
|
|
39
41
|
const api = createWaveform({
|
|
40
42
|
samples,
|
|
41
43
|
intensity,
|
|
44
|
+
amplitude,
|
|
42
45
|
variant,
|
|
43
46
|
height,
|
|
44
47
|
width,
|
|
@@ -65,6 +68,7 @@ const waveformData = {
|
|
|
65
68
|
samples: Array.from(api.samples),
|
|
66
69
|
variant: api.config.variant,
|
|
67
70
|
intensity: api.config.intensity,
|
|
71
|
+
amplitude: api.config.amplitude,
|
|
68
72
|
barCount: api.config.barCount,
|
|
69
73
|
color: api.config.color,
|
|
70
74
|
}
|
|
@@ -139,10 +143,11 @@ function toKebabCase(value: string): string {
|
|
|
139
143
|
})
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
const scaleSamples = (samples, intensity) => {
|
|
146
|
+
const scaleSamples = (samples, intensity, amplitude) => {
|
|
143
147
|
const amount = Math.max(0, Math.min(1, Number(intensity) || 0))
|
|
148
|
+
const visualHeight = Math.max(0, Math.min(1, Number(amplitude) || 0))
|
|
144
149
|
|
|
145
|
-
return samples.map((sample) => Math.max(-1, Math.min(1, sample * amount)))
|
|
150
|
+
return samples.map((sample) => Math.max(-1, Math.min(1, sample * amount * visualHeight)))
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
const resolveColor = (element, color) => {
|
|
@@ -248,6 +253,7 @@ function toKebabCase(value: string): string {
|
|
|
248
253
|
const samples = scaleSamples(
|
|
249
254
|
normalizeSamples(config.samples, config.barCount),
|
|
250
255
|
config.intensity,
|
|
256
|
+
config.amplitude,
|
|
251
257
|
)
|
|
252
258
|
const color = resolveColor(canvas.parentElement || canvas, config.color || 'currentColor')
|
|
253
259
|
|
|
@@ -22,33 +22,34 @@ export const voicePillSpeakerStyles =
|
|
|
22
22
|
'[--rfr-voice-pill-accent:hsl(var(--primary))] [--rfr-voice-pill-accent-foreground:hsl(var(--primary-foreground))] data-[speaker=user]:[--rfr-voice-pill-accent:hsl(var(--accent-foreground))] data-[speaker=user]:[--rfr-voice-pill-accent-foreground:hsl(var(--accent))] data-[muted=true]:[--rfr-voice-pill-accent:hsl(var(--muted-foreground))]'
|
|
23
23
|
|
|
24
24
|
export const voicePillPositionVariants = cva({
|
|
25
|
-
base: '
|
|
25
|
+
base: 'z-50',
|
|
26
26
|
variants: {
|
|
27
27
|
position: {
|
|
28
|
+
inline: 'relative',
|
|
28
29
|
'top-start':
|
|
29
|
-
'top-[calc(env(safe-area-inset-top)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
|
|
30
|
+
'fixed top-[calc(env(safe-area-inset-top)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
|
|
30
31
|
'top-center':
|
|
31
|
-
'top-[calc(env(safe-area-inset-top)+1rem)] left-1/2 -translate-x-1/2',
|
|
32
|
+
'fixed top-[calc(env(safe-area-inset-top)+1rem)] left-1/2 -translate-x-1/2',
|
|
32
33
|
'top-end':
|
|
33
|
-
'top-[calc(env(safe-area-inset-top)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
|
|
34
|
+
'fixed top-[calc(env(safe-area-inset-top)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
|
|
34
35
|
'bottom-start':
|
|
35
|
-
'bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
|
|
36
|
+
'fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-[calc(env(safe-area-inset-left)+1rem)]',
|
|
36
37
|
'bottom-center':
|
|
37
|
-
'bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-1/2 -translate-x-1/2',
|
|
38
|
+
'fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] left-1/2 -translate-x-1/2',
|
|
38
39
|
'bottom-end':
|
|
39
|
-
'bottom-[calc(env(safe-area-inset-bottom)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
|
|
40
|
+
'fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] right-[calc(env(safe-area-inset-right)+1rem)]',
|
|
40
41
|
'left-start':
|
|
41
|
-
'left-[calc(env(safe-area-inset-left)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
|
|
42
|
+
'fixed left-[calc(env(safe-area-inset-left)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
|
|
42
43
|
'left-center':
|
|
43
|
-
'left-[calc(env(safe-area-inset-left)+1rem)] top-1/2 -translate-y-1/2',
|
|
44
|
+
'fixed left-[calc(env(safe-area-inset-left)+1rem)] top-1/2 -translate-y-1/2',
|
|
44
45
|
'left-end':
|
|
45
|
-
'left-[calc(env(safe-area-inset-left)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
|
|
46
|
+
'fixed left-[calc(env(safe-area-inset-left)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
|
|
46
47
|
'right-start':
|
|
47
|
-
'right-[calc(env(safe-area-inset-right)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
|
|
48
|
+
'fixed right-[calc(env(safe-area-inset-right)+1rem)] top-[calc(env(safe-area-inset-top)+1rem)]',
|
|
48
49
|
'right-center':
|
|
49
|
-
'right-[calc(env(safe-area-inset-right)+1rem)] top-1/2 -translate-y-1/2',
|
|
50
|
+
'fixed right-[calc(env(safe-area-inset-right)+1rem)] top-1/2 -translate-y-1/2',
|
|
50
51
|
'right-end':
|
|
51
|
-
'right-[calc(env(safe-area-inset-right)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
|
|
52
|
+
'fixed right-[calc(env(safe-area-inset-right)+1rem)] bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
|
|
52
53
|
},
|
|
53
54
|
},
|
|
54
55
|
defaultVariants: {
|
|
@@ -3,6 +3,7 @@ import type { AccessibilityProps } from '../shared/index.ts'
|
|
|
3
3
|
export type VoicePillSpeaker = 'ai' | 'user' | (string & {})
|
|
4
4
|
|
|
5
5
|
export type VoicePillPosition =
|
|
6
|
+
| 'inline'
|
|
6
7
|
| 'top-start'
|
|
7
8
|
| 'top-center'
|
|
8
9
|
| 'top-end'
|
|
@@ -29,7 +30,7 @@ export interface VoicePillProps {
|
|
|
29
30
|
muted?: boolean
|
|
30
31
|
/** Callback for the optional mute toggle */
|
|
31
32
|
onToggleMute?: () => void
|
|
32
|
-
/**
|
|
33
|
+
/** Viewport position, or inline for embedding inside another layout */
|
|
33
34
|
position?: VoicePillPosition
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -52,7 +53,7 @@ export interface VoicePillAPI {
|
|
|
52
53
|
visualIntensity: number
|
|
53
54
|
/** Muted state */
|
|
54
55
|
muted: boolean
|
|
55
|
-
/**
|
|
56
|
+
/** Viewport position, or inline for embedding inside another layout */
|
|
56
57
|
position: VoicePillPosition
|
|
57
58
|
/** Fallback initials for the speaker avatar */
|
|
58
59
|
initials: string
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_WAVEFORM_COLOR,
|
|
3
|
+
DEFAULT_WAVEFORM_AMPLITUDE,
|
|
3
4
|
DEFAULT_WAVEFORM_INTENSITY,
|
|
4
5
|
getWaveformPeak,
|
|
6
|
+
normalizeAmplitude,
|
|
5
7
|
normalizeBarCount,
|
|
6
8
|
normalizeIntensity,
|
|
7
9
|
resampleWaveformSamples,
|
|
@@ -20,6 +22,7 @@ export interface WaveformRenderOptions {
|
|
|
20
22
|
variant?: WaveformVariant
|
|
21
23
|
color?: string
|
|
22
24
|
intensity?: number
|
|
25
|
+
amplitude?: number
|
|
23
26
|
barCount?: number
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -60,22 +63,23 @@ export function drawWaveform(
|
|
|
60
63
|
const variant = options.variant ?? 'bars'
|
|
61
64
|
const color = options.color ?? DEFAULT_WAVEFORM_COLOR
|
|
62
65
|
const intensity = normalizeIntensity(options.intensity ?? DEFAULT_WAVEFORM_INTENSITY)
|
|
66
|
+
const amplitude = normalizeAmplitude(options.amplitude ?? DEFAULT_WAVEFORM_AMPLITUDE)
|
|
63
67
|
const barCount = normalizeBarCount(options.barCount)
|
|
64
68
|
|
|
65
69
|
context.clearRect(0, 0, width, height)
|
|
66
70
|
applyCanvasColor(context, color)
|
|
67
71
|
|
|
68
72
|
if (variant === 'line') {
|
|
69
|
-
drawLine(context, samples, width, height, intensity)
|
|
73
|
+
drawLine(context, samples, width, height, intensity, amplitude)
|
|
70
74
|
return
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
if (variant === 'rings') {
|
|
74
|
-
drawRings(context, samples, width, height, intensity)
|
|
78
|
+
drawRings(context, samples, width, height, intensity, amplitude)
|
|
75
79
|
return
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
drawBars(context, samples, width, height, intensity, barCount)
|
|
82
|
+
drawBars(context, samples, width, height, intensity, amplitude, barCount)
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
function drawBars(
|
|
@@ -84,6 +88,7 @@ function drawBars(
|
|
|
84
88
|
width: number,
|
|
85
89
|
height: number,
|
|
86
90
|
intensity: number,
|
|
91
|
+
amplitude: number,
|
|
87
92
|
barCount: number,
|
|
88
93
|
) {
|
|
89
94
|
const bars = scaleWaveformSamples(resampleWaveformSamples(samples, barCount), intensity)
|
|
@@ -93,7 +98,7 @@ function drawBars(
|
|
|
93
98
|
|
|
94
99
|
for (let i = 0; i < bars.length; i += 1) {
|
|
95
100
|
const value = Math.abs(bars[i])
|
|
96
|
-
const barHeight = Math.max(value * height, value > 0 ? 1 : 0)
|
|
101
|
+
const barHeight = Math.max(value * height * amplitude, value > 0 ? 1 : 0)
|
|
97
102
|
const x = i * slotWidth + (slotWidth - barWidth) / 2
|
|
98
103
|
const y = center - barHeight / 2
|
|
99
104
|
|
|
@@ -107,10 +112,11 @@ function drawLine(
|
|
|
107
112
|
width: number,
|
|
108
113
|
height: number,
|
|
109
114
|
intensity: number,
|
|
115
|
+
amplitudeScale: number,
|
|
110
116
|
) {
|
|
111
117
|
const lineSamples = scaleWaveformSamples(samples, intensity)
|
|
112
118
|
const center = height / 2
|
|
113
|
-
const amplitude = height * 0.45
|
|
119
|
+
const amplitude = height * 0.45 * amplitudeScale
|
|
114
120
|
|
|
115
121
|
context.beginPath()
|
|
116
122
|
|
|
@@ -137,8 +143,9 @@ function drawRings(
|
|
|
137
143
|
width: number,
|
|
138
144
|
height: number,
|
|
139
145
|
intensity: number,
|
|
146
|
+
amplitude: number,
|
|
140
147
|
) {
|
|
141
|
-
const peak = getWaveformPeak(scaleWaveformSamples(samples, intensity))
|
|
148
|
+
const peak = getWaveformPeak(scaleWaveformSamples(samples, intensity)) * amplitude
|
|
142
149
|
const minDimension = Math.min(width, height)
|
|
143
150
|
const centerX = width / 2
|
|
144
151
|
const centerY = height / 2
|
|
@@ -8,6 +8,7 @@ export interface WaveformProps {
|
|
|
8
8
|
source?: WaveformSource
|
|
9
9
|
samples?: WaveformSampleInput
|
|
10
10
|
intensity?: number
|
|
11
|
+
amplitude?: number
|
|
11
12
|
variant?: WaveformVariant
|
|
12
13
|
height?: number | string
|
|
13
14
|
width?: number | string
|
|
@@ -20,6 +21,7 @@ export interface WaveformProps {
|
|
|
20
21
|
export interface NormalizedWaveformConfig {
|
|
21
22
|
variant: WaveformVariant
|
|
22
23
|
intensity: number
|
|
24
|
+
amplitude: number
|
|
23
25
|
height: number | string
|
|
24
26
|
width: number | string
|
|
25
27
|
barCount: number
|
|
@@ -40,6 +42,7 @@ export const DEFAULT_WAVEFORM_BAR_COUNT = 48
|
|
|
40
42
|
export const DEFAULT_WAVEFORM_COLOR = 'var(--accent-2)'
|
|
41
43
|
export const DEFAULT_WAVEFORM_HEIGHT = 80
|
|
42
44
|
export const DEFAULT_WAVEFORM_INTENSITY = 1
|
|
45
|
+
export const DEFAULT_WAVEFORM_AMPLITUDE = 1
|
|
43
46
|
export const DEFAULT_WAVEFORM_SMOOTHING = 0.8
|
|
44
47
|
|
|
45
48
|
const MAX_BAR_COUNT = 1024
|
|
@@ -63,10 +66,17 @@ export function normalizeIntensity(value: number | undefined): number {
|
|
|
63
66
|
return Math.min(MAX_INTENSITY, Math.max(0, value))
|
|
64
67
|
}
|
|
65
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
|
+
|
|
66
75
|
export function normalizeWaveformConfig(props: WaveformProps = {}): NormalizedWaveformConfig {
|
|
67
76
|
return {
|
|
68
77
|
variant: props.variant ?? 'bars',
|
|
69
78
|
intensity: normalizeIntensity(props.intensity),
|
|
79
|
+
amplitude: normalizeAmplitude(props.amplitude),
|
|
70
80
|
height: props.height ?? DEFAULT_WAVEFORM_HEIGHT,
|
|
71
81
|
width: props.width ?? '100%',
|
|
72
82
|
barCount: normalizeBarCount(props.barCount),
|
|
@@ -87,6 +97,7 @@ export function createSilentSamples(count = DEFAULT_WAVEFORM_BAR_COUNT): Float32
|
|
|
87
97
|
export function createIntensitySamples(
|
|
88
98
|
intensity = DEFAULT_WAVEFORM_INTENSITY,
|
|
89
99
|
count = DEFAULT_WAVEFORM_BAR_COUNT,
|
|
100
|
+
phase = 0,
|
|
90
101
|
): Float32Array {
|
|
91
102
|
const normalizedIntensity = normalizeIntensity(intensity)
|
|
92
103
|
const normalizedCount = normalizeBarCount(count)
|
|
@@ -94,11 +105,13 @@ export function createIntensitySamples(
|
|
|
94
105
|
|
|
95
106
|
if (normalizedIntensity === 0) return samples
|
|
96
107
|
|
|
108
|
+
const pulse = 0.72 + Math.sin(phase * 1.8) * 0.18
|
|
109
|
+
|
|
97
110
|
for (let i = 0; i < normalizedCount; i += 1) {
|
|
98
111
|
const progress = normalizedCount <= 1 ? 0.5 : i / (normalizedCount - 1)
|
|
99
112
|
const envelope = 0.35 + Math.sin(progress * Math.PI) * 0.65
|
|
100
|
-
const carrier = Math.sin(progress * Math.PI * 6)
|
|
101
|
-
samples[i] = carrier * envelope * normalizedIntensity
|
|
113
|
+
const carrier = Math.sin(progress * Math.PI * 6 + phase * 2.8)
|
|
114
|
+
samples[i] = carrier * envelope * normalizedIntensity * pulse
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
return samples
|