@shogo-ai/voice 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/chunk-D2EVSUZX.js +27 -0
- package/dist/chunk-Q3TJAJ54.js +165 -0
- package/dist/chunk-QHDIEE7O.js +453 -0
- package/dist/chunk-S3S2RNO6.js +671 -0
- package/dist/chunk-ZGYHDC5G.js +423 -0
- package/dist/index.cjs +1316 -0
- package/dist/index.d.cts +432 -0
- package/dist/index.d.ts +432 -0
- package/dist/index.js +735 -0
- package/dist/native/index.cjs +1667 -0
- package/dist/native/index.d.cts +285 -0
- package/dist/native/index.d.ts +285 -0
- package/dist/native/index.js +949 -0
- package/dist/organicParticlesShaders-DIva0vBO.d.cts +693 -0
- package/dist/organicParticlesShaders-DIva0vBO.d.ts +693 -0
- package/dist/react/index.cjs +1670 -0
- package/dist/react/index.d.cts +423 -0
- package/dist/react/index.d.ts +423 -0
- package/dist/react/index.js +952 -0
- package/dist/route/agent.cjs +981 -0
- package/dist/route/agent.d.cts +20 -0
- package/dist/route/agent.d.ts +20 -0
- package/dist/route/agent.js +17 -0
- package/dist/route/audio-tags.cjs +975 -0
- package/dist/route/audio-tags.d.cts +16 -0
- package/dist/route/audio-tags.d.ts +16 -0
- package/dist/route/audio-tags.js +13 -0
- package/dist/route/index.cjs +987 -0
- package/dist/route/index.d.cts +115 -0
- package/dist/route/index.d.ts +115 -0
- package/dist/route/index.js +12 -0
- package/dist/route/signed-url.cjs +975 -0
- package/dist/route/signed-url.d.cts +12 -0
- package/dist/route/signed-url.d.ts +12 -0
- package/dist/route/signed-url.js +13 -0
- package/dist/route/tts-preview.cjs +975 -0
- package/dist/route/tts-preview.d.cts +17 -0
- package/dist/route/tts-preview.d.ts +17 -0
- package/dist/route/tts-preview.js +13 -0
- package/dist/server.cjs +999 -0
- package/dist/server.d.cts +88 -0
- package/dist/server.d.ts +88 -0
- package/dist/server.js +11 -0
- package/dist/types-BKJtOUbE.d.cts +352 -0
- package/dist/types-BKJtOUbE.d.ts +352 -0
- package/package.json +148 -0
|
@@ -0,0 +1,1667 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/native/index.ts
|
|
31
|
+
var native_exports = {};
|
|
32
|
+
__export(native_exports, {
|
|
33
|
+
DEFAULT_ORGANIC_PARTICLES_CONFIG: () => DEFAULT_ORGANIC_PARTICLES_CONFIG,
|
|
34
|
+
DEFAULT_ORGANIC_SPHERE_CONFIG: () => DEFAULT_ORGANIC_SPHERE_CONFIG,
|
|
35
|
+
ORGANIC_PARTICLES_FRAGMENT_SHADER: () => ORGANIC_PARTICLES_FRAGMENT_SHADER,
|
|
36
|
+
ORGANIC_PARTICLES_VERTEX_SHADER: () => ORGANIC_PARTICLES_VERTEX_SHADER,
|
|
37
|
+
ORGANIC_SPHERE_FRAGMENT_SHADER: () => ORGANIC_SPHERE_FRAGMENT_SHADER,
|
|
38
|
+
ORGANIC_SPHERE_VERTEX_SHADER: () => ORGANIC_SPHERE_VERTEX_SHADER,
|
|
39
|
+
OrganicParticles: () => OrganicParticles,
|
|
40
|
+
OrganicSphere: () => OrganicSphere,
|
|
41
|
+
PERLIN_4D: () => PERLIN_4D,
|
|
42
|
+
ShogoVoiceProvider: () => ShogoVoiceProvider,
|
|
43
|
+
resolveOrganicParticlesConfig: () => resolveOrganicParticlesConfig,
|
|
44
|
+
resolveOrganicSphereConfig: () => resolveOrganicSphereConfig,
|
|
45
|
+
useChatConversation: () => useChatConversation,
|
|
46
|
+
useShogoChat: () => useShogoChat,
|
|
47
|
+
useShogoVoice: () => useShogoVoice,
|
|
48
|
+
useVoiceConversation: () => useVoiceConversation
|
|
49
|
+
});
|
|
50
|
+
module.exports = __toCommonJS(native_exports);
|
|
51
|
+
|
|
52
|
+
// src/native/ShogoVoiceProvider.tsx
|
|
53
|
+
var import_react_native = require("@elevenlabs/react-native");
|
|
54
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
55
|
+
function ShogoVoiceProvider(props) {
|
|
56
|
+
const { agentId, children } = props;
|
|
57
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.ConversationProvider, { agentId, children });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/native/useVoiceConversation.ts
|
|
61
|
+
var import_react = require("react");
|
|
62
|
+
var import_react_native2 = require("react-native");
|
|
63
|
+
var import_react_native3 = require("@elevenlabs/react-native");
|
|
64
|
+
|
|
65
|
+
// src/shared/postJson.ts
|
|
66
|
+
function createPostJson({
|
|
67
|
+
authHeaders,
|
|
68
|
+
fetchCredentials
|
|
69
|
+
}) {
|
|
70
|
+
return async function postJson(path, body) {
|
|
71
|
+
return fetch(path, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "content-type": "application/json", ...authHeaders() },
|
|
74
|
+
credentials: fetchCredentials,
|
|
75
|
+
body: JSON.stringify(body)
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/shared/memory.ts
|
|
81
|
+
function createMemoryAddTool({
|
|
82
|
+
postJson,
|
|
83
|
+
memoryAddPath
|
|
84
|
+
}) {
|
|
85
|
+
return async (params) => {
|
|
86
|
+
const fact = typeof params.fact === "string" ? params.fact : "";
|
|
87
|
+
if (!fact.trim()) return "Memory save failed: empty fact.";
|
|
88
|
+
try {
|
|
89
|
+
const res = await postJson(memoryAddPath, { fact });
|
|
90
|
+
if (!res.ok) return "Failed to save memory.";
|
|
91
|
+
return "Memory saved.";
|
|
92
|
+
} catch {
|
|
93
|
+
return "Memory save failed.";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function createMemoryContextInjector({
|
|
98
|
+
postJson,
|
|
99
|
+
memoryRetrievePath,
|
|
100
|
+
conversationRef,
|
|
101
|
+
enabled,
|
|
102
|
+
lastInjectedRef
|
|
103
|
+
}) {
|
|
104
|
+
return async function inject(userText) {
|
|
105
|
+
if (!enabled) return;
|
|
106
|
+
try {
|
|
107
|
+
const res = await postJson(memoryRetrievePath, { query: userText, limit: 4 });
|
|
108
|
+
if (!res.ok) return;
|
|
109
|
+
const data = await res.json();
|
|
110
|
+
const results = data.results ?? [];
|
|
111
|
+
if (!results.length) return;
|
|
112
|
+
const lines = results.map((r) => `- ${r.chunk.trim().replace(/\s+/g, " ").slice(0, 200)}`).join("\n");
|
|
113
|
+
if (lines === lastInjectedRef.current) return;
|
|
114
|
+
lastInjectedRef.current = lines;
|
|
115
|
+
const payload = `Relevant memory about this user:
|
|
116
|
+
${lines}`;
|
|
117
|
+
conversationRef.current?.sendContextualUpdate(payload);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/audioTags.ts
|
|
124
|
+
var AUDIO_TAGS = [
|
|
125
|
+
// Emotions
|
|
126
|
+
{ tag: "happy", group: "emotion", label: "Happy", description: "Warm, upbeat delivery.", example: "[happy] Oh, I\u2019m so glad you stopped by." },
|
|
127
|
+
{ tag: "sad", group: "emotion", label: "Sad", description: "Subdued, heavy-hearted tone.", example: "[sad] I wish things had gone differently." },
|
|
128
|
+
{ tag: "excited", group: "emotion", label: "Excited", description: "Energized, eager delivery.", example: "[excited] Wait until you hear this!" },
|
|
129
|
+
{ tag: "curious", group: "emotion", label: "Curious", description: "Inquisitive, rising inflection.", example: "[curious] Hmm, what did they look like?" },
|
|
130
|
+
{ tag: "sarcastic", group: "emotion", label: "Sarcastic", description: "Dry, ironic tone.", example: "[sarcastic] Oh, fantastic. Another Monday." },
|
|
131
|
+
{ tag: "angry", group: "emotion", label: "Angry", description: "Sharp, irritated edge.", example: "[angry] I told you not to touch that." },
|
|
132
|
+
{ tag: "nervous", group: "emotion", label: "Nervous", description: "Uneasy, faltering delivery.", example: "[nervous] I\u2014 I didn\u2019t think you\u2019d actually come." },
|
|
133
|
+
{ tag: "confident", group: "emotion", label: "Confident", description: "Grounded, assured delivery.", example: "[confident] Trust me. I\u2019ve done this before." },
|
|
134
|
+
// Delivery
|
|
135
|
+
{ tag: "whispers", group: "delivery", label: "Whispers", description: "Hushed, close-mic delivery.", example: "[whispers] Don\u2019t say it too loud." },
|
|
136
|
+
{ tag: "shouts", group: "delivery", label: "Shouts", description: "Loud, projected delivery.", example: "[shouts] Look out behind you!" },
|
|
137
|
+
{ tag: "softly", group: "delivery", label: "Softly", description: "Gentle, quiet tone.", example: "[softly] It\u2019s okay. I\u2019m here." },
|
|
138
|
+
{ tag: "slowly", group: "delivery", label: "Slowly", description: "Deliberate pacing.", example: "[slowly] Tell me\u2026 exactly\u2026 what happened." },
|
|
139
|
+
{ tag: "quickly", group: "delivery", label: "Quickly", description: "Rapid, urgent pacing.", example: "[quickly] We need to go, right now." },
|
|
140
|
+
// Reactions (non-verbal sounds)
|
|
141
|
+
{ tag: "laughs", group: "reaction", label: "Laughs", description: "A genuine laugh.", example: "That\u2019s ridiculous [laughs]." },
|
|
142
|
+
{ tag: "chuckles", group: "reaction", label: "Chuckles", description: "A small, warm chuckle.", example: "[chuckles] You always say that." },
|
|
143
|
+
{ tag: "sighs", group: "reaction", label: "Sighs", description: "An audible sigh.", example: "[sighs] Alright, fine. Let\u2019s do it." },
|
|
144
|
+
{ tag: "gasps", group: "reaction", label: "Gasps", description: "A sharp inhale of surprise.", example: "[gasps] You didn\u2019t!" },
|
|
145
|
+
{ tag: "hesitates", group: "reaction", label: "Hesitates", description: "A pause with uncertainty.", example: "[hesitates] I\u2019m not sure I should say." },
|
|
146
|
+
{ tag: "clears throat", group: "reaction", label: "Clears throat", description: "Throat-clearing beat.", example: "[clears throat] As I was saying\u2026" }
|
|
147
|
+
];
|
|
148
|
+
var KNOWN_TAG_SET = new Set(AUDIO_TAGS.map((t) => t.tag.toLowerCase()));
|
|
149
|
+
function stripAudioTags(text) {
|
|
150
|
+
if (!text) return "";
|
|
151
|
+
return text.replace(/\[([a-z][a-z ]{0,24})\]/gi, (match, inner) => {
|
|
152
|
+
const key = inner.trim().toLowerCase();
|
|
153
|
+
return KNOWN_TAG_SET.has(key) ? "" : match;
|
|
154
|
+
}).replace(/[ \t]{2,}/g, " ").replace(/\s+([,.!?;:])/g, "$1").trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/shared/transcript.ts
|
|
158
|
+
function createTranscriptDisconnectHandler({
|
|
159
|
+
isRestartingRef,
|
|
160
|
+
weStartedSessionRef,
|
|
161
|
+
transcriptRef,
|
|
162
|
+
onTranscript,
|
|
163
|
+
transcriptIngestPath,
|
|
164
|
+
postJson
|
|
165
|
+
}) {
|
|
166
|
+
return function handleTranscriptOnDisconnect() {
|
|
167
|
+
if (isRestartingRef.current) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!weStartedSessionRef.current) {
|
|
171
|
+
transcriptRef.current = [];
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
weStartedSessionRef.current = false;
|
|
175
|
+
const transcript = transcriptRef.current.join("\n");
|
|
176
|
+
transcriptRef.current = [];
|
|
177
|
+
if (onTranscript) {
|
|
178
|
+
void onTranscript(transcript);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const trimmed = transcript.trim();
|
|
182
|
+
if (trimmed.length < 20) return;
|
|
183
|
+
void postJson(transcriptIngestPath, { transcript: trimmed }).catch(() => {
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function appendTranscriptLine(msg, { transcriptRef, weStartedSessionRef }) {
|
|
188
|
+
if (!msg?.message || !msg?.source) return;
|
|
189
|
+
if (!weStartedSessionRef.current) return;
|
|
190
|
+
const speaker = msg.source === "user" ? "User" : "Agent";
|
|
191
|
+
const cleaned = msg.source === "user" ? msg.message : stripAudioTags(msg.message);
|
|
192
|
+
if (!cleaned) return;
|
|
193
|
+
transcriptRef.current.push(`${speaker}: ${cleaned}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/shared/sessionPayload.ts
|
|
197
|
+
function sanitizeDynamicVariables(raw) {
|
|
198
|
+
if (raw == null) return {};
|
|
199
|
+
const out = {};
|
|
200
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
201
|
+
if (v == null) continue;
|
|
202
|
+
out[k] = typeof v === "string" ? v : String(v);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
function buildSessionPayload(opts) {
|
|
207
|
+
const dynamicVariables = {
|
|
208
|
+
...sanitizeDynamicVariables(opts.dynamicVariables),
|
|
209
|
+
character_name: opts.characterName,
|
|
210
|
+
user_context: opts.userContext
|
|
211
|
+
};
|
|
212
|
+
if (typeof opts.conversationId === "string" && opts.conversationId.length > 0) {
|
|
213
|
+
dynamicVariables.conversation_id = opts.conversationId;
|
|
214
|
+
}
|
|
215
|
+
const payload = {
|
|
216
|
+
signedUrl: opts.signedUrl,
|
|
217
|
+
dynamicVariables
|
|
218
|
+
};
|
|
219
|
+
const agentOverrides = {};
|
|
220
|
+
if (opts.suppressFirstMessage) {
|
|
221
|
+
agentOverrides.firstMessage = "";
|
|
222
|
+
}
|
|
223
|
+
if (typeof opts.agentPromptOverride === "string" && opts.agentPromptOverride.length > 0) {
|
|
224
|
+
agentOverrides.prompt = { prompt: opts.agentPromptOverride };
|
|
225
|
+
}
|
|
226
|
+
if (Object.keys(agentOverrides).length > 0) {
|
|
227
|
+
payload.overrides = { agent: agentOverrides };
|
|
228
|
+
}
|
|
229
|
+
return payload;
|
|
230
|
+
}
|
|
231
|
+
async function fetchSignedUrl({
|
|
232
|
+
path,
|
|
233
|
+
fetchCredentials,
|
|
234
|
+
authHeaders
|
|
235
|
+
}) {
|
|
236
|
+
const res = await fetch(path, {
|
|
237
|
+
credentials: fetchCredentials,
|
|
238
|
+
headers: authHeaders()
|
|
239
|
+
});
|
|
240
|
+
if (!res.ok) throw new Error(`Signed URL request failed: ${res.status}`);
|
|
241
|
+
return await res.json();
|
|
242
|
+
}
|
|
243
|
+
function withProjectQuery(signedUrlPath, params) {
|
|
244
|
+
const tail = [];
|
|
245
|
+
if (typeof params.projectId === "string" && params.projectId.length > 0) {
|
|
246
|
+
tail.push(`projectId=${encodeURIComponent(params.projectId)}`);
|
|
247
|
+
}
|
|
248
|
+
if (typeof params.agentName === "string" && params.agentName.length > 0) {
|
|
249
|
+
tail.push(`agentName=${encodeURIComponent(params.agentName)}`);
|
|
250
|
+
}
|
|
251
|
+
if (tail.length === 0) return signedUrlPath;
|
|
252
|
+
const sep = signedUrlPath.includes("?") ? "&" : "?";
|
|
253
|
+
return `${signedUrlPath}${sep}${tail.join("&")}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/native/useVoiceConversation.ts
|
|
257
|
+
function useVoiceConversation(options) {
|
|
258
|
+
const {
|
|
259
|
+
characterName,
|
|
260
|
+
signedUrlPath = "/api/voice/signed-url",
|
|
261
|
+
memoryAddPath = "/api/memory/add",
|
|
262
|
+
memoryRetrievePath = "/api/memory/retrieve",
|
|
263
|
+
transcriptIngestPath = "/api/memory/ingest",
|
|
264
|
+
autoInjectMemory = true,
|
|
265
|
+
includeMemoryTool = true,
|
|
266
|
+
clientTools = {},
|
|
267
|
+
onTranscript,
|
|
268
|
+
shogoApiKey,
|
|
269
|
+
projectId,
|
|
270
|
+
agentName,
|
|
271
|
+
// RN fetch has no real "same-origin" — apps are always cross-
|
|
272
|
+
// origin to the backend. Default to `'omit'` for the bearer
|
|
273
|
+
// path and `'include'` for the cookie path so the cookie jar
|
|
274
|
+
// gets used when the consumer set up auth that way.
|
|
275
|
+
fetchCredentials = shogoApiKey ? "omit" : "include",
|
|
276
|
+
onError,
|
|
277
|
+
onMessage,
|
|
278
|
+
requestPermissions,
|
|
279
|
+
conversationId: optionConversationId,
|
|
280
|
+
dynamicVariables
|
|
281
|
+
} = options;
|
|
282
|
+
const authHeaders = (0, import_react.useCallback)(() => {
|
|
283
|
+
return shogoApiKey ? { authorization: `Bearer ${shogoApiKey}` } : {};
|
|
284
|
+
}, [shogoApiKey]);
|
|
285
|
+
const resolvedSignedUrlPath = withProjectQuery(signedUrlPath, { projectId, agentName });
|
|
286
|
+
const transcriptRef = (0, import_react.useRef)([]);
|
|
287
|
+
const lastInjectedRef = (0, import_react.useRef)("");
|
|
288
|
+
const weStartedSessionRef = (0, import_react.useRef)(false);
|
|
289
|
+
const isRestartingRef = (0, import_react.useRef)(false);
|
|
290
|
+
const [convaiConversationId, setConvaiConversationId] = (0, import_react.useState)(null);
|
|
291
|
+
const conversationRef = (0, import_react.useRef)(null);
|
|
292
|
+
const postJson = (0, import_react.useCallback)(
|
|
293
|
+
createPostJson({ authHeaders, fetchCredentials }),
|
|
294
|
+
[fetchCredentials, authHeaders]
|
|
295
|
+
);
|
|
296
|
+
const injectMemoryContext = (0, import_react.useCallback)(
|
|
297
|
+
createMemoryContextInjector({
|
|
298
|
+
postJson,
|
|
299
|
+
memoryRetrievePath,
|
|
300
|
+
conversationRef,
|
|
301
|
+
enabled: autoInjectMemory,
|
|
302
|
+
lastInjectedRef
|
|
303
|
+
}),
|
|
304
|
+
[autoInjectMemory, memoryRetrievePath, postJson]
|
|
305
|
+
);
|
|
306
|
+
const handleTranscriptOnDisconnect = (0, import_react.useCallback)(
|
|
307
|
+
createTranscriptDisconnectHandler({
|
|
308
|
+
isRestartingRef,
|
|
309
|
+
weStartedSessionRef,
|
|
310
|
+
transcriptRef,
|
|
311
|
+
onTranscript,
|
|
312
|
+
transcriptIngestPath,
|
|
313
|
+
postJson
|
|
314
|
+
}),
|
|
315
|
+
[onTranscript, postJson, transcriptIngestPath]
|
|
316
|
+
);
|
|
317
|
+
const builtInTools = includeMemoryTool ? { add_memory: createMemoryAddTool({ postJson, memoryAddPath }) } : {};
|
|
318
|
+
const mergedClientTools = { ...builtInTools, ...clientTools };
|
|
319
|
+
const conversation = (0, import_react_native3.useConversation)({
|
|
320
|
+
clientTools: mergedClientTools,
|
|
321
|
+
onConnect: (info) => {
|
|
322
|
+
const id = info?.conversationId;
|
|
323
|
+
if (typeof id === "string" && id.length > 0) {
|
|
324
|
+
setConvaiConversationId(id);
|
|
325
|
+
}
|
|
326
|
+
if (!isRestartingRef.current) {
|
|
327
|
+
transcriptRef.current = [];
|
|
328
|
+
lastInjectedRef.current = "";
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
onDisconnect: () => {
|
|
332
|
+
handleTranscriptOnDisconnect();
|
|
333
|
+
if (!isRestartingRef.current) {
|
|
334
|
+
setConvaiConversationId(null);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
onError: (e) => {
|
|
338
|
+
onError?.(e);
|
|
339
|
+
},
|
|
340
|
+
onMessage: (m) => {
|
|
341
|
+
const msg = m;
|
|
342
|
+
if (!msg?.message || !msg?.source) return;
|
|
343
|
+
appendTranscriptLine(msg, { transcriptRef, weStartedSessionRef });
|
|
344
|
+
onMessage?.({ source: msg.source, message: msg.message });
|
|
345
|
+
if (autoInjectMemory && msg.source === "user" && msg.message.trim().length >= 3 && weStartedSessionRef.current) {
|
|
346
|
+
void injectMemoryContext(msg.message);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
const { status, isSpeaking, isListening, isMuted, setMuted, startSession, endSession } = conversation;
|
|
351
|
+
conversationRef.current = conversation;
|
|
352
|
+
(0, import_react.useEffect)(() => {
|
|
353
|
+
const sub = import_react_native2.AppState.addEventListener(
|
|
354
|
+
"change",
|
|
355
|
+
(nextState) => {
|
|
356
|
+
if (nextState !== "background" && nextState !== "inactive") return;
|
|
357
|
+
if (!weStartedSessionRef.current || transcriptRef.current.length === 0) return;
|
|
358
|
+
const transcript = transcriptRef.current.join("\n").trim();
|
|
359
|
+
transcriptRef.current = [];
|
|
360
|
+
if (transcript.length < 20) return;
|
|
361
|
+
void postJson(transcriptIngestPath, { transcript }).catch(() => {
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
return () => {
|
|
366
|
+
sub.remove();
|
|
367
|
+
};
|
|
368
|
+
}, [postJson, transcriptIngestPath]);
|
|
369
|
+
const startInternal = (0, import_react.useCallback)(
|
|
370
|
+
async (opts) => {
|
|
371
|
+
if (status === "connected" || status === "connecting") {
|
|
372
|
+
try {
|
|
373
|
+
endSession();
|
|
374
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (requestPermissions) {
|
|
379
|
+
await requestPermissions();
|
|
380
|
+
}
|
|
381
|
+
const data = await fetchSignedUrl({
|
|
382
|
+
path: resolvedSignedUrlPath,
|
|
383
|
+
fetchCredentials,
|
|
384
|
+
authHeaders
|
|
385
|
+
});
|
|
386
|
+
const ctx = data.userContext || "No prior memories yet.";
|
|
387
|
+
weStartedSessionRef.current = true;
|
|
388
|
+
const sessionPayload = buildSessionPayload({
|
|
389
|
+
signedUrl: data.signedUrl,
|
|
390
|
+
characterName,
|
|
391
|
+
userContext: ctx,
|
|
392
|
+
agentPromptOverride: data.agentPromptOverride,
|
|
393
|
+
suppressFirstMessage: opts?.suppressFirstMessage,
|
|
394
|
+
conversationId: optionConversationId,
|
|
395
|
+
dynamicVariables
|
|
396
|
+
});
|
|
397
|
+
await startSession(sessionPayload);
|
|
398
|
+
},
|
|
399
|
+
[
|
|
400
|
+
status,
|
|
401
|
+
endSession,
|
|
402
|
+
resolvedSignedUrlPath,
|
|
403
|
+
fetchCredentials,
|
|
404
|
+
dynamicVariables,
|
|
405
|
+
startSession,
|
|
406
|
+
characterName,
|
|
407
|
+
authHeaders,
|
|
408
|
+
requestPermissions,
|
|
409
|
+
optionConversationId
|
|
410
|
+
]
|
|
411
|
+
);
|
|
412
|
+
const start = (0, import_react.useCallback)(
|
|
413
|
+
async (opts) => {
|
|
414
|
+
await startInternal(opts);
|
|
415
|
+
},
|
|
416
|
+
[startInternal]
|
|
417
|
+
);
|
|
418
|
+
const end = (0, import_react.useCallback)(() => {
|
|
419
|
+
endSession();
|
|
420
|
+
}, [endSession]);
|
|
421
|
+
const restart = (0, import_react.useCallback)(
|
|
422
|
+
async (opts) => {
|
|
423
|
+
const suppress = opts?.suppressFirstMessage ?? true;
|
|
424
|
+
isRestartingRef.current = true;
|
|
425
|
+
try {
|
|
426
|
+
try {
|
|
427
|
+
endSession();
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
431
|
+
await startInternal({ suppressFirstMessage: suppress });
|
|
432
|
+
} finally {
|
|
433
|
+
isRestartingRef.current = false;
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
[endSession, startInternal]
|
|
437
|
+
);
|
|
438
|
+
const sendContextualUpdate = (0, import_react.useCallback)((text) => {
|
|
439
|
+
conversationRef.current?.sendContextualUpdate(text);
|
|
440
|
+
}, []);
|
|
441
|
+
const sendUserMessage = (0, import_react.useCallback)((text) => {
|
|
442
|
+
const ref = conversationRef.current;
|
|
443
|
+
if (!ref) return;
|
|
444
|
+
if (typeof ref.sendUserMessage === "function") {
|
|
445
|
+
ref.sendUserMessage(text);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
ref.sendContextualUpdate(text);
|
|
449
|
+
try {
|
|
450
|
+
ref.sendUserActivity?.();
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
}, []);
|
|
454
|
+
const sendUserActivity = (0, import_react.useCallback)(() => {
|
|
455
|
+
conversationRef.current?.sendUserActivity?.();
|
|
456
|
+
}, []);
|
|
457
|
+
const getOutputByteFrequencyData = (0, import_react.useCallback)(() => {
|
|
458
|
+
try {
|
|
459
|
+
const fn = conversation.getOutputByteFrequencyData;
|
|
460
|
+
return fn ? fn() : null;
|
|
461
|
+
} catch {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
}, [conversation]);
|
|
465
|
+
return {
|
|
466
|
+
start,
|
|
467
|
+
end,
|
|
468
|
+
restart,
|
|
469
|
+
status,
|
|
470
|
+
isSpeaking,
|
|
471
|
+
isListening,
|
|
472
|
+
isMuted,
|
|
473
|
+
setMuted,
|
|
474
|
+
getOutputByteFrequencyData,
|
|
475
|
+
sendContextualUpdate,
|
|
476
|
+
sendUserMessage,
|
|
477
|
+
sendUserActivity,
|
|
478
|
+
conversationId: optionConversationId ?? convaiConversationId,
|
|
479
|
+
convaiConversationId
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/native/useShogoVoice.ts
|
|
484
|
+
function useShogoVoice(opts = {}) {
|
|
485
|
+
return useVoiceConversation({
|
|
486
|
+
characterName: opts.characterName ?? "Shogo",
|
|
487
|
+
signedUrlPath: opts.signedUrlPath ?? "/api/voice/signed-url",
|
|
488
|
+
fetchCredentials: opts.fetchCredentials ?? "include",
|
|
489
|
+
...opts
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/native/useChatConversation.ts
|
|
494
|
+
var import_react2 = require("react");
|
|
495
|
+
var import_react3 = require("@ai-sdk/react");
|
|
496
|
+
var import_ai = require("ai");
|
|
497
|
+
|
|
498
|
+
// src/shared/chatUrl.ts
|
|
499
|
+
function appendChatQuery(basePath, params) {
|
|
500
|
+
const tail = [];
|
|
501
|
+
for (const [k, v] of Object.entries(params)) {
|
|
502
|
+
if (typeof v === "string" && v.length > 0) {
|
|
503
|
+
tail.push(`${k}=${encodeURIComponent(v)}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (tail.length === 0) return basePath;
|
|
507
|
+
const sep = basePath.includes("?") ? "&" : "?";
|
|
508
|
+
return `${basePath}${sep}${tail.join("&")}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/native/useChatConversation.ts
|
|
512
|
+
var DEFAULT_API_PATH = "/api/chat/turn";
|
|
513
|
+
var DEFAULT_CHAT_ID = "shogo-chat";
|
|
514
|
+
function syntheticId(prefix) {
|
|
515
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
516
|
+
}
|
|
517
|
+
function useChatConversation(options = {}) {
|
|
518
|
+
const {
|
|
519
|
+
api = DEFAULT_API_PATH,
|
|
520
|
+
shogoApiKey,
|
|
521
|
+
projectId,
|
|
522
|
+
agentName,
|
|
523
|
+
conversationId,
|
|
524
|
+
initialMessages,
|
|
525
|
+
clientTools = {},
|
|
526
|
+
tools,
|
|
527
|
+
// RN fetch has no real "same-origin" — apps are always cross-
|
|
528
|
+
// origin to the backend. Default to `'omit'` for the bearer
|
|
529
|
+
// path and `'include'` for the cookie path so the cookie jar
|
|
530
|
+
// gets used when the consumer set up auth that way.
|
|
531
|
+
fetchCredentials = shogoApiKey ? "omit" : "include",
|
|
532
|
+
id,
|
|
533
|
+
onError,
|
|
534
|
+
dynamicVariables
|
|
535
|
+
} = options;
|
|
536
|
+
const resolvedApi = (0, import_react2.useMemo)(
|
|
537
|
+
() => appendChatQuery(api, { projectId, conversationId, agentName }),
|
|
538
|
+
[api, projectId, conversationId, agentName]
|
|
539
|
+
);
|
|
540
|
+
const headers = (0, import_react2.useMemo)(() => {
|
|
541
|
+
const h = {};
|
|
542
|
+
if (shogoApiKey) h.authorization = `Bearer ${shogoApiKey}`;
|
|
543
|
+
return h;
|
|
544
|
+
}, [shogoApiKey]);
|
|
545
|
+
const body = (0, import_react2.useMemo)(() => {
|
|
546
|
+
const b = {};
|
|
547
|
+
if (projectId) b.projectId = projectId;
|
|
548
|
+
if (agentName) b.agentName = agentName;
|
|
549
|
+
if (conversationId) b.conversationId = conversationId;
|
|
550
|
+
if (tools && tools.length > 0) b.tools = tools;
|
|
551
|
+
if (dynamicVariables && Object.keys(dynamicVariables).length > 0) {
|
|
552
|
+
b.dynamicVariables = dynamicVariables;
|
|
553
|
+
}
|
|
554
|
+
return b;
|
|
555
|
+
}, [projectId, agentName, conversationId, tools, dynamicVariables]);
|
|
556
|
+
const transport = (0, import_react2.useMemo)(
|
|
557
|
+
() => new import_ai.DefaultChatTransport({
|
|
558
|
+
api: resolvedApi,
|
|
559
|
+
credentials: fetchCredentials,
|
|
560
|
+
headers,
|
|
561
|
+
body
|
|
562
|
+
}),
|
|
563
|
+
[resolvedApi, fetchCredentials, headers, body]
|
|
564
|
+
);
|
|
565
|
+
const toolsRef = (0, import_react2.useRef)(clientTools);
|
|
566
|
+
toolsRef.current = clientTools;
|
|
567
|
+
const chatRef = (0, import_react2.useRef)(null);
|
|
568
|
+
const resolvedId = id ?? (conversationId ? `shogo-chat:${conversationId}` : DEFAULT_CHAT_ID);
|
|
569
|
+
const chat = (0, import_react3.useChat)({
|
|
570
|
+
id: resolvedId,
|
|
571
|
+
transport,
|
|
572
|
+
messages: initialMessages,
|
|
573
|
+
async onToolCall({ toolCall }) {
|
|
574
|
+
const name = toolCall.toolName;
|
|
575
|
+
const fn = toolsRef.current[name];
|
|
576
|
+
const current = chatRef.current;
|
|
577
|
+
if (!current) return;
|
|
578
|
+
if (!fn) {
|
|
579
|
+
await current.addToolOutput({
|
|
580
|
+
state: "output-error",
|
|
581
|
+
tool: name,
|
|
582
|
+
toolCallId: toolCall.toolCallId,
|
|
583
|
+
errorText: `Unknown chat tool: ${String(name)}`
|
|
584
|
+
});
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const params = typeof toolCall.input === "object" && toolCall.input !== null ? toolCall.input : {};
|
|
589
|
+
const output = await fn(params);
|
|
590
|
+
await current.addToolOutput({
|
|
591
|
+
tool: name,
|
|
592
|
+
toolCallId: toolCall.toolCallId,
|
|
593
|
+
output
|
|
594
|
+
});
|
|
595
|
+
} catch (err) {
|
|
596
|
+
await current.addToolOutput({
|
|
597
|
+
state: "output-error",
|
|
598
|
+
tool: name,
|
|
599
|
+
toolCallId: toolCall.toolCallId,
|
|
600
|
+
errorText: err?.message ?? String(err)
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
onError(err) {
|
|
605
|
+
onError?.(err);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
chatRef.current = chat;
|
|
609
|
+
const sendMessage = (0, import_react2.useCallback)(
|
|
610
|
+
async (text) => {
|
|
611
|
+
if (!text.trim()) return;
|
|
612
|
+
await chatRef.current?.sendMessage({ text });
|
|
613
|
+
},
|
|
614
|
+
[]
|
|
615
|
+
);
|
|
616
|
+
const setMessages = (0, import_react2.useCallback)((messages) => {
|
|
617
|
+
chatRef.current?.setMessages?.(messages);
|
|
618
|
+
}, []);
|
|
619
|
+
const appendAssistantMessage = (0, import_react2.useCallback)((text) => {
|
|
620
|
+
const next = {
|
|
621
|
+
id: syntheticId("synthetic-assistant"),
|
|
622
|
+
role: "assistant",
|
|
623
|
+
parts: [{ type: "text", text }]
|
|
624
|
+
};
|
|
625
|
+
const current = chatRef.current;
|
|
626
|
+
if (!current) return;
|
|
627
|
+
const nextList = [...current.messages, next];
|
|
628
|
+
current.setMessages?.(nextList);
|
|
629
|
+
}, []);
|
|
630
|
+
const appendUserMessage = (0, import_react2.useCallback)((text) => {
|
|
631
|
+
const next = {
|
|
632
|
+
id: syntheticId("synthetic-user"),
|
|
633
|
+
role: "user",
|
|
634
|
+
parts: [{ type: "text", text }]
|
|
635
|
+
};
|
|
636
|
+
const current = chatRef.current;
|
|
637
|
+
if (!current) return;
|
|
638
|
+
const nextList = [...current.messages, next];
|
|
639
|
+
current.setMessages?.(nextList);
|
|
640
|
+
}, []);
|
|
641
|
+
const status = chat.status;
|
|
642
|
+
return {
|
|
643
|
+
messages: chat.messages,
|
|
644
|
+
sendMessage,
|
|
645
|
+
setMessages,
|
|
646
|
+
appendAssistantMessage,
|
|
647
|
+
appendUserMessage,
|
|
648
|
+
status,
|
|
649
|
+
conversationId: conversationId ?? null
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/native/useShogoChat.ts
|
|
654
|
+
function useShogoChat(opts = {}) {
|
|
655
|
+
return useChatConversation({
|
|
656
|
+
api: opts.api ?? "/api/chat/turn",
|
|
657
|
+
fetchCredentials: opts.fetchCredentials ?? "include",
|
|
658
|
+
...opts
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// src/native/OrganicSphere.tsx
|
|
663
|
+
var import_react4 = require("react");
|
|
664
|
+
var import_react_native4 = require("react-native");
|
|
665
|
+
var import_expo_gl = require("expo-gl");
|
|
666
|
+
var import_expo_three = require("expo-three");
|
|
667
|
+
var THREE = __toESM(require("three"), 1);
|
|
668
|
+
|
|
669
|
+
// src/react/shaders/organicSphereShaders.ts
|
|
670
|
+
var PERLIN_4D = (
|
|
671
|
+
/* glsl */
|
|
672
|
+
`
|
|
673
|
+
vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
|
|
674
|
+
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
|
|
675
|
+
vec4 fade(vec4 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);}
|
|
676
|
+
|
|
677
|
+
float perlin4d(vec4 P){
|
|
678
|
+
vec4 Pi0 = floor(P);
|
|
679
|
+
vec4 Pi1 = Pi0 + 1.0;
|
|
680
|
+
Pi0 = mod(Pi0, 289.0);
|
|
681
|
+
Pi1 = mod(Pi1, 289.0);
|
|
682
|
+
vec4 Pf0 = fract(P);
|
|
683
|
+
vec4 Pf1 = Pf0 - 1.0;
|
|
684
|
+
vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
|
|
685
|
+
vec4 iy = vec4(Pi0.yy, Pi1.yy);
|
|
686
|
+
vec4 iz0 = vec4(Pi0.zzzz);
|
|
687
|
+
vec4 iz1 = vec4(Pi1.zzzz);
|
|
688
|
+
vec4 iw0 = vec4(Pi0.wwww);
|
|
689
|
+
vec4 iw1 = vec4(Pi1.wwww);
|
|
690
|
+
|
|
691
|
+
vec4 ixy = permute(permute(ix) + iy);
|
|
692
|
+
vec4 ixy0 = permute(ixy + iz0);
|
|
693
|
+
vec4 ixy1 = permute(ixy + iz1);
|
|
694
|
+
vec4 ixy00 = permute(ixy0 + iw0);
|
|
695
|
+
vec4 ixy01 = permute(ixy0 + iw1);
|
|
696
|
+
vec4 ixy10 = permute(ixy1 + iw0);
|
|
697
|
+
vec4 ixy11 = permute(ixy1 + iw1);
|
|
698
|
+
|
|
699
|
+
vec4 gx00 = ixy00 / 7.0;
|
|
700
|
+
vec4 gy00 = floor(gx00) / 7.0;
|
|
701
|
+
vec4 gz00 = floor(gy00) / 6.0;
|
|
702
|
+
gx00 = fract(gx00) - 0.5;
|
|
703
|
+
gy00 = fract(gy00) - 0.5;
|
|
704
|
+
gz00 = fract(gz00) - 0.5;
|
|
705
|
+
vec4 gw00 = vec4(0.75) - abs(gx00) - abs(gy00) - abs(gz00);
|
|
706
|
+
vec4 sw00 = step(gw00, vec4(0.0));
|
|
707
|
+
gx00 -= sw00 * (step(0.0, gx00) - 0.5);
|
|
708
|
+
gy00 -= sw00 * (step(0.0, gy00) - 0.5);
|
|
709
|
+
|
|
710
|
+
vec4 gx01 = ixy01 / 7.0;
|
|
711
|
+
vec4 gy01 = floor(gx01) / 7.0;
|
|
712
|
+
vec4 gz01 = floor(gy01) / 6.0;
|
|
713
|
+
gx01 = fract(gx01) - 0.5;
|
|
714
|
+
gy01 = fract(gy01) - 0.5;
|
|
715
|
+
gz01 = fract(gz01) - 0.5;
|
|
716
|
+
vec4 gw01 = vec4(0.75) - abs(gx01) - abs(gy01) - abs(gz01);
|
|
717
|
+
vec4 sw01 = step(gw01, vec4(0.0));
|
|
718
|
+
gx01 -= sw01 * (step(0.0, gx01) - 0.5);
|
|
719
|
+
gy01 -= sw01 * (step(0.0, gy01) - 0.5);
|
|
720
|
+
|
|
721
|
+
vec4 gx10 = ixy10 / 7.0;
|
|
722
|
+
vec4 gy10 = floor(gx10) / 7.0;
|
|
723
|
+
vec4 gz10 = floor(gy10) / 6.0;
|
|
724
|
+
gx10 = fract(gx10) - 0.5;
|
|
725
|
+
gy10 = fract(gy10) - 0.5;
|
|
726
|
+
gz10 = fract(gz10) - 0.5;
|
|
727
|
+
vec4 gw10 = vec4(0.75) - abs(gx10) - abs(gy10) - abs(gz10);
|
|
728
|
+
vec4 sw10 = step(gw10, vec4(0.0));
|
|
729
|
+
gx10 -= sw10 * (step(0.0, gx10) - 0.5);
|
|
730
|
+
gy10 -= sw10 * (step(0.0, gy10) - 0.5);
|
|
731
|
+
|
|
732
|
+
vec4 gx11 = ixy11 / 7.0;
|
|
733
|
+
vec4 gy11 = floor(gx11) / 7.0;
|
|
734
|
+
vec4 gz11 = floor(gy11) / 6.0;
|
|
735
|
+
gx11 = fract(gx11) - 0.5;
|
|
736
|
+
gy11 = fract(gy11) - 0.5;
|
|
737
|
+
gz11 = fract(gz11) - 0.5;
|
|
738
|
+
vec4 gw11 = vec4(0.75) - abs(gx11) - abs(gy11) - abs(gz11);
|
|
739
|
+
vec4 sw11 = step(gw11, vec4(0.0));
|
|
740
|
+
gx11 -= sw11 * (step(0.0, gx11) - 0.5);
|
|
741
|
+
gy11 -= sw11 * (step(0.0, gy11) - 0.5);
|
|
742
|
+
|
|
743
|
+
vec4 g0000 = vec4(gx00.x,gy00.x,gz00.x,gw00.x);
|
|
744
|
+
vec4 g1000 = vec4(gx00.y,gy00.y,gz00.y,gw00.y);
|
|
745
|
+
vec4 g0100 = vec4(gx00.z,gy00.z,gz00.z,gw00.z);
|
|
746
|
+
vec4 g1100 = vec4(gx00.w,gy00.w,gz00.w,gw00.w);
|
|
747
|
+
vec4 g0010 = vec4(gx10.x,gy10.x,gz10.x,gw10.x);
|
|
748
|
+
vec4 g1010 = vec4(gx10.y,gy10.y,gz10.y,gw10.y);
|
|
749
|
+
vec4 g0110 = vec4(gx10.z,gy10.z,gz10.z,gw10.z);
|
|
750
|
+
vec4 g1110 = vec4(gx10.w,gy10.w,gz10.w,gw10.w);
|
|
751
|
+
vec4 g0001 = vec4(gx01.x,gy01.x,gz01.x,gw01.x);
|
|
752
|
+
vec4 g1001 = vec4(gx01.y,gy01.y,gz01.y,gw01.y);
|
|
753
|
+
vec4 g0101 = vec4(gx01.z,gy01.z,gz01.z,gw01.z);
|
|
754
|
+
vec4 g1101 = vec4(gx01.w,gy01.w,gz01.w,gw01.w);
|
|
755
|
+
vec4 g0011 = vec4(gx11.x,gy11.x,gz11.x,gw11.x);
|
|
756
|
+
vec4 g1011 = vec4(gx11.y,gy11.y,gz11.y,gw11.y);
|
|
757
|
+
vec4 g0111 = vec4(gx11.z,gy11.z,gz11.z,gw11.z);
|
|
758
|
+
vec4 g1111 = vec4(gx11.w,gy11.w,gz11.w,gw11.w);
|
|
759
|
+
|
|
760
|
+
vec4 norm00 = taylorInvSqrt(vec4(dot(g0000, g0000), dot(g0100, g0100), dot(g1000, g1000), dot(g1100, g1100)));
|
|
761
|
+
g0000 *= norm00.x; g0100 *= norm00.y; g1000 *= norm00.z; g1100 *= norm00.w;
|
|
762
|
+
|
|
763
|
+
vec4 norm01 = taylorInvSqrt(vec4(dot(g0001, g0001), dot(g0101, g0101), dot(g1001, g1001), dot(g1101, g1101)));
|
|
764
|
+
g0001 *= norm01.x; g0101 *= norm01.y; g1001 *= norm01.z; g1101 *= norm01.w;
|
|
765
|
+
|
|
766
|
+
vec4 norm10 = taylorInvSqrt(vec4(dot(g0010, g0010), dot(g0110, g0110), dot(g1010, g1010), dot(g1110, g1110)));
|
|
767
|
+
g0010 *= norm10.x; g0110 *= norm10.y; g1010 *= norm10.z; g1110 *= norm10.w;
|
|
768
|
+
|
|
769
|
+
vec4 norm11 = taylorInvSqrt(vec4(dot(g0011, g0011), dot(g0111, g0111), dot(g1011, g1011), dot(g1111, g1111)));
|
|
770
|
+
g0011 *= norm11.x; g0111 *= norm11.y; g1011 *= norm11.z; g1111 *= norm11.w;
|
|
771
|
+
|
|
772
|
+
float n0000 = dot(g0000, Pf0);
|
|
773
|
+
float n1000 = dot(g1000, vec4(Pf1.x, Pf0.yzw));
|
|
774
|
+
float n0100 = dot(g0100, vec4(Pf0.x, Pf1.y, Pf0.zw));
|
|
775
|
+
float n1100 = dot(g1100, vec4(Pf1.xy, Pf0.zw));
|
|
776
|
+
float n0010 = dot(g0010, vec4(Pf0.xy, Pf1.z, Pf0.w));
|
|
777
|
+
float n1010 = dot(g1010, vec4(Pf1.x, Pf0.y, Pf1.z, Pf0.w));
|
|
778
|
+
float n0110 = dot(g0110, vec4(Pf0.x, Pf1.yz, Pf0.w));
|
|
779
|
+
float n1110 = dot(g1110, vec4(Pf1.xyz, Pf0.w));
|
|
780
|
+
float n0001 = dot(g0001, vec4(Pf0.xyz, Pf1.w));
|
|
781
|
+
float n1001 = dot(g1001, vec4(Pf1.x, Pf0.yz, Pf1.w));
|
|
782
|
+
float n0101 = dot(g0101, vec4(Pf0.x, Pf1.y, Pf0.z, Pf1.w));
|
|
783
|
+
float n1101 = dot(g1101, vec4(Pf1.xy, Pf0.z, Pf1.w));
|
|
784
|
+
float n0011 = dot(g0011, vec4(Pf0.xy, Pf1.zw));
|
|
785
|
+
float n1011 = dot(g1011, vec4(Pf1.x, Pf0.y, Pf1.zw));
|
|
786
|
+
float n0111 = dot(g0111, vec4(Pf0.x, Pf1.yzw));
|
|
787
|
+
float n1111 = dot(g1111, Pf1);
|
|
788
|
+
|
|
789
|
+
vec4 fade_xyzw = fade(Pf0);
|
|
790
|
+
vec4 n_0w = mix(vec4(n0000, n1000, n0100, n1100), vec4(n0001, n1001, n0101, n1101), fade_xyzw.w);
|
|
791
|
+
vec4 n_1w = mix(vec4(n0010, n1010, n0110, n1110), vec4(n0011, n1011, n0111, n1111), fade_xyzw.w);
|
|
792
|
+
vec4 n_zw = mix(n_0w, n_1w, fade_xyzw.z);
|
|
793
|
+
vec2 n_yzw = mix(n_zw.xy, n_zw.zw, fade_xyzw.y);
|
|
794
|
+
float n_xyzw = mix(n_yzw.x, n_yzw.y, fade_xyzw.x);
|
|
795
|
+
return 2.2 * n_xyzw;
|
|
796
|
+
}
|
|
797
|
+
`
|
|
798
|
+
);
|
|
799
|
+
var ORGANIC_SPHERE_VERTEX_SHADER = (
|
|
800
|
+
/* glsl */
|
|
801
|
+
`
|
|
802
|
+
#define M_PI 3.1415926535897932384626433832795
|
|
803
|
+
|
|
804
|
+
uniform vec3 uLightAColor;
|
|
805
|
+
uniform vec3 uLightAPosition;
|
|
806
|
+
uniform float uLightAIntensity;
|
|
807
|
+
uniform vec3 uLightBColor;
|
|
808
|
+
uniform vec3 uLightBPosition;
|
|
809
|
+
uniform float uLightBIntensity;
|
|
810
|
+
|
|
811
|
+
uniform vec2 uSubdivision;
|
|
812
|
+
|
|
813
|
+
uniform vec3 uOffset;
|
|
814
|
+
|
|
815
|
+
uniform float uDistortionFrequency;
|
|
816
|
+
uniform float uDistortionStrength;
|
|
817
|
+
uniform float uDisplacementFrequency;
|
|
818
|
+
uniform float uDisplacementStrength;
|
|
819
|
+
|
|
820
|
+
uniform float uFresnelOffset;
|
|
821
|
+
uniform float uFresnelMultiplier;
|
|
822
|
+
uniform float uFresnelPower;
|
|
823
|
+
|
|
824
|
+
uniform vec3 uRimColor;
|
|
825
|
+
uniform float uRimThreshold;
|
|
826
|
+
uniform float uRimPower;
|
|
827
|
+
|
|
828
|
+
uniform float uTime;
|
|
829
|
+
|
|
830
|
+
varying vec3 vColor;
|
|
831
|
+
|
|
832
|
+
${PERLIN_4D}
|
|
833
|
+
|
|
834
|
+
vec3 getDisplacedPosition(vec3 _position)
|
|
835
|
+
{
|
|
836
|
+
vec3 distoredPosition = _position;
|
|
837
|
+
distoredPosition += perlin4d(vec4(distoredPosition * uDistortionFrequency + uOffset, uTime)) * uDistortionStrength;
|
|
838
|
+
|
|
839
|
+
float perlinStrength = perlin4d(vec4(distoredPosition * uDisplacementFrequency + uOffset, uTime));
|
|
840
|
+
|
|
841
|
+
vec3 displacedPosition = _position;
|
|
842
|
+
displacedPosition += normalize(_position) * perlinStrength * uDisplacementStrength;
|
|
843
|
+
|
|
844
|
+
return displacedPosition;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
void main()
|
|
848
|
+
{
|
|
849
|
+
vec3 displacedPosition = getDisplacedPosition(position);
|
|
850
|
+
vec4 viewPosition = viewMatrix * vec4(displacedPosition, 1.0);
|
|
851
|
+
gl_Position = projectionMatrix * viewPosition;
|
|
852
|
+
|
|
853
|
+
float distanceA = (M_PI * 2.0) / uSubdivision.x;
|
|
854
|
+
float distanceB = M_PI / uSubdivision.x;
|
|
855
|
+
|
|
856
|
+
vec3 biTangent = cross(normal, tangent.xyz);
|
|
857
|
+
|
|
858
|
+
vec3 positionA = position + tangent.xyz * distanceA;
|
|
859
|
+
vec3 displacedPositionA = getDisplacedPosition(positionA);
|
|
860
|
+
|
|
861
|
+
vec3 positionB = position + biTangent.xyz * distanceB;
|
|
862
|
+
vec3 displacedPositionB = getDisplacedPosition(positionB);
|
|
863
|
+
|
|
864
|
+
vec3 computedNormal = cross(displacedPositionA - displacedPosition.xyz, displacedPositionB - displacedPosition.xyz);
|
|
865
|
+
computedNormal = normalize(computedNormal);
|
|
866
|
+
|
|
867
|
+
vec3 viewDirection = normalize(displacedPosition.xyz - cameraPosition);
|
|
868
|
+
float fresnel = uFresnelOffset + (1.0 + dot(viewDirection, computedNormal)) * uFresnelMultiplier;
|
|
869
|
+
fresnel = pow(max(0.0, fresnel), uFresnelPower);
|
|
870
|
+
|
|
871
|
+
float lightAIntensity = max(0.0, - dot(computedNormal.xyz, normalize(- uLightAPosition))) * uLightAIntensity;
|
|
872
|
+
float lightBIntensity = max(0.0, - dot(computedNormal.xyz, normalize(- uLightBPosition))) * uLightBIntensity;
|
|
873
|
+
|
|
874
|
+
// Clamp mix factors to [0, 1] so a high lightIntensity*fresnel (common on
|
|
875
|
+
// displacement peaks and loud audio) cannot extrapolate past the orange
|
|
876
|
+
// light color and saturate R, producing yellow-white hotspots.
|
|
877
|
+
vec3 color = vec3(0.0);
|
|
878
|
+
color = mix(color, uLightAColor, clamp(lightAIntensity * fresnel, 0.0, 1.0));
|
|
879
|
+
color = mix(color, uLightBColor, clamp(lightBIntensity * fresnel, 0.0, 1.0));
|
|
880
|
+
color = mix(color, uRimColor, clamp(pow(max(0.0, fresnel - uRimThreshold), uRimPower), 0.0, 1.0));
|
|
881
|
+
|
|
882
|
+
vColor = color;
|
|
883
|
+
}
|
|
884
|
+
`
|
|
885
|
+
);
|
|
886
|
+
var ORGANIC_SPHERE_FRAGMENT_SHADER = (
|
|
887
|
+
/* glsl */
|
|
888
|
+
`
|
|
889
|
+
varying vec3 vColor;
|
|
890
|
+
|
|
891
|
+
void main()
|
|
892
|
+
{
|
|
893
|
+
gl_FragColor = vec4(vColor, 1.0);
|
|
894
|
+
}
|
|
895
|
+
`
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
// src/react/sphereConfig.ts
|
|
899
|
+
var DEFAULT_ORGANIC_SPHERE_CONFIG = {
|
|
900
|
+
// Lighting & rim
|
|
901
|
+
lightAColor: "#c2410c",
|
|
902
|
+
lightBColor: "#fb923c",
|
|
903
|
+
lightAIntensity: 2.13,
|
|
904
|
+
lightBIntensity: 1.61,
|
|
905
|
+
rimColor: null,
|
|
906
|
+
rimThreshold: 0.92,
|
|
907
|
+
rimPower: 5,
|
|
908
|
+
// Static shape
|
|
909
|
+
distortionFrequency: 1.5,
|
|
910
|
+
displacementFrequency: 2.12,
|
|
911
|
+
fresnelOffset: -1.609,
|
|
912
|
+
fresnelPower: 1.793,
|
|
913
|
+
// Audio reactivity
|
|
914
|
+
volume: {
|
|
915
|
+
idle: 0.152,
|
|
916
|
+
gain: 0.3,
|
|
917
|
+
floor: 0,
|
|
918
|
+
attack: 0.03,
|
|
919
|
+
decay: 2e-3
|
|
920
|
+
},
|
|
921
|
+
medium: {
|
|
922
|
+
idle: 3.587,
|
|
923
|
+
gain: 2,
|
|
924
|
+
floor: 3.587,
|
|
925
|
+
attack: 8e-3,
|
|
926
|
+
decay: 4e-3
|
|
927
|
+
},
|
|
928
|
+
high: {
|
|
929
|
+
idle: 0.65,
|
|
930
|
+
gain: 5,
|
|
931
|
+
floor: 0.5,
|
|
932
|
+
attack: 0.02,
|
|
933
|
+
decay: 1e-3
|
|
934
|
+
},
|
|
935
|
+
low: {
|
|
936
|
+
idle: 3e-4,
|
|
937
|
+
gain: 3e-3,
|
|
938
|
+
floor: 1e-4,
|
|
939
|
+
attack: 5e-3,
|
|
940
|
+
decay: 2e-3
|
|
941
|
+
},
|
|
942
|
+
// Camera
|
|
943
|
+
cameraZ: 3.84,
|
|
944
|
+
fov: 35,
|
|
945
|
+
// Determinism
|
|
946
|
+
seed: 0,
|
|
947
|
+
// Init-only
|
|
948
|
+
subdivisions: 512,
|
|
949
|
+
maxPixelRatio: null,
|
|
950
|
+
backgroundColor: null
|
|
951
|
+
};
|
|
952
|
+
function resolveOrganicSphereConfig(partial) {
|
|
953
|
+
const base = DEFAULT_ORGANIC_SPHERE_CONFIG;
|
|
954
|
+
if (!partial) return { ...base, volume: { ...base.volume }, medium: { ...base.medium }, high: { ...base.high }, low: { ...base.low } };
|
|
955
|
+
return {
|
|
956
|
+
...base,
|
|
957
|
+
...partial,
|
|
958
|
+
volume: { ...base.volume, ...partial.volume ?? {} },
|
|
959
|
+
medium: { ...base.medium, ...partial.medium ?? {} },
|
|
960
|
+
high: { ...base.high, ...partial.high ?? {} },
|
|
961
|
+
low: { ...base.low, ...partial.low ?? {} }
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/native/OrganicSphere.tsx
|
|
966
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
967
|
+
function seededUnit(seed, salt) {
|
|
968
|
+
const s = Math.sin((seed + salt * 7919) * 12.9898 + salt * 78.233) * 43758.5453;
|
|
969
|
+
return s - Math.floor(s);
|
|
970
|
+
}
|
|
971
|
+
function OrganicSphere({
|
|
972
|
+
getFrequencyData,
|
|
973
|
+
active = true,
|
|
974
|
+
config: configProp,
|
|
975
|
+
style
|
|
976
|
+
}) {
|
|
977
|
+
const resolvedConfig = (0, import_react4.useMemo)(
|
|
978
|
+
() => resolveOrganicSphereConfig(configProp),
|
|
979
|
+
[configProp]
|
|
980
|
+
);
|
|
981
|
+
const configRef = (0, import_react4.useRef)(resolvedConfig);
|
|
982
|
+
(0, import_react4.useEffect)(() => {
|
|
983
|
+
configRef.current = resolvedConfig;
|
|
984
|
+
}, [resolvedConfig]);
|
|
985
|
+
const getFrequencyDataRef = (0, import_react4.useRef)(getFrequencyData);
|
|
986
|
+
const activeRef = (0, import_react4.useRef)(active);
|
|
987
|
+
(0, import_react4.useEffect)(() => {
|
|
988
|
+
getFrequencyDataRef.current = getFrequencyData;
|
|
989
|
+
}, [getFrequencyData]);
|
|
990
|
+
(0, import_react4.useEffect)(() => {
|
|
991
|
+
activeRef.current = active;
|
|
992
|
+
}, [active]);
|
|
993
|
+
const disposeRef = (0, import_react4.useRef)(null);
|
|
994
|
+
(0, import_react4.useEffect)(() => {
|
|
995
|
+
return () => {
|
|
996
|
+
disposeRef.current?.();
|
|
997
|
+
disposeRef.current = null;
|
|
998
|
+
};
|
|
999
|
+
}, []);
|
|
1000
|
+
const initSubdivisions = resolvedConfig.subdivisions;
|
|
1001
|
+
const initMaxPixelRatio = resolvedConfig.maxPixelRatio;
|
|
1002
|
+
const initBackgroundColor = resolvedConfig.backgroundColor;
|
|
1003
|
+
const onContextCreate = (0, import_react4.useMemo)(
|
|
1004
|
+
() => async (gl) => {
|
|
1005
|
+
const cfg0 = configRef.current;
|
|
1006
|
+
const width = gl.drawingBufferWidth;
|
|
1007
|
+
const height = gl.drawingBufferHeight;
|
|
1008
|
+
const pixelRatio = Math.min(initMaxPixelRatio ?? import_react_native4.PixelRatio.get(), 2);
|
|
1009
|
+
const scene = new THREE.Scene();
|
|
1010
|
+
if (initBackgroundColor) scene.background = new THREE.Color(initBackgroundColor);
|
|
1011
|
+
const camera = new THREE.PerspectiveCamera(
|
|
1012
|
+
cfg0.fov,
|
|
1013
|
+
width / height || 1,
|
|
1014
|
+
0.1,
|
|
1015
|
+
100
|
|
1016
|
+
);
|
|
1017
|
+
camera.position.set(0, 0, cfg0.cameraZ);
|
|
1018
|
+
scene.add(camera);
|
|
1019
|
+
const renderer = new import_expo_three.Renderer({
|
|
1020
|
+
gl,
|
|
1021
|
+
antialias: true,
|
|
1022
|
+
alpha: initBackgroundColor === null,
|
|
1023
|
+
powerPreference: "high-performance"
|
|
1024
|
+
});
|
|
1025
|
+
renderer.setSize(width / pixelRatio, height / pixelRatio, false);
|
|
1026
|
+
renderer.setPixelRatio(pixelRatio);
|
|
1027
|
+
renderer.setClearColor(0, initBackgroundColor === null ? 0 : 1);
|
|
1028
|
+
const geometry = new THREE.SphereGeometry(1, initSubdivisions, initSubdivisions);
|
|
1029
|
+
geometry.computeTangents();
|
|
1030
|
+
const lightAPos = new THREE.Vector3().setFromSpherical(
|
|
1031
|
+
new THREE.Spherical(1, 0.615, 2.049)
|
|
1032
|
+
);
|
|
1033
|
+
const lightBPos = new THREE.Vector3().setFromSpherical(
|
|
1034
|
+
new THREE.Spherical(1, 2.561, -1.844)
|
|
1035
|
+
);
|
|
1036
|
+
const lightAColorObj = new THREE.Color(cfg0.lightAColor);
|
|
1037
|
+
const lightBColorObj = new THREE.Color(cfg0.lightBColor);
|
|
1038
|
+
const rimColorObj = new THREE.Color(cfg0.rimColor ?? cfg0.lightBColor);
|
|
1039
|
+
const material = new THREE.ShaderMaterial({
|
|
1040
|
+
uniforms: {
|
|
1041
|
+
uLightAColor: { value: lightAColorObj },
|
|
1042
|
+
uLightAPosition: { value: lightAPos },
|
|
1043
|
+
uLightAIntensity: { value: cfg0.lightAIntensity },
|
|
1044
|
+
uLightBColor: { value: lightBColorObj },
|
|
1045
|
+
uLightBPosition: { value: lightBPos },
|
|
1046
|
+
uLightBIntensity: { value: cfg0.lightBIntensity },
|
|
1047
|
+
uSubdivision: {
|
|
1048
|
+
value: new THREE.Vector2(initSubdivisions, initSubdivisions)
|
|
1049
|
+
},
|
|
1050
|
+
uOffset: { value: new THREE.Vector3() },
|
|
1051
|
+
uDistortionFrequency: { value: cfg0.distortionFrequency },
|
|
1052
|
+
uDistortionStrength: { value: cfg0.high.idle },
|
|
1053
|
+
uDisplacementFrequency: { value: cfg0.displacementFrequency },
|
|
1054
|
+
uDisplacementStrength: { value: cfg0.volume.idle },
|
|
1055
|
+
uFresnelOffset: { value: cfg0.fresnelOffset },
|
|
1056
|
+
uFresnelMultiplier: { value: cfg0.medium.idle },
|
|
1057
|
+
uFresnelPower: { value: cfg0.fresnelPower },
|
|
1058
|
+
uRimColor: { value: rimColorObj },
|
|
1059
|
+
uRimThreshold: { value: cfg0.rimThreshold },
|
|
1060
|
+
uRimPower: { value: cfg0.rimPower },
|
|
1061
|
+
uTime: { value: 0 }
|
|
1062
|
+
},
|
|
1063
|
+
defines: { USE_TANGENT: "" },
|
|
1064
|
+
vertexShader: ORGANIC_SPHERE_VERTEX_SHADER,
|
|
1065
|
+
fragmentShader: ORGANIC_SPHERE_FRAGMENT_SHADER
|
|
1066
|
+
});
|
|
1067
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
1068
|
+
scene.add(mesh);
|
|
1069
|
+
const seedInitialPhase = (seed) => ({
|
|
1070
|
+
phi: seededUnit(seed, 1) * Math.PI,
|
|
1071
|
+
theta: seededUnit(seed, 2) * Math.PI * 2
|
|
1072
|
+
});
|
|
1073
|
+
const initialPhase = seedInitialPhase(cfg0.seed);
|
|
1074
|
+
const offsetSpherical = new THREE.Spherical(1, initialPhase.phi, initialPhase.theta);
|
|
1075
|
+
const offsetDirection = new THREE.Vector3().setFromSpherical(offsetSpherical);
|
|
1076
|
+
const variations = {
|
|
1077
|
+
volume: { target: 0, current: cfg0.volume.idle },
|
|
1078
|
+
medium: { target: 0, current: cfg0.medium.idle },
|
|
1079
|
+
high: { target: 0, current: cfg0.high.idle },
|
|
1080
|
+
low: { target: 0, current: cfg0.low.idle }
|
|
1081
|
+
};
|
|
1082
|
+
const resetAnimationState = (cfg) => {
|
|
1083
|
+
material.uniforms.uTime.value = 0;
|
|
1084
|
+
material.uniforms.uOffset.value.set(0, 0, 0);
|
|
1085
|
+
const phase = seedInitialPhase(cfg.seed);
|
|
1086
|
+
offsetSpherical.radius = 1;
|
|
1087
|
+
offsetSpherical.phi = phase.phi;
|
|
1088
|
+
offsetSpherical.theta = phase.theta;
|
|
1089
|
+
variations.volume.current = cfg.volume.idle;
|
|
1090
|
+
variations.medium.current = cfg.medium.idle;
|
|
1091
|
+
variations.high.current = cfg.high.idle;
|
|
1092
|
+
variations.low.current = cfg.low.idle;
|
|
1093
|
+
variations.volume.target = cfg.volume.idle;
|
|
1094
|
+
variations.medium.target = cfg.medium.idle;
|
|
1095
|
+
variations.high.target = cfg.high.idle;
|
|
1096
|
+
variations.low.target = cfg.low.idle;
|
|
1097
|
+
};
|
|
1098
|
+
const LEVEL_COUNT = 8;
|
|
1099
|
+
function computeLevels(buf) {
|
|
1100
|
+
const bins = Math.floor(buf.length / LEVEL_COUNT);
|
|
1101
|
+
const levels = new Array(LEVEL_COUNT).fill(0);
|
|
1102
|
+
if (bins <= 0) return levels;
|
|
1103
|
+
for (let i = 0; i < LEVEL_COUNT; i++) {
|
|
1104
|
+
let sum = 0;
|
|
1105
|
+
for (let j = 0; j < bins; j++) sum += buf[i * bins + j];
|
|
1106
|
+
levels[i] = sum / bins / 256;
|
|
1107
|
+
}
|
|
1108
|
+
return levels;
|
|
1109
|
+
}
|
|
1110
|
+
function bandTarget(reactivity, levels, sourceLevel) {
|
|
1111
|
+
if (!levels) return reactivity.idle;
|
|
1112
|
+
return Math.max(
|
|
1113
|
+
reactivity.floor,
|
|
1114
|
+
reactivity.floor + sourceLevel * reactivity.gain
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
let rafId = 0;
|
|
1118
|
+
let lastTs = Date.now();
|
|
1119
|
+
let disposed = false;
|
|
1120
|
+
let wasActive = activeRef.current;
|
|
1121
|
+
let lastSeed = cfg0.seed;
|
|
1122
|
+
const render = () => {
|
|
1123
|
+
if (disposed) return;
|
|
1124
|
+
const now = Date.now();
|
|
1125
|
+
const delta = Math.min(now - lastTs, 100);
|
|
1126
|
+
lastTs = now;
|
|
1127
|
+
const cfg = configRef.current;
|
|
1128
|
+
lightAColorObj.set(cfg.lightAColor);
|
|
1129
|
+
lightBColorObj.set(cfg.lightBColor);
|
|
1130
|
+
rimColorObj.set(cfg.rimColor ?? cfg.lightBColor);
|
|
1131
|
+
material.uniforms.uLightAIntensity.value = cfg.lightAIntensity;
|
|
1132
|
+
material.uniforms.uLightBIntensity.value = cfg.lightBIntensity;
|
|
1133
|
+
material.uniforms.uDistortionFrequency.value = cfg.distortionFrequency;
|
|
1134
|
+
material.uniforms.uDisplacementFrequency.value = cfg.displacementFrequency;
|
|
1135
|
+
material.uniforms.uFresnelOffset.value = cfg.fresnelOffset;
|
|
1136
|
+
material.uniforms.uFresnelPower.value = cfg.fresnelPower;
|
|
1137
|
+
material.uniforms.uRimThreshold.value = cfg.rimThreshold;
|
|
1138
|
+
material.uniforms.uRimPower.value = cfg.rimPower;
|
|
1139
|
+
if (camera.position.z !== cfg.cameraZ) camera.position.z = cfg.cameraZ;
|
|
1140
|
+
if (camera.fov !== cfg.fov) {
|
|
1141
|
+
camera.fov = cfg.fov;
|
|
1142
|
+
camera.updateProjectionMatrix();
|
|
1143
|
+
}
|
|
1144
|
+
const isActive = activeRef.current;
|
|
1145
|
+
if (isActive && !wasActive || cfg.seed !== lastSeed) {
|
|
1146
|
+
resetAnimationState(cfg);
|
|
1147
|
+
lastSeed = cfg.seed;
|
|
1148
|
+
}
|
|
1149
|
+
wasActive = isActive;
|
|
1150
|
+
if (isActive) {
|
|
1151
|
+
let levels = null;
|
|
1152
|
+
if (getFrequencyDataRef.current) {
|
|
1153
|
+
try {
|
|
1154
|
+
const buf = getFrequencyDataRef.current();
|
|
1155
|
+
if (buf && buf.length > 0) levels = computeLevels(buf);
|
|
1156
|
+
} catch {
|
|
1157
|
+
levels = null;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const l0 = levels ? levels[0] || 0 : 0;
|
|
1161
|
+
const l1 = levels ? levels[1] || 0 : 0;
|
|
1162
|
+
const l2 = levels ? levels[2] || 0 : 0;
|
|
1163
|
+
const loudest = Math.max(l0, l1, l2);
|
|
1164
|
+
const update = (variation, reactivity, sourceLevel) => {
|
|
1165
|
+
variation.target = bandTarget(reactivity, levels, sourceLevel);
|
|
1166
|
+
const easing = variation.target > variation.current ? reactivity.attack : reactivity.decay;
|
|
1167
|
+
variation.current += (variation.target - variation.current) * easing * delta;
|
|
1168
|
+
};
|
|
1169
|
+
update(variations.volume, cfg.volume, loudest);
|
|
1170
|
+
update(variations.medium, cfg.medium, l1);
|
|
1171
|
+
update(variations.high, cfg.high, l2);
|
|
1172
|
+
update(variations.low, cfg.low, l0);
|
|
1173
|
+
const timeFrequency = variations.low.current;
|
|
1174
|
+
const elapsed = delta * timeFrequency;
|
|
1175
|
+
material.uniforms.uDisplacementStrength.value = variations.volume.current;
|
|
1176
|
+
material.uniforms.uDistortionStrength.value = variations.high.current;
|
|
1177
|
+
material.uniforms.uFresnelMultiplier.value = variations.medium.current;
|
|
1178
|
+
const offsetTime = (material.uniforms.uTime.value + elapsed) * 0.3;
|
|
1179
|
+
offsetSpherical.phi = (Math.sin(offsetTime * 1e-3) * Math.sin(offsetTime * 321e-5) * 0.5 + 0.5) * Math.PI;
|
|
1180
|
+
offsetSpherical.theta = (Math.sin(offsetTime * 1e-4) * Math.sin(offsetTime * 321e-6) * 0.5 + 0.5) * Math.PI * 2;
|
|
1181
|
+
offsetDirection.setFromSpherical(offsetSpherical);
|
|
1182
|
+
offsetDirection.multiplyScalar(timeFrequency * 2);
|
|
1183
|
+
material.uniforms.uOffset.value.add(offsetDirection);
|
|
1184
|
+
material.uniforms.uTime.value += elapsed;
|
|
1185
|
+
}
|
|
1186
|
+
renderer.render(scene, camera);
|
|
1187
|
+
gl.endFrameEXP();
|
|
1188
|
+
rafId = requestAnimationFrame(render);
|
|
1189
|
+
};
|
|
1190
|
+
rafId = requestAnimationFrame(render);
|
|
1191
|
+
const dispose = () => {
|
|
1192
|
+
if (disposed) return;
|
|
1193
|
+
disposed = true;
|
|
1194
|
+
cancelAnimationFrame(rafId);
|
|
1195
|
+
geometry.dispose();
|
|
1196
|
+
material.dispose();
|
|
1197
|
+
renderer.dispose();
|
|
1198
|
+
};
|
|
1199
|
+
disposeRef.current = dispose;
|
|
1200
|
+
},
|
|
1201
|
+
[initSubdivisions, initMaxPixelRatio, initBackgroundColor]
|
|
1202
|
+
);
|
|
1203
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1204
|
+
import_expo_gl.GLView,
|
|
1205
|
+
{
|
|
1206
|
+
style,
|
|
1207
|
+
onContextCreate
|
|
1208
|
+
},
|
|
1209
|
+
`${initSubdivisions}-${initMaxPixelRatio ?? "auto"}-${initBackgroundColor ?? "transparent"}`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// src/native/OrganicParticles.tsx
|
|
1214
|
+
var import_react5 = require("react");
|
|
1215
|
+
var import_react_native5 = require("react-native");
|
|
1216
|
+
var import_expo_gl2 = require("expo-gl");
|
|
1217
|
+
var import_expo_three2 = require("expo-three");
|
|
1218
|
+
var THREE2 = __toESM(require("three"), 1);
|
|
1219
|
+
|
|
1220
|
+
// src/react/shaders/organicParticlesShaders.ts
|
|
1221
|
+
var ORGANIC_PARTICLES_VERTEX_SHADER = (
|
|
1222
|
+
/* glsl */
|
|
1223
|
+
`
|
|
1224
|
+
${PERLIN_4D}
|
|
1225
|
+
|
|
1226
|
+
attribute vec3 aBasePosition;
|
|
1227
|
+
attribute float aSeed;
|
|
1228
|
+
|
|
1229
|
+
uniform float uTime;
|
|
1230
|
+
uniform float uNoiseFrequency;
|
|
1231
|
+
uniform float uNoiseStrength;
|
|
1232
|
+
uniform float uDisplacement;
|
|
1233
|
+
uniform float uSwirl;
|
|
1234
|
+
uniform float uSwirlTimeRate;
|
|
1235
|
+
uniform float uSizeBase;
|
|
1236
|
+
uniform float uSizeAudio;
|
|
1237
|
+
uniform float uPixelRatio;
|
|
1238
|
+
|
|
1239
|
+
varying float vHeat;
|
|
1240
|
+
varying float vSeed;
|
|
1241
|
+
|
|
1242
|
+
void main() {
|
|
1243
|
+
vec3 pos = aBasePosition;
|
|
1244
|
+
|
|
1245
|
+
// 4D Perlin sampled in particle space. The 4th axis is time so the
|
|
1246
|
+
// cloud evolves over time without tiling.
|
|
1247
|
+
float n = perlin4d(vec4(pos * uNoiseFrequency, uTime));
|
|
1248
|
+
|
|
1249
|
+
// Per-particle phase so the cloud doesn't breathe in lockstep.
|
|
1250
|
+
float phaseShift = (aSeed - 0.5) * 0.5;
|
|
1251
|
+
float breath = sin(uTime * 0.0008 + aSeed * 6.2831) * 0.5 + 0.5;
|
|
1252
|
+
|
|
1253
|
+
// Outward push: noise + audio-driven displacement, scaled by seed so
|
|
1254
|
+
// some particles dart further than others on the same beat.
|
|
1255
|
+
vec3 radial = normalize(pos);
|
|
1256
|
+
float push = n * uNoiseStrength + uDisplacement * (0.6 + aSeed * 0.8) + breath * 0.05;
|
|
1257
|
+
pos += radial * push;
|
|
1258
|
+
|
|
1259
|
+
// Twist around Y. Amount scales with height so the cloud spirals
|
|
1260
|
+
// rather than rigid-rotating. uSwirlTimeRate is 0 by default so the
|
|
1261
|
+
// swirl is driven by audio (uSwirl) alone; crank it for a constant
|
|
1262
|
+
// drift.
|
|
1263
|
+
float swirlAngle = pos.y * uSwirl + uTime * uSwirlTimeRate + phaseShift;
|
|
1264
|
+
float c = cos(swirlAngle);
|
|
1265
|
+
float s = sin(swirlAngle);
|
|
1266
|
+
pos.xz = mat2(c, -s, s, c) * pos.xz;
|
|
1267
|
+
|
|
1268
|
+
vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
|
|
1269
|
+
gl_Position = projectionMatrix * mvPos;
|
|
1270
|
+
|
|
1271
|
+
// Point size: base + audio boost, gently perspective-attenuated. At
|
|
1272
|
+
// the default camera (z ~= 3.84) a uSizeBase of 1 renders roughly one
|
|
1273
|
+
// device pixel \u2014 intentionally tiny so 40k+ additively-blended points
|
|
1274
|
+
// read as fine dust rather than a solid blob.
|
|
1275
|
+
float size = uSizeBase + uSizeAudio * (0.5 + aSeed);
|
|
1276
|
+
gl_PointSize = max(1.0, size * uPixelRatio * (2.0 / max(0.1, -mvPos.z)));
|
|
1277
|
+
|
|
1278
|
+
// Heat: 0 at core, 1 at fringe. Used by the fragment to lerp color.
|
|
1279
|
+
vHeat = clamp((length(pos) - 0.4) / 1.4, 0.0, 1.0);
|
|
1280
|
+
vSeed = aSeed;
|
|
1281
|
+
}
|
|
1282
|
+
`
|
|
1283
|
+
);
|
|
1284
|
+
var ORGANIC_PARTICLES_FRAGMENT_SHADER = (
|
|
1285
|
+
/* glsl */
|
|
1286
|
+
`
|
|
1287
|
+
precision mediump float;
|
|
1288
|
+
|
|
1289
|
+
uniform vec3 uColorCool;
|
|
1290
|
+
uniform vec3 uColorHot;
|
|
1291
|
+
uniform float uOpacity;
|
|
1292
|
+
uniform float uSoftness;
|
|
1293
|
+
|
|
1294
|
+
varying float vHeat;
|
|
1295
|
+
varying float vSeed;
|
|
1296
|
+
|
|
1297
|
+
void main() {
|
|
1298
|
+
vec2 centered = gl_PointCoord - vec2(0.5);
|
|
1299
|
+
float distSq = dot(centered, centered);
|
|
1300
|
+
if (distSq > 0.25) discard;
|
|
1301
|
+
|
|
1302
|
+
// Soft circular falloff. uSoftness in [0.5, 1] controls how hard the
|
|
1303
|
+
// edge is: 0.5 = sharp disc, 1 = feathered blob.
|
|
1304
|
+
float innerR = 0.25 * (1.0 - uSoftness);
|
|
1305
|
+
float alpha = smoothstep(0.25, innerR, distSq);
|
|
1306
|
+
|
|
1307
|
+
// Warm/cool gradient, with a tiny seed-based tint wobble so the cloud
|
|
1308
|
+
// doesn't look banded.
|
|
1309
|
+
float wobble = (vSeed - 0.5) * 0.15;
|
|
1310
|
+
vec3 color = mix(uColorCool, uColorHot, clamp(vHeat + wobble, 0.0, 1.0));
|
|
1311
|
+
|
|
1312
|
+
gl_FragColor = vec4(color, alpha * uOpacity);
|
|
1313
|
+
}
|
|
1314
|
+
`
|
|
1315
|
+
);
|
|
1316
|
+
|
|
1317
|
+
// src/react/particlesConfig.ts
|
|
1318
|
+
var DEFAULT_ORGANIC_PARTICLES_CONFIG = {
|
|
1319
|
+
// Cloud shape
|
|
1320
|
+
count: 4e4,
|
|
1321
|
+
innerRadius: 0.6,
|
|
1322
|
+
outerRadius: 1.1,
|
|
1323
|
+
// Motion. Constant rotation / swirl drift default to 0 so the cloud
|
|
1324
|
+
// only spins in response to audio — but `idleNoiseRate` keeps the
|
|
1325
|
+
// Perlin field shimmering slightly so the cloud doesn't look frozen
|
|
1326
|
+
// while paused.
|
|
1327
|
+
noiseFrequency: 1.4,
|
|
1328
|
+
noiseStrength: 0.22,
|
|
1329
|
+
rotationBias: 0,
|
|
1330
|
+
swirlTimeRate: 0,
|
|
1331
|
+
idleNoiseRate: 1e-4,
|
|
1332
|
+
// Rendering. Additive blending stacks contributions, so keep opacity
|
|
1333
|
+
// low (<0.3) or 40k particles will saturate to white. sizeBase ~1
|
|
1334
|
+
// renders as tiny dust; crank it higher for softer plasma look.
|
|
1335
|
+
sizeBase: 1,
|
|
1336
|
+
softness: 0.55,
|
|
1337
|
+
opacity: 0.18,
|
|
1338
|
+
// Color
|
|
1339
|
+
colorCool: "#c2410c",
|
|
1340
|
+
colorHot: "#fb923c",
|
|
1341
|
+
// Audio reactivity
|
|
1342
|
+
volume: {
|
|
1343
|
+
idle: 0.02,
|
|
1344
|
+
gain: 0.4,
|
|
1345
|
+
floor: 0,
|
|
1346
|
+
attack: 0.03,
|
|
1347
|
+
decay: 25e-4
|
|
1348
|
+
},
|
|
1349
|
+
medium: {
|
|
1350
|
+
idle: 0,
|
|
1351
|
+
gain: 4,
|
|
1352
|
+
floor: 0,
|
|
1353
|
+
attack: 0.01,
|
|
1354
|
+
decay: 4e-3
|
|
1355
|
+
},
|
|
1356
|
+
high: {
|
|
1357
|
+
idle: 0.2,
|
|
1358
|
+
gain: 1.2,
|
|
1359
|
+
floor: 0.15,
|
|
1360
|
+
attack: 0.015,
|
|
1361
|
+
decay: 2e-3
|
|
1362
|
+
},
|
|
1363
|
+
low: {
|
|
1364
|
+
idle: 4e-4,
|
|
1365
|
+
gain: 4e-3,
|
|
1366
|
+
floor: 1e-4,
|
|
1367
|
+
attack: 5e-3,
|
|
1368
|
+
decay: 2e-3
|
|
1369
|
+
},
|
|
1370
|
+
// Camera
|
|
1371
|
+
cameraZ: 3.84,
|
|
1372
|
+
fov: 35,
|
|
1373
|
+
// Determinism
|
|
1374
|
+
seed: 0,
|
|
1375
|
+
// Init-only
|
|
1376
|
+
maxPixelRatio: null,
|
|
1377
|
+
backgroundColor: null
|
|
1378
|
+
};
|
|
1379
|
+
function resolveOrganicParticlesConfig(partial) {
|
|
1380
|
+
const base = DEFAULT_ORGANIC_PARTICLES_CONFIG;
|
|
1381
|
+
if (!partial) {
|
|
1382
|
+
return {
|
|
1383
|
+
...base,
|
|
1384
|
+
volume: { ...base.volume },
|
|
1385
|
+
medium: { ...base.medium },
|
|
1386
|
+
high: { ...base.high },
|
|
1387
|
+
low: { ...base.low }
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
return {
|
|
1391
|
+
...base,
|
|
1392
|
+
...partial,
|
|
1393
|
+
volume: { ...base.volume, ...partial.volume ?? {} },
|
|
1394
|
+
medium: { ...base.medium, ...partial.medium ?? {} },
|
|
1395
|
+
high: { ...base.high, ...partial.high ?? {} },
|
|
1396
|
+
low: { ...base.low, ...partial.low ?? {} }
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/native/OrganicParticles.tsx
|
|
1401
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1402
|
+
function seededUnit2(seed, salt) {
|
|
1403
|
+
const s = Math.sin((seed + salt * 7919) * 12.9898 + salt * 78.233) * 43758.5453;
|
|
1404
|
+
return s - Math.floor(s);
|
|
1405
|
+
}
|
|
1406
|
+
function buildParticleAttributes(count, innerRadius, outerRadius, seed) {
|
|
1407
|
+
const positions = new Float32Array(count * 3);
|
|
1408
|
+
const seeds = new Float32Array(count);
|
|
1409
|
+
for (let i = 0; i < count; i++) {
|
|
1410
|
+
const u = seededUnit2(seed, i * 4 + 1);
|
|
1411
|
+
const v = seededUnit2(seed, i * 4 + 2);
|
|
1412
|
+
const w = seededUnit2(seed, i * 4 + 3);
|
|
1413
|
+
const s = seededUnit2(seed, i * 4 + 4);
|
|
1414
|
+
const theta = v * Math.PI * 2;
|
|
1415
|
+
const phi = Math.acos(2 * w - 1);
|
|
1416
|
+
const r = innerRadius + u * Math.max(0, outerRadius - innerRadius);
|
|
1417
|
+
const sinPhi = Math.sin(phi);
|
|
1418
|
+
positions[i * 3 + 0] = r * sinPhi * Math.cos(theta);
|
|
1419
|
+
positions[i * 3 + 1] = r * sinPhi * Math.sin(theta);
|
|
1420
|
+
positions[i * 3 + 2] = r * Math.cos(phi);
|
|
1421
|
+
seeds[i] = s;
|
|
1422
|
+
}
|
|
1423
|
+
return { positions, seeds };
|
|
1424
|
+
}
|
|
1425
|
+
function OrganicParticles({
|
|
1426
|
+
getFrequencyData,
|
|
1427
|
+
active = true,
|
|
1428
|
+
config: configProp,
|
|
1429
|
+
style
|
|
1430
|
+
}) {
|
|
1431
|
+
const resolvedConfig = (0, import_react5.useMemo)(
|
|
1432
|
+
() => resolveOrganicParticlesConfig(configProp),
|
|
1433
|
+
[configProp]
|
|
1434
|
+
);
|
|
1435
|
+
const configRef = (0, import_react5.useRef)(resolvedConfig);
|
|
1436
|
+
(0, import_react5.useEffect)(() => {
|
|
1437
|
+
configRef.current = resolvedConfig;
|
|
1438
|
+
}, [resolvedConfig]);
|
|
1439
|
+
const getFrequencyDataRef = (0, import_react5.useRef)(getFrequencyData);
|
|
1440
|
+
const activeRef = (0, import_react5.useRef)(active);
|
|
1441
|
+
(0, import_react5.useEffect)(() => {
|
|
1442
|
+
getFrequencyDataRef.current = getFrequencyData;
|
|
1443
|
+
}, [getFrequencyData]);
|
|
1444
|
+
(0, import_react5.useEffect)(() => {
|
|
1445
|
+
activeRef.current = active;
|
|
1446
|
+
}, [active]);
|
|
1447
|
+
const disposeRef = (0, import_react5.useRef)(null);
|
|
1448
|
+
(0, import_react5.useEffect)(() => {
|
|
1449
|
+
return () => {
|
|
1450
|
+
disposeRef.current?.();
|
|
1451
|
+
disposeRef.current = null;
|
|
1452
|
+
};
|
|
1453
|
+
}, []);
|
|
1454
|
+
const initCount = resolvedConfig.count;
|
|
1455
|
+
const initInnerRadius = resolvedConfig.innerRadius;
|
|
1456
|
+
const initOuterRadius = resolvedConfig.outerRadius;
|
|
1457
|
+
const initSeed = resolvedConfig.seed;
|
|
1458
|
+
const initMaxPixelRatio = resolvedConfig.maxPixelRatio;
|
|
1459
|
+
const initBackgroundColor = resolvedConfig.backgroundColor;
|
|
1460
|
+
const onContextCreate = (0, import_react5.useMemo)(
|
|
1461
|
+
() => async (gl) => {
|
|
1462
|
+
const cfg0 = configRef.current;
|
|
1463
|
+
const width = gl.drawingBufferWidth;
|
|
1464
|
+
const height = gl.drawingBufferHeight;
|
|
1465
|
+
const pixelRatio = Math.min(initMaxPixelRatio ?? import_react_native5.PixelRatio.get(), 2);
|
|
1466
|
+
const scene = new THREE2.Scene();
|
|
1467
|
+
if (initBackgroundColor) scene.background = new THREE2.Color(initBackgroundColor);
|
|
1468
|
+
const camera = new THREE2.PerspectiveCamera(
|
|
1469
|
+
cfg0.fov,
|
|
1470
|
+
width / height || 1,
|
|
1471
|
+
0.1,
|
|
1472
|
+
100
|
|
1473
|
+
);
|
|
1474
|
+
camera.position.set(0, 0, cfg0.cameraZ);
|
|
1475
|
+
scene.add(camera);
|
|
1476
|
+
const renderer = new import_expo_three2.Renderer({
|
|
1477
|
+
gl,
|
|
1478
|
+
antialias: true,
|
|
1479
|
+
alpha: initBackgroundColor === null,
|
|
1480
|
+
powerPreference: "high-performance"
|
|
1481
|
+
});
|
|
1482
|
+
renderer.setSize(width / pixelRatio, height / pixelRatio, false);
|
|
1483
|
+
renderer.setPixelRatio(pixelRatio);
|
|
1484
|
+
renderer.setClearColor(0, initBackgroundColor === null ? 0 : 1);
|
|
1485
|
+
const { positions, seeds } = buildParticleAttributes(
|
|
1486
|
+
initCount,
|
|
1487
|
+
initInnerRadius,
|
|
1488
|
+
initOuterRadius,
|
|
1489
|
+
initSeed
|
|
1490
|
+
);
|
|
1491
|
+
const geometry = new THREE2.BufferGeometry();
|
|
1492
|
+
geometry.setAttribute("aBasePosition", new THREE2.BufferAttribute(positions, 3));
|
|
1493
|
+
geometry.setAttribute("aSeed", new THREE2.BufferAttribute(seeds, 1));
|
|
1494
|
+
geometry.setAttribute("position", new THREE2.BufferAttribute(positions, 3));
|
|
1495
|
+
const colorCoolObj = new THREE2.Color(cfg0.colorCool);
|
|
1496
|
+
const colorHotObj = new THREE2.Color(cfg0.colorHot);
|
|
1497
|
+
const material = new THREE2.ShaderMaterial({
|
|
1498
|
+
uniforms: {
|
|
1499
|
+
uTime: { value: 0 },
|
|
1500
|
+
uNoiseFrequency: { value: cfg0.noiseFrequency },
|
|
1501
|
+
uNoiseStrength: { value: cfg0.noiseStrength },
|
|
1502
|
+
uDisplacement: { value: cfg0.volume.idle },
|
|
1503
|
+
uSwirl: { value: cfg0.high.idle },
|
|
1504
|
+
uSwirlTimeRate: { value: cfg0.swirlTimeRate },
|
|
1505
|
+
uSizeBase: { value: cfg0.sizeBase },
|
|
1506
|
+
uSizeAudio: { value: cfg0.medium.idle },
|
|
1507
|
+
uPixelRatio: { value: pixelRatio },
|
|
1508
|
+
uColorCool: { value: colorCoolObj },
|
|
1509
|
+
uColorHot: { value: colorHotObj },
|
|
1510
|
+
uOpacity: { value: cfg0.opacity },
|
|
1511
|
+
uSoftness: { value: cfg0.softness }
|
|
1512
|
+
},
|
|
1513
|
+
vertexShader: ORGANIC_PARTICLES_VERTEX_SHADER,
|
|
1514
|
+
fragmentShader: ORGANIC_PARTICLES_FRAGMENT_SHADER,
|
|
1515
|
+
transparent: true,
|
|
1516
|
+
depthWrite: false,
|
|
1517
|
+
blending: THREE2.AdditiveBlending
|
|
1518
|
+
});
|
|
1519
|
+
const points = new THREE2.Points(geometry, material);
|
|
1520
|
+
scene.add(points);
|
|
1521
|
+
const variations = {
|
|
1522
|
+
volume: { target: 0, current: cfg0.volume.idle },
|
|
1523
|
+
medium: { target: 0, current: cfg0.medium.idle },
|
|
1524
|
+
high: { target: 0, current: cfg0.high.idle },
|
|
1525
|
+
low: { target: 0, current: cfg0.low.idle }
|
|
1526
|
+
};
|
|
1527
|
+
const LEVEL_COUNT = 8;
|
|
1528
|
+
function computeLevels(buf) {
|
|
1529
|
+
const bins = Math.floor(buf.length / LEVEL_COUNT);
|
|
1530
|
+
const levels = new Array(LEVEL_COUNT).fill(0);
|
|
1531
|
+
if (bins <= 0) return levels;
|
|
1532
|
+
for (let i = 0; i < LEVEL_COUNT; i++) {
|
|
1533
|
+
let sum = 0;
|
|
1534
|
+
for (let j = 0; j < bins; j++) sum += buf[i * bins + j];
|
|
1535
|
+
levels[i] = sum / bins / 256;
|
|
1536
|
+
}
|
|
1537
|
+
return levels;
|
|
1538
|
+
}
|
|
1539
|
+
function bandTarget(reactivity, levels, sourceLevel) {
|
|
1540
|
+
if (!levels) return reactivity.idle;
|
|
1541
|
+
return Math.max(
|
|
1542
|
+
reactivity.floor,
|
|
1543
|
+
reactivity.floor + sourceLevel * reactivity.gain
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
const rotationState = { y: 0 };
|
|
1547
|
+
const resetAnimationState = (cfg) => {
|
|
1548
|
+
material.uniforms.uTime.value = 0;
|
|
1549
|
+
rotationState.y = 0;
|
|
1550
|
+
points.rotation.set(0, 0, 0);
|
|
1551
|
+
variations.volume.current = cfg.volume.idle;
|
|
1552
|
+
variations.medium.current = cfg.medium.idle;
|
|
1553
|
+
variations.high.current = cfg.high.idle;
|
|
1554
|
+
variations.low.current = cfg.low.idle;
|
|
1555
|
+
variations.volume.target = cfg.volume.idle;
|
|
1556
|
+
variations.medium.target = cfg.medium.idle;
|
|
1557
|
+
variations.high.target = cfg.high.idle;
|
|
1558
|
+
variations.low.target = cfg.low.idle;
|
|
1559
|
+
};
|
|
1560
|
+
let rafId = 0;
|
|
1561
|
+
let lastTs = Date.now();
|
|
1562
|
+
let disposed = false;
|
|
1563
|
+
let wasActive = activeRef.current;
|
|
1564
|
+
const render = () => {
|
|
1565
|
+
if (disposed) return;
|
|
1566
|
+
const now = Date.now();
|
|
1567
|
+
const delta = Math.min(now - lastTs, 100);
|
|
1568
|
+
lastTs = now;
|
|
1569
|
+
const cfg = configRef.current;
|
|
1570
|
+
colorCoolObj.set(cfg.colorCool);
|
|
1571
|
+
colorHotObj.set(cfg.colorHot);
|
|
1572
|
+
material.uniforms.uNoiseFrequency.value = cfg.noiseFrequency;
|
|
1573
|
+
material.uniforms.uNoiseStrength.value = cfg.noiseStrength;
|
|
1574
|
+
material.uniforms.uSwirlTimeRate.value = cfg.swirlTimeRate;
|
|
1575
|
+
material.uniforms.uSizeBase.value = cfg.sizeBase;
|
|
1576
|
+
material.uniforms.uOpacity.value = cfg.opacity;
|
|
1577
|
+
material.uniforms.uSoftness.value = cfg.softness;
|
|
1578
|
+
if (camera.position.z !== cfg.cameraZ) camera.position.z = cfg.cameraZ;
|
|
1579
|
+
if (camera.fov !== cfg.fov) {
|
|
1580
|
+
camera.fov = cfg.fov;
|
|
1581
|
+
camera.updateProjectionMatrix();
|
|
1582
|
+
}
|
|
1583
|
+
const isActive = activeRef.current;
|
|
1584
|
+
if (isActive && !wasActive) {
|
|
1585
|
+
resetAnimationState(cfg);
|
|
1586
|
+
}
|
|
1587
|
+
wasActive = isActive;
|
|
1588
|
+
if (isActive) {
|
|
1589
|
+
let levels = null;
|
|
1590
|
+
if (getFrequencyDataRef.current) {
|
|
1591
|
+
try {
|
|
1592
|
+
const buf = getFrequencyDataRef.current();
|
|
1593
|
+
if (buf && buf.length > 0) levels = computeLevels(buf);
|
|
1594
|
+
} catch {
|
|
1595
|
+
levels = null;
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const l0 = levels ? levels[0] || 0 : 0;
|
|
1599
|
+
const l1 = levels ? levels[1] || 0 : 0;
|
|
1600
|
+
const l2 = levels ? levels[2] || 0 : 0;
|
|
1601
|
+
const loudest = Math.max(l0, l1, l2);
|
|
1602
|
+
const update = (variation, reactivity, sourceLevel) => {
|
|
1603
|
+
variation.target = bandTarget(reactivity, levels, sourceLevel);
|
|
1604
|
+
const easing = variation.target > variation.current ? reactivity.attack : reactivity.decay;
|
|
1605
|
+
variation.current += (variation.target - variation.current) * easing * delta;
|
|
1606
|
+
};
|
|
1607
|
+
update(variations.volume, cfg.volume, loudest);
|
|
1608
|
+
update(variations.medium, cfg.medium, l1);
|
|
1609
|
+
update(variations.high, cfg.high, l2);
|
|
1610
|
+
update(variations.low, cfg.low, l0);
|
|
1611
|
+
const timeFrequency = variations.low.current;
|
|
1612
|
+
const elapsed = delta * timeFrequency;
|
|
1613
|
+
material.uniforms.uDisplacement.value = variations.volume.current;
|
|
1614
|
+
material.uniforms.uSizeAudio.value = variations.medium.current;
|
|
1615
|
+
material.uniforms.uSwirl.value = variations.high.current;
|
|
1616
|
+
material.uniforms.uTime.value += elapsed;
|
|
1617
|
+
rotationState.y += delta * cfg.rotationBias + variations.high.current * delta * 8e-4;
|
|
1618
|
+
points.rotation.y = rotationState.y;
|
|
1619
|
+
} else {
|
|
1620
|
+
material.uniforms.uTime.value += delta * cfg.idleNoiseRate;
|
|
1621
|
+
}
|
|
1622
|
+
renderer.render(scene, camera);
|
|
1623
|
+
gl.endFrameEXP();
|
|
1624
|
+
rafId = requestAnimationFrame(render);
|
|
1625
|
+
};
|
|
1626
|
+
rafId = requestAnimationFrame(render);
|
|
1627
|
+
const dispose = () => {
|
|
1628
|
+
if (disposed) return;
|
|
1629
|
+
disposed = true;
|
|
1630
|
+
cancelAnimationFrame(rafId);
|
|
1631
|
+
geometry.dispose();
|
|
1632
|
+
material.dispose();
|
|
1633
|
+
renderer.dispose();
|
|
1634
|
+
};
|
|
1635
|
+
disposeRef.current = dispose;
|
|
1636
|
+
},
|
|
1637
|
+
[initCount, initInnerRadius, initOuterRadius, initSeed, initMaxPixelRatio, initBackgroundColor]
|
|
1638
|
+
);
|
|
1639
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1640
|
+
import_expo_gl2.GLView,
|
|
1641
|
+
{
|
|
1642
|
+
style,
|
|
1643
|
+
onContextCreate
|
|
1644
|
+
},
|
|
1645
|
+
`${initCount}-${initInnerRadius}-${initOuterRadius}-${initSeed}-${initMaxPixelRatio ?? "auto"}-${initBackgroundColor ?? "transparent"}`
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1649
|
+
0 && (module.exports = {
|
|
1650
|
+
DEFAULT_ORGANIC_PARTICLES_CONFIG,
|
|
1651
|
+
DEFAULT_ORGANIC_SPHERE_CONFIG,
|
|
1652
|
+
ORGANIC_PARTICLES_FRAGMENT_SHADER,
|
|
1653
|
+
ORGANIC_PARTICLES_VERTEX_SHADER,
|
|
1654
|
+
ORGANIC_SPHERE_FRAGMENT_SHADER,
|
|
1655
|
+
ORGANIC_SPHERE_VERTEX_SHADER,
|
|
1656
|
+
OrganicParticles,
|
|
1657
|
+
OrganicSphere,
|
|
1658
|
+
PERLIN_4D,
|
|
1659
|
+
ShogoVoiceProvider,
|
|
1660
|
+
resolveOrganicParticlesConfig,
|
|
1661
|
+
resolveOrganicSphereConfig,
|
|
1662
|
+
useChatConversation,
|
|
1663
|
+
useShogoChat,
|
|
1664
|
+
useShogoVoice,
|
|
1665
|
+
useVoiceConversation
|
|
1666
|
+
});
|
|
1667
|
+
//# sourceMappingURL=index.cjs.map
|