quoxcli 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -1
- package/lib/api/endpoints/admin.js +21 -0
- package/lib/api/endpoints/calendarq.js +8 -0
- package/lib/api/endpoints/governance.js +7 -0
- package/lib/api/endpoints/inboxq.js +39 -0
- package/lib/api/endpoints/index.js +4 -0
- package/lib/api/endpoints/lens.js +34 -0
- package/lib/api/endpoints/lucid.js +18 -0
- package/lib/api/endpoints/mcp.js +35 -0
- package/lib/api/endpoints/mon.js +6 -7
- package/lib/api/endpoints/notesq.js +2 -0
- package/lib/api/endpoints/quoxplan.js +51 -0
- package/lib/api/endpoints/v1.js +57 -0
- package/lib/commands/actions.js +51 -13
- package/lib/commands/admin.js +86 -0
- package/lib/commands/approvals.js +5 -1
- package/lib/commands/calendar.js +27 -0
- package/lib/commands/chat.js +1 -1
- package/lib/commands/contacts.js +2 -0
- package/lib/commands/fleet.js +164 -0
- package/lib/commands/fleetRecipe.js +174 -0
- package/lib/commands/lens.js +268 -0
- package/lib/commands/lucid.js +6 -0
- package/lib/commands/lucidGoals.js +423 -0
- package/lib/commands/mail.js +287 -7
- package/lib/commands/mcp.js +190 -0
- package/lib/commands/mon.js +14 -0
- package/lib/commands/monitoring.js +73 -1
- package/lib/commands/notesSeal.js +380 -22
- package/lib/commands/offload.js +51 -10
- package/lib/commands/plan.js +507 -54
- package/lib/commands/research.js +78 -0
- package/lib/commands/settings.js +7 -1
- package/lib/commands/volt.js +6 -1
- package/lib/commands/wazuh.js +74 -9
- package/lib/commands/wazuhIntake.js +55 -6
- package/lib/core/commandRegistrars.js +2 -0
- package/lib/core/config.js +14 -1
- package/lib/core/offload.js +83 -0
- package/lib/core/output.js +29 -4
- package/lib/core/selfUpdate.js +2 -0
- package/lib/core/version.js +15 -2
- package/lib/dragon/select.js +2 -2
- package/lib/quoxon/api.js +4 -0
- package/lib/quoxon/app.js +258 -49
- package/lib/quoxon/approvalsView.js +3 -1
- package/lib/quoxon/ask.js +4 -2
- package/lib/quoxon/bang.js +7 -1
- package/lib/quoxon/commandShort.js +1 -1
- package/lib/quoxon/commands.js +5 -5
- package/lib/quoxon/delegationDragon.js +49 -0
- package/lib/quoxon/dnd.js +47 -0
- package/lib/quoxon/dragonCompanion.js +24 -12
- package/lib/quoxon/events.js +12 -1
- package/lib/quoxon/extensions.js +2 -0
- package/lib/quoxon/foreignText.js +49 -0
- package/lib/quoxon/inboxView.js +5 -5
- package/lib/quoxon/launcher.js +2 -1
- package/lib/quoxon/loopCard.js +2 -1
- package/lib/quoxon/main.js +8 -2
- package/lib/quoxon/onboarding/dockerOffer.js +3 -1
- package/lib/quoxon/onboarding/view.js +1 -1
- package/lib/quoxon/render.js +5 -2
- package/lib/quoxon/resend.js +14 -0
- package/lib/quoxon/rows.js +3 -1
- package/lib/quoxon/secretShape.js +2 -0
- package/lib/quoxon/slash/broadcast.js +30 -6
- package/lib/quoxon/slash/broadcastRun.js +12 -6
- package/lib/quoxon/slash/cliLeaves.json +54 -0
- package/lib/quoxon/slash/dashboard.js +2 -0
- package/lib/quoxon/slash/listView.js +2 -1
- package/lib/quoxon/slash/palette.js +41 -0
- package/lib/quoxon/slash/paletteController.js +6 -2
- package/lib/quoxon/slash/restart.js +2 -0
- package/lib/quoxon/slotChat.js +7 -3
- package/lib/quoxon/slotRun.js +2 -0
- package/lib/quoxon/webPty.js +15 -0
- package/lib/quoxon/whatsnew.jsonl +7 -0
- package/lib/render/planBrief.js +614 -0
- package/lib/toolOutputShape.js +6 -3
- package/package.json +1 -1
- package/vendor/dragonlab/SOURCE +3 -3
- package/vendor/dragonlab/design/architect/v2/architect-master.svg +67 -0
- package/vendor/dragonlab/design/architect/v2/character.mjs +83 -0
- package/vendor/dragonlab/design/architect/v2/review.mjs +35 -0
- package/vendor/dragonlab/design/architect/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/archivist/v2/archivist-master.svg +77 -0
- package/vendor/dragonlab/design/archivist/v3/character.mjs +56 -0
- package/vendor/dragonlab/design/archivist/v4/character.mjs +46 -0
- package/vendor/dragonlab/design/archivist/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/archivist/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/argus/v2/character.mjs +69 -0
- package/vendor/dragonlab/design/argus/v3/argus-master.svg +68 -0
- package/vendor/dragonlab/design/argus/v3/character.mjs +63 -0
- package/vendor/dragonlab/design/argus/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/argus/v3/watch.mjs +56 -0
- package/vendor/dragonlab/design/daedalus/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/daedalus/v3/daedalus-master.svg +65 -0
- package/vendor/dragonlab/design/daedalus/v4/character.mjs +47 -0
- package/vendor/dragonlab/design/daedalus/v5/character.mjs +66 -0
- package/vendor/dragonlab/design/daedalus/v6/character.mjs +32 -0
- package/vendor/dragonlab/design/daedalus/v6/review.mjs +35 -0
- package/vendor/dragonlab/design/daedalus/v6/watch.mjs +56 -0
- package/vendor/dragonlab/design/janus/v1/janus-master.svg +70 -0
- package/vendor/dragonlab/design/janus/v2/character.mjs +56 -0
- package/vendor/dragonlab/design/janus/v3/character.mjs +7 -0
- package/vendor/dragonlab/design/janus/v4/character.mjs +150 -0
- package/vendor/dragonlab/design/janus/v5/character.mjs +55 -0
- package/vendor/dragonlab/design/janus/v5/review.mjs +35 -0
- package/vendor/dragonlab/design/janus/v5/watch.mjs +56 -0
- package/vendor/dragonlab/design/roady/v1/roady-master.svg +67 -0
- package/vendor/dragonlab/design/roady/v2/character.mjs +69 -0
- package/vendor/dragonlab/design/roady/v3/character.mjs +91 -0
- package/vendor/dragonlab/design/roady/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/roady/v3/watch.mjs +56 -0
- package/vendor/dragonlab/design/weaver/v3/weaver-master.svg +57 -0
- package/vendor/dragonlab/design/weaver/v4/character.mjs +56 -0
- package/vendor/dragonlab/design/weaver/v5/character.mjs +52 -0
- package/vendor/dragonlab/design/weaver/v6/character.mjs +88 -0
- package/vendor/dragonlab/design/weaver/v6/review.mjs +35 -0
- package/vendor/dragonlab/design/weaver/v6/watch.mjs +56 -0
- package/vendor/dragonlab/integration/companions-v1/architect.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/archivist.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/argus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +86 -0
- package/vendor/dragonlab/integration/companions-v1/daedalus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/janus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/roady.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +4 -1
- package/vendor/dragonlab/integration/companions-v1/weaver.mjs +2 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
|
|
2
|
+
import {poseRig as v2Pose,renderRoady as v2Render,ROADY_CLIPS as v2Clips} from '../v2/character.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Roady V3: companion pass on the approved V1 SVG. Idle and Journey are V2 exactly.
|
|
6
|
+
* Adds 'attach' (notice, a raised-claw wave, settle; exact handoff to idle) and 'chart' (unroll the map, trace one
|
|
7
|
+
* planned route with its milestones, point the way, roll it up). Chart has no blockage, detour or arrival flag:
|
|
8
|
+
* those would claim plan facts the host never states. Pure functions of elapsed time.
|
|
9
|
+
*/
|
|
10
|
+
export const ATTACH_MS=3400,CHART_MS=7000;
|
|
11
|
+
export const ROADY_CLIPS=Object.freeze([...v2Clips,{id:'attach',title:'Notice · wave · settle',duration:ATTACH_MS},{id:'chart',title:'Chart the route',duration:CHART_MS}]);
|
|
12
|
+
export const STORY_DURATION=ATTACH_MS+CHART_MS;
|
|
13
|
+
const STILL=Object.freeze({attach:1500,chart:3600});
|
|
14
|
+
const clamp=x=>Math.max(0,Math.min(1,x));
|
|
15
|
+
const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
|
|
16
|
+
const pulse=(t,a,b)=>t<=a||t>=b?0:Math.sin(Math.PI*(t-a)/(b-a))**2;
|
|
17
|
+
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)];
|
|
18
|
+
const REST=Object.freeze({head:0,pupil:0,near:0,far:0,tailX:0,tailY:0,streamer:0,armX:0,armY:0,open:0,route:0,marks:0});
|
|
19
|
+
|
|
20
|
+
/** One parameter set per instant. All zero is V2 idle at t=0, the rest pose. */
|
|
21
|
+
export function roadyParams(scene,t){
|
|
22
|
+
if(scene==='attach'){
|
|
23
|
+
// Notice: the head lifts and the eyes glance back at whoever arrived. Acknowledge: a nod and a raised-claw wave.
|
|
24
|
+
// Settle: wings flex, scarf and tail land last.
|
|
25
|
+
return {...REST,head:-.06*pulse(t,0,1400)+.06*pulse(t,1100,2600),pupil:-3*pulse(t,200,2400),
|
|
26
|
+
armX:4*pulse(t,900,2600),armY:-12*pulse(t,900,2600),near:.10*pulse(t,1500,3100),far:-.08*pulse(t,1700,3300),
|
|
27
|
+
tailX:4*pulse(t,1900,3400),tailY:-8*pulse(t,1900,3400),streamer:4*pulse(t,1600,3400)*Math.sin(t/300)};
|
|
28
|
+
}
|
|
29
|
+
if(scene==='chart'){
|
|
30
|
+
const env=ease(t/900)*(1-ease((t-6000)/1000));
|
|
31
|
+
return {...REST,head:.03*pulse(t,1800,4400)-.05*pulse(t,4300,5800),pupil:2*pulse(t,1500,4600),
|
|
32
|
+
armX:5*env,armY:-6*pulse(t,3800,5400),near:.05*pulse(t,4200,6000),streamer:5*env*Math.sin(t/450),
|
|
33
|
+
open:ease((t-500)/1600)*(1-ease((t-5200)/1500)),route:clamp((t-1800)/2400),marks:t<2000?0:Math.min(4,1+Math.floor((t-2000)/600))};
|
|
34
|
+
}
|
|
35
|
+
return REST;
|
|
36
|
+
}
|
|
37
|
+
const BOUNDS=Object.freeze({left:65,top:35,right:730,bottom:510});
|
|
38
|
+
function poseFromParams(rig,P){
|
|
39
|
+
const head=(x,y)=>rot(x,y,355,235,P.head);
|
|
40
|
+
// Same anatomy transforms as V2: shared head/eye joint, neck blended into the chest, rooted wings, root-weighted tail.
|
|
41
|
+
const paths=rig.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
|
|
42
|
+
if(p.group==='head'||p.group==='eyes'){const q=head(x,y);if(p.id.endsWith('pupil'))q[0]+=P.pupil;return q;}
|
|
43
|
+
if(p.group==='neck'){const w=clamp((285-y)/100),q=head(x,y);return [x+(q[0]-x)*w,y+(q[1]-y)*w];}
|
|
44
|
+
if(p.group==='near-wing')return rot(x,y,318,267,P.near);
|
|
45
|
+
if(p.group==='far-wing')return rot(x,y,391,282,P.far);
|
|
46
|
+
if(p.group==='tail'){const w=clamp((300-x)/210);return [x+P.tailX*w,y+P.tailY*w];}
|
|
47
|
+
if(p.id==='streamer'){const w=clamp((346-x)/135);return [x,y+w*P.streamer];}
|
|
48
|
+
if(p.id==='point-arm'){const w=clamp((x-405)/95);return [x+P.armX*w,y+P.armY*w];}
|
|
49
|
+
return [x,y];
|
|
50
|
+
}))}));
|
|
51
|
+
if(P.open>.001){
|
|
52
|
+
// V2's map, planned route and milestones only. No blockage, detour, traveller or flag.
|
|
53
|
+
const add=(id,rgb,poly,stroke=0,erase=false)=>paths.push({id,group:'roadmap',rgb,polygons:[poly],stroke,erase});
|
|
54
|
+
const pt=(u,v)=>[453+u*P.open,320+u*.18+v];
|
|
55
|
+
add('map-shadow',[8,13,25],[pt(0,-4),pt(256,-4),pt(256,111),pt(0,111)],0,true);
|
|
56
|
+
add('unfurled-map',[229,213,164],[pt(0,0),pt(250,0),pt(250,104),pt(0,104)]);
|
|
57
|
+
add('map-edge',[255,242,205],[pt(250,0),pt(250,104)],7);
|
|
58
|
+
const route=[[15,75],[75,51],[140,67],[222,26]],count=P.route*(route.length-1);
|
|
59
|
+
for(let i=0;i<route.length-1;i++){const f=clamp(count-i);if(f>0)add('planned-route'+i,[50,105,117],[pt(...route[i]),pt(route[i][0]+(route[i+1][0]-route[i][0])*f,route[i][1]+(route[i+1][1]-route[i][1])*f)],6);}
|
|
60
|
+
for(let i=0;i<P.marks;i++){const [u,v]=route[i];add('milestone'+i,[37,104,118],[pt(u-6,v),pt(u,v-7),pt(u+6,v),pt(u,v+7)]);}
|
|
61
|
+
}
|
|
62
|
+
const byId=new Map(paths.map(p=>[p.id,p]));
|
|
63
|
+
return {...rig,bounds:BOUNDS,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)}))};
|
|
64
|
+
}
|
|
65
|
+
const NEW=new Set(['attach','chart']);
|
|
66
|
+
export function poseRig(rig,scene,ms){
|
|
67
|
+
if(!NEW.has(scene))return v2Pose(rig,scene,ms);
|
|
68
|
+
const d=scene==='attach'?ATTACH_MS:CHART_MS;
|
|
69
|
+
if(ms>=d)return v2Pose(rig,'idle',ms-d);
|
|
70
|
+
return poseFromParams(rig,roadyParams(scene,ms));
|
|
71
|
+
}
|
|
72
|
+
function caption(scene,t){
|
|
73
|
+
if(scene==='attach')return t<1100?'Oh, hello there.':t<2600?'Roady waves you aboard.':'Ready for the next step.';
|
|
74
|
+
return t<1800?'Unroll the map.':t<4400?'Trace the route. Mark the milestones.':t<5200?'That way.':'Roll it up. Onward.';
|
|
75
|
+
}
|
|
76
|
+
export function renderRoady(rig,scene='idle',ms=0,options={}){
|
|
77
|
+
if(!ROADY_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
|
|
78
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
79
|
+
if(!NEW.has(scene))return v2Render(rig,scene,ms,options);
|
|
80
|
+
const d=scene==='attach'?ATTACH_MS:CHART_MS;
|
|
81
|
+
if(!options.still&&ms>=d)return v2Render(rig,'idle',ms-d,options);
|
|
82
|
+
// Still freezes every channel (head, pupils, wave, wings, tail, scarf, map, route) at one pose.
|
|
83
|
+
const t=options.still?STILL[scene]:ms;
|
|
84
|
+
return {...renderSvgRig(poseRig(rig,scene,t),options),scene,clip:scene,caption:caption(scene,t)};
|
|
85
|
+
}
|
|
86
|
+
/** Offline companion story: attach, then chart, then idle. Simulated; no QuoxPlan state. */
|
|
87
|
+
export function renderStory(rig,ms,options={}){
|
|
88
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
89
|
+
if(options.still)return renderRoady(rig,'chart',0,options);
|
|
90
|
+
return ms<ATTACH_MS?renderRoady(rig,'attach',ms,options):renderRoady(rig,'chart',ms-ATTACH_MS,options);
|
|
91
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import {parseSvgRig} from '../../../factory/svg-rig-contour.mjs';
|
|
4
|
+
import {renderRoady,ROADY_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('Roady V3 roadmap studio (companion attach and Chart). --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(ROADY_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','ROADY\nEvery goal needs a way forward.');
|
|
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/roady-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=renderRoady(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
|
+
const shownMessage=args.includes('--message')?message:'ROADY\n'+r.caption;
|
|
33
|
+
console.log((large?[...cellsToAnsi(r.frame),...composeMessage(null,shownMessage,{terminalCols:w})]:composeMessage(r.frame,shownMessage,{terminalCols:w})).join('\n'));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {performance} from 'node:perf_hooks';
|
|
2
|
+
import {ROADY_CLIPS,renderRoady,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?ROADY_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?renderRoady(paths,scene,t,options):renderStory(paths,t,options);
|
|
16
|
+
if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
|
|
17
|
+
const caption=message??'ROADY · '+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,57 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480"><title>Weaver — fresh anatomy study</title>
|
|
2
|
+
<g id="far-wing">
|
|
3
|
+
<path id="far-sail" d="M 355 268 Q 400 234 427 186 Q 440 226 415 249 L 425 278 L 393 270 L 373 299 Z" fill="#773c89"/>
|
|
4
|
+
<path id="far-ribs" d="M 355 268 Q 400 234 427 186 M 355 268 L 425 278" fill="none" stroke="#d480bb" stroke-width="6"/>
|
|
5
|
+
</g>
|
|
6
|
+
<g id="tail">
|
|
7
|
+
<path id="tail-mass" d="M 290 351 Q 242 366 218 395 C 188 429 131 429 113 398 Q 101 379 112 357 Q 85 375 99 411 C 119 458 195 447 232 420 Q 265 396 304 390 Z" fill="#8052ad"/>
|
|
8
|
+
<path id="tail-highlight" d="M 278 379 Q 215 438 159 436 Q 118 435 105 409 Q 137 439 186 418 Q 232 399 266 374 Z" fill="#b76bbf"/>
|
|
9
|
+
</g>
|
|
10
|
+
<g id="body">
|
|
11
|
+
<path id="torso" d="M 289 229 Q 326 218 363 239 Q 383 270 368 313 L 376 355 Q 361 395 321 398 Q 275 396 265 360 L 278 309 Q 263 263 289 229 Z" fill="#8054a6"/>
|
|
12
|
+
<path id="breast" d="M 316 246 Q 344 233 355 255 L 351 306 Q 370 344 351 373 Q 327 389 309 366 L 301 326 Q 318 286 316 246 Z" fill="#c99acb"/>
|
|
13
|
+
<path id="breast-lines" d="M 311 289 Q 331 303 353 289 M 306 318 Q 328 333 357 317 M 310 347 Q 331 360 358 346" fill="none" stroke="#8d619f" stroke-width="4"/>
|
|
14
|
+
<path id="left-haunch" d="M 289 328 Q 249 335 251 373 L 231 405 L 279 416 L 310 379 Z" fill="#9667bb"/>
|
|
15
|
+
<path id="right-haunch" d="M 352 334 Q 388 336 390 373 L 413 402 L 405 415 L 356 411 L 336 380 Z" fill="#a170c6"/>
|
|
16
|
+
<path id="claws" d="M 244 401 L 236 418 L 251 412 L 260 403 L 259 419 L 276 413 M 378 402 L 375 418 L 390 412 L 399 404 L 399 419 L 415 411" fill="#f8e9d9"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g id="near-wing">
|
|
19
|
+
<path id="near-sail" d="M 282 261 Q 225 135 129 98 Q 155 143 120 192 Q 160 197 166 246 Q 206 231 224 293 L 258 303 Z" fill="#a94595"/>
|
|
20
|
+
<path id="near-silk" d="M 282 261 L 129 98 Q 209 177 166 246 Q 213 230 224 293 L 258 303 Z" fill="#d264b2"/>
|
|
21
|
+
<path id="near-ribs" d="M 282 261 Q 225 135 129 98 M 282 261 L 120 192 M 282 261 L 166 246 M 282 261 L 224 293" fill="none" stroke="#e8a7d5" stroke-width="6"/>
|
|
22
|
+
<path id="silk-cross" d="M 154 144 Q 206 141 245 194 M 139 189 Q 204 177 262 225 M 169 236 Q 220 207 275 248" fill="none" stroke="#954084" stroke-width="4"/>
|
|
23
|
+
<path id="shoulder" d="M 262 266 C 262 254.954305 269.61115925 246 279 246 C 288.38884075 246 296 254.954305 296 266 C 296 277.045695 288.38884075 286 279 286 C 269.61115925 286 262 277.045695 262 266 Z" fill="#a273c2"/>
|
|
24
|
+
</g>
|
|
25
|
+
<g id="neck">
|
|
26
|
+
<path id="neck-mass" d="M 301 181 L 349 184 Q 343 216 361 244 Q 339 262 310 250 L 288 236 Q 310 210 301 181 Z" fill="#a675c8"/>
|
|
27
|
+
<path id="throat" d="M 335 196 L 352 193 Q 345 220 361 244 L 341 253 Q 325 228 335 196 Z" fill="#dfb2dd"/>
|
|
28
|
+
</g>
|
|
29
|
+
<g id="arms">
|
|
30
|
+
<path id="far-arm" d="M 358 248 Q 377 244 386 267 L 394 291 L 420 286 L 427 301 L 389 315 Q 378 315 370 296 L 356 273 Z" fill="#9566b8"/>
|
|
31
|
+
<path id="near-arm-outline" d="M 295 258 Q 320 242 334 265 L 344 291 L 381 295 L 387 317 L 332 318 Q 315 308 307 288 Z" fill="#080d19"/>
|
|
32
|
+
<path id="near-arm" d="M 299 261 Q 318 252 329 270 L 338 298 L 377 302 L 379 312 L 334 312 Q 320 303 312 284 Z" fill="#ba88d5"/>
|
|
33
|
+
<path id="near-shoulder" d="M 291 269 C 291 259.61115925 298.163444 252 307 252 C 315.836556 252 323 259.61115925 323 269 C 323 278.38884075 315.836556 286 307 286 C 298.163444 286 291 278.38884075 291 269 Z" fill="#ba88d5"/>
|
|
34
|
+
<path id="near-hand" d="M 373 300 L 397 291 L 400 297 L 388 304 L 408 301 L 409 308 L 379 316 Z" fill="#e3bde5"/>
|
|
35
|
+
<path id="far-hand" d="M 416 287 L 435 274 L 440 279 L 428 291 L 446 286 L 448 293 L 425 305 Z" fill="#e3bde5"/>
|
|
36
|
+
</g>
|
|
37
|
+
<g id="head">
|
|
38
|
+
<path id="far-horn" d="M 298 140 Q 278 120 263 84 Q 291 108 320 115 L 320 145 Z" fill="#e3d9ca"/>
|
|
39
|
+
<path id="near-horn" d="M 339 130 Q 317 95 325 65 Q 337 92 363 108 L 365 140 Z" fill="#fff0dc"/>
|
|
40
|
+
<path id="ear" d="M 294 160 L 268 146 L 276 174 L 299 185 Z" fill="#cc85c5"/>
|
|
41
|
+
<path id="skull" d="M 293 138 Q 326 115 356 138 Q 378 157 366 185 L 343 209 L 306 204 Q 281 181 293 138 Z" fill="#b180ce"/>
|
|
42
|
+
<path id="far-horn-root" d="M 296 138 Q 305 130 317 134" fill="none" stroke="#cf9bdf" stroke-width="6"/>
|
|
43
|
+
<path id="near-horn-root" d="M 338 131 Q 350 128 361 138" fill="none" stroke="#cf9bdf" stroke-width="6"/>
|
|
44
|
+
<path id="snout" d="M 340 177 Q 367 169 389 181 L 399 194 L 391 207 L 357 207 L 337 194 Z" fill="#d1a2df"/>
|
|
45
|
+
<path id="chin" d="M 351 203 Q 373 214 391 203 Q 381 222 360 215 Z" fill="#e5c3e5"/>
|
|
46
|
+
<path id="mouth" d="M 347 196 Q 371 208 391 195" fill="none" stroke="#080d19" stroke-width="5"/>
|
|
47
|
+
<path id="nostril" d="M 382 182 L 389 187 L 382 190 Z" fill="#080d19"/>
|
|
48
|
+
</g>
|
|
49
|
+
<g id="eyes">
|
|
50
|
+
<path id="far-eye-socket" d="M 291 164 C 291 152.40202025 298.163444 143 307 143 C 315.836556 143 323 152.40202025 323 164 C 323 175.59797975 315.836556 185 307 185 C 298.163444 185 291 175.59797975 291 164 Z" fill="#080d19"/>
|
|
51
|
+
<path id="far-eye-white" d="M 297 164 C 297 155.71572875 301.4771525 149 307 149 C 312.5228475 149 317 155.71572875 317 164 C 317 172.28427125 312.5228475 179 307 179 C 301.4771525 179 297 172.28427125 297 164 Z" fill="#ffffff"/>
|
|
52
|
+
<path id="far-pupil" d="M 305 165 C 305 161.6862915 306.790861 159 309 159 C 311.209139 159 313 161.6862915 313 165 C 313 168.3137085 311.209139 171 309 171 C 306.790861 171 305 168.3137085 305 165 Z" fill="#080d19"/>
|
|
53
|
+
<path id="near-eye-socket" d="M 321 159 C 321 144.64059650000002 330.40202025 133 342 133 C 353.59797975 133 363 144.64059650000002 363 159 C 363 173.35940349999998 353.59797975 185 342 185 C 330.40202025 185 321 173.35940349999998 321 159 Z" fill="#080d19"/>
|
|
54
|
+
<path id="near-eye-white" d="M 327 159 C 327 147.954305 333.71572875 139 342 139 C 350.28427125 139 357 147.954305 357 159 C 357 170.045695 350.28427125 179 342 179 C 333.71572875 179 327 170.045695 327 159 Z" fill="#ffffff"/>
|
|
55
|
+
<path id="near-pupil" d="M 340 160 C 340 156.6862915 341.790861 154 344 154 C 346.209139 154 348 156.6862915 348 160 C 348 163.3137085 346.209139 166 344 166 C 341.790861 166 340 163.3137085 340 160 Z" fill="#080d19"/>
|
|
56
|
+
</g>
|
|
57
|
+
</svg>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
|
|
2
|
+
export const STORY_DURATION=20000;
|
|
3
|
+
export const WEAVER_CLIPS=Object.freeze([{id:'idle',title:'Ready to weave',duration:8000},{id:'weave',title:'Weave · Untangle · Run',duration:STORY_DURATION}]);
|
|
4
|
+
const clamp=x=>Math.max(0,Math.min(1,x));
|
|
5
|
+
const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
|
|
6
|
+
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)];
|
|
7
|
+
export function poseRig(rig,scene,ms){
|
|
8
|
+
if(scene==='weave'&&ms>=STORY_DURATION)return poseRig(rig,'idle',ms-STORY_DURATION);
|
|
9
|
+
const active=scene==='weave',t=ms,env=active?ease(t/1600)*(1-ease((t-17000)/3000)):0;
|
|
10
|
+
const time=active?t*(1-ease((t-17000)/3000)):t,quiet=Math.sin(time/8000*Math.PI*2);
|
|
11
|
+
const knot=active?ease((t-6500)/900)*(1-ease((t-10000)/2200)):0;
|
|
12
|
+
const work=env*(.5+.5*Math.sin(t/700));
|
|
13
|
+
const arm=(x,y,near)=>{const w=clamp((x-(near?310:366))/(near?90:70));return [x+w*8*work,y+w*(near?18*env+7*work:-22*env-9*work)];};
|
|
14
|
+
const head=(x,y)=>rot(x,y,327,222,.055*knot-.025*env+.012*quiet);
|
|
15
|
+
const paths=rig.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
|
|
16
|
+
if(p.group==='head'||p.group==='eyes'){const q=head(x,y);if(p.id.endsWith('pupil'))q[0]+=env*(t>14500?-2:1);return q;}
|
|
17
|
+
if(p.group==='neck'){const w=clamp((252-y)/75),q=head(x,y);return [x+(q[0]-x)*w,y+(q[1]-y)*w];}
|
|
18
|
+
if(p.group==='arms')return arm(x,y,p.id.startsWith('near'));
|
|
19
|
+
if(p.group==='near-wing')return rot(x,y,282,261,.025*quiet+.035*env);
|
|
20
|
+
if(p.group==='far-wing')return rot(x,y,355,268,-.018*quiet);
|
|
21
|
+
if(p.group==='tail'){const w=clamp((290-x)/200);return [x,y-8*w*quiet];}
|
|
22
|
+
return [x,y];
|
|
23
|
+
}))}));
|
|
24
|
+
const add=(id,rgb,poly,stroke=0)=>paths.push({id,group:'threads',rgb,polygons:[poly],stroke,erase:false});
|
|
25
|
+
const circle=(id,x,y,r,c,w=0)=>add(id,c,Array.from({length:33},(_,i)=>{const a=i/32*Math.PI*2;return[x+r*Math.cos(a),y+r*Math.sin(a)];}),w);
|
|
26
|
+
const open=active?ease((t-1200)/2600)*(1-ease((t-17200)/2400)):0;
|
|
27
|
+
if(open>.001){
|
|
28
|
+
const near=arm(408,305,true),far=arm(447,290,false),join=[548,287],out=[640,287];
|
|
29
|
+
const makeThread=(start,side)=>Array.from({length:41},(_,i)=>{const u=i/40*open;return [start[0]+(join[0]-start[0])*u+Math.sin(u*Math.PI*4)*14*knot*Math.sin(u*Math.PI),start[1]+(join[1]-start[1])*u+side*(25*Math.sin(u*Math.PI)+22*knot*Math.sin(u*Math.PI*4))];});
|
|
30
|
+
const a=makeThread(far,-1),b=makeThread(near,1);
|
|
31
|
+
add('far-thread',[101,225,214],a,4);add('near-thread',[255,174,108],b,4);
|
|
32
|
+
add('output-thread',[215,153,243],[join,[join[0]+(out[0]-join[0])*open,287]],4);
|
|
33
|
+
for(const [id,x,y]of [['join',548,287],['out',640,287]]){
|
|
34
|
+
if(open>.85){circle('node-'+id,x,y,12,[203,156,231],4);}
|
|
35
|
+
}
|
|
36
|
+
if(knot>.05)circle('knot',499,289,15*knot,[245,127,166],4);
|
|
37
|
+
if(t>12500&&t<17000){
|
|
38
|
+
const u=clamp((t-12500)/3300),branch=u<.65;
|
|
39
|
+
const poly=b,index=Math.min(39,Math.floor(u/.65*40));
|
|
40
|
+
const point=branch?poly[index]:[548+(640-548)*(u-.65)/.35,287];
|
|
41
|
+
circle('run-spark',point[0],point[1],6,[255,252,219]);
|
|
42
|
+
if(u>.65)circle('join-lit',548,287,15,[117,242,212],4);
|
|
43
|
+
if(u>.98)circle('output-lit',640,287,18,[117,242,212],5);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const map=new Map(paths.map(p=>[p.id,p]));return {...rig,paths,bounds:{left:65,top:35,right:675,bottom:450},eyes:rig.eyes.map(e=>({...e,socket:map.get(e.socket.id),white:map.get(e.white.id),pupil:map.get(e.pupil.id)}))};
|
|
47
|
+
}
|
|
48
|
+
export function renderWeaver(rig,scene='idle',ms=0,options={}){
|
|
49
|
+
if(!WEAVER_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
|
|
50
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
51
|
+
if(scene==='weave'&&!options.still&&ms>=STORY_DURATION)return renderWeaver(rig,'idle',ms-STORY_DURATION,options);
|
|
52
|
+
const t=options.still?(scene==='weave'?14500:0):ms;
|
|
53
|
+
const caption=scene==='idle'?'A little order. A little magic.':t<6500?'Draw the threads together.':t<10000?'Oh. A little knot.':t<12500?'Patience. Separate the strands.':t<16000?'Now, let it flow.':t<17500?'Beautiful. It runs.':'Ready for the next idea.';
|
|
54
|
+
return {...renderSvgRig(poseRig(rig,scene,t),options),scene,clip:scene,caption};
|
|
55
|
+
}
|
|
56
|
+
export const renderStory=(rig,ms,options={})=>renderWeaver(rig,'weave',ms,options);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
|
|
2
|
+
import {poseRig as priorPose,renderWeaver as priorRender,WEAVER_CLIPS as priorClips} from '../v4/character.mjs';
|
|
3
|
+
export const STORY_DURATION=14000;
|
|
4
|
+
export const WEAVER_CLIPS=Object.freeze([...priorClips,{id:'repair',title:'Catch the fault · Stitch · Test',duration:STORY_DURATION}]);
|
|
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 function poseRig(rig,scene,ms){
|
|
8
|
+
if(scene!=='repair')return priorPose(rig,scene,ms);
|
|
9
|
+
if(ms>=STORY_DURATION)return priorPose(rig,'idle',ms-STORY_DURATION);
|
|
10
|
+
const t=ms;
|
|
11
|
+
// Open and close using the preserved hand/thread choreography, skipping its knot.
|
|
12
|
+
const baseTime=t<2200?t*2.5:t<11000?5500+450*Math.sin((t-2200)/450)*Math.sin(Math.PI*(t-2200)/8800):17000+(t-11000);
|
|
13
|
+
const base=priorPose(rig,'weave',baseTime);
|
|
14
|
+
const paths=base.paths.filter(p=>!['output-thread','run-spark','join-lit','output-lit','knot'].includes(p.id));
|
|
15
|
+
const add=(id,rgb,poly,stroke=0)=>paths.push({id,group:'repair',rgb,polygons:[poly],stroke,erase:false});
|
|
16
|
+
const line=(id,c,a,b,w=4)=>add(id,c,[a,b],w);
|
|
17
|
+
const circle=(id,x,y,r,c,w=0)=>add(id,c,Array.from({length:33},(_,i)=>{const a=i/32*Math.PI*2;return[x+r*Math.cos(a),y+r*Math.sin(a)];}),w);
|
|
18
|
+
const open=ease((baseTime-1200)/2600)*(1-ease((baseTime-17200)/2400));
|
|
19
|
+
const snap=ease((t-2500)/300)*(1-ease((t-6000)/1900));
|
|
20
|
+
if(open>.001){
|
|
21
|
+
const right=548+92*open,gap=13*snap,sag=18*snap;
|
|
22
|
+
if(t<2500||t>=7900)line('repaired-thread',[215,153,243],[548,287],[right,287]);
|
|
23
|
+
else {
|
|
24
|
+
add('broken-left',[238,137,170],[[548,287],[572,287],[594-gap,287+sag]],4);
|
|
25
|
+
add('broken-right',[238,137,170],[[594+gap,287+sag],[618,287],[right,287]],4);
|
|
26
|
+
if(t<3900){const burst=Math.sin(Math.PI*clamp((t-2500)/1400));for(let i=0;i<4;i++){const a=i*Math.PI/2;line('snap-spark'+i,[255,197,107],[594+15*Math.cos(a),287+15*Math.sin(a)],[594+(15+15*burst)*Math.cos(a),287+(15+15*burst)*Math.sin(a)],3);}}
|
|
27
|
+
}
|
|
28
|
+
if(t>=4200&&t<8500){
|
|
29
|
+
const progress=clamp((t-4200)/3300),n=Math.floor(progress*24);
|
|
30
|
+
const stitches=Array.from({length:n+1},(_,i)=>[576+i*1.5,287+Math.sin(i/24*Math.PI*6)*9]);
|
|
31
|
+
if(stitches.length>1)add('stitch',[255,222,147],stitches,4);
|
|
32
|
+
const point=stitches.at(-1);circle('needle',point[0],point[1],5,[255,251,222]);
|
|
33
|
+
const hand=base.paths.find(p=>p.id==='far-hand').polygons[0][3];
|
|
34
|
+
// Fine working silk stays attached to the posed hand and moving needle.
|
|
35
|
+
add('working-silk',[116,225,213],[hand,[(hand[0]+point[0])/2,hand[1]-24],point],3);
|
|
36
|
+
}
|
|
37
|
+
if(t>=8500&&t<11000){
|
|
38
|
+
const u=clamp((t-8500)/1800);circle('test-spark',548+92*u,287,6,[255,252,219]);
|
|
39
|
+
if(u>.98)circle('repair-success',640,287,19,[113,240,204],5);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {...base,paths};
|
|
43
|
+
}
|
|
44
|
+
export function renderWeaver(rig,scene='idle',ms=0,options={}){
|
|
45
|
+
if(scene!=='repair')return priorRender(rig,scene,ms,options);
|
|
46
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
47
|
+
if(!options.still&&ms>=STORY_DURATION)return priorRender(rig,'idle',ms-STORY_DURATION,options);
|
|
48
|
+
const t=options.still?6800:ms;
|
|
49
|
+
const caption=t<2500?'One more check.':t<4200?'That thread won’t hold.':t<7900?'Steady hands. Stitch it back together.':t<10300?'Test the repaired connection.':t<11000?'There. Good as new.':'Ready to weave again.';
|
|
50
|
+
return {...renderSvgRig(poseRig(rig,scene,t),options),scene,clip:scene,caption};
|
|
51
|
+
}
|
|
52
|
+
export const renderStory=(rig,ms,options={})=>renderWeaver(rig,'repair',ms,options);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
|
|
2
|
+
import {poseRig as v5Pose,renderWeaver as v5Render,WEAVER_CLIPS as v5Clips} from '../v5/character.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Weaver V6: companion pass on the approved V3 SVG. Idle, Weave and Repair are V4/V5 exactly.
|
|
6
|
+
* Adds 'attach' (notice, open hands in greeting, settle; exact handoff to idle) and 'thread' (both hands draw
|
|
7
|
+
* turquoise and amber threads into one join and an output, then let them go). Thread has no knot, no run spark and
|
|
8
|
+
* no lit output: those would claim a fault or a successful run the host never states. Pure functions of elapsed time.
|
|
9
|
+
*/
|
|
10
|
+
export const ATTACH_MS=3200,THREAD_MS=7000;
|
|
11
|
+
export const WEAVER_CLIPS=Object.freeze([...v5Clips,{id:'attach',title:'Notice · open hands · settle',duration:ATTACH_MS},{id:'thread',title:'Draw the threads together',duration:THREAD_MS}]);
|
|
12
|
+
export const STORY_DURATION=ATTACH_MS+THREAD_MS;
|
|
13
|
+
const STILL=Object.freeze({attach:1500,thread:3600});
|
|
14
|
+
const clamp=x=>Math.max(0,Math.min(1,x));
|
|
15
|
+
const ease=x=>{x=clamp(x);return x*x*(3-2*x);};
|
|
16
|
+
const pulse=(t,a,b)=>t<=a||t>=b?0:Math.sin(Math.PI*(t-a)/(b-a))**2;
|
|
17
|
+
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)];
|
|
18
|
+
const REST=Object.freeze({head:0,pupil:0,env:0,work:0,near:0,far:0,tail:0,open:0});
|
|
19
|
+
|
|
20
|
+
/** One parameter set per instant. All zero is V4 idle at t=0, the rest pose. */
|
|
21
|
+
export function weaverParams(scene,t){
|
|
22
|
+
if(scene==='attach'){
|
|
23
|
+
// Notice: the head lifts and the eyes glance at whoever arrived. Acknowledge: a nod while both hands open a little.
|
|
24
|
+
// Settle: wings ease back and the tail curls last.
|
|
25
|
+
return {...REST,head:-.04*pulse(t,0,1400)+.035*pulse(t,1100,2500),pupil:-2*pulse(t,200,2300),
|
|
26
|
+
env:.45*pulse(t,800,2500),work:.3*pulse(t,900,2400),near:.05*pulse(t,1300,2900),far:-.03*pulse(t,1500,3000),tail:pulse(t,1800,ATTACH_MS)};
|
|
27
|
+
}
|
|
28
|
+
if(scene==='thread'){
|
|
29
|
+
const env=ease(t/900)*(1-ease((t-5800)/1200));
|
|
30
|
+
return {...REST,head:-.025*env,pupil:env,env,work:env*(.5+.5*Math.sin(t/700)),near:.035*env,
|
|
31
|
+
open:ease((t-600)/2000)*(1-ease((t-4900)/1500))};
|
|
32
|
+
}
|
|
33
|
+
return REST;
|
|
34
|
+
}
|
|
35
|
+
const BOUNDS=Object.freeze({left:65,top:35,right:675,bottom:450});
|
|
36
|
+
function poseFromParams(rig,P){
|
|
37
|
+
// Same anatomy transforms as V4: arms deform from fixed shoulder roots; face, horns and eyes share one head joint.
|
|
38
|
+
const arm=(x,y,near)=>{const w=clamp((x-(near?310:366))/(near?90:70));return [x+w*8*P.work,y+w*(near?18*P.env+7*P.work:-22*P.env-9*P.work)];};
|
|
39
|
+
const head=(x,y)=>rot(x,y,327,222,P.head);
|
|
40
|
+
const paths=rig.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
|
|
41
|
+
if(p.group==='head'||p.group==='eyes'){const q=head(x,y);if(p.id.endsWith('pupil'))q[0]+=P.pupil;return q;}
|
|
42
|
+
if(p.group==='neck'){const w=clamp((252-y)/75),q=head(x,y);return [x+(q[0]-x)*w,y+(q[1]-y)*w];}
|
|
43
|
+
if(p.group==='arms')return arm(x,y,p.id.startsWith('near'));
|
|
44
|
+
if(p.group==='near-wing')return rot(x,y,282,261,P.near);
|
|
45
|
+
if(p.group==='far-wing')return rot(x,y,355,268,P.far);
|
|
46
|
+
if(p.group==='tail'){const w=clamp((290-x)/200);return [x,y-8*w*P.tail];}
|
|
47
|
+
return [x,y];
|
|
48
|
+
}))}));
|
|
49
|
+
if(P.open>.001){
|
|
50
|
+
// V4's thread drawing with the knot term at zero: threads leave the posed hands, meet at one join, run to the output.
|
|
51
|
+
const add=(id,rgb,poly,stroke=0)=>paths.push({id,group:'threads',rgb,polygons:[poly],stroke,erase:false});
|
|
52
|
+
const circle=(id,x,y,r,c,w=0)=>add(id,c,Array.from({length:33},(_,i)=>{const a=i/32*Math.PI*2;return[x+r*Math.cos(a),y+r*Math.sin(a)];}),w);
|
|
53
|
+
const near=arm(408,305,true),far=arm(447,290,false),join=[548,287],out=[640,287];
|
|
54
|
+
const thread=(start,side)=>Array.from({length:41},(_,i)=>{const u=i/40*P.open;return [start[0]+(join[0]-start[0])*u,start[1]+(join[1]-start[1])*u+side*25*Math.sin(u*Math.PI)];});
|
|
55
|
+
add('far-thread',[101,225,214],thread(far,-1),4);add('near-thread',[255,174,108],thread(near,1),4);
|
|
56
|
+
add('output-thread',[215,153,243],[join,[join[0]+(out[0]-join[0])*P.open,287]],4);
|
|
57
|
+
if(P.open>.85){circle('node-join',548,287,12,[203,156,231],4);circle('node-out',640,287,12,[203,156,231],4);}
|
|
58
|
+
}
|
|
59
|
+
const map=new Map(paths.map(p=>[p.id,p]));
|
|
60
|
+
return {...rig,paths,bounds:BOUNDS,eyes:rig.eyes.map(e=>({...e,socket:map.get(e.socket.id),white:map.get(e.white.id),pupil:map.get(e.pupil.id)}))};
|
|
61
|
+
}
|
|
62
|
+
const NEW=new Set(['attach','thread']);
|
|
63
|
+
export function poseRig(rig,scene,ms){
|
|
64
|
+
if(!NEW.has(scene))return v5Pose(rig,scene,ms);
|
|
65
|
+
const d=scene==='attach'?ATTACH_MS:THREAD_MS;
|
|
66
|
+
if(ms>=d)return v5Pose(rig,'idle',ms-d);
|
|
67
|
+
return poseFromParams(rig,weaverParams(scene,ms));
|
|
68
|
+
}
|
|
69
|
+
function caption(scene,t){
|
|
70
|
+
if(scene==='attach')return t<1100?'Oh. Hello.':t<2500?'Weaver opens her hands to you.':'A little order. A little magic.';
|
|
71
|
+
return t<2600?'Draw the threads together.':t<4900?'One join. One way out.':'Let the threads go.';
|
|
72
|
+
}
|
|
73
|
+
export function renderWeaver(rig,scene='idle',ms=0,options={}){
|
|
74
|
+
if(!WEAVER_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
|
|
75
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
76
|
+
if(!NEW.has(scene))return v5Render(rig,scene,ms,options);
|
|
77
|
+
const d=scene==='attach'?ATTACH_MS:THREAD_MS;
|
|
78
|
+
if(!options.still&&ms>=d)return v5Render(rig,'idle',ms-d,options);
|
|
79
|
+
// Still freezes every channel (head, pupils, hands, wings, tail, threads) at one pose.
|
|
80
|
+
const t=options.still?STILL[scene]:ms;
|
|
81
|
+
return {...renderSvgRig(poseRig(rig,scene,t),options),scene,clip:scene,caption:caption(scene,t)};
|
|
82
|
+
}
|
|
83
|
+
/** Offline companion story: attach, then thread, then idle. Simulated; no QuoxFlow state. */
|
|
84
|
+
export function renderStory(rig,ms,options={}){
|
|
85
|
+
if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
|
|
86
|
+
if(options.still)return renderWeaver(rig,'thread',0,options);
|
|
87
|
+
return ms<ATTACH_MS?renderWeaver(rig,'attach',ms,options):renderWeaver(rig,'thread',ms-ATTACH_MS,options);
|
|
88
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import {parseSvgRig} from '../../../factory/svg-rig-contour.mjs';
|
|
4
|
+
import {renderWeaver,WEAVER_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('Weaver V6 thread studio (companion attach and Thread). --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(WEAVER_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','WEAVER\nA little order. A little magic.');
|
|
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('../v3/weaver-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=renderWeaver(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
|
+
const shownMessage=args.includes('--message')?message:'WEAVER\n'+r.caption;
|
|
33
|
+
console.log((large?[...cellsToAnsi(r.frame),...composeMessage(null,shownMessage,{terminalCols:w})]:composeMessage(r.frame,shownMessage,{terminalCols:w})).join('\n'));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {performance} from 'node:perf_hooks';
|
|
2
|
+
import {WEAVER_CLIPS,renderWeaver,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?WEAVER_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?renderWeaver(paths,scene,t,options):renderStory(paths,t,options);
|
|
16
|
+
if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
|
|
17
|
+
const caption=message??'WEAVER · '+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
|
+
}
|