quoxcli 0.6.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +18 -3
  2. package/lib/api/endpoints/admin.js +21 -0
  3. package/lib/api/endpoints/calendarq.js +8 -0
  4. package/lib/api/endpoints/index.js +2 -0
  5. package/lib/api/endpoints/lens.js +34 -0
  6. package/lib/api/endpoints/lucid.js +2 -1
  7. package/lib/api/endpoints/mon.js +6 -7
  8. package/lib/api/endpoints/v1.js +57 -0
  9. package/lib/commands/admin.js +86 -0
  10. package/lib/commands/approvals.js +5 -1
  11. package/lib/commands/calendar.js +27 -0
  12. package/lib/commands/chat.js +1 -1
  13. package/lib/commands/fleet.js +164 -0
  14. package/lib/commands/fleetRecipe.js +174 -0
  15. package/lib/commands/lens.js +268 -0
  16. package/lib/commands/lucidGoals.js +36 -2
  17. package/lib/commands/mail.js +21 -2
  18. package/lib/commands/monitoring.js +5 -1
  19. package/lib/commands/plan.js +36 -1
  20. package/lib/commands/research.js +78 -0
  21. package/lib/commands/settings.js +7 -1
  22. package/lib/commands/wazuh.js +9 -0
  23. package/lib/commands/wazuhIntake.js +11 -3
  24. package/lib/core/commandRegistrars.js +1 -0
  25. package/lib/core/version.js +15 -2
  26. package/lib/dragon/select.js +2 -2
  27. package/lib/quoxon/app.js +1 -1
  28. package/lib/quoxon/commands.js +1 -1
  29. package/lib/quoxon/dragonCompanion.js +24 -12
  30. package/lib/quoxon/main.js +1 -1
  31. package/lib/quoxon/onboarding/view.js +1 -1
  32. package/lib/quoxon/slash/cliLeaves.json +19 -0
  33. package/lib/quoxon/whatsnew.jsonl +1 -0
  34. package/package.json +1 -1
  35. package/vendor/dragonlab/SOURCE +3 -3
  36. package/vendor/dragonlab/design/architect/v2/architect-master.svg +67 -0
  37. package/vendor/dragonlab/design/architect/v2/character.mjs +83 -0
  38. package/vendor/dragonlab/design/architect/v2/review.mjs +35 -0
  39. package/vendor/dragonlab/design/architect/v2/watch.mjs +56 -0
  40. package/vendor/dragonlab/design/archivist/v2/archivist-master.svg +77 -0
  41. package/vendor/dragonlab/design/archivist/v3/character.mjs +56 -0
  42. package/vendor/dragonlab/design/archivist/v4/character.mjs +46 -0
  43. package/vendor/dragonlab/design/archivist/v4/review.mjs +35 -0
  44. package/vendor/dragonlab/design/archivist/v4/watch.mjs +56 -0
  45. package/vendor/dragonlab/design/argus/v2/character.mjs +69 -0
  46. package/vendor/dragonlab/design/argus/v3/argus-master.svg +68 -0
  47. package/vendor/dragonlab/design/argus/v3/character.mjs +63 -0
  48. package/vendor/dragonlab/design/argus/v3/review.mjs +35 -0
  49. package/vendor/dragonlab/design/argus/v3/watch.mjs +56 -0
  50. package/vendor/dragonlab/design/daedalus/v2/character.mjs +49 -0
  51. package/vendor/dragonlab/design/daedalus/v3/daedalus-master.svg +65 -0
  52. package/vendor/dragonlab/design/daedalus/v4/character.mjs +47 -0
  53. package/vendor/dragonlab/design/daedalus/v5/character.mjs +66 -0
  54. package/vendor/dragonlab/design/daedalus/v6/character.mjs +32 -0
  55. package/vendor/dragonlab/design/daedalus/v6/review.mjs +35 -0
  56. package/vendor/dragonlab/design/daedalus/v6/watch.mjs +56 -0
  57. package/vendor/dragonlab/design/janus/v1/janus-master.svg +70 -0
  58. package/vendor/dragonlab/design/janus/v2/character.mjs +56 -0
  59. package/vendor/dragonlab/design/janus/v3/character.mjs +7 -0
  60. package/vendor/dragonlab/design/janus/v4/character.mjs +150 -0
  61. package/vendor/dragonlab/design/janus/v5/character.mjs +55 -0
  62. package/vendor/dragonlab/design/janus/v5/review.mjs +35 -0
  63. package/vendor/dragonlab/design/janus/v5/watch.mjs +56 -0
  64. package/vendor/dragonlab/design/roady/v1/roady-master.svg +67 -0
  65. package/vendor/dragonlab/design/roady/v2/character.mjs +69 -0
  66. package/vendor/dragonlab/design/roady/v3/character.mjs +91 -0
  67. package/vendor/dragonlab/design/roady/v3/review.mjs +35 -0
  68. package/vendor/dragonlab/design/roady/v3/watch.mjs +56 -0
  69. package/vendor/dragonlab/design/weaver/v3/weaver-master.svg +57 -0
  70. package/vendor/dragonlab/design/weaver/v4/character.mjs +56 -0
  71. package/vendor/dragonlab/design/weaver/v5/character.mjs +52 -0
  72. package/vendor/dragonlab/design/weaver/v6/character.mjs +88 -0
  73. package/vendor/dragonlab/design/weaver/v6/review.mjs +35 -0
  74. package/vendor/dragonlab/design/weaver/v6/watch.mjs +56 -0
  75. package/vendor/dragonlab/integration/companions-v1/architect.mjs +2 -0
  76. package/vendor/dragonlab/integration/companions-v1/archivist.mjs +2 -0
  77. package/vendor/dragonlab/integration/companions-v1/argus.mjs +2 -0
  78. package/vendor/dragonlab/integration/companions-v1/characters.mjs +86 -0
  79. package/vendor/dragonlab/integration/companions-v1/daedalus.mjs +2 -0
  80. package/vendor/dragonlab/integration/companions-v1/janus.mjs +2 -0
  81. package/vendor/dragonlab/integration/companions-v1/roady.mjs +2 -0
  82. package/vendor/dragonlab/integration/companions-v1/view.mjs +4 -1
  83. package/vendor/dragonlab/integration/companions-v1/weaver.mjs +2 -0
