@promptbook/remote-server 0.112.0-54 → 0.112.0-56

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.
Files changed (149) hide show
  1. package/esm/index.es.js +3577 -204
  2. package/esm/index.es.js.map +1 -1
  3. package/esm/src/_packages/cli.index.d.ts +10 -0
  4. package/esm/src/_packages/core.index.d.ts +12 -0
  5. package/esm/src/_packages/types.index.d.ts +8 -0
  6. package/esm/src/_packages/wizard.index.d.ts +10 -0
  7. package/esm/src/avatars/index.d.ts +1 -1
  8. package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  9. package/esm/src/avatars/visuals/avatarVisualRegistry.d.ts +12 -0
  10. package/esm/src/avatars/visuals/orbAvatarVisual.d.ts +48 -0
  11. package/esm/src/avatars/visuals/orbAvatarVisual.test.d.ts +1 -0
  12. package/esm/src/book-2.0/agent-source/AgentBasicInformation.d.ts +2 -0
  13. package/esm/src/book-2.0/agent-source/parseAgentSourceWithCommitments.use.test.d.ts +1 -0
  14. package/esm/src/book-components/BookEditor/createDeprecatedCommitmentDiagnostics.d.ts +9 -8
  15. package/esm/src/commitments/ACTION/ACTION.d.ts +8 -2
  16. package/esm/src/commitments/ACTION/ACTION.test.d.ts +1 -0
  17. package/esm/src/commitments/DELETE/DELETE.d.ts +7 -3
  18. package/esm/src/commitments/DELETE/DELETE.test.d.ts +1 -0
  19. package/esm/src/commitments/FORMAT/FORMAT.d.ts +10 -4
  20. package/esm/src/commitments/FORMAT/FORMAT.test.d.ts +1 -0
  21. package/esm/src/commitments/GOAL/GOAL.d.ts +4 -0
  22. package/esm/src/commitments/KNOWLEDGE/KNOWLEDGE.d.ts +4 -0
  23. package/esm/src/commitments/META/META.d.ts +2 -0
  24. package/esm/src/commitments/META_AVATAR/META_AVATAR.d.ts +26 -0
  25. package/esm/src/commitments/MODEL/MODEL.d.ts +4 -0
  26. package/esm/src/commitments/MODEL/MODEL.test.d.ts +1 -0
  27. package/esm/src/commitments/RULE/RULE.d.ts +4 -0
  28. package/esm/src/commitments/TEAM/TEAM.d.ts +4 -0
  29. package/esm/src/commitments/TEMPLATE/TEMPLATE.d.ts +10 -4
  30. package/esm/src/commitments/_base/BaseCommitmentDefinition.d.ts +12 -0
  31. package/esm/src/commitments/_base/CommitmentDefinition.d.ts +21 -1
  32. package/esm/src/commitments/_common/getAllCommitmentDefinitions.test.d.ts +1 -0
  33. package/esm/src/commitments/_common/getCommitmentNoticeMetadata.d.ts +51 -0
  34. package/esm/src/commitments/_common/getCommitmentNoticeMetadata.test.d.ts +1 -0
  35. package/esm/src/commitments/_common/getGroupedCommitmentDefinitions.openClosed.test.d.ts +1 -0
  36. package/esm/src/commitments/_common/getGroupedCommitmentDefinitions.order.test.d.ts +1 -0
  37. package/esm/src/commitments/_common/getGroupedCommitmentDefinitions.use.test.d.ts +1 -0
  38. package/esm/src/commitments/_common/sortCommitmentDefinitions.d.ts +31 -0
  39. package/esm/src/commitments/_common/teamInternalAgentAccess.d.ts +51 -0
  40. package/esm/src/commitments/_common/toolRuntimeContext.d.ts +4 -0
  41. package/esm/src/commitments/index.d.ts +2 -2
  42. package/esm/src/commitments/index.test.d.ts +1 -0
  43. package/esm/src/llm-providers/agent/Agent.d.ts +2 -0
  44. package/esm/src/llm-providers/agent/RemoteAgent.d.ts +1 -0
  45. package/esm/src/llm-providers/agent/RemoteAgentOptions.d.ts +4 -0
  46. package/esm/src/playground/playground.d.ts +1 -0
  47. package/esm/src/transpilers/_common/BookTranspilerOptions.d.ts +20 -0
  48. package/esm/src/transpilers/_common/TranspiledTeamExport.d.ts +80 -0
  49. package/esm/src/transpilers/_common/createTranspiledTeamRuntimeSection.d.ts +55 -0
  50. package/esm/src/transpilers/_common/createZodSchemaSource.d.ts +40 -0
  51. package/esm/src/transpilers/_common/formatUsedToolFunctions.d.ts +18 -0
  52. package/esm/src/transpilers/_common/formatUsedToolFunctions.test.d.ts +1 -0
  53. package/esm/src/transpilers/_common/prepareSdkTranspilerContext.d.ts +48 -0
  54. package/esm/src/transpilers/_common/resolveClaudeModelName.d.ts +12 -0
  55. package/esm/src/transpilers/_common/transpiledTeamTranspilers.test.d.ts +1 -0
  56. package/esm/src/transpilers/agent-os/AgentOsTranspiler.d.ts +16 -0
  57. package/esm/src/transpilers/agent-os/AgentOsTranspiler.test.d.ts +1 -0
  58. package/esm/src/transpilers/agent-os/register.d.ts +12 -0
  59. package/esm/src/transpilers/anthropic-claude-managed/AnthropicClaudeManagedTranspiler.d.ts +16 -0
  60. package/esm/src/transpilers/anthropic-claude-managed/AnthropicClaudeManagedTranspiler.test.d.ts +1 -0
  61. package/esm/src/transpilers/anthropic-claude-managed/register.d.ts +12 -0
  62. package/esm/src/transpilers/anthropic-claude-sdk/AnthropicClaudeSdkTranspiler.d.ts +16 -0
  63. package/esm/src/transpilers/anthropic-claude-sdk/AnthropicClaudeSdkTranspiler.test.d.ts +1 -0
  64. package/esm/src/transpilers/anthropic-claude-sdk/register.d.ts +12 -0
  65. package/esm/src/transpilers/e2b/E2BTranspiler.d.ts +16 -0
  66. package/esm/src/transpilers/e2b/E2BTranspiler.test.d.ts +1 -0
  67. package/esm/src/transpilers/e2b/register.d.ts +12 -0
  68. package/esm/src/transpilers/openai-agents/OpenAiAgentsTranspiler.d.ts +16 -0
  69. package/esm/src/transpilers/openai-agents/OpenAiAgentsTranspiler.test.d.ts +1 -0
  70. package/esm/src/transpilers/openai-agents/register.d.ts +12 -0
  71. package/esm/src/utils/agents/resolveAgentAvatarImageUrl.d.ts +15 -0
  72. package/esm/src/utils/markdown/humanizeAiTextEmdashed.d.ts +1 -1
  73. package/esm/src/version.d.ts +1 -1
  74. package/package.json +3 -3
  75. package/umd/index.umd.js +3577 -204
  76. package/umd/index.umd.js.map +1 -1
  77. package/umd/src/_packages/cli.index.d.ts +10 -0
  78. package/umd/src/_packages/core.index.d.ts +12 -0
  79. package/umd/src/_packages/types.index.d.ts +8 -0
  80. package/umd/src/_packages/wizard.index.d.ts +10 -0
  81. package/umd/src/avatars/index.d.ts +1 -1
  82. package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  83. package/umd/src/avatars/visuals/avatarVisualRegistry.d.ts +12 -0
  84. package/umd/src/avatars/visuals/orbAvatarVisual.d.ts +48 -0
  85. package/umd/src/avatars/visuals/orbAvatarVisual.test.d.ts +1 -0
  86. package/umd/src/book-2.0/agent-source/AgentBasicInformation.d.ts +2 -0
  87. package/umd/src/book-2.0/agent-source/parseAgentSourceWithCommitments.use.test.d.ts +1 -0
  88. package/umd/src/book-components/BookEditor/createDeprecatedCommitmentDiagnostics.d.ts +9 -8
  89. package/umd/src/commitments/ACTION/ACTION.d.ts +8 -2
  90. package/umd/src/commitments/ACTION/ACTION.test.d.ts +1 -0
  91. package/umd/src/commitments/DELETE/DELETE.d.ts +7 -3
  92. package/umd/src/commitments/DELETE/DELETE.test.d.ts +1 -0
  93. package/umd/src/commitments/FORMAT/FORMAT.d.ts +10 -4
  94. package/umd/src/commitments/FORMAT/FORMAT.test.d.ts +1 -0
  95. package/umd/src/commitments/GOAL/GOAL.d.ts +4 -0
  96. package/umd/src/commitments/KNOWLEDGE/KNOWLEDGE.d.ts +4 -0
  97. package/umd/src/commitments/META/META.d.ts +2 -0
  98. package/umd/src/commitments/META_AVATAR/META_AVATAR.d.ts +26 -0
  99. package/umd/src/commitments/MODEL/MODEL.d.ts +4 -0
  100. package/umd/src/commitments/MODEL/MODEL.test.d.ts +1 -0
  101. package/umd/src/commitments/RULE/RULE.d.ts +4 -0
  102. package/umd/src/commitments/TEAM/TEAM.d.ts +4 -0
  103. package/umd/src/commitments/TEMPLATE/TEMPLATE.d.ts +10 -4
  104. package/umd/src/commitments/_base/BaseCommitmentDefinition.d.ts +12 -0
  105. package/umd/src/commitments/_base/CommitmentDefinition.d.ts +21 -1
  106. package/umd/src/commitments/_common/getAllCommitmentDefinitions.test.d.ts +1 -0
  107. package/umd/src/commitments/_common/getCommitmentNoticeMetadata.d.ts +51 -0
  108. package/umd/src/commitments/_common/getCommitmentNoticeMetadata.test.d.ts +1 -0
  109. package/umd/src/commitments/_common/getGroupedCommitmentDefinitions.openClosed.test.d.ts +1 -0
  110. package/umd/src/commitments/_common/getGroupedCommitmentDefinitions.order.test.d.ts +1 -0
  111. package/umd/src/commitments/_common/getGroupedCommitmentDefinitions.use.test.d.ts +1 -0
  112. package/umd/src/commitments/_common/sortCommitmentDefinitions.d.ts +31 -0
  113. package/umd/src/commitments/_common/teamInternalAgentAccess.d.ts +51 -0
  114. package/umd/src/commitments/_common/toolRuntimeContext.d.ts +4 -0
  115. package/umd/src/commitments/index.d.ts +2 -2
  116. package/umd/src/commitments/index.test.d.ts +1 -0
  117. package/umd/src/llm-providers/agent/Agent.d.ts +2 -0
  118. package/umd/src/llm-providers/agent/RemoteAgent.d.ts +1 -0
  119. package/umd/src/llm-providers/agent/RemoteAgentOptions.d.ts +4 -0
  120. package/umd/src/playground/playground.d.ts +1 -0
  121. package/umd/src/transpilers/_common/BookTranspilerOptions.d.ts +20 -0
  122. package/umd/src/transpilers/_common/TranspiledTeamExport.d.ts +80 -0
  123. package/umd/src/transpilers/_common/createTranspiledTeamRuntimeSection.d.ts +55 -0
  124. package/umd/src/transpilers/_common/createZodSchemaSource.d.ts +40 -0
  125. package/umd/src/transpilers/_common/formatUsedToolFunctions.d.ts +18 -0
  126. package/umd/src/transpilers/_common/formatUsedToolFunctions.test.d.ts +1 -0
  127. package/umd/src/transpilers/_common/prepareSdkTranspilerContext.d.ts +48 -0
  128. package/umd/src/transpilers/_common/resolveClaudeModelName.d.ts +12 -0
  129. package/umd/src/transpilers/_common/transpiledTeamTranspilers.test.d.ts +1 -0
  130. package/umd/src/transpilers/agent-os/AgentOsTranspiler.d.ts +16 -0
  131. package/umd/src/transpilers/agent-os/AgentOsTranspiler.test.d.ts +1 -0
  132. package/umd/src/transpilers/agent-os/register.d.ts +12 -0
  133. package/umd/src/transpilers/anthropic-claude-managed/AnthropicClaudeManagedTranspiler.d.ts +16 -0
  134. package/umd/src/transpilers/anthropic-claude-managed/AnthropicClaudeManagedTranspiler.test.d.ts +1 -0
  135. package/umd/src/transpilers/anthropic-claude-managed/register.d.ts +12 -0
  136. package/umd/src/transpilers/anthropic-claude-sdk/AnthropicClaudeSdkTranspiler.d.ts +16 -0
  137. package/umd/src/transpilers/anthropic-claude-sdk/AnthropicClaudeSdkTranspiler.test.d.ts +1 -0
  138. package/umd/src/transpilers/anthropic-claude-sdk/register.d.ts +12 -0
  139. package/umd/src/transpilers/e2b/E2BTranspiler.d.ts +16 -0
  140. package/umd/src/transpilers/e2b/E2BTranspiler.test.d.ts +1 -0
  141. package/umd/src/transpilers/e2b/register.d.ts +12 -0
  142. package/umd/src/transpilers/openai-agents/OpenAiAgentsTranspiler.d.ts +16 -0
  143. package/umd/src/transpilers/openai-agents/OpenAiAgentsTranspiler.test.d.ts +1 -0
  144. package/umd/src/transpilers/openai-agents/register.d.ts +12 -0
  145. package/umd/src/utils/agents/resolveAgentAvatarImageUrl.d.ts +15 -0
  146. package/umd/src/utils/markdown/humanizeAiTextEmdashed.d.ts +1 -1
  147. package/umd/src/version.d.ts +1 -1
  148. package/esm/src/commitments/USE/USE.d.ts +0 -51
  149. package/umd/src/commitments/USE/USE.d.ts +0 -51
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-54';
43
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-56';
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
@@ -11227,6 +11227,24 @@ class BaseCommitmentDefinition {
11227
11227
  get requiresContent() {
11228
11228
  return true;
11229
11229
  }
11230
+ /**
11231
+ * Whether this commitment should be prioritized in menus, documentation, and intellisense.
11232
+ */
11233
+ get isImportant() {
11234
+ return false;
11235
+ }
11236
+ /**
11237
+ * Whether this commitment is unfinished and not ready to use.
11238
+ */
11239
+ get isUnfinished() {
11240
+ return false;
11241
+ }
11242
+ /**
11243
+ * Whether this commitment is low-level and should be surfaced with caution.
11244
+ */
11245
+ get isLowLevel() {
11246
+ return false;
11247
+ }
11230
11248
  /**
11231
11249
  * Optional UI/docs-only deprecation metadata.
11232
11250
  */
@@ -11343,8 +11361,8 @@ class BaseCommitmentDefinition {
11343
11361
  /**
11344
11362
  * ACTION commitment definition
11345
11363
  *
11346
- * The ACTION commitment defines specific actions or capabilities that the agent can perform.
11347
- * This helps define what the agent is capable of doing and how it should approach tasks.
11364
+ * Deprecated legacy commitment for broad capability notes.
11365
+ * New books should prefer the appropriate `USE*` commitment instead.
11348
11366
  *
11349
11367
  * Example usage in agent source:
11350
11368
  *
@@ -11363,7 +11381,15 @@ class ActionCommitmentDefinition extends BaseCommitmentDefinition {
11363
11381
  * Short one-line description of ACTION.
11364
11382
  */
11365
11383
  get description() {
11366
- return 'Define agent capabilities and actions it can perform.';
11384
+ return 'Deprecated legacy capability commitment. Prefer concrete `USE*` commitments.';
11385
+ }
11386
+ /**
11387
+ * Optional UI/docs-only deprecation metadata.
11388
+ */
11389
+ get deprecation() {
11390
+ return {
11391
+ message: 'Use a concrete `USE*` commitment instead.',
11392
+ };
11367
11393
  }
11368
11394
  /**
11369
11395
  * Icon for this commitment.
@@ -11378,33 +11404,43 @@ class ActionCommitmentDefinition extends BaseCommitmentDefinition {
11378
11404
  return spaceTrim$1(`
11379
11405
  # ${this.type}
11380
11406
 
11381
- Defines specific actions or capabilities that the agent can perform.
11407
+ Deprecated legacy commitment for broad capability notes.
11382
11408
 
11383
- ## Key aspects
11409
+ ## Migration
11384
11410
 
11385
- - Both terms work identically and can be used interchangeably.
11386
- - Each action adds to the agent's capability list.
11387
- - Actions help users understand what the agent can do.
11411
+ - Existing \`${this.type}\` and \`ACTIONS\` books still parse and compile.
11412
+ - New books should prefer the appropriate \`USE*\` commitment instead.
11413
+ - Keep \`${this.type}\` only when maintaining older books that already rely on it.
11388
11414
 
11389
- ## Examples
11415
+ ## Preferred replacement
11390
11416
 
11391
11417
  \`\`\`book
11392
- Code Assistant
11418
+ Research Assistant
11393
11419
 
11394
- PERSONA You are a programming assistant
11395
- ACTION Can generate code snippets and explain programming concepts
11396
- ACTION Able to debug existing code and suggest improvements
11397
- ACTION Can create unit tests for functions
11420
+ PERSONA You are a helpful research assistant
11421
+ USE SEARCH ENGINE
11422
+ RULE Always cite your sources when providing information from the web
11423
+ \`\`\`
11424
+
11425
+ ## Legacy compatibility example
11426
+
11427
+ \`\`\`book
11428
+ Research Assistant
11429
+
11430
+ PERSONA You are a helpful research assistant
11431
+ ACTION Can search for current information and summarize findings
11432
+ RULE Always cite your sources when providing information from the web
11398
11433
  \`\`\`
11399
11434
 
11435
+ ## Legacy compatibility example with additional tools
11436
+
11400
11437
  \`\`\`book
11401
- Data Scientist
11438
+ Code Assistant
11402
11439
 
11403
- PERSONA You are a data analysis expert
11404
- ACTION Able to analyze data and provide insights
11405
- ACTION Can create visualizations and charts
11406
- ACTION Capable of statistical analysis and modeling
11407
- KNOWLEDGE Data analysis best practices and statistical methods
11440
+ PERSONA You are a programming assistant
11441
+ USE BROWSER
11442
+ USE SEARCH ENGINE
11443
+ RULE Prefer the narrowest useful capability for the task.
11408
11444
  \`\`\`
11409
11445
  `);
11410
11446
  }
@@ -11413,7 +11449,7 @@ class ActionCommitmentDefinition extends BaseCommitmentDefinition {
11413
11449
  if (!trimmedContent) {
11414
11450
  return requirements;
11415
11451
  }
11416
- // Add action capability to the system message
11452
+ // Keep the legacy capability note for backward compatibility.
11417
11453
  const actionSection = `Capability: ${trimmedContent}`;
11418
11454
  return this.appendToSystemMessage(requirements, actionSection, '\n\n');
11419
11455
  }
@@ -11548,9 +11584,9 @@ class ComponentCommitmentDefinition extends BaseCommitmentDefinition {
11548
11584
  /**
11549
11585
  * DELETE commitment definition
11550
11586
  *
11551
- * The DELETE commitment (and its aliases CANCEL, DISCARD, REMOVE) is used to
11552
- * remove or disregard certain information or context. This can be useful for
11553
- * overriding previous commitments or removing unwanted behaviors.
11587
+ * The DELETE commitment (and its aliases CANCEL, DISCARD, REMOVE) is a low-level
11588
+ * unfinished commitment used to remove or disregard certain information or context.
11589
+ * It is intentionally surfaced with caution because it is not ready for broad use yet.
11554
11590
  *
11555
11591
  * Example usage in agent source:
11556
11592
  *
@@ -11571,7 +11607,13 @@ class DeleteCommitmentDefinition extends BaseCommitmentDefinition {
11571
11607
  * Short one-line description of DELETE/CANCEL/DISCARD/REMOVE.
11572
11608
  */
11573
11609
  get description() {
11574
- return 'Remove or **disregard** certain information, context, or previous commitments.';
11610
+ return 'Unfinished low-level commitment for removing or disregarding information. Use carefully.';
11611
+ }
11612
+ /**
11613
+ * Marks DELETE as unfinished and not ready to use.
11614
+ */
11615
+ get isUnfinished() {
11616
+ return true;
11575
11617
  }
11576
11618
  /**
11577
11619
  * Icon for this commitment.
@@ -11586,7 +11628,13 @@ class DeleteCommitmentDefinition extends BaseCommitmentDefinition {
11586
11628
  return spaceTrim$1(`
11587
11629
  # DELETE (CANCEL, DISCARD, REMOVE)
11588
11630
 
11589
- A commitment to remove or disregard certain information or context. This can be useful for overriding previous commitments or removing unwanted behaviors.
11631
+ A low-level unfinished commitment to remove or disregard certain information or context. It is not ready to use broadly yet, so use it carefully.
11632
+
11633
+ ## Status
11634
+
11635
+ - This commitment is unfinished and not ready to use yet.
11636
+ - Treat it as a low-level prompt-surgery tool rather than a general-purpose commitment.
11637
+ - Prefer higher-level commitments when a clearer dedicated commitment exists.
11590
11638
 
11591
11639
  ## Aliases
11592
11640
 
@@ -11601,6 +11649,7 @@ class DeleteCommitmentDefinition extends BaseCommitmentDefinition {
11601
11649
  - Useful for overriding previous commitments in the same agent definition.
11602
11650
  - Can be used to remove inherited behaviors from base personas.
11603
11651
  - Helps fine-tune agent behavior by explicitly removing unwanted elements.
11652
+ - Because this commitment is unfinished, keep an eye on future changes before relying on it in production books.
11604
11653
 
11605
11654
  ## Use cases
11606
11655
 
@@ -11608,6 +11657,7 @@ class DeleteCommitmentDefinition extends BaseCommitmentDefinition {
11608
11657
  - Removing conflicting or outdated instructions
11609
11658
  - Disabling specific response patterns
11610
11659
  - Canceling previous formatting or style requirements
11660
+ - Experimenting with low-level prompt rewrites when you know exactly what needs to be removed
11611
11661
 
11612
11662
  ## Examples
11613
11663
 
@@ -11774,11 +11824,10 @@ class DictionaryCommitmentDefinition extends BaseCommitmentDefinition {
11774
11824
  /**
11775
11825
  * FORMAT commitment definition
11776
11826
  *
11777
- * The FORMAT commitment defines the specific output structure and formatting
11778
- * that the agent should use in its responses. This includes data formats,
11779
- * response templates, and structural requirements.
11827
+ * Deprecated legacy commitment for output formatting and response structure.
11828
+ * New books should prefer `WRITING SAMPLE` and `WRITING RULES`.
11780
11829
  *
11781
- * Example usage in agent source:
11830
+ * Legacy example usage in agent source:
11782
11831
  *
11783
11832
  * ```book
11784
11833
  * FORMAT Always respond in JSON format with 'status' and 'data' fields
@@ -11795,7 +11844,16 @@ class FormatCommitmentDefinition extends BaseCommitmentDefinition {
11795
11844
  * Short one-line description of FORMAT.
11796
11845
  */
11797
11846
  get description() {
11798
- return 'Specify output structure or formatting requirements.';
11847
+ return 'Deprecated legacy formatting commitment. Prefer `WRITING SAMPLE` and `WRITING RULES` for new books.';
11848
+ }
11849
+ /**
11850
+ * Optional UI/docs-only deprecation metadata.
11851
+ */
11852
+ get deprecation() {
11853
+ return {
11854
+ message: 'Use `WRITING SAMPLE` and `WRITING RULES` instead.',
11855
+ replacedBy: ['WRITING SAMPLE', 'WRITING RULES'],
11856
+ };
11799
11857
  }
11800
11858
  /**
11801
11859
  * Icon for this commitment.
@@ -11810,31 +11868,39 @@ class FormatCommitmentDefinition extends BaseCommitmentDefinition {
11810
11868
  return spaceTrim$1(`
11811
11869
  # ${this.type}
11812
11870
 
11813
- Defines the specific output structure and formatting for responses (data formats, templates, structure).
11871
+ Deprecated legacy commitment for output formatting and response structure.
11814
11872
 
11815
- ## Key aspects
11873
+ ## Migration
11816
11874
 
11817
- - Both terms work identically and can be used interchangeably.
11818
- - If they are in conflict, the last one takes precedence.
11819
- - You can specify both data formats and presentation styles.
11875
+ - Existing \`${this.type}\` and \`FORMATS\` books still parse and compile.
11876
+ - New books should use \`WRITING RULES\` for formatting or structure constraints and \`WRITING SAMPLE\` when a concrete example communicates the target shape better.
11877
+ - Runtime behavior is intentionally unchanged for backward compatibility.
11820
11878
 
11821
- ## Examples
11879
+ ## Preferred replacement
11822
11880
 
11823
11881
  \`\`\`book
11824
- Customer Support Bot
11882
+ Data Analyst
11825
11883
 
11826
- PERSONA You are a helpful customer support agent
11827
- FORMAT Always respond in JSON format with 'status' and 'data' fields
11828
- FORMAT Use markdown formatting for all code blocks
11884
+ GOAL Present results in a clean, readable structure.
11885
+ WRITING RULES Use markdown headings for sections and bullet points for lists.
11886
+ WRITING RULES Keep tables narrow and readable.
11887
+ WRITING SAMPLE
11888
+ Summary
11889
+ - ...
11890
+ Details
11891
+ - ...
11892
+ Next steps
11893
+ - ...
11829
11894
  \`\`\`
11830
11895
 
11896
+ ## Legacy compatibility example
11897
+
11831
11898
  \`\`\`book
11832
11899
  Data Analyst
11833
11900
 
11834
- PERSONA You are a data analysis expert
11901
+ GOAL Present results in a clean structure.
11835
11902
  FORMAT Present results in structured tables
11836
11903
  FORMAT Include confidence scores for all predictions
11837
- WRITING RULES Be concise and precise in explanations
11838
11904
  \`\`\`
11839
11905
  `);
11840
11906
  }
@@ -12185,6 +12251,12 @@ class GoalCommitmentDefinition extends BaseCommitmentDefinition {
12185
12251
  get description() {
12186
12252
  return 'Define the effective agent **goal**; when multiple goals exist, only the last one stays effective.';
12187
12253
  }
12254
+ /**
12255
+ * Marks GOAL as one of the priority commitments surfaced first in catalogues.
12256
+ */
12257
+ get isImportant() {
12258
+ return true;
12259
+ }
12188
12260
  /**
12189
12261
  * Icon for this commitment.
12190
12262
  */
@@ -12596,6 +12668,12 @@ class KnowledgeCommitmentDefinition extends BaseCommitmentDefinition {
12596
12668
  get description() {
12597
12669
  return 'Add domain **knowledge** via direct text or external sources (RAG).';
12598
12670
  }
12671
+ /**
12672
+ * Marks KNOWLEDGE as one of the priority commitments surfaced first in catalogues.
12673
+ */
12674
+ get isImportant() {
12675
+ return true;
12676
+ }
12599
12677
  /**
12600
12678
  * Icon for this commitment.
12601
12679
  */
@@ -13883,6 +13961,7 @@ class MessageSuffixCommitmentDefinition extends BaseCommitmentDefinition {
13883
13961
  * META commitment definition
13884
13962
  *
13885
13963
  * The META commitment handles all meta-information about the agent such as:
13964
+ * - META AVATAR: Sets the agent's built-in default avatar visual
13886
13965
  * - META IMAGE: Sets the agent's avatar/profile image URL
13887
13966
  * - META LINK: Provides profile/source links for the person the agent models
13888
13967
  * - META DOMAIN: Sets the canonical custom domain/host of the agent
@@ -13897,6 +13976,7 @@ class MessageSuffixCommitmentDefinition extends BaseCommitmentDefinition {
13897
13976
  * Example usage in agent source:
13898
13977
  *
13899
13978
  * ```book
13979
+ * META AVATAR pixel-art
13900
13980
  * META IMAGE https://example.com/avatar.jpg
13901
13981
  * META LINK https://twitter.com/username
13902
13982
  * META DOMAIN my-agent.com
@@ -13936,6 +14016,7 @@ class MetaCommitmentDefinition extends BaseCommitmentDefinition {
13936
14016
 
13937
14017
  ## Supported META types
13938
14018
 
14019
+ - **META AVATAR** - Sets the agent's built-in default avatar visual
13939
14020
  - **META IMAGE** - Sets the agent's avatar/profile image URL
13940
14021
  - **META LINK** - Provides profile/source links for the person the agent models
13941
14022
  - **META DOMAIN** - Sets the canonical custom domain/host of the agent
@@ -13959,6 +14040,7 @@ class MetaCommitmentDefinition extends BaseCommitmentDefinition {
13959
14040
  \`\`\`book
13960
14041
  Professional Assistant
13961
14042
 
14043
+ META AVATAR octopus3
13962
14044
  META IMAGE https://example.com/professional-avatar.jpg
13963
14045
  META TITLE Senior Business Consultant
13964
14046
  META DESCRIPTION Specialized in strategic planning and project management
@@ -14020,12 +14102,3236 @@ class MetaCommitmentDefinition extends BaseCommitmentDefinition {
14020
14102
  * Checks if this is a known meta type
14021
14103
  */
14022
14104
  isKnownMetaType(metaType) {
14023
- const knownTypes = ['IMAGE', 'LINK', 'TITLE', 'DESCRIPTION', 'AUTHOR', 'VERSION', 'LICENSE'];
14105
+ const knownTypes = ['AVATAR', 'IMAGE', 'LINK', 'TITLE', 'DESCRIPTION', 'AUTHOR', 'VERSION', 'LICENSE'];
14024
14106
  return knownTypes.includes(metaType.toUpperCase());
14025
14107
  }
14026
14108
  }
14027
14109
  // Note: [💞] Ignore a discrepancy between file name and entity name
14028
14110
 
14111
+ /**
14112
+ * Makes color transformer which darker the given color
14113
+ *
14114
+ * @param amount from 0 to 1
14115
+ *
14116
+ * @public exported from `@promptbook/color`
14117
+ */
14118
+ function darken(amount) {
14119
+ return lighten(-amount);
14120
+ }
14121
+
14122
+ /* eslint-disable no-magic-numbers */
14123
+ /**
14124
+ * Corner radius ratio used for the common rounded card frame.
14125
+ *
14126
+ * @private utility of the avatar rendering system
14127
+ */
14128
+ const FRAME_RADIUS_RATIO = 0.18;
14129
+ /**
14130
+ * Draws the common rounded background frame used by most visuals.
14131
+ *
14132
+ * @param context Canvas 2D context.
14133
+ * @param size Canvas size in CSS pixels.
14134
+ * @param palette Derived avatar palette.
14135
+ *
14136
+ * @private utility of the avatar rendering system
14137
+ */
14138
+ function drawAvatarFrame(context, size, palette) {
14139
+ if (palette.background === 'transparent' && palette.backgroundSecondary === 'transparent') {
14140
+ return;
14141
+ }
14142
+ const gradient = context.createLinearGradient(0, 0, size, size);
14143
+ gradient.addColorStop(0, palette.background);
14144
+ gradient.addColorStop(1, palette.backgroundSecondary);
14145
+ context.save();
14146
+ createRoundedRectPath(context, 0, 0, size, size, size * FRAME_RADIUS_RATIO);
14147
+ context.fillStyle = gradient;
14148
+ context.fill();
14149
+ context.restore();
14150
+ context.save();
14151
+ context.strokeStyle = 'rgba(255,255,255,0.12)';
14152
+ context.lineWidth = Math.max(1.5, size * 0.012);
14153
+ createRoundedRectPath(context, size * 0.02, size * 0.02, size * 0.96, size * 0.96, size * 0.15);
14154
+ context.stroke();
14155
+ context.restore();
14156
+ }
14157
+ /**
14158
+ * Creates a rounded rectangle path on the current canvas context.
14159
+ *
14160
+ * @param context Canvas 2D context.
14161
+ * @param x Left coordinate.
14162
+ * @param y Top coordinate.
14163
+ * @param width Rectangle width.
14164
+ * @param height Rectangle height.
14165
+ * @param radius Corner radius.
14166
+ *
14167
+ * @private utility of the avatar rendering system
14168
+ */
14169
+ function createRoundedRectPath(context, x, y, width, height, radius) {
14170
+ const normalizedRadius = Math.min(radius, width / 2, height / 2);
14171
+ context.beginPath();
14172
+ context.moveTo(x + normalizedRadius, y);
14173
+ context.arcTo(x + width, y, x + width, y + height, normalizedRadius);
14174
+ context.arcTo(x + width, y + height, x, y + height, normalizedRadius);
14175
+ context.arcTo(x, y + height, x, y, normalizedRadius);
14176
+ context.arcTo(x, y, x + width, y, normalizedRadius);
14177
+ context.closePath();
14178
+ }
14179
+ /**
14180
+ * Picks one deterministic element from a non-empty collection.
14181
+ *
14182
+ * @param items Candidate items.
14183
+ * @param random Seeded random generator.
14184
+ * @returns Picked item.
14185
+ *
14186
+ * @private utility of the avatar rendering system
14187
+ */
14188
+ function pickRandomItem(items, random) {
14189
+ return items[Math.floor(random() * items.length)];
14190
+ }
14191
+
14192
+ /* eslint-disable no-magic-numbers */
14193
+ /**
14194
+ * Builds a smoothly morphing octopus-like silhouette from deterministic parameters.
14195
+ *
14196
+ * @param options Shape construction options.
14197
+ * @returns Closed-loop body points.
14198
+ *
14199
+ * @private shared geometry helper of `octopus2AvatarVisual` and `octopus3AvatarVisual`
14200
+ */
14201
+ function createOrganicOctopusBodyPoints(options) {
14202
+ const { centerX, centerY, bodyRadius, horizontalStretch, verticalStretch, mantleLift, lowerDrop, tentacleDepth, wobbleAmplitude, lobeCount, shapePhase, timeMs, pointCount = 36, } = options;
14203
+ return Array.from({ length: pointCount }, (_, pointIndex) => {
14204
+ const progress = pointIndex / pointCount;
14205
+ const angle = -Math.PI / 2 + progress * Math.PI * 2;
14206
+ const cosine = Math.cos(angle);
14207
+ const sine = Math.sin(angle);
14208
+ const upperFactor = Math.max(0, -sine);
14209
+ const lowerFactor = Math.max(0, sine);
14210
+ const lobeEnvelope = Math.pow(lowerFactor, 1.35);
14211
+ const tentacleWave = Math.max(0, Math.cos(angle * lobeCount + shapePhase + timeMs / 780)) * tentacleDepth * lobeEnvelope;
14212
+ const surfaceWave = Math.sin(angle * 3 + shapePhase + timeMs / 1200) * 0.62 +
14213
+ Math.sin(angle * 5 - shapePhase * 0.7 - timeMs / 910) * 0.38;
14214
+ const breathingWave = Math.sin(timeMs / 960 + shapePhase + angle * 0.45) * wobbleAmplitude;
14215
+ const radius = bodyRadius * (1 + upperFactor * 0.12 + lowerFactor * 0.08 + surfaceWave * 0.05) +
14216
+ tentacleWave +
14217
+ breathingWave;
14218
+ return {
14219
+ x: centerX +
14220
+ cosine * radius * horizontalStretch +
14221
+ Math.sin(angle * 2 + shapePhase) * lobeEnvelope * wobbleAmplitude * 0.7,
14222
+ y: centerY +
14223
+ sine * radius * verticalStretch -
14224
+ upperFactor * mantleLift +
14225
+ lowerFactor * lowerDrop +
14226
+ tentacleWave * 0.28,
14227
+ };
14228
+ });
14229
+ }
14230
+ /**
14231
+ * Traces a smooth closed path through the provided points.
14232
+ *
14233
+ * @param context Canvas 2D context.
14234
+ * @param points Closed-loop points.
14235
+ *
14236
+ * @private shared geometry helper of `octopus2AvatarVisual` and `octopus3AvatarVisual`
14237
+ */
14238
+ function traceSmoothClosedPath(context, points) {
14239
+ const lastPoint = points[points.length - 1];
14240
+ const firstPoint = points[0];
14241
+ const initialMidpoint = {
14242
+ x: (lastPoint.x + firstPoint.x) / 2,
14243
+ y: (lastPoint.y + firstPoint.y) / 2,
14244
+ };
14245
+ context.beginPath();
14246
+ context.moveTo(initialMidpoint.x, initialMidpoint.y);
14247
+ for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
14248
+ const point = points[pointIndex];
14249
+ const nextPoint = points[(pointIndex + 1) % points.length];
14250
+ const midpoint = {
14251
+ x: (point.x + nextPoint.x) / 2,
14252
+ y: (point.y + nextPoint.y) / 2,
14253
+ };
14254
+ context.quadraticCurveTo(point.x, point.y, midpoint.x, midpoint.y);
14255
+ }
14256
+ context.closePath();
14257
+ }
14258
+ /**
14259
+ * Creates deterministic ribbon tentacles for the organic octopus visuals.
14260
+ *
14261
+ * @param options Tentacle construction options.
14262
+ * @returns Tentacle descriptors.
14263
+ *
14264
+ * @private shared geometry helper of `octopus3AvatarVisual` and `asciiOctopusAvatarVisual`
14265
+ */
14266
+ function createOrganicOctopusTentacleShapes(options) {
14267
+ var _a, _b, _c, _d, _e, _f, _g, _h;
14268
+ const { size, centerX, centerY, bodyRadius, horizontalStretch, tentacleCount, shapePhase, createRandom, timeMs, saltPrefix, bodyPoints, variation, } = options;
14269
+ const baseY = centerY + bodyRadius * 0.74;
14270
+ const lowerBodyAnchorPoints = bodyPoints
14271
+ ? resolveTentacleBodyAnchorPoints(bodyPoints, centerY + bodyRadius * 0.04)
14272
+ : null;
14273
+ const flowLengthScale = (_a = variation === null || variation === void 0 ? void 0 : variation.flowLengthScale) !== null && _a !== void 0 ? _a : 1;
14274
+ const lateralReachScale = (_b = variation === null || variation === void 0 ? void 0 : variation.lateralReachScale) !== null && _b !== void 0 ? _b : 1;
14275
+ const tipReachScale = (_c = variation === null || variation === void 0 ? void 0 : variation.tipReachScale) !== null && _c !== void 0 ? _c : 1;
14276
+ const baseWidthScale = (_d = variation === null || variation === void 0 ? void 0 : variation.baseWidthScale) !== null && _d !== void 0 ? _d : 1;
14277
+ const tipWidthScale = (_e = variation === null || variation === void 0 ? void 0 : variation.tipWidthScale) !== null && _e !== void 0 ? _e : 1;
14278
+ const rootSpreadScale = (_f = variation === null || variation === void 0 ? void 0 : variation.rootSpreadScale) !== null && _f !== void 0 ? _f : 1;
14279
+ const startYOffsetScale = (_g = variation === null || variation === void 0 ? void 0 : variation.startYOffsetScale) !== null && _g !== void 0 ? _g : 1;
14280
+ const swayScale = (_h = variation === null || variation === void 0 ? void 0 : variation.swayScale) !== null && _h !== void 0 ? _h : 1;
14281
+ return Array.from({ length: tentacleCount }, (_, tentacleIndex) => {
14282
+ const tentacleRandom = createRandom(`${saltPrefix}-tentacle-${tentacleIndex}`);
14283
+ const spreadProgress = tentacleCount === 1 ? 0.5 : tentacleIndex / (tentacleCount - 1);
14284
+ const centeredProgress = spreadProgress - 0.5;
14285
+ const spreadCenteredProgress = centeredProgress * rootSpreadScale;
14286
+ const spreadAnchorProgress = Math.min(1, Math.max(0, 0.5 + spreadCenteredProgress));
14287
+ const temporalSway = Math.sin(timeMs / (720 + tentacleIndex * 34) + shapePhase + tentacleRandom() * Math.PI * 2) *
14288
+ size *
14289
+ (0.014 + tentacleRandom() * 0.015) *
14290
+ swayScale;
14291
+ const flowLength = size * (0.24 + tentacleRandom() * 0.18) * flowLengthScale;
14292
+ const curlDirection = spreadCenteredProgress === 0 ? (tentacleRandom() < 0.5 ? -1 : 1) : Math.sign(spreadCenteredProgress);
14293
+ const lateralReach = spreadCenteredProgress * size * (0.1 + tentacleRandom() * 0.1) * lateralReachScale + temporalSway;
14294
+ const tipReach = curlDirection * size * (0.025 + tentacleRandom() * 0.07) * tipReachScale;
14295
+ const startYOffset = (Math.abs(spreadCenteredProgress) * size * 0.012 + tentacleRandom() * size * 0.01) * startYOffsetScale;
14296
+ const startPoint = lowerBodyAnchorPoints && lowerBodyAnchorPoints.length >= 2
14297
+ ? createInsetTentacleStartPoint({
14298
+ bodyPoints: lowerBodyAnchorPoints,
14299
+ anchorProgress: spreadAnchorProgress,
14300
+ centerX,
14301
+ centerY,
14302
+ bodyRadius,
14303
+ centeredProgress: spreadCenteredProgress,
14304
+ startYOffset,
14305
+ })
14306
+ : {
14307
+ x: centerX + spreadCenteredProgress * bodyRadius * horizontalStretch * 1.52,
14308
+ y: baseY + startYOffset,
14309
+ };
14310
+ const controlPointOne = {
14311
+ x: startPoint.x + spreadCenteredProgress * size * 0.045 * lateralReachScale + temporalSway * 0.4,
14312
+ y: startPoint.y + flowLength * (0.21 + tentacleRandom() * 0.08),
14313
+ };
14314
+ const controlPointTwo = {
14315
+ x: startPoint.x + lateralReach + tipReach,
14316
+ y: startPoint.y + flowLength * (0.62 + tentacleRandom() * 0.12),
14317
+ };
14318
+ const endPoint = {
14319
+ x: startPoint.x + lateralReach + tipReach * 1.2,
14320
+ y: startPoint.y +
14321
+ flowLength * (0.9 + tentacleRandom() * 0.12) +
14322
+ Math.cos(timeMs / (840 + tentacleIndex * 41) + shapePhase) * size * (0.008 + tentacleRandom() * 0.01),
14323
+ };
14324
+ const baseWidth = size * (0.038 + tentacleRandom() * 0.02) * (1 - Math.abs(spreadCenteredProgress) * 0.18) * baseWidthScale;
14325
+ const tipWidth = baseWidth * Math.min(0.52, (0.18 + tentacleRandom() * 0.2) * tipWidthScale);
14326
+ return {
14327
+ startPoint,
14328
+ controlPointOne,
14329
+ controlPointTwo,
14330
+ endPoint,
14331
+ baseWidth,
14332
+ tipWidth,
14333
+ colorBias: tentacleRandom(),
14334
+ highlightBias: tentacleRandom(),
14335
+ sampleCount: 14 + Math.floor(tentacleRandom() * 6),
14336
+ };
14337
+ });
14338
+ }
14339
+ /**
14340
+ * Narrows the body contour to lower anchor points that can safely host tentacle roots.
14341
+ *
14342
+ * @param bodyPoints Generated closed-loop body points.
14343
+ * @param lowerBodyThresholdY Minimum Y coordinate considered part of the lower mantle.
14344
+ * @returns Body points sorted from left to right across the lower silhouette.
14345
+ *
14346
+ * @private shared geometry helper of `octopus3AvatarVisual`
14347
+ */
14348
+ function resolveTentacleBodyAnchorPoints(bodyPoints, lowerBodyThresholdY) {
14349
+ const lowerBodyPoints = bodyPoints
14350
+ .filter((bodyPoint) => bodyPoint.y >= lowerBodyThresholdY)
14351
+ .sort((leftPoint, rightPoint) => leftPoint.x - rightPoint.x);
14352
+ if (lowerBodyPoints.length >= 2) {
14353
+ return lowerBodyPoints;
14354
+ }
14355
+ return [...bodyPoints].sort((leftPoint, rightPoint) => leftPoint.x - rightPoint.x);
14356
+ }
14357
+ /**
14358
+ * Resolves one tentacle root from the provided lower body contour and nudges it inside the mantle.
14359
+ *
14360
+ * @param options Tentacle anchor options.
14361
+ * @returns Tentacle start point safely embedded inside the body silhouette.
14362
+ *
14363
+ * @private shared geometry helper of `octopus3AvatarVisual`
14364
+ */
14365
+ function createInsetTentacleStartPoint(options) {
14366
+ const { bodyPoints, anchorProgress, centerX, centerY, bodyRadius, centeredProgress, startYOffset } = options;
14367
+ const clampedAnchorProgress = Math.min(0.94, Math.max(0.06, anchorProgress));
14368
+ const bodyAnchorPoint = interpolatePointAlongTentacleAnchors(bodyPoints, clampedAnchorProgress);
14369
+ const inwardX = centerX - bodyAnchorPoint.x;
14370
+ const inwardY = centerY + bodyRadius * 0.08 - bodyAnchorPoint.y;
14371
+ const inwardLength = Math.hypot(inwardX, inwardY) || 1;
14372
+ const insetDistance = bodyRadius * (0.12 + Math.abs(centeredProgress) * 0.05) + startYOffset * 0.32;
14373
+ return {
14374
+ x: bodyAnchorPoint.x + (inwardX / inwardLength) * insetDistance,
14375
+ y: bodyAnchorPoint.y + (inwardY / inwardLength) * insetDistance,
14376
+ };
14377
+ }
14378
+ /**
14379
+ * Interpolates one left-to-right anchor point along the prepared lower body contour.
14380
+ *
14381
+ * @param bodyPoints Lower body contour points sorted from left to right.
14382
+ * @param progress Interpolation progress in the range `[0, 1]`.
14383
+ * @returns Interpolated anchor point.
14384
+ *
14385
+ * @private shared geometry helper of `octopus3AvatarVisual`
14386
+ */
14387
+ function interpolatePointAlongTentacleAnchors(bodyPoints, progress) {
14388
+ if (bodyPoints.length === 1) {
14389
+ return bodyPoints[0];
14390
+ }
14391
+ const anchorIndex = progress * (bodyPoints.length - 1);
14392
+ const startIndex = Math.floor(anchorIndex);
14393
+ const endIndex = Math.min(bodyPoints.length - 1, startIndex + 1);
14394
+ const blend = anchorIndex - startIndex;
14395
+ const startPoint = bodyPoints[startIndex];
14396
+ const endPoint = bodyPoints[endIndex];
14397
+ return {
14398
+ x: startPoint.x + (endPoint.x - startPoint.x) * blend,
14399
+ y: startPoint.y + (endPoint.y - startPoint.y) * blend,
14400
+ };
14401
+ }
14402
+ /**
14403
+ * Samples the cubic tentacle centerline and offsets normals to build a filled ribbon.
14404
+ *
14405
+ * @param tentacleShape Deterministic tentacle descriptor.
14406
+ * @returns Sampled ribbon points.
14407
+ *
14408
+ * @private shared geometry helper of `octopus3AvatarVisual` and `asciiOctopusAvatarVisual`
14409
+ */
14410
+ function sampleOrganicTentacleRibbonPoints(tentacleShape) {
14411
+ return Array.from({ length: tentacleShape.sampleCount + 1 }, (_, sampleIndex) => {
14412
+ const progress = sampleIndex / tentacleShape.sampleCount;
14413
+ const point = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, progress);
14414
+ const previousPoint = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, Math.max(0, progress - 0.04));
14415
+ const nextPoint = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, Math.min(1, progress + 0.04));
14416
+ const tangentX = nextPoint.x - previousPoint.x;
14417
+ const tangentY = nextPoint.y - previousPoint.y;
14418
+ const tangentLength = Math.hypot(tangentX, tangentY) || 1;
14419
+ const width = tentacleShape.baseWidth + (tentacleShape.tipWidth - tentacleShape.baseWidth) * Math.pow(progress, 1.1);
14420
+ return {
14421
+ x: point.x,
14422
+ y: point.y,
14423
+ normalX: -tangentY / tangentLength,
14424
+ normalY: tangentX / tangentLength,
14425
+ width,
14426
+ progress,
14427
+ };
14428
+ });
14429
+ }
14430
+ /**
14431
+ * Resolves smooth pupil offsets that blend autonomous idle drift with live viewer tracking.
14432
+ *
14433
+ * @param options Eye motion options.
14434
+ * @returns Resolved pupil offsets.
14435
+ *
14436
+ * @private shared geometry helper of octopus avatar visuals
14437
+ */
14438
+ function resolveOrganicEyeMotion(options) {
14439
+ const { radiusX, radiusY, timeMs, phase, interaction, autonomousDriftRatioX = 0.12, autonomousDriftRatioY = 0.08, } = options;
14440
+ const autonomousOffsetX = Math.sin(timeMs / 1280 + phase) * radiusX * autonomousDriftRatioX;
14441
+ const autonomousOffsetY = Math.cos(timeMs / 940 + phase) * radiusY * autonomousDriftRatioY;
14442
+ const interactionBlend = Math.min(1, interaction.intensity * 0.9);
14443
+ return {
14444
+ pupilOffsetX: autonomousOffsetX * (1 - interactionBlend) + interaction.gazeX * radiusX * (0.18 + interactionBlend * 0.18),
14445
+ pupilOffsetY: autonomousOffsetY * (1 - interactionBlend) + interaction.gazeY * radiusY * (0.16 + interactionBlend * 0.16),
14446
+ };
14447
+ }
14448
+ /**
14449
+ * Samples one point on a cubic Bezier curve.
14450
+ *
14451
+ * @param startPoint Curve start point.
14452
+ * @param controlPointOne First control point.
14453
+ * @param controlPointTwo Second control point.
14454
+ * @param endPoint Curve end point.
14455
+ * @param progress Sampling progress in the range `[0, 1]`.
14456
+ * @returns Sampled point.
14457
+ *
14458
+ * @private shared geometry helper of `octopus3AvatarVisual`
14459
+ */
14460
+ function getCubicBezierPoint(startPoint, controlPointOne, controlPointTwo, endPoint, progress) {
14461
+ const inverseProgress = 1 - progress;
14462
+ return {
14463
+ x: inverseProgress * inverseProgress * inverseProgress * startPoint.x +
14464
+ 3 * inverseProgress * inverseProgress * progress * controlPointOne.x +
14465
+ 3 * inverseProgress * progress * progress * controlPointTwo.x +
14466
+ progress * progress * progress * endPoint.x,
14467
+ y: inverseProgress * inverseProgress * inverseProgress * startPoint.y +
14468
+ 3 * inverseProgress * inverseProgress * progress * controlPointOne.y +
14469
+ 3 * inverseProgress * progress * progress * controlPointTwo.y +
14470
+ progress * progress * progress * endPoint.y,
14471
+ };
14472
+ }
14473
+
14474
+ /* eslint-disable no-magic-numbers */
14475
+ /**
14476
+ * Glyph ramp used for the main octopus body fill.
14477
+ *
14478
+ * @private helper of `asciiOctopusAvatarVisual`
14479
+ */
14480
+ const BODY_GLYPHS = ['.', ':', '-', '=', '+', '*', '#', '%', '@'];
14481
+ /**
14482
+ * Glyph ramp used on silhouette edges so the ASCII blob stays legible.
14483
+ *
14484
+ * @private helper of `asciiOctopusAvatarVisual`
14485
+ */
14486
+ const OUTLINE_GLYPHS = ['#', '%', '@'];
14487
+ /**
14488
+ * Glyph ramp used in the surrounding atmosphere.
14489
+ *
14490
+ * @private helper of `asciiOctopusAvatarVisual`
14491
+ */
14492
+ const ATMOSPHERE_GLYPHS = ['.', ':', "'", '`'];
14493
+ /**
14494
+ * AsciiOctopus avatar visual.
14495
+ *
14496
+ * @private built-in avatar visual
14497
+ */
14498
+ const asciiOctopusAvatarVisual = {
14499
+ id: 'ascii-octopus',
14500
+ title: 'AsciiOctopus',
14501
+ description: 'Morphing alien octopus translated into animated ASCII glyphs with responsive eyes and seeded geometry.',
14502
+ isAnimated: true,
14503
+ supportsPointerTracking: true,
14504
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
14505
+ const gridRandom = createRandom('ascii-octopus-grid');
14506
+ const staticRandom = createRandom('ascii-octopus-static');
14507
+ const gridMetrics = createAsciiGridMetrics(size, gridRandom);
14508
+ const layout = createAsciiOctopusLayout(size, timeMs, createRandom, staticRandom, interaction);
14509
+ drawAvatarFrame(context, size, palette);
14510
+ drawAsciiBackdrop(context, size, palette, layout, timeMs);
14511
+ context.save();
14512
+ context.font = `600 ${gridMetrics.fontSize}px monospace`;
14513
+ context.textAlign = 'center';
14514
+ context.textBaseline = 'middle';
14515
+ // The ASCII renderer samples the morphing octopus field on a low-resolution grid so the shape stays organic
14516
+ // while the glyph layout remains deterministic for the same avatar input.
14517
+ const cellRandom = createRandom('ascii-octopus-cells');
14518
+ for (let rowIndex = 0; rowIndex < gridMetrics.rowCount; rowIndex++) {
14519
+ for (let columnIndex = 0; columnIndex < gridMetrics.columnCount; columnIndex++) {
14520
+ const point = {
14521
+ x: gridMetrics.offsetX + columnIndex * gridMetrics.cellWidth,
14522
+ y: gridMetrics.offsetY + rowIndex * gridMetrics.cellHeight,
14523
+ };
14524
+ const noise = cellRandom();
14525
+ const glyphDescriptor = resolveAsciiGlyph({
14526
+ point,
14527
+ layout,
14528
+ palette,
14529
+ cellWidth: gridMetrics.cellWidth,
14530
+ cellHeight: gridMetrics.cellHeight,
14531
+ noise,
14532
+ timeMs,
14533
+ });
14534
+ if (!glyphDescriptor) {
14535
+ continue;
14536
+ }
14537
+ context.fillStyle = glyphDescriptor.color;
14538
+ context.fillText(glyphDescriptor.character, point.x, point.y);
14539
+ }
14540
+ }
14541
+ context.restore();
14542
+ },
14543
+ };
14544
+ /**
14545
+ * Draws the dark terminal-like glow behind the ASCII octopus.
14546
+ *
14547
+ * @param context Canvas 2D context.
14548
+ * @param size Canvas size in CSS pixels.
14549
+ * @param palette Derived avatar palette.
14550
+ * @param layout Prepared octopus layout.
14551
+ * @param timeMs Current animation time in milliseconds.
14552
+ *
14553
+ * @private helper of `asciiOctopusAvatarVisual`
14554
+ */
14555
+ function drawAsciiBackdrop(context, size, palette, layout, timeMs) {
14556
+ const haloGradient = context.createRadialGradient(layout.centerX, layout.centerY - size * 0.12, size * 0.06, layout.centerX, layout.centerY, size * 0.62);
14557
+ haloGradient.addColorStop(0, `${palette.highlight}26`);
14558
+ haloGradient.addColorStop(0.42, `${palette.accent}16`);
14559
+ haloGradient.addColorStop(1, `${palette.highlight}00`);
14560
+ context.fillStyle = haloGradient;
14561
+ context.fillRect(0, 0, size, size);
14562
+ const lowerGlowGradient = context.createRadialGradient(layout.centerX + Math.sin(timeMs / 1100 + layout.shapePhase) * size * 0.03, layout.centerY + size * 0.2, size * 0.05, layout.centerX, layout.centerY + size * 0.24, size * 0.46);
14563
+ lowerGlowGradient.addColorStop(0, `${palette.secondary}1f`);
14564
+ lowerGlowGradient.addColorStop(1, `${palette.secondary}00`);
14565
+ context.fillStyle = lowerGlowGradient;
14566
+ context.fillRect(0, 0, size, size);
14567
+ context.beginPath();
14568
+ context.ellipse(layout.centerX, layout.centerY + size * 0.29, size * 0.23, size * 0.065, 0, 0, Math.PI * 2);
14569
+ context.fillStyle = `${palette.shadow}33`;
14570
+ context.fill();
14571
+ }
14572
+ /**
14573
+ * Resolves the ASCII character that should be drawn for one sampled cell.
14574
+ *
14575
+ * @param options Cell evaluation options.
14576
+ * @returns Character descriptor or `null` when the cell should stay empty.
14577
+ *
14578
+ * @private helper of `asciiOctopusAvatarVisual`
14579
+ */
14580
+ function resolveAsciiGlyph(options) {
14581
+ const { point, layout, palette, cellWidth, cellHeight, noise, timeMs } = options;
14582
+ const eyeGlyphDescriptor = resolveEyeGlyph(point, layout.leftEye, layout.interaction, palette, timeMs) ||
14583
+ resolveEyeGlyph(point, layout.rightEye, layout.interaction, palette, timeMs);
14584
+ if (eyeGlyphDescriptor) {
14585
+ return eyeGlyphDescriptor;
14586
+ }
14587
+ const mouthGlyphDescriptor = resolveMouthGlyph(point, layout, palette, cellHeight);
14588
+ if (mouthGlyphDescriptor) {
14589
+ return mouthGlyphDescriptor;
14590
+ }
14591
+ const isWithinOctopusBounds = point.x >= layout.leftBound &&
14592
+ point.x <= layout.rightBound &&
14593
+ point.y >= layout.topBound &&
14594
+ point.y <= layout.bottomBound;
14595
+ if (!isWithinOctopusBounds) {
14596
+ return resolveAtmosphereGlyph(point, layout, palette, noise, timeMs);
14597
+ }
14598
+ const isInsideBody = isPointInsidePolygon(point, layout.bodyPoints);
14599
+ const bodyEdgeDistance = isInsideBody
14600
+ ? getDistanceToPolyline(point, layout.bodyPoints, true)
14601
+ : Number.POSITIVE_INFINITY;
14602
+ const tentacleCoverage = measureTentacleCoverage(point, layout.sampledTentacles, cellWidth);
14603
+ if (isInsideBody || tentacleCoverage) {
14604
+ return resolveOctopusSurfaceGlyph({
14605
+ point,
14606
+ layout,
14607
+ palette,
14608
+ isInsideBody,
14609
+ bodyEdgeDistance,
14610
+ tentacleCoverage,
14611
+ cellWidth,
14612
+ cellHeight,
14613
+ noise,
14614
+ timeMs,
14615
+ });
14616
+ }
14617
+ return resolveAtmosphereGlyph(point, layout, palette, noise, timeMs);
14618
+ }
14619
+ /**
14620
+ * Resolves the ASCII character for one eye cell.
14621
+ *
14622
+ * @param point Sampled cell point.
14623
+ * @param eyeFeature Eye geometry.
14624
+ * @param palette Derived avatar palette.
14625
+ * @param timeMs Current animation time in milliseconds.
14626
+ * @returns Eye glyph descriptor or `null`.
14627
+ *
14628
+ * @private helper of `asciiOctopusAvatarVisual`
14629
+ */
14630
+ function resolveEyeGlyph(point, eyeFeature, interaction, palette, timeMs) {
14631
+ const { pupilOffsetX, pupilOffsetY } = resolveOrganicEyeMotion({
14632
+ radiusX: eyeFeature.radiusX,
14633
+ radiusY: eyeFeature.radiusY,
14634
+ timeMs,
14635
+ phase: eyeFeature.phase,
14636
+ interaction,
14637
+ });
14638
+ const scleraDistance = measureRotatedEllipseDistance(point, eyeFeature.centerX, eyeFeature.centerY, eyeFeature.radiusX, eyeFeature.radiusY, eyeFeature.rotation);
14639
+ if (scleraDistance > 1.08) {
14640
+ return null;
14641
+ }
14642
+ const highlightDistance = measureRotatedEllipseDistance(point, eyeFeature.centerX + pupilOffsetX - eyeFeature.radiusX * 0.24, eyeFeature.centerY + pupilOffsetY - eyeFeature.radiusY * 0.26, eyeFeature.radiusX * 0.18, eyeFeature.radiusY * 0.14, eyeFeature.rotation);
14643
+ if (highlightDistance <= 1) {
14644
+ return { character: '*', color: '#ffffff' };
14645
+ }
14646
+ const pupilDistance = measureRotatedEllipseDistance(point, eyeFeature.centerX + pupilOffsetX, eyeFeature.centerY + pupilOffsetY, eyeFeature.radiusX * 0.2, eyeFeature.radiusY * 0.48, eyeFeature.rotation);
14647
+ if (pupilDistance <= 1) {
14648
+ return { character: '@', color: palette.ink };
14649
+ }
14650
+ const irisDistance = measureRotatedEllipseDistance(point, eyeFeature.centerX + pupilOffsetX, eyeFeature.centerY + pupilOffsetY, eyeFeature.radiusX * 0.64, eyeFeature.radiusY * 0.72, eyeFeature.rotation);
14651
+ if (irisDistance <= 1) {
14652
+ return {
14653
+ character: irisDistance < 0.46 ? '0' : 'o',
14654
+ color: irisDistance < 0.62 ? palette.secondary : `${palette.highlight}d9`,
14655
+ };
14656
+ }
14657
+ return {
14658
+ character: scleraDistance > 0.82 ? 'o' : '0',
14659
+ color: '#f8fbff',
14660
+ };
14661
+ }
14662
+ /**
14663
+ * Resolves the ASCII character for the octopus mouth.
14664
+ *
14665
+ * @param point Sampled cell point.
14666
+ * @param layout Prepared octopus layout.
14667
+ * @param palette Derived avatar palette.
14668
+ * @param cellHeight Character cell height.
14669
+ * @returns Mouth glyph descriptor or `null`.
14670
+ *
14671
+ * @private helper of `asciiOctopusAvatarVisual`
14672
+ */
14673
+ function resolveMouthGlyph(point, layout, palette, cellHeight) {
14674
+ const mouthDistance = getDistanceToPolyline(point, layout.mouthPoints, false);
14675
+ if (mouthDistance > cellHeight * 0.38) {
14676
+ return null;
14677
+ }
14678
+ const horizontalProgress = clamp01((point.x - layout.mouthPoints[0].x) /
14679
+ (layout.mouthPoints[layout.mouthPoints.length - 1].x - layout.mouthPoints[0].x));
14680
+ let character = '-';
14681
+ if (horizontalProgress < 0.28) {
14682
+ character = '\\';
14683
+ }
14684
+ else if (horizontalProgress > 0.72) {
14685
+ character = '/';
14686
+ }
14687
+ else if (horizontalProgress > 0.42 && horizontalProgress < 0.58) {
14688
+ character = '_';
14689
+ }
14690
+ return {
14691
+ character,
14692
+ color: `${palette.ink}bf`,
14693
+ };
14694
+ }
14695
+ /**
14696
+ * Resolves the ASCII character for body and tentacle cells.
14697
+ *
14698
+ * @param options Surface evaluation options.
14699
+ * @returns Surface glyph descriptor.
14700
+ *
14701
+ * @private helper of `asciiOctopusAvatarVisual`
14702
+ */
14703
+ function resolveOctopusSurfaceGlyph(options) {
14704
+ const { point, layout, palette, isInsideBody, bodyEdgeDistance, tentacleCoverage, cellHeight, noise, timeMs } = options;
14705
+ const isTentacleDominant = tentacleCoverage !== null && (!isInsideBody || point.y > layout.centerY + layout.bodyRadius * 0.08);
14706
+ if (isTentacleDominant && tentacleCoverage) {
14707
+ const isSuckerBand = tentacleCoverage.progress > 0.24 && tentacleCoverage.progress < 0.82 && noise > 0.78;
14708
+ if (isSuckerBand && tentacleCoverage.normalizedDistance > 0.42) {
14709
+ return {
14710
+ character: noise > 0.9 ? '0' : 'o',
14711
+ color: `${palette.highlight}d0`,
14712
+ };
14713
+ }
14714
+ return {
14715
+ character: pickTentacleCharacter(tentacleCoverage, noise),
14716
+ color: tentacleCoverage.progress < 0.24
14717
+ ? `${palette.secondary}c7`
14718
+ : tentacleCoverage.progress > 0.72
14719
+ ? `${palette.accent}bf`
14720
+ : tentacleCoverage.normalizedDistance > 0.7
14721
+ ? `${palette.highlight}bf`
14722
+ : `${palette.primary}c9`,
14723
+ };
14724
+ }
14725
+ const highlightBias = clamp01((layout.centerY - point.y + layout.bodyRadius * 0.44) / (layout.bodyRadius * 1.14));
14726
+ const bodyDepth = clamp01(1 - bodyEdgeDistance / (layout.bodyRadius * 0.9));
14727
+ const shimmer = Math.sin(timeMs / 720 + point.x * 0.085 + point.y * 0.06 + layout.shapePhase) * 0.05;
14728
+ const bodyIntensity = clamp01(0.22 + bodyDepth * 0.58 + highlightBias * 0.2 + shimmer + (noise - 0.5) * 0.18);
14729
+ const isOutline = isInsideBody && bodyEdgeDistance < cellHeight * 0.54;
14730
+ const character = isOutline
14731
+ ? pickRampCharacter(OUTLINE_GLYPHS, clamp01(0.58 + bodyIntensity * 0.42))
14732
+ : pickRampCharacter(BODY_GLYPHS, bodyIntensity);
14733
+ let color = `${palette.primary}bf`;
14734
+ if (highlightBias > 0.76) {
14735
+ color = `${palette.highlight}d9`;
14736
+ }
14737
+ else if (bodyDepth > 0.7) {
14738
+ color = `${palette.secondary}cb`;
14739
+ }
14740
+ else if ((point.x < layout.centerX && noise > 0.58) || (point.x >= layout.centerX && noise < 0.42)) {
14741
+ color = `${palette.accent}ba`;
14742
+ }
14743
+ return {
14744
+ character,
14745
+ color: isOutline ? `${palette.highlight}c9` : color,
14746
+ };
14747
+ }
14748
+ /**
14749
+ * Resolves faint atmosphere glyphs around the ASCII octopus.
14750
+ *
14751
+ * @param point Sampled cell point.
14752
+ * @param layout Prepared octopus layout.
14753
+ * @param palette Derived avatar palette.
14754
+ * @param noise Stable per-cell noise.
14755
+ * @param timeMs Current animation time in milliseconds.
14756
+ * @returns Atmosphere glyph descriptor or `null`.
14757
+ *
14758
+ * @private helper of `asciiOctopusAvatarVisual`
14759
+ */
14760
+ function resolveAtmosphereGlyph(point, layout, palette, noise, timeMs) {
14761
+ const horizontalDistance = Math.abs(point.x - layout.centerX) / (layout.bodyRadius * layout.horizontalStretch * 2.2);
14762
+ const verticalDistance = Math.abs(point.y - (layout.centerY + layout.bodyRadius * 0.04)) / (layout.bodyRadius * 2.1);
14763
+ const haloDistance = Math.hypot(horizontalDistance, verticalDistance);
14764
+ const shimmer = Math.sin(timeMs / 1450 + point.x * 0.03 + point.y * 0.04 + layout.shapePhase) * 0.06;
14765
+ const density = clamp01(1.16 - haloDistance + shimmer + (noise - 0.5) * 0.14);
14766
+ if (density < 0.18 || noise > density * 0.84 + 0.34) {
14767
+ return null;
14768
+ }
14769
+ return {
14770
+ character: pickRampCharacter(ATMOSPHERE_GLYPHS, density),
14771
+ color: point.y < layout.centerY ? `${palette.highlight}63` : `${palette.accent}47`,
14772
+ };
14773
+ }
14774
+ /**
14775
+ * Builds the grid used by the ASCII renderer.
14776
+ *
14777
+ * @param size Canvas size in CSS pixels.
14778
+ * @param staticRandom Stable random generator for this avatar.
14779
+ * @returns Grid metrics.
14780
+ *
14781
+ * @private helper of `asciiOctopusAvatarVisual`
14782
+ */
14783
+ function createAsciiGridMetrics(size, staticRandom) {
14784
+ const fontSize = Math.max(9, Math.round(size * (0.048 + staticRandom() * 0.006)));
14785
+ const cellWidth = fontSize * (0.58 + staticRandom() * 0.04);
14786
+ const cellHeight = fontSize * 0.82;
14787
+ const columnCount = Math.max(12, Math.floor(size / cellWidth) - 1);
14788
+ const rowCount = Math.max(12, Math.floor(size / cellHeight) - 1);
14789
+ const offsetX = (size - (columnCount - 1) * cellWidth) / 2;
14790
+ const offsetY = (size - (rowCount - 1) * cellHeight) / 2;
14791
+ return {
14792
+ fontSize,
14793
+ cellWidth,
14794
+ cellHeight,
14795
+ columnCount,
14796
+ rowCount,
14797
+ offsetX,
14798
+ offsetY,
14799
+ };
14800
+ }
14801
+ /**
14802
+ * Builds the deterministic octopus geometry that will later be sampled into ASCII cells.
14803
+ *
14804
+ * @param size Canvas size in CSS pixels.
14805
+ * @param timeMs Current animation time in milliseconds.
14806
+ * @param createRandom Seeded random factory scoped to the avatar.
14807
+ * @param staticRandom Stable random generator for this avatar.
14808
+ * @returns Prepared octopus layout.
14809
+ *
14810
+ * @private helper of `asciiOctopusAvatarVisual`
14811
+ */
14812
+ function createAsciiOctopusLayout(size, timeMs, createRandom, staticRandom, interaction) {
14813
+ const centerX = size * (0.5 + (staticRandom() - 0.5) * 0.02) + interaction.bodyOffsetX * size * 0.05;
14814
+ const centerY = size * (0.41 + staticRandom() * 0.05) + interaction.bodyOffsetY * size * 0.035;
14815
+ const bodyRadius = size * (0.195 + staticRandom() * 0.05);
14816
+ const horizontalStretch = 1.08 + staticRandom() * 0.22;
14817
+ const verticalStretch = 0.88 + staticRandom() * 0.14;
14818
+ const mantleLift = size * (0.1 + staticRandom() * 0.03);
14819
+ const lowerDrop = size * (0.03 + staticRandom() * 0.024);
14820
+ const tentacleDepth = size * (0.026 + staticRandom() * 0.022);
14821
+ const wobbleAmplitude = size * (0.008 + staticRandom() * 0.01);
14822
+ const lobeCount = 5 + Math.floor(staticRandom() * 4);
14823
+ const shapePhase = staticRandom() * Math.PI * 2;
14824
+ const tentacleCount = 8 + Math.floor(staticRandom() * 5);
14825
+ const eyeSpacing = size * (0.108 + staticRandom() * 0.042);
14826
+ const eyeRadiusX = size * (0.05 + staticRandom() * 0.015);
14827
+ const eyeRadiusY = eyeRadiusX * (1.16 + staticRandom() * 0.2);
14828
+ const bodyPoints = createOrganicOctopusBodyPoints({
14829
+ centerX,
14830
+ centerY,
14831
+ bodyRadius,
14832
+ horizontalStretch,
14833
+ verticalStretch,
14834
+ mantleLift,
14835
+ lowerDrop,
14836
+ tentacleDepth,
14837
+ wobbleAmplitude,
14838
+ lobeCount,
14839
+ shapePhase,
14840
+ timeMs,
14841
+ pointCount: 40,
14842
+ });
14843
+ const tentacleShapes = createOrganicOctopusTentacleShapes({
14844
+ size,
14845
+ centerX,
14846
+ centerY,
14847
+ bodyRadius,
14848
+ horizontalStretch,
14849
+ tentacleCount,
14850
+ shapePhase,
14851
+ createRandom,
14852
+ timeMs,
14853
+ saltPrefix: 'ascii-octopus',
14854
+ bodyPoints,
14855
+ });
14856
+ const sampledTentacles = tentacleShapes.map(sampleOrganicTentacleRibbonPoints);
14857
+ const leftEye = {
14858
+ centerX: centerX - eyeSpacing,
14859
+ centerY: centerY - size * 0.01,
14860
+ radiusX: eyeRadiusX,
14861
+ radiusY: eyeRadiusY,
14862
+ rotation: (staticRandom() - 0.5) * 0.24,
14863
+ phase: shapePhase,
14864
+ };
14865
+ const rightEye = {
14866
+ centerX: centerX + eyeSpacing,
14867
+ centerY: centerY - size * 0.01,
14868
+ radiusX: eyeRadiusX,
14869
+ radiusY: eyeRadiusY,
14870
+ rotation: (staticRandom() - 0.5) * 0.24,
14871
+ phase: shapePhase + Math.PI / 4,
14872
+ };
14873
+ const mouthPoints = sampleQuadraticBezierPoints({ x: centerX - size * 0.074, y: centerY + size * 0.092 }, {
14874
+ x: centerX,
14875
+ y: centerY +
14876
+ size * (0.142 + Math.sin(timeMs / 620 + shapePhase) * 0.016) +
14877
+ interaction.gazeY * size * 0.012,
14878
+ }, { x: centerX + size * 0.074, y: centerY + size * 0.092 }, 12);
14879
+ let leftBound = Number.POSITIVE_INFINITY;
14880
+ let rightBound = Number.NEGATIVE_INFINITY;
14881
+ let topBound = Number.POSITIVE_INFINITY;
14882
+ let bottomBound = Number.NEGATIVE_INFINITY;
14883
+ for (const bodyPoint of bodyPoints) {
14884
+ leftBound = Math.min(leftBound, bodyPoint.x);
14885
+ rightBound = Math.max(rightBound, bodyPoint.x);
14886
+ topBound = Math.min(topBound, bodyPoint.y);
14887
+ bottomBound = Math.max(bottomBound, bodyPoint.y);
14888
+ }
14889
+ for (const sampledTentacle of sampledTentacles) {
14890
+ for (const ribbonPoint of sampledTentacle) {
14891
+ leftBound = Math.min(leftBound, ribbonPoint.x - ribbonPoint.width);
14892
+ rightBound = Math.max(rightBound, ribbonPoint.x + ribbonPoint.width);
14893
+ topBound = Math.min(topBound, ribbonPoint.y - ribbonPoint.width);
14894
+ bottomBound = Math.max(bottomBound, ribbonPoint.y + ribbonPoint.width);
14895
+ }
14896
+ }
14897
+ return {
14898
+ centerX,
14899
+ centerY,
14900
+ bodyRadius,
14901
+ horizontalStretch,
14902
+ shapePhase,
14903
+ interaction,
14904
+ bodyPoints,
14905
+ sampledTentacles,
14906
+ leftEye,
14907
+ rightEye,
14908
+ mouthPoints,
14909
+ leftBound: leftBound - size * 0.08,
14910
+ rightBound: rightBound + size * 0.08,
14911
+ topBound: topBound - size * 0.08,
14912
+ bottomBound: bottomBound + size * 0.08,
14913
+ };
14914
+ }
14915
+ /**
14916
+ * Samples points along a quadratic Bezier curve.
14917
+ *
14918
+ * @param startPoint Curve start point.
14919
+ * @param controlPoint Curve control point.
14920
+ * @param endPoint Curve end point.
14921
+ * @param pointCount Number of intervals to sample.
14922
+ * @returns Sampled curve points.
14923
+ *
14924
+ * @private helper of `asciiOctopusAvatarVisual`
14925
+ */
14926
+ function sampleQuadraticBezierPoints(startPoint, controlPoint, endPoint, pointCount) {
14927
+ return Array.from({ length: pointCount + 1 }, (_, pointIndex) => {
14928
+ const progress = pointIndex / pointCount;
14929
+ const inverseProgress = 1 - progress;
14930
+ return {
14931
+ x: inverseProgress * inverseProgress * startPoint.x +
14932
+ 2 * inverseProgress * progress * controlPoint.x +
14933
+ progress * progress * endPoint.x,
14934
+ y: inverseProgress * inverseProgress * startPoint.y +
14935
+ 2 * inverseProgress * progress * controlPoint.y +
14936
+ progress * progress * endPoint.y,
14937
+ };
14938
+ });
14939
+ }
14940
+ /**
14941
+ * Measures how strongly the sampled cell intersects with the generated tentacles.
14942
+ *
14943
+ * @param point Sampled cell point.
14944
+ * @param sampledTentacles Pre-sampled tentacle ribbons.
14945
+ * @param cellWidth Character cell width.
14946
+ * @returns Nearest tentacle coverage or `null`.
14947
+ *
14948
+ * @private helper of `asciiOctopusAvatarVisual`
14949
+ */
14950
+ function measureTentacleCoverage(point, sampledTentacles, cellWidth) {
14951
+ let bestTentacleCoverage = null;
14952
+ let bestNormalizedDistance = 0;
14953
+ for (const sampledTentacle of sampledTentacles) {
14954
+ for (const ribbonPoint of sampledTentacle) {
14955
+ const deltaX = point.x - ribbonPoint.x;
14956
+ const deltaY = point.y - ribbonPoint.y;
14957
+ const distance = Math.hypot(deltaX, deltaY);
14958
+ const coverageRadius = ribbonPoint.width + cellWidth * 0.22;
14959
+ const normalizedDistance = 1 - distance / coverageRadius;
14960
+ if (normalizedDistance <= bestNormalizedDistance || normalizedDistance <= 0) {
14961
+ continue;
14962
+ }
14963
+ bestNormalizedDistance = normalizedDistance;
14964
+ bestTentacleCoverage = {
14965
+ tangentAngle: Math.atan2(-ribbonPoint.normalX, ribbonPoint.normalY),
14966
+ progress: ribbonPoint.progress,
14967
+ normalizedDistance,
14968
+ };
14969
+ }
14970
+ }
14971
+ return bestTentacleCoverage;
14972
+ }
14973
+ /**
14974
+ * Picks one ASCII character that matches the nearest tentacle direction.
14975
+ *
14976
+ * @param tentacleCoverage Nearest tentacle coverage.
14977
+ * @param noise Stable per-cell noise.
14978
+ * @returns Tentacle ASCII character.
14979
+ *
14980
+ * @private helper of `asciiOctopusAvatarVisual`
14981
+ */
14982
+ function pickTentacleCharacter(tentacleCoverage, noise) {
14983
+ const isSuckerBand = tentacleCoverage.progress > 0.24 && tentacleCoverage.progress < 0.82 && noise > 0.82;
14984
+ if (isSuckerBand && tentacleCoverage.normalizedDistance > 0.34) {
14985
+ return noise > 0.91 ? '0' : 'o';
14986
+ }
14987
+ const horizontalWeight = Math.abs(Math.cos(tentacleCoverage.tangentAngle));
14988
+ const verticalWeight = Math.abs(Math.sin(tentacleCoverage.tangentAngle));
14989
+ if (horizontalWeight > 0.84) {
14990
+ return noise > 0.52 ? '=' : '-';
14991
+ }
14992
+ if (verticalWeight > 0.82) {
14993
+ return noise > 0.56 ? '|' : '!';
14994
+ }
14995
+ return Math.sin(tentacleCoverage.tangentAngle) * Math.cos(tentacleCoverage.tangentAngle) > 0 ? '\\' : '/';
14996
+ }
14997
+ /**
14998
+ * Picks one character from an ordered ramp.
14999
+ *
15000
+ * @param glyphRamp Ordered glyph ramp.
15001
+ * @param intensity Normalized intensity in the range `[0, 1]`.
15002
+ * @returns Selected glyph.
15003
+ *
15004
+ * @private helper of `asciiOctopusAvatarVisual`
15005
+ */
15006
+ function pickRampCharacter(glyphRamp, intensity) {
15007
+ const characterIndex = Math.min(glyphRamp.length - 1, Math.floor(clamp01(intensity) * glyphRamp.length));
15008
+ return glyphRamp[characterIndex];
15009
+ }
15010
+ /**
15011
+ * Measures the normalized distance from a point to a rotated ellipse.
15012
+ *
15013
+ * @param point Sampled cell point.
15014
+ * @param centerX Ellipse center X coordinate.
15015
+ * @param centerY Ellipse center Y coordinate.
15016
+ * @param radiusX Horizontal ellipse radius.
15017
+ * @param radiusY Vertical ellipse radius.
15018
+ * @param rotation Ellipse rotation in radians.
15019
+ * @returns Normalized ellipse distance where values below `1` are inside.
15020
+ *
15021
+ * @private helper of `asciiOctopusAvatarVisual`
15022
+ */
15023
+ function measureRotatedEllipseDistance(point, centerX, centerY, radiusX, radiusY, rotation) {
15024
+ const cosine = Math.cos(rotation);
15025
+ const sine = Math.sin(rotation);
15026
+ const translatedX = point.x - centerX;
15027
+ const translatedY = point.y - centerY;
15028
+ const localX = translatedX * cosine + translatedY * sine;
15029
+ const localY = -translatedX * sine + translatedY * cosine;
15030
+ return Math.sqrt((localX * localX) / (radiusX * radiusX) + (localY * localY) / (radiusY * radiusY));
15031
+ }
15032
+ /**
15033
+ * Checks whether a point lies inside the given closed polygon.
15034
+ *
15035
+ * @param point Sampled cell point.
15036
+ * @param polygonPoints Polygon points in order.
15037
+ * @returns `true` when the point lies inside the polygon.
15038
+ *
15039
+ * @private helper of `asciiOctopusAvatarVisual`
15040
+ */
15041
+ function isPointInsidePolygon(point, polygonPoints) {
15042
+ let isInside = false;
15043
+ for (let currentPointIndex = 0, previousPointIndex = polygonPoints.length - 1; currentPointIndex < polygonPoints.length; previousPointIndex = currentPointIndex++) {
15044
+ const currentPoint = polygonPoints[currentPointIndex];
15045
+ const previousPoint = polygonPoints[previousPointIndex];
15046
+ const isIntersecting = currentPoint.y > point.y !== previousPoint.y > point.y &&
15047
+ point.x <
15048
+ ((previousPoint.x - currentPoint.x) * (point.y - currentPoint.y)) / (previousPoint.y - currentPoint.y) +
15049
+ currentPoint.x;
15050
+ if (isIntersecting) {
15051
+ isInside = !isInside;
15052
+ }
15053
+ }
15054
+ return isInside;
15055
+ }
15056
+ /**
15057
+ * Measures the shortest distance from a point to a polyline.
15058
+ *
15059
+ * @param point Sampled cell point.
15060
+ * @param polylinePoints Polyline points in order.
15061
+ * @param isClosed Whether the final point should connect back to the first point.
15062
+ * @returns Shortest distance to the polyline.
15063
+ *
15064
+ * @private helper of `asciiOctopusAvatarVisual`
15065
+ */
15066
+ function getDistanceToPolyline(point, polylinePoints, isClosed) {
15067
+ let shortestDistance = Number.POSITIVE_INFINITY;
15068
+ const segmentCount = isClosed ? polylinePoints.length : polylinePoints.length - 1;
15069
+ for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex++) {
15070
+ const startPoint = polylinePoints[segmentIndex];
15071
+ const endPoint = polylinePoints[(segmentIndex + 1) % polylinePoints.length];
15072
+ shortestDistance = Math.min(shortestDistance, getDistanceToLineSegment(point, startPoint, endPoint));
15073
+ }
15074
+ return shortestDistance;
15075
+ }
15076
+ /**
15077
+ * Measures the shortest distance from a point to one line segment.
15078
+ *
15079
+ * @param point Sampled cell point.
15080
+ * @param startPoint Segment start point.
15081
+ * @param endPoint Segment end point.
15082
+ * @returns Shortest distance to the segment.
15083
+ *
15084
+ * @private helper of `asciiOctopusAvatarVisual`
15085
+ */
15086
+ function getDistanceToLineSegment(point, startPoint, endPoint) {
15087
+ const deltaX = endPoint.x - startPoint.x;
15088
+ const deltaY = endPoint.y - startPoint.y;
15089
+ const segmentLengthSquared = deltaX * deltaX + deltaY * deltaY;
15090
+ if (segmentLengthSquared === 0) {
15091
+ return Math.hypot(point.x - startPoint.x, point.y - startPoint.y);
15092
+ }
15093
+ const progress = clamp01(((point.x - startPoint.x) * deltaX + (point.y - startPoint.y) * deltaY) / segmentLengthSquared);
15094
+ const projectionX = startPoint.x + deltaX * progress;
15095
+ const projectionY = startPoint.y + deltaY * progress;
15096
+ return Math.hypot(point.x - projectionX, point.y - projectionY);
15097
+ }
15098
+ /**
15099
+ * Clamps a number into the inclusive range `[0, 1]`.
15100
+ *
15101
+ * @param value Arbitrary numeric value.
15102
+ * @returns Clamped value.
15103
+ *
15104
+ * @private helper of `asciiOctopusAvatarVisual`
15105
+ */
15106
+ function clamp01(value) {
15107
+ return Math.max(0, Math.min(1, value));
15108
+ }
15109
+
15110
+ /* eslint-disable no-magic-numbers */
15111
+ /**
15112
+ * Fractal avatar visual.
15113
+ *
15114
+ * @private built-in avatar visual
15115
+ */
15116
+ const fractalAvatarVisual = {
15117
+ id: 'fractal',
15118
+ title: 'Fractal',
15119
+ description: 'Layered dragon-curve ribbons with deterministic glows, bends, and seeded color interplay.',
15120
+ isAnimated: true,
15121
+ render({ context, size, palette, createRandom, timeMs }) {
15122
+ const staticRandom = createRandom('fractal-static');
15123
+ const centerX = size * 0.5;
15124
+ const centerY = size * 0.5;
15125
+ const layerCount = 2 + Math.floor(staticRandom() * 3);
15126
+ const haloRotation = staticRandom() * Math.PI * 2;
15127
+ const colorSequence = [palette.primary, palette.secondary, palette.accent, palette.highlight];
15128
+ drawAvatarFrame(context, size, palette);
15129
+ drawFractalBackground(context, size, palette, timeMs, haloRotation);
15130
+ for (let layerIndex = 0; layerIndex < layerCount; layerIndex++) {
15131
+ const layerRandom = createRandom(`fractal-layer-${layerIndex}`);
15132
+ const order = 8 + Math.floor(layerRandom() * 4);
15133
+ const turnSequence = createDragonCurveTurns(order);
15134
+ const basePoints = createDragonCurvePoints(turnSequence);
15135
+ const transformedPoints = transformDragonCurvePoints(basePoints, {
15136
+ size,
15137
+ centerX: centerX + (layerRandom() - 0.5) * size * 0.08,
15138
+ centerY: centerY + (layerRandom() - 0.5) * size * 0.08,
15139
+ rotation: layerRandom() * Math.PI * 2 + Math.sin(timeMs / (1700 + layerIndex * 280) + layerIndex) * 0.14,
15140
+ scale: size * (0.19 + layerIndex * 0.055 + layerRandom() * 0.045),
15141
+ horizontalStretch: 0.74 + layerRandom() * 0.9,
15142
+ verticalStretch: 0.74 + layerRandom() * 0.9,
15143
+ warpAmplitude: size * (0.008 + layerRandom() * 0.012),
15144
+ warpPhase: layerRandom() * Math.PI * 2,
15145
+ mirrorX: layerRandom() < 0.5 ? -1 : 1,
15146
+ mirrorY: layerRandom() < 0.38 ? -1 : 1,
15147
+ timeMs,
15148
+ });
15149
+ const primaryColor = colorSequence[layerIndex % colorSequence.length];
15150
+ const secondaryColor = colorSequence[(layerIndex + 1) % colorSequence.length];
15151
+ const tertiaryColor = colorSequence[(layerIndex + 2) % colorSequence.length];
15152
+ const strokeWidth = size * (0.026 - layerIndex * 0.0035);
15153
+ drawDragonCurveLayer(context, transformedPoints, {
15154
+ size,
15155
+ primaryColor,
15156
+ secondaryColor,
15157
+ tertiaryColor,
15158
+ shadowColor: palette.shadow,
15159
+ strokeWidth,
15160
+ timeMs,
15161
+ layerIndex,
15162
+ });
15163
+ }
15164
+ drawFractalCore(context, size, palette, timeMs, staticRandom());
15165
+ },
15166
+ };
15167
+ /**
15168
+ * Draws the shared luminous atmosphere behind the curve layers.
15169
+ *
15170
+ * @param context Canvas 2D context.
15171
+ * @param size Canvas size in CSS pixels.
15172
+ * @param palette Derived avatar palette.
15173
+ * @param timeMs Current animation time in milliseconds.
15174
+ * @param haloRotation Seed-based phase offset.
15175
+ *
15176
+ * @private helper of `fractalAvatarVisual`
15177
+ */
15178
+ function drawFractalBackground(context, size, palette, timeMs, haloRotation) {
15179
+ const centerX = size * 0.5;
15180
+ const centerY = size * 0.5;
15181
+ const radialGlow = context.createRadialGradient(centerX, centerY, size * 0.06, centerX, centerY, size * 0.72);
15182
+ radialGlow.addColorStop(0, `${palette.highlight}55`);
15183
+ radialGlow.addColorStop(0.4, `${palette.secondary}1f`);
15184
+ radialGlow.addColorStop(1, `${palette.highlight}00`);
15185
+ context.fillStyle = radialGlow;
15186
+ context.fillRect(0, 0, size, size);
15187
+ for (let haloIndex = 0; haloIndex < 3; haloIndex++) {
15188
+ const radius = size * (0.17 + haloIndex * 0.09);
15189
+ const rotation = haloRotation + haloIndex * 0.85 + timeMs / (4400 + haloIndex * 700);
15190
+ context.beginPath();
15191
+ context.ellipse(centerX, centerY, radius, radius * (0.62 + haloIndex * 0.06), rotation, 0, Math.PI * 2);
15192
+ context.strokeStyle = haloIndex % 2 === 0 ? `${palette.secondary}24` : `${palette.accent}20`;
15193
+ context.lineWidth = size * 0.006;
15194
+ context.stroke();
15195
+ }
15196
+ }
15197
+ /**
15198
+ * Generates the left-right turn sequence for a dragon curve.
15199
+ *
15200
+ * @param order Number of folding iterations.
15201
+ * @returns Turn sequence where `1` means right and `-1` means left.
15202
+ *
15203
+ * @private helper of `fractalAvatarVisual`
15204
+ */
15205
+ function createDragonCurveTurns(order) {
15206
+ let turns = [];
15207
+ for (let iteration = 0; iteration < order; iteration++) {
15208
+ turns = [
15209
+ ...turns,
15210
+ 1,
15211
+ ...turns
15212
+ .slice()
15213
+ .reverse()
15214
+ .map((turn) => (turn === 1 ? -1 : 1)),
15215
+ ];
15216
+ }
15217
+ return turns;
15218
+ }
15219
+ /**
15220
+ * Converts a dragon-curve turn sequence into a raw grid polyline.
15221
+ *
15222
+ * @param turnSequence Ordered turn sequence.
15223
+ * @returns Unscaled polyline points.
15224
+ *
15225
+ * @private helper of `fractalAvatarVisual`
15226
+ */
15227
+ function createDragonCurvePoints(turnSequence) {
15228
+ const points = [{ x: 0, y: 0 }];
15229
+ const directions = [
15230
+ { x: 1, y: 0 },
15231
+ { x: 0, y: 1 },
15232
+ { x: -1, y: 0 },
15233
+ { x: 0, y: -1 },
15234
+ ];
15235
+ let directionIndex = 0;
15236
+ for (let segmentIndex = 0; segmentIndex <= turnSequence.length; segmentIndex++) {
15237
+ const currentPoint = points[points.length - 1];
15238
+ const direction = directions[directionIndex];
15239
+ points.push({
15240
+ x: currentPoint.x + direction.x,
15241
+ y: currentPoint.y + direction.y,
15242
+ });
15243
+ if (segmentIndex < turnSequence.length) {
15244
+ directionIndex = (directionIndex + turnSequence[segmentIndex] + directions.length) % directions.length;
15245
+ }
15246
+ }
15247
+ return points;
15248
+ }
15249
+ /**
15250
+ * Normalizes and decorates the dragon-curve polyline for avatar rendering.
15251
+ *
15252
+ * @param points Raw grid polyline points.
15253
+ * @param options Transformation parameters.
15254
+ * @returns Transformed canvas points.
15255
+ *
15256
+ * @private helper of `fractalAvatarVisual`
15257
+ */
15258
+ function transformDragonCurvePoints(points, options) {
15259
+ const { size, centerX, centerY, rotation, scale, horizontalStretch, verticalStretch, warpAmplitude, warpPhase, mirrorX, mirrorY, timeMs, } = options;
15260
+ const bounds = getPointBounds(points);
15261
+ const width = Math.max(1, bounds.maxX - bounds.minX);
15262
+ const height = Math.max(1, bounds.maxY - bounds.minY);
15263
+ const normalizationScale = scale / Math.max(width, height);
15264
+ const cosine = Math.cos(rotation);
15265
+ const sine = Math.sin(rotation);
15266
+ return points.map((point, pointIndex) => {
15267
+ const normalizedX = (point.x - (bounds.minX + width / 2)) * normalizationScale * horizontalStretch * mirrorX;
15268
+ const normalizedY = (point.y - (bounds.minY + height / 2)) * normalizationScale * verticalStretch * mirrorY;
15269
+ const progress = pointIndex / Math.max(1, points.length - 1);
15270
+ const localWarp = Math.sin(progress * Math.PI * 4 + warpPhase + timeMs / 1400) * warpAmplitude +
15271
+ Math.cos(progress * Math.PI * 7 - warpPhase + timeMs / 1800) * warpAmplitude * 0.45;
15272
+ const rotatedX = normalizedX * cosine - normalizedY * sine;
15273
+ const rotatedY = normalizedX * sine + normalizedY * cosine;
15274
+ return {
15275
+ x: centerX + rotatedX + Math.sin(progress * Math.PI * 2 + warpPhase) * localWarp,
15276
+ y: centerY +
15277
+ rotatedY +
15278
+ Math.cos(progress * Math.PI * 3 + warpPhase * 0.6) * localWarp +
15279
+ (progress - 0.5) * size * 0.02,
15280
+ };
15281
+ });
15282
+ }
15283
+ /**
15284
+ * Returns the bounding box of a point cloud.
15285
+ *
15286
+ * @param points Point cloud to inspect.
15287
+ * @returns Bounding box.
15288
+ *
15289
+ * @private helper of `fractalAvatarVisual`
15290
+ */
15291
+ function getPointBounds(points) {
15292
+ return points.reduce((bounds, point) => ({
15293
+ minX: Math.min(bounds.minX, point.x),
15294
+ maxX: Math.max(bounds.maxX, point.x),
15295
+ minY: Math.min(bounds.minY, point.y),
15296
+ maxY: Math.max(bounds.maxY, point.y),
15297
+ }), {
15298
+ minX: Number.POSITIVE_INFINITY,
15299
+ maxX: Number.NEGATIVE_INFINITY,
15300
+ minY: Number.POSITIVE_INFINITY,
15301
+ maxY: Number.NEGATIVE_INFINITY,
15302
+ });
15303
+ }
15304
+ /**
15305
+ * Draws one stylized dragon-curve ribbon with glow and spark nodes.
15306
+ *
15307
+ * @param context Canvas 2D context.
15308
+ * @param points Transformed polyline points.
15309
+ * @param options Layer styling options.
15310
+ *
15311
+ * @private helper of `fractalAvatarVisual`
15312
+ */
15313
+ function drawDragonCurveLayer(context, points, options) {
15314
+ const { size, primaryColor, secondaryColor, tertiaryColor, shadowColor, strokeWidth, timeMs, layerIndex } = options;
15315
+ const firstPoint = points[0];
15316
+ const lastPoint = points[points.length - 1];
15317
+ const ribbonGradient = context.createLinearGradient(firstPoint.x, firstPoint.y, lastPoint.x, lastPoint.y);
15318
+ ribbonGradient.addColorStop(0, `${primaryColor}f2`);
15319
+ ribbonGradient.addColorStop(0.5, `${secondaryColor}e6`);
15320
+ ribbonGradient.addColorStop(1, `${tertiaryColor}f2`);
15321
+ context.save();
15322
+ context.beginPath();
15323
+ tracePolyline(context, points);
15324
+ context.strokeStyle = `${shadowColor}82`;
15325
+ context.lineWidth = strokeWidth * 1.8;
15326
+ context.lineJoin = 'round';
15327
+ context.lineCap = 'round';
15328
+ context.filter = `blur(${size * 0.022}px)`;
15329
+ context.stroke();
15330
+ context.restore();
15331
+ context.beginPath();
15332
+ tracePolyline(context, points);
15333
+ context.strokeStyle = ribbonGradient;
15334
+ context.lineWidth = strokeWidth;
15335
+ context.lineJoin = 'round';
15336
+ context.lineCap = 'round';
15337
+ context.stroke();
15338
+ context.beginPath();
15339
+ tracePolyline(context, points);
15340
+ context.strokeStyle = 'rgba(255,255,255,0.22)';
15341
+ context.lineWidth = Math.max(1.2, strokeWidth * 0.28);
15342
+ context.lineJoin = 'round';
15343
+ context.lineCap = 'round';
15344
+ context.stroke();
15345
+ const sparkStride = Math.max(24, Math.floor(points.length / 18));
15346
+ for (let pointIndex = sparkStride; pointIndex < points.length; pointIndex += sparkStride) {
15347
+ const point = points[pointIndex];
15348
+ const pulse = 0.7 + 0.3 * Math.sin(timeMs / 700 + pointIndex * 0.12 + layerIndex);
15349
+ const radius = strokeWidth * (0.24 + pulse * 0.22);
15350
+ context.beginPath();
15351
+ context.arc(point.x, point.y, radius * 1.8, 0, Math.PI * 2);
15352
+ context.fillStyle = `${secondaryColor}20`;
15353
+ context.fill();
15354
+ context.beginPath();
15355
+ context.arc(point.x, point.y, radius, 0, Math.PI * 2);
15356
+ context.fillStyle = tertiaryColor;
15357
+ context.fill();
15358
+ }
15359
+ }
15360
+ /**
15361
+ * Traces a polyline through the provided points.
15362
+ *
15363
+ * @param context Canvas 2D context.
15364
+ * @param points Polyline points.
15365
+ *
15366
+ * @private helper of `fractalAvatarVisual`
15367
+ */
15368
+ function tracePolyline(context, points) {
15369
+ const firstPoint = points[0];
15370
+ context.moveTo(firstPoint.x, firstPoint.y);
15371
+ for (let pointIndex = 1; pointIndex < points.length; pointIndex++) {
15372
+ const point = points[pointIndex];
15373
+ context.lineTo(point.x, point.y);
15374
+ }
15375
+ }
15376
+ /**
15377
+ * Draws the central crystalline accent tying the dragon-curve layers together.
15378
+ *
15379
+ * @param context Canvas 2D context.
15380
+ * @param size Canvas size in CSS pixels.
15381
+ * @param palette Derived avatar palette.
15382
+ * @param timeMs Current animation time in milliseconds.
15383
+ * @param corePhase Seed-based phase offset.
15384
+ *
15385
+ * @private helper of `fractalAvatarVisual`
15386
+ */
15387
+ function drawFractalCore(context, size, palette, timeMs, corePhase) {
15388
+ const centerX = size * 0.5;
15389
+ const centerY = size * 0.5;
15390
+ const radius = size * 0.082;
15391
+ const rotation = corePhase * Math.PI * 2 + timeMs / 2200;
15392
+ const innerRotation = -rotation * 1.35;
15393
+ context.save();
15394
+ context.translate(centerX, centerY);
15395
+ context.rotate(rotation);
15396
+ context.beginPath();
15397
+ for (let pointIndex = 0; pointIndex < 4; pointIndex++) {
15398
+ const angle = (pointIndex / 4) * Math.PI * 2;
15399
+ const x = Math.cos(angle) * radius;
15400
+ const y = Math.sin(angle) * radius;
15401
+ if (pointIndex === 0) {
15402
+ context.moveTo(x, y);
15403
+ }
15404
+ else {
15405
+ context.lineTo(x, y);
15406
+ }
15407
+ }
15408
+ context.closePath();
15409
+ context.fillStyle = `${palette.highlight}88`;
15410
+ context.shadowColor = `${palette.highlight}77`;
15411
+ context.shadowBlur = size * 0.05;
15412
+ context.fill();
15413
+ context.restore();
15414
+ context.save();
15415
+ context.translate(centerX, centerY);
15416
+ context.rotate(innerRotation);
15417
+ context.beginPath();
15418
+ for (let pointIndex = 0; pointIndex < 4; pointIndex++) {
15419
+ const angle = Math.PI / 4 + (pointIndex / 4) * Math.PI * 2;
15420
+ const x = Math.cos(angle) * radius * 0.55;
15421
+ const y = Math.sin(angle) * radius * 0.55;
15422
+ if (pointIndex === 0) {
15423
+ context.moveTo(x, y);
15424
+ }
15425
+ else {
15426
+ context.lineTo(x, y);
15427
+ }
15428
+ }
15429
+ context.closePath();
15430
+ context.fillStyle = `${palette.ink}cc`;
15431
+ context.fill();
15432
+ context.restore();
15433
+ }
15434
+
15435
+ /* eslint-disable no-magic-numbers */
15436
+ /**
15437
+ * Minecraft-style 3D avatar visual.
15438
+ *
15439
+ * @private built-in avatar visual
15440
+ */
15441
+ const minecraftAvatarVisual = {
15442
+ id: 'minecraft',
15443
+ title: 'Minecraft 3D',
15444
+ description: 'Blocky 3D portrait with deterministic pixel textures, shoulders, and hovering depth.',
15445
+ isAnimated: true,
15446
+ render({ context, size, palette, createRandom, timeMs }) {
15447
+ const random = createRandom('minecraft');
15448
+ const bob = Math.sin(timeMs / 880) * size * 0.015;
15449
+ const headSize = size * 0.34;
15450
+ const depth = headSize * 0.22;
15451
+ const headX = size * 0.31;
15452
+ const headY = size * 0.18 + bob;
15453
+ const bodyWidth = headSize * 0.86;
15454
+ const bodyHeight = headSize * 0.82;
15455
+ const bodyDepth = depth * 0.8;
15456
+ const bodyX = size * 0.33;
15457
+ const bodyY = headY + headSize * 0.96;
15458
+ const hasHeadband = random() < 0.5;
15459
+ const faceTexture = createMinecraftFaceTexture(createRandom('minecraft-face'), palette, hasHeadband);
15460
+ const shirtTexture = createMinecraftShirtTexture(createRandom('minecraft-shirt'), palette);
15461
+ drawAvatarFrame(context, size, palette);
15462
+ const spotlight = context.createRadialGradient(size * 0.5, size * 0.18, size * 0.05, size * 0.5, size * 0.18, size * 0.5);
15463
+ spotlight.addColorStop(0, `${palette.highlight}66`);
15464
+ spotlight.addColorStop(1, `${palette.highlight}00`);
15465
+ context.fillStyle = spotlight;
15466
+ context.fillRect(0, 0, size, size);
15467
+ context.save();
15468
+ context.fillStyle = 'rgba(0, 0, 0, 0.22)';
15469
+ context.filter = `blur(${size * 0.018}px)`;
15470
+ context.beginPath();
15471
+ context.ellipse(size * 0.5, size * 0.86, size * 0.2, size * 0.06, 0, 0, Math.PI * 2);
15472
+ context.fill();
15473
+ context.restore();
15474
+ drawVoxelCuboid(context, {
15475
+ x: bodyX,
15476
+ y: bodyY,
15477
+ width: bodyWidth,
15478
+ height: bodyHeight,
15479
+ depth: bodyDepth,
15480
+ frontTexture: shirtTexture,
15481
+ topColor: `${palette.highlight}cc`,
15482
+ sideColor: `${palette.secondary}dd`,
15483
+ outlineColor: `${palette.shadow}aa`,
15484
+ });
15485
+ drawVoxelCuboid(context, {
15486
+ x: headX,
15487
+ y: headY,
15488
+ width: headSize,
15489
+ height: headSize,
15490
+ depth,
15491
+ frontTexture: faceTexture,
15492
+ topColor: `${palette.highlight}ee`,
15493
+ sideColor: `${palette.secondary}ee`,
15494
+ outlineColor: `${palette.shadow}cc`,
15495
+ });
15496
+ },
15497
+ };
15498
+ /**
15499
+ * Draws a stylized voxel cuboid with a front pixel texture.
15500
+ *
15501
+ * @param context Canvas 2D context.
15502
+ * @param cuboid Cuboid settings.
15503
+ *
15504
+ * @private helper of `minecraftAvatarVisual`
15505
+ */
15506
+ function drawVoxelCuboid(context, cuboid) {
15507
+ var _a;
15508
+ const { x, y, width, height, depth, frontTexture, topColor, sideColor, outlineColor } = cuboid;
15509
+ const lift = depth * 0.6;
15510
+ context.save();
15511
+ context.beginPath();
15512
+ context.moveTo(x, y);
15513
+ context.lineTo(x + depth, y - lift);
15514
+ context.lineTo(x + width + depth, y - lift);
15515
+ context.lineTo(x + width, y);
15516
+ context.closePath();
15517
+ context.fillStyle = topColor;
15518
+ context.fill();
15519
+ context.restore();
15520
+ context.save();
15521
+ context.beginPath();
15522
+ context.moveTo(x + width, y);
15523
+ context.lineTo(x + width + depth, y - lift);
15524
+ context.lineTo(x + width + depth, y + height - lift);
15525
+ context.lineTo(x + width, y + height);
15526
+ context.closePath();
15527
+ context.fillStyle = sideColor;
15528
+ context.fill();
15529
+ context.restore();
15530
+ const rows = frontTexture.length;
15531
+ const columns = ((_a = frontTexture[0]) === null || _a === void 0 ? void 0 : _a.length) || 0;
15532
+ const pixelWidth = width / Math.max(columns, 1);
15533
+ const pixelHeight = height / Math.max(rows, 1);
15534
+ context.save();
15535
+ createRoundedRectPath(context, x, y, width, height, width * 0.03);
15536
+ context.clip();
15537
+ for (let rowIndex = 0; rowIndex < rows; rowIndex++) {
15538
+ for (let columnIndex = 0; columnIndex < columns; columnIndex++) {
15539
+ context.fillStyle = frontTexture[rowIndex][columnIndex];
15540
+ context.fillRect(x + columnIndex * pixelWidth, y + rowIndex * pixelHeight, pixelWidth, pixelHeight);
15541
+ }
15542
+ }
15543
+ context.restore();
15544
+ context.strokeStyle = outlineColor;
15545
+ context.lineWidth = Math.max(1.2, width * 0.025);
15546
+ context.beginPath();
15547
+ context.moveTo(x, y);
15548
+ context.lineTo(x + depth, y - lift);
15549
+ context.lineTo(x + width + depth, y - lift);
15550
+ context.lineTo(x + width + depth, y + height - lift);
15551
+ context.lineTo(x + width, y + height);
15552
+ context.lineTo(x, y + height);
15553
+ context.closePath();
15554
+ context.stroke();
15555
+ }
15556
+ /**
15557
+ * Creates the front-face pixel texture for the cube head.
15558
+ *
15559
+ * @param random Seeded random generator.
15560
+ * @param palette Derived avatar palette.
15561
+ * @param hasHeadband Whether the avatar should render a headband row.
15562
+ * @returns 8x8 pixel texture.
15563
+ *
15564
+ * @private helper of `minecraftAvatarVisual`
15565
+ */
15566
+ function createMinecraftFaceTexture(random, palette, hasHeadband) {
15567
+ const texture = Array.from({ length: 8 }, () => Array.from({ length: 8 }, () => palette.highlight));
15568
+ const hairlineColor = random() < 0.5 ? palette.primary : palette.secondary;
15569
+ const cheekColor = random() < 0.5 ? `${palette.accent}bb` : `${palette.secondary}bb`;
15570
+ for (let rowIndex = 0; rowIndex < 2; rowIndex++) {
15571
+ for (let columnIndex = 0; columnIndex < 8; columnIndex++) {
15572
+ texture[rowIndex][columnIndex] = hairlineColor;
15573
+ }
15574
+ }
15575
+ texture[2][0] = hairlineColor;
15576
+ texture[2][7] = hairlineColor;
15577
+ texture[3][0] = hairlineColor;
15578
+ texture[3][7] = hairlineColor;
15579
+ if (hasHeadband) {
15580
+ for (let columnIndex = 0; columnIndex < 8; columnIndex++) {
15581
+ texture[2][columnIndex] = palette.accent;
15582
+ }
15583
+ }
15584
+ texture[3][2] = palette.ink;
15585
+ texture[3][5] = palette.ink;
15586
+ texture[4][2] = '#ffffff';
15587
+ texture[4][5] = '#ffffff';
15588
+ texture[5][1] = cheekColor;
15589
+ texture[5][6] = cheekColor;
15590
+ texture[5][3] = palette.shadow;
15591
+ texture[5][4] = palette.shadow;
15592
+ texture[6][3] = palette.shadow;
15593
+ texture[6][4] = palette.shadow;
15594
+ return texture;
15595
+ }
15596
+ /**
15597
+ * Creates the front-face pixel texture for the torso.
15598
+ *
15599
+ * @param random Seeded random generator.
15600
+ * @param palette Derived avatar palette.
15601
+ * @returns 8x8 torso texture.
15602
+ *
15603
+ * @private helper of `minecraftAvatarVisual`
15604
+ */
15605
+ function createMinecraftShirtTexture(random, palette) {
15606
+ const texture = Array.from({ length: 8 }, () => Array.from({ length: 8 }, () => palette.primary));
15607
+ const stripeColor = random() < 0.5 ? palette.secondary : palette.highlight;
15608
+ for (let rowIndex = 0; rowIndex < 2; rowIndex++) {
15609
+ for (let columnIndex = 0; columnIndex < 8; columnIndex++) {
15610
+ texture[rowIndex][columnIndex] = palette.shadow;
15611
+ }
15612
+ }
15613
+ for (let rowIndex = 2; rowIndex < 8; rowIndex++) {
15614
+ texture[rowIndex][3] = stripeColor;
15615
+ texture[rowIndex][4] = stripeColor;
15616
+ }
15617
+ texture[4][1] = palette.accent;
15618
+ texture[4][6] = palette.accent;
15619
+ texture[5][2] = palette.highlight;
15620
+ texture[5][5] = palette.highlight;
15621
+ return texture;
15622
+ }
15623
+
15624
+ /* eslint-disable no-magic-numbers */
15625
+ /**
15626
+ * Octopus avatar visual.
15627
+ *
15628
+ * @private built-in avatar visual
15629
+ */
15630
+ const octopusAvatarVisual = {
15631
+ id: 'octopus',
15632
+ title: 'Octopus',
15633
+ description: 'Playful underwater mascot with cursor-following eyes, animated tentacles, bubbles, and seeded markings.',
15634
+ isAnimated: true,
15635
+ supportsPointerTracking: true,
15636
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
15637
+ const staticRandom = createRandom('octopus-static');
15638
+ const bubbleRandom = createRandom('octopus-bubbles');
15639
+ const bubbleCount = 8;
15640
+ const bubbleRadiusBase = size * 0.02;
15641
+ const centerX = size * 0.5 + interaction.bodyOffsetX * size * 0.035;
15642
+ const centerY = size * 0.42 + interaction.bodyOffsetY * size * 0.024;
15643
+ const headRadius = size * (0.19 + staticRandom() * 0.03);
15644
+ const mantleHeight = headRadius * 1.18;
15645
+ const tentacleLength = size * (0.18 + staticRandom() * 0.06);
15646
+ const tentaclePhases = Array.from({ length: 8 }, () => staticRandom() * Math.PI * 2);
15647
+ const spotCount = 3 + Math.floor(staticRandom() * 4);
15648
+ const spotColors = [palette.secondary, palette.accent, palette.highlight];
15649
+ drawAvatarFrame(context, size, palette);
15650
+ const waterGlow = context.createRadialGradient(centerX, size * 0.22, size * 0.06, centerX, size * 0.22, size * 0.58);
15651
+ waterGlow.addColorStop(0, `${palette.highlight}66`);
15652
+ waterGlow.addColorStop(1, `${palette.highlight}00`);
15653
+ context.fillStyle = waterGlow;
15654
+ context.fillRect(0, 0, size, size);
15655
+ for (let bubbleIndex = 0; bubbleIndex < bubbleCount; bubbleIndex++) {
15656
+ const x = size * (0.15 + bubbleRandom() * 0.7);
15657
+ const y = size * (0.12 + bubbleRandom() * 0.68);
15658
+ const radius = bubbleRadiusBase * (0.6 + bubbleRandom() * 2.3);
15659
+ context.beginPath();
15660
+ context.arc(x, y, radius, 0, Math.PI * 2);
15661
+ context.fillStyle = 'rgba(255,255,255,0.08)';
15662
+ context.fill();
15663
+ context.beginPath();
15664
+ context.arc(x - radius * 0.22, y - radius * 0.22, radius * 0.25, 0, Math.PI * 2);
15665
+ context.fillStyle = 'rgba(255,255,255,0.28)';
15666
+ context.fill();
15667
+ }
15668
+ for (let tentacleIndex = 0; tentacleIndex < 8; tentacleIndex++) {
15669
+ const startX = centerX + (tentacleIndex - 3.5) * headRadius * 0.19;
15670
+ const startY = centerY + headRadius * 0.62;
15671
+ const animationPhase = tentaclePhases[tentacleIndex];
15672
+ const sway = Math.sin(timeMs / 520 + animationPhase) * size * 0.03;
15673
+ const endX = startX + (tentacleIndex - 3.5) * size * 0.025 + sway;
15674
+ const endY = startY + tentacleLength + Math.cos(timeMs / 700 + animationPhase) * size * 0.01;
15675
+ const controlX = (startX + endX) / 2 + sway * 0.8;
15676
+ const controlY = startY + tentacleLength * 0.45;
15677
+ const lineWidth = size * (0.042 - tentacleIndex * 0.0022);
15678
+ context.beginPath();
15679
+ context.moveTo(startX, startY);
15680
+ context.quadraticCurveTo(controlX, controlY, endX, endY);
15681
+ context.lineCap = 'round';
15682
+ context.strokeStyle = palette.primary;
15683
+ context.lineWidth = lineWidth;
15684
+ context.stroke();
15685
+ for (let cupIndex = 1; cupIndex <= 3; cupIndex++) {
15686
+ const cupT = cupIndex / 4;
15687
+ const cupX = (1 - cupT) * (1 - cupT) * startX + 2 * (1 - cupT) * cupT * controlX + cupT * cupT * endX;
15688
+ const cupY = (1 - cupT) * (1 - cupT) * startY + 2 * (1 - cupT) * cupT * controlY + cupT * cupT * endY;
15689
+ context.beginPath();
15690
+ context.arc(cupX, cupY, lineWidth * 0.18, 0, Math.PI * 2);
15691
+ context.fillStyle = `${palette.highlight}cc`;
15692
+ context.fill();
15693
+ }
15694
+ }
15695
+ context.save();
15696
+ context.fillStyle = palette.primary;
15697
+ context.shadowColor = `${palette.shadow}88`;
15698
+ context.shadowBlur = size * 0.08;
15699
+ context.beginPath();
15700
+ context.ellipse(centerX, centerY, headRadius, mantleHeight, 0, Math.PI, 0, true);
15701
+ context.lineTo(centerX + headRadius, centerY);
15702
+ context.ellipse(centerX, centerY, headRadius, headRadius * 0.82, 0, 0, Math.PI);
15703
+ context.closePath();
15704
+ context.fill();
15705
+ context.restore();
15706
+ context.beginPath();
15707
+ context.ellipse(centerX, centerY - headRadius * 0.22, headRadius * 0.74, headRadius * 0.42, 0, Math.PI, Math.PI * 2);
15708
+ context.fillStyle = `${palette.highlight}55`;
15709
+ context.fill();
15710
+ for (let spotIndex = 0; spotIndex < spotCount; spotIndex++) {
15711
+ const spotRandom = createRandom(`octopus-spot-${spotIndex}`);
15712
+ const spotX = centerX + (spotRandom() - 0.5) * headRadius * 1.1;
15713
+ const spotY = centerY - headRadius * 0.05 + (spotRandom() - 0.5) * headRadius * 0.9;
15714
+ const spotRadius = headRadius * (0.07 + spotRandom() * 0.07);
15715
+ context.beginPath();
15716
+ context.arc(spotX, spotY, spotRadius, 0, Math.PI * 2);
15717
+ context.fillStyle = pickRandomItem(spotColors, spotRandom);
15718
+ context.fill();
15719
+ }
15720
+ const eyeOffsetX = headRadius * 0.42;
15721
+ const eyeY = centerY + headRadius * 0.04;
15722
+ const eyeRadius = headRadius * 0.22;
15723
+ drawEye(context, centerX - eyeOffsetX, eyeY, eyeRadius, palette, timeMs, interaction, 0);
15724
+ drawEye(context, centerX + eyeOffsetX, eyeY, eyeRadius, palette, timeMs, interaction, Math.PI / 5);
15725
+ context.beginPath();
15726
+ context.arc(centerX - headRadius * 0.28, centerY + headRadius * 0.3, headRadius * 0.12, 0, Math.PI * 2);
15727
+ context.arc(centerX + headRadius * 0.28, centerY + headRadius * 0.3, headRadius * 0.12, 0, Math.PI * 2);
15728
+ context.fillStyle = `${palette.accent}44`;
15729
+ context.fill();
15730
+ context.beginPath();
15731
+ context.moveTo(centerX - headRadius * 0.18, centerY + headRadius * 0.24);
15732
+ context.quadraticCurveTo(centerX, centerY + headRadius * 0.42, centerX + headRadius * 0.18, centerY + headRadius * 0.24);
15733
+ context.strokeStyle = palette.shadow;
15734
+ context.lineWidth = size * 0.016;
15735
+ context.lineCap = 'round';
15736
+ context.stroke();
15737
+ },
15738
+ };
15739
+ /**
15740
+ * Draws one expressive octopus eye.
15741
+ *
15742
+ * @param context Canvas 2D context.
15743
+ * @param centerX Eye center X coordinate.
15744
+ * @param centerY Eye center Y coordinate.
15745
+ * @param radius Eye radius.
15746
+ * @param palette Derived avatar palette.
15747
+ * @param timeMs Current animation time in milliseconds.
15748
+ * @param interaction Smoothed avatar interaction state.
15749
+ * @param phase Seed-based phase offset.
15750
+ *
15751
+ * @private helper of `octopusAvatarVisual`
15752
+ */
15753
+ function drawEye(context, centerX, centerY, radius, palette, timeMs, interaction, phase) {
15754
+ const { pupilOffsetX, pupilOffsetY } = resolveOrganicEyeMotion({
15755
+ radiusX: radius,
15756
+ radiusY: radius,
15757
+ timeMs,
15758
+ phase,
15759
+ interaction,
15760
+ autonomousDriftRatioX: 0.05,
15761
+ autonomousDriftRatioY: 0.03,
15762
+ });
15763
+ context.beginPath();
15764
+ context.arc(centerX, centerY, radius, 0, Math.PI * 2);
15765
+ context.fillStyle = '#ffffff';
15766
+ context.fill();
15767
+ context.beginPath();
15768
+ context.arc(centerX + pupilOffsetX, centerY + pupilOffsetY, radius * 0.45, 0, Math.PI * 2);
15769
+ context.fillStyle = palette.ink;
15770
+ context.fill();
15771
+ context.beginPath();
15772
+ context.arc(centerX + pupilOffsetX - radius * 0.12, centerY + pupilOffsetY - radius * 0.12, radius * 0.15, 0, Math.PI * 2);
15773
+ context.fillStyle = '#ffffff';
15774
+ context.fill();
15775
+ context.beginPath();
15776
+ context.arc(centerX, centerY, radius, 0, Math.PI * 2);
15777
+ context.strokeStyle = palette.shadow;
15778
+ context.lineWidth = radius * 0.18;
15779
+ context.stroke();
15780
+ }
15781
+
15782
+ /* eslint-disable no-magic-numbers */
15783
+ /**
15784
+ * Octopus2 avatar visual.
15785
+ *
15786
+ * @private built-in avatar visual
15787
+ */
15788
+ const octopus2AvatarVisual = {
15789
+ id: 'octopus2',
15790
+ title: 'Octopus2',
15791
+ description: 'Organic alien octopus rendered as one continuously morphing blob with responsive luminous eyes.',
15792
+ isAnimated: true,
15793
+ supportsPointerTracking: true,
15794
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
15795
+ const staticRandom = createRandom('octopus2-static');
15796
+ const centerX = size * 0.5 + interaction.bodyOffsetX * size * 0.042;
15797
+ const centerY = size * (0.48 + staticRandom() * 0.03) + interaction.bodyOffsetY * size * 0.028;
15798
+ const bodyRadius = size * (0.25 + staticRandom() * 0.035);
15799
+ const horizontalStretch = 1.04 + staticRandom() * 0.16;
15800
+ const verticalStretch = 0.94 + staticRandom() * 0.12;
15801
+ const mantleLift = size * (0.075 + staticRandom() * 0.025);
15802
+ const lowerDrop = size * (0.05 + staticRandom() * 0.02);
15803
+ const tentacleDepth = size * (0.08 + staticRandom() * 0.03);
15804
+ const wobbleAmplitude = size * (0.014 + staticRandom() * 0.008);
15805
+ const lobeCount = 6 + Math.floor(staticRandom() * 3);
15806
+ const shapePhase = staticRandom() * Math.PI * 2;
15807
+ const bodyPoints = createOrganicOctopusBodyPoints({
15808
+ centerX,
15809
+ centerY,
15810
+ bodyRadius,
15811
+ horizontalStretch,
15812
+ verticalStretch,
15813
+ mantleLift,
15814
+ lowerDrop,
15815
+ tentacleDepth,
15816
+ wobbleAmplitude,
15817
+ lobeCount,
15818
+ shapePhase,
15819
+ timeMs,
15820
+ });
15821
+ drawAvatarFrame(context, size, palette);
15822
+ const hazeGradient = context.createRadialGradient(centerX, size * 0.22, size * 0.05, centerX, centerY, size * 0.6);
15823
+ hazeGradient.addColorStop(0, `${palette.highlight}4d`);
15824
+ hazeGradient.addColorStop(0.45, `${palette.accent}24`);
15825
+ hazeGradient.addColorStop(1, `${palette.highlight}00`);
15826
+ context.fillStyle = hazeGradient;
15827
+ context.fillRect(0, 0, size, size);
15828
+ const rimGlowGradient = context.createRadialGradient(centerX, centerY + size * 0.08, size * 0.14, centerX, centerY, size * 0.5);
15829
+ rimGlowGradient.addColorStop(0, `${palette.secondary}26`);
15830
+ rimGlowGradient.addColorStop(1, `${palette.secondary}00`);
15831
+ context.fillStyle = rimGlowGradient;
15832
+ context.fillRect(0, 0, size, size);
15833
+ context.save();
15834
+ traceSmoothClosedPath(context, bodyPoints);
15835
+ const bodyGradient = context.createRadialGradient(centerX - size * 0.09, centerY - size * 0.18, size * 0.06, centerX, centerY + size * 0.14, size * 0.5);
15836
+ bodyGradient.addColorStop(0, palette.highlight);
15837
+ bodyGradient.addColorStop(0.25, palette.secondary);
15838
+ bodyGradient.addColorStop(0.68, palette.primary);
15839
+ bodyGradient.addColorStop(1, palette.shadow);
15840
+ context.fillStyle = bodyGradient;
15841
+ context.shadowColor = `${palette.shadow}aa`;
15842
+ context.shadowBlur = size * 0.08;
15843
+ context.shadowOffsetY = size * 0.018;
15844
+ context.fill();
15845
+ context.restore();
15846
+ context.save();
15847
+ traceSmoothClosedPath(context, bodyPoints);
15848
+ context.clip();
15849
+ const interiorGlowGradient = context.createLinearGradient(centerX, centerY - size * 0.22, centerX, centerY + size * 0.36);
15850
+ interiorGlowGradient.addColorStop(0, `${palette.highlight}59`);
15851
+ interiorGlowGradient.addColorStop(0.45, `${palette.accent}1a`);
15852
+ interiorGlowGradient.addColorStop(1, `${palette.shadow}00`);
15853
+ context.fillStyle = interiorGlowGradient;
15854
+ context.fillRect(centerX - size * 0.36, centerY - size * 0.34, size * 0.72, size * 0.76);
15855
+ drawInteriorFilaments(context, centerX, centerY, size, palette, timeMs, shapePhase);
15856
+ drawLowerSuckers(context, centerX, centerY, size, palette, createRandom, timeMs);
15857
+ context.restore();
15858
+ context.save();
15859
+ traceSmoothClosedPath(context, bodyPoints);
15860
+ context.strokeStyle = `${palette.highlight}59`;
15861
+ context.lineWidth = size * 0.014;
15862
+ context.stroke();
15863
+ context.restore();
15864
+ const eyeOffsetX = size * 0.13;
15865
+ const eyeCenterY = centerY - size * 0.02;
15866
+ const eyeRadiusX = size * 0.072;
15867
+ const eyeRadiusY = size * 0.086;
15868
+ drawAlienEye(context, centerX - eyeOffsetX, eyeCenterY, eyeRadiusX, eyeRadiusY, palette, timeMs, shapePhase, interaction);
15869
+ drawAlienEye(context, centerX + eyeOffsetX, eyeCenterY, eyeRadiusX, eyeRadiusY, palette, timeMs, shapePhase + Math.PI / 5, interaction);
15870
+ context.beginPath();
15871
+ context.moveTo(centerX - size * 0.08, centerY + size * 0.12);
15872
+ context.quadraticCurveTo(centerX, centerY + size * (0.175 + Math.sin(timeMs / 520 + shapePhase) * 0.012) + interaction.gazeY * size * 0.01, centerX + size * 0.08, centerY + size * 0.12);
15873
+ context.strokeStyle = `${palette.ink}b3`;
15874
+ context.lineWidth = size * 0.013;
15875
+ context.lineCap = 'round';
15876
+ context.stroke();
15877
+ context.beginPath();
15878
+ context.ellipse(centerX, centerY - size * 0.13, size * 0.16, size * 0.065, 0, Math.PI, Math.PI * 2);
15879
+ context.fillStyle = `${palette.highlight}33`;
15880
+ context.fill();
15881
+ },
15882
+ };
15883
+ /**
15884
+ * Draws translucent inner filaments clipped inside the main body mesh.
15885
+ *
15886
+ * @param context Canvas 2D context.
15887
+ * @param centerX Body center X coordinate.
15888
+ * @param centerY Body center Y coordinate.
15889
+ * @param size Canvas size in CSS pixels.
15890
+ * @param palette Derived avatar palette.
15891
+ * @param timeMs Current animation time in milliseconds.
15892
+ * @param shapePhase Seed-based phase offset.
15893
+ *
15894
+ * @private helper of `octopus2AvatarVisual`
15895
+ */
15896
+ function drawInteriorFilaments(context, centerX, centerY, size, palette, timeMs, shapePhase) {
15897
+ for (let filamentIndex = 0; filamentIndex < 5; filamentIndex++) {
15898
+ const horizontalOffset = (filamentIndex - 2) * size * 0.075;
15899
+ const sway = Math.sin(timeMs / 720 + filamentIndex * 0.8 + shapePhase) * size * 0.028;
15900
+ context.beginPath();
15901
+ context.moveTo(centerX + horizontalOffset * 0.35, centerY - size * 0.11);
15902
+ context.bezierCurveTo(centerX + horizontalOffset - sway * 0.35, centerY - size * 0.01, centerX + horizontalOffset + sway, centerY + size * 0.13, centerX + horizontalOffset * 0.85 + sway * 0.55, centerY + size * 0.3);
15903
+ context.strokeStyle = filamentIndex % 2 === 0 ? `${palette.highlight}29` : `${palette.accent}24`;
15904
+ context.lineWidth = size * (0.01 + filamentIndex * 0.0008);
15905
+ context.lineCap = 'round';
15906
+ context.stroke();
15907
+ }
15908
+ }
15909
+ /**
15910
+ * Draws soft sucker-like highlights in the lower body area.
15911
+ *
15912
+ * @param context Canvas 2D context.
15913
+ * @param centerX Body center X coordinate.
15914
+ * @param centerY Body center Y coordinate.
15915
+ * @param size Canvas size in CSS pixels.
15916
+ * @param palette Derived avatar palette.
15917
+ * @param createRandom Seeded random factory scoped to the avatar.
15918
+ * @param timeMs Current animation time in milliseconds.
15919
+ *
15920
+ * @private helper of `octopus2AvatarVisual`
15921
+ */
15922
+ function drawLowerSuckers(context, centerX, centerY, size, palette, createRandom, timeMs) {
15923
+ const suckerRandom = createRandom('octopus2-suckers');
15924
+ for (let suckerIndex = 0; suckerIndex < 12; suckerIndex++) {
15925
+ const x = centerX + (suckerRandom() - 0.5) * size * 0.36;
15926
+ const y = centerY + size * (0.11 + suckerRandom() * 0.22);
15927
+ const radiusX = size * (0.015 + suckerRandom() * 0.012);
15928
+ const radiusY = radiusX * (0.72 + suckerRandom() * 0.34);
15929
+ const rotation = suckerRandom() * Math.PI + Math.sin(timeMs / 1100 + suckerIndex) * 0.08;
15930
+ context.beginPath();
15931
+ context.ellipse(x, y, radiusX, radiusY, rotation, 0, Math.PI * 2);
15932
+ context.fillStyle = `${palette.highlight}24`;
15933
+ context.fill();
15934
+ context.strokeStyle = `${palette.highlight}40`;
15935
+ context.lineWidth = Math.max(1, size * 0.005);
15936
+ context.stroke();
15937
+ }
15938
+ }
15939
+ /**
15940
+ * Draws one luminous alien eye on top of the organic octopus mesh.
15941
+ *
15942
+ * @param context Canvas 2D context.
15943
+ * @param centerX Eye center X coordinate.
15944
+ * @param centerY Eye center Y coordinate.
15945
+ * @param radiusX Eye horizontal radius.
15946
+ * @param radiusY Eye vertical radius.
15947
+ * @param palette Derived avatar palette.
15948
+ * @param timeMs Current animation time in milliseconds.
15949
+ * @param phase Seed-based animation phase.
15950
+ * @param interaction Smoothed avatar interaction state.
15951
+ *
15952
+ * @private helper of `octopus2AvatarVisual`
15953
+ */
15954
+ function drawAlienEye(context, centerX, centerY, radiusX, radiusY, palette, timeMs, phase, interaction) {
15955
+ const { pupilOffsetX, pupilOffsetY } = resolveOrganicEyeMotion({
15956
+ radiusX,
15957
+ radiusY,
15958
+ timeMs,
15959
+ phase,
15960
+ interaction,
15961
+ autonomousDriftRatioY: 0.1,
15962
+ });
15963
+ context.save();
15964
+ context.beginPath();
15965
+ context.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
15966
+ context.fillStyle = '#f8fbff';
15967
+ context.fill();
15968
+ context.clip();
15969
+ const irisGradient = context.createRadialGradient(centerX - radiusX * 0.18, centerY - radiusY * 0.22, radiusX * 0.05, centerX, centerY, radiusX * 0.9);
15970
+ irisGradient.addColorStop(0, palette.highlight);
15971
+ irisGradient.addColorStop(0.55, palette.secondary);
15972
+ irisGradient.addColorStop(1, palette.shadow);
15973
+ context.beginPath();
15974
+ context.ellipse(centerX + pupilOffsetX, centerY + pupilOffsetY, radiusX * 0.68, radiusY * 0.72, 0, 0, Math.PI * 2);
15975
+ context.fillStyle = irisGradient;
15976
+ context.fill();
15977
+ context.beginPath();
15978
+ context.ellipse(centerX + pupilOffsetX, centerY + pupilOffsetY, radiusX * 0.16, radiusY * 0.48, 0, 0, Math.PI * 2);
15979
+ context.fillStyle = palette.ink;
15980
+ context.fill();
15981
+ context.beginPath();
15982
+ context.ellipse(centerX + pupilOffsetX - radiusX * 0.18, centerY + pupilOffsetY - radiusY * 0.24, radiusX * 0.12, radiusY * 0.14, 0, 0, Math.PI * 2);
15983
+ context.fillStyle = '#ffffff';
15984
+ context.fill();
15985
+ context.restore();
15986
+ context.beginPath();
15987
+ context.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
15988
+ context.strokeStyle = `${palette.shadow}cc`;
15989
+ context.lineWidth = radiusX * 0.2;
15990
+ context.stroke();
15991
+ }
15992
+
15993
+ /* eslint-disable no-magic-numbers */
15994
+ /**
15995
+ * Builds one deterministic morphology profile for `Octopus3`.
15996
+ *
15997
+ * @param createRandom Seeded random factory scoped to the current avatar.
15998
+ * @returns Stable morphology profile.
15999
+ *
16000
+ * @private helper of `octopus3AvatarVisual`
16001
+ */
16002
+ function createOctopus3MorphologyProfile(createRandom) {
16003
+ const bodyRandom = createRandom('octopus3-body-profile');
16004
+ const faceRandom = createRandom('octopus3-face-profile');
16005
+ const detailRandom = createRandom('octopus3-detail-profile');
16006
+ const bodyFamilyRoll = bodyRandom();
16007
+ let bodyFamily;
16008
+ let body;
16009
+ let tentacles;
16010
+ if (bodyFamilyRoll < 0.34) {
16011
+ bodyFamily = 'lantern';
16012
+ body = {
16013
+ centerXJitterRatio: resolveSeededRange(bodyRandom, -0.018, 0.018),
16014
+ centerYRatio: resolveSeededRange(bodyRandom, 0.39, 0.435),
16015
+ bodyRadiusRatio: resolveSeededRange(bodyRandom, 0.19, 0.23),
16016
+ horizontalStretch: resolveSeededRange(bodyRandom, 0.94, 1.08),
16017
+ verticalStretch: resolveSeededRange(bodyRandom, 1.02, 1.18),
16018
+ mantleLiftRatio: resolveSeededRange(bodyRandom, 0.115, 0.148),
16019
+ lowerDropRatio: resolveSeededRange(bodyRandom, 0.042, 0.066),
16020
+ tentacleDepthRatio: resolveSeededRange(bodyRandom, 0.018, 0.03),
16021
+ wobbleAmplitudeRatio: resolveSeededRange(bodyRandom, 0.009, 0.017),
16022
+ lobeCount: resolveSeededIntegerRange(bodyRandom, 4, 6),
16023
+ pointCount: resolveSeededIntegerRange(bodyRandom, 38, 42),
16024
+ shadowWidthRatio: resolveSeededRange(bodyRandom, 0.18, 0.23),
16025
+ shadowHeightRatio: resolveSeededRange(bodyRandom, 0.055, 0.075),
16026
+ crownHighlightWidthRatio: resolveSeededRange(bodyRandom, 0.14, 0.18),
16027
+ crownHighlightHeightRatio: resolveSeededRange(bodyRandom, 0.045, 0.062),
16028
+ crownHighlightYOffsetRatio: resolveSeededRange(bodyRandom, -0.165, -0.135),
16029
+ };
16030
+ tentacles = {
16031
+ count: resolveSeededIntegerRange(bodyRandom, 7, 10),
16032
+ flowLengthScale: resolveSeededRange(bodyRandom, 1.08, 1.34),
16033
+ lateralReachScale: resolveSeededRange(bodyRandom, 0.72, 0.94),
16034
+ tipReachScale: resolveSeededRange(bodyRandom, 0.82, 1.06),
16035
+ baseWidthScale: resolveSeededRange(bodyRandom, 0.82, 0.98),
16036
+ tipWidthScale: resolveSeededRange(bodyRandom, 0.9, 1.08),
16037
+ rootSpreadScale: resolveSeededRange(bodyRandom, 0.76, 0.94),
16038
+ startYOffsetScale: resolveSeededRange(bodyRandom, 0.82, 1),
16039
+ swayScale: resolveSeededRange(bodyRandom, 0.82, 1.02),
16040
+ };
16041
+ }
16042
+ else if (bodyFamilyRoll < 0.68) {
16043
+ bodyFamily = 'drifter';
16044
+ body = {
16045
+ centerXJitterRatio: resolveSeededRange(bodyRandom, -0.025, 0.025),
16046
+ centerYRatio: resolveSeededRange(bodyRandom, 0.425, 0.46),
16047
+ bodyRadiusRatio: resolveSeededRange(bodyRandom, 0.175, 0.215),
16048
+ horizontalStretch: resolveSeededRange(bodyRandom, 1.22, 1.42),
16049
+ verticalStretch: resolveSeededRange(bodyRandom, 0.82, 0.92),
16050
+ mantleLiftRatio: resolveSeededRange(bodyRandom, 0.092, 0.115),
16051
+ lowerDropRatio: resolveSeededRange(bodyRandom, 0.02, 0.036),
16052
+ tentacleDepthRatio: resolveSeededRange(bodyRandom, 0.032, 0.052),
16053
+ wobbleAmplitudeRatio: resolveSeededRange(bodyRandom, 0.013, 0.022),
16054
+ lobeCount: resolveSeededIntegerRange(bodyRandom, 7, 9),
16055
+ pointCount: resolveSeededIntegerRange(bodyRandom, 40, 46),
16056
+ shadowWidthRatio: resolveSeededRange(bodyRandom, 0.24, 0.28),
16057
+ shadowHeightRatio: resolveSeededRange(bodyRandom, 0.06, 0.082),
16058
+ crownHighlightWidthRatio: resolveSeededRange(bodyRandom, 0.17, 0.22),
16059
+ crownHighlightHeightRatio: resolveSeededRange(bodyRandom, 0.038, 0.055),
16060
+ crownHighlightYOffsetRatio: resolveSeededRange(bodyRandom, -0.14, -0.11),
16061
+ };
16062
+ tentacles = {
16063
+ count: resolveSeededIntegerRange(bodyRandom, 10, 13),
16064
+ flowLengthScale: resolveSeededRange(bodyRandom, 0.88, 1.08),
16065
+ lateralReachScale: resolveSeededRange(bodyRandom, 1.18, 1.42),
16066
+ tipReachScale: resolveSeededRange(bodyRandom, 1.12, 1.42),
16067
+ baseWidthScale: resolveSeededRange(bodyRandom, 0.9, 1.06),
16068
+ tipWidthScale: resolveSeededRange(bodyRandom, 0.88, 1.08),
16069
+ rootSpreadScale: resolveSeededRange(bodyRandom, 1.12, 1.32),
16070
+ startYOffsetScale: resolveSeededRange(bodyRandom, 0.92, 1.14),
16071
+ swayScale: resolveSeededRange(bodyRandom, 1.04, 1.22),
16072
+ };
16073
+ }
16074
+ else {
16075
+ bodyFamily = 'rounded';
16076
+ body = {
16077
+ centerXJitterRatio: resolveSeededRange(bodyRandom, -0.02, 0.02),
16078
+ centerYRatio: resolveSeededRange(bodyRandom, 0.398, 0.442),
16079
+ bodyRadiusRatio: resolveSeededRange(bodyRandom, 0.208, 0.248),
16080
+ horizontalStretch: resolveSeededRange(bodyRandom, 1.06, 1.22),
16081
+ verticalStretch: resolveSeededRange(bodyRandom, 0.9, 1.01),
16082
+ mantleLiftRatio: resolveSeededRange(bodyRandom, 0.1, 0.128),
16083
+ lowerDropRatio: resolveSeededRange(bodyRandom, 0.032, 0.052),
16084
+ tentacleDepthRatio: resolveSeededRange(bodyRandom, 0.038, 0.06),
16085
+ wobbleAmplitudeRatio: resolveSeededRange(bodyRandom, 0.014, 0.024),
16086
+ lobeCount: resolveSeededIntegerRange(bodyRandom, 5, 8),
16087
+ pointCount: resolveSeededIntegerRange(bodyRandom, 39, 44),
16088
+ shadowWidthRatio: resolveSeededRange(bodyRandom, 0.2, 0.25),
16089
+ shadowHeightRatio: resolveSeededRange(bodyRandom, 0.055, 0.08),
16090
+ crownHighlightWidthRatio: resolveSeededRange(bodyRandom, 0.16, 0.2),
16091
+ crownHighlightHeightRatio: resolveSeededRange(bodyRandom, 0.05, 0.07),
16092
+ crownHighlightYOffsetRatio: resolveSeededRange(bodyRandom, -0.155, -0.122),
16093
+ };
16094
+ tentacles = {
16095
+ count: resolveSeededIntegerRange(bodyRandom, 8, 12),
16096
+ flowLengthScale: resolveSeededRange(bodyRandom, 0.94, 1.16),
16097
+ lateralReachScale: resolveSeededRange(bodyRandom, 0.9, 1.14),
16098
+ tipReachScale: resolveSeededRange(bodyRandom, 0.96, 1.22),
16099
+ baseWidthScale: resolveSeededRange(bodyRandom, 1.02, 1.2),
16100
+ tipWidthScale: resolveSeededRange(bodyRandom, 1.02, 1.22),
16101
+ rootSpreadScale: resolveSeededRange(bodyRandom, 0.94, 1.08),
16102
+ startYOffsetScale: resolveSeededRange(bodyRandom, 0.9, 1.08),
16103
+ swayScale: resolveSeededRange(bodyRandom, 0.9, 1.1),
16104
+ };
16105
+ }
16106
+ const faceFamilyRoll = faceRandom();
16107
+ let faceFamily;
16108
+ let face;
16109
+ if (faceFamilyRoll < 0.34) {
16110
+ faceFamily = 'watchful';
16111
+ face = {
16112
+ eyeSpacingRatio: resolveSeededRange(faceRandom, 0.118, 0.152),
16113
+ eyeCenterYOffsetRatio: resolveSeededRange(faceRandom, -0.026, -0.002),
16114
+ eyeRadiusXRatio: resolveSeededRange(faceRandom, 0.05, 0.062),
16115
+ eyeHeightRatio: resolveSeededRange(faceRandom, 1.18, 1.38),
16116
+ eyeRotationRange: resolveSeededRange(faceRandom, 0.16, 0.28),
16117
+ eyeTiltBias: resolveSeededRange(faceRandom, 0.02, 0.06),
16118
+ mouthWidthRatio: resolveSeededRange(faceRandom, 0.058, 0.074),
16119
+ mouthYOffsetRatio: resolveSeededRange(faceRandom, 0.086, 0.104),
16120
+ mouthCurveDepthRatio: resolveSeededRange(faceRandom, 0.126, 0.15),
16121
+ mouthCenterOffsetRatio: resolveSeededRange(faceRandom, -0.006, 0.006),
16122
+ mouthCornerTiltRatio: resolveSeededRange(faceRandom, -0.002, 0.002),
16123
+ eyeStyle: {
16124
+ irisScale: resolveSeededRange(faceRandom, 1, 1.1),
16125
+ pupilWidthScale: resolveSeededRange(faceRandom, 0.86, 1.02),
16126
+ pupilHeightScale: resolveSeededRange(faceRandom, 0.94, 1.08),
16127
+ upperLidArchRatio: resolveSeededRange(faceRandom, 0.96, 1.12),
16128
+ upperLidInsetRatio: resolveSeededRange(faceRandom, 0.08, 0.14),
16129
+ lowerLidOpacity: resolveSeededRange(faceRandom, 0.12, 0.22),
16130
+ },
16131
+ };
16132
+ }
16133
+ else if (faceFamilyRoll < 0.68) {
16134
+ faceFamily = 'sleepy';
16135
+ face = {
16136
+ eyeSpacingRatio: resolveSeededRange(faceRandom, 0.092, 0.124),
16137
+ eyeCenterYOffsetRatio: resolveSeededRange(faceRandom, -0.002, 0.024),
16138
+ eyeRadiusXRatio: resolveSeededRange(faceRandom, 0.058, 0.074),
16139
+ eyeHeightRatio: resolveSeededRange(faceRandom, 0.96, 1.14),
16140
+ eyeRotationRange: resolveSeededRange(faceRandom, 0.1, 0.22),
16141
+ eyeTiltBias: resolveSeededRange(faceRandom, 0.01, 0.05),
16142
+ mouthWidthRatio: resolveSeededRange(faceRandom, 0.066, 0.086),
16143
+ mouthYOffsetRatio: resolveSeededRange(faceRandom, 0.094, 0.118),
16144
+ mouthCurveDepthRatio: resolveSeededRange(faceRandom, 0.118, 0.145),
16145
+ mouthCenterOffsetRatio: resolveSeededRange(faceRandom, -0.004, 0.004),
16146
+ mouthCornerTiltRatio: resolveSeededRange(faceRandom, -0.004, 0.004),
16147
+ eyeStyle: {
16148
+ irisScale: resolveSeededRange(faceRandom, 0.9, 1),
16149
+ pupilWidthScale: resolveSeededRange(faceRandom, 1, 1.18),
16150
+ pupilHeightScale: resolveSeededRange(faceRandom, 0.78, 0.92),
16151
+ upperLidArchRatio: resolveSeededRange(faceRandom, 0.7, 0.88),
16152
+ upperLidInsetRatio: resolveSeededRange(faceRandom, -0.02, 0.06),
16153
+ lowerLidOpacity: resolveSeededRange(faceRandom, 0.22, 0.34),
16154
+ },
16155
+ };
16156
+ }
16157
+ else {
16158
+ faceFamily = 'mischief';
16159
+ face = {
16160
+ eyeSpacingRatio: resolveSeededRange(faceRandom, 0.086, 0.114),
16161
+ eyeCenterYOffsetRatio: resolveSeededRange(faceRandom, -0.018, 0.01),
16162
+ eyeRadiusXRatio: resolveSeededRange(faceRandom, 0.046, 0.06),
16163
+ eyeHeightRatio: resolveSeededRange(faceRandom, 1.08, 1.28),
16164
+ eyeRotationRange: resolveSeededRange(faceRandom, 0.28, 0.44),
16165
+ eyeTiltBias: resolveSeededRange(faceRandom, 0.12, 0.22),
16166
+ mouthWidthRatio: resolveSeededRange(faceRandom, 0.052, 0.074),
16167
+ mouthYOffsetRatio: resolveSeededRange(faceRandom, 0.082, 0.1),
16168
+ mouthCurveDepthRatio: resolveSeededRange(faceRandom, 0.116, 0.15),
16169
+ mouthCenterOffsetRatio: resolveSeededRange(faceRandom, -0.018, 0.018),
16170
+ mouthCornerTiltRatio: resolveSeededRange(faceRandom, -0.01, 0.01),
16171
+ eyeStyle: {
16172
+ irisScale: resolveSeededRange(faceRandom, 1.04, 1.12),
16173
+ pupilWidthScale: resolveSeededRange(faceRandom, 0.72, 0.9),
16174
+ pupilHeightScale: resolveSeededRange(faceRandom, 0.96, 1.14),
16175
+ upperLidArchRatio: resolveSeededRange(faceRandom, 0.88, 1.02),
16176
+ upperLidInsetRatio: resolveSeededRange(faceRandom, 0.04, 0.12),
16177
+ lowerLidOpacity: resolveSeededRange(faceRandom, 0.08, 0.18),
16178
+ },
16179
+ };
16180
+ }
16181
+ return {
16182
+ bodyFamily,
16183
+ faceFamily,
16184
+ body,
16185
+ tentacles,
16186
+ face,
16187
+ details: {
16188
+ mantleCurrentCount: resolveSeededIntegerRange(detailRandom, 4, 8),
16189
+ mantleNodeCount: resolveSeededIntegerRange(detailRandom, 3, 7),
16190
+ },
16191
+ };
16192
+ }
16193
+ /**
16194
+ * Resolves one seeded floating-point number inside the provided range.
16195
+ *
16196
+ * @param random Seeded random generator.
16197
+ * @param minimumValue Inclusive lower bound.
16198
+ * @param maximumValue Inclusive upper bound.
16199
+ * @returns Seeded number within the range.
16200
+ *
16201
+ * @private helper of `octopus3AvatarVisual`
16202
+ */
16203
+ function resolveSeededRange(random, minimumValue, maximumValue) {
16204
+ return minimumValue + random() * (maximumValue - minimumValue);
16205
+ }
16206
+ /**
16207
+ * Resolves one seeded integer inside the provided inclusive range.
16208
+ *
16209
+ * @param random Seeded random generator.
16210
+ * @param minimumValue Inclusive lower bound.
16211
+ * @param maximumValue Inclusive upper bound.
16212
+ * @returns Seeded integer within the range.
16213
+ *
16214
+ * @private helper of `octopus3AvatarVisual`
16215
+ */
16216
+ function resolveSeededIntegerRange(random, minimumValue, maximumValue) {
16217
+ return minimumValue + Math.floor(random() * (maximumValue - minimumValue + 1));
16218
+ }
16219
+ /**
16220
+ * Converts an opacity ratio into a two-digit hexadecimal alpha suffix.
16221
+ *
16222
+ * @param opacity Opacity ratio in the range `[0, 1]`.
16223
+ * @returns Two-digit hexadecimal alpha string.
16224
+ *
16225
+ * @private helper of `octopus3AvatarVisual`
16226
+ */
16227
+ function formatAlphaHex(opacity) {
16228
+ return Math.round(Math.min(1, Math.max(0, opacity)) * 255)
16229
+ .toString(16)
16230
+ .padStart(2, '0');
16231
+ }
16232
+ /**
16233
+ * Octopus3 avatar visual.
16234
+ *
16235
+ * @private built-in avatar visual
16236
+ */
16237
+ const octopus3AvatarVisual = {
16238
+ id: 'octopus3',
16239
+ title: 'Octopus3',
16240
+ description: 'Gelatinous alien octopus with a morphing mantle, responsive eyes, and visible ribbon tentacles.',
16241
+ isAnimated: true,
16242
+ supportsPointerTracking: true,
16243
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
16244
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
16245
+ const animationRandom = createRandom('octopus3-animation-profile');
16246
+ const eyeRandom = createRandom('octopus3-eye-profile');
16247
+ const centerX = size * (0.5 + morphologyProfile.body.centerXJitterRatio) + interaction.bodyOffsetX * size * 0.05;
16248
+ const centerY = size * morphologyProfile.body.centerYRatio + interaction.bodyOffsetY * size * 0.035;
16249
+ const bodyRadius = size * morphologyProfile.body.bodyRadiusRatio;
16250
+ const horizontalStretch = morphologyProfile.body.horizontalStretch;
16251
+ const verticalStretch = morphologyProfile.body.verticalStretch;
16252
+ const mantleLift = size * morphologyProfile.body.mantleLiftRatio;
16253
+ const lowerDrop = size * morphologyProfile.body.lowerDropRatio;
16254
+ const tentacleDepth = size * morphologyProfile.body.tentacleDepthRatio;
16255
+ const wobbleAmplitude = size * morphologyProfile.body.wobbleAmplitudeRatio;
16256
+ const lobeCount = morphologyProfile.body.lobeCount;
16257
+ const shapePhase = animationRandom() * Math.PI * 2;
16258
+ const eyeSpacing = size * morphologyProfile.face.eyeSpacingRatio;
16259
+ const eyeCenterY = centerY + size * morphologyProfile.face.eyeCenterYOffsetRatio;
16260
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio;
16261
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio;
16262
+ const bodyPoints = createOrganicOctopusBodyPoints({
16263
+ centerX,
16264
+ centerY,
16265
+ bodyRadius,
16266
+ horizontalStretch,
16267
+ verticalStretch,
16268
+ mantleLift,
16269
+ lowerDrop,
16270
+ tentacleDepth,
16271
+ wobbleAmplitude,
16272
+ lobeCount,
16273
+ shapePhase,
16274
+ timeMs,
16275
+ pointCount: morphologyProfile.body.pointCount,
16276
+ });
16277
+ const tentacleShapes = createOrganicOctopusTentacleShapes({
16278
+ size,
16279
+ centerX,
16280
+ centerY,
16281
+ bodyRadius,
16282
+ horizontalStretch,
16283
+ tentacleCount: morphologyProfile.tentacles.count,
16284
+ shapePhase,
16285
+ createRandom,
16286
+ timeMs,
16287
+ saltPrefix: 'octopus3',
16288
+ bodyPoints,
16289
+ variation: {
16290
+ flowLengthScale: morphologyProfile.tentacles.flowLengthScale,
16291
+ lateralReachScale: morphologyProfile.tentacles.lateralReachScale,
16292
+ tipReachScale: morphologyProfile.tentacles.tipReachScale,
16293
+ baseWidthScale: morphologyProfile.tentacles.baseWidthScale,
16294
+ tipWidthScale: morphologyProfile.tentacles.tipWidthScale,
16295
+ rootSpreadScale: morphologyProfile.tentacles.rootSpreadScale,
16296
+ startYOffsetScale: morphologyProfile.tentacles.startYOffsetScale,
16297
+ swayScale: morphologyProfile.tentacles.swayScale,
16298
+ },
16299
+ });
16300
+ drawAvatarFrame(context, size, palette);
16301
+ drawOctopus3Atmosphere(context, size, palette, centerX, centerY, timeMs, shapePhase, morphologyProfile);
16302
+ context.beginPath();
16303
+ context.ellipse(centerX, centerY + size * 0.25, size * morphologyProfile.body.shadowWidthRatio, size * morphologyProfile.body.shadowHeightRatio, 0, 0, Math.PI * 2);
16304
+ context.fillStyle = `${palette.shadow}33`;
16305
+ context.fill();
16306
+ for (const tentacleShape of tentacleShapes) {
16307
+ drawTentacleRibbon(context, tentacleShape, palette);
16308
+ }
16309
+ context.save();
16310
+ traceSmoothClosedPath(context, bodyPoints);
16311
+ const bodyGradient = context.createRadialGradient(centerX - size * 0.1, centerY - size * 0.18, size * 0.04, centerX, centerY + size * 0.16, size * 0.54);
16312
+ bodyGradient.addColorStop(0, palette.highlight);
16313
+ bodyGradient.addColorStop(0.18, palette.secondary);
16314
+ bodyGradient.addColorStop(0.55, palette.primary);
16315
+ bodyGradient.addColorStop(1, palette.shadow);
16316
+ context.fillStyle = bodyGradient;
16317
+ context.shadowColor = `${palette.shadow}aa`;
16318
+ context.shadowBlur = size * 0.08;
16319
+ context.shadowOffsetY = size * 0.02;
16320
+ context.fill();
16321
+ context.restore();
16322
+ context.save();
16323
+ traceSmoothClosedPath(context, bodyPoints);
16324
+ context.clip();
16325
+ const innerGlowGradient = context.createLinearGradient(centerX, centerY - size * 0.24, centerX, centerY + size * 0.26);
16326
+ innerGlowGradient.addColorStop(0, `${palette.highlight}66`);
16327
+ innerGlowGradient.addColorStop(0.4, `${palette.secondary}26`);
16328
+ innerGlowGradient.addColorStop(1, `${palette.shadow}00`);
16329
+ context.fillStyle = innerGlowGradient;
16330
+ context.fillRect(centerX - size * 0.36, centerY - size * 0.34, size * 0.72, size * 0.72);
16331
+ drawMantleCurrents(context, centerX, centerY, size, palette, timeMs, shapePhase, morphologyProfile);
16332
+ drawMantleNodes(context, centerX, centerY, size, palette, createRandom, morphologyProfile);
16333
+ context.restore();
16334
+ context.save();
16335
+ traceSmoothClosedPath(context, bodyPoints);
16336
+ context.strokeStyle = `${palette.highlight}73`;
16337
+ context.lineWidth = size * 0.013;
16338
+ context.stroke();
16339
+ context.restore();
16340
+ context.beginPath();
16341
+ context.ellipse(centerX, centerY + size * morphologyProfile.body.crownHighlightYOffsetRatio, size * morphologyProfile.body.crownHighlightWidthRatio, size * morphologyProfile.body.crownHighlightHeightRatio, 0, Math.PI, Math.PI * 2);
16342
+ context.fillStyle = `${palette.highlight}3d`;
16343
+ context.fill();
16344
+ drawSeededEye(context, centerX - eyeSpacing, eyeCenterY, eyeRadiusX, eyeRadiusY, -morphologyProfile.face.eyeTiltBias + (eyeRandom() - 0.5) * morphologyProfile.face.eyeRotationRange, palette, timeMs, shapePhase, interaction, morphologyProfile.face.eyeStyle);
16345
+ drawSeededEye(context, centerX + eyeSpacing, eyeCenterY, eyeRadiusX, eyeRadiusY, morphologyProfile.face.eyeTiltBias + (eyeRandom() - 0.5) * morphologyProfile.face.eyeRotationRange, palette, timeMs, shapePhase + Math.PI / 4, interaction, morphologyProfile.face.eyeStyle);
16346
+ const mouthHalfWidth = size * morphologyProfile.face.mouthWidthRatio;
16347
+ const mouthY = centerY + size * morphologyProfile.face.mouthYOffsetRatio;
16348
+ const mouthCornerTilt = size * morphologyProfile.face.mouthCornerTiltRatio;
16349
+ context.beginPath();
16350
+ context.moveTo(centerX - mouthHalfWidth, mouthY - mouthCornerTilt);
16351
+ context.quadraticCurveTo(centerX + size * morphologyProfile.face.mouthCenterOffsetRatio, centerY +
16352
+ size * (morphologyProfile.face.mouthCurveDepthRatio + Math.sin(timeMs / 620 + shapePhase) * 0.016) +
16353
+ interaction.gazeY * size * 0.012, centerX + mouthHalfWidth, mouthY + mouthCornerTilt);
16354
+ context.strokeStyle = `${palette.ink}b3`;
16355
+ context.lineWidth = size * 0.012;
16356
+ context.lineCap = 'round';
16357
+ context.stroke();
16358
+ },
16359
+ };
16360
+ /**
16361
+ * Draws the deep-sea glow around the Octopus3 silhouette.
16362
+ *
16363
+ * @param context Canvas 2D context.
16364
+ * @param size Canvas size in CSS pixels.
16365
+ * @param palette Derived avatar palette.
16366
+ * @param centerX Body center X coordinate.
16367
+ * @param centerY Body center Y coordinate.
16368
+ * @param timeMs Current animation time in milliseconds.
16369
+ * @param shapePhase Seed-based phase offset.
16370
+ * @param morphologyProfile Seeded morphology profile.
16371
+ *
16372
+ * @private helper of `octopus3AvatarVisual`
16373
+ */
16374
+ function drawOctopus3Atmosphere(context, size, palette, centerX, centerY, timeMs, shapePhase, morphologyProfile) {
16375
+ const haloGradient = context.createRadialGradient(centerX, centerY - size * (0.07 + (morphologyProfile.body.verticalStretch - 0.9) * 0.05), size * 0.06, centerX, centerY, size * (0.56 + morphologyProfile.body.bodyRadiusRatio * 0.45));
16376
+ haloGradient.addColorStop(0, `${palette.highlight}5c`);
16377
+ haloGradient.addColorStop(0.35, `${palette.accent}26`);
16378
+ haloGradient.addColorStop(1, `${palette.highlight}00`);
16379
+ context.fillStyle = haloGradient;
16380
+ context.fillRect(0, 0, size, size);
16381
+ const lowerGlowGradient = context.createRadialGradient(centerX + Math.sin(timeMs / 1600 + shapePhase) * size * 0.04, centerY + size * (0.18 + morphologyProfile.tentacles.flowLengthScale * 0.025), size * 0.04, centerX, centerY + size * (0.18 + morphologyProfile.tentacles.flowLengthScale * 0.025), size * (0.42 + morphologyProfile.tentacles.lateralReachScale * 0.06));
16382
+ lowerGlowGradient.addColorStop(0, `${palette.secondary}1f`);
16383
+ lowerGlowGradient.addColorStop(1, `${palette.secondary}00`);
16384
+ context.fillStyle = lowerGlowGradient;
16385
+ context.fillRect(0, 0, size, size);
16386
+ }
16387
+ /**
16388
+ * Draws one ribbon tentacle with a filled organic profile and visible sucker highlights.
16389
+ *
16390
+ * @param context Canvas 2D context.
16391
+ * @param tentacleShape Deterministic tentacle descriptor.
16392
+ * @param palette Derived avatar palette.
16393
+ *
16394
+ * @private helper of `octopus3AvatarVisual`
16395
+ */
16396
+ function drawTentacleRibbon(context, tentacleShape, palette) {
16397
+ const ribbonPoints = sampleOrganicTentacleRibbonPoints(tentacleShape);
16398
+ const gradient = context.createLinearGradient(tentacleShape.startPoint.x, tentacleShape.startPoint.y, tentacleShape.endPoint.x, tentacleShape.endPoint.y);
16399
+ gradient.addColorStop(0, tentacleShape.colorBias < 0.35 ? palette.secondary : palette.primary);
16400
+ gradient.addColorStop(0.58, palette.primary);
16401
+ gradient.addColorStop(1, tentacleShape.colorBias > 0.65 ? palette.highlight : palette.accent);
16402
+ context.save();
16403
+ traceTentacleRibbonPath(context, ribbonPoints);
16404
+ context.fillStyle = gradient;
16405
+ context.shadowColor = `${palette.shadow}80`;
16406
+ context.shadowBlur = tentacleShape.baseWidth * 1.2;
16407
+ context.shadowOffsetY = tentacleShape.baseWidth * 0.2;
16408
+ context.fill();
16409
+ context.restore();
16410
+ context.save();
16411
+ traceTentacleRibbonPath(context, ribbonPoints);
16412
+ context.strokeStyle = tentacleShape.highlightBias > 0.5 ? `${palette.highlight}52` : `${palette.highlight}38`;
16413
+ context.lineWidth = Math.max(1, tentacleShape.baseWidth * 0.12);
16414
+ context.stroke();
16415
+ context.restore();
16416
+ context.beginPath();
16417
+ context.moveTo(tentacleShape.startPoint.x, tentacleShape.startPoint.y);
16418
+ context.bezierCurveTo(tentacleShape.controlPointOne.x, tentacleShape.controlPointOne.y, tentacleShape.controlPointTwo.x, tentacleShape.controlPointTwo.y, tentacleShape.endPoint.x, tentacleShape.endPoint.y);
16419
+ context.strokeStyle = `${palette.highlight}2e`;
16420
+ context.lineWidth = Math.max(1, tentacleShape.tipWidth * 0.9);
16421
+ context.lineCap = 'round';
16422
+ context.stroke();
16423
+ drawTentacleSuckers(context, tentacleShape, palette);
16424
+ }
16425
+ /**
16426
+ * Traces a closed ribbon path from sampled tentacle points.
16427
+ *
16428
+ * @param context Canvas 2D context.
16429
+ * @param ribbonPoints Sampled ribbon points.
16430
+ *
16431
+ * @private helper of `octopus3AvatarVisual`
16432
+ */
16433
+ function traceTentacleRibbonPath(context, ribbonPoints) {
16434
+ const firstRibbonPoint = ribbonPoints[0];
16435
+ context.beginPath();
16436
+ context.moveTo(firstRibbonPoint.x + firstRibbonPoint.normalX * firstRibbonPoint.width, firstRibbonPoint.y + firstRibbonPoint.normalY * firstRibbonPoint.width);
16437
+ for (const ribbonPoint of ribbonPoints.slice(1)) {
16438
+ context.lineTo(ribbonPoint.x + ribbonPoint.normalX * ribbonPoint.width, ribbonPoint.y + ribbonPoint.normalY * ribbonPoint.width);
16439
+ }
16440
+ for (const ribbonPoint of [...ribbonPoints].reverse()) {
16441
+ context.lineTo(ribbonPoint.x - ribbonPoint.normalX * ribbonPoint.width, ribbonPoint.y - ribbonPoint.normalY * ribbonPoint.width);
16442
+ }
16443
+ context.closePath();
16444
+ }
16445
+ /**
16446
+ * Draws a row of soft sucker highlights along one side of the ribbon tentacle.
16447
+ *
16448
+ * @param context Canvas 2D context.
16449
+ * @param tentacleShape Deterministic tentacle descriptor.
16450
+ * @param palette Derived avatar palette.
16451
+ *
16452
+ * @private helper of `octopus3AvatarVisual`
16453
+ */
16454
+ function drawTentacleSuckers(context, tentacleShape, palette) {
16455
+ const undersideDirection = tentacleShape.endPoint.x >= tentacleShape.startPoint.x ? -1 : 1;
16456
+ for (let suckerIndex = 0; suckerIndex < 4; suckerIndex++) {
16457
+ const progress = 0.22 + suckerIndex * 0.17;
16458
+ const point = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, progress);
16459
+ const previousPoint = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, Math.max(0, progress - 0.03));
16460
+ const nextPoint = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, Math.min(1, progress + 0.03));
16461
+ const tangentX = nextPoint.x - previousPoint.x;
16462
+ const tangentY = nextPoint.y - previousPoint.y;
16463
+ const tangentLength = Math.hypot(tangentX, tangentY) || 1;
16464
+ const normalX = (-tangentY / tangentLength) * undersideDirection;
16465
+ const normalY = (tangentX / tangentLength) * undersideDirection;
16466
+ const width = tentacleShape.baseWidth + (tentacleShape.tipWidth - tentacleShape.baseWidth) * Math.pow(progress, 1.1);
16467
+ const suckerX = point.x + normalX * width * 0.52;
16468
+ const suckerY = point.y + normalY * width * 0.52;
16469
+ const rotation = Math.atan2(normalY, normalX);
16470
+ context.beginPath();
16471
+ context.ellipse(suckerX, suckerY, width * 0.22, width * 0.11, rotation, 0, Math.PI * 2);
16472
+ context.fillStyle = `${palette.highlight}73`;
16473
+ context.fill();
16474
+ context.strokeStyle = `${palette.highlight}99`;
16475
+ context.lineWidth = Math.max(1, width * 0.08);
16476
+ context.stroke();
16477
+ }
16478
+ }
16479
+ /**
16480
+ * Draws slow inner currents inside the clipped Octopus3 mantle.
16481
+ *
16482
+ * @param context Canvas 2D context.
16483
+ * @param centerX Body center X coordinate.
16484
+ * @param centerY Body center Y coordinate.
16485
+ * @param size Canvas size in CSS pixels.
16486
+ * @param palette Derived avatar palette.
16487
+ * @param timeMs Current animation time in milliseconds.
16488
+ * @param shapePhase Seed-based phase offset.
16489
+ * @param morphologyProfile Seeded morphology profile.
16490
+ *
16491
+ * @private helper of `octopus3AvatarVisual`
16492
+ */
16493
+ function drawMantleCurrents(context, centerX, centerY, size, palette, timeMs, shapePhase, morphologyProfile) {
16494
+ const centeredCurrentIndex = (morphologyProfile.details.mantleCurrentCount - 1) / 2;
16495
+ for (let currentIndex = 0; currentIndex < morphologyProfile.details.mantleCurrentCount; currentIndex++) {
16496
+ const horizontalOffset = (currentIndex - centeredCurrentIndex) *
16497
+ size *
16498
+ (0.05 + (morphologyProfile.body.horizontalStretch - 0.9) * 0.025);
16499
+ const sway = Math.sin(timeMs / 680 + currentIndex * 0.78 + shapePhase) * size * 0.024;
16500
+ context.beginPath();
16501
+ context.moveTo(centerX + horizontalOffset * 0.3, centerY - size * (0.11 + morphologyProfile.body.verticalStretch * 0.02));
16502
+ context.bezierCurveTo(centerX + horizontalOffset - sway * 0.25, centerY - size * 0.04, centerX + horizontalOffset + sway, centerY + size * 0.06, centerX + horizontalOffset * 0.7 + sway * 0.46, centerY + size * (0.16 + morphologyProfile.body.verticalStretch * 0.035));
16503
+ context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}30` : `${palette.accent}26`;
16504
+ context.lineWidth =
16505
+ size * (0.0075 + currentIndex * 0.00065 + morphologyProfile.tentacles.baseWidthScale * 0.0005);
16506
+ context.lineCap = 'round';
16507
+ context.stroke();
16508
+ }
16509
+ }
16510
+ /**
16511
+ * Draws seeded luminous nodes inside the Octopus3 mantle.
16512
+ *
16513
+ * @param context Canvas 2D context.
16514
+ * @param centerX Body center X coordinate.
16515
+ * @param centerY Body center Y coordinate.
16516
+ * @param size Canvas size in CSS pixels.
16517
+ * @param palette Derived avatar palette.
16518
+ * @param createRandom Seeded random factory scoped to the avatar.
16519
+ * @param morphologyProfile Seeded morphology profile.
16520
+ *
16521
+ * @private helper of `octopus3AvatarVisual`
16522
+ */
16523
+ function drawMantleNodes(context, centerX, centerY, size, palette, createRandom, morphologyProfile) {
16524
+ for (let nodeIndex = 0; nodeIndex < morphologyProfile.details.mantleNodeCount; nodeIndex++) {
16525
+ const nodeRandom = createRandom(`octopus3-node-${nodeIndex}`);
16526
+ const nodeX = centerX + (nodeRandom() - 0.5) * size * (0.2 + (morphologyProfile.body.horizontalStretch - 0.9) * 0.16);
16527
+ const nodeY = centerY -
16528
+ size * 0.03 +
16529
+ (nodeRandom() - 0.5) * size * (0.16 + (morphologyProfile.body.verticalStretch - 0.82) * 0.1);
16530
+ const nodeRadius = size * (0.016 + nodeRandom() * 0.016);
16531
+ context.beginPath();
16532
+ context.arc(nodeX, nodeY, nodeRadius, 0, Math.PI * 2);
16533
+ context.fillStyle = nodeIndex % 2 === 0 ? `${palette.highlight}40` : `${palette.accent}33`;
16534
+ context.fill();
16535
+ }
16536
+ }
16537
+ /**
16538
+ * Draws one expressive seeded eye with a slit pupil and slightly tilted eyelids.
16539
+ *
16540
+ * @param context Canvas 2D context.
16541
+ * @param centerX Eye center X coordinate.
16542
+ * @param centerY Eye center Y coordinate.
16543
+ * @param radiusX Eye horizontal radius.
16544
+ * @param radiusY Eye vertical radius.
16545
+ * @param rotation Eye rotation in radians.
16546
+ * @param palette Derived avatar palette.
16547
+ * @param timeMs Current animation time in milliseconds.
16548
+ * @param phase Seed-based animation phase.
16549
+ * @param interaction Smoothed avatar interaction state.
16550
+ * @param eyeStyle Seeded eye-style traits.
16551
+ *
16552
+ * @private helper of `octopus3AvatarVisual`
16553
+ */
16554
+ function drawSeededEye(context, centerX, centerY, radiusX, radiusY, rotation, palette, timeMs, phase, interaction, eyeStyle) {
16555
+ const { pupilOffsetX, pupilOffsetY } = resolveOrganicEyeMotion({
16556
+ radiusX,
16557
+ radiusY,
16558
+ timeMs,
16559
+ phase,
16560
+ interaction,
16561
+ });
16562
+ context.save();
16563
+ context.translate(centerX, centerY);
16564
+ context.rotate(rotation);
16565
+ context.beginPath();
16566
+ context.ellipse(0, 0, radiusX, radiusY, 0, 0, Math.PI * 2);
16567
+ context.fillStyle = '#f8fbff';
16568
+ context.fill();
16569
+ context.clip();
16570
+ const irisGradient = context.createRadialGradient(-radiusX * 0.18, -radiusY * 0.24, radiusX * 0.05, 0, 0, radiusX * 0.92);
16571
+ irisGradient.addColorStop(0, palette.highlight);
16572
+ irisGradient.addColorStop(0.58, palette.secondary);
16573
+ irisGradient.addColorStop(1, palette.shadow);
16574
+ context.beginPath();
16575
+ context.ellipse(pupilOffsetX, pupilOffsetY, radiusX * 0.66 * eyeStyle.irisScale, radiusY * 0.74 * eyeStyle.irisScale, 0, 0, Math.PI * 2);
16576
+ context.fillStyle = irisGradient;
16577
+ context.fill();
16578
+ context.beginPath();
16579
+ context.ellipse(pupilOffsetX, pupilOffsetY, radiusX * 0.14 * eyeStyle.pupilWidthScale, radiusY * 0.5 * eyeStyle.pupilHeightScale, 0, 0, Math.PI * 2);
16580
+ context.fillStyle = palette.ink;
16581
+ context.fill();
16582
+ context.beginPath();
16583
+ context.ellipse(pupilOffsetX - radiusX * 0.22, pupilOffsetY - radiusY * 0.24, radiusX * 0.12, radiusY * 0.14, 0, 0, Math.PI * 2);
16584
+ context.fillStyle = '#ffffff';
16585
+ context.fill();
16586
+ context.restore();
16587
+ context.save();
16588
+ context.translate(centerX, centerY);
16589
+ context.rotate(rotation);
16590
+ context.beginPath();
16591
+ context.ellipse(0, 0, radiusX, radiusY, 0, 0, Math.PI * 2);
16592
+ context.strokeStyle = `${palette.shadow}d9`;
16593
+ context.lineWidth = radiusX * 0.18;
16594
+ context.stroke();
16595
+ context.beginPath();
16596
+ context.moveTo(-radiusX * 0.88, -radiusY * eyeStyle.upperLidInsetRatio);
16597
+ context.quadraticCurveTo(0, -radiusY * (eyeStyle.upperLidArchRatio - interaction.gazeY * 0.16 + interaction.intensity * 0.08), radiusX * 0.88, -radiusY * eyeStyle.upperLidInsetRatio);
16598
+ context.strokeStyle = `${palette.shadow}73`;
16599
+ context.lineWidth = radiusX * 0.16;
16600
+ context.lineCap = 'round';
16601
+ context.stroke();
16602
+ if (eyeStyle.lowerLidOpacity > 0) {
16603
+ context.beginPath();
16604
+ context.moveTo(-radiusX * 0.74, radiusY * 0.2);
16605
+ context.quadraticCurveTo(0, radiusY * 0.38, radiusX * 0.74, radiusY * 0.2);
16606
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex(eyeStyle.lowerLidOpacity)}`;
16607
+ context.lineWidth = radiusX * 0.08;
16608
+ context.lineCap = 'round';
16609
+ context.stroke();
16610
+ }
16611
+ context.restore();
16612
+ }
16613
+
16614
+ /* eslint-disable no-magic-numbers */
16615
+ /**
16616
+ * Family variants used by the orb renderer.
16617
+ *
16618
+ * @private helper of `orbAvatarVisual`
16619
+ */
16620
+ const ORB_FAMILIES = ['pearl', 'nebula', 'ember', 'glacier'];
16621
+ /**
16622
+ * Built-in Orb avatar visual.
16623
+ *
16624
+ * @private built-in avatar visual
16625
+ */
16626
+ const orbAvatarVisual = {
16627
+ id: 'orb',
16628
+ title: 'Orb',
16629
+ description: 'Glowing morphing circle-orb with seeded gradients, smooth deformations, and luminous layered depth.',
16630
+ isAnimated: true,
16631
+ render({ context, size, palette, createRandom, timeMs }) {
16632
+ const profile = createOrbMorphologyProfile(createRandom);
16633
+ const colorSet = resolveOrbColorSet(palette, profile.family);
16634
+ const centerX = size * 0.5 + profile.coreShiftX * size * 0.06 + Math.sin(timeMs / 2600) * size * 0.009;
16635
+ const centerY = size * 0.5 +
16636
+ profile.coreShiftY * size * 0.06 +
16637
+ Math.cos(timeMs / 3100 + profile.highlightAngle) * size * 0.01;
16638
+ const radius = size * profile.baseRadiusRatio;
16639
+ const silhouettePoints = createOrbSilhouettePoints({
16640
+ centerX,
16641
+ centerY,
16642
+ radius,
16643
+ profile,
16644
+ timeMs,
16645
+ });
16646
+ drawAvatarFrame(context, size, palette);
16647
+ drawOrbAtmosphere(context, size, centerX, centerY, radius, colorSet, profile, timeMs);
16648
+ drawOrbBody(context, silhouettePoints, centerX, centerY, radius, colorSet, profile, size);
16649
+ context.save();
16650
+ traceSmoothClosedPath(context, silhouettePoints);
16651
+ context.clip();
16652
+ drawOrbInteriorRings(context, centerX, centerY, radius, colorSet, profile, size, timeMs);
16653
+ drawOrbSparkles(context, centerX, centerY, radius, colorSet, profile, size, createRandom, timeMs);
16654
+ drawOrbSheen(context, centerX, centerY, radius, colorSet, profile, size, timeMs);
16655
+ drawOrbCore(context, centerX, centerY, radius, colorSet, profile, size, timeMs);
16656
+ context.restore();
16657
+ drawOrbRim(context, silhouettePoints, colorSet, size);
16658
+ },
16659
+ };
16660
+ /**
16661
+ * Builds the deterministic orb profile from the seeded avatar random factory.
16662
+ *
16663
+ * @param createRandom Seeded random factory.
16664
+ * @returns Stable orb morphology profile.
16665
+ *
16666
+ * @private helper of `orbAvatarVisual`
16667
+ */
16668
+ function createOrbMorphologyProfile(createRandom) {
16669
+ const family = pickRandomItem(ORB_FAMILIES, createRandom('orb-family'));
16670
+ const familyAdjustment = resolveOrbFamilyAdjustment(family);
16671
+ const layoutRandom = createRandom('orb-layout');
16672
+ const effectRandom = createRandom('orb-effects');
16673
+ return {
16674
+ family,
16675
+ baseRadiusRatio: clampNumber(0.255 + layoutRandom() * 0.055 + familyAdjustment.baseRadiusRatio, 0.22, 0.335),
16676
+ horizontalStretch: clampNumber(0.93 + layoutRandom() * 0.13 + familyAdjustment.horizontalStretch, 0.88, 1.16),
16677
+ verticalStretch: clampNumber(0.91 + layoutRandom() * 0.13 + familyAdjustment.verticalStretch, 0.88, 1.15),
16678
+ wobbleAmplitude: clampNumber(0.038 + effectRandom() * 0.042 + familyAdjustment.wobbleAmplitude, 0.022, 0.12),
16679
+ wobbleFrequencyOne: clampInteger(2 + Math.floor(effectRandom() * 3) + familyAdjustment.wobbleFrequency, 2, 7),
16680
+ wobbleFrequencyTwo: clampInteger(3 + Math.floor(layoutRandom() * 3) + familyAdjustment.wobbleFrequency, 3, 8),
16681
+ wobbleFrequencyThree: clampInteger(5 + Math.floor(effectRandom() * 3) + familyAdjustment.wobbleFrequency, 4, 9),
16682
+ ringCount: clampInteger(2 + Math.floor(effectRandom() * 3) + familyAdjustment.ringCount, 2, 5),
16683
+ sparkleCount: clampInteger(6 + Math.floor(effectRandom() * 7) + familyAdjustment.sparkleCount, 4, 16),
16684
+ haloCount: clampInteger(2 + Math.floor(layoutRandom() * 2) + familyAdjustment.haloCount, 1, 4),
16685
+ coreShiftX: (layoutRandom() - 0.5) * 0.08,
16686
+ coreShiftY: (effectRandom() - 0.5) * 0.08,
16687
+ highlightAngle: layoutRandom() * Math.PI * 2,
16688
+ bandRotation: effectRandom() * Math.PI * 2,
16689
+ pulseSpeed: 0.82 + effectRandom() * 0.72,
16690
+ haloBlurRatio: clampNumber(0.18 + layoutRandom() * 0.12 + familyAdjustment.haloBlurRatio, 0.16, 0.34),
16691
+ sheenStrength: clampNumber(0.46 + effectRandom() * 0.28 + familyAdjustment.sheenStrength, 0.38, 0.88),
16692
+ };
16693
+ }
16694
+ /**
16695
+ * Resolves the family-specific adjustments that keep the orb surface varied while still circular.
16696
+ *
16697
+ * @param family Selected orb family.
16698
+ * @returns Family-specific profile adjustments.
16699
+ *
16700
+ * @private helper of `orbAvatarVisual`
16701
+ */
16702
+ function resolveOrbFamilyAdjustment(family) {
16703
+ switch (family) {
16704
+ case 'nebula':
16705
+ return {
16706
+ baseRadiusRatio: 0.006,
16707
+ horizontalStretch: 0.05,
16708
+ verticalStretch: -0.01,
16709
+ wobbleAmplitude: 0.012,
16710
+ wobbleFrequency: 1,
16711
+ ringCount: 1,
16712
+ sparkleCount: 2,
16713
+ haloCount: 0,
16714
+ haloBlurRatio: 0.02,
16715
+ sheenStrength: 0.08,
16716
+ };
16717
+ case 'ember':
16718
+ return {
16719
+ baseRadiusRatio: -0.004,
16720
+ horizontalStretch: -0.03,
16721
+ verticalStretch: 0.03,
16722
+ wobbleAmplitude: 0.02,
16723
+ wobbleFrequency: 1,
16724
+ ringCount: 0,
16725
+ sparkleCount: 3,
16726
+ haloCount: 0,
16727
+ haloBlurRatio: 0.04,
16728
+ sheenStrength: 0.06,
16729
+ };
16730
+ case 'glacier':
16731
+ return {
16732
+ baseRadiusRatio: 0.012,
16733
+ horizontalStretch: 0.01,
16734
+ verticalStretch: 0.02,
16735
+ wobbleAmplitude: -0.006,
16736
+ wobbleFrequency: -1,
16737
+ ringCount: 0,
16738
+ sparkleCount: 0,
16739
+ haloCount: 0,
16740
+ haloBlurRatio: -0.01,
16741
+ sheenStrength: 0.12,
16742
+ };
16743
+ case 'pearl':
16744
+ default:
16745
+ return {
16746
+ baseRadiusRatio: 0.02,
16747
+ horizontalStretch: 0.025,
16748
+ verticalStretch: 0.02,
16749
+ wobbleAmplitude: -0.01,
16750
+ wobbleFrequency: 0,
16751
+ ringCount: 0,
16752
+ sparkleCount: -1,
16753
+ haloCount: 1,
16754
+ haloBlurRatio: 0.02,
16755
+ sheenStrength: 0.1,
16756
+ };
16757
+ }
16758
+ }
16759
+ /**
16760
+ * Resolves the color set used by the orb renderer.
16761
+ *
16762
+ * @param palette Base avatar palette.
16763
+ * @param family Selected orb family.
16764
+ * @returns Derived orb-specific color set.
16765
+ *
16766
+ * @private helper of `orbAvatarVisual`
16767
+ */
16768
+ function resolveOrbColorSet(palette, family) {
16769
+ const primaryColor = Color.fromSafe(palette.primary);
16770
+ const secondaryColor = Color.fromSafe(palette.secondary);
16771
+ const accentColor = Color.fromSafe(palette.accent);
16772
+ const highlightColor = Color.fromSafe(palette.highlight);
16773
+ const shadowColor = Color.fromSafe(palette.shadow);
16774
+ switch (family) {
16775
+ case 'nebula':
16776
+ return {
16777
+ core: accentColor.then(lighten(0.16)).then(saturate(0.14)).toHex(),
16778
+ mid: secondaryColor.then(lighten(0.08)).then(saturate(0.08)).toHex(),
16779
+ outer: primaryColor.then(darken(0.03)).then(saturate(0.04)).toHex(),
16780
+ rim: shadowColor.then(lighten(0.08)).toHex(),
16781
+ highlight: highlightColor.then(lighten(0.14)).toHex(),
16782
+ aura: secondaryColor.then(lighten(0.18)).toHex(),
16783
+ };
16784
+ case 'ember':
16785
+ return {
16786
+ core: accentColor.then(lighten(0.2)).then(saturate(0.16)).toHex(),
16787
+ mid: primaryColor.then(lighten(0.1)).then(saturate(0.08)).toHex(),
16788
+ outer: secondaryColor.then(darken(0.04)).then(saturate(0.04)).toHex(),
16789
+ rim: shadowColor.then(lighten(0.08)).toHex(),
16790
+ highlight: highlightColor.then(lighten(0.18)).toHex(),
16791
+ aura: accentColor.then(lighten(0.12)).toHex(),
16792
+ };
16793
+ case 'glacier':
16794
+ return {
16795
+ core: highlightColor.then(lighten(0.18)).then(saturate(-0.04)).toHex(),
16796
+ mid: secondaryColor.then(lighten(0.1)).then(saturate(-0.06)).toHex(),
16797
+ outer: primaryColor.then(lighten(0.06)).toHex(),
16798
+ rim: shadowColor.then(lighten(0.12)).toHex(),
16799
+ highlight: highlightColor.then(lighten(0.26)).toHex(),
16800
+ aura: primaryColor.then(lighten(0.18)).toHex(),
16801
+ };
16802
+ case 'pearl':
16803
+ default:
16804
+ return {
16805
+ core: highlightColor.then(lighten(0.2)).then(saturate(-0.06)).toHex(),
16806
+ mid: primaryColor.then(lighten(0.12)).then(saturate(-0.02)).toHex(),
16807
+ outer: secondaryColor.then(lighten(0.08)).then(saturate(-0.04)).toHex(),
16808
+ rim: shadowColor.then(lighten(0.12)).toHex(),
16809
+ highlight: highlightColor.then(lighten(0.3)).toHex(),
16810
+ aura: highlightColor.then(lighten(0.18)).toHex(),
16811
+ };
16812
+ }
16813
+ }
16814
+ /**
16815
+ * Creates the orb silhouette points from the deterministic profile.
16816
+ *
16817
+ * @param options Orb geometry options.
16818
+ * @returns Smoothly varying orb outline.
16819
+ *
16820
+ * @private helper of `orbAvatarVisual`
16821
+ */
16822
+ function createOrbSilhouettePoints(options) {
16823
+ const { centerX, centerY, radius, profile, timeMs } = options;
16824
+ const pointCount = 48;
16825
+ return Array.from({ length: pointCount }, (_, pointIndex) => {
16826
+ const progress = pointIndex / pointCount;
16827
+ const angle = -Math.PI / 2 + progress * Math.PI * 2;
16828
+ const breathing = Math.sin(timeMs / (1450 / profile.pulseSpeed) + profile.bandRotation) * profile.wobbleAmplitude;
16829
+ const surfaceWaveOne = Math.sin(angle * profile.wobbleFrequencyOne + profile.highlightAngle + timeMs / (980 / profile.pulseSpeed)) * profile.wobbleAmplitude;
16830
+ const surfaceWaveTwo = Math.cos(angle * profile.wobbleFrequencyTwo - profile.bandRotation * 0.8 + timeMs / (1320 / profile.pulseSpeed)) *
16831
+ profile.wobbleAmplitude *
16832
+ 0.62;
16833
+ const surfaceWaveThree = Math.sin(angle * profile.wobbleFrequencyThree +
16834
+ profile.highlightAngle * 1.4 -
16835
+ timeMs / (1710 / profile.pulseSpeed)) *
16836
+ profile.wobbleAmplitude *
16837
+ 0.38;
16838
+ const surfaceTaper = Math.sin(angle * 2 + profile.highlightAngle) * profile.wobbleAmplitude * 0.2;
16839
+ const localRadius = radius * (1 + breathing * 0.12 + surfaceWaveOne + surfaceWaveTwo + surfaceWaveThree + surfaceTaper);
16840
+ return {
16841
+ x: centerX +
16842
+ Math.cos(angle) * localRadius * profile.horizontalStretch +
16843
+ Math.sin(angle * 3 + profile.highlightAngle + timeMs / 2100) * radius * 0.012,
16844
+ y: centerY +
16845
+ Math.sin(angle) * localRadius * profile.verticalStretch +
16846
+ Math.cos(angle * 2 - profile.highlightAngle + timeMs / 2400) * radius * 0.01,
16847
+ };
16848
+ });
16849
+ }
16850
+ /**
16851
+ * Draws the atmospheric glow behind the orb.
16852
+ *
16853
+ * @param context Canvas 2D context.
16854
+ * @param size Canvas size in CSS pixels.
16855
+ * @param centerX Orb center X coordinate.
16856
+ * @param centerY Orb center Y coordinate.
16857
+ * @param radius Orb base radius.
16858
+ * @param colorSet Derived orb color set.
16859
+ * @param profile Deterministic orb profile.
16860
+ * @param timeMs Current animation time in milliseconds.
16861
+ *
16862
+ * @private helper of `orbAvatarVisual`
16863
+ */
16864
+ function drawOrbAtmosphere(context, size, centerX, centerY, radius, colorSet, profile, timeMs) {
16865
+ const atmosphereGradient = context.createRadialGradient(centerX, centerY, radius * 0.08, centerX, centerY, radius * (1.9 + profile.haloBlurRatio));
16866
+ atmosphereGradient.addColorStop(0, `${colorSet.highlight}26`);
16867
+ atmosphereGradient.addColorStop(0.32, `${colorSet.aura}16`);
16868
+ atmosphereGradient.addColorStop(1, `${colorSet.aura}00`);
16869
+ context.save();
16870
+ context.globalCompositeOperation = 'screen';
16871
+ context.fillStyle = atmosphereGradient;
16872
+ context.fillRect(0, 0, size, size);
16873
+ for (let haloIndex = 0; haloIndex < profile.haloCount; haloIndex++) {
16874
+ const haloPulse = Math.sin(timeMs / (1200 + haloIndex * 180) + profile.highlightAngle) * radius * 0.025;
16875
+ const haloRadiusX = radius * (1.18 + haloIndex * 0.18) + haloPulse;
16876
+ const haloRadiusY = radius * (0.98 + haloIndex * 0.16) + haloPulse * 0.82;
16877
+ const haloGradient = context.createRadialGradient(centerX, centerY, radius * 0.12, centerX, centerY, haloRadiusX * 1.12);
16878
+ haloGradient.addColorStop(0, `${colorSet.aura}${haloIndex === 0 ? '38' : '24'}`);
16879
+ haloGradient.addColorStop(0.5, `${colorSet.highlight}${haloIndex === 0 ? '1f' : '12'}`);
16880
+ haloGradient.addColorStop(1, `${colorSet.aura}00`);
16881
+ context.beginPath();
16882
+ context.ellipse(centerX, centerY, haloRadiusX, haloRadiusY, profile.highlightAngle * 0.12 + haloIndex * 0.2 + Math.sin(timeMs / 3800) * 0.04, 0, Math.PI * 2);
16883
+ context.fillStyle = haloGradient;
16884
+ context.fill();
16885
+ }
16886
+ context.restore();
16887
+ }
16888
+ /**
16889
+ * Draws the main orb body using the smooth silhouette path.
16890
+ *
16891
+ * @param context Canvas 2D context.
16892
+ * @param points Smooth orb outline points.
16893
+ * @param centerX Orb center X coordinate.
16894
+ * @param centerY Orb center Y coordinate.
16895
+ * @param radius Orb base radius.
16896
+ * @param colorSet Derived orb color set.
16897
+ * @param profile Deterministic orb profile.
16898
+ * @param size Canvas size in CSS pixels.
16899
+ *
16900
+ * @private helper of `orbAvatarVisual`
16901
+ */
16902
+ function drawOrbBody(context, points, centerX, centerY, radius, colorSet, profile, size) {
16903
+ const bodyGradient = context.createRadialGradient(centerX - radius * 0.22, centerY - radius * 0.24, radius * 0.05, centerX + radius * 0.02, centerY + radius * 0.1, radius * (1.14 + profile.haloBlurRatio * 0.3));
16904
+ bodyGradient.addColorStop(0, `${colorSet.highlight}f8`);
16905
+ bodyGradient.addColorStop(0.22, `${colorSet.core}f2`);
16906
+ bodyGradient.addColorStop(0.58, `${colorSet.mid}e8`);
16907
+ bodyGradient.addColorStop(0.86, `${colorSet.outer}dc`);
16908
+ bodyGradient.addColorStop(1, `${colorSet.rim}ea`);
16909
+ context.save();
16910
+ traceSmoothClosedPath(context, points);
16911
+ context.shadowColor = `${colorSet.rim}aa`;
16912
+ context.shadowBlur = size * (0.06 + profile.haloBlurRatio * 0.15);
16913
+ context.fillStyle = bodyGradient;
16914
+ context.fill();
16915
+ context.restore();
16916
+ context.save();
16917
+ traceSmoothClosedPath(context, points);
16918
+ context.strokeStyle = `${colorSet.highlight}${profile.family === 'pearl' ? '72' : '58'}`;
16919
+ context.lineWidth = Math.max(1.2, size * 0.012);
16920
+ context.lineJoin = 'round';
16921
+ context.lineCap = 'round';
16922
+ context.stroke();
16923
+ context.restore();
16924
+ }
16925
+ /**
16926
+ * Draws the layered energy rings and soft internal gradients inside the orb.
16927
+ *
16928
+ * @param context Canvas 2D context.
16929
+ * @param centerX Orb center X coordinate.
16930
+ * @param centerY Orb center Y coordinate.
16931
+ * @param radius Orb base radius.
16932
+ * @param colorSet Derived orb color set.
16933
+ * @param profile Deterministic orb profile.
16934
+ * @param size Canvas size in CSS pixels.
16935
+ * @param timeMs Current animation time in milliseconds.
16936
+ *
16937
+ * @private helper of `orbAvatarVisual`
16938
+ */
16939
+ function drawOrbInteriorRings(context, centerX, centerY, radius, colorSet, profile, size, timeMs) {
16940
+ const internalGradient = context.createRadialGradient(centerX - radius * 0.08, centerY - radius * 0.1, radius * 0.06, centerX, centerY, radius * (0.95 + profile.sheenStrength * 0.15));
16941
+ internalGradient.addColorStop(0, `${colorSet.highlight}70`);
16942
+ internalGradient.addColorStop(0.48, `${colorSet.core}22`);
16943
+ internalGradient.addColorStop(1, `${colorSet.rim}00`);
16944
+ context.fillStyle = internalGradient;
16945
+ context.fillRect(centerX - radius, centerY - radius, radius * 2, radius * 2);
16946
+ for (let ringIndex = 0; ringIndex < profile.ringCount; ringIndex++) {
16947
+ const ringProgress = (ringIndex + 1) / (profile.ringCount + 1);
16948
+ const ringRadiusX = radius * (0.34 + ringProgress * 0.44);
16949
+ const ringRadiusY = radius * (0.28 + ringProgress * 0.38);
16950
+ const ringRotation = profile.highlightAngle * 0.24 +
16951
+ ringIndex * 0.4 +
16952
+ Math.sin(timeMs / (1800 + ringIndex * 180) + profile.bandRotation) * 0.08;
16953
+ context.beginPath();
16954
+ context.ellipse(centerX + Math.cos(profile.highlightAngle) * radius * 0.03, centerY + Math.sin(profile.highlightAngle) * radius * 0.02, ringRadiusX, ringRadiusY, ringRotation, 0, Math.PI * 2);
16955
+ context.strokeStyle = ringIndex % 2 === 0 ? `${colorSet.highlight}32` : `${colorSet.aura}24`;
16956
+ context.lineWidth = Math.max(1.2, size * (0.007 - ringIndex * 0.001));
16957
+ context.shadowColor = `${colorSet.highlight}55`;
16958
+ context.shadowBlur = size * 0.02;
16959
+ context.stroke();
16960
+ }
16961
+ }
16962
+ /**
16963
+ * Draws soft sparkles and seeded dust inside the orb.
16964
+ *
16965
+ * @param context Canvas 2D context.
16966
+ * @param centerX Orb center X coordinate.
16967
+ * @param centerY Orb center Y coordinate.
16968
+ * @param radius Orb base radius.
16969
+ * @param colorSet Derived orb color set.
16970
+ * @param profile Deterministic orb profile.
16971
+ * @param size Canvas size in CSS pixels.
16972
+ * @param createRandom Seeded random factory.
16973
+ * @param timeMs Current animation time in milliseconds.
16974
+ *
16975
+ * @private helper of `orbAvatarVisual`
16976
+ */
16977
+ function drawOrbSparkles(context, centerX, centerY, radius, colorSet, profile, size, createRandom, timeMs) {
16978
+ const sparkleStride = Math.max(1, Math.floor(profile.sparkleCount / 6));
16979
+ for (let sparkleIndex = 0; sparkleIndex < profile.sparkleCount; sparkleIndex++) {
16980
+ const sparkleRandom = createRandom(`orb-sparkle-${sparkleIndex}`);
16981
+ const sparkleAngle = sparkleRandom() * Math.PI * 2 + profile.highlightAngle * 0.4;
16982
+ const sparkleOrbitRadius = radius * (0.42 + sparkleRandom() * 0.55);
16983
+ const sparkleOrbitPulse = 0.92 + Math.sin(timeMs / (700 + sparkleIndex * 70) + profile.bandRotation) * 0.08;
16984
+ const sparkleCenterX = centerX + Math.cos(sparkleAngle + timeMs / (4600 / profile.pulseSpeed)) * sparkleOrbitRadius;
16985
+ const sparkleCenterY = centerY + Math.sin(sparkleAngle + timeMs / (4600 / profile.pulseSpeed)) * sparkleOrbitRadius;
16986
+ const sparkleRadius = size * (0.0028 + sparkleRandom() * 0.0058) * sparkleOrbitPulse;
16987
+ context.beginPath();
16988
+ context.arc(sparkleCenterX, sparkleCenterY, sparkleRadius * 2, 0, Math.PI * 2);
16989
+ context.fillStyle = `${colorSet.highlight}14`;
16990
+ context.fill();
16991
+ context.beginPath();
16992
+ context.arc(sparkleCenterX, sparkleCenterY, sparkleRadius, 0, Math.PI * 2);
16993
+ context.fillStyle = sparkleIndex % sparkleStride === 0 ? `${colorSet.highlight}d2` : `${colorSet.aura}c8`;
16994
+ context.shadowColor = `${colorSet.highlight}66`;
16995
+ context.shadowBlur = size * 0.012;
16996
+ context.fill();
16997
+ }
16998
+ }
16999
+ /**
17000
+ * Draws the specular sweep that makes the orb feel glossy and dimensional.
17001
+ *
17002
+ * @param context Canvas 2D context.
17003
+ * @param centerX Orb center X coordinate.
17004
+ * @param centerY Orb center Y coordinate.
17005
+ * @param radius Orb base radius.
17006
+ * @param colorSet Derived orb color set.
17007
+ * @param profile Deterministic orb profile.
17008
+ * @param size Canvas size in CSS pixels.
17009
+ * @param timeMs Current animation time in milliseconds.
17010
+ *
17011
+ * @private helper of `orbAvatarVisual`
17012
+ */
17013
+ function drawOrbSheen(context, centerX, centerY, radius, colorSet, profile, size, timeMs) {
17014
+ const sheenAngle = profile.bandRotation + timeMs / (2400 / profile.pulseSpeed);
17015
+ const sheenDirectionX = Math.cos(sheenAngle);
17016
+ const sheenDirectionY = Math.sin(sheenAngle);
17017
+ const sheenGradient = context.createLinearGradient(centerX - sheenDirectionX * radius, centerY - sheenDirectionY * radius, centerX + sheenDirectionX * radius, centerY + sheenDirectionY * radius);
17018
+ sheenGradient.addColorStop(0, `${colorSet.highlight}00`);
17019
+ sheenGradient.addColorStop(0.24, `${colorSet.highlight}0e`);
17020
+ sheenGradient.addColorStop(0.43, `${colorSet.highlight}${profile.sheenStrength > 0.65 ? '66' : '48'}`);
17021
+ sheenGradient.addColorStop(0.57, `${colorSet.core}${profile.sheenStrength > 0.65 ? '42' : '2e'}`);
17022
+ sheenGradient.addColorStop(0.75, `${colorSet.aura}1e`);
17023
+ sheenGradient.addColorStop(1, `${colorSet.highlight}00`);
17024
+ context.fillStyle = sheenGradient;
17025
+ context.fillRect(centerX - radius * 1.6, centerY - radius * 1.6, radius * 3.2, radius * 3.2);
17026
+ context.beginPath();
17027
+ context.ellipse(centerX - radius * 0.18, centerY - radius * 0.2, radius * 0.44, radius * 0.24, profile.highlightAngle * 0.32, 0, Math.PI * 2);
17028
+ context.fillStyle = `${colorSet.highlight}${profile.family === 'ember' ? '26' : '2e'}`;
17029
+ context.shadowColor = `${colorSet.highlight}55`;
17030
+ context.shadowBlur = size * 0.02;
17031
+ context.fill();
17032
+ }
17033
+ /**
17034
+ * Draws the bright nucleus that gives the orb a recognisable center.
17035
+ *
17036
+ * @param context Canvas 2D context.
17037
+ * @param centerX Orb center X coordinate.
17038
+ * @param centerY Orb center Y coordinate.
17039
+ * @param radius Orb base radius.
17040
+ * @param colorSet Derived orb color set.
17041
+ * @param profile Deterministic orb profile.
17042
+ * @param size Canvas size in CSS pixels.
17043
+ * @param timeMs Current animation time in milliseconds.
17044
+ *
17045
+ * @private helper of `orbAvatarVisual`
17046
+ */
17047
+ function drawOrbCore(context, centerX, centerY, radius, colorSet, profile, size, timeMs) {
17048
+ const coreRadiusX = radius * (0.18 + profile.sheenStrength * 0.06);
17049
+ const coreRadiusY = radius * (0.15 + profile.sheenStrength * 0.045);
17050
+ const corePulse = 1 + Math.sin(timeMs / (1100 / profile.pulseSpeed) + profile.bandRotation) * 0.04;
17051
+ context.beginPath();
17052
+ context.ellipse(centerX + profile.coreShiftX * radius * 0.18, centerY + profile.coreShiftY * radius * 0.18, coreRadiusX * corePulse, coreRadiusY * corePulse, profile.highlightAngle * 0.12, 0, Math.PI * 2);
17053
+ context.fillStyle = `${colorSet.core}f0`;
17054
+ context.shadowColor = `${colorSet.highlight}88`;
17055
+ context.shadowBlur = size * 0.03;
17056
+ context.fill();
17057
+ context.beginPath();
17058
+ context.arc(centerX - radius * 0.06, centerY - radius * 0.07, radius * 0.06, 0, Math.PI * 2);
17059
+ context.fillStyle = `${colorSet.highlight}d6`;
17060
+ context.fill();
17061
+ }
17062
+ /**
17063
+ * Draws the final rim stroke so the orb stays crisp against busy backgrounds.
17064
+ *
17065
+ * @param context Canvas 2D context.
17066
+ * @param points Smooth orb outline points.
17067
+ * @param colorSet Derived orb color set.
17068
+ * @param size Canvas size in CSS pixels.
17069
+ *
17070
+ * @private helper of `orbAvatarVisual`
17071
+ */
17072
+ function drawOrbRim(context, points, colorSet, size) {
17073
+ context.save();
17074
+ traceSmoothClosedPath(context, points);
17075
+ context.strokeStyle = `${colorSet.rim}c0`;
17076
+ context.lineWidth = Math.max(1.25, size * 0.013);
17077
+ context.lineJoin = 'round';
17078
+ context.lineCap = 'round';
17079
+ context.shadowColor = `${colorSet.highlight}4f`;
17080
+ context.shadowBlur = size * 0.018;
17081
+ context.stroke();
17082
+ context.restore();
17083
+ }
17084
+ /**
17085
+ * Clamps a number into the provided range.
17086
+ *
17087
+ * @param value Number to clamp.
17088
+ * @param minimum Minimum accepted value.
17089
+ * @param maximum Maximum accepted value.
17090
+ * @returns Clamped number.
17091
+ *
17092
+ * @private helper of `orbAvatarVisual`
17093
+ */
17094
+ function clampNumber(value, minimum, maximum) {
17095
+ return Math.min(maximum, Math.max(minimum, value));
17096
+ }
17097
+ /**
17098
+ * Clamps a number into the provided integer range.
17099
+ *
17100
+ * @param value Number to clamp.
17101
+ * @param minimum Minimum accepted value.
17102
+ * @param maximum Maximum accepted value.
17103
+ * @returns Clamped integer.
17104
+ *
17105
+ * @private helper of `orbAvatarVisual`
17106
+ */
17107
+ function clampInteger(value, minimum, maximum) {
17108
+ return Math.min(maximum, Math.max(minimum, Math.round(value)));
17109
+ }
17110
+
17111
+ /* eslint-disable no-magic-numbers */
17112
+ /**
17113
+ * Pixel-art avatar visual.
17114
+ *
17115
+ * @private built-in avatar visual
17116
+ */
17117
+ const pixelArtAvatarVisual = {
17118
+ id: 'pixel-art',
17119
+ title: 'Pixel art',
17120
+ description: 'Symmetric retro badge with a deterministic face and palette-driven pixels.',
17121
+ isAnimated: false,
17122
+ render({ context, size, palette, createRandom, avatarDefinition }) {
17123
+ const random = createRandom('pixel-art');
17124
+ const panelSize = size * 0.62;
17125
+ const panelX = (size - panelSize) / 2;
17126
+ const panelY = size * 0.19;
17127
+ const pixelGridSize = 10;
17128
+ const pixelSize = panelSize / pixelGridSize;
17129
+ const halfColumns = Math.ceil(pixelGridSize / 2);
17130
+ const colorStops = [palette.primary, palette.secondary, palette.accent, palette.highlight];
17131
+ const foreheadRowCount = 2 + Math.floor(random() * 2);
17132
+ const cheekInset = 1 + Math.floor(random() * 2);
17133
+ const emblemHeight = 2 + Math.floor(random() * 2);
17134
+ drawAvatarFrame(context, size, palette);
17135
+ const glow = context.createRadialGradient(size * 0.5, size * 0.32, size * 0.05, size * 0.5, size * 0.32, size * 0.5);
17136
+ glow.addColorStop(0, `${palette.highlight}aa`);
17137
+ glow.addColorStop(1, `${palette.highlight}00`);
17138
+ context.fillStyle = glow;
17139
+ context.fillRect(0, 0, size, size);
17140
+ context.save();
17141
+ createRoundedRectPath(context, panelX, panelY, panelSize, panelSize, panelSize * 0.16);
17142
+ context.fillStyle = palette.shadow;
17143
+ context.shadowColor = `${palette.shadow}66`;
17144
+ context.shadowBlur = size * 0.08;
17145
+ context.fill();
17146
+ context.restore();
17147
+ for (let rowIndex = 0; rowIndex < pixelGridSize; rowIndex++) {
17148
+ for (let columnIndex = 0; columnIndex < halfColumns; columnIndex++) {
17149
+ const mirroredColumnIndex = pixelGridSize - columnIndex - 1;
17150
+ const normalizedRowDistance = Math.abs(rowIndex - pixelGridSize / 2) / (pixelGridSize / 2);
17151
+ const normalizedColumnDistance = columnIndex / halfColumns;
17152
+ const fillChance = 0.85 - normalizedRowDistance * 0.32 - normalizedColumnDistance * 0.08;
17153
+ if (random() > fillChance) {
17154
+ continue;
17155
+ }
17156
+ const color = colorStops[Math.min(colorStops.length - 1, Math.floor(random() *
17157
+ (rowIndex < foreheadRowCount
17158
+ ? 2
17159
+ : rowIndex > pixelGridSize - emblemHeight - 1
17160
+ ? colorStops.length
17161
+ : 3)))];
17162
+ if (columnIndex === 0 &&
17163
+ rowIndex >= cheekInset &&
17164
+ rowIndex <= pixelGridSize - cheekInset - 1 &&
17165
+ random() < 0.4) {
17166
+ continue;
17167
+ }
17168
+ drawPixel(context, panelX + columnIndex * pixelSize, panelY + rowIndex * pixelSize, pixelSize, color);
17169
+ if (mirroredColumnIndex !== columnIndex) {
17170
+ drawPixel(context, panelX + mirroredColumnIndex * pixelSize, panelY + rowIndex * pixelSize, pixelSize, color);
17171
+ }
17172
+ }
17173
+ }
17174
+ const eyeRowIndex = 3 + Math.floor(random() * 2);
17175
+ const eyeColumnOffset = 2 + Math.floor(random() * 2);
17176
+ drawPixel(context, panelX + eyeColumnOffset * pixelSize, panelY + eyeRowIndex * pixelSize, pixelSize, palette.ink);
17177
+ drawPixel(context, panelX + (pixelGridSize - eyeColumnOffset - 1) * pixelSize, panelY + eyeRowIndex * pixelSize, pixelSize, palette.ink);
17178
+ drawPixel(context, panelX + eyeColumnOffset * pixelSize, panelY + eyeRowIndex * pixelSize, pixelSize * 0.44, '#ffffff');
17179
+ drawPixel(context, panelX + (pixelGridSize - eyeColumnOffset - 1) * pixelSize, panelY + eyeRowIndex * pixelSize, pixelSize * 0.44, '#ffffff');
17180
+ const mouthRowIndex = eyeRowIndex + 3;
17181
+ const mouthWidth = 2 + Math.floor(random() * 2);
17182
+ for (let mouthOffset = 0; mouthOffset < mouthWidth; mouthOffset++) {
17183
+ drawPixel(context, panelX + (4 + mouthOffset) * pixelSize, panelY + mouthRowIndex * pixelSize, pixelSize, palette.shadow);
17184
+ }
17185
+ context.save();
17186
+ context.fillStyle = `${palette.highlight}44`;
17187
+ context.font = `${Math.round(size * 0.12)}px sans-serif`;
17188
+ context.textAlign = 'center';
17189
+ context.fillText(avatarDefinition.agentName.charAt(0).toUpperCase(), size * 0.5, size * 0.88);
17190
+ context.restore();
17191
+ },
17192
+ };
17193
+ /**
17194
+ * Draws one pixel-art block with a tiny inner highlight.
17195
+ *
17196
+ * @param context Canvas 2D context.
17197
+ * @param x Left coordinate.
17198
+ * @param y Top coordinate.
17199
+ * @param size Pixel size.
17200
+ * @param color Pixel fill color.
17201
+ *
17202
+ * @private helper of `pixelArtAvatarVisual`
17203
+ */
17204
+ function drawPixel(context, x, y, size, color) {
17205
+ const normalizedSize = size * 0.9;
17206
+ const offset = (size - normalizedSize) / 2;
17207
+ context.fillStyle = color;
17208
+ context.fillRect(x + offset, y + offset, normalizedSize, normalizedSize);
17209
+ context.fillStyle = 'rgba(255,255,255,0.18)';
17210
+ context.fillRect(x + offset, y + offset, normalizedSize, normalizedSize * 0.14);
17211
+ }
17212
+
17213
+ // Note: [💞] Ignore a discrepancy between file name and entity name
17214
+ /**
17215
+ * Built-in avatar visuals available to the app.
17216
+ *
17217
+ * @private shared registry for the avatar rendering system
17218
+ */
17219
+ const AVATAR_VISUALS = [
17220
+ pixelArtAvatarVisual,
17221
+ octopusAvatarVisual,
17222
+ octopus2AvatarVisual,
17223
+ octopus3AvatarVisual,
17224
+ asciiOctopusAvatarVisual,
17225
+ minecraftAvatarVisual,
17226
+ fractalAvatarVisual,
17227
+ orbAvatarVisual,
17228
+ ];
17229
+ /**
17230
+ * Normalizes user-facing avatar visual names so ids can be matched case-insensitively
17231
+ * across spaces, hyphens, underscores, and future separator variants.
17232
+ *
17233
+ * @param value Raw avatar visual id or title.
17234
+ * @returns Stable lookup key.
17235
+ *
17236
+ * @private shared registry for the avatar rendering system
17237
+ */
17238
+ function normalizeAvatarVisualLookupKey(value) {
17239
+ return value
17240
+ .trim()
17241
+ .toLowerCase()
17242
+ .replace(/[^a-z0-9]+/g, '');
17243
+ }
17244
+ /**
17245
+ * Resolves a user-facing avatar visual value to a supported built-in visual id.
17246
+ *
17247
+ * The lookup is derived from `AVATAR_VISUALS`, so new visuals become selectable by
17248
+ * adding them to the registry rather than updating parser-specific option lists.
17249
+ *
17250
+ * @param value Raw visual id/title, for example `PIXEL_ART`, `pixel art`, or `pixel-art`.
17251
+ * @returns Matching visual id or `null` when the value is empty/unknown.
17252
+ *
17253
+ * @private shared registry for the avatar rendering system
17254
+ */
17255
+ function resolveAvatarVisualId(value) {
17256
+ if (!value) {
17257
+ return null;
17258
+ }
17259
+ const normalizedValue = normalizeAvatarVisualLookupKey(value);
17260
+ if (!normalizedValue) {
17261
+ return null;
17262
+ }
17263
+ const avatarVisual = AVATAR_VISUALS.find((candidateAvatarVisual) => normalizeAvatarVisualLookupKey(candidateAvatarVisual.id) === normalizedValue ||
17264
+ normalizeAvatarVisualLookupKey(candidateAvatarVisual.title) === normalizedValue);
17265
+ return (avatarVisual === null || avatarVisual === void 0 ? void 0 : avatarVisual.id) || null;
17266
+ }
17267
+
17268
+ /**
17269
+ * META AVATAR commitment definition
17270
+ *
17271
+ * The `META AVATAR` commitment sets the built-in default avatar visual used when
17272
+ * the agent does not provide an explicit `META IMAGE`.
17273
+ *
17274
+ * @private [đŸĒ”] Maybe export the commitments through some package
17275
+ */
17276
+ class MetaAvatarCommitmentDefinition extends BaseCommitmentDefinition {
17277
+ constructor() {
17278
+ super('META AVATAR');
17279
+ }
17280
+ /**
17281
+ * Short one-line description of META AVATAR.
17282
+ */
17283
+ get description() {
17284
+ return "Set the agent's built-in avatar visual.";
17285
+ }
17286
+ /**
17287
+ * Icon for this commitment.
17288
+ */
17289
+ get icon() {
17290
+ return '👤';
17291
+ }
17292
+ /**
17293
+ * Markdown documentation for META AVATAR commitment.
17294
+ */
17295
+ get documentation() {
17296
+ const supportedVisuals = AVATAR_VISUALS.map((avatarVisual) => `\`${avatarVisual.id}\``).join(', ');
17297
+ return spaceTrim$1(`
17298
+ # META AVATAR
17299
+
17300
+ Sets the built-in avatar visual used for the agent when no explicit \`META IMAGE\` is provided.
17301
+
17302
+ ## Key aspects
17303
+
17304
+ - Does not modify the agent's behavior or responses.
17305
+ - Only one \`META AVATAR\` should be used per agent.
17306
+ - If multiple are specified, the last one takes precedence.
17307
+ - Values are matched case-insensitively and spaces, hyphens, and underscores are normalized.
17308
+ - Supported visuals are derived from the shared avatar registry: ${supportedVisuals}.
17309
+
17310
+ ## Examples
17311
+
17312
+ \`\`\`book
17313
+ Pixel Assistant
17314
+
17315
+ META AVATAR PIXEL_ART
17316
+ GOAL Help users with concise answers.
17317
+ \`\`\`
17318
+
17319
+ \`\`\`book
17320
+ Orb Assistant
17321
+
17322
+ META AVATAR orb
17323
+ GOAL Answer in a calm and focused way.
17324
+ \`\`\`
17325
+ `);
17326
+ }
17327
+ applyToAgentModelRequirements(requirements, content) {
17328
+ // META AVATAR doesn't modify the system message or model requirements.
17329
+ // It's handled separately in the parsing logic for profile avatar resolution.
17330
+ return requirements;
17331
+ }
17332
+ }
17333
+ // Note: [💞] Ignore a discrepancy between file name and entity name
17334
+
14029
17335
  /**
14030
17336
  * META COLOR commitment definition
14031
17337
  *
@@ -14695,7 +18001,13 @@ class ModelCommitmentDefinition extends BaseCommitmentDefinition {
14695
18001
  * Short one-line description of MODEL.
14696
18002
  */
