@promptbook/components 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/esm/index.es.js CHANGED
@@ -40,7 +40,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
40
40
  * @generated
41
41
  * @see https://github.com/webgptorg/promptbook
42
42
  */
43
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-134';
43
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-136';
44
44
  /**
45
45
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
46
46
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -3253,7 +3253,7 @@ function fillTextureRect(texture, x, y, width, height, color) {
3253
3253
  *
3254
3254
  * @private helper of `minecraft2AvatarVisual`
3255
3255
  */
3256
- const LIGHT_DIRECTION$3 = normalizeVector3({
3256
+ const LIGHT_DIRECTION$4 = normalizeVector3({
3257
3257
  x: 0.4,
3258
3258
  y: -0.65,
3259
3259
  z: 0.92,
@@ -3477,7 +3477,7 @@ function resolveVisibleCuboidFaces(cuboid, size, sceneCenterX, sceneCenterY) {
3477
3477
  corners: projectedCorners,
3478
3478
  texture: faceDefinition.texture,
3479
3479
  averageDepth: transformedCorners.reduce((depthSum, corner) => depthSum + corner.z, 0) / transformedCorners.length,
3480
- lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$3), -1, 1),
3480
+ lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$4), -1, 1),
3481
3481
  outlineColor: cuboid.outlineColor,
3482
3482
  };
3483
3483
  });
