@promptbook/browser 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.
@@ -11,6 +11,11 @@ import type { CliAgentThinkingLevel } from '../book-3.0/CliAgent';
11
11
  import type { CliAgentRunOptions } from '../book-3.0/CliAgent';
12
12
  import type { CliAgentOptions } from '../book-3.0/CliAgent';
13
13
  import { CliAgent } from '../book-3.0/CliAgent';
14
+ import { CLI_AGENT_HARNESS_NAMES } from '../book-3.0/cliAgentEnv';
15
+ import { CLI_AGENT_THINKING_LEVEL_VALUES } from '../book-3.0/cliAgentEnv';
16
+ import { PTBK_HARNESS_ENV } from '../book-3.0/cliAgentEnv';
17
+ import { PTBK_MODEL_ENV } from '../book-3.0/cliAgentEnv';
18
+ import { PTBK_THINKING_LEVEL_ENV } from '../book-3.0/cliAgentEnv';
14
19
  import type { LiteAgentOptions } from '../book-3.0/LiteAgent';
15
20
  import type { LiteAgentRunOptions } from '../book-3.0/LiteAgent';
16
21
  import { LiteAgent } from '../book-3.0/LiteAgent';
@@ -39,6 +44,11 @@ export type { CliAgentThinkingLevel };
39
44
  export type { CliAgentRunOptions };
40
45
  export type { CliAgentOptions };
41
46
  export { CliAgent };
47
+ export { CLI_AGENT_HARNESS_NAMES };
48
+ export { CLI_AGENT_THINKING_LEVEL_VALUES };
49
+ export { PTBK_HARNESS_ENV };
50
+ export { PTBK_MODEL_ENV };
51
+ export { PTBK_THINKING_LEVEL_ENV };
42
52
  export type { LiteAgentOptions };
43
53
  export type { LiteAgentRunOptions };
44
54
  export { LiteAgent };
@@ -15,7 +15,7 @@ export type BookNodeAgentSource = Book | string_book;
15
15
  */
16
16
  export type BookNodeAgentSourceOptions = {
17
17
  readonly agentPath?: string;
18
- readonly book?: BookNodeAgentSource;
18
+ readonly book?: string | BookNodeAgentSource;
19
19
  readonly currentWorkingDirectory?: string;
20
20
  };
21
21
  /**
@@ -1,16 +1,17 @@
1
1
  import type { BookNodeAgentSourceOptions } from './BookNodeAgentSource';
2
+ import { CLI_AGENT_HARNESS_NAMES, CLI_AGENT_THINKING_LEVEL_VALUES } from './cliAgentEnv';
2
3
  /**
3
4
  * CLI harness names supported by `ptbk agent exec`.
4
5
  *
5
6
  * @public exported from `@promptbook/node`
6
7
  */
7
- export type CliAgentHarness = 'openai-codex' | 'github-copilot' | 'cline' | 'claude-code' | 'opencode' | 'gemini';
8
+ export type CliAgentHarness = (typeof CLI_AGENT_HARNESS_NAMES)[number];
8
9
  /**
9
10
  * Thinking levels supported by CLI coding harnesses.
10
11
  *
11
12
  * @public exported from `@promptbook/node`
12
13
  */
13
- export type CliAgentThinkingLevel = 'low' | 'medium' | 'high' | 'xhigh';
14
+ export type CliAgentThinkingLevel = (typeof CLI_AGENT_THINKING_LEVEL_VALUES)[number];
14
15
  /**
15
16
  * Per-run CLI options exposed by `CliAgent`.
16
17
  *
@@ -22,6 +23,7 @@ export type CliAgentRunOptions = {
22
23
  readonly allowCredits?: boolean;
23
24
  readonly context?: string;
24
25
  readonly harness?: CliAgentHarness;
26
+ readonly isVerbose?: boolean;
25
27
  readonly model?: string;
26
28
  readonly noUi?: boolean;
27
29
  readonly thinkingLevel?: CliAgentThinkingLevel;
@@ -38,6 +40,9 @@ export type CliAgentOptions = BookNodeAgentSourceOptions & CliAgentRunOptions;
38
40
  * It uses the same harnesses and execution path as `ptbk agent exec`, running the runner
39
41
  * in-process instead of spawning a separate CLI process.
40
42
  *
43
+ * When no `harness` is provided in the constructor or per-run options, `CliAgent` falls back
44
+ * to the `PTBK_HARNESS` environment variable, mirroring `ptbk agent exec` behavior.
45
+ *
41
46
  * @public exported from `@promptbook/node`
42
47
  */