14697
18003
  get description() {
14698
- return 'Enforce AI model requirements including name and technical parameters.';
18004
+ return 'Low-level commitment for explicit model selection and technical parameters. Use carefully.';
18005
+ }
18006
+ /**
18007
+ * Marks MODEL as a low-level commitment surfaced with caution.
18008
+ */
18009
+ get isLowLevel() {
18010
+ return true;
14699
18011
  }
14700
18012
  /**
14701
18013
  * Icon for this commitment.
@@ -14710,11 +18022,17 @@ class ModelCommitmentDefinition extends BaseCommitmentDefinition {
14710
18022
  return spaceTrim$1(`
14711
18023
  # ${this.type}
14712
18024
 
14713
- Enforces technical parameters for the AI model, ensuring consistent behavior across different execution environments.
18025
+ Low-level commitment for explicit AI model selection and technical parameters.
18026
+
18027
+ ## Status
18028
+
18029
+ - This commitment is low-level and not used by most of the users.
18030
+ - Use it when you need to pin a specific model or fine-tune model parameters directly.
18031
+ - Prefer automatic model selection when you do not need manual control.
14714
18032
 
14715
18033
  ## Key aspects
14716
18034
 
14717
- - When no \`MODEL\` commitment is specified, the best model requirement is picked automatically based on the agent \`PERSONA\`, \`KNOWLEDGE\`, \`TOOLS\` and other commitments
18035
+ - When no \`MODEL\` commitment is specified, the best model requirement is picked automatically based on the agent \`PERSONA\`, \`KNOWLEDGE\`, \`TOOLS\` and other commitments.
14718
18036
  - Multiple \`MODEL\` commitments can be used to specify different parameters
14719
18037
  - Both \`MODEL\` and \`MODELS\` terms work identically and can be used interchangeably
14720
18038
  - Parameters control the randomness, creativity, and technical aspects of model responses
@@ -15243,6 +18561,12 @@ class RuleCommitmentDefinition extends BaseCommitmentDefinition {
15243
18561
  get description() {
15244
18562
  return 'Add behavioral rules the agent must follow.';
15245
18563
  }
18564
+ /**
18565
+ * Marks RULE as one of the priority commitments surfaced first in catalogues.
18566
+ */
18567
+ get isImportant() {
18568
+ return true;
18569
+ }
15246
18570
  /**
15247
18571
  * Icon for this commitment.
15248
18572
  */
