react-native-ai-animations 0.0.0-stage → 0.8.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 +21 -0
- package/README.md +174 -2
- package/lib/module/brand/index.js +4 -0
- package/lib/module/brand/index.js.map +1 -0
- package/lib/module/brand/spinning-mark.js +96 -0
- package/lib/module/brand/spinning-mark.js.map +1 -0
- package/lib/module/chat/effort-bar.js +853 -0
- package/lib/module/chat/effort-bar.js.map +1 -0
- package/lib/module/chat/index.js +8 -0
- package/lib/module/chat/index.js.map +1 -0
- package/lib/module/chat/question-card.js +720 -0
- package/lib/module/chat/question-card.js.map +1 -0
- package/lib/module/chat/reasoning.js +38 -0
- package/lib/module/chat/reasoning.js.map +1 -0
- package/lib/module/chat/status-row.js +140 -0
- package/lib/module/chat/status-row.js.map +1 -0
- package/lib/module/chat/thinking-block.js +255 -0
- package/lib/module/chat/thinking-block.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/internal/color.js +15 -0
- package/lib/module/internal/color.js.map +1 -0
- package/lib/module/internal/use-elapsed.js +20 -0
- package/lib/module/internal/use-elapsed.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/skia/dot-orb.js +256 -0
- package/lib/module/skia/dot-orb.js.map +1 -0
- package/lib/module/skia/index.js +8 -0
- package/lib/module/skia/index.js.map +1 -0
- package/lib/module/skia/liquid-orb.js +303 -0
- package/lib/module/skia/liquid-orb.js.map +1 -0
- package/lib/module/skia/matrix-orb.js +272 -0
- package/lib/module/skia/matrix-orb.js.map +1 -0
- package/lib/module/skia/media-generating.js +524 -0
- package/lib/module/skia/media-generating.js.map +1 -0
- package/lib/module/skia/voice-wave.js +278 -0
- package/lib/module/skia/voice-wave.js.map +1 -0
- package/lib/module/text/index.js +6 -0
- package/lib/module/text/index.js.map +1 -0
- package/lib/module/text/shimmer-text.js +122 -0
- package/lib/module/text/shimmer-text.js.map +1 -0
- package/lib/module/text/spoken-text.js +85 -0
- package/lib/module/text/spoken-text.js.map +1 -0
- package/lib/module/text/streaming-text.js +135 -0
- package/lib/module/text/streaming-text.js.map +1 -0
- package/lib/module/theme/index.js +5 -0
- package/lib/module/theme/index.js.map +1 -0
- package/lib/module/theme/palette.js +45 -0
- package/lib/module/theme/palette.js.map +1 -0
- package/lib/module/theme/provider.js +65 -0
- package/lib/module/theme/provider.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/brand/index.d.ts +2 -0
- package/lib/typescript/src/brand/index.d.ts.map +1 -0
- package/lib/typescript/src/brand/spinning-mark.d.ts +21 -0
- package/lib/typescript/src/brand/spinning-mark.d.ts.map +1 -0
- package/lib/typescript/src/chat/effort-bar.d.ts +42 -0
- package/lib/typescript/src/chat/effort-bar.d.ts.map +1 -0
- package/lib/typescript/src/chat/index.d.ts +6 -0
- package/lib/typescript/src/chat/index.d.ts.map +1 -0
- package/lib/typescript/src/chat/question-card.d.ts +73 -0
- package/lib/typescript/src/chat/question-card.d.ts.map +1 -0
- package/lib/typescript/src/chat/reasoning.d.ts +20 -0
- package/lib/typescript/src/chat/reasoning.d.ts.map +1 -0
- package/lib/typescript/src/chat/status-row.d.ts +20 -0
- package/lib/typescript/src/chat/status-row.d.ts.map +1 -0
- package/lib/typescript/src/chat/thinking-block.d.ts +35 -0
- package/lib/typescript/src/chat/thinking-block.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/internal/color.d.ts +2 -0
- package/lib/typescript/src/internal/color.d.ts.map +1 -0
- package/lib/typescript/src/internal/use-elapsed.d.ts +3 -0
- package/lib/typescript/src/internal/use-elapsed.d.ts.map +1 -0
- package/lib/typescript/src/skia/dot-orb.d.ts +16 -0
- package/lib/typescript/src/skia/dot-orb.d.ts.map +1 -0
- package/lib/typescript/src/skia/index.d.ts +6 -0
- package/lib/typescript/src/skia/index.d.ts.map +1 -0
- package/lib/typescript/src/skia/liquid-orb.d.ts +20 -0
- package/lib/typescript/src/skia/liquid-orb.d.ts.map +1 -0
- package/lib/typescript/src/skia/matrix-orb.d.ts +12 -0
- package/lib/typescript/src/skia/matrix-orb.d.ts.map +1 -0
- package/lib/typescript/src/skia/media-generating.d.ts +59 -0
- package/lib/typescript/src/skia/media-generating.d.ts.map +1 -0
- package/lib/typescript/src/skia/voice-wave.d.ts +17 -0
- package/lib/typescript/src/skia/voice-wave.d.ts.map +1 -0
- package/lib/typescript/src/text/index.d.ts +4 -0
- package/lib/typescript/src/text/index.d.ts.map +1 -0
- package/lib/typescript/src/text/shimmer-text.d.ts +13 -0
- package/lib/typescript/src/text/shimmer-text.d.ts.map +1 -0
- package/lib/typescript/src/text/spoken-text.d.ts +15 -0
- package/lib/typescript/src/text/spoken-text.d.ts.map +1 -0
- package/lib/typescript/src/text/streaming-text.d.ts +16 -0
- package/lib/typescript/src/text/streaming-text.d.ts.map +1 -0
- package/lib/typescript/src/theme/index.d.ts +3 -0
- package/lib/typescript/src/theme/index.d.ts.map +1 -0
- package/lib/typescript/src/theme/palette.d.ts +33 -0
- package/lib/typescript/src/theme/palette.d.ts.map +1 -0
- package/lib/typescript/src/theme/provider.d.ts +13 -0
- package/lib/typescript/src/theme/provider.d.ts.map +1 -0
- package/package.json +133 -4
- package/src/brand/index.ts +5 -0
- package/src/brand/spinning-mark.tsx +118 -0
- package/src/chat/effort-bar.tsx +748 -0
- package/src/chat/index.ts +45 -0
- package/src/chat/question-card.tsx +649 -0
- package/src/chat/reasoning.ts +30 -0
- package/src/chat/status-row.tsx +127 -0
- package/src/chat/thinking-block.tsx +250 -0
- package/src/index.ts +2 -0
- package/src/internal/color.ts +18 -0
- package/src/internal/use-elapsed.ts +21 -0
- package/src/skia/dot-orb.tsx +248 -0
- package/src/skia/index.ts +34 -0
- package/src/skia/liquid-orb.tsx +274 -0
- package/src/skia/matrix-orb.tsx +337 -0
- package/src/skia/media-generating.tsx +389 -0
- package/src/skia/voice-wave.tsx +215 -0
- package/src/text/index.ts +3 -0
- package/src/text/shimmer-text.tsx +140 -0
- package/src/text/spoken-text.tsx +82 -0
- package/src/text/streaming-text.tsx +144 -0
- package/src/theme/index.ts +11 -0
- package/src/theme/palette.ts +62 -0
- package/src/theme/provider.tsx +54 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Blur,
|
|
3
|
+
Canvas,
|
|
4
|
+
Circle,
|
|
5
|
+
FractalNoise,
|
|
6
|
+
Group,
|
|
7
|
+
LinearGradient,
|
|
8
|
+
Paint,
|
|
9
|
+
Rect,
|
|
10
|
+
useClock,
|
|
11
|
+
vec,
|
|
12
|
+
} from "@shopify/react-native-skia";
|
|
13
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
14
|
+
import {
|
|
15
|
+
type DimensionValue,
|
|
16
|
+
type LayoutChangeEvent,
|
|
17
|
+
Text,
|
|
18
|
+
View,
|
|
19
|
+
type StyleProp,
|
|
20
|
+
type TextStyle,
|
|
21
|
+
type ViewStyle,
|
|
22
|
+
} from "react-native";
|
|
23
|
+
import Animated, {
|
|
24
|
+
cancelAnimation,
|
|
25
|
+
Easing,
|
|
26
|
+
FadeIn,
|
|
27
|
+
LinearTransition,
|
|
28
|
+
type SharedValue,
|
|
29
|
+
useAnimatedStyle,
|
|
30
|
+
useDerivedValue,
|
|
31
|
+
useReducedMotion,
|
|
32
|
+
useSharedValue,
|
|
33
|
+
withRepeat,
|
|
34
|
+
withSequence,
|
|
35
|
+
withTiming,
|
|
36
|
+
} from "react-native-reanimated";
|
|
37
|
+
import { withAlpha } from "../internal/color";
|
|
38
|
+
import { formatElapsed, useElapsed } from "../internal/use-elapsed";
|
|
39
|
+
import { ShimmerText } from "../text/shimmer-text";
|
|
40
|
+
import { moodstoneSwatches, type MotionPalette } from "../theme/palette";
|
|
41
|
+
import { useMotionPalette } from "../theme/provider";
|
|
42
|
+
import { DotOrb, type DotOrbVariant } from "./dot-orb";
|
|
43
|
+
import { MatrixOrb } from "./matrix-orb";
|
|
44
|
+
|
|
45
|
+
const EASE_IN_OUT = Easing.inOut(Easing.quad);
|
|
46
|
+
const HEADER_TEXT: TextStyle = { fontSize: 14, lineHeight: 20, fontWeight: "500" };
|
|
47
|
+
const CHIP_TEXT: TextStyle = { fontSize: 13, lineHeight: 18, fontWeight: "600" };
|
|
48
|
+
const DEFAULT_BLOBS = [moodstoneSwatches.lilac, moodstoneSwatches.sky, moodstoneSwatches.blossom];
|
|
49
|
+
const SWEEP_MS = 1400;
|
|
50
|
+
const BAND = 0.6;
|
|
51
|
+
const TRACK = 2;
|
|
52
|
+
const SEGMENT = 0.28;
|
|
53
|
+
const ORBITS = [
|
|
54
|
+
{ x: 0.3, y: 0.35, rx: 0.22, ry: 0.18, speed: 0.00042, phase: 0 },
|
|
55
|
+
{ x: 0.68, y: 0.6, rx: 0.2, ry: 0.24, speed: -0.00035, phase: 2.1 },
|
|
56
|
+
{ x: 0.45, y: 0.75, rx: 0.26, ry: 0.14, speed: 0.00028, phase: 4.2 },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export type MediaGeneratingVariant = "blobs" | "matrix" | DotOrbVariant;
|
|
60
|
+
|
|
61
|
+
export type MediaGeneratingStatus = "queued" | "generating";
|
|
62
|
+
|
|
63
|
+
export type MediaGeneratingLabels = Record<MediaGeneratingStatus, string>;
|
|
64
|
+
|
|
65
|
+
export type MediaGeneratingChrome = "header" | "overlay" | "none";
|
|
66
|
+
|
|
67
|
+
export type MediaGeneratingSize = "1024x1024" | "1024x1536" | "1536x1024";
|
|
68
|
+
|
|
69
|
+
export const MEDIA_GENERATING_SIZES: Record<MediaGeneratingSize, { width: number; height: number }> = {
|
|
70
|
+
"1024x1024": { width: 1024, height: 1024 },
|
|
71
|
+
"1024x1536": { width: 1024, height: 1536 },
|
|
72
|
+
"1536x1024": { width: 1536, height: 1024 },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type MediaGeneratingProps = {
|
|
76
|
+
variant?: MediaGeneratingVariant;
|
|
77
|
+
chrome?: MediaGeneratingChrome;
|
|
78
|
+
label?: string;
|
|
79
|
+
status?: MediaGeneratingStatus;
|
|
80
|
+
labels?: Partial<MediaGeneratingLabels>;
|
|
81
|
+
progress?: number;
|
|
82
|
+
meta?: string;
|
|
83
|
+
startedAt?: number;
|
|
84
|
+
showTimer?: boolean;
|
|
85
|
+
formatTimer?: (seconds: number) => string;
|
|
86
|
+
size?: MediaGeneratingSize;
|
|
87
|
+
aspectRatio?: number;
|
|
88
|
+
width?: DimensionValue;
|
|
89
|
+
maxHeight?: number;
|
|
90
|
+
radius?: number;
|
|
91
|
+
dotColor?: string;
|
|
92
|
+
accentColor?: string;
|
|
93
|
+
blobColors?: string[];
|
|
94
|
+
grain?: boolean;
|
|
95
|
+
sweep?: boolean;
|
|
96
|
+
dots?: number;
|
|
97
|
+
scatter?: boolean | "loop";
|
|
98
|
+
cover?: ReactNode;
|
|
99
|
+
icon?: ReactNode;
|
|
100
|
+
textStyle?: StyleProp<TextStyle>;
|
|
101
|
+
colors?: Partial<MotionPalette>;
|
|
102
|
+
style?: StyleProp<ViewStyle>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const DEFAULT_LABELS: MediaGeneratingLabels = { queued: "Queued", generating: "Generating" };
|
|
106
|
+
|
|
107
|
+
export function MediaGenerating({
|
|
108
|
+
variant = "blobs",
|
|
109
|
+
chrome = "header",
|
|
110
|
+
label,
|
|
111
|
+
status,
|
|
112
|
+
labels,
|
|
113
|
+
progress,
|
|
114
|
+
meta,
|
|
115
|
+
startedAt,
|
|
116
|
+
showTimer = true,
|
|
117
|
+
formatTimer = formatElapsed,
|
|
118
|
+
size = "1024x1024",
|
|
119
|
+
aspectRatio,
|
|
120
|
+
width = "100%",
|
|
121
|
+
maxHeight = 360,
|
|
122
|
+
radius = 16,
|
|
123
|
+
dotColor,
|
|
124
|
+
accentColor,
|
|
125
|
+
blobColors = DEFAULT_BLOBS,
|
|
126
|
+
grain = true,
|
|
127
|
+
sweep,
|
|
128
|
+
dots = 14,
|
|
129
|
+
scatter = false,
|
|
130
|
+
cover,
|
|
131
|
+
icon,
|
|
132
|
+
textStyle,
|
|
133
|
+
colors,
|
|
134
|
+
style,
|
|
135
|
+
}: MediaGeneratingProps) {
|
|
136
|
+
const palette = useMotionPalette(colors);
|
|
137
|
+
const reduceMotion = useReducedMotion();
|
|
138
|
+
const seconds = useElapsed(startedAt, true);
|
|
139
|
+
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
140
|
+
const title = status ? copy[status] : (label ?? copy.generating);
|
|
141
|
+
const headline = chrome === "header" && label && status ? label : title;
|
|
142
|
+
const percent = progress === undefined ? undefined : Math.round(Math.min(1, Math.max(0, progress)) * 100);
|
|
143
|
+
const chip = withAlpha(palette.background, 0.72);
|
|
144
|
+
const showSweep = sweep ?? variant === "blobs";
|
|
145
|
+
const timer = showTimer && startedAt !== undefined ? formatTimer(seconds) : null;
|
|
146
|
+
const dimensions = MEDIA_GENERATING_SIZES[size] ?? MEDIA_GENERATING_SIZES["1024x1024"];
|
|
147
|
+
const ratio = aspectRatio ?? dimensions.width / dimensions.height;
|
|
148
|
+
|
|
149
|
+
const field = cover ?? (
|
|
150
|
+
variant === "blobs" ? (
|
|
151
|
+
<BlobField colors={blobColors} grain={grain} sweep={showSweep && !reduceMotion} sweepColor={palette.text} still={reduceMotion} />
|
|
152
|
+
) : variant === "matrix" ? (
|
|
153
|
+
<MatrixOrb cover dots={dots} color={dotColor ?? palette.accent} state={status === "queued" ? "idle" : "thinking"} />
|
|
154
|
+
) : (
|
|
155
|
+
<DotOrb variant={variant} color={dotColor ?? palette.text} accent={accentColor ?? palette.accent} inset={0.09} scatter={scatter} />
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<View
|
|
161
|
+
accessible
|
|
162
|
+
accessibilityRole="progressbar"
|
|
163
|
+
accessibilityLabel={percent === undefined ? headline : `${headline} ${percent}%`}
|
|
164
|
+
accessibilityValue={percent === undefined ? undefined : { min: 0, max: 100, now: percent }}
|
|
165
|
+
style={[{ width, gap: 10 }, style]}
|
|
166
|
+
>
|
|
167
|
+
{chrome === "header" ? (
|
|
168
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
169
|
+
{icon}
|
|
170
|
+
{reduceMotion ? (
|
|
171
|
+
<Text style={[HEADER_TEXT, textStyle, { color: palette.textSecondary }]}>{headline}</Text>
|
|
172
|
+
) : (
|
|
173
|
+
<ShimmerText text={headline} activeColor={palette.text} mutedColor={palette.textTertiary} textStyle={[HEADER_TEXT, textStyle]} />
|
|
174
|
+
)}
|
|
175
|
+
{timer ? <Text style={[HEADER_TEXT, textStyle, { color: palette.textTertiary, fontVariant: ["tabular-nums"] }]}>{timer}</Text> : null}
|
|
176
|
+
</View>
|
|
177
|
+
) : null}
|
|
178
|
+
<View
|
|
179
|
+
style={{
|
|
180
|
+
width: "100%",
|
|
181
|
+
aspectRatio: ratio,
|
|
182
|
+
maxWidth: maxHeight * ratio,
|
|
183
|
+
maxHeight,
|
|
184
|
+
borderRadius: radius,
|
|
185
|
+
borderCurve: "continuous",
|
|
186
|
+
overflow: "hidden",
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<View style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }}>{field}</View>
|
|
190
|
+
{chrome === "overlay" ? (
|
|
191
|
+
<View style={{ position: "absolute", top: 12, left: 12, right: 12, flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
192
|
+
<Animated.View
|
|
193
|
+
layout={reduceMotion ? undefined : LinearTransition.duration(260).easing(EASE_IN_OUT)}
|
|
194
|
+
style={{ flexDirection: "row", alignItems: "center", gap: 6, paddingHorizontal: 10, height: 26, borderRadius: 13, backgroundColor: chip }}
|
|
195
|
+
>
|
|
196
|
+
{icon}
|
|
197
|
+
<PulseLabel key={title} text={title} animate={!reduceMotion} textStyle={[CHIP_TEXT, textStyle, { color: palette.text }]} />
|
|
198
|
+
{timer ? <Text style={[CHIP_TEXT, textStyle, { color: palette.textSecondary, fontVariant: ["tabular-nums"] }]}>{timer}</Text> : null}
|
|
199
|
+
</Animated.View>
|
|
200
|
+
<View style={{ flex: 1 }} />
|
|
201
|
+
{percent !== undefined ? (
|
|
202
|
+
<Animated.View
|
|
203
|
+
entering={reduceMotion ? undefined : FadeIn.duration(260).easing(EASE_IN_OUT)}
|
|
204
|
+
style={{ paddingHorizontal: 10, height: 26, borderRadius: 13, justifyContent: "center", backgroundColor: chip }}
|
|
205
|
+
>
|
|
206
|
+
<Text style={[CHIP_TEXT, textStyle, { color: palette.text, fontVariant: ["tabular-nums"] }]}>{`${percent}%`}</Text>
|
|
207
|
+
</Animated.View>
|
|
208
|
+
) : null}
|
|
209
|
+
</View>
|
|
210
|
+
) : null}
|
|
211
|
+
{meta ? (
|
|
212
|
+
<View style={{ position: "absolute", left: 12, bottom: 14, paddingHorizontal: 10, height: 24, borderRadius: 12, justifyContent: "center", backgroundColor: chip }}>
|
|
213
|
+
<Text style={[CHIP_TEXT, textStyle, { color: palette.textSecondary }]}>{meta}</Text>
|
|
214
|
+
</View>
|
|
215
|
+
) : null}
|
|
216
|
+
<ProgressTrack
|
|
217
|
+
progress={progress}
|
|
218
|
+
active={status === "generating" || (chrome === "overlay" && status === undefined)}
|
|
219
|
+
animate={!reduceMotion}
|
|
220
|
+
color={palette.accent}
|
|
221
|
+
track={withAlpha(palette.text, 0.12)}
|
|
222
|
+
/>
|
|
223
|
+
</View>
|
|
224
|
+
</View>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export type ImageGeneratingProps = Omit<MediaGeneratingProps, "chrome"> & { header?: boolean };
|
|
229
|
+
|
|
230
|
+
export function ImageGenerating({ header = true, label = "Creating image", width = "88%", ...props }: ImageGeneratingProps) {
|
|
231
|
+
return <MediaGenerating chrome={header ? "header" : "none"} label={label} width={width} {...props} />;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export type VideoGeneratingProps = Omit<MediaGeneratingProps, "status"> & { status: MediaGeneratingStatus };
|
|
235
|
+
|
|
236
|
+
export function VideoGenerating({ variant = "matrix", chrome = "overlay", size, aspectRatio = size ? undefined : 16 / 9, ...props }: VideoGeneratingProps) {
|
|
237
|
+
return <MediaGenerating variant={variant} chrome={chrome} size={size} aspectRatio={aspectRatio} {...props} />;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
type BlobFieldProps = { colors: string[]; grain: boolean; sweep: boolean; sweepColor: string; still: boolean };
|
|
241
|
+
|
|
242
|
+
function BlobField({ colors, grain, sweep, sweepColor, still }: BlobFieldProps) {
|
|
243
|
+
const size = useSharedValue({ width: 0, height: 0 });
|
|
244
|
+
const clock = useClock();
|
|
245
|
+
const time = useDerivedValue(() => (still ? 0 : clock.value), [still]);
|
|
246
|
+
const canvasWidth = useDerivedValue(() => size.value.width);
|
|
247
|
+
const canvasHeight = useDerivedValue(() => size.value.height);
|
|
248
|
+
return (
|
|
249
|
+
<Canvas onSize={size} style={{ flex: 1 }}>
|
|
250
|
+
<Group
|
|
251
|
+
layer={
|
|
252
|
+
<Paint>
|
|
253
|
+
<Blur blur={36} />
|
|
254
|
+
</Paint>
|
|
255
|
+
}
|
|
256
|
+
>
|
|
257
|
+
{colors.slice(0, ORBITS.length).map((color, index) => (
|
|
258
|
+
<Blob key={index} index={index} color={withAlpha(color, 0.55)} size={size} time={time} />
|
|
259
|
+
))}
|
|
260
|
+
</Group>
|
|
261
|
+
{grain ? (
|
|
262
|
+
<Rect x={0} y={0} width={canvasWidth} height={canvasHeight} opacity={0.07}>
|
|
263
|
+
<FractalNoise freqX={0.9} freqY={0.9} octaves={3} />
|
|
264
|
+
</Rect>
|
|
265
|
+
) : null}
|
|
266
|
+
{sweep ? <SweepBand size={size} time={time} color={sweepColor} /> : null}
|
|
267
|
+
</Canvas>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
type SizeValue = SharedValue<{ width: number; height: number }>;
|
|
272
|
+
|
|
273
|
+
function Blob({ index, color, size, time }: { index: number; color: string; size: SizeValue; time: SharedValue<number> }) {
|
|
274
|
+
const orbit = ORBITS[index];
|
|
275
|
+
const cx = useDerivedValue(() => size.value.width * (orbit.x + orbit.rx * Math.sin(time.value * orbit.speed + orbit.phase)));
|
|
276
|
+
const cy = useDerivedValue(() => size.value.height * (orbit.y + orbit.ry * Math.cos(time.value * orbit.speed * 1.3 + orbit.phase)));
|
|
277
|
+
const r = useDerivedValue(() => Math.min(size.value.width, size.value.height) * (0.34 + 0.04 * Math.sin(time.value * 0.0009 + orbit.phase)));
|
|
278
|
+
return <Circle cx={cx} cy={cy} r={r} color={color} />;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function SweepBand({ size, time, color }: { size: SizeValue; time: SharedValue<number>; color: string }) {
|
|
282
|
+
const start = useDerivedValue(() => {
|
|
283
|
+
const progress = (time.value % SWEEP_MS) / SWEEP_MS;
|
|
284
|
+
const w = size.value.width;
|
|
285
|
+
return vec(-w * BAND + progress * w * (1 + BAND), 0);
|
|
286
|
+
});
|
|
287
|
+
const end = useDerivedValue(() => vec(start.value.x + size.value.width * BAND, 0));
|
|
288
|
+
const w = useDerivedValue(() => size.value.width);
|
|
289
|
+
const h = useDerivedValue(() => size.value.height);
|
|
290
|
+
return (
|
|
291
|
+
<Rect x={0} y={0} width={w} height={h}>
|
|
292
|
+
<LinearGradient start={start} end={end} colors={[withAlpha(color, 0), withAlpha(color, 0.1), withAlpha(color, 0)]} />
|
|
293
|
+
</Rect>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function PulseLabel({ text, animate, textStyle }: { text: string; animate: boolean; textStyle: StyleProp<TextStyle> }) {
|
|
298
|
+
const opacity = useSharedValue(1);
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (!animate) return;
|
|
302
|
+
const curve = Easing.inOut(Easing.ease);
|
|
303
|
+
opacity.value = withRepeat(
|
|
304
|
+
withSequence(withTiming(0.45, { duration: 800, easing: curve }), withTiming(1, { duration: 800, easing: curve })),
|
|
305
|
+
-1,
|
|
306
|
+
false,
|
|
307
|
+
);
|
|
308
|
+
return () => cancelAnimation(opacity);
|
|
309
|
+
}, [animate, opacity]);
|
|
310
|
+
|
|
311
|
+
const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value }));
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<Animated.View entering={animate ? FadeIn.duration(260).easing(EASE_IN_OUT) : undefined}>
|
|
315
|
+
<Animated.Text style={[textStyle, animatedStyle]}>{text}</Animated.Text>
|
|
316
|
+
</Animated.View>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
type ProgressTrackProps = { progress?: number; active: boolean; animate: boolean; color: string; track: string };
|
|
321
|
+
|
|
322
|
+
function ProgressTrack({ progress, active, animate, color, track }: ProgressTrackProps) {
|
|
323
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
324
|
+
const fill = useSharedValue(progress ?? 0);
|
|
325
|
+
const sweep = useSharedValue(0);
|
|
326
|
+
const determinate = progress !== undefined;
|
|
327
|
+
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
if (!determinate) return;
|
|
330
|
+
const target = Math.min(1, Math.max(0, progress ?? 0));
|
|
331
|
+
fill.value = animate ? withTiming(target, { duration: 420, easing: Easing.out(Easing.cubic) }) : target;
|
|
332
|
+
}, [animate, determinate, fill, progress]);
|
|
333
|
+
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
if (determinate || !active || !animate) {
|
|
336
|
+
cancelAnimation(sweep);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
sweep.value = 0;
|
|
340
|
+
sweep.value = withRepeat(withTiming(1, { duration: 1600, easing: Easing.inOut(Easing.cubic) }), -1, false);
|
|
341
|
+
return () => cancelAnimation(sweep);
|
|
342
|
+
}, [active, animate, determinate, sweep]);
|
|
343
|
+
|
|
344
|
+
const fillStyle = useAnimatedStyle(() => {
|
|
345
|
+
if (determinate) return { width: trackWidth * fill.value, transform: [{ translateX: 0 }] };
|
|
346
|
+
const segment = trackWidth * SEGMENT;
|
|
347
|
+
return { width: segment, transform: [{ translateX: -segment + sweep.value * (trackWidth + segment) }] };
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const handleLayout = (event: LayoutChangeEvent) => setTrackWidth(event.nativeEvent.layout.width);
|
|
351
|
+
|
|
352
|
+
if (!active && !determinate) return null;
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<Animated.View
|
|
356
|
+
entering={animate ? FadeIn.duration(260).easing(EASE_IN_OUT) : undefined}
|
|
357
|
+
onLayout={handleLayout}
|
|
358
|
+
style={{ position: "absolute", left: 0, right: 0, bottom: 0, height: TRACK, backgroundColor: track, overflow: "hidden" }}
|
|
359
|
+
>
|
|
360
|
+
<Animated.View style={[{ height: TRACK, backgroundColor: color, borderRadius: TRACK / 2 }, fillStyle]} />
|
|
361
|
+
</Animated.View>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export type MediaGeneratingStyleProps = Omit<MediaGeneratingProps, "variant">;
|
|
366
|
+
|
|
367
|
+
export function MediaGeneratingBlobs(props: MediaGeneratingStyleProps) {
|
|
368
|
+
return <MediaGenerating {...props} variant="blobs" />;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function MediaGeneratingMatrix(props: MediaGeneratingStyleProps) {
|
|
372
|
+
return <MediaGenerating {...props} variant="matrix" />;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function MediaGeneratingOrbit(props: MediaGeneratingStyleProps) {
|
|
376
|
+
return <MediaGenerating {...props} variant="orbit" />;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function MediaGeneratingScan(props: MediaGeneratingStyleProps) {
|
|
380
|
+
return <MediaGenerating {...props} variant="scan" />;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function MediaGeneratingConstellation(props: MediaGeneratingStyleProps) {
|
|
384
|
+
return <MediaGenerating {...props} variant="constellation" />;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function MediaGeneratingHelix(props: MediaGeneratingStyleProps) {
|
|
388
|
+
return <MediaGenerating {...props} variant="helix" />;
|
|
389
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Canvas, LinearGradient, Path, Skia, useClock, vec } from "@shopify/react-native-skia";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { View, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native";
|
|
4
|
+
import { type SharedValue, useDerivedValue, useFrameCallback, useReducedMotion, useSharedValue } from "react-native-reanimated";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_HEIGHT = 32;
|
|
7
|
+
const SAMPLE_STEP = 2;
|
|
8
|
+
const MIN_AMPLITUDE = 1.5;
|
|
9
|
+
const EDGE_PADDING = 2;
|
|
10
|
+
const HISTORY = 96;
|
|
11
|
+
const PUSH_MS = 70;
|
|
12
|
+
|
|
13
|
+
type WaveLayer = {
|
|
14
|
+
frequency: number;
|
|
15
|
+
speed: number;
|
|
16
|
+
phase: number;
|
|
17
|
+
amplitude: number;
|
|
18
|
+
strokeWidth: number;
|
|
19
|
+
opacity: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const LAYERS: WaveLayer[] = [
|
|
23
|
+
{ frequency: 1.1, speed: 0.0024, phase: 2.6, amplitude: 0.55, strokeWidth: 1.5, opacity: 0.45 },
|
|
24
|
+
{ frequency: 2.3, speed: -0.0031, phase: 1.4, amplitude: 0.75, strokeWidth: 1.5, opacity: 0.6 },
|
|
25
|
+
{ frequency: 1.6, speed: 0.0042, phase: 0, amplitude: 1, strokeWidth: 2, opacity: 1 },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export type VoiceWaveVariant = "lines" | "bars" | "dots" | "pulse";
|
|
29
|
+
|
|
30
|
+
export type VoiceWaveProps = {
|
|
31
|
+
level: SharedValue<number>;
|
|
32
|
+
colors: [string, string, string];
|
|
33
|
+
variant?: VoiceWaveVariant;
|
|
34
|
+
height?: number;
|
|
35
|
+
style?: StyleProp<ViewStyle>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type LayerProps = {
|
|
39
|
+
layer: WaveLayer;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
level: SharedValue<number>;
|
|
43
|
+
clock: SharedValue<number>;
|
|
44
|
+
color: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function WaveLayerPath({ layer, width, height, level, clock, color }: LayerProps) {
|
|
48
|
+
const path = useDerivedValue(() => {
|
|
49
|
+
const middle = height / 2;
|
|
50
|
+
const reach = middle - EDGE_PADDING;
|
|
51
|
+
const amplitude = (MIN_AMPLITUDE + level.value * (reach - MIN_AMPLITUDE)) * layer.amplitude;
|
|
52
|
+
const offset = clock.value * layer.speed + layer.phase;
|
|
53
|
+
const next = Skia.Path.Make();
|
|
54
|
+
for (let x = 0; x <= width; x += SAMPLE_STEP) {
|
|
55
|
+
const progress = x / width;
|
|
56
|
+
const envelope = Math.sin(Math.PI * progress);
|
|
57
|
+
const y = middle + amplitude * envelope * Math.sin(2 * Math.PI * layer.frequency * progress + offset);
|
|
58
|
+
if (x === 0) next.moveTo(x, y);
|
|
59
|
+
else next.lineTo(x, y);
|
|
60
|
+
}
|
|
61
|
+
return next;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return <Path path={path} style="stroke" strokeWidth={layer.strokeWidth} strokeCap="round" strokeJoin="round" color={color} opacity={layer.opacity} />;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Lines({ width, height, level, clock, colors }: Omit<LayerProps, "layer" | "color"> & { colors: string[] }) {
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
{LAYERS.map((layer, index) => (
|
|
71
|
+
<WaveLayerPath key={index} layer={layer} width={width} height={height} level={level} clock={clock} color={colors[index]} />
|
|
72
|
+
))}
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type FieldProps = { width: number; height: number; level: SharedValue<number>; clock: SharedValue<number>; colors: string[]; still: boolean };
|
|
78
|
+
|
|
79
|
+
function Bars({ width, height, level, colors, still }: FieldProps) {
|
|
80
|
+
const history = useSharedValue<number[]>(new Array(HISTORY).fill(0));
|
|
81
|
+
const last = useSharedValue(0);
|
|
82
|
+
const smooth = useSharedValue(0);
|
|
83
|
+
|
|
84
|
+
useFrameCallback((info) => {
|
|
85
|
+
smooth.value += (level.value - smooth.value) * 0.35;
|
|
86
|
+
if (info.timestamp - last.value < PUSH_MS) return;
|
|
87
|
+
last.value = info.timestamp;
|
|
88
|
+
const next = history.value.slice(1);
|
|
89
|
+
next.push(smooth.value);
|
|
90
|
+
history.value = next;
|
|
91
|
+
}, !still);
|
|
92
|
+
|
|
93
|
+
const path = useDerivedValue(() => {
|
|
94
|
+
const bar = 3;
|
|
95
|
+
const gap = 3;
|
|
96
|
+
const count = Math.min(HISTORY, Math.floor((width + gap) / (bar + gap)));
|
|
97
|
+
const middle = height / 2;
|
|
98
|
+
const samples = still ? new Array(HISTORY).fill(level.value) : history.value;
|
|
99
|
+
const next = Skia.Path.Make();
|
|
100
|
+
for (let index = 0; index < count; index += 1) {
|
|
101
|
+
const sample = samples[HISTORY - count + index] ?? 0;
|
|
102
|
+
const h = Math.max(bar, sample * (height - 4));
|
|
103
|
+
const x = width - (count - index) * (bar + gap) + gap;
|
|
104
|
+
next.addRRect(Skia.RRectXY(Skia.XYWHRect(x, middle - h / 2, bar, h), bar / 2, bar / 2));
|
|
105
|
+
}
|
|
106
|
+
return next;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Path path={path}>
|
|
111
|
+
<LinearGradient start={vec(0, 0)} end={vec(width, 0)} colors={[colors[0], colors[1], colors[2]]} />
|
|
112
|
+
</Path>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function Dots({ width, height, level, clock, colors }: FieldProps) {
|
|
117
|
+
const path = useDerivedValue(() => {
|
|
118
|
+
const spacing = 11;
|
|
119
|
+
const count = Math.max(3, Math.floor(width / spacing));
|
|
120
|
+
const start = (width - (count - 1) * spacing) / 2;
|
|
121
|
+
const middle = height / 2;
|
|
122
|
+
const reach = middle - 4;
|
|
123
|
+
const t = clock.value * 0.006;
|
|
124
|
+
const next = Skia.Path.Make();
|
|
125
|
+
for (let index = 0; index < count; index += 1) {
|
|
126
|
+
const progress = index / (count - 1);
|
|
127
|
+
const envelope = Math.sin(Math.PI * progress);
|
|
128
|
+
const wave = Math.sin(t + index * 0.55) * Math.sin(t * 0.43 + index * 0.21);
|
|
129
|
+
const y = middle + wave * reach * envelope * (0.15 + level.value * 0.85);
|
|
130
|
+
const r = 1.6 + Math.abs(wave) * level.value * 1.8 * envelope;
|
|
131
|
+
next.addCircle(start + index * spacing, y, r);
|
|
132
|
+
}
|
|
133
|
+
return next;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Path path={path}>
|
|
138
|
+
<LinearGradient start={vec(0, 0)} end={vec(width, 0)} colors={[colors[0], colors[1], colors[2]]} />
|
|
139
|
+
</Path>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function Pulse({ width, height, level, clock, colors }: FieldProps) {
|
|
144
|
+
const path = useDerivedValue(() => {
|
|
145
|
+
const middle = height / 2;
|
|
146
|
+
const reach = middle - 3;
|
|
147
|
+
const t = clock.value * 0.004;
|
|
148
|
+
const next = Skia.Path.Make();
|
|
149
|
+
for (let x = 0; x <= width; x += 2) {
|
|
150
|
+
const progress = x / width;
|
|
151
|
+
const envelope = Math.pow(Math.sin(Math.PI * progress), 1.5);
|
|
152
|
+
const carrier = Math.sin(x * 0.21 - t * 5.2);
|
|
153
|
+
const burst = Math.pow(Math.abs(Math.sin(x * 0.027 + t * 1.7)), 3);
|
|
154
|
+
const y = middle + carrier * burst * envelope * reach * (0.08 + level.value * 0.92);
|
|
155
|
+
if (x === 0) next.moveTo(x, y);
|
|
156
|
+
else next.lineTo(x, y);
|
|
157
|
+
}
|
|
158
|
+
return next;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<>
|
|
163
|
+
<Path path={path} style="stroke" strokeWidth={5} strokeCap="round" strokeJoin="round" color={colors[1]} opacity={0.18} />
|
|
164
|
+
<Path path={path} style="stroke" strokeWidth={1.8} strokeCap="round" strokeJoin="round">
|
|
165
|
+
<LinearGradient start={vec(0, 0)} end={vec(width, 0)} colors={[colors[0], colors[2], colors[0]]} />
|
|
166
|
+
</Path>
|
|
167
|
+
</>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function VoiceWave({ level, colors, variant = "lines", height = DEFAULT_HEIGHT, style }: VoiceWaveProps) {
|
|
172
|
+
const [width, setWidth] = useState(0);
|
|
173
|
+
const reduceMotion = useReducedMotion();
|
|
174
|
+
const ticking = useClock();
|
|
175
|
+
const clock = useDerivedValue(() => (reduceMotion ? 1200 : ticking.value), [reduceMotion]);
|
|
176
|
+
|
|
177
|
+
const handleLayout = (event: LayoutChangeEvent) => setWidth(event.nativeEvent.layout.width);
|
|
178
|
+
const props = { width, height, level, clock, colors, still: reduceMotion };
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<View style={[{ flex: 1, height }, style]} onLayout={handleLayout}>
|
|
182
|
+
{width > 0 ? (
|
|
183
|
+
<Canvas style={{ width, height }}>
|
|
184
|
+
{variant === "bars" ? (
|
|
185
|
+
<Bars {...props} />
|
|
186
|
+
) : variant === "dots" ? (
|
|
187
|
+
<Dots {...props} />
|
|
188
|
+
) : variant === "pulse" ? (
|
|
189
|
+
<Pulse {...props} />
|
|
190
|
+
) : (
|
|
191
|
+
<Lines width={width} height={height} level={level} clock={clock} colors={colors} />
|
|
192
|
+
)}
|
|
193
|
+
</Canvas>
|
|
194
|
+
) : null}
|
|
195
|
+
</View>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export type VoiceWaveStyleProps = Omit<VoiceWaveProps, "variant">;
|
|
200
|
+
|
|
201
|
+
export function VoiceWaveLines(props: VoiceWaveStyleProps) {
|
|
202
|
+
return <VoiceWave {...props} variant="lines" />;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function VoiceWaveBars(props: VoiceWaveStyleProps) {
|
|
206
|
+
return <VoiceWave {...props} variant="bars" />;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function VoiceWaveDots(props: VoiceWaveStyleProps) {
|
|
210
|
+
return <VoiceWave {...props} variant="dots" />;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function VoiceWavePulse(props: VoiceWaveStyleProps) {
|
|
214
|
+
return <VoiceWave {...props} variant="pulse" />;
|
|
215
|
+
}
|