synabun 2.0.10 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +207 -0
- package/README.md +25 -2
- package/THIRD-PARTY-LICENSES.md +22 -0
- package/bin/synabun.js +71 -24
- package/docs/whatsapp.md +13 -13
- package/hooks/claude-code/pre-websearch.mjs +12 -1
- package/hooks/claude-code/shared.mjs +5 -0
- package/hooks/claude-code/stop.mjs +1 -1
- package/lib/claude-hooks.js +172 -18
- package/lib/client-config-repair.js +30 -17
- package/lib/desktop-app.js +7 -1
- package/lib/external-tools.js +7 -6
- package/lib/first-launch-protection.js +57 -12
- package/lib/mcp-dist.js +176 -0
- package/lib/release-npm-ci.mjs +138 -0
- package/lib/release-publish.mjs +91 -0
- package/lib/release-scan.mjs +50 -0
- package/lib/server-crash-record.js +123 -0
- package/lib/server-exit.js +318 -0
- package/lib/server-supervisor.js +508 -0
- package/lib/start-launcher.js +37 -4
- package/lib/tools-cli.js +45 -0
- package/lib/update-snapshot.js +302 -0
- package/lib/virustotal.mjs +203 -0
- package/lib/windows-crash-event.js +269 -0
- package/mcp-server/dist/http.d.ts +20 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +59 -12
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts +33 -27
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +128 -143
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/preload.js +1 -1
- package/mcp-server/dist/preload.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
- package/mcp-server/dist/services/budget-judgments.js +5 -3
- package/mcp-server/dist/services/budget-judgments.js.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +81 -28
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +2 -2
- package/mcp-server/dist/services/identity.js +2 -2
- package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
- package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
- package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-sessions.js +28 -2
- package/mcp-server/dist/services/memory-sessions.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +18 -11
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +67 -11
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/server-instructions.d.ts +7 -2
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
- package/mcp-server/dist/services/server-instructions.js +22 -63
- package/mcp-server/dist/services/server-instructions.js.map +1 -1
- package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
- package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-catalog.js +339 -0
- package/mcp-server/dist/services/tool-catalog.js.map +1 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
- package/mcp-server/dist/services/tool-list.d.ts +108 -0
- package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-list.js +334 -0
- package/mcp-server/dist/services/tool-list.js.map +1 -0
- package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
- package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-runtime.js +400 -0
- package/mcp-server/dist/services/tool-runtime.js.map +1 -0
- package/mcp-server/dist/services/typesafe-config.js +1 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
- package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
- package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
- package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +14 -14
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +29 -19
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
- package/mcp-server/dist/tools/choice.d.ts +6 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +18 -13
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +2 -2
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/image-tools.d.ts +1 -1
- package/mcp-server/dist/tools/image-tools.js +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +9 -6
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
- package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/skill-tools.js +642 -0
- package/mcp-server/dist/tools/skill-tools.js.map +1 -0
- package/mcp-server/dist/tools/skill.d.ts +7 -0
- package/mcp-server/dist/tools/skill.d.ts.map +1 -0
- package/mcp-server/dist/tools/skill.js +11 -0
- package/mcp-server/dist/tools/skill.js.map +1 -0
- package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
- package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
- package/mcp-server/dist/tools/tool-groups.js +147 -0
- package/mcp-server/dist/tools/tool-groups.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-format.js +7 -4
- package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/http.ts +75 -12
- package/mcp-server/src/index.ts +147 -154
- package/mcp-server/src/preload.ts +12 -12
- package/mcp-server/src/services/budget-judgments.ts +5 -3
- package/mcp-server/src/services/codex-config-heal.ts +80 -29
- package/mcp-server/src/services/identity.ts +2 -2
- package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
- package/mcp-server/src/services/memory-sessions.ts +24 -3
- package/mcp-server/src/services/neural-interface.ts +81 -21
- package/mcp-server/src/services/server-instructions.ts +29 -64
- package/mcp-server/src/services/tool-catalog.ts +403 -0
- package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
- package/mcp-server/src/services/tool-list.ts +410 -0
- package/mcp-server/src/services/tool-runtime.ts +457 -0
- package/mcp-server/src/services/typesafe-config.ts +1 -1
- package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
- package/mcp-server/src/tools/agents.ts +29 -20
- package/mcp-server/src/tools/choice.ts +17 -12
- package/mcp-server/src/tools/git-tools.ts +2 -2
- package/mcp-server/src/tools/image-tools.ts +1 -1
- package/mcp-server/src/tools/morelogin.ts +6 -4
- package/mcp-server/src/tools/skill-tools.ts +592 -0
- package/mcp-server/src/tools/skill.ts +13 -0
- package/mcp-server/src/tools/tool-groups.ts +164 -0
- package/mcp-server/src/tools/whiteboard-format.ts +9 -3
- package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
- package/neural-interface/i18n/en.json +3953 -228
- package/neural-interface/i18n/pt-BR.json +3901 -179
- package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
- package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
- package/neural-interface/lib/antigravity/acp-connection.js +299 -0
- package/neural-interface/lib/antigravity/bypass.js +100 -0
- package/neural-interface/lib/antigravity/catalog.js +273 -0
- package/neural-interface/lib/antigravity/home.js +260 -0
- package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
- package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
- package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
- package/neural-interface/lib/antigravity/policy.js +74 -0
- package/neural-interface/lib/antigravity/tool-call.js +178 -0
- package/neural-interface/lib/antigravity/transcripts.js +252 -0
- package/neural-interface/lib/antigravity/usage.js +43 -0
- package/neural-interface/lib/assistant-api.js +128 -28
- package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
- package/neural-interface/lib/assistant-brains/claude.js +4 -3
- package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
- package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
- package/neural-interface/lib/assistant-brains/codex.js +9 -5
- package/neural-interface/lib/assistant-brains/opencode.js +120 -54
- package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
- package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
- package/neural-interface/lib/assistant-budget-planner.js +7 -3
- package/neural-interface/lib/assistant-budget-split.js +42 -15
- package/neural-interface/lib/assistant-catalog.js +81 -15
- package/neural-interface/lib/assistant-config.js +6 -3
- package/neural-interface/lib/assistant-dispatch.js +1073 -200
- package/neural-interface/lib/assistant-envelope.js +156 -2
- package/neural-interface/lib/assistant-evidence.js +80 -0
- package/neural-interface/lib/assistant-hidden-models.js +7 -5
- package/neural-interface/lib/assistant-persona.js +93 -19
- package/neural-interface/lib/assistant-plan-permissions.js +25 -9
- package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
- package/neural-interface/lib/assistant-route-gate.js +6 -4
- package/neural-interface/lib/assistant-router.js +245 -38
- package/neural-interface/lib/assistant-runtime.js +214 -30
- package/neural-interface/lib/assistant-task-prompt.js +87 -21
- package/neural-interface/lib/assistant-tools.js +51 -0
- package/neural-interface/lib/assistant-usage.js +29 -0
- package/neural-interface/lib/backup-service.js +626 -4
- package/neural-interface/lib/backup-zip-reader.js +10 -5
- package/neural-interface/lib/backup-zip-writer.js +257 -0
- package/neural-interface/lib/browser-execution.js +10 -2
- package/neural-interface/lib/browser-guard-cdp.js +84 -0
- package/neural-interface/lib/browser-network-guard.js +476 -0
- package/neural-interface/lib/browser-policy-roots.js +64 -0
- package/neural-interface/lib/browser-tool-policy.js +198 -38
- package/neural-interface/lib/claude-agent-bridge.js +11 -2
- package/neural-interface/lib/claude-history-cache.js +210 -0
- package/neural-interface/lib/claude-hooks-status.js +103 -0
- package/neural-interface/lib/claude-model-catalog.js +5 -1
- package/neural-interface/lib/claude-temporary.js +5 -11
- package/neural-interface/lib/cli-install-api.js +1 -1
- package/neural-interface/lib/cli-install.js +163 -154
- package/neural-interface/lib/cli-resolver.js +13 -3
- package/neural-interface/lib/cli-update-planner.js +52 -41
- package/neural-interface/lib/cloudflared/tunnel.js +42 -2
- package/neural-interface/lib/codex-mcp-approvals.js +11 -0
- package/neural-interface/lib/codex-rollout-reader.js +52 -0
- package/neural-interface/lib/codex-worker-app-server.js +191 -0
- package/neural-interface/lib/confined-fs.js +30 -3
- package/neural-interface/lib/crash-notice-api.js +98 -0
- package/neural-interface/lib/editor-completion-opencode.js +68 -68
- package/neural-interface/lib/editor-git-api.js +3 -20
- package/neural-interface/lib/exec-loop-command.js +13 -10
- package/neural-interface/lib/file-explorer-api.js +74 -40
- package/neural-interface/lib/file-explorer-watch.js +9 -3
- package/neural-interface/lib/git-bash/install.js +52 -6
- package/neural-interface/lib/git-bash/manager.js +59 -17
- package/neural-interface/lib/git-bash/pins.js +12 -0
- package/neural-interface/lib/git-read.js +2 -2
- package/neural-interface/lib/git-status-runner.js +390 -0
- package/neural-interface/lib/git-status-watchdog.js +40 -0
- package/neural-interface/lib/greeting/budgets.js +1 -0
- package/neural-interface/lib/greeting/engine.js +2 -2
- package/neural-interface/lib/greeting/sources.js +10 -12
- package/neural-interface/lib/greeting/writer.js +1 -1
- package/neural-interface/lib/http-guards.js +8 -2
- package/neural-interface/lib/install-details.js +4 -1
- package/neural-interface/lib/mcp-agent-servers.js +67 -0
- package/neural-interface/lib/mcp-client-config.js +35 -33
- package/neural-interface/lib/mcp-external-servers.js +332 -0
- package/neural-interface/lib/mcp-installer.js +2 -1
- package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
- package/neural-interface/lib/memory-session-capture.js +17 -3
- package/neural-interface/lib/morelogin.js +177 -25
- package/neural-interface/lib/native-loop-providers.js +965 -66
- package/neural-interface/lib/native-loop-runtime.js +290 -42
- package/neural-interface/lib/opencode-config.js +557 -0
- package/neural-interface/lib/opencode-db-schema.js +17 -34
- package/neural-interface/lib/opencode-history.js +48 -218
- package/neural-interface/lib/opencode-mcp-ready.js +108 -0
- package/neural-interface/lib/opencode-native-boundary.js +53 -0
- package/neural-interface/lib/opencode-protocol.js +249 -112
- package/neural-interface/lib/opencode-runtime-config.js +32 -22
- package/neural-interface/lib/opencode-serve-records.js +39 -21
- package/neural-interface/lib/opencode-v2-client.js +48 -153
- package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
- package/neural-interface/lib/opencode-worker-data.js +27 -0
- package/neural-interface/lib/opencode2-client.js +73 -37
- package/neural-interface/lib/opencode2-proxy.js +23 -63
- package/neural-interface/lib/opencode2-translate.js +51 -2
- package/neural-interface/lib/owner-authority.js +249 -0
- package/neural-interface/lib/page-session.js +275 -0
- package/neural-interface/lib/path-confine.js +12 -9
- package/neural-interface/lib/peer-identity.js +793 -0
- package/neural-interface/lib/pty-host/core.js +28 -1
- package/neural-interface/lib/pty-host/lifetime.js +12 -0
- package/neural-interface/lib/pty-host/manager.js +12 -1
- package/neural-interface/lib/remote-policy.js +1 -1
- package/neural-interface/lib/request-gate.js +20 -2
- package/neural-interface/lib/rulesets/api.js +7 -6
- package/neural-interface/lib/rulesets/fs-safe.js +1 -1
- package/neural-interface/lib/rulesets/installer.js +180 -36
- package/neural-interface/lib/rulesets/json-edit.js +1 -1
- package/neural-interface/lib/rulesets/render.js +1 -1
- package/neural-interface/lib/session-cache-builder.js +81 -31
- package/neural-interface/lib/session-title-generator.js +58 -36
- package/neural-interface/lib/skills/api-v2.js +495 -0
- package/neural-interface/lib/skills/bundle.js +418 -0
- package/neural-interface/lib/skills/catalog.js +298 -0
- package/neural-interface/lib/skills/creation-api.js +191 -0
- package/neural-interface/lib/skills/creation-brief.js +247 -0
- package/neural-interface/lib/skills/creation-sources.js +285 -0
- package/neural-interface/lib/skills/creation.js +843 -0
- package/neural-interface/lib/skills/deploy.js +569 -0
- package/neural-interface/lib/skills/digest.js +119 -0
- package/neural-interface/lib/skills/discover.js +111 -0
- package/neural-interface/lib/skills/draft-confinement.js +216 -0
- package/neural-interface/lib/skills/eval-confinement.js +399 -0
- package/neural-interface/lib/skills/eval-sandbox.js +280 -0
- package/neural-interface/lib/skills/eval-trace.js +135 -0
- package/neural-interface/lib/skills/evals-api.js +212 -0
- package/neural-interface/lib/skills/evals.js +1573 -0
- package/neural-interface/lib/skills/export.js +203 -0
- package/neural-interface/lib/skills/frontmatter.js +64 -0
- package/neural-interface/lib/skills/health.js +75 -0
- package/neural-interface/lib/skills/json-edit.js +165 -0
- package/neural-interface/lib/skills/known-bridge-digests.json +441 -0
- package/neural-interface/lib/skills/known-bridges.js +96 -0
- package/neural-interface/lib/skills/library.js +823 -0
- package/neural-interface/lib/skills/local-store.js +21 -0
- package/neural-interface/lib/skills/migration.js +433 -0
- package/neural-interface/lib/skills/operations.js +45 -0
- package/neural-interface/lib/skills/ownership.js +427 -0
- package/neural-interface/lib/skills/packages.js +88 -0
- package/neural-interface/lib/skills/reviews.js +212 -0
- package/neural-interface/lib/skills/revision.js +80 -0
- package/neural-interface/lib/skills/roots.js +269 -0
- package/neural-interface/lib/skills/runtime-checks.js +358 -0
- package/neural-interface/lib/skills/scan-assets.js +252 -0
- package/neural-interface/lib/skills/scan-context.js +26 -0
- package/neural-interface/lib/skills/scan-shell.js +324 -0
- package/neural-interface/lib/skills/scan.js +364 -0
- package/neural-interface/lib/skills/scanners.js +54 -0
- package/neural-interface/lib/skills/service.js +530 -0
- package/neural-interface/lib/skills/telemetry.js +119 -0
- package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
- package/neural-interface/lib/synabun-version.js +1 -1
- package/neural-interface/lib/system-activity.js +1 -1
- package/neural-interface/lib/tool-lists.js +124 -0
- package/neural-interface/lib/whatsapp/brain.js +7 -3
- package/neural-interface/lib/whatsapp/bridge.js +6 -3
- package/neural-interface/lib/whatsapp/cards.js +22 -9
- package/neural-interface/lib/whatsapp/config.js +1 -1
- package/neural-interface/lib/worker-confinement.js +1268 -0
- package/neural-interface/lib/worker-guard-setting.js +56 -0
- package/neural-interface/lib/worker-mcp-channel.js +175 -0
- package/neural-interface/lib/worker-question.js +74 -0
- package/neural-interface/lib/worker-relay-ports.js +227 -0
- package/neural-interface/lib/worker-resume.js +238 -0
- package/neural-interface/lib/worker-sandbox.js +329 -0
- package/neural-interface/package-lock.json +34 -12
- package/neural-interface/package.json +3 -2
- package/neural-interface/public/index.html +3 -1
- package/neural-interface/public/index2d.html +19 -11
- package/neural-interface/public/onboarding.html +3 -3
- package/neural-interface/public/shared/account-menu.js +8 -5
- package/neural-interface/public/shared/agent-runtime-options.js +125 -31
- package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
- package/neural-interface/public/shared/agy/agy-client.js +266 -0
- package/neural-interface/public/shared/agy/agy-commands.js +120 -0
- package/neural-interface/public/shared/agy/agy-copy.js +326 -0
- package/neural-interface/public/shared/agy/agy-diff.js +161 -0
- package/neural-interface/public/shared/agy/agy-events.js +469 -0
- package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
- package/neural-interface/public/shared/agy/agy-options.js +232 -0
- package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
- package/neural-interface/public/shared/agy/agy-render.js +392 -0
- package/neural-interface/public/shared/agy/agy-setup.js +149 -0
- package/neural-interface/public/shared/agy/agy-styles.js +604 -0
- package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
- package/neural-interface/public/shared/api.js +1527 -1336
- package/neural-interface/public/shared/app-frame.js +192 -0
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +36 -6
- package/neural-interface/public/shared/assistant/asst-api.js +1 -1
- package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
- package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
- package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +105 -44
- package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
- package/neural-interface/public/shared/assistant/asst-control.js +205 -26
- package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
- package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
- package/neural-interface/public/shared/assistant/asst-limit-dock.js +45 -0
- package/neural-interface/public/shared/assistant/asst-limit-wait.js +327 -0
- package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
- package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
- package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
- package/neural-interface/public/shared/assistant/asst-panel.js +151 -19
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
- package/neural-interface/public/shared/assistant/asst-render.js +445 -82
- package/neural-interface/public/shared/assistant/asst-route.js +304 -53
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
- package/neural-interface/public/shared/assistant/asst-scenes.js +56 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +117 -10
- package/neural-interface/public/shared/assistant/asst-slash.js +17 -9
- package/neural-interface/public/shared/assistant/asst-state.js +87 -24
- package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +93 -21
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +190 -23
- package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
- package/neural-interface/public/shared/automation-context.js +5 -2
- package/neural-interface/public/shared/cc-dropdown.js +89 -19
- package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
- package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
- package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
- package/neural-interface/public/shared/cdx/cdx-output.js +34 -10
- package/neural-interface/public/shared/cdx/cdx-panel.js +59 -151
- package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +39 -11
- package/neural-interface/public/shared/cdx/cdx-render.js +57 -38
- package/neural-interface/public/shared/cdx/cdx-requests.js +14 -4
- package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
- package/neural-interface/public/shared/cdx/cdx-tabs.js +127 -44
- package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
- package/neural-interface/public/shared/cli-install-controller.js +218 -0
- package/neural-interface/public/shared/cli-install-view.js +11 -12
- package/neural-interface/public/shared/cli-status.js +11 -10
- package/neural-interface/public/shared/constants.js +5 -4
- package/neural-interface/public/shared/context-menu-areas.js +34 -0
- package/neural-interface/public/shared/context-menu-logic.js +243 -0
- package/neural-interface/public/shared/context-menu-model.js +367 -0
- package/neural-interface/public/shared/context-menu.js +372 -0
- package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
- package/neural-interface/public/shared/cp/cp-restore.js +162 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
- package/neural-interface/public/shared/crash-notice-view.js +168 -0
- package/neural-interface/public/shared/desktop-bridge.js +17 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
- package/neural-interface/public/shared/desktop-titlebar.js +13 -4
- package/neural-interface/public/shared/desktop-update-view.js +21 -0
- package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
- package/neural-interface/public/shared/explorer/fx-api.js +4 -4
- package/neural-interface/public/shared/explorer/fx-links.js +3 -3
- package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +48 -16
- package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
- package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
- package/neural-interface/public/shared/html-shell.js +30 -15
- package/neural-interface/public/shared/i18n.js +2 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +74 -259
- package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +13 -22
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
- package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
- package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
- package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -21
- package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +60 -50
- package/neural-interface/public/shared/page-session.js +96 -0
- package/neural-interface/public/shared/provider-icons.js +4 -2
- package/neural-interface/public/shared/registry.js +5 -3
- package/neural-interface/public/shared/retired-2d.js +3 -0
- package/neural-interface/public/shared/schedules-view.js +195 -61
- package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
- package/neural-interface/public/shared/settings/settings-data.js +3 -1
- package/neural-interface/public/shared/settings/settings-ia.js +6 -6
- package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
- package/neural-interface/public/shared/settings/settings-rules.js +2 -2
- package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
- package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
- package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
- package/neural-interface/public/shared/showcase-privacy.js +6 -1
- package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
- package/neural-interface/public/shared/skills/ss-args.js +96 -0
- package/neural-interface/public/shared/skills/ss-creation.js +222 -0
- package/neural-interface/public/shared/skills/ss-dialog.js +133 -0
- package/neural-interface/public/shared/skills/ss-doc.js +173 -0
- package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
- package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
- package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
- package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
- package/neural-interface/public/shared/skills/ss-evals.js +417 -0
- package/neural-interface/public/shared/skills/ss-form.js +159 -0
- package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
- package/neural-interface/public/shared/skills/ss-ids.js +32 -0
- package/neural-interface/public/shared/skills/ss-lint.js +149 -0
- package/neural-interface/public/shared/skills/ss-presets.js +117 -0
- package/neural-interface/public/shared/skills/ss-profiles.js +513 -0
- package/neural-interface/public/shared/skills/ss-resources.js +165 -0
- package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
- package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
- package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
- package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
- package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
- package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
- package/neural-interface/public/shared/skills/studio/sx-create-ai.js +237 -0
- package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
- package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
- package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
- package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
- package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
- package/neural-interface/public/shared/skills/studio/sx-dom.js +230 -0
- package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
- package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
- package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
- package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
- package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
- package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
- package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
- package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
- package/neural-interface/public/shared/skills/studio/sx-install-flow.js +282 -0
- package/neural-interface/public/shared/skills/studio/sx-install.js +313 -0
- package/neural-interface/public/shared/skills/studio/sx-library-view.js +748 -0
- package/neural-interface/public/shared/skills/studio/sx-library.js +479 -0
- package/neural-interface/public/shared/skills/studio/sx-main.js +690 -0
- package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
- package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
- package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
- package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
- package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
- package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
- package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
- package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
- package/neural-interface/public/shared/skills/studio/sx-subject.js +249 -0
- package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
- package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
- package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
- package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +426 -0
- package/neural-interface/public/shared/state.js +1 -1
- package/neural-interface/public/shared/streaming-markdown.js +58 -0
- package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
- package/neural-interface/public/shared/styles.css +1822 -1756
- package/neural-interface/public/shared/synabun-mascot.js +1115 -9
- package/neural-interface/public/shared/text-cache.js +29 -0
- package/neural-interface/public/shared/tool-list-picker.js +55 -0
- package/neural-interface/public/shared/tool-list-storage.js +24 -0
- package/neural-interface/public/shared/tool-list-view.js +112 -0
- package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
- package/neural-interface/public/shared/tour/tour-actor.js +268 -0
- package/neural-interface/public/shared/tour/tour-beats.js +336 -0
- package/neural-interface/public/shared/tour/tour-engine.js +884 -0
- package/neural-interface/public/shared/tour/tour-ink.js +390 -0
- package/neural-interface/public/shared/tour/tour-stage.js +440 -0
- package/neural-interface/public/shared/tour/tour-state.js +40 -0
- package/neural-interface/public/shared/tour/tour-tips.js +99 -0
- package/neural-interface/public/shared/tour/tour.css +144 -0
- package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
- package/neural-interface/public/shared/ui-assistant.js +2 -0
- package/neural-interface/public/shared/ui-automation-studio.js +111 -140
- package/neural-interface/public/shared/ui-claude-panel.js +376 -82
- package/neural-interface/public/shared/ui-cli-banner.js +23 -167
- package/neural-interface/public/shared/ui-command-runner.js +6 -3
- package/neural-interface/public/shared/ui-context-menu.js +295 -0
- package/neural-interface/public/shared/ui-cost-widget.js +57 -59
- package/neural-interface/public/shared/ui-crash-notice.js +187 -0
- package/neural-interface/public/shared/ui-detail.js +15 -3
- package/neural-interface/public/shared/ui-file-explorer.js +48 -6
- package/neural-interface/public/shared/ui-file-links.js +2 -2
- package/neural-interface/public/shared/ui-games.js +2 -2
- package/neural-interface/public/shared/ui-image-gallery.js +50 -47
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-keybinds.js +33 -2
- package/neural-interface/public/shared/ui-link.js +3 -3
- package/neural-interface/public/shared/ui-menubar.js +10 -20
- package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +11 -1
- package/neural-interface/public/shared/ui-navbar.js +49 -24
- package/neural-interface/public/shared/ui-notifications.js +1 -0
- package/neural-interface/public/shared/ui-panels.js +96 -65
- package/neural-interface/public/shared/ui-resume.js +9 -7
- package/neural-interface/public/shared/ui-schedules-studio.js +546 -431
- package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
- package/neural-interface/public/shared/ui-sessions.js +27 -15
- package/neural-interface/public/shared/ui-settings.js +683 -526
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +4 -4
- package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
- package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
- package/neural-interface/public/shared/ui-sidepanel-windows.js +18 -13
- package/neural-interface/public/shared/ui-skills.js +33 -2830
- package/neural-interface/public/shared/ui-styleguide.js +5 -2
- package/neural-interface/public/shared/ui-sync.js +15 -5
- package/neural-interface/public/shared/ui-terminal.js +270 -190
- package/neural-interface/public/shared/ui-tool-updates.js +6 -4
- package/neural-interface/public/shared/ui-tooltip.js +81 -51
- package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
- package/neural-interface/public/shared/ui-trash.js +6 -4
- package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
- package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
- package/neural-interface/public/shared/ui-tutorial.css +47 -100
- package/neural-interface/public/shared/ui-tutorial.js +285 -509
- package/neural-interface/public/shared/ui-update.js +38 -2
- package/neural-interface/public/shared/ui-whiteboard.js +312 -59
- package/neural-interface/public/shared/ui-worker-end.js +5 -1
- package/neural-interface/public/shared/ui-workspaces.js +3 -2
- package/neural-interface/public/shared/view-variant.js +66 -0
- package/neural-interface/public/shared/whatsapp/wa-view.js +3 -3
- package/neural-interface/public/shared/whiteboard-list.js +172 -0
- package/neural-interface/public/shared/worker-end.js +10 -3
- package/neural-interface/public/shared/worker-questions.js +34 -0
- package/neural-interface/public/shared/worker-restart.js +12 -0
- package/neural-interface/public/sidepanel-session.html +2 -2
- package/neural-interface/public/variant/2d/background.js +2 -1
- package/neural-interface/public/variant/2d/context-menu.js +8 -2
- package/neural-interface/public/variant/2d/graph.js +42 -20
- package/neural-interface/public/variant/2d/hulls.js +14 -9
- package/neural-interface/public/variant/2d/lasso.js +11 -7
- package/neural-interface/public/variant/2d/main.js +32 -1
- package/neural-interface/public/variant/2d/minimap.js +11 -13
- package/neural-interface/public/variant/3d/main.js +69 -6
- package/neural-interface/public/variant/3d/map-controls.js +24 -9
- package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
- package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
- package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
- package/neural-interface/public/vendor/caveat/caveat.css +17 -0
- package/neural-interface/public/vendor/caveat/manifest.json +12 -0
- package/neural-interface/public/vendor/yaml/LICENSE +13 -0
- package/neural-interface/public/vendor/yaml/manifest.json +4 -0
- package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
- package/neural-interface/scripts/build-vendor.mjs +32 -0
- package/neural-interface/server.js +4553 -2855
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/neural-interface/templates/rulesets/core.md +1 -1
- package/neural-interface/templates/rulesets/legacy-hashes.json +23 -5
- package/neural-interface/templates/rulesets/manifest.json +8 -8
- package/npm-shrinkwrap.json +34 -12
- package/package.json +17 -3
- package/postinstall.js +9 -4
- package/preuninstall.js +1 -1
- package/setup.js +97 -322
- package/skills/skill-forge/SKILL.md +177 -0
- package/skills/skill-forge/evals/evals.json +54 -0
- package/skills/skill-forge/references/package.md +105 -0
- package/skills/skill-forge/references/review-and-recovery.md +63 -0
- package/skills/skill-forge/references/runtime-profiles.md +20 -0
- package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
- package/skills/synabun/SKILL.md +10 -7
- package/skills/synabun/modules/audit.md +1 -5
- package/skills/synabun/modules/changelog.md +1 -5
- package/skills/synabun/modules/idea.md +1 -1
- package/skills/synabun/modules/memorize.md +6 -9
- package/skills/synabun/modules/schedule.md +6 -6
- package/mcp-server/dist/services/profiles.d.ts +0 -120
- package/mcp-server/dist/services/profiles.d.ts.map +0 -1
- package/mcp-server/dist/services/profiles.js +0 -387
- package/mcp-server/dist/services/profiles.js.map +0 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
- package/mcp-server/dist/tools/leonardo.d.ts +0 -13
- package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
- package/mcp-server/dist/tools/leonardo.js +0 -21
- package/mcp-server/dist/tools/leonardo.js.map +0 -1
- package/mcp-server/dist/tools/profile.d.ts +0 -17
- package/mcp-server/dist/tools/profile.d.ts.map +0 -1
- package/mcp-server/dist/tools/profile.js +0 -114
- package/mcp-server/dist/tools/profile.js.map +0 -1
- package/mcp-server/src/services/profiles.ts +0 -428
- package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
- package/mcp-server/src/tools/leonardo.ts +0 -30
- package/mcp-server/src/tools/profile.ts +0 -122
- package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
- package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
- package/neural-interface/lib/mcp-profile-servers.js +0 -120
- package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
- package/skills/leonardo/SKILL.md +0 -168
- package/skills/leonardo/icon.svg +0 -1
- package/skills/leonardo/modules/browser-guide.md +0 -378
- package/skills/leonardo/modules/image-prompter.md +0 -283
- package/skills/leonardo/modules/model-advisor.md +0 -100
- package/skills/leonardo/modules/prompt-library.md +0 -110
- package/skills/leonardo/modules/style-guide.md +0 -186
- package/skills/leonardo/modules/video-prompter.md +0 -317
- /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
* one-shot (success, error, blocked) has played and held. The same pose with
|
|
40
40
|
* a new count or ticks keeps its loop running. `opts.unlimited` selects the
|
|
41
41
|
* budget pose's relaxed infinity variant; project peeks into a folder.
|
|
42
|
+
* The card poses (`MASCOT_CARD_POSES`, the Assistant's follow-ups and
|
|
43
|
+
* question cards): `present` (a warm smile toward what it offers), `pick`
|
|
44
|
+
* (the plan's clipboard, one tick per picked item: `opts.ticks`) and `ask`
|
|
45
|
+
* (a tilted head and a question mark); all three are still between blinks
|
|
46
|
+
* and look where look() points.
|
|
42
47
|
* - `react(name)` — `MASCOT_REACTIONS`: hop (spring, at most one per 600 ms),
|
|
43
48
|
* wince, nod, pop (plan: pops the newest tick; ticks the next box too
|
|
44
49
|
* unless setPose passed `ticks`), startle, pulse, click
|
|
@@ -47,6 +52,27 @@
|
|
|
47
52
|
* - `look(x, y)` — gaze in [-1, 1] on both axes. Idle: a cursor that moved in
|
|
48
53
|
* the last 2.5 s wins. watch / computer / runs / wait / blocked follow it.
|
|
49
54
|
* - `blink()` — one blink.
|
|
55
|
+
* - `point(target | null, { poke, gap })` — rubber-hose arms (rig v3): the
|
|
56
|
+
* arm on the side of `target` (an element, a client rect or a client point)
|
|
57
|
+
* sprouts from the face's lower outer side with a springy overshoot, grows
|
|
58
|
+
* until its fingertip stops `gap` px short of the target and aims its hand
|
|
59
|
+
* (a round palm, one index pill and a thumb nub, in the eyes' pill
|
|
60
|
+
* language) along the line from the hand to the target, on a spring; the
|
|
61
|
+
* eyes look there too, never far enough to come to the arm; null (or a
|
|
62
|
+
* target on the other side) sends it back into the body, quicker than it
|
|
63
|
+
* grew. Pointing again re-measures and carries on from the springs' state.
|
|
64
|
+
* Rects are read when it is called and again after a resize or scroll,
|
|
65
|
+
* never per frame. No part of an arm ever crosses an eye or the smile. Only
|
|
66
|
+
* on a rig at least 96 px wide (`opts.arms` overrides): never a mini, a
|
|
67
|
+
* cameo or a still. Under reduced motion the arm shows its final pose at once.
|
|
68
|
+
* - `poke()` — two quick pokes along the line to the target (a beat landed).
|
|
69
|
+
* - `nudge(vx, vy)` — the host moved the character's box and its velocity
|
|
70
|
+
* changed by (vx, vy) px/s: the arms trail, then wobble back.
|
|
71
|
+
* - `shift(dx, dy)` — the host moved the box by (dx, dy) px since the last
|
|
72
|
+
* measure: the hand keeps pointing at the same place (no layout read).
|
|
73
|
+
* - `remeasure()` — read the rects again (the host's move has landed).
|
|
74
|
+
* - `armExtent()` — the client box the pointing arm takes once grown, or null.
|
|
75
|
+
* - `pointing` — the target, or null.
|
|
50
76
|
* - `setActive(bool)` — false unsubscribes and shows the pose's still frame
|
|
51
77
|
* (no loops, no tracking); true resumes.
|
|
52
78
|
* - `pose` — the current pose name (also the SVG's `data-pose`); `fps` — the
|
|
@@ -69,6 +95,11 @@
|
|
|
69
95
|
* (never a pause glyph); the host's CSS animates them.
|
|
70
96
|
* - `mascotPropSvg(kind, { width, height })` + `paintMascotProp(svg, kind)`
|
|
71
97
|
* — a kind's prop alone, framed to its own box: the ledger's row glyphs.
|
|
98
|
+
* - `mascotBounds(pose)` — the face-unit box a pose can draw in (the eyes over
|
|
99
|
+
* their travel, the smile, its prop): what a host keeps its drawing out of.
|
|
100
|
+
* - Arms: `createArmSim`, `armRest`, `armAim`, `armSide`, `armTargeting`,
|
|
101
|
+
* `armWidths`, `armPath`, `armHandPath`, `armDrawPath`, `armGaze`,
|
|
102
|
+
* `armKeepOut`, `MASCOT_ARM`.
|
|
72
103
|
* - v1: `mascotSvg`, `mascotRefs`, `poseMascot`, `blinkAmount`, `MASCOT_VIEWBOX`.
|
|
73
104
|
*/
|
|
74
105
|
|
|
@@ -82,7 +113,9 @@ export const MASCOT_VIEWBOX = '0 0 280 140';
|
|
|
82
113
|
/** One pose per tool kind (shared with the activity rack's kind ids). */
|
|
83
114
|
export const MASCOT_KINDS = Object.freeze(['shell', 'read', 'edit', 'search', 'web', 'subagent', 'plan', 'memory', 'runs', 'computer', 'think', 'mcp']);
|
|
84
115
|
export const MASCOT_STATES = Object.freeze(['idle', 'sleep', 'watch', 'wait', 'success', 'error', 'blocked', 'offline']);
|
|
85
|
-
|
|
116
|
+
/** The Assistant's cards the mascot presents (asst-ask.js): next steps and confirms, decisions, questions. */
|
|
117
|
+
export const MASCOT_CARD_POSES = Object.freeze(['present', 'pick', 'ask']);
|
|
118
|
+
export const MASCOT_POSES = Object.freeze([...MASCOT_KINDS, ...MASCOT_STATES, 'project', 'budget', 'skill', ...MASCOT_CARD_POSES]);
|
|
86
119
|
export const MASCOT_REACTIONS = Object.freeze(['hop', 'wince', 'nod', 'pop', 'startle', 'pulse', 'click']);
|
|
87
120
|
|
|
88
121
|
/** The character's SVG. Colour comes from `currentColor` (the mouth at 25 %). */
|
|
@@ -148,6 +181,8 @@ export function mascotCameoSvg({ className = '' } = {}) {
|
|
|
148
181
|
const PROP_BOX = Object.freeze({
|
|
149
182
|
project: [204, 77, 62, 48],
|
|
150
183
|
budget: [212, 65, 54, 60],
|
|
184
|
+
skill: [209, 25, 71, 96],
|
|
185
|
+
ask: [210, 0, 46, 66],
|
|
151
186
|
shell: [144, 116, 42, 26], // the last keycap and the caret
|
|
152
187
|
read: [65, 20, 150, 88],
|
|
153
188
|
edit: [188, 93, 56, 44],
|
|
@@ -343,6 +378,54 @@ const POSES = {
|
|
|
343
378
|
return f;
|
|
344
379
|
},
|
|
345
380
|
},
|
|
381
|
+
skill: { // writing a reusable skill: a little scroll, then a pleased sparkle
|
|
382
|
+
motion: 'loop', blinks: true,
|
|
383
|
+
frame(t, o) {
|
|
384
|
+
const f = blankFrame();
|
|
385
|
+
const c = mod(t, 3000);
|
|
386
|
+
const draw = o.still ? 0.85 : clamp01(c / 1800);
|
|
387
|
+
const spark = o.still ? 1 : bump(c, 1800, 900);
|
|
388
|
+
f.face.rot = -3 * (1 - spark);
|
|
389
|
+
f.eyes = [eye(5 + 2 * draw, 6, 1, 0.8 - 0.18 * spark, -3), eye(5 + 2 * draw, 6, 1, 0.94 - 0.2 * spark, 2)];
|
|
390
|
+
f.mouth = mouth(30 + 10 * spark, 8 + 8 * spark, 3, -1);
|
|
391
|
+
f.prop = { name: 'skill', draw, spark, jig: o.still || draw >= 1 ? 0 : Math.sin(TAU * c / 300) };
|
|
392
|
+
return f;
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
// Cards the mascot presents (asst-ask.js): still between blinks, the gaze on the option the host points at.
|
|
396
|
+
present: { // next steps, a confirm: a warm smile, the head turned a little toward what it offers
|
|
397
|
+
motion: 'still', blinks: true, look: 70,
|
|
398
|
+
frame(t, o) {
|
|
399
|
+
const f = blankFrame();
|
|
400
|
+
f.eyes = [eye(0, 2, 1, 0.94, -2), eye(0, 2, 1, 0.98, 2)];
|
|
401
|
+
f.mouth = mouth(40, 18, 0, -1);
|
|
402
|
+
f.face.rot = -2;
|
|
403
|
+
return gaze(f, o.look, { x: 16, y: 10 });
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
pick: { // decisions: the plan's clipboard ticks a box per picked item (opts.ticks)
|
|
407
|
+
motion: 'still', blinks: true, look: 70,
|
|
408
|
+
frame(t, o) {
|
|
409
|
+
const f = blankFrame();
|
|
410
|
+
f.eyes = [eye(0, 1, 1.02, 0.9, 0), eye(0, 1, 0.98, 0.86, 2)];
|
|
411
|
+
f.mouth = mouth(30, 8);
|
|
412
|
+
gaze(f, o.look, { x: 14, y: 9 });
|
|
413
|
+
f.prop = { name: 'plan', ticks: o.ticks, pop: 1 };
|
|
414
|
+
return f;
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
ask: { // a question: the head tilts, one eye wider, a small round mouth, a question mark beside it
|
|
418
|
+
motion: 'still', blinks: true, look: 70,
|
|
419
|
+
frame(t, o) {
|
|
420
|
+
const f = blankFrame();
|
|
421
|
+
f.face.rot = 5;
|
|
422
|
+
f.eyes = [eye(0, -2, 1.02, 1.06, 0), eye(0, -1, 0.98, 0.9, 3)];
|
|
423
|
+
f.mouth = mouth(16, 7, 2, 1, 0, 0.3);
|
|
424
|
+
gaze(f, o.look, { x: 14, y: 9 });
|
|
425
|
+
f.prop = { name: 'ask' };
|
|
426
|
+
return f;
|
|
427
|
+
},
|
|
428
|
+
},
|
|
346
429
|
// ── States ──
|
|
347
430
|
idle: {
|
|
348
431
|
motion: 'still', blinks: true, tracks: true,
|
|
@@ -654,6 +737,32 @@ export function stillFrame(name, opts = {}) {
|
|
|
654
737
|
return poseFrame(name, 0, { ...opts, still: true });
|
|
655
738
|
}
|
|
656
739
|
|
|
740
|
+
// The face-unit box the eyes and the smile can reach: the eyes (80-200 × 24-96) over their whole travel
|
|
741
|
+
// (±20 × ±14, the widest pose's) and swell, the smile at its widest and lowest.
|
|
742
|
+
const FACE_REACH = Object.freeze({ left: 58, top: 6, right: 222, bottom: 134 });
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* The box (face units: { left, top, right, bottom }) pose `name` can draw in:
|
|
746
|
+
* its eyes over their whole travel, the smile and its prop, at the face's own
|
|
747
|
+
* offset (the subagent pose steps aside for its minis). Arms aside. What a
|
|
748
|
+
* host keeps its own drawing out of, so nothing is drawn over the character.
|
|
749
|
+
*/
|
|
750
|
+
export function mascotBounds(name, opts = {}) {
|
|
751
|
+
const f = stillFrame(POSES[name] ? name : 'idle', opts);
|
|
752
|
+
const box = { ...FACE_REACH };
|
|
753
|
+
const prop = PROP_BOX[f.prop?.name];
|
|
754
|
+
if (prop) {
|
|
755
|
+
const [x, y, w, h] = prop;
|
|
756
|
+
box.left = Math.min(box.left, x);
|
|
757
|
+
box.top = Math.min(box.top, y);
|
|
758
|
+
box.right = Math.max(box.right, x + w);
|
|
759
|
+
box.bottom = Math.max(box.bottom, y + h);
|
|
760
|
+
}
|
|
761
|
+
const dx = f.face?.dx || 0;
|
|
762
|
+
const dy = f.face?.dy || 0;
|
|
763
|
+
return { left: box.left + dx, top: box.top + dy, right: box.right + dx, bottom: box.bottom + dy };
|
|
764
|
+
}
|
|
765
|
+
|
|
657
766
|
/** The mouth's quadratic stroke: `w` wide, control `c` below the corners. */
|
|
658
767
|
export function mouthPath(m) {
|
|
659
768
|
const hw = m.w / 2;
|
|
@@ -787,6 +896,22 @@ const PROPS = {
|
|
|
787
896
|
put(p.unlimited ? r.infinity : r.coin, 'transform', `translate(0,${n(-3 * p.lift)})`);
|
|
788
897
|
},
|
|
789
898
|
},
|
|
899
|
+
skill: {
|
|
900
|
+
markup: () => '<path d="M218 107V57q0-5 5-5h29q5 0 5 5v50M212 107h45v4q0 7-7 7h-31q-7 0-7-7z" fill="none" stroke="currentColor" stroke-opacity=".7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
901
|
+
+ '<path class="syna-skill-ink" d="M225 72h21M225 85h21M225 98h21" pathLength="1" fill="none" stroke="currentColor" stroke-opacity=".55" stroke-width="3" stroke-linecap="round" stroke-dasharray="1 1"/>'
|
|
902
|
+
+ '<g class="syna-skill-pen" fill="currentColor" fill-opacity=".75"><rect x="-3" y="-28" width="6" height="20" rx="3"/><path d="M-2.8,-9 L0,-1 L2.8,-9 Z"/></g>'
|
|
903
|
+
+ '<path class="syna-skill-spark" d="M266 28l2.4 7.6L276 38l-7.6 2.4L266 48l-2.4-7.6L256 38l7.6-2.4z" fill="currentColor"/>',
|
|
904
|
+
refs: g => ({ ink: g.querySelector('.syna-skill-ink'), pen: g.querySelector('.syna-skill-pen'), spark: g.querySelector('.syna-skill-spark') }),
|
|
905
|
+
update(r, p, put) {
|
|
906
|
+
const row = Math.min(2, Math.floor(p.draw * 3));
|
|
907
|
+
const x = 225 + 21 * (p.draw * 3 - row);
|
|
908
|
+
const y = 72 + 13 * row + p.jig - 3 * p.spark;
|
|
909
|
+
put(r.ink, 'stroke-dashoffset', n(1 - p.draw));
|
|
910
|
+
put(r.pen, 'transform', `translate(${n(x)},${n(y)}) rotate(35)`);
|
|
911
|
+
put(r.spark, 'transform', `translate(266,38) scale(${n(0.65 + 0.35 * p.spark)}) translate(-266,-38)`);
|
|
912
|
+
put(r.spark, 'fill-opacity', n(0.25 + 0.6 * p.spark));
|
|
913
|
+
},
|
|
914
|
+
},
|
|
790
915
|
shell: { // three keycaps and a block caret under the chin
|
|
791
916
|
markup: () => [0, 1, 2].map(i => `<rect class="syna-key" x="${100 + i * 24}" y="127" width="20" height="9" rx="4.5" fill="currentColor" fill-opacity=".4"/>`).join('')
|
|
792
917
|
+ '<rect class="syna-caret" x="174" y="125" width="7" height="12" rx="2" fill="currentColor" fill-opacity=".75"/>',
|
|
@@ -921,6 +1046,12 @@ const PROPS = {
|
|
|
921
1046
|
});
|
|
922
1047
|
},
|
|
923
1048
|
},
|
|
1049
|
+
ask: { // a question mark beside the face, in the outline props' weight
|
|
1050
|
+
markup: () => '<path d="M216,22 C216,10 225,4 233,4 C242,4 250,10 250,19 C250,28 243,32 236,36 C232,38 231,41 231,46" fill="none" stroke="currentColor" stroke-opacity=".7" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
1051
|
+
+ '<rect x="227.5" y="54" width="7" height="8" rx="3.5" fill="currentColor" fill-opacity=".7"/>',
|
|
1052
|
+
refs: () => ({}),
|
|
1053
|
+
update() {},
|
|
1054
|
+
},
|
|
924
1055
|
mcp: { // a two-prong plug on its cord
|
|
925
1056
|
markup: () => '<path d="M41,106 C41,120 30,122 22,134" fill="none" stroke="currentColor" stroke-opacity=".4" stroke-width="3" stroke-linecap="round"/>'
|
|
926
1057
|
+ '<g class="syna-plug" fill="currentColor" fill-opacity=".65"><rect x="30" y="86" width="22" height="20" rx="7"/><rect x="34" y="74" width="4.5" height="14" rx="2.25"/><rect x="43.5" y="74" width="4.5" height="14" rx="2.25"/></g>',
|
|
@@ -931,6 +1062,755 @@ const PROPS = {
|
|
|
931
1062
|
},
|
|
932
1063
|
};
|
|
933
1064
|
|
|
1065
|
+
// ── Arms: rubber-hose noodles the rig grows only while it points ──
|
|
1066
|
+
// One arm at each lower outer side of the face, below and outside the eyes,
|
|
1067
|
+
// where a body's sides would be: a fine line (CSS px, so it stays fine at every
|
|
1068
|
+
// size, about the smile's own stroke) from a shoulder anchor, no elbow or joint,
|
|
1069
|
+
// that swells into a hand built from pills like the eyes: a round palm, one
|
|
1070
|
+
// index pill that unfurls to point, a thumb nub. They exist only while a host
|
|
1071
|
+
// points (rig.point): the arm on the target's side sprouts with a springy
|
|
1072
|
+
// overshoot and grows until its fingertip stops just short of the target; the
|
|
1073
|
+
// index aims along the line from the hand to the target, its angle on its own
|
|
1074
|
+
// spring so it never snaps; the other arm may give a tiny balancing flick (a
|
|
1075
|
+
// relaxed hand, index folded); when the pointing ends or the target changes
|
|
1076
|
+
// side the arm goes back into the body, quicker than it grew. Every control
|
|
1077
|
+
// point is a damped spring (the hand the loosest), so an arm lags when the body
|
|
1078
|
+
// travels (rig.nudge), wobbles when it lands and settles into a low idle wiggle
|
|
1079
|
+
// while it is held; a poke moves the hand along the hand-to-target line and
|
|
1080
|
+
// squashes it there; a new target continues from the springs' state. Whatever
|
|
1081
|
+
// the springs do, every point of an arm stays inside its shoulder's free wedge
|
|
1082
|
+
// (ARM_BOUND): the eyes and the smile are never crossed, and while an arm is
|
|
1083
|
+
// out the eyes keep to MASCOT_ARM.gaze so they never come to it; a prop drawn
|
|
1084
|
+
// over a shoulder (the project folder, the budget, the edit pen) moves that
|
|
1085
|
+
// arm's root under it (armShoulder), on a spring, so no arm comes out of a
|
|
1086
|
+
// prop. Noodle and
|
|
1087
|
+
// hand are one path (a nonzero union: no seam where they meet, whatever alpha
|
|
1088
|
+
// the colour carries). Pure here (createArmSim, armRest, armPath, armHandPath,
|
|
1089
|
+
// armDrawPath: no DOM); the rig measures, steps the sim on its own frames and
|
|
1090
|
+
// paints it.
|
|
1091
|
+
|
|
1092
|
+
export const MASCOT_ARM = Object.freeze({
|
|
1093
|
+
// Left, right (face units): outside the eyes (x 80-118, 162-200), below them (y 24-96) and level with the
|
|
1094
|
+
// smile's corners (x 118-162, y 116), so neither eye nor the smile is ever near the root of an arm. A prop
|
|
1095
|
+
// drawn over one (PROP_BOX) moves that root under it: armShoulder.
|
|
1096
|
+
shoulders: Object.freeze([Object.freeze({ x: 62, y: 106 }), Object.freeze({ x: 218, y: 106 })]),
|
|
1097
|
+
reach: 96, // a grown arm, shoulder to the wrist
|
|
1098
|
+
// The line's width in CSS px, at the shoulder and at the wrist: a fine line, about the smile's stroke
|
|
1099
|
+
// (2.5 face units, .86-1.07 px on the hero), with a slight taper. Face units are px / the rig's px per
|
|
1100
|
+
// unit, kept within `units` on a very small or very large character.
|
|
1101
|
+
px: Object.freeze({ w0: 1.2, w1: 0.95 }),
|
|
1102
|
+
units: Object.freeze({ min: 1.8, max: 4.6 }),
|
|
1103
|
+
unitPx: 120 / 280, // px per face unit when a caller says nothing: the empty state's 120 px character
|
|
1104
|
+
// The hand (armHandPath), in the pill language of the eyes: a round palm, one index pill that
|
|
1105
|
+
// unfurls to point along the aim, and a thumb nub. Radii and lengths in face units, around the wrist.
|
|
1106
|
+
hand: Object.freeze({ palm: 6.6, index: 2.9, indexLen: 15, thumb: 2.6, thumbLen: 6.5 }),
|
|
1107
|
+
raise: 1.35, // radians an arm may aim above straight out (never up across the eyes)…
|
|
1108
|
+
lower: 2.3, // …and below it (down and in, under the smile, never across it)
|
|
1109
|
+
// While an arm is out the eyes travel at most this far (face units) and swell at most this much,
|
|
1110
|
+
// so an eye never comes to an arm.
|
|
1111
|
+
gaze: Object.freeze({ x: 12, y: 10, sx: 1.04, sy: 1.08 }),
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
const ARM_T = [1 / 3, 2 / 3, 1];
|
|
1115
|
+
const ARM_SPRINGS = [[520, 0.72], [400, 0.5], [310, 0.36]]; // per control point: stiffness (1/s²), damping ratio
|
|
1116
|
+
const ARM_GROW = [19, 0.46]; // the sprout: ω (rad/s), ζ (≈ 18 % overshoot)
|
|
1117
|
+
const ARM_SHRINK = [27, 1]; // going home: critically damped, quicker
|
|
1118
|
+
const ARM_WIGGLE = [[1.1, 1.05, 0.2], [1.4, 1.45, 1.7], [1.7, 0.8, 3.1]]; // per point: amplitude (units), Hz, phase
|
|
1119
|
+
const ARM_NUDGE = [0.25, 0.55, 0.9]; // how much of a body jolt each point keeps (the hand lags most)
|
|
1120
|
+
const ARM_POKE = { dist: 13, dur: 0.13, gap: 0.24, share: [0.2, 0.55, 1] }; // each poke: units along the aim, s out and back, s between the two, per point
|
|
1121
|
+
const ARM_FLICK = { e: 0.24, angle: 1.05, hold: 0.22 }; // the other arm's balancing flick: reach, aim (down and out), s held
|
|
1122
|
+
const ARM_MAX_SPEED = 900; // units/s a jolt can give a point
|
|
1123
|
+
// Where any point of an arm may be, as seen from its shoulder (0 straight out, + down): the aim's range and a
|
|
1124
|
+
// little more for overshoot, wiggle and trailing. The eyes (at their MASCOT_ARM.gaze limits) and the smile lie
|
|
1125
|
+
// outside it with room to spare; consecutive points never turn more than ARM_KINK, so no segment cuts across.
|
|
1126
|
+
const ARM_BOUND = [-(MASCOT_ARM.raise + 0.12), MASCOT_ARM.lower + 0.1];
|
|
1127
|
+
const ARM_KINK = 2.2;
|
|
1128
|
+
const ARM_HOLD = [0.25, 0.3, 0.35]; // a jolt never folds a point closer to the shoulder than this share of where it rests
|
|
1129
|
+
// What an arm keeps away from (face units, rounded boxes: centre, half sizes, corner radius): each eye over
|
|
1130
|
+
// its MASCOT_ARM.gaze travel and swell, and the smile at its widest and lowest. Every part of an arm (the
|
|
1131
|
+
// line, the palm, the index, the thumb) stays ARM_CLEAR outside them, whatever the springs do.
|
|
1132
|
+
const ARM_KEEP_OUT = Object.freeze([
|
|
1133
|
+
...[99, 181].map(cx => Object.freeze({ cx, cy: 60, hw: 19 * MASCOT_ARM.gaze.sx + MASCOT_ARM.gaze.x, hh: 36 * MASCOT_ARM.gaze.sy + MASCOT_ARM.gaze.y, r: 19 * MASCOT_ARM.gaze.sx })),
|
|
1134
|
+
Object.freeze({ cx: 140, cy: 120.5, hw: 34, hh: 12.5, r: 6 }),
|
|
1135
|
+
]);
|
|
1136
|
+
const ARM_CLEAR = 2.5; // face units, about a CSS px on the hero
|
|
1137
|
+
const ARM_MIN_REACH = 0.42; // a target close by: the arm grows at least this much of its reach
|
|
1138
|
+
const ARM_FOCUS = 0.32; // the index aims at the target's middle, this much in from each side, leaning toward the arm
|
|
1139
|
+
const HAND_SQUASH = 0.16; // how much a poke squashes the hand at its peak (shorter along the aim, wider across)
|
|
1140
|
+
const HAND_SQUASH_SPRING = [42, 0.42]; // ω (rad/s), ζ: the squash rebounds a little (a stretch), then settles
|
|
1141
|
+
const HAND_POINT_RATE = 18; // 1/s: how fast the index unfurls on a held arm, or folds once it is let go
|
|
1142
|
+
const HAND_TURN = [20, 0.9]; // ω (rad/s), ζ: the hand's angle follows the line to the target, fluid, never a snap
|
|
1143
|
+
const ARM_FLARE = 18; // units of arm, before the wrist, over which the line swells into the palm
|
|
1144
|
+
// The fingertip's distance past the wrist with the index out, at full size (armHandPath's index pill).
|
|
1145
|
+
const HAND_TIP = (4 + MASCOT_ARM.hand.indexLen + MASCOT_ARM.hand.index) * 1.08;
|
|
1146
|
+
|
|
1147
|
+
/** What an arm keeps away from: the eyes over their travel while it is out, and the smile (face units, rounded boxes). */
|
|
1148
|
+
export function armKeepOut() {
|
|
1149
|
+
return ARM_KEEP_OUT.map(b => ({ ...b }));
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const ARM_ROOT_MAX_Y = 138; // a root moved under a prop never leaves the face's own box (140 high)
|
|
1153
|
+
const ARM_ROOT_SPRING = [16, 0.9]; // ω (rad/s), ζ: a root that moves under a prop glides there, no pop
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Where the arm on `side` (0 left, 1 right) roots while the face shows `prop`:
|
|
1157
|
+
* its shoulder (MASCOT_ARM.shoulders), or, when that prop's box (PROP_BOX: the
|
|
1158
|
+
* project folder, the budget pill and the edit pen sit at the face's lower
|
|
1159
|
+
* side, over the right shoulder) covers the shoulder, just under the box, so an
|
|
1160
|
+
* arm never comes out of a prop. A prop clear of the shoulder moves nothing.
|
|
1161
|
+
*/
|
|
1162
|
+
export function armShoulder(side, prop = null) {
|
|
1163
|
+
const s = MASCOT_ARM.shoulders[side ? 1 : 0];
|
|
1164
|
+
const box = prop ? PROP_BOX[prop] : null;
|
|
1165
|
+
if (!box) return { x: s.x, y: s.y };
|
|
1166
|
+
const [x, y, w, h] = box;
|
|
1167
|
+
const root = MASCOT_ARM.units.max / 2; // the line's radius at its widest
|
|
1168
|
+
if (s.x < x - root || s.x > x + w + root || s.y < y - root || s.y > y + h + root) return { x: s.x, y: s.y };
|
|
1169
|
+
return { x: s.x, y: Math.min(ARM_ROOT_MAX_Y, y + h + root + ARM_CLEAR) };
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/** Signed distance from (x, y) to a rounded box `b` (negative inside), and the unit normal out of it. */
|
|
1173
|
+
function boxDistance(x, y, b) {
|
|
1174
|
+
const sx = x >= b.cx ? 1 : -1;
|
|
1175
|
+
const sy = y >= b.cy ? 1 : -1;
|
|
1176
|
+
const qx = Math.abs(x - b.cx) - (b.hw - b.r);
|
|
1177
|
+
const qy = Math.abs(y - b.cy) - (b.hh - b.r);
|
|
1178
|
+
if (qx > 0 && qy > 0) {
|
|
1179
|
+
const l = Math.hypot(qx, qy);
|
|
1180
|
+
return { d: l - b.r, nx: (sx * qx) / l, ny: (sy * qy) / l };
|
|
1181
|
+
}
|
|
1182
|
+
if (qx > qy) return { d: qx - b.r, nx: sx, ny: 0 };
|
|
1183
|
+
return { d: qy - b.r, nx: 0, ny: sy };
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/** How close a disc (x, y, radius r) comes to the face (ARM_KEEP_OUT): the smallest gap, negative when it overlaps. */
|
|
1187
|
+
function faceGap(x, y, r) {
|
|
1188
|
+
let gap = Infinity;
|
|
1189
|
+
for (const b of ARM_KEEP_OUT) gap = Math.min(gap, boxDistance(x, y, b).d - r);
|
|
1190
|
+
return gap;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/** The hand's size for an arm `e` of the way out (armDrawPath's). */
|
|
1194
|
+
const handScale = e => clamp(e * 1.5, 0, 1.08);
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* The discs a hand covers (face units), turned to `angle` at `wrist` on `side`,
|
|
1198
|
+
* `hs` its size and `pt` how far its index is out: the palm's front, points along
|
|
1199
|
+
* the index and the thumb's tip. armHandPath's pills, sampled.
|
|
1200
|
+
*/
|
|
1201
|
+
function handDiscs(wrist, angle, side, hs, pt) {
|
|
1202
|
+
const H = MASCOT_ARM.hand;
|
|
1203
|
+
const cos = Math.cos(angle);
|
|
1204
|
+
const sin = Math.sin(angle);
|
|
1205
|
+
const flip = side ? -1 : 1;
|
|
1206
|
+
const at = (x, y, r) => ({ x: wrist.x + (x * cos - y * flip * sin) * hs, y: wrist.y + (x * sin + y * flip * cos) * hs, r: r * hs });
|
|
1207
|
+
const open = 0.5 + 0.5 * pt;
|
|
1208
|
+
const discs = [at(3.5, 0, H.palm), at(0.5 + 3 * open, 4 + H.thumbLen * open, H.thumb)];
|
|
1209
|
+
for (let i = 0; i <= 4; i += 1) discs.push(at(4 + (H.indexLen * pt * i) / 4, 0, H.index));
|
|
1210
|
+
return discs;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/** Does a hand at `angle` keep ARM_CLEAR from the face? */
|
|
1214
|
+
function handClear(wrist, angle, side, hs, pt) {
|
|
1215
|
+
return handDiscs(wrist, angle, side, hs, pt).every(c => faceGap(c.x, c.y, c.r) >= ARM_CLEAR);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/** The direction an arm on `side` (0 left, 1 right) points at `angle` (0 straight out, + down). */
|
|
1219
|
+
function armDir(side, angle) {
|
|
1220
|
+
return { x: (side ? 1 : -1) * Math.cos(angle), y: Math.sin(angle) };
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/** Which arm faces `point` (face units): the side of the face it is on. */
|
|
1224
|
+
export function armSide(point) {
|
|
1225
|
+
return Number(point?.x) >= 140 ? 1 : 0;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const clampAim = a => clamp(Number(a) || 0, -MASCOT_ARM.raise, MASCOT_ARM.lower);
|
|
1229
|
+
|
|
1230
|
+
/** The angle the arm on `side` aims at `point` (face units), kept within its swing; `shoulder` is where it roots (armShoulder). */
|
|
1231
|
+
export function armAim(side, point, shoulder = MASCOT_ARM.shoulders[side ? 1 : 0]) {
|
|
1232
|
+
const s = shoulder;
|
|
1233
|
+
const dx = (Number(point?.x) - s.x) * (side ? 1 : -1);
|
|
1234
|
+
const dy = Number(point?.y) - s.y;
|
|
1235
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy) || (Math.abs(dx) < 1e-6 && Math.abs(dy) < 1e-6)) return 0;
|
|
1236
|
+
return clampAim(Math.atan2(dy, dx));
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* How the arm on `side` meets a target `rect` (face units: { left, top, right,
|
|
1241
|
+
* bottom }): the angle it aims at, the point its index aims at (the target's
|
|
1242
|
+
* middle, leaning toward the arm: its near side for a wide one) and how much of
|
|
1243
|
+
* its reach it grows so the fingertip stops `gap` units short of the target.
|
|
1244
|
+
* `shoulder` is where the arm roots (armShoulder: under a prop that covers it).
|
|
1245
|
+
*/
|
|
1246
|
+
export function armTargeting(side, rect, gap = 0, shoulder = MASCOT_ARM.shoulders[side ? 1 : 0]) {
|
|
1247
|
+
const s = shoulder;
|
|
1248
|
+
const l = Number(rect?.left);
|
|
1249
|
+
const t = Number(rect?.top);
|
|
1250
|
+
const r = Number.isFinite(Number(rect?.right)) ? Number(rect.right) : l;
|
|
1251
|
+
const b = Number.isFinite(Number(rect?.bottom)) ? Number(rect.bottom) : t;
|
|
1252
|
+
if (![l, t, r, b].every(Number.isFinite)) return null;
|
|
1253
|
+
const inset = (lo, hi) => { const m = (hi - lo) * ARM_FOCUS; return [lo + m, hi - m]; };
|
|
1254
|
+
const [fx0, fx1] = inset(l, r);
|
|
1255
|
+
const [fy0, fy1] = inset(t, b);
|
|
1256
|
+
const focus = { x: clamp(s.x, fx0, fx1), y: clamp(s.y, fy0, fy1) };
|
|
1257
|
+
const g = Math.max(0, Number(gap) || 0);
|
|
1258
|
+
const dx = Math.max(l - g - s.x, 0, s.x - r - g);
|
|
1259
|
+
const dy = Math.max(t - g - s.y, 0, s.y - b - g);
|
|
1260
|
+
const reach = clamp((Math.hypot(dx, dy) - HAND_TIP) / MASCOT_ARM.reach, ARM_MIN_REACH, 1);
|
|
1261
|
+
return { angle: armAim(side, focus, s), focus, reach };
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Where the arm's three control points rest (a third, two thirds, the hand):
|
|
1266
|
+
* along its aim, `extension` of its reach, bowed a little like a hose away
|
|
1267
|
+
* from the face. With `wiggleAt` (s) they sway in the low idle wiggle.
|
|
1268
|
+
* `shoulder` is where the arm roots (armShoulder).
|
|
1269
|
+
*/
|
|
1270
|
+
export function armRest(side, angle, extension, wiggleAt = null, shoulder = MASCOT_ARM.shoulders[side ? 1 : 0]) {
|
|
1271
|
+
const s = shoulder;
|
|
1272
|
+
const d = armDir(side, angle);
|
|
1273
|
+
// The hose bows away from the face: down for a level arm, outward for a raised or a lowered one.
|
|
1274
|
+
// Its size follows how far that perpendicular leans out-and-down, so it never flips at once.
|
|
1275
|
+
const nx = -d.y;
|
|
1276
|
+
const ny = d.x;
|
|
1277
|
+
const ox = (side ? 1 : -1) * 0.857;
|
|
1278
|
+
const oy = 0.514;
|
|
1279
|
+
const e = Math.max(0, Number(extension) || 0);
|
|
1280
|
+
const len = MASCOT_ARM.reach * e;
|
|
1281
|
+
const bow = 0.16 * len * (nx * ox + ny * oy);
|
|
1282
|
+
const mirror = side ? -1 : 1; // the right arm's perpendicular is the left one's mirror turned round: the wiggle mirrors too
|
|
1283
|
+
return ARM_T.map((t, i) => {
|
|
1284
|
+
let off = Math.sin(Math.PI * t) * bow;
|
|
1285
|
+
let along = len * t;
|
|
1286
|
+
if (wiggleAt != null) {
|
|
1287
|
+
const [a, f, p] = ARM_WIGGLE[i];
|
|
1288
|
+
const k = Math.min(1, e);
|
|
1289
|
+
off += mirror * a * k * Math.sin(TAU * f * wiggleAt + p);
|
|
1290
|
+
along += 0.5 * a * k * Math.sin(TAU * f * 0.7 * wiggleAt + p * 1.7);
|
|
1291
|
+
}
|
|
1292
|
+
return { x: s.x + d.x * along + nx * off, y: s.y + d.y * along + ny * off };
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/** A point `t` (0-1) along the Catmull-Rom segment p1 → p2. */
|
|
1297
|
+
function catmull(p0, p1, p2, p3, t) {
|
|
1298
|
+
const t2 = t * t;
|
|
1299
|
+
const t3 = t2 * t;
|
|
1300
|
+
const f = (a, b, c, d) => 0.5 * ((2 * b) + (-a + c) * t + (2 * a - 5 * b + 4 * c - d) * t2 + (-a + 3 * b - 3 * c + d) * t3);
|
|
1301
|
+
return { x: f(p0.x, p1.x, p2.x, p3.x), y: f(p0.y, p1.y, p2.y, p3.y) };
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
const smoothstep = (u) => { const v = clamp01(u); return v * v * (3 - 2 * v); };
|
|
1305
|
+
|
|
1306
|
+
/** The line's width (face units) at the shoulder and at the wrist, for a character drawn at `unitPx` px per unit. */
|
|
1307
|
+
export function armWidths(unitPx = MASCOT_ARM.unitPx) {
|
|
1308
|
+
const k = Number(unitPx) > 0 ? Number(unitPx) : MASCOT_ARM.unitPx;
|
|
1309
|
+
const u = px => clamp(px / k, MASCOT_ARM.units.min, MASCOT_ARM.units.max);
|
|
1310
|
+
return { w0: u(MASCOT_ARM.px.w0), w1: u(MASCOT_ARM.px.w1) };
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* The noodle's two edges through `points` (shoulder first, hand last, face
|
|
1315
|
+
* units), × `scale`: armWidths(unitPx) w0 wide at the shoulder, tapering to w1
|
|
1316
|
+
* at the wrist, and with `cuff` (a half-width, drawn units) swelling over the
|
|
1317
|
+
* last ARM_FLARE units into the palm, so the hand grows out of the arm with no
|
|
1318
|
+
* step. Also the direction the arm ends in (`tangent`, radians) and the
|
|
1319
|
+
* shoulder's radius. null when the arm is too short to draw.
|
|
1320
|
+
*/
|
|
1321
|
+
function armOutline(points, scale = 1, cuff = 0, unitPx = MASCOT_ARM.unitPx) {
|
|
1322
|
+
if (!Array.isArray(points) || points.length < 2) return null;
|
|
1323
|
+
const k = clamp(Number(scale) || 0, 0, 1.2);
|
|
1324
|
+
const { w0, w1 } = armWidths(unitPx);
|
|
1325
|
+
const line = [];
|
|
1326
|
+
const steps = 6;
|
|
1327
|
+
for (let i = 0; i < points.length - 1; i += 1) {
|
|
1328
|
+
const p0 = points[Math.max(0, i - 1)];
|
|
1329
|
+
const p3 = points[Math.min(points.length - 1, i + 2)];
|
|
1330
|
+
for (let j = 0; j < steps; j += 1) line.push(catmull(p0, points[i], points[i + 1], p3, j / steps));
|
|
1331
|
+
}
|
|
1332
|
+
line.push(points[points.length - 1]);
|
|
1333
|
+
const at = [0];
|
|
1334
|
+
for (let i = 1; i < line.length; i += 1) at.push(at[i - 1] + Math.hypot(line[i].x - line[i - 1].x, line[i].y - line[i - 1].y));
|
|
1335
|
+
const total = at[at.length - 1];
|
|
1336
|
+
if (!(total > 0.5) || k <= 0.01) return null;
|
|
1337
|
+
const swell = Math.max(0, Number(cuff) || 0);
|
|
1338
|
+
const left = [];
|
|
1339
|
+
const right = [];
|
|
1340
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
1341
|
+
const a = line[Math.max(0, i - 1)];
|
|
1342
|
+
const b = line[Math.min(line.length - 1, i + 1)];
|
|
1343
|
+
const dx = b.x - a.x;
|
|
1344
|
+
const dy = b.y - a.y;
|
|
1345
|
+
const dl = Math.hypot(dx, dy) || 1;
|
|
1346
|
+
let hw = (w0 + (w1 - w0) * (at[i] / total)) * k / 2;
|
|
1347
|
+
if (swell > hw) hw += (swell - hw) * smoothstep((at[i] - (total - ARM_FLARE)) / ARM_FLARE);
|
|
1348
|
+
left.push({ x: line[i].x - (dy / dl) * hw, y: line[i].y + (dx / dl) * hw });
|
|
1349
|
+
right.push({ x: line[i].x + (dy / dl) * hw, y: line[i].y - (dx / dl) * hw });
|
|
1350
|
+
}
|
|
1351
|
+
const end = line[line.length - 1];
|
|
1352
|
+
const before = line[line.length - 2];
|
|
1353
|
+
return { left, right, r0: w0 * k / 2, tangent: Math.atan2(end.y - before.y, end.x - before.x) };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/** An outline's path data: smooth edges (quadratics through the midpoints) and a round shoulder. */
|
|
1357
|
+
function outlinePath(o) {
|
|
1358
|
+
const edge = (pts) => {
|
|
1359
|
+
let d = '';
|
|
1360
|
+
for (let i = 1; i < pts.length - 1; i += 1) {
|
|
1361
|
+
const m = { x: (pts[i].x + pts[i + 1].x) / 2, y: (pts[i].y + pts[i + 1].y) / 2 };
|
|
1362
|
+
d += ` Q${n(pts[i].x)},${n(pts[i].y)} ${n(m.x)},${n(m.y)}`;
|
|
1363
|
+
}
|
|
1364
|
+
const last = pts[pts.length - 1];
|
|
1365
|
+
return `${d} L${n(last.x)},${n(last.y)}`;
|
|
1366
|
+
};
|
|
1367
|
+
const back = o.right.slice().reverse();
|
|
1368
|
+
return `M${n(o.left[0].x)},${n(o.left[0].y)}${edge(o.left)} L${n(back[0].x)},${n(back[0].y)}${edge(back)}`
|
|
1369
|
+
+ ` A${n(o.r0)},${n(o.r0)} 0 0 1 ${n(o.left[0].x)},${n(o.left[0].y)}Z`;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/**
|
|
1373
|
+
* The noodle's outline as path data: one smooth tapered shape, w0 wide at the
|
|
1374
|
+
* shoulder (a rounded end that goes into the body) and w1 at the wrist, × `scale`,
|
|
1375
|
+
* swelling into a `cuff` half-width at the wrist when one is given (armOutline).
|
|
1376
|
+
* '' when the arm is too short to draw. The hand is armHandPath's.
|
|
1377
|
+
*/
|
|
1378
|
+
export function armPath(points, scale = 1, cuff = 0, unitPx = MASCOT_ARM.unitPx) {
|
|
1379
|
+
const o = armOutline(points, scale, cuff, unitPx);
|
|
1380
|
+
return o ? outlinePath(o) : '';
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/** Twice a polygon's signed area (the shoelace sum): its sign is which way round it goes. */
|
|
1384
|
+
const shoelace = (pts) => pts.reduce((s, p, i) => { const q = pts[(i + 1) % pts.length]; return s + p.x * q.y - q.x * p.y; }, 0);
|
|
1385
|
+
const wrapAngle = (a) => Math.atan2(Math.sin(a), Math.cos(a));
|
|
1386
|
+
|
|
1387
|
+
/** A pill from `a` to `b` of radius `r`, as a polygon (local units). */
|
|
1388
|
+
function pillPolygon(a, b, r, segs = 10) {
|
|
1389
|
+
const ang = Math.atan2(b.y - a.y, b.x - a.x);
|
|
1390
|
+
const pts = [];
|
|
1391
|
+
for (let i = 0; i <= segs; i += 1) { const t = ang - Math.PI / 2 + (Math.PI * i) / segs; pts.push({ x: b.x + r * Math.cos(t), y: b.y + r * Math.sin(t) }); }
|
|
1392
|
+
for (let i = 0; i <= segs; i += 1) { const t = ang + Math.PI / 2 + (Math.PI * i) / segs; pts.push({ x: a.x + r * Math.cos(t), y: a.y + r * Math.sin(t) }); }
|
|
1393
|
+
return pts;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* The hand at `wrist` (face units), pointing along `angle` (radians, screen
|
|
1398
|
+
* coordinates) on the arm of `side` (0 left, 1 right): a round palm the noodle
|
|
1399
|
+
* swells into, one index pill out `point` (0-1) of its length, on the line
|
|
1400
|
+
* through the wrist at `angle` (so the index aims exactly where the hand is
|
|
1401
|
+
* turned), and a thumb nub that opens with it. Built from pills like the eyes.
|
|
1402
|
+
* The thumb is on top for a level arm and outward for a lowered one, on either
|
|
1403
|
+
* side (left mirrors right). `scale` grows it with the arm; `squash` (a poke
|
|
1404
|
+
* landing) shortens it along the aim and widens it across. Path data of three
|
|
1405
|
+
* pills, each the same way round (`sign`, the noodle's, so a nonzero fill
|
|
1406
|
+
* paints the union once, whatever alpha `currentColor` carries). '' when too
|
|
1407
|
+
* small to draw.
|
|
1408
|
+
*/
|
|
1409
|
+
export function armHandPath(wrist, angle, side, { scale = 1, point = 1, squash = 0, sign = -1 } = {}) {
|
|
1410
|
+
const H = MASCOT_ARM.hand;
|
|
1411
|
+
const hs = clamp(Number(scale) || 0, 0, 1.2);
|
|
1412
|
+
if (hs <= 0.01 || !Number.isFinite(Number(wrist?.x)) || !Number.isFinite(Number(wrist?.y))) return '';
|
|
1413
|
+
const pt = clamp01(Number(point) || 0);
|
|
1414
|
+
const sq = clamp(Number(squash) || 0, -0.3, 0.3);
|
|
1415
|
+
const th = Number(angle) || 0;
|
|
1416
|
+
const cos = Math.cos(th);
|
|
1417
|
+
const sin = Math.sin(th);
|
|
1418
|
+
const sx = hs * (1 - sq);
|
|
1419
|
+
const sy = hs * (1 + sq) * (side ? -1 : 1); // +y is the thumb's side
|
|
1420
|
+
const toWorld = (p) => { const x = p.x * sx; const y = p.y * sy; return { x: wrist.x + x * cos - y * sin, y: wrist.y + x * sin + y * cos }; };
|
|
1421
|
+
const open = 0.5 + 0.5 * pt; // the thumb: tucked on a relaxed hand, out when it points
|
|
1422
|
+
const pills = [
|
|
1423
|
+
pillPolygon({ x: -0.5, y: 0 }, { x: 3.5, y: 0 }, H.palm),
|
|
1424
|
+
pillPolygon({ x: 4, y: 0 }, { x: 4 + H.indexLen * pt, y: 0 }, H.index),
|
|
1425
|
+
pillPolygon({ x: 0.5, y: 4 }, { x: 0.5 + 3 * open, y: 4 + H.thumbLen * open }, H.thumb),
|
|
1426
|
+
];
|
|
1427
|
+
const way = sign < 0 ? -1 : 1;
|
|
1428
|
+
return pills.map((poly) => {
|
|
1429
|
+
const w = poly.map(toWorld);
|
|
1430
|
+
if (Math.sign(shoelace(w)) !== way) w.reverse();
|
|
1431
|
+
return `M${w.map(p => `${n(p.x)},${n(p.y)}`).join('L')}Z`;
|
|
1432
|
+
}).join('');
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Everything one arm draws, as one path: the noodle swelling into the palm, then
|
|
1437
|
+
* the hand at the wrist (armHandPath), turned to the frame's `hand` angle (the
|
|
1438
|
+
* line from the hand to what it points at, on its own spring; the arm's end
|
|
1439
|
+
* when there is none). `frame` is one side of createArmSim().frame(); `side` 0
|
|
1440
|
+
* left, 1 right; `unitPx` the character's px per face unit (the line's width is
|
|
1441
|
+
* in px). One path, every subpath the same way round. '' when there is nothing
|
|
1442
|
+
* to draw.
|
|
1443
|
+
*/
|
|
1444
|
+
export function armDrawPath(frame, side, { unitPx = MASCOT_ARM.unitPx } = {}) {
|
|
1445
|
+
if (!frame || !Array.isArray(frame.points) || !(frame.e > 0)) return '';
|
|
1446
|
+
const grown = frame.e / Math.max(0.05, Number(frame.span) > 0 ? Number(frame.span) : 1);
|
|
1447
|
+
const hs = handScale(grown);
|
|
1448
|
+
const sq = clamp(Number(frame.squash) || 0, -0.3, 0.3);
|
|
1449
|
+
const pt = clamp01(frame.point == null ? 1 : Number(frame.point) || 0);
|
|
1450
|
+
const o = armOutline(frame.points, clamp01(grown * 1.7), MASCOT_ARM.hand.palm * hs * (1 + sq), unitPx);
|
|
1451
|
+
if (!o) return '';
|
|
1452
|
+
const wrist = frame.points[frame.points.length - 1];
|
|
1453
|
+
const angle = Number.isFinite(frame.hand) ? frame.hand : o.tangent;
|
|
1454
|
+
const sign = Math.sign(shoelace([...o.left, ...o.right.slice().reverse()])) || -1;
|
|
1455
|
+
return outlinePath(o) + armHandPath(wrist, angle, side, { scale: hs, point: pt, squash: sq, sign });
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* The eyes of `frame` kept to MASCOT_ARM.gaze, `amount` (0-1) of the way (an arm
|
|
1460
|
+
* on its way out or home eases them in and out): while an arm is out an eye
|
|
1461
|
+
* never travels to it. A new frame; `frame` is left as it is.
|
|
1462
|
+
*/
|
|
1463
|
+
export function armGaze(frame, amount = 1) {
|
|
1464
|
+
const g = clamp01(Number(amount) || 0);
|
|
1465
|
+
if (!frame?.eyes || g <= 0) return frame;
|
|
1466
|
+
const G = MASCOT_ARM.gaze;
|
|
1467
|
+
const f = cloneFrame(frame);
|
|
1468
|
+
for (const e of f.eyes) {
|
|
1469
|
+
e.dx += (clamp(e.dx, -G.x, G.x) - e.dx) * g;
|
|
1470
|
+
e.dy += (clamp(e.dy, -G.y, G.y) - e.dy) * g;
|
|
1471
|
+
e.sx += (Math.min(e.sx, G.sx) - e.sx) * g;
|
|
1472
|
+
e.sy += (Math.min(e.sy, G.sy) - e.sy) * g;
|
|
1473
|
+
}
|
|
1474
|
+
return f;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* The arms' motion, with no DOM: two arms of damped springs. aim(side, angle,
|
|
1479
|
+
* { target, reach }) grows that arm `reach` (0-1) of the way and turns its hand
|
|
1480
|
+
* toward `target` (face units; the other one goes home, or flicks once for
|
|
1481
|
+
* balance), release() sends both home, poke() pokes twice, nudge(vx, vy) is a
|
|
1482
|
+
* jolt of the body (units/s: the points keep part of their speed, so they lag),
|
|
1483
|
+
* step(dt) advances, finish() jumps to the end (reduced motion: no overshoot,
|
|
1484
|
+
* no wiggle), frame() says what to draw.
|
|
1485
|
+
*/
|
|
1486
|
+
export function createArmSim() {
|
|
1487
|
+
// Per arm: the reach e (0 home, 1 grown) and its speed, the three spring points, the target its index
|
|
1488
|
+
// aims at, and the hand's own numbers: pt (0 a relaxed hand, 1 the index out), sq (the poke's squash,
|
|
1489
|
+
// a small spring) and ha (its angle, on a spring that follows the line to the target).
|
|
1490
|
+
// `span` is the reach the arm was last sent to (1 for the flick): the hand and the index grow with e / span.
|
|
1491
|
+
const arms = [0, 1].map(side => ({
|
|
1492
|
+
side, e: 0, ve: 0, goal: 0, span: 1, angle: 0, pts: null, held: false, flickUntil: -1, pokeAt: -1,
|
|
1493
|
+
pt: 0, sq: 0, vsq: 0, target: null, ha: 0, vha: 0, sh: null,
|
|
1494
|
+
}));
|
|
1495
|
+
const grownOf = a => a.e / Math.max(0.05, a.span);
|
|
1496
|
+
// The face's prop (PROP_BOX's name): an arm whose shoulder it covers roots under it (armShoulder). The
|
|
1497
|
+
// root `sh` is a spring toward that place, so a prop arriving while the arm is out slides it, never pops.
|
|
1498
|
+
let prop = null;
|
|
1499
|
+
const rootGoal = a => armShoulder(a.side, prop);
|
|
1500
|
+
const rootOf = (a) => {
|
|
1501
|
+
if (!a.sh) { const g = rootGoal(a); a.sh = { x: g.x, y: g.y, vx: 0, vy: 0 }; }
|
|
1502
|
+
return a.sh;
|
|
1503
|
+
};
|
|
1504
|
+
/** The two pokes' push (0-1) `t` s after they began: two quick bumps, then nothing. */
|
|
1505
|
+
const pokeBump = (t) => {
|
|
1506
|
+
const bump = (x) => (x > 0 && x < ARM_POKE.dur ? Math.sin(Math.PI * x / ARM_POKE.dur) : 0);
|
|
1507
|
+
return bump(t) + bump(t - ARM_POKE.gap);
|
|
1508
|
+
};
|
|
1509
|
+
let time = 0;
|
|
1510
|
+
const restOf = (a, wiggle) => armRest(a.side, a.angle, a.e, wiggle ? time : null, rootOf(a));
|
|
1511
|
+
const ensurePts = (a) => {
|
|
1512
|
+
if (a.pts) return;
|
|
1513
|
+
a.sh = null; // a fresh sprout roots where the prop lets it, at once
|
|
1514
|
+
a.pts = restOf(a, false).map(p => ({ x: p.x, y: p.y, vx: 0, vy: 0 }));
|
|
1515
|
+
a.ha = Math.atan2(Math.sin(a.angle), (a.side ? 1 : -1) * Math.cos(a.angle)); // the hand comes out along the arm
|
|
1516
|
+
a.vha = 0;
|
|
1517
|
+
};
|
|
1518
|
+
const moving = (a) => !!a.pts && (a.held || a.e > 1e-3 || Math.abs(a.ve) > 1e-3 || Math.abs(a.sq) > 0.002 || Math.abs(a.vsq) > 0.02
|
|
1519
|
+
|| Math.abs(a.vha) > 0.02 || (!!a.sh && Math.abs(a.sh.vx) + Math.abs(a.sh.vy) > 0.05)
|
|
1520
|
+
|| a.pts.some(p => Math.abs(p.vx) + Math.abs(p.vy) > 0.05));
|
|
1521
|
+
|
|
1522
|
+
/** Where the hand should point: the arm's own end, turned toward the target as far as the index is out. */
|
|
1523
|
+
function handGoal(a) {
|
|
1524
|
+
const hand = a.pts[2];
|
|
1525
|
+
const prev = a.pts[1];
|
|
1526
|
+
const along = Math.hypot(hand.x - prev.x, hand.y - prev.y) > 0.3
|
|
1527
|
+
? Math.atan2(hand.y - prev.y, hand.x - prev.x)
|
|
1528
|
+
: Math.atan2(Math.sin(a.angle), (a.side ? 1 : -1) * Math.cos(a.angle));
|
|
1529
|
+
if (!a.target) return along;
|
|
1530
|
+
const dx = a.target.x - hand.x;
|
|
1531
|
+
const dy = a.target.y - hand.y;
|
|
1532
|
+
if (Math.hypot(dx, dy) < 4) return a.ha;
|
|
1533
|
+
return along + wrapAngle(Math.atan2(dy, dx) - along) * smoothstep(a.pt);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Keep every point inside the shoulder's free wedge (ARM_BOUND, ARM_KINK), no
|
|
1538
|
+
* nearer the shoulder than ARM_HOLD of its rest, and clear of the face
|
|
1539
|
+
* (ARM_KEEP_OUT, by the line's or the palm's radius): a point stopped there
|
|
1540
|
+
* loses the speed that took it out, a little of it coming back as a bounce.
|
|
1541
|
+
*/
|
|
1542
|
+
function bound(a, rest) {
|
|
1543
|
+
const s = rootOf(a);
|
|
1544
|
+
const out = a.side ? 1 : -1;
|
|
1545
|
+
const hs = handScale(grownOf(a));
|
|
1546
|
+
let prev = null;
|
|
1547
|
+
a.pts.forEach((p, i) => {
|
|
1548
|
+
const stop = (nx, ny) => { // the speed into (nx, ny)'s opposite is spent
|
|
1549
|
+
const vn = p.vx * nx + p.vy * ny;
|
|
1550
|
+
if (vn < 0) { p.vx -= 1.2 * vn * nx; p.vy -= 1.2 * vn * ny; }
|
|
1551
|
+
};
|
|
1552
|
+
const dx = (p.x - s.x) * out;
|
|
1553
|
+
const dy = p.y - s.y;
|
|
1554
|
+
const r = Math.hypot(dx, dy);
|
|
1555
|
+
const hold = ARM_HOLD[i] * Math.hypot(rest[i].x - s.x, rest[i].y - s.y);
|
|
1556
|
+
if (r >= 0.5) {
|
|
1557
|
+
const th = Math.atan2(dy, dx);
|
|
1558
|
+
let c = th;
|
|
1559
|
+
if (c < ARM_BOUND[0] || c > ARM_BOUND[1]) {
|
|
1560
|
+
c = Math.abs(wrapAngle(th - ARM_BOUND[0])) <= Math.abs(wrapAngle(th - ARM_BOUND[1])) ? ARM_BOUND[0] : ARM_BOUND[1];
|
|
1561
|
+
}
|
|
1562
|
+
if (prev != null && Math.abs(c - prev) > ARM_KINK) c = prev + Math.sign(c - prev) * ARM_KINK;
|
|
1563
|
+
const rr = Math.max(r, hold);
|
|
1564
|
+
if (c !== th || rr !== r) {
|
|
1565
|
+
p.x = s.x + out * rr * Math.cos(c);
|
|
1566
|
+
p.y = s.y + rr * Math.sin(c);
|
|
1567
|
+
// Keep the speed along the radius (outward only, when it was folded in); the sideways part is spent.
|
|
1568
|
+
const ux = out * Math.cos(c);
|
|
1569
|
+
const uy = Math.sin(c);
|
|
1570
|
+
let vr = p.vx * ux + p.vy * uy;
|
|
1571
|
+
if (rr !== r && vr < 0) vr = 0;
|
|
1572
|
+
const vtx = p.vx - (p.vx * ux + p.vy * uy) * ux;
|
|
1573
|
+
const vty = p.vy - (p.vx * ux + p.vy * uy) * uy;
|
|
1574
|
+
const keep = c !== th ? -0.2 : 1;
|
|
1575
|
+
p.vx = vr * ux + keep * vtx;
|
|
1576
|
+
p.vy = vr * uy + keep * vty;
|
|
1577
|
+
}
|
|
1578
|
+
prev = c;
|
|
1579
|
+
} else prev = null;
|
|
1580
|
+
// Clear of the eyes and the smile: by the line's half-width, by the palm for the hand.
|
|
1581
|
+
const need = ARM_CLEAR + (i === 2 ? MASCOT_ARM.hand.palm * hs * 1.16 : MASCOT_ARM.units.max / 2);
|
|
1582
|
+
for (const b of ARM_KEEP_OUT) {
|
|
1583
|
+
const g = boxDistance(p.x, p.y, b);
|
|
1584
|
+
if (g.d < need) { p.x += g.nx * (need - g.d); p.y += g.ny * (need - g.d); stop(g.nx, g.ny); }
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/**
|
|
1590
|
+
* The hand's angle, kept clear of the face: as asked when its palm, index and
|
|
1591
|
+
* thumb keep ARM_CLEAR from ARM_KEEP_OUT (always, for anything beside the
|
|
1592
|
+
* face); otherwise turned the least toward the arm's own end until they do.
|
|
1593
|
+
*/
|
|
1594
|
+
function clearAngle(a, angle) {
|
|
1595
|
+
const wrist = a.pts[2];
|
|
1596
|
+
const hs = handScale(grownOf(a));
|
|
1597
|
+
if (hs <= 0.01 || handClear(wrist, angle, a.side, hs, a.pt)) return angle;
|
|
1598
|
+
const prev = a.pts[1];
|
|
1599
|
+
const along = Math.atan2(wrist.y - prev.y, wrist.x - prev.x);
|
|
1600
|
+
const toward = Math.sign(wrapAngle(along - angle)) || 1;
|
|
1601
|
+
for (let k = 1; k <= 24; k += 1) {
|
|
1602
|
+
for (const way of [toward, -toward]) {
|
|
1603
|
+
const c = angle + way * k * 0.07;
|
|
1604
|
+
if (handClear(wrist, c, a.side, hs, a.pt)) return wrapAngle(c);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return along;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function stepArm(a, h) {
|
|
1611
|
+
if (a.flickUntil >= 0 && time >= a.flickUntil) { a.goal = 0; a.flickUntil = -1; }
|
|
1612
|
+
let push = 0;
|
|
1613
|
+
let squash = 0;
|
|
1614
|
+
if (a.pokeAt >= 0) {
|
|
1615
|
+
const since = time - a.pokeAt;
|
|
1616
|
+
if (since > ARM_POKE.gap + ARM_POKE.dur) a.pokeAt = -1;
|
|
1617
|
+
else {
|
|
1618
|
+
push = pokeBump(since);
|
|
1619
|
+
squash = HAND_SQUASH * push; // the hand lands on the target at the bump's peak
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
const [w, z] = a.held || a.flickUntil >= 0 ? ARM_GROW : ARM_SHRINK;
|
|
1623
|
+
a.ve += (w * w * (a.goal - a.e) - 2 * z * w * a.ve) * h;
|
|
1624
|
+
a.e += a.ve * h;
|
|
1625
|
+
if (!a.held && a.e < 0) { a.e = 0; a.ve = 0; } // home: into the body, never out the other side
|
|
1626
|
+
// The hand: the index unfurls as a held arm grows (and folds once it is let go: the flick arm and
|
|
1627
|
+
// a retreating one show a relaxed hand); a poke squashes it, and the squash rebounds and settles.
|
|
1628
|
+
a.pt += ((a.held ? clamp01((grownOf(a) - 0.25) / 0.45) : 0) - a.pt) * (1 - Math.exp(-HAND_POINT_RATE * h));
|
|
1629
|
+
const [sw, sz] = HAND_SQUASH_SPRING;
|
|
1630
|
+
a.vsq += (sw * sw * (squash - a.sq) - 2 * sz * sw * a.vsq) * h;
|
|
1631
|
+
a.sq += a.vsq * h;
|
|
1632
|
+
// The root glides to where the face's prop lets it be (armShoulder); it never jumps.
|
|
1633
|
+
const sh = rootOf(a);
|
|
1634
|
+
const g = rootGoal(a);
|
|
1635
|
+
const [rw, rz] = ARM_ROOT_SPRING;
|
|
1636
|
+
sh.vx += (rw * rw * (g.x - sh.x) - 2 * rz * rw * sh.vx) * h;
|
|
1637
|
+
sh.vy += (rw * rw * (g.y - sh.y) - 2 * rz * rw * sh.vy) * h;
|
|
1638
|
+
sh.x += sh.vx * h;
|
|
1639
|
+
sh.y += sh.vy * h;
|
|
1640
|
+
if (Math.abs(sh.x - g.x) + Math.abs(sh.y - g.y) < 0.05 && Math.abs(sh.vx) + Math.abs(sh.vy) < 0.5) { sh.x = g.x; sh.y = g.y; sh.vx = 0; sh.vy = 0; }
|
|
1641
|
+
const rest = restOf(a, a.held);
|
|
1642
|
+
if (push > 0) {
|
|
1643
|
+
// A poke goes along the line the index aims down, the hand most.
|
|
1644
|
+
const ux = Math.cos(a.ha);
|
|
1645
|
+
const uy = Math.sin(a.ha);
|
|
1646
|
+
rest.forEach((p, i) => { p.x += ux * ARM_POKE.dist * ARM_POKE.share[i] * push; p.y += uy * ARM_POKE.dist * ARM_POKE.share[i] * push; });
|
|
1647
|
+
}
|
|
1648
|
+
a.pts.forEach((p, i) => {
|
|
1649
|
+
const [k, zeta] = ARM_SPRINGS[i];
|
|
1650
|
+
const c = 2 * zeta * Math.sqrt(k);
|
|
1651
|
+
p.vx += (k * (rest[i].x - p.x) - c * p.vx) * h;
|
|
1652
|
+
p.vy += (k * (rest[i].y - p.y) - c * p.vy) * h;
|
|
1653
|
+
p.x += p.vx * h;
|
|
1654
|
+
p.y += p.vy * h;
|
|
1655
|
+
});
|
|
1656
|
+
bound(a, rest);
|
|
1657
|
+
const [tw, tz] = HAND_TURN;
|
|
1658
|
+
a.vha += (tw * tw * wrapAngle(clearAngle(a, handGoal(a)) - a.ha) - 2 * tz * tw * a.vha) * h;
|
|
1659
|
+
const turned = wrapAngle(a.ha + a.vha * h);
|
|
1660
|
+
a.ha = clearAngle(a, turned);
|
|
1661
|
+
if (a.ha !== turned) a.vha *= 0.3; // turned back off the face: the swing is spent
|
|
1662
|
+
if (!a.held && a.flickUntil < 0 && a.e < 1e-3 && Math.abs(a.ve) < 1e-3 && a.pts.every(p => Math.abs(p.vx) + Math.abs(p.vy) < 0.05)) {
|
|
1663
|
+
a.e = 0;
|
|
1664
|
+
a.ve = 0;
|
|
1665
|
+
a.pt = 0;
|
|
1666
|
+
a.sq = 0;
|
|
1667
|
+
a.vsq = 0;
|
|
1668
|
+
a.vha = 0;
|
|
1669
|
+
a.pts = null; // home and still: nothing to draw
|
|
1670
|
+
a.sh = null;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
return {
|
|
1675
|
+
/** The prop the face shows (PROP_BOX's name, or null): an arm whose shoulder it covers roots under it (armShoulder). */
|
|
1676
|
+
prop(name) {
|
|
1677
|
+
prop = name && PROP_BOX[name] ? name : null;
|
|
1678
|
+
return prop;
|
|
1679
|
+
},
|
|
1680
|
+
/** Where the arm on `side` roots for the prop shown (face units; the glide there is the sim's own). */
|
|
1681
|
+
root(side) { return rootGoal(arms[side ? 1 : 0]); },
|
|
1682
|
+
aim(side, angle, { target = null, reach = 1 } = {}) {
|
|
1683
|
+
const a = arms[side ? 1 : 0];
|
|
1684
|
+
const other = arms[side ? 0 : 1];
|
|
1685
|
+
const sprouting = !a.held && a.e < 0.15;
|
|
1686
|
+
a.angle = clampAim(angle);
|
|
1687
|
+
ensurePts(a);
|
|
1688
|
+
a.held = true;
|
|
1689
|
+
a.goal = clamp(Number.isFinite(Number(reach)) ? Number(reach) : 1, ARM_MIN_REACH, 1);
|
|
1690
|
+
a.span = a.goal;
|
|
1691
|
+
a.target = target && Number.isFinite(Number(target.x)) && Number.isFinite(Number(target.y)) ? { x: Number(target.x), y: Number(target.y) } : null;
|
|
1692
|
+
a.flickUntil = -1;
|
|
1693
|
+
if (other.held) { other.held = false; other.goal = 0; other.pokeAt = -1; } // the target changed side
|
|
1694
|
+
else if (sprouting && other.e < 0.05 && other.flickUntil < 0) {
|
|
1695
|
+
other.angle = ARM_FLICK.angle;
|
|
1696
|
+
other.target = null;
|
|
1697
|
+
ensurePts(other);
|
|
1698
|
+
other.goal = ARM_FLICK.e;
|
|
1699
|
+
other.span = 1;
|
|
1700
|
+
other.flickUntil = time + ARM_FLICK.hold;
|
|
1701
|
+
}
|
|
1702
|
+
},
|
|
1703
|
+
release() {
|
|
1704
|
+
for (const a of arms) { a.held = false; a.goal = 0; a.pokeAt = -1; if (a.flickUntil >= 0) a.flickUntil = time; }
|
|
1705
|
+
},
|
|
1706
|
+
poke() {
|
|
1707
|
+
const a = arms.find(x => x.held);
|
|
1708
|
+
if (a) a.pokeAt = time;
|
|
1709
|
+
},
|
|
1710
|
+
nudge(vx, vy) {
|
|
1711
|
+
const dx = Number.isFinite(Number(vx)) ? Number(vx) : 0;
|
|
1712
|
+
const dy = Number.isFinite(Number(vy)) ? Number(vy) : 0;
|
|
1713
|
+
for (const a of arms) {
|
|
1714
|
+
if (!a.pts) continue;
|
|
1715
|
+
a.pts.forEach((p, i) => {
|
|
1716
|
+
p.vx -= dx * ARM_NUDGE[i];
|
|
1717
|
+
p.vy -= dy * ARM_NUDGE[i];
|
|
1718
|
+
const sp = Math.hypot(p.vx, p.vy);
|
|
1719
|
+
if (sp > ARM_MAX_SPEED) { p.vx *= ARM_MAX_SPEED / sp; p.vy *= ARM_MAX_SPEED / sp; }
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
},
|
|
1723
|
+
step(dt) {
|
|
1724
|
+
let left = clamp(Number(dt) || 0, 0, 0.064);
|
|
1725
|
+
while (left > 1e-6) {
|
|
1726
|
+
const h = Math.min(1 / 240, left);
|
|
1727
|
+
left -= h;
|
|
1728
|
+
time += h;
|
|
1729
|
+
for (const a of arms) if (a.pts) stepArm(a, h);
|
|
1730
|
+
}
|
|
1731
|
+
},
|
|
1732
|
+
/** The end of the motion at once: a held arm grown and still, its hand on the target, everything else home. */
|
|
1733
|
+
finish() {
|
|
1734
|
+
for (const a of arms) {
|
|
1735
|
+
a.pokeAt = -1;
|
|
1736
|
+
a.flickUntil = -1;
|
|
1737
|
+
a.ve = 0;
|
|
1738
|
+
a.e = a.held ? a.goal : 0;
|
|
1739
|
+
a.pt = a.held ? 1 : 0;
|
|
1740
|
+
a.sq = 0;
|
|
1741
|
+
a.vsq = 0;
|
|
1742
|
+
a.vha = 0;
|
|
1743
|
+
a.sh = a.held ? { ...rootGoal(a), vx: 0, vy: 0 } : null;
|
|
1744
|
+
a.pts = a.held ? restOf(a, false).map(p => ({ x: p.x, y: p.y, vx: 0, vy: 0 })) : null;
|
|
1745
|
+
if (a.pts) a.ha = clearAngle(a, handGoal(a));
|
|
1746
|
+
}
|
|
1747
|
+
},
|
|
1748
|
+
/** Something still moves (or is held out). */
|
|
1749
|
+
awake: () => arms.some(moving),
|
|
1750
|
+
/** Only the held wiggle is left: no growth, poke, flick, squash, turn or jolt in flight. */
|
|
1751
|
+
settled() {
|
|
1752
|
+
return arms.every((a) => {
|
|
1753
|
+
if (!a.pts) return true;
|
|
1754
|
+
if (!a.held || a.pokeAt >= 0 || a.flickUntil >= 0 || Math.abs(a.ve) > 0.03 || Math.abs(a.e - a.goal) > 0.004) return false;
|
|
1755
|
+
if (Math.abs(a.sq) > 0.004 || Math.abs(a.vsq) > 0.05 || Math.abs(a.pt - 1) > 0.01 || Math.abs(a.vha) > 1) return false; // the wiggle turns it slowly
|
|
1756
|
+
const g = rootGoal(a);
|
|
1757
|
+
if (!a.sh || Math.abs(a.sh.x - g.x) + Math.abs(a.sh.y - g.y) > 0.3) return false; // the root still slides under a prop
|
|
1758
|
+
const rest = restOf(a, true);
|
|
1759
|
+
return a.pts.every((p, i) => Math.abs(p.x - rest[i].x) + Math.abs(p.y - rest[i].y) < 1.6); // the springs trail the wiggle a little
|
|
1760
|
+
});
|
|
1761
|
+
},
|
|
1762
|
+
/** How far out the arms are (0 both home, 1 one grown): the eyes keep to MASCOT_ARM.gaze this much. */
|
|
1763
|
+
presence() {
|
|
1764
|
+
let e = 0;
|
|
1765
|
+
for (const a of arms) if (a.pts) e = Math.max(e, a.held ? 1 : a.e);
|
|
1766
|
+
return clamp01(e * 2.5);
|
|
1767
|
+
},
|
|
1768
|
+
/**
|
|
1769
|
+
* The face-unit box the held arm takes once it has grown (its rest at full
|
|
1770
|
+
* stretch, the hand turned to its target, the fingertip), or null: what a
|
|
1771
|
+
* host keeps its own drawing out of.
|
|
1772
|
+
*/
|
|
1773
|
+
extent() {
|
|
1774
|
+
const a = arms.find(x => x.held);
|
|
1775
|
+
if (!a) return null;
|
|
1776
|
+
const s = rootGoal(a);
|
|
1777
|
+
const pts = [s, ...armRest(a.side, a.angle, a.goal, null, s)];
|
|
1778
|
+
const hand = pts[3];
|
|
1779
|
+
const dir = a.target ? Math.atan2(a.target.y - hand.y, a.target.x - hand.x) : Math.atan2(hand.y - pts[2].y, hand.x - pts[2].x);
|
|
1780
|
+
const tip = { x: hand.x + Math.cos(dir) * HAND_TIP, y: hand.y + Math.sin(dir) * HAND_TIP };
|
|
1781
|
+
const r = MASCOT_ARM.hand.palm * 1.1;
|
|
1782
|
+
const xs = [...pts.map(p => p.x), tip.x];
|
|
1783
|
+
const ys = [...pts.map(p => p.y), tip.y];
|
|
1784
|
+
return { left: Math.min(...xs) - r, top: Math.min(...ys) - r, right: Math.max(...xs) + r, bottom: Math.max(...ys) + r };
|
|
1785
|
+
},
|
|
1786
|
+
/** The side held out (0 left, 1 right), or -1. */
|
|
1787
|
+
get side() { const a = arms.find(x => x.held); return a ? a.side : -1; },
|
|
1788
|
+
/**
|
|
1789
|
+
* Per side: null (nothing to draw) or { e, span, points: [shoulder, a third, two thirds, hand], aim,
|
|
1790
|
+
* point, squash, hand, target }: `span` the reach it was sent to (the hand grows with e / span), `aim` the angle the arm was asked for (0 straight out, + down),
|
|
1791
|
+
* `point` 0-1 how far the hand points (1 the index out, 0 a relaxed hand), `squash` the poke's
|
|
1792
|
+
* squash, `hand` the hand's angle (radians, screen coordinates), `target` the point (face units)
|
|
1793
|
+
* its index aims at, or null.
|
|
1794
|
+
*/
|
|
1795
|
+
frame() {
|
|
1796
|
+
return arms.map((a) => {
|
|
1797
|
+
if (!a.pts || a.e <= 0.004) return null;
|
|
1798
|
+
const sh = rootOf(a);
|
|
1799
|
+
return {
|
|
1800
|
+
e: a.e,
|
|
1801
|
+
span: a.span,
|
|
1802
|
+
points: [{ x: sh.x, y: sh.y }, ...a.pts.map(p => ({ x: p.x, y: p.y }))],
|
|
1803
|
+
aim: a.angle,
|
|
1804
|
+
point: a.pt,
|
|
1805
|
+
squash: a.sq,
|
|
1806
|
+
hand: a.ha,
|
|
1807
|
+
target: a.target ? { ...a.target } : null,
|
|
1808
|
+
};
|
|
1809
|
+
});
|
|
1810
|
+
},
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
|
|
934
1814
|
// ── Painter: frame → attributes, each written only when it changed ──
|
|
935
1815
|
|
|
936
1816
|
let uidSeq = 0;
|
|
@@ -1081,6 +1961,7 @@ function unregister(rig) {
|
|
|
1081
1961
|
|
|
1082
1962
|
const BLEND_MS = 160;
|
|
1083
1963
|
const AIM_K = 140; // px: the cursor distance at which the eyes travel half way
|
|
1964
|
+
const ARM_GAP_PX = 6; // px: how far short of what it points at a fingertip stops, unless the host says
|
|
1084
1965
|
const GLANCES = [[-0.8, -0.35], [0.85, -0.25], [0.45, 0.6], [-0.5, 0.55], [0.1, -0.7]];
|
|
1085
1966
|
const TRACK_TAU = 200; // the v1 lerp of .08 per 60 Hz frame
|
|
1086
1967
|
const SETTLE_UNITS = 0.25; // gaze closer than this (viewBox units, a tenth of a pixel at 120 px) has arrived
|
|
@@ -1102,7 +1983,7 @@ export function createMascot(container, options) {
|
|
|
1102
1983
|
}
|
|
1103
1984
|
const {
|
|
1104
1985
|
width = 128, height = 64, rangeX, rangeY, className = 'synabun-mascot',
|
|
1105
|
-
pose: firstPose = 'idle', poseOptions: firstOptions, active: startActive,
|
|
1986
|
+
pose: firstPose = 'idle', poseOptions: firstOptions, active: startActive, arms: armsOption,
|
|
1106
1987
|
} = opts || {};
|
|
1107
1988
|
const markup = mascotSvg({ width, height, className });
|
|
1108
1989
|
let svg;
|
|
@@ -1149,6 +2030,9 @@ export function createMascot(container, options) {
|
|
|
1149
2030
|
let stopPointer = null;
|
|
1150
2031
|
let stopKey = null;
|
|
1151
2032
|
let wakeTimer = 0;
|
|
2033
|
+
// Arms only on a character big enough to read them (the empty state's, 120 px): never a mini, a cameo or a still.
|
|
2034
|
+
const armsAllowed = armsOption != null ? !!armsOption : Number(width) >= 96;
|
|
2035
|
+
let arms = null; // { sim, parts, target, unitPx, box, frame, stopWatch } once point() was first called
|
|
1152
2036
|
|
|
1153
2037
|
const def = () => POSES[pose.name];
|
|
1154
2038
|
const animating = () => !destroyed && active && visible && !isReducedMotion();
|
|
@@ -1219,12 +2103,15 @@ export function createMascot(container, options) {
|
|
|
1219
2103
|
function nextRate() {
|
|
1220
2104
|
if (!animating()) return 0;
|
|
1221
2105
|
if (blend || blinkStart >= 0 || reactions.length || aim || easing() || miniPopping()) return 60;
|
|
2106
|
+
const armRate = arms?.sim.awake() ? (arms.sim.settled() ? 30 : 60) : 0;
|
|
2107
|
+
if (armRate === 60) return 60;
|
|
1222
2108
|
const d = def();
|
|
1223
2109
|
const t = poseTime();
|
|
1224
|
-
|
|
1225
|
-
if (d.motion === '
|
|
1226
|
-
if (d.motion === '
|
|
1227
|
-
|
|
2110
|
+
let rate = 0;
|
|
2111
|
+
if (d.motion === 'loop') rate = 30;
|
|
2112
|
+
else if (d.motion === 'blink') rate = d.idleFor(t) > 0 ? 0 : 30;
|
|
2113
|
+
else if (d.motion === 'once') rate = t < d.settle ? 60 : 0;
|
|
2114
|
+
return Math.max(rate, armRate);
|
|
1228
2115
|
}
|
|
1229
2116
|
|
|
1230
2117
|
function clearWake() {
|
|
@@ -1359,7 +2246,15 @@ export function createMascot(container, options) {
|
|
|
1359
2246
|
clearTempClasses();
|
|
1360
2247
|
look = def().look ? { ...hostLook } : { x: 0, y: 0 };
|
|
1361
2248
|
lastFrame = stillFrameNow();
|
|
1362
|
-
|
|
2249
|
+
if (arms) { arms.sim.prop(lastFrame?.prop?.name || null); arms.sim.finish(); }
|
|
2250
|
+
paintFace(lastFrame);
|
|
2251
|
+
if (arms) paintArms();
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
/** Paint a face frame; while an arm is out its eyes keep to MASCOT_ARM.gaze (never toward an arm). */
|
|
2255
|
+
function paintFace(f) {
|
|
2256
|
+
if (arms) arms.sim.prop(f?.prop?.name || null); // an arm whose shoulder this prop covers roots under it
|
|
2257
|
+
painter.paint(arms ? armGaze(f, arms.sim.presence()) : f);
|
|
1363
2258
|
}
|
|
1364
2259
|
|
|
1365
2260
|
function compose() {
|
|
@@ -1430,7 +2325,9 @@ export function createMascot(container, options) {
|
|
|
1430
2325
|
handleDue();
|
|
1431
2326
|
if (aim) { aimAt(aim); aim = null; }
|
|
1432
2327
|
easeLook(dt);
|
|
1433
|
-
|
|
2328
|
+
if (arms) arms.sim.step(dt / 1000);
|
|
2329
|
+
paintFace(compose());
|
|
2330
|
+
if (arms) paintArms();
|
|
1434
2331
|
const rate = nextRate();
|
|
1435
2332
|
setRate(rate);
|
|
1436
2333
|
if (!rate) armWake();
|
|
@@ -1461,7 +2358,7 @@ export function createMascot(container, options) {
|
|
|
1461
2358
|
const rate = nextRate();
|
|
1462
2359
|
setRate(rate);
|
|
1463
2360
|
if (!rate) {
|
|
1464
|
-
|
|
2361
|
+
paintFace(compose());
|
|
1465
2362
|
armWake();
|
|
1466
2363
|
} else clearWake();
|
|
1467
2364
|
}
|
|
@@ -1492,6 +2389,162 @@ export function createMascot(container, options) {
|
|
|
1492
2389
|
if (!destroyed && pose.name === 'sleep') setPose('idle');
|
|
1493
2390
|
}
|
|
1494
2391
|
|
|
2392
|
+
// ── Arms (rig.point): measured once per target, stepped on this rig's own frames ──
|
|
2393
|
+
|
|
2394
|
+
function ensureArms() {
|
|
2395
|
+
if (arms) return arms;
|
|
2396
|
+
const face = svg.querySelector('.syna-face');
|
|
2397
|
+
if (!face) return null;
|
|
2398
|
+
face.insertAdjacentHTML('afterbegin', '<g class="syna-arms">'
|
|
2399
|
+
+ ['left', 'right'].map(side => `<g class="syna-arm" data-side="${side}" display="none"><path class="syna-arm-noodle" fill="currentColor" fill-rule="nonzero"/></g>`).join('')
|
|
2400
|
+
+ '</g>');
|
|
2401
|
+
const parts = Array.from(face.firstElementChild.children).map(g => ({ g, noodle: g.firstElementChild, shown: false, d: '', reach: '' }));
|
|
2402
|
+
// box / rect: this SVG's and the target's client rects as last read (shift() moves the box without a read).
|
|
2403
|
+
arms = { sim: createArmSim(), parts, target: null, gap: ARM_GAP_PX, unitPx: 0, box: null, rect: null, frame: 0, stopWatch: null };
|
|
2404
|
+
arms.sim.prop((lastFrame || stillFrameNow())?.prop?.name || null);
|
|
2405
|
+
return arms;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
/** Draw what the sim holds: one path per arm (noodle and hand), written only when it changed; `data-reach` is the arm's e. */
|
|
2409
|
+
function paintArms() {
|
|
2410
|
+
if (!arms) return;
|
|
2411
|
+
const frame = arms.sim.frame();
|
|
2412
|
+
const unitPx = arms.unitPx || MASCOT_ARM.unitPx;
|
|
2413
|
+
frame.forEach((f, side) => {
|
|
2414
|
+
const part = arms.parts[side];
|
|
2415
|
+
const d = f ? armDrawPath(f, side, { unitPx }) : '';
|
|
2416
|
+
if (!d) {
|
|
2417
|
+
if (part.shown) { part.g.setAttribute('display', 'none'); part.shown = false; }
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
if (!part.shown) { part.g.removeAttribute('display'); part.shown = true; }
|
|
2421
|
+
if (d !== part.d) { part.d = d; part.noodle.setAttribute('d', d); }
|
|
2422
|
+
const reach = f.e.toFixed(3);
|
|
2423
|
+
if (reach !== part.reach) { part.reach = reach; part.g.setAttribute('data-reach', reach); }
|
|
2424
|
+
});
|
|
2425
|
+
if (!arms.sim.awake() && !arms.target) stopArmWatch();
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
/** A target's client rect: an element (read now), a rect, or a point. */
|
|
2429
|
+
function targetBox(target) {
|
|
2430
|
+
if (!target) return null;
|
|
2431
|
+
if (typeof target.getBoundingClientRect === 'function') {
|
|
2432
|
+
if (target.isConnected === false) return null;
|
|
2433
|
+
const r = target.getBoundingClientRect();
|
|
2434
|
+
return r.width || r.height ? r : null;
|
|
2435
|
+
}
|
|
2436
|
+
if (Number.isFinite(target.left) && Number.isFinite(target.top)) {
|
|
2437
|
+
const w = Number(target.width) || Math.max(0, (Number(target.right) || target.left) - target.left);
|
|
2438
|
+
const h = Number(target.height) || Math.max(0, (Number(target.bottom) || target.top) - target.top);
|
|
2439
|
+
return { left: target.left, top: target.top, right: target.left + w, bottom: target.top + h, width: w, height: h };
|
|
2440
|
+
}
|
|
2441
|
+
if (Number.isFinite(target.x) && Number.isFinite(target.y)) return { left: target.x, top: target.y, right: target.x, bottom: target.y, width: 0, height: 0 };
|
|
2442
|
+
return null;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
/**
|
|
2446
|
+
* Two layout reads (this SVG and the target), never per frame: then aim
|
|
2447
|
+
* (aimArms) from what was read.
|
|
2448
|
+
*/
|
|
2449
|
+
function measureArms() {
|
|
2450
|
+
if (!arms?.target || destroyed) return;
|
|
2451
|
+
const box = svg.getBoundingClientRect();
|
|
2452
|
+
const r = targetBox(arms.target);
|
|
2453
|
+
if (!box.width || !r) return;
|
|
2454
|
+
arms.box = { left: box.left, top: box.top, width: box.width, height: box.height };
|
|
2455
|
+
arms.rect = { left: r.left, top: r.top, right: r.right, bottom: r.bottom };
|
|
2456
|
+
aimArms();
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
/**
|
|
2460
|
+
* From the last rects, no layout read: where the target is in face units, the
|
|
2461
|
+
* arm on its side aims there (its index at the target's middle, its fingertip
|
|
2462
|
+
* stopping `gap` px short of it) and the eyes look the same way.
|
|
2463
|
+
*/
|
|
2464
|
+
function aimArms() {
|
|
2465
|
+
const box = arms?.box;
|
|
2466
|
+
const r = arms?.rect;
|
|
2467
|
+
if (!box || !r) return;
|
|
2468
|
+
const k = box.width / 280; // CSS px per face unit: the viewBox is 280 wide, the SVG keeps its ratio
|
|
2469
|
+
arms.unitPx = k;
|
|
2470
|
+
const rect = { left: (r.left - box.left) / k, top: (r.top - box.top) / k, right: (r.right - box.left) / k, bottom: (r.bottom - box.top) / k };
|
|
2471
|
+
const side = armSide({ x: (rect.left + rect.right) / 2 });
|
|
2472
|
+
const aimAt = armTargeting(side, rect, arms.gap / k, arms.sim.root(side));
|
|
2473
|
+
if (!aimAt) return;
|
|
2474
|
+
arms.sim.aim(side, aimAt.angle, { target: aimAt.focus, reach: aimAt.reach });
|
|
2475
|
+
// The eyes go the same way, through the rig's own gaze.
|
|
2476
|
+
const vx = (r.left + r.right) / 2 - (box.left + box.width / 2);
|
|
2477
|
+
const vy = (r.top + r.bottom) / 2 - (box.top + box.height / 2);
|
|
2478
|
+
const dist = Math.hypot(vx, vy);
|
|
2479
|
+
if (dist >= 1) {
|
|
2480
|
+
const m = (dist / (dist + AIM_K)) * 1.25;
|
|
2481
|
+
lookAt((vx / dist) * m, (vy / dist) * m);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
/** While pointing, a resize or a scroll measures again on the next frame (coalesced). */
|
|
2486
|
+
function watchArms() {
|
|
2487
|
+
if (!arms || arms.stopWatch || typeof window === 'undefined') return;
|
|
2488
|
+
const again = () => {
|
|
2489
|
+
if (!arms || arms.frame) return;
|
|
2490
|
+
arms.frame = requestAnimationFrame(() => { if (!arms) return; arms.frame = 0; measureArms(); wakeArms(); });
|
|
2491
|
+
};
|
|
2492
|
+
window.addEventListener('resize', again, { passive: true });
|
|
2493
|
+
document.addEventListener('scroll', again, { passive: true, capture: true });
|
|
2494
|
+
arms.stopWatch = () => {
|
|
2495
|
+
window.removeEventListener('resize', again);
|
|
2496
|
+
document.removeEventListener('scroll', again, true);
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
function stopArmWatch() {
|
|
2501
|
+
if (!arms?.stopWatch) return;
|
|
2502
|
+
arms.stopWatch();
|
|
2503
|
+
arms.stopWatch = null;
|
|
2504
|
+
if (arms.frame) { cancelAnimationFrame(arms.frame); arms.frame = 0; }
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
function wakeArms() {
|
|
2508
|
+
if (destroyed || !arms) return;
|
|
2509
|
+
if (!animating()) {
|
|
2510
|
+
const f = lastFrame || stillFrameNow();
|
|
2511
|
+
arms.sim.prop(f?.prop?.name || null);
|
|
2512
|
+
arms.sim.finish();
|
|
2513
|
+
paintArms();
|
|
2514
|
+
paintFace(f);
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
syncClock();
|
|
2518
|
+
ensureRate(arms.sim.settled() ? 30 : 60);
|
|
2519
|
+
requestFrames(BLEND_MS);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/**
|
|
2523
|
+
* Point at `target` (an element, a client rect or a client point) with the
|
|
2524
|
+
* arm on its side; null puts the arms away. `gap` (CSS px, default
|
|
2525
|
+
* ARM_GAP_PX) is how far short of the target the fingertip stops (a host that
|
|
2526
|
+
* rings the target keeps it outside the ring). Pointing again (the same target
|
|
2527
|
+
* or a new one) measures again and carries on from the springs' state.
|
|
2528
|
+
*/
|
|
2529
|
+
function point(target, { poke: andPoke = false, gap } = {}) {
|
|
2530
|
+
if (destroyed || !armsAllowed) return api;
|
|
2531
|
+
if (target == null) {
|
|
2532
|
+
if (!arms) return api;
|
|
2533
|
+
arms.target = null;
|
|
2534
|
+
arms.sim.release();
|
|
2535
|
+
wakeArms();
|
|
2536
|
+
return api;
|
|
2537
|
+
}
|
|
2538
|
+
if (!ensureArms()) return api;
|
|
2539
|
+
if (gap != null || target !== arms.target) arms.gap = Number(gap) >= 0 ? Number(gap) : ARM_GAP_PX;
|
|
2540
|
+
arms.target = target;
|
|
2541
|
+
measureArms();
|
|
2542
|
+
watchArms();
|
|
2543
|
+
if (andPoke) arms.sim.poke();
|
|
2544
|
+
wakeArms();
|
|
2545
|
+
return api;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
1495
2548
|
// ── Public API ──
|
|
1496
2549
|
|
|
1497
2550
|
function setPose(name, options2) {
|
|
@@ -1577,6 +2630,7 @@ export function createMascot(container, options) {
|
|
|
1577
2630
|
stopKey?.();
|
|
1578
2631
|
stopKey = null;
|
|
1579
2632
|
unobserve(svg);
|
|
2633
|
+
stopArmWatch();
|
|
1580
2634
|
svg.removeEventListener('animationend', onAnimationEnd);
|
|
1581
2635
|
clearTempClasses();
|
|
1582
2636
|
reactions.length = 0;
|
|
@@ -1608,6 +2662,58 @@ export function createMascot(container, options) {
|
|
|
1608
2662
|
setPose,
|
|
1609
2663
|
react,
|
|
1610
2664
|
setActive,
|
|
2665
|
+
point,
|
|
2666
|
+
/** Two quick pokes toward what the arm points at (the beat landed). */
|
|
2667
|
+
poke() {
|
|
2668
|
+
if (destroyed || !arms?.target) return api;
|
|
2669
|
+
arms.sim.poke();
|
|
2670
|
+
wakeArms();
|
|
2671
|
+
return api;
|
|
2672
|
+
},
|
|
2673
|
+
/**
|
|
2674
|
+
* The body was jolted: its velocity changed by (vx, vy) CSS px/s (a host
|
|
2675
|
+
* moving the character's box: + on take-off, − on landing). The arms keep
|
|
2676
|
+
* part of their old speed, so they trail and then wobble back.
|
|
2677
|
+
*/
|
|
2678
|
+
nudge(vx, vy) {
|
|
2679
|
+
if (destroyed || !arms?.unitPx || !animating()) return api;
|
|
2680
|
+
arms.sim.nudge((Number(vx) || 0) / arms.unitPx, (Number(vy) || 0) / arms.unitPx);
|
|
2681
|
+
wakeArms();
|
|
2682
|
+
return api;
|
|
2683
|
+
},
|
|
2684
|
+
/**
|
|
2685
|
+
* The host moved the character's box by (dx, dy) CSS px since the last
|
|
2686
|
+
* measure (its own transform: no layout read here): the hand keeps
|
|
2687
|
+
* pointing at the same place on the page.
|
|
2688
|
+
*/
|
|
2689
|
+
shift(dx, dy) {
|
|
2690
|
+
if (destroyed || !arms?.target || !arms.box) return api;
|
|
2691
|
+
const x = Number(dx) || 0;
|
|
2692
|
+
const y = Number(dy) || 0;
|
|
2693
|
+
if (!x && !y) return api;
|
|
2694
|
+
arms.box.left += x;
|
|
2695
|
+
arms.box.top += y;
|
|
2696
|
+
aimArms();
|
|
2697
|
+
wakeArms();
|
|
2698
|
+
return api;
|
|
2699
|
+
},
|
|
2700
|
+
/** The client box the pointing arm takes once grown (from the last measure: no layout read), or null. */
|
|
2701
|
+
armExtent() {
|
|
2702
|
+
if (destroyed || !arms?.target || !arms.box) return null;
|
|
2703
|
+
const e = arms.sim.extent();
|
|
2704
|
+
if (!e) return null;
|
|
2705
|
+
const k = arms.box.width / 280;
|
|
2706
|
+
return { left: arms.box.left + e.left * k, top: arms.box.top + e.top * k, right: arms.box.left + e.right * k, bottom: arms.box.top + e.bottom * k };
|
|
2707
|
+
},
|
|
2708
|
+
/** Read the rects again (the host's move has landed, its scale settled) and aim from them. */
|
|
2709
|
+
remeasure() {
|
|
2710
|
+
if (destroyed || !arms?.target) return api;
|
|
2711
|
+
measureArms();
|
|
2712
|
+
wakeArms();
|
|
2713
|
+
return api;
|
|
2714
|
+
},
|
|
2715
|
+
/** What the arms point at (null when they are away). */
|
|
2716
|
+
get pointing() { return arms?.target || null; },
|
|
1611
2717
|
/** The current pose name. */
|
|
1612
2718
|
get pose() { return pose.name; },
|
|
1613
2719
|
/** The frame rate this rig asks of the ticker right now (0 = still). */
|