@@ -15862,6 +19186,46 @@ function createTeammateLabel(url) {
15862
19186
  }
15863
19187
  // Note: [💞] Ignore a discrepancy between file name and entity name
15864
19188
 
19189
+ /**
19190
+ * Header used for same-server TEAM calls that may access private teammate agents.
19191
+ *
19192
+ * @private internal Agents Server access wiring
19193
+ */
19194
+ const TEAM_INTERNAL_AGENT_ACCESS_HEADER = 'x-promptbook-team-agent-access-token';
19195
+ /**
19196
+ * Creates request headers for same-server TEAM calls.
19197
+ *
19198
+ * @param options - Target agent URL, local server URL, and resolved access token.
19199
+ * @returns Header map when the target is same-origin; otherwise an empty map.
19200
+ *
19201
+ * @private internal Agents Server access wiring
19202
+ */
19203
+ function createTeamInternalAgentAccessHeaders(options) {
19204
+ if (!options.accessToken || !isSameOriginAgentUrl(options.agentUrl, options.localServerUrl)) {
19205
+ return {};
19206
+ }
19207
+ return {
19208
+ [TEAM_INTERNAL_AGENT_ACCESS_HEADER]: options.accessToken,
19209
+ };
19210
+ }
19211
+ /**
19212
+ * Checks whether a teammate URL points back to the current Agents Server origin.
19213
+ *
19214
+ * @private internal Agents Server access wiring
19215
+ */
19216
+ function isSameOriginAgentUrl(agentUrl, localServerUrl) {
19217
+ if (!localServerUrl) {
19218
+ return false;
19219
+ }
19220
+ try {
19221
+ return new URL(agentUrl).origin === new URL(localServerUrl).origin;
19222
+ }
19223
+ catch (_a) {
19224
+ return false;
19225
+ }
19226
+ }
19227
+ // Note: [💞] Ignore a discrepancy between file name and entity name
19228
+
15865
19229
  /**
15866
19230
  * Map of team tool functions.
15867
19231
  */
