@promptbook/browser 0.113.0-1 → 0.113.0-11

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 (248) hide show
  1. package/README.md +1 -17
  2. package/esm/index.es.js +1819 -1815
  3. package/esm/index.es.js.map +1 -1
  4. package/esm/src/_packages/types.index.d.ts +2 -2
  5. package/esm/src/avatars/renderAvatarVisualAsciiArt.d.ts +98 -0
  6. package/esm/src/avatars/visuals/octopus3dAvatarVisualShared.d.ts +1 -1
  7. package/esm/src/book-components/BookEditor/BookEditorMonacoUploadPanel.d.ts +1 -1
  8. package/esm/src/book-components/BookEditor/useBookEditorMonacoUploads/bookEditorMonacoUploadTypes.d.ts +120 -0
  9. package/esm/src/book-components/BookEditor/useBookEditorMonacoUploads/clearScheduledTimer.d.ts +6 -0
  10. package/esm/src/book-components/BookEditor/useBookEditorMonacoUploads/enqueueFilesForUpload.d.ts +26 -0
  11. package/esm/src/book-components/BookEditor/useBookEditorMonacoUploads/useBookEditorMonacoUploadEditorSync.d.ts +23 -0
  12. package/esm/src/book-components/BookEditor/useBookEditorMonacoUploads/useBookEditorMonacoUploadItemsState.d.ts +21 -0
  13. package/esm/src/book-components/BookEditor/useBookEditorMonacoUploads/useBookEditorMonacoUploadProgressQueue.d.ts +12 -0
  14. package/esm/src/book-components/BookEditor/useBookEditorMonacoUploads/useBookEditorMonacoUploadQueue.d.ts +27 -0
  15. package/esm/src/book-components/BookEditor/useBookEditorMonacoUploads/useCompletedUploadsAutoClear.d.ts +12 -0
  16. package/esm/src/book-components/BookEditor/useBookEditorMonacoUploads.d.ts +5 -58
  17. package/esm/src/book-components/Chat/Chat/ChatImageAttachmentModal.d.ts +25 -0
  18. package/esm/src/book-components/Chat/Chat/ChatInputArea.d.ts +3 -0
  19. package/esm/src/book-components/Chat/Chat/ChatMessageAttachments.d.ts +27 -0
  20. package/esm/src/book-components/Chat/Chat/ChatMessageItem.d.ts +1 -1
  21. package/esm/src/book-components/Chat/Chat/ChatMessageList.d.ts +1 -0
  22. package/esm/src/book-components/Chat/Chat/ChatProps.d.ts +8 -0
  23. package/esm/src/book-components/Chat/Chat/CitationIframePreview.d.ts +1 -1
  24. package/esm/src/book-components/Chat/Chat/CitationIframePreview.test.d.ts +2 -0
  25. package/esm/src/book-components/Chat/Chat/insertDictationChunk.d.ts +1 -1
  26. package/esm/src/book-components/Chat/Chat/insertDictationChunk.test.d.ts +1 -0
  27. package/esm/src/book-components/Chat/Chat/learnDictationDictionary.test.d.ts +1 -0
  28. package/esm/src/book-components/Chat/Chat/refineFinalDictationChunk.test.d.ts +1 -0
  29. package/esm/src/book-components/Chat/Chat/useChatInputAreaComposer.d.ts +12 -0
  30. package/esm/src/book-components/Chat/Chat/useChatInputAreaComposer.draftMessage.test.d.ts +2 -0
  31. package/esm/src/book-components/Chat/utils/isVisibleChatToolCall.d.ts +1 -1
  32. package/esm/src/book-components/Chat/utils/parseMessageButtons.d.ts +16 -2
  33. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/AGENTS_SERVER_BUILD_CACHE_FILENAME.d.ts +6 -0
  34. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/AGENTS_SERVER_BUILD_CACHE_VERSION.d.ts +6 -0
  35. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/AGENTS_SERVER_NEXT_BUILD_ID_FILENAME.d.ts +6 -0
  36. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/AgentsServerBuildCache.d.ts +10 -0
  37. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/AgentsServerBuildCacheOptions.d.ts +10 -0
  38. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/NODE_MODULES_DIRECTORY_NAME.d.ts +6 -0
  39. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/PreparedAgentsServerRuntime.d.ts +18 -0
  40. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/createAgentsServerBuildSourceFingerprint.d.ts +6 -0
  41. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/createAgentsServerRuntimeEnvironment.d.ts +28 -0
  42. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/ensureAgentsServerBuild.d.ts +19 -0
  43. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/getAgentsServerBuildInputRelativePaths.d.ts +11 -0
  44. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/isAgentsServerAppPath.d.ts +6 -0
  45. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/isAgentsServerAppPathMaterialized.d.ts +6 -0
  46. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/isAgentsServerBuildCacheCurrent.d.ts +7 -0
  47. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/isAgentsServerBuildInputTestFile.d.ts +6 -0
  48. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/isExcludedAgentsServerBuildInputDirectoryName.d.ts +6 -0
  49. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/isExcludedAgentsServerBuildInputPath.d.ts +6 -0
  50. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/isExcludedAgentsServerRuntimeSourcePath.d.ts +6 -0
  51. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/isFile.d.ts +6 -0
  52. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/isPathInsideNodeModules.d.ts +6 -0
  53. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/normalizeAgentsServerBuildInputPath.d.ts +6 -0
  54. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/prepareAgentsServerRuntime.d.ts +9 -0
  55. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/readAgentsServerBuildCache.d.ts +8 -0
  56. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/resolveAgentsServerAppPath.d.ts +6 -0
  57. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/resolveAgentsServerBuildAppPath.d.ts +9 -0
  58. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/resolveAgentsServerBuildOutputPath.d.ts +7 -0
  59. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/resolveNextCliPath.d.ts +6 -0
  60. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/resolveNodeModulesPath.d.ts +6 -0
  61. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/runNextBuild.d.ts +12 -0
  62. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/shouldCopyAgentsServerRuntimePath.d.ts +6 -0
  63. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/synchronizeMaterializedAgentsServerRuntime.d.ts +11 -0
  64. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer/writeAgentsServerBuildCache.d.ts +7 -0
  65. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer.d.ts +8 -109
  66. package/esm/src/cli/cli-commands/agents-server/ensureAgentsServerEnvFile.d.ts +1 -1
  67. package/esm/src/cli/cli-commands/agents-server/ensureAgentsServerGitignoreFile.d.ts +1 -1
  68. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/AgentsServerChildEnvironment.d.ts +16 -0
  69. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/AgentsServerLogStreams.d.ts +30 -0
  70. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/AgentsServerRuntimePaths.d.ts +19 -0
  71. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/AgentsServerSupervisorState.d.ts +37 -0
  72. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/LocalAgentRunnerLimits.d.ts +9 -0
  73. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/PreparedAgentsServerLaunch.d.ts +13 -0
  74. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/StartAgentsServerOptions.d.ts +24 -0
  75. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/createInternalRouteErrorMessage.d.ts +6 -0
  76. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/createLocalAgentRunOptions.d.ts +9 -0
  77. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/forwardChildOutput.d.ts +13 -0
  78. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/loadAgentsServerProjectEnvironment.d.ts +6 -0
  79. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/prepareAgentsServerLaunch.d.ts +18 -0
  80. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/readInternalRouteErrorDetails.d.ts +6 -0
  81. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/resolveAgentsServerChildHostname.d.ts +7 -0
  82. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/startNextServer.d.ts +20 -0
  83. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/startUserChatJobWorkerPump.d.ts +15 -0
  84. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/stopChildProcess.d.ts +8 -0
  85. package/esm/src/cli/cli-commands/agents-server/startAgentsServer/waitForLocalAgentRunnerLimits.d.ts +16 -0
  86. package/esm/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +3 -30
  87. package/esm/src/cli/cli-commands/common/createPositiveIntegerOptionParser.d.ts +10 -0
  88. package/esm/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +1 -1
  89. package/esm/src/collection/agent-collection/CreateAgentInput.d.ts +1 -1
  90. package/esm/src/collection/agent-collection/constructors/agent-collection-in-supabase/prepareAgentSourceForPersistence.d.ts +1 -1
  91. package/esm/src/commitments/META/META.d.ts +2 -2
  92. package/esm/src/commitments/META_AVATAR/META_AVATAR.d.ts +9 -4
  93. package/esm/src/commitments/index.d.ts +1 -1
  94. package/esm/src/conversion/validation/validatePipeline/createPipelineValidationContext.d.ts +16 -0
  95. package/esm/src/conversion/validation/validatePipeline/validatePipelineCollectionsStructure.d.ts +7 -0
  96. package/esm/src/conversion/validation/validatePipeline/validatePipelineDependencyResolution.d.ts +7 -0
  97. package/esm/src/conversion/validation/validatePipeline/validatePipelineMetadata.d.ts +7 -0
  98. package/esm/src/conversion/validation/validatePipeline/validatePipelineParameters.d.ts +7 -0
  99. package/esm/src/conversion/validation/validatePipeline/validatePipelineTasks.d.ts +7 -0
  100. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/RegisteredLlmToolsMessageContext.d.ts +46 -0
  101. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/USED_ENV_FILENAME.d.ts +11 -0
  102. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/createRegisteredLlmToolConfigurationStatusMessage.d.ts +7 -0
  103. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/createRegisteredLlmToolEntryKey.d.ts +7 -0
  104. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/createRegisteredLlmToolInstallationStatusMessage.d.ts +7 -0
  105. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/createRegisteredLlmToolStatus.d.ts +14 -0
  106. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/createRegisteredLlmToolsMessageContext.d.ts +7 -0
  107. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/getAvailableRegisteredLlmToolsEnvironmentVariables.d.ts +7 -0
  108. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/getRegisteredLlmToolRegisters.d.ts +7 -0
  109. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/hasRegisteredLlmToolEntry.d.ts +7 -0
  110. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/listRegisteredLlmToolEntries.d.ts +9 -0
  111. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/listRegisteredLlmToolStatuses.d.ts +7 -0
  112. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/renderAvailableRegisteredLlmTools.d.ts +7 -0
  113. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/renderNoRegisteredLlmToolsMessage.d.ts +7 -0
  114. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/renderRegisteredLlmToolsMessage.d.ts +8 -0
  115. package/esm/src/llm-providers/_common/register/$registeredLlmToolsMessage/renderRelevantRegisteredLlmToolsEnvironmentVariables.d.ts +7 -0
  116. package/esm/src/utils/agent-message-runtime/agentMessageRuntimePaths.d.ts +24 -0
  117. package/esm/src/utils/agents/resolveAgentAvatarImageUrl.d.ts +2 -2
  118. package/esm/src/utils/agents/terminalAgentAvatarVisual.d.ts +94 -0
  119. package/esm/src/utils/agents/terminalAgentAvatarVisual.test.d.ts +1 -0
  120. package/esm/src/utils/ascii-art/$detectTerminalAnsiColorDepth.d.ts +15 -0
  121. package/esm/src/utils/ascii-art/convertImageDataToAsciiArt.d.ts +80 -0
  122. package/esm/src/utils/color/Color.d.ts +1 -1
  123. package/esm/src/version.d.ts +1 -1
  124. package/package.json +2 -2
  125. package/umd/index.umd.js +1819 -1815
  126. package/umd/index.umd.js.map +1 -1
  127. package/umd/src/_packages/types.index.d.ts +2 -2
  128. package/umd/src/avatars/renderAvatarVisualAsciiArt.d.ts +98 -0
  129. package/umd/src/avatars/visuals/octopus3dAvatarVisualShared.d.ts +1 -1
  130. package/umd/src/book-components/BookEditor/BookEditorMonacoUploadPanel.d.ts +1 -1
  131. package/umd/src/book-components/BookEditor/useBookEditorMonacoUploads/bookEditorMonacoUploadTypes.d.ts +120 -0
  132. package/umd/src/book-components/BookEditor/useBookEditorMonacoUploads/clearScheduledTimer.d.ts +6 -0
  133. package/umd/src/book-components/BookEditor/useBookEditorMonacoUploads/enqueueFilesForUpload.d.ts +26 -0
  134. package/umd/src/book-components/BookEditor/useBookEditorMonacoUploads/useBookEditorMonacoUploadEditorSync.d.ts +23 -0
  135. package/umd/src/book-components/BookEditor/useBookEditorMonacoUploads/useBookEditorMonacoUploadItemsState.d.ts +21 -0
  136. package/umd/src/book-components/BookEditor/useBookEditorMonacoUploads/useBookEditorMonacoUploadProgressQueue.d.ts +12 -0
  137. package/umd/src/book-components/BookEditor/useBookEditorMonacoUploads/useBookEditorMonacoUploadQueue.d.ts +27 -0
  138. package/umd/src/book-components/BookEditor/useBookEditorMonacoUploads/useCompletedUploadsAutoClear.d.ts +12 -0
  139. package/umd/src/book-components/BookEditor/useBookEditorMonacoUploads.d.ts +5 -58
  140. package/umd/src/book-components/Chat/Chat/ChatImageAttachmentModal.d.ts +25 -0
  141. package/umd/src/book-components/Chat/Chat/ChatInputArea.d.ts +3 -0
  142. package/umd/src/book-components/Chat/Chat/ChatMessageAttachments.d.ts +27 -0
  143. package/umd/src/book-components/Chat/Chat/ChatMessageItem.d.ts +1 -1
  144. package/umd/src/book-components/Chat/Chat/ChatMessageList.d.ts +1 -0
  145. package/umd/src/book-components/Chat/Chat/ChatProps.d.ts +8 -0
  146. package/umd/src/book-components/Chat/Chat/CitationIframePreview.d.ts +1 -1
  147. package/umd/src/book-components/Chat/Chat/CitationIframePreview.test.d.ts +2 -0
  148. package/umd/src/book-components/Chat/Chat/insertDictationChunk.d.ts +1 -1
  149. package/umd/src/book-components/Chat/Chat/insertDictationChunk.test.d.ts +1 -0
  150. package/umd/src/book-components/Chat/Chat/learnDictationDictionary.test.d.ts +1 -0
  151. package/umd/src/book-components/Chat/Chat/refineFinalDictationChunk.test.d.ts +1 -0
  152. package/umd/src/book-components/Chat/Chat/useChatInputAreaComposer.d.ts +12 -0
  153. package/umd/src/book-components/Chat/Chat/useChatInputAreaComposer.draftMessage.test.d.ts +2 -0
  154. package/umd/src/book-components/Chat/utils/isVisibleChatToolCall.d.ts +1 -1
  155. package/umd/src/book-components/Chat/utils/parseMessageButtons.d.ts +16 -2
  156. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/AGENTS_SERVER_BUILD_CACHE_FILENAME.d.ts +6 -0
  157. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/AGENTS_SERVER_BUILD_CACHE_VERSION.d.ts +6 -0
  158. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/AGENTS_SERVER_NEXT_BUILD_ID_FILENAME.d.ts +6 -0
  159. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/AgentsServerBuildCache.d.ts +10 -0
  160. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/AgentsServerBuildCacheOptions.d.ts +10 -0
  161. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/NODE_MODULES_DIRECTORY_NAME.d.ts +6 -0
  162. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/PreparedAgentsServerRuntime.d.ts +18 -0
  163. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/createAgentsServerBuildSourceFingerprint.d.ts +6 -0
  164. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/createAgentsServerRuntimeEnvironment.d.ts +28 -0
  165. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/ensureAgentsServerBuild.d.ts +19 -0
  166. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/getAgentsServerBuildInputRelativePaths.d.ts +11 -0
  167. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/isAgentsServerAppPath.d.ts +6 -0
  168. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/isAgentsServerAppPathMaterialized.d.ts +6 -0
  169. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/isAgentsServerBuildCacheCurrent.d.ts +7 -0
  170. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/isAgentsServerBuildInputTestFile.d.ts +6 -0
  171. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/isExcludedAgentsServerBuildInputDirectoryName.d.ts +6 -0
  172. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/isExcludedAgentsServerBuildInputPath.d.ts +6 -0
  173. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/isExcludedAgentsServerRuntimeSourcePath.d.ts +6 -0
  174. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/isFile.d.ts +6 -0
  175. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/isPathInsideNodeModules.d.ts +6 -0
  176. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/normalizeAgentsServerBuildInputPath.d.ts +6 -0
  177. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/prepareAgentsServerRuntime.d.ts +9 -0
  178. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/readAgentsServerBuildCache.d.ts +8 -0
  179. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/resolveAgentsServerAppPath.d.ts +6 -0
  180. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/resolveAgentsServerBuildAppPath.d.ts +9 -0
  181. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/resolveAgentsServerBuildOutputPath.d.ts +7 -0
  182. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/resolveNextCliPath.d.ts +6 -0
  183. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/resolveNodeModulesPath.d.ts +6 -0
  184. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/runNextBuild.d.ts +12 -0
  185. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/shouldCopyAgentsServerRuntimePath.d.ts +6 -0
  186. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/synchronizeMaterializedAgentsServerRuntime.d.ts +11 -0
  187. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer/writeAgentsServerBuildCache.d.ts +7 -0
  188. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer.d.ts +8 -109
  189. package/umd/src/cli/cli-commands/agents-server/ensureAgentsServerEnvFile.d.ts +1 -1
  190. package/umd/src/cli/cli-commands/agents-server/ensureAgentsServerGitignoreFile.d.ts +1 -1
  191. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/AgentsServerChildEnvironment.d.ts +16 -0
  192. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/AgentsServerLogStreams.d.ts +30 -0
  193. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/AgentsServerRuntimePaths.d.ts +19 -0
  194. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/AgentsServerSupervisorState.d.ts +37 -0
  195. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/LocalAgentRunnerLimits.d.ts +9 -0
  196. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/PreparedAgentsServerLaunch.d.ts +13 -0
  197. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/StartAgentsServerOptions.d.ts +24 -0
  198. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/createInternalRouteErrorMessage.d.ts +6 -0
  199. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/createLocalAgentRunOptions.d.ts +9 -0
  200. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/forwardChildOutput.d.ts +13 -0
  201. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/loadAgentsServerProjectEnvironment.d.ts +6 -0
  202. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/prepareAgentsServerLaunch.d.ts +18 -0
  203. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/readInternalRouteErrorDetails.d.ts +6 -0
  204. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/resolveAgentsServerChildHostname.d.ts +7 -0
  205. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/startNextServer.d.ts +20 -0
  206. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/startUserChatJobWorkerPump.d.ts +15 -0
  207. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/stopChildProcess.d.ts +8 -0
  208. package/umd/src/cli/cli-commands/agents-server/startAgentsServer/waitForLocalAgentRunnerLimits.d.ts +16 -0
  209. package/umd/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +3 -30
  210. package/umd/src/cli/cli-commands/common/createPositiveIntegerOptionParser.d.ts +10 -0
  211. package/umd/src/cli/cli-commands/common/promptRunnerCliOptions.d.ts +1 -1
  212. package/umd/src/collection/agent-collection/CreateAgentInput.d.ts +1 -1
  213. package/umd/src/collection/agent-collection/constructors/agent-collection-in-supabase/prepareAgentSourceForPersistence.d.ts +1 -1
  214. package/umd/src/commitments/META/META.d.ts +2 -2
  215. package/umd/src/commitments/META_AVATAR/META_AVATAR.d.ts +9 -4
  216. package/umd/src/commitments/index.d.ts +1 -1
  217. package/umd/src/conversion/validation/validatePipeline/createPipelineValidationContext.d.ts +16 -0
  218. package/umd/src/conversion/validation/validatePipeline/validatePipelineCollectionsStructure.d.ts +7 -0
  219. package/umd/src/conversion/validation/validatePipeline/validatePipelineDependencyResolution.d.ts +7 -0
  220. package/umd/src/conversion/validation/validatePipeline/validatePipelineMetadata.d.ts +7 -0
  221. package/umd/src/conversion/validation/validatePipeline/validatePipelineParameters.d.ts +7 -0
  222. package/umd/src/conversion/validation/validatePipeline/validatePipelineTasks.d.ts +7 -0
  223. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/RegisteredLlmToolsMessageContext.d.ts +46 -0
  224. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/USED_ENV_FILENAME.d.ts +11 -0
  225. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/createRegisteredLlmToolConfigurationStatusMessage.d.ts +7 -0
  226. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/createRegisteredLlmToolEntryKey.d.ts +7 -0
  227. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/createRegisteredLlmToolInstallationStatusMessage.d.ts +7 -0
  228. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/createRegisteredLlmToolStatus.d.ts +14 -0
  229. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/createRegisteredLlmToolsMessageContext.d.ts +7 -0
  230. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/getAvailableRegisteredLlmToolsEnvironmentVariables.d.ts +7 -0
  231. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/getRegisteredLlmToolRegisters.d.ts +7 -0
  232. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/hasRegisteredLlmToolEntry.d.ts +7 -0
  233. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/listRegisteredLlmToolEntries.d.ts +9 -0
  234. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/listRegisteredLlmToolStatuses.d.ts +7 -0
  235. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/renderAvailableRegisteredLlmTools.d.ts +7 -0
  236. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/renderNoRegisteredLlmToolsMessage.d.ts +7 -0
  237. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/renderRegisteredLlmToolsMessage.d.ts +8 -0
  238. package/umd/src/llm-providers/_common/register/$registeredLlmToolsMessage/renderRelevantRegisteredLlmToolsEnvironmentVariables.d.ts +7 -0
  239. package/umd/src/utils/agent-message-runtime/agentMessageRuntimePaths.d.ts +24 -0
  240. package/umd/src/utils/agents/resolveAgentAvatarImageUrl.d.ts +2 -2
  241. package/umd/src/utils/agents/terminalAgentAvatarVisual.d.ts +94 -0
  242. package/umd/src/utils/agents/terminalAgentAvatarVisual.test.d.ts +1 -0
  243. package/umd/src/utils/ascii-art/$detectTerminalAnsiColorDepth.d.ts +15 -0
  244. package/umd/src/utils/ascii-art/convertImageDataToAsciiArt.d.ts +80 -0
  245. package/umd/src/utils/color/Color.d.ts +1 -1
  246. package/umd/src/version.d.ts +1 -1
  247. package/esm/src/book-components/Chat/utils/livePagePreviewConstants.d.ts +0 -12
  248. package/umd/src/book-components/Chat/utils/livePagePreviewConstants.d.ts +0 -12
package/umd/index.umd.js CHANGED
@@ -29,7 +29,7 @@
29
29
  * @generated
30
30
  * @see https://github.com/webgptorg/promptbook
31
31
  */
32
- const PROMPTBOOK_ENGINE_VERSION = '0.113.0-1';
32
+ const PROMPTBOOK_ENGINE_VERSION = '0.113.0-11';
33
33
  /**
34
34
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
35
35
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -92,6 +92,127 @@
92
92
  });
93
93
  }
94
94
 
95
+ /**
96
+ * Validates that a channel value is a valid number within the range of 0 to 255.
97
+ * Throws an error if the value is not valid.
98
+ *
99
+ * @param channelName - The name of the channel being validated.
100
+ * @param value - The value of the channel to validate.
101
+ * @throws Will throw an error if the value is not a valid channel number.
102
+ *
103
+ * @private util of `@promptbook/color`
104
+ */
105
+ function checkChannelValue(channelName, value) {
106
+ if (typeof value !== 'number') {
107
+ throw new Error(`${channelName} channel value is not number but ${typeof value}`);
108
+ }
109
+ if (isNaN(value)) {
110
+ throw new Error(`${channelName} channel value is NaN`);
111
+ }
112
+ if (Math.round(value) !== value) {
113
+ throw new Error(`${channelName} channel is not whole number, it is ${value}`);
114
+ }
115
+ if (value < 0) {
116
+ throw new Error(`${channelName} channel is lower than 0, it is ${value}`);
117
+ }
118
+ if (value > 255) {
119
+ throw new Error(`${channelName} channel is greater than 255, it is ${value}`);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Shared immutable channel storage and serialization helpers for `Color`.
125
+ *
126
+ * @private base class of Color
127
+ */
128
+ class ColorValue {
129
+ constructor(red, green, blue, alpha = 255) {
130
+ this.red = red;
131
+ this.green = green;
132
+ this.blue = blue;
133
+ this.alpha = alpha;
134
+ checkChannelValue('Red', red);
135
+ checkChannelValue('Green', green);
136
+ checkChannelValue('Blue', blue);
137
+ checkChannelValue('Alpha', alpha);
138
+ }
139
+ /**
140
+ * Shortcut for `red` property
141
+ * Number from 0 to 255
142
+ * @alias red
143
+ */
144
+ get r() {
145
+ return this.red;
146
+ }
147
+ /**
148
+ * Shortcut for `green` property
149
+ * Number from 0 to 255
150
+ * @alias green
151
+ */
152
+ get g() {
153
+ return this.green;
154
+ }
155
+ /**
156
+ * Shortcut for `blue` property
157
+ * Number from 0 to 255
158
+ * @alias blue
159
+ */
160
+ get b() {
161
+ return this.blue;
162
+ }
163
+ /**
164
+ * Shortcut for `alpha` property
165
+ * Number from 0 (transparent) to 255 (opaque)
166
+ * @alias alpha
167
+ */
168
+ get a() {
169
+ return this.alpha;
170
+ }
171
+ /**
172
+ * Shortcut for `alpha` property
173
+ * Number from 0 (transparent) to 255 (opaque)
174
+ * @alias alpha
175
+ */
176
+ get opacity() {
177
+ return this.alpha;
178
+ }
179
+ /**
180
+ * Shortcut for 1-`alpha` property
181
+ */
182
+ get transparency() {
183
+ return 255 - this.alpha;
184
+ }
185
+ clone() {
186
+ return take(this.createColor(this.red, this.green, this.blue, this.alpha));
187
+ }
188
+ toString() {
189
+ return this.toHex();
190
+ }
191
+ toHex() {
192
+ if (this.alpha === 255) {
193
+ return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
194
+ .toString(16)
195
+ .padStart(2, '0')}`;
196
+ }
197
+ else {
198
+ return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
199
+ .toString(16)
200
+ .padStart(2, '0')}${this.alpha.toString(16).padStart(2, '0')}`;
201
+ }
202
+ }
203
+ toRgb() {
204
+ if (this.alpha === 255) {
205
+ return `rgb(${this.red}, ${this.green}, ${this.blue})`;
206
+ }
207
+ else {
208
+ return `rgba(${this.red}, ${this.green}, ${this.blue}, ${Math.round((this.alpha / 255) * 100)}%)`;
209
+ }
210
+ }
211
+ toHsl() {
212
+ throw new Error(`Getting HSL is not implemented`);
213
+ }
214
+ }
215
+
95
216
  /**
96
217
  * 🎨 List of all 140 color names which are supported by CSS
97
218
  *
@@ -251,127 +372,6 @@
251
372
  };
252
373
  // Note: [💞] Ignore a discrepancy between file name and entity name
253
374
 
254
- /**
255
- * Validates that a channel value is a valid number within the range of 0 to 255.
256
- * Throws an error if the value is not valid.
257
- *
258
- * @param channelName - The name of the channel being validated.
259
- * @param value - The value of the channel to validate.
260
- * @throws Will throw an error if the value is not a valid channel number.
261
- *
262
- * @private util of `@promptbook/color`
263
- */
264
- function checkChannelValue(channelName, value) {
265
- if (typeof value !== 'number') {
266
- throw new Error(`${channelName} channel value is not number but ${typeof value}`);
267
- }
268
- if (isNaN(value)) {
269
- throw new Error(`${channelName} channel value is NaN`);
270
- }
271
- if (Math.round(value) !== value) {
272
- throw new Error(`${channelName} channel is not whole number, it is ${value}`);
273
- }
274
- if (value < 0) {
275
- throw new Error(`${channelName} channel is lower than 0, it is ${value}`);
276
- }
277
- if (value > 255) {
278
- throw new Error(`${channelName} channel is greater than 255, it is ${value}`);
279
- }
280
- }
281
-
282
- /**
283
- * Shared immutable channel storage and serialization helpers for `Color`.
284
- *
285
- * @private base class of Color
286
- */
287
- class ColorValue {
288
- constructor(red, green, blue, alpha = 255) {
289
- this.red = red;
290
- this.green = green;
291
- this.blue = blue;
292
- this.alpha = alpha;
293
- checkChannelValue('Red', red);
294
- checkChannelValue('Green', green);
295
- checkChannelValue('Blue', blue);
296
- checkChannelValue('Alpha', alpha);
297
- }
298
- /**
299
- * Shortcut for `red` property
300
- * Number from 0 to 255
301
- * @alias red
302
- */
303
- get r() {
304
- return this.red;
305
- }
306
- /**
307
- * Shortcut for `green` property
308
- * Number from 0 to 255
309
- * @alias green
310
- */
311
- get g() {
312
- return this.green;
313
- }
314
- /**
315
- * Shortcut for `blue` property
316
- * Number from 0 to 255
317
- * @alias blue
318
- */
319
- get b() {
320
- return this.blue;
321
- }
322
- /**
323
- * Shortcut for `alpha` property
324
- * Number from 0 (transparent) to 255 (opaque)
325
- * @alias alpha
326
- */
327
- get a() {
328
- return this.alpha;
329
- }
330
- /**
331
- * Shortcut for `alpha` property
332
- * Number from 0 (transparent) to 255 (opaque)
333
- * @alias alpha
334
- */
335
- get opacity() {
336
- return this.alpha;
337
- }
338
- /**
339
- * Shortcut for 1-`alpha` property
340
- */
341
- get transparency() {
342
- return 255 - this.alpha;
343
- }
344
- clone() {
345
- return take(this.createColor(this.red, this.green, this.blue, this.alpha));
346
- }
347
- toString() {
348
- return this.toHex();
349
- }
350
- toHex() {
351
- if (this.alpha === 255) {
352
- return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
353
- .toString(16)
354
- .padStart(2, '0')}`;
355
- }
356
- else {
357
- return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
358
- .toString(16)
359
- .padStart(2, '0')}${this.alpha.toString(16).padStart(2, '0')}`;
360
- }
361
- }
362
- toRgb() {
363
- if (this.alpha === 255) {
364
- return `rgb(${this.red}, ${this.green}, ${this.blue})`;
365
- }
366
- else {
367
- return `rgba(${this.red}, ${this.green}, ${this.blue}, ${Math.round((this.alpha / 255) * 100)}%)`;
368
- }
369
- }
370
- toHsl() {
371
- throw new Error(`Getting HSL is not implemented`);
372
- }
373
- }
374
-
375
375
  /**
376
376
  * Checks if the given value is a valid hex color string
377
377
  *
@@ -4816,7 +4816,7 @@
4816
4816
  * META commitment definition
4817
4817
  *
4818
4818
  * The META commitment handles all meta-information about the agent such as:
4819
- * - META AVATAR: Sets the agent's built-in default avatar visual
4819
+ * - META AVATAR / META VISUAL: Sets the agent's built-in default avatar visual
4820
4820
  * - META IMAGE: Sets the agent's avatar/profile image URL
4821
4821
  * - META LINK: Provides profile/source links for the person the agent models
4822
4822
  * - META DOMAIN: Sets the canonical custom domain/host of the agent
@@ -4831,7 +4831,7 @@
4831
4831
  * Example usage in agent source:
4832
4832
  *
4833
4833
  * ```book
4834
- * META AVATAR pixel-art
4834
+ * META VISUAL pixel-art
4835
4835
  * META IMAGE https://example.com/avatar.jpg
4836
4836
  * META LINK https://twitter.com/username
4837
4837
  * META DOMAIN my-agent.com
@@ -4871,7 +4871,7 @@
4871
4871
 
4872
4872
  ## Supported META types
4873
4873
 
4874
- - **META AVATAR** - Sets the agent's built-in default avatar visual
4874
+ - **META AVATAR** / **META VISUAL** - Sets the agent's built-in default avatar visual
4875
4875
  - **META IMAGE** - Sets the agent's avatar/profile image URL
4876
4876
  - **META LINK** - Provides profile/source links for the person the agent models
4877
4877
  - **META DOMAIN** - Sets the canonical custom domain/host of the agent
@@ -4895,7 +4895,7 @@
4895
4895
  \`\`\`book
4896
4896
  Professional Assistant
4897
4897
 
4898
- META AVATAR octopus3
4898
+ META VISUAL octopus3
4899
4899
  META IMAGE https://example.com/professional-avatar.jpg
4900
4900
  META TITLE Senior Business Consultant
4901
4901
  META DESCRIPTION Specialized in strategic planning and project management
@@ -8068,212 +8068,149 @@
8068
8068
 
8069
8069
  /* eslint-disable no-magic-numbers */
8070
8070
  /**
8071
- * Light direction used by the organic 3D octopus shading.
8071
+ * Light direction used by the single-mesh octopus shading.
8072
8072
  *
8073
- * @private helper of `octopus3dAvatarVisual`
8073
+ * @private helper of `octopus3d2AvatarVisual`
8074
8074
  */
8075
8075
  const LIGHT_DIRECTION$3 = normalizeVector3({
8076
- x: 0.48,
8077
- y: -0.62,
8078
- z: 0.94,
8076
+ x: 0.38,
8077
+ y: -0.6,
8078
+ z: 0.98,
8079
8079
  });
8080
8080
  /**
8081
8081
  * Cache keyed by the `createRandom` factory reference (stable per mounted `<Avatar/>`).
8082
8082
  *
8083
- * @private helper of `octopus3dAvatarVisual`
8083
+ * @private helper of `octopus3d2AvatarVisual`
8084
8084
  */
8085
- const octopus3dStableStateCache = new WeakMap();
8085
+ const octopus3d2StableStateCache = new WeakMap();
8086
8086
  /**
8087
8087
  * Returns the stable per-avatar state, computing it on first access and caching for subsequent frames.
8088
8088
  *
8089
- * @private helper of `octopus3dAvatarVisual`
8089
+ * @private helper of `octopus3d2AvatarVisual`
8090
8090
  */