@@ -4164,7 +4164,7 @@ function resolveSeededIntegerRange(random, minimumValue, maximumValue) {
4164
4164
  *
4165
4165
  * @private helper of `octopus3AvatarVisual`
4166
4166
  */
4167
- function formatAlphaHex$1(opacity) {
4167
+ function formatAlphaHex$2(opacity) {
4168
4168
  return Math.round(Math.min(1, Math.max(0, opacity)) * 255)
4169
4169
  .toString(16)
4170
4170
  .padStart(2, '0');
@@ -4543,7 +4543,7 @@ function drawSeededEye(context, centerX, centerY, radiusX, radiusY, rotation, pa
4543
4543
  context.beginPath();
4544
4544
  context.moveTo(-radiusX * 0.74, radiusY * 0.2);
4545
4545
  context.quadraticCurveTo(0, radiusY * 0.38, radiusX * 0.74, radiusY * 0.2);
4546
- context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
4546
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$2(eyeStyle.lowerLidOpacity)}`;
4547
4547
  context.lineWidth = radiusX * 0.08;
4548
4548
  context.lineCap = 'round';
4549
4549
  context.stroke();
@@ -4624,7 +4624,7 @@ function drawProjectedOrganicEye(context, localCenter, radiusX, radiusY, center,
4624
4624
  context.beginPath();
4625
4625
  context.moveTo(-projectedRadiusX * 0.74, projectedRadiusY * 0.2);
4626
4626
  context.quadraticCurveTo(0, projectedRadiusY * 0.38, projectedRadiusX * 0.74, projectedRadiusY * 0.2);
4627
- context.strokeStyle = `${palette.highlight}${formatAlphaHex(eyeStyle.lowerLidOpacity)}`;
4627
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
4628
4628
  context.lineWidth = projectedRadiusX * 0.08;
4629
4629
  context.lineCap = 'round';
4630
4630
  context.stroke();
@@ -4670,7 +4670,7 @@ function drawProjectedQuad(context, corners, fillStyle) {
4670
4670
  *
4671
4671
  * @private helper of the 3D octopus avatar visuals
4672
4672
  */
4673
- function formatAlphaHex(opacity) {
4673
+ function formatAlphaHex$1(opacity) {
4674
4674
  return Math.round(clampNumber$1(opacity, 0, 1) * 255)
4675
4675
  .toString(16)
4676
4676
  .padStart(2, '0');
@@ -4682,7 +4682,7 @@ function formatAlphaHex(opacity) {
4682
4682
  *
4683
4683
  * @private helper of `octopus3dAvatarVisual`
4684
4684
  */
4685
- const LIGHT_DIRECTION$2 = normalizeVector3({
4685
+ const LIGHT_DIRECTION$3 = normalizeVector3({
4686
4686
  x: 0.48,
4687
4687
  y: -0.62,
4688
4688
  z: 0.94,
@@ -4932,7 +4932,7 @@ function resolveVisibleEllipsoidPatches(options) {
4932
4932
  corners: projectedCorners,
4933
4933
  averageDepth: transformedCorners.reduce((depthSum, transformedCorner) => depthSum + transformedCorner.z, 0) /
4934
4934
  transformedCorners.length,
4935
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
4935
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$3), -1, 1),
4936
4936
  fillStyle: resolveSurfacePatchFillStyle(palette, verticalProgress + verticalColorBias),
4937
4937
  outlineColor,
4938
4938
  });
@@ -5121,7 +5121,7 @@ function resolveEllipsoidSurfaceDepth(radiusX, radiusY, radiusZ, x, y) {
5121
5121
  *
5122
5122
  * @private helper of `octopus3d2AvatarVisual`
5123
5123
  */
5124
- const LIGHT_DIRECTION$1 = normalizeVector3({
5124
+ const LIGHT_DIRECTION$2 = normalizeVector3({
5125
5125
  x: 0.38,
5126
5126
  y: -0.6,
5127
5127
  z: 0.98,
@@ -5280,13 +5280,13 @@ function drawBlobbyOctopusShadow(context, size, palette, interaction, timeMs, mo
5280
5280
  *
5281
5281
  * @private helper of `octopus3d2AvatarVisual`
5282
5282
  */
5283
- const LATITUDE_PATCH_COUNT$1 = 12;
5283
+ const LATITUDE_PATCH_COUNT$2 = 12;
5284
5284
  /**
5285
5285
  * Number of longitude segments used by the single blobby octopus mesh.
5286
5286
  *
5287
5287
  * @private helper of `octopus3d2AvatarVisual`
5288
5288
  */
5289
- const LONGITUDE_PATCH_COUNT$1 = 24;
5289
+ const LONGITUDE_PATCH_COUNT$2 = 24;
5290
5290
  /**
5291
5291
  * Resolves all visible projected patches for the single blobby octopus mesh.
5292
5292
  *
@@ -5300,8 +5300,8 @@ const LONGITUDE_PATCH_COUNT$1 = 24;
5300
5300
  */
5301
5301
  function resolveVisibleBlobbyOctopusPatches(options) {
5302
5302
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, animationPhase, timeMs, } = options;
5303
- const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
5304
- const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
5303
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$2;
5304
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$2;
5305
5305
  const surfacePatches = [];
5306
5306
  const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
5307
5307
  const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
@@ -5360,8 +5360,8 @@ function resolveVisibleBlobbyOctopusPatches(options) {
5360
5360
  transformedCorners[2].z +
5361
5361
  transformedCorners[3].z) /
5362
5362
  4,
5363
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
5364
- fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
5363
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
5364
+ fillStyle: resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
5365
5365
  outlineColor: verticalProgress < 0.58 ? `${palette.highlight}73` : `${palette.shadow}8a`,
5366
5366
  });
5367
5367
  }
@@ -5429,7 +5429,7 @@ function resolveLowerLobeWave(longitude, morphologyProfile, animationPhase, time
5429
5429
  *
5430
5430
  * @private helper of `octopus3d2AvatarVisual`
5431
5431
  */
5432
- function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, lowerLobeWave) {
5432
+ function resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, forwardness, lowerLobeWave) {
5433
5433
  const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.12 - forwardness * 0.07, 0, 1);
5434
5434
  if (tonalProgress < 0.16) {
5435
5435
  return palette.highlight;
@@ -5475,7 +5475,7 @@ function drawBlobbySurfacePatch(context, surfacePatch) {
5475
5475
  *
5476
5476
  * @private helper of `octopus3d3AvatarVisual`
5477
5477
  */
5478
- const LIGHT_DIRECTION = normalizeVector3({
5478
+ const LIGHT_DIRECTION$1 = normalizeVector3({
5479
5479
  x: 0.34,
5480
5480
  y: -0.62,
5481
5481
  z: 1,
@@ -5485,7 +5485,7 @@ const LIGHT_DIRECTION = normalizeVector3({
5485
5485
  *
5486
5486
  * @private helper of `octopus3d3AvatarVisual`
5487
5487
  */
5488
- const OCTOPUS_TENTACLE_COUNT = 8;
5488
+ const OCTOPUS_TENTACLE_COUNT$1 = 8;
5489
5489
  /**
5490
5490
  * Cache keyed by the `createRandom` factory reference, which is stable for the lifetime of one
5491
5491
  * mounted `<Avatar/>` component (created inside `resolveAvatarRenderDefinition` and held in a
@@ -5515,52 +5515,688 @@ function getOctopus3d3StableState(createRandom) {
5515
5515
  animationPhase: animationRandom() * Math.PI * 2,
5516
5516
  leftEyePhaseOffset,
5517
5517
  rightEyePhaseOffset,
5518
- tentacleProfiles: createContinuousTentacleProfiles(createRandom, morphologyProfile),
5518
+ tentacleProfiles: createContinuousTentacleProfiles(createRandom, morphologyProfile),
5519
+ };
5520
+ stableStateCache.set(createRandom, state);
5521
+ return state;
5522
+ }
5523
+ /**
5524
+ * Octopus 3D 3 avatar visual.
5525
+ *
5526
+ * @private built-in avatar visual
5527
+ */
5528
+ const octopus3d3AvatarVisual = {
5529
+ id: 'octopus3d3',
5530
+ title: 'Octopus 3D 3',
5531
+ description: 'Cute continuous 3D octopus with a blobby single mesh, waving tentacle lobes, rich shading, and cursor-aware eyes.',
5532
+ isAnimated: true,
5533
+ supportsPointerTracking: true,
5534
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
5535
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles } = getOctopus3d3StableState(createRandom);
5536
+ const sceneCenterX = size * 0.5;
5537
+ const sceneCenterY = size * 0.535;
5538
+ const bob = Math.sin(timeMs / 960 + animationPhase) * size * 0.012;
5539
+ const meshCenter = {
5540
+ x: interaction.bodyOffsetX * size * 0.048 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
5541
+ y: -size * 0.07 + interaction.bodyOffsetY * size * 0.026 + bob,
5542
+ z: interaction.intensity * size * 0.018,
5543
+ };
5544
+ const rotationY = -0.1 +
5545
+ Math.sin(timeMs / 2700 + animationPhase) * 0.035 +
5546
+ interaction.bodyOffsetX * 0.22 +
5547
+ interaction.gazeX * 0.88;
5548
+ const rotationX = -0.07 +
5549
+ Math.cos(timeMs / 3100 + animationPhase * 0.7) * 0.018 -
5550
+ interaction.bodyOffsetY * 0.08 -
5551
+ interaction.gazeY * 0.38;
5552
+ const surfaceOptions = {
5553
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.1,
5554
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.08,
5555
+ radiusZ: size *
5556
+ morphologyProfile.body.bodyRadiusRatio *
5557
+ (1.02 + (morphologyProfile.body.horizontalStretch - 1) * 0.18),
5558
+ morphologyProfile,
5559
+ timeMs,
5560
+ animationPhase,
5561
+ tentacleProfiles,
5562
+ };
5563
+ const surfacePatches = resolveVisibleContinuousOctopusPatches({
5564
+ ...surfaceOptions,
5565
+ center: meshCenter,
5566
+ rotationX,
5567
+ rotationY,
5568
+ sceneCenterX,
5569
+ sceneCenterY,
5570
+ size,
5571
+ palette,
5572
+ });
5573
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.2 - 0.03, -0.22, 0.08);
5574
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.1, 0.18, 0.32);
5575
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.2 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
5576
+ const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
5577
+ const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
5578
+ const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
5579
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.76;
5580
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.9;
5581
+ drawAvatarFrame(context, size, palette);
5582
+ drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
5583
+ drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile);
5584
+ for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
5585
+ drawContinuousSurfacePatch(context, surfacePatch);
5586
+ }
5587
+ drawProjectedSurfaceCurrents({
5588
+ context,
5589
+ surfaceOptions,
5590
+ center: meshCenter,
5591
+ rotationX,
5592
+ rotationY,
5593
+ sceneCenterX,
5594
+ sceneCenterY,
5595
+ size,
5596
+ palette,
5597
+ morphologyProfile,
5598
+ timeMs,
5599
+ animationPhase,
5600
+ });
5601
+ drawProjectedTentacleSuckers({
5602
+ context,
5603
+ surfaceOptions,
5604
+ center: meshCenter,
5605
+ rotationX,
5606
+ rotationY,
5607
+ sceneCenterX,
5608
+ sceneCenterY,
5609
+ size,
5610
+ palette,
5611
+ });
5612
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
5613
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
5614
+ drawProjectedOrganicMouth(context, [
5615
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
5616
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
5617
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
5618
+ ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
5619
+ },
5620
+ };
5621
+ /**
5622
+ * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
5623
+ *
5624
+ * @private helper of `octopus3d3AvatarVisual`
5625
+ */
5626
+ function createContinuousTentacleProfiles(createRandom, morphologyProfile) {
5627
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT$1 }, (_, tentacleIndex) => {
5628
+ const tentacleRandom = createRandom(`octopus3d3-tentacle-${tentacleIndex}`);
5629
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT$1 - 1);
5630
+ return {
5631
+ centerLongitude: -Math.PI * 0.86 +
5632
+ progress * Math.PI * 1.72 +
5633
+ (tentacleRandom() - 0.5) * (0.08 + morphologyProfile.tentacles.rootSpreadScale * 0.03),
5634
+ widthScale: 0.86 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.16,
5635
+ lengthScale: 0.86 + tentacleRandom() * 0.36 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.22,
5636
+ swayScale: 0.82 + tentacleRandom() * 0.38 + (morphologyProfile.tentacles.swayScale - 1) * 0.2,
5637
+ depthScale: 0.86 + tentacleRandom() * 0.32 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.2,
5638
+ phase: tentacleRandom() * Math.PI * 2,
5639
+ suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
5640
+ };
5641
+ });
5642
+ }
5643
+ /**
5644
+ * Draws the soft underwater atmosphere behind the continuous octopus mesh.
5645
+ *
5646
+ * @private helper of `octopus3d3AvatarVisual`
5647
+ */
5648
+ function drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
5649
+ 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));
5650
+ glowGradient.addColorStop(0, `${palette.highlight}66`);
5651
+ glowGradient.addColorStop(0.34, `${palette.accent}2e`);
5652
+ glowGradient.addColorStop(1, `${palette.highlight}00`);
5653
+ context.fillStyle = glowGradient;
5654
+ context.fillRect(0, 0, size, size);
5655
+ 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);
5656
+ lowerGradient.addColorStop(0, `${palette.secondary}25`);
5657
+ lowerGradient.addColorStop(1, `${palette.secondary}00`);
5658
+ context.fillStyle = lowerGradient;
5659
+ context.fillRect(0, 0, size, size);
5660
+ }
5661
+ /**
5662
+ * Draws the soft lower shadow that anchors the octopus in the avatar frame.
5663
+ *
5664
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
5665
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
5666
+ *
5667
+ * @private helper of `octopus3d3AvatarVisual`
5668
+ */
5669
+ function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
5670
+ const cx = size * 0.5 + interaction.gazeX * size * 0.045;
5671
+ const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
5672
+ const rx = size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02);
5673
+ const ry = size * 0.06;
5674
+ // Scale the context so that drawing a circle produces the correct ellipse aspect ratio,
5675
+ // then fill with a radial gradient that approximates the blurry edge without context.filter.
5676
+ context.save();
5677
+ context.translate(cx, cy);
5678
+ context.scale(1, ry / rx);
5679
+ const blurRadius = rx * 1.4;
5680
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
5681
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
5682
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
5683
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
5684
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
5685
+ context.fillStyle = shadowGradient;
5686
+ context.beginPath();
5687
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
5688
+ context.fill();
5689
+ context.restore();
5690
+ }
5691
+ /**
5692
+ * Number of latitude segments used by the continuous Octopus 3D 3 mesh.
5693
+ *
5694
+ * @private helper of `octopus3d3AvatarVisual`
5695
+ */
5696
+ const LATITUDE_PATCH_COUNT$1 = 16;
5697
+ /**
5698
+ * Number of longitude segments used by the continuous Octopus 3D 3 mesh.
5699
+ *
5700
+ * @private helper of `octopus3d3AvatarVisual`
5701
+ */
5702
+ const LONGITUDE_PATCH_COUNT$1 = 40;
5703
+ /**
5704
+ * Resolves visible projected patches for the continuous octopus mesh.
5705
+ *
5706
+ * Within a single frame, mesh corner samples and longitude-only computations (tentacle
5707
+ * influence and lobe wave) are quantized to the patch grid and computed once each rather
5708
+ * than re-evaluated for every patch corner — the patch loop alone calls `sampleContinuousOctopusSurfacePoint`
5709
+ * for `latitudePatchCount * longitudePatchCount * 4` corners without caching, and each call
5710
+ * triggers an inner 8-iteration `Math.exp` loop, which dominates the per-frame cost.
5711
+ *
5712
+ * @private helper of `octopus3d3AvatarVisual`
5713
+ */
5714
+ function resolveVisibleContinuousOctopusPatches(options) {
5715
+ const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
5716
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
5717
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
5718
+ const surfacePatches = [];
5719
+ const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
5720
+ const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
5721
+ for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
5722
+ latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
5723
+ }
5724
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
5725
+ longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
5726
+ }
5727
+ const cachedTentacleInfluencesByCornerLongitude = new Array(longitudePatchCount + 1);
5728
+ const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
5729
+ const cachedTentacleInfluencesByPatchCenterLongitude = new Array(longitudePatchCount);
5730
+ const cachedLobeWavesByPatchCenterLongitude = new Float64Array(longitudePatchCount);
5731
+ const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
5732
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
5733
+ const cornerLongitude = longitudeBoundaries[boundaryIndex];
5734
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveContinuousTentacleInfluence(options, cornerLongitude);
5735
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveContinuousLobeWave(options, cornerLongitude);
5736
+ }
5737
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
5738
+ const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
5739
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveContinuousTentacleInfluence(options, patchCenterLongitude);
5740
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveContinuousLobeWave(options, patchCenterLongitude);
5741
+ cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
5742
+ }
5743
+ const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
5744
+ const transformedCornerSamples = new Array(cornerCount);
5745
+ for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
5746
+ const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex];
5747
+ for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
5748
+ const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
5749
+ const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
5750
+ const cornerSample = sampleContinuousOctopusSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
5751
+ transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
5752
+ }
5753
+ }
5754
+ for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
5755
+ const startLatitude = latitudeBoundaries[latitudeIndex];
5756
+ const endLatitude = latitudeBoundaries[latitudeIndex + 1];
5757
+ const centerLatitude = (startLatitude + endLatitude) / 2;
5758
+ const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
5759
+ const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
5760
+ const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
5761
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
5762
+ const transformedCorners = [
5763
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex],
5764
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1],
5765
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1],
5766
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex],
5767
+ ];
5768
+ const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
5769
+ if (surfaceNormal.z <= 0.008) {
5770
+ continue;
5771
+ }
5772
+ const projectedCorners = [
5773
+ projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
5774
+ projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
5775
+ projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
5776
+ projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
5777
+ ];
5778
+ surfacePatches.push({
5779
+ corners: projectedCorners,
5780
+ averageDepth: (transformedCorners[0].z +
5781
+ transformedCorners[1].z +
5782
+ transformedCorners[2].z +
5783
+ transformedCorners[3].z) /
5784
+ 4,
5785
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
5786
+ fillStyle: resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
5787
+ outlineColor: verticalProgress < 0.54 ? `${palette.highlight}69` : `${palette.shadow}78`,
5788
+ });
5789
+ }
5790
+ }
5791
+ return surfacePatches;
5792
+ }
5793
+ /**
5794
+ * Samples one point on the continuous Octopus 3D 3 surface.
5795
+ *
5796
+ * The lower hemisphere is pulled into eight seeded waving lobes, so the portrait reads as
5797
+ * tentacled while still being rendered as one connected blobby mesh.
5798
+ *
5799
+ * @private helper of `octopus3d3AvatarVisual`
5800
+ */
5801
+ function sampleContinuousOctopusSurfacePoint(options, latitude, longitude) {
5802
+ return sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, resolveContinuousTentacleInfluence(options, longitude), resolveContinuousLobeWave(options, longitude));
5803
+ }
5804
+ /**
5805
+ * Samples one point on the continuous Octopus 3D 3 surface using precomputed longitude-only
5806
+ * values to skip the per-call `Math.exp` tentacle-influence loop and the lobe-wave trig call.
5807
+ *
5808
+ * The patch loop quantizes the mesh into a fixed `(latitudePatchCount + 1) * (longitudePatchCount + 1)`
5809
+ * corner grid, so the same longitude is reused across every latitude row and each
5810
+ * tentacle/lobe value can be computed once per frame instead of `latitudePatchCount * 4` times.
5811
+ *
5812
+ * @private helper of `octopus3d3AvatarVisual`
5813
+ */
5814
+ function sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
5815
+ const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
5816
+ const cosineLatitude = Math.max(0, Math.cos(latitude));
5817
+ const verticalProgress = (Math.sin(latitude) + 1) / 2;
5818
+ const upperBlend = Math.pow(1 - verticalProgress, 1.28);
5819
+ const lowerBlend = smoothStep$1(0.38, 1, verticalProgress);
5820
+ const tipBlend = smoothStep$1(0.68, 1, verticalProgress);
5821
+ const centerPull = resolveSignedAngularDistance$1(longitude, tentacleInfluence.centerLongitude);
5822
+ const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.24 + tipBlend * 0.2);
5823
+ const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
5824
+ animationPhase * 0.6 +
5825
+ timeMs / (1750 + morphologyProfile.body.lobeCount * 30)) *
5826
+ (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.8) *
5827
+ (0.3 + lowerBlend * 0.7);
5828
+ const tentacleWave = Math.sin(timeMs / 760 + tentacleInfluence.phase + verticalProgress * 2.4) *
5829
+ lowerBlend *
5830
+ tentacleInfluence.core *
5831
+ tentacleInfluence.swayScale;
5832
+ const horizontalScale = 1.04 +
5833
+ mantleRipple +
5834
+ lowerBlend * (0.16 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.1) +
5835
+ lowerBlend * tentacleInfluence.core * (0.2 + lowerLobeWave * 0.12) -
5836
+ upperBlend * 0.08;
5837
+ const depthScale = 1.06 +
5838
+ upperBlend * 0.16 +
5839
+ Math.max(0, Math.cos(effectiveLongitude)) * 0.1 +
5840
+ lowerBlend * tentacleInfluence.core * (0.1 + tentacleInfluence.depthScale * 0.06) -
5841
+ Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
5842
+ const tentacleTubeRadius = lowerBlend * tentacleInfluence.core * (0.11 + tipBlend * 0.06 + tentacleInfluence.widthScale * 0.025) * radiusX;
5843
+ const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
5844
+ const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.72;
5845
+ const lowerDrop = lowerBlend *
5846
+ radiusY *
5847
+ (0.18 +
5848
+ tentacleInfluence.core *
5849
+ (0.38 +
5850
+ tentacleInfluence.lengthScale * 0.22 +
5851
+ (morphologyProfile.tentacles.flowLengthScale - 1) * 0.08));
5852
+ return {
5853
+ x: Math.sin(effectiveLongitude) * planarRadiusX + tentacleWave * radiusX * (0.052 + tipBlend * 0.05),
5854
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.12) -
5855
+ upperBlend * radiusY * 0.1 +
5856
+ lowerDrop +
5857
+ Math.sin(timeMs / 1420 + animationPhase + latitude * 1.6) * lowerBlend * radiusY * 0.018 +
5858
+ Math.cos(timeMs / 880 + tentacleInfluence.phase) *
5859
+ lowerBlend *
5860
+ tipBlend *
5861
+ tentacleInfluence.core *
5862
+ radiusY *
5863
+ 0.034,
5864
+ z: Math.cos(effectiveLongitude) * planarRadiusZ +
5865
+ Math.cos(timeMs / 980 + tentacleInfluence.phase + verticalProgress) *
5866
+ lowerBlend *
5867
+ tentacleInfluence.core *
5868
+ radiusZ *
5869
+ 0.04,
5870
+ };
5871
+ }
5872
+ /**
5873
+ * Blends nearby seeded tentacle profiles at one mesh longitude.
5874
+ *
5875
+ * @private helper of `octopus3d3AvatarVisual`
5876
+ */
5877
+ function resolveContinuousTentacleInfluence(options, longitude) {
5878
+ let totalWeight = 0;
5879
+ let weightedSin = 0;
5880
+ let weightedCos = 0;
5881
+ let weightedWidthScale = 0;
5882
+ let weightedLengthScale = 0;
5883
+ let weightedSwayScale = 0;
5884
+ let weightedDepthScale = 0;
5885
+ let weightedPhase = 0;
5886
+ for (const tentacleProfile of options.tentacleProfiles) {
5887
+ const distance = Math.abs(resolveSignedAngularDistance$1(longitude, tentacleProfile.centerLongitude));
5888
+ const width = 0.2 * tentacleProfile.widthScale;
5889
+ const weight = Math.exp(-(distance * distance) / (width * width));
5890
+ totalWeight += weight;
5891
+ weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
5892
+ weightedCos += Math.cos(tentacleProfile.centerLongitude) * weight;
5893
+ weightedWidthScale += tentacleProfile.widthScale * weight;
5894
+ weightedLengthScale += tentacleProfile.lengthScale * weight;
5895
+ weightedSwayScale += tentacleProfile.swayScale * weight;
5896
+ weightedDepthScale += tentacleProfile.depthScale * weight;
5897
+ weightedPhase += tentacleProfile.phase * weight;
5898
+ }
5899
+ if (totalWeight < 0.0001) {
5900
+ return {
5901
+ core: 0,
5902
+ centerLongitude: longitude,
5903
+ widthScale: 1,
5904
+ lengthScale: 1,
5905
+ swayScale: 1,
5906
+ depthScale: 1,
5907
+ phase: 0,
5908
+ };
5909
+ }
5910
+ return {
5911
+ core: clampNumber$1(totalWeight, 0, 1),
5912
+ centerLongitude: Math.atan2(weightedSin / totalWeight, weightedCos / totalWeight),
5913
+ widthScale: weightedWidthScale / totalWeight,
5914
+ lengthScale: weightedLengthScale / totalWeight,
5915
+ swayScale: weightedSwayScale / totalWeight,
5916
+ depthScale: weightedDepthScale / totalWeight,
5917
+ phase: weightedPhase / totalWeight,
5918
+ };
5919
+ }
5920
+ /**
5921
+ * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
5922
+ *
5923
+ * @private helper of `octopus3d3AvatarVisual`
5924
+ */
5925
+ function resolveContinuousLobeWave(options, longitude) {
5926
+ const { morphologyProfile, animationPhase, timeMs } = options;
5927
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT$1 +
5928
+ animationPhase +
5929
+ timeMs / (980 + morphologyProfile.body.lobeCount * 18)) +
5930
+ 1) /
5931
+ 2);
5932
+ }
5933
+ /**
5934
+ * Resolves one base fill tone for a patch on the continuous octopus mesh.
5935
+ *
5936
+ * @private helper of `octopus3d3AvatarVisual`
5937
+ */
5938
+ function resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
5939
+ const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
5940
+ if (tonalProgress < 0.14) {
5941
+ return palette.highlight;
5942
+ }
5943
+ if (tonalProgress < 0.32) {
5944
+ return palette.secondary;
5945
+ }
5946
+ if (tonalProgress < 0.72) {
5947
+ return forwardness > 0.55 ? palette.secondary : palette.primary;
5948
+ }
5949
+ return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
5950
+ }
5951
+ /**
5952
+ * Draws one projected mesh patch with soft shading and a subtle edge.
5953
+ *
5954
+ * @private helper of `octopus3d3AvatarVisual`
5955
+ */
5956
+ function drawContinuousSurfacePatch(context, surfacePatch) {
5957
+ drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
5958
+ if (surfacePatch.lightIntensity > 0) {
5959
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.18 * surfacePatch.lightIntensity})`);
5960
+ }
5961
+ else if (surfacePatch.lightIntensity < 0) {
5962
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.25 * Math.abs(surfacePatch.lightIntensity)})`);
5963
+ }
5964
+ context.save();
5965
+ context.beginPath();
5966
+ context.moveTo(surfacePatch.corners[0].x, surfacePatch.corners[0].y);
5967
+ for (let cornerIndex = 1; cornerIndex < surfacePatch.corners.length; cornerIndex++) {
5968
+ context.lineTo(surfacePatch.corners[cornerIndex].x, surfacePatch.corners[cornerIndex].y);
5969
+ }
5970
+ context.closePath();
5971
+ context.strokeStyle = surfacePatch.outlineColor;
5972
+ context.lineWidth = Math.max(0.7, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0032);
5973
+ context.lineJoin = 'round';
5974
+ context.stroke();
5975
+ context.restore();
5976
+ }
5977
+ /**
5978
+ * Draws projected mantle-current lines on the front of the mesh.
5979
+ *
5980
+ * @private helper of `octopus3d3AvatarVisual`
5981
+ */
5982
+ function drawProjectedSurfaceCurrents(options) {
5983
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
5984
+ const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
5985
+ const centerIndex = (currentCount - 1) / 2;
5986
+ context.save();
5987
+ context.lineCap = 'round';
5988
+ context.lineJoin = 'round';
5989
+ for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
5990
+ const baseLongitude = (currentIndex - centerIndex) * 0.15;
5991
+ const projectedPoints = [];
5992
+ for (let sampleIndex = 0; sampleIndex < 8; sampleIndex++) {
5993
+ const progress = sampleIndex / 7;
5994
+ const latitude = -0.46 + progress * 0.74;
5995
+ const longitude = baseLongitude + Math.sin(timeMs / 1160 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.035;
5996
+ const scenePoint = transformScenePoint(sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
5997
+ if (scenePoint.z > center.z - size * 0.016) {
5998
+ projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
5999
+ }
6000
+ }
6001
+ if (projectedPoints.length < 3) {
6002
+ continue;
6003
+ }
6004
+ context.beginPath();
6005
+ context.moveTo(projectedPoints[0].x, projectedPoints[0].y);
6006
+ for (const projectedPoint of projectedPoints.slice(1)) {
6007
+ context.lineTo(projectedPoint.x, projectedPoint.y);
6008
+ }
6009
+ context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}3d` : `${palette.accent}33`;
6010
+ context.lineWidth = size * (0.0055 + currentIndex * 0.00045);
6011
+ context.stroke();
6012
+ }
6013
+ context.restore();
6014
+ }
6015
+ /**
6016
+ * Draws small projected sucker highlights on the waving lower mesh lobes.
6017
+ *
6018
+ * @private helper of `octopus3d3AvatarVisual`
6019
+ */
6020
+ function drawProjectedTentacleSuckers(options) {
6021
+ const { surfaceOptions, size } = options;
6022
+ const { timeMs } = surfaceOptions;
6023
+ for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
6024
+ if (Math.cos(tentacleProfile.centerLongitude) < -0.12) {
6025
+ continue;
6026
+ }
6027
+ for (let suckerIndex = 0; suckerIndex < 3; suckerIndex++) {
6028
+ const latitude = 0.52 + suckerIndex * 0.14;
6029
+ const sideOffset = tentacleProfile.suckerSide * (0.035 + suckerIndex * 0.012) * tentacleProfile.widthScale;
6030
+ const waveOffset = Math.sin(timeMs / 900 + tentacleProfile.phase + suckerIndex * 0.8) * 0.018;
6031
+ drawProjectedSurfaceSpot({
6032
+ ...options,
6033
+ latitude,
6034
+ longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
6035
+ radiusScale: size * (0.0065 - suckerIndex * 0.0007),
6036
+ });
6037
+ }
6038
+ }
6039
+ }
6040
+ /**
6041
+ * Draws one tiny projected surface spot by sampling local mesh tangents.
6042
+ *
6043
+ * @private helper of `octopus3d3AvatarVisual`
6044
+ */
6045
+ function drawProjectedSurfaceSpot(options) {
6046
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
6047
+ const localCenter = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude);
6048
+ const localHorizontal = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
6049
+ const localVertical = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
6050
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
6051
+ if (sceneCenterPoint.z <= center.z - size * 0.012) {
6052
+ return;
6053
+ }
6054
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
6055
+ const projectedHorizontalPoint = projectScenePoint(transformScenePoint(localHorizontal, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
6056
+ const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
6057
+ const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
6058
+ radiusScale *
6059
+ 0.74, size * 0.003, size * 0.018);
6060
+ const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
6061
+ radiusScale *
6062
+ 0.52, size * 0.0024, size * 0.014);
6063
+ const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
6064
+ context.save();
6065
+ context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
6066
+ context.rotate(rotation);
6067
+ context.beginPath();
6068
+ context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
6069
+ context.fillStyle = `${palette.highlight}73`;
6070
+ context.fill();
6071
+ context.strokeStyle = `${palette.highlight}99`;
6072
+ context.lineWidth = Math.max(0.7, size * 0.0028);
6073
+ context.stroke();
6074
+ context.restore();
6075
+ }
6076
+ /**
6077
+ * Resolves a signed angular distance from the source longitude to the target longitude.
6078
+ *
6079
+ * @private helper of `octopus3d3AvatarVisual`
6080
+ */
6081
+ function resolveSignedAngularDistance$1(sourceLongitude, targetLongitude) {
6082
+ return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
6083
+ }
6084
+ /**
6085
+ * Smoothly maps a value between two bounds into `[0, 1]`.
6086
+ *
6087
+ * @private helper of `octopus3d3AvatarVisual`
6088
+ */
6089
+ function smoothStep$1(edgeStart, edgeEnd, value) {
6090
+ const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
6091
+ return progress * progress * (3 - 2 * progress);
6092
+ }
6093
+
6094
+ /* eslint-disable no-magic-numbers */
6095
+ /**
6096
+ * Light direction used by the continuous Octopus 3D 4 mesh shading.
6097
+ *
6098
+ * @private helper of `octopus3d4AvatarVisual`
6099
+ */
6100
+ const LIGHT_DIRECTION = normalizeVector3({
6101
+ x: 0.32,
6102
+ y: -0.66,
6103
+ z: 1,
6104
+ });
6105
+ /**
6106
+ * Rim-light direction used to brighten the side of the silhouette.
6107
+ *
6108
+ * @private helper of `octopus3d4AvatarVisual`
6109
+ */
6110
+ const RIM_LIGHT_DIRECTION = normalizeVector3({
6111
+ x: -0.7,
6112
+ y: -0.18,
6113
+ z: 0.55,
6114
+ });
6115
+ /**
6116
+ * Real-octopus tentacle count used by the continuous lower mesh.
6117
+ *
6118
+ * @private helper of `octopus3d4AvatarVisual`
6119
+ */
6120
+ const OCTOPUS_TENTACLE_COUNT = 8;
6121
+ /**
6122
+ * Number of seeded skin spots painted across the octopus body.
6123
+ *
6124
+ * @private helper of `octopus3d4AvatarVisual`
6125
+ */
6126
+ const SKIN_SPOT_COUNT = 14;
6127
+ /**
6128
+ * Cache keyed by the `createRandom` factory reference, stable per mounted `<Avatar/>` component.
6129
+ *
6130
+ * @private helper of `octopus3d4AvatarVisual`
6131
+ */
6132
+ const octopus3d4StableStateCache = new WeakMap();
6133
+ /**
6134
+ * Returns the stable per-avatar state, computing it on first access and caching afterwards.
6135
+ *
6136
+ * @private helper of `octopus3d4AvatarVisual`
6137
+ */
6138
+ function getOctopus3d4StableState(createRandom) {
6139
+ const cached = octopus3d4StableStateCache.get(createRandom);
6140
+ if (cached !== undefined) {
6141
+ return cached;
6142
+ }
6143
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
6144
+ const animationRandom = createRandom('octopus3d4-animation-profile');
6145
+ const eyeRandom = createRandom('octopus3d4-eye-profile');
6146
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
6147
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
6148
+ const state = {
6149
+ morphologyProfile,
6150
+ animationPhase: animationRandom() * Math.PI * 2,
6151
+ leftEyePhaseOffset,
6152
+ rightEyePhaseOffset,
6153
+ tentacleProfiles: createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile),
6154
+ skinSpots: createBlobbySkinSpots(createRandom),
5519
6155
  };
