@promptbook/browser 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 +3653 -280
  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 +3653 -280
  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
@@ -29,7 +29,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
29
29
  * @generated
30
30
  * @see https://github.com/webgptorg/promptbook
31
31
  */
32
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-54';
32
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-56';
33
33
  /**
34
34
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
35
35
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -1335,6 +1335,24 @@ class BaseCommitmentDefinition {
1335
1335
  get requiresContent() {
1336
1336
  return true;
1337
1337
  }
1338
+ /**
1339
+ * Whether this commitment should be prioritized in menus, documentation, and intellisense.
1340
+ */
1341
+ get isImportant() {
1342
+ return false;
1343
+ }
1344
+ /**
1345
+ * Whether this commitment is unfinished and not ready to use.
1346
+ */
1347
+ get isUnfinished() {
1348
+ return false;
1349
+ }
1350
+ /**
1351
+ * Whether this commitment is low-level and should be surfaced with caution.
1352
+ */
1353
+ get isLowLevel() {
1354
+ return false;
1355
+ }
1338
1356
  /**
1339
1357
  * Optional UI/docs-only deprecation metadata.
1340
1358
  */
@@ -1451,8 +1469,8 @@ class BaseCommitmentDefinition {
1451
1469
  /**
1452
1470
  * ACTION commitment definition
1453
1471
  *
1454
- * The ACTION commitment defines specific actions or capabilities that the agent can perform.
1455
- * This helps define what the agent is capable of doing and how it should approach tasks.
1472
+ * Deprecated legacy commitment for broad capability notes.
1473
+ * New books should prefer the appropriate `USE*` commitment instead.
1456
1474
  *
1457
1475
  * Example usage in agent source:
1458
1476
  *
@@ -1471,7 +1489,15 @@ class ActionCommitmentDefinition extends BaseCommitmentDefinition {
1471
1489
  * Short one-line description of ACTION.
1472
1490
  */
1473
1491
  get description() {
1474
- return 'Define agent capabilities and actions it can perform.';
1492
+ return 'Deprecated legacy capability commitment. Prefer concrete `USE*` commitments.';
1493
+ }
1494
+ /**
1495
+ * Optional UI/docs-only deprecation metadata.
1496
+ */
1497
+ get deprecation() {
1498
+ return {
1499
+ message: 'Use a concrete `USE*` commitment instead.',
1500
+ };
1475
1501
  }
1476
1502
  /**
1477
1503
  * Icon for this commitment.
@@ -1486,33 +1512,43 @@ class ActionCommitmentDefinition extends BaseCommitmentDefinition {
1486
1512
  return spaceTrim$1(`
1487
1513
  # ${this.type}
1488
1514
 
1489
- Defines specific actions or capabilities that the agent can perform.
1515
+ Deprecated legacy commitment for broad capability notes.
1490
1516
 
1491
- ## Key aspects
1517
+ ## Migration
1492
1518
 
1493
- - Both terms work identically and can be used interchangeably.
1494
- - Each action adds to the agent's capability list.
1495
- - Actions help users understand what the agent can do.
1519
+ - Existing \`${this.type}\` and \`ACTIONS\` books still parse and compile.
1520
+ - New books should prefer the appropriate \`USE*\` commitment instead.
1521
+ - Keep \`${this.type}\` only when maintaining older books that already rely on it.
1496
1522
 
1497
- ## Examples
1523
+ ## Preferred replacement
1498
1524
 
1499
1525
  \`\`\`book
1500
- Code Assistant
1526
+ Research Assistant
1501
1527
 
1502
- PERSONA You are a programming assistant
1503
- ACTION Can generate code snippets and explain programming concepts
1504
- ACTION Able to debug existing code and suggest improvements
1505
- ACTION Can create unit tests for functions
1528
+ PERSONA You are a helpful research assistant
1529
+ USE SEARCH ENGINE
1530
+ RULE Always cite your sources when providing information from the web
1531
+ \`\`\`
1532
+
1533
+ ## Legacy compatibility example
1534
+
1535
+ \`\`\`book
1536
+ Research Assistant
1537
+
1538
+ PERSONA You are a helpful research assistant
1539
+ ACTION Can search for current information and summarize findings
1540
+ RULE Always cite your sources when providing information from the web
1506
1541
  \`\`\`
1507
1542
 
1543
+ ## Legacy compatibility example with additional tools
1544
+
1508
1545
  \`\`\`book
1509
- Data Scientist
1546
+ Code Assistant
1510
1547
 
1511
- PERSONA You are a data analysis expert
1512
- ACTION Able to analyze data and provide insights
1513
- ACTION Can create visualizations and charts
1514
- ACTION Capable of statistical analysis and modeling
1515
- KNOWLEDGE Data analysis best practices and statistical methods
1548
+ PERSONA You are a programming assistant
1549
+ USE BROWSER
1550
+ USE SEARCH ENGINE
1551
+ RULE Prefer the narrowest useful capability for the task.
1516
1552
  \`\`\`
1517
1553
  `);
1518
1554
  }
@@ -1521,7 +1557,7 @@ class ActionCommitmentDefinition extends BaseCommitmentDefinition {
1521
1557
  if (!trimmedContent) {
1522
1558
  return requirements;
1523
1559
  }
1524
- // Add action capability to the system message
1560
+ // Keep the legacy capability note for backward compatibility.
1525
1561
  const actionSection = `Capability: ${trimmedContent}`;
1526
1562
  return this.appendToSystemMessage(requirements, actionSection, '\n\n');
1527
1563
  }
@@ -1674,9 +1710,9 @@ class ComponentCommitmentDefinition extends BaseCommitmentDefinition {
1674
1710
  /**
1675
1711
  * DELETE commitment definition
1676
1712
  *
1677
- * The DELETE commitment (and its aliases CANCEL, DISCARD, REMOVE) is used to
1678
- * remove or disregard certain information or context. This can be useful for
1679
- * overriding previous commitments or removing unwanted behaviors.
1713
+ * The DELETE commitment (and its aliases CANCEL, DISCARD, REMOVE) is a low-level
1714
+ * unfinished commitment used to remove or disregard certain information or context.
1715
+ * It is intentionally surfaced with caution because it is not ready for broad use yet.
1680
1716
  *
1681
1717
  * Example usage in agent source:
1682
1718
  *
@@ -1697,7 +1733,13 @@ class DeleteCommitmentDefinition extends BaseCommitmentDefinition {
1697
1733
  * Short one-line description of DELETE/CANCEL/DISCARD/REMOVE.
1698
1734
  */
1699
1735
  get description() {
1700
- return 'Remove or **disregard** certain information, context, or previous commitments.';
1736
+ return 'Unfinished low-level commitment for removing or disregarding information. Use carefully.';
1737
+ }
1738
+ /**
1739
+ * Marks DELETE as unfinished and not ready to use.
1740
+ */
1741
+ get isUnfinished() {
1742
+ return true;
1701
1743
  }
1702
1744
  /**
1703
1745
  * Icon for this commitment.
@@ -1712,7 +1754,13 @@ class DeleteCommitmentDefinition extends BaseCommitmentDefinition {
1712
1754
  return spaceTrim$1(`
1713
1755
  # DELETE (CANCEL, DISCARD, REMOVE)
1714
1756
 
1715
- A commitment to remove or disregard certain information or context. This can be useful for overriding previous commitments or removing unwanted behaviors.
1757
+ 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.
1758
+
1759
+ ## Status
1760
+
1761
+ - This commitment is unfinished and not ready to use yet.
1762
+ - Treat it as a low-level prompt-surgery tool rather than a general-purpose commitment.
1763
+ - Prefer higher-level commitments when a clearer dedicated commitment exists.
1716
1764
 
1717
1765
  ## Aliases
1718
1766
 
@@ -1727,6 +1775,7 @@ class DeleteCommitmentDefinition extends BaseCommitmentDefinition {
1727
1775
  - Useful for overriding previous commitments in the same agent definition.
1728
1776
  - Can be used to remove inherited behaviors from base personas.
1729
1777
  - Helps fine-tune agent behavior by explicitly removing unwanted elements.
1778
+ - Because this commitment is unfinished, keep an eye on future changes before relying on it in production books.
1730
1779
 
1731
1780
  ## Use cases
1732
1781
 
@@ -1734,6 +1783,7 @@ class DeleteCommitmentDefinition extends BaseCommitmentDefinition {
1734
1783
  - Removing conflicting or outdated instructions
1735
1784
  - Disabling specific response patterns
1736
1785
  - Canceling previous formatting or style requirements
1786
+ - Experimenting with low-level prompt rewrites when you know exactly what needs to be removed
1737
1787
 
1738
1788
  ## Examples
1739
1789
 
@@ -1900,11 +1950,10 @@ class DictionaryCommitmentDefinition extends BaseCommitmentDefinition {
1900
1950
  /**
1901
1951
  * FORMAT commitment definition
1902
1952
  *
1903
- * The FORMAT commitment defines the specific output structure and formatting
1904
- * that the agent should use in its responses. This includes data formats,
1905
- * response templates, and structural requirements.
1953
+ * Deprecated legacy commitment for output formatting and response structure.
1954
+ * New books should prefer `WRITING SAMPLE` and `WRITING RULES`.
1906
1955
  *
1907
- * Example usage in agent source:
1956
+ * Legacy example usage in agent source:
1908
1957
  *
1909
1958
  * ```book
1910
1959
  * FORMAT Always respond in JSON format with 'status' and 'data' fields
@@ -1921,7 +1970,16 @@ class FormatCommitmentDefinition extends BaseCommitmentDefinition {
1921
1970
  * Short one-line description of FORMAT.
1922
1971
  */
1923
1972
  get description() {
1924
- return 'Specify output structure or formatting requirements.';
1973
+ return 'Deprecated legacy formatting commitment. Prefer `WRITING SAMPLE` and `WRITING RULES` for new books.';
1974
+ }
1975
+ /**
1976
+ * Optional UI/docs-only deprecation metadata.
1977
+ */
1978
+ get deprecation() {
1979
+ return {
1980
+ message: 'Use `WRITING SAMPLE` and `WRITING RULES` instead.',
1981
+ replacedBy: ['WRITING SAMPLE', 'WRITING RULES'],
1982
+ };
1925
1983
  }
1926
1984
  /**
1927
1985
  * Icon for this commitment.
@@ -1936,31 +1994,39 @@ class FormatCommitmentDefinition extends BaseCommitmentDefinition {
1936
1994
  return spaceTrim$1(`
1937
1995
  # ${this.type}
1938
1996
 
1939
- Defines the specific output structure and formatting for responses (data formats, templates, structure).
1997
+ Deprecated legacy commitment for output formatting and response structure.
1940
1998
 
1941
- ## Key aspects
1999
+ ## Migration
1942
2000
 
1943
- - Both terms work identically and can be used interchangeably.
1944
- - If they are in conflict, the last one takes precedence.
1945
- - You can specify both data formats and presentation styles.
2001
+ - Existing \`${this.type}\` and \`FORMATS\` books still parse and compile.
2002
+ - New books should use \`WRITING RULES\` for formatting or structure constraints and \`WRITING SAMPLE\` when a concrete example communicates the target shape better.
2003
+ - Runtime behavior is intentionally unchanged for backward compatibility.
1946
2004
 
1947
- ## Examples
2005
+ ## Preferred replacement
1948
2006
 
1949
2007
  \`\`\`book
1950
- Customer Support Bot
2008
+ Data Analyst
1951
2009
 
1952
- PERSONA You are a helpful customer support agent
1953
- FORMAT Always respond in JSON format with 'status' and 'data' fields
1954
- FORMAT Use markdown formatting for all code blocks
2010
+ GOAL Present results in a clean, readable structure.
2011
+ WRITING RULES Use markdown headings for sections and bullet points for lists.
2012
+ WRITING RULES Keep tables narrow and readable.
2013
+ WRITING SAMPLE
2014
+ Summary
2015
+ - ...
2016
+ Details
2017
+ - ...
2018
+ Next steps
2019
+ - ...
1955
2020
  \`\`\`
1956
2021
 
2022
+ ## Legacy compatibility example
2023
+
1957
2024
  \`\`\`book
1958
2025
  Data Analyst
1959
2026
 
1960
- PERSONA You are a data analysis expert
2027
+ GOAL Present results in a clean structure.
1961
2028
  FORMAT Present results in structured tables
1962
2029
  FORMAT Include confidence scores for all predictions
1963
- WRITING RULES Be concise and precise in explanations
1964
2030
  \`\`\`
1965
2031
  `);
1966
2032
  }
@@ -6483,6 +6549,12 @@ class GoalCommitmentDefinition extends BaseCommitmentDefinition {
6483
6549
  get description() {
6484
6550
  return 'Define the effective agent **goal**; when multiple goals exist, only the last one stays effective.';
6485
6551
  }
6552
+ /**
6553
+ * Marks GOAL as one of the priority commitments surfaced first in catalogues.
6554
+ */
6555
+ get isImportant() {
6556
+ return true;
6557
+ }
6486
6558
  /**
6487
6559
  * Icon for this commitment.
6488
6560
  */
@@ -6894,6 +6966,12 @@ class KnowledgeCommitmentDefinition extends BaseCommitmentDefinition {
6894
6966
  get description() {
6895
6967
  return 'Add domain **knowledge** via direct text or external sources (RAG).';
6896
6968
  }
6969
+ /**
6970
+ * Marks KNOWLEDGE as one of the priority commitments surfaced first in catalogues.
6971
+ */
6972
+ get isImportant() {
6973
+ return true;
6974
+ }
6897
6975
  /**
6898
6976
  * Icon for this commitment.
6899
6977
  */
@@ -8181,6 +8259,7 @@ class MessageSuffixCommitmentDefinition extends BaseCommitmentDefinition {
8181
8259
  * META commitment definition
8182
8260
  *
8183
8261
  * The META commitment handles all meta-information about the agent such as:
8262
+ * - META AVATAR: Sets the agent's built-in default avatar visual
8184
8263
  * - META IMAGE: Sets the agent's avatar/profile image URL
8185
8264
  * - META LINK: Provides profile/source links for the person the agent models
8186
8265
  * - META DOMAIN: Sets the canonical custom domain/host of the agent
@@ -8195,6 +8274,7 @@ class MessageSuffixCommitmentDefinition extends BaseCommitmentDefinition {
8195
8274
  * Example usage in agent source:
8196
8275
  *
8197
8276
  * ```book
8277
+ * META AVATAR pixel-art
8198
8278
  * META IMAGE https://example.com/avatar.jpg
8199
8279
  * META LINK https://twitter.com/username
8200
8280
  * META DOMAIN my-agent.com
@@ -8215,112 +8295,3338 @@ class MetaCommitmentDefinition extends BaseCommitmentDefinition {
8215
8295
  * Short one-line description of META commitments.
8216
8296
  */
8217
8297
  get description() {
8218
- return 'Set meta-information about the agent (IMAGE, LINK, TITLE, DESCRIPTION, etc.).';
8298
+ return 'Set meta-information about the agent (IMAGE, LINK, TITLE, DESCRIPTION, etc.).';
8299
+ }
8300
+ /**
8301
+ * Icon for this commitment.
8302
+ */
8303
+ get icon() {
8304
+ return 'â„šī¸';
8305
+ }
8306
+ /**
8307
+ * Markdown documentation for META commitment.
8308
+ */
8309
+ get documentation() {
8310
+ return spaceTrim$1(`
8311
+ # META
8312
+
8313
+ Sets meta-information about the agent that is used for display and attribution purposes.
8314
+
8315
+ ## Supported META types
8316
+
8317
+ - **META AVATAR** - Sets the agent's built-in default avatar visual
8318
+ - **META IMAGE** - Sets the agent's avatar/profile image URL
8319
+ - **META LINK** - Provides profile/source links for the person the agent models
8320
+ - **META DOMAIN** - Sets the canonical custom domain/host of the agent
8321
+ - **META TITLE** - Sets the agent's display title
8322
+ - **META DESCRIPTION** - Sets the agent's description
8323
+ - **META INPUT PLACEHOLDER** - Sets chat input placeholder text
8324
+ - **META [ANYTHING]** - Any other meta information in uppercase format
8325
+
8326
+ ## Key aspects
8327
+
8328
+ - Does not modify the agent's behavior or responses
8329
+ - Used for visual representation and attribution in user interfaces
8330
+ - Multiple META commitments of different types can be used
8331
+ - Multiple META LINK commitments can be used for different social profiles
8332
+ - If multiple META commitments of the same type are specified, the last one takes precedence (except for LINK)
8333
+
8334
+ ## Examples
8335
+
8336
+ ### Basic meta information
8337
+
8338
+ \`\`\`book
8339
+ Professional Assistant
8340
+
8341
+ META AVATAR octopus3
8342
+ META IMAGE https://example.com/professional-avatar.jpg
8343
+ META TITLE Senior Business Consultant
8344
+ META DESCRIPTION Specialized in strategic planning and project management
8345
+ META LINK https://linkedin.com/in/professional
8346
+ \`\`\`
8347
+
8348
+ ### Multiple links and custom meta
8349
+
8350
+ \`\`\`book
8351
+ Open Source Developer
8352
+
8353
+ META IMAGE /assets/dev-avatar.png
8354
+ META LINK https://github.com/developer
8355
+ META LINK https://twitter.com/devhandle
8356
+ META AUTHOR Jane Smith
8357
+ META VERSION 2.1
8358
+ META LICENSE MIT
8359
+ \`\`\`
8360
+
8361
+ ### Creative assistant
8362
+
8363
+ \`\`\`book
8364
+ Creative Helper
8365
+
8366
+ META IMAGE https://example.com/creative-bot.jpg
8367
+ META TITLE Creative Writing Assistant
8368
+ META DESCRIPTION Helps with brainstorming, storytelling, and creative projects
8369
+ META INSPIRATION Books, movies, and real-world experiences
8370
+ \`\`\`
8371
+ `);
8372
+ }
8373
+ applyToAgentModelRequirements(requirements, content) {
8374
+ // META commitments don't modify the system message or model requirements
8375
+ // They are handled separately in the parsing logic for meta information extraction
8376
+ // This method exists for consistency with the CommitmentDefinition interface
8377
+ return requirements;
8378
+ }
8379
+ /**
8380
+ * Extracts meta information from the content based on the meta type
8381
+ * This is used by the parsing logic
8382
+ */
8383
+ extractMetaValue(metaType, content) {
8384
+ const trimmedContent = content.trim();
8385
+ return trimmedContent || null;
8386
+ }
8387
+ /**
8388
+ * Validates if the provided content is a valid URL (for IMAGE and LINK types)
8389
+ */
8390
+ isValidUrl(content) {
8391
+ try {
8392
+ new URL(content.trim());
8393
+ return true;
8394
+ }
8395
+ catch (_a) {
8396
+ return false;
8397
+ }
8398
+ }
8399
+ /**
8400
+ * Checks if this is a known meta type
8401
+ */
8402
+ isKnownMetaType(metaType) {
8403
+ const knownTypes = ['AVATAR', 'IMAGE', 'LINK', 'TITLE', 'DESCRIPTION', 'AUTHOR', 'VERSION', 'LICENSE'];
8404
+ return knownTypes.includes(metaType.toUpperCase());
8405
+ }
8406
+ }
8407
+ // Note: [💞] Ignore a discrepancy between file name and entity name
8408
+
8409
+ /**
8410
+ * Makes color transformer which darker the given color
8411
+ *
8412
+ * @param amount from 0 to 1
8413
+ *
8414
+ * @public exported from `@promptbook/color`
8415
+ */
8416
+ function darken(amount) {
8417
+ return lighten(-amount);
8418
+ }
8419
+
8420
+ /* eslint-disable no-magic-numbers */
8421
+ /**
8422
+ * Corner radius ratio used for the common rounded card frame.
8423
+ *
8424
+ * @private utility of the avatar rendering system
8425
+ */
8426
+ const FRAME_RADIUS_RATIO = 0.18;
8427
+ /**
8428
+ * Draws the common rounded background frame used by most visuals.
8429
+ *
8430
+ * @param context Canvas 2D context.
8431
+ * @param size Canvas size in CSS pixels.
8432
+ * @param palette Derived avatar palette.
8433
+ *
8434
+ * @private utility of the avatar rendering system
8435
+ */
8436
+ function drawAvatarFrame(context, size, palette) {
8437
+ if (palette.background === 'transparent' && palette.backgroundSecondary === 'transparent') {
8438
+ return;
8439
+ }
8440
+ const gradient = context.createLinearGradient(0, 0, size, size);
8441
+ gradient.addColorStop(0, palette.background);
8442
+ gradient.addColorStop(1, palette.backgroundSecondary);
8443
+ context.save();
8444
+ createRoundedRectPath(context, 0, 0, size, size, size * FRAME_RADIUS_RATIO);
8445
+ context.fillStyle = gradient;
8446
+ context.fill();
8447
+ context.restore();
8448
+ context.save();
8449
+ context.strokeStyle = 'rgba(255,255,255,0.12)';
8450
+ context.lineWidth = Math.max(1.5, size * 0.012);
8451
+ createRoundedRectPath(context, size * 0.02, size * 0.02, size * 0.96, size * 0.96, size * 0.15);
8452
+ context.stroke();
8453
+ context.restore();
8454
+ }
8455
+ /**
8456
+ * Creates a rounded rectangle path on the current canvas context.
8457
+ *
8458
+ * @param context Canvas 2D context.
8459
+ * @param x Left coordinate.
8460
+ * @param y Top coordinate.
8461
+ * @param width Rectangle width.
8462
+ * @param height Rectangle height.
8463
+ * @param radius Corner radius.
8464
+ *
8465
+ * @private utility of the avatar rendering system
8466
+ */
8467
+ function createRoundedRectPath(context, x, y, width, height, radius) {
8468
+ const normalizedRadius = Math.min(radius, width / 2, height / 2);
8469
+ context.beginPath();
8470
+ context.moveTo(x + normalizedRadius, y);
8471
+ context.arcTo(x + width, y, x + width, y + height, normalizedRadius);
8472
+ context.arcTo(x + width, y + height, x, y + height, normalizedRadius);
8473
+ context.arcTo(x, y + height, x, y, normalizedRadius);
8474
+ context.arcTo(x, y, x + width, y, normalizedRadius);
8475
+ context.closePath();
8476
+ }
8477
+ /**
8478
+ * Picks one deterministic element from a non-empty collection.
8479
+ *
8480
+ * @param items Candidate items.
8481
+ * @param random Seeded random generator.
8482
+ * @returns Picked item.
8483
+ *
8484
+ * @private utility of the avatar rendering system
8485
+ */
8486
+ function pickRandomItem(items, random) {
8487
+ return items[Math.floor(random() * items.length)];
8488
+ }
8489
+
8490
+ /* eslint-disable no-magic-numbers */
8491
+ /**
8492
+ * Builds a smoothly morphing octopus-like silhouette from deterministic parameters.
8493
+ *
8494
+ * @param options Shape construction options.
8495
+ * @returns Closed-loop body points.
8496
+ *
8497
+ * @private shared geometry helper of `octopus2AvatarVisual` and `octopus3AvatarVisual`
8498
+ */
8499
+ function createOrganicOctopusBodyPoints(options) {
8500
+ const { centerX, centerY, bodyRadius, horizontalStretch, verticalStretch, mantleLift, lowerDrop, tentacleDepth, wobbleAmplitude, lobeCount, shapePhase, timeMs, pointCount = 36, } = options;
8501
+ return Array.from({ length: pointCount }, (_, pointIndex) => {
8502
+ const progress = pointIndex / pointCount;
8503
+ const angle = -Math.PI / 2 + progress * Math.PI * 2;
8504
+ const cosine = Math.cos(angle);
8505
+ const sine = Math.sin(angle);
8506
+ const upperFactor = Math.max(0, -sine);
8507
+ const lowerFactor = Math.max(0, sine);
8508
+ const lobeEnvelope = Math.pow(lowerFactor, 1.35);
8509
+ const tentacleWave = Math.max(0, Math.cos(angle * lobeCount + shapePhase + timeMs / 780)) * tentacleDepth * lobeEnvelope;
8510
+ const surfaceWave = Math.sin(angle * 3 + shapePhase + timeMs / 1200) * 0.62 +
8511
+ Math.sin(angle * 5 - shapePhase * 0.7 - timeMs / 910) * 0.38;
8512
+ const breathingWave = Math.sin(timeMs / 960 + shapePhase + angle * 0.45) * wobbleAmplitude;
8513
+ const radius = bodyRadius * (1 + upperFactor * 0.12 + lowerFactor * 0.08 + surfaceWave * 0.05) +
8514
+ tentacleWave +
8515
+ breathingWave;
8516
+ return {
8517
+ x: centerX +
8518
+ cosine * radius * horizontalStretch +
8519
+ Math.sin(angle * 2 + shapePhase) * lobeEnvelope * wobbleAmplitude * 0.7,
8520
+ y: centerY +
8521
+ sine * radius * verticalStretch -
8522
+ upperFactor * mantleLift +
8523
+ lowerFactor * lowerDrop +
8524
+ tentacleWave * 0.28,
8525
+ };
8526
+ });
8527
+ }
8528
+ /**
8529
+ * Traces a smooth closed path through the provided points.
8530
+ *
8531
+ * @param context Canvas 2D context.
8532
+ * @param points Closed-loop points.
8533
+ *
8534
+ * @private shared geometry helper of `octopus2AvatarVisual` and `octopus3AvatarVisual`
8535
+ */
8536
+ function traceSmoothClosedPath(context, points) {
8537
+ const lastPoint = points[points.length - 1];
8538
+ const firstPoint = points[0];
8539
+ const initialMidpoint = {
8540
+ x: (lastPoint.x + firstPoint.x) / 2,
8541
+ y: (lastPoint.y + firstPoint.y) / 2,
8542
+ };
8543
+ context.beginPath();
8544
+ context.moveTo(initialMidpoint.x, initialMidpoint.y);
8545
+ for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
8546
+ const point = points[pointIndex];
8547
+ const nextPoint = points[(pointIndex + 1) % points.length];
8548
+ const midpoint = {
8549
+ x: (point.x + nextPoint.x) / 2,
8550
+ y: (point.y + nextPoint.y) / 2,
8551
+ };
8552
+ context.quadraticCurveTo(point.x, point.y, midpoint.x, midpoint.y);
8553
+ }
8554
+ context.closePath();
8555
+ }
8556
+ /**
8557
+ * Creates deterministic ribbon tentacles for the organic octopus visuals.
8558
+ *
8559
+ * @param options Tentacle construction options.
8560
+ * @returns Tentacle descriptors.
8561
+ *
8562
+ * @private shared geometry helper of `octopus3AvatarVisual` and `asciiOctopusAvatarVisual`
8563
+ */
8564
+ function createOrganicOctopusTentacleShapes(options) {
8565
+ var _a, _b, _c, _d, _e, _f, _g, _h;
8566
+ const { size, centerX, centerY, bodyRadius, horizontalStretch, tentacleCount, shapePhase, createRandom, timeMs, saltPrefix, bodyPoints, variation, } = options;
8567
+ const baseY = centerY + bodyRadius * 0.74;
8568
+ const lowerBodyAnchorPoints = bodyPoints
8569
+ ? resolveTentacleBodyAnchorPoints(bodyPoints, centerY + bodyRadius * 0.04)
8570
+ : null;
8571
+ const flowLengthScale = (_a = variation === null || variation === void 0 ? void 0 : variation.flowLengthScale) !== null && _a !== void 0 ? _a : 1;
8572
+ const lateralReachScale = (_b = variation === null || variation === void 0 ? void 0 : variation.lateralReachScale) !== null && _b !== void 0 ? _b : 1;
8573
+ const tipReachScale = (_c = variation === null || variation === void 0 ? void 0 : variation.tipReachScale) !== null && _c !== void 0 ? _c : 1;
8574
+ const baseWidthScale = (_d = variation === null || variation === void 0 ? void 0 : variation.baseWidthScale) !== null && _d !== void 0 ? _d : 1;
8575
+ const tipWidthScale = (_e = variation === null || variation === void 0 ? void 0 : variation.tipWidthScale) !== null && _e !== void 0 ? _e : 1;
8576
+ const rootSpreadScale = (_f = variation === null || variation === void 0 ? void 0 : variation.rootSpreadScale) !== null && _f !== void 0 ? _f : 1;
8577
+ const startYOffsetScale = (_g = variation === null || variation === void 0 ? void 0 : variation.startYOffsetScale) !== null && _g !== void 0 ? _g : 1;
8578
+ const swayScale = (_h = variation === null || variation === void 0 ? void 0 : variation.swayScale) !== null && _h !== void 0 ? _h : 1;
8579
+ return Array.from({ length: tentacleCount }, (_, tentacleIndex) => {
8580
+ const tentacleRandom = createRandom(`${saltPrefix}-tentacle-${tentacleIndex}`);
8581
+ const spreadProgress = tentacleCount === 1 ? 0.5 : tentacleIndex / (tentacleCount - 1);
8582
+ const centeredProgress = spreadProgress - 0.5;
8583
+ const spreadCenteredProgress = centeredProgress * rootSpreadScale;
8584
+ const spreadAnchorProgress = Math.min(1, Math.max(0, 0.5 + spreadCenteredProgress));
8585
+ const temporalSway = Math.sin(timeMs / (720 + tentacleIndex * 34) + shapePhase + tentacleRandom() * Math.PI * 2) *
8586
+ size *
8587
+ (0.014 + tentacleRandom() * 0.015) *
8588
+ swayScale;
8589
+ const flowLength = size * (0.24 + tentacleRandom() * 0.18) * flowLengthScale;
8590
+ const curlDirection = spreadCenteredProgress === 0 ? (tentacleRandom() < 0.5 ? -1 : 1) : Math.sign(spreadCenteredProgress);
8591
+ const lateralReach = spreadCenteredProgress * size * (0.1 + tentacleRandom() * 0.1) * lateralReachScale + temporalSway;
8592
+ const tipReach = curlDirection * size * (0.025 + tentacleRandom() * 0.07) * tipReachScale;
8593
+ const startYOffset = (Math.abs(spreadCenteredProgress) * size * 0.012 + tentacleRandom() * size * 0.01) * startYOffsetScale;
8594
+ const startPoint = lowerBodyAnchorPoints && lowerBodyAnchorPoints.length >= 2
8595
+ ? createInsetTentacleStartPoint({
8596
+ bodyPoints: lowerBodyAnchorPoints,
8597
+ anchorProgress: spreadAnchorProgress,
8598
+ centerX,
8599
+ centerY,
8600
+ bodyRadius,
8601
+ centeredProgress: spreadCenteredProgress,
8602
+ startYOffset,
8603
+ })
8604
+ : {
8605
+ x: centerX + spreadCenteredProgress * bodyRadius * horizontalStretch * 1.52,
8606
+ y: baseY + startYOffset,
8607
+ };
8608
+ const controlPointOne = {
8609
+ x: startPoint.x + spreadCenteredProgress * size * 0.045 * lateralReachScale + temporalSway * 0.4,
8610
+ y: startPoint.y + flowLength * (0.21 + tentacleRandom() * 0.08),
8611
+ };
8612
+ const controlPointTwo = {
8613
+ x: startPoint.x + lateralReach + tipReach,
8614
+ y: startPoint.y + flowLength * (0.62 + tentacleRandom() * 0.12),
8615
+ };
8616
+ const endPoint = {
8617
+ x: startPoint.x + lateralReach + tipReach * 1.2,
8618
+ y: startPoint.y +
8619
+ flowLength * (0.9 + tentacleRandom() * 0.12) +
8620
+ Math.cos(timeMs / (840 + tentacleIndex * 41) + shapePhase) * size * (0.008 + tentacleRandom() * 0.01),
8621
+ };
8622
+ const baseWidth = size * (0.038 + tentacleRandom() * 0.02) * (1 - Math.abs(spreadCenteredProgress) * 0.18) * baseWidthScale;
8623
+ const tipWidth = baseWidth * Math.min(0.52, (0.18 + tentacleRandom() * 0.2) * tipWidthScale);
8624
+ return {
8625
+ startPoint,
8626
+ controlPointOne,
8627
+ controlPointTwo,
8628
+ endPoint,
8629
+ baseWidth,
8630
+ tipWidth,
8631
+ colorBias: tentacleRandom(),
8632
+ highlightBias: tentacleRandom(),
8633
+ sampleCount: 14 + Math.floor(tentacleRandom() * 6),
8634
+ };
8635
+ });
8636
+ }
8637
+ /**
8638
+ * Narrows the body contour to lower anchor points that can safely host tentacle roots.
8639
+ *
8640
+ * @param bodyPoints Generated closed-loop body points.
8641
+ * @param lowerBodyThresholdY Minimum Y coordinate considered part of the lower mantle.
8642
+ * @returns Body points sorted from left to right across the lower silhouette.
8643
+ *
8644
+ * @private shared geometry helper of `octopus3AvatarVisual`
8645
+ */
8646
+ function resolveTentacleBodyAnchorPoints(bodyPoints, lowerBodyThresholdY) {
8647
+ const lowerBodyPoints = bodyPoints
8648
+ .filter((bodyPoint) => bodyPoint.y >= lowerBodyThresholdY)
8649
+ .sort((leftPoint, rightPoint) => leftPoint.x - rightPoint.x);
8650
+ if (lowerBodyPoints.length >= 2) {
8651
+ return lowerBodyPoints;
8652
+ }
8653
+ return [...bodyPoints].sort((leftPoint, rightPoint) => leftPoint.x - rightPoint.x);
8654
+ }
8655
+ /**
8656
+ * Resolves one tentacle root from the provided lower body contour and nudges it inside the mantle.
8657
+ *
8658
+ * @param options Tentacle anchor options.
8659
+ * @returns Tentacle start point safely embedded inside the body silhouette.
8660
+ *
8661
+ * @private shared geometry helper of `octopus3AvatarVisual`
8662
+ */
8663
+ function createInsetTentacleStartPoint(options) {
8664
+ const { bodyPoints, anchorProgress, centerX, centerY, bodyRadius, centeredProgress, startYOffset } = options;
8665
+ const clampedAnchorProgress = Math.min(0.94, Math.max(0.06, anchorProgress));
8666
+ const bodyAnchorPoint = interpolatePointAlongTentacleAnchors(bodyPoints, clampedAnchorProgress);
8667
+ const inwardX = centerX - bodyAnchorPoint.x;
8668
+ const inwardY = centerY + bodyRadius * 0.08 - bodyAnchorPoint.y;
8669
+ const inwardLength = Math.hypot(inwardX, inwardY) || 1;
8670
+ const insetDistance = bodyRadius * (0.12 + Math.abs(centeredProgress) * 0.05) + startYOffset * 0.32;
8671
+ return {
8672
+ x: bodyAnchorPoint.x + (inwardX / inwardLength) * insetDistance,
8673
+ y: bodyAnchorPoint.y + (inwardY / inwardLength) * insetDistance,
8674
+ };
8675
+ }
8676
+ /**
8677
+ * Interpolates one left-to-right anchor point along the prepared lower body contour.
8678
+ *
8679
+ * @param bodyPoints Lower body contour points sorted from left to right.
8680
+ * @param progress Interpolation progress in the range `[0, 1]`.
8681
+ * @returns Interpolated anchor point.
8682
+ *
8683
+ * @private shared geometry helper of `octopus3AvatarVisual`
8684
+ */
8685
+ function interpolatePointAlongTentacleAnchors(bodyPoints, progress) {
8686
+ if (bodyPoints.length === 1) {
8687
+ return bodyPoints[0];
8688
+ }
8689
+ const anchorIndex = progress * (bodyPoints.length - 1);
8690
+ const startIndex = Math.floor(anchorIndex);
8691
+ const endIndex = Math.min(bodyPoints.length - 1, startIndex + 1);
8692
+ const blend = anchorIndex - startIndex;
8693
+ const startPoint = bodyPoints[startIndex];
8694
+ const endPoint = bodyPoints[endIndex];
8695
+ return {
8696
+ x: startPoint.x + (endPoint.x - startPoint.x) * blend,
8697
+ y: startPoint.y + (endPoint.y - startPoint.y) * blend,
8698
+ };
8699
+ }
8700
+ /**
8701
+ * Samples the cubic tentacle centerline and offsets normals to build a filled ribbon.
8702
+ *
8703
+ * @param tentacleShape Deterministic tentacle descriptor.
8704
+ * @returns Sampled ribbon points.
8705
+ *
8706
+ * @private shared geometry helper of `octopus3AvatarVisual` and `asciiOctopusAvatarVisual`
8707
+ */
8708
+ function sampleOrganicTentacleRibbonPoints(tentacleShape) {
8709
+ return Array.from({ length: tentacleShape.sampleCount + 1 }, (_, sampleIndex) => {
8710
+ const progress = sampleIndex / tentacleShape.sampleCount;
8711
+ const point = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, progress);
8712
+ const previousPoint = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, Math.max(0, progress - 0.04));
8713
+ const nextPoint = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, Math.min(1, progress + 0.04));
8714
+ const tangentX = nextPoint.x - previousPoint.x;
8715
+ const tangentY = nextPoint.y - previousPoint.y;
8716
+ const tangentLength = Math.hypot(tangentX, tangentY) || 1;
8717
+ const width = tentacleShape.baseWidth + (tentacleShape.tipWidth - tentacleShape.baseWidth) * Math.pow(progress, 1.1);
8718
+ return {
8719
+ x: point.x,
8720
+ y: point.y,
8721
+ normalX: -tangentY / tangentLength,
8722
+ normalY: tangentX / tangentLength,
8723
+ width,
8724
+ progress,
8725
+ };
8726
+ });
8727
+ }
8728
+ /**
8729
+ * Resolves smooth pupil offsets that blend autonomous idle drift with live viewer tracking.
8730
+ *
8731
+ * @param options Eye motion options.
8732
+ * @returns Resolved pupil offsets.
8733
+ *
8734
+ * @private shared geometry helper of octopus avatar visuals
8735
+ */
8736
+ function resolveOrganicEyeMotion(options) {
8737
+ const { radiusX, radiusY, timeMs, phase, interaction, autonomousDriftRatioX = 0.12, autonomousDriftRatioY = 0.08, } = options;
8738
+ const autonomousOffsetX = Math.sin(timeMs / 1280 + phase) * radiusX * autonomousDriftRatioX;
8739
+ const autonomousOffsetY = Math.cos(timeMs / 940 + phase) * radiusY * autonomousDriftRatioY;
8740
+ const interactionBlend = Math.min(1, interaction.intensity * 0.9);
8741
+ return {
8742
+ pupilOffsetX: autonomousOffsetX * (1 - interactionBlend) + interaction.gazeX * radiusX * (0.18 + interactionBlend * 0.18),
8743
+ pupilOffsetY: autonomousOffsetY * (1 - interactionBlend) + interaction.gazeY * radiusY * (0.16 + interactionBlend * 0.16),
8744
+ };
8745
+ }
8746
+ /**
8747
+ * Samples one point on a cubic Bezier curve.
8748
+ *
8749
+ * @param startPoint Curve start point.
8750
+ * @param controlPointOne First control point.
8751
+ * @param controlPointTwo Second control point.
8752
+ * @param endPoint Curve end point.
8753
+ * @param progress Sampling progress in the range `[0, 1]`.
8754
+ * @returns Sampled point.
8755
+ *
8756
+ * @private shared geometry helper of `octopus3AvatarVisual`
8757
+ */
8758
+ function getCubicBezierPoint(startPoint, controlPointOne, controlPointTwo, endPoint, progress) {
8759
+ const inverseProgress = 1 - progress;
8760
+ return {
8761
+ x: inverseProgress * inverseProgress * inverseProgress * startPoint.x +
8762
+ 3 * inverseProgress * inverseProgress * progress * controlPointOne.x +
8763
+ 3 * inverseProgress * progress * progress * controlPointTwo.x +
8764
+ progress * progress * progress * endPoint.x,
8765
+ y: inverseProgress * inverseProgress * inverseProgress * startPoint.y +
8766
+ 3 * inverseProgress * inverseProgress * progress * controlPointOne.y +
8767
+ 3 * inverseProgress * progress * progress * controlPointTwo.y +
8768
+ progress * progress * progress * endPoint.y,
8769
+ };
8770
+ }
8771
+
8772
+ /* eslint-disable no-magic-numbers */
8773
+ /**
8774
+ * Glyph ramp used for the main octopus body fill.
8775
+ *
8776
+ * @private helper of `asciiOctopusAvatarVisual`
8777
+ */
8778
+ const BODY_GLYPHS = ['.', ':', '-', '=', '+', '*', '#', '%', '@'];
8779
+ /**
8780
+ * Glyph ramp used on silhouette edges so the ASCII blob stays legible.
8781
+ *
8782
+ * @private helper of `asciiOctopusAvatarVisual`
8783
+ */
8784
+ const OUTLINE_GLYPHS = ['#', '%', '@'];
8785
+ /**
8786
+ * Glyph ramp used in the surrounding atmosphere.
8787
+ *
8788
+ * @private helper of `asciiOctopusAvatarVisual`
8789
+ */
8790
+ const ATMOSPHERE_GLYPHS = ['.', ':', "'", '`'];
8791
+ /**
8792
+ * AsciiOctopus avatar visual.
8793
+ *
8794
+ * @private built-in avatar visual
8795
+ */
8796
+ const asciiOctopusAvatarVisual = {
8797
+ id: 'ascii-octopus',
8798
+ title: 'AsciiOctopus',
8799
+ description: 'Morphing alien octopus translated into animated ASCII glyphs with responsive eyes and seeded geometry.',
8800
+ isAnimated: true,
8801
+ supportsPointerTracking: true,
8802
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
8803
+ const gridRandom = createRandom('ascii-octopus-grid');
8804
+ const staticRandom = createRandom('ascii-octopus-static');
8805
+ const gridMetrics = createAsciiGridMetrics(size, gridRandom);
8806
+ const layout = createAsciiOctopusLayout(size, timeMs, createRandom, staticRandom, interaction);
8807
+ drawAvatarFrame(context, size, palette);
8808
+ drawAsciiBackdrop(context, size, palette, layout, timeMs);
8809
+ context.save();
8810
+ context.font = `600 ${gridMetrics.fontSize}px monospace`;
8811
+ context.textAlign = 'center';
8812
+ context.textBaseline = 'middle';
8813
+ // The ASCII renderer samples the morphing octopus field on a low-resolution grid so the shape stays organic
8814
+ // while the glyph layout remains deterministic for the same avatar input.
8815
+ const cellRandom = createRandom('ascii-octopus-cells');
8816
+ for (let rowIndex = 0; rowIndex < gridMetrics.rowCount; rowIndex++) {
8817
+ for (let columnIndex = 0; columnIndex < gridMetrics.columnCount; columnIndex++) {
8818
+ const point = {
8819
+ x: gridMetrics.offsetX + columnIndex * gridMetrics.cellWidth,
8820
+ y: gridMetrics.offsetY + rowIndex * gridMetrics.cellHeight,
8821
+ };
8822
+ const noise = cellRandom();
8823
+ const glyphDescriptor = resolveAsciiGlyph({
8824
+ point,
8825
+ layout,
8826
+ palette,
8827
+ cellWidth: gridMetrics.cellWidth,
8828
+ cellHeight: gridMetrics.cellHeight,
8829
+ noise,
8830
+ timeMs,
8831
+ });
8832
+ if (!glyphDescriptor) {
8833
+ continue;
8834
+ }
8835
+ context.fillStyle = glyphDescriptor.color;
8836
+ context.fillText(glyphDescriptor.character, point.x, point.y);
8837
+ }
8838
+ }
8839
+ context.restore();
8840
+ },
8841
+ };
8842
+ /**
8843
+ * Draws the dark terminal-like glow behind the ASCII octopus.
8844
+ *
8845
+ * @param context Canvas 2D context.
8846
+ * @param size Canvas size in CSS pixels.
8847
+ * @param palette Derived avatar palette.
8848
+ * @param layout Prepared octopus layout.
8849
+ * @param timeMs Current animation time in milliseconds.
8850
+ *
8851
+ * @private helper of `asciiOctopusAvatarVisual`
8852
+ */
8853
+ function drawAsciiBackdrop(context, size, palette, layout, timeMs) {
8854
+ const haloGradient = context.createRadialGradient(layout.centerX, layout.centerY - size * 0.12, size * 0.06, layout.centerX, layout.centerY, size * 0.62);
8855
+ haloGradient.addColorStop(0, `${palette.highlight}26`);
8856
+ haloGradient.addColorStop(0.42, `${palette.accent}16`);
8857
+ haloGradient.addColorStop(1, `${palette.highlight}00`);
8858
+ context.fillStyle = haloGradient;
8859
+ context.fillRect(0, 0, size, size);
8860
+ 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);
8861
+ lowerGlowGradient.addColorStop(0, `${palette.secondary}1f`);
8862
+ lowerGlowGradient.addColorStop(1, `${palette.secondary}00`);
8863
+ context.fillStyle = lowerGlowGradient;
8864
+ context.fillRect(0, 0, size, size);
8865
+ context.beginPath();
8866
+ context.ellipse(layout.centerX, layout.centerY + size * 0.29, size * 0.23, size * 0.065, 0, 0, Math.PI * 2);
8867
+ context.fillStyle = `${palette.shadow}33`;
8868
+ context.fill();
8869
+ }
8870
+ /**
8871
+ * Resolves the ASCII character that should be drawn for one sampled cell.
8872
+ *
8873
+ * @param options Cell evaluation options.
8874
+ * @returns Character descriptor or `null` when the cell should stay empty.
8875
+ *
8876
+ * @private helper of `asciiOctopusAvatarVisual`
8877
+ */
8878
+ function resolveAsciiGlyph(options) {
8879
+ const { point, layout, palette, cellWidth, cellHeight, noise, timeMs } = options;
8880
+ const eyeGlyphDescriptor = resolveEyeGlyph(point, layout.leftEye, layout.interaction, palette, timeMs) ||
8881
+ resolveEyeGlyph(point, layout.rightEye, layout.interaction, palette, timeMs);
8882
+ if (eyeGlyphDescriptor) {
8883
+ return eyeGlyphDescriptor;
8884
+ }
8885
+ const mouthGlyphDescriptor = resolveMouthGlyph(point, layout, palette, cellHeight);
8886
+ if (mouthGlyphDescriptor) {
8887
+ return mouthGlyphDescriptor;
8888
+ }
8889
+ const isWithinOctopusBounds = point.x >= layout.leftBound &&
8890
+ point.x <= layout.rightBound &&
8891
+ point.y >= layout.topBound &&
8892
+ point.y <= layout.bottomBound;
8893
+ if (!isWithinOctopusBounds) {
8894
+ return resolveAtmosphereGlyph(point, layout, palette, noise, timeMs);
8895
+ }
8896
+ const isInsideBody = isPointInsidePolygon(point, layout.bodyPoints);
8897
+ const bodyEdgeDistance = isInsideBody
8898
+ ? getDistanceToPolyline(point, layout.bodyPoints, true)
8899
+ : Number.POSITIVE_INFINITY;
8900
+ const tentacleCoverage = measureTentacleCoverage(point, layout.sampledTentacles, cellWidth);
8901
+ if (isInsideBody || tentacleCoverage) {
8902
+ return resolveOctopusSurfaceGlyph({
8903
+ point,
8904
+ layout,
8905
+ palette,
8906
+ isInsideBody,
8907
+ bodyEdgeDistance,
8908
+ tentacleCoverage,
8909
+ cellWidth,
8910
+ cellHeight,
8911
+ noise,
8912
+ timeMs,
8913
+ });
8914
+ }
8915
+ return resolveAtmosphereGlyph(point, layout, palette, noise, timeMs);
8916
+ }
8917
+ /**
8918
+ * Resolves the ASCII character for one eye cell.
8919
+ *
8920
+ * @param point Sampled cell point.
8921
+ * @param eyeFeature Eye geometry.
8922
+ * @param palette Derived avatar palette.
8923
+ * @param timeMs Current animation time in milliseconds.
8924
+ * @returns Eye glyph descriptor or `null`.
8925
+ *
8926
+ * @private helper of `asciiOctopusAvatarVisual`
8927
+ */
8928
+ function resolveEyeGlyph(point, eyeFeature, interaction, palette, timeMs) {
8929
+ const { pupilOffsetX, pupilOffsetY } = resolveOrganicEyeMotion({
8930
+ radiusX: eyeFeature.radiusX,
8931
+ radiusY: eyeFeature.radiusY,
8932
+ timeMs,
8933
+ phase: eyeFeature.phase,
8934
+ interaction,
8935
+ });
8936
+ const scleraDistance = measureRotatedEllipseDistance(point, eyeFeature.centerX, eyeFeature.centerY, eyeFeature.radiusX, eyeFeature.radiusY, eyeFeature.rotation);
8937
+ if (scleraDistance > 1.08) {
8938
+ return null;
8939
+ }
8940
+ 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);
8941
+ if (highlightDistance <= 1) {
8942
+ return { character: '*', color: '#ffffff' };
8943
+ }
8944
+ const pupilDistance = measureRotatedEllipseDistance(point, eyeFeature.centerX + pupilOffsetX, eyeFeature.centerY + pupilOffsetY, eyeFeature.radiusX * 0.2, eyeFeature.radiusY * 0.48, eyeFeature.rotation);
8945
+ if (pupilDistance <= 1) {
8946
+ return { character: '@', color: palette.ink };
8947
+ }
8948
+ const irisDistance = measureRotatedEllipseDistance(point, eyeFeature.centerX + pupilOffsetX, eyeFeature.centerY + pupilOffsetY, eyeFeature.radiusX * 0.64, eyeFeature.radiusY * 0.72, eyeFeature.rotation);
8949
+ if (irisDistance <= 1) {
8950
+ return {
8951
+ character: irisDistance < 0.46 ? '0' : 'o',
8952
+ color: irisDistance < 0.62 ? palette.secondary : `${palette.highlight}d9`,
8953
+ };
8954
+ }
8955
+ return {
8956
+ character: scleraDistance > 0.82 ? 'o' : '0',
8957
+ color: '#f8fbff',
8958
+ };
8959
+ }
8960
+ /**
8961
+ * Resolves the ASCII character for the octopus mouth.
8962
+ *
8963
+ * @param point Sampled cell point.
8964
+ * @param layout Prepared octopus layout.
8965
+ * @param palette Derived avatar palette.
8966
+ * @param cellHeight Character cell height.
8967
+ * @returns Mouth glyph descriptor or `null`.
8968
+ *
8969
+ * @private helper of `asciiOctopusAvatarVisual`
8970
+ */
8971
+ function resolveMouthGlyph(point, layout, palette, cellHeight) {
8972
+ const mouthDistance = getDistanceToPolyline(point, layout.mouthPoints, false);
8973
+ if (mouthDistance > cellHeight * 0.38) {
8974
+ return null;
8975
+ }
8976
+ const horizontalProgress = clamp01((point.x - layout.mouthPoints[0].x) /
8977
+ (layout.mouthPoints[layout.mouthPoints.length - 1].x - layout.mouthPoints[0].x));
8978
+ let character = '-';
8979
+ if (horizontalProgress < 0.28) {
8980
+ character = '\\';
8981
+ }
8982
+ else if (horizontalProgress > 0.72) {
8983
+ character = '/';
8984
+ }
8985
+ else if (horizontalProgress > 0.42 && horizontalProgress < 0.58) {
8986
+ character = '_';
8987
+ }
8988
+ return {
8989
+ character,
8990
+ color: `${palette.ink}bf`,
8991
+ };
8992
+ }
8993
+ /**
8994
+ * Resolves the ASCII character for body and tentacle cells.
8995
+ *
8996
+ * @param options Surface evaluation options.
8997
+ * @returns Surface glyph descriptor.
8998
+ *
8999
+ * @private helper of `asciiOctopusAvatarVisual`
9000
+ */
9001
+ function resolveOctopusSurfaceGlyph(options) {
9002
+ const { point, layout, palette, isInsideBody, bodyEdgeDistance, tentacleCoverage, cellHeight, noise, timeMs } = options;
9003
+ const isTentacleDominant = tentacleCoverage !== null && (!isInsideBody || point.y > layout.centerY + layout.bodyRadius * 0.08);
9004
+ if (isTentacleDominant && tentacleCoverage) {
9005
+ const isSuckerBand = tentacleCoverage.progress > 0.24 && tentacleCoverage.progress < 0.82 && noise > 0.78;
9006
+ if (isSuckerBand && tentacleCoverage.normalizedDistance > 0.42) {
9007
+ return {
9008
+ character: noise > 0.9 ? '0' : 'o',
9009
+ color: `${palette.highlight}d0`,
9010
+ };
9011
+ }
9012
+ return {
9013
+ character: pickTentacleCharacter(tentacleCoverage, noise),
9014
+ color: tentacleCoverage.progress < 0.24
9015
+ ? `${palette.secondary}c7`
9016
+ : tentacleCoverage.progress > 0.72
9017
+ ? `${palette.accent}bf`
9018
+ : tentacleCoverage.normalizedDistance > 0.7
9019
+ ? `${palette.highlight}bf`
9020
+ : `${palette.primary}c9`,
9021
+ };
9022
+ }
9023
+ const highlightBias = clamp01((layout.centerY - point.y + layout.bodyRadius * 0.44) / (layout.bodyRadius * 1.14));
9024
+ const bodyDepth = clamp01(1 - bodyEdgeDistance / (layout.bodyRadius * 0.9));
9025
+ const shimmer = Math.sin(timeMs / 720 + point.x * 0.085 + point.y * 0.06 + layout.shapePhase) * 0.05;
9026
+ const bodyIntensity = clamp01(0.22 + bodyDepth * 0.58 + highlightBias * 0.2 + shimmer + (noise - 0.5) * 0.18);
9027
+ const isOutline = isInsideBody && bodyEdgeDistance < cellHeight * 0.54;
9028
+ const character = isOutline
9029
+ ? pickRampCharacter(OUTLINE_GLYPHS, clamp01(0.58 + bodyIntensity * 0.42))
9030
+ : pickRampCharacter(BODY_GLYPHS, bodyIntensity);
9031
+ let color = `${palette.primary}bf`;
9032
+ if (highlightBias > 0.76) {
9033
+ color = `${palette.highlight}d9`;
9034
+ }
9035
+ else if (bodyDepth > 0.7) {
9036
+ color = `${palette.secondary}cb`;
9037
+ }
9038
+ else if ((point.x < layout.centerX && noise > 0.58) || (point.x >= layout.centerX && noise < 0.42)) {
9039
+ color = `${palette.accent}ba`;
9040
+ }
9041
+ return {
9042
+ character,
9043
+ color: isOutline ? `${palette.highlight}c9` : color,
9044
+ };
9045
+ }
9046
+ /**
9047
+ * Resolves faint atmosphere glyphs around the ASCII octopus.
9048
+ *
9049
+ * @param point Sampled cell point.
9050
+ * @param layout Prepared octopus layout.
9051
+ * @param palette Derived avatar palette.
9052
+ * @param noise Stable per-cell noise.
9053
+ * @param timeMs Current animation time in milliseconds.
9054
+ * @returns Atmosphere glyph descriptor or `null`.
9055
+ *
9056
+ * @private helper of `asciiOctopusAvatarVisual`
9057
+ */
9058
+ function resolveAtmosphereGlyph(point, layout, palette, noise, timeMs) {
9059
+ const horizontalDistance = Math.abs(point.x - layout.centerX) / (layout.bodyRadius * layout.horizontalStretch * 2.2);
9060
+ const verticalDistance = Math.abs(point.y - (layout.centerY + layout.bodyRadius * 0.04)) / (layout.bodyRadius * 2.1);
9061
+ const haloDistance = Math.hypot(horizontalDistance, verticalDistance);
9062
+ const shimmer = Math.sin(timeMs / 1450 + point.x * 0.03 + point.y * 0.04 + layout.shapePhase) * 0.06;
9063
+ const density = clamp01(1.16 - haloDistance + shimmer + (noise - 0.5) * 0.14);
9064
+ if (density < 0.18 || noise > density * 0.84 + 0.34) {
9065
+ return null;
9066
+ }
9067
+ return {
9068
+ character: pickRampCharacter(ATMOSPHERE_GLYPHS, density),
9069
+ color: point.y < layout.centerY ? `${palette.highlight}63` : `${palette.accent}47`,
9070
+ };
9071
+ }
9072
+ /**
9073
+ * Builds the grid used by the ASCII renderer.
9074
+ *
9075
+ * @param size Canvas size in CSS pixels.
9076
+ * @param staticRandom Stable random generator for this avatar.
9077
+ * @returns Grid metrics.
9078
+ *
9079
+ * @private helper of `asciiOctopusAvatarVisual`
9080
+ */
9081
+ function createAsciiGridMetrics(size, staticRandom) {
9082
+ const fontSize = Math.max(9, Math.round(size * (0.048 + staticRandom() * 0.006)));
9083
+ const cellWidth = fontSize * (0.58 + staticRandom() * 0.04);
9084
+ const cellHeight = fontSize * 0.82;
9085
+ const columnCount = Math.max(12, Math.floor(size / cellWidth) - 1);
9086
+ const rowCount = Math.max(12, Math.floor(size / cellHeight) - 1);
9087
+ const offsetX = (size - (columnCount - 1) * cellWidth) / 2;
9088
+ const offsetY = (size - (rowCount - 1) * cellHeight) / 2;
9089
+ return {
9090
+ fontSize,
9091
+ cellWidth,
9092
+ cellHeight,
9093
+ columnCount,
9094
+ rowCount,
9095
+ offsetX,
9096
+ offsetY,
9097
+ };
9098
+ }
9099
+ /**
9100
+ * Builds the deterministic octopus geometry that will later be sampled into ASCII cells.
9101
+ *
9102
+ * @param size Canvas size in CSS pixels.
9103
+ * @param timeMs Current animation time in milliseconds.
9104
+ * @param createRandom Seeded random factory scoped to the avatar.
9105
+ * @param staticRandom Stable random generator for this avatar.
9106
+ * @returns Prepared octopus layout.
9107
+ *
9108
+ * @private helper of `asciiOctopusAvatarVisual`
9109
+ */
9110
+ function createAsciiOctopusLayout(size, timeMs, createRandom, staticRandom, interaction) {
9111
+ const centerX = size * (0.5 + (staticRandom() - 0.5) * 0.02) + interaction.bodyOffsetX * size * 0.05;
9112
+ const centerY = size * (0.41 + staticRandom() * 0.05) + interaction.bodyOffsetY * size * 0.035;
9113
+ const bodyRadius = size * (0.195 + staticRandom() * 0.05);
9114
+ const horizontalStretch = 1.08 + staticRandom() * 0.22;
9115
+ const verticalStretch = 0.88 + staticRandom() * 0.14;
9116
+ const mantleLift = size * (0.1 + staticRandom() * 0.03);
9117
+ const lowerDrop = size * (0.03 + staticRandom() * 0.024);
9118
+ const tentacleDepth = size * (0.026 + staticRandom() * 0.022);
9119
+ const wobbleAmplitude = size * (0.008 + staticRandom() * 0.01);
9120
+ const lobeCount = 5 + Math.floor(staticRandom() * 4);
9121
+ const shapePhase = staticRandom() * Math.PI * 2;
9122
+ const tentacleCount = 8 + Math.floor(staticRandom() * 5);
9123
+ const eyeSpacing = size * (0.108 + staticRandom() * 0.042);
9124
+ const eyeRadiusX = size * (0.05 + staticRandom() * 0.015);
9125
+ const eyeRadiusY = eyeRadiusX * (1.16 + staticRandom() * 0.2);
9126
+ const bodyPoints = createOrganicOctopusBodyPoints({
9127
+ centerX,
9128
+ centerY,
9129
+ bodyRadius,
9130
+ horizontalStretch,
9131
+ verticalStretch,
9132
+ mantleLift,
9133
+ lowerDrop,
9134
+ tentacleDepth,
9135
+ wobbleAmplitude,
9136
+ lobeCount,
9137
+ shapePhase,
9138
+ timeMs,
9139
+ pointCount: 40,
9140
+ });
9141
+ const tentacleShapes = createOrganicOctopusTentacleShapes({
9142
+ size,
9143
+ centerX,
9144
+ centerY,
9145
+ bodyRadius,
9146
+ horizontalStretch,
9147
+ tentacleCount,
9148
+ shapePhase,
9149
+ createRandom,
9150
+ timeMs,
9151
+ saltPrefix: 'ascii-octopus',
9152
+ bodyPoints,
9153
+ });
9154
+ const sampledTentacles = tentacleShapes.map(sampleOrganicTentacleRibbonPoints);
9155
+ const leftEye = {
9156
+ centerX: centerX - eyeSpacing,
9157
+ centerY: centerY - size * 0.01,
9158
+ radiusX: eyeRadiusX,
9159
+ radiusY: eyeRadiusY,
9160
+ rotation: (staticRandom() - 0.5) * 0.24,
9161
+ phase: shapePhase,
9162
+ };
9163
+ const rightEye = {
9164
+ centerX: centerX + eyeSpacing,
9165
+ centerY: centerY - size * 0.01,
9166
+ radiusX: eyeRadiusX,
9167
+ radiusY: eyeRadiusY,
9168
+ rotation: (staticRandom() - 0.5) * 0.24,
9169
+ phase: shapePhase + Math.PI / 4,
9170
+ };
9171
+ const mouthPoints = sampleQuadraticBezierPoints({ x: centerX - size * 0.074, y: centerY + size * 0.092 }, {
9172
+ x: centerX,
9173
+ y: centerY +
9174
+ size * (0.142 + Math.sin(timeMs / 620 + shapePhase) * 0.016) +
9175
+ interaction.gazeY * size * 0.012,
9176
+ }, { x: centerX + size * 0.074, y: centerY + size * 0.092 }, 12);
9177
+ let leftBound = Number.POSITIVE_INFINITY;
9178
+ let rightBound = Number.NEGATIVE_INFINITY;
9179
+ let topBound = Number.POSITIVE_INFINITY;
9180
+ let bottomBound = Number.NEGATIVE_INFINITY;
9181
+ for (const bodyPoint of bodyPoints) {
9182
+ leftBound = Math.min(leftBound, bodyPoint.x);
9183
+ rightBound = Math.max(rightBound, bodyPoint.x);
9184
+ topBound = Math.min(topBound, bodyPoint.y);
9185
+ bottomBound = Math.max(bottomBound, bodyPoint.y);
9186
+ }
9187
+ for (const sampledTentacle of sampledTentacles) {
9188
+ for (const ribbonPoint of sampledTentacle) {
9189
+ leftBound = Math.min(leftBound, ribbonPoint.x - ribbonPoint.width);
9190
+ rightBound = Math.max(rightBound, ribbonPoint.x + ribbonPoint.width);
9191
+ topBound = Math.min(topBound, ribbonPoint.y - ribbonPoint.width);
9192
+ bottomBound = Math.max(bottomBound, ribbonPoint.y + ribbonPoint.width);
9193
+ }
9194
+ }
9195
+ return {
9196
+ centerX,
9197
+ centerY,
9198
+ bodyRadius,
9199
+ horizontalStretch,
9200
+ shapePhase,
9201
+ interaction,
9202
+ bodyPoints,
9203
+ sampledTentacles,
9204
+ leftEye,
9205
+ rightEye,
9206
+ mouthPoints,
9207
+ leftBound: leftBound - size * 0.08,
9208
+ rightBound: rightBound + size * 0.08,
9209
+ topBound: topBound - size * 0.08,
9210
+ bottomBound: bottomBound + size * 0.08,
9211
+ };
9212
+ }
9213
+ /**
9214
+ * Samples points along a quadratic Bezier curve.
9215
+ *
9216
+ * @param startPoint Curve start point.
9217
+ * @param controlPoint Curve control point.
9218
+ * @param endPoint Curve end point.
9219
+ * @param pointCount Number of intervals to sample.
9220
+ * @returns Sampled curve points.
9221
+ *
9222
+ * @private helper of `asciiOctopusAvatarVisual`
9223
+ */
9224
+ function sampleQuadraticBezierPoints(startPoint, controlPoint, endPoint, pointCount) {
9225
+ return Array.from({ length: pointCount + 1 }, (_, pointIndex) => {
9226
+ const progress = pointIndex / pointCount;
9227
+ const inverseProgress = 1 - progress;
9228
+ return {
9229
+ x: inverseProgress * inverseProgress * startPoint.x +
9230
+ 2 * inverseProgress * progress * controlPoint.x +
9231
+ progress * progress * endPoint.x,
9232
+ y: inverseProgress * inverseProgress * startPoint.y +
9233
+ 2 * inverseProgress * progress * controlPoint.y +
9234
+ progress * progress * endPoint.y,
9235
+ };
9236
+ });
9237
+ }
9238
+ /**
9239
+ * Measures how strongly the sampled cell intersects with the generated tentacles.
9240
+ *
9241
+ * @param point Sampled cell point.
9242
+ * @param sampledTentacles Pre-sampled tentacle ribbons.
9243
+ * @param cellWidth Character cell width.
9244
+ * @returns Nearest tentacle coverage or `null`.
9245
+ *
9246
+ * @private helper of `asciiOctopusAvatarVisual`
9247
+ */
9248
+ function measureTentacleCoverage(point, sampledTentacles, cellWidth) {
9249
+ let bestTentacleCoverage = null;
9250
+ let bestNormalizedDistance = 0;
9251
+ for (const sampledTentacle of sampledTentacles) {
9252
+ for (const ribbonPoint of sampledTentacle) {
9253
+ const deltaX = point.x - ribbonPoint.x;
9254
+ const deltaY = point.y - ribbonPoint.y;
9255
+ const distance = Math.hypot(deltaX, deltaY);
9256
+ const coverageRadius = ribbonPoint.width + cellWidth * 0.22;
9257
+ const normalizedDistance = 1 - distance / coverageRadius;
9258
+ if (normalizedDistance <= bestNormalizedDistance || normalizedDistance <= 0) {
9259
+ continue;
9260
+ }
9261
+ bestNormalizedDistance = normalizedDistance;
9262
+ bestTentacleCoverage = {
9263
+ tangentAngle: Math.atan2(-ribbonPoint.normalX, ribbonPoint.normalY),
9264
+ progress: ribbonPoint.progress,
9265
+ normalizedDistance,
9266
+ };
9267
+ }
9268
+ }
9269
+ return bestTentacleCoverage;
9270
+ }
9271
+ /**
9272
+ * Picks one ASCII character that matches the nearest tentacle direction.
9273
+ *
9274
+ * @param tentacleCoverage Nearest tentacle coverage.
9275
+ * @param noise Stable per-cell noise.
9276
+ * @returns Tentacle ASCII character.
9277
+ *
9278
+ * @private helper of `asciiOctopusAvatarVisual`
9279
+ */
9280
+ function pickTentacleCharacter(tentacleCoverage, noise) {
9281
+ const isSuckerBand = tentacleCoverage.progress > 0.24 && tentacleCoverage.progress < 0.82 && noise > 0.82;
9282
+ if (isSuckerBand && tentacleCoverage.normalizedDistance > 0.34) {
9283
+ return noise > 0.91 ? '0' : 'o';
9284
+ }
9285
+ const horizontalWeight = Math.abs(Math.cos(tentacleCoverage.tangentAngle));
9286
+ const verticalWeight = Math.abs(Math.sin(tentacleCoverage.tangentAngle));
9287
+ if (horizontalWeight > 0.84) {
9288
+ return noise > 0.52 ? '=' : '-';
9289
+ }
9290
+ if (verticalWeight > 0.82) {
9291
+ return noise > 0.56 ? '|' : '!';
9292
+ }
9293
+ return Math.sin(tentacleCoverage.tangentAngle) * Math.cos(tentacleCoverage.tangentAngle) > 0 ? '\\' : '/';
9294
+ }
9295
+ /**
9296
+ * Picks one character from an ordered ramp.
9297
+ *
9298
+ * @param glyphRamp Ordered glyph ramp.
9299
+ * @param intensity Normalized intensity in the range `[0, 1]`.
9300
+ * @returns Selected glyph.
9301
+ *
9302
+ * @private helper of `asciiOctopusAvatarVisual`
9303
+ */
9304
+ function pickRampCharacter(glyphRamp, intensity) {
9305
+ const characterIndex = Math.min(glyphRamp.length - 1, Math.floor(clamp01(intensity) * glyphRamp.length));
9306
+ return glyphRamp[characterIndex];
9307
+ }
9308
+ /**
9309
+ * Measures the normalized distance from a point to a rotated ellipse.
9310
+ *
9311
+ * @param point Sampled cell point.
9312
+ * @param centerX Ellipse center X coordinate.
9313
+ * @param centerY Ellipse center Y coordinate.
9314
+ * @param radiusX Horizontal ellipse radius.
9315
+ * @param radiusY Vertical ellipse radius.
9316
+ * @param rotation Ellipse rotation in radians.
9317
+ * @returns Normalized ellipse distance where values below `1` are inside.
9318
+ *
9319
+ * @private helper of `asciiOctopusAvatarVisual`
9320
+ */
9321
+ function measureRotatedEllipseDistance(point, centerX, centerY, radiusX, radiusY, rotation) {
9322
+ const cosine = Math.cos(rotation);
9323
+ const sine = Math.sin(rotation);
9324
+ const translatedX = point.x - centerX;
9325
+ const translatedY = point.y - centerY;
9326
+ const localX = translatedX * cosine + translatedY * sine;
9327
+ const localY = -translatedX * sine + translatedY * cosine;
9328
+ return Math.sqrt((localX * localX) / (radiusX * radiusX) + (localY * localY) / (radiusY * radiusY));
9329
+ }
9330
+ /**
9331
+ * Checks whether a point lies inside the given closed polygon.
9332
+ *
9333
+ * @param point Sampled cell point.
9334
+ * @param polygonPoints Polygon points in order.
9335
+ * @returns `true` when the point lies inside the polygon.
9336
+ *
9337
+ * @private helper of `asciiOctopusAvatarVisual`
9338
+ */
9339
+ function isPointInsidePolygon(point, polygonPoints) {
9340
+ let isInside = false;
9341
+ for (let currentPointIndex = 0, previousPointIndex = polygonPoints.length - 1; currentPointIndex < polygonPoints.length; previousPointIndex = currentPointIndex++) {
9342
+ const currentPoint = polygonPoints[currentPointIndex];
9343
+ const previousPoint = polygonPoints[previousPointIndex];
9344
+ const isIntersecting = currentPoint.y > point.y !== previousPoint.y > point.y &&
9345
+ point.x <
9346
+ ((previousPoint.x - currentPoint.x) * (point.y - currentPoint.y)) / (previousPoint.y - currentPoint.y) +
9347
+ currentPoint.x;
9348
+ if (isIntersecting) {
9349
+ isInside = !isInside;
9350
+ }
9351
+ }
9352
+ return isInside;
9353
+ }
9354
+ /**
9355
+ * Measures the shortest distance from a point to a polyline.
9356
+ *
9357
+ * @param point Sampled cell point.
9358
+ * @param polylinePoints Polyline points in order.
9359
+ * @param isClosed Whether the final point should connect back to the first point.
9360
+ * @returns Shortest distance to the polyline.
9361
+ *
9362
+ * @private helper of `asciiOctopusAvatarVisual`
9363
+ */
9364
+ function getDistanceToPolyline(point, polylinePoints, isClosed) {
9365
+ let shortestDistance = Number.POSITIVE_INFINITY;
9366
+ const segmentCount = isClosed ? polylinePoints.length : polylinePoints.length - 1;
9367
+ for (let segmentIndex = 0; segmentIndex < segmentCount; segmentIndex++) {
9368
+ const startPoint = polylinePoints[segmentIndex];
9369
+ const endPoint = polylinePoints[(segmentIndex + 1) % polylinePoints.length];
9370
+ shortestDistance = Math.min(shortestDistance, getDistanceToLineSegment(point, startPoint, endPoint));
9371
+ }
9372
+ return shortestDistance;
9373
+ }
9374
+ /**
9375
+ * Measures the shortest distance from a point to one line segment.
9376
+ *
9377
+ * @param point Sampled cell point.
9378
+ * @param startPoint Segment start point.
9379
+ * @param endPoint Segment end point.
9380
+ * @returns Shortest distance to the segment.
9381
+ *
9382
+ * @private helper of `asciiOctopusAvatarVisual`
9383
+ */
9384
+ function getDistanceToLineSegment(point, startPoint, endPoint) {
9385
+ const deltaX = endPoint.x - startPoint.x;
9386
+ const deltaY = endPoint.y - startPoint.y;
9387
+ const segmentLengthSquared = deltaX * deltaX + deltaY * deltaY;
9388
+ if (segmentLengthSquared === 0) {
9389
+ return Math.hypot(point.x - startPoint.x, point.y - startPoint.y);
9390
+ }
9391
+ const progress = clamp01(((point.x - startPoint.x) * deltaX + (point.y - startPoint.y) * deltaY) / segmentLengthSquared);
9392
+ const projectionX = startPoint.x + deltaX * progress;
9393
+ const projectionY = startPoint.y + deltaY * progress;
9394
+ return Math.hypot(point.x - projectionX, point.y - projectionY);
9395
+ }
9396
+ /**
9397
+ * Clamps a number into the inclusive range `[0, 1]`.
9398
+ *
9399
+ * @param value Arbitrary numeric value.
9400
+ * @returns Clamped value.
9401
+ *
9402
+ * @private helper of `asciiOctopusAvatarVisual`
9403
+ */
9404
+ function clamp01(value) {
9405
+ return Math.max(0, Math.min(1, value));
9406
+ }
9407
+
9408
+ /* eslint-disable no-magic-numbers */
9409
+ /**
9410
+ * Fractal avatar visual.
9411
+ *
9412
+ * @private built-in avatar visual
9413
+ */
9414
+ const fractalAvatarVisual = {
9415
+ id: 'fractal',
9416
+ title: 'Fractal',
9417
+ description: 'Layered dragon-curve ribbons with deterministic glows, bends, and seeded color interplay.',
9418
+ isAnimated: true,
9419
+ render({ context, size, palette, createRandom, timeMs }) {
9420
+ const staticRandom = createRandom('fractal-static');
9421
+ const centerX = size * 0.5;
9422
+ const centerY = size * 0.5;
9423
+ const layerCount = 2 + Math.floor(staticRandom() * 3);
9424
+ const haloRotation = staticRandom() * Math.PI * 2;
9425
+ const colorSequence = [palette.primary, palette.secondary, palette.accent, palette.highlight];
9426
+ drawAvatarFrame(context, size, palette);
9427
+ drawFractalBackground(context, size, palette, timeMs, haloRotation);
9428
+ for (let layerIndex = 0; layerIndex < layerCount; layerIndex++) {
9429
+ const layerRandom = createRandom(`fractal-layer-${layerIndex}`);
9430
+ const order = 8 + Math.floor(layerRandom() * 4);
9431
+ const turnSequence = createDragonCurveTurns(order);
9432
+ const basePoints = createDragonCurvePoints(turnSequence);
9433
+ const transformedPoints = transformDragonCurvePoints(basePoints, {
9434
+ size,
9435
+ centerX: centerX + (layerRandom() - 0.5) * size * 0.08,
9436
+ centerY: centerY + (layerRandom() - 0.5) * size * 0.08,
9437
+ rotation: layerRandom() * Math.PI * 2 + Math.sin(timeMs / (1700 + layerIndex * 280) + layerIndex) * 0.14,
9438
+ scale: size * (0.19 + layerIndex * 0.055 + layerRandom() * 0.045),
9439
+ horizontalStretch: 0.74 + layerRandom() * 0.9,
9440
+ verticalStretch: 0.74 + layerRandom() * 0.9,
9441
+ warpAmplitude: size * (0.008 + layerRandom() * 0.012),
9442
+ warpPhase: layerRandom() * Math.PI * 2,
9443
+ mirrorX: layerRandom() < 0.5 ? -1 : 1,
9444
+ mirrorY: layerRandom() < 0.38 ? -1 : 1,
9445
+ timeMs,
9446
+ });
9447
+ const primaryColor = colorSequence[layerIndex % colorSequence.length];
9448
+ const secondaryColor = colorSequence[(layerIndex + 1) % colorSequence.length];
9449
+ const tertiaryColor = colorSequence[(layerIndex + 2) % colorSequence.length];
9450
+ const strokeWidth = size * (0.026 - layerIndex * 0.0035);
9451
+ drawDragonCurveLayer(context, transformedPoints, {
9452
+ size,
9453
+ primaryColor,
9454
+ secondaryColor,
9455
+ tertiaryColor,
9456
+ shadowColor: palette.shadow,
9457
+ strokeWidth,
9458
+ timeMs,
9459
+ layerIndex,
9460
+ });
9461
+ }
9462
+ drawFractalCore(context, size, palette, timeMs, staticRandom());
9463
+ },
9464
+ };
9465
+ /**
9466
+ * Draws the shared luminous atmosphere behind the curve layers.
9467
+ *
9468
+ * @param context Canvas 2D context.
9469
+ * @param size Canvas size in CSS pixels.
9470
+ * @param palette Derived avatar palette.
9471
+ * @param timeMs Current animation time in milliseconds.
9472
+ * @param haloRotation Seed-based phase offset.
9473
+ *
9474
+ * @private helper of `fractalAvatarVisual`
9475
+ */
9476
+ function drawFractalBackground(context, size, palette, timeMs, haloRotation) {
9477
+ const centerX = size * 0.5;
9478
+ const centerY = size * 0.5;
9479
+ const radialGlow = context.createRadialGradient(centerX, centerY, size * 0.06, centerX, centerY, size * 0.72);
9480
+ radialGlow.addColorStop(0, `${palette.highlight}55`);
9481
+ radialGlow.addColorStop(0.4, `${palette.secondary}1f`);
9482
+ radialGlow.addColorStop(1, `${palette.highlight}00`);
9483
+ context.fillStyle = radialGlow;
9484
+ context.fillRect(0, 0, size, size);
9485
+ for (let haloIndex = 0; haloIndex < 3; haloIndex++) {
9486
+ const radius = size * (0.17 + haloIndex * 0.09);
9487
+ const rotation = haloRotation + haloIndex * 0.85 + timeMs / (4400 + haloIndex * 700);
9488
+ context.beginPath();
9489
+ context.ellipse(centerX, centerY, radius, radius * (0.62 + haloIndex * 0.06), rotation, 0, Math.PI * 2);
9490
+ context.strokeStyle = haloIndex % 2 === 0 ? `${palette.secondary}24` : `${palette.accent}20`;
9491
+ context.lineWidth = size * 0.006;
9492
+ context.stroke();
9493
+ }
9494
+ }
9495
+ /**
9496
+ * Generates the left-right turn sequence for a dragon curve.
9497
+ *
9498
+ * @param order Number of folding iterations.
9499
+ * @returns Turn sequence where `1` means right and `-1` means left.
9500
+ *
9501
+ * @private helper of `fractalAvatarVisual`
9502
+ */
9503
+ function createDragonCurveTurns(order) {
9504
+ let turns = [];
9505
+ for (let iteration = 0; iteration < order; iteration++) {
9506
+ turns = [
9507
+ ...turns,
9508
+ 1,
9509
+ ...turns
9510
+ .slice()
9511
+ .reverse()
9512
+ .map((turn) => (turn === 1 ? -1 : 1)),
9513
+ ];
9514
+ }
9515
+ return turns;
9516
+ }
9517
+ /**
9518
+ * Converts a dragon-curve turn sequence into a raw grid polyline.
9519
+ *
9520
+ * @param turnSequence Ordered turn sequence.
9521
+ * @returns Unscaled polyline points.
9522
+ *
9523
+ * @private helper of `fractalAvatarVisual`
9524
+ */
9525
+ function createDragonCurvePoints(turnSequence) {
9526
+ const points = [{ x: 0, y: 0 }];
9527
+ const directions = [
9528
+ { x: 1, y: 0 },
9529
+ { x: 0, y: 1 },
9530
+ { x: -1, y: 0 },
9531
+ { x: 0, y: -1 },
9532
+ ];
9533
+ let directionIndex = 0;
9534
+ for (let segmentIndex = 0; segmentIndex <= turnSequence.length; segmentIndex++) {
9535
+ const currentPoint = points[points.length - 1];
9536
+ const direction = directions[directionIndex];
9537
+ points.push({
9538
+ x: currentPoint.x + direction.x,
9539
+ y: currentPoint.y + direction.y,
9540
+ });
9541
+ if (segmentIndex < turnSequence.length) {
9542
+ directionIndex = (directionIndex + turnSequence[segmentIndex] + directions.length) % directions.length;
9543
+ }
9544
+ }
9545
+ return points;
9546
+ }
9547
+ /**
9548
+ * Normalizes and decorates the dragon-curve polyline for avatar rendering.
9549
+ *
9550
+ * @param points Raw grid polyline points.
9551
+ * @param options Transformation parameters.
9552
+ * @returns Transformed canvas points.
9553
+ *
9554
+ * @private helper of `fractalAvatarVisual`
9555
+ */
9556
+ function transformDragonCurvePoints(points, options) {
9557
+ const { size, centerX, centerY, rotation, scale, horizontalStretch, verticalStretch, warpAmplitude, warpPhase, mirrorX, mirrorY, timeMs, } = options;
9558
+ const bounds = getPointBounds(points);
9559
+ const width = Math.max(1, bounds.maxX - bounds.minX);
9560
+ const height = Math.max(1, bounds.maxY - bounds.minY);
9561
+ const normalizationScale = scale / Math.max(width, height);
9562
+ const cosine = Math.cos(rotation);
9563
+ const sine = Math.sin(rotation);
9564
+ return points.map((point, pointIndex) => {
9565
+ const normalizedX = (point.x - (bounds.minX + width / 2)) * normalizationScale * horizontalStretch * mirrorX;
9566
+ const normalizedY = (point.y - (bounds.minY + height / 2)) * normalizationScale * verticalStretch * mirrorY;
9567
+ const progress = pointIndex / Math.max(1, points.length - 1);
9568
+ const localWarp = Math.sin(progress * Math.PI * 4 + warpPhase + timeMs / 1400) * warpAmplitude +
9569
+ Math.cos(progress * Math.PI * 7 - warpPhase + timeMs / 1800) * warpAmplitude * 0.45;
9570
+ const rotatedX = normalizedX * cosine - normalizedY * sine;
9571
+ const rotatedY = normalizedX * sine + normalizedY * cosine;
9572
+ return {
9573
+ x: centerX + rotatedX + Math.sin(progress * Math.PI * 2 + warpPhase) * localWarp,
9574
+ y: centerY +
9575
+ rotatedY +
9576
+ Math.cos(progress * Math.PI * 3 + warpPhase * 0.6) * localWarp +
9577
+ (progress - 0.5) * size * 0.02,
9578
+ };
9579
+ });
9580
+ }
9581
+ /**
9582
+ * Returns the bounding box of a point cloud.
9583
+ *
9584
+ * @param points Point cloud to inspect.
9585
+ * @returns Bounding box.
9586
+ *
9587
+ * @private helper of `fractalAvatarVisual`
9588
+ */
9589
+ function getPointBounds(points) {
9590
+ return points.reduce((bounds, point) => ({
9591
+ minX: Math.min(bounds.minX, point.x),
9592
+ maxX: Math.max(bounds.maxX, point.x),
9593
+ minY: Math.min(bounds.minY, point.y),
9594
+ maxY: Math.max(bounds.maxY, point.y),
9595
+ }), {
9596
+ minX: Number.POSITIVE_INFINITY,
9597
+ maxX: Number.NEGATIVE_INFINITY,
9598
+ minY: Number.POSITIVE_INFINITY,
9599
+ maxY: Number.NEGATIVE_INFINITY,
9600
+ });
9601
+ }
9602
+ /**
9603
+ * Draws one stylized dragon-curve ribbon with glow and spark nodes.
9604
+ *
9605
+ * @param context Canvas 2D context.
9606
+ * @param points Transformed polyline points.
9607
+ * @param options Layer styling options.
9608
+ *
9609
+ * @private helper of `fractalAvatarVisual`
9610
+ */
9611
+ function drawDragonCurveLayer(context, points, options) {
9612
+ const { size, primaryColor, secondaryColor, tertiaryColor, shadowColor, strokeWidth, timeMs, layerIndex } = options;
9613
+ const firstPoint = points[0];
9614
+ const lastPoint = points[points.length - 1];
9615
+ const ribbonGradient = context.createLinearGradient(firstPoint.x, firstPoint.y, lastPoint.x, lastPoint.y);
9616
+ ribbonGradient.addColorStop(0, `${primaryColor}f2`);
9617
+ ribbonGradient.addColorStop(0.5, `${secondaryColor}e6`);
9618
+ ribbonGradient.addColorStop(1, `${tertiaryColor}f2`);
9619
+ context.save();
9620
+ context.beginPath();
9621
+ tracePolyline(context, points);
9622
+ context.strokeStyle = `${shadowColor}82`;
9623
+ context.lineWidth = strokeWidth * 1.8;
9624
+ context.lineJoin = 'round';
9625
+ context.lineCap = 'round';
9626
+ context.filter = `blur(${size * 0.022}px)`;
9627
+ context.stroke();
9628
+ context.restore();
9629
+ context.beginPath();
9630
+ tracePolyline(context, points);
9631
+ context.strokeStyle = ribbonGradient;
9632
+ context.lineWidth = strokeWidth;
9633
+ context.lineJoin = 'round';
9634
+ context.lineCap = 'round';
9635
+ context.stroke();
9636
+ context.beginPath();
9637
+ tracePolyline(context, points);
9638
+ context.strokeStyle = 'rgba(255,255,255,0.22)';
9639
+ context.lineWidth = Math.max(1.2, strokeWidth * 0.28);
9640
+ context.lineJoin = 'round';
9641
+ context.lineCap = 'round';
9642
+ context.stroke();
9643
+ const sparkStride = Math.max(24, Math.floor(points.length / 18));
9644
+ for (let pointIndex = sparkStride; pointIndex < points.length; pointIndex += sparkStride) {
9645
+ const point = points[pointIndex];
9646
+ const pulse = 0.7 + 0.3 * Math.sin(timeMs / 700 + pointIndex * 0.12 + layerIndex);
9647
+ const radius = strokeWidth * (0.24 + pulse * 0.22);
9648
+ context.beginPath();
9649
+ context.arc(point.x, point.y, radius * 1.8, 0, Math.PI * 2);
9650
+ context.fillStyle = `${secondaryColor}20`;
9651
+ context.fill();
9652
+ context.beginPath();
9653
+ context.arc(point.x, point.y, radius, 0, Math.PI * 2);
9654
+ context.fillStyle = tertiaryColor;
9655
+ context.fill();
9656
+ }
9657
+ }
9658
+ /**
9659
+ * Traces a polyline through the provided points.
9660
+ *
9661
+ * @param context Canvas 2D context.
9662
+ * @param points Polyline points.
9663
+ *
9664
+ * @private helper of `fractalAvatarVisual`
9665
+ */
9666
+ function tracePolyline(context, points) {
9667
+ const firstPoint = points[0];
9668
+ context.moveTo(firstPoint.x, firstPoint.y);
9669
+ for (let pointIndex = 1; pointIndex < points.length; pointIndex++) {
9670
+ const point = points[pointIndex];
9671
+ context.lineTo(point.x, point.y);
9672
+ }
9673
+ }
9674
+ /**
9675
+ * Draws the central crystalline accent tying the dragon-curve layers together.
9676
+ *
9677
+ * @param context Canvas 2D context.
9678
+ * @param size Canvas size in CSS pixels.
9679
+ * @param palette Derived avatar palette.
9680
+ * @param timeMs Current animation time in milliseconds.
9681
+ * @param corePhase Seed-based phase offset.
9682
+ *
9683
+ * @private helper of `fractalAvatarVisual`
9684
+ */
9685
+ function drawFractalCore(context, size, palette, timeMs, corePhase) {
9686
+ const centerX = size * 0.5;
9687
+ const centerY = size * 0.5;
9688
+ const radius = size * 0.082;
9689
+ const rotation = corePhase * Math.PI * 2 + timeMs / 2200;
9690
+ const innerRotation = -rotation * 1.35;
9691
+ context.save();
9692
+ context.translate(centerX, centerY);
9693
+ context.rotate(rotation);
9694
+ context.beginPath();
9695
+ for (let pointIndex = 0; pointIndex < 4; pointIndex++) {
9696
+ const angle = (pointIndex / 4) * Math.PI * 2;
9697
+ const x = Math.cos(angle) * radius;
9698
+ const y = Math.sin(angle) * radius;
9699
+ if (pointIndex === 0) {
9700
+ context.moveTo(x, y);
9701
+ }
9702
+ else {
9703
+ context.lineTo(x, y);
9704
+ }
9705
+ }
9706
+ context.closePath();
9707
+ context.fillStyle = `${palette.highlight}88`;
9708
+ context.shadowColor = `${palette.highlight}77`;
9709
+ context.shadowBlur = size * 0.05;
9710
+ context.fill();
9711
+ context.restore();
9712
+ context.save();
9713
+ context.translate(centerX, centerY);
9714
+ context.rotate(innerRotation);
9715
+ context.beginPath();
9716
+ for (let pointIndex = 0; pointIndex < 4; pointIndex++) {
9717
+ const angle = Math.PI / 4 + (pointIndex / 4) * Math.PI * 2;
9718
+ const x = Math.cos(angle) * radius * 0.55;
9719
+ const y = Math.sin(angle) * radius * 0.55;
9720
+ if (pointIndex === 0) {
9721
+ context.moveTo(x, y);
9722
+ }
9723
+ else {
9724
+ context.lineTo(x, y);
9725
+ }
9726
+ }
9727
+ context.closePath();
9728
+ context.fillStyle = `${palette.ink}cc`;
9729
+ context.fill();
9730
+ context.restore();
9731
+ }
9732
+
9733
+ /* eslint-disable no-magic-numbers */
9734
+ /**
9735
+ * Minecraft-style 3D avatar visual.
9736
+ *
9737
+ * @private built-in avatar visual
9738
+ */
9739
+ const minecraftAvatarVisual = {
9740
+ id: 'minecraft',
9741
+ title: 'Minecraft 3D',
9742
+ description: 'Blocky 3D portrait with deterministic pixel textures, shoulders, and hovering depth.',
9743
+ isAnimated: true,
9744
+ render({ context, size, palette, createRandom, timeMs }) {
9745
+ const random = createRandom('minecraft');
9746
+ const bob = Math.sin(timeMs / 880) * size * 0.015;
9747
+ const headSize = size * 0.34;
9748
+ const depth = headSize * 0.22;
9749
+ const headX = size * 0.31;
9750
+ const headY = size * 0.18 + bob;
9751
+ const bodyWidth = headSize * 0.86;
9752
+ const bodyHeight = headSize * 0.82;
9753
+ const bodyDepth = depth * 0.8;
9754
+ const bodyX = size * 0.33;
9755
+ const bodyY = headY + headSize * 0.96;
9756
+ const hasHeadband = random() < 0.5;
9757
+ const faceTexture = createMinecraftFaceTexture(createRandom('minecraft-face'), palette, hasHeadband);
9758
+ const shirtTexture = createMinecraftShirtTexture(createRandom('minecraft-shirt'), palette);
9759
+ drawAvatarFrame(context, size, palette);
9760
+ const spotlight = context.createRadialGradient(size * 0.5, size * 0.18, size * 0.05, size * 0.5, size * 0.18, size * 0.5);
9761
+ spotlight.addColorStop(0, `${palette.highlight}66`);
9762
+ spotlight.addColorStop(1, `${palette.highlight}00`);
9763
+ context.fillStyle = spotlight;
9764
+ context.fillRect(0, 0, size, size);
9765
+ context.save();
9766
+ context.fillStyle = 'rgba(0, 0, 0, 0.22)';
9767
+ context.filter = `blur(${size * 0.018}px)`;
9768
+ context.beginPath();
9769
+ context.ellipse(size * 0.5, size * 0.86, size * 0.2, size * 0.06, 0, 0, Math.PI * 2);
9770
+ context.fill();
9771
+ context.restore();
9772
+ drawVoxelCuboid(context, {
9773
+ x: bodyX,
9774
+ y: bodyY,
9775
+ width: bodyWidth,
9776
+ height: bodyHeight,
9777
+ depth: bodyDepth,
9778
+ frontTexture: shirtTexture,
9779
+ topColor: `${palette.highlight}cc`,
9780
+ sideColor: `${palette.secondary}dd`,
9781
+ outlineColor: `${palette.shadow}aa`,
9782
+ });
9783
+ drawVoxelCuboid(context, {
9784
+ x: headX,
9785
+ y: headY,
9786
+ width: headSize,
9787
+ height: headSize,
9788
+ depth,
9789
+ frontTexture: faceTexture,
9790
+ topColor: `${palette.highlight}ee`,
9791
+ sideColor: `${palette.secondary}ee`,
9792
+ outlineColor: `${palette.shadow}cc`,
9793
+ });
9794
+ },
9795
+ };
9796
+ /**
9797
+ * Draws a stylized voxel cuboid with a front pixel texture.
9798
+ *
9799
+ * @param context Canvas 2D context.
9800
+ * @param cuboid Cuboid settings.
9801
+ *
9802
+ * @private helper of `minecraftAvatarVisual`
9803
+ */
9804
+ function drawVoxelCuboid(context, cuboid) {
9805
+ var _a;
9806
+ const { x, y, width, height, depth, frontTexture, topColor, sideColor, outlineColor } = cuboid;
9807
+ const lift = depth * 0.6;
9808
+ context.save();
9809
+ context.beginPath();
9810
+ context.moveTo(x, y);
9811
+ context.lineTo(x + depth, y - lift);
9812
+ context.lineTo(x + width + depth, y - lift);
9813
+ context.lineTo(x + width, y);
9814
+ context.closePath();
9815
+ context.fillStyle = topColor;
9816
+ context.fill();
9817
+ context.restore();
9818
+ context.save();
9819
+ context.beginPath();
9820
+ context.moveTo(x + width, y);
9821
+ context.lineTo(x + width + depth, y - lift);
9822
+ context.lineTo(x + width + depth, y + height - lift);
9823
+ context.lineTo(x + width, y + height);
9824
+ context.closePath();
9825
+ context.fillStyle = sideColor;
9826
+ context.fill();
9827
+ context.restore();
9828
+ const rows = frontTexture.length;
9829
+ const columns = ((_a = frontTexture[0]) === null || _a === void 0 ? void 0 : _a.length) || 0;
9830
+ const pixelWidth = width / Math.max(columns, 1);
9831
+ const pixelHeight = height / Math.max(rows, 1);
9832
+ context.save();
9833
+ createRoundedRectPath(context, x, y, width, height, width * 0.03);
9834
+ context.clip();
9835
+ for (let rowIndex = 0; rowIndex < rows; rowIndex++) {
9836
+ for (let columnIndex = 0; columnIndex < columns; columnIndex++) {
9837
+ context.fillStyle = frontTexture[rowIndex][columnIndex];
9838
+ context.fillRect(x + columnIndex * pixelWidth, y + rowIndex * pixelHeight, pixelWidth, pixelHeight);
9839
+ }
9840
+ }
9841
+ context.restore();
9842
+ context.strokeStyle = outlineColor;
9843
+ context.lineWidth = Math.max(1.2, width * 0.025);
9844
+ context.beginPath();
9845
+ context.moveTo(x, y);
9846
+ context.lineTo(x + depth, y - lift);
9847
+ context.lineTo(x + width + depth, y - lift);
9848
+ context.lineTo(x + width + depth, y + height - lift);
9849
+ context.lineTo(x + width, y + height);
9850
+ context.lineTo(x, y + height);
9851
+ context.closePath();
9852
+ context.stroke();
9853
+ }
9854
+ /**
9855
+ * Creates the front-face pixel texture for the cube head.
9856
+ *
9857
+ * @param random Seeded random generator.
9858
+ * @param palette Derived avatar palette.
9859
+ * @param hasHeadband Whether the avatar should render a headband row.
9860
+ * @returns 8x8 pixel texture.
9861
+ *
9862
+ * @private helper of `minecraftAvatarVisual`
9863
+ */
9864
+ function createMinecraftFaceTexture(random, palette, hasHeadband) {
9865
+ const texture = Array.from({ length: 8 }, () => Array.from({ length: 8 }, () => palette.highlight));
9866
+ const hairlineColor = random() < 0.5 ? palette.primary : palette.secondary;
9867
+ const cheekColor = random() < 0.5 ? `${palette.accent}bb` : `${palette.secondary}bb`;
9868
+ for (let rowIndex = 0; rowIndex < 2; rowIndex++) {
9869
+ for (let columnIndex = 0; columnIndex < 8; columnIndex++) {
9870
+ texture[rowIndex][columnIndex] = hairlineColor;
9871
+ }
9872
+ }
9873
+ texture[2][0] = hairlineColor;
9874
+ texture[2][7] = hairlineColor;
9875
+ texture[3][0] = hairlineColor;
9876
+ texture[3][7] = hairlineColor;
9877
+ if (hasHeadband) {
9878
+ for (let columnIndex = 0; columnIndex < 8; columnIndex++) {
9879
+ texture[2][columnIndex] = palette.accent;
9880
+ }
9881
+ }
9882
+ texture[3][2] = palette.ink;
9883
+ texture[3][5] = palette.ink;
9884
+ texture[4][2] = '#ffffff';
9885
+ texture[4][5] = '#ffffff';
9886
+ texture[5][1] = cheekColor;
9887
+ texture[5][6] = cheekColor;
9888
+ texture[5][3] = palette.shadow;
9889
+ texture[5][4] = palette.shadow;
9890
+ texture[6][3] = palette.shadow;
9891
+ texture[6][4] = palette.shadow;
9892
+ return texture;
9893
+ }
9894
+ /**
9895
+ * Creates the front-face pixel texture for the torso.
9896
+ *
9897
+ * @param random Seeded random generator.
9898
+ * @param palette Derived avatar palette.
9899
+ * @returns 8x8 torso texture.
9900
+ *
9901
+ * @private helper of `minecraftAvatarVisual`
9902
+ */
9903
+ function createMinecraftShirtTexture(random, palette) {
9904
+ const texture = Array.from({ length: 8 }, () => Array.from({ length: 8 }, () => palette.primary));
9905
+ const stripeColor = random() < 0.5 ? palette.secondary : palette.highlight;
9906
+ for (let rowIndex = 0; rowIndex < 2; rowIndex++) {
9907
+ for (let columnIndex = 0; columnIndex < 8; columnIndex++) {
9908
+ texture[rowIndex][columnIndex] = palette.shadow;
9909
+ }
9910
+ }
9911
+ for (let rowIndex = 2; rowIndex < 8; rowIndex++) {
9912
+ texture[rowIndex][3] = stripeColor;
9913
+ texture[rowIndex][4] = stripeColor;
9914
+ }
9915
+ texture[4][1] = palette.accent;
9916
+ texture[4][6] = palette.accent;
9917
+ texture[5][2] = palette.highlight;
9918
+ texture[5][5] = palette.highlight;
9919
+ return texture;
9920
+ }
9921
+
9922
+ /* eslint-disable no-magic-numbers */
9923
+ /**
9924
+ * Octopus avatar visual.
9925
+ *
9926
+ * @private built-in avatar visual
9927
+ */
9928
+ const octopusAvatarVisual = {
9929
+ id: 'octopus',
9930
+ title: 'Octopus',
9931
+ description: 'Playful underwater mascot with cursor-following eyes, animated tentacles, bubbles, and seeded markings.',
9932
+ isAnimated: true,
9933
+ supportsPointerTracking: true,
9934
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
9935
+ const staticRandom = createRandom('octopus-static');
9936
+ const bubbleRandom = createRandom('octopus-bubbles');
9937
+ const bubbleCount = 8;
9938
+ const bubbleRadiusBase = size * 0.02;
9939
+ const centerX = size * 0.5 + interaction.bodyOffsetX * size * 0.035;
9940
+ const centerY = size * 0.42 + interaction.bodyOffsetY * size * 0.024;
9941
+ const headRadius = size * (0.19 + staticRandom() * 0.03);
9942
+ const mantleHeight = headRadius * 1.18;
9943
+ const tentacleLength = size * (0.18 + staticRandom() * 0.06);
9944
+ const tentaclePhases = Array.from({ length: 8 }, () => staticRandom() * Math.PI * 2);
9945
+ const spotCount = 3 + Math.floor(staticRandom() * 4);
9946
+ const spotColors = [palette.secondary, palette.accent, palette.highlight];
9947
+ drawAvatarFrame(context, size, palette);
9948
+ const waterGlow = context.createRadialGradient(centerX, size * 0.22, size * 0.06, centerX, size * 0.22, size * 0.58);
9949
+ waterGlow.addColorStop(0, `${palette.highlight}66`);
9950
+ waterGlow.addColorStop(1, `${palette.highlight}00`);
9951
+ context.fillStyle = waterGlow;
9952
+ context.fillRect(0, 0, size, size);
9953
+ for (let bubbleIndex = 0; bubbleIndex < bubbleCount; bubbleIndex++) {
9954
+ const x = size * (0.15 + bubbleRandom() * 0.7);
9955
+ const y = size * (0.12 + bubbleRandom() * 0.68);
9956
+ const radius = bubbleRadiusBase * (0.6 + bubbleRandom() * 2.3);
9957
+ context.beginPath();
9958
+ context.arc(x, y, radius, 0, Math.PI * 2);
9959
+ context.fillStyle = 'rgba(255,255,255,0.08)';
9960
+ context.fill();
9961
+ context.beginPath();
9962
+ context.arc(x - radius * 0.22, y - radius * 0.22, radius * 0.25, 0, Math.PI * 2);
9963
+ context.fillStyle = 'rgba(255,255,255,0.28)';
9964
+ context.fill();
9965
+ }
9966
+ for (let tentacleIndex = 0; tentacleIndex < 8; tentacleIndex++) {
9967
+ const startX = centerX + (tentacleIndex - 3.5) * headRadius * 0.19;
9968
+ const startY = centerY + headRadius * 0.62;
9969
+ const animationPhase = tentaclePhases[tentacleIndex];
9970
+ const sway = Math.sin(timeMs / 520 + animationPhase) * size * 0.03;
9971
+ const endX = startX + (tentacleIndex - 3.5) * size * 0.025 + sway;
9972
+ const endY = startY + tentacleLength + Math.cos(timeMs / 700 + animationPhase) * size * 0.01;
9973
+ const controlX = (startX + endX) / 2 + sway * 0.8;
9974
+ const controlY = startY + tentacleLength * 0.45;
9975
+ const lineWidth = size * (0.042 - tentacleIndex * 0.0022);
9976
+ context.beginPath();
9977
+ context.moveTo(startX, startY);
9978
+ context.quadraticCurveTo(controlX, controlY, endX, endY);
9979
+ context.lineCap = 'round';
9980
+ context.strokeStyle = palette.primary;
9981
+ context.lineWidth = lineWidth;
9982
+ context.stroke();
9983
+ for (let cupIndex = 1; cupIndex <= 3; cupIndex++) {
9984
+ const cupT = cupIndex / 4;
9985
+ const cupX = (1 - cupT) * (1 - cupT) * startX + 2 * (1 - cupT) * cupT * controlX + cupT * cupT * endX;
9986
+ const cupY = (1 - cupT) * (1 - cupT) * startY + 2 * (1 - cupT) * cupT * controlY + cupT * cupT * endY;
9987
+ context.beginPath();
9988
+ context.arc(cupX, cupY, lineWidth * 0.18, 0, Math.PI * 2);
9989
+ context.fillStyle = `${palette.highlight}cc`;
9990
+ context.fill();
9991
+ }
9992
+ }
9993
+ context.save();
9994
+ context.fillStyle = palette.primary;
9995
+ context.shadowColor = `${palette.shadow}88`;
9996
+ context.shadowBlur = size * 0.08;
9997
+ context.beginPath();
9998
+ context.ellipse(centerX, centerY, headRadius, mantleHeight, 0, Math.PI, 0, true);
9999
+ context.lineTo(centerX + headRadius, centerY);
10000
+ context.ellipse(centerX, centerY, headRadius, headRadius * 0.82, 0, 0, Math.PI);
10001
+ context.closePath();
10002
+ context.fill();
10003
+ context.restore();
10004
+ context.beginPath();
10005
+ context.ellipse(centerX, centerY - headRadius * 0.22, headRadius * 0.74, headRadius * 0.42, 0, Math.PI, Math.PI * 2);
10006
+ context.fillStyle = `${palette.highlight}55`;
10007
+ context.fill();
10008
+ for (let spotIndex = 0; spotIndex < spotCount; spotIndex++) {
10009
+ const spotRandom = createRandom(`octopus-spot-${spotIndex}`);
10010
+ const spotX = centerX + (spotRandom() - 0.5) * headRadius * 1.1;
10011
+ const spotY = centerY - headRadius * 0.05 + (spotRandom() - 0.5) * headRadius * 0.9;
10012
+ const spotRadius = headRadius * (0.07 + spotRandom() * 0.07);
10013
+ context.beginPath();
10014
+ context.arc(spotX, spotY, spotRadius, 0, Math.PI * 2);
10015
+ context.fillStyle = pickRandomItem(spotColors, spotRandom);
10016
+ context.fill();
10017
+ }
10018
+ const eyeOffsetX = headRadius * 0.42;
10019
+ const eyeY = centerY + headRadius * 0.04;
10020
+ const eyeRadius = headRadius * 0.22;
10021
+ drawEye(context, centerX - eyeOffsetX, eyeY, eyeRadius, palette, timeMs, interaction, 0);
10022
+ drawEye(context, centerX + eyeOffsetX, eyeY, eyeRadius, palette, timeMs, interaction, Math.PI / 5);
10023
+ context.beginPath();
10024
+ context.arc(centerX - headRadius * 0.28, centerY + headRadius * 0.3, headRadius * 0.12, 0, Math.PI * 2);
10025
+ context.arc(centerX + headRadius * 0.28, centerY + headRadius * 0.3, headRadius * 0.12, 0, Math.PI * 2);
10026
+ context.fillStyle = `${palette.accent}44`;
10027
+ context.fill();
10028
+ context.beginPath();
10029
+ context.moveTo(centerX - headRadius * 0.18, centerY + headRadius * 0.24);
10030
+ context.quadraticCurveTo(centerX, centerY + headRadius * 0.42, centerX + headRadius * 0.18, centerY + headRadius * 0.24);
10031
+ context.strokeStyle = palette.shadow;
10032
+ context.lineWidth = size * 0.016;
10033
+ context.lineCap = 'round';
10034
+ context.stroke();
10035
+ },
10036
+ };
10037
+ /**
10038
+ * Draws one expressive octopus eye.
10039
+ *
10040
+ * @param context Canvas 2D context.
10041
+ * @param centerX Eye center X coordinate.
10042
+ * @param centerY Eye center Y coordinate.
10043
+ * @param radius Eye radius.
10044
+ * @param palette Derived avatar palette.
10045
+ * @param timeMs Current animation time in milliseconds.
10046
+ * @param interaction Smoothed avatar interaction state.
10047
+ * @param phase Seed-based phase offset.
10048
+ *
10049
+ * @private helper of `octopusAvatarVisual`
10050
+ */
10051
+ function drawEye(context, centerX, centerY, radius, palette, timeMs, interaction, phase) {
10052
+ const { pupilOffsetX, pupilOffsetY } = resolveOrganicEyeMotion({
10053
+ radiusX: radius,
10054
+ radiusY: radius,
10055
+ timeMs,
10056
+ phase,
10057
+ interaction,
10058
+ autonomousDriftRatioX: 0.05,
10059
+ autonomousDriftRatioY: 0.03,
10060
+ });
10061
+ context.beginPath();
10062
+ context.arc(centerX, centerY, radius, 0, Math.PI * 2);
10063
+ context.fillStyle = '#ffffff';
10064
+ context.fill();
10065
+ context.beginPath();
10066
+ context.arc(centerX + pupilOffsetX, centerY + pupilOffsetY, radius * 0.45, 0, Math.PI * 2);
10067
+ context.fillStyle = palette.ink;
10068
+ context.fill();
10069
+ context.beginPath();
10070
+ context.arc(centerX + pupilOffsetX - radius * 0.12, centerY + pupilOffsetY - radius * 0.12, radius * 0.15, 0, Math.PI * 2);
10071
+ context.fillStyle = '#ffffff';
10072
+ context.fill();
10073
+ context.beginPath();
10074
+ context.arc(centerX, centerY, radius, 0, Math.PI * 2);
10075
+ context.strokeStyle = palette.shadow;
10076
+ context.lineWidth = radius * 0.18;
10077
+ context.stroke();
10078
+ }
10079
+
10080
+ /* eslint-disable no-magic-numbers */
10081
+ /**
10082
+ * Octopus2 avatar visual.
10083
+ *
10084
+ * @private built-in avatar visual
10085
+ */
10086
+ const octopus2AvatarVisual = {
10087
+ id: 'octopus2',
10088
+ title: 'Octopus2',
10089
+ description: 'Organic alien octopus rendered as one continuously morphing blob with responsive luminous eyes.',
10090
+ isAnimated: true,
10091
+ supportsPointerTracking: true,
10092
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
10093
+ const staticRandom = createRandom('octopus2-static');
10094
+ const centerX = size * 0.5 + interaction.bodyOffsetX * size * 0.042;
10095
+ const centerY = size * (0.48 + staticRandom() * 0.03) + interaction.bodyOffsetY * size * 0.028;
10096
+ const bodyRadius = size * (0.25 + staticRandom() * 0.035);
10097
+ const horizontalStretch = 1.04 + staticRandom() * 0.16;
10098
+ const verticalStretch = 0.94 + staticRandom() * 0.12;
10099
+ const mantleLift = size * (0.075 + staticRandom() * 0.025);
10100
+ const lowerDrop = size * (0.05 + staticRandom() * 0.02);
10101
+ const tentacleDepth = size * (0.08 + staticRandom() * 0.03);
10102
+ const wobbleAmplitude = size * (0.014 + staticRandom() * 0.008);
10103
+ const lobeCount = 6 + Math.floor(staticRandom() * 3);
10104
+ const shapePhase = staticRandom() * Math.PI * 2;
10105
+ const bodyPoints = createOrganicOctopusBodyPoints({
10106
+ centerX,
10107
+ centerY,
10108
+ bodyRadius,
10109
+ horizontalStretch,
10110
+ verticalStretch,
10111
+ mantleLift,
10112
+ lowerDrop,
10113
+ tentacleDepth,
10114
+ wobbleAmplitude,
10115
+ lobeCount,
10116
+ shapePhase,
10117
+ timeMs,
10118
+ });
10119
+ drawAvatarFrame(context, size, palette);
10120
+ const hazeGradient = context.createRadialGradient(centerX, size * 0.22, size * 0.05, centerX, centerY, size * 0.6);
10121
+ hazeGradient.addColorStop(0, `${palette.highlight}4d`);
10122
+ hazeGradient.addColorStop(0.45, `${palette.accent}24`);
10123
+ hazeGradient.addColorStop(1, `${palette.highlight}00`);
10124
+ context.fillStyle = hazeGradient;
10125
+ context.fillRect(0, 0, size, size);
10126
+ const rimGlowGradient = context.createRadialGradient(centerX, centerY + size * 0.08, size * 0.14, centerX, centerY, size * 0.5);
10127
+ rimGlowGradient.addColorStop(0, `${palette.secondary}26`);
10128
+ rimGlowGradient.addColorStop(1, `${palette.secondary}00`);
10129
+ context.fillStyle = rimGlowGradient;
10130
+ context.fillRect(0, 0, size, size);
10131
+ context.save();
10132
+ traceSmoothClosedPath(context, bodyPoints);
10133
+ const bodyGradient = context.createRadialGradient(centerX - size * 0.09, centerY - size * 0.18, size * 0.06, centerX, centerY + size * 0.14, size * 0.5);
10134
+ bodyGradient.addColorStop(0, palette.highlight);
10135
+ bodyGradient.addColorStop(0.25, palette.secondary);
10136
+ bodyGradient.addColorStop(0.68, palette.primary);
10137
+ bodyGradient.addColorStop(1, palette.shadow);
10138
+ context.fillStyle = bodyGradient;
10139
+ context.shadowColor = `${palette.shadow}aa`;
10140
+ context.shadowBlur = size * 0.08;
10141
+ context.shadowOffsetY = size * 0.018;
10142
+ context.fill();
10143
+ context.restore();
10144
+ context.save();
10145
+ traceSmoothClosedPath(context, bodyPoints);
10146
+ context.clip();
10147
+ const interiorGlowGradient = context.createLinearGradient(centerX, centerY - size * 0.22, centerX, centerY + size * 0.36);
10148
+ interiorGlowGradient.addColorStop(0, `${palette.highlight}59`);
10149
+ interiorGlowGradient.addColorStop(0.45, `${palette.accent}1a`);
10150
+ interiorGlowGradient.addColorStop(1, `${palette.shadow}00`);
10151
+ context.fillStyle = interiorGlowGradient;
10152
+ context.fillRect(centerX - size * 0.36, centerY - size * 0.34, size * 0.72, size * 0.76);
10153
+ drawInteriorFilaments(context, centerX, centerY, size, palette, timeMs, shapePhase);
10154
+ drawLowerSuckers(context, centerX, centerY, size, palette, createRandom, timeMs);
10155
+ context.restore();
10156
+ context.save();
10157
+ traceSmoothClosedPath(context, bodyPoints);
10158
+ context.strokeStyle = `${palette.highlight}59`;
10159
+ context.lineWidth = size * 0.014;
10160
+ context.stroke();
10161
+ context.restore();
10162
+ const eyeOffsetX = size * 0.13;
10163
+ const eyeCenterY = centerY - size * 0.02;
10164
+ const eyeRadiusX = size * 0.072;
10165
+ const eyeRadiusY = size * 0.086;
10166
+ drawAlienEye(context, centerX - eyeOffsetX, eyeCenterY, eyeRadiusX, eyeRadiusY, palette, timeMs, shapePhase, interaction);
10167
+ drawAlienEye(context, centerX + eyeOffsetX, eyeCenterY, eyeRadiusX, eyeRadiusY, palette, timeMs, shapePhase + Math.PI / 5, interaction);
10168
+ context.beginPath();
10169
+ context.moveTo(centerX - size * 0.08, centerY + size * 0.12);
10170
+ 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);
10171
+ context.strokeStyle = `${palette.ink}b3`;
10172
+ context.lineWidth = size * 0.013;
10173
+ context.lineCap = 'round';
10174
+ context.stroke();
10175
+ context.beginPath();
10176
+ context.ellipse(centerX, centerY - size * 0.13, size * 0.16, size * 0.065, 0, Math.PI, Math.PI * 2);
10177
+ context.fillStyle = `${palette.highlight}33`;
10178
+ context.fill();
10179
+ },
10180
+ };
10181
+ /**
10182
+ * Draws translucent inner filaments clipped inside the main body mesh.
10183
+ *
10184
+ * @param context Canvas 2D context.
10185
+ * @param centerX Body center X coordinate.
10186
+ * @param centerY Body center Y coordinate.
10187
+ * @param size Canvas size in CSS pixels.
10188
+ * @param palette Derived avatar palette.
10189
+ * @param timeMs Current animation time in milliseconds.
10190
+ * @param shapePhase Seed-based phase offset.
10191
+ *
10192
+ * @private helper of `octopus2AvatarVisual`
10193
+ */
10194
+ function drawInteriorFilaments(context, centerX, centerY, size, palette, timeMs, shapePhase) {
10195
+ for (let filamentIndex = 0; filamentIndex < 5; filamentIndex++) {
10196
+ const horizontalOffset = (filamentIndex - 2) * size * 0.075;
10197
+ const sway = Math.sin(timeMs / 720 + filamentIndex * 0.8 + shapePhase) * size * 0.028;
10198
+ context.beginPath();
10199
+ context.moveTo(centerX + horizontalOffset * 0.35, centerY - size * 0.11);
10200
+ 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);
10201
+ context.strokeStyle = filamentIndex % 2 === 0 ? `${palette.highlight}29` : `${palette.accent}24`;
10202
+ context.lineWidth = size * (0.01 + filamentIndex * 0.0008);
10203
+ context.lineCap = 'round';
10204
+ context.stroke();
10205
+ }
10206
+ }
10207
+ /**
10208
+ * Draws soft sucker-like highlights in the lower body area.
10209
+ *
10210
+ * @param context Canvas 2D context.
10211
+ * @param centerX Body center X coordinate.
10212
+ * @param centerY Body center Y coordinate.
10213
+ * @param size Canvas size in CSS pixels.
10214
+ * @param palette Derived avatar palette.
10215
+ * @param createRandom Seeded random factory scoped to the avatar.
10216
+ * @param timeMs Current animation time in milliseconds.
10217
+ *
10218
+ * @private helper of `octopus2AvatarVisual`
10219
+ */
10220
+ function drawLowerSuckers(context, centerX, centerY, size, palette, createRandom, timeMs) {
10221
+ const suckerRandom = createRandom('octopus2-suckers');
10222
+ for (let suckerIndex = 0; suckerIndex < 12; suckerIndex++) {
10223
+ const x = centerX + (suckerRandom() - 0.5) * size * 0.36;
10224
+ const y = centerY + size * (0.11 + suckerRandom() * 0.22);
10225
+ const radiusX = size * (0.015 + suckerRandom() * 0.012);
10226
+ const radiusY = radiusX * (0.72 + suckerRandom() * 0.34);
10227
+ const rotation = suckerRandom() * Math.PI + Math.sin(timeMs / 1100 + suckerIndex) * 0.08;
10228
+ context.beginPath();
10229
+ context.ellipse(x, y, radiusX, radiusY, rotation, 0, Math.PI * 2);
10230
+ context.fillStyle = `${palette.highlight}24`;
10231
+ context.fill();
10232
+ context.strokeStyle = `${palette.highlight}40`;
10233
+ context.lineWidth = Math.max(1, size * 0.005);
10234
+ context.stroke();
10235
+ }
10236
+ }
10237
+ /**
10238
+ * Draws one luminous alien eye on top of the organic octopus mesh.
10239
+ *
10240
+ * @param context Canvas 2D context.
10241
+ * @param centerX Eye center X coordinate.
10242
+ * @param centerY Eye center Y coordinate.
10243
+ * @param radiusX Eye horizontal radius.
10244
+ * @param radiusY Eye vertical radius.
10245
+ * @param palette Derived avatar palette.
10246
+ * @param timeMs Current animation time in milliseconds.
10247
+ * @param phase Seed-based animation phase.
10248
+ * @param interaction Smoothed avatar interaction state.
10249
+ *
10250
+ * @private helper of `octopus2AvatarVisual`
10251
+ */
10252
+ function drawAlienEye(context, centerX, centerY, radiusX, radiusY, palette, timeMs, phase, interaction) {
10253
+ const { pupilOffsetX, pupilOffsetY } = resolveOrganicEyeMotion({
10254
+ radiusX,
10255
+ radiusY,
10256
+ timeMs,
10257
+ phase,
10258
+ interaction,
10259
+ autonomousDriftRatioY: 0.1,
10260
+ });
10261
+ context.save();
10262
+ context.beginPath();
10263
+ context.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
10264
+ context.fillStyle = '#f8fbff';
10265
+ context.fill();
10266
+ context.clip();
10267
+ const irisGradient = context.createRadialGradient(centerX - radiusX * 0.18, centerY - radiusY * 0.22, radiusX * 0.05, centerX, centerY, radiusX * 0.9);
10268
+ irisGradient.addColorStop(0, palette.highlight);
10269
+ irisGradient.addColorStop(0.55, palette.secondary);
10270
+ irisGradient.addColorStop(1, palette.shadow);
10271
+ context.beginPath();
10272
+ context.ellipse(centerX + pupilOffsetX, centerY + pupilOffsetY, radiusX * 0.68, radiusY * 0.72, 0, 0, Math.PI * 2);
10273
+ context.fillStyle = irisGradient;
10274
+ context.fill();
10275
+ context.beginPath();
10276
+ context.ellipse(centerX + pupilOffsetX, centerY + pupilOffsetY, radiusX * 0.16, radiusY * 0.48, 0, 0, Math.PI * 2);
10277
+ context.fillStyle = palette.ink;
10278
+ context.fill();
10279
+ context.beginPath();
10280
+ context.ellipse(centerX + pupilOffsetX - radiusX * 0.18, centerY + pupilOffsetY - radiusY * 0.24, radiusX * 0.12, radiusY * 0.14, 0, 0, Math.PI * 2);
10281
+ context.fillStyle = '#ffffff';
10282
+ context.fill();
10283
+ context.restore();
10284
+ context.beginPath();
10285
+ context.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
10286
+ context.strokeStyle = `${palette.shadow}cc`;
10287
+ context.lineWidth = radiusX * 0.2;
10288
+ context.stroke();
10289
+ }
10290
+
10291
+ /* eslint-disable no-magic-numbers */
10292
+ /**
10293
+ * Builds one deterministic morphology profile for `Octopus3`.
10294
+ *
10295
+ * @param createRandom Seeded random factory scoped to the current avatar.
10296
+ * @returns Stable morphology profile.
10297
+ *
10298
+ * @private helper of `octopus3AvatarVisual`
10299
+ */
10300
+ function createOctopus3MorphologyProfile(createRandom) {
10301
+ const bodyRandom = createRandom('octopus3-body-profile');
10302
+ const faceRandom = createRandom('octopus3-face-profile');
10303
+ const detailRandom = createRandom('octopus3-detail-profile');
10304
+ const bodyFamilyRoll = bodyRandom();
10305
+ let bodyFamily;
10306
+ let body;
10307
+ let tentacles;
10308
+ if (bodyFamilyRoll < 0.34) {
10309
+ bodyFamily = 'lantern';
10310
+ body = {
10311
+ centerXJitterRatio: resolveSeededRange(bodyRandom, -0.018, 0.018),
10312
+ centerYRatio: resolveSeededRange(bodyRandom, 0.39, 0.435),
10313
+ bodyRadiusRatio: resolveSeededRange(bodyRandom, 0.19, 0.23),
10314
+ horizontalStretch: resolveSeededRange(bodyRandom, 0.94, 1.08),
10315
+ verticalStretch: resolveSeededRange(bodyRandom, 1.02, 1.18),
10316
+ mantleLiftRatio: resolveSeededRange(bodyRandom, 0.115, 0.148),
10317
+ lowerDropRatio: resolveSeededRange(bodyRandom, 0.042, 0.066),
10318
+ tentacleDepthRatio: resolveSeededRange(bodyRandom, 0.018, 0.03),
10319
+ wobbleAmplitudeRatio: resolveSeededRange(bodyRandom, 0.009, 0.017),
10320
+ lobeCount: resolveSeededIntegerRange(bodyRandom, 4, 6),
10321
+ pointCount: resolveSeededIntegerRange(bodyRandom, 38, 42),
10322
+ shadowWidthRatio: resolveSeededRange(bodyRandom, 0.18, 0.23),
10323
+ shadowHeightRatio: resolveSeededRange(bodyRandom, 0.055, 0.075),
10324
+ crownHighlightWidthRatio: resolveSeededRange(bodyRandom, 0.14, 0.18),
10325
+ crownHighlightHeightRatio: resolveSeededRange(bodyRandom, 0.045, 0.062),
10326
+ crownHighlightYOffsetRatio: resolveSeededRange(bodyRandom, -0.165, -0.135),
10327
+ };
10328
+ tentacles = {
10329
+ count: resolveSeededIntegerRange(bodyRandom, 7, 10),
10330
+ flowLengthScale: resolveSeededRange(bodyRandom, 1.08, 1.34),
10331
+ lateralReachScale: resolveSeededRange(bodyRandom, 0.72, 0.94),
10332
+ tipReachScale: resolveSeededRange(bodyRandom, 0.82, 1.06),
10333
+ baseWidthScale: resolveSeededRange(bodyRandom, 0.82, 0.98),
10334
+ tipWidthScale: resolveSeededRange(bodyRandom, 0.9, 1.08),
10335
+ rootSpreadScale: resolveSeededRange(bodyRandom, 0.76, 0.94),
10336
+ startYOffsetScale: resolveSeededRange(bodyRandom, 0.82, 1),
10337
+ swayScale: resolveSeededRange(bodyRandom, 0.82, 1.02),
10338
+ };
10339
+ }
10340
+ else if (bodyFamilyRoll < 0.68) {
10341
+ bodyFamily = 'drifter';
10342
+ body = {
10343
+ centerXJitterRatio: resolveSeededRange(bodyRandom, -0.025, 0.025),
10344
+ centerYRatio: resolveSeededRange(bodyRandom, 0.425, 0.46),
10345
+ bodyRadiusRatio: resolveSeededRange(bodyRandom, 0.175, 0.215),
10346
+ horizontalStretch: resolveSeededRange(bodyRandom, 1.22, 1.42),
10347
+ verticalStretch: resolveSeededRange(bodyRandom, 0.82, 0.92),
10348
+ mantleLiftRatio: resolveSeededRange(bodyRandom, 0.092, 0.115),
10349
+ lowerDropRatio: resolveSeededRange(bodyRandom, 0.02, 0.036),
10350
+ tentacleDepthRatio: resolveSeededRange(bodyRandom, 0.032, 0.052),
10351
+ wobbleAmplitudeRatio: resolveSeededRange(bodyRandom, 0.013, 0.022),
10352
+ lobeCount: resolveSeededIntegerRange(bodyRandom, 7, 9),
10353
+ pointCount: resolveSeededIntegerRange(bodyRandom, 40, 46),
10354
+ shadowWidthRatio: resolveSeededRange(bodyRandom, 0.24, 0.28),
10355
+ shadowHeightRatio: resolveSeededRange(bodyRandom, 0.06, 0.082),
10356
+ crownHighlightWidthRatio: resolveSeededRange(bodyRandom, 0.17, 0.22),
10357
+ crownHighlightHeightRatio: resolveSeededRange(bodyRandom, 0.038, 0.055),
10358
+ crownHighlightYOffsetRatio: resolveSeededRange(bodyRandom, -0.14, -0.11),
10359
+ };
10360
+ tentacles = {
10361
+ count: resolveSeededIntegerRange(bodyRandom, 10, 13),
10362
+ flowLengthScale: resolveSeededRange(bodyRandom, 0.88, 1.08),
10363
+ lateralReachScale: resolveSeededRange(bodyRandom, 1.18, 1.42),
10364
+ tipReachScale: resolveSeededRange(bodyRandom, 1.12, 1.42),
10365
+ baseWidthScale: resolveSeededRange(bodyRandom, 0.9, 1.06),
10366
+ tipWidthScale: resolveSeededRange(bodyRandom, 0.88, 1.08),
10367
+ rootSpreadScale: resolveSeededRange(bodyRandom, 1.12, 1.32),
10368
+ startYOffsetScale: resolveSeededRange(bodyRandom, 0.92, 1.14),
10369
+ swayScale: resolveSeededRange(bodyRandom, 1.04, 1.22),
10370
+ };
10371
+ }
10372
+ else {
10373
+ bodyFamily = 'rounded';
10374
+ body = {
10375
+ centerXJitterRatio: resolveSeededRange(bodyRandom, -0.02, 0.02),
10376
+ centerYRatio: resolveSeededRange(bodyRandom, 0.398, 0.442),
10377
+ bodyRadiusRatio: resolveSeededRange(bodyRandom, 0.208, 0.248),
10378
+ horizontalStretch: resolveSeededRange(bodyRandom, 1.06, 1.22),
10379
+ verticalStretch: resolveSeededRange(bodyRandom, 0.9, 1.01),
10380
+ mantleLiftRatio: resolveSeededRange(bodyRandom, 0.1, 0.128),
10381
+ lowerDropRatio: resolveSeededRange(bodyRandom, 0.032, 0.052),
10382
+ tentacleDepthRatio: resolveSeededRange(bodyRandom, 0.038, 0.06),
10383
+ wobbleAmplitudeRatio: resolveSeededRange(bodyRandom, 0.014, 0.024),
10384
+ lobeCount: resolveSeededIntegerRange(bodyRandom, 5, 8),
10385
+ pointCount: resolveSeededIntegerRange(bodyRandom, 39, 44),
10386
+ shadowWidthRatio: resolveSeededRange(bodyRandom, 0.2, 0.25),
10387
+ shadowHeightRatio: resolveSeededRange(bodyRandom, 0.055, 0.08),
10388
+ crownHighlightWidthRatio: resolveSeededRange(bodyRandom, 0.16, 0.2),
10389
+ crownHighlightHeightRatio: resolveSeededRange(bodyRandom, 0.05, 0.07),
10390
+ crownHighlightYOffsetRatio: resolveSeededRange(bodyRandom, -0.155, -0.122),
10391
+ };
10392
+ tentacles = {
10393
+ count: resolveSeededIntegerRange(bodyRandom, 8, 12),
10394
+ flowLengthScale: resolveSeededRange(bodyRandom, 0.94, 1.16),
10395
+ lateralReachScale: resolveSeededRange(bodyRandom, 0.9, 1.14),
10396
+ tipReachScale: resolveSeededRange(bodyRandom, 0.96, 1.22),
10397
+ baseWidthScale: resolveSeededRange(bodyRandom, 1.02, 1.2),
10398
+ tipWidthScale: resolveSeededRange(bodyRandom, 1.02, 1.22),
10399
+ rootSpreadScale: resolveSeededRange(bodyRandom, 0.94, 1.08),
10400
+ startYOffsetScale: resolveSeededRange(bodyRandom, 0.9, 1.08),
10401
+ swayScale: resolveSeededRange(bodyRandom, 0.9, 1.1),
10402
+ };
10403
+ }
10404
+ const faceFamilyRoll = faceRandom();
10405
+ let faceFamily;
10406
+ let face;
10407
+ if (faceFamilyRoll < 0.34) {
10408
+ faceFamily = 'watchful';
10409
+ face = {
10410
+ eyeSpacingRatio: resolveSeededRange(faceRandom, 0.118, 0.152),
10411
+ eyeCenterYOffsetRatio: resolveSeededRange(faceRandom, -0.026, -0.002),
10412
+ eyeRadiusXRatio: resolveSeededRange(faceRandom, 0.05, 0.062),
10413
+ eyeHeightRatio: resolveSeededRange(faceRandom, 1.18, 1.38),
10414
+ eyeRotationRange: resolveSeededRange(faceRandom, 0.16, 0.28),
10415
+ eyeTiltBias: resolveSeededRange(faceRandom, 0.02, 0.06),
10416
+ mouthWidthRatio: resolveSeededRange(faceRandom, 0.058, 0.074),
10417
+ mouthYOffsetRatio: resolveSeededRange(faceRandom, 0.086, 0.104),
10418
+ mouthCurveDepthRatio: resolveSeededRange(faceRandom, 0.126, 0.15),
10419
+ mouthCenterOffsetRatio: resolveSeededRange(faceRandom, -0.006, 0.006),
10420
+ mouthCornerTiltRatio: resolveSeededRange(faceRandom, -0.002, 0.002),
10421
+ eyeStyle: {
10422
+ irisScale: resolveSeededRange(faceRandom, 1, 1.1),
10423
+ pupilWidthScale: resolveSeededRange(faceRandom, 0.86, 1.02),
10424
+ pupilHeightScale: resolveSeededRange(faceRandom, 0.94, 1.08),
10425
+ upperLidArchRatio: resolveSeededRange(faceRandom, 0.96, 1.12),
10426
+ upperLidInsetRatio: resolveSeededRange(faceRandom, 0.08, 0.14),
10427
+ lowerLidOpacity: resolveSeededRange(faceRandom, 0.12, 0.22),
10428
+ },
10429
+ };
10430
+ }
10431
+ else if (faceFamilyRoll < 0.68) {
10432
+ faceFamily = 'sleepy';
10433
+ face = {
10434
+ eyeSpacingRatio: resolveSeededRange(faceRandom, 0.092, 0.124),
10435
+ eyeCenterYOffsetRatio: resolveSeededRange(faceRandom, -0.002, 0.024),
10436
+ eyeRadiusXRatio: resolveSeededRange(faceRandom, 0.058, 0.074),
10437
+ eyeHeightRatio: resolveSeededRange(faceRandom, 0.96, 1.14),
10438
+ eyeRotationRange: resolveSeededRange(faceRandom, 0.1, 0.22),
10439
+ eyeTiltBias: resolveSeededRange(faceRandom, 0.01, 0.05),
10440
+ mouthWidthRatio: resolveSeededRange(faceRandom, 0.066, 0.086),
10441
+ mouthYOffsetRatio: resolveSeededRange(faceRandom, 0.094, 0.118),
10442
+ mouthCurveDepthRatio: resolveSeededRange(faceRandom, 0.118, 0.145),
10443
+ mouthCenterOffsetRatio: resolveSeededRange(faceRandom, -0.004, 0.004),
10444
+ mouthCornerTiltRatio: resolveSeededRange(faceRandom, -0.004, 0.004),
10445
+ eyeStyle: {
10446
+ irisScale: resolveSeededRange(faceRandom, 0.9, 1),
10447
+ pupilWidthScale: resolveSeededRange(faceRandom, 1, 1.18),
10448
+ pupilHeightScale: resolveSeededRange(faceRandom, 0.78, 0.92),
10449
+ upperLidArchRatio: resolveSeededRange(faceRandom, 0.7, 0.88),
10450
+ upperLidInsetRatio: resolveSeededRange(faceRandom, -0.02, 0.06),
10451
+ lowerLidOpacity: resolveSeededRange(faceRandom, 0.22, 0.34),
10452
+ },
10453
+ };
10454
+ }
10455
+ else {
10456
+ faceFamily = 'mischief';
10457
+ face = {
10458
+ eyeSpacingRatio: resolveSeededRange(faceRandom, 0.086, 0.114),
10459
+ eyeCenterYOffsetRatio: resolveSeededRange(faceRandom, -0.018, 0.01),
10460
+ eyeRadiusXRatio: resolveSeededRange(faceRandom, 0.046, 0.06),
10461
+ eyeHeightRatio: resolveSeededRange(faceRandom, 1.08, 1.28),
10462
+ eyeRotationRange: resolveSeededRange(faceRandom, 0.28, 0.44),
10463
+ eyeTiltBias: resolveSeededRange(faceRandom, 0.12, 0.22),
10464
+ mouthWidthRatio: resolveSeededRange(faceRandom, 0.052, 0.074),
10465
+ mouthYOffsetRatio: resolveSeededRange(faceRandom, 0.082, 0.1),
10466
+ mouthCurveDepthRatio: resolveSeededRange(faceRandom, 0.116, 0.15),
10467
+ mouthCenterOffsetRatio: resolveSeededRange(faceRandom, -0.018, 0.018),
10468
+ mouthCornerTiltRatio: resolveSeededRange(faceRandom, -0.01, 0.01),
10469
+ eyeStyle: {
10470
+ irisScale: resolveSeededRange(faceRandom, 1.04, 1.12),
10471
+ pupilWidthScale: resolveSeededRange(faceRandom, 0.72, 0.9),
10472
+ pupilHeightScale: resolveSeededRange(faceRandom, 0.96, 1.14),
10473
+ upperLidArchRatio: resolveSeededRange(faceRandom, 0.88, 1.02),
10474
+ upperLidInsetRatio: resolveSeededRange(faceRandom, 0.04, 0.12),
10475
+ lowerLidOpacity: resolveSeededRange(faceRandom, 0.08, 0.18),
10476
+ },
10477
+ };
10478
+ }
10479
+ return {
10480
+ bodyFamily,
10481
+ faceFamily,
10482
+ body,
10483
+ tentacles,
10484
+ face,
10485
+ details: {
10486
+ mantleCurrentCount: resolveSeededIntegerRange(detailRandom, 4, 8),
10487
+ mantleNodeCount: resolveSeededIntegerRange(detailRandom, 3, 7),
10488
+ },
10489
+ };
10490
+ }
10491
+ /**
10492
+ * Resolves one seeded floating-point number inside the provided range.
10493
+ *
10494
+ * @param random Seeded random generator.
10495
+ * @param minimumValue Inclusive lower bound.
10496
+ * @param maximumValue Inclusive upper bound.
10497
+ * @returns Seeded number within the range.
10498
+ *
10499
+ * @private helper of `octopus3AvatarVisual`
10500
+ */
10501
+ function resolveSeededRange(random, minimumValue, maximumValue) {
10502
+ return minimumValue + random() * (maximumValue - minimumValue);
10503
+ }
10504
+ /**
10505
+ * Resolves one seeded integer inside the provided inclusive range.
10506
+ *
10507
+ * @param random Seeded random generator.
10508
+ * @param minimumValue Inclusive lower bound.
10509
+ * @param maximumValue Inclusive upper bound.
10510
+ * @returns Seeded integer within the range.
10511
+ *
10512
+ * @private helper of `octopus3AvatarVisual`
10513
+ */
10514
+ function resolveSeededIntegerRange(random, minimumValue, maximumValue) {
10515
+ return minimumValue + Math.floor(random() * (maximumValue - minimumValue + 1));
10516
+ }
10517
+ /**
10518
+ * Converts an opacity ratio into a two-digit hexadecimal alpha suffix.
10519
+ *
10520
+ * @param opacity Opacity ratio in the range `[0, 1]`.
10521
+ * @returns Two-digit hexadecimal alpha string.
10522
+ *
10523
+ * @private helper of `octopus3AvatarVisual`
10524
+ */
10525
+ function formatAlphaHex(opacity) {
10526
+ return Math.round(Math.min(1, Math.max(0, opacity)) * 255)
10527
+ .toString(16)
10528
+ .padStart(2, '0');
10529
+ }
10530
+ /**
10531
+ * Octopus3 avatar visual.
10532
+ *
10533
+ * @private built-in avatar visual
10534
+ */
10535
+ const octopus3AvatarVisual = {
10536
+ id: 'octopus3',
10537
+ title: 'Octopus3',
10538
+ description: 'Gelatinous alien octopus with a morphing mantle, responsive eyes, and visible ribbon tentacles.',
10539
+ isAnimated: true,
10540
+ supportsPointerTracking: true,
10541
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
10542
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
10543
+ const animationRandom = createRandom('octopus3-animation-profile');
10544
+ const eyeRandom = createRandom('octopus3-eye-profile');
10545
+ const centerX = size * (0.5 + morphologyProfile.body.centerXJitterRatio) + interaction.bodyOffsetX * size * 0.05;
10546
+ const centerY = size * morphologyProfile.body.centerYRatio + interaction.bodyOffsetY * size * 0.035;
10547
+ const bodyRadius = size * morphologyProfile.body.bodyRadiusRatio;
10548
+ const horizontalStretch = morphologyProfile.body.horizontalStretch;
10549
+ const verticalStretch = morphologyProfile.body.verticalStretch;
10550
+ const mantleLift = size * morphologyProfile.body.mantleLiftRatio;
10551
+ const lowerDrop = size * morphologyProfile.body.lowerDropRatio;
10552
+ const tentacleDepth = size * morphologyProfile.body.tentacleDepthRatio;
10553
+ const wobbleAmplitude = size * morphologyProfile.body.wobbleAmplitudeRatio;
10554
+ const lobeCount = morphologyProfile.body.lobeCount;
10555
+ const shapePhase = animationRandom() * Math.PI * 2;
10556
+ const eyeSpacing = size * morphologyProfile.face.eyeSpacingRatio;
10557
+ const eyeCenterY = centerY + size * morphologyProfile.face.eyeCenterYOffsetRatio;
10558
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio;
10559
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio;
10560
+ const bodyPoints = createOrganicOctopusBodyPoints({
10561
+ centerX,
10562
+ centerY,
10563
+ bodyRadius,
10564
+ horizontalStretch,
10565
+ verticalStretch,
10566
+ mantleLift,
10567
+ lowerDrop,
10568
+ tentacleDepth,
10569
+ wobbleAmplitude,
10570
+ lobeCount,
10571
+ shapePhase,
10572
+ timeMs,
10573
+ pointCount: morphologyProfile.body.pointCount,
10574
+ });
10575
+ const tentacleShapes = createOrganicOctopusTentacleShapes({
10576
+ size,
10577
+ centerX,
10578
+ centerY,
10579
+ bodyRadius,
10580
+ horizontalStretch,
10581
+ tentacleCount: morphologyProfile.tentacles.count,
10582
+ shapePhase,
10583
+ createRandom,
10584
+ timeMs,
10585
+ saltPrefix: 'octopus3',
10586
+ bodyPoints,
10587
+ variation: {
10588
+ flowLengthScale: morphologyProfile.tentacles.flowLengthScale,
10589
+ lateralReachScale: morphologyProfile.tentacles.lateralReachScale,
10590
+ tipReachScale: morphologyProfile.tentacles.tipReachScale,
10591
+ baseWidthScale: morphologyProfile.tentacles.baseWidthScale,
10592
+ tipWidthScale: morphologyProfile.tentacles.tipWidthScale,
10593
+ rootSpreadScale: morphologyProfile.tentacles.rootSpreadScale,
10594
+ startYOffsetScale: morphologyProfile.tentacles.startYOffsetScale,
10595
+ swayScale: morphologyProfile.tentacles.swayScale,
10596
+ },
10597
+ });
10598
+ drawAvatarFrame(context, size, palette);
10599
+ drawOctopus3Atmosphere(context, size, palette, centerX, centerY, timeMs, shapePhase, morphologyProfile);
10600
+ context.beginPath();
10601
+ context.ellipse(centerX, centerY + size * 0.25, size * morphologyProfile.body.shadowWidthRatio, size * morphologyProfile.body.shadowHeightRatio, 0, 0, Math.PI * 2);
10602
+ context.fillStyle = `${palette.shadow}33`;
10603
+ context.fill();
10604
+ for (const tentacleShape of tentacleShapes) {
10605
+ drawTentacleRibbon(context, tentacleShape, palette);
10606
+ }
10607
+ context.save();
10608
+ traceSmoothClosedPath(context, bodyPoints);
10609
+ const bodyGradient = context.createRadialGradient(centerX - size * 0.1, centerY - size * 0.18, size * 0.04, centerX, centerY + size * 0.16, size * 0.54);
10610
+ bodyGradient.addColorStop(0, palette.highlight);
10611
+ bodyGradient.addColorStop(0.18, palette.secondary);
10612
+ bodyGradient.addColorStop(0.55, palette.primary);
10613
+ bodyGradient.addColorStop(1, palette.shadow);
10614
+ context.fillStyle = bodyGradient;
10615
+ context.shadowColor = `${palette.shadow}aa`;
10616
+ context.shadowBlur = size * 0.08;
10617
+ context.shadowOffsetY = size * 0.02;
10618
+ context.fill();
10619
+ context.restore();
10620
+ context.save();
10621
+ traceSmoothClosedPath(context, bodyPoints);
10622
+ context.clip();
10623
+ const innerGlowGradient = context.createLinearGradient(centerX, centerY - size * 0.24, centerX, centerY + size * 0.26);
10624
+ innerGlowGradient.addColorStop(0, `${palette.highlight}66`);
10625
+ innerGlowGradient.addColorStop(0.4, `${palette.secondary}26`);
10626
+ innerGlowGradient.addColorStop(1, `${palette.shadow}00`);
10627
+ context.fillStyle = innerGlowGradient;
10628
+ context.fillRect(centerX - size * 0.36, centerY - size * 0.34, size * 0.72, size * 0.72);
10629
+ drawMantleCurrents(context, centerX, centerY, size, palette, timeMs, shapePhase, morphologyProfile);
10630
+ drawMantleNodes(context, centerX, centerY, size, palette, createRandom, morphologyProfile);
10631
+ context.restore();
10632
+ context.save();
10633
+ traceSmoothClosedPath(context, bodyPoints);
10634
+ context.strokeStyle = `${palette.highlight}73`;
10635
+ context.lineWidth = size * 0.013;
10636
+ context.stroke();
10637
+ context.restore();
10638
+ context.beginPath();
10639
+ context.ellipse(centerX, centerY + size * morphologyProfile.body.crownHighlightYOffsetRatio, size * morphologyProfile.body.crownHighlightWidthRatio, size * morphologyProfile.body.crownHighlightHeightRatio, 0, Math.PI, Math.PI * 2);
10640
+ context.fillStyle = `${palette.highlight}3d`;
10641
+ context.fill();
10642
+ drawSeededEye(context, centerX - eyeSpacing, eyeCenterY, eyeRadiusX, eyeRadiusY, -morphologyProfile.face.eyeTiltBias + (eyeRandom() - 0.5) * morphologyProfile.face.eyeRotationRange, palette, timeMs, shapePhase, interaction, morphologyProfile.face.eyeStyle);
10643
+ 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);
10644
+ const mouthHalfWidth = size * morphologyProfile.face.mouthWidthRatio;
10645
+ const mouthY = centerY + size * morphologyProfile.face.mouthYOffsetRatio;
10646
+ const mouthCornerTilt = size * morphologyProfile.face.mouthCornerTiltRatio;
10647
+ context.beginPath();
10648
+ context.moveTo(centerX - mouthHalfWidth, mouthY - mouthCornerTilt);
10649
+ context.quadraticCurveTo(centerX + size * morphologyProfile.face.mouthCenterOffsetRatio, centerY +
10650
+ size * (morphologyProfile.face.mouthCurveDepthRatio + Math.sin(timeMs / 620 + shapePhase) * 0.016) +
10651
+ interaction.gazeY * size * 0.012, centerX + mouthHalfWidth, mouthY + mouthCornerTilt);
10652
+ context.strokeStyle = `${palette.ink}b3`;
10653
+ context.lineWidth = size * 0.012;
10654
+ context.lineCap = 'round';
10655
+ context.stroke();
10656
+ },
10657
+ };
10658
+ /**
10659
+ * Draws the deep-sea glow around the Octopus3 silhouette.
10660
+ *
10661
+ * @param context Canvas 2D context.
10662
+ * @param size Canvas size in CSS pixels.
10663
+ * @param palette Derived avatar palette.
10664
+ * @param centerX Body center X coordinate.
10665
+ * @param centerY Body center Y coordinate.
10666
+ * @param timeMs Current animation time in milliseconds.
10667
+ * @param shapePhase Seed-based phase offset.
10668
+ * @param morphologyProfile Seeded morphology profile.
10669
+ *
10670
+ * @private helper of `octopus3AvatarVisual`
10671
+ */
10672
+ function drawOctopus3Atmosphere(context, size, palette, centerX, centerY, timeMs, shapePhase, morphologyProfile) {
10673
+ 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));
10674
+ haloGradient.addColorStop(0, `${palette.highlight}5c`);
10675
+ haloGradient.addColorStop(0.35, `${palette.accent}26`);
10676
+ haloGradient.addColorStop(1, `${palette.highlight}00`);
10677
+ context.fillStyle = haloGradient;
10678
+ context.fillRect(0, 0, size, size);
10679
+ 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));
10680
+ lowerGlowGradient.addColorStop(0, `${palette.secondary}1f`);
10681
+ lowerGlowGradient.addColorStop(1, `${palette.secondary}00`);
10682
+ context.fillStyle = lowerGlowGradient;
10683
+ context.fillRect(0, 0, size, size);
10684
+ }
10685
+ /**
10686
+ * Draws one ribbon tentacle with a filled organic profile and visible sucker highlights.
10687
+ *
10688
+ * @param context Canvas 2D context.
10689
+ * @param tentacleShape Deterministic tentacle descriptor.
10690
+ * @param palette Derived avatar palette.
10691
+ *
10692
+ * @private helper of `octopus3AvatarVisual`
10693
+ */
10694
+ function drawTentacleRibbon(context, tentacleShape, palette) {
10695
+ const ribbonPoints = sampleOrganicTentacleRibbonPoints(tentacleShape);
10696
+ const gradient = context.createLinearGradient(tentacleShape.startPoint.x, tentacleShape.startPoint.y, tentacleShape.endPoint.x, tentacleShape.endPoint.y);
10697
+ gradient.addColorStop(0, tentacleShape.colorBias < 0.35 ? palette.secondary : palette.primary);
10698
+ gradient.addColorStop(0.58, palette.primary);
10699
+ gradient.addColorStop(1, tentacleShape.colorBias > 0.65 ? palette.highlight : palette.accent);
10700
+ context.save();
10701
+ traceTentacleRibbonPath(context, ribbonPoints);
10702
+ context.fillStyle = gradient;
10703
+ context.shadowColor = `${palette.shadow}80`;
10704
+ context.shadowBlur = tentacleShape.baseWidth * 1.2;
10705
+ context.shadowOffsetY = tentacleShape.baseWidth * 0.2;
10706
+ context.fill();
10707
+ context.restore();
10708
+ context.save();
10709
+ traceTentacleRibbonPath(context, ribbonPoints);
10710
+ context.strokeStyle = tentacleShape.highlightBias > 0.5 ? `${palette.highlight}52` : `${palette.highlight}38`;
10711
+ context.lineWidth = Math.max(1, tentacleShape.baseWidth * 0.12);
10712
+ context.stroke();
10713
+ context.restore();
10714
+ context.beginPath();
10715
+ context.moveTo(tentacleShape.startPoint.x, tentacleShape.startPoint.y);
10716
+ context.bezierCurveTo(tentacleShape.controlPointOne.x, tentacleShape.controlPointOne.y, tentacleShape.controlPointTwo.x, tentacleShape.controlPointTwo.y, tentacleShape.endPoint.x, tentacleShape.endPoint.y);
10717
+ context.strokeStyle = `${palette.highlight}2e`;
10718
+ context.lineWidth = Math.max(1, tentacleShape.tipWidth * 0.9);
10719
+ context.lineCap = 'round';
10720
+ context.stroke();
10721
+ drawTentacleSuckers(context, tentacleShape, palette);
10722
+ }
10723
+ /**
10724
+ * Traces a closed ribbon path from sampled tentacle points.
10725
+ *
10726
+ * @param context Canvas 2D context.
10727
+ * @param ribbonPoints Sampled ribbon points.
10728
+ *
10729
+ * @private helper of `octopus3AvatarVisual`
10730
+ */
10731
+ function traceTentacleRibbonPath(context, ribbonPoints) {
10732
+ const firstRibbonPoint = ribbonPoints[0];
10733
+ context.beginPath();
10734
+ context.moveTo(firstRibbonPoint.x + firstRibbonPoint.normalX * firstRibbonPoint.width, firstRibbonPoint.y + firstRibbonPoint.normalY * firstRibbonPoint.width);
10735
+ for (const ribbonPoint of ribbonPoints.slice(1)) {
10736
+ context.lineTo(ribbonPoint.x + ribbonPoint.normalX * ribbonPoint.width, ribbonPoint.y + ribbonPoint.normalY * ribbonPoint.width);
10737
+ }
10738
+ for (const ribbonPoint of [...ribbonPoints].reverse()) {
10739
+ context.lineTo(ribbonPoint.x - ribbonPoint.normalX * ribbonPoint.width, ribbonPoint.y - ribbonPoint.normalY * ribbonPoint.width);
10740
+ }
10741
+ context.closePath();
10742
+ }
10743
+ /**
10744
+ * Draws a row of soft sucker highlights along one side of the ribbon tentacle.
10745
+ *
10746
+ * @param context Canvas 2D context.
10747
+ * @param tentacleShape Deterministic tentacle descriptor.
10748
+ * @param palette Derived avatar palette.
10749
+ *
10750
+ * @private helper of `octopus3AvatarVisual`
10751
+ */
10752
+ function drawTentacleSuckers(context, tentacleShape, palette) {
10753
+ const undersideDirection = tentacleShape.endPoint.x >= tentacleShape.startPoint.x ? -1 : 1;
10754
+ for (let suckerIndex = 0; suckerIndex < 4; suckerIndex++) {
10755
+ const progress = 0.22 + suckerIndex * 0.17;
10756
+ const point = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, progress);
10757
+ const previousPoint = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, Math.max(0, progress - 0.03));
10758
+ const nextPoint = getCubicBezierPoint(tentacleShape.startPoint, tentacleShape.controlPointOne, tentacleShape.controlPointTwo, tentacleShape.endPoint, Math.min(1, progress + 0.03));
10759
+ const tangentX = nextPoint.x - previousPoint.x;
10760
+ const tangentY = nextPoint.y - previousPoint.y;
10761
+ const tangentLength = Math.hypot(tangentX, tangentY) || 1;
10762
+ const normalX = (-tangentY / tangentLength) * undersideDirection;
10763
+ const normalY = (tangentX / tangentLength) * undersideDirection;
10764
+ const width = tentacleShape.baseWidth + (tentacleShape.tipWidth - tentacleShape.baseWidth) * Math.pow(progress, 1.1);
10765
+ const suckerX = point.x + normalX * width * 0.52;
10766
+ const suckerY = point.y + normalY * width * 0.52;
10767
+ const rotation = Math.atan2(normalY, normalX);
10768
+ context.beginPath();
10769
+ context.ellipse(suckerX, suckerY, width * 0.22, width * 0.11, rotation, 0, Math.PI * 2);
10770
+ context.fillStyle = `${palette.highlight}73`;
10771
+ context.fill();
10772
+ context.strokeStyle = `${palette.highlight}99`;
10773
+ context.lineWidth = Math.max(1, width * 0.08);
10774
+ context.stroke();
10775
+ }
10776
+ }
10777
+ /**
10778
+ * Draws slow inner currents inside the clipped Octopus3 mantle.
10779
+ *
10780
+ * @param context Canvas 2D context.
10781
+ * @param centerX Body center X coordinate.
10782
+ * @param centerY Body center Y coordinate.
10783
+ * @param size Canvas size in CSS pixels.
10784
+ * @param palette Derived avatar palette.
10785
+ * @param timeMs Current animation time in milliseconds.
10786
+ * @param shapePhase Seed-based phase offset.
10787
+ * @param morphologyProfile Seeded morphology profile.
10788
+ *
10789
+ * @private helper of `octopus3AvatarVisual`
10790
+ */
10791
+ function drawMantleCurrents(context, centerX, centerY, size, palette, timeMs, shapePhase, morphologyProfile) {
10792
+ const centeredCurrentIndex = (morphologyProfile.details.mantleCurrentCount - 1) / 2;
10793
+ for (let currentIndex = 0; currentIndex < morphologyProfile.details.mantleCurrentCount; currentIndex++) {
10794
+ const horizontalOffset = (currentIndex - centeredCurrentIndex) *
10795
+ size *
10796
+ (0.05 + (morphologyProfile.body.horizontalStretch - 0.9) * 0.025);
10797
+ const sway = Math.sin(timeMs / 680 + currentIndex * 0.78 + shapePhase) * size * 0.024;
10798
+ context.beginPath();
10799
+ context.moveTo(centerX + horizontalOffset * 0.3, centerY - size * (0.11 + morphologyProfile.body.verticalStretch * 0.02));
10800
+ 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));
10801
+ context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}30` : `${palette.accent}26`;
10802
+ context.lineWidth =
10803
+ size * (0.0075 + currentIndex * 0.00065 + morphologyProfile.tentacles.baseWidthScale * 0.0005);
10804
+ context.lineCap = 'round';
10805
+ context.stroke();
10806
+ }
10807
+ }
10808
+ /**
10809
+ * Draws seeded luminous nodes inside the Octopus3 mantle.
10810
+ *
10811
+ * @param context Canvas 2D context.
10812
+ * @param centerX Body center X coordinate.
10813
+ * @param centerY Body center Y coordinate.
10814
+ * @param size Canvas size in CSS pixels.
10815
+ * @param palette Derived avatar palette.
10816
+ * @param createRandom Seeded random factory scoped to the avatar.
10817
+ * @param morphologyProfile Seeded morphology profile.
10818
+ *
10819
+ * @private helper of `octopus3AvatarVisual`
10820
+ */
10821
+ function drawMantleNodes(context, centerX, centerY, size, palette, createRandom, morphologyProfile) {
10822
+ for (let nodeIndex = 0; nodeIndex < morphologyProfile.details.mantleNodeCount; nodeIndex++) {
10823
+ const nodeRandom = createRandom(`octopus3-node-${nodeIndex}`);
10824
+ const nodeX = centerX + (nodeRandom() - 0.5) * size * (0.2 + (morphologyProfile.body.horizontalStretch - 0.9) * 0.16);
10825
+ const nodeY = centerY -
10826
+ size * 0.03 +
10827
+ (nodeRandom() - 0.5) * size * (0.16 + (morphologyProfile.body.verticalStretch - 0.82) * 0.1);
10828
+ const nodeRadius = size * (0.016 + nodeRandom() * 0.016);
10829
+ context.beginPath();
10830
+ context.arc(nodeX, nodeY, nodeRadius, 0, Math.PI * 2);
10831
+ context.fillStyle = nodeIndex % 2 === 0 ? `${palette.highlight}40` : `${palette.accent}33`;
10832
+ context.fill();
10833
+ }
10834
+ }
10835
+ /**
10836
+ * Draws one expressive seeded eye with a slit pupil and slightly tilted eyelids.
10837
+ *
10838
+ * @param context Canvas 2D context.
10839
+ * @param centerX Eye center X coordinate.
10840
+ * @param centerY Eye center Y coordinate.
10841
+ * @param radiusX Eye horizontal radius.
10842
+ * @param radiusY Eye vertical radius.
10843
+ * @param rotation Eye rotation in radians.
10844
+ * @param palette Derived avatar palette.
10845
+ * @param timeMs Current animation time in milliseconds.
10846
+ * @param phase Seed-based animation phase.
10847
+ * @param interaction Smoothed avatar interaction state.
10848
+ * @param eyeStyle Seeded eye-style traits.
10849
+ *
10850
+ * @private helper of `octopus3AvatarVisual`
10851
+ */
10852
+ function drawSeededEye(context, centerX, centerY, radiusX, radiusY, rotation, palette, timeMs, phase, interaction, eyeStyle) {
10853
+ const { pupilOffsetX, pupilOffsetY } = resolveOrganicEyeMotion({
10854
+ radiusX,
10855
+ radiusY,
10856
+ timeMs,
10857
+ phase,
10858
+ interaction,
10859
+ });
10860
+ context.save();
10861
+ context.translate(centerX, centerY);
10862
+ context.rotate(rotation);
10863
+ context.beginPath();
10864
+ context.ellipse(0, 0, radiusX, radiusY, 0, 0, Math.PI * 2);
10865
+ context.fillStyle = '#f8fbff';
10866
+ context.fill();
10867
+ context.clip();
10868
+ const irisGradient = context.createRadialGradient(-radiusX * 0.18, -radiusY * 0.24, radiusX * 0.05, 0, 0, radiusX * 0.92);
10869
+ irisGradient.addColorStop(0, palette.highlight);
10870
+ irisGradient.addColorStop(0.58, palette.secondary);
10871
+ irisGradient.addColorStop(1, palette.shadow);
10872
+ context.beginPath();
10873
+ context.ellipse(pupilOffsetX, pupilOffsetY, radiusX * 0.66 * eyeStyle.irisScale, radiusY * 0.74 * eyeStyle.irisScale, 0, 0, Math.PI * 2);
10874
+ context.fillStyle = irisGradient;
10875
+ context.fill();
10876
+ context.beginPath();
10877
+ context.ellipse(pupilOffsetX, pupilOffsetY, radiusX * 0.14 * eyeStyle.pupilWidthScale, radiusY * 0.5 * eyeStyle.pupilHeightScale, 0, 0, Math.PI * 2);
10878
+ context.fillStyle = palette.ink;
10879
+ context.fill();
10880
+ context.beginPath();
10881
+ context.ellipse(pupilOffsetX - radiusX * 0.22, pupilOffsetY - radiusY * 0.24, radiusX * 0.12, radiusY * 0.14, 0, 0, Math.PI * 2);
10882
+ context.fillStyle = '#ffffff';
10883
+ context.fill();
10884
+ context.restore();
10885
+ context.save();
10886
+ context.translate(centerX, centerY);
10887
+ context.rotate(rotation);
10888
+ context.beginPath();
10889
+ context.ellipse(0, 0, radiusX, radiusY, 0, 0, Math.PI * 2);
10890
+ context.strokeStyle = `${palette.shadow}d9`;
10891
+ context.lineWidth = radiusX * 0.18;
10892
+ context.stroke();
10893
+ context.beginPath();
10894
+ context.moveTo(-radiusX * 0.88, -radiusY * eyeStyle.upperLidInsetRatio);
10895
+ context.quadraticCurveTo(0, -radiusY * (eyeStyle.upperLidArchRatio - interaction.gazeY * 0.16 + interaction.intensity * 0.08), radiusX * 0.88, -radiusY * eyeStyle.upperLidInsetRatio);
10896
+ context.strokeStyle = `${palette.shadow}73`;
10897
+ context.lineWidth = radiusX * 0.16;
10898
+ context.lineCap = 'round';
10899
+ context.stroke();
10900
+ if (eyeStyle.lowerLidOpacity > 0) {
10901
+ context.beginPath();
10902
+ context.moveTo(-radiusX * 0.74, radiusY * 0.2);
10903
+ context.quadraticCurveTo(0, radiusY * 0.38, radiusX * 0.74, radiusY * 0.2);
10904
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex(eyeStyle.lowerLidOpacity)}`;
10905
+ context.lineWidth = radiusX * 0.08;
10906
+ context.lineCap = 'round';
10907
+ context.stroke();
10908
+ }
10909
+ context.restore();
10910
+ }
10911
+
10912
+ /* eslint-disable no-magic-numbers */
10913
+ /**
10914
+ * Family variants used by the orb renderer.
10915
+ *
10916
+ * @private helper of `orbAvatarVisual`
10917
+ */
10918
+ const ORB_FAMILIES = ['pearl', 'nebula', 'ember', 'glacier'];
10919
+ /**
10920
+ * Built-in Orb avatar visual.
10921
+ *
10922
+ * @private built-in avatar visual
10923
+ */
10924
+ const orbAvatarVisual = {
10925
+ id: 'orb',
10926
+ title: 'Orb',
10927
+ description: 'Glowing morphing circle-orb with seeded gradients, smooth deformations, and luminous layered depth.',
10928
+ isAnimated: true,
10929
+ render({ context, size, palette, createRandom, timeMs }) {
10930
+ const profile = createOrbMorphologyProfile(createRandom);
10931
+ const colorSet = resolveOrbColorSet(palette, profile.family);
10932
+ const centerX = size * 0.5 + profile.coreShiftX * size * 0.06 + Math.sin(timeMs / 2600) * size * 0.009;
10933
+ const centerY = size * 0.5 +
10934
+ profile.coreShiftY * size * 0.06 +
10935
+ Math.cos(timeMs / 3100 + profile.highlightAngle) * size * 0.01;
10936
+ const radius = size * profile.baseRadiusRatio;
10937
+ const silhouettePoints = createOrbSilhouettePoints({
10938
+ centerX,
10939
+ centerY,
10940
+ radius,
10941
+ profile,
10942
+ timeMs,
10943
+ });
10944
+ drawAvatarFrame(context, size, palette);
10945
+ drawOrbAtmosphere(context, size, centerX, centerY, radius, colorSet, profile, timeMs);
10946
+ drawOrbBody(context, silhouettePoints, centerX, centerY, radius, colorSet, profile, size);
10947
+ context.save();
10948
+ traceSmoothClosedPath(context, silhouettePoints);
10949
+ context.clip();
10950
+ drawOrbInteriorRings(context, centerX, centerY, radius, colorSet, profile, size, timeMs);
10951
+ drawOrbSparkles(context, centerX, centerY, radius, colorSet, profile, size, createRandom, timeMs);
10952
+ drawOrbSheen(context, centerX, centerY, radius, colorSet, profile, size, timeMs);
10953
+ drawOrbCore(context, centerX, centerY, radius, colorSet, profile, size, timeMs);
10954
+ context.restore();
10955
+ drawOrbRim(context, silhouettePoints, colorSet, size);
10956
+ },
10957
+ };
10958
+ /**
10959
+ * Builds the deterministic orb profile from the seeded avatar random factory.
10960
+ *
10961
+ * @param createRandom Seeded random factory.
10962
+ * @returns Stable orb morphology profile.
10963
+ *
10964
+ * @private helper of `orbAvatarVisual`
10965
+ */
10966
+ function createOrbMorphologyProfile(createRandom) {
10967
+ const family = pickRandomItem(ORB_FAMILIES, createRandom('orb-family'));
10968
+ const familyAdjustment = resolveOrbFamilyAdjustment(family);
10969
+ const layoutRandom = createRandom('orb-layout');
10970
+ const effectRandom = createRandom('orb-effects');
10971
+ return {
10972
+ family,
10973
+ baseRadiusRatio: clampNumber(0.255 + layoutRandom() * 0.055 + familyAdjustment.baseRadiusRatio, 0.22, 0.335),
10974
+ horizontalStretch: clampNumber(0.93 + layoutRandom() * 0.13 + familyAdjustment.horizontalStretch, 0.88, 1.16),
10975
+ verticalStretch: clampNumber(0.91 + layoutRandom() * 0.13 + familyAdjustment.verticalStretch, 0.88, 1.15),
10976
+ wobbleAmplitude: clampNumber(0.038 + effectRandom() * 0.042 + familyAdjustment.wobbleAmplitude, 0.022, 0.12),
10977
+ wobbleFrequencyOne: clampInteger(2 + Math.floor(effectRandom() * 3) + familyAdjustment.wobbleFrequency, 2, 7),
10978
+ wobbleFrequencyTwo: clampInteger(3 + Math.floor(layoutRandom() * 3) + familyAdjustment.wobbleFrequency, 3, 8),
10979
+ wobbleFrequencyThree: clampInteger(5 + Math.floor(effectRandom() * 3) + familyAdjustment.wobbleFrequency, 4, 9),
10980
+ ringCount: clampInteger(2 + Math.floor(effectRandom() * 3) + familyAdjustment.ringCount, 2, 5),
10981
+ sparkleCount: clampInteger(6 + Math.floor(effectRandom() * 7) + familyAdjustment.sparkleCount, 4, 16),
10982
+ haloCount: clampInteger(2 + Math.floor(layoutRandom() * 2) + familyAdjustment.haloCount, 1, 4),
10983
+ coreShiftX: (layoutRandom() - 0.5) * 0.08,
10984
+ coreShiftY: (effectRandom() - 0.5) * 0.08,
10985
+ highlightAngle: layoutRandom() * Math.PI * 2,
10986
+ bandRotation: effectRandom() * Math.PI * 2,
10987
+ pulseSpeed: 0.82 + effectRandom() * 0.72,
10988
+ haloBlurRatio: clampNumber(0.18 + layoutRandom() * 0.12 + familyAdjustment.haloBlurRatio, 0.16, 0.34),
10989
+ sheenStrength: clampNumber(0.46 + effectRandom() * 0.28 + familyAdjustment.sheenStrength, 0.38, 0.88),
10990
+ };
10991
+ }
10992
+ /**
10993
+ * Resolves the family-specific adjustments that keep the orb surface varied while still circular.
10994
+ *
10995
+ * @param family Selected orb family.
10996
+ * @returns Family-specific profile adjustments.
10997
+ *
10998
+ * @private helper of `orbAvatarVisual`
10999
+ */
11000
+ function resolveOrbFamilyAdjustment(family) {
11001
+ switch (family) {
11002
+ case 'nebula':
11003
+ return {
11004
+ baseRadiusRatio: 0.006,
11005
+ horizontalStretch: 0.05,
11006
+ verticalStretch: -0.01,
11007
+ wobbleAmplitude: 0.012,
11008
+ wobbleFrequency: 1,
11009
+ ringCount: 1,
11010
+ sparkleCount: 2,
11011
+ haloCount: 0,
11012
+ haloBlurRatio: 0.02,
11013
+ sheenStrength: 0.08,
11014
+ };
11015
+ case 'ember':
11016
+ return {
11017
+ baseRadiusRatio: -0.004,
11018
+ horizontalStretch: -0.03,
11019
+ verticalStretch: 0.03,
11020
+ wobbleAmplitude: 0.02,
11021
+ wobbleFrequency: 1,
11022
+ ringCount: 0,
11023
+ sparkleCount: 3,
11024
+ haloCount: 0,
11025
+ haloBlurRatio: 0.04,
11026
+ sheenStrength: 0.06,
11027
+ };
11028
+ case 'glacier':
11029
+ return {
11030
+ baseRadiusRatio: 0.012,
11031
+ horizontalStretch: 0.01,
11032
+ verticalStretch: 0.02,
11033
+ wobbleAmplitude: -0.006,
11034
+ wobbleFrequency: -1,
11035
+ ringCount: 0,
11036
+ sparkleCount: 0,
11037
+ haloCount: 0,
11038
+ haloBlurRatio: -0.01,
11039
+ sheenStrength: 0.12,
11040
+ };
11041
+ case 'pearl':
11042
+ default:
11043
+ return {
11044
+ baseRadiusRatio: 0.02,
11045
+ horizontalStretch: 0.025,
11046
+ verticalStretch: 0.02,
11047
+ wobbleAmplitude: -0.01,
11048
+ wobbleFrequency: 0,
11049
+ ringCount: 0,
11050
+ sparkleCount: -1,
11051
+ haloCount: 1,
11052
+ haloBlurRatio: 0.02,
11053
+ sheenStrength: 0.1,
11054
+ };
11055
+ }
11056
+ }
11057
+ /**
11058
+ * Resolves the color set used by the orb renderer.
11059
+ *
11060
+ * @param palette Base avatar palette.
11061
+ * @param family Selected orb family.
11062
+ * @returns Derived orb-specific color set.
11063
+ *
11064
+ * @private helper of `orbAvatarVisual`
11065
+ */
11066
+ function resolveOrbColorSet(palette, family) {
11067
+ const primaryColor = Color.fromSafe(palette.primary);
11068
+ const secondaryColor = Color.fromSafe(palette.secondary);
11069
+ const accentColor = Color.fromSafe(palette.accent);
11070
+ const highlightColor = Color.fromSafe(palette.highlight);
11071
+ const shadowColor = Color.fromSafe(palette.shadow);
11072
+ switch (family) {
11073
+ case 'nebula':
11074
+ return {
11075
+ core: accentColor.then(lighten(0.16)).then(saturate(0.14)).toHex(),
11076
+ mid: secondaryColor.then(lighten(0.08)).then(saturate(0.08)).toHex(),
11077
+ outer: primaryColor.then(darken(0.03)).then(saturate(0.04)).toHex(),
11078
+ rim: shadowColor.then(lighten(0.08)).toHex(),
11079
+ highlight: highlightColor.then(lighten(0.14)).toHex(),
11080
+ aura: secondaryColor.then(lighten(0.18)).toHex(),
11081
+ };
11082
+ case 'ember':
11083
+ return {
11084
+ core: accentColor.then(lighten(0.2)).then(saturate(0.16)).toHex(),
11085
+ mid: primaryColor.then(lighten(0.1)).then(saturate(0.08)).toHex(),
11086
+ outer: secondaryColor.then(darken(0.04)).then(saturate(0.04)).toHex(),
11087
+ rim: shadowColor.then(lighten(0.08)).toHex(),
11088
+ highlight: highlightColor.then(lighten(0.18)).toHex(),
11089
+ aura: accentColor.then(lighten(0.12)).toHex(),
11090
+ };
11091
+ case 'glacier':
11092
+ return {
11093
+ core: highlightColor.then(lighten(0.18)).then(saturate(-0.04)).toHex(),
11094
+ mid: secondaryColor.then(lighten(0.1)).then(saturate(-0.06)).toHex(),
11095
+ outer: primaryColor.then(lighten(0.06)).toHex(),
11096
+ rim: shadowColor.then(lighten(0.12)).toHex(),
11097
+ highlight: highlightColor.then(lighten(0.26)).toHex(),
11098
+ aura: primaryColor.then(lighten(0.18)).toHex(),
11099
+ };
11100
+ case 'pearl':
11101
+ default:
11102
+ return {
11103
+ core: highlightColor.then(lighten(0.2)).then(saturate(-0.06)).toHex(),
11104
+ mid: primaryColor.then(lighten(0.12)).then(saturate(-0.02)).toHex(),
11105
+ outer: secondaryColor.then(lighten(0.08)).then(saturate(-0.04)).toHex(),
11106
+ rim: shadowColor.then(lighten(0.12)).toHex(),
11107
+ highlight: highlightColor.then(lighten(0.3)).toHex(),
11108
+ aura: highlightColor.then(lighten(0.18)).toHex(),
11109
+ };
11110
+ }
11111
+ }
11112
+ /**
11113
+ * Creates the orb silhouette points from the deterministic profile.
11114
+ *
11115
+ * @param options Orb geometry options.
11116
+ * @returns Smoothly varying orb outline.
11117
+ *
11118
+ * @private helper of `orbAvatarVisual`
11119
+ */
11120
+ function createOrbSilhouettePoints(options) {
11121
+ const { centerX, centerY, radius, profile, timeMs } = options;
11122
+ const pointCount = 48;
11123
+ return Array.from({ length: pointCount }, (_, pointIndex) => {
11124
+ const progress = pointIndex / pointCount;
11125
+ const angle = -Math.PI / 2 + progress * Math.PI * 2;
11126
+ const breathing = Math.sin(timeMs / (1450 / profile.pulseSpeed) + profile.bandRotation) * profile.wobbleAmplitude;
11127
+ const surfaceWaveOne = Math.sin(angle * profile.wobbleFrequencyOne + profile.highlightAngle + timeMs / (980 / profile.pulseSpeed)) * profile.wobbleAmplitude;
11128
+ const surfaceWaveTwo = Math.cos(angle * profile.wobbleFrequencyTwo - profile.bandRotation * 0.8 + timeMs / (1320 / profile.pulseSpeed)) *
11129
+ profile.wobbleAmplitude *
11130
+ 0.62;
11131
+ const surfaceWaveThree = Math.sin(angle * profile.wobbleFrequencyThree +
11132
+ profile.highlightAngle * 1.4 -
11133
+ timeMs / (1710 / profile.pulseSpeed)) *
11134
+ profile.wobbleAmplitude *
11135
+ 0.38;
11136
+ const surfaceTaper = Math.sin(angle * 2 + profile.highlightAngle) * profile.wobbleAmplitude * 0.2;
11137
+ const localRadius = radius * (1 + breathing * 0.12 + surfaceWaveOne + surfaceWaveTwo + surfaceWaveThree + surfaceTaper);
11138
+ return {
11139
+ x: centerX +
11140
+ Math.cos(angle) * localRadius * profile.horizontalStretch +
11141
+ Math.sin(angle * 3 + profile.highlightAngle + timeMs / 2100) * radius * 0.012,
11142
+ y: centerY +
11143
+ Math.sin(angle) * localRadius * profile.verticalStretch +
11144
+ Math.cos(angle * 2 - profile.highlightAngle + timeMs / 2400) * radius * 0.01,
11145
+ };
11146
+ });
11147
+ }
11148
+ /**
11149
+ * Draws the atmospheric glow behind the orb.
11150
+ *
11151
+ * @param context Canvas 2D context.
11152
+ * @param size Canvas size in CSS pixels.
11153
+ * @param centerX Orb center X coordinate.
11154
+ * @param centerY Orb center Y coordinate.
11155
+ * @param radius Orb base radius.
11156
+ * @param colorSet Derived orb color set.
11157
+ * @param profile Deterministic orb profile.
11158
+ * @param timeMs Current animation time in milliseconds.
11159
+ *
11160
+ * @private helper of `orbAvatarVisual`
11161
+ */
11162
+ function drawOrbAtmosphere(context, size, centerX, centerY, radius, colorSet, profile, timeMs) {
11163
+ const atmosphereGradient = context.createRadialGradient(centerX, centerY, radius * 0.08, centerX, centerY, radius * (1.9 + profile.haloBlurRatio));
11164
+ atmosphereGradient.addColorStop(0, `${colorSet.highlight}26`);
11165
+ atmosphereGradient.addColorStop(0.32, `${colorSet.aura}16`);
11166
+ atmosphereGradient.addColorStop(1, `${colorSet.aura}00`);
11167
+ context.save();
11168
+ context.globalCompositeOperation = 'screen';
11169
+ context.fillStyle = atmosphereGradient;
11170
+ context.fillRect(0, 0, size, size);
11171
+ for (let haloIndex = 0; haloIndex < profile.haloCount; haloIndex++) {
11172
+ const haloPulse = Math.sin(timeMs / (1200 + haloIndex * 180) + profile.highlightAngle) * radius * 0.025;
11173
+ const haloRadiusX = radius * (1.18 + haloIndex * 0.18) + haloPulse;
11174
+ const haloRadiusY = radius * (0.98 + haloIndex * 0.16) + haloPulse * 0.82;
11175
+ const haloGradient = context.createRadialGradient(centerX, centerY, radius * 0.12, centerX, centerY, haloRadiusX * 1.12);
11176
+ haloGradient.addColorStop(0, `${colorSet.aura}${haloIndex === 0 ? '38' : '24'}`);
11177
+ haloGradient.addColorStop(0.5, `${colorSet.highlight}${haloIndex === 0 ? '1f' : '12'}`);
11178
+ haloGradient.addColorStop(1, `${colorSet.aura}00`);
11179
+ context.beginPath();
11180
+ context.ellipse(centerX, centerY, haloRadiusX, haloRadiusY, profile.highlightAngle * 0.12 + haloIndex * 0.2 + Math.sin(timeMs / 3800) * 0.04, 0, Math.PI * 2);
11181
+ context.fillStyle = haloGradient;
11182
+ context.fill();
11183
+ }
11184
+ context.restore();
11185
+ }
11186
+ /**
11187
+ * Draws the main orb body using the smooth silhouette path.
11188
+ *
11189
+ * @param context Canvas 2D context.
11190
+ * @param points Smooth orb outline points.
11191
+ * @param centerX Orb center X coordinate.
11192
+ * @param centerY Orb center Y coordinate.
11193
+ * @param radius Orb base radius.
11194
+ * @param colorSet Derived orb color set.
11195
+ * @param profile Deterministic orb profile.
11196
+ * @param size Canvas size in CSS pixels.
11197
+ *
11198
+ * @private helper of `orbAvatarVisual`
11199
+ */
11200
+ function drawOrbBody(context, points, centerX, centerY, radius, colorSet, profile, size) {
11201
+ 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));
11202
+ bodyGradient.addColorStop(0, `${colorSet.highlight}f8`);
11203
+ bodyGradient.addColorStop(0.22, `${colorSet.core}f2`);
11204
+ bodyGradient.addColorStop(0.58, `${colorSet.mid}e8`);
11205
+ bodyGradient.addColorStop(0.86, `${colorSet.outer}dc`);
11206
+ bodyGradient.addColorStop(1, `${colorSet.rim}ea`);
11207
+ context.save();
11208
+ traceSmoothClosedPath(context, points);
11209
+ context.shadowColor = `${colorSet.rim}aa`;
11210
+ context.shadowBlur = size * (0.06 + profile.haloBlurRatio * 0.15);
11211
+ context.fillStyle = bodyGradient;
11212
+ context.fill();
11213
+ context.restore();
11214
+ context.save();
11215
+ traceSmoothClosedPath(context, points);
11216
+ context.strokeStyle = `${colorSet.highlight}${profile.family === 'pearl' ? '72' : '58'}`;
11217
+ context.lineWidth = Math.max(1.2, size * 0.012);
11218
+ context.lineJoin = 'round';
11219
+ context.lineCap = 'round';
11220
+ context.stroke();
11221
+ context.restore();
11222
+ }
11223
+ /**
11224
+ * Draws the layered energy rings and soft internal gradients inside the orb.
11225
+ *
11226
+ * @param context Canvas 2D context.
11227
+ * @param centerX Orb center X coordinate.
11228
+ * @param centerY Orb center Y coordinate.
11229
+ * @param radius Orb base radius.
11230
+ * @param colorSet Derived orb color set.
11231
+ * @param profile Deterministic orb profile.
11232
+ * @param size Canvas size in CSS pixels.
11233
+ * @param timeMs Current animation time in milliseconds.
11234
+ *
11235
+ * @private helper of `orbAvatarVisual`
11236
+ */
11237
+ function drawOrbInteriorRings(context, centerX, centerY, radius, colorSet, profile, size, timeMs) {
11238
+ const internalGradient = context.createRadialGradient(centerX - radius * 0.08, centerY - radius * 0.1, radius * 0.06, centerX, centerY, radius * (0.95 + profile.sheenStrength * 0.15));
11239
+ internalGradient.addColorStop(0, `${colorSet.highlight}70`);
11240
+ internalGradient.addColorStop(0.48, `${colorSet.core}22`);
11241
+ internalGradient.addColorStop(1, `${colorSet.rim}00`);
11242
+ context.fillStyle = internalGradient;
11243
+ context.fillRect(centerX - radius, centerY - radius, radius * 2, radius * 2);
11244
+ for (let ringIndex = 0; ringIndex < profile.ringCount; ringIndex++) {
11245
+ const ringProgress = (ringIndex + 1) / (profile.ringCount + 1);
11246
+ const ringRadiusX = radius * (0.34 + ringProgress * 0.44);
11247
+ const ringRadiusY = radius * (0.28 + ringProgress * 0.38);
11248
+ const ringRotation = profile.highlightAngle * 0.24 +
11249
+ ringIndex * 0.4 +
11250
+ Math.sin(timeMs / (1800 + ringIndex * 180) + profile.bandRotation) * 0.08;
11251
+ context.beginPath();
11252
+ 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);
11253
+ context.strokeStyle = ringIndex % 2 === 0 ? `${colorSet.highlight}32` : `${colorSet.aura}24`;
11254
+ context.lineWidth = Math.max(1.2, size * (0.007 - ringIndex * 0.001));
11255
+ context.shadowColor = `${colorSet.highlight}55`;
11256
+ context.shadowBlur = size * 0.02;
11257
+ context.stroke();
11258
+ }
11259
+ }
11260
+ /**
11261
+ * Draws soft sparkles and seeded dust inside the orb.
11262
+ *
11263
+ * @param context Canvas 2D context.
11264
+ * @param centerX Orb center X coordinate.
11265
+ * @param centerY Orb center Y coordinate.
11266
+ * @param radius Orb base radius.
11267
+ * @param colorSet Derived orb color set.
11268
+ * @param profile Deterministic orb profile.
11269
+ * @param size Canvas size in CSS pixels.
11270
+ * @param createRandom Seeded random factory.
11271
+ * @param timeMs Current animation time in milliseconds.
11272
+ *
11273
+ * @private helper of `orbAvatarVisual`
11274
+ */
11275
+ function drawOrbSparkles(context, centerX, centerY, radius, colorSet, profile, size, createRandom, timeMs) {
11276
+ const sparkleStride = Math.max(1, Math.floor(profile.sparkleCount / 6));
11277
+ for (let sparkleIndex = 0; sparkleIndex < profile.sparkleCount; sparkleIndex++) {
11278
+ const sparkleRandom = createRandom(`orb-sparkle-${sparkleIndex}`);
11279
+ const sparkleAngle = sparkleRandom() * Math.PI * 2 + profile.highlightAngle * 0.4;
11280
+ const sparkleOrbitRadius = radius * (0.42 + sparkleRandom() * 0.55);
11281
+ const sparkleOrbitPulse = 0.92 + Math.sin(timeMs / (700 + sparkleIndex * 70) + profile.bandRotation) * 0.08;
11282
+ const sparkleCenterX = centerX + Math.cos(sparkleAngle + timeMs / (4600 / profile.pulseSpeed)) * sparkleOrbitRadius;
11283
+ const sparkleCenterY = centerY + Math.sin(sparkleAngle + timeMs / (4600 / profile.pulseSpeed)) * sparkleOrbitRadius;
11284
+ const sparkleRadius = size * (0.0028 + sparkleRandom() * 0.0058) * sparkleOrbitPulse;
11285
+ context.beginPath();
11286
+ context.arc(sparkleCenterX, sparkleCenterY, sparkleRadius * 2, 0, Math.PI * 2);
11287
+ context.fillStyle = `${colorSet.highlight}14`;
11288
+ context.fill();
11289
+ context.beginPath();
11290
+ context.arc(sparkleCenterX, sparkleCenterY, sparkleRadius, 0, Math.PI * 2);
11291
+ context.fillStyle = sparkleIndex % sparkleStride === 0 ? `${colorSet.highlight}d2` : `${colorSet.aura}c8`;
11292
+ context.shadowColor = `${colorSet.highlight}66`;
11293
+ context.shadowBlur = size * 0.012;
11294
+ context.fill();
11295
+ }
11296
+ }
11297
+ /**
11298
+ * Draws the specular sweep that makes the orb feel glossy and dimensional.
11299
+ *
11300
+ * @param context Canvas 2D context.
11301
+ * @param centerX Orb center X coordinate.
11302
+ * @param centerY Orb center Y coordinate.
11303
+ * @param radius Orb base radius.
11304
+ * @param colorSet Derived orb color set.
11305
+ * @param profile Deterministic orb profile.
11306
+ * @param size Canvas size in CSS pixels.
11307
+ * @param timeMs Current animation time in milliseconds.
11308
+ *
11309
+ * @private helper of `orbAvatarVisual`
11310
+ */
11311
+ function drawOrbSheen(context, centerX, centerY, radius, colorSet, profile, size, timeMs) {
11312
+ const sheenAngle = profile.bandRotation + timeMs / (2400 / profile.pulseSpeed);
11313
+ const sheenDirectionX = Math.cos(sheenAngle);
11314
+ const sheenDirectionY = Math.sin(sheenAngle);
11315
+ const sheenGradient = context.createLinearGradient(centerX - sheenDirectionX * radius, centerY - sheenDirectionY * radius, centerX + sheenDirectionX * radius, centerY + sheenDirectionY * radius);
11316
+ sheenGradient.addColorStop(0, `${colorSet.highlight}00`);
11317
+ sheenGradient.addColorStop(0.24, `${colorSet.highlight}0e`);
11318
+ sheenGradient.addColorStop(0.43, `${colorSet.highlight}${profile.sheenStrength > 0.65 ? '66' : '48'}`);
11319
+ sheenGradient.addColorStop(0.57, `${colorSet.core}${profile.sheenStrength > 0.65 ? '42' : '2e'}`);
11320
+ sheenGradient.addColorStop(0.75, `${colorSet.aura}1e`);
11321
+ sheenGradient.addColorStop(1, `${colorSet.highlight}00`);
11322
+ context.fillStyle = sheenGradient;
11323
+ context.fillRect(centerX - radius * 1.6, centerY - radius * 1.6, radius * 3.2, radius * 3.2);
11324
+ context.beginPath();
11325
+ context.ellipse(centerX - radius * 0.18, centerY - radius * 0.2, radius * 0.44, radius * 0.24, profile.highlightAngle * 0.32, 0, Math.PI * 2);
11326
+ context.fillStyle = `${colorSet.highlight}${profile.family === 'ember' ? '26' : '2e'}`;
11327
+ context.shadowColor = `${colorSet.highlight}55`;
11328
+ context.shadowBlur = size * 0.02;
11329
+ context.fill();
11330
+ }
11331
+ /**
11332
+ * Draws the bright nucleus that gives the orb a recognisable center.
11333
+ *
11334
+ * @param context Canvas 2D context.
11335
+ * @param centerX Orb center X coordinate.
11336
+ * @param centerY Orb center Y coordinate.
11337
+ * @param radius Orb base radius.
11338
+ * @param colorSet Derived orb color set.
11339
+ * @param profile Deterministic orb profile.
11340
+ * @param size Canvas size in CSS pixels.
11341
+ * @param timeMs Current animation time in milliseconds.
11342
+ *
11343
+ * @private helper of `orbAvatarVisual`
11344
+ */
11345
+ function drawOrbCore(context, centerX, centerY, radius, colorSet, profile, size, timeMs) {
11346
+ const coreRadiusX = radius * (0.18 + profile.sheenStrength * 0.06);
11347
+ const coreRadiusY = radius * (0.15 + profile.sheenStrength * 0.045);
11348
+ const corePulse = 1 + Math.sin(timeMs / (1100 / profile.pulseSpeed) + profile.bandRotation) * 0.04;
11349
+ context.beginPath();
11350
+ 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);
11351
+ context.fillStyle = `${colorSet.core}f0`;
11352
+ context.shadowColor = `${colorSet.highlight}88`;
11353
+ context.shadowBlur = size * 0.03;
11354
+ context.fill();
11355
+ context.beginPath();
11356
+ context.arc(centerX - radius * 0.06, centerY - radius * 0.07, radius * 0.06, 0, Math.PI * 2);
11357
+ context.fillStyle = `${colorSet.highlight}d6`;
11358
+ context.fill();
11359
+ }
11360
+ /**
11361
+ * Draws the final rim stroke so the orb stays crisp against busy backgrounds.
11362
+ *
11363
+ * @param context Canvas 2D context.
11364
+ * @param points Smooth orb outline points.
11365
+ * @param colorSet Derived orb color set.
11366
+ * @param size Canvas size in CSS pixels.
11367
+ *
11368
+ * @private helper of `orbAvatarVisual`
11369
+ */
11370
+ function drawOrbRim(context, points, colorSet, size) {
11371
+ context.save();
11372
+ traceSmoothClosedPath(context, points);
11373
+ context.strokeStyle = `${colorSet.rim}c0`;
11374
+ context.lineWidth = Math.max(1.25, size * 0.013);
11375
+ context.lineJoin = 'round';
11376
+ context.lineCap = 'round';
11377
+ context.shadowColor = `${colorSet.highlight}4f`;
11378
+ context.shadowBlur = size * 0.018;
11379
+ context.stroke();
11380
+ context.restore();
11381
+ }
11382
+ /**
11383
+ * Clamps a number into the provided range.
11384
+ *
11385
+ * @param value Number to clamp.
11386
+ * @param minimum Minimum accepted value.
11387
+ * @param maximum Maximum accepted value.
11388
+ * @returns Clamped number.
11389
+ *
11390
+ * @private helper of `orbAvatarVisual`
11391
+ */
11392
+ function clampNumber(value, minimum, maximum) {
11393
+ return Math.min(maximum, Math.max(minimum, value));
11394
+ }
11395
+ /**
11396
+ * Clamps a number into the provided integer range.
11397
+ *
11398
+ * @param value Number to clamp.
11399
+ * @param minimum Minimum accepted value.
11400
+ * @param maximum Maximum accepted value.
11401
+ * @returns Clamped integer.
11402
+ *
11403
+ * @private helper of `orbAvatarVisual`
11404
+ */
11405
+ function clampInteger(value, minimum, maximum) {
11406
+ return Math.min(maximum, Math.max(minimum, Math.round(value)));
11407
+ }
11408
+
11409
+ /* eslint-disable no-magic-numbers */
11410
+ /**
11411
+ * Pixel-art avatar visual.
11412
+ *
11413
+ * @private built-in avatar visual
11414
+ */
11415
+ const pixelArtAvatarVisual = {
11416
+ id: 'pixel-art',
11417
+ title: 'Pixel art',
11418
+ description: 'Symmetric retro badge with a deterministic face and palette-driven pixels.',
11419
+ isAnimated: false,
11420
+ render({ context, size, palette, createRandom, avatarDefinition }) {
11421
+ const random = createRandom('pixel-art');
11422
+ const panelSize = size * 0.62;
11423
+ const panelX = (size - panelSize) / 2;
11424
+ const panelY = size * 0.19;
11425
+ const pixelGridSize = 10;
11426
+ const pixelSize = panelSize / pixelGridSize;
11427
+ const halfColumns = Math.ceil(pixelGridSize / 2);
11428
+ const colorStops = [palette.primary, palette.secondary, palette.accent, palette.highlight];
11429
+ const foreheadRowCount = 2 + Math.floor(random() * 2);
11430
+ const cheekInset = 1 + Math.floor(random() * 2);
11431
+ const emblemHeight = 2 + Math.floor(random() * 2);
11432
+ drawAvatarFrame(context, size, palette);
11433
+ const glow = context.createRadialGradient(size * 0.5, size * 0.32, size * 0.05, size * 0.5, size * 0.32, size * 0.5);
11434
+ glow.addColorStop(0, `${palette.highlight}aa`);
11435
+ glow.addColorStop(1, `${palette.highlight}00`);
11436
+ context.fillStyle = glow;
11437
+ context.fillRect(0, 0, size, size);
11438
+ context.save();
11439
+ createRoundedRectPath(context, panelX, panelY, panelSize, panelSize, panelSize * 0.16);
11440
+ context.fillStyle = palette.shadow;
11441
+ context.shadowColor = `${palette.shadow}66`;
11442
+ context.shadowBlur = size * 0.08;
11443
+ context.fill();
11444
+ context.restore();
11445
+ for (let rowIndex = 0; rowIndex < pixelGridSize; rowIndex++) {
11446
+ for (let columnIndex = 0; columnIndex < halfColumns; columnIndex++) {
11447
+ const mirroredColumnIndex = pixelGridSize - columnIndex - 1;
11448
+ const normalizedRowDistance = Math.abs(rowIndex - pixelGridSize / 2) / (pixelGridSize / 2);
11449
+ const normalizedColumnDistance = columnIndex / halfColumns;
11450
+ const fillChance = 0.85 - normalizedRowDistance * 0.32 - normalizedColumnDistance * 0.08;
11451
+ if (random() > fillChance) {
11452
+ continue;
11453
+ }
11454
+ const color = colorStops[Math.min(colorStops.length - 1, Math.floor(random() *
11455
+ (rowIndex < foreheadRowCount
11456
+ ? 2
11457
+ : rowIndex > pixelGridSize - emblemHeight - 1
11458
+ ? colorStops.length
11459
+ : 3)))];
11460
+ if (columnIndex === 0 &&
11461
+ rowIndex >= cheekInset &&
11462
+ rowIndex <= pixelGridSize - cheekInset - 1 &&
11463
+ random() < 0.4) {
11464
+ continue;
11465
+ }
11466
+ drawPixel(context, panelX + columnIndex * pixelSize, panelY + rowIndex * pixelSize, pixelSize, color);
11467
+ if (mirroredColumnIndex !== columnIndex) {
11468
+ drawPixel(context, panelX + mirroredColumnIndex * pixelSize, panelY + rowIndex * pixelSize, pixelSize, color);
11469
+ }
11470
+ }
11471
+ }
11472
+ const eyeRowIndex = 3 + Math.floor(random() * 2);
11473
+ const eyeColumnOffset = 2 + Math.floor(random() * 2);
11474
+ drawPixel(context, panelX + eyeColumnOffset * pixelSize, panelY + eyeRowIndex * pixelSize, pixelSize, palette.ink);
11475
+ drawPixel(context, panelX + (pixelGridSize - eyeColumnOffset - 1) * pixelSize, panelY + eyeRowIndex * pixelSize, pixelSize, palette.ink);
11476
+ drawPixel(context, panelX + eyeColumnOffset * pixelSize, panelY + eyeRowIndex * pixelSize, pixelSize * 0.44, '#ffffff');
11477
+ drawPixel(context, panelX + (pixelGridSize - eyeColumnOffset - 1) * pixelSize, panelY + eyeRowIndex * pixelSize, pixelSize * 0.44, '#ffffff');
11478
+ const mouthRowIndex = eyeRowIndex + 3;
11479
+ const mouthWidth = 2 + Math.floor(random() * 2);
11480
+ for (let mouthOffset = 0; mouthOffset < mouthWidth; mouthOffset++) {
11481
+ drawPixel(context, panelX + (4 + mouthOffset) * pixelSize, panelY + mouthRowIndex * pixelSize, pixelSize, palette.shadow);
11482
+ }
11483
+ context.save();
11484
+ context.fillStyle = `${palette.highlight}44`;
11485
+ context.font = `${Math.round(size * 0.12)}px sans-serif`;
11486
+ context.textAlign = 'center';
11487
+ context.fillText(avatarDefinition.agentName.charAt(0).toUpperCase(), size * 0.5, size * 0.88);
11488
+ context.restore();
11489
+ },
11490
+ };
11491
+ /**
11492
+ * Draws one pixel-art block with a tiny inner highlight.
11493
+ *
11494
+ * @param context Canvas 2D context.
11495
+ * @param x Left coordinate.
11496
+ * @param y Top coordinate.
11497
+ * @param size Pixel size.
11498
+ * @param color Pixel fill color.
11499
+ *
11500
+ * @private helper of `pixelArtAvatarVisual`
11501
+ */
11502
+ function drawPixel(context, x, y, size, color) {
11503
+ const normalizedSize = size * 0.9;
11504
+ const offset = (size - normalizedSize) / 2;
11505
+ context.fillStyle = color;
11506
+ context.fillRect(x + offset, y + offset, normalizedSize, normalizedSize);
11507
+ context.fillStyle = 'rgba(255,255,255,0.18)';
11508
+ context.fillRect(x + offset, y + offset, normalizedSize, normalizedSize * 0.14);
11509
+ }
11510
+
11511
+ // Note: [💞] Ignore a discrepancy between file name and entity name
11512
+ /**
11513
+ * Built-in avatar visuals available to the app.
11514
+ *
11515
+ * @private shared registry for the avatar rendering system
11516
+ */
11517
+ const AVATAR_VISUALS = [
11518
+ pixelArtAvatarVisual,
11519
+ octopusAvatarVisual,
11520
+ octopus2AvatarVisual,
11521
+ octopus3AvatarVisual,
11522
+ asciiOctopusAvatarVisual,
11523
+ minecraftAvatarVisual,
11524
+ fractalAvatarVisual,
11525
+ orbAvatarVisual,
11526
+ ];
11527
+ /**
11528
+ * Normalizes user-facing avatar visual names so ids can be matched case-insensitively
11529
+ * across spaces, hyphens, underscores, and future separator variants.
11530
+ *
11531
+ * @param value Raw avatar visual id or title.
11532
+ * @returns Stable lookup key.
11533
+ *
11534
+ * @private shared registry for the avatar rendering system
11535
+ */
11536
+ function normalizeAvatarVisualLookupKey(value) {
11537
+ return value
11538
+ .trim()
11539
+ .toLowerCase()
11540
+ .replace(/[^a-z0-9]+/g, '');
11541
+ }
11542
+ /**
11543
+ * Resolves a user-facing avatar visual value to a supported built-in visual id.
11544
+ *
11545
+ * The lookup is derived from `AVATAR_VISUALS`, so new visuals become selectable by
11546
+ * adding them to the registry rather than updating parser-specific option lists.
11547
+ *
11548
+ * @param value Raw visual id/title, for example `PIXEL_ART`, `pixel art`, or `pixel-art`.
11549
+ * @returns Matching visual id or `null` when the value is empty/unknown.
11550
+ *
11551
+ * @private shared registry for the avatar rendering system
11552
+ */
11553
+ function resolveAvatarVisualId(value) {
11554
+ if (!value) {
11555
+ return null;
11556
+ }
11557
+ const normalizedValue = normalizeAvatarVisualLookupKey(value);
11558
+ if (!normalizedValue) {
11559
+ return null;
11560
+ }
11561
+ const avatarVisual = AVATAR_VISUALS.find((candidateAvatarVisual) => normalizeAvatarVisualLookupKey(candidateAvatarVisual.id) === normalizedValue ||
11562
+ normalizeAvatarVisualLookupKey(candidateAvatarVisual.title) === normalizedValue);
11563
+ return (avatarVisual === null || avatarVisual === void 0 ? void 0 : avatarVisual.id) || null;
11564
+ }
11565
+
11566
+ /**
11567
+ * META AVATAR commitment definition
11568
+ *
11569
+ * The `META AVATAR` commitment sets the built-in default avatar visual used when
11570
+ * the agent does not provide an explicit `META IMAGE`.
11571
+ *
11572
+ * @private [đŸĒ”] Maybe export the commitments through some package
11573
+ */
11574
+ class MetaAvatarCommitmentDefinition extends BaseCommitmentDefinition {
11575
+ constructor() {
11576
+ super('META AVATAR');
11577
+ }
11578
+ /**
11579
+ * Short one-line description of META AVATAR.
11580
+ */
11581
+ get description() {
11582
+ return "Set the agent's built-in avatar visual.";
8219
11583
  }
8220
11584
  /**
8221
11585
  * Icon for this commitment.
8222
11586
  */
8223
11587
  get icon() {
8224
- return 'â„šī¸';
11588
+ return '👤';
8225
11589
  }
8226
11590
  /**
8227
- * Markdown documentation for META commitment.
11591
+ * Markdown documentation for META AVATAR commitment.
8228
11592
  */
8229
11593
  get documentation() {
11594
+ const supportedVisuals = AVATAR_VISUALS.map((avatarVisual) => `\`${avatarVisual.id}\``).join(', ');
8230
11595
  return spaceTrim$1(`
8231
- # META
8232
-
8233
- Sets meta-information about the agent that is used for display and attribution purposes.
8234
-
8235
- ## Supported META types
11596
+ # META AVATAR
8236
11597
 
8237
- - **META IMAGE** - Sets the agent's avatar/profile image URL
8238
- - **META LINK** - Provides profile/source links for the person the agent models
8239
- - **META DOMAIN** - Sets the canonical custom domain/host of the agent
8240
- - **META TITLE** - Sets the agent's display title
8241
- - **META DESCRIPTION** - Sets the agent's description
8242
- - **META INPUT PLACEHOLDER** - Sets chat input placeholder text
8243
- - **META [ANYTHING]** - Any other meta information in uppercase format
11598
+ Sets the built-in avatar visual used for the agent when no explicit \`META IMAGE\` is provided.
8244
11599
 
8245
11600
  ## Key aspects
8246
11601
 
8247
- - Does not modify the agent's behavior or responses
8248
- - Used for visual representation and attribution in user interfaces
8249
- - Multiple META commitments of different types can be used
8250
- - Multiple META LINK commitments can be used for different social profiles
8251
- - If multiple META commitments of the same type are specified, the last one takes precedence (except for LINK)
11602
+ - Does not modify the agent's behavior or responses.
11603
+ - Only one \`META AVATAR\` should be used per agent.
11604
+ - If multiple are specified, the last one takes precedence.
11605
+ - Values are matched case-insensitively and spaces, hyphens, and underscores are normalized.
11606
+ - Supported visuals are derived from the shared avatar registry: ${supportedVisuals}.
8252
11607
 
8253
11608
  ## Examples
8254
11609
 
8255
- ### Basic meta information
8256
-
8257
- \`\`\`book
8258
- Professional Assistant
8259
-
8260
- META IMAGE https://example.com/professional-avatar.jpg
8261
- META TITLE Senior Business Consultant
8262
- META DESCRIPTION Specialized in strategic planning and project management
8263
- META LINK https://linkedin.com/in/professional
8264
- \`\`\`
8265
-
8266
- ### Multiple links and custom meta
8267
-
8268
11610
  \`\`\`book
8269
- Open Source Developer
11611
+ Pixel Assistant
8270
11612
 
8271
- META IMAGE /assets/dev-avatar.png
8272
- META LINK https://github.com/developer
8273
- META LINK https://twitter.com/devhandle
8274
- META AUTHOR Jane Smith
8275
- META VERSION 2.1
8276
- META LICENSE MIT
11613
+ META AVATAR PIXEL_ART
11614
+ GOAL Help users with concise answers.
8277
11615
  \`\`\`
8278
11616
 
8279
- ### Creative assistant
8280
-
8281
11617
  \`\`\`book
8282
- Creative Helper
11618
+ Orb Assistant
8283
11619
 
8284
- META IMAGE https://example.com/creative-bot.jpg
8285
- META TITLE Creative Writing Assistant
8286
- META DESCRIPTION Helps with brainstorming, storytelling, and creative projects
8287
- META INSPIRATION Books, movies, and real-world experiences
11620
+ META AVATAR orb
11621
+ GOAL Answer in a calm and focused way.
8288
11622
  \`\`\`
8289
11623
  `);
8290
11624
  }
8291
11625
  applyToAgentModelRequirements(requirements, content) {
8292
- // META commitments don't modify the system message or model requirements
8293
- // They are handled separately in the parsing logic for meta information extraction
8294
- // This method exists for consistency with the CommitmentDefinition interface
11626
+ // META AVATAR doesn't modify the system message or model requirements.
11627
+ // It's handled separately in the parsing logic for profile avatar resolution.
8295
11628
  return requirements;
8296
11629
  }
8297
- /**
8298
- * Extracts meta information from the content based on the meta type
8299
- * This is used by the parsing logic
8300
- */
8301
- extractMetaValue(metaType, content) {
8302
- const trimmedContent = content.trim();
8303
- return trimmedContent || null;
8304
- }
8305
- /**
8306
- * Validates if the provided content is a valid URL (for IMAGE and LINK types)
8307
- */
8308
- isValidUrl(content) {
8309
- try {
8310
- new URL(content.trim());
8311
- return true;
8312
- }
8313
- catch (_a) {
8314
- return false;
8315
- }
8316
- }
8317
- /**
8318
- * Checks if this is a known meta type
8319
- */
8320
- isKnownMetaType(metaType) {
8321
- const knownTypes = ['IMAGE', 'LINK', 'TITLE', 'DESCRIPTION', 'AUTHOR', 'VERSION', 'LICENSE'];
8322
- return knownTypes.includes(metaType.toUpperCase());
8323
- }
8324
11630
  }
