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