5520
- stableStateCache.set(createRandom, state);
6156
+ octopus3d4StableStateCache.set(createRandom, state);
5521
6157
  return state;
5522
6158
  }
5523
6159
  /**
5524
- * Octopus 3D 3 avatar visual.
6160
+ * Octopus 3D 4 avatar visual.
5525
6161
  *
5526
6162
  * @private built-in avatar visual
5527
6163
  */
5528
- const octopus3d3AvatarVisual = {
5529
- id: 'octopus3d3',
5530
- title: 'Octopus 3D 3',
5531
- description: 'Cute continuous 3D octopus with a blobby single mesh, waving tentacle lobes, rich shading, and cursor-aware eyes.',
6164
+ const octopus3d4AvatarVisual = {
6165
+ id: 'octopus3d4',
6166
+ title: 'Octopus 3D 4',
6167
+ 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.',
5532
6168
  isAnimated: true,
5533
6169
  supportsPointerTracking: true,
5534
6170
  render({ context, size, palette, createRandom, timeMs, interaction }) {
5535
- const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles } = getOctopus3d3StableState(createRandom);
6171
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles, skinSpots } = getOctopus3d4StableState(createRandom);
5536
6172
  const sceneCenterX = size * 0.5;
5537
6173
  const sceneCenterY = size * 0.535;
5538
- const bob = Math.sin(timeMs / 960 + animationPhase) * size * 0.012;
6174
+ const bob = Math.sin(timeMs / 980 + animationPhase) * size * 0.013;
5539
6175
  const meshCenter = {
5540
- x: interaction.bodyOffsetX * size * 0.048 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
5541
- y: -size * 0.07 + interaction.bodyOffsetY * size * 0.026 + bob,
5542
- z: interaction.intensity * size * 0.018,
5543
- };
5544
- const rotationY = -0.1 +
5545
- Math.sin(timeMs / 2700 + animationPhase) * 0.035 +
5546
- interaction.bodyOffsetX * 0.22 +
5547
- interaction.gazeX * 0.88;
6176
+ x: interaction.bodyOffsetX * size * 0.052 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
6177
+ y: -size * 0.07 + interaction.bodyOffsetY * size * 0.028 + bob,
6178
+ z: interaction.intensity * size * 0.02,
6179
+ };
6180
+ const rotationY = -0.08 +
6181
+ Math.sin(timeMs / 2800 + animationPhase) * 0.04 +
6182
+ interaction.bodyOffsetX * 0.24 +
6183
+ interaction.gazeX * 0.98;
5548
6184
  const rotationX = -0.07 +
