@promptbook/core 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/esm/index.es.js CHANGED
@@ -28,7 +28,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
28
28
  * @generated
29
29
  * @see https://github.com/webgptorg/promptbook
30
30
  */
31
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-134';
31
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-135';
32
32
  /**
33
33
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
34
34
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -9898,6 +9898,11 @@ function isVoidPseudoAgentReference(rawReference) {
9898
9898
  * - `isValidAgentUrl` *(this one)* which tests just agent URL
9899
9899
  * - `isValidPipelineUrl` which tests just pipeline URL
9900
9900
  *
9901
+ * Note: This is a pure structural validator and does not block private/internal network
9902
+ * addresses. Callers that fetch the URL server-side from an untrusted network context must
9903
+ * additionally apply the `assertSafeUrl` SSRF guard from the Agents Server before any
9904
+ * outbound request.
9905
+ *
9901
9906
  * @public exported from `@promptbook/utils`
9902
9907
  */
9903
9908
  function isValidAgentUrl(url) {
@@ -9911,12 +9916,6 @@ function isValidAgentUrl(url) {
9911
9916
  // TODO: [🐠]
9912
9917
  return false;
9913
9918
  }
9914
- /*
9915
- Note: [👣][🧠] Is it secure to allow pipeline URLs on private and unsecured networks?
9916
- if (isUrlOnPrivateNetwork(url)) {
9917
- return false;
9918
- }
9919
- */
9920
9919
  return true;
9921
9920
  }
9922
9921
  // TODO: [🐠] Maybe more info why the URL is invalid
@@ -13546,7 +13545,7 @@ function fillTextureRect(texture, x, y, width, height, color) {
13546
13545
  *
13547
13546
  * @private helper of `minecraft2AvatarVisual`
13548
13547
  */
13549
- const LIGHT_DIRECTION$3 = normalizeVector3({
13548
+ const LIGHT_DIRECTION$4 = normalizeVector3({
13550
13549
  x: 0.4,
13551
13550
  y: -0.65,
13552
13551
  z: 0.92,
@@ -13770,7 +13769,7 @@ function resolveVisibleCuboidFaces(cuboid, size, sceneCenterX, sceneCenterY) {
13770
13769
  corners: projectedCorners,
13771
13770
  texture: faceDefinition.texture,
13772
13771
  averageDepth: transformedCorners.reduce((depthSum, corner) => depthSum + corner.z, 0) / transformedCorners.length,
13773
- lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$3), -1, 1),
13772
+ lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$4), -1, 1),
13774
13773
  outlineColor: cuboid.outlineColor,
13775
13774
  };
13776
13775
  });
