@promptbook/components 0.112.0-134 → 0.112.0-135

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/umd/index.umd.js CHANGED
@@ -34,7 +34,7 @@
34
34
  * @generated
35
35
  * @see https://github.com/webgptorg/promptbook
36
36
  */
37
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-134';
37
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-135';
38
38
  /**
39
39
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
40
40
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -3247,7 +3247,7 @@
3247
3247
  *
3248
3248
  * @private helper of `minecraft2AvatarVisual`
3249
3249
  */
3250
- const LIGHT_DIRECTION$3 = normalizeVector3({
3250
+ const LIGHT_DIRECTION$4 = normalizeVector3({
3251
3251
  x: 0.4,
3252
3252
  y: -0.65,
3253
3253
  z: 0.92,
@@ -3471,7 +3471,7 @@
3471
3471
  corners: projectedCorners,
3472
3472
  texture: faceDefinition.texture,
3473
3473
  averageDepth: transformedCorners.reduce((depthSum, corner) => depthSum + corner.z, 0) / transformedCorners.length,
3474
- lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$3), -1, 1),
3474
+ lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$4), -1, 1),
3475
3475
  outlineColor: cuboid.outlineColor,
3476
3476
  };
3477
3477
  });
@@ -4158,7 +4158,7 @@
4158
4158
  *
4159
4159
  * @private helper of `octopus3AvatarVisual`
4160
4160
  */
