@promptbook/core 0.112.0-98 → 0.112.0

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 (158) hide show
  1. package/README.md +37 -21
  2. package/esm/index.es.js +1767 -128
  3. package/esm/index.es.js.map +1 -1
  4. package/esm/servers.d.ts +1 -9
  5. package/esm/src/_packages/components.index.d.ts +4 -0
  6. package/esm/src/_packages/core.index.d.ts +0 -2
  7. package/esm/src/_packages/node.index.d.ts +40 -0
  8. package/esm/src/_packages/types.index.d.ts +18 -0
  9. package/esm/src/avatars/avatarAnimationScheduler.d.ts +4 -0
  10. package/esm/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  11. package/esm/src/avatars/visuals/octopus3d3AvatarVisual.d.ts +7 -0
  12. package/esm/src/avatars/visuals/octopus3d4AvatarVisual.d.ts +7 -0
  13. package/esm/src/book-3.0/BookNodeAgentSource.d.ts +38 -0
  14. package/esm/src/book-3.0/CliAgent.d.ts +66 -0
  15. package/esm/src/book-3.0/CliAgent.test.d.ts +1 -0
  16. package/esm/src/book-3.0/LiteAgent.d.ts +68 -0
  17. package/esm/src/book-3.0/LiteAgent.test.d.ts +1 -0
  18. package/esm/src/book-3.0/agentFolderPaths.d.ts +30 -0
  19. package/esm/src/book-3.0/cliAgentEnv.d.ts +33 -0
  20. package/esm/src/book-components/BookEditor/BookEditor.d.ts +6 -5
  21. package/esm/src/book-components/BookEditor/BookEditorAboutPromptbookInformation.d.ts +12 -0
  22. package/esm/src/book-components/BookEditor/BookEditorBrowserConfig.d.ts +2 -0
  23. package/esm/src/book-components/BookEditor/BookEditorForClient.d.ts +7 -0
  24. package/esm/src/book-components/BookEditor/BookEditorMonacoTokenization.d.ts +1 -0
  25. package/esm/src/book-components/BookEditor/BookEditorTheme.d.ts +24 -0
  26. package/esm/src/book-components/BookEditor/createDeprecatedCommitmentDiagnostics.browser.d.ts +9 -0
  27. package/esm/src/book-components/BookEditor/useBookEditorMonacoLanguage.d.ts +1 -6
  28. package/esm/src/book-components/BookEditor/useBookEditorMonacoLifecycle.d.ts +1 -4
  29. package/esm/src/book-components/BookEditor/useBookEditorMonacoStyles.d.ts +2 -1
  30. package/esm/src/book-components/Chat/Chat/ChatCitationModal.d.ts +2 -0
  31. package/esm/src/book-components/Chat/Chat/ChatMessageItem.d.ts +4 -0
  32. package/esm/src/book-components/Chat/Chat/ChatMessageList.d.ts +4 -0
  33. package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +5 -0
  34. package/esm/src/book-components/Chat/Chat/ChatToolCallModal.d.ts +2 -0
  35. package/esm/src/book-components/Chat/Chat/ChatToolCallModalContent.d.ts +3 -1
  36. package/esm/src/book-components/Chat/Chat/CitationIframePreview.d.ts +20 -0
  37. package/esm/src/book-components/Chat/Chat/TeamToolCallModalContent.d.ts +2 -0
  38. package/esm/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  39. package/esm/src/book-components/Chat/SourceChip/SourceChip.d.ts +6 -1
  40. package/esm/src/book-components/Chat/hooks/useResolvedCitationLabel.d.ts +12 -0
  41. package/esm/src/book-components/Chat/types/ChatMessage.d.ts +4 -0
  42. package/esm/src/book-components/Chat/types/ChatParticipant.d.ts +1 -1
  43. package/esm/src/book-components/Chat/types/CitationLabelResolver.d.ts +8 -0
  44. package/esm/src/book-components/Chat/utils/citationHelpers.d.ts +9 -0
  45. package/esm/src/book-components/Chat/utils/decodeJsonUnicodeEscapesInMarkdownText.d.ts +14 -0
  46. package/esm/src/book-components/Chat/utils/decodeJsonUnicodeEscapesInMarkdownText.test.d.ts +1 -0
  47. package/esm/src/book-components/Chat/utils/isVisibleChatToolCall.d.ts +10 -0
  48. package/esm/src/book-components/Chat/utils/parseCitationsFromContent.d.ts +4 -0
  49. package/esm/src/book-components/_common/Dropdown/Dropdown.d.ts +1 -1
  50. package/esm/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +1 -1
  51. package/esm/src/book-components/_common/Modal/Modal.d.ts +1 -1
  52. package/esm/src/book-components/icons/AboutIcon.d.ts +1 -1
  53. package/esm/src/book-components/icons/DownloadIcon.d.ts +1 -1
  54. package/esm/src/book-components/icons/ExitFullscreenIcon.d.ts +1 -1
  55. package/esm/src/book-components/icons/FullscreenIcon.d.ts +1 -1
  56. package/esm/src/cli/cli-commands/agent/agentCliOptions.d.ts +38 -0
  57. package/esm/src/cli/cli-commands/agent/chat.d.ts +10 -0
  58. package/esm/src/cli/cli-commands/agent/exec.d.ts +10 -0
  59. package/esm/src/cli/cli-commands/agent/run.test.d.ts +1 -0
  60. package/esm/src/cli/cli-commands/agent-folder/agentProjectPaths.d.ts +2 -24
  61. package/esm/src/cli/cli-commands/agent.d.ts +14 -0
  62. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer.d.ts +17 -1
  63. package/esm/src/cli/cli-commands/agents-server/run.d.ts +6 -0
  64. package/esm/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +9 -2
  65. package/esm/src/cli/cli-commands/coder/ThinkingLevel.d.ts +1 -1
  66. package/esm/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.d.ts +25 -0
  67. package/esm/src/cli/cli-commands/coder/find-unwritten.d.ts +10 -0
  68. package/esm/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +2 -0
  69. package/esm/src/cli/cli-commands/coder/server.d.ts +13 -0
  70. package/esm/src/cli/cli-commands/coder/waitOptions.d.ts +14 -0
  71. package/esm/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +9 -25
  72. package/esm/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -0
  73. package/esm/src/commitments/_common/teamInternalAgentAccess.d.ts +9 -1
  74. package/esm/src/llm-providers/_common/register/$provideLlmToolsConfigurationFromEnv.d.ts +1 -1
  75. package/esm/src/llm-providers/_common/register/$provideLlmToolsFromEnv.d.ts +1 -1
  76. package/esm/src/scrapers/website/utils/createShowdownConverter.d.ts +2 -2
  77. package/esm/src/utils/isTimingSafeEqualString.d.ts +25 -0
  78. package/esm/src/utils/validators/url/isValidAgentUrl.d.ts +5 -0
  79. package/esm/src/version.d.ts +1 -1
  80. package/package.json +1 -1
  81. package/umd/index.umd.js +1769 -129
  82. package/umd/index.umd.js.map +1 -1
  83. package/umd/servers.d.ts +1 -9
  84. package/umd/src/_packages/components.index.d.ts +4 -0
  85. package/umd/src/_packages/core.index.d.ts +0 -2
  86. package/umd/src/_packages/node.index.d.ts +40 -0
  87. package/umd/src/_packages/types.index.d.ts +18 -0
  88. package/umd/src/avatars/avatarAnimationScheduler.d.ts +4 -0
  89. package/umd/src/avatars/types/AvatarVisualDefinition.d.ts +1 -1
  90. package/umd/src/avatars/visuals/octopus3d3AvatarVisual.d.ts +7 -0
  91. package/umd/src/avatars/visuals/octopus3d4AvatarVisual.d.ts +7 -0
  92. package/umd/src/book-3.0/BookNodeAgentSource.d.ts +38 -0
  93. package/umd/src/book-3.0/CliAgent.d.ts +66 -0
  94. package/umd/src/book-3.0/CliAgent.test.d.ts +1 -0
  95. package/umd/src/book-3.0/LiteAgent.d.ts +68 -0
  96. package/umd/src/book-3.0/LiteAgent.test.d.ts +1 -0
  97. package/umd/src/book-3.0/agentFolderPaths.d.ts +30 -0
  98. package/umd/src/book-3.0/cliAgentEnv.d.ts +33 -0
  99. package/umd/src/book-components/BookEditor/BookEditor.d.ts +6 -5
  100. package/umd/src/book-components/BookEditor/BookEditorAboutPromptbookInformation.d.ts +12 -0
  101. package/umd/src/book-components/BookEditor/BookEditorBrowserConfig.d.ts +2 -0
  102. package/umd/src/book-components/BookEditor/BookEditorForClient.d.ts +7 -0
  103. package/umd/src/book-components/BookEditor/BookEditorMonacoTokenization.d.ts +1 -0
  104. package/umd/src/book-components/BookEditor/BookEditorTheme.d.ts +24 -0
  105. package/umd/src/book-components/BookEditor/createDeprecatedCommitmentDiagnostics.browser.d.ts +9 -0
  106. package/umd/src/book-components/BookEditor/useBookEditorMonacoLanguage.d.ts +1 -6
  107. package/umd/src/book-components/BookEditor/useBookEditorMonacoLifecycle.d.ts +1 -4
  108. package/umd/src/book-components/BookEditor/useBookEditorMonacoStyles.d.ts +2 -1
  109. package/umd/src/book-components/Chat/Chat/ChatCitationModal.d.ts +2 -0
  110. package/umd/src/book-components/Chat/Chat/ChatMessageItem.d.ts +4 -0
  111. package/umd/src/book-components/Chat/Chat/ChatMessageList.d.ts +4 -0
  112. package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +5 -0
  113. package/umd/src/book-components/Chat/Chat/ChatToolCallModal.d.ts +2 -0
  114. package/umd/src/book-components/Chat/Chat/ChatToolCallModalContent.d.ts +3 -1
  115. package/umd/src/book-components/Chat/Chat/CitationIframePreview.d.ts +20 -0
  116. package/umd/src/book-components/Chat/Chat/TeamToolCallModalContent.d.ts +2 -0
  117. package/umd/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  118. package/umd/src/book-components/Chat/SourceChip/SourceChip.d.ts +6 -1
  119. package/umd/src/book-components/Chat/hooks/useResolvedCitationLabel.d.ts +12 -0
  120. package/umd/src/book-components/Chat/types/ChatMessage.d.ts +4 -0
  121. package/umd/src/book-components/Chat/types/ChatParticipant.d.ts +1 -1
  122. package/umd/src/book-components/Chat/types/CitationLabelResolver.d.ts +8 -0
  123. package/umd/src/book-components/Chat/utils/citationHelpers.d.ts +9 -0
  124. package/umd/src/book-components/Chat/utils/decodeJsonUnicodeEscapesInMarkdownText.d.ts +14 -0
  125. package/umd/src/book-components/Chat/utils/decodeJsonUnicodeEscapesInMarkdownText.test.d.ts +1 -0
  126. package/umd/src/book-components/Chat/utils/isVisibleChatToolCall.d.ts +10 -0
  127. package/umd/src/book-components/Chat/utils/parseCitationsFromContent.d.ts +4 -0
  128. package/umd/src/book-components/_common/Dropdown/Dropdown.d.ts +1 -1
  129. package/umd/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +1 -1
  130. package/umd/src/book-components/_common/Modal/Modal.d.ts +1 -1
  131. package/umd/src/book-components/icons/AboutIcon.d.ts +1 -1
  132. package/umd/src/book-components/icons/DownloadIcon.d.ts +1 -1
  133. package/umd/src/book-components/icons/ExitFullscreenIcon.d.ts +1 -1
  134. package/umd/src/book-components/icons/FullscreenIcon.d.ts +1 -1
  135. package/umd/src/cli/cli-commands/agent/agentCliOptions.d.ts +38 -0
  136. package/umd/src/cli/cli-commands/agent/chat.d.ts +10 -0
  137. package/umd/src/cli/cli-commands/agent/exec.d.ts +10 -0
  138. package/umd/src/cli/cli-commands/agent/run.test.d.ts +1 -0
  139. package/umd/src/cli/cli-commands/agent-folder/agentProjectPaths.d.ts +2 -24
  140. package/umd/src/cli/cli-commands/agent.d.ts +14 -0
  141. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer.d.ts +17 -1
  142. package/umd/src/cli/cli-commands/agents-server/run.d.ts +6 -0
  143. package/umd/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +9 -2
  144. package/umd/src/cli/cli-commands/coder/ThinkingLevel.d.ts +1 -1
  145. package/umd/src/cli/cli-commands/coder/ensureCoderDeveloperAgentFile.d.ts +25 -0
  146. package/umd/src/cli/cli-commands/coder/find-unwritten.d.ts +10 -0
  147. package/umd/src/cli/cli-commands/coder/initializeCoderProjectConfiguration.d.ts +2 -0
  148. package/umd/src/cli/cli-commands/coder/server.d.ts +13 -0
  149. package/umd/src/cli/cli-commands/coder/waitOptions.d.ts +14 -0
  150. package/umd/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +9 -25
  151. package/umd/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -0
  152. package/umd/src/commitments/_common/teamInternalAgentAccess.d.ts +9 -1
  153. package/umd/src/llm-providers/_common/register/$provideLlmToolsConfigurationFromEnv.d.ts +1 -1
  154. package/umd/src/llm-providers/_common/register/$provideLlmToolsFromEnv.d.ts +1 -1
  155. package/umd/src/scrapers/website/utils/createShowdownConverter.d.ts +2 -2
  156. package/umd/src/utils/isTimingSafeEqualString.d.ts +25 -0
  157. package/umd/src/utils/validators/url/isValidAgentUrl.d.ts +5 -0
  158. package/umd/src/version.d.ts +1 -1