5549
- Math.cos(timeMs / 3100 + animationPhase * 0.7) * 0.018 -
6185
+ Math.cos(timeMs / 3200 + animationPhase * 0.7) * 0.02 -
5550
6186
  interaction.bodyOffsetY * 0.08 -
5551
- interaction.gazeY * 0.38;
6187
+ interaction.gazeY * 0.42;
5552
6188
  const surfaceOptions = {
5553
- radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.1,
5554
- radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.08,
6189
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.12,
6190
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1,
5555
6191
  radiusZ: size *
5556
6192
  morphologyProfile.body.bodyRadiusRatio *
5557
- (1.02 + (morphologyProfile.body.horizontalStretch - 1) * 0.18),
6193
+ (1.04 + (morphologyProfile.body.horizontalStretch - 1) * 0.2),
5558
6194
  morphologyProfile,
5559
6195
  timeMs,
5560
6196
  animationPhase,
5561
6197
  tentacleProfiles,
5562
6198
  };
5563
- const surfacePatches = resolveVisibleContinuousOctopusPatches({
6199
+ const surfacePatches = resolveVisibleBlobbyContinuousPatches({
5564
6200
  ...surfaceOptions,
5565
6201
  center: meshCenter,
5566
6202
  rotationX,
@@ -5570,21 +6206,33 @@ const octopus3d3AvatarVisual = {
5570
6206
  size,
5571
6207
  palette,
5572
6208
  });
5573
- const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.2 - 0.03, -0.22, 0.08);
5574
- const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.1, 0.18, 0.32);
5575
- const mouthLatitude = clampNumber$1(eyeLatitude + 0.2 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
6209
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.4 - 0.04, -0.24, 0.08);
6210
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.0, 0.18, 0.32);
6211
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.21 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
5576
6212
  const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
5577
6213
  const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
5578
6214
  const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
5579
- const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.76;
5580
- const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.9;
6215
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
6216
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
5581
6217
  drawAvatarFrame(context, size, palette);
5582
- drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
5583
- drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile);
6218
+ drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
6219
+ drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile);
5584
6220
  for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
5585
- drawContinuousSurfacePatch(context, surfacePatch);
6221
+ drawBlobbyContinuousSurfacePatch(context, surfacePatch);
5586
6222
  }