8091
- function getOctopus3dStableState(createRandom) {
8092
- const cached = octopus3dStableStateCache.get(createRandom);
8091
+ function getOctopus3d2StableState(createRandom) {
8092
+ const cached = octopus3d2StableStateCache.get(createRandom);
8093
8093
  if (cached !== undefined) {
8094
8094
  return cached;
8095
8095
  }
8096
- const animationRandom = createRandom('octopus3d-animation-profile');
8097
- const eyeRandom = createRandom('octopus3d-eye-profile');
8098
- const leftEyePhaseOffset = eyeRandom() * 0.6;
8099
- const rightEyePhaseOffset = eyeRandom() * 0.6;
8096
+ const animationRandom = createRandom('octopus3d2-animation-profile');
8097
+ const eyeRandom = createRandom('octopus3d2-eye-profile');
8098
+ const leftEyePhaseOffset = eyeRandom() * 0.7;
8099
+ const rightEyePhaseOffset = eyeRandom() * 0.7;
8100
8100
  const state = {
8101
8101
  morphologyProfile: createOctopus3MorphologyProfile(createRandom),
8102
8102
  animationPhase: animationRandom() * Math.PI * 2,
8103
8103
  leftEyePhaseOffset,
8104
8104
  rightEyePhaseOffset,
8105
8105
  };
8106
- octopus3dStableStateCache.set(createRandom, state);
8106
+ octopus3d2StableStateCache.set(createRandom, state);
8107
8107
  return state;
8108
8108
  }
8109
8109
  /**
8110
- * Proper 3D Octopus visual built from projected organic meshes and tentacles.
8110
+ * Octopus 3D 2 avatar visual.
8111
8111
  *
8112
8112
  * @private built-in avatar visual
8113
8113
  */
8114
- const octopus3dAvatarVisual = {
8115
- id: 'octopus3d',
8116
- title: 'Octopus 3D',
8117
- description: 'Proper 3D octopus portrait with a turning silhouette, expressive eyes, and depth-sorted tentacles.',
8114
+ const octopus3d2AvatarVisual = {
8115
+ id: 'octopus3d2',
8116
+ title: 'Octopus 3D 2',
8117
+ description: 'Continuous blobby 3D octopus portrait with one soft mesh, turning silhouette, and cursor-aware eyes.',
8118
8118
  isAnimated: true,
8119
8119
  supportsPointerTracking: true,
8120
8120
  render({ context, size, palette, createRandom, timeMs, interaction }) {
8121
- const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset } = getOctopus3dStableState(createRandom);
8121
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset } = getOctopus3d2StableState(createRandom);
8122
8122
  const sceneCenterX = size * 0.5;
8123
- const sceneCenterY = size * 0.56;
8124
- const bob = Math.sin(timeMs / 920 + animationPhase) * size * 0.014;
8125
- const mantleCenter = {
8126
- x: interaction.bodyOffsetX * size * 0.042 + size * morphologyProfile.body.centerXJitterRatio * 0.65,
8127
- y: -size * 0.09 + interaction.bodyOffsetY * size * 0.028 + bob,
8128
- z: interaction.intensity * size * 0.012,
8129
- };
8130
- const underbodyCenter = {
8131
- x: mantleCenter.x * 0.86,
8132
- y: mantleCenter.y + size * 0.168,
8133
- z: mantleCenter.z - size * 0.018,
8123
+ const sceneCenterY = size * 0.575;
8124
+ const bob = Math.sin(timeMs / 940 + animationPhase) * size * 0.013;
8125
+ const meshCenter = {
8126
+ x: interaction.bodyOffsetX * size * 0.044 + size * morphologyProfile.body.centerXJitterRatio * 0.5,
8127
+ y: -size * 0.03 + interaction.bodyOffsetY * size * 0.026 + bob,
8128
+ z: interaction.intensity * size * 0.018,
8134
8129
  };
8135
- const mantleRadiusX = size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch;
8136
- const mantleRadiusY = size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1;
8137
- const mantleRadiusZ = size *
8138
- morphologyProfile.body.bodyRadiusRatio *
8139
- (0.9 + (morphologyProfile.body.horizontalStretch - 1) * 0.3);
8140
- const underbodyRadiusX = mantleRadiusX * (0.9 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.08);
8141
- const underbodyRadiusY = mantleRadiusY * (0.44 + morphologyProfile.body.lowerDropRatio * 3.1);
8142
- const underbodyRadiusZ = mantleRadiusZ * 0.78;
8143
- const bodyYaw = -0.18 +
8144
- Math.sin(timeMs / 2400 + animationPhase) * 0.05 +
8145
- interaction.bodyOffsetX * 0.18 +
8146
- interaction.gazeX * 0.22;
8147
- const bodyPitch = -0.08 +
8148
- Math.cos(timeMs / 2700 + animationPhase * 0.6) * 0.025 -
8130
+ const rotationY = -0.14 +
8131
+ Math.sin(timeMs / 2600 + animationPhase) * 0.04 +
8132
+ interaction.bodyOffsetX * 0.2 +
8133
+ interaction.gazeX * 0.78;
8134
+ const rotationX = -0.06 +
8135
+ Math.cos(timeMs / 3000 + animationPhase * 0.7) * 0.02 -
8149
8136
  interaction.bodyOffsetY * 0.08 -
8150
- interaction.gazeY * 0.08;
8151
- const headYaw = bodyYaw - 0.04 + interaction.gazeX * 0.56;
8152
- const headPitch = bodyPitch - 0.02 - interaction.gazeY * 0.32;
8153
- const mantlePatches = resolveVisibleEllipsoidPatches({
8154
- center: mantleCenter,
8155
- radiusX: mantleRadiusX,
8156
- radiusY: mantleRadiusY,
8157
- radiusZ: mantleRadiusZ,
8158
- rotationX: headPitch,
8159
- rotationY: headYaw,
8160
- sceneCenterX,
8161
- sceneCenterY,
8162
- size,
8163
- palette,
8164
- verticalColorBias: 0,
8165
- outlineColor: `${palette.highlight}7a`,
8166
- });
8167
- const underbodyPatches = resolveVisibleEllipsoidPatches({
8168
- center: underbodyCenter,
8169
- radiusX: underbodyRadiusX,
8170
- radiusY: underbodyRadiusY,
8171
- radiusZ: underbodyRadiusZ,
8172
- rotationX: bodyPitch,
8173
- rotationY: bodyYaw,
8174
- sceneCenterX,
8175
- sceneCenterY,
8176
- size,
8177
- palette,
8178
- verticalColorBias: 0.18,
8179
- outlineColor: `${palette.shadow}8f`,
8180
- });
8181
- const tentacleStrokes = createOctopusTentacleStrokes({
8182
- createRandom,
8137
+ interaction.gazeY * 0.34;
8138
+ const surfaceOptions = {
8139
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.02,
8140
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.22,
8141
+ radiusZ: size *
8142
+ morphologyProfile.body.bodyRadiusRatio *
8143
+ (0.98 + (morphologyProfile.body.horizontalStretch - 1) * 0.2),
8183
8144
  morphologyProfile,
8184
8145
  timeMs,
8185
- size,
8186
- center: underbodyCenter,
8187
- radiusX: underbodyRadiusX,
8188
- radiusY: underbodyRadiusY,
8189
- radiusZ: underbodyRadiusZ,
8190
- rotationX: bodyPitch,
8191
- rotationY: bodyYaw,
8146
+ animationPhase,
8147
+ };
8148
+ const surfacePatches = resolveVisibleBlobbyOctopusPatches({
8149
+ ...surfaceOptions,
8150
+ center: meshCenter,
8151
+ rotationX,
8152
+ rotationY,
8192
8153
  sceneCenterX,
8193
8154
  sceneCenterY,
8194
- animationPhase,
8155
+ size,
8156
+ palette,
8195
8157
  });
8196
- const faceEyeSpacing = size * morphologyProfile.face.eyeSpacingRatio * 0.92;
8197
- const faceEyeYOffset = size * morphologyProfile.face.eyeCenterYOffsetRatio - mantleRadiusY * 0.02;
8198
- const faceEyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.82;
8199
- const faceEyeRadiusY = faceEyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.96;
8200
- const mouthHalfWidth = size * morphologyProfile.face.mouthWidthRatio * 0.92;
8201
- const mouthY = size * morphologyProfile.face.mouthYOffsetRatio + mantleRadiusY * 0.08;
8158
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.4, -0.16, 0.11);
8159
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.25, 0.2, 0.34);
8160
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.19 + morphologyProfile.face.mouthYOffsetRatio * 1.08, 0.08, 0.34);
8161
+ const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.8, -0.08, 0.08);
8162
+ const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.82, 0.16, 0.29);
8163
+ const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.85, mouthLatitude + 0.03, 0.42);
8202
8164
  drawAvatarFrame(context, size, palette);
8203
- drawOctopus3dAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
8204
- drawOctopus3dShadow(context, size, palette, interaction, timeMs);
8205
- for (const tentacleStroke of tentacleStrokes.filter((candidateTentacleStroke) => !candidateTentacleStroke.isFrontFacing)) {
8206
- drawTentacleStroke(context, tentacleStroke, palette);
8207
- }
8208
- for (const surfacePatch of [...mantlePatches, ...underbodyPatches].sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
8209
- drawSurfacePatch(context, surfacePatch);
8210
- }
8211
- for (const tentacleStroke of tentacleStrokes.filter((candidateTentacleStroke) => candidateTentacleStroke.isFrontFacing)) {
8212
- drawTentacleStroke(context, tentacleStroke, palette);
8165
+ drawBlobbyOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
8166
+ drawBlobbyOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile);
8167
+ for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
8168
+ drawBlobbySurfacePatch(context, surfacePatch);
8213
8169
  }
8214
- drawProjectedOrganicEye(context, {
8215
- x: -faceEyeSpacing,
8216
- y: faceEyeYOffset,
8217
- z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, -faceEyeSpacing, faceEyeYOffset),
8218
- }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8219
- drawProjectedOrganicEye(context, {
8220
- x: faceEyeSpacing,
8221
- y: faceEyeYOffset,
8222
- z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, faceEyeSpacing, faceEyeYOffset),
8223
- }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.7 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8170
+ const leftEyeLocalCenter = sampleBlobbyOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude);
8171
+ const rightEyeLocalCenter = sampleBlobbyOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude);
8172
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
8173
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
8174
+ drawProjectedOrganicEye(context, leftEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8175
+ drawProjectedOrganicEye(context, rightEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.9 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8224
8176
  drawProjectedOrganicMouth(context, [
8225
- {
8226
- x: -mouthHalfWidth,
8227
- y: mouthY,
8228
- z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, -mouthHalfWidth, mouthY),
8229
- },
8230
- {
8231
- x: size * morphologyProfile.face.mouthCenterOffsetRatio,
8232
- y: mouthY +
8233
- size * morphologyProfile.face.mouthCurveDepthRatio * 0.38 +
8234
- Math.sin(timeMs / 760 + animationPhase) * size * 0.01 +
8235
- interaction.gazeY * size * 0.01,
8236
- z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, size * morphologyProfile.face.mouthCenterOffsetRatio, mouthY),
8237
- },
8238
- {
8239
- x: mouthHalfWidth,
8240
- y: mouthY,
8241
- z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, mouthHalfWidth, mouthY),
8242
- },
8243
- ], mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, palette, size);
8177
+ sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
8178
+ sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
8179
+ sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
8180
+ ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
8244
8181
  },
8245
8182
  };
8246
8183
  /**
8247
- * Draws the atmospheric underwater glow behind the octopus mesh.
8184
+ * Draws the deep-water glow behind the continuous octopus mesh.
8248
8185
  *
8249
- * @private helper of `octopus3dAvatarVisual`
8186
+ * @private helper of `octopus3d2AvatarVisual`
8250
8187
  */
8251
- function drawOctopus3dAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
8252
- const glowGradient = context.createRadialGradient(sceneCenterX + interaction.gazeX * size * 0.1, sceneCenterY - size * 0.2 + interaction.gazeY * size * 0.05, size * 0.04, sceneCenterX, sceneCenterY - size * 0.04, size * 0.62);
8253
- glowGradient.addColorStop(0, `${palette.highlight}5c`);
8254
- glowGradient.addColorStop(0.36, `${palette.accent}24`);
8188
+ function drawBlobbyOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
8189
+ const glowGradient = context.createRadialGradient(sceneCenterX + interaction.gazeX * size * 0.11, sceneCenterY - size * 0.17 + interaction.gazeY * size * 0.05, size * 0.05, sceneCenterX, sceneCenterY - size * 0.03, size * 0.66);
8190
+ glowGradient.addColorStop(0, `${palette.highlight}5e`);
8191
+ glowGradient.addColorStop(0.38, `${palette.accent}26`);
8255
8192
  glowGradient.addColorStop(1, `${palette.highlight}00`);
8256
8193
  context.fillStyle = glowGradient;
8257
8194
  context.fillRect(0, 0, size, size);
8258
- const lowerGradient = context.createRadialGradient(sceneCenterX + Math.sin(timeMs / 1700) * size * 0.05, sceneCenterY + size * 0.23, size * 0.08, sceneCenterX, sceneCenterY + size * 0.28, size * 0.5);
8259
- lowerGradient.addColorStop(0, `${palette.secondary}1d`);
8195
+ const lowerGradient = context.createRadialGradient(sceneCenterX + Math.sin(timeMs / 1650) * size * 0.045, sceneCenterY + size * 0.28, size * 0.06, sceneCenterX, sceneCenterY + size * 0.28, size * 0.52);
8196
+ lowerGradient.addColorStop(0, `${palette.secondary}22`);
8260
8197
  lowerGradient.addColorStop(1, `${palette.secondary}00`);
8261
8198
  context.fillStyle = lowerGradient;
8262
8199
  context.fillRect(0, 0, size, size);
8263
8200
  }
8264
8201
  /**
8265
- * Draws the soft ground shadow below the octopus.
8202
+ * Draws the soft floor shadow that anchors the single mesh in the frame.
8266
8203
  *
8267
8204
  * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
8268
8205
  * blurry ellipse without triggering a costly software rasterization pass on every frame.
8269
8206
  *
8270
- * @private helper of `octopus3dAvatarVisual`
8207
+ * @private helper of `octopus3d2AvatarVisual`
8271
8208
  */
8272
- function drawOctopus3dShadow(context, size, palette, interaction, timeMs) {
8273
- const cx = size * 0.5 + interaction.gazeX * size * 0.04;
8274
- const cy = size * 0.87 + Math.sin(timeMs / 920) * size * 0.008;
8275
- const rx = size * (0.18 + interaction.intensity * 0.02);
8276
- const ry = size * 0.06;
8209
+ function drawBlobbyOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
8210
+ const cx = size * 0.5 + interaction.gazeX * size * 0.045;
8211
+ const cy = size * 0.88 + Math.sin(timeMs / 940) * size * 0.008;
8212
+ const rx = size * (0.18 + (morphologyProfile.body.horizontalStretch - 1) * 0.04 + interaction.intensity * 0.018);
8213
+ const ry = size * 0.062;
8277
8214
  context.save();
8278
8215
  context.translate(cx, cy);
8279
8216
  context.scale(1, ry / rx);
@@ -8290,86 +8227,181 @@
8290
8227
  context.restore();
8291
8228
  }
8292
8229
  /**
8293
- * Resolves visible projected patches for one rotated ellipsoid mesh.
8230
+ * Number of latitude segments used by the single blobby octopus mesh.
8294
8231
  *
8295
- * @private helper of `octopus3dAvatarVisual`
8232
+ * @private helper of `octopus3d2AvatarVisual`
8296
8233
  */
8297
- function resolveVisibleEllipsoidPatches(options) {
8298
- const { center, radiusX, radiusY, radiusZ, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, verticalColorBias, outlineColor, } = options;
8299
- const latitudePatchCount = 10;
8300
- const longitudePatchCount = 18;
8234
+ const LATITUDE_PATCH_COUNT$2 = 12;
8235
+ /**
8236
+ * Number of longitude segments used by the single blobby octopus mesh.
8237
+ *
8238
+ * @private helper of `octopus3d2AvatarVisual`
8239
+ */
8240
+ const LONGITUDE_PATCH_COUNT$2 = 24;
8241
+ /**
8242
+ * Resolves all visible projected patches for the single blobby octopus mesh.
8243
+ *
8244
+ * Within a single frame, mesh corner samples and longitude-only lobe-wave values are
8245
+ * quantized to the patch grid and computed once each rather than re-evaluated for every
8246
+ * patch corner — the patch loop alone would call `sampleBlobbyOctopusSurfacePoint`
8247
+ * `latitudePatchCount * longitudePatchCount * 4` times without caching, even though most
8248
+ * corners are shared between neighboring patches.
8249
+ *
8250
+ * @private helper of `octopus3d2AvatarVisual`
8251
+ */
8252
+ function resolveVisibleBlobbyOctopusPatches(options) {
8253
+ const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, animationPhase, timeMs, } = options;
8254
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$2;
8255
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$2;
8301
8256
  const surfacePatches = [];
8302
- for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
8303
- const startLatitude = -Math.PI / 2 + (latitudeIndex / latitudePatchCount) * Math.PI;
8304
- const endLatitude = -Math.PI / 2 + ((latitudeIndex + 1) / latitudePatchCount) * Math.PI;
8305
- const verticalProgress = (latitudeIndex + 0.5) / latitudePatchCount;
8306
- for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
8307
- const startLongitude = -Math.PI + (longitudeIndex / longitudePatchCount) * Math.PI * 2;
8308
- const endLongitude = -Math.PI + ((longitudeIndex + 1) / longitudePatchCount) * Math.PI * 2;
8309
- const localCorners = [
8310
- sampleEllipsoidPoint(radiusX, radiusY, radiusZ, startLatitude, startLongitude),
8311
- sampleEllipsoidPoint(radiusX, radiusY, radiusZ, startLatitude, endLongitude),
8312
- sampleEllipsoidPoint(radiusX, radiusY, radiusZ, endLatitude, endLongitude),
8313
- sampleEllipsoidPoint(radiusX, radiusY, radiusZ, endLatitude, startLongitude),
8257
+ const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
8258
+ const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
8259
+ for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
8260
+ latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
8261
+ }
8262
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
8263
+ longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
8264
+ }
8265
+ const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
8266
+ for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
8267
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveLowerLobeWave(longitudeBoundaries[boundaryIndex], morphologyProfile, animationPhase, timeMs);
8268
+ }
8269
+ const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
8270
+ const transformedCornerSamples = new Array(cornerCount);
8271
+ for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
8272
+ const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex];
8273
+ for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
8274
+ const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
8275
+ const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
8276
+ const cornerSample = sampleBlobbyOctopusSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
8277
+ transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
8278
+ }
8279
+ }
8280
+ for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
8281
+ const startLatitude = latitudeBoundaries[latitudeIndex];
8282
+ const endLatitude = latitudeBoundaries[latitudeIndex + 1];
8283
+ const centerLatitude = (startLatitude + endLatitude) / 2;
8284
+ const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
8285
+ const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
8286
+ const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
8287
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
8288
+ const startLongitude = longitudeBoundaries[longitudeIndex];
8289
+ const endLongitude = longitudeBoundaries[longitudeIndex + 1];
8290
+ const centerLongitude = (startLongitude + endLongitude) / 2;
8291
+ const transformedCorners = [
8292
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex],
8293
+ transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1],
8294
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1],
8295
+ transformedCornerSamples[endCornerRowOffset + longitudeIndex],
8314
8296
  ];
8315
- const transformedCorners = localCorners.map((localCorner) => transformScenePoint(localCorner, center, rotationX, rotationY));
8316
8297
  const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
8317
8298
  if (surfaceNormal.z <= 0.01) {
8318
8299
  continue;
8319
8300
  }
8320
- const projectedCorners = transformedCorners.map((transformedCorner) => projectScenePoint(transformedCorner, size, sceneCenterX, sceneCenterY));
8301
+ const projectedCorners = [
8302
+ projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
8303
+ projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
8304
+ projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
8305
+ projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
8306
+ ];
8321
8307
  surfacePatches.push({
8322
8308
  corners: projectedCorners,
8323
- averageDepth: transformedCorners.reduce((depthSum, transformedCorner) => depthSum + transformedCorner.z, 0) /
8324
- transformedCorners.length,
8309
+ averageDepth: (transformedCorners[0].z +
8310
+ transformedCorners[1].z +
8311
+ transformedCorners[2].z +
8312
+ transformedCorners[3].z) /
8313
+ 4,
8325
8314
  lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$3), -1, 1),
8326
- fillStyle: resolveSurfacePatchFillStyle(palette, verticalProgress + verticalColorBias),
8327
- outlineColor,
8315
+ fillStyle: resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
8316
+ outlineColor: verticalProgress < 0.58 ? `${palette.highlight}73` : `${palette.shadow}8a`,
8328
8317
  });
8329
8318
  }
8330
8319
  }
8331
8320
  return surfacePatches;
8332
8321
  }
8333
8322
  /**
8334
- * Samples one point on an ellipsoid aligned to the local axes.
8323
+ * Samples one point on the continuous Octopus 3D 2 surface.
8335
8324
  *
8336
- * @private helper of `octopus3dAvatarVisual`
8325
+ * The lower hemisphere widens and falls into soft lobe waves so the octopus stays one connected mesh
8326
+ * instead of switching to separately rendered tentacles.
8327
+ *
8328
+ * @private helper of `octopus3d2AvatarVisual`
8337
8329
  */
8338
- function sampleEllipsoidPoint(radiusX, radiusY, radiusZ, latitude, longitude) {
8339
- const cosineLatitude = Math.cos(latitude);
8330
+ function sampleBlobbyOctopusSurfacePoint(options, latitude, longitude) {
8331
+ const { morphologyProfile, animationPhase, timeMs } = options;
8332
+ return sampleBlobbyOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, resolveLowerLobeWave(longitude, morphologyProfile, animationPhase, timeMs));
8333
+ }
8334
+ /**
8335
+ * Samples one point on the continuous Octopus 3D 2 surface using a precomputed lower-lobe wave
8336
+ * to skip the per-call trig evaluation for `latitudePatchCount + 1` longitude-shared corners.
8337
+ *
8338
+ * @private helper of `octopus3d2AvatarVisual`
8339
+ */
8340
+ function sampleBlobbyOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, lowerLobeWave) {
8341
+ const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
8342
+ const cosineLatitude = Math.max(0, Math.cos(latitude));
8343
+ const verticalProgress = (Math.sin(latitude) + 1) / 2;
8344
+ const upperBlend = Math.pow(1 - verticalProgress, 1.2);
8345
+ const lowerBlend = Math.pow(verticalProgress, 1.42);
8346
+ const skirtEnvelope = Math.pow(cosineLatitude, 0.5) * lowerBlend;
8347
+ const horizontalScale = 1.02 +
8348
+ skirtEnvelope * (0.34 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.22 + lowerLobeWave * 0.22) -
8349
+ upperBlend * 0.08;
8350
+ const depthScale = 1.04 +
8351
+ upperBlend * 0.16 +
8352
+ Math.max(0, Math.cos(longitude)) * 0.1 +
8353
+ skirtEnvelope * (0.08 + lowerLobeWave * 0.06) -
8354
+ Math.max(0, -Math.cos(longitude)) * 0.04;
8355
+ const lowerDrop = skirtEnvelope *
8356
+ radiusY *
8357
+ (0.28 + lowerLobeWave * 0.14 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.12);
8358
+ const swayX = Math.sin(timeMs / 1250 + longitude * 1.8 + animationPhase) * skirtEnvelope * radiusX * 0.05;
8359
+ const swayZ = Math.cos(timeMs / 1480 + longitude * 1.2 - animationPhase * 0.7) * skirtEnvelope * radiusZ * 0.03;
8340
8360
  return {
8341
- x: Math.sin(longitude) * cosineLatitude * radiusX,
8342
- y: Math.sin(latitude) * radiusY,
8343
- z: Math.cos(longitude) * cosineLatitude * radiusZ,
8361
+ x: Math.sin(longitude) * cosineLatitude * radiusX * horizontalScale + swayX,
8362
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.14) -
8363
+ upperBlend * radiusY * 0.1 +
8364
+ lowerDrop +
8365
+ Math.sin(timeMs / 1780 + animationPhase + latitude * 1.4) * skirtEnvelope * radiusY * 0.02,
8366
+ z: Math.cos(longitude) * cosineLatitude * radiusZ * depthScale + swayZ,
8344
8367
  };
8345
8368
  }
8346
8369
  /**
8347
- * Resolves one base fill tone for a surface patch across the octopus body.
8370
+ * Resolves the soft lower-lobe wave that makes the silhouette read more like a real octopus.
8348
8371
  *
8349
- * @private helper of `octopus3dAvatarVisual`
8372
+ * @private helper of `octopus3d2AvatarVisual`
8350
8373
  */
