@promptbook/browser 0.112.0-133 → 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-133';
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
@@ -2604,6 +2604,11 @@ function isValidUrl(url) {
2604
2604
  * - `isValidAgentUrl` *(this one)* which tests just agent URL
2605
2605
  * - `isValidPipelineUrl` which tests just pipeline URL
2606
2606
  *
2607
+ * Note: This is a pure structural validator and does not block private/internal network
2608
+ * addresses. Callers that fetch the URL server-side from an untrusted network context must
2609
+ * additionally apply the `assertSafeUrl` SSRF guard from the Agents Server before any
2610
+ * outbound request.
2611
+ *
2607
2612
  * @public exported from `@promptbook/utils`
2608
2613
  */
2609
2614
  function isValidAgentUrl(url) {
@@ -2617,12 +2622,6 @@ function isValidAgentUrl(url) {
2617
2622
  // TODO: [🐠]
2618
2623
  return false;
2619
2624
  }
2620
- /*
2621
- Note: [👣][🧠] Is it secure to allow pipeline URLs on private and unsecured networks?
2622
- if (isUrlOnPrivateNetwork(url)) {
2623
- return false;
2624
- }
2625
- */
2626
2625
  return true;
2627
2626
  }
2628
2627
  // TODO: [🐠] Maybe more info why the URL is invalid
@@ -6643,7 +6642,7 @@ function fillTextureRect(texture, x, y, width, height, color) {
6643
6642
  *
6644
6643
  * @private helper of `minecraft2AvatarVisual`
6645
6644
  */
6646
- const LIGHT_DIRECTION$3 = normalizeVector3({
6645
+ const LIGHT_DIRECTION$4 = normalizeVector3({
6647
6646
  x: 0.4,
6648
6647
  y: -0.65,
6649
6648
  z: 0.92,
@@ -6867,7 +6866,7 @@ function resolveVisibleCuboidFaces(cuboid, size, sceneCenterX, sceneCenterY) {
6867
6866
  corners: projectedCorners,
6868
6867
  texture: faceDefinition.texture,
6869
6868
  averageDepth: transformedCorners.reduce((depthSum, corner) => depthSum + corner.z, 0) / transformedCorners.length,
6870
- lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$3), -1, 1),
6869
+ lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$4), -1, 1),
6871
6870
  outlineColor: cuboid.outlineColor,
6872
6871
  };
6873
6872
  });