4161
- function formatAlphaHex$1(opacity) {
4161
+ function formatAlphaHex$2(opacity) {
4162
4162
  return Math.round(Math.min(1, Math.max(0, opacity)) * 255)
4163
4163
  .toString(16)
4164
4164
  .padStart(2, '0');
@@ -4537,7 +4537,7 @@
4537
4537
  context.beginPath();
4538
4538
  context.moveTo(-radiusX * 0.74, radiusY * 0.2);
4539
4539
  context.quadraticCurveTo(0, radiusY * 0.38, radiusX * 0.74, radiusY * 0.2);
4540
- context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
4540
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$2(eyeStyle.lowerLidOpacity)}`;
4541
4541
  context.lineWidth = radiusX * 0.08;
4542
4542
  context.lineCap = 'round';
4543
4543
  context.stroke();
@@ -4618,7 +4618,7 @@
4618
4618
  context.beginPath();
4619
4619
  context.moveTo(-projectedRadiusX * 0.74, projectedRadiusY * 0.2);
4620
4620
  context.quadraticCurveTo(0, projectedRadiusY * 0.38, projectedRadiusX * 0.74, projectedRadiusY * 0.2);
4621
- context.strokeStyle = `${palette.highlight}${formatAlphaHex(eyeStyle.lowerLidOpacity)}`;
4621
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
4622
4622
  context.lineWidth = projectedRadiusX * 0.08;
4623
4623
  context.lineCap = 'round';
4624
4624
  context.stroke();
@@ -4664,7 +4664,7 @@
4664
4664
  *
4665
4665
  * @private helper of the 3D octopus avatar visuals
4666
4666
  */
4667
- function formatAlphaHex(opacity) {
4667
+ function formatAlphaHex$1(opacity) {
4668
4668
  return Math.round(clampNumber$1(opacity, 0, 1) * 255)
4669
4669
  .toString(16)
4670
4670
  .padStart(2, '0');
@@ -4676,7 +4676,7 @@
4676
4676
  *
4677
4677
  * @private helper of `octopus3dAvatarVisual`
4678
4678
  */
4679
- const LIGHT_DIRECTION$2 = normalizeVector3({
4679
+ const LIGHT_DIRECTION$3 = normalizeVector3({
4680
4680
  x: 0.48,
4681
4681
  y: -0.62,
4682
4682
  z: 0.94,
@@ -4926,7 +4926,7 @@
4926
4926
  corners: projectedCorners,
4927
4927
  averageDepth: transformedCorners.reduce((depthSum, transformedCorner) => depthSum + transformedCorner.z, 0) /
4928
4928
  transformedCorners.length,
4929
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
4929
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$3), -1, 1),
4930
4930
  fillStyle: resolveSurfacePatchFillStyle(palette, verticalProgress + verticalColorBias),
4931
4931
  outlineColor,
4932
4932
  });
@@ -5115,7 +5115,7 @@
5115
5115
  *
5116
5116
  * @private helper of `octopus3d2AvatarVisual`
5117
5117
  */
5118
- const LIGHT_DIRECTION$1 = normalizeVector3({
5118
+ const LIGHT_DIRECTION$2 = normalizeVector3({
5119
5119
  x: 0.38,
5120
5120
  y: -0.6,
5121
5121
  z: 0.98,
@@ -5274,13 +5274,13 @@
5274
5274
  *
5275
5275
  * @private helper of `octopus3d2AvatarVisual`
5276
5276
  */
5277
- const LATITUDE_PATCH_COUNT$1 = 12;
5277
+ const LATITUDE_PATCH_COUNT$2 = 12;
5278
5278
  /**
5279
5279
  * Number of longitude segments used by the single blobby octopus mesh.
5280
5280
  *
5281
5281
  * @private helper of `octopus3d2AvatarVisual`
5282
5282
  */
5283
- const LONGITUDE_PATCH_COUNT$1 = 24;
5283
+ const LONGITUDE_PATCH_COUNT$2 = 24;
5284
5284
  /**
5285
5285
  * Resolves all visible projected patches for the single blobby octopus mesh.
5286
5286
  *
@@ -5294,8 +5294,8 @@
5294
5294
  */
5295
5295
  function resolveVisibleBlobbyOctopusPatches(options) {
5296
5296
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, animationPhase, timeMs, } = options;
5297
- const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
5298
- const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
5297
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$2;
5298
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$2;
5299
5299
  const surfacePatches = [];
5300
5300
  const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
5301
5301
  const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
@@ -5354,8 +5354,8 @@
5354
5354
  transformedCorners[2].z +
5355
5355
  transformedCorners[3].z) /
5356
5356
  4,
5357
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
5358
- fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
5357
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
5358
+ fillStyle: resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
5359
5359
  outlineColor: verticalProgress < 0.58 ? `${palette.highlight}73` : `${palette.shadow}8a`,
5360
5360
  });
5361
5361
  }
@@ -5423,7 +5423,7 @@
5423
5423
  *
5424
5424
  * @private helper of `octopus3d2AvatarVisual`
5425
5425
  */
5426
- function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, lowerLobeWave) {
5426
+ function resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, forwardness, lowerLobeWave) {
5427
5427
  const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.12 - forwardness * 0.07, 0, 1);
5428
5428
  if (tonalProgress < 0.16) {
5429
5429
  return palette.highlight;
@@ -5469,7 +5469,7 @@
5469
5469
  *
5470
5470
  * @private helper of `octopus3d3AvatarVisual`
5471
5471
  */
5472
- const LIGHT_DIRECTION = normalizeVector3({
5472
+ const LIGHT_DIRECTION$1 = normalizeVector3({
5473
5473
  x: 0.34,
5474
5474
  y: -0.62,
5475
5475
  z: 1,
@@ -5479,7 +5479,7 @@
5479
5479
  *
5480
5480
  * @private helper of `octopus3d3AvatarVisual`
5481
5481
  */
5482
- const OCTOPUS_TENTACLE_COUNT = 8;
5482
+ const OCTOPUS_TENTACLE_COUNT$1 = 8;
5483
5483
  /**
5484
5484
  * Cache keyed by the `createRandom` factory reference, which is stable for the lifetime of one
5485
5485
  * mounted `<Avatar/>` component (created inside `resolveAvatarRenderDefinition` and held in a
@@ -5492,16 +5492,651 @@
5492
5492
  * Returns the stable per-avatar state, computing it on first access and returning the cached
5493
5493
  * result on every subsequent call within the same `<Avatar/>` mount.
5494
5494
  *
5495
- * @private helper of `octopus3d3AvatarVisual`
5495
+ * @private helper of `octopus3d3AvatarVisual`
5496
+ */
5497
+ function getOctopus3d3StableState(createRandom) {
5498
+ const cached = stableStateCache.get(createRandom);
5499
+ if (cached !== undefined) {
5500
+ return cached;
5501
+ }
5502
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
5503
+ const animationRandom = createRandom('octopus3d3-animation-profile');
5504
+ const eyeRandom = createRandom('octopus3d3-eye-profile');
5505
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
5506
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
5507
+ const state = {
5508
+ morphologyProfile,
5509
+ animationPhase: animationRandom() * Math.PI * 2,
5510
+ leftEyePhaseOffset,
5511
+ rightEyePhaseOffset,
5512
+ tentacleProfiles: createContinuousTentacleProfiles(createRandom, morphologyProfile),
5513
+ };
5514
+ stableStateCache.set(createRandom, state);
5515
+ return state;
5516
+ }
5517
+ /**
5518
+ * Octopus 3D 3 avatar visual.
5519
+ *
5520
+ * @private built-in avatar visual
5521
+ */
5522
+ const octopus3d3AvatarVisual = {
5523
+ id: 'octopus3d3',
5524
+ title: 'Octopus 3D 3',
5525
+ description: 'Cute continuous 3D octopus with a blobby single mesh, waving tentacle lobes, rich shading, and cursor-aware eyes.',
5526
+ isAnimated: true,
5527
+ supportsPointerTracking: true,
5528
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
5529
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles } = getOctopus3d3StableState(createRandom);
5530
+ const sceneCenterX = size * 0.5;
5531
+ const sceneCenterY = size * 0.535;
5532
+ const bob = Math.sin(timeMs / 960 + animationPhase) * size * 0.012;
5533
+ const meshCenter = {
5534
+ x: interaction.bodyOffsetX * size * 0.048 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
5535
+ y: -size * 0.07 + interaction.bodyOffsetY * size * 0.026 + bob,
5536
+ z: interaction.intensity * size * 0.018,
5537
+ };
5538
+ const rotationY = -0.1 +
5539
+ Math.sin(timeMs / 2700 + animationPhase) * 0.035 +
5540
+ interaction.bodyOffsetX * 0.22 +
5541
+ interaction.gazeX * 0.88;
5542
+ const rotationX = -0.07 +
5543
+ Math.cos(timeMs / 3100 + animationPhase * 0.7) * 0.018 -
5544
+ interaction.bodyOffsetY * 0.08 -
5545
+ interaction.gazeY * 0.38;
5546
+ const surfaceOptions = {
5547
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.1,
5548
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.08,
5549
+ radiusZ: size *
5550
+ morphologyProfile.body.bodyRadiusRatio *
5551
+ (1.02 + (morphologyProfile.body.horizontalStretch - 1) * 0.18),
5552
+ morphologyProfile,
5553
+ timeMs,
5554
+ animationPhase,
5555
+ tentacleProfiles,
5556
+ };
5557
+ const surfacePatches = resolveVisibleContinuousOctopusPatches({
5558
+ ...surfaceOptions,
5559
+ center: meshCenter,
5560
+ rotationX,
5561
+ rotationY,
5562
+ sceneCenterX,
5563
+ sceneCenterY,
5564
+ size,
5565
+ palette,
5566
+ });
5567
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.2 - 0.03, -0.22, 0.08);
5568
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.1, 0.18, 0.32);
5569
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.2 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
5570
+ const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
5571
+ const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
5572
+ const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
5573
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.76;
5574
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.9;
5575
+ drawAvatarFrame(context, size, palette);
5576
+ drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
5577
+ drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile);
5578
+ for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
5579
+ drawContinuousSurfacePatch(context, surfacePatch);
5580
+ }
5581
+ drawProjectedSurfaceCurrents({
5582
+ context,
5583
+ surfaceOptions,
5584
+ center: meshCenter,
5585
+ rotationX,
5586
+ rotationY,
5587
+ sceneCenterX,
5588
+ sceneCenterY,
5589
+ size,
5590
+ palette,
5591
+ morphologyProfile,
5592
+ timeMs,
5593
+ animationPhase,
5594
+ });
5595
+ drawProjectedTentacleSuckers({
5596
+ context,
5597
+ surfaceOptions,
5598
+ center: meshCenter,
5599
+ rotationX,
5600
+ rotationY,
5601
+ sceneCenterX,
5602
+ sceneCenterY,
5603
+ size,
5604
+ palette,
5605
+ });
5606
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
5607
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
5608
+ drawProjectedOrganicMouth(context, [
5609
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
5610
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
5611
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
5612
+ ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
5613
+ },
5614
+ };
5615
+ /**
5616
+ * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
5617
+ *
5618
+ * @private helper of `octopus3d3AvatarVisual`
5619
+ */
5620
+ function createContinuousTentacleProfiles(createRandom, morphologyProfile) {
5621
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT$1 }, (_, tentacleIndex) => {
5622
+ const tentacleRandom = createRandom(`octopus3d3-tentacle-${tentacleIndex}`);
5623
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT$1 - 1);
5624
+ return {
5625
+ centerLongitude: -Math.PI * 0.86 +
5626
+ progress * Math.PI * 1.72 +
5627
+ (tentacleRandom() - 0.5) * (0.08 + morphologyProfile.tentacles.rootSpreadScale * 0.03),
5628
+ widthScale: 0.86 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.16,
5629
+ lengthScale: 0.86 + tentacleRandom() * 0.36 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.22,
5630
+ swayScale: 0.82 + tentacleRandom() * 0.38 + (morphologyProfile.tentacles.swayScale - 1) * 0.2,
5631
+ depthScale: 0.86 + tentacleRandom() * 0.32 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.2,
5632
+ phase: tentacleRandom() * Math.PI * 2,
5633
+ suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
5634
+ };
5635
+ });
5636
+ }
5637
+ /**
5638
+ * Draws the soft underwater atmosphere behind the continuous octopus mesh.
5639
+ *
5640
+ * @private helper of `octopus3d3AvatarVisual`
5641
+ */
5642
+ function drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
5643
+ const glowGradient = context.createRadialGradient(sceneCenterX + interaction.gazeX * size * 0.11, sceneCenterY - size * 0.17 + interaction.gazeY * size * 0.05, size * 0.04, sceneCenterX, sceneCenterY, size * (0.66 + interaction.intensity * 0.02));
5644
+ glowGradient.addColorStop(0, `${palette.highlight}66`);
5645
+ glowGradient.addColorStop(0.34, `${palette.accent}2e`);
5646
+ glowGradient.addColorStop(1, `${palette.highlight}00`);
5647
+ context.fillStyle = glowGradient;
5648
+ context.fillRect(0, 0, size, size);
5649
+ const lowerGradient = context.createRadialGradient(sceneCenterX + Math.sin(timeMs / 1550) * size * 0.05, sceneCenterY + size * 0.29, size * 0.06, sceneCenterX, sceneCenterY + size * 0.3, size * 0.54);
5650
+ lowerGradient.addColorStop(0, `${palette.secondary}25`);
5651
+ lowerGradient.addColorStop(1, `${palette.secondary}00`);
5652
+ context.fillStyle = lowerGradient;
5653
+ context.fillRect(0, 0, size, size);
5654
+ }
5655
+ /**
5656
+ * Draws the soft lower shadow that anchors the octopus in the avatar frame.
5657
+ *
5658
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
5659
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
5660
+ *
5661
+ * @private helper of `octopus3d3AvatarVisual`
5662
+ */
5663
+ function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
5664
+ const cx = size * 0.5 + interaction.gazeX * size * 0.045;
5665
+ const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
5666
+ const rx = size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02);
5667
+ const ry = size * 0.06;
5668
+ // Scale the context so that drawing a circle produces the correct ellipse aspect ratio,
5669
+ // then fill with a radial gradient that approximates the blurry edge without context.filter.
5670
+ context.save();
5671
+ context.translate(cx, cy);
5672
+ context.scale(1, ry / rx);
5673
+ const blurRadius = rx * 1.4;
5674
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
5675
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
5676
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
5677
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
5678
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
5679
+ context.fillStyle = shadowGradient;
5680
+ context.beginPath();
5681
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
5682
+ context.fill();
5683
+ context.restore();
5684
+ }
5685
+ /**
5686
+ * Number of latitude segments used by the continuous Octopus 3D 3 mesh.
5687
+ *
5688
+ * @private helper of `octopus3d3AvatarVisual`
5689
+ */
5690
+ const LATITUDE_PATCH_COUNT$1 = 16;
5691
+ /**
5692
+ * Number of longitude segments used by the continuous Octopus 3D 3 mesh.
5693
+ *
5694
+ * @private helper of `octopus3d3AvatarVisual`
5695
+ */
5696
+ const LONGITUDE_PATCH_COUNT$1 = 40;
5697
+ /**
5698
+ * Resolves visible projected patches for the continuous octopus mesh.
5699
+ *
5700
+ * Within a single frame, mesh corner samples and longitude-only computations (tentacle
5701
+ * influence and lobe wave) are quantized to the patch grid and computed once each rather
5702
+ * than re-evaluated for every patch corner — the patch loop alone calls `sampleContinuousOctopusSurfacePoint`
5703
+ * for `latitudePatchCount * longitudePatchCount * 4` corners without caching, and each call
5704
+ * triggers an inner 8-iteration `Math.exp` loop, which dominates the per-frame cost.
5705
+ *
5706
+ * @private helper of `octopus3d3AvatarVisual`
5707
+ */
5708
+ function resolveVisibleContinuousOctopusPatches(options) {
5709
+ const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
5710
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
5711
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
5712
+ const surfacePatches = [];
5713
+ const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
5714
+ const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
5715
+ for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
5716
+ latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
5717
+ }
5718
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
5719
+ longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
5720
+ }
5721
+ const cachedTentacleInfluencesByCornerLongitude = new Array(longitudePatchCount + 1);
5722
+ const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
5723
+ const cachedTentacleInfluencesByPatchCenterLongitude = new Array(longitudePatchCount);
5724
+ const cachedLobeWavesByPatchCenterLongitude = new Float64Array(longitudePatchCount);
5725
+ const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
5726
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
5727
+ const cornerLongitude = longitudeBoundaries[boundaryIndex];
5728
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveContinuousTentacleInfluence(options, cornerLongitude);
5729
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveContinuousLobeWave(options, cornerLongitude);
5730
+ }
5731
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
5732
+ const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
5733
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveContinuousTentacleInfluence(options, patchCenterLongitude);
5734
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveContinuousLobeWave(options, patchCenterLongitude);
5735
+ cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
5736
+ }
5737
+ const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
5738
+ const transformedCornerSamples = new Array(cornerCount);
5739
+ for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
5740
+ const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex];
5741
+ for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
5742
+ const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
5743
+ const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
5744
+ const cornerSample = sampleContinuousOctopusSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
5745
+ transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
5746
+ }
5747
+ }
5748
+ for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
5749
+ const startLatitude = latitudeBoundaries[latitudeIndex];
5750
+ const endLatitude = latitudeBoundaries[latitudeIndex + 1];
5751
+ const centerLatitude = (startLatitude + endLatitude) / 2;
5752
+ const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
5753
+ const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
5754
+ const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
5755
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
5756
+ const transformedCorners = [
5757
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex],
5758
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1],
5759
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1],
5760
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex],
5761
+ ];
5762
+ const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
5763
+ if (surfaceNormal.z <= 0.008) {
5764
+ continue;
5765
+ }
5766
+ const projectedCorners = [
5767
+ projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
5768
+ projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
5769
+ projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
5770
+ projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
5771
+ ];
5772
+ surfacePatches.push({
5773
+ corners: projectedCorners,
5774
+ averageDepth: (transformedCorners[0].z +
5775
+ transformedCorners[1].z +
5776
+ transformedCorners[2].z +
5777
+ transformedCorners[3].z) /
5778
+ 4,
5779
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
5780
+ fillStyle: resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
5781
+ outlineColor: verticalProgress < 0.54 ? `${palette.highlight}69` : `${palette.shadow}78`,
5782
+ });
5783
+ }
5784
+ }
5785
+ return surfacePatches;
5786
+ }
5787
+ /**
5788
+ * Samples one point on the continuous Octopus 3D 3 surface.
5789
+ *
5790
+ * The lower hemisphere is pulled into eight seeded waving lobes, so the portrait reads as
5791
+ * tentacled while still being rendered as one connected blobby mesh.
5792
+ *
5793
+ * @private helper of `octopus3d3AvatarVisual`
5794
+ */
5795
+ function sampleContinuousOctopusSurfacePoint(options, latitude, longitude) {
5796
+ return sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, resolveContinuousTentacleInfluence(options, longitude), resolveContinuousLobeWave(options, longitude));
5797
+ }
5798
+ /**
5799
+ * Samples one point on the continuous Octopus 3D 3 surface using precomputed longitude-only
5800
+ * values to skip the per-call `Math.exp` tentacle-influence loop and the lobe-wave trig call.
5801
+ *
5802
+ * The patch loop quantizes the mesh into a fixed `(latitudePatchCount + 1) * (longitudePatchCount + 1)`
5803
+ * corner grid, so the same longitude is reused across every latitude row and each
5804
+ * tentacle/lobe value can be computed once per frame instead of `latitudePatchCount * 4` times.
5805
+ *
5806
+ * @private helper of `octopus3d3AvatarVisual`
5807
+ */
5808
+ function sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
5809
+ const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
5810
+ const cosineLatitude = Math.max(0, Math.cos(latitude));
5811
+ const verticalProgress = (Math.sin(latitude) + 1) / 2;
5812
+ const upperBlend = Math.pow(1 - verticalProgress, 1.28);
5813
+ const lowerBlend = smoothStep$1(0.38, 1, verticalProgress);
5814
+ const tipBlend = smoothStep$1(0.68, 1, verticalProgress);
5815
+ const centerPull = resolveSignedAngularDistance$1(longitude, tentacleInfluence.centerLongitude);
5816
+ const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.24 + tipBlend * 0.2);
5817
+ const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
5818
+ animationPhase * 0.6 +
5819
+ timeMs / (1750 + morphologyProfile.body.lobeCount * 30)) *
5820
+ (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.8) *
5821
+ (0.3 + lowerBlend * 0.7);
5822
+ const tentacleWave = Math.sin(timeMs / 760 + tentacleInfluence.phase + verticalProgress * 2.4) *
5823
+ lowerBlend *
5824
+ tentacleInfluence.core *
5825
+ tentacleInfluence.swayScale;
5826
+ const horizontalScale = 1.04 +
5827
+ mantleRipple +
5828
+ lowerBlend * (0.16 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.1) +
5829
+ lowerBlend * tentacleInfluence.core * (0.2 + lowerLobeWave * 0.12) -
5830
+ upperBlend * 0.08;
5831
+ const depthScale = 1.06 +
5832
+ upperBlend * 0.16 +
5833
+ Math.max(0, Math.cos(effectiveLongitude)) * 0.1 +
5834
+ lowerBlend * tentacleInfluence.core * (0.1 + tentacleInfluence.depthScale * 0.06) -
5835
+ Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
5836
+ const tentacleTubeRadius = lowerBlend * tentacleInfluence.core * (0.11 + tipBlend * 0.06 + tentacleInfluence.widthScale * 0.025) * radiusX;
5837
+ const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
5838
+ const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.72;
5839
+ const lowerDrop = lowerBlend *
5840
+ radiusY *
5841
+ (0.18 +
5842
+ tentacleInfluence.core *
5843
+ (0.38 +
5844
+ tentacleInfluence.lengthScale * 0.22 +
5845
+ (morphologyProfile.tentacles.flowLengthScale - 1) * 0.08));
5846
+ return {
5847
+ x: Math.sin(effectiveLongitude) * planarRadiusX + tentacleWave * radiusX * (0.052 + tipBlend * 0.05),
5848
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.12) -
5849
+ upperBlend * radiusY * 0.1 +
5850
+ lowerDrop +
5851
+ Math.sin(timeMs / 1420 + animationPhase + latitude * 1.6) * lowerBlend * radiusY * 0.018 +
5852
+ Math.cos(timeMs / 880 + tentacleInfluence.phase) *
5853
+ lowerBlend *
5854
+ tipBlend *
5855
+ tentacleInfluence.core *
5856
+ radiusY *
5857
+ 0.034,
5858
+ z: Math.cos(effectiveLongitude) * planarRadiusZ +
5859
+ Math.cos(timeMs / 980 + tentacleInfluence.phase + verticalProgress) *
5860
+ lowerBlend *
5861
+ tentacleInfluence.core *
5862
+ radiusZ *
5863
+ 0.04,
5864
+ };
5865
+ }
5866
+ /**
5867
+ * Blends nearby seeded tentacle profiles at one mesh longitude.
5868
+ *
5869
+ * @private helper of `octopus3d3AvatarVisual`
5870
+ */
5871
+ function resolveContinuousTentacleInfluence(options, longitude) {
5872
+ let totalWeight = 0;
5873
+ let weightedSin = 0;
5874
+ let weightedCos = 0;
5875
+ let weightedWidthScale = 0;
5876
+ let weightedLengthScale = 0;
5877
+ let weightedSwayScale = 0;
5878
+ let weightedDepthScale = 0;
5879
+ let weightedPhase = 0;
5880
+ for (const tentacleProfile of options.tentacleProfiles) {
5881
+ const distance = Math.abs(resolveSignedAngularDistance$1(longitude, tentacleProfile.centerLongitude));
5882
+ const width = 0.2 * tentacleProfile.widthScale;
5883
+ const weight = Math.exp(-(distance * distance) / (width * width));
5884
+ totalWeight += weight;
5885
+ weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
5886
+ weightedCos += Math.cos(tentacleProfile.centerLongitude) * weight;
5887
+ weightedWidthScale += tentacleProfile.widthScale * weight;
5888
+ weightedLengthScale += tentacleProfile.lengthScale * weight;
5889
+ weightedSwayScale += tentacleProfile.swayScale * weight;
5890
+ weightedDepthScale += tentacleProfile.depthScale * weight;
5891
+ weightedPhase += tentacleProfile.phase * weight;
5892
+ }
5893
+ if (totalWeight < 0.0001) {
5894
+ return {
5895
+ core: 0,
5896
+ centerLongitude: longitude,
5897
+ widthScale: 1,
5898
+ lengthScale: 1,
5899
+ swayScale: 1,
5900
+ depthScale: 1,
5901
+ phase: 0,
5902
+ };
5903
+ }
5904
+ return {
5905
+ core: clampNumber$1(totalWeight, 0, 1),
5906
+ centerLongitude: Math.atan2(weightedSin / totalWeight, weightedCos / totalWeight),
5907
+ widthScale: weightedWidthScale / totalWeight,
5908
+ lengthScale: weightedLengthScale / totalWeight,
5909
+ swayScale: weightedSwayScale / totalWeight,
5910
+ depthScale: weightedDepthScale / totalWeight,
5911
+ phase: weightedPhase / totalWeight,
5912
+ };
5913
+ }
5914
+ /**
5915
+ * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
5916
+ *
5917
+ * @private helper of `octopus3d3AvatarVisual`
5918
+ */
5919
+ function resolveContinuousLobeWave(options, longitude) {
5920
+ const { morphologyProfile, animationPhase, timeMs } = options;
5921
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT$1 +
5922
+ animationPhase +
5923
+ timeMs / (980 + morphologyProfile.body.lobeCount * 18)) +
5924
+ 1) /
5925
+ 2);
5926
+ }
5927
+ /**
5928
+ * Resolves one base fill tone for a patch on the continuous octopus mesh.
5929
+ *
5930
+ * @private helper of `octopus3d3AvatarVisual`
5931
+ */
5932
+ function resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
5933
+ const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
5934
+ if (tonalProgress < 0.14) {
5935
+ return palette.highlight;
5936
+ }
5937
+ if (tonalProgress < 0.32) {
5938
+ return palette.secondary;
5939
+ }
5940
+ if (tonalProgress < 0.72) {
5941
+ return forwardness > 0.55 ? palette.secondary : palette.primary;
5942
+ }
5943
+ return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
5944
+ }
5945
+ /**
5946
+ * Draws one projected mesh patch with soft shading and a subtle edge.
5947
+ *
5948
+ * @private helper of `octopus3d3AvatarVisual`
5949
+ */
5950
+ function drawContinuousSurfacePatch(context, surfacePatch) {
5951
+ drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
5952
+ if (surfacePatch.lightIntensity > 0) {
5953
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.18 * surfacePatch.lightIntensity})`);
5954
+ }
5955
+ else if (surfacePatch.lightIntensity < 0) {
5956
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.25 * Math.abs(surfacePatch.lightIntensity)})`);
5957
+ }
5958
+ context.save();
5959
+ context.beginPath();
5960
+ context.moveTo(surfacePatch.corners[0].x, surfacePatch.corners[0].y);
5961
+ for (let cornerIndex = 1; cornerIndex < surfacePatch.corners.length; cornerIndex++) {
5962
+ context.lineTo(surfacePatch.corners[cornerIndex].x, surfacePatch.corners[cornerIndex].y);
5963
+ }
5964
+ context.closePath();
5965
+ context.strokeStyle = surfacePatch.outlineColor;
5966
+ context.lineWidth = Math.max(0.7, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0032);
5967
+ context.lineJoin = 'round';
5968
+ context.stroke();
5969
+ context.restore();
5970
+ }
5971
+ /**
5972
+ * Draws projected mantle-current lines on the front of the mesh.
5973
+ *
5974
+ * @private helper of `octopus3d3AvatarVisual`
5975
+ */
5976
+ function drawProjectedSurfaceCurrents(options) {
5977
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
5978
+ const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
5979
+ const centerIndex = (currentCount - 1) / 2;
5980
+ context.save();
5981
+ context.lineCap = 'round';
5982
+ context.lineJoin = 'round';
5983
+ for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
5984
+ const baseLongitude = (currentIndex - centerIndex) * 0.15;
5985
+ const projectedPoints = [];
5986
+ for (let sampleIndex = 0; sampleIndex < 8; sampleIndex++) {
5987
+ const progress = sampleIndex / 7;
5988
+ const latitude = -0.46 + progress * 0.74;
5989
+ const longitude = baseLongitude + Math.sin(timeMs / 1160 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.035;
5990
+ const scenePoint = transformScenePoint(sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
5991
+ if (scenePoint.z > center.z - size * 0.016) {
5992
+ projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
5993
+ }
5994
+ }
5995
+ if (projectedPoints.length < 3) {
5996
+ continue;
5997
+ }
5998
+ context.beginPath();
5999
+ context.moveTo(projectedPoints[0].x, projectedPoints[0].y);
6000
+ for (const projectedPoint of projectedPoints.slice(1)) {
6001
+ context.lineTo(projectedPoint.x, projectedPoint.y);
6002
+ }
6003
+ context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}3d` : `${palette.accent}33`;
6004
+ context.lineWidth = size * (0.0055 + currentIndex * 0.00045);
6005
+ context.stroke();
6006
+ }
6007
+ context.restore();
6008
+ }
6009
+ /**
6010
+ * Draws small projected sucker highlights on the waving lower mesh lobes.
6011
+ *
6012
+ * @private helper of `octopus3d3AvatarVisual`
6013
+ */
6014
+ function drawProjectedTentacleSuckers(options) {
6015
+ const { surfaceOptions, size } = options;
6016
+ const { timeMs } = surfaceOptions;
6017
+ for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
6018
+ if (Math.cos(tentacleProfile.centerLongitude) < -0.12) {
6019
+ continue;
6020
+ }
6021
+ for (let suckerIndex = 0; suckerIndex < 3; suckerIndex++) {
6022
+ const latitude = 0.52 + suckerIndex * 0.14;
6023
+ const sideOffset = tentacleProfile.suckerSide * (0.035 + suckerIndex * 0.012) * tentacleProfile.widthScale;
6024
+ const waveOffset = Math.sin(timeMs / 900 + tentacleProfile.phase + suckerIndex * 0.8) * 0.018;
6025
+ drawProjectedSurfaceSpot({
6026
+ ...options,
6027
+ latitude,
6028
+ longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
6029
+ radiusScale: size * (0.0065 - suckerIndex * 0.0007),
6030
+ });
6031
+ }
6032
+ }
6033
+ }
6034
+ /**
6035
+ * Draws one tiny projected surface spot by sampling local mesh tangents.
6036
+ *
6037
+ * @private helper of `octopus3d3AvatarVisual`
6038
+ */
6039
+ function drawProjectedSurfaceSpot(options) {
6040
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
6041
+ const localCenter = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude);
6042
+ const localHorizontal = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
6043
+ const localVertical = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
6044
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
6045
+ if (sceneCenterPoint.z <= center.z - size * 0.012) {
6046
+ return;
6047
+ }
6048
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
6049
+ const projectedHorizontalPoint = projectScenePoint(transformScenePoint(localHorizontal, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
6050
+ const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
6051
+ const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
6052
+ radiusScale *
6053
+ 0.74, size * 0.003, size * 0.018);
6054
+ const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
6055
+ radiusScale *
6056
+ 0.52, size * 0.0024, size * 0.014);
6057
+ const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
6058
+ context.save();
6059
+ context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
6060
+ context.rotate(rotation);
6061
+ context.beginPath();
6062
+ context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
6063
+ context.fillStyle = `${palette.highlight}73`;
6064
+ context.fill();
6065
+ context.strokeStyle = `${palette.highlight}99`;
6066
+ context.lineWidth = Math.max(0.7, size * 0.0028);
6067
+ context.stroke();
6068
+ context.restore();
6069
+ }
6070
+ /**
6071
+ * Resolves a signed angular distance from the source longitude to the target longitude.
6072
+ *
6073
+ * @private helper of `octopus3d3AvatarVisual`
6074
+ */
6075
+ function resolveSignedAngularDistance$1(sourceLongitude, targetLongitude) {
6076
+ return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
6077
+ }
6078
+ /**
6079
+ * Smoothly maps a value between two bounds into `[0, 1]`.
6080
+ *
6081
+ * @private helper of `octopus3d3AvatarVisual`
6082
+ */
6083
+ function smoothStep$1(edgeStart, edgeEnd, value) {
6084
+ const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
6085
+ return progress * progress * (3 - 2 * progress);
6086
+ }
6087
+
6088
+ /* eslint-disable no-magic-numbers */
6089
+ /**
6090
+ * Light direction used by the continuous Octopus 3D 4 mesh shading.
6091
+ *
6092
+ * @private helper of `octopus3d4AvatarVisual`
6093
+ */
6094
+ const LIGHT_DIRECTION = normalizeVector3({
6095
+ x: 0.32,
6096
+ y: -0.66,
6097
+ z: 1,
6098
+ });
6099
+ /**
6100
+ * Rim-light direction used to brighten the side of the silhouette.
6101
+ *
6102
+ * @private helper of `octopus3d4AvatarVisual`
6103
+ */
6104
+ const RIM_LIGHT_DIRECTION = normalizeVector3({
6105
+ x: -0.7,
6106
+ y: -0.18,
6107
+ z: 0.55,
6108
+ });
6109
+ /**
6110
+ * Real-octopus tentacle count used by the continuous lower mesh.
6111
+ *
6112
+ * @private helper of `octopus3d4AvatarVisual`
6113
+ */
6114
+ const OCTOPUS_TENTACLE_COUNT = 8;
6115
+ /**
6116
+ * Number of seeded skin spots painted across the octopus body.
6117
+ *
6118
+ * @private helper of `octopus3d4AvatarVisual`
6119
+ */
6120
+ const SKIN_SPOT_COUNT = 14;
6121
+ /**
6122
+ * Cache keyed by the `createRandom` factory reference, stable per mounted `<Avatar/>` component.
6123
+ *
6124
+ * @private helper of `octopus3d4AvatarVisual`
6125
+ */
6126
+ const octopus3d4StableStateCache = new WeakMap();
6127
+ /**
6128
+ * Returns the stable per-avatar state, computing it on first access and caching afterwards.
6129
+ *
6130
+ * @private helper of `octopus3d4AvatarVisual`
5496
6131
  */
