@promptbook/wizard 0.112.0-117 → 0.112.0-118

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
@@ -37,7 +37,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
37
37
  * @generated
38
38
  * @see https://github.com/webgptorg/promptbook
39
39
  */
40
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-117';
40
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-118';
41
41
  /**
42
42
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
43
43
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -22093,21 +22093,22 @@ function getPointBounds(points) {
22093
22093
  * @private helper of `fractalAvatarVisual`
22094
22094
  */
22095
22095
  function drawDragonCurveLayer(context, points, options) {
22096
- const { size, primaryColor, secondaryColor, tertiaryColor, shadowColor, strokeWidth, timeMs, layerIndex } = options;
22096
+ const { primaryColor, secondaryColor, tertiaryColor, shadowColor, strokeWidth, timeMs, layerIndex } = options;
22097
22097
  const firstPoint = points[0];
22098
22098
  const lastPoint = points[points.length - 1];
22099
22099
  const ribbonGradient = context.createLinearGradient(firstPoint.x, firstPoint.y, lastPoint.x, lastPoint.y);
22100
22100
  ribbonGradient.addColorStop(0, `${primaryColor}f2`);
22101
22101
  ribbonGradient.addColorStop(0.5, `${secondaryColor}e6`);
22102
22102
  ribbonGradient.addColorStop(1, `${tertiaryColor}f2`);
22103
+ // Approximate the blurred shadow stroke with a wider semi-transparent stroke instead of
22104
+ // context.filter blur, which triggers a costly software rasterization pass every frame.
22103
22105
  context.save();
22104
22106
  context.beginPath();
22105
22107
  tracePolyline(context, points);
22106
- context.strokeStyle = `${shadowColor}82`;
22107
- context.lineWidth = strokeWidth * 1.8;
22108
+ context.strokeStyle = `${shadowColor}48`;
22109
+ context.lineWidth = strokeWidth * 4.5;
22108
22110
  context.lineJoin = 'round';
22109
22111
  context.lineCap = 'round';
22110
- context.filter = `blur(${size * 0.022}px)`;
22111
22112
  context.stroke();
22112
22113
  context.restore();
22113
22114
  context.beginPath();
@@ -22691,11 +22692,23 @@ function drawMinecraftBackdrop(context, size, palette, sceneCenterX, spotlightY,
22691
22692
  * @private helper of `minecraft2AvatarVisual`
22692
22693
  */
22693
22694
  function drawMinecraftShadow(context, size, palette, interaction, timeMs) {
22695
+ const cx = size * 0.5 + interaction.gazeX * size * 0.03;
22696
+ const cy = size * 0.85 + Math.sin(timeMs / 880) * size * 0.01;
22697
+ const rx = size * (0.16 + interaction.intensity * 0.015);
22698
+ const ry = size * 0.055;
22699
+ // Radial gradient approximates the blurry ellipse shadow without context.filter blur.
22694
22700
  context.save();
22695
- context.fillStyle = `${palette.shadow}66`;
22696
- context.filter = `blur(${size * 0.02}px)`;
22701
+ context.translate(cx, cy);
22702
+ context.scale(1, ry / rx);
22703
+ const blurRadius = rx * 1.4;
22704
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
22705
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
22706
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
22707
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
22708
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
22709
+ context.fillStyle = shadowGradient;
22697
22710
  context.beginPath();
22698
- context.ellipse(size * 0.5 + interaction.gazeX * size * 0.03, size * 0.85 + Math.sin(timeMs / 880) * size * 0.01, size * (0.16 + interaction.intensity * 0.015), size * 0.055, 0, 0, Math.PI * 2);
22711
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
22699
22712
  context.fill();
22700
22713
  context.restore();
22701
22714
  }
@@ -22919,13 +22932,27 @@ const minecraftAvatarVisual = {
22919
22932
  spotlight.addColorStop(1, `${palette.highlight}00`);
22920
22933
  context.fillStyle = spotlight;
22921
22934
  context.fillRect(0, 0, size, size);
22922
- context.save();
22923
- context.fillStyle = 'rgba(0, 0, 0, 0.22)';
22924
- context.filter = `blur(${size * 0.018}px)`;
22925
- context.beginPath();
22926
- context.ellipse(size * 0.5, size * 0.86, size * 0.2, size * 0.06, 0, 0, Math.PI * 2);
22927
- context.fill();
22928
- context.restore();
22935
+ {
22936
+ // Radial gradient approximates the blurry ellipse shadow without context.filter blur.
22937
+ const cx = size * 0.5;
22938
+ const cy = size * 0.86;
22939
+ const rx = size * 0.2;
22940
+ const ry = size * 0.06;
22941
+ const blurRadius = rx * 1.4;
22942
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
22943
+ shadowGradient.addColorStop(0, 'rgba(0,0,0,0.28)');
22944
+ shadowGradient.addColorStop(0.45, 'rgba(0,0,0,0.14)');
22945
+ shadowGradient.addColorStop(0.8, 'rgba(0,0,0,0.05)');
22946
+ shadowGradient.addColorStop(1, 'rgba(0,0,0,0)');
22947
+ context.save();
22948
+ context.translate(cx, cy);
22949
+ context.scale(1, ry / rx);
22950
+ context.fillStyle = shadowGradient;
22951
+ context.beginPath();
22952
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
22953
+ context.fill();
22954
+ context.restore();
22955
+ }
22929
22956
  drawVoxelCuboid(context, {
22930
22957
  x: bodyX,
22931
22958
  y: bodyY,
@@ -23977,6 +24004,35 @@ const LIGHT_DIRECTION$2 = normalizeVector3({
23977
24004
  y: -0.62,
23978
24005
  z: 0.94,
23979
24006
  });
24007
+ /**
24008
+ * Cache keyed by the `createRandom` factory reference (stable per mounted `<Avatar/>`).
24009
+ *
24010
+ * @private helper of `octopus3dAvatarVisual`
24011
+ */
24012
+ const octopus3dStableStateCache = new WeakMap();
24013
+ /**
24014
+ * Returns the stable per-avatar state, computing it on first access and caching for subsequent frames.
24015
+ *
24016
+ * @private helper of `octopus3dAvatarVisual`
24017
+ */
24018
+ function getOctopus3dStableState(createRandom) {
24019
+ const cached = octopus3dStableStateCache.get(createRandom);
24020
+ if (cached !== undefined) {
24021
+ return cached;
24022
+ }
24023
+ const animationRandom = createRandom('octopus3d-animation-profile');
24024
+ const eyeRandom = createRandom('octopus3d-eye-profile');
24025
+ const leftEyePhaseOffset = eyeRandom() * 0.6;
24026
+ const rightEyePhaseOffset = eyeRandom() * 0.6;
24027
+ const state = {
24028
+ morphologyProfile: createOctopus3MorphologyProfile(createRandom),
24029
+ animationPhase: animationRandom() * Math.PI * 2,
24030
+ leftEyePhaseOffset,
24031
+ rightEyePhaseOffset,
24032
+ };
24033
+ octopus3dStableStateCache.set(createRandom, state);
24034
+ return state;
24035
+ }
23980
24036
  /**
23981
24037
  * Proper 3D Octopus visual built from projected organic meshes and tentacles.
23982
24038
  *
@@ -23989,10 +24045,7 @@ const octopus3dAvatarVisual = {
23989
24045
  isAnimated: true,
23990
24046
  supportsPointerTracking: true,
23991
24047
  render({ context, size, palette, createRandom, timeMs, interaction }) {
23992
- const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
23993
- const animationRandom = createRandom('octopus3d-animation-profile');
23994
- const eyeRandom = createRandom('octopus3d-eye-profile');
23995
- const animationPhase = animationRandom() * Math.PI * 2;
24048
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset } = getOctopus3dStableState(createRandom);
23996
24049
  const sceneCenterX = size * 0.5;
23997
24050
  const sceneCenterY = size * 0.56;
23998
24051
  const bob = Math.sin(timeMs / 920 + animationPhase) * size * 0.014;
@@ -24089,12 +24142,12 @@ const octopus3dAvatarVisual = {
24089
24142
  x: -faceEyeSpacing,
24090
24143
  y: faceEyeYOffset,
24091
24144
  z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, -faceEyeSpacing, faceEyeYOffset),
24092
- }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + eyeRandom() * 0.6, interaction, morphologyProfile.face.eyeStyle);
24145
+ }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
24093
24146
  drawProjectedOrganicEye(context, {
24094
24147
  x: faceEyeSpacing,
24095
24148
  y: faceEyeYOffset,
24096
24149
  z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, faceEyeSpacing, faceEyeYOffset),
24097
- }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.7 + eyeRandom() * 0.6, interaction, morphologyProfile.face.eyeStyle);
24150
+ }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.7 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
24098
24151
  drawProjectedOrganicMouth(context, [
24099
24152
  {
24100
24153
  x: -mouthHalfWidth,
@@ -24138,14 +24191,28 @@ function drawOctopus3dAtmosphere(context, size, palette, sceneCenterX, sceneCent
24138
24191
  /**
24139
24192
  * Draws the soft ground shadow below the octopus.
24140
24193
  *
24194
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
24195
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
24196
+ *
24141
24197
  * @private helper of `octopus3dAvatarVisual`
24142
24198
  */
24143
24199
  function drawOctopus3dShadow(context, size, palette, interaction, timeMs) {
24200
+ const cx = size * 0.5 + interaction.gazeX * size * 0.04;
24201
+ const cy = size * 0.87 + Math.sin(timeMs / 920) * size * 0.008;
24202
+ const rx = size * (0.18 + interaction.intensity * 0.02);
24203
+ const ry = size * 0.06;
24144
24204
  context.save();
24145
- context.fillStyle = `${palette.shadow}66`;
24146
- context.filter = `blur(${size * 0.022}px)`;
24205
+ context.translate(cx, cy);
24206
+ context.scale(1, ry / rx);
24207
+ const blurRadius = rx * 1.4;
24208
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
24209
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
24210
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
24211
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
24212
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
24213
+ context.fillStyle = shadowGradient;
24147
24214
  context.beginPath();
24148
- context.ellipse(size * 0.5 + interaction.gazeX * size * 0.04, size * 0.87 + Math.sin(timeMs / 920) * size * 0.008, size * (0.18 + interaction.intensity * 0.02), size * 0.06, 0, 0, Math.PI * 2);
24215
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
24149
24216
  context.fill();
24150
24217
  context.restore();
24151
24218
  }
@@ -24376,6 +24443,35 @@ const LIGHT_DIRECTION$1 = normalizeVector3({
24376
24443
  y: -0.6,
24377
24444
  z: 0.98,
24378
24445
  });
24446
+ /**
24447
+ * Cache keyed by the `createRandom` factory reference (stable per mounted `<Avatar/>`).
24448
+ *
24449
+ * @private helper of `octopus3d2AvatarVisual`
24450
+ */
24451
+ const octopus3d2StableStateCache = new WeakMap();
24452
+ /**
24453
+ * Returns the stable per-avatar state, computing it on first access and caching for subsequent frames.
24454
+ *
24455
+ * @private helper of `octopus3d2AvatarVisual`
24456
+ */
24457
+ function getOctopus3d2StableState(createRandom) {
24458
+ const cached = octopus3d2StableStateCache.get(createRandom);
24459
+ if (cached !== undefined) {
24460
+ return cached;
24461
+ }
24462
+ const animationRandom = createRandom('octopus3d2-animation-profile');
24463
+ const eyeRandom = createRandom('octopus3d2-eye-profile');
24464
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
24465
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
24466
+ const state = {
24467
+ morphologyProfile: createOctopus3MorphologyProfile(createRandom),
24468
+ animationPhase: animationRandom() * Math.PI * 2,
24469
+ leftEyePhaseOffset,
24470
+ rightEyePhaseOffset,
24471
+ };
24472
+ octopus3d2StableStateCache.set(createRandom, state);
24473
+ return state;
24474
+ }
24379
24475
  /**
24380
24476
  * Octopus 3D 2 avatar visual.
24381
24477
  *
@@ -24388,10 +24484,7 @@ const octopus3d2AvatarVisual = {
24388
24484
  isAnimated: true,
24389
24485
  supportsPointerTracking: true,
24390
24486
  render({ context, size, palette, createRandom, timeMs, interaction }) {
24391
- const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
24392
- const animationRandom = createRandom('octopus3d2-animation-profile');
24393
- const eyeRandom = createRandom('octopus3d2-eye-profile');
24394
- const animationPhase = animationRandom() * Math.PI * 2;
24487
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset } = getOctopus3d2StableState(createRandom);
24395
24488
  const sceneCenterX = size * 0.5;
24396
24489
  const sceneCenterY = size * 0.575;
24397
24490
  const bob = Math.sin(timeMs / 940 + animationPhase) * size * 0.013;
@@ -24444,8 +24537,8 @@ const octopus3d2AvatarVisual = {
24444
24537
  const rightEyeLocalCenter = sampleBlobbyOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude);
24445
24538
  const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
24446
24539
  const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
24447
- drawProjectedOrganicEye(context, leftEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + eyeRandom() * 0.7, interaction, morphologyProfile.face.eyeStyle);
24448
- drawProjectedOrganicEye(context, rightEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.9 + eyeRandom() * 0.7, interaction, morphologyProfile.face.eyeStyle);
24540
+ drawProjectedOrganicEye(context, leftEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
24541
+ drawProjectedOrganicEye(context, rightEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.9 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
24449
24542
  drawProjectedOrganicMouth(context, [
24450
24543
  sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
24451
24544
  sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
@@ -24474,14 +24567,28 @@ function drawBlobbyOctopusAtmosphere(context, size, palette, sceneCenterX, scene
24474
24567
  /**
24475
24568
  * Draws the soft floor shadow that anchors the single mesh in the frame.
24476
24569
  *
24570
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
24571
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
24572
+ *
24477
24573
  * @private helper of `octopus3d2AvatarVisual`
24478
24574
  */
24479
24575
  function drawBlobbyOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
24576
+ const cx = size * 0.5 + interaction.gazeX * size * 0.045;
24577
+ const cy = size * 0.88 + Math.sin(timeMs / 940) * size * 0.008;
24578
+ const rx = size * (0.18 + (morphologyProfile.body.horizontalStretch - 1) * 0.04 + interaction.intensity * 0.018);
24579
+ const ry = size * 0.062;
24480
24580
  context.save();
24481
- context.fillStyle = `${palette.shadow}66`;
24482
- context.filter = `blur(${size * 0.024}px)`;
24581
+ context.translate(cx, cy);
24582
+ context.scale(1, ry / rx);
24583
+ const blurRadius = rx * 1.4;
24584
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
24585
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
24586
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
24587
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
24588
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
24589
+ context.fillStyle = shadowGradient;
24483
24590
  context.beginPath();
24484
- context.ellipse(size * 0.5 + interaction.gazeX * size * 0.045, size * 0.88 + Math.sin(timeMs / 940) * size * 0.008, size * (0.18 + (morphologyProfile.body.horizontalStretch - 1) * 0.04 + interaction.intensity * 0.018), size * 0.062, 0, 0, Math.PI * 2);
24591
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
24485
24592
  context.fill();
24486
24593
  context.restore();
24487
24594
  }
@@ -24637,6 +24744,40 @@ const LIGHT_DIRECTION = normalizeVector3({
24637
24744
  * @private helper of `octopus3d3AvatarVisual`
24638
24745
  */
24639
24746
  const OCTOPUS_TENTACLE_COUNT = 8;
24747
+ /**
24748
+ * Cache keyed by the `createRandom` factory reference, which is stable for the lifetime of one
24749
+ * mounted `<Avatar/>` component (created inside `resolveAvatarRenderDefinition` and held in a
24750
+ * React `useMemo`). Using a `WeakMap` ensures the entry is collected when the component unmounts.
24751
+ *
24752
+ * @private helper of `octopus3d3AvatarVisual`
24753
+ */
24754
+ const stableStateCache = new WeakMap();
24755
+ /**
24756
+ * Returns the stable per-avatar state, computing it on first access and returning the cached
24757
+ * result on every subsequent call within the same `<Avatar/>` mount.
24758
+ *
24759
+ * @private helper of `octopus3d3AvatarVisual`
24760
+ */
24761
+ function getOctopus3d3StableState(createRandom) {
24762
+ const cached = stableStateCache.get(createRandom);
24763
+ if (cached !== undefined) {
24764
+ return cached;
24765
+ }
24766
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
24767
+ const animationRandom = createRandom('octopus3d3-animation-profile');
24768
+ const eyeRandom = createRandom('octopus3d3-eye-profile');
24769
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
24770
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
24771
+ const state = {
24772
+ morphologyProfile,
24773
+ animationPhase: animationRandom() * Math.PI * 2,
24774
+ leftEyePhaseOffset,
24775
+ rightEyePhaseOffset,
24776
+ tentacleProfiles: createContinuousTentacleProfiles(createRandom, morphologyProfile),
24777
+ };
24778
+ stableStateCache.set(createRandom, state);
24779
+ return state;
24780
+ }
24640
24781
  /**
24641
24782
  * Octopus 3D 3 avatar visual.
24642
24783
  *
@@ -24649,11 +24790,7 @@ const octopus3d3AvatarVisual = {
24649
24790
  isAnimated: true,
24650
24791
  supportsPointerTracking: true,
24651
24792
  render({ context, size, palette, createRandom, timeMs, interaction }) {
24652
- const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
24653
- const animationRandom = createRandom('octopus3d3-animation-profile');
24654
- const eyeRandom = createRandom('octopus3d3-eye-profile');
24655
- const animationPhase = animationRandom() * Math.PI * 2;
24656
- const tentacleProfiles = createContinuousTentacleProfiles(createRandom, morphologyProfile);
24793
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles } = getOctopus3d3StableState(createRandom);
24657
24794
  const sceneCenterX = size * 0.5;
24658
24795
  const sceneCenterY = size * 0.535;
24659
24796
  const bob = Math.sin(timeMs / 960 + animationPhase) * size * 0.012;
@@ -24730,8 +24867,8 @@ const octopus3d3AvatarVisual = {
24730
24867
  size,
24731
24868
  palette,
24732
24869
  });
24733
- drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + eyeRandom() * 0.7, interaction, morphologyProfile.face.eyeStyle);
24734
- drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + eyeRandom() * 0.7, interaction, morphologyProfile.face.eyeStyle);
24870
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
24871
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
24735
24872
  drawProjectedOrganicMouth(context, [
24736
24873
  sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
24737
24874
  sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
@@ -24782,14 +24919,30 @@ function drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, s
24782
24919
  /**
24783
24920
  * Draws the soft lower shadow that anchors the octopus in the avatar frame.
24784
24921
  *
24922
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
24923
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
24924
+ *
24785
24925
  * @private helper of `octopus3d3AvatarVisual`
24786
24926
  */
24787
24927
  function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
24928
+ const cx = size * 0.5 + interaction.gazeX * size * 0.045;
24929
+ const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
24930
+ const rx = size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02);
24931
+ const ry = size * 0.06;
24932
+ // Scale the context so that drawing a circle produces the correct ellipse aspect ratio,
24933
+ // then fill with a radial gradient that approximates the blurry edge without context.filter.
24788
24934
  context.save();
24789
- context.fillStyle = `${palette.shadow}66`;
24790
- context.filter = `blur(${size * 0.025}px)`;
24935
+ context.translate(cx, cy);
24936
+ context.scale(1, ry / rx);
24937
+ const blurRadius = rx * 1.4;
24938
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
24939
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
24940
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
24941
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
24942
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
24943
+ context.fillStyle = shadowGradient;
24791
24944
  context.beginPath();
24792
- context.ellipse(size * 0.5 + interaction.gazeX * size * 0.045, size * 0.9 + Math.sin(timeMs / 980) * size * 0.007, size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02), size * 0.06, 0, 0, Math.PI * 2);
24945
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
24793
24946
  context.fill();
24794
24947
  context.restore();
24795
24948
  }