package/umd/index.umd.js CHANGED
@@ -1,13 +1,15 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('crypto-js'), require('crypto-js/enc-hex'), require('spacetrim'), require('crypto'), require('rxjs'), require('waitasecond'), require('crypto-js/sha256'), require('path'), require('mime-types'), require('papaparse'), require('moment'), require('colors'), require('@openai/agents'), require('bottleneck'), require('openai')) :
3
- typeof define === 'function' && define.amd ? define(['exports', 'crypto-js', 'crypto-js/enc-hex', 'spacetrim', 'crypto', 'rxjs', 'waitasecond', 'crypto-js/sha256', 'path', 'mime-types', 'papaparse', 'moment', 'colors', '@openai/agents', 'bottleneck', 'openai'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-core"] = {}, global.cryptoJs, global.hexEncoder, global.spacetrim, global.crypto, global.rxjs, global.waitasecond, global.sha256, global.path, global.mimeTypes, global.papaparse, global.moment, global.colors, global.agents, global.Bottleneck, global.OpenAI));
5
- })(this, (function (exports, cryptoJs, hexEncoder, spacetrim, crypto, rxjs, waitasecond, sha256, path, mimeTypes, papaparse, moment, colors, agents, Bottleneck, OpenAI) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('crypto-js'), require('crypto-js/enc-hex'), require('spacetrim'), require('rxjs'), require('waitasecond'), require('crypto-js/sha256'), require('path'), require('mime-types'), require('papaparse'), require('moment'), require('crypto'), require('colors'), require('@openai/agents'), require('bottleneck'), require('openai')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', 'crypto-js', 'crypto-js/enc-hex', 'spacetrim', 'rxjs', 'waitasecond', 'crypto-js/sha256', 'path', 'mime-types', 'papaparse', 'moment', 'crypto', 'colors', '@openai/agents', 'bottleneck', 'openai'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["promptbook-core"] = {}, global.CryptoJS, global.hexEncoder, global.spacetrim, global.rxjs, global.waitasecond, global.sha256, global.path, global.mimeTypes, global.papaparse, global.moment, global.crypto, global.colors, global.agents, global.Bottleneck, global.OpenAI));
5
+ })(this, (function (exports, CryptoJS, hexEncoder, spacetrim, rxjs, waitasecond, sha256, path, mimeTypes, papaparse, moment, crypto, colors, agents, Bottleneck, OpenAI) { 'use strict';
6
6
 
7
7
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
8
8
 
9
+ var CryptoJS__default = /*#__PURE__*/_interopDefaultLegacy(CryptoJS);
9
10
  var hexEncoder__default = /*#__PURE__*/_interopDefaultLegacy(hexEncoder);
10
11
  var sha256__default = /*#__PURE__*/_interopDefaultLegacy(sha256);
12
+ var papaparse__default = /*#__PURE__*/_interopDefaultLegacy(papaparse);
11
13
  var moment__default = /*#__PURE__*/_interopDefaultLegacy(moment);
12
14
  var colors__default = /*#__PURE__*/_interopDefaultLegacy(colors);
13
15
  var Bottleneck__default = /*#__PURE__*/_interopDefaultLegacy(Bottleneck);
@@ -27,7 +29,7 @@
27
29
  * @generated
28
30
  * @see https://github.com/webgptorg/promptbook
29
31
  */
30
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-98';
32
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0';
31
33
  /**
32
34
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
33
35
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -1652,7 +1654,7 @@
1652
1654
  * @public exported from `@promptbook/utils`
1653
1655
  */
1654
1656
  function computeHash(value) {
1655
- return cryptoJs.SHA256(hexEncoder__default["default"].parse(spacetrim.spaceTrim(valueToString(value)))).toString( /* hex */);
1657
+ return CryptoJS__default["default"].SHA256(hexEncoder__default["default"].parse(spacetrim.spaceTrim(valueToString(value)))).toString( /* hex */);
1656
1658
  }
1657
1659
  // TODO: [🥬][🥬] Use this ACRY
1658
1660
 
@@ -3772,7 +3774,7 @@
3772
3774
  * @private internal helper function
3773
3775
  */
3774
3776
  function $randomToken(randomness) {
3775
- return crypto.randomBytes(randomness).toString('hex');
3777
+ return CryptoJS__default["default"].lib.WordArray.random(randomness).toString(CryptoJS__default["default"].enc.Hex);
3776
3778
  }
3777
3779
  // TODO: [🤶] Maybe export through `@promptbook/utils` or `@promptbook/random` package
3778
3780
  // TODO: Maybe use nanoid instead https://github.com/ai/nanoid
@@ -5456,7 +5458,7 @@
5456
5458
  * @public exported from `@promptbook/editable`
5457
5459
  */
5458
5460
  function knowledgeSourceContentToName(knowledgeSourceContent) {
5459
- const hash = cryptoJs.SHA256(hexEncoder__default["default"].parse(JSON.stringify(knowledgeSourceContent)))
5461
+ const hash = CryptoJS__default["default"].SHA256(hexEncoder__default["default"].parse(JSON.stringify(knowledgeSourceContent)))
5460
5462
  // <- TODO: [🥬] Encapsulate sha256 to some private utility function
5461
5463
  .toString( /* hex */)
5462
5464
  .substring(0, 20);
@@ -6369,7 +6371,7 @@
6369
6371
  console.warn('CSV string contains carriage return characters, but in the CSV settings the `newline` setting does not include them. Autohealing the CSV string.');
6370
6372
  value = value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
6371
6373
  }
6372
- const csv = papaparse.parse(value, settings);
6374
+ const csv = papaparse__default["default"].parse(value, settings);
6373
6375
  return csv;
6374
6376
  }
6375
6377
 
@@ -6454,10 +6456,10 @@
6454
6456
  i > index ? { ...row, [outputParameterName]: PENDING_VALUE_PLACEHOLDER } : row,
6455
6457
  );
6456
6458
  */
6457
- await onProgress(papaparse.unparse(mappedData, { ...settings, ...MANDATORY_CSV_SETTINGS }));
6459
+ await onProgress(papaparse__default["default"].unparse(mappedData, { ...settings, ...MANDATORY_CSV_SETTINGS }));
6458
6460
  }
6459
6461
  }
6460
- return papaparse.unparse(mappedData, { ...settings, ...MANDATORY_CSV_SETTINGS });
6462
+ return papaparse__default["default"].unparse(mappedData, { ...settings, ...MANDATORY_CSV_SETTINGS });
6461
6463
  },
6462
6464
  },
6463
6465
  {
@@ -6485,7 +6487,7 @@
6485
6487
  return /* not await */ mapCallback({ [key]: value }, index, array.length);
6486
6488
  }));
6487
6489
  }));
6488
- return papaparse.unparse(mappedData, { ...settings, ...MANDATORY_CSV_SETTINGS });
6490
+ return papaparse__default["default"].unparse(mappedData, { ...settings, ...MANDATORY_CSV_SETTINGS });
6489
6491
  },
6490
6492
  },
6491
6493
  ],
@@ -9897,6 +9899,11 @@
9897
9899
  * - `isValidAgentUrl` *(this one)* which tests just agent URL
9898
9900
  * - `isValidPipelineUrl` which tests just pipeline URL
9899
9901
  *
9902
+ * Note: This is a pure structural validator and does not block private/internal network
9903
+ * addresses. Callers that fetch the URL server-side from an untrusted network context must
9904
+ * additionally apply the `assertSafeUrl` SSRF guard from the Agents Server before any
9905
+ * outbound request.
9906
+ *
9900
9907
  * @public exported from `@promptbook/utils`
9901
9908
  */
9902
9909
  function isValidAgentUrl(url) {
@@ -9910,12 +9917,6 @@
9910
9917
  // TODO: [🐠]
9911
9918
  return false;
9912
9919
  }
9913
- /*
9914
- Note: [👣][🧠] Is it secure to allow pipeline URLs on private and unsecured networks?
9915
- if (isUrlOnPrivateNetwork(url)) {
9916
- return false;
9917
- }
9918
- */
9919
9920
  return true;
9920
9921
  }
9921
9922
  // TODO: [🐠] Maybe more info why the URL is invalid
@@ -10091,8 +10092,11 @@
10091
10092
  if (!trimmedContent) {
10092
10093
  return requirements;
10093
10094
  }
10094
- // Add goal as a proper h2 section to the system message
10095
- const goalSection = `## Goal\n\n${trimmedContent}`;
10095
+ const goalSection = spacetrim.spaceTrim((block) => `
10096
+ ## Goal
10097
+
10098
+ ${block(trimmedContent)}
10099
+ `);
10096
10100
  const requirementsWithGoal = this.appendToSystemMessage(requirements, goalSection, '\n\n');
10097
10101
  return this.appendToPromptSuffix(requirementsWithGoal, trimmedContent);
10098
10102
  }
@@ -10614,8 +10618,11 @@
10614
10618
  if (!trimmedContent) {
10615
10619
  return requirements;
10616
10620
  }
10617
- // Add language as a bullet under a ## Language section
10618
- const languageSection = `## Language\n\n- Your language is ${trimmedContent}`;
10621
+ const languageSection = spacetrim.spaceTrim((block) => `
10622
+ ## Language
10623
+
10624
+ - Your language is ${block(trimmedContent)}
10625
+ `);
10619
10626
  return this.appendToSystemMessage(requirements, languageSection, '\n\n');
10620
10627
  }
10621
10628
  }
@@ -13062,21 +13069,22 @@
13062
13069
  * @private helper of `fractalAvatarVisual`
13063
13070
  */
13064
13071
  function drawDragonCurveLayer(context, points, options) {
13065
- const { size, primaryColor, secondaryColor, tertiaryColor, shadowColor, strokeWidth, timeMs, layerIndex } = options;
13072
+ const { primaryColor, secondaryColor, tertiaryColor, shadowColor, strokeWidth, timeMs, layerIndex } = options;
13066
13073
  const firstPoint = points[0];
13067
13074
  const lastPoint = points[points.length - 1];
13068
13075
  const ribbonGradient = context.createLinearGradient(firstPoint.x, firstPoint.y, lastPoint.x, lastPoint.y);
13069
13076
  ribbonGradient.addColorStop(0, `${primaryColor}f2`);
13070
13077
  ribbonGradient.addColorStop(0.5, `${secondaryColor}e6`);
13071
13078
  ribbonGradient.addColorStop(1, `${tertiaryColor}f2`);
13079
+ // Approximate the blurred shadow stroke with a wider semi-transparent stroke instead of
13080
+ // context.filter blur, which triggers a costly software rasterization pass every frame.
13072
13081
  context.save();
13073
13082
  context.beginPath();
13074
13083
  tracePolyline(context, points);
13075
- context.strokeStyle = `${shadowColor}82`;
13076
- context.lineWidth = strokeWidth * 1.8;
13084
+ context.strokeStyle = `${shadowColor}48`;
13085
+ context.lineWidth = strokeWidth * 4.5;
13077
13086
  context.lineJoin = 'round';
13078
13087
  context.lineCap = 'round';
13079
- context.filter = `blur(${size * 0.022}px)`;
13080
13088
  context.stroke();
13081
13089
  context.restore();
13082
13090
  context.beginPath();
@@ -13538,7 +13546,7 @@
13538
13546
  *
13539
13547
  * @private helper of `minecraft2AvatarVisual`
13540
13548
  */
13541
- const LIGHT_DIRECTION$2 = normalizeVector3({
13549
+ const LIGHT_DIRECTION$4 = normalizeVector3({
13542
13550
  x: 0.4,
13543
13551
  y: -0.65,
13544
13552
  z: 0.92,
@@ -13660,11 +13668,23 @@
13660
13668
  * @private helper of `minecraft2AvatarVisual`
13661
13669
  */
13662
13670
  function drawMinecraftShadow(context, size, palette, interaction, timeMs) {
13671
+ const cx = size * 0.5 + interaction.gazeX * size * 0.03;
13672
+ const cy = size * 0.85 + Math.sin(timeMs / 880) * size * 0.01;
13673
+ const rx = size * (0.16 + interaction.intensity * 0.015);
13674
+ const ry = size * 0.055;
13675
+ // Radial gradient approximates the blurry ellipse shadow without context.filter blur.
13663
13676
  context.save();
13664
- context.fillStyle = `${palette.shadow}66`;
13665
- context.filter = `blur(${size * 0.02}px)`;
13677
+ context.translate(cx, cy);
13678
+ context.scale(1, ry / rx);
13679
+ const blurRadius = rx * 1.4;
13680
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
13681
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
13682
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
13683
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
13684
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
13685
+ context.fillStyle = shadowGradient;
13666
13686
  context.beginPath();
13667
- context.ellipse(size * 0.5 + interaction.gazeX * size * 0.03, size * 0.85 + Math.sin(timeMs / 880) * size * 0.01, size * (0.16 + interaction.intensity * 0.015), size * 0.055, 0, 0, Math.PI * 2);
13687
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
13668
13688
  context.fill();
13669
13689
  context.restore();
13670
13690
  }
@@ -13750,7 +13770,7 @@
13750
13770
  corners: projectedCorners,
13751
13771
  texture: faceDefinition.texture,
13752
13772
  averageDepth: transformedCorners.reduce((depthSum, corner) => depthSum + corner.z, 0) / transformedCorners.length,
13753
- lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$2), -1, 1),
13773
+ lightIntensity: clampNumber$1(dotProduct3D(faceNormal, LIGHT_DIRECTION$4), -1, 1),
13754
13774
  outlineColor: cuboid.outlineColor,
13755
13775
  };
13756
13776
  });
@@ -13888,13 +13908,27 @@
13888
13908
  spotlight.addColorStop(1, `${palette.highlight}00`);
13889
13909
  context.fillStyle = spotlight;
13890
13910
  context.fillRect(0, 0, size, size);
