quoxcli 0.2.0 → 0.4.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/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +46 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +14 -0
- package/lib/api/endpoints/contactsq.js +6 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +44 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +26 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +179 -0
- package/lib/api/endpoints/objectives.js +12 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +12 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +120 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +131 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +170 -27
- package/lib/commands/agentAttach.js +140 -13
- package/lib/commands/approvals.js +78 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +146 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +180 -26
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +35 -38
- package/lib/commands/contacts.js +134 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +107 -6
- package/lib/commands/evidence.js +154 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +336 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +254 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/kuma.js +1050 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +52 -155
- package/lib/commands/loop.js +291 -0
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +59 -1
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +30 -3
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +217 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +69 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/research.js +79 -0
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +367 -0
- package/lib/commands/run.js +82 -10
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +405 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +68 -9
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actingAs.js +2 -2
- package/lib/core/actionItems.js +115 -0
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +152 -35
- package/lib/core/commandRegistrars.js +130 -0
- package/lib/core/config.js +143 -25
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +105 -5
- package/lib/core/governedSession.js +1 -3
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +533 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +11 -11
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/shortcut.js +83 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +366 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/EXTENSIONS.md +23 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +690 -25
- package/lib/quoxon/app.js +3586 -250
- package/lib/quoxon/approvalFacts.js +96 -0
- package/lib/quoxon/approvalSentence.js +44 -0
- package/lib/quoxon/approvalsView.js +384 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +124 -0
- package/lib/quoxon/commands.js +429 -39
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +20 -0
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +46 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +192 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonCompanion.js +255 -0
- package/lib/quoxon/dragonModes.js +151 -0
- package/lib/quoxon/dragonMoments.js +82 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +266 -26
- package/lib/quoxon/extensions.js +276 -0
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/inboxView.js +79 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +51 -8
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/loopCard.js +456 -0
- package/lib/quoxon/loopCards.js +180 -0
- package/lib/quoxon/loopStream.js +87 -0
- package/lib/quoxon/main.js +101 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +222 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +46 -7
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +59 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +12 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +62 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +75 -37
- package/lib/quoxon/render.js +740 -124
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +117 -17
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +954 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +182 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +94 -0
- package/lib/quoxon/slash/cliLeaves.json +1023 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +22 -8
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +95 -13
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +90 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +134 -0
- package/lib/quoxon/windowOrg.js +123 -0
- package/lib/server/devServers.js +137 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +5 -1
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- package/vendor/dragonlab/design/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +88 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
- package/lib/core/websiteSession.js +0 -148
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** The SVG is the anatomy source. Q's existing sampler and packer stay intact. */
|
|
2
|
+
import {movePoint} from './motion.mjs';
|
|
3
|
+
import { makeField, fieldToCells } from '../../../vendor/quox-dragon/qart.js';
|
|
4
|
+
import { flattenPath, sampleShape } from '../../stage3/svg-coverage.js';
|
|
5
|
+
|
|
6
|
+
const CUTOUTS = new Set(['eye-socket', 'pupil', 'nostril', 'mouth-line']);
|
|
7
|
+
const IRIS = new Set(['iris', 'iris-light']);
|
|
8
|
+
const EYE = new Set(['eye-socket','eye-rim','iris','iris-light','pupil','eye-glint','lower-lid']);
|
|
9
|
+
const attr = (tag, key) => tag.match(new RegExp(`(?:^|\\s)${key}="([^"]*)"`))?.[1];
|
|
10
|
+
const rgb = hex => [1,3,5].map(i => parseInt(hex.slice(i,i+2),16));
|
|
11
|
+
|
|
12
|
+
export function parseMaster(svg) {
|
|
13
|
+
if (!svg.includes('viewBox="0 0 800 480"')) throw new Error('Expected 800×480 master.');
|
|
14
|
+
const ids = new Set(), groups=new Map(), stack=[];
|
|
15
|
+
for(const [tag] of svg.matchAll(/<g\s[^>]*>|<\/g>|<path\s[^>]*\/>/g)){
|
|
16
|
+
if(tag.startsWith('<g '))stack.push(attr(tag,'id'));
|
|
17
|
+
else if(tag==='</g>')stack.pop();
|
|
18
|
+
else groups.set(attr(tag,'id'),stack.at(-1));
|
|
19
|
+
}
|
|
20
|
+
const paths = [...svg.matchAll(/<path\s[^>]*\/>/g)].map(([tag]) => {
|
|
21
|
+
const id = attr(tag,'id'), d = attr(tag,'d');
|
|
22
|
+
const stroke = attr(tag,'stroke'), fill = attr(tag,'fill');
|
|
23
|
+
if (!id || ids.has(id) || !d) throw new Error('Every path needs a unique id and geometry.');
|
|
24
|
+
ids.add(id);
|
|
25
|
+
const colour = stroke || fill;
|
|
26
|
+
if (!/^#[0-9a-f]{6}$/i.test(colour)) throw new Error(`Unsupported colour: ${id}`);
|
|
27
|
+
return {id, group:groups.get(id), polygons:flattenPath(d), rgb:rgb(colour), stroke:stroke ? Number(attr(tag,'stroke-width')) : 0};
|
|
28
|
+
});
|
|
29
|
+
if (!paths.length || !ids.has('iris') || !ids.has('tail-mass')) throw new Error('Incomplete master.');
|
|
30
|
+
return paths;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function renderMaster(paths, {cols=39, rows=11, mono=false, eyes='white', face={}, motion=null}={}) {
|
|
34
|
+
if (![cols,rows].every(Number.isSafeInteger) || cols<28 || rows<10 || cols>200 || rows>72)
|
|
35
|
+
throw new RangeError('Full dragon requires 28–200 columns and 10–72 rows.');
|
|
36
|
+
if (!['white','violet'].includes(eyes)) throw new RangeError('Eyes must be white or violet.');
|
|
37
|
+
const W=cols*2, H=rows*4, field=makeField(W,H);
|
|
38
|
+
const colours=new Uint8Array(W*H*3), priorities=new Uint8Array(W*H);
|
|
39
|
+
const labels=new Array(W*H).fill('');
|
|
40
|
+
// Fit the authored ink bounds, rather than its generously padded SVG canvas.
|
|
41
|
+
const points=paths.flatMap(p=>p.polygons.flat());
|
|
42
|
+
const xs=points.map(p=>p[0]),ys=points.map(p=>p[1]);
|
|
43
|
+
const left=Math.min(...xs),top=Math.min(...ys);
|
|
44
|
+
const width=Math.max(...xs)-left,height=Math.max(...ys)-top;
|
|
45
|
+
const scale=Math.min((W-4)/width,(H-4)/height);
|
|
46
|
+
const geometry={W,H,scale,ox:(W-width*scale)/2-left*scale,oy:(H-height*scale)/2-top*scale};
|
|
47
|
+
const gx=face.gx??0,gy=face.gy??0,open=face.open??1;
|
|
48
|
+
if(![gx,gy,open].every(Number.isFinite)||Math.abs(gx)>1||Math.abs(gy)>1||open<0.1||open>1.25)throw new RangeError('Invalid eye pose.');
|
|
49
|
+
const coverage=path => {
|
|
50
|
+
const result=[];
|
|
51
|
+
let polygons=['iris','pupil'].includes(path.id) ? path.polygons.map(poly=>poly.map(([x,y])=>[x+(path.id==='pupil'?gx*12:0),177+(y-177)*open+(path.id==='pupil'?gy*5*open:0)])) : path.polygons;
|
|
52
|
+
if(motion)polygons=polygons.map(poly=>poly.map(([x,y])=>movePoint(path.group,x,y,motion)));
|
|
53
|
+
sampleShape(polygons,{...geometry,stroke:path.stroke},(x,y)=>result.push(y*W+x));
|
|
54
|
+
// At compact resolution a sub-dot pupil can otherwise disappear as the
|
|
55
|
+
// head moves between samples. Keep one centre dot inside the eye white.
|
|
56
|
+
if(path.id==='pupil'&&!result.length&&open>.3){
|
|
57
|
+
const [cx,cy]=movePoint(path.group,178+gx*12,177+gy*5*open,motion);
|
|
58
|
+
const px=geometry.ox+cx*scale,py=geometry.oy+cy*scale;
|
|
59
|
+
let best=-1,distance=Infinity;
|
|
60
|
+
for(let i=0;i<labels.length;i++)if(labels[i]==='iris'&&field.v[i]){
|
|
61
|
+
const d=(i%W+.5-px)**2+(Math.floor(i/W)+.5-py)**2;
|
|
62
|
+
if(d<distance){best=i;distance=d;}
|
|
63
|
+
}
|
|
64
|
+
if(best>=0)result.push(best);
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
};
|
|
68
|
+
const owned=new Set();
|
|
69
|
+
const socket=paths.find(p=>p.id==='eye-socket');
|
|
70
|
+
for (const i of coverage(socket)) owned.add(Math.floor((i%W)/2)+Math.floor(Math.floor(i/W)/4)*cols);
|
|
71
|
+
for (const path of paths) {
|
|
72
|
+
if (path.id==='eye-socket') {
|
|
73
|
+
// Same whole-cell ownership principle as Q's eyes: remove body under
|
|
74
|
+
// every socket cell before painting the eye; no purple body bleed.
|
|
75
|
+
for (const cell of owned) {
|
|
76
|
+
const x=(cell%cols)*2,y=Math.floor(cell/cols)*4;
|
|
77
|
+
for(let dy=0;dy<4;dy++)for(let dx=0;dx<2;dx++) {
|
|
78
|
+
const i=(y+dy)*W+x+dx;field.v[i]=0;labels[i]='';priorities[i]=0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const i of coverage(path)) {
|
|
83
|
+
const erase=CUTOUTS.has(path.id);
|
|
84
|
+
field.v[i]=erase?0:1;
|
|
85
|
+
colours.set(IRIS.has(path.id) ? (eyes==='white' ? [242,246,255] : [182,107,255]) : path.id==='eye-rim' ? [242,246,255] : path.rgb,i*3);labels[i]=path.id;
|
|
86
|
+
priorities[i]=IRIS.has(path.id)?5:EYE.has(path.id)?3:1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Binary coverage means mono and colour produce identical Braille masks.
|
|
90
|
+
const frame=fieldToCells(field,{color:false,quant:false});
|
|
91
|
+
let irisCells=0, eyeCutoutDots=0, pupilDots=labels.filter(x=>x==='pupil').length, ink=0;
|
|
92
|
+
for(let y=0;y<rows;y++)for(let x=0;x<cols;x++) {
|
|
93
|
+
const cell=frame[y][x];cell.bg=null;
|
|
94
|
+
let winner=-1,rank=-1;
|
|
95
|
+
for(let dy=0;dy<4;dy++)for(let dx=0;dx<2;dx++) {
|
|
96
|
+
const i=(y*4+dy)*W+x*2+dx;
|
|
97
|
+
if(field.v[i] && priorities[i]>rank){winner=i;rank=priorities[i];}
|
|
98
|
+
if(owned.has(y*cols+x)&&!field.v[i])eyeCutoutDots++;
|
|
99
|
+
}
|
|
100
|
+
if(winner>=0) {
|
|
101
|
+
ink++;
|
|
102
|
+
if(IRIS.has(labels[winner]))irisCells++;
|
|
103
|
+
cell.rgb=mono?null:Array.from(colours.slice(winner*3,winner*3+3));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {frame,metrics:{cols,rows,ink,irisCells,eyeOwnedCells:owned.size,eyeCutoutDots,pupilDots,scale},geometry};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Exact emitted Braille masks, not a font screenshot or a source SVG raster. */
|
|
110
|
+
export function cellsSvg(frame, {label='Lucid · actual emitted dots',dot=4}={}) {
|
|
111
|
+
const cols=frame[0].length,rows=frame.length,width=cols*dot*2+48,height=rows*dot*4+76;
|
|
112
|
+
const bits=[[1,8],[2,16],[4,32],[64,128]];
|
|
113
|
+
let body=`<rect width="100%" height="100%" fill="#080d19"/><text x="24" y="27" fill="#a7b6cc" font-family="monospace" font-size="13">${label.replaceAll('&','&').replaceAll('<','<')}</text>`;
|
|
114
|
+
for(let y=0;y<rows;y++)for(let x=0;x<cols;x++) {
|
|
115
|
+
const cell=frame[y][x],mask=cell.ch===' '?0:cell.ch.codePointAt(0)-0x2800;
|
|
116
|
+
const fill=cell.rgb?`rgb(${cell.rgb.join(',')})`:'#d7e1ef';
|
|
117
|
+
for(let dy=0;dy<4;dy++)for(let dx=0;dx<2;dx++)if(mask&bits[dy][dx])
|
|
118
|
+
body+=`<circle cx="${24+(x*2+dx+.5)*dot}" cy="${50+(y*4+dy+.5)*dot}" r="${dot*.39}" fill="${fill}"/>`;
|
|
119
|
+
}
|
|
120
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${body}</svg>\n`;
|
|
121
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {readFile,writeFile,mkdir} from 'node:fs/promises';
|
|
3
|
+
import {resolve} from 'node:path';
|
|
4
|
+
import {parseMaster,renderMaster,cellsSvg} from './render.mjs';
|
|
5
|
+
import {LUCID_CLIPS,renderLucidCharacter,renderLucidStory,STORY_DURATION} from './character.mjs';
|
|
6
|
+
import {artSlot,composeMessage} from './layout.mjs';
|
|
7
|
+
import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
|
|
8
|
+
|
|
9
|
+
const args=process.argv.slice(2);
|
|
10
|
+
function option(name,fallback){const i=args.indexOf(name);return i<0?fallback:args[i+1];}
|
|
11
|
+
const allowed=new Set(['--cols','--rows','--mono','--export','--verify','--help','--message','--eyes','--terminal-cols','--terminal-rows','--large','--intro','--scene','--at','--watch','--list','--still','--attach']);
|
|
12
|
+
for(let i=0;i<args.length;i++) {
|
|
13
|
+
if(!allowed.has(args[i]))throw new Error(`Unknown option: ${args[i]}`);
|
|
14
|
+
if(['--cols','--rows','--export','--message','--eyes','--terminal-cols','--terminal-rows','--scene','--at'].includes(args[i])) {
|
|
15
|
+
if(!args[i+1]||args[i+1].startsWith('--'))throw new Error(`Missing value for ${args[i]}`);
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if(args.includes('--help')) {
|
|
20
|
+
console.log('Lucid full-body SVG review (Node 20+, no install)\n node design/lucid/recovery/review.mjs [--message "Your message"] [--eyes white|violet] [--mono]\n --attach Arrival followed by quiet idle, requires TTY\n --still Freeze the whole character at its scene pose\n --large Large still for deck review\n --intro Large animated story, requires TTY\n --watch Companion-sized story, requires TTY\n --scene ID --at MS Inspect one expression/power\n --list List clips\n --export DIR Write exact-cell SVG/JSON/TXT previews at compact and large sizes\n --verify Check deterministic masks, safe margins and white default eyes');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
if(args.includes('--list')){console.log(LUCID_CLIPS.map(c=>c.id.padEnd(16)+c.title).join('\n'));process.exit(0);}
|
|
24
|
+
const source=await readFile(new URL('./lucid-master.svg',import.meta.url),'utf8');
|
|
25
|
+
const paths=parseMaster(source);
|
|
26
|
+
if(args.includes('--verify')) {
|
|
27
|
+
const {verify}=await import('./verify.mjs');verify(paths);process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
const destination=option('--export',null);
|
|
30
|
+
if(destination) {
|
|
31
|
+
await mkdir(resolve(destination),{recursive:true});
|
|
32
|
+
const metrics=[];
|
|
33
|
+
for(const [cols,rows] of [[28,10],[36,10],[40,12],[48,14],[88,25]])for(const mono of [false,true]) {
|
|
34
|
+
const result=renderMaster(paths,{cols,rows,mono}),stem=`lucid-${cols}x${rows}-${mono?'mono':'colour'}`;
|
|
35
|
+
const label=`LUCID / ${cols}x${rows} / ${mono?'MONO':'COLOUR'} / exact emitted Braille dots`;
|
|
36
|
+
await writeFile(resolve(destination,stem+'.svg'),cellsSvg(result.frame,{label}));
|
|
37
|
+
await writeFile(resolve(destination,stem+'.json'),JSON.stringify(result,null,2)+'\n');
|
|
38
|
+
await writeFile(resolve(destination,stem+'.txt'),cellsToAnsi(result.frame).join('\n')+'\n');
|
|
39
|
+
metrics.push({...result.metrics,mono});
|
|
40
|
+
}
|
|
41
|
+
await writeFile(resolve(destination,'metrics.json'),JSON.stringify(metrics,null,2)+'\n');
|
|
42
|
+
console.log(`Exported actual cells to ${resolve(destination)}`);
|
|
43
|
+
} else {
|
|
44
|
+
const terminalCols=Number(option('--terminal-cols',process.stdout.columns||80));
|
|
45
|
+
const terminalRows=Number(option('--terminal-rows',process.stdout.rows||24));
|
|
46
|
+
const slot=artSlot(terminalCols,terminalRows);
|
|
47
|
+
const large=args.includes('--large')||args.includes('--intro');
|
|
48
|
+
const cols=Number(option('--cols',large?Math.min(120,terminalCols-4):slot.cols)),rows=Number(option('--rows',large?Math.min(36,terminalRows-4):slot.rows));
|
|
49
|
+
const explicit=large||args.includes('--cols')||args.includes('--rows');
|
|
50
|
+
if(explicit&&(cols>terminalCols-4||rows>terminalRows-2))
|
|
51
|
+
throw new Error('Requested art exceeds terminal margins. Widen the terminal or reduce --cols/--rows.');
|
|
52
|
+
const message=option('--message','LUCID\nAware. Attentive. Present.');
|
|
53
|
+
if(!slot.showArt&&!explicit) {
|
|
54
|
+
console.log(composeMessage(null,message,{terminalCols}).join('\n'));
|
|
55
|
+
} else {
|
|
56
|
+
const opts={cols,rows,mono:args.includes('--mono'),eyes:option('--eyes','white'),still:args.includes('--still')};
|
|
57
|
+
const scene=option('--scene',args.includes('--attach')?(large?'deck-arrival':'attach'):'idle'),at=Number(option('--at',0));
|
|
58
|
+
const result=renderLucidCharacter(paths,scene,at,opts);
|
|
59
|
+
if(!result.ok)throw new Error(result.code);
|
|
60
|
+
if(args.includes('--watch')||args.includes('--intro')||args.includes('--attach')){
|
|
61
|
+
const {watch}=await import('./watch.mjs');
|
|
62
|
+
await watch(paths,opts,{terminalCols,terminalRows,scene:args.includes('--scene')||args.includes('--attach')?scene:null,arrival:args.includes('--attach'),large,message:args.includes('--message')?message:null});
|
|
63
|
+
}else{
|
|
64
|
+
const caption=args.includes('--message')?message:'LUCID\n'+result.caption;
|
|
65
|
+
console.log((large?[...cellsToAnsi(result.frame),caption]:composeMessage(result.frame,caption,{terminalCols})).join('\n'));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {artSlot,composeMessage,wrapMessage,cleanText} from './layout.mjs';
|
|
3
|
+
import {LUCID_CLIPS,renderLucidCharacter,renderLucidStory,STORY_DURATION} from './character.mjs';
|
|
4
|
+
import {motionAt,movePoint,MOTION_PERIOD} from './motion.mjs';
|
|
5
|
+
import {renderMaster} from './render.mjs';
|
|
6
|
+
|
|
7
|
+
export function verify(paths) {
|
|
8
|
+
for(const [cols,rows] of [[28,10],[36,10],[40,12],[48,14],[88,25]]) {
|
|
9
|
+
const colour=renderMaster(paths,{cols,rows}),mono=renderMaster(paths,{cols,rows,mono:true});
|
|
10
|
+
assert.deepEqual(colour,renderMaster(paths,{cols,rows}),'Deterministic render');
|
|
11
|
+
assert.deepEqual(colour.frame.map(r=>r.map(c=>c.ch)),mono.frame.map(r=>r.map(c=>c.ch)),'Mono must retain geometry');
|
|
12
|
+
assert(colour.metrics.irisCells>0,'Actual iris must survive rasterisation');
|
|
13
|
+
assert(colour.metrics.pupilDots>0,'Round pupil survives');
|
|
14
|
+
assert(colour.metrics.eyeCutoutDots>0,'Eye needs negative space');
|
|
15
|
+
assert(colour.frame.flat().some(c=>c.rgb&&c.rgb.every(v=>v>=240)),'Emitted white eye foreground');
|
|
16
|
+
for(let y=0;y<rows;y++)for(let x=0;x<cols;x++) {
|
|
17
|
+
const c=colour.frame[y][x];
|
|
18
|
+
assert(c.bg===null,'Transparent terminal background');
|
|
19
|
+
assert(c.ch===' '||(c.ch.codePointAt(0)>=0x2800&&c.ch.codePointAt(0)<=0x28ff),'Braille only');
|
|
20
|
+
const mask=c.ch===' '?0:c.ch.codePointAt(0)-0x2800;
|
|
21
|
+
if(x===0)assert.equal(mask&71,0,'Left dot margin');
|
|
22
|
+
if(x===cols-1)assert.equal(mask&184,0,'Right dot margin');
|
|
23
|
+
if(y===0)assert.equal(mask&9,0,'Top dot margin');
|
|
24
|
+
if(y===rows-1)assert.equal(mask&192,0,'Bottom dot margin');
|
|
25
|
+
}
|
|
26
|
+
console.log(`${cols}x${rows}: ${colour.metrics.ink} ink cells; ${colour.metrics.irisCells} iris cells; verified`);
|
|
27
|
+
}
|
|
28
|
+
for(const [w,h] of [[64,24],[80,24],[100,30],[120,40],[52,20],[80,18]]) {
|
|
29
|
+
const slot=artSlot(w,h);
|
|
30
|
+
assert(slot.cols*slot.rows<w*h/4,'Art allocation below quarter-screen');
|
|
31
|
+
const frame=slot.showArt?renderMaster(paths,slot).frame:null;
|
|
32
|
+
const message='Lucid is watching. A change needs your attention. The rest of your terminal stays available.';
|
|
33
|
+
const lines=composeMessage(frame,message,{terminalCols:w});
|
|
34
|
+
assert(lines.every(l=>cleanText(l).length<=w),'No line wrapping beyond viewport');
|
|
35
|
+
if(frame)assert(lines.length===slot.rows,'Short message fits beside dragon');
|
|
36
|
+
console.log(`Layout ${w}x${h}: ${slot.showArt?slot.cols+'x'+slot.rows:'text fallback'}`);
|
|
37
|
+
}
|
|
38
|
+
assert.equal(cleanText('hello\x1b[2Jworld'),'helloworld');
|
|
39
|
+
assert(wrapMessage('界界界',4).length===2,'Wide characters respect cells');
|
|
40
|
+
for(const [cols,rows] of [[31,11],[39,11],[49,14],[88,25],[120,36]]){
|
|
41
|
+
for(const clip of LUCID_CLIPS){
|
|
42
|
+
const r=renderLucidCharacter(paths,clip.id,clip.duration/2,{cols,rows});
|
|
43
|
+
assert(r.ok&&r.frame.length===rows&&r.frame.every(row=>row.length===cols));
|
|
44
|
+
if(clip.power)assert(r.effectCells>0,'Visible power study');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
assert(renderLucidStory(paths,STORY_DURATION).ok);
|
|
48
|
+
assert(!renderLucidCharacter(paths,'missing',0).ok);
|
|
49
|
+
console.log(`${LUCID_CLIPS.length} acting clips verified at companion and intro sizes`);
|
|
50
|
+
assert.deepEqual(motionAt(0),motionAt(MOTION_PERIOD),'Seamless motion loop');
|
|
51
|
+
for(const t of [0,1200,2400,3200,5200,9000]){
|
|
52
|
+
const motion=motionAt(t);
|
|
53
|
+
for(const group of ['body','near-feet','far-feet'])assert.deepEqual(movePoint(group,300,380,motion),[300,380],'Planted torso and feet');
|
|
54
|
+
assert.deepEqual(movePoint('head',178,177,motion),movePoint('eye',178,177,motion),'Face stays attached');
|
|
55
|
+
for(const [cols,rows] of [[28,10],[31,11],[39,11],[49,14],[120,36]]){
|
|
56
|
+
const r=renderMaster(paths,{cols,rows,motion});
|
|
57
|
+
assert(r.metrics.pupilDots>0,`Pupil survives motion at ${cols}x${rows}, ${t}`);
|
|
58
|
+
assert.equal(r.metrics.scale,renderMaster(paths,{cols,rows}).metrics.scale,'No camera breathing');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for(const scene of ['attach','deck-arrival']){
|
|
62
|
+
const duration=scene==='attach'?3200:4400;
|
|
63
|
+
for(const [cols,rows] of [[31,11],[49,14],[120,36],[200,72]]){
|
|
64
|
+
const end=renderLucidCharacter(paths,scene,duration,{cols,rows});
|
|
65
|
+
assert.deepEqual(end.frame,renderLucidCharacter(paths,'idle',0,{cols,rows}).frame,'Exact idle handoff');
|
|
66
|
+
for(const t of [0,500,1000,1700,2400,3100]){
|
|
67
|
+
const r=renderLucidCharacter(paths,scene,t,{cols,rows});
|
|
68
|
+
assert(r.metrics.pupilDots>0,'Arrival pupil');
|
|
69
|
+
for(let y=0;y<rows;y++)for(let x=0;x<cols;x++){
|
|
70
|
+
const mask=r.frame[y][x].ch===' '?0:r.frame[y][x].ch.codePointAt(0)-0x2800;
|
|
71
|
+
if(x===0)assert.equal(mask&71,0);if(x===cols-1)assert.equal(mask&184,0);
|
|
72
|
+
if(y===0)assert.equal(mask&9,0);if(y===rows-1)assert.equal(mask&192,0);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for(const c of LUCID_CLIPS)assert.deepEqual(
|
|
78
|
+
renderLucidCharacter(paths,c.id,0,{still:true}).frame,
|
|
79
|
+
renderLucidCharacter(paths,c.id,c.duration*.7,{still:true}).frame,'Still freezes '+c.id);
|
|
80
|
+
assert.deepEqual(renderLucidStory(paths,0,{still:true}).frame,renderLucidStory(paths,11000,{still:true}).frame,'Still story');
|
|
81
|
+
console.log('Arrival bounds, pupils, idle handoff and complete still mode verified');
|
|
82
|
+
console.log('Quiet motion: loop, planted feet, attached eye and compact pupils verified');
|
|
83
|
+
console.log('Technical checks passed. Artistic acceptance remains a human judgment.');
|
|
84
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {performance} from 'node:perf_hooks';
|
|
2
|
+
import {LUCID_CLIPS,renderLucidCharacter,renderLucidStory,STORY_DURATION} from './character.mjs';
|
|
3
|
+
import {composeMessage} from './layout.mjs';
|
|
4
|
+
import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
|
|
5
|
+
|
|
6
|
+
/** One bounded performance. No alternate screen or full-screen clearing. */
|
|
7
|
+
export async function watch(paths,options,config){
|
|
8
|
+
if(!process.stdout.isTTY||!process.stdin.isTTY)throw new Error('--watch/--intro needs an interactive terminal. Use --scene and --at for snapshots.');
|
|
9
|
+
const {terminalCols,terminalRows,scene,large,message}=config;
|
|
10
|
+
const clip=scene?LUCID_CLIPS.find(c=>c.id===scene):null;
|
|
11
|
+
const duration=(clip?.duration??STORY_DURATION)+(config.arrival?6000:0);
|
|
12
|
+
const realCols=process.stdout.columns,realRows=process.stdout.rows;
|
|
13
|
+
const start=performance.now();
|
|
14
|
+
const render=t=>{
|
|
15
|
+
const r=scene?renderLucidCharacter(paths,scene,t,options):renderLucidStory(paths,t,options);
|
|
16
|
+
if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
|
|
17
|
+
const caption=message??'LUCID · '+r.clip+'\n'+r.caption;
|
|
18
|
+
return large?[...cellsToAnsi(r.frame),...caption.split('\n')]:composeMessage(r.frame,caption,{terminalCols});
|
|
19
|
+
};
|
|
20
|
+
// Reserve enough lines for captions too; keep within the real terminal.
|
|
21
|
+
const height=options.rows+3;
|
|
22
|
+
if(height>=terminalRows)throw new Error('Animation needs room for its caption; reduce --rows.');
|
|
23
|
+
let ended=false,drawn=false,timer,resolveDone,rejectDone;
|
|
24
|
+
const wasRaw=Boolean(process.stdin.isRaw);
|
|
25
|
+
const done=new Promise((resolve,reject)=>{resolveDone=resolve;rejectDone=reject;});
|
|
26
|
+
const cleanup=(error)=>{
|
|
27
|
+
if(ended)return;ended=true;clearInterval(timer);
|
|
28
|
+
process.stdin.off('data',key);process.stdout.off('resize',resized);
|
|
29
|
+
process.off('SIGINT',sigint);process.off('SIGTERM',sigterm);
|
|
30
|
+
process.stdin.setRawMode(wasRaw);process.stdin.pause();
|
|
31
|
+
if(drawn)process.stdout.write(`\x1b[${height}B\r`);
|
|
32
|
+
process.stdout.write('\x1b[0m\x1b[?25h');
|
|
33
|
+
error?rejectDone(error):resolveDone();
|
|
34
|
+
};
|
|
35
|
+
const key=data=>{if(data.includes(3)||data.toString().includes('q'))cleanup();};
|
|
36
|
+
const sigint=()=>cleanup(),sigterm=()=>cleanup();
|
|
37
|
+
const resized=()=>cleanup(new Error('Terminal resized; rerun for the new art slot.'));
|
|
38
|
+
try{
|
|
39
|
+
process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on('data',key);
|
|
40
|
+
process.stdout.on('resize',resized);process.on('SIGINT',sigint);process.on('SIGTERM',sigterm);
|
|
41
|
+
process.stdout.write('\x1b[?25l');
|
|
42
|
+
// Allocate the block once; each frame replaces only these lines.
|
|
43
|
+
process.stdout.write('\n'.repeat(height)+`\x1b[${height}A`);drawn=true;
|
|
44
|
+
const tick=()=>{
|
|
45
|
+
try{
|
|
46
|
+
if(process.stdout.columns!==realCols||process.stdout.rows!==realRows)return resized();
|
|
47
|
+
const t=Math.min(performance.now()-start,duration-1),lines=render(t);
|
|
48
|
+
if(lines.length>height)throw new Error('Message exceeds the animation block; use a shorter message or a static preview.');
|
|
49
|
+
process.stdout.write(Array.from({length:height},(_,i)=>'\r\x1b[2K'+(lines[i]??'')+'\n').join('')+`\x1b[${height}A`);
|
|
50
|
+
if(performance.now()-start>=duration)cleanup();
|
|
51
|
+
}catch(e){cleanup(e);}
|
|
52
|
+
};
|
|
53
|
+
tick();if(!ended)timer=setInterval(tick,1000/12);
|
|
54
|
+
await done;
|
|
55
|
+
}catch(e){cleanup(e);await done;}
|
|
56
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
|
|
2
|
+
<title>Seer — jade coil, golden belly and red mane</title>
|
|
3
|
+
<g id="far-feet">
|
|
4
|
+
<path id="far-front-leg" fill="#287c50" d="M562 253 Q605 244 616 267 L614 284 L631 287 Q641 297 630 302 L603 298 Q593 291 597 274 L575 279 Z"/>
|
|
5
|
+
<path id="far-front-claws" fill="#ffe6a0" d="M613 286 L621 282 L624 296 L616 296 Z M624 289 L632 288 L636 300 L628 300 Z"/>
|
|
6
|
+
<path id="far-back-leg" fill="#287c50" d="M257 303 Q230 279 205 291 L186 310 L173 307 Q161 310 168 320 L188 330 L218 310 L246 324 Z"/>
|
|
7
|
+
<path id="far-back-claws" fill="#ffe6a0" d="M168 309 L181 307 L177 319 L168 319 Z M183 314 L194 312 L188 326 L180 323 Z"/>
|
|
8
|
+
</g>
|
|
9
|
+
<g id="body">
|
|
10
|
+
<path id="spine-fins" fill="#dd5946" d="M488 197 L461 203 L490 220 L470 232 L510 243 L493 258 L537 268 L530 285 L562 280 L559 252 L532 229 Z M393 289 L373 265 L360 282 L336 259 L328 278 L300 258 L293 278 L268 263 L265 291 Z M188 318 L162 316 L169 340 L146 344 L160 367 L140 380 L170 394 L165 410 L194 416 Z"/>
|
|
11
|
+
<path id="serpentine-body" fill="#42b66c" d="M480 177 C470 215 504 252 540 266 C568 280 559 302 528 306 C472 321 379 258 270 277 C177 293 125 350 169 403 C212 456 347 444 438 416 C506 394 544 351 589 353 C621 355 640 375 625 388 C610 399 591 388 603 377 C573 378 577 410 605 418 C654 431 693 396 662 365 C634 333 592 320 550 328 C486 339 444 391 355 408 C283 425 202 414 203 378 C201 344 252 320 293 322 C372 323 455 363 539 344 C597 331 620 299 594 264 C565 226 523 221 533 185 Z"/>
|
|
12
|
+
<path id="back-highlight" fill="#80d77e" d="M487 194 C486 219 514 244 544 257 C590 279 577 309 542 316 C465 339 387 280 292 289 C218 295 166 337 170 370 C155 337 204 290 271 280 C374 260 466 326 530 308 C561 302 573 284 542 269 C505 255 477 222 480 195 Z"/>
|
|
13
|
+
<path id="belly-ribbon" fill="#efc75b" d="M514 194 C515 231 552 243 575 270 C600 300 572 324 533 332 C455 349 369 308 291 309 C235 310 181 342 186 378 C188 423 274 435 360 418 C453 400 497 352 552 339 C594 329 625 343 647 365 C626 349 607 346 589 350 C540 347 502 393 435 415 C344 442 217 450 177 401 C141 355 192 305 266 289 C365 270 456 334 530 318 C565 312 579 292 557 276 C524 253 501 229 500 203 Z"/>
|
|
14
|
+
<path id="belly-bands" stroke="#b49c42" stroke-width="4" fill="none" d="M510 222 L527 218 M522 245 L542 237 M546 265 L561 254 M567 285 L586 282 M563 306 L576 319 M537 318 L542 331 M500 321 L500 336 M460 315 L456 332 M420 305 L412 323 M378 295 L371 314 M336 290 L333 309 M292 294 L292 309 M249 302 L255 316 M212 322 L225 334 M184 352 L199 357 M185 383 L201 380 M205 410 L215 397 M244 426 L248 411 M284 431 L284 419 M326 427 L325 416 M370 417 L365 408 M409 404 L403 395 M450 385 L443 377 M489 361 L480 354 M529 345 L522 335"/>
|
|
15
|
+
</g>
|
|
16
|
+
<g id="tail">
|
|
17
|
+
<path id="tail-tuft" fill="#d9503e" d="M598 379 Q590 366 607 350 L607 363 L626 352 L623 368 L641 365 Q633 384 615 389 Z"/>
|
|
18
|
+
<path id="tail-tuft-light" fill="#f3924b" d="M603 379 L616 364 L613 378 L628 371 L616 385 Z"/>
|
|
19
|
+
</g>
|
|
20
|
+
<g id="near-feet">
|
|
21
|
+
<path id="near-back-leg" fill="#4ebc70" d="M243 315 Q271 331 264 356 L247 371 L263 385 Q268 397 253 398 L227 384 Q220 376 229 363 L240 351 L222 337 Z"/>
|
|
22
|
+
<path id="near-back-claws" fill="#ffe6a0" d="M244 382 L254 377 L258 391 L249 393 Z M230 378 L238 374 L242 388 L233 387 Z"/>
|
|
23
|
+
<path id="near-front-leg" fill="#56c779" d="M527 275 Q503 283 491 306 Q483 322 496 331 L514 331 L514 345 Q522 355 532 342 L534 322 Q531 313 516 311 L538 293 Z"/>
|
|
24
|
+
<path id="near-front-claws" fill="#ffe6a0" d="M516 331 L522 328 L525 343 L517 345 Z M526 328 L534 325 L534 341 L527 344 Z"/>
|
|
25
|
+
</g>
|
|
26
|
+
<g id="head">
|
|
27
|
+
<path id="mane" fill="#d9503e" d="M480 104 Q462 85 439 87 L450 107 L421 105 L437 128 L412 136 L433 151 L410 166 L438 178 L429 199 L458 195 L455 216 L487 203 L514 184 Z"/>
|
|
28
|
+
<path id="mane-light" fill="#f38848" d="M455 117 L439 116 L449 139 L431 141 L448 155 L433 171 L455 175 L449 191 L474 184 Z"/>
|
|
29
|
+
<path id="far-horn" fill="#ebc76b" d="M551 108 Q546 82 562 61 L558 44 L568 38 L576 61 L566 82 L582 73 L591 81 L565 100 L565 116 Z"/>
|
|
30
|
+
<path id="near-horn" fill="#ffdb7e" d="M480 112 Q465 91 477 67 L460 54 L464 44 L489 62 L494 40 L505 41 L500 73 Q485 93 496 111 Z"/>
|
|
31
|
+
<path id="ear" fill="#59bb6c" d="M463 126 Q429 101 411 116 Q411 145 453 155 Z"/>
|
|
32
|
+
<path id="ear-inner" fill="#e5bc58" d="M451 134 L423 119 Q427 139 451 146 Z"/>
|
|
33
|
+
<path id="face" fill="#59c879" d="M460 116 C478 91 528 91 554 108 C581 112 595 135 593 156 L622 164 Q657 174 640 197 Q627 216 589 213 C574 235 525 240 489 218 C458 207 440 155 460 116 Z"/>
|
|
34
|
+
<path id="forehead-light" fill="#9ce18b" d="M465 126 Q489 99 526 108 Q493 113 477 142 Z"/>
|
|
35
|
+
<path id="muzzle" fill="#c4dd84" d="M558 169 Q591 153 626 174 Q648 185 629 199 Q600 212 563 199 Z"/>
|
|
36
|
+
<path id="chin" fill="#f3d478" d="M496 201 Q523 218 555 205 Q571 214 591 211 Q569 233 534 225 Q509 219 496 201 Z"/>
|
|
37
|
+
<path id="nostril" fill="#23664b" d="M621 175 Q631 171 631 179 Q627 186 621 181 Z"/>
|
|
38
|
+
<path id="mouth" stroke="#317b4f" stroke-width="5" fill="none" d="M558 198 Q588 215 614 202"/>
|
|
39
|
+
<path id="cheek" fill="#e5c263" d="M470 180 Q483 174 492 185 Q489 199 477 194 Z"/>
|
|
40
|
+
</g>
|
|
41
|
+
<g id="whiskers">
|
|
42
|
+
<path id="far-whisker" stroke="#f7d786" stroke-width="6" fill="none" d="M619 187 C653 174 669 146 694 150 Q708 154 700 166"/>
|
|
43
|
+
<path id="near-whisker" stroke="#ffe29b" stroke-width="7" fill="none" d="M592 200 C614 229 656 243 679 223 Q691 212 681 202"/>
|
|
44
|
+
</g>
|
|
45
|
+
<g id="eyes">
|
|
46
|
+
<path id="far-eye-socket" fill="#080d19" d="M548 131 C552 113 572 113 580 130 C588 149 580 170 566 171 C550 169 544 149 548 131 Z"/>
|
|
47
|
+
<path id="far-eye-white" fill="#ffffff" d="M552 132 C555 119 570 119 576 132 C582 147 577 165 566 166 C554 164 549 147 552 132 Z"/>
|
|
48
|
+
<path id="far-pupil" fill="#080d19" d="M561 142 C561 132 572 131 575 141 C578 151 567 157 563 150 Z"/>
|
|
49
|
+
<path id="near-eye-socket" fill="#080d19" d="M475 130 C482 110 509 109 521 128 C534 151 522 180 502 182 C479 181 466 153 475 130 Z"/>
|
|
50
|
+
<path id="near-eye-white" fill="#ffffff" d="M480 132 C486 116 506 115 516 131 C527 151 517 175 502 176 C483 176 473 151 480 132 Z"/>
|
|
51
|
+
<path id="near-pupil" fill="#080d19" d="M495 143 C495 132 508 128 514 140 C519 151 510 162 501 157 C497 155 495 149 495 143 Z"/>
|
|
52
|
+
</g>
|
|
53
|
+
</svg>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {renderSvgRig} from '../../../factory/svg-rig.mjs';
|
|
2
|
+
const clamp=x=>Math.max(0,Math.min(1,x));
|
|
3
|
+
const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
|
|
4
|
+
const pulse=(t,a,b)=>t<=a||t>=b?0:Math.sin(Math.PI*(t-a)/(b-a))**2;
|
|
5
|
+
export const SEER_CLIPS=[
|
|
6
|
+
{id:'idle',title:'Quiet Watch',duration:14000},
|
|
7
|
+
{id:'attach',title:'A Closer Look',duration:3400},
|
|
8
|
+
{id:'curious',title:'Following a Detail',duration:3000},
|
|
9
|
+
{id:'focus',title:'Reading the Pattern',duration:3000},
|
|
10
|
+
{id:'concerned',title:'Something Does Not Fit',duration:3000},
|
|
11
|
+
{id:'proud',title:'There It Is',duration:2800},
|
|
12
|
+
{id:'standby',title:'Patient Company',duration:4000},
|
|
13
|
+
{id:'pattern',title:'Pattern Sight',duration:5200,power:true},
|
|
14
|
+
];
|
|
15
|
+
export const STORY=[['attach',3400],['idle',2200],['curious',3000],['focus',2400],['pattern',5200],['proud',2800],['standby',3000]];
|
|
16
|
+
export const STORY_DURATION=STORY.reduce((s,b)=>s+b[1],0);
|
|
17
|
+
const captions={idle:'Quietly watching.',attach:'Let me look closer.',curious:'Something caught my eye.',focus:'Following the details.',concerned:'This needs another look.',proud:'There it is · expression study',standby:'Take your time.',pattern:'Pattern Sight · visual demonstration'};
|
|
18
|
+
export function bodyPose(ms){
|
|
19
|
+
const t=ms%14000;
|
|
20
|
+
return {turn:.018*pulse(t,800,4300)-.012*pulse(t,6500,9500),nod:2*pulse(t,1300,4600),coil:pulse(t,3200,8800),whisker:pulse(t,1600,5000)-.6*pulse(t,7300,10300)};
|
|
21
|
+
}
|
|
22
|
+
export function poseRig(rig,p,face){
|
|
23
|
+
const paths=rig.paths.map(path=>({...path,polygons:path.polygons.map(poly=>poly.map(([sx,sy])=>{
|
|
24
|
+
let x=sx,y=sy;
|
|
25
|
+
if(path.id.endsWith('-eye-white')||path.id.endsWith('-pupil')){
|
|
26
|
+
const cy=path.id.startsWith('near-')?146:143;
|
|
27
|
+
y=cy+(y-cy)*face.open;
|
|
28
|
+
if(path.id.endsWith('-pupil'))x+=face.gaze*4;
|
|
29
|
+
}
|
|
30
|
+
// The same spatial warp moves all anatomy, so neck, fins and feet stay joined.
|
|
31
|
+
const upper=ease((310-sy)/110),a=p.turn*upper;
|
|
32
|
+
const dx=x-520,dy=y-270;
|
|
33
|
+
x=520+dx*Math.cos(a)-dy*Math.sin(a);y=270+dx*Math.sin(a)+dy*Math.cos(a)+p.nod*upper;
|
|
34
|
+
// Compress inward instead of enlarging the envelope; leave the planted bottom still.
|
|
35
|
+
const coil=ease((sy-275)/55)*(1-ease((sy-385)/40));
|
|
36
|
+
x+=(395-sx)*.009*p.coil*coil;
|
|
37
|
+
y-=2*p.coil*coil;
|
|
38
|
+
if(path.group==='whiskers'){
|
|
39
|
+
const root=path.id==='far-whisker'?619:592;
|
|
40
|
+
const w=ease((sx-root)/90);y+=3*p.whisker*w;x-=1.5*Math.abs(p.whisker)*w;
|
|
41
|
+
}
|
|
42
|
+
return [x,y];
|
|
43
|
+
}))}));
|
|
44
|
+
const byId=new Map(paths.map(p=>[p.id,p]));
|
|
45
|
+
return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}))};
|
|
46
|
+
}
|
|
47
|
+
function expression(scene,t){
|
|
48
|
+
const blink=pulse(t%7000,5400,5620);
|
|
49
|
+
switch(scene){
|
|
50
|
+
case 'curious':return {gaze:.7*ease(t/700),open:1.07};
|
|
51
|
+
case 'focus':return {gaze:.35,open:.72};
|
|
52
|
+
case 'concerned':return {gaze:-.35,open:.88};
|
|
53
|
+
case 'proud':return {gaze:0,open:1.1};
|
|
54
|
+
case 'standby':return {gaze:0,open:.85};
|
|
55
|
+
case 'pattern':return {gaze:-.65*ease(t/900),open:1};
|
|
56
|
+
default:return {gaze:.25*Math.sin(t/2300),open:1-.65*blink};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function pattern(frame,t,mono){
|
|
60
|
+
const h=frame.length,w=frame[0].length,nodes=[[.14,.28],[.24,.18],[.34,.29],[.40,.40]];
|
|
61
|
+
const progress=ease(t/3400),fade=1-ease((t-4300)/900);let count=0;
|
|
62
|
+
const put=(x,y,bright)=>{x=Math.round(x*w);y=Math.round(y*h);if(x<1||x>=w-1||y<1||y>=h-1||fade<.05||frame[y][x].ch!==' ')return;frame[y][x]={ch:bright?'⠿':'⠂',bg:null,rgb:mono?null:(bright?[255,235,156]:[229,186,83]).map(n=>Math.round(n*fade))};count++;};
|
|
63
|
+
for(let i=0;i<nodes.length;i++)if(progress>(i+1)*.09)put(...nodes[i],true);
|
|
64
|
+
for(let i=0;i<3;i++)for(let j=1;j<8;j++)if(progress>.42+(i+j/8)*.18){const a=nodes[i],b=nodes[i+1];put(a[0]+(b[0]-a[0])*j/8,a[1]+(b[1]-a[1])*j/8,false);}
|
|
65
|
+
return count;
|
|
66
|
+
}
|
|
67
|
+
export function renderSeer(rig,scene='idle',ms=0,options={}){
|
|
68
|
+
const clip=SEER_CLIPS.find(c=>c.id===scene);
|
|
69
|
+
if(!clip)return {ok:false,code:'UNKNOWN_CLIP'};
|
|
70
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
71
|
+
const t=options.still?(clip.power?3300:0):ms;
|
|
72
|
+
let face=expression(scene,t),pose=bodyPose(options.motionTime??t);
|
|
73
|
+
if(scene==='attach'){
|
|
74
|
+
const duration=options.deck?4400:3400;
|
|
75
|
+
if(t>=duration){pose=bodyPose(t-duration);face=expression('idle',t-duration);}
|
|
76
|
+
else{const u=t/duration;pose={turn:-.02*pulse(u,.1,.8),nod:3*pulse(u,.25,.9),coil:.8*pulse(u,.35,1),whisker:pulse(u,.3,.95)};face={gaze:-.5*pulse(u,0,.85),open:1};}
|
|
77
|
+
}
|
|
78
|
+
if(scene==='proud')pose={...pose,nod:pose.nod-1.5*pulse(t,0,2800)};
|
|
79
|
+
if(options.still)pose={turn:0,nod:0,coil:0,whisker:0};
|
|
80
|
+
const r=renderSvgRig(poseRig(rig,pose,face),options);
|
|
81
|
+
const effectCells=clip.power?pattern(r.frame,t,options.mono):0;
|
|
82
|
+
return {...r,scene,clip:clip.title,caption:captions[scene],pose,face,effectCells};
|
|
83
|
+
}
|
|
84
|
+
export function renderStory(rig,ms,options={}){
|
|
85
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
86
|
+
let t=options.still?0:Math.min(ms,STORY_DURATION-1);
|
|
87
|
+
for(const [scene,duration] of STORY){if(t<duration)return renderSeer(rig,scene,t,{...options,deck:false,motionTime:Math.max(0,ms-3400)});t-=duration;}
|
|
88
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {renderSeer as renderV2,SEER_CLIPS as V2_CLIPS} from '../v2/character.mjs';
|
|
2
|
+
const ease=x=>{x=Math.max(0,Math.min(1,x));return x*x*(3-2*x);};
|
|
3
|
+
export const SEER_CLIPS=[...V2_CLIPS,{id:'eight',title:'Eight · HI!',duration:10000}];
|
|
4
|
+
export const STORY_DURATION=10000;
|
|
5
|
+
import {flattenPath} from '../../stage3/svg-coverage.js';
|
|
6
|
+
// Authored rest and entrance spines; transfer existing artwork along their arc length.
|
|
7
|
+
const rest=flattenPath('M506 185 C500 225 565 245 576 280 C590 315 555 330 528 325 C460 345 370 285 286 300 C220 310 170 343 184 380 C200 430 280 439 357 420 C440 400 490 360 554 340 C600 324 635 349 650 376 C660 400 617 420 596 399 C586 389 593 381 603 377')[0];
|
|
8
|
+
const target=flattenPath('M506 185 C490 215 410 235 350 270 C280 305 250 320 280 355 C320 405 414 405 450 355 C490 310 405 285 350 250 C285 215 245 195 275 155 C310 115 390 115 423 154 C453 187 420 220 385 222 C365 223 347 210 354 197')[0];
|
|
9
|
+
function spine(points){let total=0;const segments=[];for(let i=1;i<points.length;i++){const a=points[i-1],b=points[i],dx=b[0]-a[0],dy=b[1]-a[1],len=Math.hypot(dx,dy);if(!len)continue;segments.push({a,dx,dy,len,start:total});total+=len;}return {segments,total};}
|
|
10
|
+
const original=spine(rest),destination=spine(target),cache=new WeakMap();
|
|
11
|
+
function mapPoint(x,y){
|
|
12
|
+
let nearest,dist=Infinity,along=0,offset=0;
|
|
13
|
+
for(const s of original.segments){const u=Math.max(0,Math.min(1,((x-s.a[0])*s.dx+(y-s.a[1])*s.dy)/(s.len*s.len)));const px=s.a[0]+u*s.dx,py=s.a[1]+u*s.dy,d=(x-px)**2+(y-py)**2;if(d<dist){dist=d;nearest=s;along=(s.start+u*s.len)/original.total;offset=((x-px)*-s.dy+(y-py)*s.dx)/s.len;}}
|
|
14
|
+
const at=along*destination.total,s=destination.segments.find(s=>at<=s.start+s.len)||destination.segments.at(-1),u=(at-s.start)/s.len;
|
|
15
|
+
return [s.a[0]+u*s.dx-offset*s.dy/s.len,s.a[1]+u*s.dy+offset*s.dx/s.len];
|
|
16
|
+
}
|
|
17
|
+
export function figureEight(rig,amount){
|
|
18
|
+
if(!cache.has(rig))cache.set(rig,rig.paths.map(p=>p.polygons.map(poly=>poly.map(([x,y])=>['head','eyes','whiskers'].includes(p.group)?[x,y]:mapPoint(x,y)))));
|
|
19
|
+
const mapped=cache.get(rig);
|
|
20
|
+
const paths=rig.paths.map((p,i)=>({...p,polygons:p.polygons.map((poly,j)=>poly.map(([x,y],k)=>[x+(mapped[i][j][k][0]-x)*amount,y+(mapped[i][j][k][1]-y)*amount]))}));
|
|
21
|
+
const byId=new Map(paths.map(p=>[p.id,p]));
|
|
22
|
+
return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}))};
|
|
23
|
+
}
|
|
24
|
+
export function renderSeer(rig,scene='eight',ms=0,options={}){
|
|
25
|
+
if(scene!=='eight')return renderV2(rig,scene,ms,options);
|
|
26
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
27
|
+
const t=options.still?3000:ms;
|
|
28
|
+
if(t>=7000)return renderV2(rig,'idle',t-7000,options);
|
|
29
|
+
const amount=ease(t/2200)*(1-ease((t-4500)/2500));
|
|
30
|
+
const r=renderV2(figureEight(rig,amount),'idle',0,{...options,motionTime:0});
|
|
31
|
+
return {...r,scene:'eight',clip:'Eight · HI!',caption:t>=2200&&t<4500?'HI!':'A little twist…',amount};
|
|
32
|
+
}
|
|
33
|
+
export function renderStory(rig,ms,options={}){return renderSeer(rig,'eight',ms,options);}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {renderSeer as previous,SEER_CLIPS as previousClips} from '../v3/character.mjs';
|
|
2
|
+
import {flattenPath} from '../../stage3/svg-coverage.js';
|
|
3
|
+
import {renderSvgRig} from '../../../factory/svg-rig.mjs';
|
|
4
|
+
const TAU=2*Math.PI;
|
|
5
|
+
const ease=x=>{x=Math.max(0,Math.min(1,x));return x*x*(3-2*x);};
|
|
6
|
+
export const SEER_CLIPS=[...previousClips,{id:'swim',title:'Swimming Eight · HI!',duration:11000}];
|
|
7
|
+
export const STORY_DURATION=11000;
|
|
8
|
+
export const arrivalDuration=deck=>deck?8500:5800;
|
|
9
|
+
const rest=flattenPath('M506 185 C500 225 565 245 576 280 C590 315 555 330 528 325 C460 345 370 285 286 300 C220 310 170 343 184 380 C200 430 280 439 357 420 C440 400 490 360 554 340 C600 324 635 349 650 376 C660 400 617 420 596 399 C586 389 593 381 603 377')[0];
|
|
10
|
+
let length=0;
|
|
11
|
+
const segments=rest.slice(1).map((b,i)=>{const a=rest[i],dx=b[0]-a[0],dy=b[1]-a[1],n=Math.hypot(dx,dy),s={a,dx,dy,n,start:length};length+=n;return s;}).filter(s=>s.n);
|
|
12
|
+
const cache=new WeakMap();
|
|
13
|
+
function bind(x,y){
|
|
14
|
+
let distance=Infinity,binding;
|
|
15
|
+
for(const s of segments){const u=Math.max(0,Math.min(1,((x-s.a[0])*s.dx+(y-s.a[1])*s.dy)/s.n**2));
|
|
16
|
+
const px=s.a[0]+u*s.dx,py=s.a[1]+u*s.dy,d=(x-px)**2+(y-py)**2;
|
|
17
|
+
if(d<distance){distance=d;binding={along:(s.start+u*s.n)/length,offset:((x-px)*-s.dy+(y-py)*s.dx)/s.n};}}
|
|
18
|
+
return binding;
|
|
19
|
+
}
|
|
20
|
+
// Upright eight. Arc-length lookup makes skin sections travel at equal speed.
|
|
21
|
+
const track=Array.from({length:721},(_,i)=>{const a=i/720*TAU;return [390+100*Math.sin(2*a),277-81*Math.cos(a)];});
|
|
22
|
+
const lengths=[0];for(let i=1;i<track.length;i++)lengths.push(lengths.at(-1)+Math.hypot(track[i][0]-track[i-1][0],track[i][1]-track[i-1][1]));
|
|
23
|
+
function sample(u){
|
|
24
|
+
const d=((u%1)+1)%1*lengths.at(-1);let lo=0,hi=720;
|
|
25
|
+
while(hi-lo>1){const m=(lo+hi)>>1;if(lengths[m]<d)lo=m;else hi=m;}
|
|
26
|
+
const f=(d-lengths[lo])/(lengths[hi]-lengths[lo]),a=track[lo],b=track[hi],n=Math.hypot(b[0]-a[0],b[1]-a[1]);
|
|
27
|
+
return {x:a[0]+f*(b[0]-a[0]),y:a[1]+f*(b[1]-a[1]),nx:-(b[1]-a[1])/n,ny:(b[0]-a[0])/n};
|
|
28
|
+
}
|
|
29
|
+
export function swimRig(rig,ms,{deck=false,still=false}={}){
|
|
30
|
+
const duration=arrivalDuration(deck),t=still?duration*.69:Math.min(ms,duration),u=t/duration;
|
|
31
|
+
// Slow into a greeting at the top; settle with zero endpoint velocity.
|
|
32
|
+
const amount=ease(u/.18)*(1-ease((u-.78)/.22));
|
|
33
|
+
const phase=1-ease((u-.13)/.54),head=sample(phase);
|
|
34
|
+
const angle=.13*Math.sin(TAU*phase),c=Math.cos(angle),s=Math.sin(angle);
|
|
35
|
+
if(!cache.has(rig))cache.set(rig,rig.paths.map(p=>p.polygons.map(poly=>poly.map(([x,y])=>bind(x,y)))));
|
|
36
|
+
const bindings=cache.get(rig);
|
|
37
|
+
const paths=rig.paths.map((p,i)=>({...p,polygons:p.polygons.map((poly,j)=>poly.map(([x,y],k)=>{
|
|
38
|
+
const b=bindings[i][j][k];let tx,ty;
|
|
39
|
+
if(['head','eyes','whiskers'].includes(p.group)){
|
|
40
|
+
tx=head.x+(x-506)*c-(y-185)*s;ty=head.y+(x-506)*s+(y-185)*c;
|
|
41
|
+
if(p.group==='whiskers'){
|
|
42
|
+
const root=p.id==='far-whisker'?619:592,w=ease((x-root)/85);
|
|
43
|
+
ty+=7*w*Math.sin(TAU*phase-.8)*Math.sin(Math.PI*ease(u/.78));
|
|
44
|
+
}
|
|
45
|
+
}else{
|
|
46
|
+
const q=sample(phase+b.along*.9);
|
|
47
|
+
tx=q.x+q.nx*b.offset*.85;ty=q.y+q.ny*b.offset*.85;
|
|
48
|
+
// Blend the neck root into the same rigid face transform.
|
|
49
|
+
const root=1-ease(b.along/.055);
|
|
50
|
+
tx=tx*(1-root)+(head.x+(x-506)*c-(y-185)*s)*root;
|
|
51
|
+
ty=ty*(1-root)+(head.y+(x-506)*s+(y-185)*c)*root;
|
|
52
|
+
}
|
|
53
|
+
return [x+(tx-x)*amount,y+(ty-y)*amount];
|
|
54
|
+
}))}));
|
|
55
|
+
const byId=new Map(paths.map(p=>[p.id,p]));
|
|
56
|
+
return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)})),flight:{amount,phase,head,u}};
|
|
57
|
+
}
|
|
58
|
+
export function renderSeer(rig,scene='swim',ms=0,options={}){
|
|
59
|
+
if(scene!=='swim')return previous(rig,scene,ms,options);
|
|
60
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
61
|
+
const end=arrivalDuration(options.deck);
|
|
62
|
+
if(!options.still&&ms>=end)return previous(rig,'idle',ms-end,options);
|
|
63
|
+
const posed=swimRig(rig,ms,options),r=renderSvgRig(posed,options),u=posed.flight.u;
|
|
64
|
+
return {...r,scene:'swim',clip:'Swimming Eight · HI!',caption:u>=.67&&u<.83?'HI!':u<.67?'Following my own trail…':'Right here with you.',flight:posed.flight};
|
|
65
|
+
}
|
|
66
|
+
export function renderStory(rig,ms,options={}){return renderSeer(rig,'swim',ms,options);}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import {parseSvgRig} from '../../../factory/svg-rig.mjs';
|
|
4
|
+
import {renderSeer,SEER_CLIPS} from './character.mjs';
|
|
5
|
+
import {artSlot,composeMessage} from '../../lucid/recovery/layout.mjs';
|
|
6
|
+
import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
|
|
7
|
+
const args=process.argv.slice(2),values=new Set(['--cols','--rows','--terminal-cols','--terminal-rows','--message','--scene','--at']);
|
|
8
|
+
for(let i=0;i<args.length;i++){
|
|
9
|
+
if(values.has(args[i])){if(!args[i+1]||args[i+1].startsWith('--'))throw Error('Missing option value');i++;}
|
|
10
|
+
else if(!['--large','--mono','--help','--watch','--attach','--intro','--still','--list'].includes(args[i]))throw Error('Unknown option: '+args[i]);
|
|
11
|
+
}
|
|
12
|
+
const value=(name,fallback)=>args.includes(name)?args[args.indexOf(name)+1]:fallback;
|
|
13
|
+
if(args.includes('--help')){console.log('Seer V4 flowing figure-eight entrance. --watch --attach --intro --still --scene ID --at MS --list --large --mono --message TEXT --cols N --rows N --terminal-cols N --terminal-rows N');process.exit(0);}
|
|
14
|
+
if(args.includes('--list')){console.log(SEER_CLIPS.map(c=>c.id+' · '+c.title).join('\n'));process.exit(0);}
|
|
15
|
+
const w=Number(value('--terminal-cols',process.stdout.columns||80)),h=Number(value('--terminal-rows',process.stdout.rows||24));
|
|
16
|
+
if(!Number.isSafeInteger(w)||!Number.isSafeInteger(h)||w<1||h<1)throw Error('Invalid terminal size');
|
|
17
|
+
const slot=artSlot(w,h),large=args.includes('--large')||args.includes('--intro'),explicit=large||args.includes('--cols')||args.includes('--rows');
|
|
18
|
+
const cols=Number(value('--cols',large?Math.min(120,w-4):Math.min(39,slot.cols))),rows=Number(value('--rows',large?Math.min(36,h-4):Math.min(11,slot.rows)));
|
|
19
|
+
const message=value('--message','SEER\nLet me look closer.');
|
|
20
|
+
if(!slot.showArt&&!explicit){console.log(composeMessage(null,message,{terminalCols:w}).join('\n'));}
|
|
21
|
+
else{
|
|
22
|
+
if(cols>w-4||rows>h-2)throw Error('Requested art exceeds terminal margins');
|
|
23
|
+
const rig=parseSvgRig(await fs.readFile(new URL('../v1/seer-master.svg',import.meta.url),'utf8'));
|
|
24
|
+
const opts={cols,rows,mono:args.includes('--mono'),still:args.includes('--still'),deck:large};
|
|
25
|
+
const scene=value('--scene','swim');
|
|
26
|
+
const r=renderSeer(rig,scene,Number(value('--at',0)),opts);
|
|
27
|
+
if(!r.ok)throw Error(r.code);
|
|
28
|
+
if(args.includes('--watch')||args.includes('--attach')||args.includes('--intro')){
|
|
29
|
+
const {watch}=await import('./watch.mjs');
|
|
30
|
+
await watch(rig,opts,{terminalCols:w,terminalRows:h,large,scene:args.includes('--scene')||args.includes('--attach')?scene:null,arrival:args.includes('--attach')&&scene==='attach',message:args.includes('--message')?message:null});
|
|
31
|
+
}else
|
|
32
|
+
console.log((large?[...cellsToAnsi(r.frame),...composeMessage(null,message,{terminalCols:w})]:composeMessage(r.frame,message,{terminalCols:w})).join('\n'));
|
|
33
|
+
}
|