quoxcli 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +27 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/contactsq.js +4 -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/hat.js +14 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +19 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +295 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +47 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +61 -7
- package/lib/commands/approvals.js +198 -3
- package/lib/commands/ask.js +4 -2
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +142 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +7 -33
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +155 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +1123 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +318 -0
- package/lib/commands/lucid.js +1 -0
- package/lib/commands/mail.js +161 -20
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +55 -144
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +259 -0
- package/lib/commands/objective.js +53 -3
- package/lib/commands/offload.js +2 -3
- package/lib/commands/org.js +21 -4
- package/lib/commands/plan.js +17 -0
- package/lib/commands/provider.js +87 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/quoxseo.js +31 -0
- package/lib/commands/research.js +79 -0
- package/lib/commands/rows.js +9 -0
- package/lib/commands/run.js +20 -2
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +150 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +24 -5
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/vm.js +2 -2
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +824 -0
- package/lib/commands/wazuhIntake.js +317 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actionItems.js +115 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +37 -3
- package/lib/core/commandRegistrars.js +17 -0
- package/lib/core/config.js +29 -3
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +136 -8
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +73 -7
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +8 -8
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- package/lib/core/walkthrough.js +9 -5
- 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/quoxon/EXTENSIONS.md +146 -0
- package/lib/quoxon/api.js +364 -27
- package/lib/quoxon/app.js +1629 -183
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +64 -8
- package/lib/quoxon/approvalSentence.js +48 -0
- package/lib/quoxon/approvalsView.js +124 -7
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +1 -1
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +22 -3
- package/lib/quoxon/commands.js +110 -15
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dnd.js +71 -0
- package/lib/quoxon/dragonCompanion.js +285 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +160 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +67 -10
- package/lib/quoxon/extensions.js +636 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +182 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +4 -4
- package/lib/quoxon/loopCard.js +460 -0
- package/lib/quoxon/loopCards.js +189 -0
- package/lib/quoxon/loopStream.js +98 -0
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +29 -5
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/objects/index.js +22 -4
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +49 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +8 -3
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +187 -41
- package/lib/quoxon/roster.js +36 -17
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +86 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +18 -5
- package/lib/quoxon/slash/broadcastRun.js +13 -2
- package/lib/quoxon/slash/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +139 -2
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/palette.js +23 -2
- package/lib/quoxon/slash/paletteController.js +10 -4
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +31 -4
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +17 -0
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/suggestCommand.js +34 -3
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +81 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +55 -0
- package/package.json +4 -2
- 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/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/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/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -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/quoxperticus.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/companions-v1/vulcan.mjs +2 -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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Lucid's pure acting layer: clip metadata, deterministic performances, story. */
|
|
2
|
+
import {arrivalPose,ARRIVAL_DURATION,DECK_ARRIVAL_DURATION} from './arrival.mjs';
|
|
3
|
+
import {motionAt} from './motion.mjs';
|
|
4
|
+
import {renderMaster} from './render.mjs';
|
|
5
|
+
const clamp=x=>Math.max(0,Math.min(1,x));
|
|
6
|
+
const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
|
|
7
|
+
export const LUCID_CLIPS=Object.freeze([
|
|
8
|
+
{id:'attach',title:'I See You',duration:ARRIVAL_DURATION,message:'Hello. I am here.'},
|
|
9
|
+
{id:'deck-arrival',title:'Lucid Arrives',duration:DECK_ARRIVAL_DURATION,message:'LUCID · Awareness in the present'},
|
|
10
|
+
{id:'idle',title:'Here and Now',duration:12000,message:'Aware. Attentive. Present.'},
|
|
11
|
+
{id:'greeting',title:'Hello There',duration:1800,message:'Hello. What shall we look at?'},
|
|
12
|
+
{id:'curious',title:'A Closer Look',duration:2600,message:'That is interesting…'},
|
|
13
|
+
{id:'notice',title:'Something Changed',duration:1800,message:'I noticed a change.'},
|
|
14
|
+
{id:'focus',title:'Hold That Thought',duration:2200,message:'Looking closely.'},
|
|
15
|
+
{id:'uncertain',title:'Missing Piece',duration:2200,message:'There is a part I cannot see yet.'},
|
|
16
|
+
{id:'delighted',title:'Oh, There It Is',duration:1800,message:'Oh! There it is.'},
|
|
17
|
+
{id:'standby',title:'Quiet Company',duration:3000,message:'I am here.'},
|
|
18
|
+
{id:'sweep',title:'Horizon Sweep',duration:3200,message:'Horizon Sweep · visual demo',power:true},
|
|
19
|
+
{id:'threadlight',title:'Threadlight',duration:3600,message:'Threadlight · visual demo',power:true},
|
|
20
|
+
{id:'constellation',title:'Living Constellation',duration:4200,message:'Living Constellation · visual demo',power:true},
|
|
21
|
+
]);
|
|
22
|
+
export const LUCID_STORY=Object.freeze([
|
|
23
|
+
{scene:'greeting',duration:1800},{scene:'idle',duration:2400},
|
|
24
|
+
{scene:'curious',duration:2200},{scene:'notice',duration:1500},
|
|
25
|
+
{scene:'sweep',duration:3200},{scene:'focus',duration:1600},
|
|
26
|
+
{scene:'threadlight',duration:3600},{scene:'uncertain',duration:1800},
|
|
27
|
+
{scene:'constellation',duration:4200},{scene:'delighted',duration:1800},
|
|
28
|
+
{scene:'idle',duration:2000},
|
|
29
|
+
]);
|
|
30
|
+
export const STORY_DURATION=LUCID_STORY.reduce((n,s)=>n+s.duration,0);
|
|
31
|
+
export const LUCID_MANIFEST=Object.freeze({id:'lucid',displayName:'Lucid',
|
|
32
|
+
role:'Attentive observer of the living present',defaultClip:'idle',
|
|
33
|
+
clips:LUCID_CLIPS,story:LUCID_STORY,
|
|
34
|
+
powers:LUCID_CLIPS.filter(c=>c.power).map(c=>({name:c.title,scene:c.id,visualOnly:true})),
|
|
35
|
+
notes:'SVG recovery rig. White round eyes, black centre pupil. Not yet registered in the shared Character Pack.'});
|
|
36
|
+
|
|
37
|
+
export function eyePerformance(scene,t){
|
|
38
|
+
const blink=t%4800;
|
|
39
|
+
const base={gx:0,gy:0,open:blink>3600&&blink<3800?.12:1};
|
|
40
|
+
switch(scene){
|
|
41
|
+
case 'attach':case 'deck-arrival':return {gx:0,gy:0,open:1};
|
|
42
|
+
case 'idle':return base;
|
|
43
|
+
case 'greeting':return {gx:0,gy:0,open:t>700&&t<900?.12:1.12};
|
|
44
|
+
case 'curious':return {gx:-.85*ease(t/650),gy:-.3,open:1.18};
|
|
45
|
+
case 'notice':return {gx:ease((t-120)/220),gy:-.4,open:1.2};
|
|
46
|
+
case 'focus':return {gx:.9,gy:0,open:.65};
|
|
47
|
+
case 'uncertain':return {gx:t<1100?-.7:.65,gy:.45,open:.9};
|
|
48
|
+
case 'delighted':return {gx:0,gy:0,open:t>1200&&t<1360?.2:1.2};
|
|
49
|
+
case 'standby':return {...base,open:blink>2500?.2:.85};
|
|
50
|
+
case 'sweep':return {gx:-1+2*ease(t/2200),gy:-.3,open:1};
|
|
51
|
+
case 'threadlight':return {gx:.9,gy:-.3,open:1};
|
|
52
|
+
case 'constellation':return {gx:.8,gy:-.6,open:1.15};
|
|
53
|
+
default:throw new RangeError('Unknown expression');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function powerOverlay(frame,scene,t,duration,mono){
|
|
58
|
+
const rows=frame.length,cols=frame[0].length;
|
|
59
|
+
const fade=ease(t/450)*(1-ease((t-duration+650)/650));
|
|
60
|
+
if(fade<.08)return 0;
|
|
61
|
+
let count=0;
|
|
62
|
+
const point=(x,y,strong=false)=>{
|
|
63
|
+
x=Math.round(x);y=Math.round(y);
|
|
64
|
+
if(x<1||y<1||x>=cols-1||y>=rows-1||frame[y][x].ch!==' ')return;
|
|
65
|
+
frame[y][x]={ch:strong?'⠿':'⠂',rgb:mono?null:(strong?[232,245,255]:[94,205,230]).map(v=>Math.round(v*fade)),bg:null};count++;
|
|
66
|
+
};
|
|
67
|
+
// Deliberately use only the empty sky to the right of the sail.
|
|
68
|
+
const x0=cols*.74,x1=cols-.06*cols,y0=rows*.10,y1=rows*.43;
|
|
69
|
+
const nodes=[[x0,y1],[x0+(x1-x0)*.45,y0],[x1,y1*.85]];
|
|
70
|
+
if(scene==='sweep'){
|
|
71
|
+
const u=clamp(t/(duration-500));
|
|
72
|
+
for(let i=0;i<7;i++){const a=-1.2+i*.4;point(x0+u*(x1-x0)+Math.cos(a)*2,y0+(y1-y0)*.5+Math.sin(a)*(y1-y0)*.5,i===3);}
|
|
73
|
+
}else {
|
|
74
|
+
const amount=clamp((t-300)/(duration-1100));
|
|
75
|
+
for(let i=0;i<3;i++)if(amount>(i/5))point(...nodes[i],true);
|
|
76
|
+
for(let edge=0;edge<(scene==='constellation'?3:2);edge++){
|
|
77
|
+
const a=nodes[edge],b=nodes[(edge+1)%3];
|
|
78
|
+
for(let k=1;k<8;k++)if(amount>(edge+k/8)/3)
|
|
79
|
+
point(a[0]+(b[0]-a[0])*k/8,a[1]+(b[1]-a[1])*k/8);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return count;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function renderLucidCharacter(paths,scene='idle',ms=0,options={}){
|
|
86
|
+
const clip=LUCID_CLIPS.find(c=>c.id===scene);
|
|
87
|
+
if(!clip)return {ok:false,code:'UNKNOWN_CLIP'};
|
|
88
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
89
|
+
const arrival=scene==='attach'||scene==='deck-arrival';
|
|
90
|
+
const time=options.still?0:Math.min(ms,clip.duration);
|
|
91
|
+
const pose=arrival&&!options.still?arrivalPose(ms,{deck:scene==='deck-arrival'}):null;
|
|
92
|
+
const idleTime=pose?.phase==='present'?ms-pose.duration:ms;
|
|
93
|
+
const face=pose?.face??eyePerformance(pose?.phase==='present'?'idle':scene,options.still?0:arrival?idleTime:time);
|
|
94
|
+
const motionTime=options.motionTime??ms;
|
|
95
|
+
const motion=options.still?null:pose?.motion??motionAt(motionTime);
|
|
96
|
+
const result=renderMaster(paths,{...options,face,motion});
|
|
97
|
+
const effectCells=clip.power?powerOverlay(result.frame,scene,options.still?clip.duration/2:time,clip.duration,options.mono):0;
|
|
98
|
+
return {...result,ok:true,scene,clip:clip.title,time,duration:clip.duration,face,motion,
|
|
99
|
+
phase:pose?.phase??scene,caption:clip.message,visualOnly:true,demo:true,effectCells};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function renderLucidStory(paths,ms,options={}){
|
|
103
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
104
|
+
let t=options.still?0:options.loop?ms%STORY_DURATION:Math.min(ms,STORY_DURATION-1);
|
|
105
|
+
for(const beat of LUCID_STORY){if(t<beat.duration)return renderLucidCharacter(paths,beat.scene,t,{...options,motionTime:ms});t-=beat.duration;}
|
|
106
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
|
|
2
|
+
|
|
3
|
+
/** Half-width by half-height, minus margins: below a quarter of the screen. */
|
|
4
|
+
export function artSlot(terminalCols=80,terminalRows=24) {
|
|
5
|
+
if(![terminalCols,terminalRows].every(n=>Number.isSafeInteger(n)&&n>0))
|
|
6
|
+
throw new RangeError('Terminal dimensions must be positive integers.');
|
|
7
|
+
const cols=Math.min(54,Math.floor(terminalCols/2)-1);
|
|
8
|
+
const rows=Math.min(16,Math.floor(terminalRows/2)-1);
|
|
9
|
+
return {cols:Math.max(0,cols),rows:Math.max(0,rows),
|
|
10
|
+
showArt:cols>=28&&rows>=10,terminalCols,terminalRows};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Terminal cell widths for ordinary message text, including CJK and emoji.
|
|
14
|
+
// Never pass message control sequences through to the terminal.
|
|
15
|
+
export function cleanText(text) {
|
|
16
|
+
return String(text).replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|$))/g,'')
|
|
17
|
+
.replace(/[\x00-\x09\x0b-\x1f\x7f-\x9f]/g,'');
|
|
18
|
+
}
|
|
19
|
+
const segmenter=new Intl.Segmenter(undefined,{granularity:'grapheme'});
|
|
20
|
+
const width=g=>/\p{Extended_Pictographic}|[\u1100-\u115f\u2329\u232a\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe6f\uff01-\uff60\uffe0-\uffe6]/u.test(g)?2:1;
|
|
21
|
+
export function wrapMessage(text,cols) {
|
|
22
|
+
if(cols<1)return [];
|
|
23
|
+
const result=[];
|
|
24
|
+
for(const paragraph of cleanText(text).split('\n')) {
|
|
25
|
+
let line='',n=0;
|
|
26
|
+
for(const {segment} of segmenter.segment(paragraph)) {
|
|
27
|
+
const w=width(segment);
|
|
28
|
+
if(n+w>cols){result.push(line.trimEnd());line='';n=0;}
|
|
29
|
+
if(w>cols)continue;
|
|
30
|
+
if(!line&&segment===' ')continue;
|
|
31
|
+
line+=segment;n+=w;
|
|
32
|
+
}
|
|
33
|
+
result.push(line.trimEnd());
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function composeMessage(frame,message,{terminalCols=80}={}) {
|
|
39
|
+
const margin=1,gap=2,artCols=frame?.[0]?.length??0;
|
|
40
|
+
const available=terminalCols-margin*2-artCols-gap;
|
|
41
|
+
if(!frame||available<12) {
|
|
42
|
+
const text=wrapMessage(message,Math.max(1,terminalCols-margin*2)).map(l=>' '.repeat(Math.min(margin,terminalCols-1))+l);
|
|
43
|
+
return frame?[...cellsToAnsi(frame).map(l=>' '.repeat(margin)+l),...text]:text;
|
|
44
|
+
}
|
|
45
|
+
const text=wrapMessage(message,available),art=cellsToAnsi(frame);
|
|
46
|
+
// Messages keep all their content and flow below if taller than the art.
|
|
47
|
+
const offset=text.length<frame.length?Math.floor((frame.length-text.length)/2):0;
|
|
48
|
+
return Array.from({length:Math.max(frame.length,offset+text.length)},(_,i)=>
|
|
49
|
+
' '.repeat(margin)+(art[i]??' '.repeat(artCols))+' '.repeat(gap)+(text[i-offset]??''));
|
|
50
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Lucid — the living present</title>
|
|
3
|
+
<desc id="desc">Full-body vector study: a poised left-facing watcher, swept horns, white eye, tall curved neck, resting haunches, articulated claws, sail wings and a spiral tail. Unapproved art study.</desc>
|
|
4
|
+
<g id="tail">
|
|
5
|
+
<path id="tail-mass" fill="#7453ce" d="M480 352 C533 360 569 405 633 405 C692 405 741 375 741 326 C741 298 721 283 701 285 C679 287 666 305 670 324 C673 341 688 348 702 341 C711 336 715 326 710 317 C723 326 719 347 704 354 C682 365 655 349 651 326 C645 293 668 267 700 266 C740 265 762 292 761 326 C759 390 705 431 638 433 C573 435 519 408 475 393 Z"/>
|
|
6
|
+
<path id="tail-light" fill="#49bdd3" d="M509 384 C565 413 607 428 652 422 C712 415 753 375 752 327 C752 297 731 277 706 278 C733 274 754 293 757 322 C763 378 718 425 655 432 C601 438 552 419 509 397 Z"/>
|
|
7
|
+
<path id="tail-coil-light" fill="#b09af2" d="M700 275 C673 275 655 297 659 323 C662 345 679 355 696 350 C682 350 669 339 667 323 C664 302 678 284 700 282 Z"/>
|
|
8
|
+
<path id="tail-tip" fill="#b5a2f7" d="M702 341 C712 337 714 327 710 317 C706 307 698 305 691 310 C697 299 709 301 715 312 C722 325 715 339 702 341 Z"/>
|
|
9
|
+
</g>
|
|
10
|
+
<g id="far-wing">
|
|
11
|
+
<path id="far-sail" fill="#44357c" d="M314 339 C338 265 361 194 409 114 C413 186 444 231 484 249 C424 245 379 281 346 343 Z"/>
|
|
12
|
+
<path id="far-spar" fill="#9b80e6" d="M314 339 C341 245 371 175 409 114 C388 185 365 254 333 342 Z"/>
|
|
13
|
+
</g>
|
|
14
|
+
<g id="far-feet">
|
|
15
|
+
<path id="far-foreleg" fill="#4b397f" d="M270 330 C285 353 278 385 265 407 L215 418 L199 412 L210 399 L242 392 L245 350 Z"/>
|
|
16
|
+
<path id="far-hindleg" fill="#514080" d="M472 363 C490 378 488 394 473 410 L418 420 L404 414 L420 399 L450 391 Z"/>
|
|
17
|
+
<path id="far-claw-1" fill="#b1c9e6" d="M211 402 C201 408 197 417 197 425 L207 418 L219 408 Z"/>
|
|
18
|
+
<path id="far-claw-2" fill="#b1c9e6" d="M424 404 C414 412 411 420 412 427 L422 419 L434 408 Z"/>
|
|
19
|
+
</g>
|
|
20
|
+
<g id="body">
|
|
21
|
+
<path id="body-mass" fill="#7250bc" d="M239 291 C267 284 296 298 322 319 C359 342 393 319 441 320 C493 320 532 350 554 384 C532 401 490 408 453 402 C421 396 395 390 364 392 C321 402 283 399 252 378 C224 359 219 320 239 291 Z"/>
|
|
22
|
+
<path id="back-plane" fill="#a184e2" d="M288 311 C333 346 370 319 416 320 C469 310 504 333 530 358 C492 337 464 333 433 340 C389 351 365 359 332 346 Z"/>
|
|
23
|
+
<path id="haunch" fill="#8563ce" d="M439 344 C477 331 506 352 516 378 C511 395 485 404 456 400 C431 397 413 384 416 367 C418 356 427 349 439 344 Z"/>
|
|
24
|
+
<path id="haunch-shadow" fill="#4f3b88" d="M454 350 C434 354 427 368 438 380 C451 390 477 389 494 383 C478 399 450 401 433 389 C415 376 427 353 454 350 Z"/>
|
|
25
|
+
</g>
|
|
26
|
+
<g id="near-wing">
|
|
27
|
+
<path id="near-sail" fill="#456cba" d="M299 346 C342 291 371 230 418 170 C453 125 504 90 569 69 C535 106 518 144 516 181 C512 218 533 248 558 265 C519 256 488 259 462 274 C424 297 395 325 361 340 Z"/>
|
|
28
|
+
<path id="upper-membrane" fill="#618fd0" d="M328 325 C376 252 403 192 450 145 C480 116 514 95 552 82 C500 123 464 170 433 222 C403 269 367 303 328 325 Z"/>
|
|
29
|
+
<path id="lower-membrane" fill="#408aa9" d="M345 333 C405 299 457 273 514 252 C494 225 493 199 500 170 C457 225 414 285 345 333 Z"/>
|
|
30
|
+
<path id="wing-leading-edge" fill="#b79af1" d="M295 347 C343 282 367 224 415 165 C457 116 509 86 569 69 C519 93 469 126 428 174 C381 231 357 294 318 345 Z"/>
|
|
31
|
+
<path id="wing-finger-1" fill="#9fcde7" d="M316 340 C379 298 423 247 458 191 C487 144 521 105 561 75 C521 118 494 155 465 201 C431 258 385 308 330 339 Z"/>
|
|
32
|
+
<path id="wing-finger-2" fill="#8cc8e0" d="M330 340 C406 316 466 282 549 263 C478 289 420 324 350 342 Z"/>
|
|
33
|
+
<path id="wing-fold-shadow" fill="#383168" d="M317 343 C364 323 405 296 443 264 C411 303 384 331 360 341 Z"/>
|
|
34
|
+
</g>
|
|
35
|
+
<g id="neck">
|
|
36
|
+
<path id="neck-mass" fill="#7753c5" d="M217 166 C247 181 263 210 266 245 C269 282 254 310 265 339 C273 359 294 372 319 379 C291 397 256 394 229 377 C205 362 199 338 206 309 C214 280 226 256 222 231 C219 210 203 197 188 191 Z"/>
|
|
37
|
+
<path id="neck-shadow" fill="#4e3a89" d="M223 196 C251 216 257 245 248 274 C234 309 238 340 260 360 C247 355 229 337 230 316 C230 291 244 269 244 244 C244 223 235 209 223 196 Z"/>
|
|
38
|
+
<path id="throat" fill="#87bfd6" d="M194 189 C214 206 230 221 229 245 C229 272 211 302 212 327 C213 352 226 367 247 378 C227 366 218 345 224 321 C229 301 245 277 245 249 C245 221 230 203 210 190 Z"/>
|
|
39
|
+
<path id="chest-plane" fill="#91a9df" d="M214 317 C213 341 229 365 254 374 C272 381 289 382 307 378 C292 390 263 389 242 381 C221 372 206 349 208 329 Z"/>
|
|
40
|
+
<path id="throat-segments" fill="none" stroke="#5263a0" stroke-width="3" d="M218 226 Q229 229 240 225 M227 246 Q235 250 245 247 M224 266 Q234 270 241 269 M217 287 Q226 291 234 292 M212 309 Q220 313 227 313 M213 331 Q221 335 227 333 M220 352 Q230 353 236 350"/>
|
|
41
|
+
</g>
|
|
42
|
+
<g id="near-feet">
|
|
43
|
+
<path id="foreleg" fill="#9273d6" d="M247 337 C261 341 268 354 263 371 C258 385 243 394 225 402 L215 414 L200 414 L194 422 L182 417 L171 422 L161 416 C167 401 184 395 203 388 C218 382 224 371 226 354 Z"/>
|
|
44
|
+
<path id="foreleg-plane" fill="#b29de9" d="M246 346 C251 362 245 378 227 388 L193 402 L179 414 L169 413 C176 403 188 400 204 392 C227 381 234 368 233 355 Z"/>
|
|
45
|
+
<path id="hindfoot" fill="#9476d5" d="M472 379 C480 388 477 398 463 403 L430 410 L415 425 L401 423 L395 430 L383 424 L371 429 L362 421 C369 404 386 397 408 395 L432 386 Z"/>
|
|
46
|
+
<path id="hindfoot-plane" fill="#ae95e1" d="M449 392 L411 403 C392 404 379 414 372 424 L364 420 C372 405 389 400 407 398 Z"/>
|
|
47
|
+
<path id="fore-claw-1" fill="#d2e3ef" d="M169 412 C159 414 154 426 157 439 C161 429 165 426 175 423 Z"/>
|
|
48
|
+
<path id="fore-claw-2" fill="#d2e3ef" d="M188 414 C180 420 178 430 181 443 C185 433 189 428 196 425 Z"/>
|
|
49
|
+
<path id="fore-claw-3" fill="#d2e3ef" d="M209 409 C203 415 201 425 204 436 C208 426 212 422 218 420 Z"/>
|
|
50
|
+
<path id="hind-claw-1" fill="#c4d9eb" d="M372 420 C362 423 359 434 361 445 C366 436 370 433 378 431 Z"/>
|
|
51
|
+
<path id="hind-claw-2" fill="#c4d9eb" d="M392 423 C385 429 384 438 387 449 C391 440 395 436 402 433 Z"/>
|
|
52
|
+
<path id="hind-claw-3" fill="#c4d9eb" d="M413 418 C407 424 406 432 409 442 C414 433 418 430 424 427 Z"/>
|
|
53
|
+
</g>
|
|
54
|
+
<g id="horns">
|
|
55
|
+
<path id="far-horn" fill="#8ba1d7" d="M197 148 C222 114 256 87 302 57 C277 84 254 110 237 133 L218 159 Z"/>
|
|
56
|
+
<path id="far-horn-tip" fill="#c5dce9" d="M239 112 C259 91 278 75 302 57 C286 74 270 91 256 107 Z"/>
|
|
57
|
+
<path id="near-horn" fill="#a68be4" d="M165 151 C171 127 190 102 219 78 C239 62 261 45 281 28 C265 53 244 74 226 95 C205 118 196 139 194 157 Z"/>
|
|
58
|
+
<path id="near-horn-light" fill="#d1daf3" d="M174 143 C181 121 200 99 221 81 C242 63 263 46 281 28 C267 50 243 73 228 90 C204 116 191 138 188 153 Z"/>
|
|
59
|
+
<path id="horn-root" fill="#8461ce" d="M168 141 L184 129 L204 144 L220 133 L231 153 L214 174 L181 167 Z"/>
|
|
60
|
+
</g>
|
|
61
|
+
<g id="head">
|
|
62
|
+
<path id="head-mass" fill="#8963d4" d="M156.48 146.84 C172.16 130.2 197.92 130.2 218.08 141.72 L243.84 162.2 L227.04 180.12 L236 195.48 L211.36 199.32 C201.28 214.68 182.24 219.8 166.56 213.4 L136.32 212.12 L118.4 223.64 L91.52 219.8 L83.68 209.56 L103.84 185.24 L119.52 162.2 Z"/>
|
|
63
|
+
<path id="brow-plane" fill="#b797f0" d="M119.52 162.2 C143.04 153.24 158.72 139.16 181.12 139.16 C197.92 137.88 211.36 144.28 222.56 151.96 L204.64 155.8 L181.12 149.4 L158.72 166.04 L131.84 173.72 Z"/>
|
|
64
|
+
<path id="muzzle-plane" fill="#629fca" d="M103.84 185.24 L126.24 180.12 L137.44 187.8 L124 201.88 L96 209.56 L83.68 209.56 Z"/>
|
|
65
|
+
<path id="jaw-plane" fill="#b4a2e2" d="M92.64 212.12 L120.64 213.4 L140.8 200.6 L162.08 200.6 C175.52 212.12 193.44 209.56 208 200.6 C195.68 222.36 177.76 226.2 159.84 215.96 L138.56 214.68 L120.64 224.92 L93.76 221.08 Z"/>
|
|
66
|
+
<path id="cheek-plane" fill="#6846a8" d="M204.64 172.44 L225.92 181.4 L220.32 191.64 L202.4 195.48 L187.84 207 L175.52 200.6 Z"/>
|
|
67
|
+
<path id="ear-fin" fill="#a184e4" d="M209.12 153.24 C231.52 143 251.68 144.28 269.6 136.6 L247.2 162.2 L227.04 169.88 Z"/>
|
|
68
|
+
<path id="ear-fold" fill="#4d4286" d="M225.92 157.08 L255.04 145.56 L240.48 159.64 L225.92 164.76 Z"/>
|
|
69
|
+
<path id="cheek-fin" fill="#6caac9" d="M221.44 181.4 L258.4 185.24 L237.12 195.48 L220.32 192.92 Z"/>
|
|
70
|
+
<path id="nostril" fill="#24213c" d="M104.96 194.2 Q110.56 187.8 113.92 191.64 Q110.56 198.04 104.96 198.04 Z"/>
|
|
71
|
+
<path id="mouth-line" fill="none" stroke="#312542" stroke-width="3" d="M96 212.12 Q118.4 218.52 138.56 204.44 Q150.88 200.6 163.2 204.44"/>
|
|
72
|
+
</g>
|
|
73
|
+
<g id="eye" data-cell-owner="eye">
|
|
74
|
+
<path id="eye-socket" fill="#29213d" d="M146 175 C146 158 157 149 178 149 C199 149 211 159 211 176 C211 195 198 205 178 205 C158 205 146 194 146 175 Z"/>
|
|
75
|
+
<path id="iris" fill="#f2f6ff" d="M149 175 C149 160 160 152 178 152 C196 152 208 161 208 176 C208 192 196 202 178 202 C161 202 149 192 149 175 Z"/>
|
|
76
|
+
<path id="pupil" fill="#111520" d="M186 177 C186 182 182 185 178 185 C173 185 170 182 170 177 C170 172 173 169 178 169 C182 169 186 172 186 177 Z"/>
|
|
77
|
+
</g>
|
|
78
|
+
</svg>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Small joint movements over a twelve-second cycle, with deliberate rests. */
|
|
2
|
+
const clamp=x=>Math.max(0,Math.min(1,x));
|
|
3
|
+
const smooth=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 MOTION_PERIOD=12000;
|
|
6
|
+
export function motionAt(ms){
|
|
7
|
+
if(!Number.isFinite(ms)||ms<0)throw new RangeError('Motion time must be nonnegative.');
|
|
8
|
+
const t=ms%MOTION_PERIOD;
|
|
9
|
+
return {
|
|
10
|
+
tail:pulse(t,600,4200)-.45*pulse(t,4500,6200)+.22*pulse(t,6500,8000),
|
|
11
|
+
wing:.045*pulse(t,2600,4400)-.010*pulse(t,4400,5100)+.025*pulse(t,9200,10800),
|
|
12
|
+
farWing:.026*pulse(t,2800,4600)-.006*pulse(t,4600,5300)+.016*pulse(t,9400,11000),
|
|
13
|
+
neck:.032*pulse(t,1100,4700)-.014*pulse(t,7200,9900),
|
|
14
|
+
nod:3*pulse(t,1300,4900)+2*pulse(t,7400,10100),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const rotate=(x,y,cx,cy,a)=>[
|
|
18
|
+
cx+(x-cx)*Math.cos(a)-(y-cy)*Math.sin(a),
|
|
19
|
+
cy+(x-cx)*Math.sin(a)+(y-cy)*Math.cos(a),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/** Deform authored paths together, never append procedural anatomy. */
|
|
23
|
+
export function movePoint(group,x,y,pose){
|
|
24
|
+
if(!pose)return [x,y];
|
|
25
|
+
if(group==='tail'){
|
|
26
|
+
// Root is pinned; only the distal tail and spiral tighten/open/recoil.
|
|
27
|
+
const w=smooth((x-515)/145),curl=pose.tail*w;
|
|
28
|
+
const [rx,ry]=rotate(x,y,700,325,curl*.15);
|
|
29
|
+
const scale=1-.14*curl;
|
|
30
|
+
return [700+(rx-700)*scale-8*Math.abs(curl),325+(ry-325)*scale-10*curl];
|
|
31
|
+
}
|
|
32
|
+
if(group==='near-wing')return rotate(x,y,310,341,pose.wing);
|
|
33
|
+
if(group==='far-wing')return rotate(x,y,330,340,pose.farWing);
|
|
34
|
+
if(['neck','head','horns','eye'].includes(group)){
|
|
35
|
+
// The neck smoothly blends to its anchored chest. Face, eye and horns
|
|
36
|
+
// share the upper joint transform, so the eye never slides off the head.
|
|
37
|
+
const w=group==='neck'?smooth((330-y)/130):1;
|
|
38
|
+
const [rx,ry]=rotate(x,y,235,300,pose.neck*w);
|
|
39
|
+
return [rx,ry+pose.nod*w];
|
|
40
|
+
}
|
|
41
|
+
return [x,y];
|
|
42
|
+
}
|
|
@@ -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
|
+
}
|