43
48
  export declare class CliAgent {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * All CLI harness names supported by `CliAgent` and `ptbk agent exec`.
3
+ *
4
+ * @public exported from `@promptbook/node`
5
+ */
6
+ export declare const CLI_AGENT_HARNESS_NAMES: readonly ["openai-codex", "github-copilot", "cline", "claude-code", "opencode", "gemini"];
7
+ /**
8
+ * All supported thinking-level values for CLI coding-agent runners.
9
+ *
10
+ * @public exported from `@promptbook/node`
11
+ */
12
+ export declare const CLI_AGENT_THINKING_LEVEL_VALUES: readonly ["low", "medium", "high", "xhigh"];
13
+ /**
14
+ * Environment variable used as the default runner identifier when `--harness` is omitted or not set in `CliAgent`.
15
+ *
16
+ * Set this to one of the harness names (`openai-codex`, `github-copilot`, `cline`, `claude-code`, `opencode`, `gemini`)
17
+ * so that `CliAgent` and `ptbk agent exec` can run without an explicit `harness` option.
18
+ *
19
+ * @public exported from `@promptbook/node`
20
+ */
21
+ export declare const PTBK_HARNESS_ENV = "PTBK_HARNESS";
22
+ /**
23
+ * Environment variable used as the default runner model when `--model` is omitted or not set in `CliAgent`.
24
+ *
25
+ * @public exported from `@promptbook/node`
26
+ */
27
+ export declare const PTBK_MODEL_ENV = "PTBK_MODEL";
28
+ /**
29
+ * Environment variable used as the default thinking level when `--thinking-level` is omitted or not set in `CliAgent`.
30
+ *
31
+ * @public exported from `@promptbook/node`
32
+ */
33
+ export declare const PTBK_THINKING_LEVEL_ENV = "PTBK_THINKING_LEVEL";
@@ -1,29 +1,13 @@
1
1
  import { Command as Program } from 'commander';
2
2
  import type { ThinkingLevel } from '../coder/ThinkingLevel';
3
+ import { PTBK_HARNESS_ENV, PTBK_MODEL_ENV, PTBK_THINKING_LEVEL_ENV } from '../../../book-3.0/cliAgentEnv';
4
+ export { PTBK_HARNESS_ENV, PTBK_MODEL_ENV, PTBK_THINKING_LEVEL_ENV };
3
5
  /**
4
6
  * Runner identifiers supported by Promptbook CLI agent orchestration commands.
5
7
  *
6
8
  * @private internal utility of `promptbookCli`
7
9
  */
8
10
  export declare const PROMPT_RUNNER_HARNESS_NAMES: readonly ["openai-codex", "github-copilot", "cline", "claude-code", "opencode", "gemini"];
9
- /**
10
- * Environment variable used as the default runner identifier when `--harness` is omitted.
11
- *
12
- * @private internal utility of `promptbookCli`
13
- */
14
- export declare const PTBK_HARNESS_ENV = "PTBK_HARNESS";
15
- /**
16
- * Environment variable used as the default runner model when `--model` is omitted.
17
- *
18
- * @private internal utility of `promptbookCli`
19
- */
20
- export declare const PTBK_MODEL_ENV = "PTBK_MODEL";
21
- /**
22
- * Environment variable used as the default runner thinking level when `--thinking-level` is omitted.
23
- *
24
- * @private internal utility of `promptbookCli`
25
- */
26
- export declare const PTBK_THINKING_LEVEL_ENV = "PTBK_THINKING_LEVEL";
27
11
  /**
28
12
  * Runner identifier supported by Promptbook CLI agent orchestration commands.
29
13
  *
@@ -15,7 +15,7 @@ export declare const BOOK_LANGUAGE_VERSION: string_semantic_version;
15
15
  export declare const PROMPTBOOK_ENGINE_VERSION: string_promptbook_version;
16
16
  /**
17
17
  * Represents the version string of the Promptbook engine.
18
- * It follows semantic versioning (e.g., `0.112.0-116`).
18
+ * It follows semantic versioning (e.g., `0.112.0-117`).
19
19
  *
20
20
  * @generated
21
21
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptbook/browser",
3
- "version": "0.112.0-117",
3
+ "version": "0.112.0-118",
4
4
  "description": "Promptbook: Create persistent AI agents that turn your company's scattered knowledge into action",
5
5
  "private": false,
6
6
  "sideEffects": false,
@@ -98,7 +98,7 @@
98
98
  "types": "./esm/src/_packages/browser.index.d.ts",
99
99
  "typings": "./esm/src/_packages/browser.index.d.ts",
100
100
  "peerDependencies": {
101
- "@promptbook/core": "0.112.0-117"
101
+ "@promptbook/core": "0.112.0-118"
102
102
  },
103
103
  "dependencies": {
104
104
  "@openai/agents": "0.4.15",
package/umd/index.umd.js CHANGED
@@ -29,7 +29,7 @@
29
29
  * @generated
30
30
  * @see https://github.com/webgptorg/promptbook
31
31
  */
32
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-117';
32
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-118';
33
33
  /**
34
34
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
35
35
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -6161,21 +6161,22 @@
6161
6161
  * @private helper of `fractalAvatarVisual`
6162
6162
  */
6163
6163
  function drawDragonCurveLayer(context, points, options) {
6164
- const { size, primaryColor, secondaryColor, tertiaryColor, shadowColor, strokeWidth, timeMs, layerIndex } = options;
6164
+ const { primaryColor, secondaryColor, tertiaryColor, shadowColor, strokeWidth, timeMs, layerIndex } = options;
6165
6165
  const firstPoint = points[0];
6166
6166
  const lastPoint = points[points.length - 1];
6167
6167
  const ribbonGradient = context.createLinearGradient(firstPoint.x, firstPoint.y, lastPoint.x, lastPoint.y);
6168
6168
  ribbonGradient.addColorStop(0, `${primaryColor}f2`);
6169
6169
  ribbonGradient.addColorStop(0.5, `${secondaryColor}e6`);
6170
6170
  ribbonGradient.addColorStop(1, `${tertiaryColor}f2`);
6171
+ // Approximate the blurred shadow stroke with a wider semi-transparent stroke instead of
6172
+ // context.filter blur, which triggers a costly software rasterization pass every frame.
6171
6173
  context.save();
6172
6174
  context.beginPath();
6173
6175
  tracePolyline(context, points);
6174
- context.strokeStyle = `${shadowColor}82`;
6175
- context.lineWidth = strokeWidth * 1.8;
6176
+ context.strokeStyle = `${shadowColor}48`;
6177
+ context.lineWidth = strokeWidth * 4.5;
6176
6178
  context.lineJoin = 'round';
6177
6179
  context.lineCap = 'round';
6178
- context.filter = `blur(${size * 0.022}px)`;
6179
6180
  context.stroke();
6180
6181
  context.restore();
6181
6182
  context.beginPath();
@@ -6759,11 +6760,23 @@
6759
6760
  * @private helper of `minecraft2AvatarVisual`
6760
6761
  */
6761
6762
  function drawMinecraftShadow(context, size, palette, interaction, timeMs) {
6763
+ const cx = size * 0.5 + interaction.gazeX * size * 0.03;
6764
+ const cy = size * 0.85 + Math.sin(timeMs / 880) * size * 0.01;
6765
+ const rx = size * (0.16 + interaction.intensity * 0.015);
6766
+ const ry = size * 0.055;
6767
+ // Radial gradient approximates the blurry ellipse shadow without context.filter blur.
6762
6768
  context.save();
6763
- context.fillStyle = `${palette.shadow}66`;
6764
- context.filter = `blur(${size * 0.02}px)`;
6769
+ context.translate(cx, cy);
6770
+ context.scale(1, ry / rx);
6771
+ const blurRadius = rx * 1.4;
6772
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
6773
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
6774
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
6775
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
6776
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
6777
+ context.fillStyle = shadowGradient;
6765
6778
  context.beginPath();
6766
- 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);
6779
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
6767
6780
  context.fill();
6768
6781
  context.restore();
6769
6782
  }
@@ -6987,13 +7000,27 @@
6987
7000
  spotlight.addColorStop(1, `${palette.highlight}00`);
6988
7001
  context.fillStyle = spotlight;
6989
7002
  context.fillRect(0, 0, size, size);
6990
- context.save();
6991
- context.fillStyle = 'rgba(0, 0, 0, 0.22)';
6992
- context.filter = `blur(${size * 0.018}px)`;
6993
- context.beginPath();
6994
- context.ellipse(size * 0.5, size * 0.86, size * 0.2, size * 0.06, 0, 0, Math.PI * 2);
6995
- context.fill();
6996
- context.restore();
7003
+ {
7004
+ // Radial gradient approximates the blurry ellipse shadow without context.filter blur.
7005
+ const cx = size * 0.5;
7006
+ const cy = size * 0.86;
7007
+ const rx = size * 0.2;
7008
+ const ry = size * 0.06;
7009
+ const blurRadius = rx * 1.4;
7010
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
7011
+ shadowGradient.addColorStop(0, 'rgba(0,0,0,0.28)');
7012
+ shadowGradient.addColorStop(0.45, 'rgba(0,0,0,0.14)');
7013
+ shadowGradient.addColorStop(0.8, 'rgba(0,0,0,0.05)');
7014
+ shadowGradient.addColorStop(1, 'rgba(0,0,0,0)');
7015
+ context.save();
7016
+ context.translate(cx, cy);
7017
+ context.scale(1, ry / rx);
7018
+ context.fillStyle = shadowGradient;
7019
+ context.beginPath();
7020
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
7021
+ context.fill();
7022
+ context.restore();
7023
+ }
6997
7024
  drawVoxelCuboid(context, {
6998
7025
  x: bodyX,
6999
7026
  y: bodyY,
@@ -8045,6 +8072,35 @@
8045
8072
  y: -0.62,
8046
8073
  z: 0.94,
8047
8074
  });
8075
+ /**
8076
+ * Cache keyed by the `createRandom` factory reference (stable per mounted `<Avatar/>`).
8077
+ *
8078
+ * @private helper of `octopus3dAvatarVisual`
8079
+ */
8080
+ const octopus3dStableStateCache = new WeakMap();
8081
+ /**
8082
+ * Returns the stable per-avatar state, computing it on first access and caching for subsequent frames.
8083
+ *
8084
+ * @private helper of `octopus3dAvatarVisual`
8085
+ */
8086
+ function getOctopus3dStableState(createRandom) {
8087
+ const cached = octopus3dStableStateCache.get(createRandom);
8088
+ if (cached !== undefined) {
8089
+ return cached;
8090
+ }
8091
+ const animationRandom = createRandom('octopus3d-animation-profile');
8092
+ const eyeRandom = createRandom('octopus3d-eye-profile');
8093
+ const leftEyePhaseOffset = eyeRandom() * 0.6;
8094
+ const rightEyePhaseOffset = eyeRandom() * 0.6;
8095
+ const state = {
8096
+ morphologyProfile: createOctopus3MorphologyProfile(createRandom),
8097
+ animationPhase: animationRandom() * Math.PI * 2,
8098
+ leftEyePhaseOffset,
8099
+ rightEyePhaseOffset,
8100
+ };
8101
+ octopus3dStableStateCache.set(createRandom, state);
8102
+ return state;
8103
+ }
8048
8104
  /**
8049
8105
  * Proper 3D Octopus visual built from projected organic meshes and tentacles.
8050
8106
  *
@@ -8057,10 +8113,7 @@
8057
8113
  isAnimated: true,
8058
8114
  supportsPointerTracking: true,
8059
8115
  render({ context, size, palette, createRandom, timeMs, interaction }) {
8060
- const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
8061
- const animationRandom = createRandom('octopus3d-animation-profile');
8062
- const eyeRandom = createRandom('octopus3d-eye-profile');
8063
- const animationPhase = animationRandom() * Math.PI * 2;
8116
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset } = getOctopus3dStableState(createRandom);
8064
8117
  const sceneCenterX = size * 0.5;
8065
8118
  const sceneCenterY = size * 0.56;
8066
8119
  const bob = Math.sin(timeMs / 920 + animationPhase) * size * 0.014;
@@ -8157,12 +8210,12 @@
8157
8210
  x: -faceEyeSpacing,
8158
8211
  y: faceEyeYOffset,
8159
8212
  z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, -faceEyeSpacing, faceEyeYOffset),
8160
- }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + eyeRandom() * 0.6, interaction, morphologyProfile.face.eyeStyle);
8213
+ }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8161
8214
  drawProjectedOrganicEye(context, {
8162
8215
  x: faceEyeSpacing,
8163
8216
  y: faceEyeYOffset,
8164
8217
  z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, faceEyeSpacing, faceEyeYOffset),
8165
- }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.7 + eyeRandom() * 0.6, interaction, morphologyProfile.face.eyeStyle);
8218
+ }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.7 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8166
8219
  drawProjectedOrganicMouth(context, [
8167
8220
  {
8168
8221
  x: -mouthHalfWidth,
@@ -8206,14 +8259,28 @@
8206
8259
  /**
8207
8260
  * Draws the soft ground shadow below the octopus.
8208
8261
  *
8262
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
8263
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
8264
+ *
8209
8265
  * @private helper of `octopus3dAvatarVisual`
8210
8266
  */
8211
8267
  function drawOctopus3dShadow(context, size, palette, interaction, timeMs) {
8268
+ const cx = size * 0.5 + interaction.gazeX * size * 0.04;
8269
+ const cy = size * 0.87 + Math.sin(timeMs / 920) * size * 0.008;
8270
+ const rx = size * (0.18 + interaction.intensity * 0.02);
8271
+ const ry = size * 0.06;
8212
8272
  context.save();
8213
- context.fillStyle = `${palette.shadow}66`;
8214
- context.filter = `blur(${size * 0.022}px)`;
8273
+ context.translate(cx, cy);
8274
+ context.scale(1, ry / rx);
8275
+ const blurRadius = rx * 1.4;
8276
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
8277
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
8278
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
8279
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
8280
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
8281
+ context.fillStyle = shadowGradient;
8215
8282
  context.beginPath();
8216
- 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);
8283
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
8217
8284
  context.fill();
8218
8285
  context.restore();
8219
8286
  }