13891
- context.save();
13892
- context.fillStyle = 'rgba(0, 0, 0, 0.22)';
13893
- context.filter = `blur(${size * 0.018}px)`;
13894
- context.beginPath();
13895
- context.ellipse(size * 0.5, size * 0.86, size * 0.2, size * 0.06, 0, 0, Math.PI * 2);
13896
- context.fill();
13897
- context.restore();
13911
+ {
13912
+ // Radial gradient approximates the blurry ellipse shadow without context.filter blur.
13913
+ const cx = size * 0.5;
13914
+ const cy = size * 0.86;
13915
+ const rx = size * 0.2;
13916
+ const ry = size * 0.06;
13917
+ const blurRadius = rx * 1.4;
13918
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
13919
+ shadowGradient.addColorStop(0, 'rgba(0,0,0,0.28)');
13920
+ shadowGradient.addColorStop(0.45, 'rgba(0,0,0,0.14)');
13921
+ shadowGradient.addColorStop(0.8, 'rgba(0,0,0,0.05)');
13922
+ shadowGradient.addColorStop(1, 'rgba(0,0,0,0)');
13923
+ context.save();
13924
+ context.translate(cx, cy);
13925
+ context.scale(1, ry / rx);
13926
+ context.fillStyle = shadowGradient;
13927
+ context.beginPath();
13928
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
13929
+ context.fill();
13930
+ context.restore();
13931
+ }
13898
13932
  drawVoxelCuboid(context, {
13899
13933
  x: bodyX,
13900
13934
  y: bodyY,
@@ -14423,7 +14457,7 @@
14423
14457
  *
14424
14458
  * @private helper of `octopus3AvatarVisual`
14425
14459
  */
14426
- function formatAlphaHex$1(opacity) {
14460
+ function formatAlphaHex$2(opacity) {
14427
14461
  return Math.round(Math.min(1, Math.max(0, opacity)) * 255)
14428
14462
  .toString(16)
14429
14463
  .padStart(2, '0');
@@ -14802,7 +14836,7 @@
14802
14836
  context.beginPath();
14803
14837
  context.moveTo(-radiusX * 0.74, radiusY * 0.2);
14804
14838
  context.quadraticCurveTo(0, radiusY * 0.38, radiusX * 0.74, radiusY * 0.2);
14805
- context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
14839
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$2(eyeStyle.lowerLidOpacity)}`;
14806
14840
  context.lineWidth = radiusX * 0.08;
14807
14841
  context.lineCap = 'round';
14808
14842
  context.stroke();
@@ -14883,7 +14917,7 @@
14883
14917
  context.beginPath();
14884
14918
  context.moveTo(-projectedRadiusX * 0.74, projectedRadiusY * 0.2);
14885
14919
  context.quadraticCurveTo(0, projectedRadiusY * 0.38, projectedRadiusX * 0.74, projectedRadiusY * 0.2);
14886
- context.strokeStyle = `${palette.highlight}${formatAlphaHex(eyeStyle.lowerLidOpacity)}`;
14920
+ context.strokeStyle = `${palette.highlight}${formatAlphaHex$1(eyeStyle.lowerLidOpacity)}`;
14887
14921
  context.lineWidth = projectedRadiusX * 0.08;
14888
14922
  context.lineCap = 'round';
14889
14923
  context.stroke();
@@ -14929,7 +14963,7 @@
14929
14963
  *
14930
14964
  * @private helper of the 3D octopus avatar visuals
14931
14965
  */
14932
- function formatAlphaHex(opacity) {
14966
+ function formatAlphaHex$1(opacity) {
14933
14967
  return Math.round(clampNumber$1(opacity, 0, 1) * 255)
14934
14968
  .toString(16)
14935
14969
  .padStart(2, '0');
@@ -14941,11 +14975,40 @@
14941
14975
  *
14942
14976
  * @private helper of `octopus3dAvatarVisual`
14943
14977
  */
14944
- const LIGHT_DIRECTION$1 = normalizeVector3({
14978
+ const LIGHT_DIRECTION$3 = normalizeVector3({
14945
14979
  x: 0.48,
14946
14980
  y: -0.62,
14947
14981
  z: 0.94,
14948
14982
  });
14983
+ /**
14984
+ * Cache keyed by the `createRandom` factory reference (stable per mounted `<Avatar/>`).
14985
+ *
14986
+ * @private helper of `octopus3dAvatarVisual`
14987
+ */
14988
+ const octopus3dStableStateCache = new WeakMap();
14989
+ /**
14990
+ * Returns the stable per-avatar state, computing it on first access and caching for subsequent frames.
14991
+ *
14992
+ * @private helper of `octopus3dAvatarVisual`
14993
+ */
14994
+ function getOctopus3dStableState(createRandom) {
14995
+ const cached = octopus3dStableStateCache.get(createRandom);
14996
+ if (cached !== undefined) {
14997
+ return cached;
14998
+ }
14999
+ const animationRandom = createRandom('octopus3d-animation-profile');
15000
+ const eyeRandom = createRandom('octopus3d-eye-profile');
15001
+ const leftEyePhaseOffset = eyeRandom() * 0.6;
15002
+ const rightEyePhaseOffset = eyeRandom() * 0.6;
15003
+ const state = {
15004
+ morphologyProfile: createOctopus3MorphologyProfile(createRandom),
15005
+ animationPhase: animationRandom() * Math.PI * 2,
15006
+ leftEyePhaseOffset,
15007
+ rightEyePhaseOffset,
15008
+ };
15009
+ octopus3dStableStateCache.set(createRandom, state);
15010
+ return state;
15011
+ }
14949
15012
  /**
14950
15013
  * Proper 3D Octopus visual built from projected organic meshes and tentacles.
14951
15014
  *
@@ -14958,10 +15021,7 @@
14958
15021
  isAnimated: true,
14959
15022
  supportsPointerTracking: true,
14960
15023
  render({ context, size, palette, createRandom, timeMs, interaction }) {
14961
- const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
14962
- const animationRandom = createRandom('octopus3d-animation-profile');
14963
- const eyeRandom = createRandom('octopus3d-eye-profile');
14964
- const animationPhase = animationRandom() * Math.PI * 2;
15024
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset } = getOctopus3dStableState(createRandom);
14965
15025
  const sceneCenterX = size * 0.5;
14966
15026
  const sceneCenterY = size * 0.56;
14967
15027
  const bob = Math.sin(timeMs / 920 + animationPhase) * size * 0.014;
@@ -15058,12 +15118,12 @@
15058
15118
  x: -faceEyeSpacing,
15059
15119
  y: faceEyeYOffset,
15060
15120
  z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, -faceEyeSpacing, faceEyeYOffset),
15061
- }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + eyeRandom() * 0.6, interaction, morphologyProfile.face.eyeStyle);
15121
+ }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
15062
15122
  drawProjectedOrganicEye(context, {
15063
15123
  x: faceEyeSpacing,
15064
15124
  y: faceEyeYOffset,
15065
15125
  z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, faceEyeSpacing, faceEyeYOffset),
15066
- }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.7 + eyeRandom() * 0.6, interaction, morphologyProfile.face.eyeStyle);
15126
+ }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.7 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
15067
15127
  drawProjectedOrganicMouth(context, [
15068
15128
  {
15069
15129
  x: -mouthHalfWidth,
@@ -15107,14 +15167,28 @@
15107
15167
  /**
15108
15168
  * Draws the soft ground shadow below the octopus.
15109
15169
  *
15170
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
15171
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
15172
+ *
15110
15173
  * @private helper of `octopus3dAvatarVisual`
15111
15174
  */
15112
15175
  function drawOctopus3dShadow(context, size, palette, interaction, timeMs) {
15176
+ const cx = size * 0.5 + interaction.gazeX * size * 0.04;
15177
+ const cy = size * 0.87 + Math.sin(timeMs / 920) * size * 0.008;
15178
+ const rx = size * (0.18 + interaction.intensity * 0.02);
15179
+ const ry = size * 0.06;
15113
15180
  context.save();
15114
- context.fillStyle = `${palette.shadow}66`;
15115
- context.filter = `blur(${size * 0.022}px)`;
15181
+ context.translate(cx, cy);
15182
+ context.scale(1, ry / rx);
15183
+ const blurRadius = rx * 1.4;
15184
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
15185
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
15186
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
15187
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
15188
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
15189
+ context.fillStyle = shadowGradient;
15116
15190
  context.beginPath();
15117
- context.ellipse(size * 0.5 + interaction.gazeX * size * 0.04, size * 0.87 + Math.sin(timeMs / 920) * size * 0.008, size * (0.18 + interaction.intensity * 0.02), size * 0.06, 0, 0, Math.PI * 2);
15191
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
15118
15192
  context.fill();
15119
15193
  context.restore();
15120
15194
  }
@@ -15151,7 +15225,7 @@
15151
15225
  corners: projectedCorners,
15152
15226
  averageDepth: transformedCorners.reduce((depthSum, transformedCorner) => depthSum + transformedCorner.z, 0) /
15153
15227
  transformedCorners.length,
15154
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
15228
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$3), -1, 1),
15155
15229
  fillStyle: resolveSurfacePatchFillStyle(palette, verticalProgress + verticalColorBias),
15156
15230
  outlineColor,
15157
15231
  });
@@ -15340,11 +15414,40 @@
15340
15414
  *
15341
15415
  * @private helper of `octopus3d2AvatarVisual`
15342
15416
  */
15343
- const LIGHT_DIRECTION = normalizeVector3({
15417
+ const LIGHT_DIRECTION$2 = normalizeVector3({
15344
15418
  x: 0.38,
15345
15419
  y: -0.6,
15346
15420
  z: 0.98,
15347
15421
  });
15422
+ /**
15423
+ * Cache keyed by the `createRandom` factory reference (stable per mounted `<Avatar/>`).
15424
+ *
15425
+ * @private helper of `octopus3d2AvatarVisual`
15426
+ */
15427
+ const octopus3d2StableStateCache = new WeakMap();
15428
+ /**
15429
+ * Returns the stable per-avatar state, computing it on first access and caching for subsequent frames.
15430
+ *
15431
+ * @private helper of `octopus3d2AvatarVisual`
15432
+ */
15433
+ function getOctopus3d2StableState(createRandom) {
15434
+ const cached = octopus3d2StableStateCache.get(createRandom);
15435
+ if (cached !== undefined) {
15436
+ return cached;
15437
+ }
15438
+ const animationRandom = createRandom('octopus3d2-animation-profile');
15439
+ const eyeRandom = createRandom('octopus3d2-eye-profile');
15440
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
15441
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
15442
+ const state = {
15443
+ morphologyProfile: createOctopus3MorphologyProfile(createRandom),
15444
+ animationPhase: animationRandom() * Math.PI * 2,
15445
+ leftEyePhaseOffset,
15446
+ rightEyePhaseOffset,
15447
+ };
15448
+ octopus3d2StableStateCache.set(createRandom, state);
15449
+ return state;
15450
+ }
15348
15451
  /**
15349
15452
  * Octopus 3D 2 avatar visual.
15350
15453
  *
@@ -15357,10 +15460,7 @@
15357
15460
  isAnimated: true,
15358
15461
  supportsPointerTracking: true,
15359
15462
  render({ context, size, palette, createRandom, timeMs, interaction }) {
15360
- const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
15361
- const animationRandom = createRandom('octopus3d2-animation-profile');
15362
- const eyeRandom = createRandom('octopus3d2-eye-profile');
15363
- const animationPhase = animationRandom() * Math.PI * 2;
15463
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset } = getOctopus3d2StableState(createRandom);
15364
15464
  const sceneCenterX = size * 0.5;
15365
15465
  const sceneCenterY = size * 0.575;
15366
15466
  const bob = Math.sin(timeMs / 940 + animationPhase) * size * 0.013;
@@ -15413,8 +15513,8 @@
15413
15513
  const rightEyeLocalCenter = sampleBlobbyOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude);
15414
15514
  const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
15415
15515
  const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
15416
- drawProjectedOrganicEye(context, leftEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + eyeRandom() * 0.7, interaction, morphologyProfile.face.eyeStyle);
15417
- drawProjectedOrganicEye(context, rightEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.9 + eyeRandom() * 0.7, interaction, morphologyProfile.face.eyeStyle);
15516
+ drawProjectedOrganicEye(context, leftEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
15517
+ drawProjectedOrganicEye(context, rightEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.9 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
15418
15518
  drawProjectedOrganicMouth(context, [
15419
15519
  sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
15420
15520
  sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
@@ -15443,54 +15543,118 @@
15443
15543
  /**
15444
15544
  * Draws the soft floor shadow that anchors the single mesh in the frame.
15445
15545
  *
15546
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
15547
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
15548
+ *
15446
15549
  * @private helper of `octopus3d2AvatarVisual`
15447
15550
  */
15448
15551
  function drawBlobbyOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
15552
+ const cx = size * 0.5 + interaction.gazeX * size * 0.045;
15553
+ const cy = size * 0.88 + Math.sin(timeMs / 940) * size * 0.008;
15554
+ const rx = size * (0.18 + (morphologyProfile.body.horizontalStretch - 1) * 0.04 + interaction.intensity * 0.018);
15555
+ const ry = size * 0.062;
15449
15556
  context.save();
15450
- context.fillStyle = `${palette.shadow}66`;
15451
- context.filter = `blur(${size * 0.024}px)`;
15557
+ context.translate(cx, cy);
15558
+ context.scale(1, ry / rx);
15559
+ const blurRadius = rx * 1.4;
15560
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
15561
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
15562
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
15563
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
15564
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
15565
+ context.fillStyle = shadowGradient;
15452
15566
  context.beginPath();
15453
- context.ellipse(size * 0.5 + interaction.gazeX * size * 0.045, size * 0.88 + Math.sin(timeMs / 940) * size * 0.008, size * (0.18 + (morphologyProfile.body.horizontalStretch - 1) * 0.04 + interaction.intensity * 0.018), size * 0.062, 0, 0, Math.PI * 2);
15567
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
15454
15568
  context.fill();
15455
15569
  context.restore();
15456
15570
  }
15571
+ /**
15572
+ * Number of latitude segments used by the single blobby octopus mesh.
15573
+ *
15574
+ * @private helper of `octopus3d2AvatarVisual`
15575
+ */
15576
+ const LATITUDE_PATCH_COUNT$2 = 12;
15577
+ /**
15578
+ * Number of longitude segments used by the single blobby octopus mesh.
15579
+ *
15580
+ * @private helper of `octopus3d2AvatarVisual`
15581
+ */
15582
+ const LONGITUDE_PATCH_COUNT$2 = 24;
15457
15583
  /**
15458
15584
  * Resolves all visible projected patches for the single blobby octopus mesh.
15459
15585
  *
15586
+ * Within a single frame, mesh corner samples and longitude-only lobe-wave values are
15587
+ * quantized to the patch grid and computed once each rather than re-evaluated for every
15588
+ * patch corner — the patch loop alone would call `sampleBlobbyOctopusSurfacePoint`
15589
+ * `latitudePatchCount * longitudePatchCount * 4` times without caching, even though most
15590
+ * corners are shared between neighboring patches.
15591
+ *
15460
15592
  * @private helper of `octopus3d2AvatarVisual`
15461
15593
  */
15462
15594
  function resolveVisibleBlobbyOctopusPatches(options) {
15463
15595
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, animationPhase, timeMs, } = options;
15464
- const latitudePatchCount = 12;
15465
- const longitudePatchCount = 24;
15596
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$2;
15597
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$2;
15466
15598
  const surfacePatches = [];
15599
+ const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
15600
+ const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
15601
+ for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
15602
+ latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
15603
+ }
15604
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
15605
+ longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
15606
+ }
15607
+ const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
15608
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
15609
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveLowerLobeWave(longitudeBoundaries[boundaryIndex], morphologyProfile, animationPhase, timeMs);
15610
+ }
15611
+ const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
15612
+ const transformedCornerSamples = new Array(cornerCount);
15613
+ for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
15614
+ const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex];
15615
+ for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
15616
+ const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
15617
+ const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
15618
+ const cornerSample = sampleBlobbyOctopusSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
15619
+ transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
15620
+ }
15621
+ }
15467
15622
  for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
15468
- const startLatitude = -Math.PI / 2 + (latitudeIndex / latitudePatchCount) * Math.PI;
15469
- const endLatitude = -Math.PI / 2 + ((latitudeIndex + 1) / latitudePatchCount) * Math.PI;
15623
+ const startLatitude = latitudeBoundaries[latitudeIndex];
15624
+ const endLatitude = latitudeBoundaries[latitudeIndex + 1];
15470
15625
  const centerLatitude = (startLatitude + endLatitude) / 2;
15471
15626
  const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
15627
+ const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
15628
+ const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
15472
15629
  for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
15473
- const startLongitude = -Math.PI + (longitudeIndex / longitudePatchCount) * Math.PI * 2;
15474
- const endLongitude = -Math.PI + ((longitudeIndex + 1) / longitudePatchCount) * Math.PI * 2;
15630
+ const startLongitude = longitudeBoundaries[longitudeIndex];
15631
+ const endLongitude = longitudeBoundaries[longitudeIndex + 1];
15475
15632
  const centerLongitude = (startLongitude + endLongitude) / 2;
15476
- const localCorners = [
15477
- sampleBlobbyOctopusSurfacePoint(options, startLatitude, startLongitude),
15478
- sampleBlobbyOctopusSurfacePoint(options, startLatitude, endLongitude),
15479
- sampleBlobbyOctopusSurfacePoint(options, endLatitude, endLongitude),
15480
- sampleBlobbyOctopusSurfacePoint(options, endLatitude, startLongitude),
15633
+ const transformedCorners = [
15634
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex],
15635
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1],
15636
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1],
15637
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex],
15481
15638
  ];
15482
- const transformedCorners = localCorners.map((localCorner) => transformScenePoint(localCorner, center, rotationX, rotationY));
15483
15639
  const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
15484
15640
  if (surfaceNormal.z <= 0.01) {
15485
15641
  continue;
15486
15642
  }
15487
- const projectedCorners = transformedCorners.map((transformedCorner) => projectScenePoint(transformedCorner, size, sceneCenterX, sceneCenterY));
15643
+ const projectedCorners = [
15644
+ projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
15645
+ projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
15646
+ projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
15647
+ projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
15648
+ ];
15488
15649
  surfacePatches.push({
15489
15650
  corners: projectedCorners,
15490
- averageDepth: transformedCorners.reduce((depthSum, transformedCorner) => depthSum + transformedCorner.z, 0) /
15491
- transformedCorners.length,
15492
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1),
15493
- fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
15651
+ averageDepth: (transformedCorners[0].z +
15652
+ transformedCorners[1].z +
15653
+ transformedCorners[2].z +
15654
+ transformedCorners[3].z) /
15655
+ 4,
15656
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
15657
+ fillStyle: resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
15494
15658
  outlineColor: verticalProgress < 0.58 ? `${palette.highlight}73` : `${palette.shadow}8a`,
15495
15659
  });
15496
15660
  }
@@ -15506,12 +15670,21 @@
15506
15670
  * @private helper of `octopus3d2AvatarVisual`
15507
15671
  */
15508
15672
  function sampleBlobbyOctopusSurfacePoint(options, latitude, longitude) {
15673
+ const { morphologyProfile, animationPhase, timeMs } = options;
15674
+ return sampleBlobbyOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, resolveLowerLobeWave(longitude, morphologyProfile, animationPhase, timeMs));
15675
+ }
15676
+ /**
15677
+ * Samples one point on the continuous Octopus 3D 2 surface using a precomputed lower-lobe wave
15678
+ * to skip the per-call trig evaluation for `latitudePatchCount + 1` longitude-shared corners.
15679
+ *
15680
+ * @private helper of `octopus3d2AvatarVisual`
15681
+ */
15682
+ function sampleBlobbyOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, lowerLobeWave) {
15509
15683
  const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
15510
15684
  const cosineLatitude = Math.max(0, Math.cos(latitude));
15511
15685
  const verticalProgress = (Math.sin(latitude) + 1) / 2;
15512
15686
  const upperBlend = Math.pow(1 - verticalProgress, 1.2);
15513
15687
  const lowerBlend = Math.pow(verticalProgress, 1.42);
15514
- const lowerLobeWave = resolveLowerLobeWave(longitude, morphologyProfile, animationPhase, timeMs);
15515
15688
  const skirtEnvelope = Math.pow(cosineLatitude, 0.5) * lowerBlend;
15516
15689
  const horizontalScale = 1.02 +
15517
15690
  skirtEnvelope * (0.34 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.22 + lowerLobeWave * 0.22) -
@@ -15549,7 +15722,7 @@
15549
15722
  *
15550
15723
  * @private helper of `octopus3d2AvatarVisual`
15551
15724
  */
15552
- function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, lowerLobeWave) {
15725
+ function resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, forwardness, lowerLobeWave) {
15553
15726
  const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.12 - forwardness * 0.07, 0, 1);
15554
15727
  if (tonalProgress < 0.16) {
15555
15728
  return palette.highlight;
@@ -15589,6 +15762,1384 @@
15589
15762
  context.restore();
15590
15763
  }
15591
15764
 
15765
+ /* eslint-disable no-magic-numbers */
15766
+ /**
15767
+ * Light direction used by the continuous octopus mesh shading.
15768
+ *
15769
+ * @private helper of `octopus3d3AvatarVisual`
15770
+ */
15771
+ const LIGHT_DIRECTION$1 = normalizeVector3({
15772
+ x: 0.34,
15773
+ y: -0.62,
15774
+ z: 1,
15775
+ });
15776
+ /**
15777
+ * Real-octopus tentacle count used by the continuous lower mesh.
15778
+ *
15779
+ * @private helper of `octopus3d3AvatarVisual`
15780
+ */
15781
+ const OCTOPUS_TENTACLE_COUNT$1 = 8;
15782
+ /**
15783
+ * Cache keyed by the `createRandom` factory reference, which is stable for the lifetime of one
15784
+ * mounted `<Avatar/>` component (created inside `resolveAvatarRenderDefinition` and held in a
15785
+ * React `useMemo`). Using a `WeakMap` ensures the entry is collected when the component unmounts.
15786
+ *
15787
+ * @private helper of `octopus3d3AvatarVisual`
15788
+ */
15789
+ const stableStateCache = new WeakMap();
15790
+ /**
15791
+ * Returns the stable per-avatar state, computing it on first access and returning the cached
15792
+ * result on every subsequent call within the same `<Avatar/>` mount.
15793
+ *
15794
+ * @private helper of `octopus3d3AvatarVisual`
15795
+ */
15796
+ function getOctopus3d3StableState(createRandom) {
15797
+ const cached = stableStateCache.get(createRandom);
15798
+ if (cached !== undefined) {
15799
+ return cached;
15800
+ }
15801
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
15802
+ const animationRandom = createRandom('octopus3d3-animation-profile');
15803
+ const eyeRandom = createRandom('octopus3d3-eye-profile');
15804
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
15805
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
15806
+ const state = {
15807
+ morphologyProfile,
15808
+ animationPhase: animationRandom() * Math.PI * 2,
15809
+ leftEyePhaseOffset,
15810
+ rightEyePhaseOffset,
15811
+ tentacleProfiles: createContinuousTentacleProfiles(createRandom, morphologyProfile),
15812
+ };
15813
+ stableStateCache.set(createRandom, state);
15814
+ return state;
15815
+ }
15816
+ /**
15817
+ * Octopus 3D 3 avatar visual.
15818
+ *
15819
+ * @private built-in avatar visual
15820
+ */
15821
+ const octopus3d3AvatarVisual = {
15822
+ id: 'octopus3d3',
15823
+ title: 'Octopus 3D 3',
15824
+ description: 'Cute continuous 3D octopus with a blobby single mesh, waving tentacle lobes, rich shading, and cursor-aware eyes.',
15825
+ isAnimated: true,
15826
+ supportsPointerTracking: true,
15827
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
15828
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles } = getOctopus3d3StableState(createRandom);
15829
+ const sceneCenterX = size * 0.5;
15830
+ const sceneCenterY = size * 0.535;
15831
+ const bob = Math.sin(timeMs / 960 + animationPhase) * size * 0.012;
15832
+ const meshCenter = {
15833
+ x: interaction.bodyOffsetX * size * 0.048 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
15834
+ y: -size * 0.07 + interaction.bodyOffsetY * size * 0.026 + bob,
15835
+ z: interaction.intensity * size * 0.018,
15836
+ };
15837
+ const rotationY = -0.1 +
15838
+ Math.sin(timeMs / 2700 + animationPhase) * 0.035 +
15839
+ interaction.bodyOffsetX * 0.22 +
15840
+ interaction.gazeX * 0.88;
15841
+ const rotationX = -0.07 +
15842
+ Math.cos(timeMs / 3100 + animationPhase * 0.7) * 0.018 -
15843
+ interaction.bodyOffsetY * 0.08 -
15844
+ interaction.gazeY * 0.38;
15845
+ const surfaceOptions = {
15846
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.1,
15847
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.08,
15848
+ radiusZ: size *
15849
+ morphologyProfile.body.bodyRadiusRatio *
15850
+ (1.02 + (morphologyProfile.body.horizontalStretch - 1) * 0.18),
15851
+ morphologyProfile,
15852
+ timeMs,
15853
+ animationPhase,
15854
+ tentacleProfiles,
15855
+ };
15856
+ const surfacePatches = resolveVisibleContinuousOctopusPatches({
15857
+ ...surfaceOptions,
15858
+ center: meshCenter,
15859
+ rotationX,
15860
+ rotationY,
15861
+ sceneCenterX,
15862
+ sceneCenterY,
15863
+ size,
15864
+ palette,
15865
+ });
15866
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.2 - 0.03, -0.22, 0.08);
15867
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.1, 0.18, 0.32);
15868
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.2 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
15869
+ const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
15870
+ const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
15871
+ const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
15872
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.76;
15873
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.9;
15874
+ drawAvatarFrame(context, size, palette);
15875
+ drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
15876
+ drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile);
15877
+ for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
15878
+ drawContinuousSurfacePatch(context, surfacePatch);
15879
+ }
15880
+ drawProjectedSurfaceCurrents({
15881
+ context,
15882
+ surfaceOptions,
15883
+ center: meshCenter,
15884
+ rotationX,
15885
+ rotationY,
15886
+ sceneCenterX,
15887
+ sceneCenterY,
15888
+ size,
15889
+ palette,
15890
+ morphologyProfile,
15891
+ timeMs,
15892
+ animationPhase,
15893
+ });
15894
+ drawProjectedTentacleSuckers({
15895
+ context,
15896
+ surfaceOptions,
15897
+ center: meshCenter,
15898
+ rotationX,
15899
+ rotationY,
15900
+ sceneCenterX,
15901
+ sceneCenterY,
15902
+ size,
15903
+ palette,
15904
+ });
15905
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
15906
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
15907
+ drawProjectedOrganicMouth(context, [
15908
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
15909
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
15910
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
15911
+ ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
15912
+ },
15913
+ };
15914
+ /**
15915
+ * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
15916
+ *
15917
+ * @private helper of `octopus3d3AvatarVisual`
15918
+ */
15919
+ function createContinuousTentacleProfiles(createRandom, morphologyProfile) {
15920
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT$1 }, (_, tentacleIndex) => {
15921
+ const tentacleRandom = createRandom(`octopus3d3-tentacle-${tentacleIndex}`);
15922
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT$1 - 1);
15923
+ return {
15924
+ centerLongitude: -Math.PI * 0.86 +
15925
+ progress * Math.PI * 1.72 +
15926
+ (tentacleRandom() - 0.5) * (0.08 + morphologyProfile.tentacles.rootSpreadScale * 0.03),
15927
+ widthScale: 0.86 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.16,
15928
+ lengthScale: 0.86 + tentacleRandom() * 0.36 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.22,
15929
+ swayScale: 0.82 + tentacleRandom() * 0.38 + (morphologyProfile.tentacles.swayScale - 1) * 0.2,
15930
+ depthScale: 0.86 + tentacleRandom() * 0.32 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.2,
15931
+ phase: tentacleRandom() * Math.PI * 2,
15932
+ suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
15933
+ };
15934
+ });
15935
+ }
15936
+ /**
15937
+ * Draws the soft underwater atmosphere behind the continuous octopus mesh.
15938
+ *
15939
+ * @private helper of `octopus3d3AvatarVisual`
15940
+ */
15941
+ function drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
15942
+ const glowGradient = context.createRadialGradient(sceneCenterX + interaction.gazeX * size * 0.11, sceneCenterY - size * 0.17 + interaction.gazeY * size * 0.05, size * 0.04, sceneCenterX, sceneCenterY, size * (0.66 + interaction.intensity * 0.02));
15943
+ glowGradient.addColorStop(0, `${palette.highlight}66`);
15944
+ glowGradient.addColorStop(0.34, `${palette.accent}2e`);
15945
+ glowGradient.addColorStop(1, `${palette.highlight}00`);
15946
+ context.fillStyle = glowGradient;
15947
+ context.fillRect(0, 0, size, size);
15948
+ const lowerGradient = context.createRadialGradient(sceneCenterX + Math.sin(timeMs / 1550) * size * 0.05, sceneCenterY + size * 0.29, size * 0.06, sceneCenterX, sceneCenterY + size * 0.3, size * 0.54);
15949
+ lowerGradient.addColorStop(0, `${palette.secondary}25`);
15950
+ lowerGradient.addColorStop(1, `${palette.secondary}00`);
15951
+ context.fillStyle = lowerGradient;
15952
+ context.fillRect(0, 0, size, size);
15953
+ }
15954
+ /**
15955
+ * Draws the soft lower shadow that anchors the octopus in the avatar frame.
15956
+ *
15957
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
15958
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
15959
+ *
15960
+ * @private helper of `octopus3d3AvatarVisual`
15961
+ */
15962
+ function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
15963
+ const cx = size * 0.5 + interaction.gazeX * size * 0.045;
15964
+ const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
15965
+ const rx = size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02);
15966
+ const ry = size * 0.06;
15967
+ // Scale the context so that drawing a circle produces the correct ellipse aspect ratio,
15968
+ // then fill with a radial gradient that approximates the blurry edge without context.filter.
15969
+ context.save();
15970
+ context.translate(cx, cy);
15971
+ context.scale(1, ry / rx);
15972
+ const blurRadius = rx * 1.4;
15973
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
15974
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
15975
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
15976
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
15977
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
15978
+ context.fillStyle = shadowGradient;
15979
+ context.beginPath();
15980
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
15981
+ context.fill();
15982
+ context.restore();
15983
+ }
15984
+ /**
15985
+ * Number of latitude segments used by the continuous Octopus 3D 3 mesh.
15986
+ *
15987
+ * @private helper of `octopus3d3AvatarVisual`
15988
+ */
15989
+ const LATITUDE_PATCH_COUNT$1 = 16;
15990
+ /**
15991
+ * Number of longitude segments used by the continuous Octopus 3D 3 mesh.
15992
+ *
15993
+ * @private helper of `octopus3d3AvatarVisual`
15994
+ */
15995
+ const LONGITUDE_PATCH_COUNT$1 = 40;
15996
+ /**
15997
+ * Resolves visible projected patches for the continuous octopus mesh.
15998
+ *
15999
+ * Within a single frame, mesh corner samples and longitude-only computations (tentacle
16000
+ * influence and lobe wave) are quantized to the patch grid and computed once each rather
16001
+ * than re-evaluated for every patch corner — the patch loop alone calls `sampleContinuousOctopusSurfacePoint`
16002
+ * for `latitudePatchCount * longitudePatchCount * 4` corners without caching, and each call
16003
+ * triggers an inner 8-iteration `Math.exp` loop, which dominates the per-frame cost.
16004
+ *
16005
+ * @private helper of `octopus3d3AvatarVisual`
16006
+ */
16007
+ function resolveVisibleContinuousOctopusPatches(options) {
16008
+ const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
16009
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
16010
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
16011
+ const surfacePatches = [];
16012
+ const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
16013
+ const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
16014
+ for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
16015
+ latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
16016
+ }
16017
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
16018
+ longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
16019
+ }
16020
+ const cachedTentacleInfluencesByCornerLongitude = new Array(longitudePatchCount + 1);
16021
+ const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
16022
+ const cachedTentacleInfluencesByPatchCenterLongitude = new Array(longitudePatchCount);
16023
+ const cachedLobeWavesByPatchCenterLongitude = new Float64Array(longitudePatchCount);
16024
+ const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
16025
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
16026
+ const cornerLongitude = longitudeBoundaries[boundaryIndex];
16027
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveContinuousTentacleInfluence(options, cornerLongitude);
16028
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveContinuousLobeWave(options, cornerLongitude);
16029
+ }
16030
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
16031
+ const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
16032
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveContinuousTentacleInfluence(options, patchCenterLongitude);
16033
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveContinuousLobeWave(options, patchCenterLongitude);
16034
+ cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
16035
+ }
16036
+ const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
16037
+ const transformedCornerSamples = new Array(cornerCount);
16038
+ for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
16039
+ const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex];
16040
+ for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
16041
+ const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
16042
+ const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
16043
+ const cornerSample = sampleContinuousOctopusSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
16044
+ transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
16045
+ }
16046
+ }
16047
+ for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
16048
+ const startLatitude = latitudeBoundaries[latitudeIndex];
16049
+ const endLatitude = latitudeBoundaries[latitudeIndex + 1];
16050
+ const centerLatitude = (startLatitude + endLatitude) / 2;
16051
+ const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
16052
+ const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
16053
+ const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
16054
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
16055
+ const transformedCorners = [
16056
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex],
16057
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1],
16058
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1],
16059
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex],
16060
+ ];
16061
+ const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
16062
+ if (surfaceNormal.z <= 0.008) {
16063
+ continue;
16064
+ }
16065
+ const projectedCorners = [
16066
+ projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
16067
+ projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
16068
+ projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
16069
+ projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
16070
+ ];
16071
+ surfacePatches.push({
16072
+ corners: projectedCorners,
16073
+ averageDepth: (transformedCorners[0].z +
16074
+ transformedCorners[1].z +
16075
+ transformedCorners[2].z +
16076
+ transformedCorners[3].z) /
16077
+ 4,
16078
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
16079
+ fillStyle: resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
16080
+ outlineColor: verticalProgress < 0.54 ? `${palette.highlight}69` : `${palette.shadow}78`,
16081
+ });
16082
+ }
16083
+ }
16084
+ return surfacePatches;
16085
+ }
16086
+ /**
16087
+ * Samples one point on the continuous Octopus 3D 3 surface.
16088
+ *
16089
+ * The lower hemisphere is pulled into eight seeded waving lobes, so the portrait reads as
16090
+ * tentacled while still being rendered as one connected blobby mesh.
16091
+ *
16092
+ * @private helper of `octopus3d3AvatarVisual`
16093
+ */
16094
+ function sampleContinuousOctopusSurfacePoint(options, latitude, longitude) {
16095
+ return sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, resolveContinuousTentacleInfluence(options, longitude), resolveContinuousLobeWave(options, longitude));
16096
+ }
16097
+ /**
16098
+ * Samples one point on the continuous Octopus 3D 3 surface using precomputed longitude-only
16099
+ * values to skip the per-call `Math.exp` tentacle-influence loop and the lobe-wave trig call.
16100
+ *
16101
+ * The patch loop quantizes the mesh into a fixed `(latitudePatchCount + 1) * (longitudePatchCount + 1)`
16102
+ * corner grid, so the same longitude is reused across every latitude row and each
16103
+ * tentacle/lobe value can be computed once per frame instead of `latitudePatchCount * 4` times.
16104
+ *
16105
+ * @private helper of `octopus3d3AvatarVisual`
16106
+ */
16107
+ function sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
16108
+ const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
16109
+ const cosineLatitude = Math.max(0, Math.cos(latitude));
16110
+ const verticalProgress = (Math.sin(latitude) + 1) / 2;
16111
+ const upperBlend = Math.pow(1 - verticalProgress, 1.28);
16112
+ const lowerBlend = smoothStep$1(0.38, 1, verticalProgress);
16113
+ const tipBlend = smoothStep$1(0.68, 1, verticalProgress);
16114
+ const centerPull = resolveSignedAngularDistance$1(longitude, tentacleInfluence.centerLongitude);
16115
+ const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.24 + tipBlend * 0.2);
16116
+ const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
16117
+ animationPhase * 0.6 +
16118
+ timeMs / (1750 + morphologyProfile.body.lobeCount * 30)) *
16119
+ (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.8) *
16120
+ (0.3 + lowerBlend * 0.7);
16121
+ const tentacleWave = Math.sin(timeMs / 760 + tentacleInfluence.phase + verticalProgress * 2.4) *
16122
+ lowerBlend *
16123
+ tentacleInfluence.core *
16124
+ tentacleInfluence.swayScale;
16125
+ const horizontalScale = 1.04 +
16126
+ mantleRipple +
16127
+ lowerBlend * (0.16 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.1) +
16128
+ lowerBlend * tentacleInfluence.core * (0.2 + lowerLobeWave * 0.12) -
16129
+ upperBlend * 0.08;
16130
+ const depthScale = 1.06 +
16131
+ upperBlend * 0.16 +
16132
+ Math.max(0, Math.cos(effectiveLongitude)) * 0.1 +
16133
+ lowerBlend * tentacleInfluence.core * (0.1 + tentacleInfluence.depthScale * 0.06) -
16134
+ Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
16135
+ const tentacleTubeRadius = lowerBlend * tentacleInfluence.core * (0.11 + tipBlend * 0.06 + tentacleInfluence.widthScale * 0.025) * radiusX;
16136
+ const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
16137
+ const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.72;
16138
+ const lowerDrop = lowerBlend *
16139
+ radiusY *
16140
+ (0.18 +
16141
+ tentacleInfluence.core *
16142
+ (0.38 +
16143
+ tentacleInfluence.lengthScale * 0.22 +
16144
+ (morphologyProfile.tentacles.flowLengthScale - 1) * 0.08));
16145
+ return {
16146
+ x: Math.sin(effectiveLongitude) * planarRadiusX + tentacleWave * radiusX * (0.052 + tipBlend * 0.05),
16147
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.12) -
16148
+ upperBlend * radiusY * 0.1 +
16149
+ lowerDrop +
16150
+ Math.sin(timeMs / 1420 + animationPhase + latitude * 1.6) * lowerBlend * radiusY * 0.018 +
16151
+ Math.cos(timeMs / 880 + tentacleInfluence.phase) *
16152
+ lowerBlend *
16153
+ tipBlend *
16154
+ tentacleInfluence.core *
16155
+ radiusY *
16156
+ 0.034,
16157
+ z: Math.cos(effectiveLongitude) * planarRadiusZ +
16158
+ Math.cos(timeMs / 980 + tentacleInfluence.phase + verticalProgress) *
16159
+ lowerBlend *
16160
+ tentacleInfluence.core *
16161
+ radiusZ *
16162
+ 0.04,
16163
+ };
16164
+ }
16165
+ /**
16166
+ * Blends nearby seeded tentacle profiles at one mesh longitude.
16167
+ *
16168
+ * @private helper of `octopus3d3AvatarVisual`
16169
+ */
16170
+ function resolveContinuousTentacleInfluence(options, longitude) {
16171
+ let totalWeight = 0;
16172
+ let weightedSin = 0;
16173
+ let weightedCos = 0;
16174
+ let weightedWidthScale = 0;
16175
+ let weightedLengthScale = 0;
16176
+ let weightedSwayScale = 0;
16177
+ let weightedDepthScale = 0;
16178
+ let weightedPhase = 0;
16179
+ for (const tentacleProfile of options.tentacleProfiles) {
16180
+ const distance = Math.abs(resolveSignedAngularDistance$1(longitude, tentacleProfile.centerLongitude));
16181
+ const width = 0.2 * tentacleProfile.widthScale;
16182
+ const weight = Math.exp(-(distance * distance) / (width * width));
16183
+ totalWeight += weight;
16184
+ weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
16185
+ weightedCos += Math.cos(tentacleProfile.centerLongitude) * weight;
16186
+ weightedWidthScale += tentacleProfile.widthScale * weight;
16187
+ weightedLengthScale += tentacleProfile.lengthScale * weight;
16188
+ weightedSwayScale += tentacleProfile.swayScale * weight;
16189
+ weightedDepthScale += tentacleProfile.depthScale * weight;
16190
+ weightedPhase += tentacleProfile.phase * weight;
16191
+ }
16192
+ if (totalWeight < 0.0001) {
16193
+ return {
16194
+ core: 0,
16195
+ centerLongitude: longitude,
16196
+ widthScale: 1,
16197
+ lengthScale: 1,
16198
+ swayScale: 1,
16199
+ depthScale: 1,
16200
+ phase: 0,
16201
+ };
16202
+ }
16203
+ return {
16204
+ core: clampNumber$1(totalWeight, 0, 1),
16205
+ centerLongitude: Math.atan2(weightedSin / totalWeight, weightedCos / totalWeight),
16206
+ widthScale: weightedWidthScale / totalWeight,
16207
+ lengthScale: weightedLengthScale / totalWeight,
16208
+ swayScale: weightedSwayScale / totalWeight,
16209
+ depthScale: weightedDepthScale / totalWeight,
16210
+ phase: weightedPhase / totalWeight,
16211
+ };
16212
+ }
16213
+ /**
16214
+ * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
16215
+ *
16216
+ * @private helper of `octopus3d3AvatarVisual`
16217
+ */
16218
+ function resolveContinuousLobeWave(options, longitude) {
16219
+ const { morphologyProfile, animationPhase, timeMs } = options;
16220
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT$1 +
16221
+ animationPhase +
16222
+ timeMs / (980 + morphologyProfile.body.lobeCount * 18)) +
16223
+ 1) /
16224
+ 2);
16225
+ }
16226
+ /**
16227
+ * Resolves one base fill tone for a patch on the continuous octopus mesh.
16228
+ *
16229
+ * @private helper of `octopus3d3AvatarVisual`
16230
+ */
16231
+ function resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
16232
+ const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
16233
+ if (tonalProgress < 0.14) {
16234
+ return palette.highlight;
16235
+ }
16236
+ if (tonalProgress < 0.32) {
16237
+ return palette.secondary;
16238
+ }
16239
+ if (tonalProgress < 0.72) {
16240
+ return forwardness > 0.55 ? palette.secondary : palette.primary;
16241
+ }
16242
+ return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
16243
+ }
16244
+ /**
16245
+ * Draws one projected mesh patch with soft shading and a subtle edge.
16246
+ *
16247
+ * @private helper of `octopus3d3AvatarVisual`
16248
+ */
16249
+ function drawContinuousSurfacePatch(context, surfacePatch) {
16250
+ drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
16251
+ if (surfacePatch.lightIntensity > 0) {
16252
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.18 * surfacePatch.lightIntensity})`);
16253
+ }
16254
+ else if (surfacePatch.lightIntensity < 0) {
16255
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.25 * Math.abs(surfacePatch.lightIntensity)})`);
16256
+ }
16257
+ context.save();
16258
+ context.beginPath();
16259
+ context.moveTo(surfacePatch.corners[0].x, surfacePatch.corners[0].y);
16260
+ for (let cornerIndex = 1; cornerIndex < surfacePatch.corners.length; cornerIndex++) {
16261
+ context.lineTo(surfacePatch.corners[cornerIndex].x, surfacePatch.corners[cornerIndex].y);
16262
+ }
16263
+ context.closePath();
16264
+ context.strokeStyle = surfacePatch.outlineColor;
16265
+ context.lineWidth = Math.max(0.7, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0032);
16266
+ context.lineJoin = 'round';
16267
+ context.stroke();
16268
+ context.restore();
16269
+ }
16270
+ /**
16271
+ * Draws projected mantle-current lines on the front of the mesh.
16272
+ *
16273
+ * @private helper of `octopus3d3AvatarVisual`
16274
+ */
16275
+ function drawProjectedSurfaceCurrents(options) {
16276
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
16277
+ const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
16278
+ const centerIndex = (currentCount - 1) / 2;
16279
+ context.save();
16280
+ context.lineCap = 'round';
16281
+ context.lineJoin = 'round';
16282
+ for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
16283
+ const baseLongitude = (currentIndex - centerIndex) * 0.15;
16284
+ const projectedPoints = [];
16285
+ for (let sampleIndex = 0; sampleIndex < 8; sampleIndex++) {
16286
+ const progress = sampleIndex / 7;
16287
+ const latitude = -0.46 + progress * 0.74;
16288
+ const longitude = baseLongitude + Math.sin(timeMs / 1160 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.035;
16289
+ const scenePoint = transformScenePoint(sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
16290
+ if (scenePoint.z > center.z - size * 0.016) {
16291
+ projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
16292
+ }
16293
+ }
16294
+ if (projectedPoints.length < 3) {
16295
+ continue;
16296
+ }
16297
+ context.beginPath();
16298
+ context.moveTo(projectedPoints[0].x, projectedPoints[0].y);
16299
+ for (const projectedPoint of projectedPoints.slice(1)) {
16300
+ context.lineTo(projectedPoint.x, projectedPoint.y);
16301
+ }
16302
+ context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}3d` : `${palette.accent}33`;
16303
+ context.lineWidth = size * (0.0055 + currentIndex * 0.00045);
16304
+ context.stroke();
16305
+ }
16306
+ context.restore();
16307
+ }
16308
+ /**
16309
+ * Draws small projected sucker highlights on the waving lower mesh lobes.
16310
+ *
16311
+ * @private helper of `octopus3d3AvatarVisual`
16312
+ */
16313
+ function drawProjectedTentacleSuckers(options) {
16314
+ const { surfaceOptions, size } = options;
16315
+ const { timeMs } = surfaceOptions;
16316
+ for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
16317
+ if (Math.cos(tentacleProfile.centerLongitude) < -0.12) {
16318
+ continue;
16319
+ }
16320
+ for (let suckerIndex = 0; suckerIndex < 3; suckerIndex++) {
16321
+ const latitude = 0.52 + suckerIndex * 0.14;
16322
+ const sideOffset = tentacleProfile.suckerSide * (0.035 + suckerIndex * 0.012) * tentacleProfile.widthScale;
16323
+ const waveOffset = Math.sin(timeMs / 900 + tentacleProfile.phase + suckerIndex * 0.8) * 0.018;
16324
+ drawProjectedSurfaceSpot({
16325
+ ...options,
16326
+ latitude,
16327
+ longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
16328
+ radiusScale: size * (0.0065 - suckerIndex * 0.0007),
16329
+ });
16330
+ }
16331
+ }
16332
+ }
16333
+ /**
16334
+ * Draws one tiny projected surface spot by sampling local mesh tangents.
16335
+ *
16336
+ * @private helper of `octopus3d3AvatarVisual`
16337
+ */
16338
+ function drawProjectedSurfaceSpot(options) {
16339
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
16340
+ const localCenter = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude);
16341
+ const localHorizontal = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
16342
+ const localVertical = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
16343
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
16344
+ if (sceneCenterPoint.z <= center.z - size * 0.012) {
16345
+ return;
16346
+ }
16347
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
16348
+ const projectedHorizontalPoint = projectScenePoint(transformScenePoint(localHorizontal, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
16349
+ const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
16350
+ const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
16351
+ radiusScale *
16352
+ 0.74, size * 0.003, size * 0.018);
16353
+ const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
16354
+ radiusScale *
16355
+ 0.52, size * 0.0024, size * 0.014);
16356
+ const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
16357
+ context.save();
16358
+ context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
16359
+ context.rotate(rotation);
16360
+ context.beginPath();
16361
+ context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
16362
+ context.fillStyle = `${palette.highlight}73`;
16363
+ context.fill();
16364
+ context.strokeStyle = `${palette.highlight}99`;
16365
+ context.lineWidth = Math.max(0.7, size * 0.0028);
16366
+ context.stroke();
16367
+ context.restore();
16368
+ }
16369
+ /**
16370
+ * Resolves a signed angular distance from the source longitude to the target longitude.
16371
+ *
16372
+ * @private helper of `octopus3d3AvatarVisual`
16373
+ */
16374
+ function resolveSignedAngularDistance$1(sourceLongitude, targetLongitude) {
16375
+ return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
16376
+ }
16377
+ /**
16378
+ * Smoothly maps a value between two bounds into `[0, 1]`.
16379
+ *
16380
+ * @private helper of `octopus3d3AvatarVisual`
16381
+ */
16382
+ function smoothStep$1(edgeStart, edgeEnd, value) {
16383
+ const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
16384
+ return progress * progress * (3 - 2 * progress);
16385
+ }
16386
+
16387
+ /* eslint-disable no-magic-numbers */
16388
+ /**
16389
+ * Light direction used by the continuous Octopus 3D 4 mesh shading.
16390
+ *
16391
+ * @private helper of `octopus3d4AvatarVisual`
16392
+ */
16393
+ const LIGHT_DIRECTION = normalizeVector3({
16394
+ x: 0.32,
16395
+ y: -0.66,
16396
+ z: 1,
16397
+ });
16398
+ /**
16399
+ * Rim-light direction used to brighten the side of the silhouette.
16400
+ *
16401
+ * @private helper of `octopus3d4AvatarVisual`
16402
+ */
16403
+ const RIM_LIGHT_DIRECTION = normalizeVector3({
16404
+ x: -0.7,
16405
+ y: -0.18,
16406
+ z: 0.55,
16407
+ });
16408
+ /**
16409
+ * Real-octopus tentacle count used by the continuous lower mesh.
16410
+ *
16411
+ * @private helper of `octopus3d4AvatarVisual`
16412
+ */
16413
+ const OCTOPUS_TENTACLE_COUNT = 8;
16414
+ /**
16415
+ * Number of seeded skin spots painted across the octopus body.
16416
+ *
16417
+ * @private helper of `octopus3d4AvatarVisual`
16418
+ */
16419
+ const SKIN_SPOT_COUNT = 14;
16420
+ /**
16421
+ * Cache keyed by the `createRandom` factory reference, stable per mounted `<Avatar/>` component.
16422
+ *
16423
+ * @private helper of `octopus3d4AvatarVisual`
16424
+ */
16425
+ const octopus3d4StableStateCache = new WeakMap();
16426
+ /**
16427
+ * Returns the stable per-avatar state, computing it on first access and caching afterwards.
16428
+ *
16429
+ * @private helper of `octopus3d4AvatarVisual`
16430
+ */
16431
+ function getOctopus3d4StableState(createRandom) {
16432
+ const cached = octopus3d4StableStateCache.get(createRandom);
16433
+ if (cached !== undefined) {
16434
+ return cached;
16435
+ }
16436
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
16437
+ const animationRandom = createRandom('octopus3d4-animation-profile');
16438
+ const eyeRandom = createRandom('octopus3d4-eye-profile');
16439
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
16440
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
16441
+ const state = {
16442
+ morphologyProfile,
16443
+ animationPhase: animationRandom() * Math.PI * 2,
16444
+ leftEyePhaseOffset,
16445
+ rightEyePhaseOffset,
16446
+ tentacleProfiles: createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile),
16447
+ skinSpots: createBlobbySkinSpots(createRandom),
16448
+ };
16449
+ octopus3d4StableStateCache.set(createRandom, state);
16450
+ return state;
16451
+ }
16452
+ /**
16453
+ * Octopus 3D 4 avatar visual.
16454
+ *
16455
+ * @private built-in avatar visual
16456
+ */
16457
+ const octopus3d4AvatarVisual = {
16458
+ id: 'octopus3d4',
16459
+ title: 'Octopus 3D 4',
16460
+ description: 'Cute blobby 3D octopus rendered as one smooth continuous mesh with naturally undulating tentacles, rich multi-pass shading, pigment spots, and cursor-aware head and eyes.',
16461
+ isAnimated: true,
16462
+ supportsPointerTracking: true,
16463
+ render({ context, size, palette, createRandom, timeMs, interaction }) {
16464
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles, skinSpots } = getOctopus3d4StableState(createRandom);
16465
+ const sceneCenterX = size * 0.5;
16466
+ const sceneCenterY = size * 0.535;
16467
+ const bob = Math.sin(timeMs / 980 + animationPhase) * size * 0.013;
16468
+ const meshCenter = {
16469
+ x: interaction.bodyOffsetX * size * 0.052 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
16470
+ y: -size * 0.07 + interaction.bodyOffsetY * size * 0.028 + bob,
16471
+ z: interaction.intensity * size * 0.02,
16472
+ };
16473
+ const rotationY = -0.08 +
16474
+ Math.sin(timeMs / 2800 + animationPhase) * 0.04 +
16475
+ interaction.bodyOffsetX * 0.24 +
16476
+ interaction.gazeX * 0.98;
16477
+ const rotationX = -0.07 +
16478
+ Math.cos(timeMs / 3200 + animationPhase * 0.7) * 0.02 -
16479
+ interaction.bodyOffsetY * 0.08 -
16480
+ interaction.gazeY * 0.42;
16481
+ const surfaceOptions = {
16482
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.12,
16483
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1,
16484
+ radiusZ: size *
16485
+ morphologyProfile.body.bodyRadiusRatio *
16486
+ (1.04 + (morphologyProfile.body.horizontalStretch - 1) * 0.2),
16487
+ morphologyProfile,
16488
+ timeMs,
16489
+ animationPhase,
16490
+ tentacleProfiles,
16491
+ };
16492
+ const surfacePatches = resolveVisibleBlobbyContinuousPatches({
16493
+ ...surfaceOptions,
16494
+ center: meshCenter,
16495
+ rotationX,
16496
+ rotationY,
16497
+ sceneCenterX,
16498
+ sceneCenterY,
16499
+ size,
16500
+ palette,
16501
+ });
16502
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.4 - 0.04, -0.24, 0.08);
16503
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.0, 0.18, 0.32);
16504
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.21 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
16505
+ const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
16506
+ const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
16507
+ const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
16508
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
16509
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
16510
+ drawAvatarFrame(context, size, palette);
16511
+ drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
16512
+ drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile);
16513
+ for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
16514
+ drawBlobbyContinuousSurfacePatch(context, surfacePatch);
16515
+ }
16516
+ drawBlobbySkinSpots({
16517
+ context,
16518
+ surfaceOptions,
16519
+ center: meshCenter,
16520
+ rotationX,
16521
+ rotationY,
16522
+ sceneCenterX,
16523
+ sceneCenterY,
16524
+ size,
16525
+ palette,
16526
+ skinSpots,
16527
+ });
16528
+ drawBlobbyContinuousCurrents({
16529
+ context,
16530
+ surfaceOptions,
16531
+ center: meshCenter,
16532
+ rotationX,
16533
+ rotationY,
16534
+ sceneCenterX,
16535
+ sceneCenterY,
16536
+ size,
16537
+ palette,
16538
+ morphologyProfile,
16539
+ timeMs,
16540
+ animationPhase,
16541
+ });
16542
+ drawBlobbyContinuousSuckers({
16543
+ context,
16544
+ surfaceOptions,
16545
+ center: meshCenter,
16546
+ rotationX,
16547
+ rotationY,
16548
+ sceneCenterX,
16549
+ sceneCenterY,
16550
+ size,
16551
+ palette,
16552
+ });
16553
+ drawBlobbyContinuousGloss({
16554
+ context,
16555
+ surfaceOptions,
16556
+ center: meshCenter,
16557
+ rotationX,
16558
+ rotationY,
16559
+ sceneCenterX,
16560
+ sceneCenterY,
16561
+ size,
16562
+ palette,
16563
+ });
16564
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
16565
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
16566
+ drawProjectedOrganicMouth(context, [
16567
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
16568
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
16569
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
16570
+ ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
16571
+ },
16572
+ };
16573
+ /**
16574
+ * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
16575
+ *
16576
+ * @private helper of `octopus3d4AvatarVisual`
16577
+ */
16578
+ function createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile) {
16579
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
16580
+ const tentacleRandom = createRandom(`octopus3d4-tentacle-${tentacleIndex}`);
16581
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
16582
+ return {
16583
+ centerLongitude: -Math.PI * 0.9 +
16584
+ progress * Math.PI * 1.8 +
16585
+ (tentacleRandom() - 0.5) * (0.06 + morphologyProfile.tentacles.rootSpreadScale * 0.025),
16586
+ widthScale: 0.92 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.18,
16587
+ lengthScale: 0.9 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.24,
16588
+ swayScale: 0.86 + tentacleRandom() * 0.4 + (morphologyProfile.tentacles.swayScale - 1) * 0.22,
16589
+ depthScale: 0.9 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.22,
16590
+ curlScale: 0.62 + tentacleRandom() * 0.46,
16591
+ primaryPhase: tentacleRandom() * Math.PI * 2,
16592
+ secondaryPhase: tentacleRandom() * Math.PI * 2,
16593
+ suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
16594
+ };
16595
+ });
16596
+ }
16597
+ /**
16598
+ * Creates seeded skin pigment spots distributed across the upper octopus mesh.
16599
+ *
16600
+ * @private helper of `octopus3d4AvatarVisual`
16601
+ */
16602
+ function createBlobbySkinSpots(createRandom) {
16603
+ const spotRandom = createRandom('octopus3d4-skin-spots');
16604
+ return Array.from({ length: SKIN_SPOT_COUNT }, () => ({
16605
+ latitude: -0.45 + spotRandom() * 0.7,
16606
+ longitude: -0.5 + spotRandom() * 1.0,
16607
+ radiusScale: 0.0028 + spotRandom() * 0.0052,
16608
+ opacity: 0.16 + spotRandom() * 0.22,
16609
+ }));
16610
+ }
16611
+ /**
16612
+ * Draws the soft underwater atmosphere behind the continuous octopus mesh.
16613
+ *
16614
+ * @private helper of `octopus3d4AvatarVisual`
16615
+ */
16616
+ function drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
16617
+ const glowGradient = context.createRadialGradient(sceneCenterX + interaction.gazeX * size * 0.12, sceneCenterY - size * 0.18 + interaction.gazeY * size * 0.05, size * 0.04, sceneCenterX, sceneCenterY, size * (0.68 + interaction.intensity * 0.025));
16618
+ glowGradient.addColorStop(0, `${palette.highlight}74`);
16619
+ glowGradient.addColorStop(0.32, `${palette.accent}32`);
16620
+ glowGradient.addColorStop(1, `${palette.highlight}00`);
16621
+ context.fillStyle = glowGradient;
16622
+ context.fillRect(0, 0, size, size);
16623
+ const lowerGradient = context.createRadialGradient(sceneCenterX + Math.sin(timeMs / 1500) * size * 0.05, sceneCenterY + size * 0.3, size * 0.06, sceneCenterX, sceneCenterY + size * 0.3, size * 0.56);
16624
+ lowerGradient.addColorStop(0, `${palette.secondary}28`);
16625
+ lowerGradient.addColorStop(1, `${palette.secondary}00`);
16626
+ context.fillStyle = lowerGradient;
16627
+ context.fillRect(0, 0, size, size);
16628
+ }
16629
+ /**
16630
+ * Draws the soft lower shadow that anchors the octopus in the avatar frame.
16631
+ *
16632
+ * @private helper of `octopus3d4AvatarVisual`
16633
+ */
16634
+ function drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
16635
+ const cx = size * 0.5 + interaction.gazeX * size * 0.046;
16636
+ const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
16637
+ const rx = size * (0.2 + morphologyProfile.tentacles.rootSpreadScale * 0.024 + interaction.intensity * 0.022);
16638
+ const ry = size * 0.062;
16639
+ context.save();
16640
+ context.translate(cx, cy);
16641
+ context.scale(1, ry / rx);
16642
+ const blurRadius = rx * 1.42;
16643
+ const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
16644
+ shadowGradient.addColorStop(0, `${palette.shadow}82`);
16645
+ shadowGradient.addColorStop(0.45, `${palette.shadow}4a`);
16646
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1c`);
16647
+ shadowGradient.addColorStop(1, `${palette.shadow}00`);
16648
+ context.fillStyle = shadowGradient;
16649
+ context.beginPath();
16650
+ context.arc(0, 0, blurRadius, 0, Math.PI * 2);
16651
+ context.fill();
16652
+ context.restore();
16653
+ }
16654
+ /**
16655
+ * Number of latitude segments used by the continuous Octopus 3D 4 mesh.
16656
+ *
16657
+ * @private helper of `octopus3d4AvatarVisual`
16658
+ */
16659
+ const LATITUDE_PATCH_COUNT = 20;
16660
+ /**
16661
+ * Number of longitude segments used by the continuous Octopus 3D 4 mesh.
16662
+ *
16663
+ * @private helper of `octopus3d4AvatarVisual`
16664
+ */
16665
+ const LONGITUDE_PATCH_COUNT = 48;
16666
+ /**
16667
+ * Resolves visible projected patches for the continuous Octopus 3D 4 mesh.
16668
+ *
16669
+ * Within a single frame, mesh corner samples and longitude-only computations (tentacle
16670
+ * influence and lobe wave) are quantized to the patch grid and computed once each rather
16671
+ * than re-evaluated for every patch corner.
16672
+ *
16673
+ * @private helper of `octopus3d4AvatarVisual`
16674
+ */
16675
+ function resolveVisibleBlobbyContinuousPatches(options) {
16676
+ const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
16677
+ const latitudePatchCount = LATITUDE_PATCH_COUNT;
16678
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT;
16679
+ const surfacePatches = [];
16680
+ const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
16681
+ const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
16682
+ for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
16683
+ latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
16684
+ }
16685
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
16686
+ longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
16687
+ }
16688
+ const cachedTentacleInfluencesByCornerLongitude = new Array(longitudePatchCount + 1);
16689
+ const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
16690
+ const cachedTentacleInfluencesByPatchCenterLongitude = new Array(longitudePatchCount);
16691
+ const cachedLobeWavesByPatchCenterLongitude = new Float64Array(longitudePatchCount);
16692
+ const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
16693
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
16694
+ const cornerLongitude = longitudeBoundaries[boundaryIndex];
16695
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveBlobbyTentacleInfluence(options, cornerLongitude);
16696
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveBlobbyLobeWave(options, cornerLongitude);
16697
+ }
16698
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
16699
+ const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
16700
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyTentacleInfluence(options, patchCenterLongitude);
16701
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyLobeWave(options, patchCenterLongitude);
16702
+ cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
16703
+ }
16704
+ const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
16705
+ const transformedCornerSamples = new Array(cornerCount);
16706
+ for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
16707
+ const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex];
16708
+ for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
16709
+ const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
16710
+ const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
16711
+ const cornerSample = sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
16712
+ transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
16713
+ }
16714
+ }
16715
+ for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
16716
+ const startLatitude = latitudeBoundaries[latitudeIndex];
16717
+ const endLatitude = latitudeBoundaries[latitudeIndex + 1];
16718
+ const centerLatitude = (startLatitude + endLatitude) / 2;
16719
+ const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
16720
+ const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
16721
+ const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
16722
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
16723
+ const transformedCorners = [
16724
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex],
16725
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1],
16726
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1],
16727
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex],
16728
+ ];
16729
+ const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
16730
+ if (surfaceNormal.z <= 0.006) {
16731
+ continue;
16732
+ }
16733
+ const projectedCorners = [
16734
+ projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
16735
+ projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
16736
+ projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
16737
+ projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
16738
+ ];
16739
+ const lightIntensity = clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1);
16740
+ const rimLightIntensity = Math.pow(clampNumber$1(dotProduct3D(surfaceNormal, RIM_LIGHT_DIRECTION), 0, 1), 2.6);
16741
+ surfacePatches.push({
16742
+ corners: projectedCorners,
16743
+ averageDepth: (transformedCorners[0].z +
16744
+ transformedCorners[1].z +
16745
+ transformedCorners[2].z +
16746
+ transformedCorners[3].z) /
16747
+ 4,
16748
+ lightIntensity,
16749
+ rimLightIntensity,
16750
+ fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
16751
+ outlineColor: verticalProgress < 0.52 ? `${palette.highlight}5c` : `${palette.shadow}66`,
16752
+ });
16753
+ }
16754
+ }
16755
+ return surfacePatches;
16756
+ }
16757
+ /**
16758
+ * Samples one point on the continuous Octopus 3D 4 surface.
16759
+ *
16760
+ * @private helper of `octopus3d4AvatarVisual`
16761
+ */
16762
+ function sampleBlobbyContinuousSurfacePoint(options, latitude, longitude) {
16763
+ return sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, resolveBlobbyTentacleInfluence(options, longitude), resolveBlobbyLobeWave(options, longitude));
16764
+ }
16765
+ /**
16766
+ * Samples one point on the continuous Octopus 3D 4 surface using precomputed longitude-only values.
16767
+ *
16768
+ * The patch loop quantizes the mesh into a fixed corner grid, so the same longitude is reused
16769
+ * across every latitude row and each tentacle/lobe value is computed once per frame instead
16770
+ * of `latitudePatchCount * 4` times.
16771
+ *
16772
+ * @private helper of `octopus3d4AvatarVisual`
16773
+ */
16774
+ function sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
16775
+ const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
16776
+ const cosineLatitude = Math.max(0, Math.cos(latitude));
16777
+ const verticalProgress = (Math.sin(latitude) + 1) / 2;
16778
+ const upperBlend = Math.pow(1 - verticalProgress, 1.32);
16779
+ const lowerBlend = smoothStep(0.34, 1, verticalProgress);
16780
+ const tipBlend = smoothStep(0.66, 1, verticalProgress);
16781
+ const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
16782
+ const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.26 + tipBlend * 0.22);
16783
+ const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
16784
+ animationPhase * 0.6 +
16785
+ timeMs / (1700 + morphologyProfile.body.lobeCount * 28)) *
16786
+ (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.78) *
16787
+ (0.32 + lowerBlend * 0.7);
16788
+ const primaryTentacleWave = Math.sin(timeMs / 740 + tentacleInfluence.primaryPhase + verticalProgress * 2.6) *
16789
+ lowerBlend *
16790
+ tentacleInfluence.core *
16791
+ tentacleInfluence.swayScale;
16792
+ const secondaryTentacleWave = Math.sin(timeMs / 470 + tentacleInfluence.secondaryPhase + verticalProgress * 4.2) *
16793
+ lowerBlend *
16794
+ tentacleInfluence.core *
16795
+ tentacleInfluence.swayScale *
16796
+ 0.42;
16797
+ const tentacleCurl = Math.sin(timeMs / 1180 + tentacleInfluence.primaryPhase * 0.6 + verticalProgress * 3.2) *
16798
+ tipBlend *
16799
+ tentacleInfluence.core *
16800
+ tentacleInfluence.curlScale *
16801
+ 0.36;
16802
+ const horizontalScale = 1.06 +
16803
+ mantleRipple +
16804
+ lowerBlend * (0.18 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.12) +
16805
+ lowerBlend * tentacleInfluence.core * (0.22 + lowerLobeWave * 0.14) -
16806
+ upperBlend * 0.08;
16807
+ const depthScale = 1.08 +
16808
+ upperBlend * 0.16 +
16809
+ Math.max(0, Math.cos(effectiveLongitude)) * 0.12 +
16810
+ lowerBlend * tentacleInfluence.core * (0.12 + tentacleInfluence.depthScale * 0.07) -
16811
+ Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
16812
+ const tentacleTubeRadius = lowerBlend *
16813
+ tentacleInfluence.core *
16814
+ (0.12 + tipBlend * 0.07 + tentacleInfluence.widthScale * 0.028) *
16815
+ radiusX;
16816
+ const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
16817
+ const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.74;
16818
+ const lowerDrop = lowerBlend *
16819
+ radiusY *
16820
+ (0.2 +
16821
+ tentacleInfluence.core *
16822
+ (0.42 +
16823
+ tentacleInfluence.lengthScale * 0.24 +
16824
+ (morphologyProfile.tentacles.flowLengthScale - 1) * 0.1));
16825
+ const combinedTentacleSway = (primaryTentacleWave + secondaryTentacleWave) * radiusX * (0.06 + tipBlend * 0.06);
16826
+ return {
16827
+ x: Math.sin(effectiveLongitude) * planarRadiusX +
16828
+ combinedTentacleSway +
16829
+ tentacleCurl * radiusX * 0.18,
16830
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.14) -
16831
+ upperBlend * radiusY * 0.12 +
16832
+ lowerDrop +
16833
+ Math.sin(timeMs / 1380 + animationPhase + latitude * 1.5) * lowerBlend * radiusY * 0.022 +
16834
+ Math.cos(timeMs / 820 + tentacleInfluence.primaryPhase) *
16835
+ lowerBlend *
16836
+ tipBlend *
16837
+ tentacleInfluence.core *
16838
+ radiusY *
16839
+ 0.04,
16840
+ z: Math.cos(effectiveLongitude) * planarRadiusZ +
16841
+ Math.cos(timeMs / 960 + tentacleInfluence.primaryPhase + verticalProgress) *
16842
+ lowerBlend *
16843
+ tentacleInfluence.core *
16844
+ radiusZ *
16845
+ 0.044 +
16846
+ tentacleCurl * radiusZ * 0.14,
16847
+ };
16848
+ }
16849
+ /**
16850
+ * Blends nearby seeded tentacle profiles at one mesh longitude.
16851
+ *
16852
+ * @private helper of `octopus3d4AvatarVisual`
16853
+ */
16854
+ function resolveBlobbyTentacleInfluence(options, longitude) {
16855
+ let totalWeight = 0;
16856
+ let weightedSin = 0;
16857
+ let weightedCos = 0;
16858
+ let weightedWidthScale = 0;
16859
+ let weightedLengthScale = 0;
16860
+ let weightedSwayScale = 0;
16861
+ let weightedDepthScale = 0;
16862
+ let weightedCurlScale = 0;
16863
+ let weightedPrimaryPhase = 0;
16864
+ let weightedSecondaryPhase = 0;
16865
+ for (const tentacleProfile of options.tentacleProfiles) {
16866
+ const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
16867
+ const width = 0.22 * tentacleProfile.widthScale;
16868
+ const weight = Math.exp(-(distance * distance) / (width * width));
16869
+ totalWeight += weight;
16870
+ weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
16871
+ weightedCos += Math.cos(tentacleProfile.centerLongitude) * weight;
16872
+ weightedWidthScale += tentacleProfile.widthScale * weight;
16873
+ weightedLengthScale += tentacleProfile.lengthScale * weight;
16874
+ weightedSwayScale += tentacleProfile.swayScale * weight;
16875
+ weightedDepthScale += tentacleProfile.depthScale * weight;
16876
+ weightedCurlScale += tentacleProfile.curlScale * weight;
16877
+ weightedPrimaryPhase += tentacleProfile.primaryPhase * weight;
16878
+ weightedSecondaryPhase += tentacleProfile.secondaryPhase * weight;
16879
+ }
16880
+ if (totalWeight < 0.0001) {
16881
+ return {
16882
+ core: 0,
16883
+ centerLongitude: longitude,
16884
+ widthScale: 1,
16885
+ lengthScale: 1,
16886
+ swayScale: 1,
16887
+ depthScale: 1,
16888
+ curlScale: 1,
16889
+ primaryPhase: 0,
16890
+ secondaryPhase: 0,
16891
+ };
16892
+ }
16893
+ return {
16894
+ core: clampNumber$1(totalWeight, 0, 1),
16895
+ centerLongitude: Math.atan2(weightedSin / totalWeight, weightedCos / totalWeight),
16896
+ widthScale: weightedWidthScale / totalWeight,
16897
+ lengthScale: weightedLengthScale / totalWeight,
16898
+ swayScale: weightedSwayScale / totalWeight,
16899
+ depthScale: weightedDepthScale / totalWeight,
16900
+ curlScale: weightedCurlScale / totalWeight,
16901
+ primaryPhase: weightedPrimaryPhase / totalWeight,
16902
+ secondaryPhase: weightedSecondaryPhase / totalWeight,
16903
+ };
16904
+ }
16905
+ /**
16906
+ * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
16907
+ *
16908
+ * @private helper of `octopus3d4AvatarVisual`
16909
+ */
16910
+ function resolveBlobbyLobeWave(options, longitude) {
16911
+ const { morphologyProfile, animationPhase, timeMs } = options;
16912
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT +
16913
+ animationPhase +
16914
+ timeMs / (940 + morphologyProfile.body.lobeCount * 18)) +
16915
+ 1) /
16916
+ 2);
16917
+ }
16918
+ /**
16919
+ * Resolves one base fill tone for a patch on the continuous octopus mesh.
16920
+ *
16921
+ * @private helper of `octopus3d4AvatarVisual`
16922
+ */
16923
+ function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
16924
+ const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
16925
+ if (tonalProgress < 0.12) {
16926
+ return palette.highlight;
16927
+ }
16928
+ if (tonalProgress < 0.3) {
16929
+ return palette.secondary;
16930
+ }
16931
+ if (tonalProgress < 0.72) {
16932
+ return forwardness > 0.56 ? palette.secondary : palette.primary;
16933
+ }
16934
+ return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
16935
+ }
16936
+ /**
16937
+ * Draws one projected mesh patch with multi-pass shading and a soft edge.
16938
+ *
16939
+ * @private helper of `octopus3d4AvatarVisual`
16940
+ */
16941
+ function drawBlobbyContinuousSurfacePatch(context, surfacePatch) {
16942
+ drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
16943
+ if (surfacePatch.lightIntensity > 0) {
16944
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.2 * surfacePatch.lightIntensity})`);
16945
+ }
16946
+ else if (surfacePatch.lightIntensity < 0) {
16947
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.26 * Math.abs(surfacePatch.lightIntensity)})`);
16948
+ }
16949
+ if (surfacePatch.rimLightIntensity > 0.04) {
16950
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 247, 230, ${0.22 * surfacePatch.rimLightIntensity})`);
16951
+ }
16952
+ context.save();
16953
+ context.beginPath();
16954
+ context.moveTo(surfacePatch.corners[0].x, surfacePatch.corners[0].y);
16955
+ for (let cornerIndex = 1; cornerIndex < surfacePatch.corners.length; cornerIndex++) {
16956
+ context.lineTo(surfacePatch.corners[cornerIndex].x, surfacePatch.corners[cornerIndex].y);
16957
+ }
16958
+ context.closePath();
16959
+ context.strokeStyle = surfacePatch.outlineColor;
16960
+ context.lineWidth = Math.max(0.6, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0026);
16961
+ context.lineJoin = 'round';
16962
+ context.stroke();
16963
+ context.restore();
16964
+ }
16965
+ /**
16966
+ * Draws projected mantle-current lines on the front of the mesh.
16967
+ *
16968
+ * @private helper of `octopus3d4AvatarVisual`
16969
+ */
16970
+ function drawBlobbyContinuousCurrents(options) {
16971
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
16972
+ const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
16973
+ const centerIndex = (currentCount - 1) / 2;
16974
+ context.save();
16975
+ context.lineCap = 'round';
16976
+ context.lineJoin = 'round';
16977
+ for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
16978
+ const baseLongitude = (currentIndex - centerIndex) * 0.15;
16979
+ const projectedPoints = [];
16980
+ for (let sampleIndex = 0; sampleIndex < 9; sampleIndex++) {
16981
+ const progress = sampleIndex / 8;
16982
+ const latitude = -0.48 + progress * 0.78;
16983
+ const longitude = baseLongitude + Math.sin(timeMs / 1140 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.038;
16984
+ const scenePoint = transformScenePoint(sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
16985
+ if (scenePoint.z > center.z - size * 0.016) {
16986
+ projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
16987
+ }
16988
+ }
16989
+ if (projectedPoints.length < 3) {
16990
+ continue;
16991
+ }
16992
+ context.beginPath();
16993
+ context.moveTo(projectedPoints[0].x, projectedPoints[0].y);
16994
+ for (const projectedPoint of projectedPoints.slice(1)) {
16995
+ context.lineTo(projectedPoint.x, projectedPoint.y);
16996
+ }
16997
+ context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}3d` : `${palette.accent}33`;
16998
+ context.lineWidth = size * (0.0055 + currentIndex * 0.00045);
16999
+ context.stroke();
17000
+ }
17001
+ context.restore();
17002
+ }
17003
+ /**
17004
+ * Draws small projected sucker highlights on the waving lower mesh lobes.
17005
+ *
17006
+ * @private helper of `octopus3d4AvatarVisual`
17007
+ */
17008
+ function drawBlobbyContinuousSuckers(options) {
17009
+ const { surfaceOptions, size } = options;
17010
+ const { timeMs } = surfaceOptions;
17011
+ for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
17012
+ if (Math.cos(tentacleProfile.centerLongitude) < -0.16) {
17013
+ continue;
17014
+ }
17015
+ for (let suckerIndex = 0; suckerIndex < 4; suckerIndex++) {
17016
+ const latitude = 0.5 + suckerIndex * 0.12;
17017
+ const sideOffset = tentacleProfile.suckerSide * (0.038 + suckerIndex * 0.014) * tentacleProfile.widthScale;
17018
+ const waveOffset = Math.sin(timeMs / 880 + tentacleProfile.primaryPhase + suckerIndex * 0.78) * 0.02;
17019
+ drawBlobbyContinuousSurfaceSpot({
17020
+ ...options,
17021
+ latitude,
17022
+ longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
17023
+ radiusScale: size * (0.0068 - suckerIndex * 0.0006),
17024
+ });
17025
+ }
17026
+ }
17027
+ }
17028
+ /**
17029
+ * Draws seeded pigment spots across the upper mesh for a richer skin texture.
17030
+ *
17031
+ * @private helper of `octopus3d4AvatarVisual`
17032
+ */
17033
+ function drawBlobbySkinSpots(options) {
17034
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, skinSpots, } = options;
17035
+ for (const skinSpot of skinSpots) {
17036
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, skinSpot.latitude, skinSpot.longitude);
17037
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
17038
+ if (sceneCenterPoint.z <= center.z - size * 0.01) {
17039
+ continue;
17040
+ }
17041
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
17042
+ const spotRadius = size * skinSpot.radiusScale;
17043
+ context.save();
17044
+ context.beginPath();
17045
+ context.arc(projectedCenterPoint.x, projectedCenterPoint.y, spotRadius, 0, Math.PI * 2);
17046
+ context.fillStyle = `${palette.shadow}${formatAlphaHex(skinSpot.opacity)}`;
17047
+ context.fill();
17048
+ context.restore();
17049
+ }
17050
+ }
17051
+ /**
17052
+ * Draws one tiny projected surface spot by sampling local mesh tangents.
17053
+ *
17054
+ * @private helper of `octopus3d4AvatarVisual`
17055
+ */
17056
+ function drawBlobbyContinuousSurfaceSpot(options) {
17057
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
17058
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude);
17059
+ const localHorizontal = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
17060
+ const localVertical = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
17061
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
17062
+ if (sceneCenterPoint.z <= center.z - size * 0.012) {
17063
+ return;
17064
+ }
17065
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
17066
+ const projectedHorizontalPoint = projectScenePoint(transformScenePoint(localHorizontal, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
17067
+ const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
17068
+ const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
17069
+ radiusScale *
17070
+ 0.78, size * 0.003, size * 0.02);
17071
+ const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
17072
+ radiusScale *
17073
+ 0.54, size * 0.0024, size * 0.015);
17074
+ const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
17075
+ context.save();
17076
+ context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
17077
+ context.rotate(rotation);
17078
+ context.beginPath();
17079
+ context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
17080
+ context.fillStyle = `${palette.highlight}80`;
17081
+ context.fill();
17082
+ context.strokeStyle = `${palette.highlight}a8`;
17083
+ context.lineWidth = Math.max(0.7, size * 0.0028);
17084
+ context.stroke();
17085
+ context.restore();
17086
+ }
17087
+ /**
17088
+ * Draws a soft, slowly drifting gloss highlight on the front of the mesh.
17089
+ *
17090
+ * @private helper of `octopus3d4AvatarVisual`
17091
+ */
17092
+ function drawBlobbyContinuousGloss(options) {
17093
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size } = options;
17094
+ const { timeMs, animationPhase } = surfaceOptions;
17095
+ const glossLatitude = -0.3 + Math.sin(timeMs / 2700 + animationPhase) * 0.04;
17096
+ const glossLongitude = -0.18 + Math.cos(timeMs / 2300 + animationPhase * 0.8) * 0.05;
17097
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, glossLatitude, glossLongitude);
17098
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
17099
+ if (sceneCenterPoint.z <= center.z) {
17100
+ return;
17101
+ }
17102
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
17103
+ const glossRadius = size * 0.058;
17104
+ context.save();
17105
+ const glossGradient = context.createRadialGradient(projectedCenterPoint.x - glossRadius * 0.3, projectedCenterPoint.y - glossRadius * 0.4, glossRadius * 0.04, projectedCenterPoint.x, projectedCenterPoint.y, glossRadius);
17106
+ glossGradient.addColorStop(0, 'rgba(255, 255, 255, 0.34)');
17107
+ glossGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
17108
+ glossGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
17109
+ context.fillStyle = glossGradient;
17110
+ context.beginPath();
17111
+ context.ellipse(projectedCenterPoint.x, projectedCenterPoint.y, glossRadius * 1.05, glossRadius * 0.78, 0, 0, Math.PI * 2);
17112
+ context.fill();
17113
+ context.restore();
17114
+ }
17115
+ /**
17116
+ * Resolves a signed angular distance from the source longitude to the target longitude.
17117
+ *
17118
+ * @private helper of `octopus3d4AvatarVisual`
17119
+ */
17120
+ function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
17121
+ return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
17122
+ }
17123
+ /**
17124
+ * Smoothly maps a value between two bounds into `[0, 1]`.
17125
+ *
17126
+ * @private helper of `octopus3d4AvatarVisual`
17127
+ */
17128
+ function smoothStep(edgeStart, edgeEnd, value) {
17129
+ const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
17130
+ return progress * progress * (3 - 2 * progress);
17131
+ }
17132
+ /**
17133
+ * Converts an opacity ratio into a two-digit hexadecimal alpha suffix.
17134
+ *
17135
+ * @private helper of `octopus3d4AvatarVisual`
17136
+ */
17137
+ function formatAlphaHex(opacity) {
17138
+ return Math.round(clampNumber$1(opacity, 0, 1) * 255)
17139
+ .toString(16)
17140
+ .padStart(2, '0');
17141
+ }
17142
+
15592
17143
  /* eslint-disable no-magic-numbers */
15593
17144
  /**
15594
17145
  * Octopus avatar visual.
@@ -16359,6 +17910,8 @@
16359
17910
  octopus3AvatarVisual,
16360
17911
  octopus3dAvatarVisual,
16361
17912
  octopus3d2AvatarVisual,
17913
+ octopus3d3AvatarVisual,
17914
+ octopus3d4AvatarVisual,
16362
17915
  asciiOctopusAvatarVisual,
16363
17916
  minecraftAvatarVisual,
16364
17917
  minecraft2AvatarVisual,
@@ -18647,7 +20200,15 @@
18647
20200
  * Builds the teammate request text, optionally including context.
18648
20201
  */
18649
20202
  function buildTeammateRequest(message, context) {
18650
- return context ? `${message}\n\nContext:\n${context}` : message;
20203
+ if (!context) {
20204
+ return message;
20205
+ }
20206
+ return spacetrim.spaceTrim((block) => `
20207
+ ${block(message)}
20208
+
20209
+ Context:
20210
+ ${block(context)}
20211
+ `);
18651
20212
  }
18652
20213
  /**
18653
20214
  * Builds a minimal chat prompt for teammate calls.
@@ -22473,7 +24034,11 @@
22473
24034
  const lineRangedContent = applyOptionalLineRange(decodedContent, args.startLine, args.endLine);
22474
24035
  const wasCharacterTruncated = lineRangedContent.length > MAX_PROJECT_FILE_CONTENT_CHARACTERS;
22475
24036
  const contentToReturn = wasCharacterTruncated
22476
- ? `${lineRangedContent.slice(0, MAX_PROJECT_FILE_CONTENT_CHARACTERS)}\n\n[...truncated...]`
24037
+ ? spacetrim.spaceTrim((block) => `
24038
+ ${block(lineRangedContent.slice(0, MAX_PROJECT_FILE_CONTENT_CHARACTERS))}
24039
+
24040
+ [...truncated...]
24041
+ `)
22477
24042
  : lineRangedContent;
22478
24043
  const wasTruncated = decoded.isTruncated || wasCharacterTruncated;
22479
24044
  if (wasCharacterTruncated) {
@@ -26009,11 +27574,19 @@
26009
27574
  try {
26010
27575
  const json = JSON.parse(content);
26011
27576
  const formattedJson = JSON.stringify(json, null, 4);
26012
- return `\`\`\`json\n${formattedJson}\n\`\`\``;
27577
+ return spacetrim.spaceTrim((block) => `
27578
+ \`\`\`json
27579
+ ${block(formattedJson)}
27580
+ \`\`\`
27581
+ `);
26013
27582
  }
26014
27583
  catch (error) {
26015
27584
  // If JSON is invalid, still import it but maybe not as pretty JSON
26016
- return `\`\`\`json\n${content}\n\`\`\``;
27585
+ return spacetrim.spaceTrim((block) => `
27586
+ \`\`\`json
27587
+ ${block(content)}
27588
+ \`\`\`
27589
+ `);
26017
27590
  }
26018
27591
  },