8325
11631
  // Note: [💞] Ignore a discrepancy between file name and entity name
8326
11632
 
@@ -8993,7 +12299,13 @@ class ModelCommitmentDefinition extends BaseCommitmentDefinition {
8993
12299
  * Short one-line description of MODEL.
8994
12300
  */
8995
12301
  get description() {
8996
- return 'Enforce AI model requirements including name and technical parameters.';
12302
+ return 'Low-level commitment for explicit model selection and technical parameters. Use carefully.';
12303
+ }
12304
+ /**
12305
+ * Marks MODEL as a low-level commitment surfaced with caution.
12306
+ */
12307
+ get isLowLevel() {
12308
+ return true;
8997
12309
  }
8998
12310
  /**
8999
12311
  * Icon for this commitment.
@@ -9008,11 +12320,17 @@ class ModelCommitmentDefinition extends BaseCommitmentDefinition {
9008
12320
  return spaceTrim$1(`
9009
12321
  # ${this.type}
9010
12322
 
9011
- Enforces technical parameters for the AI model, ensuring consistent behavior across different execution environments.
12323
+ Low-level commitment for explicit AI model selection and technical parameters.
12324
+
12325
+ ## Status
12326
+
12327
+ - This commitment is low-level and not used by most of the users.
12328
+ - Use it when you need to pin a specific model or fine-tune model parameters directly.
12329
+ - Prefer automatic model selection when you do not need manual control.
9012
12330
 
9013
12331
  ## Key aspects
9014
12332
 
9015
- - When no \`MODEL\` commitment is specified, the best model requirement is picked automatically based on the agent \`PERSONA\`, \`KNOWLEDGE\`, \`TOOLS\` and other commitments
12333
+ - When no \`MODEL\` commitment is specified, the best model requirement is picked automatically based on the agent \`PERSONA\`, \`KNOWLEDGE\`, \`TOOLS\` and other commitments.
9016
12334
  - Multiple \`MODEL\` commitments can be used to specify different parameters
9017
12335
  - Both \`MODEL\` and \`MODELS\` terms work identically and can be used interchangeably
9018
12336
  - Parameters control the randomness, creativity, and technical aspects of model responses
@@ -9541,6 +12859,12 @@ class RuleCommitmentDefinition extends BaseCommitmentDefinition {
9541
12859
  get description() {
9542
12860
  return 'Add behavioral rules the agent must follow.';
9543
12861
  }
12862
+ /**
12863
+ * Marks RULE as one of the priority commitments surfaced first in catalogues.
12864
+ */
12865
+ get isImportant() {
12866
+ return true;
12867
+ }
9544
12868
  /**
9545
12869
  * Icon for this commitment.
9546
12870
  */
@@ -10160,6 +13484,46 @@ function createTeammateLabel(url) {
10160
13484
  }
10161
13485
  // Note: [💞] Ignore a discrepancy between file name and entity name
10162
13486
 
13487
+ /**
13488
+ * Header used for same-server TEAM calls that may access private teammate agents.
13489
+ *
13490
+ * @private internal Agents Server access wiring
13491
+ */
13492
+ const TEAM_INTERNAL_AGENT_ACCESS_HEADER = 'x-promptbook-team-agent-access-token';
13493
+ /**
13494
+ * Creates request headers for same-server TEAM calls.
13495
+ *
13496
+ * @param options - Target agent URL, local server URL, and resolved access token.
13497
+ * @returns Header map when the target is same-origin; otherwise an empty map.
13498
+ *
13499
+ * @private internal Agents Server access wiring
13500
+ */
13501
+ function createTeamInternalAgentAccessHeaders(options) {
13502
+ if (!options.accessToken || !isSameOriginAgentUrl(options.agentUrl, options.localServerUrl)) {
13503
+ return {};
13504
+ }
13505
+ return {
13506
+ [TEAM_INTERNAL_AGENT_ACCESS_HEADER]: options.accessToken,
13507
+ };
13508
+ }
13509
+ /**
13510
+ * Checks whether a teammate URL points back to the current Agents Server origin.
13511
+ *
13512
+ * @private internal Agents Server access wiring
13513
+ */
13514
+ function isSameOriginAgentUrl(agentUrl, localServerUrl) {
13515
+ if (!localServerUrl) {
13516
+ return false;
13517
+ }
13518
+ try {
13519
+ return new URL(agentUrl).origin === new URL(localServerUrl).origin;
13520
+ }
13521
+ catch (_a) {
13522
+ return false;
13523
+ }
13524
+ }
13525
+ // Note: [💞] Ignore a discrepancy between file name and entity name
13526
+
10163
13527
  /**
10164
13528
  * Map of team tool functions.
10165
13529
  */
@@ -10205,11 +13569,17 @@ class TeamCommitmentDefinition extends BaseCommitmentDefinition {
10205
13569
  get description() {
10206
13570
  return 'Enable the agent to consult teammate agents via dedicated tools.';
10207
13571
  }
13572
+ /**
13573
+ * Marks TEAM as one of the priority commitments surfaced first in catalogues.
13574
+ */
13575
+ get isImportant() {
13576
+ return true;
13577
+ }
10208
13578
  /**
10209
13579
  * Icon for this commitment.
10210
13580
  */
10211
13581
  get icon() {
10212
- return '??';
13582
+ return 'đŸ‘Ĩ';
10213
13583
  }
10214
13584
  /**
10215
13585
  * Markdown documentation for TEAM commitment.
@@ -10511,21 +13881,28 @@ function createPseudoVoidTeamToolResult(entry, request) {
10511
13881
  /**
10512
13882
  * Resolves a RemoteAgent for the given teammate URL, caching the connection.
10513
13883
  */
10514
- async function getRemoteTeammateAgent(agentUrl) {
10515
- const cached = remoteAgentsByUrl.get(agentUrl);
13884
+ async function getRemoteTeammateAgent(agentUrl, runtimeContext) {
13885
+ var _a, _b;
13886
+ const requestHeaders = createTeamInternalAgentAccessHeaders({
13887
+ agentUrl,
13888
+ localServerUrl: (_a = runtimeContext.agentsServer) === null || _a === void 0 ? void 0 : _a.localServerUrl,
13889
+ accessToken: (_b = runtimeContext.agentsServer) === null || _b === void 0 ? void 0 : _b.teamInternalAccessToken,
13890
+ });
13891
+ const cacheKey = `${agentUrl}|${requestHeaders[TEAM_INTERNAL_AGENT_ACCESS_HEADER] || ''}`;
13892
+ const cached = remoteAgentsByUrl.get(cacheKey);
10516
13893
  if (cached) {
10517
13894
  return cached;
10518
13895
  }
10519
13896
  const connection = (async () => {
10520
13897
  const { RemoteAgent } = await Promise.resolve().then(function () { return RemoteAgent$1; });
10521
- return RemoteAgent.connect({ agentUrl });
13898
+ return RemoteAgent.connect({ agentUrl, requestHeaders });
10522
13899
  })();
10523
- remoteAgentsByUrl.set(agentUrl, connection);
13900
+ remoteAgentsByUrl.set(cacheKey, connection);
10524
13901
  try {
10525
13902
  return await connection;
10526
13903
  }
10527
13904
  catch (error) {
10528
- remoteAgentsByUrl.delete(agentUrl);
13905
+ remoteAgentsByUrl.delete(cacheKey);
10529
13906
  throw error;
10530
13907
  }
10531
13908
  }
@@ -10555,8 +13932,9 @@ function createTeamToolFunction(entry) {
10555
13932
  let error = null;
10556
13933
  let toolCalls;
10557
13934
  try {
10558
- const remoteAgent = await getRemoteTeammateAgent(entry.teammate.url);
10559
- const prompt = buildTeammatePrompt(request, createTeamConversationRuntimeContext(args[TOOL_RUNTIME_CONTEXT_ARGUMENT]));
13935
+ const runtimeContext = createTeamConversationRuntimeContext(args[TOOL_RUNTIME_CONTEXT_ARGUMENT]);
13936
+ const remoteAgent = await getRemoteTeammateAgent(entry.teammate.url, runtimeContext);
13937
+ const prompt = buildTeammatePrompt(request, runtimeContext);
10560
13938
  const teammateResult = await remoteAgent.callChatModel(prompt);
10561
13939
  response = teammateResult.content || '';
10562
13940
  toolCalls =
@@ -10595,11 +13973,10 @@ function createTeamToolFunction(entry) {
10595
13973
  /**
10596
13974
  * TEMPLATE commitment definition
10597
13975
  *
10598
- * The TEMPLATE commitment enforces a specific response structure or template
10599
- * that the agent must follow when generating responses. This helps ensure
10600
- * consistent message formatting across all agent interactions.
13976
+ * Deprecated legacy commitment for response templates and output structure.
13977
+ * New books should prefer `WRITING SAMPLE` and `WRITING RULES`.
10601
13978
  *
10602
- * Example usage in agent source:
13979
+ * Legacy example usage in agent source:
10603
13980
  *
10604
13981
  * ```book
10605
13982
  * TEMPLATE Always structure your response with: 1) Summary, 2) Details, 3) Next steps
@@ -10619,7 +13996,16 @@ class TemplateCommitmentDefinition extends BaseCommitmentDefinition {
10619
13996
  * Short one-line description of TEMPLATE.
10620
13997
  */
10621
13998
  get description() {
10622
- return 'Enforce a specific message structure or response template.';
13999
+ return 'Deprecated legacy template commitment. Prefer `WRITING SAMPLE` and `WRITING RULES` for new books.';
14000
+ }
14001
+ /**
14002
+ * Optional UI/docs-only deprecation metadata.
14003
+ */
14004
+ get deprecation() {
14005
+ return {
14006
+ message: 'Use `WRITING SAMPLE` and `WRITING RULES` instead.',
14007
+ replacedBy: ['WRITING SAMPLE', 'WRITING RULES'],
14008
+ };
10623
14009
  }
10624
14010
  /**
10625
14011
  * Icon for this commitment.
@@ -10634,38 +14020,32 @@ class TemplateCommitmentDefinition extends BaseCommitmentDefinition {
10634
14020
  return spaceTrim$1(`
10635
14021
  # ${this.type}
10636
14022
 
10637
- Enforces a specific response structure or template that the agent must follow when generating responses.
14023
+ Deprecated legacy commitment for response structure and templates.
10638
14024
 
10639
- ## Key aspects
14025
+ ## Migration
10640
14026
 
10641
- - Both terms work identically and can be used interchangeably.
10642
- - Can be used with or without content.
10643
- - When used without content, enables template mode for structured responses.
10644
- - When used with content, defines the specific template structure to follow.
10645
- - Multiple templates can be combined, with later ones taking precedence.
14027
+ - Existing \`${this.type}\` and \`TEMPLATES\` books still parse and compile.
14028
+ - New books should use \`WRITING SAMPLE\` for concrete response exemplars and \`WRITING RULES\` for structure or formatting constraints.
14029
+ - Runtime behavior is intentionally unchanged for backward compatibility.
10646
14030
 
10647
- ## Examples
14031
+ ## Preferred replacement
10648
14032
 
10649
14033
  \`\`\`book
10650
14034
  Customer Support Agent
10651
14035
 
10652
- PERSONA You are a helpful customer support representative
10653
- TEMPLATE Always structure your response with: 1) Acknowledgment, 2) Solution, 3) Follow-up question
10654
- WRITING RULES Be professional and empathetic
14036
+ GOAL Help the user with support questions.
14037
+ WRITING SAMPLE
14038
+ Thanks for reaching out. Here is the summary, details, and next step.
14039
+ WRITING RULES Keep the response structured as: summary, details, next step.
10655
14040
  \`\`\`
10656
14041
 
10657
- \`\`\`book
10658
- Technical Documentation Assistant
10659
-
10660
- PERSONA You are a technical writing expert
10661
- TEMPLATE Use the following format: **Topic:** [topic] | **Explanation:** [details] | **Example:** [code]
10662
- FORMAT Use markdown with clear headings
10663
- \`\`\`
14042
+ ## Legacy compatibility example
10664
14043
 
10665
14044
  \`\`\`book
10666
- Simple Agent
14045
+ Customer Support Agent
10667
14046
 
10668
- PERSONA You are a virtual assistant
14047
+ GOAL Help the user with support questions.
14048
+ TEMPLATE Always structure your response with: 1) Acknowledgment, 2) Solution, 3) Follow-up question
10669
14049
  TEMPLATE
10670
14050
  \`\`\`
10671
14051
  `);
@@ -10713,120 +14093,6 @@ class TemplateCommitmentDefinition extends BaseCommitmentDefinition {
10713
14093
  }
10714
14094
  // Note: [💞] Ignore a discrepancy between file name and entity name
10715
14095
 
10716
- /**
10717
- * USE commitment definition
10718
- *
10719
- * The USE commitment indicates that the agent should utilize specific tools or capabilities
10720
- * to access and interact with external systems when necessary.
10721
- *
10722
- * Supported USE types:
10723
- * - USE BROWSER: Enables the agent to use a web browser tool
10724
- * - USE SEARCH ENGINE (future): Enables search engine access
10725
- * - USE DEEPSEARCH: Enables deeper research-oriented search access
10726
- * - USE FILE SYSTEM (future): Enables file system operations
10727
- * - USE MCP (future): Enables MCP server connections
10728
- *
10729
- * The content following the USE commitment is ignored (similar to NOTE).
10730
- *
10731
- * Example usage in agent source:
10732
- *
10733
- * ```book
10734
- * USE BROWSER
10735
- * USE SEARCH ENGINE
10736
- * ```
10737
- *
10738
- * @private [đŸĒ”] Maybe export the commitments through some package
10739
- */
10740
- class UseCommitmentDefinition extends BaseCommitmentDefinition {
10741
- constructor() {
10742
- super('USE');
10743
- }
10744
- /**
10745
- * Short one-line description of USE commitments.
10746
- */
10747
- get description() {
10748
- return 'Enable the agent to use specific tools or capabilities (BROWSER, SEARCH ENGINE, DEEPSEARCH, etc.).';
10749
- }
10750
- /**
10751
- * Icon for this commitment.
10752
- */
10753
- get icon() {
10754
- return '🔧';
10755
- }
10756
- /**
10757
- * Markdown documentation for USE commitment.
10758
- */
10759
- get documentation() {
10760
- return spaceTrim$1(`
10761
- # USE
10762
-
10763
- Enables the agent to use specific tools or capabilities for interacting with external systems.
10764
-
10765
- ## Supported USE types
10766
-
10767
- - **USE BROWSER** - Enables the agent to use a web browser tool to access and retrieve information from the internet
10768
- - **USE SEARCH ENGINE** (future) - Enables search engine access
10769
- - **USE DEEPSEARCH** - Enables deeper research-oriented search access
10770
- - **USE FILE SYSTEM** (future) - Enables file system operations
10771
- - **USE MCP** (future) - Enables MCP server connections
10772
-
10773
- ## Key aspects
10774
-
10775
- - The content following the USE commitment is ignored (similar to NOTE)
10776
- - Multiple USE commitments can be specified to enable multiple capabilities
10777
- - The actual tool usage is handled by the agent runtime
10778
-
10779
- ## Examples
10780
-
10781
- ### Basic browser usage
10782
-
10783
- \`\`\`book
10784
- Research Assistant
10785
-
10786
- PERSONA You are a helpful research assistant
10787
- USE BROWSER
10788
- KNOWLEDGE Can search the web for up-to-date information
10789
- \`\`\`
10790
-
10791
- ### Multiple tools
10792
-
10793
- \`\`\`book
10794
- Data Analyst
10795
-
10796
- PERSONA You are a data analyst assistant
10797
- USE BROWSER
10798
- USE FILE SYSTEM
10799
- ACTION Can analyze data from various sources
10800
- \`\`\`
10801
- `);
10802
- }
10803
- applyToAgentModelRequirements(requirements, content) {
10804
- // USE commitments don't modify the system message or model requirements directly
10805
- // They are handled separately in the parsing logic for capability extraction
10806
- // This method exists for consistency with the CommitmentDefinition interface
10807
- return requirements;
10808
- }
10809
- /**
10810
- * Extracts the tool type from the USE commitment
10811
- * This is used by the parsing logic
10812
- */
10813
- extractToolType(content) {
10814
- var _a, _b;
10815
- const trimmedContent = content.trim();
10816
- // The tool type is the first word after USE (already stripped)
10817
- const match = trimmedContent.match(/^(\w+)/);
10818
- 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;
10819
- }
10820
- /**
10821
- * Checks if this is a known USE type
10822
- */
10823
- isKnownUseType(useType) {
10824
- const knownTypes = ['BROWSER', 'SEARCH ENGINE', 'DEEPSEARCH', 'FILE SYSTEM', 'MCP'];
10825
- return knownTypes.includes(useType.toUpperCase());
10826
- }
10827
- }
10828
- // Note: [💞] Ignore a discrepancy between file name and entity name
10829
-
10830
14096
  /**
10831
14097
  * All `USE` commitment types currently participating in final system-message aggregation.
10832
14098
  *
@@ -16812,10 +20078,10 @@ const COMMITMENT_REGISTRY = [
16812
20078
  new MemoryCommitmentDefinition('MEMORIES'),
16813
20079
  new StyleCommitmentDefinition('STYLE'),
16814
20080
  new StyleCommitmentDefinition('STYLES'),
16815
- new RuleCommitmentDefinition('RULES'),
16816
20081
  new RuleCommitmentDefinition('RULE'),
16817
- new LanguageCommitmentDefinition('LANGUAGES'),
20082
+ new RuleCommitmentDefinition('RULES'),
16818
20083
  new LanguageCommitmentDefinition('LANGUAGE'),
20084
+ new LanguageCommitmentDefinition('LANGUAGES'),
16819
20085
  new WritingSampleCommitmentDefinition(),
16820
20086
  new WritingRulesCommitmentDefinition(),
16821
20087
  new SampleCommitmentDefinition('SAMPLE'),
@@ -16832,6 +20098,7 @@ const COMMITMENT_REGISTRY = [
16832
20098
  new ActionCommitmentDefinition('ACTION'),
16833
20099
  new ActionCommitmentDefinition('ACTIONS'),
16834
20100
  new ComponentCommitmentDefinition(),
20101
+ new MetaAvatarCommitmentDefinition(),
16835
20102
  new MetaImageCommitmentDefinition(),
16836
20103
  new MetaColorCommitmentDefinition(),
16837
20104
  new MetaFontCommitmentDefinition(),
@@ -16879,7 +20146,6 @@ const COMMITMENT_REGISTRY = [
16879
20146
  new UseMcpCommitmentDefinition(),
16880
20147
  new UsePrivacyCommitmentDefinition(),
16881
20148
  new UseProjectCommitmentDefinition(),
16882
- new UseCommitmentDefinition(),
16883
20149
  // Not yet implemented commitments (using placeholder)
16884
20150
  new NotYetImplementedCommitmentDefinition('EXPECT'),
16885
20151
  new NotYetImplementedCommitmentDefinition('BEHAVIOUR'),
@@ -16892,6 +20158,92 @@ const COMMITMENT_REGISTRY = [
16892
20158
  // TODO: [🧠] Maybe create through standardized $register
16893
20159
  // Note: [💞] Ignore a discrepancy between file name and entity name
16894
20160
 
20161
+ /**
20162
+ * Priority order for the important commitments shown first in catalogues and intellisense.
20163
+ *
20164
+ * Canonical singular names stay ahead of their plural aliases so the most important
20165
+ * commitments remain easy to scan.
20166
+ *
20167
+ * @private internal constant of commitment catalog sorting
20168
+ */
20169
+ const IMPORTANT_COMMITMENT_TYPE_SORT_ORDER = new Map([
20170
+ ['GOAL', 0],
20171
+ ['GOALS', 1],
20172
+ ['RULE', 2],
20173
+ ['RULES', 3],
20174
+ ['KNOWLEDGE', 4],
20175
+ ['TEAM', 5],
20176
+ ]);
20177
+ /**
20178
+ * Sort rank used when unfinished, low-level, and deprecated commitments should be grouped last.
20179
+ *
20180
+ * @private internal constant of commitment catalog sorting
20181
+ */
20182
+ const COMMITMENT_STATUS_SORT_ORDER = {
20183
+ normal: 0,
20184
+ deprecated: 1,
20185
+ unfinished: 2,
20186
+ lowLevel: 3,
20187
+ };
20188
+ /**
20189
+ * Resolves the relative sort rank of one commitment status.
20190
+ *
20191
+ * @param definition - Commitment definition to rank.
20192
+ * @param options - Sorting options.
20193
+ * @returns Relative sort rank for the definition.
20194
+ *
20195
+ * @private internal helper of commitment catalog sorting
20196
+ */
20197
+ function resolveCommitmentStatusSortRank(definition, options) {
20198
+ let statusSortRank = COMMITMENT_STATUS_SORT_ORDER.normal;
20199
+ if (options.isDeprecatedLast && definition.deprecation) {
20200
+ statusSortRank = Math.max(statusSortRank, COMMITMENT_STATUS_SORT_ORDER.deprecated);
20201
+ }
20202
+ if (options.isUnfinishedLast && definition.isUnfinished) {
20203
+ statusSortRank = Math.max(statusSortRank, COMMITMENT_STATUS_SORT_ORDER.unfinished);
20204
+ }
20205
+ if (options.isLowLevelLast && definition.isLowLevel) {
20206
+ statusSortRank = Math.max(statusSortRank, COMMITMENT_STATUS_SORT_ORDER.lowLevel);
20207
+ }
20208
+ return statusSortRank;
20209
+ }
20210
+ /**
20211
+ * Sorts commitment definitions so the important ones stay at the top.
20212
+ *
20213
+ * @param commitmentDefinitions - Definitions to sort.
20214
+ * @param options - Sorting options.
20215
+ * @returns Sorted commitment definitions.
20216
+ *
20217
+ * @private internal helper of commitment catalog sorting
20218
+ */
20219
+ function sortCommitmentDefinitions(commitmentDefinitions, options = {}) {
20220
+ return [...commitmentDefinitions]
20221
+ .map((definition, index) => ({
20222
+ definition,
20223
+ index,
20224
+ }))
20225
+ .sort((left, right) => {
20226
+ var _a, _b;
20227
+ if (left.definition.isImportant !== right.definition.isImportant) {
20228
+ return left.definition.isImportant ? -1 : 1;
20229
+ }
20230
+ if (left.definition.isImportant && right.definition.isImportant) {
20231
+ const leftPriority = (_a = IMPORTANT_COMMITMENT_TYPE_SORT_ORDER.get(left.definition.type)) !== null && _a !== void 0 ? _a : Number.MAX_SAFE_INTEGER;
20232
+ const rightPriority = (_b = IMPORTANT_COMMITMENT_TYPE_SORT_ORDER.get(right.definition.type)) !== null && _b !== void 0 ? _b : Number.MAX_SAFE_INTEGER;
20233
+ if (leftPriority !== rightPriority) {
20234
+ return leftPriority - rightPriority;
20235
+ }
20236
+ }
20237
+ const leftStatusSortRank = resolveCommitmentStatusSortRank(left.definition, options);
20238
+ const rightStatusSortRank = resolveCommitmentStatusSortRank(right.definition, options);
20239
+ if (leftStatusSortRank !== rightStatusSortRank) {
20240
+ return leftStatusSortRank - rightStatusSortRank;
20241
+ }
20242
+ return left.index - right.index;
20243
+ })
20244
+ .map(({ definition }) => definition);
20245
+ }
20246
+
16895
20247
  /**
16896
20248
  * Gets all available commitment definitions
16897
20249
  *
@@ -16900,7 +20252,7 @@ const COMMITMENT_REGISTRY = [
16900
20252
  * @public exported from `@promptbook/core`
16901
20253
  */
16902
20254
  function getAllCommitmentDefinitions() {
16903
- return $deepFreeze([...COMMITMENT_REGISTRY]);
20255
+ return $deepFreeze(sortCommitmentDefinitions(COMMITMENT_REGISTRY, { isUnfinishedLast: true, isLowLevelLast: true }));
16904
20256
  }
16905
20257
 
16906
20258
  /**
@@ -17812,6 +21164,7 @@ const SIMPLE_CAPABILITY_BY_COMMITMENT_TYPE = {
17812
21164
  * @private internal utility of `parseAgentSource`
17813
21165
  */
17814
21166
  const META_COMMITMENT_APPLIERS = {
21167
+ 'META AVATAR': applyMetaAvatarContent,
17815
21168
  'META LINK': applyMetaLinkContent,
17816
21169
  'META DOMAIN': applyMetaDomainContent,
17817
21170
  'META IMAGE': applyMetaImageContent,
@@ -18175,9 +21528,26 @@ function applyGenericMetaCommitment(state, content) {
18175
21528
  if (metaTypeRaw === 'LINK') {
18176
21529
  state.links.push(metaValue);
18177
21530
  }
21531
+ if (metaTypeRaw.toUpperCase() === 'AVATAR') {
21532
+ applyMetaAvatarContent(state, metaValue);
21533
+ return;
21534
+ }
18178
21535
  const metaType = normalizeTo_camelCase(metaTypeRaw);
18179
21536
  state.meta[metaType] = metaValue;
18180
21537
  }
21538
+ /**
21539
+ * Applies META AVATAR content into the canonical `meta.avatar` field.
21540
+ *
21541
+ * @private internal utility of `parseAgentSource`
21542
+ */
21543
+ function applyMetaAvatarContent(state, content) {
21544
+ const avatarVisualId = resolveAvatarVisualId(content);
21545
+ if (avatarVisualId) {
21546
+ state.meta.avatar = avatarVisualId;
21547
+ return;
21548
+ }
21549
+ delete state.meta.avatar;
21550
+ }
18181
21551
  /**
18182
21552
  * Applies META LINK content into links and the canonical `meta.link` field.
18183
21553
  *
@@ -18901,7 +22271,7 @@ function deduplicatePreparationToolCalls(toolCalls) {
18901
22271
  lastPreparationIndex = index;
18902
22272
  }
18903
22273
  else {
18904
- // Remove earlier duplicate — keep only the last (most recent) one.
22274
+ // Remove earlier duplicate - keep only the last (most recent) one.
18905
22275
  toolCalls.splice(index, 1);
18906
22276
  }
18907
22277
  }
@@ -24525,7 +27895,7 @@ function humanizeAiTextEllipsis(aiText) {
24525
27895
  // Note: [🏂] This function is not tested by itself but together with other cleanup functions with `humanizeAiText`
24526
27896
 
24527
27897
  /**
24528
- * Change dash-like characters to regular dashes `—` -> `-` and remove soft hyphens
27898
+ * Change dash-like characters to regular dashes `-` -> `-` and remove soft hyphens
24529
27899
  *
24530
27900
  * Note: [🔂] This function is idempotent.
24531
27901
  * Tip: If you want to do the full cleanup, look for `humanizeAiText` exported `@promptbook/markdown-utils`
@@ -24533,7 +27903,7 @@ function humanizeAiTextEllipsis(aiText) {
24533
27903
  * @public exported from `@promptbook/markdown-utils`
24534
27904
  */
24535
27905
  function humanizeAiTextEmdashed(aiText) {
24536
- return aiText.replace(/\u00AD/g, '').replace(/[â€â€‘â€’â€“â€”â€•âˆ’âƒīšŖīŧ]/g, '-');
27906
+ return aiText.replace(/\u00AD/g, '').replace(/[‐‑‒–-â€•âˆ’âƒīšŖīŧ]/g, '-');
24537
27907
  }
24538
27908
  // Note: [🏂] This function is not tested by itself but together with other cleanup functions with `humanizeAiText`
24539
27909
 
@@ -31246,6 +34616,7 @@ function buildRemoteAgentSource(profile, meta) {
31246
34616
  const isMetaImageExplicit = profile.isMetaImageExplicit !== false;
31247
34617
  const metaLines = [
31248
34618
  formatMetaLine('FULLNAME', meta === null || meta === void 0 ? void 0 : meta.fullname),
34619
+ formatMetaLine('AVATAR', meta === null || meta === void 0 ? void 0 : meta.avatar),
31249
34620
  formatMetaLine('IMAGE', isMetaImageExplicit ? meta === null || meta === void 0 ? void 0 : meta.image : undefined),
31250
34621
  formatMetaLine('DESCRIPTION', meta === null || meta === void 0 ? void 0 : meta.description),
31251
34622
  formatMetaLine('COLOR', meta === null || meta === void 0 ? void 0 : meta.color),
@@ -31286,7 +34657,7 @@ class RemoteAgent extends Agent {
31286
34657
  var _a, _b, _c;
31287
34658
  const agentProfileUrl = `${options.agentUrl}/api/profile`;
31288
34659
  const profileResponse = await fetch(agentProfileUrl, {
31289
- headers: attachClientVersionHeader(),
34660
+ headers: attachClientVersionHeader(options.requestHeaders),
31290
34661
  });
31291
34662
  // <- TODO: [🐱‍🚀] What about closed-source agents?
31292
34663
  // <- TODO: [🐱‍🚀] Maybe use promptbookFetch
@@ -31366,6 +34737,7 @@ class RemoteAgent extends Agent {
31366
34737
  this.avatarVisualId = undefined;
31367
34738
  this.knowledgeSources = [];
31368
34739
  this.agentUrl = options.agentUrl;
34740
+ this.requestHeaders = options.requestHeaders || {};
31369
34741
  }
31370
34742
  get agentName() {
31371
34743
  return this._remoteAgentName || super.agentName;
@@ -31402,7 +34774,7 @@ class RemoteAgent extends Agent {
31402
34774
  }
31403
34775
  const response = await fetch(`${this.agentUrl}/api/voice`, {
31404
34776
  method: 'POST',
31405
- headers: attachClientVersionHeader(),
34777
+ headers: attachClientVersionHeader(this.requestHeaders),
31406
34778
  body: formData,
31407
34779
  });
31408
34780
  if (!response.ok) {
@@ -31433,6 +34805,7 @@ class RemoteAgent extends Agent {
31433
34805
  const bookResponse = await fetch(`${this.agentUrl}/api/chat`, {
31434
34806
  method: 'POST',
31435
34807
  headers: attachClientVersionHeader({
34808
+ ...this.requestHeaders,
31436
34809
  'Content-Type': 'application/json',
31437
34810
  }),
31438
34811
  body: JSON.stringify({