@@ -14457,7 +14456,7 @@ function resolveSeededIntegerRange(random, minimumValue, maximumValue) {
14457
14456
  *
14458
14457
  * @private helper of `octopus3AvatarVisual`
14459
14458
  */
14460
- function formatAlphaHex$1(opacity) {
14459
+ function formatAlphaHex$2(opacity) {
14461
14460
  return Math.round(Math.min(1, Math.max(0, opacity)) * 255)
14462
14461
  .toString(16)
14463
14462
  .padStart(2, '0');
@@ -14836,7 +14835,7 @@ function drawSeededEye(context, centerX, centerY, radiusX, radiusY, rotation, pa
14836
14835
  context.beginPath();
14837
14836
  context.moveTo(-radiusX * 0.74, radiusY * 0.2);
14838
14837
  context.quadraticCurveTo(0, radiusY * 0.38, radiusX * 0.74, radiusY * 0.2);
14839
- context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
14838
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$2(eyeStyle.lowerLidOpacity)}`;
14840
14839
  context.lineWidth = radiusX * 0.08;
14841
14840
  context.lineCap = 'round';
14842
14841
  context.stroke();
@@ -14917,7 +14916,7 @@ function drawProjectedOrganicEye(context, localCenter, radiusX, radiusY, center,
14917
14916
  context.beginPath();
14918
14917
  context.moveTo(-projectedRadiusX * 0.74, projectedRadiusY * 0.2);
14919
14918
  context.quadraticCurveTo(0, projectedRadiusY * 0.38, projectedRadiusX * 0.74, projectedRadiusY * 0.2);
14920
- context.strokeStyle = `${palette.highlight}${formatAlphaHex(eyeStyle.lowerLidOpacity)}`;
14919
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
14921
14920
  context.lineWidth = projectedRadiusX * 0.08;
14922
14921
  context.lineCap = 'round';
14923
14922
  context.stroke();
@@ -14963,7 +14962,7 @@ function drawProjectedQuad(context, corners, fillStyle) {
14963
14962
  *
14964
14963
  * @private helper of the 3D octopus avatar visuals
14965
14964
  */
14966
- function formatAlphaHex(opacity) {
14965
+ function formatAlphaHex$1(opacity) {
14967
14966
  return Math.round(clampNumber$1(opacity, 0, 1) * 255)
14968
14967
  .toString(16)
14969
14968
  .padStart(2, '0');
@@ -14975,7 +14974,7 @@ function formatAlphaHex(opacity) {
14975
14974
  *
14976
14975
  * @private helper of `octopus3dAvatarVisual`
14977
14976
  */
14978
- const LIGHT_DIRECTION$2 = normalizeVector3({
14977
+ const LIGHT_DIRECTION$3 = normalizeVector3({
14979
14978
  x: 0.48,
14980
14979
  y: -0.62,
14981
14980
  z: 0.94,
@@ -15225,7 +15224,7 @@ function resolveVisibleEllipsoidPatches(options) {
15225
15224
  corners: projectedCorners,
15226
15225
  averageDepth: transformedCorners.reduce((depthSum, transformedCorner) => depthSum + transformedCorner.z, 0) /
15227
15226
  transformedCorners.length,
15228
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
15227
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$3), -1, 1),
15229
15228
  fillStyle: resolveSurfacePatchFillStyle(palette, verticalProgress + verticalColorBias),
15230
15229
  outlineColor,
15231
15230
  });
@@ -15414,7 +15413,7 @@ function resolveEllipsoidSurfaceDepth(radiusX, radiusY, radiusZ, x, y) {
15414
15413
  *
15415
15414
  * @private helper of `octopus3d2AvatarVisual`
15416
15415
  */
15417
- const LIGHT_DIRECTION$1 = normalizeVector3({
15416
+ const LIGHT_DIRECTION$2 = normalizeVector3({
15418
15417
  x: 0.38,
15419
15418
  y: -0.6,
15420
15419
  z: 0.98,
@@ -15573,13 +15572,13 @@ function drawBlobbyOctopusShadow(context, size, palette, interaction, timeMs, mo
15573
15572
  *
15574
15573
  * @private helper of `octopus3d2AvatarVisual`
15575
15574
  */
15576
- const LATITUDE_PATCH_COUNT$1 = 12;
15575
+ const LATITUDE_PATCH_COUNT$2 = 12;
15577
15576
  /**
15578
15577
  * Number of longitude segments used by the single blobby octopus mesh.
15579
15578
  *
15580
15579
  * @private helper of `octopus3d2AvatarVisual`
15581
15580
  */
15582
- const LONGITUDE_PATCH_COUNT$1 = 24;
15581
+ const LONGITUDE_PATCH_COUNT$2 = 24;
15583
15582
  /**
15584
15583
  * Resolves all visible projected patches for the single blobby octopus mesh.
15585
15584
  *
@@ -15593,8 +15592,8 @@ const LONGITUDE_PATCH_COUNT$1 = 24;
15593
15592
  */
15594
15593
  function resolveVisibleBlobbyOctopusPatches(options) {
15595
15594
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, animationPhase, timeMs, } = options;
15596
- const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
15597
- const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
15595
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$2;
15596
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$2;
15598
15597
  const surfacePatches = [];
15599
15598
  const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
15600
15599
  const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
@@ -15653,8 +15652,8 @@ function resolveVisibleBlobbyOctopusPatches(options) {
15653
15652
  transformedCorners[2].z +
15654
15653
  transformedCorners[3].z) /
15655
15654
  4,
15656
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
15657
- fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
15655
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
15656
+ fillStyle: resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
15658
15657
  outlineColor: verticalProgress < 0.58 ? `${palette.highlight}73` : `${palette.shadow}8a`,
15659
15658
  });
15660
15659
  }
@@ -15722,7 +15721,7 @@ function resolveLowerLobeWave(longitude, morphologyProfile, animationPhase, time
15722
15721
  *
15723
15722
  * @private helper of `octopus3d2AvatarVisual`
15724
15723
  */
15725
- function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, lowerLobeWave) {
15724
+ function resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, forwardness, lowerLobeWave) {
15726
15725
  const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.12 - forwardness * 0.07, 0, 1);
15727
15726
  if (tonalProgress < 0.16) {
15728
15727
  return palette.highlight;
@@ -15768,7 +15767,7 @@ function drawBlobbySurfacePatch(context, surfacePatch) {
15768
15767
  *
15769
15768
  * @private helper of `octopus3d3AvatarVisual`
15770
15769
  */
15771
- const LIGHT_DIRECTION = normalizeVector3({
15770
+ const LIGHT_DIRECTION$1 = normalizeVector3({
15772
15771
  x: 0.34,
15773
15772
  y: -0.62,
15774
15773
  z: 1,
@@ -15778,7 +15777,7 @@ const LIGHT_DIRECTION = normalizeVector3({
15778
15777
  *
15779
15778
  * @private helper of `octopus3d3AvatarVisual`
15780
15779
  */
15781
- const OCTOPUS_TENTACLE_COUNT = 8;
15780
+ const OCTOPUS_TENTACLE_COUNT$1 = 8;
15782
15781
  /**
15783
15782
  * Cache keyed by the `createRandom` factory reference, which is stable for the lifetime of one
15784
15783
  * mounted `<Avatar/>` component (created inside `resolveAvatarRenderDefinition` and held in a
@@ -15917,9 +15916,9 @@ const octopus3d3AvatarVisual = {
15917
15916
  * @private helper of `octopus3d3AvatarVisual`
15918
15917
  */
15919
15918
  function createContinuousTentacleProfiles(createRandom, morphologyProfile) {
15920
- return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
15919
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT$1 }, (_, tentacleIndex) => {
15921
15920
  const tentacleRandom = createRandom(`octopus3d3-tentacle-${tentacleIndex}`);
15922
- const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
15921
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT$1 - 1);
15923
15922
  return {
15924
15923
  centerLongitude: -Math.PI * 0.86 +
15925
15924
  progress * Math.PI * 1.72 +
@@ -15986,13 +15985,13 @@ function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs
15986
15985
  *
15987
15986
  * @private helper of `octopus3d3AvatarVisual`
15988
15987
  */
15989
- const LATITUDE_PATCH_COUNT = 16;
15988
+ const LATITUDE_PATCH_COUNT$1 = 16;
15990
15989
  /**
15991
15990
  * Number of longitude segments used by the continuous Octopus 3D 3 mesh.
15992
15991
  *
15993
15992
  * @private helper of `octopus3d3AvatarVisual`
15994
15993
  */
15995
- const LONGITUDE_PATCH_COUNT = 40;
15994
+ const LONGITUDE_PATCH_COUNT$1 = 40;
15996
15995
  /**
15997
15996
  * Resolves visible projected patches for the continuous octopus mesh.
15998
15997
  *
@@ -16006,8 +16005,8 @@ const LONGITUDE_PATCH_COUNT = 40;
16006
16005
  */
16007
16006
  function resolveVisibleContinuousOctopusPatches(options) {
16008
16007
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
16009
- const latitudePatchCount = LATITUDE_PATCH_COUNT;
16010
- const longitudePatchCount = LONGITUDE_PATCH_COUNT;
16008
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
16009
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
16011
16010
  const surfacePatches = [];
16012
16011
  const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
16013
16012
  const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
@@ -16075,7 +16074,7 @@ function resolveVisibleContinuousOctopusPatches(options) {
16075
16074
  transformedCorners[2].z +
16076
16075
  transformedCorners[3].z) /
16077
16076
  4,
16078
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1),
16077
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
16079
16078
  fillStyle: resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
16080
16079
  outlineColor: verticalProgress < 0.54 ? `${palette.highlight}69` : `${palette.shadow}78`,
16081
16080
  });
@@ -16109,9 +16108,9 @@ function sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude
16109
16108
  const cosineLatitude = Math.max(0, Math.cos(latitude));
16110
16109
  const verticalProgress = (Math.sin(latitude) + 1) / 2;
16111
16110
  const upperBlend = Math.pow(1 - verticalProgress, 1.28);
16112
- const lowerBlend = smoothStep(0.38, 1, verticalProgress);
16113
- const tipBlend = smoothStep(0.68, 1, verticalProgress);
16114
- const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
16111
+ const lowerBlend = smoothStep$1(0.38, 1, verticalProgress);
16112
+ const tipBlend = smoothStep$1(0.68, 1, verticalProgress);
16113
+ const centerPull = resolveSignedAngularDistance$1(longitude, tentacleInfluence.centerLongitude);
16115
16114
  const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.24 + tipBlend * 0.2);
16116
16115
  const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
16117
16116
  animationPhase * 0.6 +
@@ -16177,7 +16176,7 @@ function resolveContinuousTentacleInfluence(options, longitude) {
16177
16176
  let weightedDepthScale = 0;
16178
16177
  let weightedPhase = 0;
16179
16178
  for (const tentacleProfile of options.tentacleProfiles) {
16180
- const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
16179
+ const distance = Math.abs(resolveSignedAngularDistance$1(longitude, tentacleProfile.centerLongitude));
16181
16180
  const width = 0.2 * tentacleProfile.widthScale;
16182
16181
  const weight = Math.exp(-(distance * distance) / (width * width));
16183
16182
  totalWeight += weight;
@@ -16217,7 +16216,7 @@ function resolveContinuousTentacleInfluence(options, longitude) {
16217
16216
  */
16218
16217
  function resolveContinuousLobeWave(options, longitude) {
16219
16218
  const { morphologyProfile, animationPhase, timeMs } = options;
16220
- return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT +
16219
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT$1 +
16221
16220
  animationPhase +
16222
16221
  timeMs / (980 + morphologyProfile.body.lobeCount * 18)) +
16223
16222
  1) /
@@ -16371,7 +16370,7 @@ function drawProjectedSurfaceSpot(options) {
16371
16370
  *
16372
16371
  * @private helper of `octopus3d3AvatarVisual`
16373
16372
  */
16374
- function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
16373
+ function resolveSignedAngularDistance$1(sourceLongitude, targetLongitude) {
16375
16374
  return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
16376
16375
  }
16377
16376
  /**
@@ -16379,11 +16378,767 @@ function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
16379
16378
  *
16380
16379
  * @private helper of `octopus3d3AvatarVisual`
16381
16380
  */
16382
- function smoothStep(edgeStart, edgeEnd, value) {
16381
+ function smoothStep$1(edgeStart, edgeEnd, value) {
16383
16382
  const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
16384
16383
  return progress * progress * (3 - 2 * progress);
16385
16384
  }
16386
16385
 
16386
+ /* eslint-disable no-magic-numbers */
16387
+ /**
16388
+ * Light direction used by the continuous Octopus 3D 4 mesh shading.
16389
+ *
16390
+ * @private helper of `octopus3d4AvatarVisual`
16391
+ */
16392
+ const LIGHT_DIRECTION = normalizeVector3({
16393
+ x: 0.32,
16394
+ y: -0.66,
16395
+ z: 1,
16396
+ });
16397
+ /**
16398
+ * Rim-light direction used to brighten the side of the silhouette.
16399
+ *
16400
+ * @private helper of `octopus3d4AvatarVisual`
16401
+ */
16402
+ const RIM_LIGHT_DIRECTION = normalizeVector3({
16403
+ x: -0.7,
16404
+ y: -0.18,
16405
+ z: 0.55,
16406
+ });
16407
+ /**
16408
+ * Real-octopus tentacle count used by the continuous lower mesh.
16409
+ *
16410
+ * @private helper of `octopus3d4AvatarVisual`
16411
+ */
16412
+ const OCTOPUS_TENTACLE_COUNT = 8;
16413
+ /**
16414
+ * Number of seeded skin spots painted across the octopus body.
16415
+ *
16416
+ * @private helper of `octopus3d4AvatarVisual`
16417
+ */
16418
+ const SKIN_SPOT_COUNT = 14;
16419
+ /**
16420
+ * Cache keyed by the `createRandom` factory reference, stable per mounted `<Avatar/>` component.
16421
+ *
16422
+ * @private helper of `octopus3d4AvatarVisual`
16423
+ */
16424
+ const octopus3d4StableStateCache = new WeakMap();
16425
+ /**
16426
+ * Returns the stable per-avatar state, computing it on first access and caching afterwards.
16427
+ *
16428
+ * @private helper of `octopus3d4AvatarVisual`
16429
+ */
16430
+ function getOctopus3d4StableState(createRandom) {
16431
+ const cached = octopus3d4StableStateCache.get(createRandom);
16432
+ if (cached !== undefined) {
16433
+ return cached;
16434
+ }
16435
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
16436
+ const animationRandom = createRandom('octopus3d4-animation-profile');
16437
+ const eyeRandom = createRandom('octopus3d4-eye-profile');
16438
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
16439
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
16440
+ const state = {
16441
+ morphologyProfile,
16442
+ animationPhase: animationRandom() * Math.PI * 2,
16443
+ leftEyePhaseOffset,
16444
+ rightEyePhaseOffset,
16445
+ tentacleProfiles: createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile),
16446
+ skinSpots: createBlobbySkinSpots(createRandom),
16447
+ };
16448
+ octopus3d4StableStateCache.set(createRandom, state);
16449
+ return state;
16450
+ }
16451
+ /**
16452
+ * Octopus 3D 4 avatar visual.
16453
+ *
16454
+ * @private built-in avatar visual
16455
+ */
16456
+ const octopus3d4AvatarVisual = {
16457
+ id: 'octopus3d4',
16458
+ title: 'Octopus 3D 4',
16459
+ description: '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.',
16460
+ isAnimated: true,
16461
+ supportsPointerTracking: true,
16462
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
16463
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles, skinSpots } = getOctopus3d4StableState(createRandom);
16464
+ const sceneCenterX = size * 0.5;
16465
+ const sceneCenterY = size * 0.535;
16466
+ const bob = Math.sin(timeMs / 980 + animationPhase) * size * 0.013;
16467
+ const meshCenter = {
16468
+ x: interaction.bodyOffsetX * size * 0.052 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
16469
+ y: -size * 0.07 + interaction.bodyOffsetY * size * 0.028 + bob,
16470
+ z: interaction.intensity * size * 0.02,
16471
+ };
16472
+ const rotationY = -0.08 +
16473
+ Math.sin(timeMs / 2800 + animationPhase) * 0.04 +
16474
+ interaction.bodyOffsetX * 0.24 +
16475
+ interaction.gazeX * 0.98;
16476
+ const rotationX = -0.07 +
16477
+ Math.cos(timeMs / 3200 + animationPhase * 0.7) * 0.02 -
16478
+ interaction.bodyOffsetY * 0.08 -
16479
+ interaction.gazeY * 0.42;
16480
+ const surfaceOptions = {
16481
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.12,
16482
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1,
16483
+ radiusZ: size *
16484
+ morphologyProfile.body.bodyRadiusRatio *
16485
+ (1.04 + (morphologyProfile.body.horizontalStretch - 1) * 0.2),
16486
+ morphologyProfile,
16487
+ timeMs,
16488
+ animationPhase,
16489
+ tentacleProfiles,
16490
+ };
16491
+ const surfacePatches = resolveVisibleBlobbyContinuousPatches({
16492
+ ...surfaceOptions,
16493
+ center: meshCenter,
16494
+ rotationX,
16495
+ rotationY,
16496
+ sceneCenterX,
16497
+ sceneCenterY,
16498
+ size,
16499
+ palette,
16500
+ });
16501
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.4 - 0.04, -0.24, 0.08);
16502
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.0, 0.18, 0.32);
16503
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.21 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
16504
+ const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
16505
+ const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
16506
+ const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
16507
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
16508
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
16509
+ drawAvatarFrame(context, size, palette);
16510
+ drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
16511
+ drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile);
16512
+ for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
16513
+ drawBlobbyContinuousSurfacePatch(context, surfacePatch);
16514
+ }
16515
+ drawBlobbySkinSpots({
16516
+ context,
16517
+ surfaceOptions,
16518
+ center: meshCenter,
16519
+ rotationX,
16520
+ rotationY,
16521
+ sceneCenterX,
16522
+ sceneCenterY,
16523
+ size,
16524
+ palette,
16525
+ skinSpots,
16526
+ });
16527
+ drawBlobbyContinuousCurrents({
16528
+ context,
16529
+ surfaceOptions,
16530
+ center: meshCenter,
16531
+ rotationX,
16532
+ rotationY,
16533
+ sceneCenterX,
16534
+ sceneCenterY,
16535
+ size,
16536
+ palette,
16537
+ morphologyProfile,
16538
+ timeMs,
16539
+ animationPhase,
16540
+ });
16541
+ drawBlobbyContinuousSuckers({
16542
+ context,
16543
+ surfaceOptions,
16544
+ center: meshCenter,
16545
+ rotationX,
16546
+ rotationY,
16547
+ sceneCenterX,
16548
+ sceneCenterY,
16549
+ size,
16550
+ palette,
16551
+ });
16552
+ drawBlobbyContinuousGloss({
16553
+ context,
16554
+ surfaceOptions,
16555
+ center: meshCenter,
16556
+ rotationX,
16557
+ rotationY,
16558
+ sceneCenterX,
16559
+ sceneCenterY,
16560
+ size,
16561
+ palette,
16562
+ });
16563
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
16564
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
16565
+ drawProjectedOrganicMouth(context, [
16566
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
16567
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
16568
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
16569
+ ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
16570
+ },
16571
+ };
16572
+ /**
16573
+ * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
16574
+ *
16575
+ * @private helper of `octopus3d4AvatarVisual`
16576
+ */
16577
+ function createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile) {
16578
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
16579
+ const tentacleRandom = createRandom(`octopus3d4-tentacle-${tentacleIndex}`);
16580
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
16581
+ return {
16582
+ centerLongitude: -Math.PI * 0.9 +
16583
+ progress * Math.PI * 1.8 +
16584
+ (tentacleRandom() - 0.5) * (0.06 + morphologyProfile.tentacles.rootSpreadScale * 0.025),
16585
+ widthScale: 0.92 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.18,
16586
+ lengthScale: 0.9 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.24,
16587
+ swayScale: 0.86 + tentacleRandom() * 0.4 + (morphologyProfile.tentacles.swayScale - 1) * 0.22,
16588
+ depthScale: 0.9 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.22,
16589
+ curlScale: 0.62 + tentacleRandom() * 0.46,
16590
+ primaryPhase: tentacleRandom() * Math.PI * 2,
16591
+ secondaryPhase: tentacleRandom() * Math.PI * 2,
16592
+ suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
16593
+ };
16594
+ });
16595
+ }
16596
+ /**
16597
+ * Creates seeded skin pigment spots distributed across the upper octopus mesh.
16598
+ *
16599
+ * @private helper of `octopus3d4AvatarVisual`
16600
+ */
16601
+ function createBlobbySkinSpots(createRandom) {
16602
+ const spotRandom = createRandom('octopus3d4-skin-spots');
16603
+ return Array.from({ length: SKIN_SPOT_COUNT }, () => ({
16604
+ latitude: -0.45 + spotRandom() * 0.7,
16605
+ longitude: -0.5 + spotRandom() * 1.0,
16606
+ radiusScale: 0.0028 + spotRandom() * 0.0052,
16607
+ opacity: 0.16 + spotRandom() * 0.22,
16608
+ }));
16609
+ }
16610
+ /**
16611
+ * Draws the soft underwater atmosphere behind the continuous octopus mesh.
16612
+ *
16613
+ * @private helper of `octopus3d4AvatarVisual`
16614
+ */
16615
+ function drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
16616
+ const glowGradient = context.createRadialGradient(sceneCenterX + interaction.gazeX * size * 0.12, sceneCenterY - size * 0.18 + interaction.gazeY * size * 0.05, size * 0.04, sceneCenterX, sceneCenterY, size * (0.68 + interaction.intensity * 0.025));
16617
+ glowGradient.addColorStop(0, `${palette.highlight}74`);
16618
+ glowGradient.addColorStop(0.32, `${palette.accent}32`);
16619
+ glowGradient.addColorStop(1, `${palette.highlight}00`);
16620
+ context.fillStyle = glowGradient;
16621
+ context.fillRect(0, 0, size, size);
16622
+ const lowerGradient = context.createRadialGradient(sceneCenterX + Math.sin(timeMs / 1500) * size * 0.05, sceneCenterY + size * 0.3, size * 0.06, sceneCenterX, sceneCenterY + size * 0.3, size * 0.56);
16623
+ lowerGradient.addColorStop(0, `${palette.secondary}28`);
16624
+ lowerGradient.addColorStop(1, `${palette.secondary}00`);
16625
+ context.fillStyle = lowerGradient;
16626
+ context.fillRect(0, 0, size, size);
16627
+ }
16628
+ /**
16629
+ * Draws the soft lower shadow that anchors the octopus in the avatar frame.
16630
+ *
16631
+ * @private helper of `octopus3d4AvatarVisual`
16632
+ */
16633
+ function drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
16634
+ const cx = size * 0.5 + interaction.gazeX * size * 0.046;
16635
+ const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
16636
+ const rx = size * (0.2 + morphologyProfile.tentacles.rootSpreadScale * 0.024 + interaction.intensity * 0.022);
16637
+ const ry = size * 0.062;
16638
+ context.save();
16639
+ context.translate(cx, cy);
16640
+ context.scale(1, ry / rx);
16641
+ const blurRadius = rx * 1.42;
16642
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
16643
+ shadowGradient.addColorStop(0, `${palette.shadow}82`);
16644
+ shadowGradient.addColorStop(0.45, `${palette.shadow}4a`);
16645
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1c`);
16646
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
16647
+ context.fillStyle = shadowGradient;
16648
+ context.beginPath();
16649
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
16650
+ context.fill();
16651
+ context.restore();
16652
+ }
16653
+ /**
16654
+ * Number of latitude segments used by the continuous Octopus 3D 4 mesh.
16655
+ *
16656
+ * @private helper of `octopus3d4AvatarVisual`
16657
+ */
16658
+ const LATITUDE_PATCH_COUNT = 20;
16659
+ /**
16660
+ * Number of longitude segments used by the continuous Octopus 3D 4 mesh.
16661
+ *
16662
+ * @private helper of `octopus3d4AvatarVisual`
16663
+ */
16664
+ const LONGITUDE_PATCH_COUNT = 48;
16665
+ /**
16666
+ * Resolves visible projected patches for the continuous Octopus 3D 4 mesh.
16667
+ *
16668
+ * Within a single frame, mesh corner samples and longitude-only computations (tentacle
16669
+ * influence and lobe wave) are quantized to the patch grid and computed once each rather
16670
+ * than re-evaluated for every patch corner.
16671
+ *
16672
+ * @private helper of `octopus3d4AvatarVisual`
16673
+ */
16674
+ function resolveVisibleBlobbyContinuousPatches(options) {
16675
+ const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
16676
+ const latitudePatchCount = LATITUDE_PATCH_COUNT;
16677
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT;
16678
+ const surfacePatches = [];
16679
+ const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
16680
+ const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
16681
+ for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
16682
+ latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
16683
+ }
16684
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
16685
+ longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
16686
+ }
16687
+ const cachedTentacleInfluencesByCornerLongitude = new Array(longitudePatchCount + 1);
16688
+ const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
16689
+ const cachedTentacleInfluencesByPatchCenterLongitude = new Array(longitudePatchCount);
16690
+ const cachedLobeWavesByPatchCenterLongitude = new Float64Array(longitudePatchCount);
16691
+ const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
16692
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
16693
+ const cornerLongitude = longitudeBoundaries[boundaryIndex];
16694
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveBlobbyTentacleInfluence(options, cornerLongitude);
16695
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveBlobbyLobeWave(options, cornerLongitude);
16696
+ }
16697
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
16698
+ const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
16699
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyTentacleInfluence(options, patchCenterLongitude);
16700
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyLobeWave(options, patchCenterLongitude);
16701
+ cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
16702
+ }
16703
+ const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
16704
+ const transformedCornerSamples = new Array(cornerCount);
16705
+ for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
16706
+ const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex];
16707
+ for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
16708
+ const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
16709
+ const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
16710
+ const cornerSample = sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
16711
+ transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
16712
+ }
16713
+ }
16714
+ for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
16715
+ const startLatitude = latitudeBoundaries[latitudeIndex];
16716
+ const endLatitude = latitudeBoundaries[latitudeIndex + 1];
16717
+ const centerLatitude = (startLatitude + endLatitude) / 2;
16718
+ const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
16719
+ const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
16720
+ const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
16721
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
16722
+ const transformedCorners = [
16723
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex],
16724
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1],
16725
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1],
16726
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex],
16727
+ ];
16728
+ const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
16729
+ if (surfaceNormal.z <= 0.006) {
16730
+ continue;
16731
+ }
16732
+ const projectedCorners = [
16733
+ projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
16734
+ projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
16735
+ projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
16736
+ projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
16737
+ ];
16738
+ const lightIntensity = clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1);
16739
+ const rimLightIntensity = Math.pow(clampNumber$1(dotProduct3D(surfaceNormal, RIM_LIGHT_DIRECTION), 0, 1), 2.6);
16740
+ surfacePatches.push({
16741
+ corners: projectedCorners,
16742
+ averageDepth: (transformedCorners[0].z +
16743
+ transformedCorners[1].z +
16744
+ transformedCorners[2].z +
16745
+ transformedCorners[3].z) /
16746
+ 4,
16747
+ lightIntensity,
16748
+ rimLightIntensity,
16749
+ fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
16750
+ outlineColor: verticalProgress < 0.52 ? `${palette.highlight}5c` : `${palette.shadow}66`,
16751
+ });
16752
+ }
16753
+ }
16754
+ return surfacePatches;
16755
+ }
16756
+ /**
16757
+ * Samples one point on the continuous Octopus 3D 4 surface.
16758
+ *
16759
+ * @private helper of `octopus3d4AvatarVisual`
16760
+ */
16761
+ function sampleBlobbyContinuousSurfacePoint(options, latitude, longitude) {
16762
+ return sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, resolveBlobbyTentacleInfluence(options, longitude), resolveBlobbyLobeWave(options, longitude));
16763
+ }
16764
+ /**
16765
+ * Samples one point on the continuous Octopus 3D 4 surface using precomputed longitude-only values.
16766
+ *
16767
+ * The patch loop quantizes the mesh into a fixed corner grid, so the same longitude is reused
16768
+ * across every latitude row and each tentacle/lobe value is computed once per frame instead
16769
+ * of `latitudePatchCount * 4` times.
16770
+ *
16771
+ * @private helper of `octopus3d4AvatarVisual`
16772
+ */
16773
+ function sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
16774
+ const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
16775
+ const cosineLatitude = Math.max(0, Math.cos(latitude));
16776
+ const verticalProgress = (Math.sin(latitude) + 1) / 2;
16777
+ const upperBlend = Math.pow(1 - verticalProgress, 1.32);
16778
+ const lowerBlend = smoothStep(0.34, 1, verticalProgress);
16779
+ const tipBlend = smoothStep(0.66, 1, verticalProgress);
16780
+ const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
16781
+ const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.26 + tipBlend * 0.22);
16782
+ const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
16783
+ animationPhase * 0.6 +
16784
+ timeMs / (1700 + morphologyProfile.body.lobeCount * 28)) *
16785
+ (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.78) *
16786
+ (0.32 + lowerBlend * 0.7);
16787
+ const primaryTentacleWave = Math.sin(timeMs / 740 + tentacleInfluence.primaryPhase + verticalProgress * 2.6) *
16788
+ lowerBlend *
16789
+ tentacleInfluence.core *
16790
+ tentacleInfluence.swayScale;
16791
+ const secondaryTentacleWave = Math.sin(timeMs / 470 + tentacleInfluence.secondaryPhase + verticalProgress * 4.2) *
16792
+ lowerBlend *
16793
+ tentacleInfluence.core *
16794
+ tentacleInfluence.swayScale *
16795
+ 0.42;
16796
+ const tentacleCurl = Math.sin(timeMs / 1180 + tentacleInfluence.primaryPhase * 0.6 + verticalProgress * 3.2) *
16797
+ tipBlend *
16798
+ tentacleInfluence.core *
16799
+ tentacleInfluence.curlScale *
16800
+ 0.36;
16801
+ const horizontalScale = 1.06 +
16802
+ mantleRipple +
16803
+ lowerBlend * (0.18 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.12) +
16804
+ lowerBlend * tentacleInfluence.core * (0.22 + lowerLobeWave * 0.14) -
16805
+ upperBlend * 0.08;
16806
+ const depthScale = 1.08 +
16807
+ upperBlend * 0.16 +
16808
+ Math.max(0, Math.cos(effectiveLongitude)) * 0.12 +
16809
+ lowerBlend * tentacleInfluence.core * (0.12 + tentacleInfluence.depthScale * 0.07) -
16810
+ Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
16811
+ const tentacleTubeRadius = lowerBlend *
16812
+ tentacleInfluence.core *
16813
+ (0.12 + tipBlend * 0.07 + tentacleInfluence.widthScale * 0.028) *
16814
+ radiusX;
16815
+ const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
16816
+ const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.74;
16817
+ const lowerDrop = lowerBlend *
16818
+ radiusY *
16819
+ (0.2 +
16820
+ tentacleInfluence.core *
16821
+ (0.42 +
16822
+ tentacleInfluence.lengthScale * 0.24 +
16823
+ (morphologyProfile.tentacles.flowLengthScale - 1) * 0.1));
16824
+ const combinedTentacleSway = (primaryTentacleWave + secondaryTentacleWave) * radiusX * (0.06 + tipBlend * 0.06);
16825
+ return {
16826
+ x: Math.sin(effectiveLongitude) * planarRadiusX +
16827
+ combinedTentacleSway +
16828
+ tentacleCurl * radiusX * 0.18,
16829
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.14) -
16830
+ upperBlend * radiusY * 0.12 +
16831
+ lowerDrop +
16832
+ Math.sin(timeMs / 1380 + animationPhase + latitude * 1.5) * lowerBlend * radiusY * 0.022 +
16833
+ Math.cos(timeMs / 820 + tentacleInfluence.primaryPhase) *
16834
+ lowerBlend *
16835
+ tipBlend *
16836
+ tentacleInfluence.core *
16837
+ radiusY *
16838
+ 0.04,
16839
+ z: Math.cos(effectiveLongitude) * planarRadiusZ +
16840
+ Math.cos(timeMs / 960 + tentacleInfluence.primaryPhase + verticalProgress) *
16841
+ lowerBlend *
16842
+ tentacleInfluence.core *
16843
+ radiusZ *
16844
+ 0.044 +
16845
+ tentacleCurl * radiusZ * 0.14,
16846
+ };
16847
+ }
16848
+ /**
16849
+ * Blends nearby seeded tentacle profiles at one mesh longitude.
16850
+ *
16851
+ * @private helper of `octopus3d4AvatarVisual`
16852
+ */
16853
+ function resolveBlobbyTentacleInfluence(options, longitude) {
16854
+ let totalWeight = 0;
16855
+ let weightedSin = 0;
16856
+ let weightedCos = 0;
16857
+ let weightedWidthScale = 0;
16858
+ let weightedLengthScale = 0;
16859
+ let weightedSwayScale = 0;
16860
+ let weightedDepthScale = 0;
16861
+ let weightedCurlScale = 0;
16862
+ let weightedPrimaryPhase = 0;
16863
+ let weightedSecondaryPhase = 0;
16864
+ for (const tentacleProfile of options.tentacleProfiles) {
16865
+ const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
16866
+ const width = 0.22 * tentacleProfile.widthScale;
16867
+ const weight = Math.exp(-(distance * distance) / (width * width));
16868
+ totalWeight += weight;
16869
+ weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
16870
+ weightedCos += Math.cos(tentacleProfile.centerLongitude) * weight;
16871
+ weightedWidthScale += tentacleProfile.widthScale * weight;
16872
+ weightedLengthScale += tentacleProfile.lengthScale * weight;
16873
+ weightedSwayScale += tentacleProfile.swayScale * weight;
16874
+ weightedDepthScale += tentacleProfile.depthScale * weight;
16875
+ weightedCurlScale += tentacleProfile.curlScale * weight;
16876
+ weightedPrimaryPhase += tentacleProfile.primaryPhase * weight;
16877
+ weightedSecondaryPhase += tentacleProfile.secondaryPhase * weight;
16878
+ }
16879
+ if (totalWeight < 0.0001) {
16880
+ return {
16881
+ core: 0,
16882
+ centerLongitude: longitude,
16883
+ widthScale: 1,
16884
+ lengthScale: 1,
16885
+ swayScale: 1,
16886
+ depthScale: 1,
16887
+ curlScale: 1,
16888
+ primaryPhase: 0,
16889
+ secondaryPhase: 0,
16890
+ };
16891
+ }
16892
+ return {
16893
+ core: clampNumber$1(totalWeight, 0, 1),
16894
+ centerLongitude: Math.atan2(weightedSin / totalWeight, weightedCos / totalWeight),
16895
+ widthScale: weightedWidthScale / totalWeight,
16896
+ lengthScale: weightedLengthScale / totalWeight,
16897
+ swayScale: weightedSwayScale / totalWeight,
16898
+ depthScale: weightedDepthScale / totalWeight,
16899
+ curlScale: weightedCurlScale / totalWeight,
16900
+ primaryPhase: weightedPrimaryPhase / totalWeight,
16901
+ secondaryPhase: weightedSecondaryPhase / totalWeight,
16902
+ };
16903
+ }
16904
+ /**
16905
+ * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
16906
+ *
16907
+ * @private helper of `octopus3d4AvatarVisual`
16908
+ */
16909
+ function resolveBlobbyLobeWave(options, longitude) {
16910
+ const { morphologyProfile, animationPhase, timeMs } = options;
16911
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT +
16912
+ animationPhase +
16913
+ timeMs / (940 + morphologyProfile.body.lobeCount * 18)) +
16914
+ 1) /
16915
+ 2);
16916
+ }
16917
+ /**
16918
+ * Resolves one base fill tone for a patch on the continuous octopus mesh.
16919
+ *
16920
+ * @private helper of `octopus3d4AvatarVisual`
16921
+ */
16922
+ function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
16923
+ const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
16924
+ if (tonalProgress < 0.12) {
16925
+ return palette.highlight;
16926
+ }
16927
+ if (tonalProgress < 0.3) {
16928
+ return palette.secondary;
16929
+ }
16930
+ if (tonalProgress < 0.72) {
16931
+ return forwardness > 0.56 ? palette.secondary : palette.primary;
16932
+ }
16933
+ return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
16934
+ }
16935
+ /**
16936
+ * Draws one projected mesh patch with multi-pass shading and a soft edge.
16937
+ *
16938
+ * @private helper of `octopus3d4AvatarVisual`
16939
+ */
16940
+ function drawBlobbyContinuousSurfacePatch(context, surfacePatch) {
16941
+ drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
16942
+ if (surfacePatch.lightIntensity > 0) {
16943
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.2 * surfacePatch.lightIntensity})`);
16944
+ }
16945
+ else if (surfacePatch.lightIntensity < 0) {
16946
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.26 * Math.abs(surfacePatch.lightIntensity)})`);
16947
+ }
16948
+ if (surfacePatch.rimLightIntensity > 0.04) {
16949
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 247, 230, ${0.22 * surfacePatch.rimLightIntensity})`);
16950
+ }
16951
+ context.save();
16952
+ context.beginPath();
16953
+ context.moveTo(surfacePatch.corners[0].x, surfacePatch.corners[0].y);
16954
+ for (let cornerIndex = 1; cornerIndex < surfacePatch.corners.length; cornerIndex++) {
16955
+ context.lineTo(surfacePatch.corners[cornerIndex].x, surfacePatch.corners[cornerIndex].y);
16956
+ }
16957
+ context.closePath();
16958
+ context.strokeStyle = surfacePatch.outlineColor;
16959
+ context.lineWidth = Math.max(0.6, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0026);
16960
+ context.lineJoin = 'round';
16961
+ context.stroke();
16962
+ context.restore();
16963
+ }
16964
+ /**
16965
+ * Draws projected mantle-current lines on the front of the mesh.
16966
+ *
16967
+ * @private helper of `octopus3d4AvatarVisual`
16968
+ */
16969
+ function drawBlobbyContinuousCurrents(options) {
16970
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
16971
+ const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
16972
+ const centerIndex = (currentCount - 1) / 2;
16973
+ context.save();
16974
+ context.lineCap = 'round';
16975
+ context.lineJoin = 'round';
16976
+ for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
16977
+ const baseLongitude = (currentIndex - centerIndex) * 0.15;
16978
+ const projectedPoints = [];
16979
+ for (let sampleIndex = 0; sampleIndex < 9; sampleIndex++) {
16980
+ const progress = sampleIndex / 8;
16981
+ const latitude = -0.48 + progress * 0.78;
16982
+ const longitude = baseLongitude + Math.sin(timeMs / 1140 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.038;
16983
+ const scenePoint = transformScenePoint(sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
16984
+ if (scenePoint.z > center.z - size * 0.016) {
16985
+ projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
16986
+ }
16987
+ }
16988
+ if (projectedPoints.length < 3) {
16989
+ continue;
16990
+ }
16991
+ context.beginPath();
16992
+ context.moveTo(projectedPoints[0].x, projectedPoints[0].y);
16993
+ for (const projectedPoint of projectedPoints.slice(1)) {
16994
+ context.lineTo(projectedPoint.x, projectedPoint.y);
16995
+ }
16996
+ context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}3d` : `${palette.accent}33`;
16997
+ context.lineWidth = size * (0.0055 + currentIndex * 0.00045);
16998
+ context.stroke();
16999
+ }
17000
+ context.restore();
17001
+ }
17002
+ /**
17003
+ * Draws small projected sucker highlights on the waving lower mesh lobes.
17004
+ *
17005
+ * @private helper of `octopus3d4AvatarVisual`
17006
+ */
17007
+ function drawBlobbyContinuousSuckers(options) {
17008
+ const { surfaceOptions, size } = options;
17009
+ const { timeMs } = surfaceOptions;
17010
+ for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
17011
+ if (Math.cos(tentacleProfile.centerLongitude) < -0.16) {
17012
+ continue;
17013
+ }
17014
+ for (let suckerIndex = 0; suckerIndex < 4; suckerIndex++) {
17015
+ const latitude = 0.5 + suckerIndex * 0.12;
17016
+ const sideOffset = tentacleProfile.suckerSide * (0.038 + suckerIndex * 0.014) * tentacleProfile.widthScale;
17017
+ const waveOffset = Math.sin(timeMs / 880 + tentacleProfile.primaryPhase + suckerIndex * 0.78) * 0.02;
17018
+ drawBlobbyContinuousSurfaceSpot({
17019
+ ...options,
17020
+ latitude,
17021
+ longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
17022
+ radiusScale: size * (0.0068 - suckerIndex * 0.0006),
17023
+ });
17024
+ }
17025
+ }
17026
+ }
17027
+ /**
17028
+ * Draws seeded pigment spots across the upper mesh for a richer skin texture.
17029
+ *
17030
+ * @private helper of `octopus3d4AvatarVisual`
17031
+ */
17032
+ function drawBlobbySkinSpots(options) {
17033
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, skinSpots, } = options;
17034
+ for (const skinSpot of skinSpots) {
17035
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, skinSpot.latitude, skinSpot.longitude);
17036
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
17037
+ if (sceneCenterPoint.z <= center.z - size * 0.01) {
17038
+ continue;
17039
+ }
17040
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
17041
+ const spotRadius = size * skinSpot.radiusScale;
17042
+ context.save();
17043
+ context.beginPath();
17044
+ context.arc(projectedCenterPoint.x, projectedCenterPoint.y, spotRadius, 0, Math.PI * 2);
17045
+ context.fillStyle = `${palette.shadow}${formatAlphaHex(skinSpot.opacity)}`;
17046
+ context.fill();
17047
+ context.restore();
17048
+ }
17049
+ }
17050
+ /**
17051
+ * Draws one tiny projected surface spot by sampling local mesh tangents.
17052
+ *
17053
+ * @private helper of `octopus3d4AvatarVisual`
17054
+ */
17055
+ function drawBlobbyContinuousSurfaceSpot(options) {
17056
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
17057
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude);
17058
+ const localHorizontal = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
17059
+ const localVertical = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
17060
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
17061
+ if (sceneCenterPoint.z <= center.z - size * 0.012) {
17062
+ return;
17063
+ }
17064
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
17065
+ const projectedHorizontalPoint = projectScenePoint(transformScenePoint(localHorizontal, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
17066
+ const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
17067
+ const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
17068
+ radiusScale *
17069
+ 0.78, size * 0.003, size * 0.02);
17070
+ const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
17071
+ radiusScale *
17072
+ 0.54, size * 0.0024, size * 0.015);
17073
+ const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
17074
+ context.save();
17075
+ context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
17076
+ context.rotate(rotation);
17077
+ context.beginPath();
17078
+ context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
17079
+ context.fillStyle = `${palette.highlight}80`;
17080
+ context.fill();
17081
+ context.strokeStyle = `${palette.highlight}a8`;
17082
+ context.lineWidth = Math.max(0.7, size * 0.0028);
17083
+ context.stroke();
17084
+ context.restore();
17085
+ }
17086
+ /**
17087
+ * Draws a soft, slowly drifting gloss highlight on the front of the mesh.
17088
+ *
17089
+ * @private helper of `octopus3d4AvatarVisual`
17090
+ */
17091
+ function drawBlobbyContinuousGloss(options) {
17092
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size } = options;
17093
+ const { timeMs, animationPhase } = surfaceOptions;
17094
+ const glossLatitude = -0.3 + Math.sin(timeMs / 2700 + animationPhase) * 0.04;
17095
+ const glossLongitude = -0.18 + Math.cos(timeMs / 2300 + animationPhase * 0.8) * 0.05;
17096
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, glossLatitude, glossLongitude);
17097
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
17098
+ if (sceneCenterPoint.z <= center.z) {
17099
+ return;
17100
+ }
17101
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
17102
+ const glossRadius = size * 0.058;
17103
+ context.save();
17104
+ const glossGradient = context.createRadialGradient(projectedCenterPoint.x - glossRadius * 0.3, projectedCenterPoint.y - glossRadius * 0.4, glossRadius * 0.04, projectedCenterPoint.x, projectedCenterPoint.y, glossRadius);
17105
+ glossGradient.addColorStop(0, 'rgba(255, 255, 255, 0.34)');
17106
+ glossGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
17107
+ glossGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
17108
+ context.fillStyle = glossGradient;
17109
+ context.beginPath();
17110
+ context.ellipse(projectedCenterPoint.x, projectedCenterPoint.y, glossRadius * 1.05, glossRadius * 0.78, 0, 0, Math.PI * 2);
17111
+ context.fill();
17112
+ context.restore();
17113
+ }
17114
+ /**
17115
+ * Resolves a signed angular distance from the source longitude to the target longitude.
17116
+ *
17117
+ * @private helper of `octopus3d4AvatarVisual`
17118
+ */
17119
+ function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
17120
+ return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
17121
+ }
17122
+ /**
17123
+ * Smoothly maps a value between two bounds into `[0, 1]`.
17124
+ *
17125
+ * @private helper of `octopus3d4AvatarVisual`
17126
+ */
17127
+ function smoothStep(edgeStart, edgeEnd, value) {
17128
+ const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
17129
+ return progress * progress * (3 - 2 * progress);
17130
+ }
17131
+ /**
17132
+ * Converts an opacity ratio into a two-digit hexadecimal alpha suffix.
17133
+ *
17134
+ * @private helper of `octopus3d4AvatarVisual`
17135
+ */
17136
+ function formatAlphaHex(opacity) {
17137
+ return Math.round(clampNumber$1(opacity, 0, 1) * 255)
17138
+ .toString(16)
17139
+ .padStart(2, '0');
17140
+ }
17141
+
16387
17142
  /* eslint-disable no-magic-numbers */
16388
17143
  /**
16389
17144
  * Octopus avatar visual.
@@ -17155,6 +17910,7 @@ const AVATAR_VISUALS = [
17155
17910
  octopus3dAvatarVisual,
17156
17911
  octopus3d2AvatarVisual,
17157
17912
  octopus3d3AvatarVisual,
17913
+ octopus3d4AvatarVisual,
17158
17914
  asciiOctopusAvatarVisual,
17159
17915
  minecraftAvatarVisual,
17160
17916
  minecraft2AvatarVisual,