26019
27592
  };
@@ -26037,7 +27610,11 @@
26037
27610
  import(content, mimeType) {
26038
27611
  const extension = mimeTypeToExtension(mimeType);
26039
27612
  const codeBlockType = extension || 'txt';
26040
- return `\`\`\`${codeBlockType}\n${content}\n\`\`\``;
27613
+ return spacetrim.spaceTrim((block) => `
27614
+ \`\`\`${codeBlockType}
27615
+ ${block(content)}
27616
+ \`\`\`
27617
+ `);
26041
27618
  },
26042
27619
  };
26043
27620
 
@@ -26284,11 +27861,19 @@
26284
27861
  const examples = [];
26285
27862
  const initialMessage = (_a = parseResult.commitments.find((commitment) => commitment.type === 'INITIAL MESSAGE')) === null || _a === void 0 ? void 0 : _a.content;
26286
27863
  if (initialMessage) {
26287
- examples.push(`**Agent:**\n${initialMessage}`);
27864
+ examples.push(spacetrim.spaceTrim((block) => `
27865
+ **Agent:**
27866
+ ${block(initialMessage)}
27867
+ `));
26288
27868
  }
26289
27869
  if (samples && samples.length > 0) {
26290
27870
  for (const sample of samples) {
26291
- examples.push(`**User:** ${sample.question}\n\n**Agent:**\n${sample.answer}`);
27871
+ examples.push(spacetrim.spaceTrim((block) => `
27872
+ **User:** ${block(String(sample.question))}
27873
+
27874
+ **Agent:**
27875
+ ${block(sample.answer)}
27876
+ `));
26292
27877
  }
26293
27878
  }