5497
- function getOctopus3d3StableState(createRandom) {
5498
- const cached = stableStateCache.get(createRandom);
6132
+ function getOctopus3d4StableState(createRandom) {
6133
+ const cached = octopus3d4StableStateCache.get(createRandom);
5499
6134
  if (cached !== undefined) {
5500
6135
  return cached;
5501
6136
  }
5502
6137
  const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
5503
- const animationRandom = createRandom('octopus3d3-animation-profile');
5504
- const eyeRandom = createRandom('octopus3d3-eye-profile');
6138
+ const animationRandom = createRandom('octopus3d4-animation-profile');
6139
+ const eyeRandom = createRandom('octopus3d4-eye-profile');
5505
6140
  const leftEyePhaseOffset = eyeRandom() * 0.7;
5506
6141
  const rightEyePhaseOffset = eyeRandom() * 0.7;
5507
6142
  const state = {
@@ -5509,52 +6144,53 @@
5509
6144
  animationPhase: animationRandom() * Math.PI * 2,
5510
6145
  leftEyePhaseOffset,
5511
6146
  rightEyePhaseOffset,
5512
- tentacleProfiles: createContinuousTentacleProfiles(createRandom, morphologyProfile),
6147
+ tentacleProfiles: createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile),
6148
+ skinSpots: createBlobbySkinSpots(createRandom),
5513
6149
  };
5514
- stableStateCache.set(createRandom, state);
6150
+ octopus3d4StableStateCache.set(createRandom, state);
5515
6151
  return state;
5516
6152
  }