8351
- function resolveSurfacePatchFillStyle(palette, verticalProgress) {
8352
- const clampedVerticalProgress = clampNumber$1(verticalProgress, 0, 1);
8353
- if (clampedVerticalProgress < 0.2) {
8374
+ function resolveLowerLobeWave(longitude, morphologyProfile, animationPhase, timeMs) {
8375
+ const lobeCount = Math.max(4, Math.round((morphologyProfile.body.lobeCount + morphologyProfile.tentacles.count) / 2));
8376
+ return (Math.cos(longitude * lobeCount + animationPhase + timeMs / 1040) + 1) / 2;
8377
+ }
8378
+ /**
8379
+ * Resolves one base fill tone for a surface patch on the single octopus mesh.
8380
+ *
8381
+ * @private helper of `octopus3d2AvatarVisual`
8382
+ */
8383
+ function resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, forwardness, lowerLobeWave) {
8384
+ const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.12 - forwardness * 0.07, 0, 1);
8385
+ if (tonalProgress < 0.16) {
8354
8386
  return palette.highlight;
8355
8387
  }
8356
- if (clampedVerticalProgress < 0.45) {
8388
+ if (tonalProgress < 0.34) {
8357
8389
  return palette.secondary;
8358
8390
  }
8359
- if (clampedVerticalProgress < 0.8) {
8360
- return palette.primary;
8391
+ if (tonalProgress < 0.72) {
8392
+ return forwardness > 0.58 ? palette.secondary : palette.primary;
8361
8393
  }
8362
8394
  return `${palette.shadow}f2`;
8363
8395
  }
8364
8396
  /**
8365
- * Draws one projected mesh patch with organic shading.
8397
+ * Draws one projected patch with soft octopus shading.
8366
8398
  *
8367
- * @private helper of `octopus3dAvatarVisual`
8399
+ * @private helper of `octopus3d2AvatarVisual`
8368
8400
  */
8369
- function drawSurfacePatch(context, surfacePatch) {
8401
+ function drawBlobbySurfacePatch(context, surfacePatch) {
8370
8402
  drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
8371
8403
  if (surfacePatch.lightIntensity > 0) {
8372
- drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.15 * surfacePatch.lightIntensity})`);
8404
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.16 * surfacePatch.lightIntensity})`);
8373
8405
  }
8374
8406
  else if (surfacePatch.lightIntensity < 0) {
8375
8407
  drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.24 * Math.abs(surfacePatch.lightIntensity)})`);
@@ -8382,209 +8414,104 @@
8382
8414
  }
8383
8415
  context.closePath();
8384
8416
  context.strokeStyle = surfacePatch.outlineColor;
8385
- context.lineWidth = Math.max(1, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0044);
8417
+ context.lineWidth = Math.max(1, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0042);
8386
8418
  context.lineJoin = 'round';
8387
8419
  context.stroke();
8388
8420
  context.restore();
8389
8421
  }
8422
+
8423
+ /* eslint-disable no-magic-numbers */
8390
8424
  /**
8391
- * Creates the projected 3D tentacle strokes orbiting around the lower octopus body.
8425
+ * Light direction used by the continuous octopus mesh shading.
8392
8426
  *
8393
- * @private helper of `octopus3dAvatarVisual`
8427
+ * @private helper of `octopus3d3AvatarVisual`
8394
8428
  */
8395
- function createOctopusTentacleStrokes(options) {
8396
- const { createRandom, morphologyProfile, timeMs, size, center, radiusX, radiusY, radiusZ, rotationX, rotationY, sceneCenterX, sceneCenterY, animationPhase, } = options;
8397
- return Array.from({ length: morphologyProfile.tentacles.count }, (_, tentacleIndex) => {
8398
- const tentacleRandom = createRandom(`octopus3d-tentacle-${tentacleIndex}`);
8399
- const spreadProgress = morphologyProfile.tentacles.count === 1 ? 0.5 : tentacleIndex / (morphologyProfile.tentacles.count - 1);
8400
- const orbitAngle = -Math.PI * 0.92 + spreadProgress * Math.PI * 1.84 + (tentacleRandom() - 0.5) * 0.16;
8401
- const flowLength = size * (0.19 + morphologyProfile.tentacles.flowLengthScale * 0.075 + tentacleRandom() * 0.018);
8402
- const lateralReach = size *
8403
- (0.08 + morphologyProfile.tentacles.lateralReachScale * 0.05 + Math.abs(Math.sin(orbitAngle)) * 0.018);
8404
- const depthReach = size * (0.028 + morphologyProfile.tentacles.tipReachScale * 0.032);
8405
- const sway = Math.sin(timeMs / (760 + tentacleIndex * 36) + animationPhase + tentacleRandom() * Math.PI * 2);
8406
- const anchorPoint = {
8407
- x: Math.sin(orbitAngle) * radiusX * (0.84 + tentacleRandom() * 0.08),
8408
- y: radiusY * (0.22 + tentacleRandom() * 0.18),
8409
- z: Math.cos(orbitAngle) * radiusZ * (0.72 + tentacleRandom() * 0.12),
8410
- };
8411
- const controlPointOne = {
8412
- x: anchorPoint.x + Math.sin(orbitAngle) * lateralReach * 0.44,
8413
- y: anchorPoint.y + flowLength * 0.26,
8414
- z: anchorPoint.z + Math.cos(orbitAngle) * depthReach * 0.3 + sway * size * 0.012,
8415
- };
8416
- const controlPointTwo = {
8417
- x: anchorPoint.x +
8418
- Math.sin(orbitAngle) * lateralReach * (0.82 + morphologyProfile.tentacles.swayScale * 0.12),
8419
- y: anchorPoint.y + flowLength * 0.66,
8420
- z: anchorPoint.z + Math.cos(orbitAngle) * depthReach * 0.72 + sway * size * 0.02,
8421
- };
8422
- const endPoint = {
8423
- x: anchorPoint.x +
8424
- Math.sin(orbitAngle) * lateralReach * (1.02 + morphologyProfile.tentacles.tipWidthScale * 0.12) +
8425
- sway * size * 0.028,
8426
- y: anchorPoint.y + flowLength,
8427
- z: anchorPoint.z + Math.cos(orbitAngle) * depthReach + sway * size * 0.016,
8428
- };
8429
- const scenePoints = Array.from({ length: 12 }, (_, sampleIndex) => transformScenePoint(sampleCubicBezierPoint3D(anchorPoint, controlPointOne, controlPointTwo, endPoint, sampleIndex / 11), center, rotationX, rotationY));
8430
- const projectedPoints = scenePoints.map((scenePoint) => projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
8431
- const averageDepth = scenePoints.reduce((depthSum, scenePoint) => depthSum + scenePoint.z, 0) / scenePoints.length;
8432
- return {
8433
- projectedPoints,
8434
- averageDepth,
8435
- isFrontFacing: averageDepth >= center.z - size * 0.006,
8436
- baseWidth: size *
8437
- (0.019 +
8438
- morphologyProfile.tentacles.baseWidthScale * 0.007 +
8439
- tentacleRandom() * 0.003 +
8440
- Math.abs(Math.sin(orbitAngle)) * 0.002),
8441
- tipWidth: size * (0.0046 + morphologyProfile.tentacles.tipWidthScale * 0.0018),
8442
- colorBias: tentacleRandom(),
8443
- };
8444
- });
8445
- }
8429
+ const LIGHT_DIRECTION$2 = normalizeVector3({
8430
+ x: 0.34,
8431
+ y: -0.62,
8432
+ z: 1,
8433
+ });
8446
8434
  /**
8447
- * Samples one point on a cubic Bezier curve in 3D.
8435
+ * Real-octopus tentacle count used by the continuous lower mesh.
8448
8436
  *
8449
- * @private helper of `octopus3dAvatarVisual`
8437
+ * @private helper of `octopus3d3AvatarVisual`
8450
8438
  */
8451
- function sampleCubicBezierPoint3D(startPoint, controlPointOne, controlPointTwo, endPoint, progress) {
8452
- const inverseProgress = 1 - progress;
8453
- return {
8454
- x: inverseProgress * inverseProgress * inverseProgress * startPoint.x +
8455
- 3 * inverseProgress * inverseProgress * progress * controlPointOne.x +
8456
- 3 * inverseProgress * progress * progress * controlPointTwo.x +
8457
- progress * progress * progress * endPoint.x,
8458
- y: inverseProgress * inverseProgress * inverseProgress * startPoint.y +
8459
- 3 * inverseProgress * inverseProgress * progress * controlPointOne.y +
8460
- 3 * inverseProgress * progress * progress * controlPointTwo.y +
8461
- progress * progress * progress * endPoint.y,
8462
- z: inverseProgress * inverseProgress * inverseProgress * startPoint.z +
8463
- 3 * inverseProgress * inverseProgress * progress * controlPointOne.z +
8464
- 3 * inverseProgress * progress * progress * controlPointTwo.z +
8465
- progress * progress * progress * endPoint.z,
8466
- };
8467
- }
8439
+ const OCTOPUS_TENTACLE_COUNT$1 = 8;
8468
8440
  /**
8469
- * Draws one projected tentacle stroke with a slim highlight ridge.
8441
+ * Cache keyed by the `createRandom` factory reference, which is stable for the lifetime of one
8442
+ * mounted `<Avatar/>` component (created inside `resolveAvatarRenderDefinition` and held in a
8443
+ * React `useMemo`). Using a `WeakMap` ensures the entry is collected when the component unmounts.
8470
8444
  *
8471
- * @private helper of `octopus3dAvatarVisual`
8445
+ * @private helper of `octopus3d3AvatarVisual`
8472
8446
  */
8473
- function drawTentacleStroke(context, tentacleStroke, palette) {
8474
- const projectedSegments = tentacleStroke.projectedPoints.length - 1;
8475
- for (let segmentIndex = 0; segmentIndex < projectedSegments; segmentIndex++) {
8476
- const startPoint = tentacleStroke.projectedPoints[segmentIndex];
8477
- const endPoint = tentacleStroke.projectedPoints[segmentIndex + 1];
8478
- const progress = segmentIndex / projectedSegments;
8479
- const width = tentacleStroke.baseWidth + (tentacleStroke.tipWidth - tentacleStroke.baseWidth) * progress;
8480
- context.beginPath();
8481
- context.moveTo(startPoint.x, startPoint.y);
8482
- context.lineTo(endPoint.x, endPoint.y);
8483
- context.strokeStyle = tentacleStroke.colorBias > 0.6 ? `${palette.secondary}f0` : `${palette.primary}f0`;
8484
- context.lineWidth = width;
8485
- context.lineCap = 'round';
8486
- context.stroke();
8487
- context.beginPath();
8488
- context.moveTo(startPoint.x, startPoint.y);
8489
- context.lineTo(endPoint.x, endPoint.y);
8490
- context.strokeStyle = tentacleStroke.isFrontFacing ? `${palette.highlight}80` : `${palette.highlight}40`;
8491
- context.lineWidth = Math.max(1, width * 0.34);
8492
- context.lineCap = 'round';
8493
- context.stroke();
8494
- }
8495
- }
8496
- /**
8497
- * Resolves the front surface depth on an ellipsoid for one local face point.
8498
- *
8499
- * @private helper of `octopus3dAvatarVisual`
8500
- */
8501
- function resolveEllipsoidSurfaceDepth(radiusX, radiusY, radiusZ, x, y) {
8502
- const normalizedX = x / radiusX;
8503
- const normalizedY = y / radiusY;
8504
- const remainingDepthRatio = Math.max(0, 1 - normalizedX * normalizedX - normalizedY * normalizedY);
8505
- return Math.sqrt(remainingDepthRatio) * radiusZ;
8506
- }
8507
-
8508
- /* eslint-disable no-magic-numbers */
8509
- /**
8510
- * Light direction used by the single-mesh octopus shading.
8511
- *
8512
- * @private helper of `octopus3d2AvatarVisual`
8513
- */
8514
- const LIGHT_DIRECTION$2 = normalizeVector3({
8515
- x: 0.38,
8516
- y: -0.6,
8517
- z: 0.98,
8518
- });
8519
- /**
8520
- * Cache keyed by the `createRandom` factory reference (stable per mounted `<Avatar/>`).
8521
- *
8522
- * @private helper of `octopus3d2AvatarVisual`
8523
- */
8524
- const octopus3d2StableStateCache = new WeakMap();
8447
+ const stableStateCache = new WeakMap();
8525
8448
  /**
8526
- * Returns the stable per-avatar state, computing it on first access and caching for subsequent frames.
8449
+ * Returns the stable per-avatar state, computing it on first access and returning the cached
8450
+ * result on every subsequent call within the same `<Avatar/>` mount.
8527
8451
  *
8528
- * @private helper of `octopus3d2AvatarVisual`
8452
+ * @private helper of `octopus3d3AvatarVisual`
8529
8453
  */
8530
- function getOctopus3d2StableState(createRandom) {
8531
- const cached = octopus3d2StableStateCache.get(createRandom);
8454
+ function getOctopus3d3StableState(createRandom) {
8455
+ const cached = stableStateCache.get(createRandom);
8532
8456
  if (cached !== undefined) {
8533
8457
  return cached;
8534
8458
  }
8535
- const animationRandom = createRandom('octopus3d2-animation-profile');
8536
- const eyeRandom = createRandom('octopus3d2-eye-profile');
8459
+ const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
8460
+ const animationRandom = createRandom('octopus3d3-animation-profile');
8461
+ const eyeRandom = createRandom('octopus3d3-eye-profile');
8537
8462
  const leftEyePhaseOffset = eyeRandom() * 0.7;
8538
8463
  const rightEyePhaseOffset = eyeRandom() * 0.7;
8539
8464
  const state = {
8540
- morphologyProfile: createOctopus3MorphologyProfile(createRandom),
8465
+ morphologyProfile,
8541
8466
  animationPhase: animationRandom() * Math.PI * 2,
8542
8467
  leftEyePhaseOffset,
8543
8468
  rightEyePhaseOffset,
8469
+ tentacleProfiles: createContinuousTentacleProfiles(createRandom, morphologyProfile),
8544
8470
  };
8545
- octopus3d2StableStateCache.set(createRandom, state);
8471
+ stableStateCache.set(createRandom, state);
8546
8472
  return state;
8547
8473
  }
8548
8474
  /**
8549
- * Octopus 3D 2 avatar visual.
8475
+ * Octopus 3D 3 avatar visual.
8550
8476
  *
8551
8477
  * @private built-in avatar visual
8552
8478
  */
8553
- const octopus3d2AvatarVisual = {
8554
- id: 'octopus3d2',
8555
- title: 'Octopus 3D 2',
8556
- description: 'Continuous blobby 3D octopus portrait with one soft mesh, turning silhouette, and cursor-aware eyes.',
8479
+ const octopus3d3AvatarVisual = {
8480
+ id: 'octopus3d3',
8481
+ title: 'Octopus 3D 3',
8482
+ description: 'Cute continuous 3D octopus with a blobby single mesh, waving tentacle lobes, rich shading, and cursor-aware eyes.',
8557
8483
  isAnimated: true,
8558
8484
  supportsPointerTracking: true,
8559
8485
  render({ context, size, palette, createRandom, timeMs, interaction }) {
8560
- const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset } = getOctopus3d2StableState(createRandom);
8486
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles } = getOctopus3d3StableState(createRandom);
8561
8487
  const sceneCenterX = size * 0.5;
8562
- const sceneCenterY = size * 0.575;
8563
- const bob = Math.sin(timeMs / 940 + animationPhase) * size * 0.013;
8488
+ const sceneCenterY = size * 0.535;
8489
+ const bob = Math.sin(timeMs / 960 + animationPhase) * size * 0.012;
8564
8490
  const meshCenter = {
8565
- x: interaction.bodyOffsetX * size * 0.044 + size * morphologyProfile.body.centerXJitterRatio * 0.5,
8566
- y: -size * 0.03 + interaction.bodyOffsetY * size * 0.026 + bob,
8491
+ x: interaction.bodyOffsetX * size * 0.048 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
8492
+ y: -size * 0.07 + interaction.bodyOffsetY * size * 0.026 + bob,
8567
8493
  z: interaction.intensity * size * 0.018,
8568
8494
  };
8569
- const rotationY = -0.14 +
8570
- Math.sin(timeMs / 2600 + animationPhase) * 0.04 +
8571
- interaction.bodyOffsetX * 0.2 +
8572
- interaction.gazeX * 0.78;
8573
- const rotationX = -0.06 +
8574
- Math.cos(timeMs / 3000 + animationPhase * 0.7) * 0.02 -
8495
+ const rotationY = -0.1 +
8496
+ Math.sin(timeMs / 2700 + animationPhase) * 0.035 +
8497
+ interaction.bodyOffsetX * 0.22 +
8498
+ interaction.gazeX * 0.88;
8499
+ const rotationX = -0.07 +
8500
+ Math.cos(timeMs / 3100 + animationPhase * 0.7) * 0.018 -
8575
8501
  interaction.bodyOffsetY * 0.08 -
8576
- interaction.gazeY * 0.34;
8502
+ interaction.gazeY * 0.38;
8577
8503
  const surfaceOptions = {
8578
- radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.02,
8579
- radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.22,
8504
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.1,
8505
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.08,
8580
8506
  radiusZ: size *
8581
8507
  morphologyProfile.body.bodyRadiusRatio *
8582
- (0.98 + (morphologyProfile.body.horizontalStretch - 1) * 0.2),
8508
+ (1.02 + (morphologyProfile.body.horizontalStretch - 1) * 0.18),
8583
8509
  morphologyProfile,
8584
8510
  timeMs,
8585
8511
  animationPhase,
8512
+ tentacleProfiles,
8586
8513
  };
8587
- const surfacePatches = resolveVisibleBlobbyOctopusPatches({
8514
+ const surfacePatches = resolveVisibleContinuousOctopusPatches({
8588
8515
  ...surfaceOptions,
8589
8516
  center: meshCenter,
8590
8517
  rotationX,
@@ -8594,62 +8521,109 @@
8594
8521
  size,
8595
8522
  palette,
8596
8523
  });
8597
- const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.4, -0.16, 0.11);
8598
- const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.25, 0.2, 0.34);
8599
- const mouthLatitude = clampNumber$1(eyeLatitude + 0.19 + morphologyProfile.face.mouthYOffsetRatio * 1.08, 0.08, 0.34);
8600
- const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.8, -0.08, 0.08);
8601
- const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.82, 0.16, 0.29);
8602
- const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.85, mouthLatitude + 0.03, 0.42);
8524
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.2 - 0.03, -0.22, 0.08);
8525
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.1, 0.18, 0.32);
8526
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.2 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
8527
+ const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
8528
+ const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
8529
+ const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
8530
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.76;
8531
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.9;
8603
8532
  drawAvatarFrame(context, size, palette);
8604
- drawBlobbyOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
8605
- drawBlobbyOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile);
8533
+ drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
8534
+ drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile);
8606
8535
  for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
8607
- drawBlobbySurfacePatch(context, surfacePatch);
8536
+ drawContinuousSurfacePatch(context, surfacePatch);
8608
8537
  }
8609
- const leftEyeLocalCenter = sampleBlobbyOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude);
8610
- const rightEyeLocalCenter = sampleBlobbyOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude);
8611
- const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
8612
- const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
8613
- drawProjectedOrganicEye(context, leftEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8614
- drawProjectedOrganicEye(context, rightEyeLocalCenter, eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.9 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8538
+ drawProjectedSurfaceCurrents({
8539
+ context,
8540
+ surfaceOptions,
8541
+ center: meshCenter,
8542
+ rotationX,
8543
+ rotationY,
8544
+ sceneCenterX,
8545
+ sceneCenterY,
8546
+ size,
8547
+ palette,
8548
+ morphologyProfile,
8549
+ timeMs,
8550
+ animationPhase,
8551
+ });
8552
+ drawProjectedTentacleSuckers({
8553
+ context,
8554
+ surfaceOptions,
8555
+ center: meshCenter,
8556
+ rotationX,
8557
+ rotationY,
8558
+ sceneCenterX,
8559
+ sceneCenterY,
8560
+ size,
8561
+ palette,
8562
+ });
8563
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8564
+ drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
8615
8565
  drawProjectedOrganicMouth(context, [
8616
- sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
8617
- sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
8618
- sampleBlobbyOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
8566
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
8567
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
8568
+ sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
8619
8569
  ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
8620
8570
  },
8621
8571
  };
8622
8572
  /**
8623
- * Draws the deep-water glow behind the continuous octopus mesh.
8573
+ * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
8624
8574
  *
8625
- * @private helper of `octopus3d2AvatarVisual`
8575
+ * @private helper of `octopus3d3AvatarVisual`
8626
8576
  */
8627
- function drawBlobbyOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
8628
- const glowGradient = context.createRadialGradient(sceneCenterX + interaction.gazeX * size * 0.11, sceneCenterY - size * 0.17 + interaction.gazeY * size * 0.05, size * 0.05, sceneCenterX, sceneCenterY - size * 0.03, size * 0.66);
8629
- glowGradient.addColorStop(0, `${palette.highlight}5e`);
8630
- glowGradient.addColorStop(0.38, `${palette.accent}26`);
8577
+ function createContinuousTentacleProfiles(createRandom, morphologyProfile) {
8578
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT$1 }, (_, tentacleIndex) => {
8579
+ const tentacleRandom = createRandom(`octopus3d3-tentacle-${tentacleIndex}`);
8580
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT$1 - 1);
8581
+ return {
8582
+ centerLongitude: -Math.PI * 0.86 +
8583
+ progress * Math.PI * 1.72 +
8584
+ (tentacleRandom() - 0.5) * (0.08 + morphologyProfile.tentacles.rootSpreadScale * 0.03),
8585
+ widthScale: 0.86 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.16,
8586
+ lengthScale: 0.86 + tentacleRandom() * 0.36 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.22,
8587
+ swayScale: 0.82 + tentacleRandom() * 0.38 + (morphologyProfile.tentacles.swayScale - 1) * 0.2,
8588
+ depthScale: 0.86 + tentacleRandom() * 0.32 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.2,
8589
+ phase: tentacleRandom() * Math.PI * 2,
8590
+ suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
8591
+ };
8592
+ });
8593
+ }
8594
+ /**
8595
+ * Draws the soft underwater atmosphere behind the continuous octopus mesh.
8596
+ *
8597
+ * @private helper of `octopus3d3AvatarVisual`
8598
+ */
8599
+ function drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
8600
+ 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));
8601
+ glowGradient.addColorStop(0, `${palette.highlight}66`);
8602
+ glowGradient.addColorStop(0.34, `${palette.accent}2e`);
8631
8603
  glowGradient.addColorStop(1, `${palette.highlight}00`);
8632
8604
  context.fillStyle = glowGradient;
8633
8605
  context.fillRect(0, 0, size, size);
8634
- const lowerGradient = context.createRadialGradient(sceneCenterX + Math.sin(timeMs / 1650) * size * 0.045, sceneCenterY + size * 0.28, size * 0.06, sceneCenterX, sceneCenterY + size * 0.28, size * 0.52);
8635
- lowerGradient.addColorStop(0, `${palette.secondary}22`);
8606
+ 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);
8607
+ lowerGradient.addColorStop(0, `${palette.secondary}25`);
8636
8608
  lowerGradient.addColorStop(1, `${palette.secondary}00`);
8637
8609
  context.fillStyle = lowerGradient;
8638
8610
  context.fillRect(0, 0, size, size);
8639
8611
  }
8640
8612
  /**
8641
- * Draws the soft floor shadow that anchors the single mesh in the frame.
8613
+ * Draws the soft lower shadow that anchors the octopus in the avatar frame.
8642
8614
  *
8643
8615
  * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
8644
8616
  * blurry ellipse without triggering a costly software rasterization pass on every frame.
8645
8617
  *
8646
- * @private helper of `octopus3d2AvatarVisual`
8618
+ * @private helper of `octopus3d3AvatarVisual`
8647
8619
  */
8648
- function drawBlobbyOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
8620
+ function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
8649
8621
  const cx = size * 0.5 + interaction.gazeX * size * 0.045;
8650
- const cy = size * 0.88 + Math.sin(timeMs / 940) * size * 0.008;
8651
- const rx = size * (0.18 + (morphologyProfile.body.horizontalStretch - 1) * 0.04 + interaction.intensity * 0.018);
8652
- const ry = size * 0.062;
8622
+ const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
8623
+ const rx = size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02);
8624
+ const ry = size * 0.06;
8625
+ // Scale the context so that drawing a circle produces the correct ellipse aspect ratio,
8626
+ // then fill with a radial gradient that approximates the blurry edge without context.filter.
8653
8627
  context.save();
8654
8628
  context.translate(cx, cy);
8655
8629
  context.scale(1, ry / rx);
@@ -8666,32 +8640,32 @@
8666
8640
  context.restore();
8667
8641
  }
8668
8642
  /**
8669
- * Number of latitude segments used by the single blobby octopus mesh.
8643
+ * Number of latitude segments used by the continuous Octopus 3D 3 mesh.
8670
8644
  *
8671
- * @private helper of `octopus3d2AvatarVisual`
8645
+ * @private helper of `octopus3d3AvatarVisual`
8672
8646
  */
8673
- const LATITUDE_PATCH_COUNT$2 = 12;
8647
+ const LATITUDE_PATCH_COUNT$1 = 16;
8674
8648
  /**
8675
- * Number of longitude segments used by the single blobby octopus mesh.
8649
+ * Number of longitude segments used by the continuous Octopus 3D 3 mesh.
8676
8650
  *
8677
- * @private helper of `octopus3d2AvatarVisual`
8651
+ * @private helper of `octopus3d3AvatarVisual`
8678
8652
  */
8679
- const LONGITUDE_PATCH_COUNT$2 = 24;
8653
+ const LONGITUDE_PATCH_COUNT$1 = 40;
8680
8654
  /**
8681
- * Resolves all visible projected patches for the single blobby octopus mesh.
8655
+ * Resolves visible projected patches for the continuous octopus mesh.
8682
8656
  *
8683
- * Within a single frame, mesh corner samples and longitude-only lobe-wave values are
8684
- * quantized to the patch grid and computed once each rather than re-evaluated for every
8685
- * patch corner — the patch loop alone would call `sampleBlobbyOctopusSurfacePoint`
8686
- * `latitudePatchCount * longitudePatchCount * 4` times without caching, even though most
8687
- * corners are shared between neighboring patches.
8657
+ * Within a single frame, mesh corner samples and longitude-only computations (tentacle
8658
+ * influence and lobe wave) are quantized to the patch grid and computed once each rather
8659
+ * than re-evaluated for every patch corner — the patch loop alone calls `sampleContinuousOctopusSurfacePoint`
8660
+ * for `latitudePatchCount * longitudePatchCount * 4` corners without caching, and each call
8661
+ * triggers an inner 8-iteration `Math.exp` loop, which dominates the per-frame cost.
8688
8662
  *
8689
- * @private helper of `octopus3d2AvatarVisual`
8663
+ * @private helper of `octopus3d3AvatarVisual`
8690
8664
  */
8691
- function resolveVisibleBlobbyOctopusPatches(options) {
8692
- const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, animationPhase, timeMs, } = options;
8693
- const latitudePatchCount = LATITUDE_PATCH_COUNT$2;
8694
- const longitudePatchCount = LONGITUDE_PATCH_COUNT$2;
8665
+ function resolveVisibleContinuousOctopusPatches(options) {
8666
+ const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
8667
+ const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
8668
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
8695
8669
  const surfacePatches = [];
8696
8670
  const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
8697
8671
  const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
@@ -8701,9 +8675,21 @@
8701
8675
  for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
8702
8676
  longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
8703
8677
  }
8678
+ const cachedTentacleInfluencesByCornerLongitude = new Array(longitudePatchCount + 1);
8704
8679
  const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
8680
+ const cachedTentacleInfluencesByPatchCenterLongitude = new Array(longitudePatchCount);
8681
+ const cachedLobeWavesByPatchCenterLongitude = new Float64Array(longitudePatchCount);
8682
+ const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
8705
8683
  for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
8706
- cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveLowerLobeWave(longitudeBoundaries[boundaryIndex], morphologyProfile, animationPhase, timeMs);
8684
+ const cornerLongitude = longitudeBoundaries[boundaryIndex];
8685
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveContinuousTentacleInfluence(options, cornerLongitude);
8686
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveContinuousLobeWave(options, cornerLongitude);
8687
+ }
8688
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
8689
+ const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
8690
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveContinuousTentacleInfluence(options, patchCenterLongitude);
8691
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveContinuousLobeWave(options, patchCenterLongitude);
8692
+ cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
8707
8693
  }
8708
8694
  const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
8709
8695
  const transformedCornerSamples = new Array(cornerCount);
@@ -8712,7 +8698,7 @@
8712
8698
  for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
8713
8699
  const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
8714
8700
  const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
8715
- const cornerSample = sampleBlobbyOctopusSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
8701
+ const cornerSample = sampleContinuousOctopusSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
8716
8702
  transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
8717
8703
  }
8718
8704
  }
@@ -8724,9 +8710,6 @@
8724
8710
  const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
8725
8711
  const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
8726
8712
  for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
8727
- const startLongitude = longitudeBoundaries[longitudeIndex];
8728
- const endLongitude = longitudeBoundaries[longitudeIndex + 1];
8729
- const centerLongitude = (startLongitude + endLongitude) / 2;
8730
8713
  const transformedCorners = [
8731
8714
  transformedCornerSamples[startCornerRowOffset + longitudeIndex],
8732
8715
  transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1],
@@ -8734,7 +8717,7 @@
8734
8717
  transformedCornerSamples[endCornerRowOffset + longitudeIndex],
8735
8718
  ];
8736
8719
  const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
8737
- if (surfaceNormal.z <= 0.01) {
8720
+ if (surfaceNormal.z <= 0.008) {
8738
8721
  continue;
8739
8722
  }
8740
8723
  const projectedCorners = [
@@ -8751,99 +8734,183 @@
8751
8734
  transformedCorners[3].z) /
8752
8735
  4,
8753
8736
  lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$2), -1, 1),
8754
- fillStyle: resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, Math.max(0, Math.cos(centerLongitude)), resolveLowerLobeWave(centerLongitude, morphologyProfile, animationPhase, timeMs)),
8755
- outlineColor: verticalProgress < 0.58 ? `${palette.highlight}73` : `${palette.shadow}8a`,
8737
+ fillStyle: resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
8738
+ outlineColor: verticalProgress < 0.54 ? `${palette.highlight}69` : `${palette.shadow}78`,
8756
8739
  });
8757
8740
  }
8758
8741
  }
8759
8742
  return surfacePatches;
8760
8743
  }
8761
8744
  /**
8762
- * Samples one point on the continuous Octopus 3D 2 surface.
8745
+ * Samples one point on the continuous Octopus 3D 3 surface.
8763
8746
  *
8764
- * The lower hemisphere widens and falls into soft lobe waves so the octopus stays one connected mesh
8765
- * instead of switching to separately rendered tentacles.
8747
+ * The lower hemisphere is pulled into eight seeded waving lobes, so the portrait reads as
8748
+ * tentacled while still being rendered as one connected blobby mesh.
8766
8749
  *
8767
- * @private helper of `octopus3d2AvatarVisual`
8750
+ * @private helper of `octopus3d3AvatarVisual`
8768
8751
  */
8769
- function sampleBlobbyOctopusSurfacePoint(options, latitude, longitude) {
8770
- const { morphologyProfile, animationPhase, timeMs } = options;
8771
- return sampleBlobbyOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, resolveLowerLobeWave(longitude, morphologyProfile, animationPhase, timeMs));
8752
+ function sampleContinuousOctopusSurfacePoint(options, latitude, longitude) {
8753
+ return sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, resolveContinuousTentacleInfluence(options, longitude), resolveContinuousLobeWave(options, longitude));
8772
8754
  }
8773
8755
  /**
8774
- * Samples one point on the continuous Octopus 3D 2 surface using a precomputed lower-lobe wave
8775
- * to skip the per-call trig evaluation for `latitudePatchCount + 1` longitude-shared corners.
8756
+ * Samples one point on the continuous Octopus 3D 3 surface using precomputed longitude-only
8757
+ * values to skip the per-call `Math.exp` tentacle-influence loop and the lobe-wave trig call.
8776
8758
  *
8777
- * @private helper of `octopus3d2AvatarVisual`
8759
+ * The patch loop quantizes the mesh into a fixed `(latitudePatchCount + 1) * (longitudePatchCount + 1)`
8760
+ * corner grid, so the same longitude is reused across every latitude row and each
8761
+ * tentacle/lobe value can be computed once per frame instead of `latitudePatchCount * 4` times.
8762
+ *
8763
+ * @private helper of `octopus3d3AvatarVisual`
8778
8764
  */
8779
- function sampleBlobbyOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, lowerLobeWave) {
8765
+ function sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
8780
8766
  const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
8781
8767
  const cosineLatitude = Math.max(0, Math.cos(latitude));
8782
8768
  const verticalProgress = (Math.sin(latitude) + 1) / 2;
8783
- const upperBlend = Math.pow(1 - verticalProgress, 1.2);
8784
- const lowerBlend = Math.pow(verticalProgress, 1.42);
8785
- const skirtEnvelope = Math.pow(cosineLatitude, 0.5) * lowerBlend;
8786
- const horizontalScale = 1.02 +
8787
- skirtEnvelope * (0.34 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.22 + lowerLobeWave * 0.22) -
8769
+ const upperBlend = Math.pow(1 - verticalProgress, 1.28);
8770
+ const lowerBlend = smoothStep$1(0.38, 1, verticalProgress);
8771
+ const tipBlend = smoothStep$1(0.68, 1, verticalProgress);
8772
+ const centerPull = resolveSignedAngularDistance$1(longitude, tentacleInfluence.centerLongitude);
8773
+ const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.24 + tipBlend * 0.2);
8774
+ const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
8775
+ animationPhase * 0.6 +
8776
+ timeMs / (1750 + morphologyProfile.body.lobeCount * 30)) *
8777
+ (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.8) *
8778
+ (0.3 + lowerBlend * 0.7);
8779
+ const tentacleWave = Math.sin(timeMs / 760 + tentacleInfluence.phase + verticalProgress * 2.4) *
8780
+ lowerBlend *
8781
+ tentacleInfluence.core *
8782
+ tentacleInfluence.swayScale;
8783
+ const horizontalScale = 1.04 +
8784
+ mantleRipple +
8785
+ lowerBlend * (0.16 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.1) +
8786
+ lowerBlend * tentacleInfluence.core * (0.2 + lowerLobeWave * 0.12) -
8788
8787
  upperBlend * 0.08;
8789
- const depthScale = 1.04 +
8788
+ const depthScale = 1.06 +
8790
8789
  upperBlend * 0.16 +
8791
- Math.max(0, Math.cos(longitude)) * 0.1 +
8792
- skirtEnvelope * (0.08 + lowerLobeWave * 0.06) -
8793
- Math.max(0, -Math.cos(longitude)) * 0.04;
8794
- const lowerDrop = skirtEnvelope *
8790
+ Math.max(0, Math.cos(effectiveLongitude)) * 0.1 +
8791
+ lowerBlend * tentacleInfluence.core * (0.1 + tentacleInfluence.depthScale * 0.06) -
8792
+ Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
8793
+ const tentacleTubeRadius = lowerBlend * tentacleInfluence.core * (0.11 + tipBlend * 0.06 + tentacleInfluence.widthScale * 0.025) * radiusX;
8794
+ const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
8795
+ const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.72;
8796
+ const lowerDrop = lowerBlend *
8795
8797
  radiusY *
8796
- (0.28 + lowerLobeWave * 0.14 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.12);
8797
- const swayX = Math.sin(timeMs / 1250 + longitude * 1.8 + animationPhase) * skirtEnvelope * radiusX * 0.05;
8798
- const swayZ = Math.cos(timeMs / 1480 + longitude * 1.2 - animationPhase * 0.7) * skirtEnvelope * radiusZ * 0.03;
8798
+ (0.18 +
8799
+ tentacleInfluence.core *
8800
+ (0.38 +
8801
+ tentacleInfluence.lengthScale * 0.22 +
8802
+ (morphologyProfile.tentacles.flowLengthScale - 1) * 0.08));
8799
8803
  return {
8800
- x: Math.sin(longitude) * cosineLatitude * radiusX * horizontalScale + swayX,
8801
- y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.14) -
8804
+ x: Math.sin(effectiveLongitude) * planarRadiusX + tentacleWave * radiusX * (0.052 + tipBlend * 0.05),
8805
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.12) -
8802
8806
  upperBlend * radiusY * 0.1 +
8803
8807
  lowerDrop +
8804
- Math.sin(timeMs / 1780 + animationPhase + latitude * 1.4) * skirtEnvelope * radiusY * 0.02,
8805
- z: Math.cos(longitude) * cosineLatitude * radiusZ * depthScale + swayZ,
8808
+ Math.sin(timeMs / 1420 + animationPhase + latitude * 1.6) * lowerBlend * radiusY * 0.018 +
8809
+ Math.cos(timeMs / 880 + tentacleInfluence.phase) *
8810
+ lowerBlend *
8811
+ tipBlend *
8812
+ tentacleInfluence.core *
8813
+ radiusY *
8814
+ 0.034,
8815
+ z: Math.cos(effectiveLongitude) * planarRadiusZ +
8816
+ Math.cos(timeMs / 980 + tentacleInfluence.phase + verticalProgress) *
8817
+ lowerBlend *
8818
+ tentacleInfluence.core *
8819
+ radiusZ *
8820
+ 0.04,
8806
8821
  };
8807
8822
  }
8808
8823
  /**
8809
- * Resolves the soft lower-lobe wave that makes the silhouette read more like a real octopus.
8824
+ * Blends nearby seeded tentacle profiles at one mesh longitude.
8810
8825
  *
8811
- * @private helper of `octopus3d2AvatarVisual`
8826
+ * @private helper of `octopus3d3AvatarVisual`
8812
8827
  */
8813
- function resolveLowerLobeWave(longitude, morphologyProfile, animationPhase, timeMs) {
8814
- const lobeCount = Math.max(4, Math.round((morphologyProfile.body.lobeCount + morphologyProfile.tentacles.count) / 2));
8815
- return (Math.cos(longitude * lobeCount + animationPhase + timeMs / 1040) + 1) / 2;
8828
+ function resolveContinuousTentacleInfluence(options, longitude) {
8829
+ let totalWeight = 0;
8830
+ let weightedSin = 0;
8831
+ let weightedCos = 0;
8832
+ let weightedWidthScale = 0;
8833
+ let weightedLengthScale = 0;
8834
+ let weightedSwayScale = 0;
8835
+ let weightedDepthScale = 0;
8836
+ let weightedPhase = 0;
8837
+ for (const tentacleProfile of options.tentacleProfiles) {
8838
+ const distance = Math.abs(resolveSignedAngularDistance$1(longitude, tentacleProfile.centerLongitude));
8839
+ const width = 0.2 * tentacleProfile.widthScale;
8840
+ const weight = Math.exp(-(distance * distance) / (width * width));
8841
+ totalWeight += weight;
8842
+ weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
8843
+ weightedCos += Math.cos(tentacleProfile.centerLongitude) * weight;
8844
+ weightedWidthScale += tentacleProfile.widthScale * weight;
8845
+ weightedLengthScale += tentacleProfile.lengthScale * weight;
8846
+ weightedSwayScale += tentacleProfile.swayScale * weight;
8847
+ weightedDepthScale += tentacleProfile.depthScale * weight;
8848
+ weightedPhase += tentacleProfile.phase * weight;
8849
+ }
8850
+ if (totalWeight < 0.0001) {
8851
+ return {
8852
+ core: 0,
8853
+ centerLongitude: longitude,
8854
+ widthScale: 1,
8855
+ lengthScale: 1,
8856
+ swayScale: 1,
8857
+ depthScale: 1,
8858
+ phase: 0,
8859
+ };
8860
+ }
8861
+ return {
8862
+ core: clampNumber$1(totalWeight, 0, 1),
8863
+ centerLongitude: Math.atan2(weightedSin / totalWeight, weightedCos / totalWeight),
8864
+ widthScale: weightedWidthScale / totalWeight,
8865
+ lengthScale: weightedLengthScale / totalWeight,
8866
+ swayScale: weightedSwayScale / totalWeight,
8867
+ depthScale: weightedDepthScale / totalWeight,
8868
+ phase: weightedPhase / totalWeight,
8869
+ };
8816
8870
  }
8817
8871
  /**
8818
- * Resolves one base fill tone for a surface patch on the single octopus mesh.
8872
+ * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
8819
8873
  *
8820
- * @private helper of `octopus3d2AvatarVisual`
8874
+ * @private helper of `octopus3d3AvatarVisual`
8821
8875
  */
8822
- function resolveBlobbySurfacePatchFillStyle$1(palette, verticalProgress, forwardness, lowerLobeWave) {
8823
- const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.12 - forwardness * 0.07, 0, 1);
8824
- if (tonalProgress < 0.16) {
8876
+ function resolveContinuousLobeWave(options, longitude) {
8877
+ const { morphologyProfile, animationPhase, timeMs } = options;
8878
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT$1 +
8879
+ animationPhase +
8880
+ timeMs / (980 + morphologyProfile.body.lobeCount * 18)) +
8881
+ 1) /
8882
+ 2);
8883
+ }
8884
+ /**
8885
+ * Resolves one base fill tone for a patch on the continuous octopus mesh.
8886
+ *
8887
+ * @private helper of `octopus3d3AvatarVisual`
8888
+ */
8889
+ function resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
8890
+ const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
8891
+ if (tonalProgress < 0.14) {
8825
8892
  return palette.highlight;
8826
8893
  }
8827
- if (tonalProgress < 0.34) {
8894
+ if (tonalProgress < 0.32) {
8828
8895
  return palette.secondary;
8829
8896
  }
8830
8897
  if (tonalProgress < 0.72) {
8831
- return forwardness > 0.58 ? palette.secondary : palette.primary;
8898
+ return forwardness > 0.55 ? palette.secondary : palette.primary;
8832
8899
  }
8833
- return `${palette.shadow}f2`;
8900
+ return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
8834
8901
  }
8835
8902
  /**
8836
- * Draws one projected patch with soft octopus shading.
8903
+ * Draws one projected mesh patch with soft shading and a subtle edge.
8837
8904
  *
8838
- * @private helper of `octopus3d2AvatarVisual`
8905
+ * @private helper of `octopus3d3AvatarVisual`
8839
8906
  */
8840
- function drawBlobbySurfacePatch(context, surfacePatch) {
8907
+ function drawContinuousSurfacePatch(context, surfacePatch) {
8841
8908
  drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
8842
8909
  if (surfacePatch.lightIntensity > 0) {
8843
- drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.16 * surfacePatch.lightIntensity})`);
8910
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.18 * surfacePatch.lightIntensity})`);
8844
8911
  }
8845
8912
  else if (surfacePatch.lightIntensity < 0) {
8846
- drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.24 * Math.abs(surfacePatch.lightIntensity)})`);
8913
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.25 * Math.abs(surfacePatch.lightIntensity)})`);
8847
8914
  }
8848
8915
  context.save();
8849
8916
  context.beginPath();
@@ -8853,51 +8920,180 @@
8853
8920
  }
8854
8921
  context.closePath();
8855
8922
  context.strokeStyle = surfacePatch.outlineColor;
8856
- context.lineWidth = Math.max(1, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0042);
8923
+ context.lineWidth = Math.max(0.7, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0032);
8857
8924
  context.lineJoin = 'round';
8858
8925
  context.stroke();
8859
8926
  context.restore();
8860
8927
  }
8861
-
8862
- /* eslint-disable no-magic-numbers */
8863
8928
  /**
8864
- * Light direction used by the continuous octopus mesh shading.
8929
+ * Draws projected mantle-current lines on the front of the mesh.
8865
8930
  *
8866
8931
  * @private helper of `octopus3d3AvatarVisual`
8867
8932
  */
8868
- const LIGHT_DIRECTION$1 = normalizeVector3({
8869
- x: 0.34,
8870
- y: -0.62,
8871
- z: 1,
8872
- });
8933
+ function drawProjectedSurfaceCurrents(options) {
8934
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
8935
+ const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
8936
+ const centerIndex = (currentCount - 1) / 2;
8937
+ context.save();
8938
+ context.lineCap = 'round';
8939
+ context.lineJoin = 'round';
8940
+ for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
8941
+ const baseLongitude = (currentIndex - centerIndex) * 0.15;
8942
+ const projectedPoints = [];
8943
+ for (let sampleIndex = 0; sampleIndex < 8; sampleIndex++) {
8944
+ const progress = sampleIndex / 7;
8945
+ const latitude = -0.46 + progress * 0.74;
8946
+ const longitude = baseLongitude + Math.sin(timeMs / 1160 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.035;
8947
+ const scenePoint = transformScenePoint(sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
8948
+ if (scenePoint.z > center.z - size * 0.016) {
8949
+ projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
8950
+ }
8951
+ }
8952
+ if (projectedPoints.length < 3) {
8953
+ continue;
8954
+ }
8955
+ context.beginPath();
8956
+ context.moveTo(projectedPoints[0].x, projectedPoints[0].y);
8957
+ for (const projectedPoint of projectedPoints.slice(1)) {
8958
+ context.lineTo(projectedPoint.x, projectedPoint.y);
8959
+ }
8960
+ context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}3d` : `${palette.accent}33`;
8961
+ context.lineWidth = size * (0.0055 + currentIndex * 0.00045);
8962
+ context.stroke();
8963
+ }
8964
+ context.restore();
8965
+ }
8873
8966
  /**
8874
- * Real-octopus tentacle count used by the continuous lower mesh.
8967
+ * Draws small projected sucker highlights on the waving lower mesh lobes.
8875
8968
  *
8876
8969
  * @private helper of `octopus3d3AvatarVisual`
8877
8970
  */