26294
27879
  return examples;
@@ -28003,7 +29588,11 @@
28003
29588
  * @private internal utility of `createStandaloneBookLanguageMarkdown`
28004
29589
  */
28005
29590
  function renderDocumentationSection(title, documentation) {
28006
- return `#### ${title}\n\n${removeLeadingTopLevelHeading(documentation)}`;
29591
+ return spacetrim.spaceTrim((block) => `
29592
+ #### ${title}
29593
+
29594
+ ${block(removeLeadingTopLevelHeading(documentation))}
29595
+ `);
28007
29596
  }
28008
29597
  /**
28009
29598
  * Renders the documentation body for one grouped commitment entry.
@@ -28654,7 +30243,9 @@
28654
30243
  * @private internal utility of `Book`
28655
30244
  */
28656
30245
  function parseCommitmentHeader(line) {
28657
- const match = /^([A-Z][A-Z0-9]*(?: [A-Z0-9]+)*)(?:\s+(.*))?$/u.exec(line);
30246
+ // Require at least 2 characters in the first keyword word to avoid treating common
30247
+ // single-letter words (e.g. "V" in Czech, "I" or "A" in English) as commitment headers.
30248
+ const match = /^([A-Z][A-Z0-9]+(?: [A-Z0-9]+)*)(?:\s+(.*))?$/u.exec(line);
28658
30249
  if (!match) {
28659
30250
  return null;
28660
30251
  }
@@ -28850,6 +30441,18 @@
28850
30441
  const normalizedVersionName = versionName.trim();
28851
30442
  return normalizedVersionName.length > 0 ? normalizedVersionName : null;
28852
30443
  }
