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,248 @@
|
|
|
1
|
+
import { Canvas, Path, Skia, type SkPath, useClock } from "@shopify/react-native-skia";
|
|
2
|
+
import { useEffect, useMemo } from "react";
|
|
3
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
4
|
+
import { Easing, useDerivedValue, useReducedMotion, useSharedValue, withDelay, withRepeat, withSequence, withTiming } from "react-native-reanimated";
|
|
5
|
+
import { useMotionPalette } from "../theme/provider";
|
|
6
|
+
|
|
7
|
+
export type DotOrbVariant = "orbit" | "scan" | "constellation" | "helix";
|
|
8
|
+
|
|
9
|
+
export type DotOrbProps = {
|
|
10
|
+
variant?: DotOrbVariant;
|
|
11
|
+
size?: number;
|
|
12
|
+
color?: string;
|
|
13
|
+
accent?: string;
|
|
14
|
+
speed?: number;
|
|
15
|
+
dotScale?: number;
|
|
16
|
+
paused?: boolean;
|
|
17
|
+
inset?: number;
|
|
18
|
+
scatter?: boolean | "loop";
|
|
19
|
+
style?: StyleProp<ViewStyle>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type Frame = { back: SkPath; mid: SkPath; front: SkPath; accent: SkPath; lines: SkPath; linesOpacity: number };
|
|
23
|
+
|
|
24
|
+
const TAU = Math.PI * 2;
|
|
25
|
+
const STILL_TIME = 1800;
|
|
26
|
+
const SCATTER_MS = 700;
|
|
27
|
+
const LOOP_MOVE_MS = 900;
|
|
28
|
+
const LOOP_GATHERED_MS = 1100;
|
|
29
|
+
const LOOP_SCATTERED_MS = 400;
|
|
30
|
+
|
|
31
|
+
function fibonacciSphere(count: number) {
|
|
32
|
+
const points: number[] = [];
|
|
33
|
+
const golden = Math.PI * (3 - Math.sqrt(5));
|
|
34
|
+
for (let index = 0; index < count; index += 1) {
|
|
35
|
+
const y = 1 - (index / (count - 1)) * 2;
|
|
36
|
+
const radius = Math.sqrt(1 - y * y);
|
|
37
|
+
const theta = golden * index;
|
|
38
|
+
points.push(Math.cos(theta) * radius, y, Math.sin(theta) * radius);
|
|
39
|
+
}
|
|
40
|
+
return points;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildEdges(points: number[], limit: number) {
|
|
44
|
+
const edges: number[] = [];
|
|
45
|
+
const count = points.length / 3;
|
|
46
|
+
for (let a = 0; a < count; a += 1) {
|
|
47
|
+
for (let b = a + 1; b < count; b += 1) {
|
|
48
|
+
const dx = points[a * 3] - points[b * 3];
|
|
49
|
+
const dy = points[a * 3 + 1] - points[b * 3 + 1];
|
|
50
|
+
const dz = points[a * 3 + 2] - points[b * 3 + 2];
|
|
51
|
+
if (dx * dx + dy * dy + dz * dz < limit * limit) edges.push(a, b);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const order: number[] = [];
|
|
55
|
+
const pairs = edges.length / 2;
|
|
56
|
+
for (let index = 0; index < pairs; index += 1) order.push(index);
|
|
57
|
+
for (let index = pairs - 1; index > 0; index -= 1) {
|
|
58
|
+
const swap = Math.floor((Math.sin(index * 12.9898) * 43758.5453 - Math.floor(Math.sin(index * 12.9898) * 43758.5453)) * (index + 1));
|
|
59
|
+
const temp = order[index];
|
|
60
|
+
order[index] = order[swap];
|
|
61
|
+
order[swap] = temp;
|
|
62
|
+
}
|
|
63
|
+
const shuffled: number[] = [];
|
|
64
|
+
order.forEach((pair) => shuffled.push(edges[pair * 2], edges[pair * 2 + 1]));
|
|
65
|
+
return shuffled;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function project(x: number, y: number, z: number, spin: number, tilt: number) {
|
|
69
|
+
"worklet";
|
|
70
|
+
const x1 = x * Math.cos(spin) + z * Math.sin(spin);
|
|
71
|
+
const z1 = -x * Math.sin(spin) + z * Math.cos(spin);
|
|
72
|
+
const y2 = y * Math.cos(tilt) - z1 * Math.sin(tilt);
|
|
73
|
+
const z2 = y * Math.sin(tilt) + z1 * Math.cos(tilt);
|
|
74
|
+
return [x1, y2, z2];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function smooth(value: number) {
|
|
78
|
+
"worklet";
|
|
79
|
+
const t = Math.min(1, Math.max(0, value));
|
|
80
|
+
return t * t * (3 - 2 * t);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function DotOrb({
|
|
84
|
+
variant = "orbit",
|
|
85
|
+
size,
|
|
86
|
+
color,
|
|
87
|
+
accent,
|
|
88
|
+
speed = 1,
|
|
89
|
+
dotScale = 1,
|
|
90
|
+
paused = false,
|
|
91
|
+
inset = 0,
|
|
92
|
+
scatter = false,
|
|
93
|
+
style,
|
|
94
|
+
}: DotOrbProps) {
|
|
95
|
+
const palette = useMotionPalette();
|
|
96
|
+
const reduceMotion = useReducedMotion();
|
|
97
|
+
const ink = color ?? palette.text;
|
|
98
|
+
const highlight = accent ?? palette.accent;
|
|
99
|
+
const clock = useClock();
|
|
100
|
+
const layout = useSharedValue({ width: size ?? 0, height: size ?? 0 });
|
|
101
|
+
const still = reduceMotion || paused;
|
|
102
|
+
const spread = useSharedValue(scatter === true ? 1 : 0);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (reduceMotion) {
|
|
106
|
+
spread.value = scatter === true ? 1 : 0;
|
|
107
|
+
} else if (scatter === "loop") {
|
|
108
|
+
const move = { duration: LOOP_MOVE_MS, easing: Easing.inOut(Easing.cubic) };
|
|
109
|
+
spread.value = withRepeat(withSequence(withDelay(LOOP_GATHERED_MS, withTiming(1, move)), withDelay(LOOP_SCATTERED_MS, withTiming(0, move))), -1);
|
|
110
|
+
} else {
|
|
111
|
+
spread.value = withTiming(scatter ? 1 : 0, { duration: SCATTER_MS, easing: Easing.out(Easing.cubic) });
|
|
112
|
+
}
|
|
113
|
+
}, [reduceMotion, scatter, spread]);
|
|
114
|
+
|
|
115
|
+
const sphere = useMemo(() => fibonacciSphere(variant === "constellation" ? 46 : 240), [variant]);
|
|
116
|
+
const edges = useMemo(() => (variant === "constellation" ? buildEdges(sphere, 0.6) : []), [sphere, variant]);
|
|
117
|
+
|
|
118
|
+
const frame = useDerivedValue<Frame>(() => {
|
|
119
|
+
const t = (still ? STILL_TIME : clock.value) * 0.001 * speed;
|
|
120
|
+
const width = layout.value.width;
|
|
121
|
+
const height = layout.value.height;
|
|
122
|
+
const cx = width / 2;
|
|
123
|
+
const cy = height / 2;
|
|
124
|
+
const pad = inset * width;
|
|
125
|
+
const radius = Math.min(width - pad * 2, height - pad * 2) * 0.4;
|
|
126
|
+
const s = spread.value;
|
|
127
|
+
let order = 0;
|
|
128
|
+
const dot = Math.max(0.6, radius * 0.045 * dotScale);
|
|
129
|
+
const back = Skia.Path.Make();
|
|
130
|
+
const mid = Skia.Path.Make();
|
|
131
|
+
const front = Skia.Path.Make();
|
|
132
|
+
const accentPath = Skia.Path.Make();
|
|
133
|
+
const lines = Skia.Path.Make();
|
|
134
|
+
let linesOpacity = 0;
|
|
135
|
+
|
|
136
|
+
const spot = (x: number, y: number) => {
|
|
137
|
+
const px = cx + x * radius;
|
|
138
|
+
const py = cy + y * radius;
|
|
139
|
+
order += 1;
|
|
140
|
+
if (s === 0) return [px, py];
|
|
141
|
+
const hx = Math.sin(order * 12.9898) * 43758.5453;
|
|
142
|
+
const hy = Math.sin(order * 78.233) * 43758.5453;
|
|
143
|
+
const tx = width * (0.06 + 0.88 * (hx - Math.floor(hx)));
|
|
144
|
+
const ty = height * (0.06 + 0.88 * (hy - Math.floor(hy)));
|
|
145
|
+
return [px + (tx - px) * s, py + (ty - py) * s];
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const place = (x: number, y: number, z: number, scale: number) => {
|
|
149
|
+
const depth = (z + 1) / 2;
|
|
150
|
+
const r = dot * scale * (0.45 + 0.55 * depth);
|
|
151
|
+
const target = z < -0.3 ? back : z < 0.35 ? mid : front;
|
|
152
|
+
const at = spot(x, y);
|
|
153
|
+
target.addCircle(at[0], at[1], r);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (variant === "orbit") {
|
|
157
|
+
const spin = t * 0.25;
|
|
158
|
+
const orbits = [
|
|
159
|
+
[0.35, 0.0, 0.9],
|
|
160
|
+
[1.15, 0.95, -0.7],
|
|
161
|
+
[-0.75, 2.2, 1.15],
|
|
162
|
+
];
|
|
163
|
+
for (let o = 0; o < orbits.length; o += 1) {
|
|
164
|
+
const tiltX = orbits[o][0];
|
|
165
|
+
const rotZ = orbits[o][1];
|
|
166
|
+
const rate = orbits[o][2];
|
|
167
|
+
for (let p = 0; p < 13; p += 1) {
|
|
168
|
+
const theta = (p / 13) * TAU + Math.sin(p * 2.3 + o) * 0.25 + t * rate;
|
|
169
|
+
const bx = Math.cos(theta);
|
|
170
|
+
const bz = Math.sin(theta);
|
|
171
|
+
const y1 = -bz * Math.sin(tiltX);
|
|
172
|
+
const z1 = bz * Math.cos(tiltX);
|
|
173
|
+
const x2 = bx * Math.cos(rotZ) - y1 * Math.sin(rotZ);
|
|
174
|
+
const y2 = bx * Math.sin(rotZ) + y1 * Math.cos(rotZ);
|
|
175
|
+
const projected = project(x2, y2, z1, spin, 0.2);
|
|
176
|
+
place(projected[0], projected[1], projected[2], 1.15 + 0.35 * Math.sin(p * 1.7 + o));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (let p = 0; p < 7; p += 1) {
|
|
180
|
+
const a = p * 0.9 + t * 0.6;
|
|
181
|
+
place(Math.cos(a) * 0.22, Math.sin(a * 1.3) * 0.22, Math.sin(a) * 0.5, 1.3);
|
|
182
|
+
}
|
|
183
|
+
} else if (variant === "scan") {
|
|
184
|
+
const spin = t * 0.35;
|
|
185
|
+
const sweep = -1.75 + ((t * 0.55) % 1) * 3.5;
|
|
186
|
+
for (let index = 0; index < sphere.length; index += 3) {
|
|
187
|
+
const projected = project(sphere[index], sphere[index + 1], sphere[index + 2], spin, 0.28);
|
|
188
|
+
const longitude = Math.atan2(projected[0], projected[2]);
|
|
189
|
+
const near = Math.abs(longitude - sweep);
|
|
190
|
+
if (projected[2] > 0 && near < 0.2) {
|
|
191
|
+
const at = spot(projected[0], projected[1]);
|
|
192
|
+
accentPath.addCircle(at[0], at[1], dot * 0.95 * (1.25 - near * 2));
|
|
193
|
+
} else {
|
|
194
|
+
place(projected[0], projected[1], projected[2], 0.62);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else if (variant === "constellation") {
|
|
198
|
+
const spin = t * 0.22;
|
|
199
|
+
const cycle = (t % 6.5) / 6.5;
|
|
200
|
+
const grow = smooth(cycle / 0.6);
|
|
201
|
+
linesOpacity = (cycle < 0.85 ? 0.42 : 0.42 * (1 - smooth((cycle - 0.85) / 0.15))) * (1 - s);
|
|
202
|
+
const points: number[] = [];
|
|
203
|
+
for (let index = 0; index < sphere.length; index += 3) {
|
|
204
|
+
const projected = project(sphere[index], sphere[index + 1], sphere[index + 2], spin, 0.35);
|
|
205
|
+
points.push(projected[0], projected[1], projected[2]);
|
|
206
|
+
place(projected[0], projected[1], projected[2], 1.05);
|
|
207
|
+
}
|
|
208
|
+
const visible = Math.floor((edges.length / 2) * grow);
|
|
209
|
+
for (let pair = 0; pair < visible; pair += 1) {
|
|
210
|
+
const a = edges[pair * 2] * 3;
|
|
211
|
+
const b = edges[pair * 2 + 1] * 3;
|
|
212
|
+
lines.moveTo(cx + points[a] * radius, cy + points[a + 1] * radius);
|
|
213
|
+
lines.lineTo(cx + points[b] * radius, cy + points[b + 1] * radius);
|
|
214
|
+
}
|
|
215
|
+
} else if (variant === "helix") {
|
|
216
|
+
const spin = t * 0.2;
|
|
217
|
+
for (let strand = 0; strand < 3; strand += 1) {
|
|
218
|
+
for (let step = 0; step < 30; step += 1) {
|
|
219
|
+
const u = step / 29;
|
|
220
|
+
const y = Math.cos(Math.PI * u);
|
|
221
|
+
const ring = Math.sqrt(Math.max(0, 1 - y * y));
|
|
222
|
+
const longitude = (strand * TAU) / 3 + u * Math.PI * 2.4 + t * 1.1;
|
|
223
|
+
const projected = project(ring * Math.cos(longitude), y, ring * Math.sin(longitude), spin, 0.25);
|
|
224
|
+
place(projected[0], projected[1], projected[2], 0.85 + 0.6 * Math.sin(Math.PI * u));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { back, mid, front, accent: accentPath, lines, linesOpacity };
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const back = useDerivedValue(() => frame.value.back);
|
|
233
|
+
const mid = useDerivedValue(() => frame.value.mid);
|
|
234
|
+
const front = useDerivedValue(() => frame.value.front);
|
|
235
|
+
const accentLayer = useDerivedValue(() => frame.value.accent);
|
|
236
|
+
const lines = useDerivedValue(() => frame.value.lines);
|
|
237
|
+
const linesOpacity = useDerivedValue(() => frame.value.linesOpacity);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<Canvas onSize={layout} style={[size !== undefined ? { width: size, height: size } : { flex: 1, alignSelf: "stretch" }, style]}>
|
|
241
|
+
{variant === "constellation" ? <Path path={lines} style="stroke" strokeWidth={1} color={ink} opacity={linesOpacity} /> : null}
|
|
242
|
+
<Path path={back} color={ink} opacity={0.25} />
|
|
243
|
+
<Path path={mid} color={ink} opacity={0.55} />
|
|
244
|
+
<Path path={front} color={ink} opacity={0.95} />
|
|
245
|
+
{variant === "scan" ? <Path path={accentLayer} color={highlight} /> : null}
|
|
246
|
+
</Canvas>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { DotOrb, type DotOrbProps, type DotOrbVariant } from "./dot-orb";
|
|
2
|
+
export { MatrixOrb, type MatrixOrbProps, type MatrixOrbState } from "./matrix-orb";
|
|
3
|
+
export {
|
|
4
|
+
ImageGenerating,
|
|
5
|
+
MEDIA_GENERATING_SIZES,
|
|
6
|
+
MediaGenerating,
|
|
7
|
+
MediaGeneratingBlobs,
|
|
8
|
+
MediaGeneratingConstellation,
|
|
9
|
+
MediaGeneratingHelix,
|
|
10
|
+
MediaGeneratingMatrix,
|
|
11
|
+
MediaGeneratingOrbit,
|
|
12
|
+
MediaGeneratingScan,
|
|
13
|
+
VideoGenerating,
|
|
14
|
+
type ImageGeneratingProps,
|
|
15
|
+
type MediaGeneratingChrome,
|
|
16
|
+
type MediaGeneratingLabels,
|
|
17
|
+
type MediaGeneratingProps,
|
|
18
|
+
type MediaGeneratingSize,
|
|
19
|
+
type MediaGeneratingStyleProps,
|
|
20
|
+
type MediaGeneratingStatus,
|
|
21
|
+
type MediaGeneratingVariant,
|
|
22
|
+
type VideoGeneratingProps,
|
|
23
|
+
} from "./media-generating";
|
|
24
|
+
export {
|
|
25
|
+
VoiceWave,
|
|
26
|
+
VoiceWaveBars,
|
|
27
|
+
VoiceWaveDots,
|
|
28
|
+
VoiceWaveLines,
|
|
29
|
+
VoiceWavePulse,
|
|
30
|
+
type VoiceWaveProps,
|
|
31
|
+
type VoiceWaveStyleProps,
|
|
32
|
+
type VoiceWaveVariant,
|
|
33
|
+
} from "./voice-wave";
|
|
34
|
+
export { LiquidOrb, type LiquidOrbMaterial, type LiquidOrbProps, type LiquidOrbState } from "./liquid-orb";
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { Blur, Canvas, Fill, Group, Oval, Paint, Path, Shader, Skia } from "@shopify/react-native-skia";
|
|
2
|
+
import { useEffect, useMemo } from "react";
|
|
3
|
+
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
4
|
+
import {
|
|
5
|
+
type SharedValue,
|
|
6
|
+
useDerivedValue,
|
|
7
|
+
useFrameCallback,
|
|
8
|
+
useReducedMotion,
|
|
9
|
+
useSharedValue,
|
|
10
|
+
withSpring,
|
|
11
|
+
} from "react-native-reanimated";
|
|
12
|
+
import { useMotionPalette } from "../theme/provider";
|
|
13
|
+
|
|
14
|
+
export type LiquidOrbMaterial = "metal" | "water" | "iridescent" | "ring";
|
|
15
|
+
|
|
16
|
+
export type LiquidOrbState = "idle" | "listening" | "thinking" | "speaking";
|
|
17
|
+
|
|
18
|
+
export type LiquidOrbProps = {
|
|
19
|
+
material?: LiquidOrbMaterial;
|
|
20
|
+
state?: LiquidOrbState;
|
|
21
|
+
level?: SharedValue<number>;
|
|
22
|
+
size?: number;
|
|
23
|
+
speed?: number;
|
|
24
|
+
wobble?: number;
|
|
25
|
+
distortion?: number;
|
|
26
|
+
detail?: number;
|
|
27
|
+
tint?: string;
|
|
28
|
+
colors?: [string, string, string];
|
|
29
|
+
shadow?: boolean;
|
|
30
|
+
style?: StyleProp<ViewStyle>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const MATERIALS: Record<LiquidOrbMaterial, number> = { metal: 0, water: 1, iridescent: 2, ring: 1 };
|
|
34
|
+
const DEFAULT_TINTS: Record<LiquidOrbMaterial, string> = { metal: "#B8C0CC", water: "#6FA8E8", iridescent: "#F2E9FF", ring: "#3FA9F5" };
|
|
35
|
+
const RING_LAYERS = [
|
|
36
|
+
{ lobes: 3, speed: 2.1, phase: 0, amplitude: 1, opacity: 1, stroke: 2 },
|
|
37
|
+
{ lobes: 5, speed: -1.6, phase: 1.7, amplitude: 0.7, opacity: 0.6, stroke: 1.5 },
|
|
38
|
+
{ lobes: 4, speed: 1.2, phase: 3.1, amplitude: 0.5, opacity: 0.4, stroke: 1.5 },
|
|
39
|
+
];
|
|
40
|
+
const STATE_MOTION: Record<LiquidOrbState, { speed: number; wobble: number; scale: number; distortion: number }> = {
|
|
41
|
+
idle: { speed: 0.6, wobble: 0.7, scale: 0.94, distortion: 0.8 },
|
|
42
|
+
listening: { speed: 1, wobble: 1, scale: 1, distortion: 1 },
|
|
43
|
+
thinking: { speed: 2.2, wobble: 0.45, scale: 0.86, distortion: 1.5 },
|
|
44
|
+
speaking: { speed: 1.2, wobble: 1.2, scale: 1.02, distortion: 1.1 },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const ORB = Skia.RuntimeEffect.Make(`
|
|
48
|
+
uniform float2 resolution;
|
|
49
|
+
uniform float time;
|
|
50
|
+
uniform float level;
|
|
51
|
+
uniform float wobble;
|
|
52
|
+
uniform float distortion;
|
|
53
|
+
uniform float detail;
|
|
54
|
+
uniform float material;
|
|
55
|
+
uniform float scale;
|
|
56
|
+
uniform float3 tint;
|
|
57
|
+
|
|
58
|
+
float hash(float2 p) {
|
|
59
|
+
return fract(sin(dot(p, float2(127.1, 311.7))) * 43758.5453);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
float noise(float2 p) {
|
|
63
|
+
float2 i = floor(p);
|
|
64
|
+
float2 f = fract(p);
|
|
65
|
+
float2 u = f * f * (3.0 - 2.0 * f);
|
|
66
|
+
return mix(mix(hash(i), hash(i + float2(1.0, 0.0)), u.x), mix(hash(i + float2(0.0, 1.0)), hash(i + float2(1.0, 1.0)), u.x), u.y);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
float fbm(float2 p) {
|
|
70
|
+
float v = 0.0;
|
|
71
|
+
float a = 0.5;
|
|
72
|
+
for (int k = 0; k < 4; k++) {
|
|
73
|
+
v += a * noise(p);
|
|
74
|
+
p = p * 2.03 + float2(1.7, 9.2);
|
|
75
|
+
a *= 0.5;
|
|
76
|
+
}
|
|
77
|
+
return v;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
half4 main(float2 xy) {
|
|
81
|
+
float2 c = resolution * 0.5;
|
|
82
|
+
float R = min(resolution.x, resolution.y) * 0.34 * scale;
|
|
83
|
+
float2 p = (xy - c) / R;
|
|
84
|
+
float ang = atan(p.y, p.x);
|
|
85
|
+
float t = time;
|
|
86
|
+
float w = wobble * (0.06 * sin(3.0 * ang + t * 1.3) + 0.04 * sin(5.0 * ang - t * 0.9) + 0.03 * sin(2.0 * ang + t * 0.6));
|
|
87
|
+
w += level * (0.07 * sin(4.0 * ang + t * 3.1) + 0.05 * sin(7.0 * ang - t * 2.3) + 0.06);
|
|
88
|
+
float rad = 1.0 + w;
|
|
89
|
+
float d = length(p) - rad;
|
|
90
|
+
float aa = 1.5 / R;
|
|
91
|
+
float mask = 1.0 - smoothstep(-aa, aa, d);
|
|
92
|
+
|
|
93
|
+
float2 q = p / rad;
|
|
94
|
+
float z = sqrt(max(0.0, 1.0 - dot(q, q)));
|
|
95
|
+
float3 n = normalize(float3(q, z));
|
|
96
|
+
float2 wp = q * detail * 1.6 + float2(t * 0.25, -t * 0.18);
|
|
97
|
+
float warp = fbm(wp + fbm(wp + t * 0.15) * 1.5);
|
|
98
|
+
n = normalize(n + distortion * float3(warp - 0.5, fbm(wp * 1.3 + 3.1) - 0.5, 0.0) * 1.4);
|
|
99
|
+
|
|
100
|
+
float3 v = float3(0.0, 0.0, 1.0);
|
|
101
|
+
float fres = pow(1.0 - max(0.0, n.z), 2.5);
|
|
102
|
+
float3 L = normalize(float3(-0.45, -0.6, 0.7));
|
|
103
|
+
float spec = pow(max(0.0, dot(reflect(-L, n), v)), 48.0);
|
|
104
|
+
float3 col = float3(0.0);
|
|
105
|
+
float alpha = 0.0;
|
|
106
|
+
|
|
107
|
+
if (material < 0.5) {
|
|
108
|
+
float3 rv = reflect(-v, n);
|
|
109
|
+
float band = rv.y + 0.35 * fbm(rv.xy * 2.5 + t * 0.2);
|
|
110
|
+
float env = smoothstep(-0.06, 0.06, band) * 0.85 + 0.1;
|
|
111
|
+
float stripes = 0.5 + 0.5 * sin(rv.x * 7.0 + warp * 6.0);
|
|
112
|
+
col = mix(float3(0.05, 0.05, 0.06), float3(0.93, 0.93, 0.96), env * 0.7 + stripes * 0.3);
|
|
113
|
+
col = mix(col, tint, 0.12) + spec * 0.9 + fres * 0.15;
|
|
114
|
+
alpha = 1.0;
|
|
115
|
+
} else if (material < 1.5) {
|
|
116
|
+
float3 deep = tint * 0.5;
|
|
117
|
+
float3 shallow = mix(tint, float3(1.0), 0.55);
|
|
118
|
+
col = mix(shallow, deep, z * 0.75);
|
|
119
|
+
float sparkle = smoothstep(0.6, 0.76, fbm(q * detail * 4.0 + t * 0.6));
|
|
120
|
+
col += sparkle * 0.4 + fres * float3(0.7, 0.9, 1.0) * 0.85 + spec;
|
|
121
|
+
alpha = 0.8 + fres * 0.2;
|
|
122
|
+
} else {
|
|
123
|
+
float th = fres * 1.6 + warp * 0.9 + t * 0.05;
|
|
124
|
+
float3 film = 0.5 + 0.5 * cos(6.2831 * (th + float3(0.0, 0.33, 0.67)));
|
|
125
|
+
col = mix(float3(0.97), film, 0.55 * fres + 0.25);
|
|
126
|
+
col = mix(col, tint, 0.1) + spec * 1.2;
|
|
127
|
+
alpha = 0.16 + fres * 0.78 + spec;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
alpha = clamp(alpha, 0.0, 1.0) * mask;
|
|
131
|
+
float glow = (material > 0.5 && material < 1.5) ? exp(-max(d, 0.0) * 8.0) * (0.35 + level * 0.3) * (1.0 - mask) : 0.0;
|
|
132
|
+
float3 rgb = col * alpha + tint * glow;
|
|
133
|
+
return half4(half3(rgb), half(alpha + glow));
|
|
134
|
+
}
|
|
135
|
+
`);
|
|
136
|
+
|
|
137
|
+
type RingLayerProps = {
|
|
138
|
+
index: number;
|
|
139
|
+
size: number;
|
|
140
|
+
time: SharedValue<number>;
|
|
141
|
+
level: SharedValue<number>;
|
|
142
|
+
scale: SharedValue<number>;
|
|
143
|
+
still: boolean;
|
|
144
|
+
color: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function RingLayer({ index, size, time, level, scale, still, color }: RingLayerProps) {
|
|
148
|
+
const layer = RING_LAYERS[index];
|
|
149
|
+
const path = useDerivedValue(() => {
|
|
150
|
+
const center = size / 2;
|
|
151
|
+
const base = size * 0.3 * scale.value;
|
|
152
|
+
const amp = size * 0.14 * layer.amplitude * (0.12 + level.value * 0.88);
|
|
153
|
+
const t = (still ? 2.4 : time.value) * layer.speed + layer.phase;
|
|
154
|
+
const next = Skia.Path.Make();
|
|
155
|
+
for (let step = 0; step <= 96; step += 1) {
|
|
156
|
+
const theta = (step / 96) * Math.PI * 2;
|
|
157
|
+
const r = base + amp * Math.sin(layer.lobes * theta + t) * Math.cos(2 * theta - t * 0.7);
|
|
158
|
+
const x = center + Math.cos(theta) * r;
|
|
159
|
+
const y = center + Math.sin(theta) * r;
|
|
160
|
+
if (step === 0) next.moveTo(x, y);
|
|
161
|
+
else next.lineTo(x, y);
|
|
162
|
+
}
|
|
163
|
+
next.close();
|
|
164
|
+
return next;
|
|
165
|
+
}, [size, still]);
|
|
166
|
+
return <Path path={path} style="stroke" strokeWidth={layer.stroke * (size / 120)} strokeJoin="round" color={color} opacity={layer.opacity} />;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function LiquidOrb({
|
|
170
|
+
material = "water",
|
|
171
|
+
state = "idle",
|
|
172
|
+
level,
|
|
173
|
+
size = 200,
|
|
174
|
+
speed = 1,
|
|
175
|
+
wobble = 0.6,
|
|
176
|
+
distortion = 0.5,
|
|
177
|
+
detail = 1,
|
|
178
|
+
tint,
|
|
179
|
+
colors,
|
|
180
|
+
shadow = true,
|
|
181
|
+
style,
|
|
182
|
+
}: LiquidOrbProps) {
|
|
183
|
+
const palette = useMotionPalette();
|
|
184
|
+
const reduceMotion = useReducedMotion();
|
|
185
|
+
const color = tint ?? DEFAULT_TINTS[material];
|
|
186
|
+
const rgb = useMemo(() => {
|
|
187
|
+
const parsed = Skia.Color(color);
|
|
188
|
+
return [parsed[0], parsed[1], parsed[2]];
|
|
189
|
+
}, [color]);
|
|
190
|
+
const motion = STATE_MOTION[state];
|
|
191
|
+
const time = useSharedValue(0);
|
|
192
|
+
const smooth = useSharedValue(0);
|
|
193
|
+
const rate = useSharedValue(motion.speed * speed);
|
|
194
|
+
const shape = useSharedValue(motion.scale);
|
|
195
|
+
const wob = useSharedValue(motion.wobble * wobble);
|
|
196
|
+
const dist = useSharedValue(motion.distortion * distortion);
|
|
197
|
+
const fallback = useSharedValue(0);
|
|
198
|
+
const input = level ?? fallback;
|
|
199
|
+
const listensToLevel = state === "listening" || state === "speaking";
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const config = { damping: 18, stiffness: 120, mass: 1 };
|
|
203
|
+
rate.value = withSpring(motion.speed * speed, config);
|
|
204
|
+
shape.value = reduceMotion ? motion.scale : withSpring(motion.scale, config);
|
|
205
|
+
wob.value = withSpring(motion.wobble * wobble, config);
|
|
206
|
+
dist.value = withSpring(motion.distortion * distortion, config);
|
|
207
|
+
}, [dist, distortion, motion, rate, reduceMotion, shape, speed, wob, wobble]);
|
|
208
|
+
|
|
209
|
+
useFrameCallback((info) => {
|
|
210
|
+
const delta = Math.min(0.05, (info.timeSincePreviousFrame ?? 16) / 1000);
|
|
211
|
+
time.value += delta * rate.value;
|
|
212
|
+
const target = listensToLevel ? Math.min(1, Math.max(0, input.value)) : state === "thinking" ? 0.15 + 0.1 * Math.sin(time.value * 3) : 0;
|
|
213
|
+
smooth.value += (target - smooth.value) * Math.min(1, delta * 9);
|
|
214
|
+
}, !reduceMotion);
|
|
215
|
+
|
|
216
|
+
const uniforms = useDerivedValue(() => ({
|
|
217
|
+
resolution: [size, size],
|
|
218
|
+
time: reduceMotion ? 2.4 : time.value,
|
|
219
|
+
level: smooth.value,
|
|
220
|
+
wobble: wob.value,
|
|
221
|
+
distortion: dist.value,
|
|
222
|
+
detail,
|
|
223
|
+
material: MATERIALS[material],
|
|
224
|
+
scale: shape.value,
|
|
225
|
+
tint: rgb,
|
|
226
|
+
}), [detail, material, rgb, size, reduceMotion]);
|
|
227
|
+
|
|
228
|
+
const shadowWidth = useDerivedValue(() => size * 0.5 * shape.value * (1 + smooth.value * 0.08));
|
|
229
|
+
const shadowX = useDerivedValue(() => (size - shadowWidth.value) / 2);
|
|
230
|
+
const shadowOpacity = material === "iridescent" ? 0.1 : material === "water" ? 0.16 : 0.22;
|
|
231
|
+
const ring = material === "ring";
|
|
232
|
+
const hasShadow = shadow && !ring;
|
|
233
|
+
const ringColors = colors ?? [color, color, color];
|
|
234
|
+
|
|
235
|
+
if (!ORB) return null;
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<View style={[{ width: size, height: size * (hasShadow ? 1.12 : 1) }, style]}>
|
|
239
|
+
<Canvas style={{ width: size, height: size * (hasShadow ? 1.12 : 1) }}>
|
|
240
|
+
{hasShadow ? (
|
|
241
|
+
<Group
|
|
242
|
+
layer={
|
|
243
|
+
<Paint>
|
|
244
|
+
<Blur blur={size * 0.05} />
|
|
245
|
+
</Paint>
|
|
246
|
+
}
|
|
247
|
+
>
|
|
248
|
+
<Oval x={shadowX} y={size * 0.98} width={shadowWidth} height={size * 0.07} color={palette.text} opacity={shadowOpacity} />
|
|
249
|
+
</Group>
|
|
250
|
+
) : null}
|
|
251
|
+
{ring ? (
|
|
252
|
+
RING_LAYERS.map((_, index) => (
|
|
253
|
+
<RingLayer
|
|
254
|
+
key={index}
|
|
255
|
+
index={index}
|
|
256
|
+
size={size}
|
|
257
|
+
time={time}
|
|
258
|
+
level={smooth}
|
|
259
|
+
scale={shape}
|
|
260
|
+
still={reduceMotion}
|
|
261
|
+
color={ringColors[index]}
|
|
262
|
+
/>
|
|
263
|
+
))
|
|
264
|
+
) : (
|
|
265
|
+
<Group clip={Skia.XYWHRect(0, 0, size, size)}>
|
|
266
|
+
<Fill>
|
|
267
|
+
<Shader source={ORB} uniforms={uniforms} />
|
|
268
|
+
</Fill>
|
|
269
|
+
</Group>
|
|
270
|
+
)}
|
|
271
|
+
</Canvas>
|
|
272
|
+
</View>
|
|
273
|
+
);
|
|
274
|
+
}
|