@promptbook/remote-server 0.112.0-134 → 0.112.0-136

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/umd/index.umd.js CHANGED
@@ -53,7 +53,7 @@
53
53
  * @generated
54
54
  * @see https://github.com/webgptorg/promptbook
55
55
  */
56
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-134';
56
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-136';
57
57
  /**
58
58
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
59
59
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -11070,6 +11070,11 @@
11070
11070
  * - `isValidAgentUrl` *(this one)* which tests just agent URL
11071
11071
  * - `isValidPipelineUrl` which tests just pipeline URL
11072
11072
  *
11073
+ * Note: This is a pure structural validator and does not block private/internal network
11074
+ * addresses. Callers that fetch the URL server-side from an untrusted network context must
11075
+ * additionally apply the `assertSafeUrl` SSRF guard from the Agents Server before any
11076
+ * outbound request.
11077
+ *
11073
11078
  * @public exported from `@promptbook/utils`
11074
11079
  */
11075
11080
  function isValidAgentUrl(url) {
@@ -11083,12 +11088,6 @@
11083
11088
  // TODO: [🐠]
11084
11089
  return false;
11085
11090
  }
11086
- /*
11087
- Note: [👣][🧠] Is it secure to allow pipeline URLs on private and unsecured networks?
11088
- if (isUrlOnPrivateNetwork(url)) {
11089
- return false;
11090
- }
11091
- */
11092
11091
  return true;
11093
11092
  }
11094
11093
  // TODO: [🐠] Maybe more info why the URL is invalid
@@ -14737,7 +14736,7 @@
14737
14736
  *
14738
14737
  * @private helper of `minecraft2AvatarVisual`
14739
14738
  */
14740
- const LIGHT_DIRECTION$3 = normalizeVector3({
14739
+ const LIGHT_DIRECTION$4 = normalizeVector3({
14741
14740
  x: 0.4,
14742
14741
  y: -0.65,
14743
14742
  z: 0.92,
@@ -14961,7 +14960,7 @@
14961
14960
  corners: projectedCorners,
14962
14961
  texture: faceDefinition.texture,
14963
14962
  averageDepth: transformedCorners.reduce((depthSum, corner) => depthSum + corner.z, 0) / transformedCorners.length,
14964
- lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$3), -1, 1),
14963
+ lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$4), -1, 1),
14965
14964
  outlineColor: cuboid.outlineColor,
14966
14965
  };
14967
14966
  });
@@ -15648,7 +15647,7 @@
15648
15647
  *
15649
15648
  * @private helper of `octopus3AvatarVisual`
15650
15649
  */