30444
+ /**
30445
+ * Builds a Supabase `.or()` filter for agent name or permanent id lookups.
30446
+ *
30447
+ * @param agentNameOrPermanentId - Agent name or stable permanent identifier to match.
30448
+ * @returns `.or()` filter string safe to pass to Supabase.
30449
+ *
30450
+ * @private internal helper of `AgentCollectionInSupabase`
30451
+ */
30452
+ function buildAgentNameOrPermanentIdFilter(agentNameOrPermanentId) {
30453
+ const encodedAgentIdentifier = encodeURIComponent(agentNameOrPermanentId);
30454
+ return `agentName.eq.${encodedAgentIdentifier},permanentId.eq.${encodedAgentIdentifier}`;
30455
+ }
28853
30456
  /**
28854
30457
  * Agent collection stored in a Supabase table.
28855
30458
  *
@@ -28897,21 +30500,63 @@
28897
30500
  if (isVerbose) {
28898
30501
  console.info(`Found ${selectResult.data.length} agents in directory`);
28899
30502
  }
28900
- return selectResult.data.map(({ agentName, agentProfile, permanentId }) => {
28901
- if (isVerbose && agentProfile.agentName !== agentName) {
28902
- console.warn(spaceTrim(`
30503
+ return selectResult.data.map((row) => this.mapAgentBasicInformationRow(row, isVerbose));
30504
+ }
30505
+ /**
30506
+ * Finds one active agent profile by its human-readable name or permanent id.
30507
+ *
30508
+ * This keeps route-level lookups from loading the whole agent collection when only
30509
+ * one canonical route identifier is needed.
30510
+ *
30511
+ * @param agentNameOrPermanentId - Agent name or stable permanent identifier.
30512
+ * @returns Matching active agent profile or `null` when not found.
30513
+ *
30514
+ * @public exported from `@promptbook/core`
30515
+ */
30516
+ async findAgentBasicInformation(agentNameOrPermanentId) {
30517
+ var _a;
30518
+ const { isVerbose = exports.DEFAULT_IS_VERBOSE } = this.options || {};
30519
+ const selectResult = await this.supabaseClient
30520
+ .from(this.getTableName('Agent'))
30521
+ .select('agentName,agentProfile,permanentId')
30522
+ .or(buildAgentNameOrPermanentIdFilter(agentNameOrPermanentId))
30523
+ .is('deletedAt', null)
30524
+ .order('createdAt', { ascending: true })
30525
+ .limit(1);
30526
+ if (selectResult.error) {
30527
+ throw new DatabaseError(spaceTrim((block) => `
30528
+
30529
+ Error fetching agent "${agentNameOrPermanentId}" from Supabase:
30530
+
30531
+ ${block(selectResult.error.message)}
30532
+ `));
30533
+ }
30534
+ const row = (_a = selectResult.data) === null || _a === void 0 ? void 0 : _a[0];
30535
+ return row ? this.mapAgentBasicInformationRow(row, isVerbose) : null;
30536
+ }
30537
+ /**
30538
+ * Converts one database row into public agent information.
30539
+ *
30540
+ * @param row - Database row carrying the persisted profile snapshot.
30541
+ * @param isVerbose - Whether profile-name mismatches should be logged.
30542
+ * @returns Agent profile with canonical database name and permanent id.
30543
+ *
30544
+ * @private internal helper of `AgentCollectionInSupabase`
30545
+ */
30546
+ mapAgentBasicInformationRow({ agentName, agentProfile, permanentId }, isVerbose) {
30547
+ if (isVerbose && agentProfile.agentName !== agentName) {
30548
+ console.warn(spaceTrim(`
28903
30549
  Agent name mismatch for agent "${agentName}". Using name from database.
28904
30550
 
28905
30551
  agentName: "${agentName}"
28906
30552
  agentProfile.agentName: "${agentProfile.agentName}"
28907
30553
  `));
28908
- }
28909
- return {
28910
- ...agentProfile,
28911
- agentName,
28912
- permanentId: permanentId || agentProfile.permanentId,
28913
- };
28914
- });
30554
+ }
30555
+ return {
30556
+ ...agentProfile,
30557
+ agentName,
30558
+ permanentId: permanentId || agentProfile.permanentId,
30559
+ };
28915
30560
  }