@@ -8444,6 +8511,35 @@
8444
8511
  y: -0.6,
8445
8512
  z: 0.98,
8446
8513
  });
8514
+ /**
8515
+ * Cache keyed by the `createRandom` factory reference (stable per mounted `<Avatar/>`).
8516
+ *
8517
+ * @private helper of `octopus3d2AvatarVisual`
8518
+ */
8519
+ const octopus3d2StableStateCache = new WeakMap();
8520
+ /**
8521
+ * Returns the stable per-avatar state, computing it on first access and caching for subsequent frames.
8522
+ *
8523
+ * @private helper of `octopus3d2AvatarVisual`
8524
+ */
8525
+ function getOctopus3d2StableState(createRandom) {
8526
+ const cached = octopus3d2StableStateCache.get(createRandom);
8527
+ if (cached !== undefined) {
8528
+ return cached;
8529
+ }
8530
+ const animationRandom = createRandom('octopus3d2-animation-profile');
8531
+ const eyeRandom = createRandom('octopus3d2-eye-profile');
8532
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
8533
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
8534
+ const state = {
8535
+ morphologyProfile: createOctopus3MorphologyProfile(createRandom),
8536
+ animationPhase: animationRandom() * Math.PI * 2,
8537
+ leftEyePhaseOffset,
8538
+ rightEyePhaseOffset,
8539
+ };
8540
+ octopus3d2StableStateCache.set(createRandom, state);
8541
+ return state;
8542
+ }
8447
8543
  /**
8448
8544
  * Octopus 3D 2 avatar visual.
8449
8545
  *
@@ -8456,10 +8552,7 @@
8456
8552
  isAnimated: true,
8457
8553
  supportsPointerTracking: true,
8458
8554
  render({ context, size, palette, createRandom, timeMs, interaction }) {
8459
- const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
8460
- const animationRandom = createRandom('octopus3d2-animation-profile');
8461
- const eyeRandom = createRandom('octopus3d2-eye-profile');
8462
- const animationPhase = animationRandom() * Math.PI * 2;
8555
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset } = getOctopus3d2StableState(createRandom);
8463
8556
  const sceneCenterX = size * 0.5;
8464
8557
  const sceneCenterY = size * 0.575;
8465
8558
  const bob = Math.sin(timeMs / 940 + animationPhase) * size * 0.013;
@@ -8512,8 +8605,8 @@
8512
8605
  const rightEyeLocalCenter = sampleBlobbyOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude);
8513
8606
  const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
8514
8607
  const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
8515
- drawProjectedOrganicEye(context, leftEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + eyeRandom() * 0.7, interaction, morphologyProfile.face.eyeStyle);
8516
- drawProjectedOrganicEye(context, rightEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.9 + eyeRandom() * 0.7, interaction, morphologyProfile.face.eyeStyle);
8608
+ drawProjectedOrganicEye(context, leftEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8609
+ drawProjectedOrganicEye(context, rightEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.9 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8517
8610
  drawProjectedOrganicMouth(context, [
8518
8611
  sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
8519
8612
  sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
@@ -8542,14 +8635,28 @@
8542
8635
  /**
8543
8636
  * Draws the soft floor shadow that anchors the single mesh in the frame.
8544
8637
  *
8638
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
8639
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
8640
+ *
8545
8641
  * @private helper of `octopus3d2AvatarVisual`
8546
8642
  */