15651
- function formatAlphaHex$1(opacity) {
15650
+ function formatAlphaHex$2(opacity) {
15652
15651
  return Math.round(Math.min(1, Math.max(0, opacity)) * 255)
15653
15652
  .toString(16)
15654
15653
  .padStart(2, '0');
@@ -16027,7 +16026,7 @@
16027
16026
  context.beginPath();
16028
16027
  context.moveTo(-radiusX * 0.74, radiusY * 0.2);
16029
16028
  context.quadraticCurveTo(0, radiusY * 0.38, radiusX * 0.74, radiusY * 0.2);
16030
- context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
16029
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$2(eyeStyle.lowerLidOpacity)}`;
16031
16030
  context.lineWidth = radiusX * 0.08;
16032
16031
  context.lineCap = 'round';
16033
16032
  context.stroke();
@@ -16108,7 +16107,7 @@
16108
16107
  context.beginPath();
16109
16108
  context.moveTo(-projectedRadiusX * 0.74, projectedRadiusY * 0.2);
16110
16109
  context.quadraticCurveTo(0, projectedRadiusY * 0.38, projectedRadiusX * 0.74, projectedRadiusY * 0.2);
16111
- context.strokeStyle = `${palette.highlight}${formatAlphaHex(eyeStyle.lowerLidOpacity)}`;
16110
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
16112
16111
  context.lineWidth = projectedRadiusX * 0.08;
16113
16112
  context.lineCap = 'round';
16114
16113
  context.stroke();
@@ -16154,7 +16153,7 @@
16154
16153
  *
16155
16154
  * @private helper of the 3D octopus avatar visuals
16156
16155
  */
16157
- function formatAlphaHex(opacity) {
16156
+ function formatAlphaHex$1(opacity) {
16158
16157
  return Math.round(clampNumber$1(opacity, 0, 1) * 255)
16159
16158
  .toString(16)
16160
16159
  .padStart(2, '0');
@@ -16166,7 +16165,7 @@
16166
16165
  *
16167
16166
  * @private helper of `octopus3dAvatarVisual`
16168
16167
  */
16169
- const LIGHT_DIRECTION$2 = normalizeVector3({
16168
+ const LIGHT_DIRECTION$3 = normalizeVector3({
16170
16169
  x: 0.48,
16171
16170
  y: -0.62,
16172
16171
  z: 0.94,
@@ -16416,7 +16415,7 @@
16416
16415
  corners: projectedCorners,
16417
16416
  averageDepth: transformedCorners.reduce((depthSum, transformedCorner) => depthSum + transformedCorner.z, 0) /
16418
16417
  transformedCorners.length,
16419
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
16418
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$3), -1, 1),
16420
16419
  fillStyle: resolveSurfacePatchFillStyle(palette, verticalProgress + verticalColorBias),
16421
16420
  outlineColor,
16422
16421
  });
@@ -16605,7 +16604,7 @@
16605
16604
  *
16606
16605
  * @private helper of `octopus3d2AvatarVisual`
16607
16606
  */
16608
- const LIGHT_DIRECTION$1 = normalizeVector3({
16607
+ const LIGHT_DIRECTION$2 = normalizeVector3({
16609
16608
  x: 0.38,
16610
16609
  y: -0.6,
16611
16610
  z: 0.98,
@@ -16764,13 +16763,13 @@
16764
16763
  *
16765
16764
  * @private helper of `octopus3d2AvatarVisual`
16766
16765
  */
16767
- const LATITUDE_PATCH_COUNT$1 = 12;
16766
+ const LATITUDE_PATCH_COUNT$2 = 12;
16768
16767
  /**
16769
16768
  * Number of longitude segments used by the single blobby octopus mesh.
16770
16769
  *
16771
16770
  * @private helper of `octopus3d2AvatarVisual`
16772
16771
  */
16773
- const LONGITUDE_PATCH_COUNT$1 = 24;
16772
+ const LONGITUDE_PATCH_COUNT$2 = 24;
16774
16773
  /**
16775
16774
  * Resolves all visible projected patches for the single blobby octopus mesh.
16776
16775
  *
@@ -16784,8 +16783,8 @@
16784
16783
  */
16785
16784
  function resolveVisibleBlobbyOctopusPatches(options) {
16786
16785
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, animationPhase, timeMs, } = options;
16787
- const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
16788
- const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
16786
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$2;
16787
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$2;
16789
16788
  const surfacePatches = [];
16790
16789
  const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
16791
16790
  const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
@@ -16844,8 +16843,8 @@
16844
16843
  transformedCorners[2].z +
16845
16844
  transformedCorners[3].z) /
16846
16845
  4,
16847
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
16848
- fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
16846
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
16847
+ fillStyle: resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
16849
16848
  outlineColor: verticalProgress < 0.58 ? `${palette.highlight}73` : `${palette.shadow}8a`,
16850
16849
  });
16851
16850
  }
@@ -16913,7 +16912,7 @@
16913
16912
  *
16914
16913
  * @private helper of `octopus3d2AvatarVisual`
16915
16914
  */
16916
- function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, lowerLobeWave) {
16915
+ function resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, forwardness, lowerLobeWave) {
16917
16916
  const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.12 - forwardness * 0.07, 0, 1);
16918
16917
  if (tonalProgress < 0.16) {
16919
16918
  return palette.highlight;
@@ -16959,7 +16958,7 @@
16959
16958
  *
16960
16959
  * @private helper of `octopus3d3AvatarVisual`
16961
16960
  */
16962
- const LIGHT_DIRECTION = normalizeVector3({
16961
+ const LIGHT_DIRECTION$1 = normalizeVector3({
16963
16962
  x: 0.34,
16964
16963
  y: -0.62,
16965
16964
  z: 1,
@@ -16969,7 +16968,7 @@
16969
16968
  *
16970
16969
  * @private helper of `octopus3d3AvatarVisual`
16971
16970
  */
16972
- const OCTOPUS_TENTACLE_COUNT = 8;
16971
+ const OCTOPUS_TENTACLE_COUNT$1 = 8;
16973
16972
  /**
16974
16973
  * Cache keyed by the `createRandom` factory reference, which is stable for the lifetime of one
16975
16974
  * mounted `<Avatar/>` component (created inside `resolveAvatarRenderDefinition` and held in a
@@ -17108,9 +17107,9 @@
17108
17107
  * @private helper of `octopus3d3AvatarVisual`
17109
17108
  */
17110
17109
  function createContinuousTentacleProfiles(createRandom, morphologyProfile) {
17111
- return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
17110
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT$1 }, (_, tentacleIndex) => {
17112
17111
  const tentacleRandom = createRandom(`octopus3d3-tentacle-${tentacleIndex}`);
17113
- const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
17112
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT$1 - 1);
17114
17113
  return {
17115
17114
  centerLongitude: -Math.PI * 0.86 +
17116
17115
  progress * Math.PI * 1.72 +
@@ -17177,13 +17176,13 @@
17177
17176
  *
17178
17177
  * @private helper of `octopus3d3AvatarVisual`
17179
17178
  */
17180
- const LATITUDE_PATCH_COUNT = 16;
17179
+ const LATITUDE_PATCH_COUNT$1 = 16;
17181
17180
  /**
17182
17181
  * Number of longitude segments used by the continuous Octopus 3D 3 mesh.
17183
17182
  *
17184
17183
  * @private helper of `octopus3d3AvatarVisual`
17185
17184
  */
17186
- const LONGITUDE_PATCH_COUNT = 40;
17185
+ const LONGITUDE_PATCH_COUNT$1 = 40;
17187
17186
  /**
17188
17187
  * Resolves visible projected patches for the continuous octopus mesh.
17189
17188
  *
@@ -17197,8 +17196,8 @@
17197
17196
  */
17198
17197
  function resolveVisibleContinuousOctopusPatches(options) {
17199
17198
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
17200
- const latitudePatchCount = LATITUDE_PATCH_COUNT;
17201
- const longitudePatchCount = LONGITUDE_PATCH_COUNT;
17199
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
17200
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
17202
17201
  const surfacePatches = [];
17203
17202
  const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
17204
17203
  const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
@@ -17266,7 +17265,7 @@
17266
17265
  transformedCorners[2].z +
17267
17266
  transformedCorners[3].z) /
17268
17267
  4,
17269
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1),
17268
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
17270
17269
  fillStyle: resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
17271
17270
  outlineColor: verticalProgress < 0.54 ? `${palette.highlight}69` : `${palette.shadow}78`,
17272
17271
  });
@@ -17300,9 +17299,9 @@
17300
17299
  const cosineLatitude = Math.max(0, Math.cos(latitude));
17301
17300
  const verticalProgress = (Math.sin(latitude) + 1) / 2;
17302
17301
  const upperBlend = Math.pow(1 - verticalProgress, 1.28);
17303
- const lowerBlend = smoothStep(0.38, 1, verticalProgress);
17304
- const tipBlend = smoothStep(0.68, 1, verticalProgress);
17305
- const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
17302
+ const lowerBlend = smoothStep$1(0.38, 1, verticalProgress);
17303
+ const tipBlend = smoothStep$1(0.68, 1, verticalProgress);
17304
+ const centerPull = resolveSignedAngularDistance$1(longitude, tentacleInfluence.centerLongitude);
17306
17305
  const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.24 + tipBlend * 0.2);
17307
17306
  const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
17308
17307
  animationPhase * 0.6 +
@@ -17368,7 +17367,7 @@
17368
17367
  let weightedDepthScale = 0;
17369
17368
  let weightedPhase = 0;
17370
17369
  for (const tentacleProfile of options.tentacleProfiles) {
17371
- const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
17370
+ const distance = Math.abs(resolveSignedAngularDistance$1(longitude, tentacleProfile.centerLongitude));
17372
17371
  const width = 0.2 * tentacleProfile.widthScale;
17373
17372
  const weight = Math.exp(-(distance * distance) / (width * width));
17374
17373
  totalWeight += weight;
@@ -17408,7 +17407,7 @@
17408
17407
  */
17409
17408
  function resolveContinuousLobeWave(options, longitude) {
17410
17409
  const { morphologyProfile, animationPhase, timeMs } = options;
17411
- return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT +
17410
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT$1 +
17412
17411
  animationPhase +
17413
17412
  timeMs / (980 + morphologyProfile.body.lobeCount * 18)) +
17414
17413
  1) /
@@ -17562,7 +17561,7 @@
17562
17561
  *
17563
17562
  * @private helper of `octopus3d3AvatarVisual`
17564
17563
  */
17565
- function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
17564
+ function resolveSignedAngularDistance$1(sourceLongitude, targetLongitude) {
17566
17565
  return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
17567
17566
  }
17568
17567
  /**
@@ -17570,11 +17569,767 @@
17570
17569
  *
17571
17570
  * @private helper of `octopus3d3AvatarVisual`
17572
17571
  */
17573
- function smoothStep(edgeStart, edgeEnd, value) {
17572
+ function smoothStep$1(edgeStart, edgeEnd, value) {
17574
17573
  const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
17575
17574
  return progress * progress * (3 - 2 * progress);
17576
17575
  }
17577
17576
 
17577
+ /* eslint-disable no-magic-numbers */
17578
+ /**
17579
+ * Light direction used by the continuous Octopus 3D 4 mesh shading.
17580
+ *
17581
+ * @private helper of `octopus3d4AvatarVisual`
17582
+ */
17583
+ const LIGHT_DIRECTION = normalizeVector3({
17584
+ x: 0.32,
17585
+ y: -0.66,
17586
+ z: 1,
17587
+ });
17588
+ /**
17589
+ * Rim-light direction used to brighten the side of the silhouette.
17590
+ *
17591
+ * @private helper of `octopus3d4AvatarVisual`
17592
+ */
17593
+ const RIM_LIGHT_DIRECTION = normalizeVector3({
17594
+ x: -0.7,
17595
+ y: -0.18,
17596
+ z: 0.55,
17597
+ });
17598
+ /**
17599
+ * Real-octopus tentacle count used by the continuous lower mesh.
17600
+ *
17601
+ * @private helper of `octopus3d4AvatarVisual`
17602
+ */
17603
+ const OCTOPUS_TENTACLE_COUNT = 8;
17604
+ /**
17605
+ * Number of seeded skin spots painted across the octopus body.
17606
+ *
17607
+ * @private helper of `octopus3d4AvatarVisual`
17608
+ */
17609
+ const SKIN_SPOT_COUNT = 14;
17610
+ /**
17611
+ * Cache keyed by the `createRandom` factory reference, stable per mounted `<Avatar/>` component.
17612
+ *
17613
+ * @private helper of `octopus3d4AvatarVisual`
17614
+ */
17615
+ const octopus3d4StableStateCache = new WeakMap();
17616
+ /**
17617
+ * Returns the stable per-avatar state, computing it on first access and caching afterwards.
17618
+ *
17619
+ * @private helper of `octopus3d4AvatarVisual`
17620
+ */
17621
+ function getOctopus3d4StableState(createRandom) {
17622
+ const cached = octopus3d4StableStateCache.get(createRandom);
17623
+ if (cached !== undefined) {
17624
+ return cached;
17625
+ }
17626
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
17627
+ const animationRandom = createRandom('octopus3d4-animation-profile');
17628
+ const eyeRandom = createRandom('octopus3d4-eye-profile');
17629
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
17630
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
17631
+ const state = {
17632
+ morphologyProfile,
17633
+ animationPhase: animationRandom() * Math.PI * 2,
17634
+ leftEyePhaseOffset,
17635
+ rightEyePhaseOffset,
17636
+ tentacleProfiles: createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile),
17637
+ skinSpots: createBlobbySkinSpots(createRandom),
17638
+ };
17639
+ octopus3d4StableStateCache.set(createRandom, state);
17640
+ return state;
17641
+ }
17642
+ /**
17643
+ * Octopus 3D 4 avatar visual.
17644
+ *
17645
+ * @private built-in avatar visual
17646
+ */
17647
+ const octopus3d4AvatarVisual = {
17648
+ id: 'octopus3d4',
17649
+ title: 'Octopus 3D 4',
17650
+ 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.',
17651
+ isAnimated: true,
17652
+ supportsPointerTracking: true,
17653
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
17654
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles, skinSpots } = getOctopus3d4StableState(createRandom);
17655
+ const sceneCenterX = size * 0.5;
17656
+ const sceneCenterY = size * 0.535;
17657
+ const bob = Math.sin(timeMs / 980 + animationPhase) * size * 0.013;
17658
+ const meshCenter = {
17659
+ x: interaction.bodyOffsetX * size * 0.052 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
17660
+ y: -size * 0.07 + interaction.bodyOffsetY * size * 0.028 + bob,
17661
+ z: interaction.intensity * size * 0.02,
17662
+ };
17663
+ const rotationY = -0.08 +
17664
+ Math.sin(timeMs / 2800 + animationPhase) * 0.04 +
17665
+ interaction.bodyOffsetX * 0.24 +
17666
+ interaction.gazeX * 0.98;
17667
+ const rotationX = -0.07 +
17668
+ Math.cos(timeMs / 3200 + animationPhase * 0.7) * 0.02 -
17669
+ interaction.bodyOffsetY * 0.08 -
17670
+ interaction.gazeY * 0.42;
17671
+ const surfaceOptions = {
17672
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.12,
17673
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1,
17674
+ radiusZ: size *
17675
+ morphologyProfile.body.bodyRadiusRatio *
17676
+ (1.04 + (morphologyProfile.body.horizontalStretch - 1) * 0.2),
17677
+ morphologyProfile,
17678
+ timeMs,
17679
+ animationPhase,
17680
+ tentacleProfiles,
17681
+ };
17682
+ const surfacePatches = resolveVisibleBlobbyContinuousPatches({
17683
+ ...surfaceOptions,
17684
+ center: meshCenter,
17685
+ rotationX,
17686
+ rotationY,
17687
+ sceneCenterX,
17688
+ sceneCenterY,
17689
+ size,
17690
+ palette,
17691
+ });
17692
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.4 - 0.04, -0.24, 0.08);
17693
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.0, 0.18, 0.32);
17694
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.21 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
17695
+ const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
17696
+ const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
17697
+ const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
17698
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
17699
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
17700
+ drawAvatarFrame(context, size, palette);
17701
+ drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
17702
+ drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile);
17703
+ for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
17704
+ drawBlobbyContinuousSurfacePatch(context, surfacePatch);
17705
+ }
17706
+ drawBlobbySkinSpots({
17707
+ context,
17708
+ surfaceOptions,
17709
+ center: meshCenter,
17710
+ rotationX,
17711
+ rotationY,
17712
+ sceneCenterX,
17713
+ sceneCenterY,
17714
+ size,
17715
+ palette,
17716
+ skinSpots,
17717
+ });
17718
+ drawBlobbyContinuousCurrents({
17719
+ context,
17720
+ surfaceOptions,
17721
+ center: meshCenter,
17722
+ rotationX,
17723
+ rotationY,
17724
+ sceneCenterX,
17725
+ sceneCenterY,
17726
+ size,
17727
+ palette,
17728
+ morphologyProfile,
17729
+ timeMs,
17730
+ animationPhase,
17731
+ });
17732
+ drawBlobbyContinuousSuckers({
17733
+ context,
17734
+ surfaceOptions,
17735
+ center: meshCenter,
17736
+ rotationX,
17737
+ rotationY,
17738
+ sceneCenterX,
17739
+ sceneCenterY,
17740
+ size,
17741
+ palette,
17742
+ });
17743
+ drawBlobbyContinuousGloss({
17744
+ context,
17745
+ surfaceOptions,
17746
+ center: meshCenter,
17747
+ rotationX,
17748
+ rotationY,
17749
+ sceneCenterX,
17750
+ sceneCenterY,
17751
+ size,
17752
+ palette,
17753
+ });
17754
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
17755
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
17756
+ drawProjectedOrganicMouth(context, [
17757
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
17758
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
17759
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
17760
+ ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
17761
+ },
17762
+ };
17763
+ /**
17764
+ * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
17765
+ *
17766
+ * @private helper of `octopus3d4AvatarVisual`
17767
+ */
17768
+ function createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile) {
17769
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
17770
+ const tentacleRandom = createRandom(`octopus3d4-tentacle-${tentacleIndex}`);
17771
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
17772
+ return {
17773
+ centerLongitude: -Math.PI * 0.9 +
17774
+ progress * Math.PI * 1.8 +
17775
+ (tentacleRandom() - 0.5) * (0.06 + morphologyProfile.tentacles.rootSpreadScale * 0.025),
17776
+ widthScale: 0.92 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.18,
17777
+ lengthScale: 0.9 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.24,
17778
+ swayScale: 0.86 + tentacleRandom() * 0.4 + (morphologyProfile.tentacles.swayScale - 1) * 0.22,
17779
+ depthScale: 0.9 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.22,
17780
+ curlScale: 0.62 + tentacleRandom() * 0.46,
17781
+ primaryPhase: tentacleRandom() * Math.PI * 2,
17782
+ secondaryPhase: tentacleRandom() * Math.PI * 2,
17783
+ suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
17784
+ };
17785
+ });
17786
+ }
17787
+ /**
17788
+ * Creates seeded skin pigment spots distributed across the upper octopus mesh.
17789
+ *
17790
+ * @private helper of `octopus3d4AvatarVisual`
17791
+ */
17792
+ function createBlobbySkinSpots(createRandom) {
17793
+ const spotRandom = createRandom('octopus3d4-skin-spots');
17794
+ return Array.from({ length: SKIN_SPOT_COUNT }, () => ({
17795
+ latitude: -0.45 + spotRandom() * 0.7,
17796
+ longitude: -0.5 + spotRandom() * 1.0,
17797
+ radiusScale: 0.0028 + spotRandom() * 0.0052,
17798
+ opacity: 0.16 + spotRandom() * 0.22,
17799
+ }));
17800
+ }
17801
+ /**
17802
+ * Draws the soft underwater atmosphere behind the continuous octopus mesh.
17803
+ *
17804
+ * @private helper of `octopus3d4AvatarVisual`
17805
+ */
17806
+ function drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
17807
+ 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));
17808
+ glowGradient.addColorStop(0, `${palette.highlight}74`);
17809
+ glowGradient.addColorStop(0.32, `${palette.accent}32`);
17810
+ glowGradient.addColorStop(1, `${palette.highlight}00`);
17811
+ context.fillStyle = glowGradient;
17812
+ context.fillRect(0, 0, size, size);
17813
+ 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);
17814
+ lowerGradient.addColorStop(0, `${palette.secondary}28`);
17815
+ lowerGradient.addColorStop(1, `${palette.secondary}00`);
17816
+ context.fillStyle = lowerGradient;
17817
+ context.fillRect(0, 0, size, size);
17818
+ }
17819
+ /**
17820
+ * Draws the soft lower shadow that anchors the octopus in the avatar frame.
17821
+ *
17822
+ * @private helper of `octopus3d4AvatarVisual`
17823
+ */
17824
+ function drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
17825
+ const cx = size * 0.5 + interaction.gazeX * size * 0.046;
17826
+ const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
17827
+ const rx = size * (0.2 + morphologyProfile.tentacles.rootSpreadScale * 0.024 + interaction.intensity * 0.022);
17828
+ const ry = size * 0.062;
17829
+ context.save();
17830
+ context.translate(cx, cy);
17831
+ context.scale(1, ry / rx);
17832
+ const blurRadius = rx * 1.42;
17833
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
17834
+ shadowGradient.addColorStop(0, `${palette.shadow}82`);
17835
+ shadowGradient.addColorStop(0.45, `${palette.shadow}4a`);
17836
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1c`);
17837
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
17838
+ context.fillStyle = shadowGradient;
17839
+ context.beginPath();
17840
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
17841
+ context.fill();
17842
+ context.restore();
17843
+ }
17844
+ /**
17845
+ * Number of latitude segments used by the continuous Octopus 3D 4 mesh.
17846
+ *
17847
+ * @private helper of `octopus3d4AvatarVisual`
17848
+ */
17849
+ const LATITUDE_PATCH_COUNT = 20;
17850
+ /**
17851
+ * Number of longitude segments used by the continuous Octopus 3D 4 mesh.
17852
+ *
17853
+ * @private helper of `octopus3d4AvatarVisual`
17854
+ */
17855
+ const LONGITUDE_PATCH_COUNT = 48;
17856
+ /**
17857
+ * Resolves visible projected patches for the continuous Octopus 3D 4 mesh.
17858
+ *
17859
+ * Within a single frame, mesh corner samples and longitude-only computations (tentacle
17860
+ * influence and lobe wave) are quantized to the patch grid and computed once each rather
17861
+ * than re-evaluated for every patch corner.
17862
+ *
17863
+ * @private helper of `octopus3d4AvatarVisual`
17864
+ */
17865
+ function resolveVisibleBlobbyContinuousPatches(options) {
17866
+ const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
17867
+ const latitudePatchCount = LATITUDE_PATCH_COUNT;
17868
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT;
17869
+ const surfacePatches = [];
17870
+ const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
17871
+ const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
17872
+ for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
17873
+ latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
17874
+ }
17875
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
17876
+ longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
17877
+ }
17878
+ const cachedTentacleInfluencesByCornerLongitude = new Array(longitudePatchCount + 1);
17879
+ const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
17880
+ const cachedTentacleInfluencesByPatchCenterLongitude = new Array(longitudePatchCount);
17881
+ const cachedLobeWavesByPatchCenterLongitude = new Float64Array(longitudePatchCount);
17882
+ const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
17883
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
17884
+ const cornerLongitude = longitudeBoundaries[boundaryIndex];
17885
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveBlobbyTentacleInfluence(options, cornerLongitude);
17886
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveBlobbyLobeWave(options, cornerLongitude);
17887
+ }
17888
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
17889
+ const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
17890
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyTentacleInfluence(options, patchCenterLongitude);
17891
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyLobeWave(options, patchCenterLongitude);
17892
+ cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
17893
+ }
17894
+ const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
17895
+ const transformedCornerSamples = new Array(cornerCount);
17896
+ for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
17897
+ const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex];
17898
+ for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
17899
+ const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
17900
+ const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
17901
+ const cornerSample = sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
17902
+ transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
17903
+ }
17904
+ }
17905
+ for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
17906
+ const startLatitude = latitudeBoundaries[latitudeIndex];
17907
+ const endLatitude = latitudeBoundaries[latitudeIndex + 1];
17908
+ const centerLatitude = (startLatitude + endLatitude) / 2;
17909
+ const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
17910
+ const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
17911
+ const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
17912
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
17913
+ const transformedCorners = [
17914
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex],
17915
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1],
17916
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1],
17917
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex],
17918
+ ];
17919
+ const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
17920
+ if (surfaceNormal.z <= 0.006) {
17921
+ continue;
17922
+ }
17923
+ const projectedCorners = [
17924
+ projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
17925
+ projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
17926
+ projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
17927
+ projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
17928
+ ];
17929
+ const lightIntensity = clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1);
17930
+ const rimLightIntensity = Math.pow(clampNumber$1(dotProduct3D(surfaceNormal, RIM_LIGHT_DIRECTION), 0, 1), 2.6);
17931
+ surfacePatches.push({
17932
+ corners: projectedCorners,
17933
+ averageDepth: (transformedCorners[0].z +
17934
+ transformedCorners[1].z +
17935
+ transformedCorners[2].z +
17936
+ transformedCorners[3].z) /
17937
+ 4,
17938
+ lightIntensity,
17939
+ rimLightIntensity,
17940
+ fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
17941
+ outlineColor: verticalProgress < 0.52 ? `${palette.highlight}5c` : `${palette.shadow}66`,
17942
+ });
17943
+ }
17944
+ }
17945
+ return surfacePatches;
17946
+ }
17947
+ /**
17948
+ * Samples one point on the continuous Octopus 3D 4 surface.
17949
+ *
17950
+ * @private helper of `octopus3d4AvatarVisual`
17951
+ */
17952
+ function sampleBlobbyContinuousSurfacePoint(options, latitude, longitude) {
17953
+ return sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, resolveBlobbyTentacleInfluence(options, longitude), resolveBlobbyLobeWave(options, longitude));
17954
+ }
17955
+ /**
17956
+ * Samples one point on the continuous Octopus 3D 4 surface using precomputed longitude-only values.
17957
+ *
17958
+ * The patch loop quantizes the mesh into a fixed corner grid, so the same longitude is reused
17959
+ * across every latitude row and each tentacle/lobe value is computed once per frame instead
17960
+ * of `latitudePatchCount * 4` times.
17961
+ *
17962
+ * @private helper of `octopus3d4AvatarVisual`
17963
+ */
17964
+ function sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
17965
+ const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
17966
+ const cosineLatitude = Math.max(0, Math.cos(latitude));
17967
+ const verticalProgress = (Math.sin(latitude) + 1) / 2;
17968
+ const upperBlend = Math.pow(1 - verticalProgress, 1.32);
17969
+ const lowerBlend = smoothStep(0.34, 1, verticalProgress);
17970
+ const tipBlend = smoothStep(0.66, 1, verticalProgress);
17971
+ const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
17972
+ const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.26 + tipBlend * 0.22);
17973
+ const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
17974
+ animationPhase * 0.6 +
17975
+ timeMs / (1700 + morphologyProfile.body.lobeCount * 28)) *
17976
+ (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.78) *
17977
+ (0.32 + lowerBlend * 0.7);
17978
+ const primaryTentacleWave = Math.sin(timeMs / 740 + tentacleInfluence.primaryPhase + verticalProgress * 2.6) *
17979
+ lowerBlend *
17980
+ tentacleInfluence.core *
17981
+ tentacleInfluence.swayScale;
17982
+ const secondaryTentacleWave = Math.sin(timeMs / 470 + tentacleInfluence.secondaryPhase + verticalProgress * 4.2) *
17983
+ lowerBlend *
17984
+ tentacleInfluence.core *
17985
+ tentacleInfluence.swayScale *
17986
+ 0.42;
17987
+ const tentacleCurl = Math.sin(timeMs / 1180 + tentacleInfluence.primaryPhase * 0.6 + verticalProgress * 3.2) *
17988
+ tipBlend *
17989
+ tentacleInfluence.core *
17990
+ tentacleInfluence.curlScale *
17991
+ 0.36;
17992
+ const horizontalScale = 1.06 +
17993
+ mantleRipple +
17994
+ lowerBlend * (0.18 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.12) +
17995
+ lowerBlend * tentacleInfluence.core * (0.22 + lowerLobeWave * 0.14) -
17996
+ upperBlend * 0.08;
17997
+ const depthScale = 1.08 +
17998
+ upperBlend * 0.16 +
17999
+ Math.max(0, Math.cos(effectiveLongitude)) * 0.12 +
18000
+ lowerBlend * tentacleInfluence.core * (0.12 + tentacleInfluence.depthScale * 0.07) -
18001
+ Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
18002
+ const tentacleTubeRadius = lowerBlend *
18003
+ tentacleInfluence.core *
18004
+ (0.12 + tipBlend * 0.07 + tentacleInfluence.widthScale * 0.028) *
18005
+ radiusX;
18006
+ const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
18007
+ const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.74;
18008
+ const lowerDrop = lowerBlend *
18009
+ radiusY *
18010
+ (0.2 +
18011
+ tentacleInfluence.core *
18012
+ (0.42 +
18013
+ tentacleInfluence.lengthScale * 0.24 +
18014
+ (morphologyProfile.tentacles.flowLengthScale - 1) * 0.1));
18015
+ const combinedTentacleSway = (primaryTentacleWave + secondaryTentacleWave) * radiusX * (0.06 + tipBlend * 0.06);
18016
+ return {
18017
+ x: Math.sin(effectiveLongitude) * planarRadiusX +
18018
+ combinedTentacleSway +
18019
+ tentacleCurl * radiusX * 0.18,
18020
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.14) -
18021
+ upperBlend * radiusY * 0.12 +
18022
+ lowerDrop +
18023
+ Math.sin(timeMs / 1380 + animationPhase + latitude * 1.5) * lowerBlend * radiusY * 0.022 +
18024
+ Math.cos(timeMs / 820 + tentacleInfluence.primaryPhase) *
18025
+ lowerBlend *
18026
+ tipBlend *
18027
+ tentacleInfluence.core *
18028
+ radiusY *
18029
+ 0.04,
18030
+ z: Math.cos(effectiveLongitude) * planarRadiusZ +
18031
+ Math.cos(timeMs / 960 + tentacleInfluence.primaryPhase + verticalProgress) *
18032
+ lowerBlend *
18033
+ tentacleInfluence.core *
18034
+ radiusZ *
18035
+ 0.044 +
18036
+ tentacleCurl * radiusZ * 0.14,
18037
+ };
18038
+ }
18039
+ /**
18040
+ * Blends nearby seeded tentacle profiles at one mesh longitude.
18041
+ *
18042
+ * @private helper of `octopus3d4AvatarVisual`
18043
+ */
18044
+ function resolveBlobbyTentacleInfluence(options, longitude) {
18045
+ let totalWeight = 0;
18046
+ let weightedSin = 0;
18047
+ let weightedCos = 0;
18048
+ let weightedWidthScale = 0;
18049
+ let weightedLengthScale = 0;
18050
+ let weightedSwayScale = 0;
18051
+ let weightedDepthScale = 0;
18052
+ let weightedCurlScale = 0;
18053
+ let weightedPrimaryPhase = 0;
18054
+ let weightedSecondaryPhase = 0;
18055
+ for (const tentacleProfile of options.tentacleProfiles) {
18056
+ const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
18057
+ const width = 0.22 * tentacleProfile.widthScale;
18058
+ const weight = Math.exp(-(distance * distance) / (width * width));
18059
+ totalWeight += weight;
18060
+ weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
18061
+ weightedCos += Math.cos(tentacleProfile.centerLongitude) * weight;
18062
+ weightedWidthScale += tentacleProfile.widthScale * weight;
18063
+ weightedLengthScale += tentacleProfile.lengthScale * weight;
18064
+ weightedSwayScale += tentacleProfile.swayScale * weight;
18065
+ weightedDepthScale += tentacleProfile.depthScale * weight;
18066
+ weightedCurlScale += tentacleProfile.curlScale * weight;
18067
+ weightedPrimaryPhase += tentacleProfile.primaryPhase * weight;
18068
+ weightedSecondaryPhase += tentacleProfile.secondaryPhase * weight;
18069
+ }
18070
+ if (totalWeight < 0.0001) {
18071
+ return {
18072
+ core: 0,
18073
+ centerLongitude: longitude,
18074
+ widthScale: 1,
18075
+ lengthScale: 1,
18076
+ swayScale: 1,
18077
+ depthScale: 1,
18078
+ curlScale: 1,
18079
+ primaryPhase: 0,
18080
+ secondaryPhase: 0,
18081
+ };
18082
+ }
18083
+ return {
18084
+ core: clampNumber$1(totalWeight, 0, 1),
18085
+ centerLongitude: Math.atan2(weightedSin / totalWeight, weightedCos / totalWeight),
18086
+ widthScale: weightedWidthScale / totalWeight,
18087
+ lengthScale: weightedLengthScale / totalWeight,
18088
+ swayScale: weightedSwayScale / totalWeight,
18089
+ depthScale: weightedDepthScale / totalWeight,
18090
+ curlScale: weightedCurlScale / totalWeight,
18091
+ primaryPhase: weightedPrimaryPhase / totalWeight,
18092
+ secondaryPhase: weightedSecondaryPhase / totalWeight,
18093
+ };
18094
+ }
18095
+ /**
18096
+ * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
18097
+ *
18098
+ * @private helper of `octopus3d4AvatarVisual`
18099
+ */
18100
+ function resolveBlobbyLobeWave(options, longitude) {
18101
+ const { morphologyProfile, animationPhase, timeMs } = options;
18102
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT +
18103
+ animationPhase +
18104
+ timeMs / (940 + morphologyProfile.body.lobeCount * 18)) +
18105
+ 1) /
18106
+ 2);
18107
+ }
18108
+ /**
18109
+ * Resolves one base fill tone for a patch on the continuous octopus mesh.
18110
+ *
18111
+ * @private helper of `octopus3d4AvatarVisual`
18112
+ */
18113
+ function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
18114
+ const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
18115
+ if (tonalProgress < 0.12) {
18116
+ return palette.highlight;
18117
+ }
18118
+ if (tonalProgress < 0.3) {
18119
+ return palette.secondary;
18120
+ }
18121
+ if (tonalProgress < 0.72) {
18122
+ return forwardness > 0.56 ? palette.secondary : palette.primary;
18123
+ }
18124
+ return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
18125
+ }
18126
+ /**
18127
+ * Draws one projected mesh patch with multi-pass shading and a soft edge.
18128
+ *
18129
+ * @private helper of `octopus3d4AvatarVisual`
18130
+ */
18131
+ function drawBlobbyContinuousSurfacePatch(context, surfacePatch) {
18132
+ drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
18133
+ if (surfacePatch.lightIntensity > 0) {
18134
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.2 * surfacePatch.lightIntensity})`);
18135
+ }
18136
+ else if (surfacePatch.lightIntensity < 0) {
18137
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.26 * Math.abs(surfacePatch.lightIntensity)})`);
18138
+ }
18139
+ if (surfacePatch.rimLightIntensity > 0.04) {
18140
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 247, 230, ${0.22 * surfacePatch.rimLightIntensity})`);
18141
+ }
18142
+ context.save();
18143
+ context.beginPath();
18144
+ context.moveTo(surfacePatch.corners[0].x, surfacePatch.corners[0].y);
18145
+ for (let cornerIndex = 1; cornerIndex < surfacePatch.corners.length; cornerIndex++) {
18146
+ context.lineTo(surfacePatch.corners[cornerIndex].x, surfacePatch.corners[cornerIndex].y);
18147
+ }
18148
+ context.closePath();
18149
+ context.strokeStyle = surfacePatch.outlineColor;
18150
+ context.lineWidth = Math.max(0.6, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0026);
18151
+ context.lineJoin = 'round';
18152
+ context.stroke();
18153
+ context.restore();
18154
+ }
18155
+ /**
18156
+ * Draws projected mantle-current lines on the front of the mesh.
18157
+ *
18158
+ * @private helper of `octopus3d4AvatarVisual`
18159
+ */
18160
+ function drawBlobbyContinuousCurrents(options) {
18161
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
18162
+ const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
18163
+ const centerIndex = (currentCount - 1) / 2;
18164
+ context.save();
18165
+ context.lineCap = 'round';
18166
+ context.lineJoin = 'round';
18167
+ for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
18168
+ const baseLongitude = (currentIndex - centerIndex) * 0.15;
18169
+ const projectedPoints = [];
18170
+ for (let sampleIndex = 0; sampleIndex < 9; sampleIndex++) {
18171
+ const progress = sampleIndex / 8;
18172
+ const latitude = -0.48 + progress * 0.78;
18173
+ const longitude = baseLongitude + Math.sin(timeMs / 1140 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.038;
18174
+ const scenePoint = transformScenePoint(sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
18175
+ if (scenePoint.z > center.z - size * 0.016) {
18176
+ projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
18177
+ }
18178
+ }
18179
+ if (projectedPoints.length < 3) {
18180
+ continue;
18181
+ }
18182
+ context.beginPath();
18183
+ context.moveTo(projectedPoints[0].x, projectedPoints[0].y);
18184
+ for (const projectedPoint of projectedPoints.slice(1)) {
18185
+ context.lineTo(projectedPoint.x, projectedPoint.y);
18186
+ }
18187
+ context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}3d` : `${palette.accent}33`;
18188
+ context.lineWidth = size * (0.0055 + currentIndex * 0.00045);
18189
+ context.stroke();
18190
+ }
18191
+ context.restore();
18192
+ }
18193
+ /**
18194
+ * Draws small projected sucker highlights on the waving lower mesh lobes.
18195
+ *
18196
+ * @private helper of `octopus3d4AvatarVisual`
18197
+ */
18198
+ function drawBlobbyContinuousSuckers(options) {
18199
+ const { surfaceOptions, size } = options;
18200
+ const { timeMs } = surfaceOptions;
18201
+ for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
18202
+ if (Math.cos(tentacleProfile.centerLongitude) < -0.16) {
18203
+ continue;
18204
+ }
18205
+ for (let suckerIndex = 0; suckerIndex < 4; suckerIndex++) {
18206
+ const latitude = 0.5 + suckerIndex * 0.12;
18207
+ const sideOffset = tentacleProfile.suckerSide * (0.038 + suckerIndex * 0.014) * tentacleProfile.widthScale;
18208
+ const waveOffset = Math.sin(timeMs / 880 + tentacleProfile.primaryPhase + suckerIndex * 0.78) * 0.02;
18209
+ drawBlobbyContinuousSurfaceSpot({
18210
+ ...options,
18211
+ latitude,
18212
+ longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
18213
+ radiusScale: size * (0.0068 - suckerIndex * 0.0006),
18214
+ });
18215
+ }
18216
+ }
18217
+ }
18218
+ /**
18219
+ * Draws seeded pigment spots across the upper mesh for a richer skin texture.
18220
+ *
18221
+ * @private helper of `octopus3d4AvatarVisual`
18222
+ */
18223
+ function drawBlobbySkinSpots(options) {
18224
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, skinSpots, } = options;
18225
+ for (const skinSpot of skinSpots) {
18226
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, skinSpot.latitude, skinSpot.longitude);
18227
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
18228
+ if (sceneCenterPoint.z <= center.z - size * 0.01) {
18229
+ continue;
18230
+ }
18231
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
18232
+ const spotRadius = size * skinSpot.radiusScale;
18233
+ context.save();
18234
+ context.beginPath();
18235
+ context.arc(projectedCenterPoint.x, projectedCenterPoint.y, spotRadius, 0, Math.PI * 2);
18236
+ context.fillStyle = `${palette.shadow}${formatAlphaHex(skinSpot.opacity)}`;
18237
+ context.fill();
18238
+ context.restore();
18239
+ }
18240
+ }
18241
+ /**
18242
+ * Draws one tiny projected surface spot by sampling local mesh tangents.
18243
+ *
18244
+ * @private helper of `octopus3d4AvatarVisual`
18245
+ */
18246
+ function drawBlobbyContinuousSurfaceSpot(options) {
18247
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
18248
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude);
18249
+ const localHorizontal = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
18250
+ const localVertical = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
18251
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
18252
+ if (sceneCenterPoint.z <= center.z - size * 0.012) {
18253
+ return;
18254
+ }
18255
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
18256
+ const projectedHorizontalPoint = projectScenePoint(transformScenePoint(localHorizontal, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
18257
+ const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
18258
+ const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
18259
+ radiusScale *
18260
+ 0.78, size * 0.003, size * 0.02);
18261
+ const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
18262
+ radiusScale *
18263
+ 0.54, size * 0.0024, size * 0.015);
18264
+ const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
18265
+ context.save();
18266
+ context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
18267
+ context.rotate(rotation);
18268
+ context.beginPath();
18269
+ context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
18270
+ context.fillStyle = `${palette.highlight}80`;
18271
+ context.fill();
18272
+ context.strokeStyle = `${palette.highlight}a8`;
18273
+ context.lineWidth = Math.max(0.7, size * 0.0028);
18274
+ context.stroke();
18275
+ context.restore();
18276
+ }
18277
+ /**
18278
+ * Draws a soft, slowly drifting gloss highlight on the front of the mesh.
18279
+ *
18280
+ * @private helper of `octopus3d4AvatarVisual`
18281
+ */
18282
+ function drawBlobbyContinuousGloss(options) {
18283
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size } = options;
18284
+ const { timeMs, animationPhase } = surfaceOptions;
18285
+ const glossLatitude = -0.3 + Math.sin(timeMs / 2700 + animationPhase) * 0.04;
18286
+ const glossLongitude = -0.18 + Math.cos(timeMs / 2300 + animationPhase * 0.8) * 0.05;
18287
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, glossLatitude, glossLongitude);
18288
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
18289
+ if (sceneCenterPoint.z <= center.z) {
18290
+ return;
18291
+ }
18292
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
18293
+ const glossRadius = size * 0.058;
18294
+ context.save();
18295
+ const glossGradient = context.createRadialGradient(projectedCenterPoint.x - glossRadius * 0.3, projectedCenterPoint.y - glossRadius * 0.4, glossRadius * 0.04, projectedCenterPoint.x, projectedCenterPoint.y, glossRadius);
18296
+ glossGradient.addColorStop(0, 'rgba(255, 255, 255, 0.34)');
18297
+ glossGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
18298
+ glossGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
18299
+ context.fillStyle = glossGradient;
18300
+ context.beginPath();
18301
+ context.ellipse(projectedCenterPoint.x, projectedCenterPoint.y, glossRadius * 1.05, glossRadius * 0.78, 0, 0, Math.PI * 2);
18302
+ context.fill();
18303
+ context.restore();
18304
+ }
18305
+ /**
18306
+ * Resolves a signed angular distance from the source longitude to the target longitude.
18307
+ *
18308
+ * @private helper of `octopus3d4AvatarVisual`
18309
+ */
18310
+ function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
18311
+ return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
18312
+ }
18313
+ /**
18314
+ * Smoothly maps a value between two bounds into `[0, 1]`.
18315
+ *
18316
+ * @private helper of `octopus3d4AvatarVisual`
18317
+ */
18318
+ function smoothStep(edgeStart, edgeEnd, value) {
18319
+ const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
18320
+ return progress * progress * (3 - 2 * progress);
18321
+ }
18322
+ /**
18323
+ * Converts an opacity ratio into a two-digit hexadecimal alpha suffix.
18324
+ *
18325
+ * @private helper of `octopus3d4AvatarVisual`
18326
+ */
18327
+ function formatAlphaHex(opacity) {
18328
+ return Math.round(clampNumber$1(opacity, 0, 1) * 255)
18329
+ .toString(16)
18330
+ .padStart(2, '0');
18331
+ }
18332
+
17578
18333
  /* eslint-disable no-magic-numbers */
17579
18334
  /**
17580
18335
  * Octopus avatar visual.
@@ -18346,6 +19101,7 @@
18346
19101
  octopus3dAvatarVisual,
18347
19102
  octopus3d2AvatarVisual,
18348
19103
  octopus3d3AvatarVisual,
19104
+ octopus3d4AvatarVisual,
18349
19105
  asciiOctopusAvatarVisual,
18350
19106
  minecraftAvatarVisual,
18351
19107
  minecraft2AvatarVisual,