5587
- drawProjectedSurfaceCurrents({
6223
+ drawBlobbySkinSpots({
6224
+ context,
6225
+ surfaceOptions,
6226
+ center: meshCenter,
6227
+ rotationX,
6228
+ rotationY,
6229
+ sceneCenterX,
6230
+ sceneCenterY,
6231
+ size,
6232
+ palette,
6233
+ skinSpots,
6234
+ });
6235
+ drawBlobbyContinuousCurrents({
5588
6236
  context,
5589
6237
  surfaceOptions,
5590
6238
  center: meshCenter,
@@ -5598,7 +6246,7 @@ const octopus3d3AvatarVisual = {
5598
6246
  timeMs,
5599
6247
  animationPhase,
5600
6248
  });
5601
- drawProjectedTentacleSuckers({
6249
+ drawBlobbyContinuousSuckers({
5602
6250
  context,
5603
6251
  surfaceOptions,
5604
6252
  center: meshCenter,
@@ -5609,51 +6257,78 @@ const octopus3d3AvatarVisual = {
5609
6257
  size,
5610
6258
  palette,
5611
6259
  });
5612
- drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
5613
- drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
6260
+ drawBlobbyContinuousGloss({
6261
+ context,
6262
+ surfaceOptions,
6263
+ center: meshCenter,
6264
+ rotationX,
6265
+ rotationY,
6266
+ sceneCenterX,
6267
+ sceneCenterY,
6268
+ size,
6269
+ palette,
6270
+ });
6271
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
6272
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
5614
6273
  drawProjectedOrganicMouth(context, [
5615
- sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
5616
- sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
5617
- sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
6274
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
6275
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
6276
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
5618
6277
  ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
5619
6278
  },
5620
6279
  };
5621
6280
  /**
5622
6281
  * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
5623
6282
  *
5624
- * @private helper of `octopus3d3AvatarVisual`
6283
+ * @private helper of `octopus3d4AvatarVisual`
5625
6284
  */
5626
- function createContinuousTentacleProfiles(createRandom, morphologyProfile) {
6285
+ function createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile) {
5627
6286
  return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
5628
- const tentacleRandom = createRandom(`octopus3d3-tentacle-${tentacleIndex}`);
6287
+ const tentacleRandom = createRandom(`octopus3d4-tentacle-${tentacleIndex}`);
5629
6288
  const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
5630
6289
  return {
5631
- centerLongitude: -Math.PI * 0.86 +
5632
- progress * Math.PI * 1.72 +
5633
- (tentacleRandom() - 0.5) * (0.08 + morphologyProfile.tentacles.rootSpreadScale * 0.03),
5634
- widthScale: 0.86 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.16,
5635
- lengthScale: 0.86 + tentacleRandom() * 0.36 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.22,
5636
- swayScale: 0.82 + tentacleRandom() * 0.38 + (morphologyProfile.tentacles.swayScale - 1) * 0.2,
5637
- depthScale: 0.86 + tentacleRandom() * 0.32 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.2,
5638
- phase: tentacleRandom() * Math.PI * 2,
6290
+ centerLongitude: -Math.PI * 0.9 +
6291
+ progress * Math.PI * 1.8 +
6292
+ (tentacleRandom() - 0.5) * (0.06 + morphologyProfile.tentacles.rootSpreadScale * 0.025),
6293
+ widthScale: 0.92 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.18,
6294
+ lengthScale: 0.9 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.24,
6295
+ swayScale: 0.86 + tentacleRandom() * 0.4 + (morphologyProfile.tentacles.swayScale - 1) * 0.22,
6296
+ depthScale: 0.9 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.22,
6297
+ curlScale: 0.62 + tentacleRandom() * 0.46,
6298
+ primaryPhase: tentacleRandom() * Math.PI * 2,
6299
+ secondaryPhase: tentacleRandom() * Math.PI * 2,
5639
6300
  suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
5640
6301
  };
5641
6302
  });
5642
6303
  }
6304
+ /**
6305
+ * Creates seeded skin pigment spots distributed across the upper octopus mesh.
6306
+ *
6307
+ * @private helper of `octopus3d4AvatarVisual`
6308
+ */
6309
+ function createBlobbySkinSpots(createRandom) {
6310
+ const spotRandom = createRandom('octopus3d4-skin-spots');
6311
+ return Array.from({ length: SKIN_SPOT_COUNT }, () => ({
6312
+ latitude: -0.45 + spotRandom() * 0.7,
6313
+ longitude: -0.5 + spotRandom() * 1.0,
6314
+ radiusScale: 0.0028 + spotRandom() * 0.0052,
6315
+ opacity: 0.16 + spotRandom() * 0.22,
6316
+ }));
6317
+ }
5643
6318
  /**
5644
6319
  * Draws the soft underwater atmosphere behind the continuous octopus mesh.
5645
6320
  *
5646
- * @private helper of `octopus3d3AvatarVisual`
6321
+ * @private helper of `octopus3d4AvatarVisual`
5647
6322
  */
5648
- function drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
5649
- 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));
5650
- glowGradient.addColorStop(0, `${palette.highlight}66`);
5651
- glowGradient.addColorStop(0.34, `${palette.accent}2e`);
6323
+ function drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
6324
+ 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));
6325
+ glowGradient.addColorStop(0, `${palette.highlight}74`);
6326
+ glowGradient.addColorStop(0.32, `${palette.accent}32`);
5652
6327
  glowGradient.addColorStop(1, `${palette.highlight}00`);
5653
6328
  context.fillStyle = glowGradient;
5654
6329
  context.fillRect(0, 0, size, size);
5655
- 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);
5656
- lowerGradient.addColorStop(0, `${palette.secondary}25`);
6330
+ 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);
6331
+ lowerGradient.addColorStop(0, `${palette.secondary}28`);
5657
6332
  lowerGradient.addColorStop(1, `${palette.secondary}00`);
5658
6333
  context.fillStyle = lowerGradient;
5659
6334
  context.fillRect(0, 0, size, size);
@@ -5661,26 +6336,21 @@ function drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, s
5661
6336
  /**
5662
6337
  * Draws the soft lower shadow that anchors the octopus in the avatar frame.
5663
6338
  *
5664
- * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
5665
- * blurry ellipse without triggering a costly software rasterization pass on every frame.
5666
- *
5667
- * @private helper of `octopus3d3AvatarVisual`
6339
+ * @private helper of `octopus3d4AvatarVisual`
5668
6340
  */
5669
- function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
5670
- const cx = size * 0.5 + interaction.gazeX * size * 0.045;
6341
+ function drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
6342
+ const cx = size * 0.5 + interaction.gazeX * size * 0.046;
5671
6343
  const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
5672
- const rx = size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02);
5673
- const ry = size * 0.06;
5674
- // Scale the context so that drawing a circle produces the correct ellipse aspect ratio,
5675
- // then fill with a radial gradient that approximates the blurry edge without context.filter.
6344
+ const rx = size * (0.2 + morphologyProfile.tentacles.rootSpreadScale * 0.024 + interaction.intensity * 0.022);
6345
+ const ry = size * 0.062;
5676
6346
  context.save();
5677
6347
  context.translate(cx, cy);
5678
6348
  context.scale(1, ry / rx);
5679
- const blurRadius = rx * 1.4;
6349
+ const blurRadius = rx * 1.42;
5680
6350
  const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