28916
30561
  /**
28917
30562
  * Retrieves the permanent ID of an agent by its name or permanent ID.
@@ -28920,7 +30565,7 @@
28920
30565
  const selectResult = await this.supabaseClient
28921
30566
  .from(this.getTableName('Agent'))
28922
30567
  .select('permanentId')
28923
- .or(`agentName.eq.${agentNameOrPermanentId},permanentId.eq.${agentNameOrPermanentId}`)
30568
+ .or(buildAgentNameOrPermanentIdFilter(agentNameOrPermanentId))
28924
30569
  .order('createdAt', { ascending: true }) // Pick oldest if multiple match by name
28925
30570
  .limit(1);
28926
30571
  if (selectResult.error || !selectResult.data || selectResult.data.length === 0) {
@@ -28935,7 +30580,7 @@
28935
30580
  const selectResult = await this.supabaseClient
28936
30581
  .from(this.getTableName('Agent'))
28937
30582
  .select('agentSource')
28938
- .or(`agentName.eq.${agentNameOrPermanentId},permanentId.eq.${agentNameOrPermanentId}`)
30583
+ .or(buildAgentNameOrPermanentIdFilter(agentNameOrPermanentId))
28939
30584
  .is('deletedAt', null)
28940
30585
  .order('createdAt', { ascending: true }) // Pick oldest if multiple match by name
28941
30586
  .limit(1);
@@ -34416,7 +36061,8 @@
34416
36061
  * @private internal function of `$registeredLlmToolsMessage`
34417
36062
  */