@@ -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 {renderArgus,ARGUS_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('Argus V3 forge 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(ARGUS_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','ARGUS\nWatch. Triage. Inspect.');
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('./argus-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=renderArgus(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:'ARGUS\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 {ARGUS_CLIPS,renderArgus,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?ARGUS_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?renderArgus(paths,scene,t,options):renderStory(paths,t,options);
16
+ if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
17
+ const caption=message??'ARGUS · '+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,49 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
2
+ export const DAEDALUS_CLIPS=Object.freeze([{id:'idle',title:'Architect at Rest',duration:12000},{id:'attach',title:'Open the Workshop',duration:3000},{id:'starforge',title:'Starforge',duration:10000}]);
3
+ export const STORY_DURATION=15000;
4
+ const ease=x=>{x=Math.max(0,Math.min(1,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 function poseRig(rig,scene,ms){
7
+ const t=scene==='attach'?ms:ms%(scene==='starforge'?10000:12000);
8
+ const active=scene==='starforge',gather=active?ease((t-1000)/2200)*(1-ease((t-8000)/2000)):0;
9
+ const gesture=active?pulse(t,300,4300):scene==='attach'?pulse(t,300,2300):.2*pulse(t,2000,6500);
10
+ const paths=rig.paths.map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
11
+ if(p.id==='component-left')return [x+29*gather,y+5*gather];
12
+ if(p.id==='component-right')return [x-37*gather,y-10*gather];
13
+ if(p.group==='head'||p.group==='eyes')return [x+3*gesture,y+6*gesture];
14
+ if(p.group==='neck'){const w=Math.max(0,Math.min(1,(290-y)/90));return [x+3*gesture*w,y+6*gesture*w];}
15
+ if(p.group==='arms'){const w=Math.max(0,Math.min(1,(x-350)/110));return [x+12*gesture*w,y+(y<290?8:-8)*gesture*w];}
16
+ if(p.group==='tail'){const w=Math.max(0,Math.min(1,(300-x)/210));return [x,y-5*gesture*w*w];}
17
+ return [x,y];
18
+ }))}));
19
+ const stages=['assembly-top','component-left','component-right'];
20
+ for(let i=0;i<stages.length;i++){
21
+ const p=paths.find(p=>p.id===stages[i]);
22
+ const lit=active?ease((t-3600-i*650)/450)*(1-ease((t-7300)/700)):0;
23
+ p.rgb=[Math.round(p.rgb[0]+(255-p.rgb[0])*lit),Math.round(p.rgb[1]+(239-p.rgb[1])*lit),Math.round(p.rgb[2]+(180-p.rgb[2])*lit)];
24
+ p.stroke+=2*lit;
25
+ }
26
+ if(active&&t>3200&&t<8000){
27
+ const alpha=ease((t-3200)/400)*(1-ease((t-7400)/600));
28
+ paths.push({id:'assembly-links',group:'effect',erase:false,stroke:4,rgb:[Math.round(240*alpha),Math.round(210*alpha),Math.round(140*alpha)],polygons:[[[592,351],[592,344],[607,344],[622,344],[622,351]]]});
29
+ if(t>=3600&&t<6000){const u=(t-3600)/2400,y=292+96*u;
30
+ paths.push({id:'deployment-wave',group:'effect',erase:false,stroke:4,rgb:[255,248,218],polygons:[[[577,y],[638,y]]]});
31
+ }
32
+ if(t>=6000&&t<7400){const r=4+10*pulse(t,6000,7400);paths.push({id:'completion-star',group:'effect',erase:false,stroke:0,rgb:[255,239,180],polygons:[[[607-r,402],[604,399],[607,402-r],[610,399],[607+r,402],[610,405],[607,402+r],[604,405]]]});}
33
+ }
34
+ const map=new Map(paths.map(p=>[p.id,p]));return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:map.get(e.socket.id),white:map.get(e.white.id),pupil:map.get(e.pupil.id)}))};
35
+ }
36
+ export function renderDaedalus(rig,scene='idle',ms=0,options={}){
37
+ if(!DAEDALUS_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
38
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
39
+ if(scene==='attach'&&ms>=3000&&!options.still)return renderDaedalus(rig,'idle',ms-3000,options);
40
+ const t=options.still?({idle:4000,attach:1400,starforge:4800}[scene]):ms;
41
+ const q=t%10000;
42
+ const caption=scene==='starforge'?(q<3200?'Draw the pieces together.':q<6000?'Lock the structure. Sweep the light.':q<8000?'Starforge complete.':'Release and reset.'):scene==='attach'?'The workshop opens.':'Imagine. Assemble. Deploy.';
43
+ return {...renderSvgRig(poseRig(rig,scene,t),options),clip:scene,scene,caption};
44
+ }
45
+ export function renderStory(rig,ms,options={}){
46
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
47
+ if(options.still)return renderDaedalus(rig,'starforge',4800,options);
48
+ return ms<3000?renderDaedalus(rig,'attach',ms,options):ms<13000?renderDaedalus(rig,'starforge',ms-3000,options):renderDaedalus(rig,'idle',ms-13000,options);
49
+ }
@@ -0,0 +1,65 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
2
+ <title>Daedalus — midnight architect, formerly Nova</title>
3
+ <g id="wings">
4
+ <path id="far-wing" fill="#344a76" d="M 308 279 L 226 244 L 177 75 L 161 160 L 112 124 L 143 231 L 120 223 L 174 278 L 267 308 Z"/>
5
+ <path id="far-spars" fill="none" stroke="#b5a679" stroke-width="8" d="M 300 277 L 226 244 L 177 75 L 161 160 L 143 231 M 226 244 L 112 124 M 226 244 L 174 278 L 267 308"/>
6
+ <path id="near-wing" fill="#405c91" d="M 345 278 Q 377 242 400 185 L 490 65 L 475 169 L 541 125 L 515 231 L 555 215 L 492 285 L 402 316 L 350 309 Z"/>
7
+ <path id="near-panel-shade" fill="#304571" d="M 359 289 L 475 169 L 400 185 Z M 359 289 L 515 231 L 492 285 L 402 316 Z"/>
8
+ <path id="near-spars" fill="none" stroke="#e7ce95" stroke-width="8" d="M 352 292 L 400 185 L 490 65 L 475 169 L 352 292 M 400 185 L 475 169 L 541 125 L 515 231 L 352 292 M 515 231 L 555 215 L 492 285 L 402 316 L 352 292"/>
9
+ <path id="wing-crossbraces" fill="none" stroke="#9faed4" stroke-width="5" d="M 429 148 L 480 136 M 405 238 L 467 218 M 427 285 L 492 285 M 168 177 L 208 184"/>
10
+ <path id="hinges" fill="#fff0c6" d="M 391 185 L 400 176 L 409 185 L 400 194 Z M 466 169 L 475 160 L 484 169 L 475 178 Z M 506 231 L 515 222 L 524 231 L 515 240 Z"/>
11
+ </g>
12
+ <g id="tail">
13
+ <path id="tail-coil" fill="#405b91" d="M 320 351 C 250 354 236 431 150 429 C 84 431 62 381 95 350 Q 116 334 140 349 Q 100 345 100 377 C 103 414 173 413 208 375 Q 253 321 319 324 Z"/>
14
+ <path id="tail-trace" fill="none" stroke="#dec78c" stroke-width="7" d="M 302 343 C 242 356 227 413 159 417 C 104 421 78 383 104 357"/>
15
+ <path id="tail-tip" fill="#e6d7ae" d="M 130 347 L 160 325 L 151 354 L 164 369 L 136 361 Z"/>
16
+ </g>
17
+ <g id="body">
18
+ <path id="torso" fill="#3e5687" d="M 300 235 Q 362 224 396 271 L 423 350 Q 414 399 369 413 L 288 394 L 263 353 Q 270 284 300 235 Z"/>
19
+ <path id="belly" fill="#c9d3eb" d="M 326 257 Q 358 259 374 288 L 394 347 L 372 390 L 325 377 L 304 331 Z"/>
20
+ <path id="belly-joints" fill="none" stroke="#667ba6" stroke-width="6" d="M 316 296 L 380 305 M 308 328 L 390 338 M 317 357 L 385 368"/>
21
+ <path id="haunch" fill="#4f6b9d" d="M 281 323 Q 317 311 333 342 L 321 384 L 337 419 L 266 429 L 248 410 L 262 362 Z"/>
22
+ <path id="far-foot" fill="#405783" d="M 369 364 L 404 359 L 425 410 L 460 424 L 457 439 L 379 436 L 356 410 Z"/>
23
+ <path id="feet" fill="#6881ac" d="M 266 401 L 306 402 L 339 421 L 334 438 L 252 439 L 240 424 Z"/>
24
+ <path id="talons" fill="#f5e5b6" d="M 256 422 L 247 441 L 265 435 L 279 421 L 276 440 L 296 435 L 310 422 L 309 439 L 333 433 M 392 422 L 382 439 L 402 435 L 418 422 L 416 440 L 437 435 L 449 426 L 448 440 L 465 435"/>
25
+ </g>
26
+ <g id="neck">
27
+ <path id="neck-mass" fill="#4d679a" d="M 260 168 L 317 164 Q 340 200 329 243 L 354 281 L 306 302 L 274 268 Q 291 218 260 168 Z"/>
28
+ <path id="throat" fill="#d7dff0" d="M 288 183 L 310 183 Q 325 216 315 247 L 336 278 L 311 285 L 291 262 Q 309 218 288 183 Z"/>
29
+ </g>
30
+ <g id="head">
31
+ <path id="horn-far" fill="#e6c98d" d="M 248 125 Q 210 92 230 52 Q 224 85 264 98 Z"/>
32
+ <path id="horn-near" fill="#fff0bf" d="M 295 109 Q 276 65 319 33 Q 298 70 322 100 Z"/>
33
+ <path id="head-mass" fill="#6a82af" d="M 241 121 Q 273 92 307 112 Q 335 130 328 165 L 307 193 L 266 193 L 231 166 Z"/>
34
+ <path id="ear-fin" fill="#d7dcf0" d="M 241 136 L 202 119 L 213 149 L 194 157 L 238 175 Z"/>
35
+ <path id="crest" fill="#e8d096" d="M 271 109 L 273 83 L 290 106 L 305 90 L 312 119 Z"/>
36
+ <path id="muzzle" fill="#c2cfea" d="M 299 160 Q 334 148 355 170 L 354 185 L 335 198 L 306 199 L 281 184 Z"/>
37
+ <path id="nose" fill="#080d19" d="M 334 169 L 343 176 L 331 179 Z"/>
38
+ <path id="mouth-opening" fill="#080d19" d="M 299 179 Q 324 187 354 176 L 350 191 Q 324 207 301 190 Z"/>
39
+ <path id="lower-jaw" fill="#91a8ce" d="M 301 191 Q 326 204 350 190 L 340 207 L 316 211 L 296 199 Z"/>
40
+ <path id="fang" fill="#fff0c5" d="M 337 184 L 345 181 L 339 192 Z"/>
41
+ </g>
42
+ <g id="shoulder">
43
+ <path id="wing-shoulder" fill="#647ea9" d="M 342 258 Q 365 244 380 263 L 382 284 L 364 302 L 342 288 Z"/>
44
+ <path id="shoulder-rim" fill="none" stroke="#e2c992" stroke-width="6" d="M 347 261 Q 369 252 376 273 L 364 291"/>
45
+ </g>
46
+ <g id="arms">
47
+ <path id="upper-arm" fill="#617ca8" d="M 326 241 Q 345 220 370 229 L 407 245 L 442 231 L 455 238 L 450 252 L 411 268 L 369 253 L 347 270 Z"/>
48
+ <path id="lower-arm" fill="#7993bd" d="M 343 296 Q 369 281 384 302 L 405 326 L 452 321 L 468 330 L 455 344 L 399 350 L 365 327 Z"/>
49
+ <path id="claw-tips" fill="#fff0c5" d="M 439 232 L 457 216 L 451 237 L 467 230 L 455 246 L 449 253 M 447 322 L 461 309 L 457 326 L 473 320 L 465 336 L 452 343"/>
50
+ <path id="cuffs" fill="none" stroke="#e2c992" stroke-width="7" d="M 402 244 L 411 264 M 401 326 L 405 348"/>
51
+ </g>
52
+ <g id="blueprint">
53
+ <path id="assembly-top" fill="none" stroke="#ead59d" stroke-width="6" d="M 580 306 L 607 289 L 634 306 L 607 323 Z M 580 306 L 580 335 L 607 352 L 634 335 L 634 306 M 607 323 L 607 352"/>
54
+ <path id="component-left" fill="none" stroke="#acbfe9" stroke-width="5" d="M 548 355 L 563 346 L 578 355 L 578 373 L 563 382 L 548 373 Z M 548 355 L 563 364 L 578 355 M 563 364 L 563 382"/>
55
+ <path id="component-right" fill="none" stroke="#acbfe9" stroke-width="5" d="M 644 370 L 659 361 L 674 370 L 674 388 L 659 397 L 644 388 Z M 644 370 L 659 379 L 674 370 M 659 379 L 659 397"/>
56
+ </g>
57
+ <g id="eyes">
58
+ <path id="far-eye-socket" fill="#080d19" d="M 245 147 C 245 134.8497355 253.50658975 125 264 125 C 274.49341025 125 283 134.8497355 283 147 C 283 159.1502645 274.49341025 169 264 169 C 253.50658975 169 245 159.1502645 245 147 Z"/>
59
+ <path id="far-eye-white" fill="#ffffff" d="M 250 147 C 250 137.61115925000001 256.2680135 130 264 130 C 271.7319865 130 278 137.61115925000001 278 147 C 278 156.38884074999999 271.7319865 164 264 164 C 256.2680135 164 250 156.38884074999999 250 147 Z"/>
60
+ <path id="far-pupil" fill="#080d19" d="M 263.5 147 C 263.5 143.13400675 265.514718625 140 268 140 C 270.485281375 140 272.5 143.13400675 272.5 147 C 272.5 150.86599325 270.485281375 154 268 154 C 265.514718625 154 263.5 150.86599325 263.5 147 Z"/>
61
+ <path id="near-eye-socket" fill="#080d19" d="M 279 146 C 279 132.19288125 289.29745075 121 302 121 C 314.70254925 121 325 132.19288125 325 146 C 325 159.80711875 314.70254925 171 302 171 C 289.29745075 171 279 159.80711875 279 146 Z"/>
62
+ <path id="near-eye-white" fill="#ffffff" d="M 284 146 C 284 134.954305 292.0588745 126 302 126 C 311.9411255 126 320 134.954305 320 146 C 320 157.045695 311.9411255 166 302 166 C 292.0588745 166 284 157.045695 284 146 Z"/>
63
+ <path id="near-pupil" fill="#080d19" d="M 301.5 146 C 301.5 142.13400675 303.514718625 139 306 139 C 308.485281375 139 310.5 142.13400675 310.5 146 C 310.5 149.86599325 308.485281375 153 306 153 C 303.514718625 153 301.5 149.86599325 301.5 146 Z"/>
64
+ </g>
65
+ </svg>
@@ -0,0 +1,47 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
2
+ import {poseRig as originalPose,renderDaedalus as originalRender,DAEDALUS_CLIPS as clips} from '../v2/character.mjs';
3
+ export const DAEDALUS_CLIPS=Object.freeze([...clips,{id:'repair',title:'Isolate. Replace. Resume.',duration:12000}]);
4
+ export const STORY_DURATION=15200;
5
+ const ease=x=>{x=Math.max(0,Math.min(1,x));return x*x*(3-2*x);};
6
+ export function poseRig(rig,ms){
7
+ if(ms>=12000)return originalPose(rig,'idle',ms-12000);
8
+ const t=ms;
9
+ if(t>=10000)return originalPose(rig,'starforge',8000+t-10000);
10
+ const base=originalPose(rig,'starforge',3200);
11
+ const out=ease((t-2000)/1800),insert=ease((t-4400)/1800);
12
+ const paths=base.paths.filter(p=>p.group!=='effect').map(p=>({...p}));
13
+ const slot=paths.find(p=>p.id==='component-right');
14
+ const originalPolygons=slot.polygons;
15
+ if(t>=1500){
16
+ slot.id='quarantined-component';slot.rgb=[230,80,60];
17
+ slot.polygons=originalPolygons.map(poly=>poly.map(([x,y])=>[x+34*out,y-66*out]));
18
+ if(t>=4400){
19
+ paths.push({...slot,id:'replacement-component',rgb:insert<1?[150,198,255]:[255,236,176],polygons:originalPolygons.map(poly=>poly.map(([x,y])=>[x+34*(1-insert),y+27*(1-insert)]))});
20
+ }
21
+ if(t>=9000){slot.rgb=slot.rgb.map(c=>Math.round(c*(1-ease((t-9000)/1000))));}
22
+ }
23
+ const guide=t>=2000&&t<6500?Math.sin(Math.PI*(t-2000)/4500)**2:0;
24
+ for(const p of paths)if(p.group==='arms')p.polygons=p.polygons.map(poly=>poly.map(([x,y])=>{const w=Math.max(0,Math.min(1,(x-350)/110));return [x+10*guide*w,y-10*guide*w];}));
25
+ const add=(id,rgb,polygons,stroke)=>paths.push({id,group:'effect',erase:false,rgb,polygons,stroke});
26
+ // The deployment wave holds at the failed lower section until replacement seats.
27
+ const y=t<1500?292+68*ease(t/1500):t<6500?360:360+28*ease((t-6500)/1800);
28
+ if(t<8500)add('deployment-wave',t>=1500&&t<6500?[255,172,66]:[255,246,207],[[[577,y],[638,y]]],4);
29
+ add('assembly-links',[220,195,139],[[[592,351],[592,344],[607,344]],...(t>=2000&&t<6200?[]:[[[607,344],[622,344],[622,351]]])],4);
30
+ if(t>=1500&&t<9000){const x=622+34*out,cy=369-66*out;add('fault-cross',[255,100,70],[[[x-5,cy-5],[x+5,cy+5]],[[x+5,cy-5],[x-5,cy+5]]],3);}
31
+ if(t>=8500){const r=5+6*Math.sin(Math.PI*(t-8500)/1500)**2;add('recovery-star',[255,239,180],[[[607-r,409],[607,409-r],[607+r,409],[607,409+r]]],0);}
32
+ return {...base,paths};
33
+ }
34
+ export function renderDaedalus(rig,scene='idle',ms=0,options={}){
35
+ if(scene!=='repair')return originalRender(rig,scene,ms,options);
36
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
37
+ if(ms>=12000&&!options.still)return originalRender(rig,'idle',ms-12000,options);
38
+ const t=options.still?5000:ms;
39
+ const caption=t<1500?'Deployment begins.':t<2000?'Fault found. Hold the wave.':t<4400?'Isolate the failed component.':t<6500?'Seat the replacement.':t<8500?'Resume from the held step.':t<10000?'Repair complete.':'Release and reset.';
40
+ return {...renderSvgRig(poseRig(rig,t),options),scene,clip:scene,caption};
41
+ }
42
+ export function renderStory(rig,ms,options={}){
43
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
44
+ if(options.still)return renderDaedalus(rig,'repair',5000,options);
45
+ if(ms<3200)return renderDaedalus(rig,'starforge',ms,options);
46
+ return renderDaedalus(rig,'repair',ms-3200,options);
47
+ }
@@ -0,0 +1,66 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
2
+ import {poseRig as forgePose} from '../v2/character.mjs';
3
+ import {renderDaedalus as previousRender,DAEDALUS_CLIPS as clips} from '../v4/character.mjs';
4
+ export const DAEDALUS_CLIPS=Object.freeze([...clips,{id:'rollback',title:'Rewind to the checkpoint',duration:12000}]);
5
+ export const STORY_DURATION=15200;
6
+ const ease=x=>{x=Math.max(0,Math.min(1,x));return x*x*(3-2*x);};
7
+ const gold=[255,236,176],blue=[139,193,255],red=[248,100,76];
8
+ const mix=(a,b,u)=>a.map((c,i)=>Math.round(c+(b[i]-c)*u));
9
+ const components=new Set(['assembly-top','component-left','component-right']);
10
+
11
+ export function poseRollback(rig,t){
12
+ if(t>=12000)return forgePose(rig,'idle',t-12000);
13
+ if(t>=10000)return forgePose(rig,'starforge',t-2000);
14
+ // Reverse authored construction, hold its checkpoint, then rebuild.
15
+ const clock=t<1800?3200:t<4800?3200-2200*ease((t-1800)/3000):t<6200?1000:t<9000?1000+2200*ease((t-6200)/2800):3200+4800*ease((t-9000)/1000);
16
+ const base=forgePose(rig,'starforge',clock);
17
+ const paths=base.paths.filter(p=>p.group!=='effect').map(p=>({...p}));
18
+ const ready=ease((t-8800)/400)*(1-ease((t-9600)/400));
19
+ for(const p of paths)if(components.has(p.id)){
20
+ const original=rig.paths.find(q=>q.id===p.id).rgb;
21
+ const color=t<1800?mix(original,red,ease(t/700)):t<4800?red:t<6200?mix(red,blue,ease((t-4800)/800)):mix(blue,gold,ease((t-6200)/2800));
22
+ p.rgb=t>9600?mix(color,original,ease((t-9600)/400)):color;
23
+ p.stroke+=2*ready;
24
+ }
25
+ const add=(id,rgb,polygons,stroke=3)=>paths.push({id,group:'effect',erase:false,rgb,polygons,stroke});
26
+ if(t>=1800&&t<6200){
27
+ const growth=ease((t-1800)/500)*(1-ease((t-5700)/500));
28
+ const theta=-(t-1800)/4400*Math.PI*4;
29
+ for(let j=0;j<2;j++){
30
+ const a=theta+j*Math.PI,points=[];
31
+ for(let i=0;i<=20;i++){const q=a+i/20*1.8;points.push([607+59*growth*Math.cos(q),350+66*growth*Math.sin(q)]);}
32
+ add('rewind-arc-'+j,gold,[points],3);
33
+ const [x,y]=points[0],dx=Math.sin(a),dy=-Math.cos(a);
34
+ add('rewind-arrow-'+j,gold,[[[x+7*dx,y+7*dy],[x-5*dx-5*dy,y-5*dy+5*dx],[x-5*dx+5*dy,y-5*dy-5*dx]]],0);
35
+ }
36
+ const y=388-96*ease((t-1800)/3000);
37
+ add('reverse-wave',gold,[[[551,y],[668,y]]],4);
38
+ }
39
+ if(t>=4800&&t<9000){
40
+ const glow=ease((t-4800)/600)*(1-ease((t-8500)/500));
41
+ if(glow>.01)add('checkpoint',[120,180,245],[[[548,402],[548,414],[667,414],[667,402]]],3*glow);
42
+ }
43
+ if(t>=6200&&t<9000){
44
+ const y=292+96*ease((t-6200)/2800);
45
+ add('verify-wave',gold,[[[551,y],[668,y]]],4);
46
+ }
47
+ if(t>=9000&&t<10000){
48
+ const s=Math.sin(Math.PI*(t-9000)/1000);
49
+ if(s>.01)add('checkpoint-confirmed',gold,[[[592,401],[602,411],[624,389]]],5*s);
50
+ }
51
+ return {...base,paths};
52
+ }
53
+
54
+ export function renderDaedalus(rig,scene='idle',ms=0,options={}){
55
+ if(scene!=='rollback')return previousRender(rig,scene,ms,options);
56
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
57
+ if(ms>=12000&&!options.still)return previousRender(rig,'idle',ms-12000,options);
58
+ const t=options.still?3500:ms;
59
+ const caption=t<1800?'Bad release. Stop deployment.':t<4800?'Rewind the assembly.':t<6200?'Recover the saved blueprint.':t<9000?'Rebuild. Verify each layer.':t<10000?'Checkpoint restored.':'Release and reset.';
60
+ return {...renderSvgRig(poseRollback(rig,t),options),scene,clip:scene,caption};
61
+ }
62
+ export function renderStory(rig,ms,options={}){
63
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
64
+ if(options.still)return renderDaedalus(rig,'rollback',3500,options);
65
+ return ms<3200?renderDaedalus(rig,'starforge',ms,options):renderDaedalus(rig,'rollback',ms-3200,options);
66
+ }
@@ -0,0 +1,32 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
2
+ import {poseRig as forgePose} from '../v2/character.mjs';
3
+ import {renderDaedalus as previousRender,DAEDALUS_CLIPS as clips} from '../v5/character.mjs';
4
+
5
+ /**
6
+ * Daedalus V6: companion power. Anatomy is the approved V3 SVG, untouched; every V2-V5 clip is unchanged.
7
+ * Adds 'assemble', the V2 Starforge choreography without its completion star: the pieces gather, link and
8
+ * are swept by light, then release. A companion plays it while the host reports work in progress, so it must
9
+ * not announce a finished deployment. Pure function of elapsed time; the host supplies every fact.
10
+ */
11
+ export const ASSEMBLE_MS=10000;
12
+ export const DAEDALUS_CLIPS=Object.freeze([...clips,{id:'assemble',title:'Starforge, no verdict',duration:ASSEMBLE_MS}]);
13
+ export const STORY_DURATION=3000+ASSEMBLE_MS;
14
+ export function poseAssemble(rig,t){
15
+ if(t>=ASSEMBLE_MS)return forgePose(rig,'idle',t-ASSEMBLE_MS);
16
+ const base=forgePose(rig,'starforge',t);
17
+ return {...base,paths:base.paths.filter(p=>p.id!=='completion-star')};
18
+ }
19
+ export function renderDaedalus(rig,scene='idle',ms=0,options={}){
20
+ if(scene!=='assemble')return previousRender(rig,scene,ms,options);
21
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
22
+ if(ms>=ASSEMBLE_MS&&!options.still)return previousRender(rig,'idle',ms-ASSEMBLE_MS,options);
23
+ // Still freezes every channel (gesture, components, links, wave) at the Starforge still pose.
24
+ const t=options.still?4800:ms;
25
+ const caption=t<3200?'Draw the pieces together.':t<8000?'Lock the structure. Sweep the light.':'Release and reset.';
26
+ return {...renderSvgRig(poseAssemble(rig,t),options),scene,clip:scene,caption};
27
+ }
28
+ export function renderStory(rig,ms,options={}){
29
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
30
+ if(options.still)return renderDaedalus(rig,'assemble',4800,options);
31
+ return ms<3000?renderDaedalus(rig,'attach',ms,options):renderDaedalus(rig,'assemble',ms-3000,options);
32
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import {parseSvgRig} from '../../../factory/svg-rig.mjs';
4
+ import {renderDaedalus,DAEDALUS_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('Daedalus V6 forge studio (companion Starforge). --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(DAEDALUS_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','DAEDALUS\nImagine. Assemble. Deploy.');
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/daedalus-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=renderDaedalus(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:'DAEDALUS\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 {DAEDALUS_CLIPS,renderDaedalus,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?DAEDALUS_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?renderDaedalus(paths,scene,t,options):renderStory(paths,t,options);
16
+ if(!r?.ok)throw new Error(r?.code??'Invalid story frame');
17
+ const caption=message??'DAEDALUS · '+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,70 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 480">
2
+ <title>Janus — two-headed keeper of the threshold</title>
3
+ <g id="wings">
4
+ <path id="left-cloak" fill="#39465c" d="M 326 284 Q 281 179 166 139 Q 191 219 143 289 Q 200 274 229 329 Q 270 304 306 351 Z"/>
5
+ <path id="right-cloak" fill="#474453" d="M 424 284 Q 469 179 584 139 Q 559 219 607 289 Q 550 274 521 329 Q 480 304 444 351 Z"/>
6
+ <path id="cloak-ribs" fill="none" stroke="#a68b61" stroke-width="8" d="M 326 284 Q 281 179 166 139 M 326 284 Q 237 213 143 289 M 326 284 L 229 329 M 424 284 Q 469 179 584 139 M 424 284 Q 513 213 607 289 M 424 284 L 521 329"/>
7
+ </g>
8
+ <g id="tail">
9
+ <path id="short-tail" fill="#43566a" d="M 323 353 C 251 355 219 413 158 399 Q 121 391 118 363 Q 95 401 145 422 C 221 452 267 394 331 386 Z"/>
10
+ <path id="tail-edge" fill="none" stroke="#719ea7" stroke-width="6" d="M 309 372 Q 224 427 162 411"/>
11
+ </g>
12
+ <g id="body">
13
+ <path id="broad-torso" fill="#465369" d="M 320 235 Q 375 218 430 235 L 467 330 Q 470 393 422 412 L 328 412 Q 280 393 283 330 Z"/>
14
+ <path id="breastplate" fill="#b29161" d="M 322 275 L 375 253 L 428 275 L 432 347 L 409 388 L 375 402 L 341 388 L 318 347 Z"/>
15
+ <path id="plate-inner" fill="#4c5060" d="M 334 285 L 375 269 L 416 285 L 419 340 L 399 378 L 375 388 L 351 378 L 331 340 Z"/>
16
+ <path id="plate-lines" fill="none" stroke="#927d60" stroke-width="6" d="M 334 307 L 375 320 L 416 307 M 331 338 L 375 351 L 419 338 M 344 366 L 375 377 L 406 366"/>
17
+ <path id="left-haunch" fill="#394356" d="M 299 332 Q 335 322 345 361 L 330 400 L 337 428 L 266 432 L 255 410 L 274 385 Z"/>
18
+ <path id="right-haunch" fill="#494958" d="M 451 332 Q 415 322 405 361 L 420 400 L 413 428 L 484 432 L 495 410 L 476 385 Z"/>
19
+ <path id="feet" fill="#667080" d="M 270 405 L 314 406 L 345 427 L 338 443 L 249 442 L 245 426 Z M 480 405 L 436 406 L 405 427 L 412 443 L 501 442 L 505 426 Z"/>
20
+ <path id="talons" fill="#e0e6e5" d="M 260 430 L 251 445 L 271 440 L 284 427 L 281 445 L 302 439 L 314 429 L 313 444 L 335 439 M 490 430 L 499 445 L 479 440 L 466 427 L 469 445 L 448 439 L 436 429 L 437 444 L 415 439"/>
21
+ </g>
22
+ <g id="necks">
23
+ <path id="left-neck" fill="#4b6576" d="M 263 184 L 306 166 Q 331 206 346 232 L 366 266 L 327 291 Q 307 248 283 228 Z"/>
24
+ <path id="right-neck" fill="#685e62" d="M 487 184 L 444 166 Q 419 206 404 232 L 384 266 L 423 291 Q 443 248 467 228 Z"/>
25
+ <path id="cyan-throat" fill="#64b4bb" d="M 279 193 L 296 190 Q 320 221 326 246 L 347 271 L 331 280 Q 311 243 294 225 Z"/>
26
+ <path id="amber-throat" fill="#d2a267" d="M 471 193 L 454 190 Q 430 221 424 246 L 403 271 L 419 280 Q 439 243 456 225 Z"/>
27
+ </g>
28
+ <g id="left-head">
29
+ <path id="left-skull" fill="#77828c" d="M 235 137 Q 267 115 294 137 Q 317 157 308 185 L 282 209 L 247 203 L 224 178 Z"/>
30
+ <path id="left-ear" fill="#536575" d="M 295 143 L 327 132 L 316 156 L 331 165 L 304 179 Z"/>
31
+ <path id="left-muzzle" fill="#a8b4bc" d="M 246 169 Q 211 158 190 179 L 191 195 L 236 211 L 260 193 Z"/>
32
+ <path id="left-mouth" fill="#080d19" d="M 192 190 Q 216 202 248 195 L 236 207 Q 209 210 193 199 Z"/>
33
+ <path id="left-jaw" fill="#6f7f8a" d="M 198 202 Q 218 217 243 205 L 252 218 Q 216 228 198 211 Z"/>
34
+ <path id="left-nostril" fill="#080d19" d="M 199 180 Q 209 175 210 186 L 200 188 Z"/>
35
+ <path id="left-outer-curl" fill="#eef2f2" d="M 243 147 C 206 137 202 91 228 72 C 255 53 284 75 275 99 Q 268 118 249 107 Q 268 108 264 91 C 258 73 235 79 231 96 Q 221 121 256 133 Z"/>
36
+ <path id="left-inner-curl" fill="#cbd5dd" d="M 285 140 C 302 122 314 98 299 84 Q 283 73 278 88 Q 275 98 286 100 Q 270 106 266 91 C 260 65 292 56 311 75 Q 339 108 303 150 Z"/>
37
+ <path id="left-horn-tip" fill="#526071" d="M 249 107 Q 268 118 275 99 L 264 95 Q 262 107 249 107 Z"/>
38
+ <path id="left-beard" fill="#e0e7e7" d="M 226 212 Q 242 222 258 211 L 271 232 L 257 228 L 259 261 L 244 244 L 235 258 L 230 235 L 220 241 Z"/>
39
+ </g>
40
+ <g id="right-head">
41
+ <path id="right-skull" fill="#77828c" d="M 515.0 137 Q 483.0 115 456.0 137 Q 433.0 157 442.0 185 L 468.0 209 L 503.0 203 L 526.0 178 Z"/>
42
+ <path id="right-ear" fill="#536575" d="M 455.0 143 L 423.0 132 L 434.0 156 L 419.0 165 L 446.0 179 Z"/>
43
+ <path id="right-muzzle" fill="#a8b4bc" d="M 504.0 169 Q 539.0 158 560.0 179 L 559.0 195 L 514.0 211 L 490.0 193 Z"/>
44
+ <path id="right-mouth" fill="#080d19" d="M 558.0 190 Q 534.0 202 502.0 195 L 514.0 207 Q 541.0 210 557.0 199 Z"/>
45
+ <path id="right-jaw" fill="#6f7f8a" d="M 552.0 202 Q 532.0 217 507.0 205 L 498.0 218 Q 534.0 228 552.0 211 Z"/>
46
+ <path id="right-nostril" fill="#080d19" d="M 551.0 180 Q 541.0 175 540.0 186 L 550.0 188 Z"/>
47
+ <path id="right-outer-curl" fill="#eef2f2" d="M 507.0 147 C 544.0 137 548.0 91 522.0 72 C 495.0 53 466.0 75 475.0 99 Q 482.0 118 501.0 107 Q 482.0 108 486.0 91 C 492.0 73 515.0 79 519.0 96 Q 529.0 121 494.0 133 Z"/>
48
+ <path id="right-inner-curl" fill="#cbd5dd" d="M 465.0 140 C 448.0 122 436.0 98 451.0 84 Q 467.0 73 472.0 88 Q 475.0 98 464.0 100 Q 480.0 106 484.0 91 C 490.0 65 458.0 56 439.0 75 Q 411.0 108 447.0 150 Z"/>
49
+ <path id="right-horn-tip" fill="#526071" d="M 501.0 107 Q 482.0 118 475.0 99 L 486.0 95 Q 488.0 107 501.0 107 Z"/>
50
+ <path id="right-beard" fill="#e0e7e7" d="M 524.0 212 Q 508.0 222 492.0 211 L 479.0 232 L 493.0 228 L 491.0 261 L 506.0 244 L 515.0 258 L 520.0 235 L 530.0 241 Z"/>
51
+ </g>
52
+ <g id="arms">
53
+ <path id="left-arm" fill="#61778a" d="M 313 278 Q 338 274 346 299 L 340 320 L 362 333 L 354 354 L 318 337 L 301 307 Z"/>
54
+ <path id="right-arm" fill="#79717a" d="M 437 278 Q 412 274 404 299 L 410 320 L 388 333 L 396 354 L 432 337 L 449 307 Z"/>
55
+ <path id="claws" fill="#dce6e8" d="M 348 326 L 366 320 L 362 333 L 371 336 L 357 345 L 361 353 L 345 354 Z M 402 326 L 384 320 L 388 333 L 379 336 L 393 345 L 389 353 L 405 354 Z"/>
56
+ </g>
57
+ <g id="threshold">
58
+ <path id="closed-seam-dark" fill="none" stroke="#080d19" stroke-width="18" d="M 375 294 L 375 375"/>
59
+ <path id="closed-seam-cyan" fill="none" stroke="#6fe9eb" stroke-width="4" d="M 369 294 L 369 375"/>
60
+ <path id="closed-seam-amber" fill="none" stroke="#f6bc6a" stroke-width="4" d="M 381 294 L 381 375"/>
61
+ </g>
62
+ <g id="eyes">
63
+ <path id="left-eye-socket" fill="#080d19" d="M 243 164 C 243 150.19288125 253.745166 139 267 139 C 280.254834 139 291 150.19288125 291 164 C 291 177.80711875 280.254834 189 267 189 C 253.745166 189 243 177.80711875 243 164 Z"/>
64
+ <path id="left-eye-white" fill="#ffffff" d="M 250 164 C 250 154.0588745 257.61115925 146 267 146 C 276.38884075 146 284 154.0588745 284 164 C 284 173.9411255 276.38884075 182 267 182 C 257.61115925 182 250 173.9411255 250 164 Z"/>
65
+ <path id="left-pupil" fill="#080d19" d="M 257 165 C 257 161.13400675 259.23857625 158 262 158 C 264.76142375 158 267 161.13400675 267 165 C 267 168.86599325 264.76142375 172 262 172 C 259.23857625 172 257 168.86599325 257 165 Z"/>
66
+ <path id="right-eye-socket" fill="#080d19" d="M 459 164 C 459 150.19288125 469.745166 139 483 139 C 496.254834 139 507 150.19288125 507 164 C 507 177.80711875 496.254834 189 483 189 C 469.745166 189 459 177.80711875 459 164 Z"/>
67
+ <path id="right-eye-white" fill="#ffffff" d="M 466 164 C 466 154.0588745 473.61115925 146 483 146 C 492.38884075 146 500 154.0588745 500 164 C 500 173.9411255 492.38884075 182 483 182 C 473.61115925 182 466 173.9411255 466 164 Z"/>
68
+ <path id="right-pupil" fill="#080d19" d="M 483 165 C 483 161.13400675 485.23857625 158 488 158 C 490.76142375 158 493 161.13400675 493 165 C 493 168.86599325 490.76142375 172 488 172 C 485.23857625 172 483 168.86599325 483 165 Z"/>
69
+ </g>
70
+ </svg>
@@ -0,0 +1,56 @@
1
+ import {renderSvgRig} from '../../../factory/svg-rig-contour.mjs';
2
+ export const JANUS_CLIPS=Object.freeze([{id:'idle',title:'Watch both sides',duration:12000},{id:'attach',title:'Two guardians acknowledge',duration:3000},{id:'threshold',title:'Open the threshold',duration:12000}]);
3
+ export const STORY_DURATION=15000;
4
+ const ease=x=>{x=Math.max(0,Math.min(1,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 function poseRig(rig,scene,ms,{deck=false}={}){
7
+ if(scene==='threshold'&&ms>=12000)return poseRig(rig,'idle',ms-12000,{deck});
8
+ if(scene==='attach'&&ms>=3000)return poseRig(rig,'idle',ms-3000,{deck});
9
+ const t=ms%12000,active=scene==='threshold';
10
+ const left=active?pulse(t,0,11500):scene==='attach'?pulse(t,0,2300):pulse(t,1000,5000);
11
+ const right=active?pulse(t,700,11500):scene==='attach'?pulse(t,600,3000):pulse(t,6500,10500);
12
+ const opening=active?ease((t-3000)/2200)*(1-ease((t-8500)/2500)):0;
13
+ const move=(x,y,side,w=1)=>{const v=side==='left'?left:right,dir=side==='left'?1:-1;return [x+dir*(active?8:-3)*v*w,y+(active?6:3)*v*w];};
14
+ const paths=rig.paths.filter(p=>p.group!=='threshold').map(p=>({...p,polygons:p.polygons.map(poly=>poly.map(([x,y])=>{
15
+ if(p.group==='left-head'||p.group==='right-head')return move(x,y,p.group.split('-')[0]);
16
+ if(p.group==='eyes')return move(x,y,p.id.split('-')[0]);
17
+ if(p.group==='necks')return move(x,y,x<375?'left':'right',Math.max(0,Math.min(1,(280-y)/85)));
18
+ if(p.group==='arms'){const w=Math.max(0,Math.min(1,(y-285)/45));return [x+(x<375?-1:1)*30*opening*w,y];}
19
+ return [x,y];
20
+ }))}));
21
+ const add=(id,rgb,polygons,stroke=3,erase=false)=>paths.push({id,group:'effect',rgb,polygons,stroke,erase});
22
+ const half=6+40*opening,top=294-14*opening,bottom=375+14*opening;
23
+ add('gateway-void',[8,13,25],[[[375-half,top],[375+half,top],[375+half,bottom],[375-half,bottom]]],0,true);
24
+ add('cyan-jamb',[111,233,235],[[[375-half,top],[375-half,bottom]]],4);
25
+ add('amber-jamb',[246,188,106],[[[375+half,top],[375+half,bottom]]],4);
26
+ if(opening>.01){
27
+ add('gate-lintel',[147,191,209],[[[375-half,top],[375,top-8*opening],[375+half,top]],[[375-half,bottom],[375,bottom+8*opening],[375+half,bottom]]],3);
28
+ if(deck)for(let i=1;i<=3;i++){
29
+ const inset=i*7*opening,h=half-inset;
30
+ add('passage-depth-'+i,[32+i*11,71+i*9,95+i*11],[[[375-h,top+inset],[375+h,top+inset],[375+h,bottom-inset],[375-h,bottom-inset],[375-h,top+inset]]],2);
31
+ }
32
+ }
33
+ if(active&&t>800&&t<8300){
34
+ const approach=ease((t-800)/1400),transit=ease((t-5500)/2200);
35
+ const x=t<5500?275+40*approach:315+120*transit;
36
+ const size=5*ease((t-800)/300)*(1-ease((t-7800)/500));
37
+ const c=t<5500?[243,194,109]:[195,255,245];
38
+ add('traveller',c,[[[x-size,335],[x,335-size],[x+size,335],[x,335+size]]],0);
39
+ }
40
+ const map=new Map(paths.map(p=>[p.id,p]));
41
+ return {...rig,paths,eyes:rig.eyes.map(e=>({...e,socket:map.get(e.socket.id),white:map.get(e.white.id),pupil:map.get(e.pupil.id)}))};
42
+ }
43
+ export function renderJanus(rig,scene='idle',ms=0,options={}){
44
+ if(!JANUS_CLIPS.some(c=>c.id===scene))return {ok:false,code:'UNKNOWN_CLIP'};
45
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
46
+ if(!options.still&&scene==='threshold'&&ms>=12000)return renderJanus(rig,'idle',ms-12000,options);
47
+ if(!options.still&&scene==='attach'&&ms>=3000)return renderJanus(rig,'idle',ms-3000,options);
48
+ const t=options.still?({idle:0,attach:1500,threshold:6500}[scene]):ms;
49
+ const caption=scene==='threshold'?(t<2200?'An arrival at the gate.':t<3000?'Wait at the threshold.':t<5500?'Both guardians attend. Open the way.':t<8300?'Pass through the gateway.':'Close the passage. Resume watch.'):scene==='attach'?'Both sides acknowledged.':'One watches each side.';
50
+ return {...renderSvgRig(poseRig(rig,scene,t,options),options),scene,clip:scene,caption};
51
+ }
52
+ export function renderStory(rig,ms,options={}){
53
+ if(!Number.isFinite(ms)||ms<0)return {ok:false,code:'INVALID_TIME'};
54
+ if(options.still)return renderJanus(rig,'threshold',6500,options);
55
+ return ms<3000?renderJanus(rig,'attach',ms,options):renderJanus(rig,'threshold',ms-3000,options);
56
+ }
@@ -0,0 +1,7 @@
1
+ import {poseRig as pose,renderJanus as render,renderStory as story} from '../v2/character.mjs';
2
+ export {JANUS_CLIPS,STORY_DURATION} from '../v2/character.mjs';
3
+ // Preserve V2 camera framing while lowering and separating the horns.
4
+ const framed=rig=>({...rig,bounds:{left:110.7777777777778,top:64.82150205761317,right:607,bottom:445}});
5
+ export const poseRig=(rig,...args)=>pose(framed(rig),...args);
6
+ export const renderJanus=(rig,...args)=>render(framed(rig),...args);
7
+ export const renderStory=(rig,...args)=>story(framed(rig),...args);