@@ -15907,11 +19271,17 @@ class TeamCommitmentDefinition extends BaseCommitmentDefinition {
15907
19271
  get description() {
15908
19272
  return 'Enable the agent to consult teammate agents via dedicated tools.';
15909
19273
  }
19274
+ /**
19275
+ * Marks TEAM as one of the priority commitments surfaced first in catalogues.
19276
+ */
19277
+ get isImportant() {
19278
+ return true;
19279
+ }
15910
19280
  /**
15911
19281
  * Icon for this commitment.
15912
19282
  */
15913
19283
  get icon() {
15914
- return '??';
19284
+ return 'đŸ‘Ĩ';
15915
19285
  }
15916
19286
  /**
15917
19287
  * Markdown documentation for TEAM commitment.
@@ -16213,21 +19583,28 @@ function createPseudoVoidTeamToolResult(entry, request) {
16213
19583
  /**
16214
19584
  * Resolves a RemoteAgent for the given teammate URL, caching the connection.
16215
19585
  */
16216
- async function getRemoteTeammateAgent(agentUrl) {
16217
- const cached = remoteAgentsByUrl.get(agentUrl);
19586
+ async function getRemoteTeammateAgent(agentUrl, runtimeContext) {
19587
+ var _a, _b;
19588
+ const requestHeaders = createTeamInternalAgentAccessHeaders({
19589
+ agentUrl,
19590
+ localServerUrl: (_a = runtimeContext.agentsServer) === null || _a === void 0 ? void 0 : _a.localServerUrl,
19591
+ accessToken: (_b = runtimeContext.agentsServer) === null || _b === void 0 ? void 0 : _b.teamInternalAccessToken,
19592
+ });
19593
+ const cacheKey = `${agentUrl}|${requestHeaders[TEAM_INTERNAL_AGENT_ACCESS_HEADER] || ''}`;
19594
+ const cached = remoteAgentsByUrl.get(cacheKey);
16218
19595
  if (cached) {
16219
19596
  return cached;
16220
19597
  }
16221
19598
  const connection = (async () => {
16222
19599
  const { RemoteAgent } = await Promise.resolve().then(function () { return RemoteAgent$1; });
16223
- return RemoteAgent.connect({ agentUrl });
19600
+ return RemoteAgent.connect({ agentUrl, requestHeaders });
16224
19601
  })();
16225
- remoteAgentsByUrl.set(agentUrl, connection);
19602
+ remoteAgentsByUrl.set(cacheKey, connection);
16226
19603
  try {
16227
19604
  return await connection;
16228
19605
  }
16229
19606
  catch (error) {
16230
- remoteAgentsByUrl.delete(agentUrl);
19607
+ remoteAgentsByUrl.delete(cacheKey);
16231
19608
  throw error;
16232
19609
  }
16233
19610
  }
@@ -16257,8 +19634,9 @@ function createTeamToolFunction(entry) {
16257
19634
  let error = null;
16258
19635
  let toolCalls;
16259
19636
  try {
16260
- const remoteAgent = await getRemoteTeammateAgent(entry.teammate.url);
16261
- const prompt = buildTeammatePrompt(request, createTeamConversationRuntimeContext(args[TOOL_RUNTIME_CONTEXT_ARGUMENT]));
19637
+ const runtimeContext = createTeamConversationRuntimeContext(args[TOOL_RUNTIME_CONTEXT_ARGUMENT]);
19638
+ const remoteAgent = await getRemoteTeammateAgent(entry.teammate.url, runtimeContext);
19639
+ const prompt = buildTeammatePrompt(request, runtimeContext);
16262
19640
  const teammateResult = await remoteAgent.callChatModel(prompt);
16263
19641
  response = teammateResult.content || '';
16264
19642
  toolCalls =
@@ -16297,11 +19675,10 @@ function createTeamToolFunction(entry) {
16297
19675
  /**
16298
19676
  * TEMPLATE commitment definition
16299
19677
  *
16300
- * The TEMPLATE commitment enforces a specific response structure or template
16301
- * that the agent must follow when generating responses. This helps ensure
16302
- * consistent message formatting across all agent interactions.
19678
+ * Deprecated legacy commitment for response templates and output structure.
19679
+ * New books should prefer `WRITING SAMPLE` and `WRITING RULES`.
16303
19680
  *
16304
- * Example usage in agent source:
19681
+ * Legacy example usage in agent source:
16305
19682
  *
16306
19683
  * ```book
16307
19684
  * TEMPLATE Always structure your response with: 1) Summary, 2) Details, 3) Next steps
@@ -16321,7 +19698,16 @@ class TemplateCommitmentDefinition extends BaseCommitmentDefinition {
16321
19698
  * Short one-line description of TEMPLATE.
16322
19699
  */
16323
19700
  get description() {
16324
- return 'Enforce a specific message structure or response template.';
19701
+ return 'Deprecated legacy template commitment. Prefer `WRITING SAMPLE` and `WRITING RULES` for new books.';
19702
+ }
19703
+ /**
19704
+ * Optional UI/docs-only deprecation metadata.
19705
+ */
19706
+ get deprecation() {
19707
+ return {
19708
+ message: 'Use `WRITING SAMPLE` and `WRITING RULES` instead.',
19709
+ replacedBy: ['WRITING SAMPLE', 'WRITING RULES'],
19710
+ };
16325
19711
  }
16326
19712
  /**
16327
19713
  * Icon for this commitment.
@@ -16336,38 +19722,32 @@ class TemplateCommitmentDefinition extends BaseCommitmentDefinition {
16336
19722
  return spaceTrim$1(`
16337
19723
  # ${this.type}
16338
19724
 
16339
- Enforces a specific response structure or template that the agent must follow when generating responses.
19725
+ Deprecated legacy commitment for response structure and templates.
16340
19726
 
16341
- ## Key aspects
19727
+ ## Migration
16342
19728
 
16343
- - Both terms work identically and can be used interchangeably.
16344
- - Can be used with or without content.
16345
- - When used without content, enables template mode for structured responses.
16346
- - When used with content, defines the specific template structure to follow.
16347
- - Multiple templates can be combined, with later ones taking precedence.
19729
+ - Existing \`${this.type}\` and \`TEMPLATES\` books still parse and compile.
19730
+ - New books should use \`WRITING SAMPLE\` for concrete response exemplars and \`WRITING RULES\` for structure or formatting constraints.
19731
+ - Runtime behavior is intentionally unchanged for backward compatibility.
16348
19732
 
16349
- ## Examples
19733
+ ## Preferred replacement
16350
19734
 
16351
19735
  \`\`\`book
16352
19736
  Customer Support Agent
16353
19737
 
16354
- PERSONA You are a helpful customer support representative
16355
- TEMPLATE Always structure your response with: 1) Acknowledgment, 2) Solution, 3) Follow-up question
16356
- WRITING RULES Be professional and empathetic
19738
+ GOAL Help the user with support questions.
19739
+ WRITING SAMPLE
19740
+ Thanks for reaching out. Here is the summary, details, and next step.
19741
+ WRITING RULES Keep the response structured as: summary, details, next step.
16357
19742
  \`\`\`
16358
19743
 
16359
- \`\`\`book
16360
- Technical Documentation Assistant
16361
-
16362
- PERSONA You are a technical writing expert
16363
- TEMPLATE Use the following format: **Topic:** [topic] | **Explanation:** [details] | **Example:** [code]
16364
- FORMAT Use markdown with clear headings
16365
- \`\`\`
19744
+ ## Legacy compatibility example
16366
19745
 
16367
19746
  \`\`\`book
16368
- Simple Agent
19747
+ Customer Support Agent
16369
19748
 
16370
- PERSONA You are a virtual assistant
19749
+ GOAL Help the user with support questions.
19750
+ TEMPLATE Always structure your response with: 1) Acknowledgment, 2) Solution, 3) Follow-up question
16371
19751
  TEMPLATE
16372
19752
  \`\`\`
16373
19753
  `);
@@ -16415,120 +19795,6 @@ class TemplateCommitmentDefinition extends BaseCommitmentDefinition {
16415
19795
  }
16416
19796
  // Note: [💞] Ignore a discrepancy between file name and entity name
16417
19797
 
16418
- /**
16419
- * USE commitment definition
16420
- *
16421
- * The USE commitment indicates that the agent should utilize specific tools or capabilities
16422
- * to access and interact with external systems when necessary.
16423
- *
16424
- * Supported USE types:
16425
- * - USE BROWSER: Enables the agent to use a web browser tool
16426
- * - USE SEARCH ENGINE (future): Enables search engine access
16427
- * - USE DEEPSEARCH: Enables deeper research-oriented search access
16428
- * - USE FILE SYSTEM (future): Enables file system operations
16429
- * - USE MCP (future): Enables MCP server connections
16430
- *
16431
- * The content following the USE commitment is ignored (similar to NOTE).
16432
- *
16433
- * Example usage in agent source:
16434
- *
16435
- * ```book
16436
- * USE BROWSER
16437
- * USE SEARCH ENGINE
16438
- * ```
16439
- *
16440
- * @private [đŸĒ”] Maybe export the commitments through some package
16441
- */
16442
- class UseCommitmentDefinition extends BaseCommitmentDefinition {
16443
- constructor() {
16444
- super('USE');
16445
- }
16446
- /**
16447
- * Short one-line description of USE commitments.
16448
- */
16449
- get description() {
16450
- return 'Enable the agent to use specific tools or capabilities (BROWSER, SEARCH ENGINE, DEEPSEARCH, etc.).';
16451
- }
16452
- /**
16453
- * Icon for this commitment.
16454
- */
16455
- get icon() {
16456
- return '🔧';
16457
- }
16458
- /**
16459
- * Markdown documentation for USE commitment.
16460
- */
16461
- get documentation() {
16462
- return spaceTrim$1(`
16463
- # USE
16464
-
16465
- Enables the agent to use specific tools or capabilities for interacting with external systems.
16466
-
16467
- ## Supported USE types
16468
-
16469
- - **USE BROWSER** - Enables the agent to use a web browser tool to access and retrieve information from the internet
16470
- - **USE SEARCH ENGINE** (future) - Enables search engine access
16471
- - **USE DEEPSEARCH** - Enables deeper research-oriented search access
16472
- - **USE FILE SYSTEM** (future) - Enables file system operations
16473
- - **USE MCP** (future) - Enables MCP server connections
16474
-
16475
- ## Key aspects
16476
-
16477
- - The content following the USE commitment is ignored (similar to NOTE)
16478
- - Multiple USE commitments can be specified to enable multiple capabilities
16479
- - The actual tool usage is handled by the agent runtime
16480
-
16481
- ## Examples
16482
-
16483
- ### Basic browser usage
16484
-
16485
- \`\`\`book
16486
- Research Assistant
16487
-
16488
- PERSONA You are a helpful research assistant
16489
- USE BROWSER
16490
- KNOWLEDGE Can search the web for up-to-date information
16491
- \`\`\`
16492
-
16493
- ### Multiple tools
16494
-
16495
- \`\`\`book
16496
- Data Analyst
16497
-
16498
- PERSONA You are a data analyst assistant
16499
- USE BROWSER
16500
- USE FILE SYSTEM
16501
- ACTION Can analyze data from various sources
16502
- \`\`\`
16503
- `);
16504
- }
16505
- applyToAgentModelRequirements(requirements, content) {
16506
- // USE commitments don't modify the system message or model requirements directly
16507
- // They are handled separately in the parsing logic for capability extraction
16508
- // This method exists for consistency with the CommitmentDefinition interface
16509
- return requirements;
16510
- }
16511
- /**
16512
- * Extracts the tool type from the USE commitment
16513
- * This is used by the parsing logic
16514
- */
16515
- extractToolType(content) {
16516
- var _a, _b;
16517
- const trimmedContent = content.trim();
16518
- // The tool type is the first word after USE (already stripped)
16519
- const match = trimmedContent.match(/^(\w+)/);
16520
- return (_b = (_a = match === null || match === void 0 ? void 0 : match[1]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) !== null && _b !== void 0 ? _b : null;
16521
- }
16522
- /**
16523
- * Checks if this is a known USE type
16524
- */
16525
- isKnownUseType(useType) {
16526
- const knownTypes = ['BROWSER', 'SEARCH ENGINE', 'DEEPSEARCH', 'FILE SYSTEM', 'MCP'];
16527
- return knownTypes.includes(useType.toUpperCase());
16528
- }
16529
- }
16530
- // Note: [💞] Ignore a discrepancy between file name and entity name
16531
-
16532
19798
  /**
16533
19799
  * All `USE` commitment types currently participating in final system-message aggregation.
16534
19800
  *
@@ -22495,10 +25761,10 @@ const COMMITMENT_REGISTRY = [
22495
25761
  new MemoryCommitmentDefinition('MEMORIES'),
22496
25762
  new StyleCommitmentDefinition('STYLE'),
22497
25763
  new StyleCommitmentDefinition('STYLES'),
22498
- new RuleCommitmentDefinition('RULES'),
22499
25764
  new RuleCommitmentDefinition('RULE'),
22500
- new LanguageCommitmentDefinition('LANGUAGES'),
25765
+ new RuleCommitmentDefinition('RULES'),
22501
25766
  new LanguageCommitmentDefinition('LANGUAGE'),
25767
+ new LanguageCommitmentDefinition('LANGUAGES'),
22502
25768
  new WritingSampleCommitmentDefinition(),
22503
25769
  new WritingRulesCommitmentDefinition(),
22504
25770
  new SampleCommitmentDefinition('SAMPLE'),
@@ -22515,6 +25781,7 @@ const COMMITMENT_REGISTRY = [
22515
25781
  new ActionCommitmentDefinition('ACTION'),
22516
25782
  new ActionCommitmentDefinition('ACTIONS'),
22517
25783
  new ComponentCommitmentDefinition(),
25784
+ new MetaAvatarCommitmentDefinition(),
22518
25785
  new MetaImageCommitmentDefinition(),
22519
25786
  new MetaColorCommitmentDefinition(),
22520
25787
  new MetaFontCommitmentDefinition(),
@@ -22562,7 +25829,6 @@ const COMMITMENT_REGISTRY = [
22562
25829
  new UseMcpCommitmentDefinition(),
22563
25830
  new UsePrivacyCommitmentDefinition(),
22564
25831
  new UseProjectCommitmentDefinition(),
22565
- new UseCommitmentDefinition(),
22566
25832
  // Not yet implemented commitments (using placeholder)
22567
25833
  new NotYetImplementedCommitmentDefinition('EXPECT'),
22568
25834
  new NotYetImplementedCommitmentDefinition('BEHAVIOUR'),
@@ -22575,6 +25841,92 @@ const COMMITMENT_REGISTRY = [
22575
25841
  // TODO: [🧠] Maybe create through standardized $register
22576
25842
  // Note: [💞] Ignore a discrepancy between file name and entity name
22577
25843
 
25844
+ /**
25845
+ * Priority order for the important commitments shown first in catalogues and intellisense.
25846
+ *
25847
+ * Canonical singular names stay ahead of their plural aliases so the most important
25848
+ * commitments remain easy to scan.
25849
+ *
25850
+ * @private internal constant of commitment catalog sorting
25851
+ */
25852
+ const IMPORTANT_COMMITMENT_TYPE_SORT_ORDER = new Map([
25853
+ ['GOAL', 0],
25854
+ ['GOALS', 1],
25855
+ ['RULE', 2],
25856
+ ['RULES', 3],
25857
+ ['KNOWLEDGE', 4],
25858
+ ['TEAM', 5],
25859
+ ]);
25860
+ /**
25861
+ * Sort rank used when unfinished, low-level, and deprecated commitments should be grouped last.
25862
+ *
25863
+ * @private internal constant of commitment catalog sorting
25864
+ */
25865
+ const COMMITMENT_STATUS_SORT_ORDER = {
25866
+ normal: 0,
25867
+ deprecated: 1,
25868
+ unfinished: 2,
25869
+ lowLevel: 3,
25870
+ };
25871
+ /**
25872
+ * Resolves the relative sort rank of one commitment status.
25873
+ *
25874
+ * @param definition - Commitment definition to rank.
25875
+ * @param options - Sorting options.
25876
+ * @returns Relative sort rank for the definition.
25877
+ *
25878
+ * @private internal helper of commitment catalog sorting
25879
+ */
25880
+ function resolveCommitmentStatusSortRank(definition, options) {
25881
+ let statusSortRank = COMMITMENT_STATUS_SORT_ORDER.normal;
25882
+ if (options.isDeprecatedLast && definition.deprecation) {
25883
+ statusSortRank = Math.max(statusSortRank, COMMITMENT_STATUS_SORT_ORDER.deprecated);
25884
+ }
25885
+ if (options.isUnfinishedLast && definition.isUnfinished) {
25886
+ statusSortRank = Math.max(statusSortRank, COMMITMENT_STATUS_SORT_ORDER.unfinished);
25887
+ }
25888
+ if (options.isLowLevelLast && definition.isLowLevel) {
25889
+ statusSortRank = Math.max(statusSortRank, COMMITMENT_STATUS_SORT_ORDER.lowLevel);
25890
+ }
25891
+ return statusSortRank;
25892
+ }
25893
+ /**
25894
+ * Sorts commitment definitions so the important ones stay at the top.
25895
+ *
25896
+ * @param commitmentDefinitions - Definitions to sort.
25897
+ * @param options - Sorting options.
25898
+ * @returns Sorted commitment definitions.
25899
+ *
25900
+ * @private internal helper of commitment catalog sorting
25901
+ */
25902
+ function sortCommitmentDefinitions(commitmentDefinitions, options = {}) {
25903
+ return [...commitmentDefinitions]
25904
+ .map((definition, index) => ({
25905
+ definition,
25906
+ index,
25907
+ }))
25908
+ .sort((left, right) => {
25909
+ var _a, _b;
25910
+ if (left.definition.isImportant !== right.definition.isImportant) {
25911
+ return left.definition.isImportant ? -1 : 1;
25912
+ }
25913
+ if (left.definition.isImportant && right.definition.isImportant) {
25914
+ const leftPriority = (_a = IMPORTANT_COMMITMENT_TYPE_SORT_ORDER.get(left.definition.type)) !== null && _a !== void 0 ? _a : Number.MAX_SAFE_INTEGER;
25915
+ const rightPriority = (_b = IMPORTANT_COMMITMENT_TYPE_SORT_ORDER.get(right.definition.type)) !== null && _b !== void 0 ? _b : Number.MAX_SAFE_INTEGER;
25916
+ if (leftPriority !== rightPriority) {
25917
+ return leftPriority - rightPriority;
25918
+ }
25919
+ }
25920
+ const leftStatusSortRank = resolveCommitmentStatusSortRank(left.definition, options);
25921
+ const rightStatusSortRank = resolveCommitmentStatusSortRank(right.definition, options);
25922
+ if (leftStatusSortRank !== rightStatusSortRank) {
25923
+ return leftStatusSortRank - rightStatusSortRank;
25924
+ }
25925
+ return left.index - right.index;
25926
+ })
25927
+ .map(({ definition }) => definition);
25928
+ }
25929
+
22578
25930
  /**
22579
25931
  * Gets all available commitment definitions
22580
25932
  *
@@ -22583,7 +25935,7 @@ const COMMITMENT_REGISTRY = [
22583
25935
  * @public exported from `@promptbook/core`
22584
25936
  */
22585
25937
  function getAllCommitmentDefinitions() {
22586
- return $deepFreeze([...COMMITMENT_REGISTRY]);
25938
+ return $deepFreeze(sortCommitmentDefinitions(COMMITMENT_REGISTRY, { isUnfinishedLast: true, isLowLevelLast: true }));
22587
25939
  }
22588
25940
 
22589
25941
  /**
@@ -24923,7 +28275,7 @@ function deduplicatePreparationToolCalls(toolCalls) {
24923
28275
  lastPreparationIndex = index;
24924
28276
  }
24925
28277
  else {
24926
- // Remove earlier duplicate — keep only the last (most recent) one.
28278
+ // Remove earlier duplicate - keep only the last (most recent) one.
24927
28279
  toolCalls.splice(index, 1);
24928
28280
  }
24929
28281
  }
@@ -25336,6 +28688,7 @@ const SIMPLE_CAPABILITY_BY_COMMITMENT_TYPE = {
25336
28688
  * @private internal utility of `parseAgentSource`
25337
28689
  */
25338
28690
  const META_COMMITMENT_APPLIERS = {
28691
+ 'META AVATAR': applyMetaAvatarContent,
25339
28692
  'META LINK': applyMetaLinkContent,
25340
28693
  'META DOMAIN': applyMetaDomainContent,
25341
28694
  'META IMAGE': applyMetaImageContent,
@@ -25699,9 +29052,26 @@ function applyGenericMetaCommitment(state, content) {
25699
29052
  if (metaTypeRaw === 'LINK') {
25700
29053
  state.links.push(metaValue);
25701
29054
  }
29055
+ if (metaTypeRaw.toUpperCase() === 'AVATAR') {
29056
+ applyMetaAvatarContent(state, metaValue);
29057
+ return;
29058
+ }
25702
29059
  const metaType = normalizeTo_camelCase(metaTypeRaw);
25703
29060
  state.meta[metaType] = metaValue;
25704
29061
  }
29062
+ /**
29063
+ * Applies META AVATAR content into the canonical `meta.avatar` field.
29064
+ *
29065
+ * @private internal utility of `parseAgentSource`
29066
+ */
29067
+ function applyMetaAvatarContent(state, content) {
29068
+ const avatarVisualId = resolveAvatarVisualId(content);
29069
+ if (avatarVisualId) {
29070
+ state.meta.avatar = avatarVisualId;
29071
+ return;
29072
+ }
29073
+ delete state.meta.avatar;
29074
+ }
25705
29075
  /**
25706
29076
  * Applies META LINK content into links and the canonical `meta.link` field.
25707
29077
  *
@@ -27286,7 +30656,7 @@ function humanizeAiTextEllipsis(aiText) {
27286
30656
  // Note: [🏂] This function is not tested by itself but together with other cleanup functions with `humanizeAiText`
27287
30657
 
27288
30658
  /**
27289
- * Change dash-like characters to regular dashes `—` -> `-` and remove soft hyphens
30659
+ * Change dash-like characters to regular dashes `-` -> `-` and remove soft hyphens
27290
30660
  *
27291
30661
  * Note: [🔂] This function is idempotent.
27292
30662
  * Tip: If you want to do the full cleanup, look for `humanizeAiText` exported `@promptbook/markdown-utils`
@@ -27294,7 +30664,7 @@ function humanizeAiTextEllipsis(aiText) {
27294
30664
  * @public exported from `@promptbook/markdown-utils`
27295
30665
  */
27296
30666
  function humanizeAiTextEmdashed(aiText) {
27297
- return aiText.replace(/\u00AD/g, '').replace(/[â€â€‘â€’â€“â€”â€•âˆ’âƒīšŖīŧ]/g, '-');
30667
+ return aiText.replace(/\u00AD/g, '').replace(/[‐‑‒–-â€•âˆ’âƒīšŖīŧ]/g, '-');
27298
30668
  }
27299
30669
  // Note: [🏂] This function is not tested by itself but together with other cleanup functions with `humanizeAiText`
27300
30670
 
@@ -34007,6 +37377,7 @@ function buildRemoteAgentSource(profile, meta) {
34007
37377
  const isMetaImageExplicit = profile.isMetaImageExplicit !== false;
34008
37378
  const metaLines = [
34009
37379
  formatMetaLine('FULLNAME', meta === null || meta === void 0 ? void 0 : meta.fullname),
37380
+ formatMetaLine('AVATAR', meta === null || meta === void 0 ? void 0 : meta.avatar),
34010
37381
  formatMetaLine('IMAGE', isMetaImageExplicit ? meta === null || meta === void 0 ? void 0 : meta.image : undefined),
34011
37382
  formatMetaLine('DESCRIPTION', meta === null || meta === void 0 ? void 0 : meta.description),
34012
37383
  formatMetaLine('COLOR', meta === null || meta === void 0 ? void 0 : meta.color),
@@ -34047,7 +37418,7 @@ class RemoteAgent extends Agent {
34047
37418
  var _a, _b, _c;
34048
37419
  const agentProfileUrl = `${options.agentUrl}/api/profile`;
34049
37420
  const profileResponse = await fetch(agentProfileUrl, {
34050
- headers: attachClientVersionHeader(),
37421
+ headers: attachClientVersionHeader(options.requestHeaders),
34051
37422
  });
34052
37423
  // <- TODO: [🐱‍🚀] What about closed-source agents?
34053
37424
  // <- TODO: [🐱‍🚀] Maybe use promptbookFetch
@@ -34127,6 +37498,7 @@ class RemoteAgent extends Agent {
34127
37498
  this.avatarVisualId = undefined;
34128
37499
  this.knowledgeSources = [];
34129
37500
  this.agentUrl = options.agentUrl;
37501
+ this.requestHeaders = options.requestHeaders || {};
34130
37502
  }
34131
37503
  get agentName() {
34132
37504
  return this._remoteAgentName || super.agentName;
@@ -34163,7 +37535,7 @@ class RemoteAgent extends Agent {
34163
37535
  }
34164
37536
  const response = await fetch(`${this.agentUrl}/api/voice`, {
34165
37537
  method: 'POST',
34166
- headers: attachClientVersionHeader(),
37538
+ headers: attachClientVersionHeader(this.requestHeaders),
34167
37539
  body: formData,
34168
37540
  });
34169
37541
  if (!response.ok) {
@@ -34194,6 +37566,7 @@ class RemoteAgent extends Agent {
34194
37566
  const bookResponse = await fetch(`${this.agentUrl}/api/chat`, {
34195
37567
  method: 'POST',
34196
37568
  headers: attachClientVersionHeader({
37569
+ ...this.requestHeaders,
34197
37570
  'Content-Type': 'application/json',
34198
37571
  }),
34199
37572
  body: JSON.stringify({