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,87 @@
|
|
|
1
|
+
import {renderSvgRig} from '../../../factory/svg-rig.mjs';
|
|
2
|
+
import {poseDrago} from '../v1/character.mjs';
|
|
3
|
+
const clamp=x=>Math.max(0,Math.min(1,x));
|
|
4
|
+
const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
|
|
5
|
+
const pulse=(t,a,b)=>t<=a||t>=b?0:Math.sin(Math.PI*(t-a)/(b-a))**2;
|
|
6
|
+
export const DRAGO_CLIPS=Object.freeze([
|
|
7
|
+
{id:'idle',title:'Keeping Watch',duration:14000},
|
|
8
|
+
{id:'attach',title:'The Guardian Arrives',duration:3800},
|
|
9
|
+
{id:'active',title:'Locked On',duration:3600},
|
|
10
|
+
{id:'inspect',title:'Closer Inspection',duration:3800},
|
|
11
|
+
{id:'guard',title:'Hold the Line',duration:4200,power:true},
|
|
12
|
+
{id:'concerned',title:'Needs Attention',duration:3200},
|
|
13
|
+
{id:'acknowledge',title:'Understood',duration:2800},
|
|
14
|
+
{id:'standby',title:'Awaiting You',duration:3200}
|
|
15
|
+
]);
|
|
16
|
+
export const STORY=[['attach',3800],['idle',2400],['inspect',3800],['active',3600],['guard',4200],['concerned',3200],['acknowledge',2800],['standby',3200],['idle',2500]];
|
|
17
|
+
export const STORY_DURATION=STORY.reduce((n,b)=>n+b[1],0);
|
|
18
|
+
const captions={idle:'On guard.',attach:'At your side.',active:'Active expression study.',inspect:'Inspection expression study.',guard:'Shield-forward visual study.',concerned:'Concern expression study.',acknowledge:'Acknowledgement expression study.',standby:'Waiting expression study.'};
|
|
19
|
+
export function bodyPose(ms){const t=ms%14000;return {
|
|
20
|
+
breath:1.8*pulse(t,500,6500),wing:.025*pulse(t,3000,6500),far:.019*pulse(t,3500,7000),tail:1.1*pulse(t,7500,12000),
|
|
21
|
+
turn:.009*pulse(t,900,3800),nod:1.8*pulse(t,900,3800),shield:0,grip:0,gaze:0,glare:false,eye:1
|
|
22
|
+
};}
|
|
23
|
+
function performance(scene,ms,options){
|
|
24
|
+
const t=options.still?0:ms;
|
|
25
|
+
let pose=bodyPose(options.still?0:(options.motionTime??t));
|
|
26
|
+
if(scene==='attach'){
|
|
27
|
+
const duration=options.deck?4800:3800;
|
|
28
|
+
if(t>=duration)return bodyPose(t-duration);
|
|
29
|
+
const u=t/duration;
|
|
30
|
+
pose={...bodyPose(0),gaze:.6*pulse(u,0,.55),turn:-.015*pulse(u,.1,.7),nod:5*pulse(u,.32,.9),
|
|
31
|
+
wing:.06*pulse(u,.16,.8),far:.045*pulse(u,.23,.88),shield:.65*pulse(u,.3,.86),grip:pulse(u,.3,.86),tail:pulse(u,.45,1),glare:u>.32&&u<.67};
|
|
32
|
+
}
|
|
33
|
+
if(scene==='active')pose={...pose,glare:true,nod:pose.nod+3,grip:.7,gaze:.2};
|
|
34
|
+
if(scene==='inspect')pose={...pose,gaze:.8*Math.sin(t/1100),turn:pose.turn+.018*Math.sin(t/1100),nod:pose.nod+1};
|
|
35
|
+
if(scene==='guard'){const p=options.still?1:pulse(t,0,4200);pose={...pose,glare:true,shield:p,grip:p,wing:pose.wing+.045*p,far:pose.far+.035*p,nod:pose.nod+5*p};}
|
|
36
|
+
if(scene==='concerned')pose={...pose,turn:pose.turn-.022,nod:pose.nod+2,gaze:-.5,eye:1.08};
|
|
37
|
+
if(scene==='acknowledge')pose={...pose,nod:pose.nod+6*pulse(t,300,2100),eye:1.05};
|
|
38
|
+
if(scene==='standby')pose={...pose,gaze:-.65,turn:pose.turn-.012,eye:.9};
|
|
39
|
+
if(options.still)pose={...pose,breath:0,wing:0,far:0,tail:0,nod:0,turn:0,gaze:0};
|
|
40
|
+
return pose;
|
|
41
|
+
}
|
|
42
|
+
const rotate=(x,y,cx,cy,a)=>[cx+(x-cx)*Math.cos(a)-(y-cy)*Math.sin(a),cy+(x-cx)*Math.sin(a)+(y-cy)*Math.cos(a)];
|
|
43
|
+
export function poseRig(rig,pose){
|
|
44
|
+
const source=poseDrago(rig,pose.glare?'active':'idle');
|
|
45
|
+
const paths=source.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
|
|
46
|
+
if(p.id.endsWith('-eye-white')||p.id.endsWith('-pupil')){
|
|
47
|
+
y=127+(y-127)*pose.eye;
|
|
48
|
+
if(p.id.endsWith('-pupil'))x+=pose.gaze*3;
|
|
49
|
+
}
|
|
50
|
+
if(p.group==='head'||p.group==='eyes'){
|
|
51
|
+
const v=rotate(x,y,385,190,pose.turn);return [v[0],v[1]+pose.nod-pose.breath];
|
|
52
|
+
}
|
|
53
|
+
if(['neck','throat','throat-plates','spine'].includes(p.id)){
|
|
54
|
+
const w=ease((290-y)/100),v=rotate(x,y,385,190,pose.turn*w);return [v[0],v[1]+(pose.nod-pose.breath)*w];
|
|
55
|
+
}
|
|
56
|
+
if(p.group==='wings'){
|
|
57
|
+
const right=x>400,a=right?-pose.far:pose.wing;return rotate(x,y,right?455:335,290,a);
|
|
58
|
+
}
|
|
59
|
+
if(p.group==='tail'){
|
|
60
|
+
// Pin the upper-right attachment. Distal coil gathers inward with a small settling recoil.
|
|
61
|
+
const w=ease((y-360)/55)*ease((610-x)/160),recoil=pose.tail-.24*pose.tail*pose.tail;
|
|
62
|
+
return [x+(390-x)*.025*w*recoil,y-5*w*recoil];
|
|
63
|
+
}
|
|
64
|
+
if(p.group==='shield')return [390+(x-390)*(1+.06*pose.shield),350+(y-350)*(1+.06*pose.shield)-9*pose.shield-pose.breath*.3];
|
|
65
|
+
if(p.group==='hands'){
|
|
66
|
+
const w=ease((y-270)/40);return [x+(x<390?2:-2)*pose.grip*w+(x-390)*.06*pose.shield*w,y-(9*pose.shield+pose.breath*.3)*w];
|
|
67
|
+
}
|
|
68
|
+
return [x,y];
|
|
69
|
+
}))}));
|
|
70
|
+
const byId=new Map(paths.map(p=>[p.id,p]));
|
|
71
|
+
return {...source,paths,bounds:{left:rig.bounds.left-10,top:rig.bounds.top-10,right:rig.bounds.right+10,bottom:rig.bounds.bottom+6},
|
|
72
|
+
eyes:source.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}))};
|
|
73
|
+
}
|
|
74
|
+
export function renderDrago(rig,scene='idle',ms=0,options={}){
|
|
75
|
+
const clip=DRAGO_CLIPS.find(c=>c.id===scene);
|
|
76
|
+
if(!clip)return {ok:false,code:'UNKNOWN_CLIP'};
|
|
77
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
78
|
+
const pose=performance(scene,ms,options),posed=poseRig(rig,pose),r=renderSvgRig(posed,options);
|
|
79
|
+
// A local shield glow, never a claim about security posture or real authorisation.
|
|
80
|
+
if(scene==='guard'&&!options.mono){const brightness=options.still?.9:pulse(ms,0,4200);for(const row of r.frame)for(const cell of row)if(cell.rgb&&cell.rgb[0]===255&&cell.rgb[1]===112){cell.rgb=[255,Math.round(112+70*brightness),Math.round(88+50*brightness)];}}
|
|
81
|
+
return {...r,scene,clip:clip.title,caption:captions[scene],pose,staticStudy:false};
|
|
82
|
+
}
|
|
83
|
+
export function renderStory(rig,ms,options={}){
|
|
84
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
85
|
+
let t=options.still?0:Math.min(ms,STORY_DURATION-1);
|
|
86
|
+
for(const [scene,duration]of STORY){if(t<duration)return renderDrago(rig,scene,t,{...options,deck:false,motionTime:Math.max(0,ms-3800)});t-=duration;}
|
|
87
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {turnHead} from './profile.mjs';
|
|
2
|
+
import {renderDrago as renderV2,poseRig,bodyPose,DRAGO_CLIPS as V2_CLIPS} from '../v2/character.mjs';
|
|
3
|
+
import {renderSvgRig} from '../../../factory/svg-rig.mjs';
|
|
4
|
+
import {flattenPath} from '../../stage3/svg-coverage.js';
|
|
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
|
+
const envelope=(t,a,b,c,d)=>ease((t-a)/(b-a))*(1-ease((t-c)/(d-c)));
|
|
8
|
+
export const FIRE_DURATION=6800;
|
|
9
|
+
export const DRAGO_CLIPS=Object.freeze([...V2_CLIPS,{id:'fire',title:'Scoped Inferno',duration:FIRE_DURATION,power:true}]);
|
|
10
|
+
export const STORY=[['attach',3800],['idle',1400],['fire',FIRE_DURATION],['idle',2400]];
|
|
11
|
+
export const STORY_DURATION=STORY.reduce((n,b)=>n+b[1],0);
|
|
12
|
+
const path=(id,d,rgb,stroke=0)=>({id,group:'fire-effect',polygons:flattenPath(d),rgb,stroke,erase:rgb.every((v,i)=>v===[8,13,25][i])});
|
|
13
|
+
const point=([x,y])=>`${x.toFixed(3)} ${y.toFixed(3)}`;
|
|
14
|
+
export function fireRig(rig,ms,options={}){
|
|
15
|
+
const t=options.still?3000:ms;
|
|
16
|
+
const aim=envelope(t,0,900,5600,FIRE_DURATION),blast=envelope(t,1200,1700,4300,4800),embers=envelope(t,4400,4700,5200,5600);
|
|
17
|
+
const pose={...bodyPose(0),glare:aim>.2,grip:aim,shield:.25*aim,nod:2*aim,gaze:.5*aim,turn:.018*aim,wing:.012*aim,far:0};
|
|
18
|
+
const posed=poseRig(rig,pose);
|
|
19
|
+
// Lower the right sail so the line of fire clears its claw and membrane.
|
|
20
|
+
let paths=posed.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>p.group==='wings'&&x>400?[x,y+28*aim*ease((300-y)/160)]:[x,y]))}));
|
|
21
|
+
const turn=ease(Math.max(0,Math.min(1,(t-180)/720)))*(1-ease((t-5600)/1200));
|
|
22
|
+
paths=turnHead(paths,turn);
|
|
23
|
+
const mouth=[419+(502-419)*turn,169+(124-169)*turn],target=[650,57],dx=target[0]-mouth[0],dy=target[1]-mouth[1],length=Math.hypot(dx,dy),normal=[-dy/length,dx/length];
|
|
24
|
+
if(aim>.02){
|
|
25
|
+
// A visible target box describes the animation's scope; it is not an approval badge.
|
|
26
|
+
const c=blast>.1?[255,116,56]:[210,148,92];
|
|
27
|
+
paths.push(path('scope-brackets','M 634 24 L 619 24 L 619 39 M 665 24 L 681 24 L 681 39 M 619 75 L 619 91 L 635 91 M 665 91 L 681 91 L 681 75',c,5));
|
|
28
|
+
paths.push(path('scope-target','M 650 44 L 663 57 L 650 70 L 637 57 Z',[255,185,94],3));
|
|
29
|
+
}
|
|
30
|
+
if(blast>.01){
|
|
31
|
+
const reach=ease((t-1200)/500),tip=[mouth[0]+dx*reach,mouth[1]+dy*reach];
|
|
32
|
+
const flame=(id,width,rgb,phase)=>{
|
|
33
|
+
const sides=[[],[]];
|
|
34
|
+
for(let i=0;i<=24;i++){
|
|
35
|
+
const u=i/24,x=mouth[0]+(tip[0]-mouth[0])*u,y=mouth[1]+(tip[1]-mouth[1])*u;
|
|
36
|
+
const rip=.70+.30*Math.sin(u*33-t*.025+phase),w=(2+width*Math.sin(Math.PI*u*.76))*blast*rip;
|
|
37
|
+
for(let side=0;side<2;side++){const s=side?1:-1;sides[side].push([x+normal[0]*w*s,y+normal[1]*w*s]);}
|
|
38
|
+
}
|
|
39
|
+
paths.push(path(id,'M '+[...sides[0],...sides[1].reverse()].map(point).join(' L ')+' Z',rgb));
|
|
40
|
+
};
|
|
41
|
+
flame('fire-outer',20,[235,48,30],0);flame('fire-orange',13,[255,116,30],.5);flame('fire-gold',7,[255,211,70],1);flame('fire-core',2.5,[255,249,215],1.4);
|
|
42
|
+
if(reach>.95){
|
|
43
|
+
// Impact burst remains wholly within the target's fixed box.
|
|
44
|
+
const ring=[];for(let j=0;j<20;j++){const a=j*Math.PI/10,r=(j%2?9:19+4*Math.sin(t*.03+j))*blast;ring.push([650+Math.cos(a)*r,57+Math.sin(a)*r]);}
|
|
45
|
+
paths.push(path('fire-impact','M '+ring.map(point).join(' L ')+' Z',[255,176,45]));
|
|
46
|
+
paths.push(path('fire-impact-core','M 650 48 L 658 57 L 650 66 L 642 57 Z',[255,249,215]));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if(embers>.01)for(let i=0;i<7;i++){
|
|
50
|
+
const age=clamp((t-4400)/1200),x=632+i*5,y=45+((i*13)%22)+age*10;
|
|
51
|
+
paths.push(path('fire-ember-'+i,`M ${x} ${y} L ${x+2} ${y} L ${x+2} ${y+3} L ${x} ${y+3} Z`,[255,Math.round(110+80*embers),35]));
|
|
52
|
+
}
|
|
53
|
+
const byId=new Map(paths.map(p=>[p.id,p]));
|
|
54
|
+
return {rig:{...posed,paths,eyes:posed.eyes.filter(e=>turn<.95||e.name!=='far').map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}))},pose,phase:t<1200?'Aim':t<4800?'Directed fire':t<5600?'Embers':'Settle',blast,aim,mouth,target};
|
|
55
|
+
}
|
|
56
|
+
export function renderDrago(rig,scene='idle',ms=0,options={}){
|
|
57
|
+
if(scene!=='fire')return renderV2(rig,scene,ms,options);
|
|
58
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
59
|
+
if(!options.still&&ms>=FIRE_DURATION)return renderV2(rig,'idle',ms-FIRE_DURATION,options);
|
|
60
|
+
const f=fireRig(rig,ms,options),r=renderSvgRig(f.rig,options);
|
|
61
|
+
return {...r,scene,clip:'Scoped Inferno',caption:`${f.phase} · visual rehearsal`,pose:f.pose,phase:f.phase,blast:f.blast,mouth:f.mouth,target:f.target,staticStudy:false};
|
|
62
|
+
}
|
|
63
|
+
export function renderStory(rig,ms,options={}){
|
|
64
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
65
|
+
let t=options.still?0:Math.min(ms,STORY_DURATION-1);
|
|
66
|
+
for(const [scene,duration]of STORY){if(t<duration)return renderDrago(rig,scene,t,{...options,deck:false,motionTime:Math.max(0,ms-3800)});t-=duration;}
|
|
67
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {flattenPath} from '../../stage3/svg-coverage.js';
|
|
2
|
+
// Authored right-facing head: swept-back horns, one visible eye, elongated muzzle and open jaw.
|
|
3
|
+
const shapes={
|
|
4
|
+
'far-horn':'M 380 101 Q 330 87 313 51 Q 353 77 405 77 Z',
|
|
5
|
+
'near-horn':'M 403 92 Q 362 59 346 23 Q 391 59 435 72 Z',
|
|
6
|
+
'skull':'M 365 123 Q 356 91 398 84 Q 434 78 456 99 L 486 98 L 505 110 L 500 120 L 446 139 Q 422 171 383 167 Z',
|
|
7
|
+
'brow-armour':'M 376 100 L 393 79 L 413 84 L 430 71 L 448 95 L 430 109 Z',
|
|
8
|
+
'cheek-blades':'M 386 118 L 344 105 L 362 131 L 339 142 L 376 152 L 357 173 L 400 161 Z',
|
|
9
|
+
'muzzle':'M 427 120 L 491 99 L 506 109 L 502 118 L 443 139 Z M 402 145 L 443 145 L 493 130 L 503 137 Q 458 164 420 170 L 393 159 Z',
|
|
10
|
+
'nose':'M 483 107 L 496 106 L 492 113 L 484 114 Z',
|
|
11
|
+
'mouth-line':'M 425 144 L 500 119',
|
|
12
|
+
'mouth':'M 425 144 L 500 119',
|
|
13
|
+
'fangs':'M 465 129 L 474 140 L 477 125 Z M 487 120 L 493 132 L 498 117 Z',
|
|
14
|
+
'near-eye-socket':'M 397 110 L 439 103 L 440 126 Q 421 143 400 131 Z',
|
|
15
|
+
'near-eye-white':'M 403 115 L 434 109 L 434 124 Q 422 135 405 128 Z',
|
|
16
|
+
'near-pupil':'M 422 121 C 422 112 430 112 430 121 C 430 130 422 130 422 121 Z'
|
|
17
|
+
};
|
|
18
|
+
function samples(polys,n=72){
|
|
19
|
+
const p=polys[0],edges=p.map((v,i)=>{const q=p[(i+1)%p.length];return Math.hypot(q[0]-v[0],q[1]-v[1]);}),total=edges.reduce((a,b)=>a+b,0);
|
|
20
|
+
return Array.from({length:n},(_,i)=>{let s=i*total/n,j=0;while(j<edges.length-1&&s>edges[j])s-=edges[j++];const a=p[j],b=p[(j+1)%p.length],f=edges[j]?s/edges[j]:0;return [a[0]+(b[0]-a[0])*f,a[1]+(b[1]-a[1])*f];});
|
|
21
|
+
}
|
|
22
|
+
export function turnHead(paths,amount){
|
|
23
|
+
return paths.map(p=>{
|
|
24
|
+
if(p.group!=='head'&&p.group!=='eyes'&&!['neck','throat','throat-plates','spine'].includes(p.id))return p;
|
|
25
|
+
if(p.group!=='head'&&p.group!=='eyes')return {...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>[x+22*amount*Math.max(0,Math.min(1,(290-y)/110)),y]))};
|
|
26
|
+
if(p.id.startsWith('far-eye')||p.id==='far-pupil'){
|
|
27
|
+
return {...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>[390+(x-390)*(1-amount),122+(y-122)*(1-amount)]))};
|
|
28
|
+
}
|
|
29
|
+
const d=shapes[p.id];if(!d)return p;
|
|
30
|
+
const target=flattenPath(d);if(amount>=.999)return {...p,polygons:target};
|
|
31
|
+
if(amount<=0)return p;
|
|
32
|
+
const a=samples(p.polygons),b=samples(target);
|
|
33
|
+
return {...p,polygons:[a.map(([x,y],i)=>[x+(b[i][0]-x)*amount,y+(b[i][1]-y)*amount])]};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import {parseSvgRig} from '../../../factory/svg-rig.mjs';
|
|
4
|
+
import {renderDrago,DRAGO_CLIPS} from './character.mjs';
|
|
5
|
+
import {artSlot,composeMessage} from '../../lucid/recovery/layout.mjs';
|
|
6
|
+
import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
|
|
7
|
+
const args=process.argv.slice(2),values=new Set(['--cols','--rows','--terminal-cols','--terminal-rows','--message','--scene','--at']);
|
|
8
|
+
for(let i=0;i<args.length;i++){
|
|
9
|
+
if(values.has(args[i])){if(!args[i+1]||args[i+1].startsWith('--'))throw Error('Missing option value');i++;}
|
|
10
|
+
else if(!['--large','--mono','--help','--watch','--attach','--intro','--still','--list'].includes(args[i]))throw Error('Unknown option: '+args[i]);
|
|
11
|
+
}
|
|
12
|
+
const value=(name,fallback)=>args.includes(name)?args[args.indexOf(name)+1]:fallback;
|
|
13
|
+
if(args.includes('--help')){console.log('Drago V4 scoped fire studio. --watch --attach --intro --still --scene ID --at MS --list --large --mono --message TEXT --cols N --rows N --terminal-cols N --terminal-rows N');process.exit(0);}
|
|
14
|
+
if(args.includes('--list')){console.log(DRAGO_CLIPS.map(c=>c.id+' · '+c.title).join('\n'));process.exit(0);}
|
|
15
|
+
const w=Number(value('--terminal-cols',process.stdout.columns||80)),h=Number(value('--terminal-rows',process.stdout.rows||24));
|
|
16
|
+
if(!Number.isSafeInteger(w)||!Number.isSafeInteger(h)||w<1||h<1)throw Error('Invalid terminal size');
|
|
17
|
+
const slot=artSlot(w,h),large=args.includes('--large')||args.includes('--intro'),explicit=large||args.includes('--cols')||args.includes('--rows');
|
|
18
|
+
const cols=Number(value('--cols',large?Math.min(120,w-4):Math.min(39,slot.cols))),rows=Number(value('--rows',large?Math.min(36,h-4):Math.min(11,slot.rows)));
|
|
19
|
+
const message=value('--message','DRAGO\nOn guard.');
|
|
20
|
+
if(!slot.showArt&&!explicit){console.log(composeMessage(null,message,{terminalCols:w}).join('\n'));}
|
|
21
|
+
else{
|
|
22
|
+
if(cols>w-4||rows>h-2)throw Error('Requested art exceeds terminal margins');
|
|
23
|
+
const rig=parseSvgRig(await fs.readFile(new URL('../v1/drago-master.svg',import.meta.url),'utf8'));
|
|
24
|
+
const opts={cols,rows,mono:args.includes('--mono'),still:args.includes('--still'),deck:large};
|
|
25
|
+
const scene=value('--scene',args.includes('--attach')?'attach':'idle');
|
|
26
|
+
const r=renderDrago(rig,scene,Number(value('--at',0)),opts);
|
|
27
|
+
if(!r.ok)throw Error(r.code);
|
|
28
|
+
if(args.includes('--watch')||args.includes('--attach')||args.includes('--intro')){
|
|
29
|
+
const {watch}=await import('./watch.mjs');
|
|
30
|
+
await watch(rig,opts,{terminalCols:w,terminalRows:h,large,scene:args.includes('--scene')||args.includes('--attach')?scene:null,arrival:args.includes('--attach'),message:args.includes('--message')?message:null});
|
|
31
|
+
}else
|
|
32
|
+
console.log((large?[...cellsToAnsi(r.frame),...composeMessage(null,message,{terminalCols:w})]:composeMessage(r.frame,message,{terminalCols:w})).join('\n'));
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {performance} from 'node:perf_hooks';
|
|
2
|
+
import {DRAGO_CLIPS,renderDrago,renderStory,STORY_DURATION} from './character.mjs';
|
|
3
|
+
import {composeMessage} from '../../lucid/recovery/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?DRAGO_CLIPS.find(c=>c.id===scene):null;
|
|
11
|
+
const duration=(config.arrival&&options.deck?4800: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?renderDrago(paths,scene,t,options):renderStory(paths,t,options);
|
|
16
|
+
if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
|
|
17
|
+
const caption=message??'DRAGO · '+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/6);
|
|
54
|
+
await done;
|
|
55
|
+
}catch(e){cleanup(e);await done;}
|
|
56
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
|
|
2
|
+
<title>Hyperion — gentle bronze guardian, first terminal anatomy</title>
|
|
3
|
+
<g id="far-wing">
|
|
4
|
+
<path id="far-wing-membrane" fill="#a66b32" d="M 298 288 C 242 247 206 210 182 157 C 151 99 130 61 111 32 C 115 127 94 227 51 326 C 90 292 110 271 130 268 C 145 280 155 291 162 307 C 190 272 220 269 242 297 L 298 323 Z"/>
|
|
5
|
+
<path id="far-wing-spar" fill="none" stroke="#e9b966" stroke-width="10" d="M 294 294 C 210 224 158 125 111 32 M 111 32 C 118 141 96 238 51 326 M 120 123 C 134 199 145 256 162 307 M 153 179 C 195 210 220 249 242 297"/>
|
|
6
|
+
<path id="far-wing-claw" fill="#f7dfac" d="M 106 55 C 98 35 104 18 127 16 C 121 30 115 45 106 55 Z"/>
|
|
7
|
+
</g>
|
|
8
|
+
<g id="near-wing">
|
|
9
|
+
<path id="near-wing-membrane" fill="#bf813e" d="M 409 280 C 470 241 507 187 540 118 C 561 71 582 43 608 28 C 611 134 665 223 718 310 C 677 281 644 270 623 282 C 599 291 589 307 582 326 C 552 294 524 285 499 304 C 474 325 462 344 459 362 L 412 324 Z"/>
|
|
10
|
+
<path id="near-wing-spar" fill="none" stroke="#edb967" stroke-width="11" d="M 420 293 C 500 227 541 112 608 28 M 608 28 C 609 142 660 228 718 310 M 608 28 C 559 142 563 233 582 326 M 555 134 C 519 188 504 246 499 304"/>
|
|
11
|
+
<path id="near-wing-claw" fill="#ffe6b0" d="M 600 47 C 582 31 597 11 624 16 C 617 25 611 36 600 47 Z"/>
|
|
12
|
+
</g>
|
|
13
|
+
<g id="tail">
|
|
14
|
+
<path id="tail-mass" fill="#b77935" d="M 421 363 C 477 386 523 410 590 413 C 652 416 716 401 733 362 C 753 317 735 277 703 270 C 670 261 645 277 641 302 C 635 327 650 348 671 349 C 694 351 705 335 701 320 C 696 336 678 336 671 326 C 659 311 674 293 691 299 C 710 304 714 331 699 351 C 676 378 626 382 580 368 C 522 351 486 326 440 321 Z"/>
|
|
15
|
+
<path id="tail-belly" fill="#e2c18a" d="M 438 370 C 509 404 557 425 622 417 C 665 414 707 396 720 377 C 672 399 625 403 586 396 C 528 387 489 362 448 342 Z"/>
|
|
16
|
+
<path id="tail-ridges" fill="#e9b968" d="M 503 349 L 516 330 L 529 361 M 548 365 L 563 346 L 576 376 M 596 380 L 612 363 L 624 382 M 651 375 L 665 354 L 676 368 M 699 348 L 719 343 L 707 365"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g id="rear-feet">
|
|
19
|
+
<path id="far-rear-leg" fill="#80532d" d="M 409 333 C 447 321 486 343 497 378 L 510 410 C 525 421 522 440 502 442 L 452 440 C 440 428 444 406 429 392 Z"/>
|
|
20
|
+
<path id="far-foot" fill="#d5ad72" d="M 448 420 Q 460 406 469 424 Q 477 405 487 424 Q 501 410 510 431 L 453 434 Z"/>
|
|
21
|
+
</g>
|
|
22
|
+
<g id="body">
|
|
23
|
+
<path id="body-mass" fill="#b97f3c" d="M 299 223 C 260 242 245 300 262 355 C 280 404 332 422 401 404 C 454 412 483 370 466 327 C 452 289 415 272 391 244 Z"/>
|
|
24
|
+
<path id="shoulder-light" fill="#dba04f" d="M 363 236 C 399 243 431 280 437 317 C 413 300 395 291 373 301 L 331 270 Z"/>
|
|
25
|
+
<path id="belly" fill="#f4dcb0" d="M 290 220 C 270 262 269 310 288 350 C 301 378 331 392 361 383 C 387 375 395 342 385 308 C 375 274 368 244 356 219 Z"/>
|
|
26
|
+
<path id="belly-bands" fill="none" stroke="#b89159" stroke-width="5" d="M 281 254 Q 325 277 370 252 M 277 285 Q 323 309 379 281 M 280 319 Q 328 340 386 314 M 295 350 Q 337 371 382 345"/>
|
|
27
|
+
</g>
|
|
28
|
+
<g id="front-feet">
|
|
29
|
+
<path id="far-front-leg" fill="#a76e34" d="M 274 296 C 248 300 241 341 239 378 C 224 393 212 412 222 432 C 235 447 276 446 289 434 C 299 421 287 400 290 379 L 306 332 Z"/>
|
|
30
|
+
<path id="far-front-toes" fill="#efd6a5" d="M 220 424 C 218 411 231 404 238 421 C 240 400 253 402 258 422 C 266 403 278 411 278 433 L 225 435 Z"/>
|
|
31
|
+
<path id="near-front-leg" fill="#cf9247" d="M 385 289 C 358 291 352 321 356 351 C 363 376 349 402 338 414 C 325 433 335 451 357 454 L 403 454 C 423 448 428 434 417 416 C 407 398 421 369 423 343 C 425 315 411 292 385 289 Z"/>
|
|
32
|
+
<path id="near-front-toes" fill="#ffe4ae" d="M 339 441 C 337 423 349 417 357 438 C 359 412 374 412 380 438 C 387 417 401 418 405 442 C 409 431 416 432 417 445 Q 380 458 339 449 Z"/>
|
|
33
|
+
<path id="toe-cuts" fill="none" stroke="#080d19" stroke-width="4" d="M 357 438 L 358 451 M 380 438 L 381 452 M 405 442 L 405 448"/>
|
|
34
|
+
</g>
|
|
35
|
+
<g id="head">
|
|
36
|
+
<path id="far-horn" fill="#e7cf9a" d="M 283 124 C 254 101 243 76 246 53 C 262 70 282 80 304 89 Z"/>
|
|
37
|
+
<path id="neck" fill="#c98c43" d="M 290 173 C 263 209 265 247 280 264 C 302 280 356 278 380 254 C 390 233 379 201 369 174 Z"/>
|
|
38
|
+
<path id="throat" fill="#f8deb0" d="M 284 188 C 271 212 277 242 293 254 C 314 263 344 252 353 232 L 351 192 Z"/>
|
|
39
|
+
<path id="head-mass" fill="#d39b50" d="M 271 113 C 278 82 318 73 353 91 C 381 105 391 131 386 158 C 385 188 366 212 339 220 C 306 229 273 210 262 186 C 251 162 257 137 271 113 Z"/>
|
|
40
|
+
<path id="near-horn" fill="#ffe7b7" d="M 350 112 C 360 86 383 76 402 69 C 425 62 436 49 446 33 C 449 64 428 91 405 106 C 385 119 367 123 350 112 Z"/>
|
|
41
|
+
<path id="crest" fill="#efc57e" d="M 291 98 L 298 64 L 315 86 L 335 55 L 342 87 L 361 70 L 365 102 Z"/>
|
|
42
|
+
<path id="cheek-fin" fill="#e9b86b" d="M 372 148 Q 403 128 421 155 Q 399 154 384 170 Q 411 172 414 192 Q 393 182 377 188 Z"/>
|
|
43
|
+
<path id="muzzle" fill="#efd09a" d="M 261 166 C 241 166 235 180 242 195 C 250 211 283 216 310 214 C 340 215 360 201 358 189 C 338 196 318 193 304 181 C 289 169 273 166 261 166 Z"/>
|
|
44
|
+
<path id="nostril" fill="#080d19" d="M 256 180 C 256 173 267 173 268 179 C 269 185 257 186 256 180 Z"/>
|
|
45
|
+
<path id="mouth-line" fill="none" stroke="#080d19" stroke-width="4" d="M 252 199 Q 299 219 347 196"/>
|
|
46
|
+
</g>
|
|
47
|
+
<g id="eyes">
|
|
48
|
+
<path id="far-eye-socket" fill="#080d19" d="M 271 139 C 271 121 282 112 293 122 C 303 131 302 150 295 161 C 280 173 270 158 271 139 Z"/>
|
|
49
|
+
<path id="far-eye-white" fill="#f8faff" d="M 276 140 C 276 125 284 119 291 127 C 298 136 297 150 291 157 C 281 163 275 153 276 140 Z"/>
|
|
50
|
+
<path id="far-pupil" fill="#080d19" d="M 283 143 C 283 133 293 133 293 143 C 293 153 283 153 283 143 Z"/>
|
|
51
|
+
<path id="near-eye-socket" fill="#080d19" d="M 314 144 C 314 121 334 109 351 121 C 368 134 365 157 353 172 C 335 185 314 171 314 144 Z"/>
|
|
52
|
+
<path id="near-eye-white" fill="#f8faff" d="M 320 144 C 320 126 334 117 347 128 C 360 139 356 157 348 165 C 335 175 320 163 320 144 Z"/>
|
|
53
|
+
<path id="near-pupil" fill="#080d19" d="M 331 146 C 331 133 346 133 346 146 C 346 159 331 159 331 146 Z"/>
|
|
54
|
+
<path id="near-brow" fill="none" stroke="#f0bf71" stroke-width="5" d="M 315 116 Q 336 102 355 117"/>
|
|
55
|
+
</g>
|
|
56
|
+
</svg>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {renderSvgRig} from '../../../factory/svg-rig.mjs';
|
|
2
|
+
const clamp=x=>Math.max(0,Math.min(1,x));
|
|
3
|
+
const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
|
|
4
|
+
const pulse=(t,a,b)=>t<=a||t>=b?0:Math.sin(Math.PI*(t-a)/(b-a))**2;
|
|
5
|
+
export const HYPERION_CLIPS=Object.freeze([
|
|
6
|
+
{id:'idle',title:'Steady Company',duration:12000},
|
|
7
|
+
{id:'attach',title:'At Your Service',duration:3400},
|
|
8
|
+
{id:'curious',title:'Let Me Look',duration:2600},
|
|
9
|
+
{id:'focus',title:'Careful Work',duration:2800},
|
|
10
|
+
{id:'concerned',title:'Something Needs Care',duration:2600},
|
|
11
|
+
{id:'proud',title:'A Job Well Done',duration:2600},
|
|
12
|
+
{id:'standby',title:'Keeping Watch',duration:3000},
|
|
13
|
+
{id:'cluster',title:'Cluster Forge',duration:4800,power:true},
|
|
14
|
+
]);
|
|
15
|
+
export const STORY=[['attach',3400],['idle',1800],['curious',2200],['focus',2200],['cluster',4800],['proud',2200],['concerned',2200],['standby',2200],['idle',3000]];
|
|
16
|
+
export const STORY_DURATION=STORY.reduce((n,b)=>n+b[1],0);
|
|
17
|
+
const captions={idle:'Steady company.',attach:'I am here.',curious:'Let me take a closer look.',focus:'One careful step at a time.',concerned:'This needs attention.',proud:'A satisfying result · expression demo',standby:'Keeping watch.',cluster:'Cluster Forge · visual demonstration'};
|
|
18
|
+
export function bodyPose(ms){const t=ms%12000;return {wing:.018*pulse(t,2200,5400),far:.012*pulse(t,2600,5800),tail:.65*pulse(t,5500,9500),nod:2*pulse(t,800,3700),turn:.012*pulse(t,800,3700)};}
|
|
19
|
+
function performance(scene,t){
|
|
20
|
+
const base={gx:0,open:t%6000>4500&&t%6000<4680?.18:1};
|
|
21
|
+
switch(scene){
|
|
22
|
+
case 'curious':return {gx:-.65*ease(t/700),open:1.12};
|
|
23
|
+
case 'focus':return {gx:.6,open:.65};
|
|
24
|
+
case 'concerned':return {gx:.45*Math.sin(t/600),open:.85};
|
|
25
|
+
case 'proud':return {gx:0,open:1.15};
|
|
26
|
+
case 'standby':return {gx:0,open:.8};
|
|
27
|
+
default:return base;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const rot=(x,y,cx,cy,a)=>[cx+(x-cx)*Math.cos(a)-(y-cy)*Math.sin(a),cy+(x-cx)*Math.sin(a)+(y-cy)*Math.cos(a)];
|
|
31
|
+
export function poseRig(rig,pose,face){
|
|
32
|
+
const paths=rig.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
|
|
33
|
+
if(p.id.endsWith('-eye-white')||p.id.endsWith('-pupil')){
|
|
34
|
+
const cy=p.id.startsWith('far-')?141:145;y=cy+(y-cy)*face.open;
|
|
35
|
+
if(p.id.endsWith('-pupil'))x+=face.gx*5;
|
|
36
|
+
}
|
|
37
|
+
if(p.group==='near-wing')return rot(x,y,420,293,pose.wing);
|
|
38
|
+
if(p.group==='far-wing')return rot(x,y,294,294,-pose.far);
|
|
39
|
+
if(p.group==='tail'){const w=ease((x-490)/210);return [x-4*pose.tail*w,y-5*pose.tail*w];}
|
|
40
|
+
if(p.group==='head'||p.group==='eyes'){
|
|
41
|
+
const w=p.id==='neck'||p.id==='throat'?ease((275-y)/85):1;
|
|
42
|
+
const [rx,ry]=rot(x,y,330,255,pose.turn*w);return [rx,ry+pose.nod*w];
|
|
43
|
+
}
|
|
44
|
+
return [x,y];
|
|
45
|
+
}))}));
|
|
46
|
+
const byId=new Map(paths.map(p=>[p.id,p]));
|
|
47
|
+
return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:byId.get(e.socket.id),white:byId.get(e.white.id),pupil:byId.get(e.pupil.id)}))};
|
|
48
|
+
}
|
|
49
|
+
function cluster(frame,t,mono){
|
|
50
|
+
// Ordered points in the empty gap above the right shoulder, never over ink.
|
|
51
|
+
const rows=frame.length,cols=frame[0].length;
|
|
52
|
+
const nodes=[[.51,.22],[.57,.12],[.64,.22]],progress=ease(t/3300),fade=1-ease((t-3900)/900);let count=0;
|
|
53
|
+
const put=(x,y,bright=false)=>{x=Math.round(x*cols);y=Math.round(y*rows);if(x<1||x>=cols-1||y<1||y>=rows-1||frame[y][x].ch!==' '||fade<.05)return;frame[y][x]={ch:bright?'⠿':'⠂',bg:null,rgb:mono?null:(bright?[246,247,255]:[104,211,229]).map(v=>Math.round(v*fade))};count++;};
|
|
54
|
+
for(let i=0;i<3;i++)if(progress>(i+1)/8)put(...nodes[i],true);
|
|
55
|
+
for(let e=0;e<3;e++){const a=nodes[e],b=nodes[(e+1)%3];for(let j=1;j<8;j++)if(progress>.45+(e+j/8)*.16)put(a[0]+(b[0]-a[0])*j/8,a[1]+(b[1]-a[1])*j/8);}
|
|
56
|
+
return count;
|
|
57
|
+
}
|
|
58
|
+
export function renderHyperion(rig,scene='idle',ms=0,options={}){
|
|
59
|
+
const clip=HYPERION_CLIPS.find(c=>c.id===scene);
|
|
60
|
+
if(!clip)return {ok:false,code:'UNKNOWN_CLIP'};
|
|
61
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
62
|
+
const t=options.still?(clip.power?clip.duration*.65:0):ms;
|
|
63
|
+
let face=performance(scene,t),pose=bodyPose(options.motionTime??t);
|
|
64
|
+
if(scene==='attach'){
|
|
65
|
+
const duration=options.deck?4400:3400;
|
|
66
|
+
if(t>=duration){pose=bodyPose(t-duration);face=performance('idle',t-duration);}
|
|
67
|
+
else{const u=t/duration;pose={wing:.025*pulse(u,.15,.85),far:.018*pulse(u,.22,.92),tail:.55*pulse(u,.25,1),nod:4*pulse(u,.35,.9),turn:.016*pulse(u,0,.8)};face={gx:.5*pulse(u,0,.85),open:1+.1*pulse(u,.1,.85)};}
|
|
68
|
+
}
|
|
69
|
+
if(scene==='proud')pose={...pose,nod:pose.nod-2*pulse(t,0,2600)};
|
|
70
|
+
if(options.still)pose={wing:0,far:0,tail:0,nod:0,turn:0};
|
|
71
|
+
const posed=poseRig(rig,pose,face),r=renderSvgRig(posed,options);
|
|
72
|
+
const effectCells=clip.power?cluster(r.frame,t,options.mono):0;
|
|
73
|
+
return {...r,scene,clip:clip.title,caption:captions[scene],pose,face,effectCells,staticStudy:false};
|
|
74
|
+
}
|
|
75
|
+
export function renderStory(rig,ms,options={}){
|
|
76
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
77
|
+
let t=options.still?0:Math.min(ms,STORY_DURATION-1);
|
|
78
|
+
for(const [scene,duration]of STORY){if(t<duration)return renderHyperion(rig,scene,t,{...options,deck:false,motionTime:Math.max(0,ms-3400)});t-=duration;}
|
|
79
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import {parseSvgRig} from '../../../factory/svg-rig.mjs';
|
|
4
|
+
import {renderHyperion,HYPERION_CLIPS} from './character.mjs';
|
|
5
|
+
import {artSlot,composeMessage} from '../../lucid/recovery/layout.mjs';
|
|
6
|
+
import {cellsToAnsi} from '../../../vendor/quox-dragon/qart.js';
|
|
7
|
+
const args=process.argv.slice(2),values=new Set(['--cols','--rows','--terminal-cols','--terminal-rows','--message','--scene','--at']);
|
|
8
|
+
for(let i=0;i<args.length;i++){
|
|
9
|
+
if(values.has(args[i])){if(!args[i+1]||args[i+1].startsWith('--'))throw Error('Missing option value');i++;}
|
|
10
|
+
else if(!['--large','--mono','--help','--watch','--attach','--intro','--still','--list'].includes(args[i]))throw Error('Unknown option: '+args[i]);
|
|
11
|
+
}
|
|
12
|
+
const value=(name,fallback)=>args.includes(name)?args[args.indexOf(name)+1]:fallback;
|
|
13
|
+
if(args.includes('--help')){console.log('Hyperion V2 acting studio. --watch --attach --intro --still --scene ID --at MS --list --large --mono --message TEXT --cols N --rows N --terminal-cols N --terminal-rows N');process.exit(0);}
|
|
14
|
+
if(args.includes('--list')){console.log(HYPERION_CLIPS.map(c=>c.id+' · '+c.title).join('\n'));process.exit(0);}
|
|
15
|
+
const w=Number(value('--terminal-cols',process.stdout.columns||80)),h=Number(value('--terminal-rows',process.stdout.rows||24));
|
|
16
|
+
if(!Number.isSafeInteger(w)||!Number.isSafeInteger(h)||w<1||h<1)throw Error('Invalid terminal size');
|
|
17
|
+
const slot=artSlot(w,h),large=args.includes('--large')||args.includes('--intro'),explicit=large||args.includes('--cols')||args.includes('--rows');
|
|
18
|
+
const cols=Number(value('--cols',large?Math.min(120,w-4):slot.cols)),rows=Number(value('--rows',large?Math.min(36,h-4):slot.rows));
|
|
19
|
+
const message=value('--message','HYPERION\nSteady company.');
|
|
20
|
+
if(!slot.showArt&&!explicit){console.log(composeMessage(null,message,{terminalCols:w}).join('\n'));}
|
|
21
|
+
else{
|
|
22
|
+
if(cols>w-4||rows>h-2)throw Error('Requested art exceeds terminal margins');
|
|
23
|
+
const rig=parseSvgRig(await fs.readFile(new URL('../v1/hyperion-master.svg',import.meta.url),'utf8'));
|
|
24
|
+
const opts={cols,rows,mono:args.includes('--mono'),still:args.includes('--still'),deck:large};
|
|
25
|
+
const scene=value('--scene',args.includes('--attach')?'attach':'idle');
|
|
26
|
+
const r=renderHyperion(rig,scene,Number(value('--at',0)),opts);
|
|
27
|
+
if(!r.ok)throw Error(r.code);
|
|
28
|
+
if(args.includes('--watch')||args.includes('--attach')||args.includes('--intro')){
|
|
29
|
+
const {watch}=await import('./watch.mjs');
|
|
30
|
+
await watch(rig,opts,{terminalCols:w,terminalRows:h,large,scene:args.includes('--scene')||args.includes('--attach')?scene:null,arrival:args.includes('--attach'),message:args.includes('--message')?message:null});
|
|
31
|
+
}else
|
|
32
|
+
console.log((large?[...cellsToAnsi(r.frame),...composeMessage(null,message,{terminalCols:w})]:composeMessage(r.frame,message,{terminalCols:w})).join('\n'));
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {performance} from 'node:perf_hooks';
|
|
2
|
+
import {HYPERION_CLIPS,renderHyperion,renderStory,STORY_DURATION} from './character.mjs';
|
|
3
|
+
import {composeMessage} from '../../lucid/recovery/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?HYPERION_CLIPS.find(c=>c.id===scene):null;
|
|
11
|
+
const duration=(config.arrival&&options.deck?4400: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?renderHyperion(paths,scene,t,options):renderStory(paths,t,options);
|
|
16
|
+
if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
|
|
17
|
+
const caption=message??'HYPERION · '+r.clip+'\n'+r.caption;
|
|
18
|
+
return large?[...cellsToAnsi(r.frame),...caption.split('\n')]:composeMessage(r.frame,caption,{terminalCols});
|
|
19
|
+
};
|
|
20
|
+
// Reserve enough lines for captions too; keep within the real terminal.
|
|
21
|
+
const height=options.rows+3;
|
|
22
|
+
if(height>=terminalRows)throw new Error('Animation needs room for its caption; reduce --rows.');
|
|
23
|
+
let ended=false,drawn=false,timer,resolveDone,rejectDone;
|
|
24
|
+
const wasRaw=Boolean(process.stdin.isRaw);
|
|
25
|
+
const done=new Promise((resolve,reject)=>{resolveDone=resolve;rejectDone=reject;});
|
|
26
|
+
const cleanup=(error)=>{
|
|
27
|
+
if(ended)return;ended=true;clearInterval(timer);
|
|
28
|
+
process.stdin.off('data',key);process.stdout.off('resize',resized);
|
|
29
|
+
process.off('SIGINT',sigint);process.off('SIGTERM',sigterm);
|
|
30
|
+
process.stdin.setRawMode(wasRaw);process.stdin.pause();
|
|
31
|
+
if(drawn)process.stdout.write(`\x1b[${height}B\r`);
|
|
32
|
+
process.stdout.write('\x1b[0m\x1b[?25h');
|
|
33
|
+
error?rejectDone(error):resolveDone();
|
|
34
|
+
};
|
|
35
|
+
const key=data=>{if(data.includes(3)||data.toString().includes('q'))cleanup();};
|
|
36
|
+
const sigint=()=>cleanup(),sigterm=()=>cleanup();
|
|
37
|
+
const resized=()=>cleanup(new Error('Terminal resized; rerun for the new art slot.'));
|
|
38
|
+
try{
|
|
39
|
+
process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on('data',key);
|
|
40
|
+
process.stdout.on('resize',resized);process.on('SIGINT',sigint);process.on('SIGTERM',sigterm);
|
|
41
|
+
process.stdout.write('\x1b[?25l');
|
|
42
|
+
// Allocate the block once; each frame replaces only these lines.
|
|
43
|
+
process.stdout.write('\n'.repeat(height)+`\x1b[${height}A`);drawn=true;
|
|
44
|
+
const tick=()=>{
|
|
45
|
+
try{
|
|
46
|
+
if(process.stdout.columns!==realCols||process.stdout.rows!==realRows)return resized();
|
|
47
|
+
const t=Math.min(performance.now()-start,duration-1),lines=render(t);
|
|
48
|
+
if(lines.length>height)throw new Error('Message exceeds the animation block; use a shorter message or a static preview.');
|
|
49
|
+
process.stdout.write(Array.from({length:height},(_,i)=>'\r\x1b[2K'+(lines[i]??'')+'\n').join('')+`\x1b[${height}A`);
|
|
50
|
+
if(performance.now()-start>=duration)cleanup();
|
|
51
|
+
}catch(e){cleanup(e);}
|
|
52
|
+
};
|
|
53
|
+
tick();if(!ended)timer=setInterval(tick,1000/12);
|
|
54
|
+
await done;
|
|
55
|
+
}catch(e){cleanup(e);await done;}
|
|
56
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Notice → acknowledge → settle. Pure authored timing; no visibility wipe. */
|
|
2
|
+
import {motionAt} from './motion.mjs';
|
|
3
|
+
export const ARRIVAL_DURATION=3200;
|
|
4
|
+
export const DECK_ARRIVAL_DURATION=4400;
|
|
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
|
+
const pulse=(t,a,b)=>t<=a||t>=b?0:Math.sin(Math.PI*(t-a)/(b-a))**2;
|
|
8
|
+
export function arrivalPose(ms,{deck=false}={}){
|
|
9
|
+
if(!Number.isFinite(ms)||ms<0)throw new RangeError('Arrival time must be nonnegative.');
|
|
10
|
+
const duration=deck?DECK_ARRIVAL_DURATION:ARRIVAL_DURATION;
|
|
11
|
+
if(ms>=duration)return {face:null,motion:motionAt(ms-duration),phase:'present',duration};
|
|
12
|
+
const t=ms/duration*ARRIVAL_DURATION,gain=deck?1.12:1;
|
|
13
|
+
const attention=pulse(t,100,2300),welcome=pulse(t,650,2750);
|
|
14
|
+
return {
|
|
15
|
+
phase:t<700?'notice':t<1800?'acknowledge':'settle',duration,
|
|
16
|
+
face:{gx:.65*ease(t/450)*(1-ease((t-1700)/1100)),gy:-.18*attention,
|
|
17
|
+
open:1+.15*welcome},
|
|
18
|
+
motion:{tail:gain*(.82*pulse(t,150,1850)-.32*pulse(t,1800,3050)),
|
|
19
|
+
wing:gain*.052*pulse(t,650,2650),farWing:gain*.032*pulse(t,850,2850),
|
|
20
|
+
neck:gain*.031*attention,nod:gain*(-2*attention+4*pulse(t,1400,2800))},
|
|
21
|
+
};
|
|
22
|
+
}
|