5681
- shadowGradient.addColorStop(0, `${palette.shadow}7a`);
5682
- shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
5683
- shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
6351
+ shadowGradient.addColorStop(0, `${palette.shadow}82`);
6352
+ shadowGradient.addColorStop(0.45, `${palette.shadow}4a`);
6353
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1c`);
5684
6354
  shadowGradient.addColorStop(1, `${palette.shadow}00`);
5685
6355
  context.fillStyle = shadowGradient;
5686
6356
  context.beginPath();
@@ -5689,29 +6359,27 @@ function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs
5689
6359
  context.restore();
5690
6360
  }
5691
6361
  /**
5692
- * Number of latitude segments used by the continuous Octopus 3D 3 mesh.
6362
+ * Number of latitude 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 LATITUDE_PATCH_COUNT = 16;
6366
+ const LATITUDE_PATCH_COUNT = 20;
5697
6367
  /**
5698
- * Number of longitude segments used by the continuous Octopus 3D 3 mesh.
6368
+ * Number of longitude segments used by the continuous Octopus 3D 4 mesh.
5699
6369
  *
5700
- * @private helper of `octopus3d3AvatarVisual`
6370
+ * @private helper of `octopus3d4AvatarVisual`
5701
6371
  */
5702
- const LONGITUDE_PATCH_COUNT = 40;
6372
+ const LONGITUDE_PATCH_COUNT = 48;
5703
6373
  /**
5704
- * Resolves visible projected patches for the continuous octopus mesh.
6374
+ * Resolves visible projected patches for the continuous Octopus 3D 4 mesh.
5705
6375
  *
5706
6376
  * Within a single frame, mesh corner samples and longitude-only computations (tentacle
5707
6377
  * influence and lobe wave) are quantized to the patch grid and computed once each rather
5708
- * than re-evaluated for every patch corner — the patch loop alone calls `sampleContinuousOctopusSurfacePoint`
5709
- * for `latitudePatchCount * longitudePatchCount * 4` corners without caching, and each call
5710
- * triggers an inner 8-iteration `Math.exp` loop, which dominates the per-frame cost.
6378
+ * than re-evaluated for every patch corner.
5711
6379
  *
5712
- * @private helper of `octopus3d3AvatarVisual`
6380
+ * @private helper of `octopus3d4AvatarVisual`
5713
6381
  */
5714
- function resolveVisibleContinuousOctopusPatches(options) {
6382
+ function resolveVisibleBlobbyContinuousPatches(options) {
5715
6383
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
5716
6384
  const latitudePatchCount = LATITUDE_PATCH_COUNT;
5717
6385
  const longitudePatchCount = LONGITUDE_PATCH_COUNT;
@@ -5731,13 +6399,13 @@ function resolveVisibleContinuousOctopusPatches(options) {
5731
6399
  const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
5732
6400
  for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
5733
6401
  const cornerLongitude = longitudeBoundaries[boundaryIndex];
5734
- cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveContinuousTentacleInfluence(options, cornerLongitude);
5735
- cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveContinuousLobeWave(options, cornerLongitude);
6402
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveBlobbyTentacleInfluence(options, cornerLongitude);
6403
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveBlobbyLobeWave(options, cornerLongitude);
5736
6404
  }
5737
6405
  for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
5738
6406
  const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
5739
- cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveContinuousTentacleInfluence(options, patchCenterLongitude);
5740
- cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveContinuousLobeWave(options, patchCenterLongitude);
6407
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyTentacleInfluence(options, patchCenterLongitude);
6408
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyLobeWave(options, patchCenterLongitude);
5741
6409
  cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
5742
6410
  }
5743
6411
  const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
@@ -5747,7 +6415,7 @@ function resolveVisibleContinuousOctopusPatches(options) {
5747
6415
  for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
5748
6416
  const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
5749
6417
  const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
5750
- const cornerSample = sampleContinuousOctopusSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
6418
+ const cornerSample = sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
5751
6419
  transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
5752
6420
  }
5753
6421
  }
@@ -5766,7 +6434,7 @@ function resolveVisibleContinuousOctopusPatches(options) {
5766
6434
  transformedCornerSamples[endCornerRowOffset + longitudeIndex],
5767
6435
  ];
5768
6436
  const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
5769
- if (surfaceNormal.z <= 0.008) {
6437
+ if (surfaceNormal.z <= 0.006) {
5770
6438
  continue;
5771
6439
  }
5772
6440
  const projectedCorners = [
@@ -5775,6 +6443,8 @@ function resolveVisibleContinuousOctopusPatches(options) {
5775
6443
  projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
5776
6444
  projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
5777
6445
  ];
6446
+ const lightIntensity = clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1);
6447
+ const rimLightIntensity = Math.pow(clampNumber$1(dotProduct3D(surfaceNormal, RIM_LIGHT_DIRECTION), 0, 1), 2.6);
5778
6448
  surfacePatches.push({
5779
6449
  corners: projectedCorners,
5780
6450
  averageDepth: (transformedCorners[0].z +
@@ -5782,99 +6452,113 @@ function resolveVisibleContinuousOctopusPatches(options) {
5782
6452
  transformedCorners[2].z +
5783
6453
  transformedCorners[3].z) /
5784
6454
  4,
5785
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1),
5786
- fillStyle: resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
5787
- outlineColor: verticalProgress < 0.54 ? `${palette.highlight}69` : `${palette.shadow}78`,
6455
+ lightIntensity,
6456
+ rimLightIntensity,
6457
+ fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
6458
+ outlineColor: verticalProgress < 0.52 ? `${palette.highlight}5c` : `${palette.shadow}66`,
5788
6459
  });
5789
6460
  }
5790
6461
  }
5791
6462
  return surfacePatches;
5792
6463
  }
5793
6464
  /**
5794
- * Samples one point on the continuous Octopus 3D 3 surface.
5795
- *
5796
- * The lower hemisphere is pulled into eight seeded waving lobes, so the portrait reads as
5797
- * tentacled while still being rendered as one connected blobby mesh.
6465
+ * Samples one point on the continuous Octopus 3D 4 surface.
5798
6466
  *
5799
- * @private helper of `octopus3d3AvatarVisual`
6467
+ * @private helper of `octopus3d4AvatarVisual`
5800
6468
  */
5801
- function sampleContinuousOctopusSurfacePoint(options, latitude, longitude) {
5802
- return sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, resolveContinuousTentacleInfluence(options, longitude), resolveContinuousLobeWave(options, longitude));
6469
+ function sampleBlobbyContinuousSurfacePoint(options, latitude, longitude) {
6470
+ return sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, resolveBlobbyTentacleInfluence(options, longitude), resolveBlobbyLobeWave(options, longitude));
5803
6471
  }
5804
6472
  /**
5805
- * Samples one point on the continuous Octopus 3D 3 surface using precomputed longitude-only
5806
- * values to skip the per-call `Math.exp` tentacle-influence loop and the lobe-wave trig call.
6473
+ * Samples one point on the continuous Octopus 3D 4 surface using precomputed longitude-only values.
5807
6474
  *
5808
- * The patch loop quantizes the mesh into a fixed `(latitudePatchCount + 1) * (longitudePatchCount + 1)`
5809
- * corner grid, so the same longitude is reused across every latitude row and each
5810
- * tentacle/lobe value can be computed once per frame instead of `latitudePatchCount * 4` times.
6475
+ * The patch loop quantizes the mesh into a fixed corner grid, so the same longitude is reused
6476
+ * across every latitude row and each tentacle/lobe value is computed once per frame instead
6477
+ * of `latitudePatchCount * 4` times.
5811
6478
  *
5812
- * @private helper of `octopus3d3AvatarVisual`
6479
+ * @private helper of `octopus3d4AvatarVisual`
5813
6480
  */
5814
- function sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
6481
+ function sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
5815
6482
  const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
5816
6483
  const cosineLatitude = Math.max(0, Math.cos(latitude));
5817
6484
  const verticalProgress = (Math.sin(latitude) + 1) / 2;
5818
- const upperBlend = Math.pow(1 - verticalProgress, 1.28);
5819
- const lowerBlend = smoothStep(0.38, 1, verticalProgress);
5820
- const tipBlend = smoothStep(0.68, 1, verticalProgress);
6485
+ const upperBlend = Math.pow(1 - verticalProgress, 1.32);
6486
+ const lowerBlend = smoothStep(0.34, 1, verticalProgress);
6487
+ const tipBlend = smoothStep(0.66, 1, verticalProgress);
5821
6488
  const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
5822
- const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.24 + tipBlend * 0.2);
6489
+ const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.26 + tipBlend * 0.22);
5823
6490
  const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
5824
6491
  animationPhase * 0.6 +
5825
- timeMs / (1750 + morphologyProfile.body.lobeCount * 30)) *
5826
- (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.8) *
5827
- (0.3 + lowerBlend * 0.7);
5828
- const tentacleWave = Math.sin(timeMs / 760 + tentacleInfluence.phase + verticalProgress * 2.4) *
6492
+ timeMs / (1700 + morphologyProfile.body.lobeCount * 28)) *
6493
+ (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.78) *
6494
+ (0.32 + lowerBlend * 0.7);
6495
+ const primaryTentacleWave = Math.sin(timeMs / 740 + tentacleInfluence.primaryPhase + verticalProgress * 2.6) *
5829
6496
  lowerBlend *
5830
6497
  tentacleInfluence.core *
5831
6498
  tentacleInfluence.swayScale;
5832
- const horizontalScale = 1.04 +
6499
+ const secondaryTentacleWave = Math.sin(timeMs / 470 + tentacleInfluence.secondaryPhase + verticalProgress * 4.2) *
6500
+ lowerBlend *
6501
+ tentacleInfluence.core *
6502
+ tentacleInfluence.swayScale *
6503
+ 0.42;
6504
+ const tentacleCurl = Math.sin(timeMs / 1180 + tentacleInfluence.primaryPhase * 0.6 + verticalProgress * 3.2) *
6505
+ tipBlend *
6506
+ tentacleInfluence.core *
6507
+ tentacleInfluence.curlScale *
6508
+ 0.36;
6509
+ const horizontalScale = 1.06 +
5833
6510
  mantleRipple +
5834
- lowerBlend * (0.16 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.1) +
5835
- lowerBlend * tentacleInfluence.core * (0.2 + lowerLobeWave * 0.12) -
6511
+ lowerBlend * (0.18 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.12) +
6512
+ lowerBlend * tentacleInfluence.core * (0.22 + lowerLobeWave * 0.14) -
5836
6513
  upperBlend * 0.08;
5837
- const depthScale = 1.06 +
6514
+ const depthScale = 1.08 +
5838
6515
  upperBlend * 0.16 +
5839
- Math.max(0, Math.cos(effectiveLongitude)) * 0.1 +
5840
- lowerBlend * tentacleInfluence.core * (0.1 + tentacleInfluence.depthScale * 0.06) -
6516
+ Math.max(0, Math.cos(effectiveLongitude)) * 0.12 +
6517
+ lowerBlend * tentacleInfluence.core * (0.12 + tentacleInfluence.depthScale * 0.07) -
5841
6518
  Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
5842
- const tentacleTubeRadius = lowerBlend * tentacleInfluence.core * (0.11 + tipBlend * 0.06 + tentacleInfluence.widthScale * 0.025) * radiusX;
6519
+ const tentacleTubeRadius = lowerBlend *
6520
+ tentacleInfluence.core *
6521
+ (0.12 + tipBlend * 0.07 + tentacleInfluence.widthScale * 0.028) *
6522
+ radiusX;
5843
6523
  const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
5844
- const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.72;
6524
+ const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.74;
5845
6525
  const lowerDrop = lowerBlend *
5846
6526
  radiusY *
5847
- (0.18 +
6527
+ (0.2 +
5848
6528
  tentacleInfluence.core *
5849
- (0.38 +
5850
- tentacleInfluence.lengthScale * 0.22 +
5851
- (morphologyProfile.tentacles.flowLengthScale - 1) * 0.08));
6529
+ (0.42 +
6530
+ tentacleInfluence.lengthScale * 0.24 +
6531
+ (morphologyProfile.tentacles.flowLengthScale - 1) * 0.1));
6532
+ const combinedTentacleSway = (primaryTentacleWave + secondaryTentacleWave) * radiusX * (0.06 + tipBlend * 0.06);
5852
6533
  return {
5853
- x: Math.sin(effectiveLongitude) * planarRadiusX + tentacleWave * radiusX * (0.052 + tipBlend * 0.05),
5854
- y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.12) -
5855
- upperBlend * radiusY * 0.1 +
6534
+ x: Math.sin(effectiveLongitude) * planarRadiusX +
6535
+ combinedTentacleSway +
6536
+ tentacleCurl * radiusX * 0.18,
6537
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.14) -
6538
+ upperBlend * radiusY * 0.12 +
5856
6539
  lowerDrop +
5857
- Math.sin(timeMs / 1420 + animationPhase + latitude * 1.6) * lowerBlend * radiusY * 0.018 +
5858
- Math.cos(timeMs / 880 + tentacleInfluence.phase) *
6540
+ Math.sin(timeMs / 1380 + animationPhase + latitude * 1.5) * lowerBlend * radiusY * 0.022 +
6541
+ Math.cos(timeMs / 820 + tentacleInfluence.primaryPhase) *
5859
6542
  lowerBlend *
5860
6543
  tipBlend *
5861
6544
  tentacleInfluence.core *
5862
6545
  radiusY *
5863
- 0.034,
6546
+ 0.04,
5864
6547
  z: Math.cos(effectiveLongitude) * planarRadiusZ +
5865
- Math.cos(timeMs / 980 + tentacleInfluence.phase + verticalProgress) *
6548
+ Math.cos(timeMs / 960 + tentacleInfluence.primaryPhase + verticalProgress) *
5866
6549
  lowerBlend *
5867
6550
  tentacleInfluence.core *
5868
6551
  radiusZ *
5869
- 0.04,
6552
+ 0.044 +
6553
+ tentacleCurl * radiusZ * 0.14,
5870
6554
  };
5871
6555
  }
5872
6556
  /**
5873
6557
  * Blends nearby seeded tentacle profiles at one mesh longitude.
5874
6558
  *
5875
- * @private helper of `octopus3d3AvatarVisual`
6559
+ * @private helper of `octopus3d4AvatarVisual`
5876
6560
  */
5877
- function resolveContinuousTentacleInfluence(options, longitude) {
6561
+ function resolveBlobbyTentacleInfluence(options, longitude) {
5878
6562
  let totalWeight = 0;
5879
6563
  let weightedSin = 0;
5880
6564
  let weightedCos = 0;
@@ -5882,10 +6566,12 @@ function resolveContinuousTentacleInfluence(options, longitude) {
5882
6566
  let weightedLengthScale = 0;
5883
6567
  let weightedSwayScale = 0;
5884
6568
  let weightedDepthScale = 0;
5885
- let weightedPhase = 0;
6569
+ let weightedCurlScale = 0;
6570
+ let weightedPrimaryPhase = 0;
6571
+ let weightedSecondaryPhase = 0;
5886
6572
  for (const tentacleProfile of options.tentacleProfiles) {
5887
6573
  const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
5888
- const width = 0.2 * tentacleProfile.widthScale;
6574
+ const width = 0.22 * tentacleProfile.widthScale;
5889
6575
  const weight = Math.exp(-(distance * distance) / (width * width));
5890
6576
  totalWeight += weight;
5891
6577
  weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
@@ -5894,7 +6580,9 @@ function resolveContinuousTentacleInfluence(options, longitude) {
5894
6580
  weightedLengthScale += tentacleProfile.lengthScale * weight;
5895
6581
  weightedSwayScale += tentacleProfile.swayScale * weight;
5896
6582
  weightedDepthScale += tentacleProfile.depthScale * weight;
5897
- weightedPhase += tentacleProfile.phase * weight;
6583
+ weightedCurlScale += tentacleProfile.curlScale * weight;
6584
+ weightedPrimaryPhase += tentacleProfile.primaryPhase * weight;
6585
+ weightedSecondaryPhase += tentacleProfile.secondaryPhase * weight;
5898
6586
  }
5899
6587
  if (totalWeight < 0.0001) {
5900
6588
  return {
@@ -5904,7 +6592,9 @@ function resolveContinuousTentacleInfluence(options, longitude) {
5904
6592
  lengthScale: 1,
5905
6593
  swayScale: 1,
5906
6594
  depthScale: 1,
5907
- phase: 0,
6595
+ curlScale: 1,
6596
+ primaryPhase: 0,
6597
+ secondaryPhase: 0,
5908
6598
  };
5909
6599
  }
5910
6600
  return {
@@ -5914,52 +6604,57 @@ function resolveContinuousTentacleInfluence(options, longitude) {
5914
6604
  lengthScale: weightedLengthScale / totalWeight,
5915
6605
  swayScale: weightedSwayScale / totalWeight,
5916
6606
  depthScale: weightedDepthScale / totalWeight,
5917
- phase: weightedPhase / totalWeight,
6607
+ curlScale: weightedCurlScale / totalWeight,
6608
+ primaryPhase: weightedPrimaryPhase / totalWeight,
6609
+ secondaryPhase: weightedSecondaryPhase / totalWeight,
5918
6610
  };
5919
6611
  }
5920
6612
  /**
5921
6613
  * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
5922
6614
  *
5923
- * @private helper of `octopus3d3AvatarVisual`
6615
+ * @private helper of `octopus3d4AvatarVisual`
5924
6616
  */
5925
- function resolveContinuousLobeWave(options, longitude) {
6617
+ function resolveBlobbyLobeWave(options, longitude) {
5926
6618
  const { morphologyProfile, animationPhase, timeMs } = options;
5927
6619
  return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT +
5928
6620
  animationPhase +
5929
- timeMs / (980 + morphologyProfile.body.lobeCount * 18)) +
6621
+ timeMs / (940 + morphologyProfile.body.lobeCount * 18)) +
5930
6622
  1) /
5931
6623
  2);
5932
6624
  }
5933
6625
  /**
5934
6626
  * Resolves one base fill tone for a patch on the continuous octopus mesh.
5935
6627
  *
5936
- * @private helper of `octopus3d3AvatarVisual`
6628
+ * @private helper of `octopus3d4AvatarVisual`
5937
6629
  */
5938
- function resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
6630
+ function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
5939
6631
  const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
5940
- if (tonalProgress < 0.14) {
6632
+ if (tonalProgress < 0.12) {
5941
6633
  return palette.highlight;
5942
6634
  }
5943
- if (tonalProgress < 0.32) {
6635
+ if (tonalProgress < 0.3) {
5944
6636
  return palette.secondary;
5945
6637
  }
5946
6638
  if (tonalProgress < 0.72) {
5947
- return forwardness > 0.55 ? palette.secondary : palette.primary;
6639
+ return forwardness > 0.56 ? palette.secondary : palette.primary;
5948
6640
  }
5949
6641
  return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
5950
6642
  }
5951
6643
  /**
5952
- * Draws one projected mesh patch with soft shading and a subtle edge.
6644
+ * Draws one projected mesh patch with multi-pass shading and a soft edge.
5953
6645
  *
5954
- * @private helper of `octopus3d3AvatarVisual`
6646
+ * @private helper of `octopus3d4AvatarVisual`
5955
6647
  */
5956
- function drawContinuousSurfacePatch(context, surfacePatch) {
6648
+ function drawBlobbyContinuousSurfacePatch(context, surfacePatch) {
5957
6649
  drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
5958
6650
  if (surfacePatch.lightIntensity > 0) {
5959
- drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.18 * surfacePatch.lightIntensity})`);
6651
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.2 * surfacePatch.lightIntensity})`);
5960
6652
  }
5961
6653
  else if (surfacePatch.lightIntensity < 0) {
5962
- drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.25 * Math.abs(surfacePatch.lightIntensity)})`);
6654
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.26 * Math.abs(surfacePatch.lightIntensity)})`);
6655
+ }
6656
+ if (surfacePatch.rimLightIntensity > 0.04) {
6657
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 247, 230, ${0.22 * surfacePatch.rimLightIntensity})`);
5963
6658
  }
