quoxcli 0.2.0 → 0.3.1
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/admin.js +26 -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/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -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 +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -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 +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -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 +86 -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 +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -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/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -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 +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- 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 +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- 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 +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- 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/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -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 +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- 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/sessionsOverview.js +64 -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 +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -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/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -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 +112 -0
- package/lib/quoxon/commands.js +363 -38
- 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 +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -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 +244 -25
- 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/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -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 +211 -0
- package/lib/quoxon/noai.js +57 -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 +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- 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/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -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 +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -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/session.js +172 -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 +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -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/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 +21 -7
- 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/restart.js +71 -13
- 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/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 +59 -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/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 +89 -0
- package/lib/quoxon/windowOrg.js +113 -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 +4 -1
- package/lib/core/websiteSession.js +0 -148
package/lib/quoxon/qart.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The horned Q as Braille art, and every motion the owner reviewed in the lab (~/quoxon-work/intro/lab):
|
|
3
|
-
* Orbit & Lock (the intro), Portal, Particle collapse
|
|
3
|
+
* Orbit & Lock (the intro), Portal, Particle collapse. CLI-QUOX.
|
|
4
4
|
*
|
|
5
5
|
* PURE AND PORTABLE on purpose: no Ink, no process, no timers, no terminal APIs. Inputs are size, progress
|
|
6
|
-
* (or ms), state and seed; the core output is rows of cells `{ ch, rgb }` (rgb null = default colour
|
|
6
|
+
* (or ms), state and seed; the core output is rows of cells `{ ch, rgb, bg? }` (rgb null = default colour; bg, when
|
|
7
|
+
* present, a background colour: the dragon's eyes in qdragon.js use it). The
|
|
7
8
|
* same module renders in the terminal (cellsToAnsi) and in a browser (web/qface.js), which is how the Q
|
|
8
9
|
* becomes the face of the app on cli.quox.ai as well as in the CLI.
|
|
9
10
|
*
|
|
@@ -160,17 +161,25 @@ export function fieldToCells(field, { color = true, quant = true, tint = null }
|
|
|
160
161
|
return out;
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
const sgr = (rgb) => `\x1b[38;2;${rgb.map((x) => Math.max(0, Math.min(255, Math.round(x)))).join(';')}m`;
|
|
164
|
-
/**
|
|
164
|
+
const sgr = (rgb, bg = false) => `\x1b[${bg ? 48 : 38};2;${rgb.map((x) => Math.max(0, Math.min(255, Math.round(x)))).join(';')}m`;
|
|
165
|
+
/**
|
|
166
|
+
* Cells -> ANSI lines (truecolor SGR, one escape per colour run). Mono cells carry no SGR at all. A cell may
|
|
167
|
+
* also carry `bg` (the dragon's eyes are quadrant blocks with a foreground AND a background colour); a
|
|
168
|
+
* background run is closed with a reset before the next cell without one, so it never bleeds. Cells without
|
|
169
|
+
* bg render exactly as they always have.
|
|
170
|
+
*/
|
|
165
171
|
export function cellsToAnsi(rows) {
|
|
166
172
|
return rows.map((row) => {
|
|
167
|
-
let s = '', last = '';
|
|
173
|
+
let s = '', last = '', lastBg = '';
|
|
168
174
|
for (const c of row) {
|
|
175
|
+
const b = c.bg ? sgr(c.bg, true) : '';
|
|
176
|
+
if (lastBg && !b) { s += RESET; last = ''; lastBg = ''; }
|
|
169
177
|
if (c.rgb) { const e = sgr(c.rgb); if (e !== last) { s += e; last = e; } }
|
|
170
|
-
else if (last && c.ch !== ' ') { s += RESET; last = ''; }
|
|
178
|
+
else if (last && c.ch !== ' ') { s += RESET; last = ''; if (lastBg) lastBg = ''; }
|
|
179
|
+
if (b && b !== lastBg) { s += b; lastBg = b; }
|
|
171
180
|
s += c.ch;
|
|
172
181
|
}
|
|
173
|
-
return s + (last ? RESET : '');
|
|
182
|
+
return s + (last || lastBg ? RESET : '');
|
|
174
183
|
});
|
|
175
184
|
}
|
|
176
185
|
/** Cells -> plain text lines. */
|
|
@@ -313,44 +322,6 @@ export function portalCellsAt(size, t, opts = {}) {
|
|
|
313
322
|
return fieldToCells(f, opts);
|
|
314
323
|
}
|
|
315
324
|
|
|
316
|
-
// ---- 5: Dragon's Eye -------------------------------------------------------------------------------------
|
|
317
|
-
export const EYE_MS = 2800;
|
|
318
|
-
/** Lid openness 0..1 over the eye's progress u: open, hold, one blink, hold, close (lab timings). */
|
|
319
|
-
export function eyeOpenAt(u) {
|
|
320
|
-
if (u < 0.12) return 0;
|
|
321
|
-
if (u < 0.34) return smooth((u - 0.12) / 0.22);
|
|
322
|
-
if (u < 0.5) return 1;
|
|
323
|
-
if (u < 0.58) return 1 - smooth((u - 0.5) / 0.08);
|
|
324
|
-
if (u < 0.68) return smooth((u - 0.58) / 0.1);
|
|
325
|
-
if (u < 0.86) return 1;
|
|
326
|
-
if (u < 0.96) return 1 - smooth((u - 0.86) / 0.1);
|
|
327
|
-
return 0;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* The Dragon's Eye at t ms: a slit-pupil eye opens in the bowl, blinks once, and dissolves back into the Q
|
|
332
|
-
* with a flash. The Q dims while the eye is open so the eye reads. t >= EYE_MS + 400 is the settled Q.
|
|
333
|
-
*/
|
|
334
|
-
export function eyeCellsAt(size, t, opts = {}) {
|
|
335
|
-
if (t >= EYE_MS + 400 || t < 0) return qCells(size, opts);
|
|
336
|
-
const m = qMask(size), H = m.H, f = makeField(m.W, m.H), u = t / EYE_MS, k = eyeOpenAt(u), big = H >= 40;
|
|
337
|
-
const ecx = m.ox + BOWL.cx * H, ecy = 0.51 * H, ew = 0.2 * H * ASPECT + 0.5, eh = 0.155 * H;
|
|
338
|
-
const back = u > 0.94 ? 1 + (1 - clamp((u - 0.94) / 0.06)) * 0.8 : 1;
|
|
339
|
-
const qv = u < 0.12 ? 0.55 + 0.45 * smooth(u / 0.12) : 0.75 + 0.25 * (1 - k);
|
|
340
|
-
for (const d of m.dots) f.set(d.x, d.y, back > 1 ? back : (k > 0.02 && d.part === 'tail' && d.t < 0.4 ? qv * 0.6 : qv));
|
|
341
|
-
if (k > 0.02) {
|
|
342
|
-
for (let dx = -ew; dx <= ew + 1e-6; dx += 0.5) {
|
|
343
|
-
const hh = k * eh * Math.sqrt(Math.max(0, 1 - (dx / ew) ** 2));
|
|
344
|
-
f.set(ecx + dx, ecy - hh, 1.05); f.set(ecx + dx, ecy + hh, 1.05);
|
|
345
|
-
if (big && hh > 1.2 && Math.abs(dx) < ew * 0.8) for (let yy = -hh + 1; yy < hh; yy += 1) f.set(ecx + dx, ecy + yy, 0.18);
|
|
346
|
-
}
|
|
347
|
-
const dil = 0.8 + 0.25 * Math.sin(u * 14), pw = (big ? H / 36 : 0.6) * dil;
|
|
348
|
-
for (let dx = -pw; dx <= pw; dx++) for (let yy = -k * eh * 0.92; yy <= k * eh * 0.92; yy += 0.5) f.set(ecx + dx, ecy + yy, 1.7);
|
|
349
|
-
if (big) { f.set(ecx - ew, ecy, 1.05); f.set(ecx + ew, ecy, 1.05); }
|
|
350
|
-
}
|
|
351
|
-
return fieldToCells(f, opts);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
325
|
// ---- resting --------------------------------------------------------------------------------------------
|
|
355
326
|
export const BREATH_MS = 2400;
|
|
356
327
|
/** The settled Q breathing gently (idle on the web face): brightness 0.82 .. 1 over BREATH_MS. */
|
|
@@ -363,16 +334,14 @@ export function restCellsAt(size, t, opts = {}) {
|
|
|
363
334
|
|
|
364
335
|
// ---- intro plan ------------------------------------------------------------------------------------------
|
|
365
336
|
/**
|
|
366
|
-
* The intro as one timeline: Orbit & Lock, then
|
|
367
|
-
*
|
|
337
|
+
* The intro as one timeline: Orbit & Lock, then the settled Q. Returns { ms, at(t) -> cells }. Callers drive t
|
|
338
|
+
* from their own clock.
|
|
368
339
|
*/
|
|
369
|
-
export function introPlan(size,
|
|
370
|
-
const ms = INTRO_MS + (eye ? EYE_MS + 400 : 0);
|
|
340
|
+
export function introPlan(size, opts = {}) {
|
|
371
341
|
return {
|
|
372
|
-
ms,
|
|
342
|
+
ms: INTRO_MS,
|
|
373
343
|
at(t) {
|
|
374
344
|
if (t < INTRO_MS) return orbitCellsAt(size, t, opts);
|
|
375
|
-
if (eye) return eyeCellsAt(size, t - INTRO_MS, opts);
|
|
376
345
|
return qCells(size, opts);
|
|
377
346
|
},
|
|
378
347
|
};
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Quox dragon: the horned Q brought alive (eyes, tail, fire). Ported from the lab (~/quoxon-work/intro/lab/dragon-core.mjs), tuned there with the owner (CLI-QUOX F3).
|
|
3
|
+
*
|
|
4
|
+
* PURE AND PORTABLE: no process, no timers, no Node or DOM APIs. Everything is a function of
|
|
5
|
+
* (layout, pose, time, seed). A scene maps t (ms) to a pose; renderDragon(pose, layout) maps a pose to rows
|
|
6
|
+
* of cells { ch, rgb, bg } (rgb/bg null = terminal default). The terminal (qart.cellsToAnsi, qmotion.cellNodes)
|
|
7
|
+
* and the web face (web/qface.js) draw these same cells; both understand the bg colour.
|
|
8
|
+
*
|
|
9
|
+
* ONE dragon (v8): a Braille field with the body (bowl ring + horns, from qart.js's vector silhouette), dim bat
|
|
10
|
+
* wings, a spade-tailed spine, the smile and pink cheeks, and the fire (with its embers and smoke) from the mouth.
|
|
11
|
+
* 2x4 dots per cell, one colour per cell. The eyes are NOT drawn here: the googly dot eyes (qeyes.js) are laid over
|
|
12
|
+
* the cells by qdragonScenes.js composeCells, so every scene, mood, greeting and the flight share one face.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const PAL = {
|
|
16
|
+
SKY: [56, 189, 248], INDIGO: [99, 102, 241], VIOLET: [168, 85, 247], TEAL: [6, 182, 212],
|
|
17
|
+
AMBER: [245, 158, 11], ORANGE: [251, 146, 60], RED: [239, 68, 68], CORE: [255, 250, 235],
|
|
18
|
+
SMOKE: [120, 126, 138],
|
|
19
|
+
};
|
|
20
|
+
const { SKY, VIOLET, AMBER, ORANGE, RED, CORE, SMOKE } = PAL;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Themes. dark: the default (terminal background is dark). light: for light terminal backgrounds; the body
|
|
24
|
+
* gradient is deepened, smoke darkens, and the fire keeps its saturation (no white-hot core, which would vanish on a light background).
|
|
25
|
+
*/
|
|
26
|
+
export const THEMES = {
|
|
27
|
+
dark: {
|
|
28
|
+
name: 'dark', bg: [12, 15, 21], light: false, body0: SKY, body1: VIOLET,
|
|
29
|
+
smoke0: [150, 156, 168], smoke1: [72, 78, 90], smoke: SMOKE, label: (c) => c,
|
|
30
|
+
},
|
|
31
|
+
light: {
|
|
32
|
+
name: 'light', bg: [248, 250, 252], light: true, body0: [3, 116, 178], body1: [109, 40, 217],
|
|
33
|
+
smoke0: [96, 104, 118], smoke1: [196, 201, 210], smoke: [110, 116, 128],
|
|
34
|
+
label: (c) => c.map((v) => Math.round(v * 0.62)),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
let TH = THEMES.dark; // set per renderDragon call (synchronous, so this stays pure per frame)
|
|
38
|
+
|
|
39
|
+
const TAU = Math.PI * 2;
|
|
40
|
+
const ASPECT = 0.82;
|
|
41
|
+
export const clamp = (x, a = 0, b = 1) => Math.max(a, Math.min(b, x));
|
|
42
|
+
export const smooth = (e) => { e = clamp(e); return e * e * (3 - 2 * e); };
|
|
43
|
+
const mix = (A, B, t) => A.map((v, i) => v + (B[i] - v) * t);
|
|
44
|
+
export function rng(seed) { return () => { seed |= 0; seed = (seed + 0x6d2b79f5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
|
|
45
|
+
export const hash = (n) => { let x = Math.imul((n | 0) ^ 0x9e3779b9, 0x85ebca6b); x ^= x >>> 13; x = Math.imul(x, 0xc2b2ae35); x ^= x >>> 16; return (x >>> 0) / 4294967296; };
|
|
46
|
+
/** Keyframes: [[ms, value], ...], eased (smoothstep) between neighbours, held outside. */
|
|
47
|
+
export function kf(t, keys) {
|
|
48
|
+
if (t <= keys[0][0]) return keys[0][1];
|
|
49
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
50
|
+
const [t0, v0] = keys[i], [t1, v1] = keys[i + 1];
|
|
51
|
+
if (t < t1) return t1 === t0 ? v1 : v0 + (v1 - v0) * smooth((t - t0) / (t1 - t0));
|
|
52
|
+
}
|
|
53
|
+
return keys[keys.length - 1][1];
|
|
54
|
+
}
|
|
55
|
+
const win = (t, a, b) => t >= a && t < b;
|
|
56
|
+
|
|
57
|
+
/** Fire colour by heat 0..1: dark red, red, orange, amber, pale gold, hot white core. */
|
|
58
|
+
const FIRE_DARK = [[0, [70, 16, 16]], [0.22, RED], [0.45, ORANGE], [0.66, AMBER], [0.84, [253, 224, 140]], [1, CORE]];
|
|
59
|
+
const FIRE_LIGHT = [[0, [127, 29, 29]], [0.25, [220, 38, 38]], [0.5, [234, 88, 12]], [0.75, [245, 140, 11]], [1, [251, 191, 36]]];
|
|
60
|
+
export function fireRGB(h) {
|
|
61
|
+
const S = TH.light ? FIRE_LIGHT : FIRE_DARK;
|
|
62
|
+
h = clamp(h);
|
|
63
|
+
for (let i = 0; i < S.length - 1; i++) if (h <= S[i + 1][0]) return mix(S[i][1], S[i + 1][1], (h - S[i][0]) / (S[i + 1][0] - S[i][0]));
|
|
64
|
+
return S[S.length - 1][1];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- geometry (unit space: y 0..1 down the Q, x 0..ASPECT; the same silhouette as qart.js) ----------------
|
|
68
|
+
const BOWL = { cx: ASPECT / 2, cy: 0.6, rx: 0.4, ry: 0.385, w: 0.118 };
|
|
69
|
+
const bez = (a, b, c, t) => [(1 - t) ** 2 * a[0] + 2 * (1 - t) * t * b[0] + t * t * c[0], (1 - t) ** 2 * a[1] + 2 * (1 - t) * t * b[1] + t * t * c[1]];
|
|
70
|
+
const HORNS = [
|
|
71
|
+
{ a: [0.2, 0.29], b: [-0.02, 0.27], c: [0.02, 0.03], r0: 0.048 },
|
|
72
|
+
{ a: [0.62, 0.29], b: [0.84, 0.27], c: [0.8, 0.0], r0: 0.048 },
|
|
73
|
+
];
|
|
74
|
+
const HORN_PTS = HORNS.map((hn) => Array.from({ length: 24 }, (_, i) => ({ p: bez(hn.a, hn.b, hn.c, i / 23), t: i / 23, r0: hn.r0 })));
|
|
75
|
+
const TAIL0 = { a: [0.4, 0.69], b: [0.79, 0.985], r0: 0.07, r1: 0.04 };
|
|
76
|
+
const TAIL_LEN0 = Math.hypot(TAIL0.b[0] - TAIL0.a[0], TAIL0.b[1] - TAIL0.a[1]);
|
|
77
|
+
const TAIL_ANG0 = Math.atan2(TAIL0.b[1] - TAIL0.a[1], TAIL0.b[0] - TAIL0.a[0]);
|
|
78
|
+
const TAIL_FIXED = 0.5 * TAIL_LEN0; // the stroke inside the bowl and through the ring never bends
|
|
79
|
+
// The mouth is a jaw notch at the bowl's lower right, just above where the tail leaves the ring: the bowl is the
|
|
80
|
+
// upper jaw and the tail stroke the lower one, so the fire comes out of the Q's own stroke rather than a hole in
|
|
81
|
+
// the side of its head. A small notch is always there (the mouth corner); it opens wide to breathe.
|
|
82
|
+
const MOUTH_ANG = 0.52; // radians below the bowl's right-hand horizontal (normalised ellipse angle)
|
|
83
|
+
const NOTCH = 0.07; // the resting notch half-width (radians)
|
|
84
|
+
|
|
85
|
+
function bodyAt(x, y) {
|
|
86
|
+
const dx = (x - BOWL.cx) / BOWL.rx, dy = (y - BOWL.cy) / BOWL.ry, e = Math.hypot(dx, dy);
|
|
87
|
+
const inner = 1 - BOWL.w / ((BOWL.rx + BOWL.ry) / 2);
|
|
88
|
+
if (e <= 1 && e >= inner) return { part: 'bowl', t: 0, ang: Math.atan2(dy, dx) };
|
|
89
|
+
if (y > 0.34) return null;
|
|
90
|
+
for (const pts of HORN_PTS) for (const q of pts) if (Math.hypot(x - q.p[0], y - q.p[1]) <= q.r0 * (1 - q.t * 0.93) + 0.004) return { part: 'horn', t: q.t, ang: 0 };
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const DSIZES = { large: 72, mid: 44, small: 24 }; // small = qart's compact Q (under 60 cols), for the intro hand-over
|
|
95
|
+
/**
|
|
96
|
+
* Layout for a frame `cols` wide. large (18-row Q) from 64 cols, mid (11 rows) below. The Q sits at the left;
|
|
97
|
+
* everything to its right is room for fire and labels; rows below hold the tail.
|
|
98
|
+
*/
|
|
99
|
+
export function dragonLayout(cols, { size, lm, ty = 0, rows: fixedRows } = {}) {
|
|
100
|
+
size ??= cols >= 64 ? 'large' : 'mid';
|
|
101
|
+
const H = DSIZES[size], qw = Math.round(H * ASPECT);
|
|
102
|
+
lm ??= cols >= 64 ? 2 : 1; // left margin in cells (0 lines the dragon up with qart's Q, for the intro hand-over)
|
|
103
|
+
const rows = fixedRows ?? Math.ceil((ty + H * 1.34) / 4); // ty: dots of room above the Q (the flight's raised wings and its bob)
|
|
104
|
+
const L = { size, H, cols, rows, Wd: cols * 2, Hd: rows * 4, qx0: lm * 2, qy0: ty, qw, qCols: Math.ceil((lm * 2 + qw) / 2) };
|
|
105
|
+
L.X = (x) => L.qx0 + x * H;
|
|
106
|
+
L.Y = (y) => L.qy0 + y * H;
|
|
107
|
+
L.grad = (xd) => mix(TH.body0, TH.body1, clamp((xd - L.qx0) / qw));
|
|
108
|
+
L.mouth = [L.X(BOWL.cx + BOWL.rx * Math.cos(MOUTH_ANG) * 0.97), L.Y(BOWL.cy + BOWL.ry * Math.sin(MOUTH_ANG) * 0.97)];
|
|
109
|
+
L.mouthL = [L.X(BOWL.cx - BOWL.rx * Math.cos(MOUTH_ANG) * 0.97), L.mouth[1]]; // the mirrored mouth (pose.mouthSide 'left')
|
|
110
|
+
// large eyes are a tall oval (8 quadrant rows) so the iris can move up and down with a row of white to spare
|
|
111
|
+
return L;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const bodyCache = new Map();
|
|
115
|
+
function bodyDots(L) {
|
|
116
|
+
const key = `${L.size}:${L.qx0}:${L.qy0}`;
|
|
117
|
+
if (bodyCache.has(key)) return bodyCache.get(key);
|
|
118
|
+
const dots = [];
|
|
119
|
+
for (let y = 0; y < L.H; y++) for (let x = 0; x < L.qw + 2; x++) {
|
|
120
|
+
let n = 0;
|
|
121
|
+
for (const [sx, sy] of [[0.25, 0.25], [0.75, 0.25], [0.25, 0.75], [0.75, 0.75]]) if (bodyAt((x + sx) / L.H, (y + sy) / L.H)) n++;
|
|
122
|
+
if (n >= 2) { const p = bodyAt((x + 0.5) / L.H, (y + 0.5) / L.H) || { part: 'bowl', t: 0, ang: 0 }; dots.push({ x: x + L.qx0, y: y + L.qy0, ...p }); }
|
|
123
|
+
}
|
|
124
|
+
bodyCache.set(key, dots);
|
|
125
|
+
return dots;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- poses -------------------------------------------------------------------------------------------------
|
|
129
|
+
/** The settled dragon: tail at rest, no fire (the eyes are the dot layer, see qdragonScenes.js). Also the static (reduced motion) form. */
|
|
130
|
+
export function restPose() {
|
|
131
|
+
return {
|
|
132
|
+
tail: { len: 1.32, bend: -0.6, amp: 0, phase: 0, k: 2.4, curl: 0, wrap: 0, spade: 1 },
|
|
133
|
+
glow: 1, tint: null, tintAmt: 0, mouth: 0, mouthSide: 'right', // 'left' = screen left: the face and the fire mirror
|
|
134
|
+
fire: null, label: null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// ---- drawing -----------------------------------------------------------------------------------------------
|
|
138
|
+
function makeField(W, H) {
|
|
139
|
+
const v = new Float32Array(W * H), c = new Float32Array(W * H * 3);
|
|
140
|
+
return {
|
|
141
|
+
W, H, v, c,
|
|
142
|
+
set(x, y, val, rgb) {
|
|
143
|
+
x = Math.round(x); y = Math.round(y);
|
|
144
|
+
if (x < 0 || y < 0 || x >= W || y >= H) return;
|
|
145
|
+
const i = y * W + x;
|
|
146
|
+
if (val > v[i]) { v[i] = val; c[i * 3] = rgb[0]; c[i * 3 + 1] = rgb[1]; c[i * 3 + 2] = rgb[2]; }
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function drawBody(F, p, L) {
|
|
152
|
+
for (const d of bodyDots(L)) {
|
|
153
|
+
let rgb = L.grad(d.x);
|
|
154
|
+
if (p.tint && p.tintAmt) rgb = mix(rgb, p.tint, p.tintAmt);
|
|
155
|
+
F.set(d.x, d.y, p.glow, rgb);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The tail spine: fixed through the ring, then bent by sway (a travelling wave), curl (tip spiral) and wrap. */
|
|
160
|
+
export function tailSpine(tp, L) {
|
|
161
|
+
const len = (tp.len + 0.85 * tp.wrap) * TAIL_LEN0, step = 0.5 / L.H, pts = [];
|
|
162
|
+
let x = TAIL0.a[0], y = TAIL0.a[1], th = TAIL_ANG0;
|
|
163
|
+
// wrap: a heading offset (not extra curvature, which spirals into a loop). The tail turns once, runs back to
|
|
164
|
+
// the left under the bowl, and the tip hooks up again: a sweep under the Q and back.
|
|
165
|
+
const wrapOff = (f) => tp.wrap * (2.6 * smooth((f - 0.2) / 0.34) + 0.95 * smooth((f - 0.78) / 0.22));
|
|
166
|
+
for (let s = 0; s <= len; s += step) {
|
|
167
|
+
const f = s > TAIL_FIXED ? (s - TAIL_FIXED) / (len - TAIL_FIXED) : 0, h = th + (tp.wrap ? wrapOff(f) : 0);
|
|
168
|
+
pts.push({ x, y, th: h, s, r: Math.max(0.017, TAIL0.r0 - ((TAIL0.r0 - TAIL0.r1) * s) / TAIL_LEN0) });
|
|
169
|
+
if (s > TAIL_FIXED) {
|
|
170
|
+
let k = tp.bend * 2 * f * (1 - tp.wrap) + tp.amp * Math.sin(tp.phase - tp.k * f) * (0.35 + f) * (1 - 0.6 * tp.wrap);
|
|
171
|
+
k -= tp.curl * 26 * smooth((f - 0.45) / 0.45);
|
|
172
|
+
th += k * step;
|
|
173
|
+
}
|
|
174
|
+
x += Math.cos(h) * step; y += Math.sin(h) * step;
|
|
175
|
+
}
|
|
176
|
+
return pts;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// v2: how much of the tail shows at (xd, yd) dots: inside the bowl it is behind the head, so it fades to a ghost
|
|
180
|
+
const INNER = 1 - BOWL.w / ((BOWL.rx + BOWL.ry) / 2);
|
|
181
|
+
function behind(L, xd, yd) {
|
|
182
|
+
const x = (xd - L.qx0) / L.H, y = (yd - L.qy0) / L.H, e = Math.hypot((x - BOWL.cx) / BOWL.rx, (y - BOWL.cy) / BOWL.ry);
|
|
183
|
+
return e < INNER ? 0 : 1; // wholly behind the head
|
|
184
|
+
}
|
|
185
|
+
function drawTail(F, p, L) {
|
|
186
|
+
const tp = p.tail, pts = tailSpine(tp, L);
|
|
187
|
+
const glow = p.glow, tint = p.tint && p.tintAmt ? p.tint : null;
|
|
188
|
+
const col = (xd) => (tint ? mix(L.grad(xd), tint, p.tintAmt) : L.grad(xd));
|
|
189
|
+
const n = pts.length;
|
|
190
|
+
for (let i = 0; i < n; i++) {
|
|
191
|
+
const q = pts[i], f = i / (n - 1);
|
|
192
|
+
const cx = L.X(q.x), cy = L.Y(q.y), R = Math.max(0.6, q.r * L.H * (1 - 0.55 * smooth((f - 0.6) / 0.4)));
|
|
193
|
+
for (let dy = -Math.ceil(R); dy <= Math.ceil(R); dy++) for (let dx = -Math.ceil(R); dx <= Math.ceil(R); dx++) {
|
|
194
|
+
if (dx * dx + dy * dy <= R * R + 0.25) F.set(cx + dx, cy + dy, glow * behind(L, cx + dx, cy + dy), col(cx + dx));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// v2: little spines along the back of the tail (a dragon, not an imp)
|
|
198
|
+
for (const fs of [0.42, 0.6, 0.76]) {
|
|
199
|
+
const i = Math.round(fs * (n - 1)), q = pts[i], R = q.r * L.H * (1 - 0.55 * smooth((fs - 0.6) / 0.4));
|
|
200
|
+
if (q.s <= TAIL_FIXED * 0.98) continue;
|
|
201
|
+
const c = Math.cos(q.th), s = Math.sin(q.th), nx = s, ny = -c, h = (0.08 - 0.03 * fs) * L.H, b = 0.032 * L.H;
|
|
202
|
+
const bx = L.X(q.x) + nx * R * 0.7, by = L.Y(q.y) + ny * R * 0.7;
|
|
203
|
+
for (let a = -b; a <= b; a += 0.5) for (let u = 0; u <= h * (1 - Math.abs(a + b * 0.5) / (b * 1.5)); u += 0.5) {
|
|
204
|
+
F.set(bx + c * a + nx * u, by + s * a + ny * u, glow, col(bx + c * a));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (tp.spade > 0.01) {
|
|
208
|
+
// a sharp spade at the tip: a long point and two barbs swept back, aligned to the tail's last heading
|
|
209
|
+
const e = pts[pts.length - 1], c = Math.cos(e.th), s = Math.sin(e.th), sz = 0.075 * tp.spade;
|
|
210
|
+
const P = (along, side) => [L.X(e.x + c * along * sz - s * side * sz), L.Y(e.y + s * along * sz + c * side * sz)];
|
|
211
|
+
const poly = [P(2.4, 0), P(0.2, 1.0), P(0.55, 0.25), P(-0.3, 0.12), P(-0.3, -0.12), P(0.55, -0.25), P(0.2, -1.0)];
|
|
212
|
+
const xs = poly.map((q) => q[0]), ys = poly.map((q) => q[1]);
|
|
213
|
+
for (let y = Math.floor(Math.min(...ys)); y <= Math.ceil(Math.max(...ys)); y++) for (let x = Math.floor(Math.min(...xs)); x <= Math.ceil(Math.max(...xs)); x++) {
|
|
214
|
+
let inside = false;
|
|
215
|
+
for (let a = 0, b = poly.length - 1; a < poly.length; b = a++) {
|
|
216
|
+
const [xa, ya] = poly[a], [xb, yb] = poly[b];
|
|
217
|
+
if ((ya > y) !== (yb > y) && x < ((xb - xa) * (y - ya)) / (yb - ya) + xa) inside = !inside;
|
|
218
|
+
}
|
|
219
|
+
if (inside) F.set(x, y, glow, col(x));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The face, so the bowl reads as a head: a smile that opens to talk (and to breathe), pink cheeks. p.frown turns the closed smile over. */
|
|
225
|
+
export const FACE = { y: 0.75, w: 0.07 };
|
|
226
|
+
function drawFace(F, p, L) {
|
|
227
|
+
const ink = [224, 231, 255], o = clamp(p.smile || 0), w = FACE.w, y0 = FACE.y, fx = BOWL.cx + (p.faceX || 0);
|
|
228
|
+
for (let u = -1; u <= 1.0001; u += 0.03) {
|
|
229
|
+
const x = L.X(fx + u * w), top = L.Y(y0 + 0.03 * (1 - u * u)), bot = L.Y(y0 + (0.03 + 0.07 * o) * (1 - u * u));
|
|
230
|
+
if (o > 0.05) { for (let y = Math.round(top); y <= bot; y++) F.set(x, y, 1, Math.abs(u) < 0.6 && y > top + 2 && y < bot - 1 ? [244, 114, 182] : ink); }
|
|
231
|
+
else F.set(x, p.frown ? L.Y(y0 + 0.03 * u * u) : top, 1.05, ink); // frown: the closed smile turned over (a mood, with the eyes)
|
|
232
|
+
}
|
|
233
|
+
for (const sd of [-1, 1]) for (let dy = -1; dy <= 1; dy++) for (let dx = -3; dx <= 3; dx++) {
|
|
234
|
+
if ((dx / 3.4) ** 2 + (dy / 1.6) ** 2 <= 1) F.set(L.X(BOWL.cx + (p.faceX || 0) * 0.5 + sd * 0.2) + dx, L.Y(0.7) + dy, 0.9, [244, 114, 182]);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** v2: two small bat wings tucked behind the head (dim: they are behind it), flapping gently with p.flap (radians). */
|
|
239
|
+
function fillPoly(F, poly, val, rgbAt, L) {
|
|
240
|
+
const xs = poly.map((q) => q[0]), ys = poly.map((q) => q[1]);
|
|
241
|
+
for (let y = Math.floor(Math.min(...ys)); y <= Math.ceil(Math.max(...ys)); y++) for (let x = Math.floor(Math.min(...xs)); x <= Math.ceil(Math.max(...xs)); x++) {
|
|
242
|
+
let inside = false;
|
|
243
|
+
for (let a = 0, b = poly.length - 1; a < poly.length; b = a++) {
|
|
244
|
+
const [xa, ya] = poly[a], [xb, yb] = poly[b];
|
|
245
|
+
if ((ya > y) !== (yb > y) && x < ((xb - xa) * (y - ya)) / (yb - ya) + xa) inside = !inside;
|
|
246
|
+
}
|
|
247
|
+
if (inside && (!L || behind(L, x, y))) F.set(x, y, val(x, y), rgbAt(x, y));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const WING = [[0, -0.2], [0.34, -0.5], [0.36, -0.3], [0.27, -0.28], [0.25, -0.15], [0.16, -0.17], [0.13, -0.04], [0, 0.02]];
|
|
251
|
+
export let WING_K = 0.66, WING_V = 0.42;
|
|
252
|
+
export const setWing = (k, v) => { WING_K = k; WING_V = v; };
|
|
253
|
+
function drawWings(F, p, L) {
|
|
254
|
+
if (p.wings === 0) return;
|
|
255
|
+
const a = p.flap || 0;
|
|
256
|
+
for (const sd of [-1, 1]) {
|
|
257
|
+
const K = WING_K * (p.wingScale || 1), rx = BOWL.cx + sd * BOWL.rx * 0.86, ry = BOWL.cy - 0.04, c = Math.cos(a * sd), s = Math.sin(a * sd);
|
|
258
|
+
const poly = WING.map(([u, v]) => { const x = K * (u * c + v * s * sd), y = K * (-u * s * sd + v * c); return [L.X(rx + sd * x), L.Y(ry + y)]; });
|
|
259
|
+
fillPoly(F, poly, () => WING_V, (x) => L.grad(x), L);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Fire breath, embers and smoke at ms since the breath began. Particles are seeded, positions are closed-form. */
|
|
263
|
+
const fireCache = new Map();
|
|
264
|
+
function fireModel(L, seed, dur) {
|
|
265
|
+
const key = `${L.size}:${L.cols}:${seed}:${dur}${L.left ? `:L${L.qx0}` : ''}`;
|
|
266
|
+
if (fireCache.has(key)) return fireCache.get(key);
|
|
267
|
+
const R = rng(seed), reach = Math.max(14, Math.min(L.H * 1.25, L.left ? L.mouth[0] - 6 : L.Wd - L.mouth[0] - 6));
|
|
268
|
+
const N = Math.round(reach * 26), parts = [];
|
|
269
|
+
for (let i = 0; i < N; i++) {
|
|
270
|
+
const b = R() * dur, u = b / dur;
|
|
271
|
+
const env = smooth(u / 0.12) * (1 - smooth((u - 0.78) / 0.22));
|
|
272
|
+
if (R() > 0.15 + 0.85 * env) continue;
|
|
273
|
+
const ember = R() < 0.07;
|
|
274
|
+
parts.push({
|
|
275
|
+
b, ember, life: ember ? 1400 + R() * 1300 : 300 + R() * 330, ang: 0.06 + (R() - 0.5) * 0.3, v: (reach / 520) * (0.75 + 0.5 * R()),
|
|
276
|
+
ph: R() * TAU, wf: 0.012 + R() * 0.012, lat: (R() - 0.5) * 2, hj: 0.85 + 0.15 * R(), id: i,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const smoke = [];
|
|
280
|
+
for (let i = 0; i < Math.round(reach * 0.32); i++) {
|
|
281
|
+
smoke.push({
|
|
282
|
+
b: dur * 0.45 + R() * (dur * 0.6 + 1400), d: (0.15 + 0.8 * R()) * reach, dy: (R() - 0.5) * 4, life: 1700 + R() * 1300,
|
|
283
|
+
rise: 0.010 + R() * 0.007, drift: (R() - 0.25) * 0.004, amp: 1.4 + R() * 1.8, w: 0.004 + R() * 0.003, ph: R() * TAU,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const m = { parts, smoke, reach };
|
|
287
|
+
fireCache.set(key, m);
|
|
288
|
+
return m;
|
|
289
|
+
}
|
|
290
|
+
function drawFire(F, fire, L) {
|
|
291
|
+
const { t, dur = 1200, seed = 3, scale = 1 } = fire;
|
|
292
|
+
const M = fireModel(L, seed, dur), [mx, my] = L.mouth, sg = L.left ? -1 : 1;
|
|
293
|
+
for (const q of M.parts) {
|
|
294
|
+
const age = t - q.b;
|
|
295
|
+
if (age < 0 || age > q.life) continue;
|
|
296
|
+
const s = age / q.life;
|
|
297
|
+
if (q.ember) {
|
|
298
|
+
const tau = 260, dist = q.v * tau * (1 - Math.exp(-age / tau)) * 1.25 * scale;
|
|
299
|
+
const x = mx + sg * (dist * Math.cos(q.ang) + Math.sin(age * 0.004 + q.ph) * 2.2), y = my + dist * Math.sin(q.ang) + q.lat * 3 - age * 0.009;
|
|
300
|
+
if (hash(q.id * 31 + Math.floor(age / 85)) < 0.28) continue; // flicker
|
|
301
|
+
F.set(x, y, 0.75 + 0.35 * (1 - s), fireRGB(0.72 - 0.4 * s));
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
for (let k = 0; k < 2; k++) {
|
|
305
|
+
const a = Math.max(0, age - k * 22), sa = a / q.life;
|
|
306
|
+
const dist = q.v * a * (1 - 0.32 * sa) * scale;
|
|
307
|
+
const fr = dist / M.reach;
|
|
308
|
+
const spread = q.lat * dist * 0.2 + Math.sin(a * q.wf + q.ph) * dist * 0.08;
|
|
309
|
+
const x = mx + sg * (dist * Math.cos(q.ang) - Math.sin(q.ang) * spread), y = my + dist * Math.sin(q.ang) + spread * Math.cos(q.ang) - fr * fr * M.reach * 0.14;
|
|
310
|
+
const heat = clamp((1 - sa) ** 0.75 * q.hj * (1 - 0.45 * fr) * (1 - 0.45 * Math.abs(q.lat)) + (dist < 6 ? 0.35 : 0));
|
|
311
|
+
F.set(x, y, (k ? 0.55 : 0.7) + 0.55 * heat, fireRGB(heat - k * 0.08));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const q of M.smoke) drawWisp(F, mx + sg * q.d * Math.cos(0.08), my + q.d * Math.sin(0.08) + q.dy, t - q.b, q, sg);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* A smoke wisp: a rising, gently swaying streak (its head plus a trail of where it has been). Grey shades fade
|
|
319
|
+
* with age (lighter grey to background), and the whole wisp drifts sideways as it rises.
|
|
320
|
+
*/
|
|
321
|
+
function drawWisp(F, x0, y0, age, q, sg = 1) {
|
|
322
|
+
if (age < 0 || age > q.life + 500) return;
|
|
323
|
+
const pos = (a) => [x0 + sg * (a * q.drift + Math.sin(a * q.w + q.ph) * q.amp * (0.4 + a / q.life)), y0 - a * q.rise];
|
|
324
|
+
let prev = null;
|
|
325
|
+
for (let k = 0; k < 12; k++) {
|
|
326
|
+
const a = age - k * 45;
|
|
327
|
+
if (a < 0) break;
|
|
328
|
+
if (a > q.life) continue;
|
|
329
|
+
const s = a / q.life, fade = (1 - s) ** 1.2 * smooth(a / 220) * (1 - k / 13);
|
|
330
|
+
if (fade < 0.06) continue;
|
|
331
|
+
const [x, y] = pos(a), rgb = mix(TH.smoke0, TH.smoke1, s);
|
|
332
|
+
F.set(x, y, 0.2 + 0.6 * fade, rgb);
|
|
333
|
+
if (prev && Math.hypot(x - prev[0], y - prev[1]) > 1.2) F.set((x + prev[0]) / 2, (y + prev[1]) / 2, 0.2 + 0.55 * fade, rgb);
|
|
334
|
+
prev = [x, y];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---- compose -----------------------------------------------------------------------------------------------
|
|
339
|
+
const BR = [[0x1, 0x8], [0x2, 0x10], [0x4, 0x20], [0x40, 0x80]];
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Render a pose: rows of cells { ch, rgb, bg }. opts: { mono, theme }. (t is accepted and ignored: nothing flickers.)
|
|
343
|
+
* The label (pose.label = { text | cells, rgb, row, col }) is written into the frame to the right of the Q.
|
|
344
|
+
*/
|
|
345
|
+
export function renderDragon(pose, L0, { mono = false, theme = 'dark' } = {}) {
|
|
346
|
+
TH = THEMES[theme] || THEMES.dark;
|
|
347
|
+
const L = pose.mouthSide === 'left' ? { ...L0, mouth: [L0.X(BOWL.cx + (pose.faceX || 0) - FACE.w * 0.6), L0.Y(FACE.y + 0.05)], left: true } : L0; // the mouth-dependent parts see the mirrored mouth
|
|
348
|
+
const F = makeField(L.Wd, L.Hd);
|
|
349
|
+
drawWings(F, pose, L);
|
|
350
|
+
drawBody(F, pose, L);
|
|
351
|
+
drawTail(F, pose, L);
|
|
352
|
+
drawFace(F, pose, L);
|
|
353
|
+
if (pose.fire) drawFire(F, pose.fire, L);
|
|
354
|
+
const thr = mono ? 0.35 : 0.04, rows = [];
|
|
355
|
+
for (let cy = 0; cy < L.rows; cy++) {
|
|
356
|
+
const row = [];
|
|
357
|
+
for (let cx = 0; cx < L.cols; cx++) {
|
|
358
|
+
let bits = 0, m = 0, mi = -1;
|
|
359
|
+
for (let dy = 0; dy < 4; dy++) for (let dx = 0; dx < 2; dx++) {
|
|
360
|
+
const i = (cy * 4 + dy) * L.Wd + cx * 2 + dx, val = F.v[i];
|
|
361
|
+
if (val > thr) { bits |= BR[dy][dx]; if (val > m) { m = val; mi = i; } }
|
|
362
|
+
}
|
|
363
|
+
if (!bits) { row.push({ ch: ' ', rgb: null, bg: null }); continue; }
|
|
364
|
+
let rgb = null;
|
|
365
|
+
if (!mono) {
|
|
366
|
+
const base = [F.c[mi * 3], F.c[mi * 3 + 1], F.c[mi * 3 + 2]], f = 0.28 + 0.72 * Math.min(1, m), fl = clamp(m - 1);
|
|
367
|
+
rgb = TH.light
|
|
368
|
+
? base.map((c, j) => Math.round((TH.bg[j] + (c - TH.bg[j]) * (0.12 + 0.88 * Math.min(1, m))) * (1 - 0.25 * fl)))
|
|
369
|
+
: base.map((c) => { c *= f; return Math.round(c + (255 - c) * fl * 0.9); });
|
|
370
|
+
}
|
|
371
|
+
row.push({ ch: String.fromCharCode(0x2800 + bits), rgb, bg: null });
|
|
372
|
+
}
|
|
373
|
+
rows.push(row);
|
|
374
|
+
}
|
|
375
|
+
const lb = pose.label;
|
|
376
|
+
if (lb) {
|
|
377
|
+
const cells = (lb.cells || [...lb.text].map((ch) => ({ ch, rgb: lb.rgb || null }))).map((cl) => ({ ch: cl.ch, rgb: cl.rgb ? TH.label(cl.rgb) : null }));
|
|
378
|
+
let r = lb.row ?? Math.floor(L.H / 8), c0 = lb.col ?? L.qCols + (L.cols >= 64 ? 7 : 2);
|
|
379
|
+
if (c0 + cells.length > L.cols) { r = L.H / 4 + 1; c0 = Math.max(0, Math.floor((L.qCols - cells.length) / 2) + 2); } // no room beside: under the Q
|
|
380
|
+
const target = r < rows.length ? rows[r] : null;
|
|
381
|
+
if (target) cells.forEach((cl, k) => { if (c0 + k < L.cols) target[c0 + k] = { ch: cl.ch, rgb: mono ? null : cl.rgb, bg: null }; });
|
|
382
|
+
}
|
|
383
|
+
return rows;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ---- output helpers ----------------------------------------------------------------------------------------
|
|
387
|
+
const ANSI16 = [[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0], [0, 0, 128], [128, 0, 128], [0, 128, 128], [192, 192, 192],
|
|
388
|
+
[128, 128, 128], [255, 0, 0], [0, 255, 0], [255, 255, 0], [92, 92, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255]];
|
|
389
|
+
function sgr(rgb, depth, bg) {
|
|
390
|
+
const [r, g, b] = rgb;
|
|
391
|
+
if (depth === 3) return `\x1b[${bg ? 48 : 38};2;${r};${g};${b}m`;
|
|
392
|
+
if (depth === 2) return `\x1b[${bg ? 48 : 38};5;${16 + 36 * Math.round(r / 51) + 6 * Math.round(g / 51) + Math.round(b / 51)}m`;
|
|
393
|
+
let best = 0, bd = 1e9;
|
|
394
|
+
ANSI16.forEach((c, i) => { const d = (c[0] - r) ** 2 + (c[1] - g) ** 2 + (c[2] - b) ** 2; if (d < bd) { bd = d; best = i; } });
|
|
395
|
+
return `\x1b[${(best < 8 ? 30 + best : 82 + best) + (bg ? 10 : 0)}m`;
|
|
396
|
+
}
|
|
397
|
+
/** Cells -> ANSI lines. depth 3 truecolor, 2 = 256, 1 = 16, 0 = none. */
|
|
398
|
+
export function cellsToAnsi(rows, depth = 3) {
|
|
399
|
+
return rows.map((row) => {
|
|
400
|
+
let s = '', lf = '', lb = '';
|
|
401
|
+
for (const c of row) {
|
|
402
|
+
const f = depth && c.rgb ? sgr(c.rgb, depth, false) : '', b = depth && c.bg ? sgr(c.bg, depth, true) : '';
|
|
403
|
+
if ((lb && !b) || (lf && !f && c.ch !== ' ')) { s += '\x1b[0m'; lf = ''; lb = ''; }
|
|
404
|
+
if (f && f !== lf) { s += f; lf = f; }
|
|
405
|
+
if (b && b !== lb) { s += b; lb = b; }
|
|
406
|
+
s += c.ch;
|
|
407
|
+
}
|
|
408
|
+
return s + (lf || lb ? '\x1b[0m' : '');
|
|
409
|
+
});
|
|
410
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// /dragon-fly: the v8 dragon in flight (canonical source: quox cli/lib/quoxon/qdragonFly.js).
|
|
2
|
+
//
|
|
3
|
+
// The same dragon as everywhere else (googly dot eyes, the smile, pink cheeks, dim bat wings, the spade tail),
|
|
4
|
+
// made by the same composeCells; in flight the wings are bigger and beat hard, the body bobs with each stroke, the
|
|
5
|
+
// tail streams behind and the eyes look the way it is going. It crosses the sky once, left to right.
|
|
6
|
+
//
|
|
7
|
+
// PURE AND PORTABLE: no DOM, no timers, no Node APIs. flyFrame(ms, cols, opts) -> rows of { ch, rgb, bg } cells.
|
|
8
|
+
import { dragonLayout, restPose, kf, DSIZES } from './qdragon.js';
|
|
9
|
+
import { composeCells, PICK_EYES } from './qdragonScenes.js';
|
|
10
|
+
import { eyeState } from './qeyes.js';
|
|
11
|
+
|
|
12
|
+
const TAU = Math.PI * 2;
|
|
13
|
+
export const FLAP_MS = 760; // one wingbeat
|
|
14
|
+
export const FLY_PASS_MS = 9000; // one pass across the sky
|
|
15
|
+
export const FLY_STILL_MS = 3000; // how long the still frame stays for reduced motion / NO_COLOR
|
|
16
|
+
const SIZE = 'mid';
|
|
17
|
+
const WING_SCALE = 1.7;
|
|
18
|
+
const TOP = 26; // dots of sky above the Q: room for the raised wings and the bob
|
|
19
|
+
const BOB = 5; // dots the body rises and falls with each stroke
|
|
20
|
+
const SPAN = 30; // cells of overshoot either side so the wings enter and leave whole
|
|
21
|
+
|
|
22
|
+
/** The widest sky (in cells) worth drawing: never wider than `cols`; the dragon is 18 cells tall at most, so termRows is not a limit. */
|
|
23
|
+
export function flyCols(cols) {
|
|
24
|
+
return Math.max(40, Math.min(cols, 120));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The dragon's pose in flight at ms into the pass. */
|
|
28
|
+
export function flyPose(ms) {
|
|
29
|
+
const p = restPose(), ph = (TAU * ms) / FLAP_MS;
|
|
30
|
+
p.wingScale = WING_SCALE;
|
|
31
|
+
p.flap = -0.2 + 0.8 * Math.sin(ph);
|
|
32
|
+
p.tail.len = 1.55; p.tail.k = 3.2; p.tail.amp = 1.5; p.tail.phase = ms * 0.009;
|
|
33
|
+
p.smile = 0.3;
|
|
34
|
+
return p;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The frame at `ms` since the pass began. still: the one fixed frame (mid-sky, wings raised). mono: shapes only. */
|
|
38
|
+
/** The sky's size for a terminal `termRows` tall: the mid Q, or the small Q (headroom scaled to match) when the mid sky would
|
|
39
|
+
* not leave 10 rows for the transcript and composer. Never taller than max(12, termRows - 10). */
|
|
40
|
+
export function flySky(termRows) {
|
|
41
|
+
const room = Math.max(12, (termRows || 40) - 10);
|
|
42
|
+
const fits = (size, top) => dragonLayout(120, { size, lm: 0, ty: top }).rows <= room;
|
|
43
|
+
if (fits(SIZE, TOP)) return { size: SIZE, top: TOP, bob: BOB };
|
|
44
|
+
const k = DSIZES.small / DSIZES[SIZE];
|
|
45
|
+
return { size: 'small', top: Math.round(TOP * k), bob: Math.max(1, Math.round(BOB * k)) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function flyFrame(ms, cols, { still = false, mono = false, theme = 'dark', termRows } = {}) {
|
|
49
|
+
const t = still ? 0 : Math.max(0, Math.min(ms, FLY_PASS_MS));
|
|
50
|
+
const { size: SIZE, top: TOP, bob: BOB } = flySky(termRows);
|
|
51
|
+
const probe = dragonLayout(cols, { size: SIZE, lm: 0, ty: TOP });
|
|
52
|
+
const rows = probe.rows;
|
|
53
|
+
const xc = still ? cols / 2 : -SPAN + (cols + 2 * SPAN) * kf(t, [[0, 0], [FLY_PASS_MS, 1]]);
|
|
54
|
+
const lm = Math.round(xc - probe.qw / 4);
|
|
55
|
+
const ph = (TAU * t) / FLAP_MS, bob = still ? 0 : BOB * Math.cos(ph);
|
|
56
|
+
const L = dragonLayout(cols, { size: SIZE, lm, ty: Math.round(TOP - 2 + bob), rows });
|
|
57
|
+
const pose = flyPose(t);
|
|
58
|
+
if (still) pose.flap = 0.55; // wings up
|
|
59
|
+
const eyes = still ? eyeState('open', 0) : { ...eyeState('idle', t), gx: 0.9, gy: 0.5 };
|
|
60
|
+
return composeCells(pose, eyes, L, PICK_EYES, { theme, mono, t });
|
|
61
|
+
}
|