8878
- const OCTOPUS_TENTACLE_COUNT$1 = 8;
8971
+ function drawProjectedTentacleSuckers(options) {
8972
+ const { surfaceOptions, size } = options;
8973
+ const { timeMs } = surfaceOptions;
8974
+ for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
8975
+ if (Math.cos(tentacleProfile.centerLongitude) < -0.12) {
8976
+ continue;
8977
+ }
8978
+ for (let suckerIndex = 0; suckerIndex < 3; suckerIndex++) {
8979
+ const latitude = 0.52 + suckerIndex * 0.14;
8980
+ const sideOffset = tentacleProfile.suckerSide * (0.035 + suckerIndex * 0.012) * tentacleProfile.widthScale;
8981
+ const waveOffset = Math.sin(timeMs / 900 + tentacleProfile.phase + suckerIndex * 0.8) * 0.018;
8982
+ drawProjectedSurfaceSpot({
8983
+ ...options,
8984
+ latitude,
8985
+ longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
8986
+ radiusScale: size * (0.0065 - suckerIndex * 0.0007),
8987
+ });
8988
+ }
8989
+ }
8990
+ }
8879
8991
  /**
8880
- * Cache keyed by the `createRandom` factory reference, which is stable for the lifetime of one
8881
- * mounted `<Avatar/>` component (created inside `resolveAvatarRenderDefinition` and held in a
8882
- * React `useMemo`). Using a `WeakMap` ensures the entry is collected when the component unmounts.
8992
+ * Draws one tiny projected surface spot by sampling local mesh tangents.
8883
8993
  *
8884
8994
  * @private helper of `octopus3d3AvatarVisual`
8885
8995
  */
8886
- const stableStateCache = new WeakMap();
8887
- /**
8888
- * Returns the stable per-avatar state, computing it on first access and returning the cached
8889
- * result on every subsequent call within the same `<Avatar/>` mount.
8996
+ function drawProjectedSurfaceSpot(options) {
8997
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
8998
+ const localCenter = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude);
8999
+ const localHorizontal = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
9000
+ const localVertical = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
9001
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
9002
+ if (sceneCenterPoint.z <= center.z - size * 0.012) {
9003
+ return;
9004
+ }
9005
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
9006
+ const projectedHorizontalPoint = projectScenePoint(transformScenePoint(localHorizontal, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
9007
+ const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
9008
+ const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
9009
+ radiusScale *
9010
+ 0.74, size * 0.003, size * 0.018);
9011
+ const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
9012
+ radiusScale *
9013
+ 0.52, size * 0.0024, size * 0.014);
9014
+ const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
9015
+ context.save();
9016
+ context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
9017
+ context.rotate(rotation);
9018
+ context.beginPath();
9019
+ context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
9020
+ context.fillStyle = `${palette.highlight}73`;
9021
+ context.fill();
9022
+ context.strokeStyle = `${palette.highlight}99`;
9023
+ context.lineWidth = Math.max(0.7, size * 0.0028);
9024
+ context.stroke();
9025
+ context.restore();
9026
+ }
9027
+ /**
9028
+ * Resolves a signed angular distance from the source longitude to the target longitude.
8890
9029
  *
8891
9030
  * @private helper of `octopus3d3AvatarVisual`
8892
9031
  */
8893
- function getOctopus3d3StableState(createRandom) {
8894
- const cached = stableStateCache.get(createRandom);
9032
+ function resolveSignedAngularDistance$1(sourceLongitude, targetLongitude) {
9033
+ return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
9034
+ }
9035
+ /**
9036
+ * Smoothly maps a value between two bounds into `[0, 1]`.
9037
+ *
9038
+ * @private helper of `octopus3d3AvatarVisual`
9039
+ */
9040
+ function smoothStep$1(edgeStart, edgeEnd, value) {
9041
+ const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
9042
+ return progress * progress * (3 - 2 * progress);
9043
+ }
9044
+
9045
+ /* eslint-disable no-magic-numbers */
9046
+ /**
9047
+ * Light direction used by the continuous Octopus 3D 4 mesh shading.
9048
+ *
9049
+ * @private helper of `octopus3d4AvatarVisual`
9050
+ */
9051
+ const LIGHT_DIRECTION$1 = normalizeVector3({
9052
+ x: 0.32,
9053
+ y: -0.66,
9054
+ z: 1,
9055
+ });
9056
+ /**
9057
+ * Rim-light direction used to brighten the side of the silhouette.
9058
+ *
9059
+ * @private helper of `octopus3d4AvatarVisual`
9060
+ */
9061
+ const RIM_LIGHT_DIRECTION = normalizeVector3({
9062
+ x: -0.7,
9063
+ y: -0.18,
9064
+ z: 0.55,
9065
+ });
9066
+ /**
9067
+ * Real-octopus tentacle count used by the continuous lower mesh.
9068
+ *
9069
+ * @private helper of `octopus3d4AvatarVisual`
9070
+ */
9071
+ const OCTOPUS_TENTACLE_COUNT = 8;
9072
+ /**
9073
+ * Number of seeded skin spots painted across the octopus body.
9074
+ *
9075
+ * @private helper of `octopus3d4AvatarVisual`
9076
+ */
9077
+ const SKIN_SPOT_COUNT = 14;
9078
+ /**
9079
+ * Cache keyed by the `createRandom` factory reference, stable per mounted `<Avatar/>` component.
9080
+ *
9081
+ * @private helper of `octopus3d4AvatarVisual`
9082
+ */
9083
+ const octopus3d4StableStateCache = new WeakMap();
9084
+ /**
9085
+ * Returns the stable per-avatar state, computing it on first access and caching afterwards.
9086
+ *
9087
+ * @private helper of `octopus3d4AvatarVisual`
9088
+ */
9089
+ function getOctopus3d4StableState(createRandom) {
9090
+ const cached = octopus3d4StableStateCache.get(createRandom);
8895
9091
  if (cached !== undefined) {
8896
9092
  return cached;
8897
9093
  }
8898
9094
  const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
8899
- const animationRandom = createRandom('octopus3d3-animation-profile');
8900
- const eyeRandom = createRandom('octopus3d3-eye-profile');
9095
+ const animationRandom = createRandom('octopus3d4-animation-profile');
9096
+ const eyeRandom = createRandom('octopus3d4-eye-profile');
8901
9097
  const leftEyePhaseOffset = eyeRandom() * 0.7;
8902
9098
  const rightEyePhaseOffset = eyeRandom() * 0.7;
8903
9099
  const state = {
@@ -8905,52 +9101,53 @@
8905
9101
  animationPhase: animationRandom() * Math.PI * 2,
8906
9102
  leftEyePhaseOffset,
8907
9103
  rightEyePhaseOffset,
8908
- tentacleProfiles: createContinuousTentacleProfiles(createRandom, morphologyProfile),
9104
+ tentacleProfiles: createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile),
9105
+ skinSpots: createBlobbySkinSpots(createRandom),
8909
9106
  };
8910
- stableStateCache.set(createRandom, state);
9107
+ octopus3d4StableStateCache.set(createRandom, state);
8911
9108
  return state;
8912
9109
  }
8913
9110
  /**
8914
- * Octopus 3D 3 avatar visual.
9111
+ * Octopus 3D 4 avatar visual.
8915
9112
  *
8916
9113
  * @private built-in avatar visual
8917
9114
  */
8918
- const octopus3d3AvatarVisual = {
8919
- id: 'octopus3d3',
8920
- title: 'Octopus 3D 3',
8921
- description: 'Cute continuous 3D octopus with a blobby single mesh, waving tentacle lobes, rich shading, and cursor-aware eyes.',
9115
+ const octopus3d4AvatarVisual = {
9116
+ id: 'octopus3d4',
9117
+ title: 'Octopus 3D 4',
9118
+ 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.',
8922
9119
  isAnimated: true,
8923
9120
  supportsPointerTracking: true,
8924
9121
  render({ context, size, palette, createRandom, timeMs, interaction }) {
8925
- const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles } = getOctopus3d3StableState(createRandom);
9122
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles, skinSpots, } = getOctopus3d4StableState(createRandom);
8926
9123
  const sceneCenterX = size * 0.5;
8927
9124
  const sceneCenterY = size * 0.535;
8928
- const bob = Math.sin(timeMs / 960 + animationPhase) * size * 0.012;
9125
+ const bob = Math.sin(timeMs / 980 + animationPhase) * size * 0.013;
8929
9126
  const meshCenter = {
8930
- x: interaction.bodyOffsetX * size * 0.048 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
8931
- y: -size * 0.07 + interaction.bodyOffsetY * size * 0.026 + bob,
8932
- z: interaction.intensity * size * 0.018,
9127
+ x: interaction.bodyOffsetX * size * 0.052 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
9128
+ y: -size * 0.07 + interaction.bodyOffsetY * size * 0.028 + bob,
9129
+ z: interaction.intensity * size * 0.02,
8933
9130
  };
8934
- const rotationY = -0.1 +
8935
- Math.sin(timeMs / 2700 + animationPhase) * 0.035 +
8936
- interaction.bodyOffsetX * 0.22 +
8937
- interaction.gazeX * 0.88;
9131
+ const rotationY = -0.08 +
9132
+ Math.sin(timeMs / 2800 + animationPhase) * 0.04 +
9133
+ interaction.bodyOffsetX * 0.24 +
9134
+ interaction.gazeX * 0.98;
8938
9135
  const rotationX = -0.07 +
8939
- Math.cos(timeMs / 3100 + animationPhase * 0.7) * 0.018 -
9136
+ Math.cos(timeMs / 3200 + animationPhase * 0.7) * 0.02 -
8940
9137
  interaction.bodyOffsetY * 0.08 -
8941
- interaction.gazeY * 0.38;
9138
+ interaction.gazeY * 0.42;
8942
9139
  const surfaceOptions = {
8943
- radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.1,
8944
- radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.08,
9140
+ radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.12,
9141
+ radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1,
8945
9142
  radiusZ: size *
8946
9143
  morphologyProfile.body.bodyRadiusRatio *
8947
- (1.02 + (morphologyProfile.body.horizontalStretch - 1) * 0.18),
9144
+ (1.04 + (morphologyProfile.body.horizontalStretch - 1) * 0.2),
8948
9145
  morphologyProfile,
8949
9146
  timeMs,
8950
9147
  animationPhase,
8951
9148
  tentacleProfiles,
8952
9149
  };
8953
- const surfacePatches = resolveVisibleContinuousOctopusPatches({
9150
+ const surfacePatches = resolveVisibleBlobbyContinuousPatches({
8954
9151
  ...surfaceOptions,
8955
9152
  center: meshCenter,
8956
9153
  rotationX,
@@ -8960,21 +9157,33 @@
8960
9157
  size,
8961
9158
  palette,
8962
9159
  });
8963
- const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.2 - 0.03, -0.22, 0.08);
8964
- const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.1, 0.18, 0.32);
8965
- const mouthLatitude = clampNumber$1(eyeLatitude + 0.2 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
9160
+ const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.4 - 0.04, -0.24, 0.08);
9161
+ const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.0, 0.18, 0.32);
9162
+ const mouthLatitude = clampNumber$1(eyeLatitude + 0.21 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
8966
9163
  const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
8967
9164
  const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
8968
9165
  const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
8969
- const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.76;
8970
- const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.9;
9166
+ const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
9167
+ const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
8971
9168
  drawAvatarFrame(context, size, palette);
8972
- drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
8973
- drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile);
9169
+ drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
9170
+ drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile);
8974
9171
  for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
8975
- drawContinuousSurfacePatch(context, surfacePatch);
9172
+ drawBlobbyContinuousSurfacePatch(context, surfacePatch);
8976
9173
  }
8977
- drawProjectedSurfaceCurrents({
9174
+ drawBlobbySkinSpots({
9175
+ context,
9176
+ surfaceOptions,
9177
+ center: meshCenter,
9178
+ rotationX,
9179
+ rotationY,
9180
+ sceneCenterX,
9181
+ sceneCenterY,
9182
+ size,
9183
+ palette,
9184
+ skinSpots,
9185
+ });
9186
+ drawBlobbyContinuousCurrents({
8978
9187
  context,
8979
9188
  surfaceOptions,
8980
9189
  center: meshCenter,
@@ -8988,7 +9197,7 @@
8988
9197
  timeMs,
8989
9198
  animationPhase,
8990
9199
  });
8991
- drawProjectedTentacleSuckers({
9200
+ drawBlobbyContinuousSuckers({
8992
9201
  context,
8993
9202
  surfaceOptions,
8994
9203
  center: meshCenter,
@@ -8999,51 +9208,78 @@
8999
9208
  size,
9000
9209
  palette,
9001
9210
  });
9002
- drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
9003
- drawProjectedOrganicEye(context, sampleContinuousOctopusSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
9211
+ drawBlobbyContinuousGloss({
9212
+ context,
9213
+ surfaceOptions,
9214
+ center: meshCenter,
9215
+ rotationX,
9216
+ rotationY,
9217
+ sceneCenterX,
9218
+ sceneCenterY,
9219
+ size,
9220
+ palette,
9221
+ });
9222
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
9223
+ drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
9004
9224
  drawProjectedOrganicMouth(context, [
9005
- sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
9006
- sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
9007
- sampleContinuousOctopusSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
9225
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
9226
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
9227
+ sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
9008
9228
  ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
9009
9229
  },
9010
9230
  };
9011
9231
  /**
9012
9232
  * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
9013
9233
  *
9014
- * @private helper of `octopus3d3AvatarVisual`
9234
+ * @private helper of `octopus3d4AvatarVisual`
9015
9235
  */
9016
- function createContinuousTentacleProfiles(createRandom, morphologyProfile) {
9017
- return Array.from({ length: OCTOPUS_TENTACLE_COUNT$1 }, (_, tentacleIndex) => {
9018
- const tentacleRandom = createRandom(`octopus3d3-tentacle-${tentacleIndex}`);
9019
- const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT$1 - 1);
9236
+ function createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile) {
9237
+ return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
9238
+ const tentacleRandom = createRandom(`octopus3d4-tentacle-${tentacleIndex}`);
9239
+ const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
9020
9240
  return {
9021
- centerLongitude: -Math.PI * 0.86 +
9022
- progress * Math.PI * 1.72 +
9023
- (tentacleRandom() - 0.5) * (0.08 + morphologyProfile.tentacles.rootSpreadScale * 0.03),
9024
- widthScale: 0.86 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.16,
9025
- lengthScale: 0.86 + tentacleRandom() * 0.36 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.22,
9026
- swayScale: 0.82 + tentacleRandom() * 0.38 + (morphologyProfile.tentacles.swayScale - 1) * 0.2,
9027
- depthScale: 0.86 + tentacleRandom() * 0.32 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.2,
9028
- phase: tentacleRandom() * Math.PI * 2,
9241
+ centerLongitude: -Math.PI * 0.9 +
9242
+ progress * Math.PI * 1.8 +
9243
+ (tentacleRandom() - 0.5) * (0.06 + morphologyProfile.tentacles.rootSpreadScale * 0.025),
9244
+ widthScale: 0.92 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.18,
9245
+ lengthScale: 0.9 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.24,
9246
+ swayScale: 0.86 + tentacleRandom() * 0.4 + (morphologyProfile.tentacles.swayScale - 1) * 0.22,
9247
+ depthScale: 0.9 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.22,
9248
+ curlScale: 0.62 + tentacleRandom() * 0.46,
9249
+ primaryPhase: tentacleRandom() * Math.PI * 2,
9250
+ secondaryPhase: tentacleRandom() * Math.PI * 2,
9029
9251
  suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
9030
9252
  };
9031
9253
  });
9032
9254
  }
9255
+ /**
9256
+ * Creates seeded skin pigment spots distributed across the upper octopus mesh.
9257
+ *
9258
+ * @private helper of `octopus3d4AvatarVisual`
9259
+ */
9260
+ function createBlobbySkinSpots(createRandom) {
9261
+ const spotRandom = createRandom('octopus3d4-skin-spots');
9262
+ return Array.from({ length: SKIN_SPOT_COUNT }, () => ({
9263
+ latitude: -0.45 + spotRandom() * 0.7,
9264
+ longitude: -0.5 + spotRandom() * 1.0,
9265
+ radiusScale: 0.0028 + spotRandom() * 0.0052,
9266
+ opacity: 0.16 + spotRandom() * 0.22,
9267
+ }));
9268
+ }
9033
9269
  /**
9034
9270
  * Draws the soft underwater atmosphere behind the continuous octopus mesh.
9035
9271
  *
9036
- * @private helper of `octopus3d3AvatarVisual`
9272
+ * @private helper of `octopus3d4AvatarVisual`
9037
9273
  */
9038
- function drawContinuousOctopusAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
9039
- 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));
9040
- glowGradient.addColorStop(0, `${palette.highlight}66`);
9041
- glowGradient.addColorStop(0.34, `${palette.accent}2e`);
9274
+ function drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
9275
+ 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));
9276
+ glowGradient.addColorStop(0, `${palette.highlight}74`);
9277
+ glowGradient.addColorStop(0.32, `${palette.accent}32`);
9042
9278
  glowGradient.addColorStop(1, `${palette.highlight}00`);
9043
9279
  context.fillStyle = glowGradient;
9044
9280
  context.fillRect(0, 0, size, size);
9045
- 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);
9046
- lowerGradient.addColorStop(0, `${palette.secondary}25`);
9281
+ 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);
9282
+ lowerGradient.addColorStop(0, `${palette.secondary}28`);
9047
9283
  lowerGradient.addColorStop(1, `${palette.secondary}00`);
9048
9284
  context.fillStyle = lowerGradient;
9049
9285
  context.fillRect(0, 0, size, size);
@@ -9051,26 +9287,21 @@
9051
9287
  /**
9052
9288
  * Draws the soft lower shadow that anchors the octopus in the avatar frame.
9053
9289
  *
9054
- * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
9055
- * blurry ellipse without triggering a costly software rasterization pass on every frame.
9056
- *
9057
- * @private helper of `octopus3d3AvatarVisual`
9290
+ * @private helper of `octopus3d4AvatarVisual`
9058
9291
  */
9059
- function drawContinuousOctopusShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
9060
- const cx = size * 0.5 + interaction.gazeX * size * 0.045;
9292
+ function drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
9293
+ const cx = size * 0.5 + interaction.gazeX * size * 0.046;
9061
9294
  const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
9062
- const rx = size * (0.19 + morphologyProfile.tentacles.rootSpreadScale * 0.022 + interaction.intensity * 0.02);
9063
- const ry = size * 0.06;
9064
- // Scale the context so that drawing a circle produces the correct ellipse aspect ratio,
9065
- // then fill with a radial gradient that approximates the blurry edge without context.filter.
9295
+ const rx = size * (0.2 + morphologyProfile.tentacles.rootSpreadScale * 0.024 + interaction.intensity * 0.022);
9296
+ const ry = size * 0.062;
9066
9297
  context.save();
9067
9298
  context.translate(cx, cy);
9068
9299
  context.scale(1, ry / rx);
9069
- const blurRadius = rx * 1.4;
9300
+ const blurRadius = rx * 1.42;
9070
9301
  const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
9071
- shadowGradient.addColorStop(0, `${palette.shadow}7a`);
9072
- shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
9073
- shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
9302
+ shadowGradient.addColorStop(0, `${palette.shadow}82`);
9303
+ shadowGradient.addColorStop(0.45, `${palette.shadow}4a`);
9304
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1c`);
9074
9305
  shadowGradient.addColorStop(1, `${palette.shadow}00`);
9075
9306
  context.fillStyle = shadowGradient;
9076
9307
  context.beginPath();
@@ -9079,32 +9310,30 @@
9079
9310
  context.restore();
9080
9311
  }
9081
9312
  /**
9082
- * Number of latitude segments used by the continuous Octopus 3D 3 mesh.
9313
+ * Number of latitude segments used by the continuous Octopus 3D 4 mesh.
9083
9314
  *
9084
- * @private helper of `octopus3d3AvatarVisual`
9315
+ * @private helper of `octopus3d4AvatarVisual`
9085
9316
  */
9086
- const LATITUDE_PATCH_COUNT$1 = 16;
9317
+ const LATITUDE_PATCH_COUNT = 20;
9087
9318
  /**
9088
- * Number of longitude segments used by the continuous Octopus 3D 3 mesh.
9319
+ * Number of longitude segments used by the continuous Octopus 3D 4 mesh.
9089
9320
  *
9090
- * @private helper of `octopus3d3AvatarVisual`
9321
+ * @private helper of `octopus3d4AvatarVisual`
9091
9322
  */
9092
- const LONGITUDE_PATCH_COUNT$1 = 40;
9323
+ const LONGITUDE_PATCH_COUNT = 48;
9093
9324
  /**
9094
- * Resolves visible projected patches for the continuous octopus mesh.
9325
+ * Resolves visible projected patches for the continuous Octopus 3D 4 mesh.
9095
9326
  *
9096
9327
  * Within a single frame, mesh corner samples and longitude-only computations (tentacle
9097
9328
  * influence and lobe wave) are quantized to the patch grid and computed once each rather
9098
- * than re-evaluated for every patch corner — the patch loop alone calls `sampleContinuousOctopusSurfacePoint`
9099
- * for `latitudePatchCount * longitudePatchCount * 4` corners without caching, and each call
9100
- * triggers an inner 8-iteration `Math.exp` loop, which dominates the per-frame cost.
9329
+ * than re-evaluated for every patch corner.
9101
9330
  *
9102
- * @private helper of `octopus3d3AvatarVisual`
9331
+ * @private helper of `octopus3d4AvatarVisual`
9103
9332
  */
9104
- function resolveVisibleContinuousOctopusPatches(options) {
9333
+ function resolveVisibleBlobbyContinuousPatches(options) {
9105
9334
  const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
9106
- const latitudePatchCount = LATITUDE_PATCH_COUNT$1;
9107
- const longitudePatchCount = LONGITUDE_PATCH_COUNT$1;
9335
+ const latitudePatchCount = LATITUDE_PATCH_COUNT;
9336
+ const longitudePatchCount = LONGITUDE_PATCH_COUNT;
9108
9337
  const surfacePatches = [];
9109
9338
  const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
9110
9339
  const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
@@ -9121,13 +9350,13 @@
9121
9350
  const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
9122
9351
  for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
9123
9352
  const cornerLongitude = longitudeBoundaries[boundaryIndex];
9124
- cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveContinuousTentacleInfluence(options, cornerLongitude);
9125
- cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveContinuousLobeWave(options, cornerLongitude);
9353
+ cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveBlobbyTentacleInfluence(options, cornerLongitude);
9354
+ cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveBlobbyLobeWave(options, cornerLongitude);
9126
9355
  }
9127
9356
  for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
9128
9357
  const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
9129
- cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveContinuousTentacleInfluence(options, patchCenterLongitude);
9130
- cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveContinuousLobeWave(options, patchCenterLongitude);
9358
+ cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyTentacleInfluence(options, patchCenterLongitude);
9359
+ cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyLobeWave(options, patchCenterLongitude);
9131
9360
  cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
9132
9361
  }
9133
9362
  const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
@@ -9137,7 +9366,7 @@
9137
9366
  for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
9138
9367
  const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
9139
9368
  const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
9140
- const cornerSample = sampleContinuousOctopusSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
9369
+ const cornerSample = sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
9141
9370
  transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
9142
9371
  }
9143
9372
  }
@@ -9156,7 +9385,7 @@
9156
9385
  transformedCornerSamples[endCornerRowOffset + longitudeIndex],
9157
9386
  ];
9158
9387
  const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
9159
- if (surfaceNormal.z <= 0.008) {
9388
+ if (surfaceNormal.z <= 0.006) {
9160
9389
  continue;
9161
9390
  }
9162
9391
  const projectedCorners = [
@@ -9165,6 +9394,8 @@
9165
9394
  projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
9166
9395
  projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
9167
9396
  ];
9397
+ const lightIntensity = clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1);
9398
+ const rimLightIntensity = Math.pow(clampNumber$1(dotProduct3D(surfaceNormal, RIM_LIGHT_DIRECTION), 0, 1), 2.6);
9168
9399
  surfacePatches.push({
9169
9400
  corners: projectedCorners,
9170
9401
  averageDepth: (transformedCorners[0].z +
@@ -9172,99 +9403,108 @@
9172
9403
  transformedCorners[2].z +
9173
9404
  transformedCorners[3].z) /
9174
9405
  4,
9175
- lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION$1), -1, 1),
9176
- fillStyle: resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
9177
- outlineColor: verticalProgress < 0.54 ? `${palette.highlight}69` : `${palette.shadow}78`,
9406
+ lightIntensity,
9407
+ rimLightIntensity,
9408
+ fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
9409
+ outlineColor: verticalProgress < 0.52 ? `${palette.highlight}5c` : `${palette.shadow}66`,
9178
9410
  });
9179
9411
  }
9180
9412
  }
9181
9413
  return surfacePatches;
9182
9414
  }
9183
9415
  /**
9184
- * Samples one point on the continuous Octopus 3D 3 surface.
9185
- *
9186
- * The lower hemisphere is pulled into eight seeded waving lobes, so the portrait reads as
9187
- * tentacled while still being rendered as one connected blobby mesh.
9416
+ * Samples one point on the continuous Octopus 3D 4 surface.
9188
9417
  *
9189
- * @private helper of `octopus3d3AvatarVisual`
9418
+ * @private helper of `octopus3d4AvatarVisual`
9190
9419
  */
9191
- function sampleContinuousOctopusSurfacePoint(options, latitude, longitude) {
9192
- return sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, resolveContinuousTentacleInfluence(options, longitude), resolveContinuousLobeWave(options, longitude));
9420
+ function sampleBlobbyContinuousSurfacePoint(options, latitude, longitude) {
9421
+ return sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, resolveBlobbyTentacleInfluence(options, longitude), resolveBlobbyLobeWave(options, longitude));
9193
9422
  }
9194
9423
  /**
9195
- * Samples one point on the continuous Octopus 3D 3 surface using precomputed longitude-only
9196
- * values to skip the per-call `Math.exp` tentacle-influence loop and the lobe-wave trig call.
9424
+ * Samples one point on the continuous Octopus 3D 4 surface using precomputed longitude-only values.
9197
9425
  *
9198
- * The patch loop quantizes the mesh into a fixed `(latitudePatchCount + 1) * (longitudePatchCount + 1)`
9199
- * corner grid, so the same longitude is reused across every latitude row and each
9200
- * tentacle/lobe value can be computed once per frame instead of `latitudePatchCount * 4` times.
9426
+ * The patch loop quantizes the mesh into a fixed corner grid, so the same longitude is reused
9427
+ * across every latitude row and each tentacle/lobe value is computed once per frame instead
9428
+ * of `latitudePatchCount * 4` times.
9201
9429
  *
9202
- * @private helper of `octopus3d3AvatarVisual`
9430
+ * @private helper of `octopus3d4AvatarVisual`
9203
9431
  */
9204
- function sampleContinuousOctopusSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
9432
+ function sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
9205
9433
  const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
9206
9434
  const cosineLatitude = Math.max(0, Math.cos(latitude));
9207
9435
  const verticalProgress = (Math.sin(latitude) + 1) / 2;
9208
- const upperBlend = Math.pow(1 - verticalProgress, 1.28);
9209
- const lowerBlend = smoothStep$1(0.38, 1, verticalProgress);
9210
- const tipBlend = smoothStep$1(0.68, 1, verticalProgress);
9211
- const centerPull = resolveSignedAngularDistance$1(longitude, tentacleInfluence.centerLongitude);
9212
- const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.24 + tipBlend * 0.2);
9436
+ const upperBlend = Math.pow(1 - verticalProgress, 1.32);
9437
+ const lowerBlend = smoothStep(0.34, 1, verticalProgress);
9438
+ const tipBlend = smoothStep(0.66, 1, verticalProgress);
9439
+ const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
9440
+ const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.26 + tipBlend * 0.22);
9213
9441
  const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
9214
9442
  animationPhase * 0.6 +
9215
- timeMs / (1750 + morphologyProfile.body.lobeCount * 30)) *
9216
- (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.8) *
9217
- (0.3 + lowerBlend * 0.7);
9218
- const tentacleWave = Math.sin(timeMs / 760 + tentacleInfluence.phase + verticalProgress * 2.4) *
9443
+ timeMs / (1700 + morphologyProfile.body.lobeCount * 28)) *
9444
+ (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.78) *
9445
+ (0.32 + lowerBlend * 0.7);
9446
+ const primaryTentacleWave = Math.sin(timeMs / 740 + tentacleInfluence.primaryPhase + verticalProgress * 2.6) *
9219
9447
  lowerBlend *
9220
9448
  tentacleInfluence.core *
9221
9449
  tentacleInfluence.swayScale;
9222
- const horizontalScale = 1.04 +
9450
+ const secondaryTentacleWave = Math.sin(timeMs / 470 + tentacleInfluence.secondaryPhase + verticalProgress * 4.2) *
9451
+ lowerBlend *
9452
+ tentacleInfluence.core *
9453
+ tentacleInfluence.swayScale *
9454
+ 0.42;
9455
+ const tentacleCurl = Math.sin(timeMs / 1180 + tentacleInfluence.primaryPhase * 0.6 + verticalProgress * 3.2) *
9456
+ tipBlend *
9457
+ tentacleInfluence.core *
9458
+ tentacleInfluence.curlScale *
9459
+ 0.36;
9460
+ const horizontalScale = 1.06 +
9223
9461
  mantleRipple +
9224
- lowerBlend * (0.16 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.1) +
9225
- lowerBlend * tentacleInfluence.core * (0.2 + lowerLobeWave * 0.12) -
9462
+ lowerBlend * (0.18 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.12) +
9463
+ lowerBlend * tentacleInfluence.core * (0.22 + lowerLobeWave * 0.14) -
9226
9464
  upperBlend * 0.08;
9227
- const depthScale = 1.06 +
9465
+ const depthScale = 1.08 +
9228
9466
  upperBlend * 0.16 +
9229
- Math.max(0, Math.cos(effectiveLongitude)) * 0.1 +
9230
- lowerBlend * tentacleInfluence.core * (0.1 + tentacleInfluence.depthScale * 0.06) -
9467
+ Math.max(0, Math.cos(effectiveLongitude)) * 0.12 +
9468
+ lowerBlend * tentacleInfluence.core * (0.12 + tentacleInfluence.depthScale * 0.07) -
9231
9469
  Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
9232
- const tentacleTubeRadius = lowerBlend * tentacleInfluence.core * (0.11 + tipBlend * 0.06 + tentacleInfluence.widthScale * 0.025) * radiusX;
9470
+ const tentacleTubeRadius = lowerBlend * tentacleInfluence.core * (0.12 + tipBlend * 0.07 + tentacleInfluence.widthScale * 0.028) * radiusX;
9233
9471
  const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
9234
- const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.72;
9472
+ const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.74;
9235
9473
  const lowerDrop = lowerBlend *
9236
9474
  radiusY *
9237
- (0.18 +
9475
+ (0.2 +
9238
9476
  tentacleInfluence.core *
9239
- (0.38 +
9240
- tentacleInfluence.lengthScale * 0.22 +
9241
- (morphologyProfile.tentacles.flowLengthScale - 1) * 0.08));
9477
+ (0.42 +
9478
+ tentacleInfluence.lengthScale * 0.24 +
9479
+ (morphologyProfile.tentacles.flowLengthScale - 1) * 0.1));
9480
+ const combinedTentacleSway = (primaryTentacleWave + secondaryTentacleWave) * radiusX * (0.06 + tipBlend * 0.06);
9242
9481
  return {
9243
- x: Math.sin(effectiveLongitude) * planarRadiusX + tentacleWave * radiusX * (0.052 + tipBlend * 0.05),
9244
- y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.12) -
9245
- upperBlend * radiusY * 0.1 +
9482
+ x: Math.sin(effectiveLongitude) * planarRadiusX + combinedTentacleSway + tentacleCurl * radiusX * 0.18,
9483
+ y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.14) -
9484
+ upperBlend * radiusY * 0.12 +
9246
9485
  lowerDrop +
9247
- Math.sin(timeMs / 1420 + animationPhase + latitude * 1.6) * lowerBlend * radiusY * 0.018 +
9248
- Math.cos(timeMs / 880 + tentacleInfluence.phase) *
9486
+ Math.sin(timeMs / 1380 + animationPhase + latitude * 1.5) * lowerBlend * radiusY * 0.022 +
9487
+ Math.cos(timeMs / 820 + tentacleInfluence.primaryPhase) *
9249
9488
  lowerBlend *
9250
9489
  tipBlend *
9251
9490
  tentacleInfluence.core *
9252
9491
  radiusY *
9253
- 0.034,
9492
+ 0.04,
9254
9493
  z: Math.cos(effectiveLongitude) * planarRadiusZ +
9255
- Math.cos(timeMs / 980 + tentacleInfluence.phase + verticalProgress) *
9494
+ Math.cos(timeMs / 960 + tentacleInfluence.primaryPhase + verticalProgress) *
9256
9495
  lowerBlend *
9257
9496
  tentacleInfluence.core *
9258
9497
  radiusZ *
9259
- 0.04,
9498
+ 0.044 +
9499
+ tentacleCurl * radiusZ * 0.14,
9260
9500
  };
9261
9501
  }
9262
9502
  /**
9263
9503
  * Blends nearby seeded tentacle profiles at one mesh longitude.
9264
9504
  *
9265
- * @private helper of `octopus3d3AvatarVisual`
9505
+ * @private helper of `octopus3d4AvatarVisual`
9266
9506
  */
9267
- function resolveContinuousTentacleInfluence(options, longitude) {
9507
+ function resolveBlobbyTentacleInfluence(options, longitude) {
9268
9508
  let totalWeight = 0;
9269
9509
  let weightedSin = 0;
9270
9510
  let weightedCos = 0;
@@ -9272,10 +9512,12 @@
9272
9512
  let weightedLengthScale = 0;
9273
9513
  let weightedSwayScale = 0;
9274
9514
  let weightedDepthScale = 0;
9275
- let weightedPhase = 0;
9515
+ let weightedCurlScale = 0;
9516
+ let weightedPrimaryPhase = 0;
9517
+ let weightedSecondaryPhase = 0;
9276
9518
  for (const tentacleProfile of options.tentacleProfiles) {
9277
- const distance = Math.abs(resolveSignedAngularDistance$1(longitude, tentacleProfile.centerLongitude));
9278
- const width = 0.2 * tentacleProfile.widthScale;
9519
+ const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
9520
+ const width = 0.22 * tentacleProfile.widthScale;
9279
9521
  const weight = Math.exp(-(distance * distance) / (width * width));
9280
9522
  totalWeight += weight;
9281
9523
  weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
@@ -9284,7 +9526,9 @@
9284
9526
  weightedLengthScale += tentacleProfile.lengthScale * weight;
9285
9527
  weightedSwayScale += tentacleProfile.swayScale * weight;
9286
9528
  weightedDepthScale += tentacleProfile.depthScale * weight;
9287
- weightedPhase += tentacleProfile.phase * weight;
9529
+ weightedCurlScale += tentacleProfile.curlScale * weight;
9530
+ weightedPrimaryPhase += tentacleProfile.primaryPhase * weight;
9531
+ weightedSecondaryPhase += tentacleProfile.secondaryPhase * weight;
9288
9532
  }
9289
9533
  if (totalWeight < 0.0001) {
9290
9534
  return {
@@ -9294,7 +9538,9 @@
9294
9538
  lengthScale: 1,
9295
9539
  swayScale: 1,
9296
9540
  depthScale: 1,
9297
- phase: 0,
9541
+ curlScale: 1,
9542
+ primaryPhase: 0,
9543
+ secondaryPhase: 0,
9298
9544
  };
9299
9545
  }
9300
9546
  return {
@@ -9304,52 +9550,57 @@
9304
9550
  lengthScale: weightedLengthScale / totalWeight,
9305
9551
  swayScale: weightedSwayScale / totalWeight,
9306
9552
  depthScale: weightedDepthScale / totalWeight,
9307
- phase: weightedPhase / totalWeight,
9553
+ curlScale: weightedCurlScale / totalWeight,
9554
+ primaryPhase: weightedPrimaryPhase / totalWeight,
9555
+ secondaryPhase: weightedSecondaryPhase / totalWeight,
9308
9556
  };
9309
9557
  }
9310
9558
  /**
9311
9559
  * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
9312
9560
  *
9313
- * @private helper of `octopus3d3AvatarVisual`
9561
+ * @private helper of `octopus3d4AvatarVisual`
9314
9562
  */
9315
- function resolveContinuousLobeWave(options, longitude) {
9563
+ function resolveBlobbyLobeWave(options, longitude) {
9316
9564
  const { morphologyProfile, animationPhase, timeMs } = options;
9317
- return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT$1 +
9565
+ return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT +
9318
9566
  animationPhase +
9319
- timeMs / (980 + morphologyProfile.body.lobeCount * 18)) +
9567
+ timeMs / (940 + morphologyProfile.body.lobeCount * 18)) +
9320
9568
  1) /
9321
9569
  2);
9322
9570
  }
9323
9571
  /**
9324
9572
  * Resolves one base fill tone for a patch on the continuous octopus mesh.
9325
9573
  *
9326
- * @private helper of `octopus3d3AvatarVisual`
9574
+ * @private helper of `octopus3d4AvatarVisual`
9327
9575
  */
9328
- function resolveContinuousSurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
9576
+ function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
9329
9577
  const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
9330
- if (tonalProgress < 0.14) {
9578
+ if (tonalProgress < 0.12) {
9331
9579
  return palette.highlight;
9332
9580
  }
9333
- if (tonalProgress < 0.32) {
9581
+ if (tonalProgress < 0.3) {
9334
9582
  return palette.secondary;
9335
9583
  }
9336
9584
  if (tonalProgress < 0.72) {
9337
- return forwardness > 0.55 ? palette.secondary : palette.primary;
9585
+ return forwardness > 0.56 ? palette.secondary : palette.primary;
9338
9586
  }
9339
9587
  return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
9340
9588
  }
9341
9589
  /**
9342
- * Draws one projected mesh patch with soft shading and a subtle edge.
9590
+ * Draws one projected mesh patch with multi-pass shading and a soft edge.
9343
9591
  *
9344
- * @private helper of `octopus3d3AvatarVisual`
9592
+ * @private helper of `octopus3d4AvatarVisual`
9345
9593
  */
9346
- function drawContinuousSurfacePatch(context, surfacePatch) {
9594
+ function drawBlobbyContinuousSurfacePatch(context, surfacePatch) {
9347
9595
  drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
9348
9596
  if (surfacePatch.lightIntensity > 0) {
9349
- drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.18 * surfacePatch.lightIntensity})`);
9597
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.2 * surfacePatch.lightIntensity})`);
9350
9598
  }
9351
9599
  else if (surfacePatch.lightIntensity < 0) {
9352
- drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.25 * Math.abs(surfacePatch.lightIntensity)})`);
9600
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.26 * Math.abs(surfacePatch.lightIntensity)})`);
9601
+ }
9602
+ if (surfacePatch.rimLightIntensity > 0.04) {
9603
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 247, 230, ${0.22 * surfacePatch.rimLightIntensity})`);
9353
9604
  }