5964
6659
  context.save();
5965
6660
  context.beginPath();
@@ -5969,7 +6664,7 @@ function drawContinuousSurfacePatch(context, surfacePatch) {
5969
6664
  }
5970
6665
  context.closePath();
5971
6666
  context.strokeStyle = surfacePatch.outlineColor;
5972
- context.lineWidth = Math.max(0.7, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0032);
6667
+ context.lineWidth = Math.max(0.6, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0026);
5973
6668
  context.lineJoin = 'round';
5974
6669
  context.stroke();
5975
6670
  context.restore();
@@ -5977,9 +6672,9 @@ function drawContinuousSurfacePatch(context, surfacePatch) {
5977
6672
  /**
5978
6673
  * Draws projected mantle-current lines on the front of the mesh.
5979
6674
  *
5980
- * @private helper of `octopus3d3AvatarVisual`
6675
+ * @private helper of `octopus3d4AvatarVisual`
5981
6676
  */
5982
- function drawProjectedSurfaceCurrents(options) {
6677
+ function drawBlobbyContinuousCurrents(options) {
5983
6678
  const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
5984
6679
  const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
5985
6680
  const centerIndex = (currentCount - 1) / 2;
@@ -5989,11 +6684,11 @@ function drawProjectedSurfaceCurrents(options) {
5989
6684
  for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
5990
6685
  const baseLongitude = (currentIndex - centerIndex) * 0.15;
5991
6686
  const projectedPoints = [];
5992
- for (let sampleIndex = 0; sampleIndex < 8; sampleIndex++) {
5993
- const progress = sampleIndex / 7;
5994
- const latitude = -0.46 + progress * 0.74;
5995
- const longitude = baseLongitude + Math.sin(timeMs / 1160 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.035;
5996
- const scenePoint = transformScenePoint(sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
6687
+ for (let sampleIndex = 0; sampleIndex < 9; sampleIndex++) {
6688
+ const progress = sampleIndex / 8;
6689
+ const latitude = -0.48 + progress * 0.78;
6690
+ const longitude = baseLongitude + Math.sin(timeMs / 1140 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.038;
6691
+ const scenePoint = transformScenePoint(sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
5997
6692
  if (scenePoint.z > center.z - size * 0.016) {
5998
6693
  projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
5999
6694
  }
@@ -6015,38 +6710,61 @@ function drawProjectedSurfaceCurrents(options) {
6015
6710
  /**
6016
6711
  * Draws small projected sucker highlights on the waving lower mesh lobes.
6017
6712
  *
6018
- * @private helper of `octopus3d3AvatarVisual`
6713
+ * @private helper of `octopus3d4AvatarVisual`
6019
6714
  */
6020
- function drawProjectedTentacleSuckers(options) {
6715
+ function drawBlobbyContinuousSuckers(options) {
6021
6716
  const { surfaceOptions, size } = options;
6022
6717
  const { timeMs } = surfaceOptions;
6023
6718
  for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
6024
- if (Math.cos(tentacleProfile.centerLongitude) < -0.12) {
6719
+ if (Math.cos(tentacleProfile.centerLongitude) < -0.16) {
6025
6720
  continue;
6026
6721
  }
6027
- for (let suckerIndex = 0; suckerIndex < 3; suckerIndex++) {
6028
- const latitude = 0.52 + suckerIndex * 0.14;
6029
- const sideOffset = tentacleProfile.suckerSide * (0.035 + suckerIndex * 0.012) * tentacleProfile.widthScale;
6030
- const waveOffset = Math.sin(timeMs / 900 + tentacleProfile.phase + suckerIndex * 0.8) * 0.018;
6031
- drawProjectedSurfaceSpot({
6722
+ for (let suckerIndex = 0; suckerIndex < 4; suckerIndex++) {
6723
+ const latitude = 0.5 + suckerIndex * 0.12;
6724
+ const sideOffset = tentacleProfile.suckerSide * (0.038 + suckerIndex * 0.014) * tentacleProfile.widthScale;
6725
+ const waveOffset = Math.sin(timeMs / 880 + tentacleProfile.primaryPhase + suckerIndex * 0.78) * 0.02;
6726
+ drawBlobbyContinuousSurfaceSpot({
6032
6727
  ...options,
6033
6728
  latitude,
6034
6729
  longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
6035
- radiusScale: size * (0.0065 - suckerIndex * 0.0007),
6730
+ radiusScale: size * (0.0068 - suckerIndex * 0.0006),
6036
6731
  });
6037
6732
  }
6038
6733
  }
6039
6734
  }
6735
+ /**
6736
+ * Draws seeded pigment spots across the upper mesh for a richer skin texture.
6737
+ *
6738
+ * @private helper of `octopus3d4AvatarVisual`
6739
+ */
6740
+ function drawBlobbySkinSpots(options) {
6741
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, skinSpots, } = options;
6742
+ for (const skinSpot of skinSpots) {
6743
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, skinSpot.latitude, skinSpot.longitude);
6744
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
6745
+ if (sceneCenterPoint.z <= center.z - size * 0.01) {
6746
+ continue;
6747
+ }
6748
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
6749
+ const spotRadius = size * skinSpot.radiusScale;
6750
+ context.save();
6751
+ context.beginPath();
6752
+ context.arc(projectedCenterPoint.x, projectedCenterPoint.y, spotRadius, 0, Math.PI * 2);
6753
+ context.fillStyle = `${palette.shadow}${formatAlphaHex(skinSpot.opacity)}`;
6754
+ context.fill();
6755
+ context.restore();
6756
+ }
6757
+ }
6040
6758
  /**
6041
6759
  * Draws one tiny projected surface spot by sampling local mesh tangents.
6042
6760
  *
6043
- * @private helper of `octopus3d3AvatarVisual`
6761
+ * @private helper of `octopus3d4AvatarVisual`
6044
6762
  */
6045
- function drawProjectedSurfaceSpot(options) {
6763
+ function drawBlobbyContinuousSurfaceSpot(options) {
6046
6764
  const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
6047
- const localCenter = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude);
6048
- const localHorizontal = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
6049
- const localVertical = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
6765
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude);
6766
+ const localHorizontal = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
6767
+ const localVertical = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
6050
6768
  const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
6051
6769
  if (sceneCenterPoint.z <= center.z - size * 0.012) {
6052
6770
  return;
@@ -6056,27 +6774,55 @@ function drawProjectedSurfaceSpot(options) {
6056
6774
  const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
6057
6775
  const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
6058
6776
  radiusScale *
6059
- 0.74, size * 0.003, size * 0.018);
6777
+ 0.78, size * 0.003, size * 0.02);
6060
6778
  const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
6061
6779
  radiusScale *
6062
- 0.52, size * 0.0024, size * 0.014);
6780
+ 0.54, size * 0.0024, size * 0.015);
6063
6781
  const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
6064
6782
  context.save();
6065
6783
  context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
6066
6784
  context.rotate(rotation);
6067
6785
  context.beginPath();
6068
6786
  context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
6069
- context.fillStyle = `${palette.highlight}73`;
6787
+ context.fillStyle = `${palette.highlight}80`;
6070
6788
  context.fill();
6071
- context.strokeStyle = `${palette.highlight}99`;
6789
+ context.strokeStyle = `${palette.highlight}a8`;
6072
6790
  context.lineWidth = Math.max(0.7, size * 0.0028);
6073
6791
  context.stroke();
6074
6792
  context.restore();
6075
6793
  }
6794
+ /**
6795
+ * Draws a soft, slowly drifting gloss highlight on the front of the mesh.
6796
+ *
6797
+ * @private helper of `octopus3d4AvatarVisual`
6798
+ */
6799
+ function drawBlobbyContinuousGloss(options) {
6800
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size } = options;
6801
+ const { timeMs, animationPhase } = surfaceOptions;
6802
+ const glossLatitude = -0.3 + Math.sin(timeMs / 2700 + animationPhase) * 0.04;
6803
+ const glossLongitude = -0.18 + Math.cos(timeMs / 2300 + animationPhase * 0.8) * 0.05;
6804
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, glossLatitude, glossLongitude);
6805
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
6806
+ if (sceneCenterPoint.z <= center.z) {
6807
+ return;
6808
+ }
6809
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
6810
+ const glossRadius = size * 0.058;
6811
+ context.save();
6812
+ const glossGradient = context.createRadialGradient(projectedCenterPoint.x - glossRadius * 0.3, projectedCenterPoint.y - glossRadius * 0.4, glossRadius * 0.04, projectedCenterPoint.x, projectedCenterPoint.y, glossRadius);
6813
+ glossGradient.addColorStop(0, 'rgba(255, 255, 255, 0.34)');
6814
+ glossGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
6815
+ glossGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
6816
+ context.fillStyle = glossGradient;
6817
+ context.beginPath();
6818
+ context.ellipse(projectedCenterPoint.x, projectedCenterPoint.y, glossRadius * 1.05, glossRadius * 0.78, 0, 0, Math.PI * 2);
6819
+ context.fill();
6820
+ context.restore();
6821
+ }
6076
6822
  /**
6077
6823
  * Resolves a signed angular distance from the source longitude to the target longitude.
6078
6824
  *
6079
- * @private helper of `octopus3d3AvatarVisual`
6825
+ * @private helper of `octopus3d4AvatarVisual`
6080
6826
  */
6081
6827
  function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
6082
6828
  return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
@@ -6084,12 +6830,22 @@ function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
6084
6830
  /**
6085
6831
  * Smoothly maps a value between two bounds into `[0, 1]`.
6086
6832
  *
6087
- * @private helper of `octopus3d3AvatarVisual`
6833
+ * @private helper of `octopus3d4AvatarVisual`
6088
6834
  */
6089
6835
  function smoothStep(edgeStart, edgeEnd, value) {
6090
6836
  const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
6091
6837
  return progress * progress * (3 - 2 * progress);
6092
6838
  }
6839
+ /**
6840
+ * Converts an opacity ratio into a two-digit hexadecimal alpha suffix.
6841
+ *
6842
+ * @private helper of `octopus3d4AvatarVisual`
6843
+ */
6844
+ function formatAlphaHex(opacity) {
6845
+ return Math.round(clampNumber$1(opacity, 0, 1) * 255)
6846
+ .toString(16)
6847
+ .padStart(2, '0');
6848
+ }
6093
6849
 
6094
6850
  /* eslint-disable no-magic-numbers */
6095
6851
  /**
@@ -6862,6 +7618,7 @@ const AVATAR_VISUALS = [
6862
7618
  octopus3dAvatarVisual,
6863
7619
  octopus3d2AvatarVisual,
6864
7620
  octopus3d3AvatarVisual,
7621
+ octopus3d4AvatarVisual,
6865
7622
  asciiOctopusAvatarVisual,
6866
7623
  minecraftAvatarVisual,
6867
7624
  minecraft2AvatarVisual,
@@ -6943,7 +7700,7 @@ function generatePlaceholderAgentProfileImageUrl(agentIdOrName, agentsServerUrl)
6943
7700
  *
6944
7701
  * @private shared avatar contract
6945
7702
  */
6946
- const DEFAULT_AGENT_AVATAR_VISUAL_ID = 'octopus3d3';
7703
+ const DEFAULT_AGENT_AVATAR_VISUAL_ID = 'octopus3d4';
6947
7704
  /**
6948
7705
  * Resolves the avatar visual preferred by an agent, then falls back to a federated server default
6949
7706
  * and finally to the caller/server default.
@@ -9314,6 +10071,11 @@ function isVoidPseudoAgentReference(rawReference) {
9314
10071
  * - `isValidAgentUrl` *(this one)* which tests just agent URL
9315
10072
  * - `isValidPipelineUrl` which tests just pipeline URL
9316
10073
  *
10074
+ * Note: This is a pure structural validator and does not block private/internal network
10075
+ * addresses. Callers that fetch the URL server-side from an untrusted network context must
10076
+ * additionally apply the `assertSafeUrl` SSRF guard from the Agents Server before any
10077
+ * outbound request.
10078
+ *
9317
10079
  * @public exported from `@promptbook/utils`
9318
10080
  */
9319
10081
  function isValidAgentUrl(url) {
@@ -9327,12 +10089,6 @@ function isValidAgentUrl(url) {
9327
10089
  // TODO: [🐠]
9328
10090
  return false;
9329
10091
  }
9330
- /*
9331
- Note: [👣][🧠] Is it secure to allow pipeline URLs on private and unsecured networks?
9332
- if (isUrlOnPrivateNetwork(url)) {
9333
- return false;
9334
- }
9335
- */
9336
10092
  return true;
9337
10093
  }
9338
10094
  // TODO: [🐠] Maybe more info why the URL is invalid
@@ -21922,19 +22678,6 @@ var css_248z$d = ".BookEditor-module_BookEditor__s-0PU{width:100%}.BookEditor-mo
21922
22678
  var styles$d = {"BookEditor":"BookEditor-module_BookEditor__s-0PU","bookEditorContainer":"BookEditor-module_bookEditorContainer__wLMwM","isVerbose":"BookEditor-module_isVerbose__VQ6iL","bookEditorWrapper":"BookEditor-module_bookEditorWrapper__twppD","isBorderRadiusDisabled":"BookEditor-module_isBorderRadiusDisabled__h1I3v","dropOverlay":"BookEditor-module_dropOverlay__xWWoX","bookEditorActionbar":"BookEditor-module_bookEditorActionbar__KW6dc","fullscreen":"BookEditor-module_fullscreen__rktsl","button":"BookEditor-module_button__hS390","savedNotification":"BookEditor-module_savedNotification__OiX9L","fadeOut":"BookEditor-module_fadeOut__q8JnR","uploadPanel":"BookEditor-module_uploadPanel__2JJtD","uploadPanelHeader":"BookEditor-module_uploadPanelHeader__pdJd2","uploadPanelTitle":"BookEditor-module_uploadPanelTitle__TJIVF","uploadPanelHeaderMeta":"BookEditor-module_uploadPanelHeaderMeta__Xw0uI","uploadPanelSummary":"BookEditor-module_uploadPanelSummary__rwSbG","uploadRowStatus":"BookEditor-module_uploadRowStatus__jsUb-","uploadRowMeta":"BookEditor-module_uploadRowMeta__1lz9h","uploadPanelProgressBar":"BookEditor-module_uploadPanelProgressBar__a6pjf","uploadPanelProgressFill":"BookEditor-module_uploadPanelProgressFill__l-TKR","uploadRowProgressBar":"BookEditor-module_uploadRowProgressBar__NoBA7","uploadPanelList":"BookEditor-module_uploadPanelList__VxEd5","uploadRow":"BookEditor-module_uploadRow__QiSFg","uploadRowHeader":"BookEditor-module_uploadRowHeader__po0j5","uploadRowName":"BookEditor-module_uploadRowName__doQRO","uploadRowProgressFill":"BookEditor-module_uploadRowProgressFill__TrP7e","uploadRowActions":"BookEditor-module_uploadRowActions__5Y1Mq","uploadActionButton":"BookEditor-module_uploadActionButton__CqJrr","uploadRowError":"BookEditor-module_uploadRowError__eEHWw","aboutPromptbookInformation":"BookEditor-module_aboutPromptbookInformation__eiBL0"};
21923
22679
  styleInject(css_248z$d);
21924
22680
 
21925
- /**
21926
- * Core Promptbook server configuration
21927
- *
21928
- * Used for "Adam" agent which is built in as default ancestor for new agents and other well known agents
21929
- *
21930
- * @public exported from `@promptbook/core`
21931
- */
21932
- const CORE_AGENTS_SERVER = {
21933
- title: 'Promptbook Core',
21934
- description: `Core Promptbook server used for Adam agent which is built in as default ancestor for new agents and other well known agents.`,
21935
- owner: PROMPTBOOK_LEGAL_ENTITY,
21936
- url: 'https://core.ptbk.io/',
21937
- };
21938
22681
  // <- TODO: [🆎] Allow to override (set) well-known agent names via Metadata
21939
22682
  /**
21940
22683
  * Available agents servers for the Promptbook
@@ -21944,7 +22687,6 @@ const CORE_AGENTS_SERVER = {
21944
22687
  * @public exported from `@promptbook/core`
21945
22688
  */
21946
22689
  const PUBLIC_AGENTS_SERVERS = [
21947
- CORE_AGENTS_SERVER,
21948
22690
  {
21949
22691
  title: 'Promptbook Gallery',
21950
22692
  description: `Gallery of ideas, AI professions,... like AI Agenta photobank.`,