5517
6153
  /**
5518
- * Octopus 3D 3 avatar visual.
6154
+ * Octopus 3D 4 avatar visual.
5519
6155
  *
5520
6156
  * @private built-in avatar visual
5521
6157
  */
5522
- const octopus3d3AvatarVisual = {
5523
- id: 'octopus3d3',
5524
- title: 'Octopus 3D 3',
5525
- description: 'Cute continuous 3D octopus with a blobby single mesh, waving tentacle lobes, rich shading, and cursor-aware eyes.',
6158
+ const octopus3d4AvatarVisual = {
6159
+ id: 'octopus3d4',
6160
+ title: 'Octopus 3D 4',
6161
+ 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.',
5526
6162
  isAnimated: true,
5527
6163
  supportsPointerTracking: true,
5528
6164
  render({ context, size, palette, createRandom, timeMs, interaction }) {
5529
- const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles } = getOctopus3d3StableState(createRandom);
6165
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles, skinSpots } = getOctopus3d4StableState(createRandom);
5530
6166
  const sceneCenterX = size * 0.5;
5531
6167
  const sceneCenterY = size * 0.535;
5532
- const bob = Math.sin(timeMs / 960 + animationPhase) * size * 0.012;
6168
+ const bob = Math.sin(timeMs / 980 + animationPhase) * size * 0.013;
5533
6169
  const meshCenter = {
5534
- x: interaction.bodyOffsetX * size * 0.048 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
5535
- y: -size * 0.07 + interaction.bodyOffsetY * size * 0.026 + bob,
5536
- z: interaction.intensity * size * 0.018,
5537
- };
5538
- const rotationY = -0.1 +
5539
- Math.sin(timeMs / 2700 + animationPhase) * 0.035 +
5540
- interaction.bodyOffsetX * 0.22 +
5541
- interaction.gazeX * 0.88;
6170
+ x: interaction.bodyOffsetX * size * 0.052 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
6171
+ y: -size * 0.07 + interaction.bodyOffsetY * size * 0.028 + bob,
6172
+ z: interaction.intensity * size * 0.02,
6173
+ };
6174
+ const rotationY = -0.08 +
6175
+ Math.sin(timeMs / 2800 + animationPhase) * 0.04 +
6176
+ interaction.bodyOffsetX * 0.24 +
6177
+ interaction.gazeX * 0.98;
5542
6178
  const rotationX = -0.07 +
5543
- Math.cos(timeMs / 3100 + animationPhase * 0.7) * 0.018 -
6179
+ Math.cos(timeMs / 3200 + animationPhase * 0.7) * 0.02 -
5544
6180
  interaction.bodyOffsetY * 0.08 -
5545
- interaction.gazeY * 0.38;
6181
+ interaction.gazeY * 0.42;
5546
6182
  const surfaceOptions = {
5547
- radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.1,
5548
- radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.08,
6183
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.12,
6184
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1,
5549
6185
  radiusZ: size *
5550
6186
  morphologyProfile.body.bodyRadiusRatio *
5551
- (1.02 + (morphologyProfile.body.horizontalStretch - 1) * 0.18),
6187
+ (1.04 + (morphologyProfile.body.horizontalStretch - 1) * 0.2),
5552
6188
  morphologyProfile,
5553
6189
  timeMs,
5554
6190
  animationPhase,
5555
6191
  tentacleProfiles,
5556
6192
  };