34418
36063
  function createUsedEnvMessage() {
34419
- return `Unknown \`.env\` file` ;
36064
+ return `Unknown \`.env\` file`
36065
+ ;
34420
36066
  }
34421
36067
  // TODO: [®] DRY Register logic
34422
36068
  // TODO: [🧠][⚛] Maybe pass env as argument
@@ -35729,7 +37375,9 @@
35729
37375
  if (!executionTools || !executionTools.script) {
35730
37376
  throw new PipelineExecutionError(`Model requested tools but no executionTools.script were provided in OpenAiAgentKitExecutionTools options`);
35731
37377
  }
35732
- return Array.isArray(executionTools.script) ? executionTools.script : [executionTools.script];
37378
+ return Array.isArray(executionTools.script)
37379
+ ? executionTools.script
37380
+ : [executionTools.script];
35733
37381
  }
35734
37382
  /**
35735
37383
  * Resolves the assistant-visible AgentKit tool response while preserving structured tool result data.
@@ -38791,7 +40439,7 @@
38791
40439
  return { skippedReason: 'invalid_data_url' };
38792
40440
  }
38793
40441
  return {
38794
- file: new File([parsed.buffer], parsed.filename, {
40442
+ file: new File([new Uint8Array(parsed.buffer)], parsed.filename, {
38795
40443
  type: parsed.mimeType,
38796
40444
  }),
38797
40445
  sizeBytes: parsed.buffer.length,
@@ -40554,7 +42202,7 @@
40554
42202
  * Computes one stable hash from a JSON-serializable value.
40555
42203
  */
40556
42204
  function computeJsonHash$1(value) {
40557
- return cryptoJs.SHA256(JSON.stringify(value)).toString();
42205
+ return CryptoJS__default["default"].SHA256(JSON.stringify(value)).toString();
40558
42206
  }
40559
42207
  /**
40560
42208
  * Handles OpenAI AgentKit-backed executions for `AgentLlmExecutionTools`.
@@ -40712,7 +42360,7 @@
40712
42360
  * Computes one stable hash from a JSON-serializable value.
40713
42361
  */
40714
42362
  function computeJsonHash(value) {
40715
- return cryptoJs.SHA256(JSON.stringify(value)).toString();
42363
+ return CryptoJS__default["default"].SHA256(JSON.stringify(value)).toString();
40716
42364
  }
40717
42365
  /**
40718
42366
  * Removes assistant-managed requirements before the prompt is executed via OpenAI Assistants.
@@ -41102,7 +42750,11 @@
41102
42750
  };
41103
42751
  }
41104
42752
  return {
41105
- content: `${content.slice(0, Math.max(0, maxCharacters))}\n\n[...truncated...]`,
42753
+ content: spacetrim.spaceTrim((block) => `
42754
+ ${block(content.slice(0, Math.max(0, maxCharacters)))}
42755
+
42756
+ [...truncated...]
42757
+ `),
41106
42758
  isTruncated: true,
41107
42759
  };
41108
42760
  }
@@ -41568,7 +43220,7 @@
41568
43220
  * Returns a virtual model name representing the agent behavior.
41569
43221
  */
41570
43222
  get modelName() {
41571
- const hash = cryptoJs.SHA256(hexEncoder__default["default"].parse(this.options.agentSource)).toString( /* hex */);
43223
+ const hash = CryptoJS__default["default"].SHA256(hexEncoder__default["default"].parse(this.options.agentSource)).toString( /* hex */);
41572
43224
  const agentId = hash.substring(0, 10);
41573
43225
  return (normalizeToKebabCase(this.title) + '-' + agentId);
41574
43226
  }
@@ -45279,7 +46931,7 @@
45279
46931
  title: 'Anthropic Claude',
45280
46932
  packageName: '@promptbook/anthropic-claude',
45281
46933
  className: 'AnthropicClaudeExecutionTools',
45282
- envVariables: ['ANTHROPIC_CLAUDE_API_KEY'],
46934
+ envVariables: ['ANTHROPIC_API_KEY', 'ANTHROPIC_CLAUDE_API_KEY'],
45283
46935
  trustLevel: 'CLOSED',
45284
46936
  order: MODEL_ORDERS.TOP_TIER,
45285
46937
  getBoilerplateConfiguration() {
@@ -45296,14 +46948,17 @@
45296
46948
  };
45297
46949
  },
45298
46950
  createConfigurationFromEnv(env) {
46951
+ var _a;
45299
46952
  // Note: Note using `process.env` BUT `env` to pass in the environment variables dynamically
45300
- if (typeof env.ANTHROPIC_CLAUDE_API_KEY === 'string') {
46953
+ // Note: `ANTHROPIC_API_KEY` takes precedence over the deprecated `ANTHROPIC_CLAUDE_API_KEY`
46954
+ const apiKey = (_a = env.ANTHROPIC_API_KEY) !== null && _a !== void 0 ? _a : env.ANTHROPIC_CLAUDE_API_KEY;
46955
+ if (typeof apiKey === 'string') {
45301
46956
  return {
45302
46957
  title: 'Claude (from env)',
45303
46958
  packageName: '@promptbook/anthropic-claude',
45304
46959
  className: 'AnthropicClaudeExecutionTools',
45305
46960
  options: {
45306
- apiKey: env.ANTHROPIC_CLAUDE_API_KEY,
46961
+ apiKey,
45307
46962
  },
45308
46963
  };
45309
46964
  }
@@ -48714,20 +50369,7 @@
48714
50369
  }
48715
50370
 
48716
50371
  /**
48717
- * Core Promptbook server configuration
48718
- *
48719
- * Used for "Adam" agent which is built in as default ancestor for new agents and other well known agents
48720
- *
48721
- * @public exported from `@promptbook/core`
48722
- */
48723
- const CORE_AGENTS_SERVER = {
48724
- title: 'Promptbook Core',
48725
- description: `Core Promptbook server used for Adam agent which is built in as default ancestor for new agents and other well known agents.`,
48726
- owner: PROMPTBOOK_LEGAL_ENTITY,
48727
- url: 'https://core.ptbk.io/',
48728
- };
48729
- /**
48730
- * Names of well known agents hosted on Core Promptbook server
50372
+ * Names of well known agents bundled in the local `.core` folder of every Agents Server
48731
50373
  *
48732
50374
  * - `Adam`: The default ancestor agent for new agents
48733
50375
  * - `Teacher`: Agent that knows book syntax and can help with self-learning
@@ -48753,7 +50395,6 @@
48753
50395
  * @public exported from `@promptbook/core`
48754
50396
  */
48755
50397
  const PUBLIC_AGENTS_SERVERS = [
48756
- CORE_AGENTS_SERVER,
48757
50398
  {
48758
50399
  title: 'Promptbook Gallery',
48759
50400
  description: `Gallery of ideas, AI professions,... like AI Agenta photobank.`,
@@ -49229,7 +50870,6 @@
49229
50870
  exports.CLAIM = CLAIM;
49230
50871
  exports.CLI_APP_ID = CLI_APP_ID;
49231
50872
  exports.COLOR_CONSTANTS = COLOR_CONSTANTS;
49232
- exports.CORE_AGENTS_SERVER = CORE_AGENTS_SERVER;
49233
50873
  exports.CORE_AGENTS_SERVER_WELL_KNOWN_AGENT_NAMES = CORE_AGENTS_SERVER_WELL_KNOWN_AGENT_NAMES;
49234
50874
  exports.CallbackInterfaceTools = CallbackInterfaceTools;
49235
50875
  exports.ChatbotFormfactorDefinition = ChatbotFormfactorDefinition;