9354
9605
  context.save();
9355
9606
  context.beginPath();
@@ -9359,7 +9610,7 @@
9359
9610
  }
9360
9611
  context.closePath();
9361
9612
  context.strokeStyle = surfacePatch.outlineColor;
9362
- context.lineWidth = Math.max(0.7, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0032);
9613
+ context.lineWidth = Math.max(0.6, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0026);
9363
9614
  context.lineJoin = 'round';
9364
9615
  context.stroke();
9365
9616
  context.restore();
@@ -9367,9 +9618,9 @@
9367
9618
  /**
9368
9619
  * Draws projected mantle-current lines on the front of the mesh.
9369
9620
  *
9370
- * @private helper of `octopus3d3AvatarVisual`
9621
+ * @private helper of `octopus3d4AvatarVisual`
9371
9622
  */
9372
- function drawProjectedSurfaceCurrents(options) {
9623
+ function drawBlobbyContinuousCurrents(options) {
9373
9624
  const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
9374
9625
  const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
9375
9626
  const centerIndex = (currentCount - 1) / 2;
@@ -9379,11 +9630,11 @@
9379
9630
  for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
9380
9631
  const baseLongitude = (currentIndex - centerIndex) * 0.15;
9381
9632
  const projectedPoints = [];
9382
- for (let sampleIndex = 0; sampleIndex < 8; sampleIndex++) {
9383
- const progress = sampleIndex / 7;
9384
- const latitude = -0.46 + progress * 0.74;
9385
- const longitude = baseLongitude + Math.sin(timeMs / 1160 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.035;
9386
- const scenePoint = transformScenePoint(sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
9633
+ for (let sampleIndex = 0; sampleIndex < 9; sampleIndex++) {
9634
+ const progress = sampleIndex / 8;
9635
+ const latitude = -0.48 + progress * 0.78;
9636
+ const longitude = baseLongitude + Math.sin(timeMs / 1140 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.038;
9637
+ const scenePoint = transformScenePoint(sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
9387
9638
  if (scenePoint.z > center.z - size * 0.016) {
9388
9639
  projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
9389
9640
  }
@@ -9405,39 +9656,62 @@
9405
9656
  /**
9406
9657
  * Draws small projected sucker highlights on the waving lower mesh lobes.
9407
9658
  *
9408
- * @private helper of `octopus3d3AvatarVisual`
9659
+ * @private helper of `octopus3d4AvatarVisual`
9409
9660
  */
9410
- function drawProjectedTentacleSuckers(options) {
9661
+ function drawBlobbyContinuousSuckers(options) {
9411
9662
  const { surfaceOptions, size } = options;
9412
9663
  const { timeMs } = surfaceOptions;
9413
9664
  for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
9414
- if (Math.cos(tentacleProfile.centerLongitude) < -0.12) {
9665
+ if (Math.cos(tentacleProfile.centerLongitude) < -0.16) {
9415
9666
  continue;
9416
9667
  }
9417
- for (let suckerIndex = 0; suckerIndex < 3; suckerIndex++) {
9418
- const latitude = 0.52 + suckerIndex * 0.14;
9419
- const sideOffset = tentacleProfile.suckerSide * (0.035 + suckerIndex * 0.012) * tentacleProfile.widthScale;
9420
- const waveOffset = Math.sin(timeMs / 900 + tentacleProfile.phase + suckerIndex * 0.8) * 0.018;
9421
- drawProjectedSurfaceSpot({
9668
+ for (let suckerIndex = 0; suckerIndex < 4; suckerIndex++) {
9669
+ const latitude = 0.5 + suckerIndex * 0.12;
9670
+ const sideOffset = tentacleProfile.suckerSide * (0.038 + suckerIndex * 0.014) * tentacleProfile.widthScale;
9671
+ const waveOffset = Math.sin(timeMs / 880 + tentacleProfile.primaryPhase + suckerIndex * 0.78) * 0.02;
9672
+ drawBlobbyContinuousSurfaceSpot({
9422
9673
  ...options,
9423
9674
  latitude,
9424
9675
  longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
9425
- radiusScale: size * (0.0065 - suckerIndex * 0.0007),
9676
+ radiusScale: size * (0.0068 - suckerIndex * 0.0006),
9426
9677
  });
9427
9678
  }
9428
9679
  }
9429
9680
  }
9430
9681
  /**
9431
- * Draws one tiny projected surface spot by sampling local mesh tangents.
9682
+ * Draws seeded pigment spots across the upper mesh for a richer skin texture.
9432
9683
  *
9433
- * @private helper of `octopus3d3AvatarVisual`
9684
+ * @private helper of `octopus3d4AvatarVisual`
9434
9685
  */
9435
- function drawProjectedSurfaceSpot(options) {
9436
- const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
9437
- const localCenter = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude);
9438
- const localHorizontal = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
9439
- const localVertical = sampleContinuousOctopusSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
9440
- const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
9686
+ function drawBlobbySkinSpots(options) {
9687
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, skinSpots, } = options;
9688
+ for (const skinSpot of skinSpots) {
9689
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, skinSpot.latitude, skinSpot.longitude);
9690
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
9691
+ if (sceneCenterPoint.z <= center.z - size * 0.01) {
9692
+ continue;
9693
+ }
9694
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
9695
+ const spotRadius = size * skinSpot.radiusScale;
9696
+ context.save();
9697
+ context.beginPath();
9698
+ context.arc(projectedCenterPoint.x, projectedCenterPoint.y, spotRadius, 0, Math.PI * 2);
9699
+ context.fillStyle = `${palette.shadow}${formatAlphaHex(skinSpot.opacity)}`;
9700
+ context.fill();
9701
+ context.restore();
9702
+ }
9703
+ }
9704
+ /**
9705
+ * Draws one tiny projected surface spot by sampling local mesh tangents.
9706
+ *
9707
+ * @private helper of `octopus3d4AvatarVisual`
9708
+ */
9709
+ function drawBlobbyContinuousSurfaceSpot(options) {
9710
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
9711
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude);
9712
+ const localHorizontal = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
9713
+ const localVertical = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
9714
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
9441
9715
  if (sceneCenterPoint.z <= center.z - size * 0.012) {
9442
9716
  return;
9443
9717
  }
@@ -9446,301 +9720,295 @@
9446
9720
  const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
9447
9721
  const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
9448
9722
  radiusScale *
9449
- 0.74, size * 0.003, size * 0.018);
9723
+ 0.78, size * 0.003, size * 0.02);
9450
9724
  const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
9451
9725
  radiusScale *
9452
- 0.52, size * 0.0024, size * 0.014);
9726
+ 0.54, size * 0.0024, size * 0.015);
9453
9727
  const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
9454
9728
  context.save();
9455
9729
  context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
9456
9730
  context.rotate(rotation);
9457
9731
  context.beginPath();
9458
9732
  context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
9459
- context.fillStyle = `${palette.highlight}73`;
9733
+ context.fillStyle = `${palette.highlight}80`;
9460
9734
  context.fill();
9461
- context.strokeStyle = `${palette.highlight}99`;
9735
+ context.strokeStyle = `${palette.highlight}a8`;
9462
9736
  context.lineWidth = Math.max(0.7, size * 0.0028);
9463
9737
  context.stroke();
9464
9738
  context.restore();
9465
9739
  }
9740
+ /**
9741
+ * Draws a soft, slowly drifting gloss highlight on the front of the mesh.
9742
+ *
9743
+ * @private helper of `octopus3d4AvatarVisual`
9744
+ */
9745
+ function drawBlobbyContinuousGloss(options) {
9746
+ const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size } = options;
9747
+ const { timeMs, animationPhase } = surfaceOptions;
9748
+ const glossLatitude = -0.3 + Math.sin(timeMs / 2700 + animationPhase) * 0.04;
9749
+ const glossLongitude = -0.18 + Math.cos(timeMs / 2300 + animationPhase * 0.8) * 0.05;
9750
+ const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, glossLatitude, glossLongitude);
9751
+ const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
9752
+ if (sceneCenterPoint.z <= center.z) {
9753
+ return;
9754
+ }
9755
+ const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
9756
+ const glossRadius = size * 0.058;
9757
+ context.save();
9758
+ const glossGradient = context.createRadialGradient(projectedCenterPoint.x - glossRadius * 0.3, projectedCenterPoint.y - glossRadius * 0.4, glossRadius * 0.04, projectedCenterPoint.x, projectedCenterPoint.y, glossRadius);
9759
+ glossGradient.addColorStop(0, 'rgba(255, 255, 255, 0.34)');
9760
+ glossGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
9761
+ glossGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
9762
+ context.fillStyle = glossGradient;
9763
+ context.beginPath();
9764
+ context.ellipse(projectedCenterPoint.x, projectedCenterPoint.y, glossRadius * 1.05, glossRadius * 0.78, 0, 0, Math.PI * 2);
9765
+ context.fill();
9766
+ context.restore();
9767
+ }
9466
9768
  /**
9467
9769
  * Resolves a signed angular distance from the source longitude to the target longitude.
9468
9770
  *
9469
- * @private helper of `octopus3d3AvatarVisual`
9771
+ * @private helper of `octopus3d4AvatarVisual`
9470
9772
  */
9471
- function resolveSignedAngularDistance$1(sourceLongitude, targetLongitude) {
9773
+ function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
9472
9774
  return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
9473
9775
  }
9474
9776
  /**
9475
9777
  * Smoothly maps a value between two bounds into `[0, 1]`.
9476
9778
  *
9477
- * @private helper of `octopus3d3AvatarVisual`
9779
+ * @private helper of `octopus3d4AvatarVisual`
9478
9780
  */
9479
- function smoothStep$1(edgeStart, edgeEnd, value) {
9781
+ function smoothStep(edgeStart, edgeEnd, value) {
9480
9782
  const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
9481
9783
  return progress * progress * (3 - 2 * progress);
9482
9784
  }
9483
-
9484
- /* eslint-disable no-magic-numbers */
9485
9785
  /**
9486
- * Light direction used by the continuous Octopus 3D 4 mesh shading.
9786
+ * Converts an opacity ratio into a two-digit hexadecimal alpha suffix.
9487
9787
  *
9488
9788
  * @private helper of `octopus3d4AvatarVisual`
9489
9789
  */
9490
- const LIGHT_DIRECTION = normalizeVector3({
9491
- x: 0.32,
9492
- y: -0.66,
9493
- z: 1,
9494
- });
9790
+ function formatAlphaHex(opacity) {
9791
+ return Math.round(clampNumber$1(opacity, 0, 1) * 255)
9792
+ .toString(16)
9793
+ .padStart(2, '0');
9794
+ }
9795
+
9796
+ /* eslint-disable no-magic-numbers */
9495
9797
  /**
9496
- * Rim-light direction used to brighten the side of the silhouette.
9798
+ * Light direction used by the organic 3D octopus shading.
9497
9799
  *
9498
- * @private helper of `octopus3d4AvatarVisual`
9800
+ * @private helper of `octopus3dAvatarVisual`
9499
9801
  */
9500
- const RIM_LIGHT_DIRECTION = normalizeVector3({
9501
- x: -0.7,
9502
- y: -0.18,
9503
- z: 0.55,
9802
+ const LIGHT_DIRECTION = normalizeVector3({
9803
+ x: 0.48,
9804
+ y: -0.62,
9805
+ z: 0.94,
9504
9806
  });
9505
9807
  /**
9506
- * Real-octopus tentacle count used by the continuous lower mesh.
9507
- *
9508
- * @private helper of `octopus3d4AvatarVisual`
9509
- */
9510
- const OCTOPUS_TENTACLE_COUNT = 8;
9511
- /**
9512
- * Number of seeded skin spots painted across the octopus body.
9513
- *
9514
- * @private helper of `octopus3d4AvatarVisual`
9515
- */
9516
- const SKIN_SPOT_COUNT = 14;
9517
- /**
9518
- * Cache keyed by the `createRandom` factory reference, stable per mounted `<Avatar/>` component.
9808
+ * Cache keyed by the `createRandom` factory reference (stable per mounted `<Avatar/>`).
9519
9809
  *
9520
- * @private helper of `octopus3d4AvatarVisual`
9810
+ * @private helper of `octopus3dAvatarVisual`
9521
9811
  */
9522
- const octopus3d4StableStateCache = new WeakMap();
9812
+ const octopus3dStableStateCache = new WeakMap();
9523
9813
  /**
9524
- * Returns the stable per-avatar state, computing it on first access and caching afterwards.
9814
+ * Returns the stable per-avatar state, computing it on first access and caching for subsequent frames.
9525
9815
  *
9526
- * @private helper of `octopus3d4AvatarVisual`
9816
+ * @private helper of `octopus3dAvatarVisual`
9527
9817
  */
9528
- function getOctopus3d4StableState(createRandom) {
9529
- const cached = octopus3d4StableStateCache.get(createRandom);
9818
+ function getOctopus3dStableState(createRandom) {
9819
+ const cached = octopus3dStableStateCache.get(createRandom);
9530
9820
  if (cached !== undefined) {
9531
9821
  return cached;
9532
9822
  }
9533
- const morphologyProfile = createOctopus3MorphologyProfile(createRandom);
9534
- const animationRandom = createRandom('octopus3d4-animation-profile');
9535
- const eyeRandom = createRandom('octopus3d4-eye-profile');
9536
- const leftEyePhaseOffset = eyeRandom() * 0.7;
9537
- const rightEyePhaseOffset = eyeRandom() * 0.7;
9823
+ const animationRandom = createRandom('octopus3d-animation-profile');
9824
+ const eyeRandom = createRandom('octopus3d-eye-profile');
9825
+ const leftEyePhaseOffset = eyeRandom() * 0.6;
9826
+ const rightEyePhaseOffset = eyeRandom() * 0.6;
9538
9827
  const state = {
9539
- morphologyProfile,
9828
+ morphologyProfile: createOctopus3MorphologyProfile(createRandom),
9540
9829
  animationPhase: animationRandom() * Math.PI * 2,
9541
9830
  leftEyePhaseOffset,
9542
9831
  rightEyePhaseOffset,
9543
- tentacleProfiles: createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile),
9544
- skinSpots: createBlobbySkinSpots(createRandom),
9545
9832
  };
9546
- octopus3d4StableStateCache.set(createRandom, state);
9833
+ octopus3dStableStateCache.set(createRandom, state);
9547
9834
  return state;
9548
9835
  }
9549
9836
  /**
9550
- * Octopus 3D 4 avatar visual.
9837
+ * Proper 3D Octopus visual built from projected organic meshes and tentacles.
9551
9838
  *
9552
9839
  * @private built-in avatar visual
9553
9840
  */
9554
- const octopus3d4AvatarVisual = {
9555
- id: 'octopus3d4',
9556
- title: 'Octopus 3D 4',
9557
- 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.',
9841
+ const octopus3dAvatarVisual = {
9842
+ id: 'octopus3d',
9843
+ title: 'Octopus 3D',
9844
+ description: 'Proper 3D octopus portrait with a turning silhouette, expressive eyes, and depth-sorted tentacles.',
9558
9845
  isAnimated: true,
9559
9846
  supportsPointerTracking: true,
9560
9847
  render({ context, size, palette, createRandom, timeMs, interaction }) {
9561
- const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset, tentacleProfiles, skinSpots } = getOctopus3d4StableState(createRandom);
9848
+ const { morphologyProfile, animationPhase, leftEyePhaseOffset, rightEyePhaseOffset } = getOctopus3dStableState(createRandom);
9562
9849
  const sceneCenterX = size * 0.5;
9563
- const sceneCenterY = size * 0.535;
9564
- const bob = Math.sin(timeMs / 980 + animationPhase) * size * 0.013;
9565
- const meshCenter = {
9566
- x: interaction.bodyOffsetX * size * 0.052 + size * morphologyProfile.body.centerXJitterRatio * 0.44,
9567
- y: -size * 0.07 + interaction.bodyOffsetY * size * 0.028 + bob,
9568
- z: interaction.intensity * size * 0.02,
9850
+ const sceneCenterY = size * 0.56;
9851
+ const bob = Math.sin(timeMs / 920 + animationPhase) * size * 0.014;
9852
+ const mantleCenter = {
9853
+ x: interaction.bodyOffsetX * size * 0.042 + size * morphologyProfile.body.centerXJitterRatio * 0.65,
9854
+ y: -size * 0.09 + interaction.bodyOffsetY * size * 0.028 + bob,
9855
+ z: interaction.intensity * size * 0.012,
9569
9856
  };
9570
- const rotationY = -0.08 +
9571
- Math.sin(timeMs / 2800 + animationPhase) * 0.04 +
9572
- interaction.bodyOffsetX * 0.24 +
9573
- interaction.gazeX * 0.98;
9574
- const rotationX = -0.07 +
9575
- Math.cos(timeMs / 3200 + animationPhase * 0.7) * 0.02 -
9576
- interaction.bodyOffsetY * 0.08 -
9577
- interaction.gazeY * 0.42;
9578
- const surfaceOptions = {
9579
- radiusX: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch * 1.12,
9580
- radiusY: size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1,
9581
- radiusZ: size *
9582
- morphologyProfile.body.bodyRadiusRatio *
9583
- (1.04 + (morphologyProfile.body.horizontalStretch - 1) * 0.2),
9584
- morphologyProfile,
9585
- timeMs,
9586
- animationPhase,
9587
- tentacleProfiles,
9857
+ const underbodyCenter = {
9858
+ x: mantleCenter.x * 0.86,
9859
+ y: mantleCenter.y + size * 0.168,
9860
+ z: mantleCenter.z - size * 0.018,
9588
9861
  };
9589
- const surfacePatches = resolveVisibleBlobbyContinuousPatches({
9590
- ...surfaceOptions,
9591
- center: meshCenter,
9592
- rotationX,
9593
- rotationY,
9862
+ const mantleRadiusX = size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.horizontalStretch;
9863
+ const mantleRadiusY = size * morphologyProfile.body.bodyRadiusRatio * morphologyProfile.body.verticalStretch * 1.1;
9864
+ const mantleRadiusZ = size *
9865
+ morphologyProfile.body.bodyRadiusRatio *
9866
+ (0.9 + (morphologyProfile.body.horizontalStretch - 1) * 0.3);
9867
+ const underbodyRadiusX = mantleRadiusX * (0.9 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.08);
9868
+ const underbodyRadiusY = mantleRadiusY * (0.44 + morphologyProfile.body.lowerDropRatio * 3.1);
9869
+ const underbodyRadiusZ = mantleRadiusZ * 0.78;
9870
+ const bodyYaw = -0.18 +
9871
+ Math.sin(timeMs / 2400 + animationPhase) * 0.05 +
9872
+ interaction.bodyOffsetX * 0.18 +
9873
+ interaction.gazeX * 0.22;
9874
+ const bodyPitch = -0.08 +
9875
+ Math.cos(timeMs / 2700 + animationPhase * 0.6) * 0.025 -
9876
+ interaction.bodyOffsetY * 0.08 -
9877
+ interaction.gazeY * 0.08;
9878
+ const headYaw = bodyYaw - 0.04 + interaction.gazeX * 0.56;
9879
+ const headPitch = bodyPitch - 0.02 - interaction.gazeY * 0.32;
9880
+ const mantlePatches = resolveVisibleEllipsoidPatches({
9881
+ center: mantleCenter,
9882
+ radiusX: mantleRadiusX,
9883
+ radiusY: mantleRadiusY,
9884
+ radiusZ: mantleRadiusZ,
9885
+ rotationX: headPitch,
9886
+ rotationY: headYaw,
9594
9887
  sceneCenterX,
9595
9888
  sceneCenterY,
9596
9889
  size,
9597
9890
  palette,
9891
+ verticalColorBias: 0,
9892
+ outlineColor: `${palette.highlight}7a`,
9598
9893
  });
9599
- const eyeLatitude = clampNumber$1(morphologyProfile.face.eyeCenterYOffsetRatio * 4.4 - 0.04, -0.24, 0.08);
9600
- const eyeLongitude = clampNumber$1(morphologyProfile.face.eyeSpacingRatio * 3.0, 0.18, 0.32);
9601
- const mouthLatitude = clampNumber$1(eyeLatitude + 0.21 + morphologyProfile.face.mouthYOffsetRatio, 0.08, 0.34);
9602
- const mouthCenterLongitude = clampNumber$1(morphologyProfile.face.mouthCenterOffsetRatio * 5.6, -0.08, 0.08);
9603
- const mouthHalfLongitude = clampNumber$1(eyeLongitude * 0.78, 0.15, 0.28);
9604
- const mouthCurveLatitude = clampNumber$1(mouthLatitude + morphologyProfile.face.mouthCurveDepthRatio * 0.78, mouthLatitude + 0.03, 0.42);
9605
- const eyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.78;
9606
- const eyeRadiusY = eyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.92;
9607
- drawAvatarFrame(context, size, palette);
9608
- drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
9609
- drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile);
9610
- for (const surfacePatch of surfacePatches.sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
9611
- drawBlobbyContinuousSurfacePatch(context, surfacePatch);
9612
- }
9613
- drawBlobbySkinSpots({
9614
- context,
9615
- surfaceOptions,
9616
- center: meshCenter,
9617
- rotationX,
9618
- rotationY,
9894
+ const underbodyPatches = resolveVisibleEllipsoidPatches({
9895
+ center: underbodyCenter,
9896
+ radiusX: underbodyRadiusX,
9897
+ radiusY: underbodyRadiusY,
9898
+ radiusZ: underbodyRadiusZ,
9899
+ rotationX: bodyPitch,
9900
+ rotationY: bodyYaw,
9619
9901
  sceneCenterX,
9620
9902
  sceneCenterY,
9621
9903
  size,
9622
9904
  palette,
9623
- skinSpots,
9905
+ verticalColorBias: 0.18,
9906
+ outlineColor: `${palette.shadow}8f`,
9624
9907
  });
9625
- drawBlobbyContinuousCurrents({
9626
- context,
9627
- surfaceOptions,
9628
- center: meshCenter,
9629
- rotationX,
9630
- rotationY,
9631
- sceneCenterX,
9632
- sceneCenterY,
9633
- size,
9634
- palette,
9908
+ const tentacleStrokes = createOctopusTentacleStrokes({
9909
+ createRandom,
9635
9910
  morphologyProfile,
9636
9911
  timeMs,
9637
- animationPhase,
9638
- });
9639
- drawBlobbyContinuousSuckers({
9640
- context,
9641
- surfaceOptions,
9642
- center: meshCenter,
9643
- rotationX,
9644
- rotationY,
9645
- sceneCenterX,
9646
- sceneCenterY,
9647
9912
  size,
9648
- palette,
9649
- });
9650
- drawBlobbyContinuousGloss({
9651
- context,
9652
- surfaceOptions,
9653
- center: meshCenter,
9654
- rotationX,
9655
- rotationY,
9913
+ center: underbodyCenter,
9914
+ radiusX: underbodyRadiusX,
9915
+ radiusY: underbodyRadiusY,
9916
+ radiusZ: underbodyRadiusZ,
9917
+ rotationX: bodyPitch,
9918
+ rotationY: bodyYaw,
9656
9919
  sceneCenterX,
9657
9920
  sceneCenterY,
9658
- size,
9659
- palette,
9921
+ animationPhase,
9660
9922
  });
9661
- drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, -eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
9662
- drawProjectedOrganicEye(context, sampleBlobbyContinuousSurfacePoint(surfaceOptions, eyeLatitude, eyeLongitude), eyeRadiusX, eyeRadiusY, meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.85 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
9923
+ const faceEyeSpacing = size * morphologyProfile.face.eyeSpacingRatio * 0.92;
9924
+ const faceEyeYOffset = size * morphologyProfile.face.eyeCenterYOffsetRatio - mantleRadiusY * 0.02;
9925
+ const faceEyeRadiusX = size * morphologyProfile.face.eyeRadiusXRatio * 0.82;
9926
+ const faceEyeRadiusY = faceEyeRadiusX * morphologyProfile.face.eyeHeightRatio * 0.96;
9927
+ const mouthHalfWidth = size * morphologyProfile.face.mouthWidthRatio * 0.92;
9928
+ const mouthY = size * morphologyProfile.face.mouthYOffsetRatio + mantleRadiusY * 0.08;
9929
+ drawAvatarFrame(context, size, palette);
9930
+ drawOctopus3dAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs);
9931
+ drawOctopus3dShadow(context, size, palette, interaction, timeMs);
9932
+ for (const tentacleStroke of tentacleStrokes.filter((candidateTentacleStroke) => !candidateTentacleStroke.isFrontFacing)) {
9933
+ drawTentacleStroke(context, tentacleStroke, palette);
9934
+ }
9935
+ for (const surfacePatch of [...mantlePatches, ...underbodyPatches].sort((firstSurfacePatch, secondSurfacePatch) => firstSurfacePatch.averageDepth - secondSurfacePatch.averageDepth)) {
9936
+ drawSurfacePatch(context, surfacePatch);
9937
+ }
9938
+ for (const tentacleStroke of tentacleStrokes.filter((candidateTentacleStroke) => candidateTentacleStroke.isFrontFacing)) {
9939
+ drawTentacleStroke(context, tentacleStroke, palette);
9940
+ }
9941
+ drawProjectedOrganicEye(context, {
9942
+ x: -faceEyeSpacing,
9943
+ y: faceEyeYOffset,
9944
+ z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, -faceEyeSpacing, faceEyeYOffset),
9945
+ }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + leftEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
9946
+ drawProjectedOrganicEye(context, {
9947
+ x: faceEyeSpacing,
9948
+ y: faceEyeYOffset,
9949
+ z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, faceEyeSpacing, faceEyeYOffset),
9950
+ }, faceEyeRadiusX, faceEyeRadiusY, mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, size, palette, timeMs, animationPhase + 0.7 + rightEyePhaseOffset, interaction, morphologyProfile.face.eyeStyle);
9663
9951
  drawProjectedOrganicMouth(context, [
9664
- sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude - mouthHalfLongitude),
9665
- sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthCurveLatitude, mouthCenterLongitude),
9666
- sampleBlobbyContinuousSurfacePoint(surfaceOptions, mouthLatitude, mouthCenterLongitude + mouthHalfLongitude),
9667
- ], meshCenter, rotationX, rotationY, sceneCenterX, sceneCenterY, palette, size);
9952
+ {
9953
+ x: -mouthHalfWidth,
9954
+ y: mouthY,
9955
+ z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, -mouthHalfWidth, mouthY),
9956
+ },
9957
+ {
9958
+ x: size * morphologyProfile.face.mouthCenterOffsetRatio,
9959
+ y: mouthY +
9960
+ size * morphologyProfile.face.mouthCurveDepthRatio * 0.38 +
9961
+ Math.sin(timeMs / 760 + animationPhase) * size * 0.01 +
9962
+ interaction.gazeY * size * 0.01,
9963
+ z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, size * morphologyProfile.face.mouthCenterOffsetRatio, mouthY),
9964
+ },
9965
+ {
9966
+ x: mouthHalfWidth,
9967
+ y: mouthY,
9968
+ z: resolveEllipsoidSurfaceDepth(mantleRadiusX, mantleRadiusY, mantleRadiusZ, mouthHalfWidth, mouthY),
9969
+ },
9970
+ ], mantleCenter, headPitch, headYaw, sceneCenterX, sceneCenterY, palette, size);
9668
9971
  },
9669
9972
  };
9670
9973
  /**
9671
- * Creates seeded tentacle-lobe profiles around the visible lower octopus body.
9672
- *
9673
- * @private helper of `octopus3d4AvatarVisual`
9674
- */
9675
- function createBlobbyContinuousTentacleProfiles(createRandom, morphologyProfile) {
9676
- return Array.from({ length: OCTOPUS_TENTACLE_COUNT }, (_, tentacleIndex) => {
9677
- const tentacleRandom = createRandom(`octopus3d4-tentacle-${tentacleIndex}`);
9678
- const progress = tentacleIndex / (OCTOPUS_TENTACLE_COUNT - 1);
9679
- return {
9680
- centerLongitude: -Math.PI * 0.9 +
9681
- progress * Math.PI * 1.8 +
9682
- (tentacleRandom() - 0.5) * (0.06 + morphologyProfile.tentacles.rootSpreadScale * 0.025),
9683
- widthScale: 0.92 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.baseWidthScale - 1) * 0.18,
9684
- lengthScale: 0.9 + tentacleRandom() * 0.34 + (morphologyProfile.tentacles.flowLengthScale - 1) * 0.24,
9685
- swayScale: 0.86 + tentacleRandom() * 0.4 + (morphologyProfile.tentacles.swayScale - 1) * 0.22,
9686
- depthScale: 0.9 + tentacleRandom() * 0.3 + (morphologyProfile.tentacles.tipReachScale - 1) * 0.22,
9687
- curlScale: 0.62 + tentacleRandom() * 0.46,
9688
- primaryPhase: tentacleRandom() * Math.PI * 2,
9689
- secondaryPhase: tentacleRandom() * Math.PI * 2,
9690
- suckerSide: tentacleRandom() > 0.5 ? 1 : -1,
9691
- };
9692
- });
9693
- }
9694
- /**
9695
- * Creates seeded skin pigment spots distributed across the upper octopus mesh.
9696
- *
9697
- * @private helper of `octopus3d4AvatarVisual`
9698
- */
9699
- function createBlobbySkinSpots(createRandom) {
9700
- const spotRandom = createRandom('octopus3d4-skin-spots');
9701
- return Array.from({ length: SKIN_SPOT_COUNT }, () => ({
9702
- latitude: -0.45 + spotRandom() * 0.7,
9703
- longitude: -0.5 + spotRandom() * 1.0,
9704
- radiusScale: 0.0028 + spotRandom() * 0.0052,
9705
- opacity: 0.16 + spotRandom() * 0.22,
9706
- }));
9707
- }
9708
- /**
9709
- * Draws the soft underwater atmosphere behind the continuous octopus mesh.
9974
+ * Draws the atmospheric underwater glow behind the octopus mesh.
9710
9975
  *
9711
- * @private helper of `octopus3d4AvatarVisual`
9976
+ * @private helper of `octopus3dAvatarVisual`
9712
9977
  */
9713
- function drawBlobbyContinuousAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
9714
- 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));
9715
- glowGradient.addColorStop(0, `${palette.highlight}74`);
9716
- glowGradient.addColorStop(0.32, `${palette.accent}32`);
9978
+ function drawOctopus3dAtmosphere(context, size, palette, sceneCenterX, sceneCenterY, interaction, timeMs) {
9979
+ const glowGradient = context.createRadialGradient(sceneCenterX + interaction.gazeX * size * 0.1, sceneCenterY - size * 0.2 + interaction.gazeY * size * 0.05, size * 0.04, sceneCenterX, sceneCenterY - size * 0.04, size * 0.62);
9980
+ glowGradient.addColorStop(0, `${palette.highlight}5c`);
9981
+ glowGradient.addColorStop(0.36, `${palette.accent}24`);
9717
9982
  glowGradient.addColorStop(1, `${palette.highlight}00`);
9718
9983
  context.fillStyle = glowGradient;
9719
9984
  context.fillRect(0, 0, size, size);
9720
- 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);
9721
- lowerGradient.addColorStop(0, `${palette.secondary}28`);
9985
+ const lowerGradient = context.createRadialGradient(sceneCenterX + Math.sin(timeMs / 1700) * size * 0.05, sceneCenterY + size * 0.23, size * 0.08, sceneCenterX, sceneCenterY + size * 0.28, size * 0.5);
9986
+ lowerGradient.addColorStop(0, `${palette.secondary}1d`);
9722
9987
  lowerGradient.addColorStop(1, `${palette.secondary}00`);
9723
9988
  context.fillStyle = lowerGradient;
9724
9989
  context.fillRect(0, 0, size, size);
9725
9990
  }
9726
9991
  /**
9727
- * Draws the soft lower shadow that anchors the octopus in the avatar frame.
9992
+ * Draws the soft ground shadow below the octopus.
9728
9993
  *
9729
- * @private helper of `octopus3d4AvatarVisual`
9994
+ * Uses a scaled radial gradient instead of `context.filter = 'blur()'` to approximate the
9995
+ * blurry ellipse without triggering a costly software rasterization pass on every frame.
9996
+ *
9997
+ * @private helper of `octopus3dAvatarVisual`
9730
9998
  */
9731
- function drawBlobbyContinuousShadow(context, size, palette, interaction, timeMs, morphologyProfile) {
9732
- const cx = size * 0.5 + interaction.gazeX * size * 0.046;
9733
- const cy = size * 0.9 + Math.sin(timeMs / 980) * size * 0.007;
9734
- const rx = size * (0.2 + morphologyProfile.tentacles.rootSpreadScale * 0.024 + interaction.intensity * 0.022);
9735
- const ry = size * 0.062;
9999
+ function drawOctopus3dShadow(context, size, palette, interaction, timeMs) {
10000
+ const cx = size * 0.5 + interaction.gazeX * size * 0.04;
10001
+ const cy = size * 0.87 + Math.sin(timeMs / 920) * size * 0.008;
10002
+ const rx = size * (0.18 + interaction.intensity * 0.02);
10003
+ const ry = size * 0.06;
9736
10004
  context.save();
9737
10005
  context.translate(cx, cy);
9738
10006
  context.scale(1, ry / rx);
9739
- const blurRadius = rx * 1.42;
10007
+ const blurRadius = rx * 1.4;
9740
10008
  const shadowGradient = context.createRadialGradient(0, 0, 0, 0, 0, blurRadius);
9741
- shadowGradient.addColorStop(0, `${palette.shadow}82`);
9742
- shadowGradient.addColorStop(0.45, `${palette.shadow}4a`);
9743
- shadowGradient.addColorStop(0.8, `${palette.shadow}1c`);
10009
+ shadowGradient.addColorStop(0, `${palette.shadow}7a`);
10010
+ shadowGradient.addColorStop(0.45, `${palette.shadow}44`);
10011
+ shadowGradient.addColorStop(0.8, `${palette.shadow}1a`);
9744
10012
  shadowGradient.addColorStop(1, `${palette.shadow}00`);
9745
10013
  context.fillStyle = shadowGradient;
9746
10014
  context.beginPath();
@@ -9749,302 +10017,89 @@
9749
10017
  context.restore();
9750
10018
  }
9751
10019
  /**
9752
- * Number of latitude segments used by the continuous Octopus 3D 4 mesh.
10020
+ * Resolves visible projected patches for one rotated ellipsoid mesh.
9753
10021
  *
9754
- * @private helper of `octopus3d4AvatarVisual`
10022
+ * @private helper of `octopus3dAvatarVisual`
9755
10023
  */
9756
- const LATITUDE_PATCH_COUNT = 20;
10024
+ function resolveVisibleEllipsoidPatches(options) {
10025
+ const { center, radiusX, radiusY, radiusZ, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, verticalColorBias, outlineColor, } = options;
10026
+ const latitudePatchCount = 10;
10027
+ const longitudePatchCount = 18;
10028
+ const surfacePatches = [];
10029
+ for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
10030
+ const startLatitude = -Math.PI / 2 + (latitudeIndex / latitudePatchCount) * Math.PI;
10031
+ const endLatitude = -Math.PI / 2 + ((latitudeIndex + 1) / latitudePatchCount) * Math.PI;
10032
+ const verticalProgress = (latitudeIndex + 0.5) / latitudePatchCount;
10033
+ for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
10034
+ const startLongitude = -Math.PI + (longitudeIndex / longitudePatchCount) * Math.PI * 2;
10035
+ const endLongitude = -Math.PI + ((longitudeIndex + 1) / longitudePatchCount) * Math.PI * 2;
10036
+ const localCorners = [
10037
+ sampleEllipsoidPoint(radiusX, radiusY, radiusZ, startLatitude, startLongitude),
10038
+ sampleEllipsoidPoint(radiusX, radiusY, radiusZ, startLatitude, endLongitude),
10039
+ sampleEllipsoidPoint(radiusX, radiusY, radiusZ, endLatitude, endLongitude),
10040
+ sampleEllipsoidPoint(radiusX, radiusY, radiusZ, endLatitude, startLongitude),
10041
+ ];
10042
+ const transformedCorners = localCorners.map((localCorner) => transformScenePoint(localCorner, center, rotationX, rotationY));
10043
+ const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
10044
+ if (surfaceNormal.z <= 0.01) {
10045
+ continue;
10046
+ }
10047
+ const projectedCorners = transformedCorners.map((transformedCorner) => projectScenePoint(transformedCorner, size, sceneCenterX, sceneCenterY));
10048
+ surfacePatches.push({
10049
+ corners: projectedCorners,
10050
+ averageDepth: transformedCorners.reduce((depthSum, transformedCorner) => depthSum + transformedCorner.z, 0) /
10051
+ transformedCorners.length,
10052
+ lightIntensity: clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1),
10053
+ fillStyle: resolveSurfacePatchFillStyle(palette, verticalProgress + verticalColorBias),
10054
+ outlineColor,
10055
+ });
10056
+ }
10057
+ }
10058
+ return surfacePatches;
10059
+ }
9757
10060
  /**
9758
- * Number of longitude segments used by the continuous Octopus 3D 4 mesh.
10061
+ * Samples one point on an ellipsoid aligned to the local axes.
9759
10062
  *
9760
- * @private helper of `octopus3d4AvatarVisual`
10063
+ * @private helper of `octopus3dAvatarVisual`
9761
10064
  */