5557
- const surfacePatches = resolveVisibleContinuousOctopusPatches({
6193
+ const surfacePatches = resolveVisibleBlobbyContinuousPatches({
5558
6194
  ...surfaceOptions,
5559
6195
  center: meshCenter,
5560
6196
  rotationX,
@@ -5564,21 +6200,33 @@
5564
6200
  size,
5565
6201
  palette,
5566
6202
  });
5567
- const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.2 - 0.03, -0.22, 0.08);
5568
- const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.1, 0.18, 0.32);
5569
- const mouthLatitude = clampNumber$1(eyeLatitude + 0.2 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
6203
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.4 - 0.04, -0.24, 0.08);
6204
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.0, 0.18, 0.32);
6205
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.21 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
5570
6206
  const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
5571
6207
  const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
5572
6208
  const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
5573
- const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.76;
5574
- const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.9;
6209
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
6210
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
5575
6211
  drawAvatarFrame(context, size, palette);
5576
- drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
5577
- drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile);
6212
+ drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
6213
+ drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile);
5578
6214
  for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
5579
- drawContinuousSurfacePatch(context, surfacePatch);
6215
+ drawBlobbyContinuousSurfacePatch(context, surfacePatch);
5580
6216
  }
5581
- drawProjectedSurfaceCurrents({
6217
+ drawBlobbySkinSpots({
6218
+ context,
6219
+ surfaceOptions,
6220
+ center: meshCenter,
6221
+ rotationX,
6222
+ rotationY,
6223
+ sceneCenterX,
6224
+ sceneCenterY,
6225
+ size,
6226
+ palette,
6227
+ skinSpots,
6228
+ });
6229
+ drawBlobbyContinuousCurrents({
5582
6230
  context,
5583
6231
  surfaceOptions,
5584
6232
  center: meshCenter,
@@ -5592,7 +6240,7 @@
5592
6240
  timeMs,
5593
6241
  animationPhase,
5594
6242
  });
5595
- drawProjectedTentacleSuckers({
6243
+ drawBlobbyContinuousSuckers({
5596
6244
  context,
5597
6245
  surfaceOptions,
5598
6246
  center: meshCenter,
@@ -5603,51 +6251,78 @@
5603
6251
  size,
5604
6252
  palette,
5605
6253
  });
5606
- drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
5607
- drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
6254
+ drawBlobbyContinuousGloss({
6255
+ context,
6256
+ surfaceOptions,
6257
+ center: meshCenter,
6258
+ rotationX,
6259
+ rotationY,
6260
+ sceneCenterX,
6261
+ sceneCenterY,
6262
+ size,
6263
+ palette,
6264
+ });
6265
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
6266
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
5608
6267
  drawProjectedOrganicMouth(context, [
5609
- sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
5610
- sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
5611
- sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
6268
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
6269
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
6270
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
5612
6271
  ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
5613
6272
  },
5614
6273
  };
5615
6274
  /**
5616
6275
  * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
5617
6276
  *
5618
- * @private helper of `octopus3d3AvatarVisual`
6277
+ * @private helper of `octopus3d4AvatarVisual`
5619
6278
  */
5620
- function createContinuousTentacleProfiles(createRandom, morphologyProfile) {
6279
+ function createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile) {
5621
6280
  return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
5622
- const tentacleRandom = createRandom(`octopus3d3-tentacle-${tentacleIndex}`);
6281
+ const tentacleRandom = createRandom(`octopus3d4-tentacle-${tentacleIndex}`);
5623
6282
  const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
5624
6283
  return {
5625
- centerLongitude: -Math.PI * 0.86 +
5626
- progress * Math.PI * 1.72 +
5627
- (tentacleRandom() - 0.5) * (0.08 + morphologyProfile.tentacles.rootSpreadScale * 0.03),
5628
- widthScale: 0.86 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.16,
5629
- lengthScale: 0.86 + tentacleRandom() * 0.36 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.22,
5630
- swayScale: 0.82 + tentacleRandom() * 0.38 + (morphologyProfile.tentacles.swayScale - 1) * 0.2,
5631
- depthScale: 0.86 + tentacleRandom() * 0.32 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.2,
5632
- phase: tentacleRandom() * Math.PI * 2,
6284
+ centerLongitude: -Math.PI * 0.9 +
6285
+ progress * Math.PI * 1.8 +
6286
+ (tentacleRandom() - 0.5) * (0.06 + morphologyProfile.tentacles.rootSpreadScale * 0.025),
6287
+ widthScale: 0.92 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.18,
6288
+ lengthScale: 0.9 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.24,
6289
+ swayScale: 0.86 + tentacleRandom() * 0.4 + (morphologyProfile.tentacles.swayScale - 1) * 0.22,
6290
+ depthScale: 0.9 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.22,
6291
+ curlScale: 0.62 + tentacleRandom() * 0.46,
6292
+ primaryPhase: tentacleRandom() * Math.PI * 2,
6293
+ secondaryPhase: tentacleRandom() * Math.PI * 2,
5633
6294
  suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
5634
6295
  };
5635
6296
  });
5636
6297
  }
6298
+ /**
6299
+ * Creates seeded skin pigment spots distributed across the upper octopus mesh.
6300
+ *
6301
+ * @private helper of `octopus3d4AvatarVisual`
6302
+ */
6303
+ function createBlobbySkinSpots(createRandom) {
6304
+ const spotRandom = createRandom('octopus3d4-skin-spots');
6305
+ return Array.from({ length: SKIN_SPOT_COUNT }, () => ({
6306
+ latitude: -0.45 + spotRandom() * 0.7,
6307
+ longitude: -0.5 + spotRandom() * 1.0,
6308
+ radiusScale: 0.0028 + spotRandom() * 0.0052,
6309
+ opacity: 0.16 + spotRandom() * 0.22,
6310
+ }));
6311
+ }
5637
6312
  /**
5638
6313
  * Draws the soft underwater atmosphere behind the continuous octopus mesh.
5639
6314
  *
5640
- * @private helper of `octopus3d3AvatarVisual`
6315
+ * @private helper of `octopus3d4AvatarVisual`
5641
6316
  */
5642
- function drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
5643
- const glowGradient = context.createRadialGradient(sceneCenterX + interaction.gazeX * size * 0.11, sceneCenterY - size * 0.17 + interaction.gazeY * size * 0.05, size * 0.04, sceneCenterX, sceneCenterY, size * (0.66 + interaction.intensity * 0.02));
5644
- glowGradient.addColorStop(0, `${palette.highlight}66`);
5645
- glowGradient.addColorStop(0.34, `${palette.accent}2e`);
6317
+ function drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
6318
+ 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));
6319
+ glowGradient.addColorStop(0, `${palette.highlight}74`);
6320
+ glowGradient.addColorStop(0.32, `${palette.accent}32`);
5646
6321
  glowGradient.addColorStop(1, `${palette.highlight}00`);
5647
6322
  context.fillStyle = glowGradient;
5648
6323
  context.fillRect(0, 0, size, size);
5649
- const lowerGradient = context.createRadialGradient(sceneCenterX + Math.sin(timeMs / 1550) * size * 0.05, sceneCenterY + size * 0.29, size * 0.06, sceneCenterX, sceneCenterY + size * 0.3, size * 0.54);
5650
- lowerGradient.addColorStop(0, `${palette.secondary}25`);
6324
+ 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);
6325
+ lowerGradient.addColorStop(0, `${palette.secondary}28`);
5651
6326
  lowerGradient.addColorStop(1, `${palette.secondary}00`);
5652
6327
  context.fillStyle = lowerGradient;
5653
6328
  context.fillRect(0, 0, size, size);
@@ -5655,26 +6330,21 @@
5655
6330
  /**
5656
6331
  * Draws the soft lower shadow that anchors the octopus in the avatar frame.
5657
6332
  *
5658
- * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
5659
- * blurry ellipse without triggering a costly software rasterization pass on every frame.
5660
- *
5661
- * @private helper of `octopus3d3AvatarVisual`
6333
+ * @private helper of `octopus3d4AvatarVisual`
5662
6334
  */
5663
- function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
5664
- const cx = size * 0.5 + interaction.gazeX * size * 0.045;
6335
+ function drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
6336
+ const cx = size * 0.5 + interaction.gazeX * size * 0.046;
5665
6337
  const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
5666
- const rx = size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02);
5667
- const ry = size * 0.06;
5668
- // Scale the context so that drawing a circle produces the correct ellipse aspect ratio,
5669
- // then fill with a radial gradient that approximates the blurry edge without context.filter.
6338
+ const rx = size * (0.2 + morphologyProfile.tentacles.rootSpreadScale * 0.024 + interaction.intensity * 0.022);
6339
+ const ry = size * 0.062;
5670
6340
  context.save();
5671
6341
  context.translate(cx, cy);
5672
6342
  context.scale(1, ry / rx);
5673
- const blurRadius = rx * 1.4;
6343
+ const blurRadius = rx * 1.42;
5674
6344
  const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
