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