9762
- const LONGITUDE_PATCH_COUNT = 48;
10065
+ function sampleEllipsoidPoint(radiusX, radiusY, radiusZ, latitude, longitude) {
10066
+ const cosineLatitude = Math.cos(latitude);
10067
+ return {
10068
+ x: Math.sin(longitude) * cosineLatitude * radiusX,
10069
+ y: Math.sin(latitude) * radiusY,
10070
+ z: Math.cos(longitude) * cosineLatitude * radiusZ,
10071
+ };
10072
+ }
9763
10073
  /**
9764
- * Resolves visible projected patches for the continuous Octopus 3D 4 mesh.
9765
- *
9766
- * Within a single frame, mesh corner samples and longitude-only computations (tentacle
9767
- * influence and lobe wave) are quantized to the patch grid and computed once each rather
9768
- * than re-evaluated for every patch corner.
10074
+ * Resolves one base fill tone for a surface patch across the octopus body.
9769
10075
  *
9770
- * @private helper of `octopus3d4AvatarVisual`
10076
+ * @private helper of `octopus3dAvatarVisual`
9771
10077
  */
9772
- function resolveVisibleBlobbyContinuousPatches(options) {
9773
- const { center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette } = options;
9774
- const latitudePatchCount = LATITUDE_PATCH_COUNT;
9775
- const longitudePatchCount = LONGITUDE_PATCH_COUNT;
9776
- const surfacePatches = [];
9777
- const latitudeBoundaries = new Float64Array(latitudePatchCount + 1);
9778
- const longitudeBoundaries = new Float64Array(longitudePatchCount + 1);
9779
- for (let boundaryIndex = 0; boundaryIndex <= latitudePatchCount; boundaryIndex++) {
9780
- latitudeBoundaries[boundaryIndex] = -Math.PI / 2 + (boundaryIndex / latitudePatchCount) * Math.PI;
10078
+ function resolveSurfacePatchFillStyle(palette, verticalProgress) {
10079
+ const clampedVerticalProgress = clampNumber$1(verticalProgress, 0, 1);
10080
+ if (clampedVerticalProgress < 0.2) {
10081
+ return palette.highlight;
9781
10082
  }
9782
- for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
9783
- longitudeBoundaries[boundaryIndex] = -Math.PI + (boundaryIndex / longitudePatchCount) * Math.PI * 2;
10083
+ if (clampedVerticalProgress < 0.45) {
10084
+ return palette.secondary;
9784
10085
  }
9785
- const cachedTentacleInfluencesByCornerLongitude = new Array(longitudePatchCount + 1);
9786
- const cachedLobeWavesByCornerLongitude = new Float64Array(longitudePatchCount + 1);
9787
- const cachedTentacleInfluencesByPatchCenterLongitude = new Array(longitudePatchCount);
9788
- const cachedLobeWavesByPatchCenterLongitude = new Float64Array(longitudePatchCount);
9789
- const cachedCosByPatchCenterLongitude = new Float64Array(longitudePatchCount);
9790
- for (let boundaryIndex = 0; boundaryIndex <= longitudePatchCount; boundaryIndex++) {
9791
- const cornerLongitude = longitudeBoundaries[boundaryIndex];
9792
- cachedTentacleInfluencesByCornerLongitude[boundaryIndex] = resolveBlobbyTentacleInfluence(options, cornerLongitude);
9793
- cachedLobeWavesByCornerLongitude[boundaryIndex] = resolveBlobbyLobeWave(options, cornerLongitude);
10086
+ if (clampedVerticalProgress < 0.8) {
10087
+ return palette.primary;
9794
10088
  }
9795
- for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
9796
- const patchCenterLongitude = (longitudeBoundaries[longitudeIndex] + longitudeBoundaries[longitudeIndex + 1]) / 2;
9797
- cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyTentacleInfluence(options, patchCenterLongitude);
9798
- cachedLobeWavesByPatchCenterLongitude[longitudeIndex] = resolveBlobbyLobeWave(options, patchCenterLongitude);
9799
- cachedCosByPatchCenterLongitude[longitudeIndex] = Math.max(0, Math.cos(patchCenterLongitude));
9800
- }
9801
- const cornerCount = (latitudePatchCount + 1) * (longitudePatchCount + 1);
9802
- const transformedCornerSamples = new Array(cornerCount);
9803
- for (let latitudeBoundaryIndex = 0; latitudeBoundaryIndex <= latitudePatchCount; latitudeBoundaryIndex++) {
9804
- const cornerLatitude = latitudeBoundaries[latitudeBoundaryIndex];
9805
- for (let longitudeBoundaryIndex = 0; longitudeBoundaryIndex <= longitudePatchCount; longitudeBoundaryIndex++) {
9806
- const cornerLongitude = longitudeBoundaries[longitudeBoundaryIndex];
9807
- const cornerIndex = latitudeBoundaryIndex * (longitudePatchCount + 1) + longitudeBoundaryIndex;
9808
- const cornerSample = sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, cornerLatitude, cornerLongitude, cachedTentacleInfluencesByCornerLongitude[longitudeBoundaryIndex], cachedLobeWavesByCornerLongitude[longitudeBoundaryIndex]);
9809
- transformedCornerSamples[cornerIndex] = transformScenePoint(cornerSample, center, rotationX, rotationY);
9810
- }
9811
- }
9812
- for (let latitudeIndex = 0; latitudeIndex < latitudePatchCount; latitudeIndex++) {
9813
- const startLatitude = latitudeBoundaries[latitudeIndex];
9814
- const endLatitude = latitudeBoundaries[latitudeIndex + 1];
9815
- const centerLatitude = (startLatitude + endLatitude) / 2;
9816
- const verticalProgress = (Math.sin(centerLatitude) + 1) / 2;
9817
- const startCornerRowOffset = latitudeIndex * (longitudePatchCount + 1);
9818
- const endCornerRowOffset = (latitudeIndex + 1) * (longitudePatchCount + 1);
9819
- for (let longitudeIndex = 0; longitudeIndex < longitudePatchCount; longitudeIndex++) {
9820
- const transformedCorners = [
9821
- transformedCornerSamples[startCornerRowOffset + longitudeIndex],
9822
- transformedCornerSamples[startCornerRowOffset + longitudeIndex + 1],
9823
- transformedCornerSamples[endCornerRowOffset + longitudeIndex + 1],
9824
- transformedCornerSamples[endCornerRowOffset + longitudeIndex],
9825
- ];
9826
- const surfaceNormal = normalizeVector3(crossProduct3D(subtractPoint3D(transformedCorners[1], transformedCorners[0]), subtractPoint3D(transformedCorners[2], transformedCorners[0])));
9827
- if (surfaceNormal.z <= 0.006) {
9828
- continue;
9829
- }
9830
- const projectedCorners = [
9831
- projectScenePoint(transformedCorners[0], size, sceneCenterX, sceneCenterY),
9832
- projectScenePoint(transformedCorners[1], size, sceneCenterX, sceneCenterY),
9833
- projectScenePoint(transformedCorners[2], size, sceneCenterX, sceneCenterY),
9834
- projectScenePoint(transformedCorners[3], size, sceneCenterX, sceneCenterY),
9835
- ];
9836
- const lightIntensity = clampNumber$1(dotProduct3D(surfaceNormal, LIGHT_DIRECTION), -1, 1);
9837
- const rimLightIntensity = Math.pow(clampNumber$1(dotProduct3D(surfaceNormal, RIM_LIGHT_DIRECTION), 0, 1), 2.6);
9838
- surfacePatches.push({
9839
- corners: projectedCorners,
9840
- averageDepth: (transformedCorners[0].z +
9841
- transformedCorners[1].z +
9842
- transformedCorners[2].z +
9843
- transformedCorners[3].z) /
9844
- 4,
9845
- lightIntensity,
9846
- rimLightIntensity,
9847
- fillStyle: resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, cachedCosByPatchCenterLongitude[longitudeIndex], cachedTentacleInfluencesByPatchCenterLongitude[longitudeIndex].core, cachedLobeWavesByPatchCenterLongitude[longitudeIndex]),
9848
- outlineColor: verticalProgress < 0.52 ? `${palette.highlight}5c` : `${palette.shadow}66`,
9849
- });
9850
- }
9851
- }
9852
- return surfacePatches;
9853
- }
9854
- /**
9855
- * Samples one point on the continuous Octopus 3D 4 surface.
9856
- *
9857
- * @private helper of `octopus3d4AvatarVisual`
9858
- */
9859
- function sampleBlobbyContinuousSurfacePoint(options, latitude, longitude) {
9860
- return sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, resolveBlobbyTentacleInfluence(options, longitude), resolveBlobbyLobeWave(options, longitude));
9861
- }
9862
- /**
9863
- * Samples one point on the continuous Octopus 3D 4 surface using precomputed longitude-only values.
9864
- *
9865
- * The patch loop quantizes the mesh into a fixed corner grid, so the same longitude is reused
9866
- * across every latitude row and each tentacle/lobe value is computed once per frame instead
9867
- * of `latitudePatchCount * 4` times.
9868
- *
9869
- * @private helper of `octopus3d4AvatarVisual`
9870
- */
9871
- function sampleBlobbyContinuousSurfacePointWithLongitudeCache(options, latitude, longitude, tentacleInfluence, lowerLobeWave) {
9872
- const { radiusX, radiusY, radiusZ, morphologyProfile, timeMs, animationPhase } = options;
9873
- const cosineLatitude = Math.max(0, Math.cos(latitude));
9874
- const verticalProgress = (Math.sin(latitude) + 1) / 2;
9875
- const upperBlend = Math.pow(1 - verticalProgress, 1.32);
9876
- const lowerBlend = smoothStep(0.34, 1, verticalProgress);
9877
- const tipBlend = smoothStep(0.66, 1, verticalProgress);
9878
- const centerPull = resolveSignedAngularDistance(longitude, tentacleInfluence.centerLongitude);
9879
- const effectiveLongitude = longitude + centerPull * lowerBlend * tentacleInfluence.core * (0.26 + tipBlend * 0.22);
9880
- const mantleRipple = Math.sin(longitude * morphologyProfile.body.lobeCount +
9881
- animationPhase * 0.6 +
9882
- timeMs / (1700 + morphologyProfile.body.lobeCount * 28)) *
9883
- (0.018 + morphologyProfile.body.wobbleAmplitudeRatio * 0.78) *
9884
- (0.32 + lowerBlend * 0.7);
9885
- const primaryTentacleWave = Math.sin(timeMs / 740 + tentacleInfluence.primaryPhase + verticalProgress * 2.6) *
9886
- lowerBlend *
9887
- tentacleInfluence.core *
9888
- tentacleInfluence.swayScale;
9889
- const secondaryTentacleWave = Math.sin(timeMs / 470 + tentacleInfluence.secondaryPhase + verticalProgress * 4.2) *
9890
- lowerBlend *
9891
- tentacleInfluence.core *
9892
- tentacleInfluence.swayScale *
9893
- 0.42;
9894
- const tentacleCurl = Math.sin(timeMs / 1180 + tentacleInfluence.primaryPhase * 0.6 + verticalProgress * 3.2) *
9895
- tipBlend *
9896
- tentacleInfluence.core *
9897
- tentacleInfluence.curlScale *
9898
- 0.36;
9899
- const horizontalScale = 1.06 +
9900
- mantleRipple +
9901
- lowerBlend * (0.18 + (morphologyProfile.tentacles.rootSpreadScale - 1) * 0.12) +
9902
- lowerBlend * tentacleInfluence.core * (0.22 + lowerLobeWave * 0.14) -
9903
- upperBlend * 0.08;
9904
- const depthScale = 1.08 +
9905
- upperBlend * 0.16 +
9906
- Math.max(0, Math.cos(effectiveLongitude)) * 0.12 +
9907
- lowerBlend * tentacleInfluence.core * (0.12 + tentacleInfluence.depthScale * 0.07) -
9908
- Math.max(0, -Math.cos(effectiveLongitude)) * 0.05;
9909
- const tentacleTubeRadius = lowerBlend *
9910
- tentacleInfluence.core *
9911
- (0.12 + tipBlend * 0.07 + tentacleInfluence.widthScale * 0.028) *
9912
- radiusX;
9913
- const planarRadiusX = cosineLatitude * radiusX * horizontalScale + tentacleTubeRadius;
9914
- const planarRadiusZ = cosineLatitude * radiusZ * depthScale + tentacleTubeRadius * 0.74;
9915
- const lowerDrop = lowerBlend *
9916
- radiusY *
9917
- (0.2 +
9918
- tentacleInfluence.core *
9919
- (0.42 +
9920
- tentacleInfluence.lengthScale * 0.24 +
9921
- (morphologyProfile.tentacles.flowLengthScale - 1) * 0.1));
9922
- const combinedTentacleSway = (primaryTentacleWave + secondaryTentacleWave) * radiusX * (0.06 + tipBlend * 0.06);
9923
- return {
9924
- x: Math.sin(effectiveLongitude) * planarRadiusX +
9925
- combinedTentacleSway +
9926
- tentacleCurl * radiusX * 0.18,
9927
- y: Math.sin(latitude) * radiusY * (1 + upperBlend * 0.14) -
9928
- upperBlend * radiusY * 0.12 +
9929
- lowerDrop +
9930
- Math.sin(timeMs / 1380 + animationPhase + latitude * 1.5) * lowerBlend * radiusY * 0.022 +
9931
- Math.cos(timeMs / 820 + tentacleInfluence.primaryPhase) *
9932
- lowerBlend *
9933
- tipBlend *
9934
- tentacleInfluence.core *
9935
- radiusY *
9936
- 0.04,
9937
- z: Math.cos(effectiveLongitude) * planarRadiusZ +
9938
- Math.cos(timeMs / 960 + tentacleInfluence.primaryPhase + verticalProgress) *
9939
- lowerBlend *
9940
- tentacleInfluence.core *
9941
- radiusZ *
9942
- 0.044 +
9943
- tentacleCurl * radiusZ * 0.14,
9944
- };
9945
- }
9946
- /**
9947
- * Blends nearby seeded tentacle profiles at one mesh longitude.
9948
- *
9949
- * @private helper of `octopus3d4AvatarVisual`
9950
- */
9951
- function resolveBlobbyTentacleInfluence(options, longitude) {
9952
- let totalWeight = 0;
9953
- let weightedSin = 0;
9954
- let weightedCos = 0;
9955
- let weightedWidthScale = 0;
9956
- let weightedLengthScale = 0;
9957
- let weightedSwayScale = 0;
9958
- let weightedDepthScale = 0;
9959
- let weightedCurlScale = 0;
9960
- let weightedPrimaryPhase = 0;
9961
- let weightedSecondaryPhase = 0;
9962
- for (const tentacleProfile of options.tentacleProfiles) {
9963
- const distance = Math.abs(resolveSignedAngularDistance(longitude, tentacleProfile.centerLongitude));
9964
- const width = 0.22 * tentacleProfile.widthScale;
9965
- const weight = Math.exp(-(distance * distance) / (width * width));
9966
- totalWeight += weight;
9967
- weightedSin += Math.sin(tentacleProfile.centerLongitude) * weight;
9968
- weightedCos += Math.cos(tentacleProfile.centerLongitude) * weight;
9969
- weightedWidthScale += tentacleProfile.widthScale * weight;
9970
- weightedLengthScale += tentacleProfile.lengthScale * weight;
9971
- weightedSwayScale += tentacleProfile.swayScale * weight;
9972
- weightedDepthScale += tentacleProfile.depthScale * weight;
9973
- weightedCurlScale += tentacleProfile.curlScale * weight;
9974
- weightedPrimaryPhase += tentacleProfile.primaryPhase * weight;
9975
- weightedSecondaryPhase += tentacleProfile.secondaryPhase * weight;
9976
- }
9977
- if (totalWeight < 0.0001) {
9978
- return {
9979
- core: 0,
9980
- centerLongitude: longitude,
9981
- widthScale: 1,
9982
- lengthScale: 1,
9983
- swayScale: 1,
9984
- depthScale: 1,
9985
- curlScale: 1,
9986
- primaryPhase: 0,
9987
- secondaryPhase: 0,
9988
- };
9989
- }
9990
- return {
9991
- core: clampNumber$1(totalWeight, 0, 1),
9992
- centerLongitude: Math.atan2(weightedSin / totalWeight, weightedCos / totalWeight),
9993
- widthScale: weightedWidthScale / totalWeight,
9994
- lengthScale: weightedLengthScale / totalWeight,
9995
- swayScale: weightedSwayScale / totalWeight,
9996
- depthScale: weightedDepthScale / totalWeight,
9997
- curlScale: weightedCurlScale / totalWeight,
9998
- primaryPhase: weightedPrimaryPhase / totalWeight,
9999
- secondaryPhase: weightedSecondaryPhase / totalWeight,
10000
- };
10001
- }
10002
- /**
10003
- * Resolves the soft lower wave that makes the continuous mesh read as a set of tentacles.
10004
- *
10005
- * @private helper of `octopus3d4AvatarVisual`
10006
- */
10007
- function resolveBlobbyLobeWave(options, longitude) {
10008
- const { morphologyProfile, animationPhase, timeMs } = options;
10009
- return ((Math.cos(longitude * OCTOPUS_TENTACLE_COUNT +
10010
- animationPhase +
10011
- timeMs / (940 + morphologyProfile.body.lobeCount * 18)) +
10012
- 1) /
10013
- 2);
10014
- }
10015
- /**
10016
- * Resolves one base fill tone for a patch on the continuous octopus mesh.
10017
- *
10018
- * @private helper of `octopus3d4AvatarVisual`
10019
- */
10020
- function resolveBlobbySurfacePatchFillStyle(palette, verticalProgress, forwardness, tentacleCore, lowerLobeWave) {
10021
- const tonalProgress = clampNumber$1(verticalProgress + lowerLobeWave * 0.1 + tentacleCore * 0.08 - forwardness * 0.08, 0, 1);
10022
- if (tonalProgress < 0.12) {
10023
- return palette.highlight;
10024
- }
10025
- if (tonalProgress < 0.3) {
10026
- return palette.secondary;
10027
- }
10028
- if (tonalProgress < 0.72) {
10029
- return forwardness > 0.56 ? palette.secondary : palette.primary;
10030
- }
10031
- return tentacleCore > 0.44 ? `${palette.primary}f4` : `${palette.shadow}ee`;
10089
+ return `${palette.shadow}f2`;
10032
10090
  }
10033
10091
  /**
10034
- * Draws one projected mesh patch with multi-pass shading and a soft edge.
10092
+ * Draws one projected mesh patch with organic shading.
10035
10093
  *
10036
- * @private helper of `octopus3d4AvatarVisual`
10094
+ * @private helper of `octopus3dAvatarVisual`
10037
10095
  */
10038
- function drawBlobbyContinuousSurfacePatch(context, surfacePatch) {
10096
+ function drawSurfacePatch(context, surfacePatch) {
10039
10097
  drawProjectedQuad(context, surfacePatch.corners, surfacePatch.fillStyle);
10040
10098
  if (surfacePatch.lightIntensity > 0) {
10041
- drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.2 * surfacePatch.lightIntensity})`);
10099
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 255, 255, ${0.15 * surfacePatch.lightIntensity})`);
10042
10100
  }