5675
- shadowGradient.addColorStop(0, `${palette.shadow}7a`);
5676
- shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
5677
- shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
6345
+ shadowGradient.addColorStop(0, `${palette.shadow}82`);
6346
+ shadowGradient.addColorStop(0.45, `${palette.shadow}4a`);
6347
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1c`);
5678
6348
  shadowGradient.addColorStop(1, `${palette.shadow}00`);
5679
6349
  context.fillStyle = shadowGradient;
5680
6350
  context.beginPath();
@@ -5683,29 +6353,27 @@
5683
6353
  context.restore();
5684
6354
  }
5685
6355
  /**
5686
- * Number of latitude segments used by the continuous Octopus 3D 3 mesh.
6356
+ * Number of latitude segments used by the continuous Octopus 3D 4 mesh.
5687
6357
  *
5688
- * @private helper of `octopus3d3AvatarVisual`
6358
+ * @private helper of `octopus3d4AvatarVisual`
5689
6359
  */
5690
- const LATITUDE_PATCH_COUNT = 16;
6360
+ const LATITUDE_PATCH_COUNT = 20;
5691
6361
  /**
5692
- * Number of longitude segments used by the continuous Octopus 3D 3 mesh.
6362
+ * Number of longitude segments used by the continuous Octopus 3D 4 mesh.
5693
6363
  *
5694
- * @private helper of `octopus3d3AvatarVisual`
6364
+ * @private helper of `octopus3d4AvatarVisual`
5695
6365
  */
5696
- const LONGITUDE_PATCH_COUNT = 40;
6366
+ const LONGITUDE_PATCH_COUNT = 48;
5697
6367
  /**
5698
- * Resolves visible projected patches for the continuous octopus mesh.
6368
+ * Resolves visible projected patches for the continuous Octopus 3D 4 mesh.
5699
6369
  *
5700
6370
  * Within a single frame, mesh corner samples and longitude-only computations (tentacle
5701
6371
  * influence and lobe wave) are quantized to the patch grid and computed once each rather
5702
- * than re-evaluated for every patch corner — the patch loop alone calls `sampleContinuousOctopusSurfacePoint`
5703
- * for `latitudePatchCount * longitudePatchCount * 4` corners without caching, and each call
5704
- * triggers an inner 8-iteration `Math.exp` loop, which dominates the per-frame cost.
6372
+ * than re-evaluated for every patch corner.
5705
6373
  *
5706
- * @private helper of `octopus3d3AvatarVisual`
6374
+ * @private helper of `octopus3d4AvatarVisual`
5707
6375
  */
5708
- function resolveVisibleContinuousOctopusPatches(options) {
6376
+ function resolveVisibleBlobbyContinuousPatches(options) {
5709
6377
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
5710
6378
  const latitudePatchCount = LATITUDE_PATCH_COUNT;
5711
6379
  const longitudePatchCount = LONGITUDE_PATCH_COUNT;
@@ -5725,13 +6393,13 @@
5725
6393
  const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
5726
6394
  for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
5727
6395
  const cornerLongitude = longitudeBoundaries[boundaryIndex];
5728
- cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveContinuousTentacleInfluence(options, cornerLongitude);
5729
- cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveContinuousLobeWave(options, cornerLongitude);
6396
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveBlobbyTentacleInfluence(options, cornerLongitude);
6397
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveBlobbyLobeWave(options, cornerLongitude);
5730
6398
  }
5731
6399
  for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
5732
6400
  const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
5733
- cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveContinuousTentacleInfluence(options, patchCenterLongitude);
5734
- cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveContinuousLobeWave(options, patchCenterLongitude);
6401
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyTentacleInfluence(options, patchCenterLongitude);
6402
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyLobeWave(options, patchCenterLongitude);
5735
6403
  cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
5736
6404
  }
5737
6405
  const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
@@ -5741,7 +6409,7 @@
5741
6409
  for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
5742
6410
  const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
5743
6411
  const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
5744
- const cornerSample = sampleContinuousOctopusSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
6412
+ const cornerSample = sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
5745
6413
  transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
5746
6414
  }
5747
6415
  }
@@ -5760,7 +6428,7 @@
5760
6428
  transformedCornerSamples[endCornerRowOffset + longitudeIndex],
5761
6429
  ];
5762
6430
  const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
5763
- if (surfaceNormal.z <= 0.008) {
6431
+ if (surfaceNormal.z <= 0.006) {
5764
6432
  continue;
5765
6433
  }
5766
6434
  const projectedCorners = [
@@ -5769,6 +6437,8 @@
5769
6437
  projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
5770
6438
  projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
5771
6439
  ];
6440
+ const lightIntensity = clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1);
6441
+ const rimLightIntensity = Math.pow(clampNumber$1(dotProduct3D(surfaceNormal, RIM_LIGHT_DIRECTION), 0, 1), 2.6);
5772
6442
  surfacePatches.push({
5773
6443
  corners: projectedCorners,
5774
6444
  averageDepth: (transformedCorners[0].z +
@@ -5776,99 +6446,113 @@
5776
6446
  transformedCorners[2].z +
5777
6447
  transformedCorners[3].z) /
5778
6448
  4,
5779
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1),
5780
- fillStyle: resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
5781
- outlineColor: verticalProgress < 0.54 ? `${palette.highlight}69` : `${palette.shadow}78`,
6449
+ lightIntensity,
6450
+ rimLightIntensity,
6451
+ fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
6452
+ outlineColor: verticalProgress < 0.52 ? `${palette.highlight}5c` : `${palette.shadow}66`,
5782
6453
  });
5783
6454
  }
5784
6455
  }
5785
6456
  return surfacePatches;
5786
6457
  }
5787
6458
  /**
5788
- * Samples one point on the continuous Octopus 3D 3 surface.
5789
- *
5790
- * The lower hemisphere is pulled into eight seeded waving lobes, so the portrait reads as
5791
- * tentacled while still being rendered as one connected blobby mesh.
6459
+ * Samples one point on the continuous Octopus 3D 4 surface.
5792
6460
  *
5793
- * @private helper of `octopus3d3AvatarVisual`
6461
+ * @private helper of `octopus3d4AvatarVisual`
5794
6462
  */
5795
- function sampleContinuousOctopusSurfacePoint(options, latitude, longitude) {
5796
- return sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, resolveContinuousTentacleInfluence(options, longitude), resolveContinuousLobeWave(options, longitude));
6463
+ function sampleBlobbyContinuousSurfacePoint(options, latitude, longitude) {
6464
+ return sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, resolveBlobbyTentacleInfluence(options, longitude), resolveBlobbyLobeWave(options, longitude));
5797
6465
  }
5798
6466
  /**
5799
- * Samples one point on the continuous Octopus 3D 3 surface using precomputed longitude-only
5800
- * values to skip the per-call `Math.exp` tentacle-influence loop and the lobe-wave trig call.
6467
+ * Samples one point on the continuous Octopus 3D 4 surface using precomputed longitude-only values.
5801
6468
  *
5802
- * The patch loop quantizes the mesh into a fixed `(latitudePatchCount + 1) * (longitudePatchCount + 1)`
5803
- * corner grid, so the same longitude is reused across every latitude row and each
5804
- * tentacle/lobe value can be computed once per frame instead of `latitudePatchCount * 4` times.
6469
+ * The patch loop quantizes the mesh into a fixed corner grid, so the same longitude is reused
6470
+ * across every latitude row and each tentacle/lobe value is computed once per frame instead
6471
+ * of `latitudePatchCount * 4` times.
5805
6472
  *
5806
- * @private helper of `octopus3d3AvatarVisual`
6473
+ * @private helper of `octopus3d4AvatarVisual`
5807
6474
  */
5808
- function sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
6475
+ function sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
5809
6476
  const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
5810
6477
  const cosineLatitude = Math.max(0, Math.cos(latitude));
5811
6478
  const verticalProgress = (Math.sin(latitude) + 1) / 2;
5812
- const upperBlend = Math.pow(1 - verticalProgress, 1.28);
5813
- const lowerBlend = smoothStep(0.38, 1, verticalProgress);
5814
- const tipBlend = smoothStep(0.68, 1, verticalProgress);
6479
+ const upperBlend = Math.pow(1 - verticalProgress, 1.32);
6480
+ const lowerBlend = smoothStep(0.34, 1, verticalProgress);
6481
+ const tipBlend = smoothStep(0.66, 1, verticalProgress);
5815
6482
  const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
5816
- const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.24 + tipBlend * 0.2);
6483
+ const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.26 + tipBlend * 0.22);
5817
6484
  const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
5818
6485
  animationPhase * 0.6 +
5819
- timeMs / (1750 + morphologyProfile.body.lobeCount * 30)) *
5820
- (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.8) *
5821
- (0.3 + lowerBlend * 0.7);
5822
- const tentacleWave = Math.sin(timeMs / 760 + tentacleInfluence.phase + verticalProgress * 2.4) *
6486
+ timeMs / (1700 + morphologyProfile.body.lobeCount * 28)) *
6487
+ (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.78) *
6488
+ (0.32 + lowerBlend * 0.7);
6489
+ const primaryTentacleWave = Math.sin(timeMs / 740 + tentacleInfluence.primaryPhase + verticalProgress * 2.6) *
5823
6490
  lowerBlend *
5824
6491
  tentacleInfluence.core *
5825
6492
  tentacleInfluence.swayScale;
5826
- const horizontalScale = 1.04 +
6493
+ const secondaryTentacleWave = Math.sin(timeMs / 470 + tentacleInfluence.secondaryPhase + verticalProgress * 4.2) *
6494
+ lowerBlend *
6495
+ tentacleInfluence.core *
6496
+ tentacleInfluence.swayScale *
6497
+ 0.42;
6498
+ const tentacleCurl = Math.sin(timeMs / 1180 + tentacleInfluence.primaryPhase * 0.6 + verticalProgress * 3.2) *
6499
+ tipBlend *
6500
+ tentacleInfluence.core *
6501
+ tentacleInfluence.curlScale *
6502
+ 0.36;
6503
+ const horizontalScale = 1.06 +
5827
6504
  mantleRipple +
5828
- lowerBlend * (0.16 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.1) +
5829
- lowerBlend * tentacleInfluence.core * (0.2 + lowerLobeWave * 0.12) -
6505
+ lowerBlend * (0.18 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.12) +
6506
+ lowerBlend * tentacleInfluence.core * (0.22 + lowerLobeWave * 0.14) -
5830
6507
  upperBlend * 0.08;
5831
- const depthScale = 1.06 +
6508
+ const depthScale = 1.08 +
5832
6509
  upperBlend * 0.16 +
5833
- Math.max(0, Math.cos(effectiveLongitude)) * 0.1 +
5834
- lowerBlend * tentacleInfluence.core * (0.1 + tentacleInfluence.depthScale * 0.06) -
6510
+ Math.max(0, Math.cos(effectiveLongitude)) * 0.12 +
6511
+ lowerBlend * tentacleInfluence.core * (0.12 + tentacleInfluence.depthScale * 0.07) -
5835
6512
  Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
5836
- const tentacleTubeRadius = lowerBlend * tentacleInfluence.core * (0.11 + tipBlend * 0.06 + tentacleInfluence.widthScale * 0.025) * radiusX;
6513
+ const tentacleTubeRadius = lowerBlend *
6514
+ tentacleInfluence.core *
6515
+ (0.12 + tipBlend * 0.07 + tentacleInfluence.widthScale * 0.028) *
6516
+ radiusX;
5837
6517
  const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
5838
- const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.72;
6518
+ const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.74;
5839
6519
  const lowerDrop = lowerBlend *
5840
6520
  radiusY *
5841
- (0.18 +
6521
+ (0.2 +
5842
6522
  tentacleInfluence.core *
5843
- (0.38 +
5844
- tentacleInfluence.lengthScale * 0.22 +
5845
- (morphologyProfile.tentacles.flowLengthScale - 1) * 0.08));
6523
+ (0.42 +
6524
+ tentacleInfluence.lengthScale * 0.24 +
6525
+ (morphologyProfile.tentacles.flowLengthScale - 1) * 0.1));
6526
+ const combinedTentacleSway = (primaryTentacleWave + secondaryTentacleWave) * radiusX * (0.06 + tipBlend * 0.06);
5846
6527
  return {
5847
- x: Math.sin(effectiveLongitude) * planarRadiusX + tentacleWave * radiusX * (0.052 + tipBlend * 0.05),
5848
- y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.12) -
5849
- upperBlend * radiusY * 0.1 +
6528
+ x: Math.sin(effectiveLongitude) * planarRadiusX +
6529
+ combinedTentacleSway +
6530
+ tentacleCurl * radiusX * 0.18,
6531
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.14) -
6532
+ upperBlend * radiusY * 0.12 +
5850
6533
  lowerDrop +
5851
- Math.sin(timeMs / 1420 + animationPhase + latitude * 1.6) * lowerBlend * radiusY * 0.018 +
5852
- Math.cos(timeMs / 880 + tentacleInfluence.phase) *
6534
+ Math.sin(timeMs / 1380 + animationPhase + latitude * 1.5) * lowerBlend * radiusY * 0.022 +
6535
+ Math.cos(timeMs / 820 + tentacleInfluence.primaryPhase) *
5853
6536
  lowerBlend *
5854
6537
  tipBlend *
5855
6538
  tentacleInfluence.core *
5856
6539
  radiusY *
5857
- 0.034,
6540
+ 0.04,
5858
6541
  z: Math.cos(effectiveLongitude) * planarRadiusZ +
5859
- Math.cos(timeMs / 980 + tentacleInfluence.phase + verticalProgress) *
6542
+ Math.cos(timeMs / 960 + tentacleInfluence.primaryPhase + verticalProgress) *
5860
6543
  lowerBlend *
5861
6544
  tentacleInfluence.core *
5862
6545
  radiusZ *
5863
- 0.04,
6546
+ 0.044 +
6547
+ tentacleCurl * radiusZ * 0.14,
5864
6548
  };
5865
6549
  }
5866
6550
  /**
5867
6551
  * Blends nearby seeded tentacle profiles at one mesh longitude.
5868
6552
  *
5869
- * @private helper of `octopus3d3AvatarVisual`
6553
+ * @private helper of `octopus3d4AvatarVisual`
5870
6554
  */
5871
- function resolveContinuousTentacleInfluence(options, longitude) {
6555
+ function resolveBlobbyTentacleInfluence(options, longitude) {
5872
6556
  let totalWeight = 0;
5873
6557
  let weightedSin = 0;
5874
6558
  let weightedCos = 0;
@@ -5876,10 +6560,12 @@
5876
6560
  let weightedLengthScale = 0;
5877
6561
  let weightedSwayScale = 0;
5878
6562
  let weightedDepthScale = 0;
5879
- let weightedPhase = 0;
6563
+ let weightedCurlScale = 0;
6564
+ let weightedPrimaryPhase = 0;
6565
+ let weightedSecondaryPhase = 0;
5880
6566
  for (const tentacleProfile of options.tentacleProfiles) {
5881
6567
  const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
5882
- const width = 0.2 * tentacleProfile.widthScale;
6568
+ const width = 0.22 * tentacleProfile.widthScale;
5883
6569
  const weight = Math.exp(-(distance * distance) / (width * width));
5884
6570
  totalWeight += weight;
5885
6571
  weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
@@ -5888,7 +6574,9 @@
5888
6574
  weightedLengthScale += tentacleProfile.lengthScale * weight;
5889
6575
  weightedSwayScale += tentacleProfile.swayScale * weight;
5890
6576
  weightedDepthScale += tentacleProfile.depthScale * weight;
5891
- weightedPhase += tentacleProfile.phase * weight;
6577
+ weightedCurlScale += tentacleProfile.curlScale * weight;
6578
+ weightedPrimaryPhase += tentacleProfile.primaryPhase * weight;
6579
+ weightedSecondaryPhase += tentacleProfile.secondaryPhase * weight;
5892
6580
  }
5893
6581
  if (totalWeight < 0.0001) {
5894
6582
  return {
@@ -5898,7 +6586,9 @@
5898
6586
  lengthScale: 1,
5899
6587
  swayScale: 1,
5900
6588
  depthScale: 1,
5901
- phase: 0,
6589
+ curlScale: 1,
6590
+ primaryPhase: 0,
6591
+ secondaryPhase: 0,
5902
6592
  };
5903
6593
  }
5904
6594
  return {
@@ -5908,52 +6598,57 @@
5908
6598
  lengthScale: weightedLengthScale / totalWeight,
5909
6599
  swayScale: weightedSwayScale / totalWeight,
5910
6600
  depthScale: weightedDepthScale / totalWeight,
5911
- phase: weightedPhase / totalWeight,
6601
+ curlScale: weightedCurlScale / totalWeight,
6602
+ primaryPhase: weightedPrimaryPhase / totalWeight,
6603
+ secondaryPhase: weightedSecondaryPhase / totalWeight,
5912
6604
  };
5913
6605
  }
5914
6606
  /**
5915
6607
  * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
5916
6608
  *
5917
- * @private helper of `octopus3d3AvatarVisual`
6609
+ * @private helper of `octopus3d4AvatarVisual`
5918
6610
  */
5919
- function resolveContinuousLobeWave(options, longitude) {
6611
+ function resolveBlobbyLobeWave(options, longitude) {
5920
6612
  const { morphologyProfile, animationPhase, timeMs } = options;
5921
6613
  return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT +
5922
6614
  animationPhase +
5923
- timeMs / (980 + morphologyProfile.body.lobeCount * 18)) +
6615
+ timeMs / (940 + morphologyProfile.body.lobeCount * 18)) +
5924
6616
  1) /
5925
6617
  2);
5926
6618
  }
5927
6619
  /**
5928
6620
  * Resolves one base fill tone for a patch on the continuous octopus mesh.
5929
6621
  *
5930
- * @private helper of `octopus3d3AvatarVisual`
6622
+ * @private helper of `octopus3d4AvatarVisual`
5931
6623
  */
5932
- function resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
6624
+ function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
5933
6625
  const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
5934
- if (tonalProgress < 0.14) {
6626
+ if (tonalProgress < 0.12) {
5935
6627
  return palette.highlight;
5936
6628
  }
5937
- if (tonalProgress < 0.32) {
6629
+ if (tonalProgress < 0.3) {
5938
6630
  return palette.secondary;
5939
6631
  }
5940
6632
  if (tonalProgress < 0.72) {
5941
- return forwardness > 0.55 ? palette.secondary : palette.primary;
6633
+ return forwardness > 0.56 ? palette.secondary : palette.primary;
5942
6634
  }
5943
6635
  return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
5944
6636
  }
5945
6637
  /**
5946
- * Draws one projected mesh patch with soft shading and a subtle edge.
6638
+ * Draws one projected mesh patch with multi-pass shading and a soft edge.
5947
6639
  *
5948
- * @private helper of `octopus3d3AvatarVisual`
6640
+ * @private helper of `octopus3d4AvatarVisual`
5949
6641
  */
5950
- function drawContinuousSurfacePatch(context, surfacePatch) {
6642
+ function drawBlobbyContinuousSurfacePatch(context, surfacePatch) {
5951
6643
  drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
5952
6644
  if (surfacePatch.lightIntensity > 0) {
5953
- drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.18 * surfacePatch.lightIntensity})`);
6645
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.2 * surfacePatch.lightIntensity})`);
5954
6646
  }
5955
6647
  else if (surfacePatch.lightIntensity < 0) {
5956
- drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.25 * Math.abs(surfacePatch.lightIntensity)})`);
6648
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.26 * Math.abs(surfacePatch.lightIntensity)})`);
6649
+ }
6650
+ if (surfacePatch.rimLightIntensity > 0.04) {
6651
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 247, 230, ${0.22 * surfacePatch.rimLightIntensity})`);
5957
6652
  }
