@promptbook/cli 0.112.0-134 → 0.112.0-135
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/apps/agents-server/src/app/admin/chat-feedback/page.tsx +2 -4
- package/apps/agents-server/src/app/admin/chat-history/page.tsx +2 -4
- package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +28 -9
- package/apps/agents-server/src/app/api/chat/route.ts +29 -7
- package/apps/agents-server/src/app/api/chat-feedback/export/route.ts +2 -2
- package/apps/agents-server/src/app/api/chat-feedback/route.ts +3 -3
- package/apps/agents-server/src/app/api/chat-history/[id]/route.ts +2 -2
- package/apps/agents-server/src/app/api/chat-history/export/route.ts +2 -2
- package/apps/agents-server/src/app/api/chat-history/route.ts +3 -3
- package/apps/agents-server/src/app/api/chat-streaming/route.ts +29 -0
- package/apps/agents-server/src/app/api/elevenlabs/tts/route.ts +60 -2
- package/apps/agents-server/src/app/api/images/[filename]/route.ts +102 -0
- package/apps/agents-server/src/app/api/openai/v1/audio/transcriptions/route.ts +111 -2
- package/apps/agents-server/src/app/api/page-preview/check/route.ts +18 -0
- package/apps/agents-server/src/app/api/page-preview/screenshot/route.ts +15 -8
- package/apps/agents-server/src/app/api/team-agent-profile/route.ts +20 -0
- package/apps/agents-server/src/components/AgentContextMenu/useAgentContextMenuItems.ts +27 -19
- package/apps/agents-server/src/constants/defaultAgentAvatarVisual.ts +1 -1
- package/apps/agents-server/src/database/migrations/2026-06-2700-default-agent-avatar-visual-octopus3d4.sql +16 -0
- package/apps/agents-server/src/utils/agentOwnership.ts +54 -5
- package/apps/agents-server/src/utils/findAgentForCallerWriteAccess.ts +36 -0
- package/apps/agents-server/src/utils/paidApiRequestGuard.ts +241 -0
- package/apps/agents-server/src/utils/userChat/createRunUserChatJobExecutionContext.ts +36 -8
- package/apps/agents-server/src/utils/userChat/persistUserChatJobProgressCard.ts +11 -2
- package/apps/agents-server/src/utils/userChat/resolveUserChatProgressToolHighlights.ts +100 -0
- package/apps/agents-server/src/utils/userChat/runUserChatJob.ts +4 -3
- package/apps/agents-server/src/utils/userChat/userChatProgressCard.ts +272 -27
- package/apps/agents-server/src/utils/validateApiKey.ts +7 -3
- package/esm/index.es.js +795 -39
- package/esm/index.es.js.map +1 -1
- package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
- package/esm/src/avatars/visuals/octopus3d4AvatarVisual.d.ts +7 -0
- package/esm/src/utils/validators/url/isValidAgentUrl.d.ts +5 -0
- package/package.json +1 -1
- package/src/avatars/types/AvatarVisualDefinition.ts +1 -0
- package/src/avatars/visuals/avatarVisualRegistry.ts +2 -0
- package/src/avatars/visuals/octopus3d4AvatarVisual.ts +1295 -0
- package/src/other/templates/getTemplatesPipelineCollection.ts +772 -942
- package/src/utils/agents/resolveAgentAvatarImageUrl.ts +1 -1
- package/src/utils/validators/url/isValidAgentUrl.ts +5 -7
- package/src/version.ts +1 -1
- package/src/versions.txt +1 -1
- package/umd/index.umd.js +795 -39
- package/umd/index.umd.js.map +1 -1
- package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
- package/umd/src/avatars/visuals/octopus3d4AvatarVisual.d.ts +7 -0
- package/umd/src/utils/validators/url/isValidAgentUrl.d.ts +5 -0
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
/* eslint-disable no-magic-numbers */
|
|
2
|
+
|
|
3
|
+
import { drawAvatarFrame } from '../avatarRenderingUtils';
|
|
4
|
+
import type { AvatarPalette, AvatarVisualDefinition } from '../types/AvatarVisualDefinition';
|
|
5
|
+
import {
|
|
6
|
+
clampNumber,
|
|
7
|
+
crossProduct3D,
|
|
8
|
+
dotProduct3D,
|
|
9
|
+
getProjectedQuadPerimeter,
|
|
10
|
+
normalizeVector3,
|
|
11
|
+
projectScenePoint,
|
|
12
|
+
subtractPoint3D,
|
|
13
|
+
transformScenePoint,
|
|
14
|
+
type Point3D,
|
|
15
|
+
type ProjectedPoint,
|
|
16
|
+
} from './avatar3dProjectionShared';
|
|
17
|
+
import { drawProjectedOrganicEye, drawProjectedOrganicMouth, drawProjectedQuad } from './octopus3dAvatarVisualShared';
|
|
18
|
+
import { createOctopus3MorphologyProfile, type Octopus3MorphologyProfile } from './octopus3AvatarVisual';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One visible projected patch on the continuous Octopus 3D 4 mesh.
|
|
22
|
+
*
|
|
23
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
24
|
+
*/
|
|
25
|
+
type BlobbyContinuousSurfacePatch = {
|
|
26
|
+
readonly corners: [ProjectedPoint, ProjectedPoint, ProjectedPoint, ProjectedPoint];
|
|
27
|
+
readonly averageDepth: number;
|
|
28
|
+
readonly lightIntensity: number;
|
|
29
|
+
readonly rimLightIntensity: number;
|
|
30
|
+
readonly fillStyle: string;
|
|
31
|
+
readonly outlineColor: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Seeded profile for one continuous lower mesh lobe that reads as a tentacle.
|
|
36
|
+
*
|
|
37
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
38
|
+
*/
|
|
39
|
+
type BlobbyContinuousTentacleProfile = {
|
|
40
|
+
readonly centerLongitude: number;
|
|
41
|
+
readonly widthScale: number;
|
|
42
|
+
readonly lengthScale: number;
|
|
43
|
+
readonly swayScale: number;
|
|
44
|
+
readonly depthScale: number;
|
|
45
|
+
readonly curlScale: number;
|
|
46
|
+
readonly primaryPhase: number;
|
|
47
|
+
readonly secondaryPhase: number;
|
|
48
|
+
readonly suckerSide: -1 | 1;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Stable surface options used by the continuous Octopus 3D 4 mesh sampler.
|
|
53
|
+
*
|
|
54
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
55
|
+
*/
|
|
56
|
+
type BlobbyContinuousSurfaceOptions = {
|
|
57
|
+
readonly radiusX: number;
|
|
58
|
+
readonly radiusY: number;
|
|
59
|
+
readonly radiusZ: number;
|
|
60
|
+
readonly morphologyProfile: Octopus3MorphologyProfile;
|
|
61
|
+
readonly timeMs: number;
|
|
62
|
+
readonly animationPhase: number;
|
|
63
|
+
readonly tentacleProfiles: ReadonlyArray<BlobbyContinuousTentacleProfile>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Smoothly blended influence of nearby tentacle lobes at one longitude.
|
|
68
|
+
*
|
|
69
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
70
|
+
*/
|
|
71
|
+
type BlobbyContinuousTentacleInfluence = {
|
|
72
|
+
readonly core: number;
|
|
73
|
+
readonly centerLongitude: number;
|
|
74
|
+
readonly widthScale: number;
|
|
75
|
+
readonly lengthScale: number;
|
|
76
|
+
readonly swayScale: number;
|
|
77
|
+
readonly depthScale: number;
|
|
78
|
+
readonly curlScale: number;
|
|
79
|
+
readonly primaryPhase: number;
|
|
80
|
+
readonly secondaryPhase: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* One seeded skin-spot used as a subtle pigment patch on the octopus body.
|
|
85
|
+
*
|
|
86
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
87
|
+
*/
|
|
88
|
+
type BlobbySkinSpot = {
|
|
89
|
+
readonly latitude: number;
|
|
90
|
+
readonly longitude: number;
|
|
91
|
+
readonly radiusScale: number;
|
|
92
|
+
readonly opacity: number;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Light direction used by the continuous Octopus 3D 4 mesh shading.
|
|
97
|
+
*
|
|
98
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
99
|
+
*/
|
|
100
|
+
const LIGHT_DIRECTION: Point3D = normalizeVector3({
|
|
101
|
+
x: 0.32,
|
|
102
|
+
y: -0.66,
|
|
103
|
+
z: 1,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Rim-light direction used to brighten the side of the silhouette.
|
|
108
|
+
*
|
|
109
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
110
|
+
*/
|
|
111
|
+
const RIM_LIGHT_DIRECTION: Point3D = normalizeVector3({
|
|
112
|
+
x: -0.7,
|
|
113
|
+
y: -0.18,
|
|
114
|
+
z: 0.55,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Real-octopus tentacle count used by the continuous lower mesh.
|
|
119
|
+
*
|
|
120
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
121
|
+
*/
|
|
122
|
+
const OCTOPUS_TENTACLE_COUNT = 8;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Number of seeded skin spots painted across the octopus body.
|
|
126
|
+
*
|
|
127
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
128
|
+
*/
|
|
129
|
+
const SKIN_SPOT_COUNT = 14;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Per-avatar stable state derived once from the seeded random factory and reused across frames.
|
|
133
|
+
*
|
|
134
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
135
|
+
*/
|
|
136
|
+
type Octopus3d4StableState = {
|
|
137
|
+
readonly morphologyProfile: Octopus3MorphologyProfile;
|
|
138
|
+
readonly animationPhase: number;
|
|
139
|
+
readonly leftEyePhaseOffset: number;
|
|
140
|
+
readonly rightEyePhaseOffset: number;
|
|
141
|
+
readonly tentacleProfiles: ReadonlyArray<BlobbyContinuousTentacleProfile>;
|
|
142
|
+
readonly skinSpots: ReadonlyArray<BlobbySkinSpot>;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Cache keyed by the `createRandom` factory reference, stable per mounted `<Avatar/>` component.
|
|
147
|
+
*
|
|
148
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
149
|
+
*/
|
|
150
|
+
const octopus3d4StableStateCache = new WeakMap<(salt: string) => () => number, Octopus3d4StableState>();
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Returns the stable per-avatar state, computing it on first access and caching afterwards.
|
|
154
|
+
*
|
|
155
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
156
|
+
*/
|
|
157
|
+
function getOctopus3d4StableState(createRandom: (salt: string) => () => number): Octopus3d4StableState {
|
|
158
|
+
const cached = octopus3d4StableStateCache.get(createRandom);
|
|
159
|
+
|
|
160
|
+
if (cached !== undefined) {
|
|
161
|
+
return cached;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
|
|
165
|
+
const animationRandom = createRandom('octopus3d4-animation-profile');
|
|
166
|
+
const eyeRandom = createRandom('octopus3d4-eye-profile');
|
|
167
|
+
const leftEyePhaseOffset = eyeRandom() * 0.7;
|
|
168
|
+
const rightEyePhaseOffset = eyeRandom() * 0.7;
|
|
169
|
+
const state: Octopus3d4StableState = {
|
|
170
|
+
morphologyProfile,
|
|
171
|
+
animationPhase: animationRandom() * Math.PI * 2,
|
|
172
|
+
leftEyePhaseOffset,
|
|
173
|
+
rightEyePhaseOffset,
|
|
174
|
+
tentacleProfiles: createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile),
|
|
175
|
+
skinSpots: createBlobbySkinSpots(createRandom),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
octopus3d4StableStateCache.set(createRandom, state);
|
|
179
|
+
|
|
180
|
+
return state;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Octopus 3D 4 avatar visual.
|
|
185
|
+
*
|
|
186
|
+
* @private built-in avatar visual
|
|
187
|
+
*/
|
|
188
|
+
export const octopus3d4AvatarVisual: AvatarVisualDefinition = {
|
|
189
|
+
id: 'octopus3d4',
|
|
190
|
+
title: 'Octopus 3D 4',
|
|
191
|
+
description:
|
|
192
|
+
'Cute blobby 3D octopus rendered as one smooth continuous mesh with naturally undulating tentacles, rich multi-pass shading, pigment spots, and cursor-aware head and eyes.',
|
|
193
|
+
isAnimated: true,
|
|
194
|
+
supportsPointerTracking: true,
|
|
195
|
+
render({ context, size, palette, createRandom, timeMs, interaction }) {
|
|
196
|
+
const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles, skinSpots } =
|
|
197
|
+
getOctopus3d4StableState(createRandom);
|
|
198
|
+
const sceneCenterX = size * 0.5;
|
|
199
|
+
const sceneCenterY = size * 0.535;
|
|
200
|
+
const bob = Math.sin(timeMs / 980 + animationPhase) * size * 0.013;
|
|
201
|
+
const meshCenter: Point3D = {
|
|
202
|
+
x: interaction.bodyOffsetX * size * 0.052 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
|
|
203
|
+
y: -size * 0.07 + interaction.bodyOffsetY * size * 0.028 + bob,
|
|
204
|
+
z: interaction.intensity * size * 0.02,
|
|
205
|
+
};
|
|
206
|
+
const rotationY =
|
|
207
|
+
-0.08 +
|
|
208
|
+
Math.sin(timeMs / 2800 + animationPhase) * 0.04 +
|
|
209
|
+
interaction.bodyOffsetX * 0.24 +
|
|
210
|
+
interaction.gazeX * 0.98;
|
|
211
|
+
const rotationX =
|
|
212
|
+
-0.07 +
|
|
213
|
+
Math.cos(timeMs / 3200 + animationPhase * 0.7) * 0.02 -
|
|
214
|
+
interaction.bodyOffsetY * 0.08 -
|
|
215
|
+
interaction.gazeY * 0.42;
|
|
216
|
+
const surfaceOptions: BlobbyContinuousSurfaceOptions = {
|
|
217
|
+
radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.12,
|
|
218
|
+
radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1,
|
|
219
|
+
radiusZ:
|
|
220
|
+
size *
|
|
221
|
+
morphologyProfile.body.bodyRadiusRatio *
|
|
222
|
+
(1.04 + (morphologyProfile.body.horizontalStretch - 1) * 0.2),
|
|
223
|
+
morphologyProfile,
|
|
224
|
+
timeMs,
|
|
225
|
+
animationPhase,
|
|
226
|
+
tentacleProfiles,
|
|
227
|
+
};
|
|
228
|
+
const surfacePatches = resolveVisibleBlobbyContinuousPatches({
|
|
229
|
+
...surfaceOptions,
|
|
230
|
+
center: meshCenter,
|
|
231
|
+
rotationX,
|
|
232
|
+
rotationY,
|
|
233
|
+
sceneCenterX,
|
|
234
|
+
sceneCenterY,
|
|
235
|
+
size,
|
|
236
|
+
palette,
|
|
237
|
+
});
|
|
238
|
+
const eyeLatitude = clampNumber(morphologyProfile.face.eyeCenterYOffsetRatio * 4.4 - 0.04, -0.24, 0.08);
|
|
239
|
+
const eyeLongitude = clampNumber(morphologyProfile.face.eyeSpacingRatio * 3.0, 0.18, 0.32);
|
|
240
|
+
const mouthLatitude = clampNumber(eyeLatitude + 0.21 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
|
|
241
|
+
const mouthCenterLongitude = clampNumber(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
|
|
242
|
+
const mouthHalfLongitude = clampNumber(eyeLongitude * 0.78, 0.15, 0.28);
|
|
243
|
+
const mouthCurveLatitude = clampNumber(
|
|
244
|
+
mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78,
|
|
245
|
+
mouthLatitude + 0.03,
|
|
246
|
+
0.42,
|
|
247
|
+
);
|
|
248
|
+
const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
|
|
249
|
+
const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
|
|
250
|
+
|
|
251
|
+
drawAvatarFrame(context, size, palette);
|
|
252
|
+
drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
|
|
253
|
+
drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile);
|
|
254
|
+
|
|
255
|
+
for (const surfacePatch of surfacePatches.sort(
|
|
256
|
+
(firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth,
|
|
257
|
+
)) {
|
|
258
|
+
drawBlobbyContinuousSurfacePatch(context, surfacePatch);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
drawBlobbySkinSpots({
|
|
262
|
+
context,
|
|
263
|
+
surfaceOptions,
|
|
264
|
+
center: meshCenter,
|
|
265
|
+
rotationX,
|
|
266
|
+
rotationY,
|
|
267
|
+
sceneCenterX,
|
|
268
|
+
sceneCenterY,
|
|
269
|
+
size,
|
|
270
|
+
palette,
|
|
271
|
+
skinSpots,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
drawBlobbyContinuousCurrents({
|
|
275
|
+
context,
|
|
276
|
+
surfaceOptions,
|
|
277
|
+
center: meshCenter,
|
|
278
|
+
rotationX,
|
|
279
|
+
rotationY,
|
|
280
|
+
sceneCenterX,
|
|
281
|
+
sceneCenterY,
|
|
282
|
+
size,
|
|
283
|
+
palette,
|
|
284
|
+
morphologyProfile,
|
|
285
|
+
timeMs,
|
|
286
|
+
animationPhase,
|
|
287
|
+
});
|
|
288
|
+
drawBlobbyContinuousSuckers({
|
|
289
|
+
context,
|
|
290
|
+
surfaceOptions,
|
|
291
|
+
center: meshCenter,
|
|
292
|
+
rotationX,
|
|
293
|
+
rotationY,
|
|
294
|
+
sceneCenterX,
|
|
295
|
+
sceneCenterY,
|
|
296
|
+
size,
|
|
297
|
+
palette,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
drawBlobbyContinuousGloss({
|
|
301
|
+
context,
|
|
302
|
+
surfaceOptions,
|
|
303
|
+
center: meshCenter,
|
|
304
|
+
rotationX,
|
|
305
|
+
rotationY,
|
|
306
|
+
sceneCenterX,
|
|
307
|
+
sceneCenterY,
|
|
308
|
+
size,
|
|
309
|
+
palette,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
drawProjectedOrganicEye(
|
|
313
|
+
context,
|
|
314
|
+
sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude),
|
|
315
|
+
eyeRadiusX,
|
|
316
|
+
eyeRadiusY,
|
|
317
|
+
meshCenter,
|
|
318
|
+
rotationX,
|
|
319
|
+
rotationY,
|
|
320
|
+
sceneCenterX,
|
|
321
|
+
sceneCenterY,
|
|
322
|
+
size,
|
|
323
|
+
palette,
|
|
324
|
+
timeMs,
|
|
325
|
+
animationPhase + leftEyePhaseOffset,
|
|
326
|
+
interaction,
|
|
327
|
+
morphologyProfile.face.eyeStyle,
|
|
328
|
+
);
|
|
329
|
+
drawProjectedOrganicEye(
|
|
330
|
+
context,
|
|
331
|
+
sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude),
|
|
332
|
+
eyeRadiusX,
|
|
333
|
+
eyeRadiusY,
|
|
334
|
+
meshCenter,
|
|
335
|
+
rotationX,
|
|
336
|
+
rotationY,
|
|
337
|
+
sceneCenterX,
|
|
338
|
+
sceneCenterY,
|
|
339
|
+
size,
|
|
340
|
+
palette,
|
|
341
|
+
timeMs,
|
|
342
|
+
animationPhase + 0.85 + rightEyePhaseOffset,
|
|
343
|
+
interaction,
|
|
344
|
+
morphologyProfile.face.eyeStyle,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
drawProjectedOrganicMouth(
|
|
348
|
+
context,
|
|
349
|
+
[
|
|
350
|
+
sampleBlobbyContinuousSurfacePoint(
|
|
351
|
+
surfaceOptions,
|
|
352
|
+
mouthLatitude,
|
|
353
|
+
mouthCenterLongitude - mouthHalfLongitude,
|
|
354
|
+
),
|
|
355
|
+
sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
|
|
356
|
+
sampleBlobbyContinuousSurfacePoint(
|
|
357
|
+
surfaceOptions,
|
|
358
|
+
mouthLatitude,
|
|
359
|
+
mouthCenterLongitude + mouthHalfLongitude,
|
|
360
|
+
),
|
|
361
|
+
],
|
|
362
|
+
meshCenter,
|
|
363
|
+
rotationX,
|
|
364
|
+
rotationY,
|
|
365
|
+
sceneCenterX,
|
|
366
|
+
sceneCenterY,
|
|
367
|
+
palette,
|
|
368
|
+
size,
|
|
369
|
+
);
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Creates seeded tentacle-lobe profiles around the visible lower octopus body.
|
|
375
|
+
*
|
|
376
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
377
|
+
*/
|
|
378
|
+
function createBlobbyContinuousTentacleProfiles(
|
|
379
|
+
createRandom: (salt: string) => () => number,
|
|
380
|
+
morphologyProfile: Octopus3MorphologyProfile,
|
|
381
|
+
): ReadonlyArray<BlobbyContinuousTentacleProfile> {
|
|
382
|
+
return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
|
|
383
|
+
const tentacleRandom = createRandom(`octopus3d4-tentacle-${tentacleIndex}`);
|
|
384
|
+
const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
centerLongitude:
|
|
388
|
+
-Math.PI * 0.9 +
|
|
389
|
+
progress * Math.PI * 1.8 +
|
|
390
|
+
(tentacleRandom() - 0.5) * (0.06 + morphologyProfile.tentacles.rootSpreadScale * 0.025),
|
|
391
|
+
widthScale: 0.92 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.18,
|
|
392
|
+
lengthScale: 0.9 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.24,
|
|
393
|
+
swayScale: 0.86 + tentacleRandom() * 0.4 + (morphologyProfile.tentacles.swayScale - 1) * 0.22,
|
|
394
|
+
depthScale: 0.9 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.22,
|
|
395
|
+
curlScale: 0.62 + tentacleRandom() * 0.46,
|
|
396
|
+
primaryPhase: tentacleRandom() * Math.PI * 2,
|
|
397
|
+
secondaryPhase: tentacleRandom() * Math.PI * 2,
|
|
398
|
+
suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Creates seeded skin pigment spots distributed across the upper octopus mesh.
|
|
405
|
+
*
|
|
406
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
407
|
+
*/
|
|
408
|
+
function createBlobbySkinSpots(createRandom: (salt: string) => () => number): ReadonlyArray<BlobbySkinSpot> {
|
|
409
|
+
const spotRandom = createRandom('octopus3d4-skin-spots');
|
|
410
|
+
|
|
411
|
+
return Array.from({ length: SKIN_SPOT_COUNT }, () => ({
|
|
412
|
+
latitude: -0.45 + spotRandom() * 0.7,
|
|
413
|
+
longitude: -0.5 + spotRandom() * 1.0,
|
|
414
|
+
radiusScale: 0.0028 + spotRandom() * 0.0052,
|
|
415
|
+
opacity: 0.16 + spotRandom() * 0.22,
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Draws the soft underwater atmosphere behind the continuous octopus mesh.
|
|
421
|
+
*
|
|
422
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
423
|
+
*/
|
|
424
|
+
function drawBlobbyContinuousAtmosphere(
|
|
425
|
+
context: CanvasRenderingContext2D,
|
|
426
|
+
size: number,
|
|
427
|
+
palette: AvatarPalette,
|
|
428
|
+
sceneCenterX: number,
|
|
429
|
+
sceneCenterY: number,
|
|
430
|
+
interaction: {
|
|
431
|
+
readonly gazeX: number;
|
|
432
|
+
readonly gazeY: number;
|
|
433
|
+
readonly intensity: number;
|
|
434
|
+
},
|
|
435
|
+
timeMs: number,
|
|
436
|
+
): void {
|
|
437
|
+
const glowGradient = context.createRadialGradient(
|
|
438
|
+
sceneCenterX + interaction.gazeX * size * 0.12,
|
|
439
|
+
sceneCenterY - size * 0.18 + interaction.gazeY * size * 0.05,
|
|
440
|
+
size * 0.04,
|
|
441
|
+
sceneCenterX,
|
|
442
|
+
sceneCenterY,
|
|
443
|
+
size * (0.68 + interaction.intensity * 0.025),
|
|
444
|
+
);
|
|
445
|
+
glowGradient.addColorStop(0, `${palette.highlight}74`);
|
|
446
|
+
glowGradient.addColorStop(0.32, `${palette.accent}32`);
|
|
447
|
+
glowGradient.addColorStop(1, `${palette.highlight}00`);
|
|
448
|
+
context.fillStyle = glowGradient;
|
|
449
|
+
context.fillRect(0, 0, size, size);
|
|
450
|
+
|
|
451
|
+
const lowerGradient = context.createRadialGradient(
|
|
452
|
+
sceneCenterX + Math.sin(timeMs / 1500) * size * 0.05,
|
|
453
|
+
sceneCenterY + size * 0.3,
|
|
454
|
+
size * 0.06,
|
|
455
|
+
sceneCenterX,
|
|
456
|
+
sceneCenterY + size * 0.3,
|
|
457
|
+
size * 0.56,
|
|
458
|
+
);
|
|
459
|
+
lowerGradient.addColorStop(0, `${palette.secondary}28`);
|
|
460
|
+
lowerGradient.addColorStop(1, `${palette.secondary}00`);
|
|
461
|
+
context.fillStyle = lowerGradient;
|
|
462
|
+
context.fillRect(0, 0, size, size);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Draws the soft lower shadow that anchors the octopus in the avatar frame.
|
|
467
|
+
*
|
|
468
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
469
|
+
*/
|
|
470
|
+
function drawBlobbyContinuousShadow(
|
|
471
|
+
context: CanvasRenderingContext2D,
|
|
472
|
+
size: number,
|
|
473
|
+
palette: AvatarPalette,
|
|
474
|
+
interaction: {
|
|
475
|
+
readonly gazeX: number;
|
|
476
|
+
readonly intensity: number;
|
|
477
|
+
},
|
|
478
|
+
timeMs: number,
|
|
479
|
+
morphologyProfile: Octopus3MorphologyProfile,
|
|
480
|
+
): void {
|
|
481
|
+
const cx = size * 0.5 + interaction.gazeX * size * 0.046;
|
|
482
|
+
const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
|
|
483
|
+
const rx = size * (0.2 + morphologyProfile.tentacles.rootSpreadScale * 0.024 + interaction.intensity * 0.022);
|
|
484
|
+
const ry = size * 0.062;
|
|
485
|
+
|
|
486
|
+
context.save();
|
|
487
|
+
context.translate(cx, cy);
|
|
488
|
+
context.scale(1, ry / rx);
|
|
489
|
+
const blurRadius = rx * 1.42;
|
|
490
|
+
const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
|
|
491
|
+
shadowGradient.addColorStop(0, `${palette.shadow}82`);
|
|
492
|
+
shadowGradient.addColorStop(0.45, `${palette.shadow}4a`);
|
|
493
|
+
shadowGradient.addColorStop(0.8, `${palette.shadow}1c`);
|
|
494
|
+
shadowGradient.addColorStop(1, `${palette.shadow}00`);
|
|
495
|
+
context.fillStyle = shadowGradient;
|
|
496
|
+
context.beginPath();
|
|
497
|
+
context.arc(0, 0, blurRadius, 0, Math.PI * 2);
|
|
498
|
+
context.fill();
|
|
499
|
+
context.restore();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Number of latitude segments used by the continuous Octopus 3D 4 mesh.
|
|
504
|
+
*
|
|
505
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
506
|
+
*/
|
|
507
|
+
const LATITUDE_PATCH_COUNT = 20;
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Number of longitude segments used by the continuous Octopus 3D 4 mesh.
|
|
511
|
+
*
|
|
512
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
513
|
+
*/
|
|
514
|
+
const LONGITUDE_PATCH_COUNT = 48;
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Resolves visible projected patches for the continuous Octopus 3D 4 mesh.
|
|
518
|
+
*
|
|
519
|
+
* Within a single frame, mesh corner samples and longitude-only computations (tentacle
|
|
520
|
+
* influence and lobe wave) are quantized to the patch grid and computed once each rather
|
|
521
|
+
* than re-evaluated for every patch corner.
|
|
522
|
+
*
|
|
523
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
524
|
+
*/
|
|
525
|
+
function resolveVisibleBlobbyContinuousPatches(
|
|
526
|
+
options: BlobbyContinuousSurfaceOptions & {
|
|
527
|
+
readonly center: Point3D;
|
|
528
|
+
readonly rotationX: number;
|
|
529
|
+
readonly rotationY: number;
|
|
530
|
+
readonly sceneCenterX: number;
|
|
531
|
+
readonly sceneCenterY: number;
|
|
532
|
+
readonly size: number;
|
|
533
|
+
readonly palette: AvatarPalette;
|
|
534
|
+
},
|
|
535
|
+
): Array<BlobbyContinuousSurfacePatch> {
|
|
536
|
+
const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
|
|
537
|
+
const latitudePatchCount = LATITUDE_PATCH_COUNT;
|
|
538
|
+
const longitudePatchCount = LONGITUDE_PATCH_COUNT;
|
|
539
|
+
const surfacePatches: Array<BlobbyContinuousSurfacePatch> = [];
|
|
540
|
+
const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
|
|
541
|
+
const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
|
|
542
|
+
|
|
543
|
+
for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
|
|
544
|
+
latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
|
|
548
|
+
longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const cachedTentacleInfluencesByCornerLongitude = new Array<BlobbyContinuousTentacleInfluence>(
|
|
552
|
+
longitudePatchCount + 1,
|
|
553
|
+
);
|
|
554
|
+
const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
|
|
555
|
+
const cachedTentacleInfluencesByPatchCenterLongitude = new Array<BlobbyContinuousTentacleInfluence>(
|
|
556
|
+
longitudePatchCount,
|
|
557
|
+
);
|
|
558
|
+
const cachedLobeWavesByPatchCenterLongitude = new Float64Array(longitudePatchCount);
|
|
559
|
+
const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
|
|
560
|
+
|
|
561
|
+
for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
|
|
562
|
+
const cornerLongitude = longitudeBoundaries[boundaryIndex]!;
|
|
563
|
+
cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveBlobbyTentacleInfluence(
|
|
564
|
+
options,
|
|
565
|
+
cornerLongitude,
|
|
566
|
+
);
|
|
567
|
+
cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveBlobbyLobeWave(options, cornerLongitude);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
|
|
571
|
+
const patchCenterLongitude = (longitudeBoundaries[longitudeIndex]! + longitudeBoundaries[longitudeIndex + 1]!) / 2;
|
|
572
|
+
cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyTentacleInfluence(
|
|
573
|
+
options,
|
|
574
|
+
patchCenterLongitude,
|
|
575
|
+
);
|
|
576
|
+
cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyLobeWave(options, patchCenterLongitude);
|
|
577
|
+
cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
|
|
581
|
+
const transformedCornerSamples = new Array<Point3D>(cornerCount);
|
|
582
|
+
|
|
583
|
+
for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
|
|
584
|
+
const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex]!;
|
|
585
|
+
|
|
586
|
+
for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
|
|
587
|
+
const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex]!;
|
|
588
|
+
const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
|
|
589
|
+
const cornerSample = sampleBlobbyContinuousSurfacePointWithLongitudeCache(
|
|
590
|
+
options,
|
|
591
|
+
cornerLatitude,
|
|
592
|
+
cornerLongitude,
|
|
593
|
+
cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex]!,
|
|
594
|
+
cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]!,
|
|
595
|
+
);
|
|
596
|
+
transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
|
|
601
|
+
const startLatitude = latitudeBoundaries[latitudeIndex]!;
|
|
602
|
+
const endLatitude = latitudeBoundaries[latitudeIndex + 1]!;
|
|
603
|
+
const centerLatitude = (startLatitude + endLatitude) / 2;
|
|
604
|
+
const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
|
|
605
|
+
const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
|
|
606
|
+
const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
|
|
607
|
+
|
|
608
|
+
for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
|
|
609
|
+
const transformedCorners: [Point3D, Point3D, Point3D, Point3D] = [
|
|
610
|
+
transformedCornerSamples[startCornerRowOffset + longitudeIndex]!,
|
|
611
|
+
transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1]!,
|
|
612
|
+
transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1]!,
|
|
613
|
+
transformedCornerSamples[endCornerRowOffset + longitudeIndex]!,
|
|
614
|
+
];
|
|
615
|
+
const surfaceNormal = normalizeVector3(
|
|
616
|
+
crossProduct3D(
|
|
617
|
+
subtractPoint3D(transformedCorners[1], transformedCorners[0]),
|
|
618
|
+
subtractPoint3D(transformedCorners[2], transformedCorners[0]),
|
|
619
|
+
),
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
if (surfaceNormal.z <= 0.006) {
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const projectedCorners: [ProjectedPoint, ProjectedPoint, ProjectedPoint, ProjectedPoint] = [
|
|
627
|
+
projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
|
|
628
|
+
projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
|
|
629
|
+
projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
|
|
630
|
+
projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
|
|
631
|
+
];
|
|
632
|
+
const lightIntensity = clampNumber(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1);
|
|
633
|
+
const rimLightIntensity = Math.pow(
|
|
634
|
+
clampNumber(dotProduct3D(surfaceNormal, RIM_LIGHT_DIRECTION), 0, 1),
|
|
635
|
+
2.6,
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
surfacePatches.push({
|
|
639
|
+
corners: projectedCorners,
|
|
640
|
+
averageDepth:
|
|
641
|
+
(transformedCorners[0].z +
|
|
642
|
+
transformedCorners[1].z +
|
|
643
|
+
transformedCorners[2].z +
|
|
644
|
+
transformedCorners[3].z) /
|
|
645
|
+
4,
|
|
646
|
+
lightIntensity,
|
|
647
|
+
rimLightIntensity,
|
|
648
|
+
fillStyle: resolveBlobbySurfacePatchFillStyle(
|
|
649
|
+
palette,
|
|
650
|
+
verticalProgress,
|
|
651
|
+
cachedCosByPatchCenterLongitude[longitudeIndex]!,
|
|
652
|
+
cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex]!.core,
|
|
653
|
+
cachedLobeWavesByPatchCenterLongitude[longitudeIndex]!,
|
|
654
|
+
),
|
|
655
|
+
outlineColor: verticalProgress < 0.52 ? `${palette.highlight}5c` : `${palette.shadow}66`,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return surfacePatches;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Samples one point on the continuous Octopus 3D 4 surface.
|
|
665
|
+
*
|
|
666
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
667
|
+
*/
|
|
668
|
+
function sampleBlobbyContinuousSurfacePoint(
|
|
669
|
+
options: BlobbyContinuousSurfaceOptions,
|
|
670
|
+
latitude: number,
|
|
671
|
+
longitude: number,
|
|
672
|
+
): Point3D {
|
|
673
|
+
return sampleBlobbyContinuousSurfacePointWithLongitudeCache(
|
|
674
|
+
options,
|
|
675
|
+
latitude,
|
|
676
|
+
longitude,
|
|
677
|
+
resolveBlobbyTentacleInfluence(options, longitude),
|
|
678
|
+
resolveBlobbyLobeWave(options, longitude),
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Samples one point on the continuous Octopus 3D 4 surface using precomputed longitude-only values.
|
|
684
|
+
*
|
|
685
|
+
* The patch loop quantizes the mesh into a fixed corner grid, so the same longitude is reused
|
|
686
|
+
* across every latitude row and each tentacle/lobe value is computed once per frame instead
|
|
687
|
+
* of `latitudePatchCount * 4` times.
|
|
688
|
+
*
|
|
689
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
690
|
+
*/
|
|
691
|
+
function sampleBlobbyContinuousSurfacePointWithLongitudeCache(
|
|
692
|
+
options: BlobbyContinuousSurfaceOptions,
|
|
693
|
+
latitude: number,
|
|
694
|
+
longitude: number,
|
|
695
|
+
tentacleInfluence: BlobbyContinuousTentacleInfluence,
|
|
696
|
+
lowerLobeWave: number,
|
|
697
|
+
): Point3D {
|
|
698
|
+
const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
|
|
699
|
+
const cosineLatitude = Math.max(0, Math.cos(latitude));
|
|
700
|
+
const verticalProgress = (Math.sin(latitude) + 1) / 2;
|
|
701
|
+
const upperBlend = Math.pow(1 - verticalProgress, 1.32);
|
|
702
|
+
const lowerBlend = smoothStep(0.34, 1, verticalProgress);
|
|
703
|
+
const tipBlend = smoothStep(0.66, 1, verticalProgress);
|
|
704
|
+
const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
|
|
705
|
+
const effectiveLongitude =
|
|
706
|
+
longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.26 + tipBlend * 0.22);
|
|
707
|
+
const mantleRipple =
|
|
708
|
+
Math.sin(
|
|
709
|
+
longitude * morphologyProfile.body.lobeCount +
|
|
710
|
+
animationPhase * 0.6 +
|
|
711
|
+
timeMs / (1700 + morphologyProfile.body.lobeCount * 28),
|
|
712
|
+
) *
|
|
713
|
+
(0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.78) *
|
|
714
|
+
(0.32 + lowerBlend * 0.7);
|
|
715
|
+
const primaryTentacleWave =
|
|
716
|
+
Math.sin(timeMs / 740 + tentacleInfluence.primaryPhase + verticalProgress * 2.6) *
|
|
717
|
+
lowerBlend *
|
|
718
|
+
tentacleInfluence.core *
|
|
719
|
+
tentacleInfluence.swayScale;
|
|
720
|
+
const secondaryTentacleWave =
|
|
721
|
+
Math.sin(timeMs / 470 + tentacleInfluence.secondaryPhase + verticalProgress * 4.2) *
|
|
722
|
+
lowerBlend *
|
|
723
|
+
tentacleInfluence.core *
|
|
724
|
+
tentacleInfluence.swayScale *
|
|
725
|
+
0.42;
|
|
726
|
+
const tentacleCurl =
|
|
727
|
+
Math.sin(timeMs / 1180 + tentacleInfluence.primaryPhase * 0.6 + verticalProgress * 3.2) *
|
|
728
|
+
tipBlend *
|
|
729
|
+
tentacleInfluence.core *
|
|
730
|
+
tentacleInfluence.curlScale *
|
|
731
|
+
0.36;
|
|
732
|
+
const horizontalScale =
|
|
733
|
+
1.06 +
|
|
734
|
+
mantleRipple +
|
|
735
|
+
lowerBlend * (0.18 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.12) +
|
|
736
|
+
lowerBlend * tentacleInfluence.core * (0.22 + lowerLobeWave * 0.14) -
|
|
737
|
+
upperBlend * 0.08;
|
|
738
|
+
const depthScale =
|
|
739
|
+
1.08 +
|
|
740
|
+
upperBlend * 0.16 +
|
|
741
|
+
Math.max(0, Math.cos(effectiveLongitude)) * 0.12 +
|
|
742
|
+
lowerBlend * tentacleInfluence.core * (0.12 + tentacleInfluence.depthScale * 0.07) -
|
|
743
|
+
Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
|
|
744
|
+
const tentacleTubeRadius =
|
|
745
|
+
lowerBlend *
|
|
746
|
+
tentacleInfluence.core *
|
|
747
|
+
(0.12 + tipBlend * 0.07 + tentacleInfluence.widthScale * 0.028) *
|
|
748
|
+
radiusX;
|
|
749
|
+
const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
|
|
750
|
+
const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.74;
|
|
751
|
+
const lowerDrop =
|
|
752
|
+
lowerBlend *
|
|
753
|
+
radiusY *
|
|
754
|
+
(0.2 +
|
|
755
|
+
tentacleInfluence.core *
|
|
756
|
+
(0.42 +
|
|
757
|
+
tentacleInfluence.lengthScale * 0.24 +
|
|
758
|
+
(morphologyProfile.tentacles.flowLengthScale - 1) * 0.1));
|
|
759
|
+
const combinedTentacleSway = (primaryTentacleWave + secondaryTentacleWave) * radiusX * (0.06 + tipBlend * 0.06);
|
|
760
|
+
|
|
761
|
+
return {
|
|
762
|
+
x:
|
|
763
|
+
Math.sin(effectiveLongitude) * planarRadiusX +
|
|
764
|
+
combinedTentacleSway +
|
|
765
|
+
tentacleCurl * radiusX * 0.18,
|
|
766
|
+
y:
|
|
767
|
+
Math.sin(latitude) * radiusY * (1 + upperBlend * 0.14) -
|
|
768
|
+
upperBlend * radiusY * 0.12 +
|
|
769
|
+
lowerDrop +
|
|
770
|
+
Math.sin(timeMs / 1380 + animationPhase + latitude * 1.5) * lowerBlend * radiusY * 0.022 +
|
|
771
|
+
Math.cos(timeMs / 820 + tentacleInfluence.primaryPhase) *
|
|
772
|
+
lowerBlend *
|
|
773
|
+
tipBlend *
|
|
774
|
+
tentacleInfluence.core *
|
|
775
|
+
radiusY *
|
|
776
|
+
0.04,
|
|
777
|
+
z:
|
|
778
|
+
Math.cos(effectiveLongitude) * planarRadiusZ +
|
|
779
|
+
Math.cos(timeMs / 960 + tentacleInfluence.primaryPhase + verticalProgress) *
|
|
780
|
+
lowerBlend *
|
|
781
|
+
tentacleInfluence.core *
|
|
782
|
+
radiusZ *
|
|
783
|
+
0.044 +
|
|
784
|
+
tentacleCurl * radiusZ * 0.14,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Blends nearby seeded tentacle profiles at one mesh longitude.
|
|
790
|
+
*
|
|
791
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
792
|
+
*/
|
|
793
|
+
function resolveBlobbyTentacleInfluence(
|
|
794
|
+
options: BlobbyContinuousSurfaceOptions,
|
|
795
|
+
longitude: number,
|
|
796
|
+
): BlobbyContinuousTentacleInfluence {
|
|
797
|
+
let totalWeight = 0;
|
|
798
|
+
let weightedSin = 0;
|
|
799
|
+
let weightedCos = 0;
|
|
800
|
+
let weightedWidthScale = 0;
|
|
801
|
+
let weightedLengthScale = 0;
|
|
802
|
+
let weightedSwayScale = 0;
|
|
803
|
+
let weightedDepthScale = 0;
|
|
804
|
+
let weightedCurlScale = 0;
|
|
805
|
+
let weightedPrimaryPhase = 0;
|
|
806
|
+
let weightedSecondaryPhase = 0;
|
|
807
|
+
|
|
808
|
+
for (const tentacleProfile of options.tentacleProfiles) {
|
|
809
|
+
const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
|
|
810
|
+
const width = 0.22 * tentacleProfile.widthScale;
|
|
811
|
+
const weight = Math.exp(-(distance * distance) / (width * width));
|
|
812
|
+
|
|
813
|
+
totalWeight += weight;
|
|
814
|
+
weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
|
|
815
|
+
weightedCos += Math.cos(tentacleProfile.centerLongitude) * weight;
|
|
816
|
+
weightedWidthScale += tentacleProfile.widthScale * weight;
|
|
817
|
+
weightedLengthScale += tentacleProfile.lengthScale * weight;
|
|
818
|
+
weightedSwayScale += tentacleProfile.swayScale * weight;
|
|
819
|
+
weightedDepthScale += tentacleProfile.depthScale * weight;
|
|
820
|
+
weightedCurlScale += tentacleProfile.curlScale * weight;
|
|
821
|
+
weightedPrimaryPhase += tentacleProfile.primaryPhase * weight;
|
|
822
|
+
weightedSecondaryPhase += tentacleProfile.secondaryPhase * weight;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (totalWeight < 0.0001) {
|
|
826
|
+
return {
|
|
827
|
+
core: 0,
|
|
828
|
+
centerLongitude: longitude,
|
|
829
|
+
widthScale: 1,
|
|
830
|
+
lengthScale: 1,
|
|
831
|
+
swayScale: 1,
|
|
832
|
+
depthScale: 1,
|
|
833
|
+
curlScale: 1,
|
|
834
|
+
primaryPhase: 0,
|
|
835
|
+
secondaryPhase: 0,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return {
|
|
840
|
+
core: clampNumber(totalWeight, 0, 1),
|
|
841
|
+
centerLongitude: Math.atan2(weightedSin / totalWeight, weightedCos / totalWeight),
|
|
842
|
+
widthScale: weightedWidthScale / totalWeight,
|
|
843
|
+
lengthScale: weightedLengthScale / totalWeight,
|
|
844
|
+
swayScale: weightedSwayScale / totalWeight,
|
|
845
|
+
depthScale: weightedDepthScale / totalWeight,
|
|
846
|
+
curlScale: weightedCurlScale / totalWeight,
|
|
847
|
+
primaryPhase: weightedPrimaryPhase / totalWeight,
|
|
848
|
+
secondaryPhase: weightedSecondaryPhase / totalWeight,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
|
|
854
|
+
*
|
|
855
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
856
|
+
*/
|
|
857
|
+
function resolveBlobbyLobeWave(options: BlobbyContinuousSurfaceOptions, longitude: number): number {
|
|
858
|
+
const { morphologyProfile, animationPhase, timeMs } = options;
|
|
859
|
+
|
|
860
|
+
return (
|
|
861
|
+
(Math.cos(
|
|
862
|
+
longitude * OCTOPUS_TENTACLE_COUNT +
|
|
863
|
+
animationPhase +
|
|
864
|
+
timeMs / (940 + morphologyProfile.body.lobeCount * 18),
|
|
865
|
+
) +
|
|
866
|
+
1) /
|
|
867
|
+
2
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Resolves one base fill tone for a patch on the continuous octopus mesh.
|
|
873
|
+
*
|
|
874
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
875
|
+
*/
|
|
876
|
+
function resolveBlobbySurfacePatchFillStyle(
|
|
877
|
+
palette: AvatarPalette,
|
|
878
|
+
verticalProgress: number,
|
|
879
|
+
forwardness: number,
|
|
880
|
+
tentacleCore: number,
|
|
881
|
+
lowerLobeWave: number,
|
|
882
|
+
): string {
|
|
883
|
+
const tonalProgress = clampNumber(
|
|
884
|
+
verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08,
|
|
885
|
+
0,
|
|
886
|
+
1,
|
|
887
|
+
);
|
|
888
|
+
|
|
889
|
+
if (tonalProgress < 0.12) {
|
|
890
|
+
return palette.highlight;
|
|
891
|
+
}
|
|
892
|
+
if (tonalProgress < 0.3) {
|
|
893
|
+
return palette.secondary;
|
|
894
|
+
}
|
|
895
|
+
if (tonalProgress < 0.72) {
|
|
896
|
+
return forwardness > 0.56 ? palette.secondary : palette.primary;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Draws one projected mesh patch with multi-pass shading and a soft edge.
|
|
904
|
+
*
|
|
905
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
906
|
+
*/
|
|
907
|
+
function drawBlobbyContinuousSurfacePatch(
|
|
908
|
+
context: CanvasRenderingContext2D,
|
|
909
|
+
surfacePatch: BlobbyContinuousSurfacePatch,
|
|
910
|
+
): void {
|
|
911
|
+
drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
|
|
912
|
+
|
|
913
|
+
if (surfacePatch.lightIntensity > 0) {
|
|
914
|
+
drawProjectedQuad(
|
|
915
|
+
context,
|
|
916
|
+
surfacePatch.corners,
|
|
917
|
+
`rgba(255, 255, 255, ${0.2 * surfacePatch.lightIntensity})`,
|
|
918
|
+
);
|
|
919
|
+
} else if (surfacePatch.lightIntensity < 0) {
|
|
920
|
+
drawProjectedQuad(
|
|
921
|
+
context,
|
|
922
|
+
surfacePatch.corners,
|
|
923
|
+
`rgba(0, 0, 0, ${0.26 * Math.abs(surfacePatch.lightIntensity)})`,
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (surfacePatch.rimLightIntensity > 0.04) {
|
|
928
|
+
drawProjectedQuad(
|
|
929
|
+
context,
|
|
930
|
+
surfacePatch.corners,
|
|
931
|
+
`rgba(255, 247, 230, ${0.22 * surfacePatch.rimLightIntensity})`,
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
context.save();
|
|
936
|
+
context.beginPath();
|
|
937
|
+
context.moveTo(surfacePatch.corners[0].x, surfacePatch.corners[0].y);
|
|
938
|
+
for (let cornerIndex = 1; cornerIndex < surfacePatch.corners.length; cornerIndex++) {
|
|
939
|
+
context.lineTo(surfacePatch.corners[cornerIndex]!.x, surfacePatch.corners[cornerIndex]!.y);
|
|
940
|
+
}
|
|
941
|
+
context.closePath();
|
|
942
|
+
context.strokeStyle = surfacePatch.outlineColor;
|
|
943
|
+
context.lineWidth = Math.max(0.6, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0026);
|
|
944
|
+
context.lineJoin = 'round';
|
|
945
|
+
context.stroke();
|
|
946
|
+
context.restore();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Draws projected mantle-current lines on the front of the mesh.
|
|
951
|
+
*
|
|
952
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
953
|
+
*/
|
|
954
|
+
function drawBlobbyContinuousCurrents(options: {
|
|
955
|
+
readonly context: CanvasRenderingContext2D;
|
|
956
|
+
readonly surfaceOptions: BlobbyContinuousSurfaceOptions;
|
|
957
|
+
readonly center: Point3D;
|
|
958
|
+
readonly rotationX: number;
|
|
959
|
+
readonly rotationY: number;
|
|
960
|
+
readonly sceneCenterX: number;
|
|
961
|
+
readonly sceneCenterY: number;
|
|
962
|
+
readonly size: number;
|
|
963
|
+
readonly palette: AvatarPalette;
|
|
964
|
+
readonly morphologyProfile: Octopus3MorphologyProfile;
|
|
965
|
+
readonly timeMs: number;
|
|
966
|
+
readonly animationPhase: number;
|
|
967
|
+
}): void {
|
|
968
|
+
const {
|
|
969
|
+
context,
|
|
970
|
+
surfaceOptions,
|
|
971
|
+
center,
|
|
972
|
+
rotationX,
|
|
973
|
+
rotationY,
|
|
974
|
+
sceneCenterX,
|
|
975
|
+
sceneCenterY,
|
|
976
|
+
size,
|
|
977
|
+
palette,
|
|
978
|
+
morphologyProfile,
|
|
979
|
+
timeMs,
|
|
980
|
+
animationPhase,
|
|
981
|
+
} = options;
|
|
982
|
+
const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
|
|
983
|
+
const centerIndex = (currentCount - 1) / 2;
|
|
984
|
+
|
|
985
|
+
context.save();
|
|
986
|
+
context.lineCap = 'round';
|
|
987
|
+
context.lineJoin = 'round';
|
|
988
|
+
|
|
989
|
+
for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
|
|
990
|
+
const baseLongitude = (currentIndex - centerIndex) * 0.15;
|
|
991
|
+
const projectedPoints: Array<ProjectedPoint> = [];
|
|
992
|
+
|
|
993
|
+
for (let sampleIndex = 0; sampleIndex < 9; sampleIndex++) {
|
|
994
|
+
const progress = sampleIndex / 8;
|
|
995
|
+
const latitude = -0.48 + progress * 0.78;
|
|
996
|
+
const longitude =
|
|
997
|
+
baseLongitude + Math.sin(timeMs / 1140 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.038;
|
|
998
|
+
const scenePoint = transformScenePoint(
|
|
999
|
+
sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude),
|
|
1000
|
+
center,
|
|
1001
|
+
rotationX,
|
|
1002
|
+
rotationY,
|
|
1003
|
+
);
|
|
1004
|
+
|
|
1005
|
+
if (scenePoint.z > center.z - size * 0.016) {
|
|
1006
|
+
projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (projectedPoints.length < 3) {
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
context.beginPath();
|
|
1015
|
+
context.moveTo(projectedPoints[0]!.x, projectedPoints[0]!.y);
|
|
1016
|
+
for (const projectedPoint of projectedPoints.slice(1)) {
|
|
1017
|
+
context.lineTo(projectedPoint.x, projectedPoint.y);
|
|
1018
|
+
}
|
|
1019
|
+
context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}3d` : `${palette.accent}33`;
|
|
1020
|
+
context.lineWidth = size * (0.0055 + currentIndex * 0.00045);
|
|
1021
|
+
context.stroke();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
context.restore();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Draws small projected sucker highlights on the waving lower mesh lobes.
|
|
1029
|
+
*
|
|
1030
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
1031
|
+
*/
|
|
1032
|
+
function drawBlobbyContinuousSuckers(options: {
|
|
1033
|
+
readonly context: CanvasRenderingContext2D;
|
|
1034
|
+
readonly surfaceOptions: BlobbyContinuousSurfaceOptions;
|
|
1035
|
+
readonly center: Point3D;
|
|
1036
|
+
readonly rotationX: number;
|
|
1037
|
+
readonly rotationY: number;
|
|
1038
|
+
readonly sceneCenterX: number;
|
|
1039
|
+
readonly sceneCenterY: number;
|
|
1040
|
+
readonly size: number;
|
|
1041
|
+
readonly palette: AvatarPalette;
|
|
1042
|
+
}): void {
|
|
1043
|
+
const { surfaceOptions, size } = options;
|
|
1044
|
+
const { timeMs } = surfaceOptions;
|
|
1045
|
+
|
|
1046
|
+
for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
|
|
1047
|
+
if (Math.cos(tentacleProfile.centerLongitude) < -0.16) {
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
for (let suckerIndex = 0; suckerIndex < 4; suckerIndex++) {
|
|
1052
|
+
const latitude = 0.5 + suckerIndex * 0.12;
|
|
1053
|
+
const sideOffset = tentacleProfile.suckerSide * (0.038 + suckerIndex * 0.014) * tentacleProfile.widthScale;
|
|
1054
|
+
const waveOffset = Math.sin(timeMs / 880 + tentacleProfile.primaryPhase + suckerIndex * 0.78) * 0.02;
|
|
1055
|
+
|
|
1056
|
+
drawBlobbyContinuousSurfaceSpot({
|
|
1057
|
+
...options,
|
|
1058
|
+
latitude,
|
|
1059
|
+
longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
|
|
1060
|
+
radiusScale: size * (0.0068 - suckerIndex * 0.0006),
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Draws seeded pigment spots across the upper mesh for a richer skin texture.
|
|
1068
|
+
*
|
|
1069
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
1070
|
+
*/
|
|
1071
|
+
function drawBlobbySkinSpots(options: {
|
|
1072
|
+
readonly context: CanvasRenderingContext2D;
|
|
1073
|
+
readonly surfaceOptions: BlobbyContinuousSurfaceOptions;
|
|
1074
|
+
readonly center: Point3D;
|
|
1075
|
+
readonly rotationX: number;
|
|
1076
|
+
readonly rotationY: number;
|
|
1077
|
+
readonly sceneCenterX: number;
|
|
1078
|
+
readonly sceneCenterY: number;
|
|
1079
|
+
readonly size: number;
|
|
1080
|
+
readonly palette: AvatarPalette;
|
|
1081
|
+
readonly skinSpots: ReadonlyArray<BlobbySkinSpot>;
|
|
1082
|
+
}): void {
|
|
1083
|
+
const {
|
|
1084
|
+
context,
|
|
1085
|
+
surfaceOptions,
|
|
1086
|
+
center,
|
|
1087
|
+
rotationX,
|
|
1088
|
+
rotationY,
|
|
1089
|
+
sceneCenterX,
|
|
1090
|
+
sceneCenterY,
|
|
1091
|
+
size,
|
|
1092
|
+
palette,
|
|
1093
|
+
skinSpots,
|
|
1094
|
+
} = options;
|
|
1095
|
+
|
|
1096
|
+
for (const skinSpot of skinSpots) {
|
|
1097
|
+
const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, skinSpot.latitude, skinSpot.longitude);
|
|
1098
|
+
const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
|
|
1099
|
+
|
|
1100
|
+
if (sceneCenterPoint.z <= center.z - size * 0.01) {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
|
|
1105
|
+
const spotRadius = size * skinSpot.radiusScale;
|
|
1106
|
+
|
|
1107
|
+
context.save();
|
|
1108
|
+
context.beginPath();
|
|
1109
|
+
context.arc(projectedCenterPoint.x, projectedCenterPoint.y, spotRadius, 0, Math.PI * 2);
|
|
1110
|
+
context.fillStyle = `${palette.shadow}${formatAlphaHex(skinSpot.opacity)}`;
|
|
1111
|
+
context.fill();
|
|
1112
|
+
context.restore();
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Draws one tiny projected surface spot by sampling local mesh tangents.
|
|
1118
|
+
*
|
|
1119
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
1120
|
+
*/
|
|
1121
|
+
function drawBlobbyContinuousSurfaceSpot(options: {
|
|
1122
|
+
readonly context: CanvasRenderingContext2D;
|
|
1123
|
+
readonly surfaceOptions: BlobbyContinuousSurfaceOptions;
|
|
1124
|
+
readonly center: Point3D;
|
|
1125
|
+
readonly rotationX: number;
|
|
1126
|
+
readonly rotationY: number;
|
|
1127
|
+
readonly sceneCenterX: number;
|
|
1128
|
+
readonly sceneCenterY: number;
|
|
1129
|
+
readonly size: number;
|
|
1130
|
+
readonly palette: AvatarPalette;
|
|
1131
|
+
readonly latitude: number;
|
|
1132
|
+
readonly longitude: number;
|
|
1133
|
+
readonly radiusScale: number;
|
|
1134
|
+
}): void {
|
|
1135
|
+
const {
|
|
1136
|
+
context,
|
|
1137
|
+
surfaceOptions,
|
|
1138
|
+
center,
|
|
1139
|
+
rotationX,
|
|
1140
|
+
rotationY,
|
|
1141
|
+
sceneCenterX,
|
|
1142
|
+
sceneCenterY,
|
|
1143
|
+
size,
|
|
1144
|
+
palette,
|
|
1145
|
+
latitude,
|
|
1146
|
+
longitude,
|
|
1147
|
+
radiusScale,
|
|
1148
|
+
} = options;
|
|
1149
|
+
const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude);
|
|
1150
|
+
const localHorizontal = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
|
|
1151
|
+
const localVertical = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
|
|
1152
|
+
const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
|
|
1153
|
+
|
|
1154
|
+
if (sceneCenterPoint.z <= center.z - size * 0.012) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
|
|
1159
|
+
const projectedHorizontalPoint = projectScenePoint(
|
|
1160
|
+
transformScenePoint(localHorizontal, center, rotationX, rotationY),
|
|
1161
|
+
size,
|
|
1162
|
+
sceneCenterX,
|
|
1163
|
+
sceneCenterY,
|
|
1164
|
+
);
|
|
1165
|
+
const projectedVerticalPoint = projectScenePoint(
|
|
1166
|
+
transformScenePoint(localVertical, center, rotationX, rotationY),
|
|
1167
|
+
size,
|
|
1168
|
+
sceneCenterX,
|
|
1169
|
+
sceneCenterY,
|
|
1170
|
+
);
|
|
1171
|
+
const horizontalRadius = clampNumber(
|
|
1172
|
+
Math.hypot(
|
|
1173
|
+
projectedHorizontalPoint.x - projectedCenterPoint.x,
|
|
1174
|
+
projectedHorizontalPoint.y - projectedCenterPoint.y,
|
|
1175
|
+
) *
|
|
1176
|
+
radiusScale *
|
|
1177
|
+
0.78,
|
|
1178
|
+
size * 0.003,
|
|
1179
|
+
size * 0.02,
|
|
1180
|
+
);
|
|
1181
|
+
const verticalRadius = clampNumber(
|
|
1182
|
+
Math.hypot(
|
|
1183
|
+
projectedVerticalPoint.x - projectedCenterPoint.x,
|
|
1184
|
+
projectedVerticalPoint.y - projectedCenterPoint.y,
|
|
1185
|
+
) *
|
|
1186
|
+
radiusScale *
|
|
1187
|
+
0.54,
|
|
1188
|
+
size * 0.0024,
|
|
1189
|
+
size * 0.015,
|
|
1190
|
+
);
|
|
1191
|
+
const rotation = Math.atan2(
|
|
1192
|
+
projectedHorizontalPoint.y - projectedCenterPoint.y,
|
|
1193
|
+
projectedHorizontalPoint.x - projectedCenterPoint.x,
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
context.save();
|
|
1197
|
+
context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
|
|
1198
|
+
context.rotate(rotation);
|
|
1199
|
+
context.beginPath();
|
|
1200
|
+
context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
|
|
1201
|
+
context.fillStyle = `${palette.highlight}80`;
|
|
1202
|
+
context.fill();
|
|
1203
|
+
context.strokeStyle = `${palette.highlight}a8`;
|
|
1204
|
+
context.lineWidth = Math.max(0.7, size * 0.0028);
|
|
1205
|
+
context.stroke();
|
|
1206
|
+
context.restore();
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Draws a soft, slowly drifting gloss highlight on the front of the mesh.
|
|
1211
|
+
*
|
|
1212
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
1213
|
+
*/
|
|
1214
|
+
function drawBlobbyContinuousGloss(options: {
|
|
1215
|
+
readonly context: CanvasRenderingContext2D;
|
|
1216
|
+
readonly surfaceOptions: BlobbyContinuousSurfaceOptions;
|
|
1217
|
+
readonly center: Point3D;
|
|
1218
|
+
readonly rotationX: number;
|
|
1219
|
+
readonly rotationY: number;
|
|
1220
|
+
readonly sceneCenterX: number;
|
|
1221
|
+
readonly sceneCenterY: number;
|
|
1222
|
+
readonly size: number;
|
|
1223
|
+
readonly palette: AvatarPalette;
|
|
1224
|
+
}): void {
|
|
1225
|
+
const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size } = options;
|
|
1226
|
+
const { timeMs, animationPhase } = surfaceOptions;
|
|
1227
|
+
const glossLatitude = -0.3 + Math.sin(timeMs / 2700 + animationPhase) * 0.04;
|
|
1228
|
+
const glossLongitude = -0.18 + Math.cos(timeMs / 2300 + animationPhase * 0.8) * 0.05;
|
|
1229
|
+
const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, glossLatitude, glossLongitude);
|
|
1230
|
+
const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
|
|
1231
|
+
|
|
1232
|
+
if (sceneCenterPoint.z <= center.z) {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
|
|
1237
|
+
const glossRadius = size * 0.058;
|
|
1238
|
+
|
|
1239
|
+
context.save();
|
|
1240
|
+
const glossGradient = context.createRadialGradient(
|
|
1241
|
+
projectedCenterPoint.x - glossRadius * 0.3,
|
|
1242
|
+
projectedCenterPoint.y - glossRadius * 0.4,
|
|
1243
|
+
glossRadius * 0.04,
|
|
1244
|
+
projectedCenterPoint.x,
|
|
1245
|
+
projectedCenterPoint.y,
|
|
1246
|
+
glossRadius,
|
|
1247
|
+
);
|
|
1248
|
+
glossGradient.addColorStop(0, 'rgba(255, 255, 255, 0.34)');
|
|
1249
|
+
glossGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
|
|
1250
|
+
glossGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
|
1251
|
+
context.fillStyle = glossGradient;
|
|
1252
|
+
context.beginPath();
|
|
1253
|
+
context.ellipse(
|
|
1254
|
+
projectedCenterPoint.x,
|
|
1255
|
+
projectedCenterPoint.y,
|
|
1256
|
+
glossRadius * 1.05,
|
|
1257
|
+
glossRadius * 0.78,
|
|
1258
|
+
0,
|
|
1259
|
+
0,
|
|
1260
|
+
Math.PI * 2,
|
|
1261
|
+
);
|
|
1262
|
+
context.fill();
|
|
1263
|
+
context.restore();
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Resolves a signed angular distance from the source longitude to the target longitude.
|
|
1268
|
+
*
|
|
1269
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
1270
|
+
*/
|
|
1271
|
+
function resolveSignedAngularDistance(sourceLongitude: number, targetLongitude: number): number {
|
|
1272
|
+
return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Smoothly maps a value between two bounds into `[0, 1]`.
|
|
1277
|
+
*
|
|
1278
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
1279
|
+
*/
|
|
1280
|
+
function smoothStep(edgeStart: number, edgeEnd: number, value: number): number {
|
|
1281
|
+
const progress = clampNumber((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
|
|
1282
|
+
|
|
1283
|
+
return progress * progress * (3 - 2 * progress);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Converts an opacity ratio into a two-digit hexadecimal alpha suffix.
|
|
1288
|
+
*
|
|
1289
|
+
* @private helper of `octopus3d4AvatarVisual`
|
|
1290
|
+
*/
|
|
1291
|
+
function formatAlphaHex(opacity: number): string {
|
|
1292
|
+
return Math.round(clampNumber(opacity, 0, 1) * 255)
|
|
1293
|
+
.toString(16)
|
|
1294
|
+
.padStart(2, '0');
|
|
1295
|
+
}
|