panelui-native 0.13.0 → 0.16.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 +41 -23
- package/lib/module/components/button/index.js +12 -9
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/chip/index.js +313 -0
- package/lib/module/components/chip/index.js.map +1 -0
- package/lib/module/components/direction/index.js +86 -0
- package/lib/module/components/direction/index.js.map +1 -0
- package/lib/module/components/frame/index.js +41 -39
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +772 -0
- package/lib/module/components/heatmap-chart/index.js.map +1 -0
- package/lib/module/components/input/index.js +4 -0
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/message/index.js +16 -1
- package/lib/module/components/message/index.js.map +1 -1
- package/lib/module/components/scroll-canvas/index.js +175 -0
- package/lib/module/components/scroll-canvas/index.js.map +1 -0
- package/lib/module/components/scroll-text/index.js +213 -0
- package/lib/module/components/scroll-text/index.js.map +1 -0
- package/lib/module/components/select/index.js +20 -24
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slider/index.js +2 -5
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/soundwave/index.js +813 -0
- package/lib/module/components/soundwave/index.js.map +1 -0
- package/lib/module/components/switch/index.js +15 -17
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/thinking-orb/index.js +741 -0
- package/lib/module/components/thinking-orb/index.js.map +1 -0
- package/lib/module/components/toggle-button/index.js +12 -3
- package/lib/module/components/toggle-button/index.js.map +1 -1
- package/lib/module/components/typography/index.js +196 -5
- package/lib/module/components/typography/index.js.map +1 -1
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +99 -59
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/hooks/use-reveal-progress.js +75 -0
- package/lib/module/hooks/use-reveal-progress.js.map +1 -0
- package/lib/module/icons/index.js +69 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +9 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/primitives/keyboard-avoider.js +21 -2
- package/lib/module/primitives/keyboard-avoider.js.map +1 -1
- package/lib/module/primitives/scroll-progress.js +114 -0
- package/lib/module/primitives/scroll-progress.js.map +1 -0
- package/lib/typescript/src/components/button/index.d.ts +3 -4
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/chip/index.d.ts +304 -0
- package/lib/typescript/src/components/chip/index.d.ts.map +1 -0
- package/lib/typescript/src/components/direction/index.d.ts +64 -0
- package/lib/typescript/src/components/direction/index.d.ts.map +1 -0
- package/lib/typescript/src/components/frame/index.d.ts +15 -13
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +260 -0
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/input/index.d.ts +19 -4
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message/index.d.ts.map +1 -1
- package/lib/typescript/src/components/scroll-canvas/index.d.ts +66 -0
- package/lib/typescript/src/components/scroll-canvas/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-text/index.d.ts +46 -0
- package/lib/typescript/src/components/scroll-text/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/soundwave/index.d.ts +91 -0
- package/lib/typescript/src/components/soundwave/index.d.ts.map +1 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/thinking-orb/index.d.ts +28 -0
- package/lib/typescript/src/components/thinking-orb/index.d.ts.map +1 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts +12 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/typography/index.d.ts +154 -1
- package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +2 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +16 -3
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts +56 -0
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +6 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +9 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +15 -7
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +30 -3
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
- package/lib/typescript/src/primitives/scroll-progress.d.ts +61 -0
- package/lib/typescript/src/primitives/scroll-progress.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/button/index.tsx +13 -11
- package/src/components/direction/index.tsx +90 -0
- package/src/components/frame/index.tsx +39 -50
- package/src/components/heatmap-chart/index.tsx +1024 -0
- package/src/components/input/index.tsx +23 -4
- package/src/components/message/index.tsx +17 -1
- package/src/components/scroll-canvas/index.tsx +220 -0
- package/src/components/scroll-text/index.tsx +277 -0
- package/src/components/select/index.tsx +4 -9
- package/src/components/slider/index.tsx +4 -7
- package/src/components/soundwave/index.tsx +1029 -0
- package/src/components/switch/index.tsx +5 -8
- package/src/components/thinking-orb/index.tsx +792 -0
- package/src/components/typography/index.tsx +231 -10
- package/src/hooks/index.ts +6 -0
- package/src/hooks/use-keyboard-avoidance.ts +111 -65
- package/src/hooks/use-reveal-progress.ts +106 -0
- package/src/icons/index.tsx +36 -0
- package/src/index.ts +53 -0
- package/src/native/index.ts +15 -7
- package/src/primitives/keyboard-avoider.tsx +41 -4
- package/src/primitives/scroll-progress.tsx +167 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Soundwave — what a voice looks like while an app is listening to it.
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* <Soundwave variant="pills" state="listening" level={level} />
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Four looks, because a voice screen needs different ones in different places:
|
|
11
|
+
* `pills` for the few big capsules over a microphone button, `bars` for a
|
|
12
|
+
* metering strip in a transcript, `line` for a travelling wave while the
|
|
13
|
+
* assistant talks back, and `ambient` for a glow that takes the whole screen.
|
|
14
|
+
*
|
|
15
|
+
* ## It draws a level; it does not record one
|
|
16
|
+
*
|
|
17
|
+
* Nothing here touches the microphone. The app owns the recorder — the
|
|
18
|
+
* permission prompt, the session, the platform quirks — and hands over a number
|
|
19
|
+
* between 0 and 1, which keeps this component free of an audio dependency and
|
|
20
|
+
* usable against a real meter, a synthesised one, or a remote peer's.
|
|
21
|
+
*
|
|
22
|
+
* With no `level` at all it animates its own plausible motion for the current
|
|
23
|
+
* `state`, so a screen can be built and reviewed before any audio exists.
|
|
24
|
+
*
|
|
25
|
+
* ## Levels arrive faster than React should re-render
|
|
26
|
+
*
|
|
27
|
+
* A recorder reports every 30–60ms. Setting that in state is dozens of renders
|
|
28
|
+
* a second for a number that only moves pixels, so `level` also accepts a
|
|
29
|
+
* `SharedValue`: write metering straight into it and nothing above this
|
|
30
|
+
* component ever re-renders. A plain number works too and is smoothed the same
|
|
31
|
+
* way — it is the right choice when the level comes from something slow, like a
|
|
32
|
+
* server-side speaking flag.
|
|
33
|
+
*
|
|
34
|
+
* Either way the value is smoothed with a fast attack and a slow release, which
|
|
35
|
+
* is what makes a meter read as a meter: it snaps up on a syllable and falls
|
|
36
|
+
* back gently, instead of chattering around every sample.
|
|
37
|
+
*
|
|
38
|
+
* ## How each one is drawn
|
|
39
|
+
*
|
|
40
|
+
* `bars` and `line` are a *single* animated SVG path — one vertical segment per
|
|
41
|
+
* bar with a round cap, or one polyline — so forty bars cost one animated prop
|
|
42
|
+
* a frame rather than forty animated views, and the capsule ends come free from
|
|
43
|
+
* the stroke cap. `pills` is a handful of views, where that machinery would be
|
|
44
|
+
* more expensive than the thing it saves. Everything runs in a worklet on the
|
|
45
|
+
* UI thread; React renders on a resize and otherwise not at all.
|
|
46
|
+
*/
|
|
47
|
+
import { useEffect, useId, useState } from 'react';
|
|
48
|
+
import { StyleSheet, View } from 'react-native';
|
|
49
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
50
|
+
import Animated, { useAnimatedProps, useAnimatedStyle, useFrameCallback, useReducedMotion, useSharedValue } from 'react-native-reanimated';
|
|
51
|
+
import Svg, { Defs, LinearGradient as SvgGradient, Path, Stop } from 'react-native-svg';
|
|
52
|
+
import { useCSSVariable } from 'uniwind';
|
|
53
|
+
import { useIconColor } from "../../icons/index.js";
|
|
54
|
+
import { cn } from "../../utils/cn.js";
|
|
55
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
56
|
+
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
57
|
+
/** What each state is doing, for anyone who cannot see it. */
|
|
58
|
+
const STATE_LABEL = {
|
|
59
|
+
idle: 'Idle',
|
|
60
|
+
listening: 'Listening',
|
|
61
|
+
thinking: 'Thinking',
|
|
62
|
+
speaking: 'Speaking'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* How fast the smoothed level rises and falls, per second.
|
|
67
|
+
*
|
|
68
|
+
* Wildly asymmetric on purpose. A meter that rises and falls at the same rate
|
|
69
|
+
* either lags the syllable that caused it or flickers on every sample; snapping
|
|
70
|
+
* up and easing down tracks speech the way an ear expects it to.
|
|
71
|
+
*/
|
|
72
|
+
const ATTACK = 16;
|
|
73
|
+
const RELEASE = 5;
|
|
74
|
+
|
|
75
|
+
/** Never quite silent: a wave flat at zero reads as broken rather than quiet. */
|
|
76
|
+
const FLOOR = 0.04;
|
|
77
|
+
|
|
78
|
+
/* -------------------------------------------------------------------------- */
|
|
79
|
+
/* Worklet maths */
|
|
80
|
+
/* -------------------------------------------------------------------------- */
|
|
81
|
+
|
|
82
|
+
function clamp01(value) {
|
|
83
|
+
'worklet';
|
|
84
|
+
|
|
85
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Deterministic hash in `[0, 1)`. Stable across frames and across mounts. */
|
|
89
|
+
function hashD(a, b) {
|
|
90
|
+
'worklet';
|
|
91
|
+
|
|
92
|
+
const h = Math.sin(a * 12.9898 + b * 78.233) * 43758.5453;
|
|
93
|
+
return h - Math.floor(h);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One decimal place. Path strings are rebuilt every frame; every digit costs. */
|
|
97
|
+
function q(value) {
|
|
98
|
+
'worklet';
|
|
99
|
+
|
|
100
|
+
return Math.round(value * 10) / 10;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The motion a state runs on its own, with no level supplied.
|
|
105
|
+
*
|
|
106
|
+
* Layered at unrelated tempi so it never repeats on a beat — a single sine
|
|
107
|
+
* reads as a pulse, and a pulse reads as a progress indicator rather than as a
|
|
108
|
+
* voice.
|
|
109
|
+
*/
|
|
110
|
+
function idleEnergy(state, t) {
|
|
111
|
+
'worklet';
|
|
112
|
+
|
|
113
|
+
if (state === 'listening') {
|
|
114
|
+
return clamp01(0.34 + 0.22 * Math.sin(t * 2.3) + 0.16 * Math.sin(t * 3.9 + 1.3) + 0.1 * Math.sin(t * 7.1 + 0.6));
|
|
115
|
+
}
|
|
116
|
+
if (state === 'speaking') {
|
|
117
|
+
return clamp01(0.46 + 0.27 * Math.sin(t * 3.1) + 0.18 * Math.sin(t * 5.7 + 0.8) + 0.09 * Math.sin(t * 9.3));
|
|
118
|
+
}
|
|
119
|
+
if (state === 'thinking') {
|
|
120
|
+
return clamp01(0.18 + 0.1 * Math.sin(t * 1.6) + 0.05 * Math.sin(t * 2.9 + 2.1));
|
|
121
|
+
}
|
|
122
|
+
return clamp01(0.07 + 0.05 * Math.sin(t * 1.1));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* -------------------------------------------------------------------------- */
|
|
126
|
+
/* The clock every variant shares */
|
|
127
|
+
/* -------------------------------------------------------------------------- */
|
|
128
|
+
|
|
129
|
+
const isShared = value => typeof value === 'object' && value !== null && 'value' in value;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* One shared value to read, whether the caller passed a number or their own.
|
|
133
|
+
*
|
|
134
|
+
* A plain number is copied in; a `SharedValue` is handed straight back, which
|
|
135
|
+
* is the whole reason for accepting one — metering written into it never
|
|
136
|
+
* re-renders anything.
|
|
137
|
+
*/
|
|
138
|
+
function useNumberSource(value) {
|
|
139
|
+
const own = useSharedValue(0);
|
|
140
|
+
const external = isShared(value) ? value : null;
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (typeof value === 'number') own.value = value;
|
|
143
|
+
}, [value, own]);
|
|
144
|
+
return external ?? own;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** How often `scrolling` mode takes a sample, in seconds. */
|
|
148
|
+
const SAMPLE_INTERVAL = 1 / 24;
|
|
149
|
+
function useEngine({
|
|
150
|
+
level,
|
|
151
|
+
state,
|
|
152
|
+
sensitivity,
|
|
153
|
+
speed,
|
|
154
|
+
paused,
|
|
155
|
+
history
|
|
156
|
+
}) {
|
|
157
|
+
const reducedMotion = useReducedMotion();
|
|
158
|
+
const clock = useSharedValue(0);
|
|
159
|
+
const energy = useSharedValue(0);
|
|
160
|
+
const samples = useSharedValue([]);
|
|
161
|
+
const nextSample = useSharedValue(0);
|
|
162
|
+
const source = useNumberSource(level);
|
|
163
|
+
const driven = level !== undefined;
|
|
164
|
+
const running = !paused && !reducedMotion;
|
|
165
|
+
const frame = useFrameCallback(info => {
|
|
166
|
+
'worklet';
|
|
167
|
+
|
|
168
|
+
// Elapsed time is accumulated rather than read off the total, so `speed`
|
|
169
|
+
// can change mid-animation without the wave jumping to wherever the new
|
|
170
|
+
// rate would have put it. A dropped frame is clamped rather than honoured —
|
|
171
|
+
// a 300ms hitch played back at full rate is a lurch.
|
|
172
|
+
const delta = Math.min(info.timeSincePreviousFrame ?? 16, 48) / 1000;
|
|
173
|
+
clock.value += delta * speed;
|
|
174
|
+
const target = driven ? clamp01(source.value * sensitivity) : idleEnergy(state, clock.value);
|
|
175
|
+
const rate = target > energy.value ? ATTACK : RELEASE;
|
|
176
|
+
energy.value += (target - energy.value) * Math.min(1, delta * rate);
|
|
177
|
+
if (history > 0 && clock.value >= nextSample.value) {
|
|
178
|
+
nextSample.value = clock.value + SAMPLE_INTERVAL;
|
|
179
|
+
// A little per-sample texture, so a held note is a band of varying bars
|
|
180
|
+
// rather than a solid block — real speech never gives two equal samples.
|
|
181
|
+
const jitter = 0.82 + 0.36 * hashD(clock.value, 3.3);
|
|
182
|
+
const next = samples.value.slice(samples.value.length >= history ? 1 : 0);
|
|
183
|
+
next.push(clamp01(energy.value * jitter));
|
|
184
|
+
samples.value = next;
|
|
185
|
+
}
|
|
186
|
+
}, false);
|
|
187
|
+
const {
|
|
188
|
+
setActive
|
|
189
|
+
} = frame;
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
setActive(running);
|
|
192
|
+
return () => setActive(false);
|
|
193
|
+
}, [running, setActive]);
|
|
194
|
+
|
|
195
|
+
// Stopped is not empty: reduced motion and `paused` both get a representative
|
|
196
|
+
// frame rather than a flat line, which is the difference between "not
|
|
197
|
+
// animating" and "broken".
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (running) return;
|
|
200
|
+
const still = driven ? clamp01(source.value * sensitivity) : 0.42;
|
|
201
|
+
energy.value = still;
|
|
202
|
+
if (history > 0) {
|
|
203
|
+
samples.value = Array.from({
|
|
204
|
+
length: history
|
|
205
|
+
}, (_unused, index) => clamp01(still * (0.45 + 0.55 * Math.abs(Math.sin(index * 0.7)))));
|
|
206
|
+
}
|
|
207
|
+
}, [running, driven, sensitivity, history, source, energy, samples]);
|
|
208
|
+
return {
|
|
209
|
+
clock,
|
|
210
|
+
energy,
|
|
211
|
+
samples
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* -------------------------------------------------------------------------- */
|
|
216
|
+
/* pills */
|
|
217
|
+
/* -------------------------------------------------------------------------- */
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* One capsule.
|
|
221
|
+
*
|
|
222
|
+
* A component rather than a loop of `useAnimatedStyle` in the parent, because
|
|
223
|
+
* the count is a prop — hooks in a loop is a rule waiting to be broken by
|
|
224
|
+
* whoever changes the default.
|
|
225
|
+
*/
|
|
226
|
+
function Pill({
|
|
227
|
+
index,
|
|
228
|
+
count,
|
|
229
|
+
engine,
|
|
230
|
+
width,
|
|
231
|
+
minHeight,
|
|
232
|
+
maxHeight,
|
|
233
|
+
color
|
|
234
|
+
}) {
|
|
235
|
+
const style = useAnimatedStyle(() => {
|
|
236
|
+
/*
|
|
237
|
+
* Each capsule runs at its own tempo and phase, hashed off its index. Give
|
|
238
|
+
* them one tempo and they rise and fall as a block, which reads as a
|
|
239
|
+
* loading bar; detune them and the group reads as something responding to a
|
|
240
|
+
* voice, even though they all follow the same level.
|
|
241
|
+
*/
|
|
242
|
+
const rate = 2.2 + hashD(index, 1.7) * 2.6;
|
|
243
|
+
const phase = hashD(index, 5.1) * Math.PI * 2;
|
|
244
|
+
const wobble = 0.55 + 0.45 * Math.sin(engine.clock.value * rate + phase);
|
|
245
|
+
|
|
246
|
+
// The middle capsules lead. A voice meter with a flat profile looks like a
|
|
247
|
+
// level indicator; a slight hump looks like a mouth.
|
|
248
|
+
const centre = count > 1 ? 1 - Math.abs(index / (count - 1) * 2 - 1) : 1;
|
|
249
|
+
const shape = 0.68 + 0.32 * centre;
|
|
250
|
+
const value = clamp01(Math.max(FLOOR, engine.energy.value) * wobble * shape);
|
|
251
|
+
return {
|
|
252
|
+
height: minHeight + (maxHeight - minHeight) * value
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
256
|
+
style: [{
|
|
257
|
+
width,
|
|
258
|
+
borderRadius: width / 2,
|
|
259
|
+
backgroundColor: color
|
|
260
|
+
}, style]
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function PillsWave({
|
|
264
|
+
engine,
|
|
265
|
+
count,
|
|
266
|
+
barWidth,
|
|
267
|
+
barGap,
|
|
268
|
+
height,
|
|
269
|
+
color
|
|
270
|
+
}) {
|
|
271
|
+
// Never shorter than a circle: a capsule squashed past its own width stops
|
|
272
|
+
// being a capsule.
|
|
273
|
+
const minHeight = barWidth * 1.35;
|
|
274
|
+
return /*#__PURE__*/_jsx(View, {
|
|
275
|
+
style: {
|
|
276
|
+
height,
|
|
277
|
+
columnGap: barGap
|
|
278
|
+
},
|
|
279
|
+
className: "flex-row items-center justify-center",
|
|
280
|
+
children: Array.from({
|
|
281
|
+
length: count
|
|
282
|
+
}, (_unused, index) => /*#__PURE__*/_jsx(Pill, {
|
|
283
|
+
index: index,
|
|
284
|
+
count: count,
|
|
285
|
+
engine: engine,
|
|
286
|
+
width: barWidth,
|
|
287
|
+
minHeight: minHeight,
|
|
288
|
+
maxHeight: height,
|
|
289
|
+
color: color
|
|
290
|
+
}, index))
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* -------------------------------------------------------------------------- */
|
|
295
|
+
/* bars */
|
|
296
|
+
/* -------------------------------------------------------------------------- */
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* One bar's height, 0–1, from whichever source is driving this wave.
|
|
300
|
+
*
|
|
301
|
+
* Pulled out of the drawing loop because two paths walk the same bars — the
|
|
302
|
+
* played part of a recording and the rest of it — and a bar has to come out
|
|
303
|
+
* identical in both or the split shows as a step.
|
|
304
|
+
*/
|
|
305
|
+
function barValue(i, count, scrolling, supplied, history, clock, energy) {
|
|
306
|
+
'worklet';
|
|
307
|
+
|
|
308
|
+
if (scrolling) {
|
|
309
|
+
// The buffer fills from empty, so the newest sample sits at the trailing
|
|
310
|
+
// edge from the first frame rather than the wave sliding in from nowhere.
|
|
311
|
+
const offset = i - (count - history.length);
|
|
312
|
+
return offset >= 0 ? history[offset] ?? 0 : 0;
|
|
313
|
+
}
|
|
314
|
+
if (supplied.length) {
|
|
315
|
+
// Real bands, resampled to the bar count — an analysis rarely hands back
|
|
316
|
+
// exactly as many numbers as there are bars, and a recorded waveform never
|
|
317
|
+
// does.
|
|
318
|
+
return supplied[Math.floor(i / count * supplied.length)] ?? 0;
|
|
319
|
+
}
|
|
320
|
+
const rate = 2.1 + hashD(i, 1.3) * 2.9;
|
|
321
|
+
const phase = hashD(i, 4.7) * Math.PI * 2;
|
|
322
|
+
const wobble = 0.5 + 0.5 * Math.sin(clock * rate + phase);
|
|
323
|
+
const centre = count > 1 ? Math.sin(Math.PI * (i + 0.5) / count) : 1;
|
|
324
|
+
return energy * wobble * (0.45 + 0.55 * centre);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Ink left on the part of a recording that has not played yet. */
|
|
328
|
+
const UNPLAYED_OPACITY = 0.3;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The stroke a wave is painted with, when it is not one flat colour.
|
|
332
|
+
*
|
|
333
|
+
* One definition covers both jobs, because they are the same object: a run of
|
|
334
|
+
* colour stops across the width, optionally pinned to transparent at each end.
|
|
335
|
+
* Doing the edge fade this way rather than as an overlay in the background
|
|
336
|
+
* colour is what lets a wave sit on a card, a bubble or a photograph — an
|
|
337
|
+
* overlay only disappears against the one surface it was told about.
|
|
338
|
+
*/
|
|
339
|
+
function WaveGradient({
|
|
340
|
+
id,
|
|
341
|
+
colors,
|
|
342
|
+
fade,
|
|
343
|
+
edge
|
|
344
|
+
}) {
|
|
345
|
+
const stops = colors.length ? colors : ['#000000'];
|
|
346
|
+
const span = fade ? 1 - edge * 2 : 1;
|
|
347
|
+
const from = fade ? edge : 0;
|
|
348
|
+
const nodes = [];
|
|
349
|
+
if (fade) nodes.push(/*#__PURE__*/_jsx(Stop, {
|
|
350
|
+
offset: "0",
|
|
351
|
+
stopColor: stops[0],
|
|
352
|
+
stopOpacity: "0"
|
|
353
|
+
}, "fade-start"));
|
|
354
|
+
stops.forEach((stop, index) => {
|
|
355
|
+
const at = stops.length > 1 ? from + index / (stops.length - 1) * span : from;
|
|
356
|
+
nodes.push(/*#__PURE__*/_jsx(Stop, {
|
|
357
|
+
offset: String(at),
|
|
358
|
+
stopColor: stop,
|
|
359
|
+
stopOpacity: "1"
|
|
360
|
+
}, index));
|
|
361
|
+
});
|
|
362
|
+
if (fade) {
|
|
363
|
+
nodes.push(/*#__PURE__*/_jsx(Stop, {
|
|
364
|
+
offset: "1",
|
|
365
|
+
stopColor: stops[stops.length - 1],
|
|
366
|
+
stopOpacity: "0"
|
|
367
|
+
}, "fade-end"));
|
|
368
|
+
}
|
|
369
|
+
return /*#__PURE__*/_jsx(Defs, {
|
|
370
|
+
children: /*#__PURE__*/_jsx(SvgGradient, {
|
|
371
|
+
id: id,
|
|
372
|
+
x1: "0",
|
|
373
|
+
y1: "0",
|
|
374
|
+
x2: "1",
|
|
375
|
+
y2: "0",
|
|
376
|
+
children: nodes
|
|
377
|
+
})
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function BarsWave({
|
|
381
|
+
engine,
|
|
382
|
+
bands,
|
|
383
|
+
progress,
|
|
384
|
+
hasProgress,
|
|
385
|
+
count,
|
|
386
|
+
barWidth,
|
|
387
|
+
height,
|
|
388
|
+
width,
|
|
389
|
+
centered,
|
|
390
|
+
scrolling,
|
|
391
|
+
stroke,
|
|
392
|
+
trackStroke,
|
|
393
|
+
defs
|
|
394
|
+
}) {
|
|
395
|
+
/*
|
|
396
|
+
* Two paths, split at the playhead: the bars behind it keep full ink, the
|
|
397
|
+
* ones ahead are dimmed. It is two animated props a frame rather than one,
|
|
398
|
+
* and still nothing per bar — which is the point of drawing bars as a stroked
|
|
399
|
+
* path in the first place.
|
|
400
|
+
*
|
|
401
|
+
* With no `progress` the first path takes everything and the second is empty,
|
|
402
|
+
* so a live meter and a voice note run the same code.
|
|
403
|
+
*/
|
|
404
|
+
const drawSegment = played => () => {
|
|
405
|
+
'worklet';
|
|
406
|
+
|
|
407
|
+
const step = count > 1 ? (width - barWidth) / (count - 1) : 0;
|
|
408
|
+
const span = height - barWidth;
|
|
409
|
+
const supplied = bands.value;
|
|
410
|
+
const history = engine.samples.value;
|
|
411
|
+
const head = hasProgress ? clamp01(progress.value) * count : count;
|
|
412
|
+
let d = '';
|
|
413
|
+
for (let i = 0; i < count; i++) {
|
|
414
|
+
// The playhead cuts between bars, not through one: a bar is either
|
|
415
|
+
// played or it is not, which is what makes the fill land on a beat.
|
|
416
|
+
const isPlayed = i + 0.5 <= head;
|
|
417
|
+
if (isPlayed !== played) continue;
|
|
418
|
+
const value = barValue(i, count, scrolling, supplied, history, engine.clock.value, engine.energy.value);
|
|
419
|
+
const length = span * clamp01(Math.max(FLOOR, value));
|
|
420
|
+
const x = q(barWidth / 2 + i * step);
|
|
421
|
+
if (centered) {
|
|
422
|
+
const half = length / 2;
|
|
423
|
+
d += `M${x} ${q(height / 2 - half)}L${x} ${q(height / 2 + half)}`;
|
|
424
|
+
} else {
|
|
425
|
+
d += `M${x} ${q(height - barWidth / 2)}L${x} ${q(height - barWidth / 2 - length)}`;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
d
|
|
430
|
+
};
|
|
431
|
+
};
|
|
432
|
+
const playedProps = useAnimatedProps(drawSegment(true));
|
|
433
|
+
const restProps = useAnimatedProps(drawSegment(false));
|
|
434
|
+
return /*#__PURE__*/_jsxs(Svg, {
|
|
435
|
+
width: width,
|
|
436
|
+
height: height,
|
|
437
|
+
children: [defs, /*#__PURE__*/_jsx(AnimatedPath, {
|
|
438
|
+
animatedProps: restProps,
|
|
439
|
+
stroke: trackStroke ?? stroke
|
|
440
|
+
// A track colour of its own is drawn as given; without one, the unplayed
|
|
441
|
+
// part is the same ink turned down.
|
|
442
|
+
,
|
|
443
|
+
strokeOpacity: trackStroke ? 1 : hasProgress ? UNPLAYED_OPACITY : 1,
|
|
444
|
+
strokeWidth: barWidth,
|
|
445
|
+
strokeLinecap: "round",
|
|
446
|
+
fill: "none"
|
|
447
|
+
}), /*#__PURE__*/_jsx(AnimatedPath, {
|
|
448
|
+
animatedProps: playedProps,
|
|
449
|
+
stroke: stroke,
|
|
450
|
+
strokeWidth: barWidth,
|
|
451
|
+
strokeLinecap: "round",
|
|
452
|
+
fill: "none"
|
|
453
|
+
})]
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* -------------------------------------------------------------------------- */
|
|
458
|
+
/* line */
|
|
459
|
+
/* -------------------------------------------------------------------------- */
|
|
460
|
+
|
|
461
|
+
/** Points along the wave. Enough to read as a curve, few enough to rebuild. */
|
|
462
|
+
const LINE_POINTS = 56;
|
|
463
|
+
function LineWave({
|
|
464
|
+
engine,
|
|
465
|
+
width,
|
|
466
|
+
height,
|
|
467
|
+
strokeWidth,
|
|
468
|
+
stroke,
|
|
469
|
+
defs
|
|
470
|
+
}) {
|
|
471
|
+
const animatedProps = useAnimatedProps(() => {
|
|
472
|
+
const mid = height / 2;
|
|
473
|
+
const amplitude = (height / 2 - strokeWidth) * clamp01(Math.max(FLOOR, engine.energy.value));
|
|
474
|
+
const t = engine.clock.value;
|
|
475
|
+
let d = '';
|
|
476
|
+
for (let i = 0; i <= LINE_POINTS; i++) {
|
|
477
|
+
const f = i / LINE_POINTS;
|
|
478
|
+
const x = f * width;
|
|
479
|
+
/*
|
|
480
|
+
* Three waves at unrelated wavelengths, tapered to nothing at both ends.
|
|
481
|
+
* The taper is what makes it a wave and not a rope: the line leaves and
|
|
482
|
+
* meets the edges flat, so there is no hard stop where it is cut off.
|
|
483
|
+
*/
|
|
484
|
+
const taper = Math.sin(Math.PI * f);
|
|
485
|
+
const y = mid - amplitude * taper * (0.62 * Math.sin(f * 12.6 - t * 3.1) + 0.26 * Math.sin(f * 21.4 - t * 4.7 + 1.1) + 0.12 * Math.sin(f * 33.2 - t * 2.3));
|
|
486
|
+
d += `${i === 0 ? 'M' : 'L'}${q(x)} ${q(y)}`;
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
d
|
|
490
|
+
};
|
|
491
|
+
});
|
|
492
|
+
return /*#__PURE__*/_jsxs(Svg, {
|
|
493
|
+
width: width,
|
|
494
|
+
height: height,
|
|
495
|
+
children: [defs, /*#__PURE__*/_jsx(AnimatedPath, {
|
|
496
|
+
animatedProps: animatedProps,
|
|
497
|
+
stroke: stroke,
|
|
498
|
+
strokeWidth: strokeWidth,
|
|
499
|
+
strokeLinecap: "round",
|
|
500
|
+
strokeLinejoin: "round",
|
|
501
|
+
fill: "none"
|
|
502
|
+
})]
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/* -------------------------------------------------------------------------- */
|
|
507
|
+
/* ambient */
|
|
508
|
+
/* -------------------------------------------------------------------------- */
|
|
509
|
+
|
|
510
|
+
/** A theme token name — `--color-info` — rather than a literal colour. */
|
|
511
|
+
const isToken = value => typeof value === 'string' && value.startsWith('--');
|
|
512
|
+
|
|
513
|
+
/** `useCSSVariable` can hand back a non-string when a variable is unset. */
|
|
514
|
+
const asString = value => typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
515
|
+
|
|
516
|
+
/** Turns any resolved colour into the same colour at a given alpha. */
|
|
517
|
+
function withAlpha(color, alpha) {
|
|
518
|
+
if (color.startsWith('#')) {
|
|
519
|
+
const hex = color.slice(1);
|
|
520
|
+
const full = hex.length === 3 ? hex.split('').map(c => c + c).join('') : hex.slice(0, 6);
|
|
521
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
522
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
523
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
524
|
+
if (Number.isNaN(r + g + b)) return color;
|
|
525
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
526
|
+
}
|
|
527
|
+
const channels = color.match(/rgba?\(([^)]+)\)/)?.[1];
|
|
528
|
+
if (channels) {
|
|
529
|
+
const [r, g, b] = channels.split(',').map(part => part.trim());
|
|
530
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
531
|
+
}
|
|
532
|
+
return color;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* The glow that fills a screen: a bloom rising off the bottom edge and a rim of
|
|
537
|
+
* light around the rest.
|
|
538
|
+
*
|
|
539
|
+
* Both breathe on the level rather than only fading in and out — a glow that
|
|
540
|
+
* changes opacity alone reads as a screen dimming, where one that also grows
|
|
541
|
+
* reads as something in the room getting louder.
|
|
542
|
+
*/
|
|
543
|
+
function AmbientWave({
|
|
544
|
+
engine,
|
|
545
|
+
color,
|
|
546
|
+
bloomColors,
|
|
547
|
+
radius
|
|
548
|
+
}) {
|
|
549
|
+
const bloom = useAnimatedStyle(() => {
|
|
550
|
+
const e = clamp01(Math.max(FLOOR, engine.energy.value));
|
|
551
|
+
const breath = 0.94 + 0.06 * Math.sin(engine.clock.value * 1.7);
|
|
552
|
+
return {
|
|
553
|
+
opacity: 0.25 + 0.75 * e,
|
|
554
|
+
transform: [{
|
|
555
|
+
scaleY: (0.72 + 0.4 * e) * breath
|
|
556
|
+
}]
|
|
557
|
+
};
|
|
558
|
+
});
|
|
559
|
+
const rim = useAnimatedStyle(() => {
|
|
560
|
+
const e = clamp01(Math.max(FLOOR, engine.energy.value));
|
|
561
|
+
return {
|
|
562
|
+
opacity: 0.12 + 0.5 * e
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
/*
|
|
567
|
+
* The bloom fades in from nothing at the top whatever it is made of, so a
|
|
568
|
+
* supplied palette is ramped rather than used flat — a gradient that starts
|
|
569
|
+
* at full strength has a visible edge across the middle of the screen.
|
|
570
|
+
*/
|
|
571
|
+
const ramp = bloomColors?.length ? [withAlpha(bloomColors[0], 0), ...bloomColors.map((stop, index) => withAlpha(stop, 0.16 + 0.34 * index / Math.max(1, bloomColors.length - 1)))] : [withAlpha(color, 0), withAlpha(color, 0.16), withAlpha(color, 0.5)];
|
|
572
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
573
|
+
pointerEvents: "none",
|
|
574
|
+
style: [StyleSheet.absoluteFill, {
|
|
575
|
+
borderRadius: radius,
|
|
576
|
+
overflow: 'hidden'
|
|
577
|
+
}],
|
|
578
|
+
children: [/*#__PURE__*/_jsx(Animated.View, {
|
|
579
|
+
style: [StyleSheet.absoluteFill, rim],
|
|
580
|
+
children: [0, 1, 2].map(ring => /*#__PURE__*/_jsx(View, {
|
|
581
|
+
style: [StyleSheet.absoluteFill, {
|
|
582
|
+
margin: ring * 3,
|
|
583
|
+
borderRadius: Math.max(0, radius - ring * 3),
|
|
584
|
+
borderWidth: 3,
|
|
585
|
+
borderColor: withAlpha(color, 0.4 - ring * 0.13)
|
|
586
|
+
}]
|
|
587
|
+
}, ring))
|
|
588
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
589
|
+
style: [{
|
|
590
|
+
position: 'absolute',
|
|
591
|
+
left: 0,
|
|
592
|
+
right: 0,
|
|
593
|
+
bottom: 0,
|
|
594
|
+
height: '55%'
|
|
595
|
+
},
|
|
596
|
+
// Anchored to the bottom so growing pushes the bloom up the screen
|
|
597
|
+
// rather than pulling it off the edge.
|
|
598
|
+
{
|
|
599
|
+
transformOrigin: 'bottom'
|
|
600
|
+
}, bloom],
|
|
601
|
+
children: /*#__PURE__*/_jsx(LinearGradient, {
|
|
602
|
+
colors: ramp,
|
|
603
|
+
start: {
|
|
604
|
+
x: 0.5,
|
|
605
|
+
y: 0
|
|
606
|
+
},
|
|
607
|
+
end: {
|
|
608
|
+
x: 0.5,
|
|
609
|
+
y: 1
|
|
610
|
+
},
|
|
611
|
+
style: StyleSheet.absoluteFill
|
|
612
|
+
})
|
|
613
|
+
})]
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* -------------------------------------------------------------------------- */
|
|
618
|
+
/* Component */
|
|
619
|
+
/* -------------------------------------------------------------------------- */
|
|
620
|
+
|
|
621
|
+
/** Per-variant geometry, so a bare `<Soundwave variant="bars" />` looks right. */
|
|
622
|
+
const DEFAULTS = {
|
|
623
|
+
pills: {
|
|
624
|
+
bars: 4,
|
|
625
|
+
barWidth: 28,
|
|
626
|
+
barGap: 10,
|
|
627
|
+
height: 96
|
|
628
|
+
},
|
|
629
|
+
bars: {
|
|
630
|
+
bars: 40,
|
|
631
|
+
barWidth: 3,
|
|
632
|
+
barGap: 3,
|
|
633
|
+
height: 56
|
|
634
|
+
},
|
|
635
|
+
line: {
|
|
636
|
+
bars: 0,
|
|
637
|
+
barWidth: 3,
|
|
638
|
+
barGap: 0,
|
|
639
|
+
height: 72
|
|
640
|
+
},
|
|
641
|
+
ambient: {
|
|
642
|
+
bars: 0,
|
|
643
|
+
barWidth: 0,
|
|
644
|
+
barGap: 0,
|
|
645
|
+
height: 0
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
export function Soundwave({
|
|
649
|
+
className,
|
|
650
|
+
variant = 'pills',
|
|
651
|
+
state = 'listening',
|
|
652
|
+
level,
|
|
653
|
+
levels,
|
|
654
|
+
progress,
|
|
655
|
+
bars,
|
|
656
|
+
barWidth,
|
|
657
|
+
barGap,
|
|
658
|
+
height,
|
|
659
|
+
mode = 'static',
|
|
660
|
+
centered = true,
|
|
661
|
+
fadeEdges,
|
|
662
|
+
sensitivity = 1,
|
|
663
|
+
speed = 1,
|
|
664
|
+
color,
|
|
665
|
+
gradient,
|
|
666
|
+
trackColor,
|
|
667
|
+
paused = false,
|
|
668
|
+
radius = 44,
|
|
669
|
+
accessibilityLabel,
|
|
670
|
+
style,
|
|
671
|
+
...props
|
|
672
|
+
}) {
|
|
673
|
+
const defaults = DEFAULTS[variant];
|
|
674
|
+
const count = bars ?? defaults.bars;
|
|
675
|
+
const stroke = barWidth ?? defaults.barWidth;
|
|
676
|
+
const gap = barGap ?? defaults.barGap;
|
|
677
|
+
const box = height ?? defaults.height;
|
|
678
|
+
const scrolling = variant === 'bars' && mode === 'scrolling';
|
|
679
|
+
const fade = fadeEdges ?? (variant === 'line' || scrolling);
|
|
680
|
+
const foreground = useCSSVariable('--color-foreground');
|
|
681
|
+
const info = useCSSVariable('--color-info');
|
|
682
|
+
/*
|
|
683
|
+
* A token name is resolved here rather than by the caller, so `color` and
|
|
684
|
+
* `trackColor` can be written the way the rest of the library is themed —
|
|
685
|
+
* `--color-info` instead of a hex someone has to keep in step with the theme.
|
|
686
|
+
*/
|
|
687
|
+
const colorToken = useCSSVariable(isToken(color) ? color : '--color-foreground');
|
|
688
|
+
const trackToken = useCSSVariable(isToken(trackColor) ? trackColor : '--color-foreground');
|
|
689
|
+
const themed = variant === 'ambient' ? info : foreground;
|
|
690
|
+
/*
|
|
691
|
+
* A wave inside a coloured surface has to be drawn in that surface's
|
|
692
|
+
* foreground, not the page's: a sent chat bubble is painted in the primary
|
|
693
|
+
* colour, and in most themes the primary colour *is* the text colour — so a
|
|
694
|
+
* wave that resolved `--color-foreground` for itself would be invisible on
|
|
695
|
+
* the one screen it is most likely to appear on. Surfaces already publish
|
|
696
|
+
* their readable foreground for icons; a wave is ink for the same reason.
|
|
697
|
+
*
|
|
698
|
+
* `ambient` opts out: it is a glow behind a whole screen, not ink on a
|
|
699
|
+
* surface, and it wants its own colour.
|
|
700
|
+
*/
|
|
701
|
+
const inherited = useIconColor();
|
|
702
|
+
const surface = variant === 'ambient' ? undefined : inherited;
|
|
703
|
+
const explicit = isToken(color) ? asString(colorToken) : color;
|
|
704
|
+
const ink = explicit ?? surface ?? asString(themed) ?? '#0a0a0a';
|
|
705
|
+
const track = isToken(trackColor) ? asString(trackToken) : trackColor;
|
|
706
|
+
|
|
707
|
+
/*
|
|
708
|
+
* A recording drawn from `levels` has no motion in it: every bar comes from
|
|
709
|
+
* the stored shape, so the clock advances a frame nobody reads. A transcript
|
|
710
|
+
* can hold twenty voice notes, and twenty idle frame callbacks is twenty too
|
|
711
|
+
* many — so a supplied waveform stops the engine, and `progress` still
|
|
712
|
+
* redraws it, because that is a shared value of its own.
|
|
713
|
+
*/
|
|
714
|
+
const stillWaveform = variant === 'bars' && !scrolling && (levels?.length ?? 0) > 0;
|
|
715
|
+
const engine = useEngine({
|
|
716
|
+
level,
|
|
717
|
+
state,
|
|
718
|
+
sensitivity,
|
|
719
|
+
speed,
|
|
720
|
+
paused: paused || stillWaveform,
|
|
721
|
+
history: scrolling ? count : 0
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// Supplied bands are copied into a shared value so the drawing worklet has a
|
|
725
|
+
// single place to read from, whichever way the level arrived.
|
|
726
|
+
const bands = useSharedValue([]);
|
|
727
|
+
useEffect(() => {
|
|
728
|
+
bands.value = levels ?? [];
|
|
729
|
+
}, [levels, bands]);
|
|
730
|
+
const playhead = useNumberSource(progress);
|
|
731
|
+
|
|
732
|
+
// `bars` and `line` are drawn to the width they are given, which is only
|
|
733
|
+
// known after layout — a metering strip is nearly always as wide as its row.
|
|
734
|
+
const [measured, setMeasured] = useState(0);
|
|
735
|
+
const onLayout = event => {
|
|
736
|
+
const next = Math.round(event.nativeEvent.layout.width);
|
|
737
|
+
if (next !== measured) setMeasured(next);
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// Two Defs in one tree cannot share an id, and a screen can hold more than
|
|
741
|
+
// one wave.
|
|
742
|
+
const gradientId = `panelui-soundwave-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
743
|
+
|
|
744
|
+
/*
|
|
745
|
+
* A gradient and an edge fade are the same object — colour stops across the
|
|
746
|
+
* width — so one definition serves both, and a wave that needs neither is
|
|
747
|
+
* painted with a flat colour and no `Defs` at all.
|
|
748
|
+
*/
|
|
749
|
+
const ramp = gradient?.length ? gradient : null;
|
|
750
|
+
const needsDefs = fade || ramp !== null && ramp.length > 1;
|
|
751
|
+
const paint = needsDefs ? `url(#${gradientId})` : ink;
|
|
752
|
+
const defs = needsDefs ? /*#__PURE__*/_jsx(WaveGradient, {
|
|
753
|
+
id: gradientId,
|
|
754
|
+
colors: ramp ?? [ink],
|
|
755
|
+
fade: fade,
|
|
756
|
+
edge: variant === 'line' ? 0.18 : 0.12
|
|
757
|
+
}) : null;
|
|
758
|
+
let content = null;
|
|
759
|
+
if (variant === 'pills') {
|
|
760
|
+
content = /*#__PURE__*/_jsx(PillsWave, {
|
|
761
|
+
engine: engine,
|
|
762
|
+
count: count,
|
|
763
|
+
barWidth: stroke,
|
|
764
|
+
barGap: gap,
|
|
765
|
+
height: box,
|
|
766
|
+
color: ink
|
|
767
|
+
});
|
|
768
|
+
} else if (variant === 'ambient') {
|
|
769
|
+
content = /*#__PURE__*/_jsx(AmbientWave, {
|
|
770
|
+
engine: engine,
|
|
771
|
+
color: ink,
|
|
772
|
+
bloomColors: ramp,
|
|
773
|
+
radius: radius
|
|
774
|
+
});
|
|
775
|
+
} else if (measured > 0) {
|
|
776
|
+
content = variant === 'bars' ? /*#__PURE__*/_jsx(BarsWave, {
|
|
777
|
+
engine: engine,
|
|
778
|
+
bands: bands,
|
|
779
|
+
progress: playhead,
|
|
780
|
+
hasProgress: progress !== undefined,
|
|
781
|
+
count: count,
|
|
782
|
+
barWidth: stroke,
|
|
783
|
+
height: box,
|
|
784
|
+
width: measured,
|
|
785
|
+
centered: centered,
|
|
786
|
+
scrolling: scrolling,
|
|
787
|
+
stroke: paint,
|
|
788
|
+
trackStroke: track ?? null,
|
|
789
|
+
defs: defs
|
|
790
|
+
}) : /*#__PURE__*/_jsx(LineWave, {
|
|
791
|
+
engine: engine,
|
|
792
|
+
width: measured,
|
|
793
|
+
height: box,
|
|
794
|
+
strokeWidth: stroke,
|
|
795
|
+
stroke: paint,
|
|
796
|
+
defs: defs
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
return /*#__PURE__*/_jsx(View, {
|
|
800
|
+
...props,
|
|
801
|
+
onLayout: variant === 'bars' || variant === 'line' ? onLayout : undefined,
|
|
802
|
+
pointerEvents: variant === 'ambient' ? 'none' : props.pointerEvents,
|
|
803
|
+
accessibilityRole: "image",
|
|
804
|
+
accessibilityLabel: accessibilityLabel ?? STATE_LABEL[state],
|
|
805
|
+
className: cn(variant === 'ambient' ? 'absolute inset-0' : 'w-full justify-center', className),
|
|
806
|
+
style: [variant === 'ambient' ? null : {
|
|
807
|
+
height: box
|
|
808
|
+
}, style],
|
|
809
|
+
children: content
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
Soundwave.displayName = 'Soundwave';
|
|
813
|
+
//# sourceMappingURL=index.js.map
|