5958
6653
  context.save();
5959
6654
  context.beginPath();
@@ -5963,7 +6658,7 @@
5963
6658
  }
5964
6659
  context.closePath();
5965
6660
  context.strokeStyle = surfacePatch.outlineColor;
5966
- context.lineWidth = Math.max(0.7, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0032);
6661
+ context.lineWidth = Math.max(0.6, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0026);
5967
6662
  context.lineJoin = 'round';
5968
6663
  context.stroke();
5969
6664
  context.restore();
@@ -5971,9 +6666,9 @@
5971
6666
  /**
5972
6667
  * Draws projected mantle-current lines on the front of the mesh.
5973
6668
  *
5974
- * @private helper of `octopus3d3AvatarVisual`
6669
+ * @private helper of `octopus3d4AvatarVisual`
5975
6670
  */
5976
- function drawProjectedSurfaceCurrents(options) {
6671
+ function drawBlobbyContinuousCurrents(options) {
5977
6672
  const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
5978
6673
  const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
5979
6674
  const centerIndex = (currentCount - 1) / 2;
@@ -5983,11 +6678,11 @@
5983
6678
  for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
5984
6679
  const baseLongitude = (currentIndex - centerIndex) * 0.15;
5985
6680
  const projectedPoints = [];
5986
- for (let sampleIndex = 0; sampleIndex < 8; sampleIndex++) {
5987
- const progress = sampleIndex / 7;
5988
- const latitude = -0.46 + progress * 0.74;
5989
- const longitude = baseLongitude + Math.sin(timeMs / 1160 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.035;
5990
- const scenePoint = transformScenePoint(sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
6681
+ for (let sampleIndex = 0; sampleIndex < 9; sampleIndex++) {
6682
+ const progress = sampleIndex / 8;
6683
+ const latitude = -0.48 + progress * 0.78;
6684
+ const longitude = baseLongitude + Math.sin(timeMs / 1140 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.038;
6685
+ const scenePoint = transformScenePoint(sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
5991
6686
  if (scenePoint.z > center.z - size * 0.016) {
5992
6687
  projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
5993
6688
  }
@@ -6009,38 +6704,61 @@
6009
6704
  /**
6010
6705
  * Draws small projected sucker highlights on the waving lower mesh lobes.
6011
6706
  *
6012
- * @private helper of `octopus3d3AvatarVisual`
6707
+ * @private helper of `octopus3d4AvatarVisual`
6013
6708
  */
6014
- function drawProjectedTentacleSuckers(options) {
6709
+ function drawBlobbyContinuousSuckers(options) {
6015
6710
  const { surfaceOptions, size } = options;
6016
6711
  const { timeMs } = surfaceOptions;
6017
6712
  for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
6018
- if (Math.cos(tentacleProfile.centerLongitude) < -0.12) {
6713
+ if (Math.cos(tentacleProfile.centerLongitude) < -0.16) {
6019
6714
  continue;
6020
6715
  }
6021
- for (let suckerIndex = 0; suckerIndex < 3; suckerIndex++) {
6022
- const latitude = 0.52 + suckerIndex * 0.14;
6023
- const sideOffset = tentacleProfile.suckerSide * (0.035 + suckerIndex * 0.012) * tentacleProfile.widthScale;
6024
- const waveOffset = Math.sin(timeMs / 900 + tentacleProfile.phase + suckerIndex * 0.8) * 0.018;
6025
- drawProjectedSurfaceSpot({
6716
+ for (let suckerIndex = 0; suckerIndex < 4; suckerIndex++) {
6717
+ const latitude = 0.5 + suckerIndex * 0.12;
6718
+ const sideOffset = tentacleProfile.suckerSide * (0.038 + suckerIndex * 0.014) * tentacleProfile.widthScale;
6719
+ const waveOffset = Math.sin(timeMs / 880 + tentacleProfile.primaryPhase + suckerIndex * 0.78) * 0.02;
6720
+ drawBlobbyContinuousSurfaceSpot({
6026
6721
  ...options,
6027
6722
  latitude,
6028
6723
  longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
6029
- radiusScale: size * (0.0065 - suckerIndex * 0.0007),
6724
+ radiusScale: size * (0.0068 - suckerIndex * 0.0006),
6030
6725
  });
6031
6726
  }
6032
6727
  }
6033
6728
  }
6729
+ /**
6730
+ * Draws seeded pigment spots across the upper mesh for a richer skin texture.
6731
+ *
6732
+ * @private helper of `octopus3d4AvatarVisual`
6733
+ */
6734
+ function drawBlobbySkinSpots(options) {
6735
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, skinSpots, } = options;
6736
+ for (const skinSpot of skinSpots) {
6737
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, skinSpot.latitude, skinSpot.longitude);
6738
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
6739
+ if (sceneCenterPoint.z <= center.z - size * 0.01) {
6740
+ continue;
6741
+ }
6742
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
6743
+ const spotRadius = size * skinSpot.radiusScale;
6744
+ context.save();
6745
+ context.beginPath();
6746
+ context.arc(projectedCenterPoint.x, projectedCenterPoint.y, spotRadius, 0, Math.PI * 2);
6747
+ context.fillStyle = `${palette.shadow}${formatAlphaHex(skinSpot.opacity)}`;
6748
+ context.fill();
6749
+ context.restore();
6750
+ }
6751
+ }
6034
6752
  /**
6035
6753
  * Draws one tiny projected surface spot by sampling local mesh tangents.
6036
6754
  *
6037
- * @private helper of `octopus3d3AvatarVisual`
6755
+ * @private helper of `octopus3d4AvatarVisual`
6038
6756
  */
6039
- function drawProjectedSurfaceSpot(options) {
6757
+ function drawBlobbyContinuousSurfaceSpot(options) {
6040
6758
  const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
6041
- const localCenter = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude);
6042
- const localHorizontal = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
6043
- const localVertical = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
6759
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude);
6760
+ const localHorizontal = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
6761
+ const localVertical = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
6044
6762
  const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
6045
6763
  if (sceneCenterPoint.z <= center.z - size * 0.012) {
6046
6764
  return;
@@ -6050,27 +6768,55 @@
6050
6768
  const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
6051
6769
  const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
6052
6770
  radiusScale *
6053
- 0.74, size * 0.003, size * 0.018);
6771
+ 0.78, size * 0.003, size * 0.02);
6054
6772
  const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
6055
6773
  radiusScale *
6056
- 0.52, size * 0.0024, size * 0.014);
6774
+ 0.54, size * 0.0024, size * 0.015);
6057
6775
  const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
6058
6776
  context.save();
6059
6777
  context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
6060
6778
  context.rotate(rotation);
6061
6779
  context.beginPath();
6062
6780
  context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
6063
- context.fillStyle = `${palette.highlight}73`;
6781
+ context.fillStyle = `${palette.highlight}80`;
6064
6782
  context.fill();
6065
- context.strokeStyle = `${palette.highlight}99`;
6783
+ context.strokeStyle = `${palette.highlight}a8`;
6066
6784
  context.lineWidth = Math.max(0.7, size * 0.0028);
6067
6785
  context.stroke();
6068
6786
  context.restore();
6069
6787
  }
6788
+ /**
6789
+ * Draws a soft, slowly drifting gloss highlight on the front of the mesh.
6790
+ *
6791
+ * @private helper of `octopus3d4AvatarVisual`
6792
+ */
6793
+ function drawBlobbyContinuousGloss(options) {
6794
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size } = options;
6795
+ const { timeMs, animationPhase } = surfaceOptions;
6796
+ const glossLatitude = -0.3 + Math.sin(timeMs / 2700 + animationPhase) * 0.04;
6797
+ const glossLongitude = -0.18 + Math.cos(timeMs / 2300 + animationPhase * 0.8) * 0.05;
6798
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, glossLatitude, glossLongitude);
6799
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
6800
+ if (sceneCenterPoint.z <= center.z) {
6801
+ return;
6802
+ }
6803
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
6804
+ const glossRadius = size * 0.058;
6805
+ context.save();
6806
+ const glossGradient = context.createRadialGradient(projectedCenterPoint.x - glossRadius * 0.3, projectedCenterPoint.y - glossRadius * 0.4, glossRadius * 0.04, projectedCenterPoint.x, projectedCenterPoint.y, glossRadius);
6807
+ glossGradient.addColorStop(0, 'rgba(255, 255, 255, 0.34)');
6808
+ glossGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
6809
+ glossGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
6810
+ context.fillStyle = glossGradient;
6811
+ context.beginPath();
6812
+ context.ellipse(projectedCenterPoint.x, projectedCenterPoint.y, glossRadius * 1.05, glossRadius * 0.78, 0, 0, Math.PI * 2);
6813
+ context.fill();
6814
+ context.restore();
6815
+ }
6070
6816
  /**
6071
6817
  * Resolves a signed angular distance from the source longitude to the target longitude.
6072
6818
  *
6073
- * @private helper of `octopus3d3AvatarVisual`
6819
+ * @private helper of `octopus3d4AvatarVisual`
6074
6820
  */
6075
6821
  function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
6076
6822
  return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
@@ -6078,12 +6824,22 @@
6078
6824
  /**
6079
6825
  * Smoothly maps a value between two bounds into `[0, 1]`.
6080
6826
  *
6081
- * @private helper of `octopus3d3AvatarVisual`
6827
+ * @private helper of `octopus3d4AvatarVisual`
6082
6828
  */
6083
6829
  function smoothStep(edgeStart, edgeEnd, value) {
6084
6830
  const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
6085
6831
  return progress * progress * (3 - 2 * progress);
6086
6832
  }
6833
+ /**
6834
+ * Converts an opacity ratio into a two-digit hexadecimal alpha suffix.
6835
+ *
6836
+ * @private helper of `octopus3d4AvatarVisual`
6837
+ */
6838
+ function formatAlphaHex(opacity) {
6839
+ return Math.round(clampNumber$1(opacity, 0, 1) * 255)
6840
+ .toString(16)
6841
+ .padStart(2, '0');
6842
+ }
6087
6843
 
6088
6844
  /* eslint-disable no-magic-numbers */
6089
6845
  /**
@@ -6856,6 +7612,7 @@
6856
7612
  octopus3dAvatarVisual,
6857
7613
  octopus3d2AvatarVisual,
6858
7614
  octopus3d3AvatarVisual,
7615
+ octopus3d4AvatarVisual,
6859
7616
  asciiOctopusAvatarVisual,
6860
7617
  minecraftAvatarVisual,
6861
7618
  minecraft2AvatarVisual,
@@ -6937,7 +7694,7 @@
6937
7694
  *
6938
7695
  * @private shared avatar contract
6939
7696
  */
6940
- const DEFAULT_AGENT_AVATAR_VISUAL_ID = 'octopus3d3';
7697
+ const DEFAULT_AGENT_AVATAR_VISUAL_ID = 'octopus3d4';
6941
7698
  /**
6942
7699
  * Resolves the avatar visual preferred by an agent, then falls back to a federated server default
6943
7700
  * and finally to the caller/server default.
@@ -9308,6 +10065,11 @@
9308
10065
  * - `isValidAgentUrl` *(this one)* which tests just agent URL
9309
10066
  * - `isValidPipelineUrl` which tests just pipeline URL
9310
10067
  *
10068
+ * Note: This is a pure structural validator and does not block private/internal network
10069
+ * addresses. Callers that fetch the URL server-side from an untrusted network context must
10070
+ * additionally apply the `assertSafeUrl` SSRF guard from the Agents Server before any
10071
+ * outbound request.
10072
+ *
9311
10073
  * @public exported from `@promptbook/utils`
9312
10074
  */
9313
10075
  function isValidAgentUrl(url) {
@@ -9321,12 +10083,6 @@
9321
10083
  // TODO: [🐠]
9322
10084
  return false;
9323
10085
  }
9324
- /*
9325
- Note: [👣][🧠] Is it secure to allow pipeline URLs on private and unsecured networks?
9326
- if (isUrlOnPrivateNetwork(url)) {
9327
- return false;
9328
- }
9329
- */
9330
10086
  return true;
9331
10087
  }
9332
10088
  // TODO: [🐠] Maybe more info why the URL is invalid