10043
10101
  else if (surfacePatch.lightIntensity < 0) {
10044
- drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.26 * Math.abs(surfacePatch.lightIntensity)})`);
10045
- }
10046
- if (surfacePatch.rimLightIntensity > 0.04) {
10047
- drawProjectedQuad(context, surfacePatch.corners, `rgba(255, 247, 230, ${0.22 * surfacePatch.rimLightIntensity})`);
10102
+ drawProjectedQuad(context, surfacePatch.corners, `rgba(0, 0, 0, ${0.24 * Math.abs(surfacePatch.lightIntensity)})`);
10048
10103
  }
10049
10104
  context.save();
10050
10105
  context.beginPath();
@@ -10054,187 +10109,127 @@
10054
10109
  }
10055
10110
  context.closePath();
10056
10111
  context.strokeStyle = surfacePatch.outlineColor;
10057
- context.lineWidth = Math.max(0.6, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0026);
10112
+ context.lineWidth = Math.max(1, getProjectedQuadPerimeter(surfacePatch.corners) * 0.0044);
10058
10113
  context.lineJoin = 'round';
10059
10114
  context.stroke();
10060
10115
  context.restore();
10061
10116
  }
10062
10117
  /**
10063
- * Draws projected mantle-current lines on the front of the mesh.
10064
- *
10065
- * @private helper of `octopus3d4AvatarVisual`
10066
- */
10067
- function drawBlobbyContinuousCurrents(options) {
10068
- const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, morphologyProfile, timeMs, animationPhase, } = options;
10069
- const currentCount = Math.min(6, morphologyProfile.details.mantleCurrentCount);
10070
- const centerIndex = (currentCount - 1) / 2;
10071
- context.save();
10072
- context.lineCap = 'round';
10073
- context.lineJoin = 'round';
10074
- for (let currentIndex = 0; currentIndex < currentCount; currentIndex++) {
10075
- const baseLongitude = (currentIndex - centerIndex) * 0.15;
10076
- const projectedPoints = [];
10077
- for (let sampleIndex = 0; sampleIndex < 9; sampleIndex++) {
10078
- const progress = sampleIndex / 8;
10079
- const latitude = -0.48 + progress * 0.78;
10080
- const longitude = baseLongitude + Math.sin(timeMs / 1140 + animationPhase + currentIndex * 0.7 + progress * 2) * 0.038;
10081
- const scenePoint = transformScenePoint(sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude), center, rotationX, rotationY);
10082
- if (scenePoint.z > center.z - size * 0.016) {
10083
- projectedPoints.push(projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
10084
- }
10085
- }
10086
- if (projectedPoints.length < 3) {
10087
- continue;
10088
- }
10089
- context.beginPath();
10090
- context.moveTo(projectedPoints[0].x, projectedPoints[0].y);
10091
- for (const projectedPoint of projectedPoints.slice(1)) {
10092
- context.lineTo(projectedPoint.x, projectedPoint.y);
10093
- }
10094
- context.strokeStyle = currentIndex % 2 === 0 ? `${palette.highlight}3d` : `${palette.accent}33`;
10095
- context.lineWidth = size * (0.0055 + currentIndex * 0.00045);
10096
- context.stroke();
10097
- }
10098
- context.restore();
10099
- }
10100
- /**
10101
- * Draws small projected sucker highlights on the waving lower mesh lobes.
10102
- *
10103
- * @private helper of `octopus3d4AvatarVisual`
10104
- */
10105
- function drawBlobbyContinuousSuckers(options) {
10106
- const { surfaceOptions, size } = options;
10107
- const { timeMs } = surfaceOptions;
10108
- for (const tentacleProfile of surfaceOptions.tentacleProfiles) {
10109
- if (Math.cos(tentacleProfile.centerLongitude) < -0.16) {
10110
- continue;
10111
- }
10112
- for (let suckerIndex = 0; suckerIndex < 4; suckerIndex++) {
10113
- const latitude = 0.5 + suckerIndex * 0.12;
10114
- const sideOffset = tentacleProfile.suckerSide * (0.038 + suckerIndex * 0.014) * tentacleProfile.widthScale;
10115
- const waveOffset = Math.sin(timeMs / 880 + tentacleProfile.primaryPhase + suckerIndex * 0.78) * 0.02;
10116
- drawBlobbyContinuousSurfaceSpot({
10117
- ...options,
10118
- latitude,
10119
- longitude: tentacleProfile.centerLongitude + sideOffset + waveOffset,
10120
- radiusScale: size * (0.0068 - suckerIndex * 0.0006),
10121
- });
10122
- }
10123
- }
10124
- }
10125
- /**
10126
- * Draws seeded pigment spots across the upper mesh for a richer skin texture.
10127
- *
10128
- * @private helper of `octopus3d4AvatarVisual`
10129
- */
10130
- function drawBlobbySkinSpots(options) {
10131
- const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, skinSpots, } = options;
10132
- for (const skinSpot of skinSpots) {
10133
- const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, skinSpot.latitude, skinSpot.longitude);
10134
- const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
10135
- if (sceneCenterPoint.z <= center.z - size * 0.01) {
10136
- continue;
10137
- }
10138
- const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
10139
- const spotRadius = size * skinSpot.radiusScale;
10140
- context.save();
10141
- context.beginPath();
10142
- context.arc(projectedCenterPoint.x, projectedCenterPoint.y, spotRadius, 0, Math.PI * 2);
10143
- context.fillStyle = `${palette.shadow}${formatAlphaHex(skinSpot.opacity)}`;
10144
- context.fill();
10145
- context.restore();
10146
- }
10147
- }
10148
- /**
10149
- * Draws one tiny projected surface spot by sampling local mesh tangents.
10150
- *
10151
- * @private helper of `octopus3d4AvatarVisual`
10152
- */
10153
- function drawBlobbyContinuousSurfaceSpot(options) {
10154
- const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size, palette, latitude, longitude, radiusScale, } = options;
10155
- const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude);
10156
- const localHorizontal = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude, longitude + 0.018);
10157
- const localVertical = sampleBlobbyContinuousSurfacePoint(surfaceOptions, latitude + 0.018, longitude);
10158
- const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
10159
- if (sceneCenterPoint.z <= center.z - size * 0.012) {
10160
- return;
10161
- }
10162
- const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
10163
- const projectedHorizontalPoint = projectScenePoint(transformScenePoint(localHorizontal, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
10164
- const projectedVerticalPoint = projectScenePoint(transformScenePoint(localVertical, center, rotationX, rotationY), size, sceneCenterX, sceneCenterY);
10165
- const horizontalRadius = clampNumber$1(Math.hypot(projectedHorizontalPoint.x - projectedCenterPoint.x, projectedHorizontalPoint.y - projectedCenterPoint.y) *
10166
- radiusScale *
10167
- 0.78, size * 0.003, size * 0.02);
10168
- const verticalRadius = clampNumber$1(Math.hypot(projectedVerticalPoint.x - projectedCenterPoint.x, projectedVerticalPoint.y - projectedCenterPoint.y) *
10169
- radiusScale *
10170
- 0.54, size * 0.0024, size * 0.015);
10171
- const rotation = Math.atan2(projectedHorizontalPoint.y - projectedCenterPoint.y, projectedHorizontalPoint.x - projectedCenterPoint.x);
10172
- context.save();
10173
- context.translate(projectedCenterPoint.x, projectedCenterPoint.y);
10174
- context.rotate(rotation);
10175
- context.beginPath();
10176
- context.ellipse(0, 0, horizontalRadius, verticalRadius, 0, 0, Math.PI * 2);
10177
- context.fillStyle = `${palette.highlight}80`;
10178
- context.fill();
10179
- context.strokeStyle = `${palette.highlight}a8`;
10180
- context.lineWidth = Math.max(0.7, size * 0.0028);
10181
- context.stroke();
10182
- context.restore();
10183
- }
10184
- /**
10185
- * Draws a soft, slowly drifting gloss highlight on the front of the mesh.
10118
+ * Creates the projected 3D tentacle strokes orbiting around the lower octopus body.
10186
10119
  *
10187
- * @private helper of `octopus3d4AvatarVisual`
10120
+ * @private helper of `octopus3dAvatarVisual`
10188
10121
  */
10189
- function drawBlobbyContinuousGloss(options) {
10190
- const { context, surfaceOptions, center, rotationX, rotationY, sceneCenterX, sceneCenterY, size } = options;
10191
- const { timeMs, animationPhase } = surfaceOptions;
10192
- const glossLatitude = -0.3 + Math.sin(timeMs / 2700 + animationPhase) * 0.04;
10193
- const glossLongitude = -0.18 + Math.cos(timeMs / 2300 + animationPhase * 0.8) * 0.05;
10194
- const localCenter = sampleBlobbyContinuousSurfacePoint(surfaceOptions, glossLatitude, glossLongitude);
10195
- const sceneCenterPoint = transformScenePoint(localCenter, center, rotationX, rotationY);
10196
- if (sceneCenterPoint.z <= center.z) {
10197
- return;
10198
- }
10199
- const projectedCenterPoint = projectScenePoint(sceneCenterPoint, size, sceneCenterX, sceneCenterY);
10200
- const glossRadius = size * 0.058;
10201
- context.save();
10202
- const glossGradient = context.createRadialGradient(projectedCenterPoint.x - glossRadius * 0.3, projectedCenterPoint.y - glossRadius * 0.4, glossRadius * 0.04, projectedCenterPoint.x, projectedCenterPoint.y, glossRadius);
10203
- glossGradient.addColorStop(0, 'rgba(255, 255, 255, 0.34)');
10204
- glossGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.1)');
10205
- glossGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
10206
- context.fillStyle = glossGradient;
10207
- context.beginPath();
10208
- context.ellipse(projectedCenterPoint.x, projectedCenterPoint.y, glossRadius * 1.05, glossRadius * 0.78, 0, 0, Math.PI * 2);
10209
- context.fill();
10210
- context.restore();
10122
+ function createOctopusTentacleStrokes(options) {
10123
+ const { createRandom, morphologyProfile, timeMs, size, center, radiusX, radiusY, radiusZ, rotationX, rotationY, sceneCenterX, sceneCenterY, animationPhase, } = options;
10124
+ return Array.from({ length: morphologyProfile.tentacles.count }, (_, tentacleIndex) => {
10125
+ const tentacleRandom = createRandom(`octopus3d-tentacle-${tentacleIndex}`);
10126
+ const spreadProgress = morphologyProfile.tentacles.count === 1 ? 0.5 : tentacleIndex / (morphologyProfile.tentacles.count - 1);
10127
+ const orbitAngle = -Math.PI * 0.92 + spreadProgress * Math.PI * 1.84 + (tentacleRandom() - 0.5) * 0.16;
10128
+ const flowLength = size * (0.19 + morphologyProfile.tentacles.flowLengthScale * 0.075 + tentacleRandom() * 0.018);
10129
+ const lateralReach = size *
10130
+ (0.08 + morphologyProfile.tentacles.lateralReachScale * 0.05 + Math.abs(Math.sin(orbitAngle)) * 0.018);
10131
+ const depthReach = size * (0.028 + morphologyProfile.tentacles.tipReachScale * 0.032);
10132
+ const sway = Math.sin(timeMs / (760 + tentacleIndex * 36) + animationPhase + tentacleRandom() * Math.PI * 2);
10133
+ const anchorPoint = {
10134
+ x: Math.sin(orbitAngle) * radiusX * (0.84 + tentacleRandom() * 0.08),
10135
+ y: radiusY * (0.22 + tentacleRandom() * 0.18),
10136
+ z: Math.cos(orbitAngle) * radiusZ * (0.72 + tentacleRandom() * 0.12),
10137
+ };
10138
+ const controlPointOne = {
10139
+ x: anchorPoint.x + Math.sin(orbitAngle) * lateralReach * 0.44,
10140
+ y: anchorPoint.y + flowLength * 0.26,
10141
+ z: anchorPoint.z + Math.cos(orbitAngle) * depthReach * 0.3 + sway * size * 0.012,
10142
+ };
10143
+ const controlPointTwo = {
10144
+ x: anchorPoint.x +
10145
+ Math.sin(orbitAngle) * lateralReach * (0.82 + morphologyProfile.tentacles.swayScale * 0.12),
10146
+ y: anchorPoint.y + flowLength * 0.66,
10147
+ z: anchorPoint.z + Math.cos(orbitAngle) * depthReach * 0.72 + sway * size * 0.02,
10148
+ };
10149
+ const endPoint = {
10150
+ x: anchorPoint.x +
10151
+ Math.sin(orbitAngle) * lateralReach * (1.02 + morphologyProfile.tentacles.tipWidthScale * 0.12) +
10152
+ sway * size * 0.028,
10153
+ y: anchorPoint.y + flowLength,
10154
+ z: anchorPoint.z + Math.cos(orbitAngle) * depthReach + sway * size * 0.016,
10155
+ };
10156
+ const scenePoints = Array.from({ length: 12 }, (_, sampleIndex) => transformScenePoint(sampleCubicBezierPoint3D(anchorPoint, controlPointOne, controlPointTwo, endPoint, sampleIndex / 11), center, rotationX, rotationY));
10157
+ const projectedPoints = scenePoints.map((scenePoint) => projectScenePoint(scenePoint, size, sceneCenterX, sceneCenterY));
10158
+ const averageDepth = scenePoints.reduce((depthSum, scenePoint) => depthSum + scenePoint.z, 0) / scenePoints.length;
10159
+ return {
10160
+ projectedPoints,
10161
+ averageDepth,
10162
+ isFrontFacing: averageDepth >= center.z - size * 0.006,
10163
+ baseWidth: size *
10164
+ (0.019 +
10165
+ morphologyProfile.tentacles.baseWidthScale * 0.007 +
10166
+ tentacleRandom() * 0.003 +
10167
+ Math.abs(Math.sin(orbitAngle)) * 0.002),
10168
+ tipWidth: size * (0.0046 + morphologyProfile.tentacles.tipWidthScale * 0.0018),
10169
+ colorBias: tentacleRandom(),
10170
+ };
10171
+ });
10211
10172
  }
10212
10173
  /**
10213
- * Resolves a signed angular distance from the source longitude to the target longitude.
10174
+ * Samples one point on a cubic Bezier curve in 3D.
10214
10175
  *
10215
- * @private helper of `octopus3d4AvatarVisual`
10176
+ * @private helper of `octopus3dAvatarVisual`
10216
10177
  */
10217
- function resolveSignedAngularDistance(sourceLongitude, targetLongitude) {
10218
- return Math.atan2(Math.sin(targetLongitude - sourceLongitude), Math.cos(targetLongitude - sourceLongitude));
10178
+ function sampleCubicBezierPoint3D(startPoint, controlPointOne, controlPointTwo, endPoint, progress) {
10179
+ const inverseProgress = 1 - progress;
10180
+ return {
10181
+ x: inverseProgress * inverseProgress * inverseProgress * startPoint.x +
10182
+ 3 * inverseProgress * inverseProgress * progress * controlPointOne.x +
10183
+ 3 * inverseProgress * progress * progress * controlPointTwo.x +
10184
+ progress * progress * progress * endPoint.x,
10185
+ y: inverseProgress * inverseProgress * inverseProgress * startPoint.y +
10186
+ 3 * inverseProgress * inverseProgress * progress * controlPointOne.y +
10187
+ 3 * inverseProgress * progress * progress * controlPointTwo.y +
10188
+ progress * progress * progress * endPoint.y,
10189
+ z: inverseProgress * inverseProgress * inverseProgress * startPoint.z +
10190
+ 3 * inverseProgress * inverseProgress * progress * controlPointOne.z +
10191
+ 3 * inverseProgress * progress * progress * controlPointTwo.z +
10192
+ progress * progress * progress * endPoint.z,
10193
+ };
10219
10194
  }
10220
10195
  /**
10221
- * Smoothly maps a value between two bounds into `[0, 1]`.
10196
+ * Draws one projected tentacle stroke with a slim highlight ridge.
10222
10197
  *
10223
- * @private helper of `octopus3d4AvatarVisual`
10198
+ * @private helper of `octopus3dAvatarVisual`
10224
10199
  */
10225
- function smoothStep(edgeStart, edgeEnd, value) {
10226
- const progress = clampNumber$1((value - edgeStart) / (edgeEnd - edgeStart), 0, 1);
10227
- return progress * progress * (3 - 2 * progress);
10200
+ function drawTentacleStroke(context, tentacleStroke, palette) {
10201
+ const projectedSegments = tentacleStroke.projectedPoints.length - 1;
10202
+ for (let segmentIndex = 0; segmentIndex < projectedSegments; segmentIndex++) {
10203
+ const startPoint = tentacleStroke.projectedPoints[segmentIndex];
10204
+ const endPoint = tentacleStroke.projectedPoints[segmentIndex + 1];
10205
+ const progress = segmentIndex / projectedSegments;
10206
+ const width = tentacleStroke.baseWidth + (tentacleStroke.tipWidth - tentacleStroke.baseWidth) * progress;
10207
+ context.beginPath();
10208
+ context.moveTo(startPoint.x, startPoint.y);
10209
+ context.lineTo(endPoint.x, endPoint.y);
10210
+ context.strokeStyle = tentacleStroke.colorBias > 0.6 ? `${palette.secondary}f0` : `${palette.primary}f0`;
10211
+ context.lineWidth = width;
10212
+ context.lineCap = 'round';
10213
+ context.stroke();
10214
+ context.beginPath();
10215
+ context.moveTo(startPoint.x, startPoint.y);
10216
+ context.lineTo(endPoint.x, endPoint.y);
10217
+ context.strokeStyle = tentacleStroke.isFrontFacing ? `${palette.highlight}80` : `${palette.highlight}40`;
10218
+ context.lineWidth = Math.max(1, width * 0.34);
10219
+ context.lineCap = 'round';
10220
+ context.stroke();
10221
+ }
10228
10222
  }
10229
10223
  /**
10230
- * Converts an opacity ratio into a two-digit hexadecimal alpha suffix.
10224
+ * Resolves the front surface depth on an ellipsoid for one local face point.
10231
10225
  *
10232
- * @private helper of `octopus3d4AvatarVisual`
10226
+ * @private helper of `octopus3dAvatarVisual`
10233
10227
  */
10234
- function formatAlphaHex(opacity) {
10235
- return Math.round(clampNumber$1(opacity, 0, 1) * 255)
10236
- .toString(16)
10237
- .padStart(2, '0');
10228
+ function resolveEllipsoidSurfaceDepth(radiusX, radiusY, radiusZ, x, y) {
10229
+ const normalizedX = x / radiusX;
10230
+ const normalizedY = y / radiusY;
10231
+ const remainingDepthRatio = Math.max(0, 1 - normalizedX * normalizedX - normalizedY * normalizedY);
10232
+ return Math.sqrt(remainingDepthRatio) * radiusZ;
10238
10233
  }
10239
10234
 
10240
10235
  /* eslint-disable no-magic-numbers */
@@ -11057,14 +11052,14 @@
11057
11052
  /**
11058
11053
  * META AVATAR commitment definition
11059
11054
  *
11060
- * The `META AVATAR` commitment sets the built-in default avatar visual used when
11061
- * the agent does not provide an explicit `META IMAGE`.
11055
+ * The `META AVATAR` and `META VISUAL` commitments set the built-in default avatar visual
11056
+ * used when the agent does not provide an explicit `META IMAGE`.
11062
11057
  *
11063
11058
  * @private [🪔] Maybe export the commitments through some package
11064
11059
  */
11065
11060
  class MetaAvatarCommitmentDefinition extends BaseCommitmentDefinition {
11066
- constructor() {
11067
- super('META AVATAR');
11061
+ constructor(type = 'META AVATAR') {
11062
+ super(type);
11068
11063
  }
11069
11064
  /**
11070
11065
  * Short one-line description of META AVATAR.
@@ -11084,14 +11079,15 @@
11084
11079
  get documentation() {
11085
11080
  const supportedVisuals = AVATAR_VISUALS.map((avatarVisual) => `\`${avatarVisual.id}\``).join(', ');
11086
11081
  return spacetrim.spaceTrim(`
11087
- # META AVATAR
11082
+ # ${this.type}
11088
11083
 
11089
11084
  Sets the built-in avatar visual used for the agent when no explicit \`META IMAGE\` is provided.
11085
+ \`META AVATAR\` and \`META VISUAL\` are equivalent.
11090
11086
 
11091
11087
  ## Key aspects
11092
11088
 
11093
11089
  - Does not modify the agent's behavior or responses.
11094
- - Only one \`META AVATAR\` should be used per agent.
11090
+ - Only one \`META AVATAR\` or \`META VISUAL\` should be used per agent.
11095
11091
  - If multiple are specified, the last one takes precedence.
11096
11092
  - Values are matched case-insensitively and spaces, hyphens, and underscores are normalized.
11097
11093
  - Supported visuals are derived from the shared avatar registry: ${supportedVisuals}.
@@ -11106,15 +11102,15 @@
11106
11102
  \`\`\`
11107
11103
 
11108
11104
  \`\`\`book
11109
- Orb Assistant
11105
+ Minecraft Assistant
11110
11106
 
11111
- META AVATAR orb
11107
+ META VISUAL Minecraft2
11112
11108
  GOAL Answer in a calm and focused way.
11113
11109
  \`\`\`
11114
11110
  `);
11115
11111
  }
11116
11112
  applyToAgentModelRequirements(requirements, content) {
11117
- // META AVATAR doesn't modify the system message or model requirements.
11113
+ // META AVATAR and META VISUAL don't modify the system message or model requirements.
11118
11114
  // It's handled separately in the parsing logic for profile avatar resolution.
11119
11115
  return requirements;
11120
11116
  }
@@ -13167,10 +13163,10 @@
13167
13163
  *
13168
13164
  * @private
13169
13165
  */
13170
- const TEAM_SYSTEM_MESSAGE_GUIDANCE_LINES = [
13171
- '- If a teammate is relevant to the request, consult that teammate using the matching tool.',
13172
- '- Do not ask the user for information that a listed teammate can provide directly.',
13173
- ];
13166
+ const TEAM_SYSTEM_MESSAGE_GUIDANCE = spacetrim.spaceTrim(`
13167
+ - If a teammate is relevant to the request, consult that teammate using the matching tool.
13168
+ - Do not ask the user for information that a listed teammate can provide directly.
13169
+ `);
13174
13170
  /**
13175
13171
  * Constant for remote agents by Url.
13176
13172
  */
@@ -13352,7 +13348,7 @@
13352
13348
  `);
13353
13349
  });
13354
13350
  return spacetrim.spaceTrim((block) => `
13355
- ${block(TEAM_SYSTEM_MESSAGE_GUIDANCE_LINES.join('\n'))}
13351
+ ${block(TEAM_SYSTEM_MESSAGE_GUIDANCE)}
13356
13352
 
13357
13353
  ${block(teammateSections.join('\n\n'))}
13358
13354
  `);
@@ -20633,6 +20629,7 @@
20633
20629
  new ActionCommitmentDefinition('ACTIONS'),
20634
20630
  new ComponentCommitmentDefinition(),
20635
20631
  new MetaAvatarCommitmentDefinition(),
20632
+ new MetaAvatarCommitmentDefinition('META VISUAL'),
20636
20633
  new MetaImageCommitmentDefinition(),
20637
20634
  new MetaColorCommitmentDefinition(),
20638
20635
  new MetaFontCommitmentDefinition(),
@@ -22285,6 +22282,7 @@
22285
22282
  */
22286
22283
  const META_COMMITMENT_APPLIERS = {
22287
22284
  'META AVATAR': applyMetaAvatarContent,
22285
+ 'META VISUAL': applyMetaAvatarContent,
22288
22286
  'META LINK': applyMetaLinkContent,
22289
22287
  'META DOMAIN': applyMetaDomainContent,
22290
22288
  'META IMAGE': applyMetaImageContent,
@@ -22333,7 +22331,7 @@
22333
22331
  state.meta[metaType] = metaValue;
22334
22332
  }
22335
22333
  /**
22336
- * Applies META AVATAR content into the canonical `meta.avatar` field.
22334
+ * Applies META AVATAR / META VISUAL content into the canonical `meta.avatar` field.
22337
22335
  */
22338
22336
  function applyMetaAvatarContent(state, content) {
22339
22337
  const avatarVisualId = resolveAvatarVisualId(content);
@@ -24505,89 +24503,299 @@
24505
24503
  if (max !== undefined) {
24506
24504
  commands.push(`EXPECT MAX ${max} ${formatExpectationUnit(unit, max)}`);
24507
24505
  }
24508
- return commands;
24506
+ return commands;
24507
+ }
24508
+ /**
24509
+ * Formats the expectation unit exactly as the legacy serializer does.
24510
+ *
24511
+ * @private internal utility of `createTaskSerialization`
24512
+ */
24513
+ function formatExpectationUnit(unit, amount) {
24514
+ return capitalize(unit + (amount > 1 ? 's' : ''));
24515
+ }
24516
+ /**
24517
+ * Collects format commands.
24518
+ *
24519
+ * @private internal utility of `createTaskSerialization`
24520
+ */
24521
+ function createFormatCommands(task) {
24522
+ if (task.format === 'JSON') {
24523
+ // TODO: @deprecated remove
24524
+ return ['FORMAT JSON'];
24525
+ }
24526
+ return [];
24527
+ }
24528
+
24529
+ /**
24530
+ * Stringifies one task section of the pipeline.
24531
+ *
24532
+ * @private internal utility of `pipelineJsonToString`
24533
+ */
24534
+ function stringifyTask(task) {
24535
+ const { title, description, content, resultingParameterName } = task;
24536
+ const { commands, contentLanguage } = createTaskSerialization(task);
24537
+ return spacetrim.spaceTrim((block) => `
24538
+ ## ${title}
24539
+
24540
+ ${block(description || '')}
24541
+
24542
+ ${block(stringifyCommands(commands))}
24543
+
24544
+ \`\`\`${contentLanguage}
24545
+ ${block(spacetrim.spaceTrim(content))}
24546
+ \`\`\`
24547
+
24548
+ \`-> {${resultingParameterName}}\`
24549
+ `); // <- TODO: [main] !!3 If the parameter here has description, add it and use taskParameterJsonToString
24550
+ // <- TODO: [main] !!3 Escape
24551
+ // <- TODO: [🧠] Some clear strategy how to spaceTrim the blocks
24552
+ }
24553
+
24554
+ /**
24555
+ * Converts promptbook in JSON format to string format
24556
+ *
24557
+ * @deprecated TODO: [🥍][🧠] Backup original files in `PipelineJson` same as in Promptbook.studio
24558
+ * @param pipelineJson Promptbook in JSON format (.bookc)
24559
+ * @returns Promptbook in string format (.book.md)
24560
+ *
24561
+ * @public exported from `@promptbook/core`
24562
+ */
24563
+ function pipelineJsonToString(pipelineJson) {
24564
+ let pipelineString = createPipelineIntroduction(pipelineJson);
24565
+ const pipelineCommands = createPipelineCommands(pipelineJson);
24566
+ pipelineString = appendMarkdownBlock(pipelineString, stringifyCommands(pipelineCommands));
24567
+ for (const task of pipelineJson.tasks) {
24568
+ pipelineString = appendMarkdownBlock(pipelineString, stringifyTask(task));
24569
+ }
24570
+ return validatePipelineString(pipelineString);
24571
+ }
24572
+ // TODO: [🛋] Implement new features and commands into `pipelineJsonToString` + `taskParameterJsonToString` , use `stringifyCommand`
24573
+ // TODO: [🧠] Is there a way to auto-detect missing features in pipelineJsonToString
24574
+ // TODO: [🏛] Maybe make some markdown builder
24575
+ // TODO: [🏛] Escape all
24576
+ // TODO: [🧠] Should be in generated .book.md file GENERATOR_WARNING
24577
+
24578
+ /**
24579
+ * This error indicates that the promptbook object has valid syntax (=can be parsed) but contains logical errors (like circular dependencies)
24580
+ *
24581
+ * @public exported from `@promptbook/core`
24582
+ */
24583
+ class PipelineLogicError extends Error {
24584
+ constructor(message) {
24585
+ super(message);
24586
+ this.name = 'PipelineLogicError';
24587
+ Object.setPrototypeOf(this, PipelineLogicError.prototype);
24588
+ }
24589
+ }
24590
+
24591
+ /**
24592
+ * Creates the shared validation context for one pipeline.
24593
+ *
24594
+ * @private function of `validatePipeline`
24595
+ */
24596
+ function createPipelineValidationContext(pipeline) {
24597
+ return {
24598
+ pipeline,
24599
+ pipelineIdentification: getPipelineIdentification(pipeline),
24600
+ };
24601
+ }
24602
+ /**
24603
+ * Builds a short file/url identification block for validation errors.
24604
+ *
24605
+ * @private internal utility of `validatePipeline`
24606
+ */
24607
+ function getPipelineIdentification(pipeline) {
24608
+ // Note: This is a 😐 implementation of [🚞]
24609
+ const pipelineIdentificationParts = [];
24610
+ if (pipeline.sourceFile !== undefined) {
24611
+ pipelineIdentificationParts.push(`File: ${pipeline.sourceFile}`);
24612
+ }
24613
+ if (pipeline.pipelineUrl !== undefined) {
24614
+ pipelineIdentificationParts.push(`Url: ${pipeline.pipelineUrl}`);
24615
+ }
24616
+ return pipelineIdentificationParts.join('\n');
24617
+ }
24618
+
24619
+ /**
24620
+ * Validates that the expected top-level collections have array structure.
24621
+ *
24622
+ * @private function of `validatePipeline`
24623
+ */
24624
+ function validatePipelineCollectionsStructure({ pipeline, pipelineIdentification, }) {
24625
+ validatePipelineParametersCollection(pipeline, pipelineIdentification);
24626
+ validatePipelineTasksCollection(pipeline, pipelineIdentification);
24627
+ }
24628
+ /**
24629
+ * Validates that `pipeline.parameters` is an array.
24630
+ *
24631
+ * @private internal utility of `validatePipelineCollectionsStructure`
24632
+ */
24633
+ function validatePipelineParametersCollection(pipeline, pipelineIdentification) {
24634
+ // TODO: [🧠] Maybe do here some proper JSON-schema / ZOD checking
24635
+ if (Array.isArray(pipeline.parameters)) {
24636
+ return;
24637
+ }
24638
+ // TODO: [🧠] what is the correct error tp throw - maybe PromptbookSchemaError
24639
+ throw new ParseError(spacetrim.spaceTrim((block) => `
24640
+ Pipeline is valid JSON but with wrong structure
24641
+
24642
+ \`PipelineJson.parameters\` expected to be an array, but got ${typeof pipeline.parameters}
24643
+
24644
+ ${block(pipelineIdentification)}
24645
+ `));
24646
+ }
24647
+ /**
24648
+ * Validates that `pipeline.tasks` is an array.
24649
+ *
24650
+ * @private internal utility of `validatePipelineCollectionsStructure`
24651
+ */
24652
+ function validatePipelineTasksCollection(pipeline, pipelineIdentification) {
24653
+ // TODO: [🧠] Maybe do here some proper JSON-schema / ZOD checking
24654
+ if (Array.isArray(pipeline.tasks)) {
24655
+ return;
24656
+ }
24657
+ // TODO: [🧠] what is the correct error tp throw - maybe PromptbookSchemaError
24658
+ throw new ParseError(spacetrim.spaceTrim((block) => `
24659
+ Pipeline is valid JSON but with wrong structure
24660
+
24661
+ \`PipelineJson.tasks\` expected to be an array, but got ${typeof pipeline.tasks}
24662
+
24663
+ ${block(pipelineIdentification)}
24664
+ `));
24665
+ }
24666
+
24667
+ /**
24668
+ * Validates that task dependencies can be resolved without cycles or missing definitions.
24669
+ *
24670
+ * @private function of `validatePipeline`
24671
+ */
24672
+ function validatePipelineDependencyResolution({ pipeline, pipelineIdentification, }) {
24673
+ let dependencyResolutionState = createInitialDependencyResolutionState(pipeline);
24674
+ let loopLimit = LOOP_LIMIT;
24675
+ while (hasUnresolvedTasks(dependencyResolutionState)) {
24676
+ if (loopLimit-- < 0) {
24677
+ throw createDependencyResolutionLoopLimitError(pipelineIdentification);
24678
+ }
24679
+ dependencyResolutionState = resolveNextDependencyResolutionState(dependencyResolutionState, pipelineIdentification);
24680
+ }
24681
+ }
24682
+ /**
24683
+ * Collects the parameter names that are already resolvable before dependency traversal starts.
24684
+ *
24685
+ * @private internal utility of `validatePipelineDependencyResolution`
24686
+ */
24687
+ function createInitialDependencyResolutionState(pipeline) {
24688
+ return {
24689
+ resolvedParameterNames: createInitiallyResolvedParameterNames(pipeline),
24690
+ unresolvedTasks: [...pipeline.tasks],
24691
+ };
24692
+ }
24693
+ /**
24694
+ * Checks whether dependency resolution still has tasks left to process.
24695
+ *
24696
+ * @private internal utility of `validatePipelineDependencyResolution`
24697
+ */
24698
+ function hasUnresolvedTasks({ unresolvedTasks }) {
24699
+ return unresolvedTasks.length > 0;
24700
+ }
24701
+ /**
24702
+ * Resolves the next batch of currently satisfiable tasks.
24703
+ *
24704
+ * @private internal utility of `validatePipelineDependencyResolution`
24705
+ */
24706
+ function resolveNextDependencyResolutionState(dependencyResolutionState, pipelineIdentification) {
24707
+ const currentlyResolvedTasks = getCurrentlyResolvedTasks(dependencyResolutionState.unresolvedTasks, dependencyResolutionState.resolvedParameterNames);
24708
+ if (currentlyResolvedTasks.length === 0) {
24709
+ throw createUnresolvedTasksError(dependencyResolutionState.unresolvedTasks, dependencyResolutionState.resolvedParameterNames, pipelineIdentification);
24710
+ }
24711
+ return {
24712
+ resolvedParameterNames: appendResolvedTaskParameterNames(dependencyResolutionState.resolvedParameterNames, currentlyResolvedTasks),
24713
+ unresolvedTasks: dependencyResolutionState.unresolvedTasks.filter((task) => !currentlyResolvedTasks.includes(task)),
24714
+ };
24715
+ }
24716
+ /**
24717
+ * Collects the parameter names that are already resolvable before dependency traversal starts.
24718
+ *
24719
+ * @private internal utility of `validatePipelineDependencyResolution`
24720
+ */
24721
+ function createInitiallyResolvedParameterNames(pipeline) {
24722
+ let resolvedParameterNames = pipeline.parameters
24723
+ .filter(({ isInput }) => isInput)
24724
+ .map(({ name }) => name);
24725
+ for (const reservedParameterName of RESERVED_PARAMETER_NAMES) {
24726
+ resolvedParameterNames = [...resolvedParameterNames, reservedParameterName];
24727
+ }
24728
+ return resolvedParameterNames;
24509
24729
  }
24510
24730
  /**
24511
- * Formats the expectation unit exactly as the legacy serializer does.
24731
+ * Adds newly resolved task outputs to the resolved parameter list.
24512
24732
  *
24513
- * @private internal utility of `createTaskSerialization`
24733
+ * @private internal utility of `validatePipelineDependencyResolution`
24514
24734
  */