8547
8643
  function drawBlobbyOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
8644
+ const cx = size * 0.5 + interaction.gazeX * size * 0.045;
8645
+ const cy = size * 0.88 + Math.sin(timeMs / 940) * size * 0.008;
8646
+ const rx = size * (0.18 + (morphologyProfile.body.horizontalStretch - 1) * 0.04 + interaction.intensity * 0.018);
8647
+ const ry = size * 0.062;
8548
8648
  context.save();
8549
- context.fillStyle = `${palette.shadow}66`;
8550
- context.filter = `blur(${size * 0.024}px)`;
8649
+ context.translate(cx, cy);
8650
+ context.scale(1, ry / rx);
8651
+ const blurRadius = rx * 1.4;
8652
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
8653
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
8654
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
8655
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
8656
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
8657
+ context.fillStyle = shadowGradient;
8551
8658
  context.beginPath();
8552
- 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);
8659
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
8553
8660
  context.fill();
8554
8661
  context.restore();
8555
8662
  }
@@ -8705,6 +8812,40 @@
8705
8812
  * @private helper of `octopus3d3AvatarVisual`
8706
8813
  */
8707
8814
  const OCTOPUS_TENTACLE_COUNT = 8;
8815
+ /**
8816
+ * Cache keyed by the `createRandom` factory reference, which is stable for the lifetime of one
8817
+ * mounted `<Avatar/>` component (created inside `resolveAvatarRenderDefinition` and held in a
8818
+ * React `useMemo`). Using a `WeakMap` ensures the entry is collected when the component unmounts.
8819
+ *
8820
+ * @private helper of `octopus3d3AvatarVisual`
8821
+ */
8822
+ const stableStateCache = new WeakMap();
8823
+ /**
8824
+ * Returns the stable per-avatar state, computing it on first access and returning the cached
8825
+ * result on every subsequent call within the same `<Avatar/>` mount.
8826
+ *
8827
+ * @private helper of `octopus3d3AvatarVisual`
8828
+ */
8829
+ function getOctopus3d3StableState(createRandom) {
8830
+ const cached = stableStateCache.get(createRandom);
8831
+ if (cached !== undefined) {
8832
+ return cached;
8833
+ }
8834
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
8835
+ const animationRandom = createRandom('octopus3d3-animation-profile');
8836
+ const eyeRandom = createRandom('octopus3d3-eye-profile');
8837
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
8838
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
8839
+ const state = {
8840
+ morphologyProfile,
8841
+ animationPhase: animationRandom() * Math.PI * 2,
8842
+ leftEyePhaseOffset,
8843
+ rightEyePhaseOffset,
8844
+ tentacleProfiles: createContinuousTentacleProfiles(createRandom, morphologyProfile),
8845
+ };
8846
+ stableStateCache.set(createRandom, state);
8847
+ return state;
8848
+ }
8708
8849
  /**
8709
8850
  * Octopus 3D 3 avatar visual.
8710
8851
  *
@@ -8717,11 +8858,7 @@
8717
8858
  isAnimated: true,
8718
8859
  supportsPointerTracking: true,
8719
8860
  render({ context, size, palette, createRandom, timeMs, interaction }) {
8720
- const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
8721
- const animationRandom = createRandom('octopus3d3-animation-profile');
8722
- const eyeRandom = createRandom('octopus3d3-eye-profile');
8723
- const animationPhase = animationRandom() * Math.PI * 2;
8724
- const tentacleProfiles = createContinuousTentacleProfiles(createRandom, morphologyProfile);
8861
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles } = getOctopus3d3StableState(createRandom);
8725
8862
  const sceneCenterX = size * 0.5;
8726
8863
  const sceneCenterY = size * 0.535;
8727
8864
  const bob = Math.sin(timeMs / 960 + animationPhase) * size * 0.012;
@@ -8798,8 +8935,8 @@
8798
8935
  size,
8799
8936
  palette,
8800
8937
  });
8801
- drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + eyeRandom() * 0.7, interaction, morphologyProfile.face.eyeStyle);
8802
- 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);
8938
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8939
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8803
8940
  drawProjectedOrganicMouth(context, [
8804
8941
  sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
8805
8942
  sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
@@ -8850,14 +8987,30 @@
8850
8987
  /**
8851
8988
  * Draws the soft lower shadow that anchors the octopus in the avatar frame.
8852
8989
  *
8990
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
8991
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
8992
+ *
8853
8993
  * @private helper of `octopus3d3AvatarVisual`
8854
8994
  */
8855
8995
  function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
8996
+ const cx = size * 0.5 + interaction.gazeX * size * 0.045;
8997
+ const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
8998
+ const rx = size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02);
8999
+ const ry = size * 0.06;
9000
+ // Scale the context so that drawing a circle produces the correct ellipse aspect ratio,
9001
+ // then fill with a radial gradient that approximates the blurry edge without context.filter.
8856
9002
  context.save();
8857
- context.fillStyle = `${palette.shadow}66`;
8858
- context.filter = `blur(${size * 0.025}px)`;
9003
+ context.translate(cx, cy);
9004
+ context.scale(1, ry / rx);
9005
+ const blurRadius = rx * 1.4;
9006
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
9007
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
9008
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
9009
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
9010
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
9011
+ context.fillStyle = shadowGradient;
8859
9012
  context.beginPath();
8860
- 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);
9013
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
8861
9014
  context.fill();
8862
9015
  context.restore();
8863
9016
  }