@@ -7554,7 +7553,7 @@ function resolveSeededIntegerRange(random, minimumValue, maximumValue) {
7554
7553
  *
7555
7554
  * @private helper of `octopus3AvatarVisual`
7556
7555
  */
7557
- function formatAlphaHex$1(opacity) {
7556
+ function formatAlphaHex$2(opacity) {
7558
7557
  return Math.round(Math.min(1, Math.max(0, opacity)) * 255)
7559
7558
  .toString(16)
7560
7559
  .padStart(2, '0');
@@ -7933,7 +7932,7 @@ function drawSeededEye(context, centerX, centerY, radiusX, radiusY, rotation, pa
7933
7932
  context.beginPath();
7934
7933
  context.moveTo(-radiusX * 0.74, radiusY * 0.2);
7935
7934
  context.quadraticCurveTo(0, radiusY * 0.38, radiusX * 0.74, radiusY * 0.2);
7936
- context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
7935
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$2(eyeStyle.lowerLidOpacity)}`;
7937
7936
  context.lineWidth = radiusX * 0.08;
7938
7937
  context.lineCap = 'round';
7939
7938
  context.stroke();
@@ -8014,7 +8013,7 @@ function drawProjectedOrganicEye(context, localCenter, radiusX, radiusY, center,
8014
8013
  context.beginPath();
8015
8014
  context.moveTo(-projectedRadiusX * 0.74, projectedRadiusY * 0.2);
8016
8015
  context.quadraticCurveTo(0, projectedRadiusY * 0.38, projectedRadiusX * 0.74, projectedRadiusY * 0.2);
8017
- context.strokeStyle = `${palette.highlight}${formatAlphaHex(eyeStyle.lowerLidOpacity)}`;
8016
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
8018
8017
  context.lineWidth = projectedRadiusX * 0.08;
8019
8018
  context.lineCap = 'round';
8020
8019
  context.stroke();
@@ -8060,7 +8059,7 @@ function drawProjectedQuad(context, corners, fillStyle) {
8060
8059
  *
8061
8060
  * @private helper of the 3D octopus avatar visuals
8062
8061
  */
8063
- function formatAlphaHex(opacity) {
8062
+ function formatAlphaHex$1(opacity) {
8064
8063
  return Math.round(clampNumber$1(opacity, 0, 1) * 255)
8065
8064
  .toString(16)
8066
8065
  .padStart(2, '0');
@@ -8072,7 +8071,7 @@ function formatAlphaHex(opacity) {
8072
8071
  *
8073
8072
  * @private helper of `octopus3dAvatarVisual`
8074
8073
  */
8075
- const LIGHT_DIRECTION$2 = normalizeVector3({
8074
+ const LIGHT_DIRECTION$3 = normalizeVector3({
8076
8075
  x: 0.48,
8077
8076
  y: -0.62,
8078
8077
  z: 0.94,
@@ -8322,7 +8321,7 @@ function resolveVisibleEllipsoidPatches(options) {
8322
8321
  corners: projectedCorners,
8323
8322
  averageDepth: transformedCorners.reduce((depthSum, transformedCorner) => depthSum + transformedCorner.z, 0) /
8324
8323
  transformedCorners.length,
8325
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
8324
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$3), -1, 1),
8326
8325
  fillStyle: resolveSurfacePatchFillStyle(palette, verticalProgress + verticalColorBias),
8327
8326
  outlineColor,
8328
8327
  });
@@ -8511,7 +8510,7 @@ function resolveEllipsoidSurfaceDepth(radiusX, radiusY, radiusZ, x, y) {
8511
8510
  *
8512
8511
  * @private helper of `octopus3d2AvatarVisual`
8513
8512
  */
8514
- const LIGHT_DIRECTION$1 = normalizeVector3({
8513
+ const LIGHT_DIRECTION$2 = normalizeVector3({
8515
8514
  x: 0.38,
8516
8515
  y: -0.6,
8517
8516
  z: 0.98,
@@ -8670,13 +8669,13 @@ function drawBlobbyOctopusShadow(context, size, palette, interaction, timeMs, mo
8670
8669
  *
8671
8670
  * @private helper of `octopus3d2AvatarVisual`
8672
8671
  */
8673
- const LATITUDE_PATCH_COUNT$1 = 12;
8672
+ const LATITUDE_PATCH_COUNT$2 = 12;
8674
8673
  /**
8675
8674
  * Number of longitude segments used by the single blobby octopus mesh.
8676
8675
  *
8677
8676
  * @private helper of `octopus3d2AvatarVisual`
8678
8677
  */
8679
- const LONGITUDE_PATCH_COUNT$1 = 24;
8678
+ const LONGITUDE_PATCH_COUNT$2 = 24;
8680
8679
  /**
8681
8680
  * Resolves all visible projected patches for the single blobby octopus mesh.
8682
8681
  *
@@ -8690,8 +8689,8 @@ const LONGITUDE_PATCH_COUNT$1 = 24;
8690
8689
  */
8691
8690
  function resolveVisibleBlobbyOctopusPatches(options) {
8692
8691
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, animationPhase, timeMs, } = options;
8693
- const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
8694
- const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
8692
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$2;
8693
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$2;
8695
8694
  const surfacePatches = [];
8696
8695
  const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
8697
8696
  const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
@@ -8750,8 +8749,8 @@ function resolveVisibleBlobbyOctopusPatches(options) {
8750
8749
  transformedCorners[2].z +
8751
8750
  transformedCorners[3].z) /
8752
8751
  4,
8753
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
8754
- fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
8752
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
8753
+ fillStyle: resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
8755
8754
  outlineColor: verticalProgress < 0.58 ? `${palette.highlight}73` : `${palette.shadow}8a`,
8756
8755
  });
8757
8756
  }
@@ -8819,7 +8818,7 @@ function resolveLowerLobeWave(longitude, morphologyProfile, animationPhase, time
8819
8818
  *
8820
8819
  * @private helper of `octopus3d2AvatarVisual`
8821
8820
  */
8822
- function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, lowerLobeWave) {
8821
+ function resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, forwardness, lowerLobeWave) {
8823
8822
  const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.12 - forwardness * 0.07, 0, 1);
8824
8823
  if (tonalProgress < 0.16) {
8825
8824
  return palette.highlight;
@@ -8865,7 +8864,7 @@ function drawBlobbySurfacePatch(context, surfacePatch) {
8865
8864
  *
8866
8865
  * @private helper of `octopus3d3AvatarVisual`
8867
8866
  */
8868
- const LIGHT_DIRECTION = normalizeVector3({
8867
+ const LIGHT_DIRECTION$1 = normalizeVector3({
8869
8868
  x: 0.34,
8870
8869
  y: -0.62,
8871
8870
  z: 1,
@@ -8875,7 +8874,7 @@ const LIGHT_DIRECTION = normalizeVector3({
8875
8874
  *
8876
8875
  * @private helper of `octopus3d3AvatarVisual`
8877
8876
  */
8878
- const OCTOPUS_TENTACLE_COUNT = 8;
8877
+ const OCTOPUS_TENTACLE_COUNT$1 = 8;
8879
8878
  /**
8880
8879
  * Cache keyed by the `createRandom` factory reference, which is stable for the lifetime of one
8881
8880
  * mounted `<Avatar/>` component (created inside `resolveAvatarRenderDefinition` and held in a
@@ -9014,9 +9013,9 @@ const octopus3d3AvatarVisual = {
9014
9013
  * @private helper of `octopus3d3AvatarVisual`
9015
9014
  */
9016
9015
  function createContinuousTentacleProfiles(createRandom, morphologyProfile) {
9017
- return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
9016
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT$1 }, (_, tentacleIndex) => {
9018
9017
  const tentacleRandom = createRandom(`octopus3d3-tentacle-${tentacleIndex}`);
9019
- const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
9018
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT$1 - 1);
9020
9019
  return {
9021
9020
  centerLongitude: -Math.PI * 0.86 +
9022
9021
  progress * Math.PI * 1.72 +
@@ -9083,13 +9082,13 @@ function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs
9083
9082
  *
9084
9083
  * @private helper of `octopus3d3AvatarVisual`
9085
9084
  */
9086
- const LATITUDE_PATCH_COUNT = 16;
9085
+ const LATITUDE_PATCH_COUNT$1 = 16;
9087
9086
  /**
9088
9087
  * Number of longitude segments used by the continuous Octopus 3D 3 mesh.
9089
9088
  *
9090
9089
  * @private helper of `octopus3d3AvatarVisual`
9091
9090
  */
9092
- const LONGITUDE_PATCH_COUNT = 40;
9091
+ const LONGITUDE_PATCH_COUNT$1 = 40;
9093
9092
  /**
9094
9093
  * Resolves visible projected patches for the continuous octopus mesh.
9095
9094
  *
@@ -9103,8 +9102,8 @@ const LONGITUDE_PATCH_COUNT = 40;
9103
9102
  */
9104
9103
  function resolveVisibleContinuousOctopusPatches(options) {
9105
9104
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
9106
- const latitudePatchCount = LATITUDE_PATCH_COUNT;
9107
- const longitudePatchCount = LONGITUDE_PATCH_COUNT;
9105
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
9106
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
9108
9107
  const surfacePatches = [];
9109
9108
  const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
9110
9109
  const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
@@ -9172,7 +9171,7 @@ function resolveVisibleContinuousOctopusPatches(options) {
9172
9171
  transformedCorners[2].z +
9173
9172
  transformedCorners[3].z) /
9174
9173
  4,
9175
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1),
9174
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
9176
9175
  fillStyle: resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
9177
9176
  outlineColor: verticalProgress < 0.54 ? `${palette.highlight}69` : `${palette.shadow}78`,
9178
9177
  });
@@ -9206,9 +9205,9 @@ function sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude
9206
9205
  const cosineLatitude = Math.max(0, Math.cos(latitude));
9207
9206
  const verticalProgress = (Math.sin(latitude) + 1) / 2;
9208
9207
  const upperBlend = Math.pow(1 - verticalProgress, 1.28);
9209
- const lowerBlend = smoothStep(0.38, 1, verticalProgress);
9210
- const tipBlend = smoothStep(0.68, 1, verticalProgress);
9211
- const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
9208
+ const lowerBlend = smoothStep$1(0.38, 1, verticalProgress);
9209
+ const tipBlend = smoothStep$1(0.68, 1, verticalProgress);
9210
+ const centerPull = resolveSignedAngularDistance$1(longitude, tentacleInfluence.centerLongitude);
9212
9211
  const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.24 + tipBlend * 0.2);
9213
9212
  const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
9214
9213
  animationPhase * 0.6 +
@@ -9274,7 +9273,7 @@ function resolveContinuousTentacleInfluence(options, longitude) {
9274
9273
  let weightedDepthScale = 0;
9275
9274
  let weightedPhase = 0;
9276
9275
  for (const tentacleProfile of options.tentacleProfiles) {
9277
- const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
9276
+ const distance = Math.abs(resolveSignedAngularDistance$1(longitude, tentacleProfile.centerLongitude));
9278
9277
  const width = 0.2 * tentacleProfile.widthScale;
9279
9278
  const weight = Math.exp(-(distance * distance) / (width * width));
9280
9279
  totalWeight += weight;
@@ -9314,7 +9313,7 @@ function resolveContinuousTentacleInfluence(options, longitude) {
9314
9313
  */
9315
9314
  function resolveContinuousLobeWave(options, longitude) {
9316
9315
  const { morphologyProfile, animationPhase, timeMs } = options;
9317
- return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT +
9316
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT$1 +
9318
9317
  animationPhase +
9319
9318
  timeMs / (980 + morphologyProfile.body.lobeCount * 18)) +
9320
9319
  1) /
@@ -9468,7 +9467,7 @@ function drawProjectedSurfaceSpot(options) {
9468
9467
  *
9469
9468
  * @private helper of `octopus3d3AvatarVisual`
9470
9469
  */
9471
- function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
9470
+ function resolveSignedAngularDistance$1(sourceLongitude, targetLongitude) {
9472
9471
  return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
9473
9472
  }
9474
9473
  /**
@@ -9476,11 +9475,767 @@ function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
9476
9475
  *
9477
9476
  * @private helper of `octopus3d3AvatarVisual`
9478
9477
  */
9479
- function smoothStep(edgeStart, edgeEnd, value) {
9478
+ function smoothStep$1(edgeStart, edgeEnd, value) {
9480
9479
  const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
9481
9480
  return progress * progress * (3 - 2 * progress);
9482
9481
  }
9483
9482
 
9483
+ /* eslint-disable no-magic-numbers */
9484
+ /**
9485
+ * Light direction used by the continuous Octopus 3D 4 mesh shading.
9486
+ *
9487
+ * @private helper of `octopus3d4AvatarVisual`
9488
+ */
9489
+ const LIGHT_DIRECTION = normalizeVector3({
9490
+ x: 0.32,
9491
+ y: -0.66,
9492
+ z: 1,
9493
+ });
9494
+ /**
9495
+ * Rim-light direction used to brighten the side of the silhouette.
9496
+ *
9497
+ * @private helper of `octopus3d4AvatarVisual`
9498
+ */
9499
+ const RIM_LIGHT_DIRECTION = normalizeVector3({
9500
+ x: -0.7,
9501
+ y: -0.18,
9502
+ z: 0.55,
9503
+ });
9504
+ /**
9505
+ * Real-octopus tentacle count used by the continuous lower mesh.
9506
+ *
9507
+ * @private helper of `octopus3d4AvatarVisual`
9508
+ */
9509
+ const OCTOPUS_TENTACLE_COUNT = 8;
9510
+ /**
9511
+ * Number of seeded skin spots painted across the octopus body.
9512
+ *
9513
+ * @private helper of `octopus3d4AvatarVisual`
9514
+ */
9515
+ const SKIN_SPOT_COUNT = 14;
9516
+ /**
9517
+ * Cache keyed by the `createRandom` factory reference, stable per mounted `<Avatar/>` component.
9518
+ *
9519
+ * @private helper of `octopus3d4AvatarVisual`
9520
+ */
9521
+ const octopus3d4StableStateCache = new WeakMap();
9522
+ /**
9523
+ * Returns the stable per-avatar state, computing it on first access and caching afterwards.
9524
+ *
9525
+ * @private helper of `octopus3d4AvatarVisual`
9526
+ */
9527
+ function getOctopus3d4StableState(createRandom) {
9528
+ const cached = octopus3d4StableStateCache.get(createRandom);
9529
+ if (cached !== undefined) {
9530
+ return cached;
9531
+ }
9532
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
9533
+ const animationRandom = createRandom('octopus3d4-animation-profile');
9534
+ const eyeRandom = createRandom('octopus3d4-eye-profile');
9535
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
9536
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
9537
+ const state = {
9538
+ morphologyProfile,
9539
+ animationPhase: animationRandom() * Math.PI * 2,
9540
+ leftEyePhaseOffset,
9541
+ rightEyePhaseOffset,
9542
+ tentacleProfiles: createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile),
9543
+ skinSpots: createBlobbySkinSpots(createRandom),
9544
+ };
9545
+ octopus3d4StableStateCache.set(createRandom, state);
9546
+ return state;
9547
+ }
9548
+ /**
9549
+ * Octopus 3D 4 avatar visual.
9550
+ *
9551
+ * @private built-in avatar visual
9552
+ */
9553
+ const octopus3d4AvatarVisual = {
9554
+ id: 'octopus3d4',
9555
+ title: 'Octopus 3D 4',
9556
+ 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.',
9557
+ isAnimated: true,
9558
+ supportsPointerTracking: true,
9559
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
9560
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles, skinSpots } = getOctopus3d4StableState(createRandom);
9561
+ const sceneCenterX = size * 0.5;
9562
+ const sceneCenterY = size * 0.535;
9563
+ const bob = Math.sin(timeMs / 980 + animationPhase) * size * 0.013;
9564
+ const meshCenter = {
9565
+ x: interaction.bodyOffsetX * size * 0.052 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
9566
+ y: -size * 0.07 + interaction.bodyOffsetY * size * 0.028 + bob,
9567
+ z: interaction.intensity * size * 0.02,
9568
+ };
9569
+ const rotationY = -0.08 +
9570
+ Math.sin(timeMs / 2800 + animationPhase) * 0.04 +
9571
+ interaction.bodyOffsetX * 0.24 +
9572
+ interaction.gazeX * 0.98;
9573
+ const rotationX = -0.07 +
9574
+ Math.cos(timeMs / 3200 + animationPhase * 0.7) * 0.02 -
9575
+ interaction.bodyOffsetY * 0.08 -
9576
+ interaction.gazeY * 0.42;
9577
+ const surfaceOptions = {
9578
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.12,
9579
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1,
9580
+ radiusZ: size *
9581
+ morphologyProfile.body.bodyRadiusRatio *
9582
+ (1.04 + (morphologyProfile.body.horizontalStretch - 1) * 0.2),
9583
+ morphologyProfile,
9584
+ timeMs,
9585
+ animationPhase,
9586
+ tentacleProfiles,
9587
+ };
9588
+ const surfacePatches = resolveVisibleBlobbyContinuousPatches({
9589
+ ...surfaceOptions,
9590
+ center: meshCenter,
9591
+ rotationX,
9592
+ rotationY,
9593
+ sceneCenterX,
9594
+ sceneCenterY,
9595
+ size,
9596
+ palette,
9597
+ });
9598
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.4 - 0.04, -0.24, 0.08);
9599
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.0, 0.18, 0.32);
9600
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.21 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
9601
+ const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
9602
+ const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
9603
+ const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
9604
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
9605
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
9606
+ drawAvatarFrame(context, size, palette);
9607
+ drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
9608
+ drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile);
9609
+ for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
9610
+ drawBlobbyContinuousSurfacePatch(context, surfacePatch);
9611
+ }
9612
+ drawBlobbySkinSpots({
9613
+ context,
9614
+ surfaceOptions,
9615
+ center: meshCenter,
9616
+ rotationX,
9617
+ rotationY,
9618
+ sceneCenterX,
9619
+ sceneCenterY,
9620
+ size,
9621
+ palette,
9622
+ skinSpots,
9623
+ });
9624
+ drawBlobbyContinuousCurrents({
9625
+ context,
9626
+ surfaceOptions,
9627
+ center: meshCenter,
9628
+ rotationX,
9629
+ rotationY,
9630
+ sceneCenterX,
9631
+ sceneCenterY,
9632
+ size,
9633
+ palette,
9634
+ morphologyProfile,
9635
+ timeMs,
9636
+ animationPhase,
9637
+ });
9638
+ drawBlobbyContinuousSuckers({
9639
+ context,
9640
+ surfaceOptions,
9641
+ center: meshCenter,
9642
+ rotationX,
9643
+ rotationY,
9644
+ sceneCenterX,
9645
+ sceneCenterY,
9646
+ size,
9647
+ palette,
9648
+ });
9649
+ drawBlobbyContinuousGloss({
9650
+ context,
9651
+ surfaceOptions,
9652
+ center: meshCenter,
9653
+ rotationX,
9654
+ rotationY,
9655
+ sceneCenterX,
9656
+ sceneCenterY,
9657
+ size,
9658
+ palette,
9659
+ });
9660
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
9661
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
9662
+ drawProjectedOrganicMouth(context, [
9663
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
9664
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
9665
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
9666
+ ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
9667
+ },
9668
+ };
9669
+ /**
9670
+ * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
9671
+ *
9672
+ * @private helper of `octopus3d4AvatarVisual`
9673
+ */
9674
+ function createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile) {
9675
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
9676
+ const tentacleRandom = createRandom(`octopus3d4-tentacle-${tentacleIndex}`);
9677
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
9678
+ return {
9679
+ centerLongitude: -Math.PI * 0.9 +
9680
+ progress * Math.PI * 1.8 +
9681
+ (tentacleRandom() - 0.5) * (0.06 + morphologyProfile.tentacles.rootSpreadScale * 0.025),
9682
+ widthScale: 0.92 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.18,
9683
+ lengthScale: 0.9 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.24,
9684
+ swayScale: 0.86 + tentacleRandom() * 0.4 + (morphologyProfile.tentacles.swayScale - 1) * 0.22,
9685
+ depthScale: 0.9 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.22,
9686
+ curlScale: 0.62 + tentacleRandom() * 0.46,
9687
+ primaryPhase: tentacleRandom() * Math.PI * 2,
9688
+ secondaryPhase: tentacleRandom() * Math.PI * 2,
9689
+ suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
9690
+ };
9691
+ });
9692
+ }
9693
+ /**
9694
+ * Creates seeded skin pigment spots distributed across the upper octopus mesh.
9695
+ *
9696
+ * @private helper of `octopus3d4AvatarVisual`
9697
+ */
9698
+ function createBlobbySkinSpots(createRandom) {
9699
+ const spotRandom = createRandom('octopus3d4-skin-spots');
9700
+ return Array.from({ length: SKIN_SPOT_COUNT }, () => ({
9701
+ latitude: -0.45 + spotRandom() * 0.7,
9702
+ longitude: -0.5 + spotRandom() * 1.0,
9703
+ radiusScale: 0.0028 + spotRandom() * 0.0052,
9704
+ opacity: 0.16 + spotRandom() * 0.22,
9705
+ }));
9706
+ }
9707
+ /**
9708
+ * Draws the soft underwater atmosphere behind the continuous octopus mesh.
9709
+ *
9710
+ * @private helper of `octopus3d4AvatarVisual`
9711
+ */
9712
+ function drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
9713
+ 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));
9714
+ glowGradient.addColorStop(0, `${palette.highlight}74`);
9715
+ glowGradient.addColorStop(0.32, `${palette.accent}32`);
9716
+ glowGradient.addColorStop(1, `${palette.highlight}00`);
9717
+ context.fillStyle = glowGradient;
9718
+ context.fillRect(0, 0, size, size);
9719
+ 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);
9720
+ lowerGradient.addColorStop(0, `${palette.secondary}28`);
9721
+ lowerGradient.addColorStop(1, `${palette.secondary}00`);
9722
+ context.fillStyle = lowerGradient;
9723
+ context.fillRect(0, 0, size, size);
9724
+ }
9725
+ /**
9726
+ * Draws the soft lower shadow that anchors the octopus in the avatar frame.
9727
+ *
9728
+ * @private helper of `octopus3d4AvatarVisual`
9729
+ */
9730
+ function drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
9731
+ const cx = size * 0.5 + interaction.gazeX * size * 0.046;
9732
+ const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
9733
+ const rx = size * (0.2 + morphologyProfile.tentacles.rootSpreadScale * 0.024 + interaction.intensity * 0.022);
9734
+ const ry = size * 0.062;
9735
+ context.save();
9736
+ context.translate(cx, cy);
9737
+ context.scale(1, ry / rx);
9738
+ const blurRadius = rx * 1.42;
9739
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
9740
+ shadowGradient.addColorStop(0, `${palette.shadow}82`);
9741
+ shadowGradient.addColorStop(0.45, `${palette.shadow}4a`);
9742
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1c`);
9743
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
9744
+ context.fillStyle = shadowGradient;
9745
+ context.beginPath();
9746
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
9747
+ context.fill();
9748
+ context.restore();
9749
+ }
9750
+ /**
9751
+ * Number of latitude segments used by the continuous Octopus 3D 4 mesh.
9752
+ *
9753
+ * @private helper of `octopus3d4AvatarVisual`
9754
+ */
9755
+ const LATITUDE_PATCH_COUNT = 20;
9756
+ /**
9757
+ * Number of longitude segments used by the continuous Octopus 3D 4 mesh.
9758
+ *
9759
+ * @private helper of `octopus3d4AvatarVisual`
9760
+ */
9761
+ const LONGITUDE_PATCH_COUNT = 48;
9762
+ /**
9763
+ * Resolves visible projected patches for the continuous Octopus 3D 4 mesh.
9764
+ *
9765
+ * Within a single frame, mesh corner samples and longitude-only computations (tentacle
9766
+ * influence and lobe wave) are quantized to the patch grid and computed once each rather
9767
+ * than re-evaluated for every patch corner.
9768
+ *
9769
+ * @private helper of `octopus3d4AvatarVisual`
9770
+ */
9771
+ function resolveVisibleBlobbyContinuousPatches(options) {
9772
+ const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
9773
+ const latitudePatchCount = LATITUDE_PATCH_COUNT;
9774
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT;
9775
+ const surfacePatches = [];
9776
+ const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
9777
+ const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
9778
+ for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
9779
+ latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
9780
+ }
9781
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
9782
+ longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
9783
+ }
9784
+ const cachedTentacleInfluencesByCornerLongitude = new Array(longitudePatchCount + 1);
9785
+ const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
9786
+ const cachedTentacleInfluencesByPatchCenterLongitude = new Array(longitudePatchCount);
9787
+ const cachedLobeWavesByPatchCenterLongitude = new Float64Array(longitudePatchCount);
9788
+ const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
9789
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
9790
+ const cornerLongitude = longitudeBoundaries[boundaryIndex];
9791
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveBlobbyTentacleInfluence(options, cornerLongitude);
9792
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveBlobbyLobeWave(options, cornerLongitude);
9793
+ }
9794
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
9795
+ const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
9796
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyTentacleInfluence(options, patchCenterLongitude);
9797
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyLobeWave(options, patchCenterLongitude);
9798
+ cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
9799
+ }
9800
+ const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
9801
+ const transformedCornerSamples = new Array(cornerCount);
9802
+ for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
9803
+ const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex];
9804
+ for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
9805
+ const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
9806
+ const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
9807
+ const cornerSample = sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
9808
+ transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
9809
+ }
9810
+ }
9811
+ for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
9812
+ const startLatitude = latitudeBoundaries[latitudeIndex];
9813
+ const endLatitude = latitudeBoundaries[latitudeIndex + 1];
9814
+ const centerLatitude = (startLatitude + endLatitude) / 2;
9815
+ const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
9816
+ const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
9817
+ const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
9818
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
9819
+ const transformedCorners = [
9820
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex],
9821
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1],
9822
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1],
9823
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex],
9824
+ ];
9825
+ const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
9826
+ if (surfaceNormal.z <= 0.006) {
9827
+ continue;
9828
+ }
9829
+ const projectedCorners = [
9830
+ projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
9831
+ projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
9832
+ projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
9833
+ projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
9834
+ ];
9835
+ const lightIntensity = clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1);
9836
+ const rimLightIntensity = Math.pow(clampNumber$1(dotProduct3D(surfaceNormal, RIM_LIGHT_DIRECTION), 0, 1), 2.6);
9837
+ surfacePatches.push({
9838
+ corners: projectedCorners,
9839
+ averageDepth: (transformedCorners[0].z +
9840
+ transformedCorners[1].z +
9841
+ transformedCorners[2].z +
9842
+ transformedCorners[3].z) /
9843
+ 4,
9844
+ lightIntensity,
9845
+ rimLightIntensity,
9846
+ fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
9847
+ outlineColor: verticalProgress < 0.52 ? `${palette.highlight}5c` : `${palette.shadow}66`,
9848
+ });
9849
+ }
9850
+ }
9851
+ return surfacePatches;
9852
+ }
9853
+ /**
9854
+ * Samples one point on the continuous Octopus 3D 4 surface.
9855
+ *
9856
+ * @private helper of `octopus3d4AvatarVisual`
9857
+ */
9858
+ function sampleBlobbyContinuousSurfacePoint(options, latitude, longitude) {
9859
+ return sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, resolveBlobbyTentacleInfluence(options, longitude), resolveBlobbyLobeWave(options, longitude));
9860
+ }
9861
+ /**
9862
+ * Samples one point on the continuous Octopus 3D 4 surface using precomputed longitude-only values.
9863
+ *
9864
+ * The patch loop quantizes the mesh into a fixed corner grid, so the same longitude is reused
9865
+ * across every latitude row and each tentacle/lobe value is computed once per frame instead
9866
+ * of `latitudePatchCount * 4` times.
9867
+ *
9868
+ * @private helper of `octopus3d4AvatarVisual`
9869
+ */
9870
+ function sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
9871
+ const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
9872
+ const cosineLatitude = Math.max(0, Math.cos(latitude));
9873
+ const verticalProgress = (Math.sin(latitude) + 1) / 2;
9874
+ const upperBlend = Math.pow(1 - verticalProgress, 1.32);
9875
+ const lowerBlend = smoothStep(0.34, 1, verticalProgress);
9876
+ const tipBlend = smoothStep(0.66, 1, verticalProgress);
9877
+ const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
9878
+ const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.26 + tipBlend * 0.22);
9879
+ const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
9880
+ animationPhase * 0.6 +
9881
+ timeMs / (1700 + morphologyProfile.body.lobeCount * 28)) *
9882
+ (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.78) *
9883
+ (0.32 + lowerBlend * 0.7);
9884
+ const primaryTentacleWave = Math.sin(timeMs / 740 + tentacleInfluence.primaryPhase + verticalProgress * 2.6) *
9885
+ lowerBlend *
9886
+ tentacleInfluence.core *
9887
+ tentacleInfluence.swayScale;
9888
+ const secondaryTentacleWave = Math.sin(timeMs / 470 + tentacleInfluence.secondaryPhase + verticalProgress * 4.2) *
9889
+ lowerBlend *
9890
+ tentacleInfluence.core *
9891
+ tentacleInfluence.swayScale *
9892
+ 0.42;
9893
+ const tentacleCurl = Math.sin(timeMs / 1180 + tentacleInfluence.primaryPhase * 0.6 + verticalProgress * 3.2) *
9894
+ tipBlend *
9895
+ tentacleInfluence.core *
9896
+ tentacleInfluence.curlScale *
9897
+ 0.36;
9898
+ const horizontalScale = 1.06 +
9899
+ mantleRipple +
9900
+ lowerBlend * (0.18 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.12) +
9901
+ lowerBlend * tentacleInfluence.core * (0.22 + lowerLobeWave * 0.14) -
9902
+ upperBlend * 0.08;
9903
+ const depthScale = 1.08 +
9904
+ upperBlend * 0.16 +
9905
+ Math.max(0, Math.cos(effectiveLongitude)) * 0.12 +
9906
+ lowerBlend * tentacleInfluence.core * (0.12 + tentacleInfluence.depthScale * 0.07) -
9907
+ Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
9908
+ const tentacleTubeRadius = lowerBlend *
9909
+ tentacleInfluence.core *
9910
+ (0.12 + tipBlend * 0.07 + tentacleInfluence.widthScale * 0.028) *
9911
+ radiusX;
9912
+ const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
9913
+ const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.74;
9914
+ const lowerDrop = lowerBlend *
9915
+ radiusY *
9916
+ (0.2 +
9917
+ tentacleInfluence.core *
9918
+ (0.42 +
9919
+ tentacleInfluence.lengthScale * 0.24 +
9920
+ (morphologyProfile.tentacles.flowLengthScale - 1) * 0.1));
9921
+ const combinedTentacleSway = (primaryTentacleWave + secondaryTentacleWave) * radiusX * (0.06 + tipBlend * 0.06);
9922
+ return {
9923
+ x: Math.sin(effectiveLongitude) * planarRadiusX +
9924
+ combinedTentacleSway +
9925
+ tentacleCurl * radiusX * 0.18,
9926
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.14) -
9927
+ upperBlend * radiusY * 0.12 +
9928
+ lowerDrop +
9929
+ Math.sin(timeMs / 1380 + animationPhase + latitude * 1.5) * lowerBlend * radiusY * 0.022 +
9930
+ Math.cos(timeMs / 820 + tentacleInfluence.primaryPhase) *
9931
+ lowerBlend *
9932
+ tipBlend *
9933
+ tentacleInfluence.core *
9934
+ radiusY *
9935
+ 0.04,
9936
+ z: Math.cos(effectiveLongitude) * planarRadiusZ +
9937
+ Math.cos(timeMs / 960 + tentacleInfluence.primaryPhase + verticalProgress) *
9938
+ lowerBlend *
9939
+ tentacleInfluence.core *
9940
+ radiusZ *
9941
+ 0.044 +
9942
+ tentacleCurl * radiusZ * 0.14,
9943
+ };
9944
+ }
9945
+ /**
9946
+ * Blends nearby seeded tentacle profiles at one mesh longitude.
9947
+ *
9948
+ * @private helper of `octopus3d4AvatarVisual`
9949
+ */
9950
+ function resolveBlobbyTentacleInfluence(options, longitude) {
9951
+ let totalWeight = 0;
9952
+ let weightedSin = 0;
9953
+ let weightedCos = 0;
9954
+ let weightedWidthScale = 0;
9955
+ let weightedLengthScale = 0;
9956
+ let weightedSwayScale = 0;
9957
+ let weightedDepthScale = 0;
9958
+ let weightedCurlScale = 0;
9959
+ let weightedPrimaryPhase = 0;
9960
+ let weightedSecondaryPhase = 0;
9961
+ for (const tentacleProfile of options.tentacleProfiles) {
9962
+ const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
9963
+ const width = 0.22 * tentacleProfile.widthScale;
9964
+ const weight = Math.exp(-(distance * distance) / (width * width));
9965
+ totalWeight += weight;
9966
+ weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
9967
+ weightedCos += Math.cos(tentacleProfile.centerLongitude) * weight;
9968
+ weightedWidthScale += tentacleProfile.widthScale * weight;
9969
+ weightedLengthScale += tentacleProfile.lengthScale * weight;
9970
+ weightedSwayScale += tentacleProfile.swayScale * weight;
9971
+ weightedDepthScale += tentacleProfile.depthScale * weight;
9972
+ weightedCurlScale += tentacleProfile.curlScale * weight;
9973
+ weightedPrimaryPhase += tentacleProfile.primaryPhase * weight;
9974
+ weightedSecondaryPhase += tentacleProfile.secondaryPhase * weight;
9975
+ }
9976
+ if (totalWeight < 0.0001) {
9977
+ return {
9978
+ core: 0,
9979
+ centerLongitude: longitude,
9980
+ widthScale: 1,
9981
+ lengthScale: 1,
9982
+ swayScale: 1,
9983
+ depthScale: 1,
9984
+ curlScale: 1,
9985
+ primaryPhase: 0,
9986
+ secondaryPhase: 0,
9987
+ };
9988
+ }
9989
+ return {
9990
+ core: clampNumber$1(totalWeight, 0, 1),
9991
+ centerLongitude: Math.atan2(weightedSin / totalWeight, weightedCos / totalWeight),
9992
+ widthScale: weightedWidthScale / totalWeight,
9993
+ lengthScale: weightedLengthScale / totalWeight,
9994
+ swayScale: weightedSwayScale / totalWeight,
9995
+ depthScale: weightedDepthScale / totalWeight,
9996
+ curlScale: weightedCurlScale / totalWeight,
9997
+ primaryPhase: weightedPrimaryPhase / totalWeight,
9998
+ secondaryPhase: weightedSecondaryPhase / totalWeight,
9999
+ };
10000
+ }
10001
+ /**
10002
+ * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
10003
+ *
10004
+ * @private helper of `octopus3d4AvatarVisual`
10005
+ */
10006
+ function resolveBlobbyLobeWave(options, longitude) {
10007
+ const { morphologyProfile, animationPhase, timeMs } = options;
10008
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT +
10009
+ animationPhase +
10010
+ timeMs / (940 + morphologyProfile.body.lobeCount * 18)) +
10011
+ 1) /
10012
+ 2);
10013
+ }
10014
+ /**
10015
+ * Resolves one base fill tone for a patch on the continuous octopus mesh.
10016
+ *
10017
+ * @private helper of `octopus3d4AvatarVisual`
10018
+ */
10019
+ function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
10020
+ const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
10021
+ if (tonalProgress < 0.12) {
10022
+ return palette.highlight;
10023
+ }
10024
+ if (tonalProgress < 0.3) {
10025
+ return palette.secondary;
10026
+ }
10027
+ if (tonalProgress < 0.72) {
10028
+ return forwardness > 0.56 ? palette.secondary : palette.primary;
10029
+ }
10030
+ return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
10031
+ }
10032
+ /**
10033
+ * Draws one projected mesh patch with multi-pass shading and a soft edge.
10034
+ *
10035
+ * @private helper of `octopus3d4AvatarVisual`
10036
+ */
10037
+ function drawBlobbyContinuousSurfacePatch(context, surfacePatch) {
10038
+ drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
10039
+ if (surfacePatch.lightIntensity > 0) {
10040
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.2 * surfacePatch.lightIntensity})`);
10041
+ }
10042
+ else if (surfacePatch.lightIntensity < 0) {
10043
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.26 * Math.abs(surfacePatch.lightIntensity)})`);
10044
+ }
10045
+ if (surfacePatch.rimLightIntensity > 0.04) {
10046
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 247, 230, ${0.22 * surfacePatch.rimLightIntensity})`);
10047
+ }
10048
+ context.save();
10049
+ context.beginPath();
10050
+ context.moveTo(surfacePatch.corners[0].x, surfacePatch.corners[0].y);
10051
+ for (let cornerIndex = 1; cornerIndex < surfacePatch.corners.length; cornerIndex++) {
10052
+ context.lineTo(surfacePatch.corners[cornerIndex].x, surfacePatch.corners[cornerIndex].y);
10053
+ }
10054
+ context.closePath();
10055
+ context.strokeStyle = surfacePatch.outlineColor;
10056
+ context.lineWidth = Math.max(0.6, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0026);
10057
+ context.lineJoin = 'round';
10058
+ context.stroke();
10059
+ context.restore();
10060
+ }
10061
+ /**
10062
+ * Draws projected mantle-current lines on the front of the mesh.
10063
+ *
10064
+ * @private helper of `octopus3d4AvatarVisual`
10065
+ */
10066
+ function drawBlobbyContinuousCurrents(options) {
10067
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
10068
+ const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
10069
+ const centerIndex = (currentCount - 1) / 2;
10070
+ context.save();
10071
+ context.lineCap = 'round';
10072
+ context.lineJoin = 'round';
10073
+ for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
10074
+ const baseLongitude = (currentIndex - centerIndex) * 0.15;
10075
+ const projectedPoints = [];
10076
+ for (let sampleIndex = 0; sampleIndex < 9; sampleIndex++) {
10077
+ const progress = sampleIndex / 8;
10078
+ const latitude = -0.48 + progress * 0.78;
10079
+ const longitude = baseLongitude + Math.sin(timeMs / 1140 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.038;
10080
+ const scenePoint = transformScenePoint(sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
10081
+ if (scenePoint.z > center.z - size * 0.016) {
10082
+ projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
10083
+ }
10084
+ }
10085
+ if (projectedPoints.length < 3) {
10086
+ continue;
10087
+ }
10088
+ context.beginPath();
10089
+ context.moveTo(projectedPoints[0].x, projectedPoints[0].y);
10090
+ for (const projectedPoint of projectedPoints.slice(1)) {
10091
+ context.lineTo(projectedPoint.x, projectedPoint.y);
10092
+ }
10093
+ context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}3d` : `${palette.accent}33`;
10094
+ context.lineWidth = size * (0.0055 + currentIndex * 0.00045);
10095
+ context.stroke();
10096
+ }
10097
+ context.restore();
10098
+ }
10099
+ /**
10100
+ * Draws small projected sucker highlights on the waving lower mesh lobes.
10101
+ *
10102
+ * @private helper of `octopus3d4AvatarVisual`
10103
+ */
10104
+ function drawBlobbyContinuousSuckers(options) {
10105
+ const { surfaceOptions, size } = options;
10106
+ const { timeMs } = surfaceOptions;
10107
+ for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
10108
+ if (Math.cos(tentacleProfile.centerLongitude) < -0.16) {
10109
+ continue;
10110
+ }
10111
+ for (let suckerIndex = 0; suckerIndex < 4; suckerIndex++) {
10112
+ const latitude = 0.5 + suckerIndex * 0.12;
10113
+ const sideOffset = tentacleProfile.suckerSide * (0.038 + suckerIndex * 0.014) * tentacleProfile.widthScale;
10114
+ const waveOffset = Math.sin(timeMs / 880 + tentacleProfile.primaryPhase + suckerIndex * 0.78) * 0.02;
10115
+ drawBlobbyContinuousSurfaceSpot({
10116
+ ...options,
10117
+ latitude,
10118
+ longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
10119
+ radiusScale: size * (0.0068 - suckerIndex * 0.0006),
10120
+ });
10121
+ }
10122
+ }
10123
+ }
10124
+ /**
10125
+ * Draws seeded pigment spots across the upper mesh for a richer skin texture.
10126
+ *
10127
+ * @private helper of `octopus3d4AvatarVisual`
10128
+ */
10129
+ function drawBlobbySkinSpots(options) {
10130
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, skinSpots, } = options;
10131
+ for (const skinSpot of skinSpots) {
10132
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, skinSpot.latitude, skinSpot.longitude);
10133
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
10134
+ if (sceneCenterPoint.z <= center.z - size * 0.01) {
10135
+ continue;
10136
+ }
10137
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
10138
+ const spotRadius = size * skinSpot.radiusScale;
10139
+ context.save();
10140
+ context.beginPath();
10141
+ context.arc(projectedCenterPoint.x, projectedCenterPoint.y, spotRadius, 0, Math.PI * 2);
10142
+ context.fillStyle = `${palette.shadow}${formatAlphaHex(skinSpot.opacity)}`;
10143
+ context.fill();
10144
+ context.restore();
10145
+ }
10146
+ }
10147
+ /**
10148
+ * Draws one tiny projected surface spot by sampling local mesh tangents.
10149
+ *
10150
+ * @private helper of `octopus3d4AvatarVisual`
10151
+ */
10152
+ function drawBlobbyContinuousSurfaceSpot(options) {
10153
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
10154
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude);
10155
+ const localHorizontal = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
10156
+ const localVertical = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
10157
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
10158
+ if (sceneCenterPoint.z <= center.z - size * 0.012) {
10159
+ return;
10160
+ }
10161
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
10162
+ const projectedHorizontalPoint = projectScenePoint(transformScenePoint(localHorizontal, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
10163
+ const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
10164
+ const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
10165
+ radiusScale *
10166
+ 0.78, size * 0.003, size * 0.02);
10167
+ const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
10168
+ radiusScale *
10169
+ 0.54, size * 0.0024, size * 0.015);
10170
+ const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
10171
+ context.save();
10172
+ context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
10173
+ context.rotate(rotation);
10174
+ context.beginPath();
10175
+ context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
10176
+ context.fillStyle = `${palette.highlight}80`;
10177
+ context.fill();
10178
+ context.strokeStyle = `${palette.highlight}a8`;
10179
+ context.lineWidth = Math.max(0.7, size * 0.0028);
10180
+ context.stroke();
10181
+ context.restore();
10182
+ }
10183
+ /**
10184
+ * Draws a soft, slowly drifting gloss highlight on the front of the mesh.
10185
+ *
10186
+ * @private helper of `octopus3d4AvatarVisual`
10187
+ */
10188
+ function drawBlobbyContinuousGloss(options) {
10189
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size } = options;
10190
+ const { timeMs, animationPhase } = surfaceOptions;
10191
+ const glossLatitude = -0.3 + Math.sin(timeMs / 2700 + animationPhase) * 0.04;
10192
+ const glossLongitude = -0.18 + Math.cos(timeMs / 2300 + animationPhase * 0.8) * 0.05;
10193
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, glossLatitude, glossLongitude);
10194
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
10195
+ if (sceneCenterPoint.z <= center.z) {
10196
+ return;
10197
+ }
10198
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
10199
+ const glossRadius = size * 0.058;
10200
+ context.save();
10201
+ const glossGradient = context.createRadialGradient(projectedCenterPoint.x - glossRadius * 0.3, projectedCenterPoint.y - glossRadius * 0.4, glossRadius * 0.04, projectedCenterPoint.x, projectedCenterPoint.y, glossRadius);
10202
+ glossGradient.addColorStop(0, 'rgba(255, 255, 255, 0.34)');
10203
+ glossGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
10204
+ glossGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
10205
+ context.fillStyle = glossGradient;
10206
+ context.beginPath();
10207
+ context.ellipse(projectedCenterPoint.x, projectedCenterPoint.y, glossRadius * 1.05, glossRadius * 0.78, 0, 0, Math.PI * 2);
10208
+ context.fill();
10209
+ context.restore();
10210
+ }
10211
+ /**
10212
+ * Resolves a signed angular distance from the source longitude to the target longitude.
10213
+ *
10214
+ * @private helper of `octopus3d4AvatarVisual`
10215
+ */
10216
+ function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
10217
+ return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
10218
+ }
10219
+ /**
10220
+ * Smoothly maps a value between two bounds into `[0, 1]`.
10221
+ *
10222
+ * @private helper of `octopus3d4AvatarVisual`
10223
+ */
10224
+ function smoothStep(edgeStart, edgeEnd, value) {
10225
+ const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
10226
+ return progress * progress * (3 - 2 * progress);
10227
+ }
10228
+ /**
10229
+ * Converts an opacity ratio into a two-digit hexadecimal alpha suffix.
10230
+ *
10231
+ * @private helper of `octopus3d4AvatarVisual`
10232
+ */
10233
+ function formatAlphaHex(opacity) {
10234
+ return Math.round(clampNumber$1(opacity, 0, 1) * 255)
10235
+ .toString(16)
10236
+ .padStart(2, '0');
10237
+ }
10238
+
9484
10239
  /* eslint-disable no-magic-numbers */
9485
10240
  /**
9486
10241
  * Octopus avatar visual.
@@ -10252,6 +11007,7 @@ const AVATAR_VISUALS = [
10252
11007
  octopus3dAvatarVisual,
10253
11008
  octopus3d2AvatarVisual,
10254
11009
  octopus3d3AvatarVisual,
11010
+ octopus3d4AvatarVisual,
10255
11011
  asciiOctopusAvatarVisual,
10256
11012
  minecraftAvatarVisual,
10257
11013
  minecraft2AvatarVisual,