24515
- function formatExpectationUnit(unit, amount) {
24516
- return capitalize(unit + (amount > 1 ? 's' : ''));
24735
+ function appendResolvedTaskParameterNames(resolvedParameterNames, currentlyResolvedTasks) {
24736
+ return [
24737
+ ...resolvedParameterNames,
24738
+ ...currentlyResolvedTasks.map(({ resultingParameterName }) => resultingParameterName),
24739
+ ];
24517
24740
  }
24518
24741
  /**
24519
- * Collects format commands.
24742
+ * Selects tasks whose dependencies are already resolved.
24520
24743
  *
24521
- * @private internal utility of `createTaskSerialization`
24744
+ * @private internal utility of `validatePipelineDependencyResolution`
24522
24745
  */
24523
- function createFormatCommands(task) {
24524
- if (task.format === 'JSON') {
24525
- // TODO: @deprecated remove
24526
- return ['FORMAT JSON'];
24527
- }
24528
- return [];
24746
+ function getCurrentlyResolvedTasks(unresolvedTasks, resolvedParameterNames) {
24747
+ return unresolvedTasks.filter((task) => task.dependentParameterNames.every((name) => resolvedParameterNames.includes(name)));
24529
24748
  }
24749
+ /**
24750
+ * Creates the unexpected loop-limit error for dependency resolution.
24751
+ *
24752
+ * @private internal utility of `validatePipelineDependencyResolution`
24753
+ */
24754
+ function createDependencyResolutionLoopLimitError(pipelineIdentification) {
24755
+ // Note: Really UnexpectedError not LimitReachedError - this should not happen and be caught below
24756
+ return new UnexpectedError(spacetrim.spaceTrim((block) => `
24757
+ Loop limit reached during detection of circular dependencies in \`validatePipeline\`
24530
24758
 
24759
+ ${block(pipelineIdentification)}
24760
+ `));
24761
+ }
24531
24762
  /**
24532
- * Stringifies one task section of the pipeline.
24763
+ * Creates the detailed error for unresolved or circular task dependencies.
24533
24764
  *
24534
- * @private internal utility of `pipelineJsonToString`
24765
+ * @private internal utility of `validatePipelineDependencyResolution`
24535
24766
  */
24536
- function stringifyTask(task) {
24537
- const { title, description, content, resultingParameterName } = task;
24538
- const { commands, contentLanguage } = createTaskSerialization(task);
24539
- return spacetrim.spaceTrim((block) => `
24540
- ## ${title}
24767
+ function createUnresolvedTasksError(unresolvedTasks, resolvedParameterNames, pipelineIdentification) {
24768
+ return new PipelineLogicError(
24769
+ // TODO: [🐎] DRY
24770
+ spacetrim.spaceTrim((block) => `
24541
24771
 
24542
- ${block(description || '')}
24772
+ Can not resolve some parameters:
24773
+ Either you are using a parameter that is not defined, or there are some circular dependencies.
24543
24774
 
24544
- ${block(stringifyCommands(commands))}
24775
+ ${block(pipelineIdentification)}
24545
24776
 
24546
- \`\`\`${contentLanguage}
24547
- ${block(spacetrim.spaceTrim(content))}
24548
- \`\`\`
24777
+ **Can not resolve:**
24778
+ ${block(unresolvedTasks
24779
+ .map(({ resultingParameterName, dependentParameterNames }) => `- Parameter \`{${resultingParameterName}}\` which depends on ${dependentParameterNames
24780
+ .map((dependentParameterName) => `\`{${dependentParameterName}}\``)
24781
+ .join(' and ')}`)
24782
+ .join('\n'))}
24549
24783
 
24550
- \`-> {${resultingParameterName}}\`
24551
- `); // <- TODO: [main] !!3 If the parameter here has description, add it and use taskParameterJsonToString
24552
- // <- TODO: [main] !!3 Escape
24553
- // <- TODO: [🧠] Some clear strategy how to spaceTrim the blocks
24554
- }
24784
+ **Resolved:**
24785
+ ${block(resolvedParameterNames
24786
+ .filter((name) => !RESERVED_PARAMETER_NAMES.includes(name))
24787
+ .map((name) => `- Parameter \`{${name}}\``)
24788
+ .join('\n'))}
24555
24789
 
24556
- /**
24557
- * Converts promptbook in JSON format to string format
24558
- *
24559
- * @deprecated TODO: [🥍][🧠] Backup original files in `PipelineJson` same as in Promptbook.studio
24560
- * @param pipelineJson Promptbook in JSON format (.bookc)
24561
- * @returns Promptbook in string format (.book.md)
24562
- *
24563
- * @public exported from `@promptbook/core`
24564
- */
24565
- function pipelineJsonToString(pipelineJson) {
24566
- let pipelineString = createPipelineIntroduction(pipelineJson);
24567
- const pipelineCommands = createPipelineCommands(pipelineJson);
24568
- pipelineString = appendMarkdownBlock(pipelineString, stringifyCommands(pipelineCommands));
24569
- for (const task of pipelineJson.tasks) {
24570
- pipelineString = appendMarkdownBlock(pipelineString, stringifyTask(task));
24571
- }
24572
- return validatePipelineString(pipelineString);
24573
- }
24574
- // TODO: [🛋] Implement new features and commands into `pipelineJsonToString` + `taskParameterJsonToString` , use `stringifyCommand`
24575
- // TODO: [🧠] Is there a way to auto-detect missing features in pipelineJsonToString
24576
- // TODO: [🏛] Maybe make some markdown builder
24577
- // TODO: [🏛] Escape all
24578
- // TODO: [🧠] Should be in generated .book.md file GENERATOR_WARNING
24579
24790
 
24580
- /**
24581
- * This error indicates that the promptbook object has valid syntax (=can be parsed) but contains logical errors (like circular dependencies)
24582
- *
24583
- * @public exported from `@promptbook/core`
24584
- */
24585
- class PipelineLogicError extends Error {
24586
- constructor(message) {
24587
- super(message);
24588
- this.name = 'PipelineLogicError';
24589
- Object.setPrototypeOf(this, PipelineLogicError.prototype);
24590
- }
24791
+ **Reserved (which are available):**
24792
+ ${block(resolvedParameterNames
24793
+ .filter((name) => RESERVED_PARAMETER_NAMES.includes(name))
24794
+ .map((name) => `- Parameter \`{${name}}\``)
24795
+ .join('\n'))}
24796
+
24797
+
24798
+ `));
24591
24799
  }
24592
24800
 
24593
24801
  /**
@@ -24650,182 +24858,33 @@
24650
24858
  if (!url.startsWith('https://') && !url.startsWith('http://') /* <- Note: [👣] */) {
24651
24859
  return false;
24652
24860
  }
24653
- if (url.includes('#')) {
24654
- // TODO: [🐠]
24655
- return false;
24656
- }
24657
- /*
24658
- Note: [👣][🧠] Is it secure to allow pipeline URLs on private and unsecured networks?
24659
- if (isUrlOnPrivateNetwork(url)) {
24660
- return false;
24661
- }
24662
- */
24663
- return true;
24664
- }
24665
- // TODO: [🐠] Maybe more info why the URL is invalid
24666
-
24667
- /**
24668
- * Validates PipelineJson if it is logically valid
24669
- *
24670
- * It checks:
24671
- * - if it has correct parameters dependency
24672
- *
24673
- * It does NOT check:
24674
- * - if it is valid json
24675
- * - if it is meaningful
24676
- *
24677
- * Note: [🔂] This function is idempotent.
24678
- *
24679
- * @param pipeline valid or invalid PipelineJson
24680
- * @returns the same pipeline if it is logically valid
24681
- * @throws {PipelineLogicError} on logical error in the pipeline
24682
- *
24683
- * @public exported from `@promptbook/core`
24684
- */
24685
- function validatePipeline(pipeline) {
24686
- if (IS_PIPELINE_LOGIC_VALIDATED) {
24687
- validatePipeline_InnerFunction(pipeline);
24688
- }
24689
- else {
24690
- try {
24691
- validatePipeline_InnerFunction(pipeline);
24692
- }
24693
- catch (error) {
24694
- if (!(error instanceof PipelineLogicError)) {
24695
- throw error;
24696
- }
24697
- console.error(spacetrim.spaceTrim((block) => `
24698
- Pipeline is not valid but logic errors are temporarily disabled via \`IS_PIPELINE_LOGIC_VALIDATED\`
24699
-
24700
- ${block(error.message)}
24701
- `));
24702
- }
24703
- }
24704
- return pipeline;
24705
- }
24706
- /**
24707
- * Validates pipeline inner function.
24708
- *
24709
- * @private internal function for `validatePipeline`
24710
- */
24711
- function validatePipeline_InnerFunction(pipeline) {
24712
- // TODO: [🧠] Maybe test if promptbook is a promise and make specific error case for that
24713
- const context = createPipelineValidationContext(pipeline);
24714
- validatePipelineMetadata(context);
24715
- validatePipelineCollectionsStructure(context);
24716
- validatePipelineParameters(context);
24717
- validatePipelineTasks(context);
24718
- validatePipelineDependencyResolution(context);
24719
- // Note: Check that formfactor is corresponding to the pipeline interface
24720
- // TODO: !!6 Implement this
24721
- // pipeline.formfactorName
24722
- }
24723
- /**
24724
- * Creates the shared validation context for one pipeline.
24725
- *
24726
- * @private internal utility of `validatePipeline`
24727
- */
24728
- function createPipelineValidationContext(pipeline) {
24729
- return {
24730
- pipeline,
24731
- pipelineIdentification: getPipelineIdentification(pipeline),
24732
- };
24733
- }
24734
- /**
24735
- * Builds a short file/url identification block for validation errors.
24736
- *
24737
- * @private internal utility of `validatePipeline`
24738
- */
24739
- function getPipelineIdentification(pipeline) {
24740
- // Note: This is a 😐 implementation of [🚞]
24741
- const pipelineIdentificationParts = [];
24742
- if (pipeline.sourceFile !== undefined) {
24743
- pipelineIdentificationParts.push(`File: ${pipeline.sourceFile}`);
24744
- }
24745
- if (pipeline.pipelineUrl !== undefined) {
24746
- pipelineIdentificationParts.push(`Url: ${pipeline.pipelineUrl}`);
24747
- }
24748
- return pipelineIdentificationParts.join('\n');
24749
- }
24750
- /**
24751
- * Validates pipeline-level metadata fields.
24752
- *
24753
- * @private internal step of `validatePipeline`
24754
- */
24755
- function validatePipelineMetadata({ pipeline, pipelineIdentification }) {
24756
- validatePipelineUrl(pipeline, pipelineIdentification);
24757
- validatePipelineBookVersion(pipeline, pipelineIdentification);
24758
- }
24759
- /**
24760
- * Validates that the expected top-level collections have array structure.
24761
- *
24762
- * @private internal step of `validatePipeline`
24763
- */
24764
- function validatePipelineCollectionsStructure({ pipeline, pipelineIdentification }) {
24765
- validatePipelineParametersCollection(pipeline, pipelineIdentification);
24766
- validatePipelineTasksCollection(pipeline, pipelineIdentification);
24767
- }
24768
- /**
24769
- * Validates all pipeline parameter declarations.
24770
- *
24771
- * @private internal step of `validatePipeline`
24772
- */
24773
- function validatePipelineParameters({ pipeline, pipelineIdentification }) {
24774
- for (const parameter of pipeline.parameters) {
24775
- validatePipelineParameter(parameter, pipeline, pipelineIdentification);
24776
- }
24777
- }
24778
- /**
24779
- * Validates all pipeline tasks and their per-task invariants.
24780
- *
24781
- * @private internal step of `validatePipeline`
24782
- */
24783
- function validatePipelineTasks({ pipeline, pipelineIdentification }) {
24784
- // Note: All input parameters are defined - so that they can be used as result of some task
24785
- const definedParameters = createInitiallyDefinedParameters(pipeline);
24786
- for (const task of pipeline.tasks) {
24787
- validatePipelineTask(task, definedParameters, pipelineIdentification);
24788
- }
24789
- }
24790
- /**
24791
- * Validates that task dependencies can be resolved without cycles or missing definitions.
24792
- *
24793
- * @private internal step of `validatePipeline`
24794
- */
24795
- function validatePipelineDependencyResolution({ pipeline, pipelineIdentification }) {
24796
- let dependencyResolutionState = createInitialDependencyResolutionState(pipeline);
24797
- let loopLimit = LOOP_LIMIT;
24798
- while (hasUnresolvedTasks(dependencyResolutionState)) {
24799
- if (loopLimit-- < 0) {
24800
- throw createDependencyResolutionLoopLimitError(pipelineIdentification);
24801
- }
24802
- dependencyResolutionState = resolveNextDependencyResolutionState(dependencyResolutionState, pipelineIdentification);
24861
+ if (url.includes('#')) {
24862
+ // TODO: [🐠]
24863
+ return false;
24803
24864
  }
24865
+ /*
24866
+ Note: [👣][🧠] Is it secure to allow pipeline URLs on private and unsecured networks?
24867
+ if (isUrlOnPrivateNetwork(url)) {
24868
+ return false;
24869
+ }
24870
+ */
24871
+ return true;
24804
24872
  }
24873
+ // TODO: [🐠] Maybe more info why the URL is invalid
24874
+
24805
24875
  /**
24806
- * Validates one pipeline parameter declaration.
24807
- *
24808
- * @private internal step of `validatePipeline`
24809
- */
24810
- function validatePipelineParameter(parameter, pipeline, pipelineIdentification) {
24811
- validateParameterDirection(parameter, pipelineIdentification);
24812
- validateParameterUsage(parameter, pipeline, pipelineIdentification);
24813
- validateParameterDefinition(parameter, pipeline, pipelineIdentification);
24814
- }
24815
- /**
24816
- * Validates one pipeline task and its invariants.
24876
+ * Validates pipeline-level metadata fields.
24817
24877
  *
24818
- * @private internal step of `validatePipeline`
24878
+ * @private function of `validatePipeline`
24819
24879
  */
24820
- function validatePipelineTask(task, definedParameters, pipelineIdentification) {
24821
- validateTaskResultingParameter(task, definedParameters, pipelineIdentification);
24822
- validateTaskJokers(task, pipelineIdentification);
24823
- validateTaskExpectations(task, pipelineIdentification);
24880
+ function validatePipelineMetadata({ pipeline, pipelineIdentification }) {
24881
+ validatePipelineUrl(pipeline, pipelineIdentification);
24882
+ validatePipelineBookVersion(pipeline, pipelineIdentification);
24824
24883
  }
24825
24884
  /**
24826
24885
  * Validates the pipeline URL, when present.
24827
24886
  *
24828
- * @private internal utility of `validatePipeline`
24887
+ * @private internal utility of `validatePipelineMetadata`
24829
24888
  */
24830
24889
  function validatePipelineUrl(pipeline, pipelineIdentification) {
24831
24890
  if (pipeline.pipelineUrl === undefined || isValidPipelineUrl(pipeline.pipelineUrl)) {
@@ -24841,7 +24900,7 @@
24841
24900
  /**
24842
24901
  * Validates the Promptbook version, when present.
24843
24902
  *
24844
- * @private internal utility of `validatePipeline`
24903
+ * @private internal utility of `validatePipelineMetadata`
24845
24904
  */
24846
24905
  function validatePipelineBookVersion(pipeline, pipelineIdentification) {
24847
24906
  if (pipeline.bookVersion === undefined || isValidPromptbookVersion(pipeline.bookVersion)) {
@@ -24854,48 +24913,31 @@
24854
24913
  ${block(pipelineIdentification)}
24855
24914
  `));
24856
24915
  }
24916
+
24857
24917
  /**
24858
- * Validates that `pipeline.parameters` is an array.
24918
+ * Validates all pipeline parameter declarations.
24859
24919
  *
24860
- * @private internal utility of `validatePipeline`
24920
+ * @private function of `validatePipeline`
24861
24921
  */
24862
- function validatePipelineParametersCollection(pipeline, pipelineIdentification) {
24863
- // TODO: [🧠] Maybe do here some proper JSON-schema / ZOD checking
24864
- if (Array.isArray(pipeline.parameters)) {
24865
- return;
24922
+ function validatePipelineParameters({ pipeline, pipelineIdentification }) {
24923
+ for (const parameter of pipeline.parameters) {
24924
+ validatePipelineParameter(parameter, pipeline, pipelineIdentification);
24866
24925
  }
24867
- // TODO: [🧠] what is the correct error tp throw - maybe PromptbookSchemaError
24868
- throw new ParseError(spacetrim.spaceTrim((block) => `
24869
- Pipeline is valid JSON but with wrong structure
24870
-
24871
- \`PipelineJson.parameters\` expected to be an array, but got ${typeof pipeline.parameters}
24872
-
24873
- ${block(pipelineIdentification)}
24874
- `));
24875
24926
  }
24876
24927
  /**
24877
- * Validates that `pipeline.tasks` is an array.
24928
+ * Validates one pipeline parameter declaration.
24878
24929
  *
24879
- * @private internal utility of `validatePipeline`
24930
+ * @private internal utility of `validatePipelineParameters`
24880
24931
  */
24881
- function validatePipelineTasksCollection(pipeline, pipelineIdentification) {
24882
- // TODO: [🧠] Maybe do here some proper JSON-schema / ZOD checking
24883
- if (Array.isArray(pipeline.tasks)) {
24884
- return;
24885
- }
24886
- // TODO: [🧠] what is the correct error tp throw - maybe PromptbookSchemaError
24887
- throw new ParseError(spacetrim.spaceTrim((block) => `
24888
- Pipeline is valid JSON but with wrong structure
24889
-
24890
- \`PipelineJson.tasks\` expected to be an array, but got ${typeof pipeline.tasks}
24891
-
24892
- ${block(pipelineIdentification)}
24893
- `));
24932
+ function validatePipelineParameter(parameter, pipeline, pipelineIdentification) {
24933
+ validateParameterDirection(parameter, pipelineIdentification);
24934
+ validateParameterUsage(parameter, pipeline, pipelineIdentification);
24935
+ validateParameterDefinition(parameter, pipeline, pipelineIdentification);
24894
24936
  }
24895
24937
  /**
24896
24938
  * Validates that one parameter does not declare incompatible directions.
24897
24939
  *
24898
- * @private internal utility of `validatePipeline`
24940
+ * @private internal utility of `validatePipelineParameters`
24899
24941
  */
24900
24942
  function validateParameterDirection(parameter, pipelineIdentification) {
24901
24943
  if (!parameter.isInput || !parameter.isOutput) {
@@ -24912,7 +24954,7 @@
24912
24954
  /**
24913
24955
  * Validates that one intermediate parameter is actually consumed by at least one task.
24914
24956
  *
24915
- * @private internal utility of `validatePipeline`
24957
+ * @private internal utility of `validatePipelineParameters`
24916
24958
  */
24917
24959
  function validateParameterUsage(parameter, pipeline, pipelineIdentification) {
24918
24960
  if (parameter.isInput || parameter.isOutput || isParameterUsedByAnyTask(parameter, pipeline.tasks)) {
@@ -24931,7 +24973,7 @@
24931
24973
  /**
24932
24974
  * Validates that one non-input parameter is produced by at least one task.
24933
24975
  *
24934
- * @private internal utility of `validatePipeline`
24976
+ * @private internal utility of `validatePipelineParameters`
24935
24977
  */
24936
24978
  function validateParameterDefinition(parameter, pipeline, pipelineIdentification) {
24937
24979
  if (parameter.isInput || isParameterDefinedByAnyTask(parameter, pipeline.tasks)) {
@@ -24950,7 +24992,7 @@
24950
24992
  /**
24951
24993
  * Checks whether one parameter is consumed by at least one task dependency list.
24952
24994
  *
24953
- * @private internal utility of `validatePipeline`
24995
+ * @private internal utility of `validatePipelineParameters`
24954
24996
  */
24955
24997
  function isParameterUsedByAnyTask(parameter, tasks) {
24956
24998
  return tasks.some((task) => task.dependentParameterNames.includes(parameter.name));
@@ -24958,23 +25000,46 @@
24958
25000
  /**
24959
25001
  * Checks whether one parameter is produced by at least one task.
24960
25002
  *
24961
- * @private internal utility of `validatePipeline`
25003
+ * @private internal utility of `validatePipelineParameters`
24962
25004
  */
24963
25005
  function isParameterDefinedByAnyTask(parameter, tasks) {
24964
25006
  return tasks.some((task) => task.resultingParameterName === parameter.name);
24965
25007
  }
25008
+
25009
+ /**
25010
+ * Validates all pipeline tasks and their per-task invariants.
25011
+ *
25012
+ * @private function of `validatePipeline`
25013
+ */
25014
+ function validatePipelineTasks({ pipeline, pipelineIdentification }) {
25015
+ // Note: All input parameters are defined - so that they can be used as result of some task
25016
+ const definedParameters = createInitiallyDefinedParameters(pipeline);
25017
+ for (const task of pipeline.tasks) {
25018
+ validatePipelineTask(task, definedParameters, pipelineIdentification);
25019
+ }
25020
+ }
24966
25021
  /**
24967
25022
  * Collects the parameter names that are already defined before task validation starts.
24968
25023
  *
24969
- * @private internal utility of `validatePipeline`
25024
+ * @private internal utility of `validatePipelineTasks`
24970
25025
  */
24971
25026
  function createInitiallyDefinedParameters(pipeline) {
24972
25027
  return new Set(pipeline.parameters.filter(({ isInput }) => isInput).map(({ name }) => name));
24973
25028
  }
25029
+ /**
25030
+ * Validates one pipeline task and its invariants.
25031
+ *
25032
+ * @private internal utility of `validatePipelineTasks`
25033
+ */
25034
+ function validatePipelineTask(task, definedParameters, pipelineIdentification) {
25035
+ validateTaskResultingParameter(task, definedParameters, pipelineIdentification);
25036
+ validateTaskJokers(task, pipelineIdentification);
25037
+ validateTaskExpectations(task, pipelineIdentification);
25038
+ }
24974
25039
  /**
24975
25040
  * Validates one task result parameter declaration and marks it as defined.
24976
25041
  *
24977
- * @private internal utility of `validatePipeline`
25042
+ * @private internal utility of `validatePipelineTasks`
24978
25043
  */
24979
25044
  function validateTaskResultingParameter(task, definedParameters, pipelineIdentification) {
24980
25045
  if (definedParameters.has(task.resultingParameterName)) {
@@ -24996,7 +25061,7 @@
24996
25061
  /**
24997
25062
  * Validates joker parameters for one task.
24998
25063
  *
24999
- * @private internal utility of `validatePipeline`
25064
+ * @private internal utility of `validatePipelineTasks`
25000
25065
  */
25001
25066
  function validateTaskJokers(task, pipelineIdentification) {
25002
25067
  if (!hasTaskJokers(task)) {
@@ -25008,7 +25073,7 @@
25008
25073
  /**
25009
25074
  * Checks whether one task declares any joker parameters.
25010
25075
  *
25011
- * @private internal utility of `validatePipeline`
25076
+ * @private internal utility of `validatePipelineTasks`
25012
25077
  */
25013
25078
  function hasTaskJokers(task) {
25014
25079
  return !!task.jokerParameterNames && task.jokerParameterNames.length > 0;
@@ -25016,7 +25081,7 @@
25016
25081
  /**
25017
25082
  * Validates that a task has the required supporting features when using jokers.
25018
25083
  *
25019
- * @private internal utility of `validatePipeline`
25084
+ * @private internal utility of `validatePipelineTasks`
25020
25085
  */
25021
25086
  function validateTaskSupportsJokers(task, pipelineIdentification) {
25022
25087
  if (task.format || task.expectations /* <- TODO: Require at least 1 -> min <- expectation to use jokers */) {
@@ -25031,7 +25096,7 @@
25031
25096
  /**
25032
25097
  * Validates that every joker parameter is also listed among task dependencies.
25033
25098
  *
25034
- * @private internal utility of `validatePipeline`
25099
+ * @private internal utility of `validatePipelineTasks`
25035
25100
  */
25036
25101
  function validateTaskJokerDependencies(task, pipelineIdentification) {
25037
25102
  for (const joker of task.jokerParameterNames) {
@@ -25048,7 +25113,7 @@
25048
25113
  /**
25049
25114
  * Validates all expectation bounds configured on one task.
25050
25115
  *
25051
- * @private internal utility of `validatePipeline`
25116
+ * @private internal utility of `validatePipelineTasks`
25052
25117
  */
25053
25118
  function validateTaskExpectations(task, pipelineIdentification) {
25054
25119
  if (!task.expectations) {
@@ -25063,7 +25128,7 @@
25063
25128
  /**
25064
25129
  * Validates the minimum and maximum expectation ordering for one unit.
25065
25130
  *
25066
- * @private internal utility of `validatePipeline`
25131
+ * @private internal utility of `validatePipelineTasks`
25067
25132
  */
25068
25133
  function validateTaskExpectationRange(unit, min, max, pipelineIdentification) {
25069
25134
  if (min === undefined || max === undefined || min <= max) {
@@ -25078,7 +25143,7 @@
25078
25143
  /**
25079
25144
  * Validates the minimum expectation bound for one unit.
25080
25145
  *
25081
- * @private internal utility of `validatePipeline`
25146
+ * @private internal utility of `validatePipelineTasks`
25082
25147
  */
25083
25148
  function validateTaskExpectationMin(unit, min, pipelineIdentification) {
25084
25149
  if (min === undefined || min >= 0) {
@@ -25093,7 +25158,7 @@
25093
25158
  /**
25094
25159
  * Validates the maximum expectation bound for one unit.
25095
25160
  *
25096
- * @private internal utility of `validatePipeline`
25161
+ * @private internal utility of `validatePipelineTasks`
25097
25162
  */
25098
25163
  function validateTaskExpectationMax(unit, max, pipelineIdentification) {
25099
25164
  if (max === undefined || max > 0) {
@@ -25105,123 +25170,62 @@
25105
25170
  ${block(pipelineIdentification)}
25106
25171
  `));
25107
25172
  }
25173
+
25108
25174
  /**
25109
- * Collects the parameter names that are already resolvable before dependency traversal starts.
25110
- *
25111
- * @private internal utility of `validatePipeline`
25112
- */
25113
- function createInitialDependencyResolutionState(pipeline) {
25114
- return {
25115
- resolvedParameterNames: createInitiallyResolvedParameterNames(pipeline),
25116
- unresolvedTasks: [...pipeline.tasks],
25117
- };
25118
- }
25119
- /**
25120
- * Checks whether dependency resolution still has tasks left to process.
25121
- *
25122
- * @private internal utility of `validatePipeline`
25123
- */
25124
- function hasUnresolvedTasks({ unresolvedTasks }) {
25125
- return unresolvedTasks.length > 0;
25126
- }
25127
- /**
25128
- * Resolves the next batch of currently satisfiable tasks.
25175
+ * Validates PipelineJson if it is logically valid
25129
25176
  *
25130
- * @private internal utility of `validatePipeline`
25131
- */
25132
- function resolveNextDependencyResolutionState(dependencyResolutionState, pipelineIdentification) {
25133
- const currentlyResolvedTasks = getCurrentlyResolvedTasks(dependencyResolutionState.unresolvedTasks, dependencyResolutionState.resolvedParameterNames);
25134
- if (currentlyResolvedTasks.length === 0) {
25135
- throw createUnresolvedTasksError(dependencyResolutionState.unresolvedTasks, dependencyResolutionState.resolvedParameterNames, pipelineIdentification);
25136
- }
25137
- return {
25138
- resolvedParameterNames: appendResolvedTaskParameterNames(dependencyResolutionState.resolvedParameterNames, currentlyResolvedTasks),
25139
- unresolvedTasks: dependencyResolutionState.unresolvedTasks.filter((task) => !currentlyResolvedTasks.includes(task)),
25140
- };
25141
- }
25142
- /**
25143
- * Collects the parameter names that are already resolvable before dependency traversal starts.
25177
+ * It checks:
25178
+ * - if it has correct parameters dependency
25144
25179
  *
25145
- * @private internal utility of `validatePipeline`
25146
- */
25147
- function createInitiallyResolvedParameterNames(pipeline) {
25148
- let resolvedParameterNames = pipeline.parameters
25149
- .filter(({ isInput }) => isInput)
25150
- .map(({ name }) => name);
25151
- for (const reservedParameterName of RESERVED_PARAMETER_NAMES) {
25152
- resolvedParameterNames = [...resolvedParameterNames, reservedParameterName];
25153
- }
25154
- return resolvedParameterNames;
25155
- }
25156
- /**
25157
- * Adds newly resolved task outputs to the resolved parameter list.
25180
+ * It does NOT check:
25181
+ * - if it is valid json
25182
+ * - if it is meaningful
25158
25183
  *
25159
- * @private internal utility of `validatePipeline`
25160
- */
25161
- function appendResolvedTaskParameterNames(resolvedParameterNames, currentlyResolvedTasks) {
25162
- return [
25163
- ...resolvedParameterNames,
25164
- ...currentlyResolvedTasks.map(({ resultingParameterName }) => resultingParameterName),
25165
- ];
25166
- }
25167
- /**
25168
- * Selects tasks whose dependencies are already resolved.
25184
+ * Note: [🔂] This function is idempotent.
25169
25185
  *
25170
- * @private internal utility of `validatePipeline`
25171
- */
25172
- function getCurrentlyResolvedTasks(unresolvedTasks, resolvedParameterNames) {
25173
- return unresolvedTasks.filter((task) => task.dependentParameterNames.every((name) => resolvedParameterNames.includes(name)));
25174
- }
25175
- /**
25176
- * Creates the unexpected loop-limit error for dependency resolution.
25186
+ * @param pipeline valid or invalid PipelineJson
25187
+ * @returns the same pipeline if it is logically valid
25188
+ * @throws {PipelineLogicError} on logical error in the pipeline
25177
25189
  *
25178
- * @private internal utility of `validatePipeline`
25190
+ * @public exported from `@promptbook/core`
25179
25191
  */
25180
- function createDependencyResolutionLoopLimitError(pipelineIdentification) {
25181
- // Note: Really UnexpectedError not LimitReachedError - this should not happen and be caught below
25182
- return new UnexpectedError(spacetrim.spaceTrim((block) => `
25183
- Loop limit reached during detection of circular dependencies in \`validatePipeline\`
25192
+ function validatePipeline(pipeline) {
25193
+ if (IS_PIPELINE_LOGIC_VALIDATED) {
25194
+ validatePipeline_InnerFunction(pipeline);
25195
+ }
25196
+ else {
25197
+ try {
25198
+ validatePipeline_InnerFunction(pipeline);
25199
+ }
25200
+ catch (error) {
25201
+ if (!(error instanceof PipelineLogicError)) {
25202
+ throw error;
25203
+ }
25204
+ console.error(spacetrim.spaceTrim((block) => `
25205
+ Pipeline is not valid but logic errors are temporarily disabled via \`IS_PIPELINE_LOGIC_VALIDATED\`
25184
25206
 
25185
- ${block(pipelineIdentification)}
25186
- `));
25207
+ ${block(error.message)}
25208
+ `));
25209
+ }
25210
+ }
25211
+ return pipeline;
25187
25212
  }
25188
25213
  /**
25189
- * Creates the detailed error for unresolved or circular task dependencies.
25214
+ * Validates pipeline inner function.
25190
25215
  *
25191
- * @private internal utility of `validatePipeline`
25216
+ * @private internal function for `validatePipeline`
25192
25217
  */
25193
- function createUnresolvedTasksError(unresolvedTasks, resolvedParameterNames, pipelineIdentification) {
25194
- return new PipelineLogicError(
25195
- // TODO: [🐎] DRY
25196
- spacetrim.spaceTrim((block) => `
25197
-
25198
- Can not resolve some parameters:
25199
- Either you are using a parameter that is not defined, or there are some circular dependencies.
25200
-
25201
- ${block(pipelineIdentification)}
25202
-
25203
- **Can not resolve:**
25204
- ${block(unresolvedTasks
25205
- .map(({ resultingParameterName, dependentParameterNames }) => `- Parameter \`{${resultingParameterName}}\` which depends on ${dependentParameterNames
25206
- .map((dependentParameterName) => `\`{${dependentParameterName}}\``)
25207
- .join(' and ')}`)
25208
- .join('\n'))}
25209
-
25210
- **Resolved:**
25211
- ${block(resolvedParameterNames
25212
- .filter((name) => !RESERVED_PARAMETER_NAMES.includes(name))
25213
- .map((name) => `- Parameter \`{${name}}\``)
25214
- .join('\n'))}
25215
-
25216
-
25217
- **Reserved (which are available):**
25218
- ${block(resolvedParameterNames
25219
- .filter((name) => RESERVED_PARAMETER_NAMES.includes(name))
25220
- .map((name) => `- Parameter \`{${name}}\``)
25221
- .join('\n'))}
25222
-
25223
-
25224
- `));
25218
+ function validatePipeline_InnerFunction(pipeline) {
25219
+ // TODO: [🧠] Maybe test if promptbook is a promise and make specific error case for that
25220
+ const context = createPipelineValidationContext(pipeline);
25221
+ validatePipelineMetadata(context);
25222
+ validatePipelineCollectionsStructure(context);
25223
+ validatePipelineParameters(context);
25224
+ validatePipelineTasks(context);
25225
+ validatePipelineDependencyResolution(context);
25226
+ // Note: Check that formfactor is corresponding to the pipeline interface
25227
+ // TODO: !!6 Implement this
25228
+ // pipeline.formfactorName
25225
25229
  }
25226
25230
  /**
25227
25231
  * TODO: [🧞‍♀️] Do not allow joker + foreach