@jossuealcala/madre 1.0.2 → 1.0.4
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.en.md +76 -1
- package/CHANGELOG.md +76 -1
- package/README.en.md +11 -15
- package/README.md +3 -3
- package/bin/madre.cjs +40 -0
- package/bin/madre.mjs +5 -0
- package/docs/REFERENCE.md +29 -3
- package/docs/SDK.md +4 -2
- package/docs/en/REFERENCE.md +29 -3
- package/docs/en/SDK.md +4 -2
- package/docs/training/README.md +28 -1
- package/docs/training/train.sh +5 -1
- package/package.json +3 -3
- package/public/app.js +1635 -159
- package/public/attribution.js +44 -1
- package/public/brands.js +20 -5
- package/public/code-tools.js +143 -0
- package/public/editor.js +20 -4
- package/public/es.js +395 -26
- package/public/hardware.js +50 -0
- package/public/index.html +5 -1
- package/public/macroverso.js +187 -96
- package/public/market-art.js +161 -0
- package/public/market-view.js +529 -0
- package/public/market.js +189 -0
- package/public/neural.js +110 -0
- package/public/nostromo-engine.js +412 -47
- package/public/power.js +100 -0
- package/public/resay.js +5 -0
- package/public/salas-mac.css +105 -14
- package/public/salas-mac.html +9 -1
- package/public/salas-mac.js +350 -31
- package/public/styles.css +844 -61
- package/public/tour.js +48 -6
- package/public/troubleshooting.js +6 -1
- package/public/wallpaper.js +275 -0
- package/src/adapters/cursor.mjs +96 -0
- package/src/adapters/gemini.mjs +10 -3
- package/src/adapters/madre.mjs +2 -1
- package/src/auth-probe.mjs +143 -12
- package/src/bridge/policy.mjs +1 -1
- package/src/capabilities.mjs +9 -0
- package/src/checkpoint.mjs +16 -1
- package/src/credentials.mjs +3 -0
- package/src/dataset.mjs +76 -4
- package/src/enlaces.mjs +208 -2
- package/src/extensions.mjs +11 -3
- package/src/i18n.mjs +16 -0
- package/src/looks.mjs +125 -2
- package/src/market-key.mjs +8 -0
- package/src/market-remote.mjs +239 -0
- package/src/models.mjs +4 -0
- package/src/module-scope.mjs +68 -0
- package/src/modules/ahp.mjs +102 -7
- package/src/modules/bishop-exam.mjs +122 -0
- package/src/modules/bishop.mjs +71 -12
- package/src/modules/enlaces.mjs +87 -6
- package/src/modules/image-studio.mjs +10 -1
- package/src/modules/intents.mjs +3 -1
- package/src/modules/local-models.mjs +124 -0
- package/src/modules/ollama.mjs +12 -3
- package/src/modules/playwright.mjs +73 -15
- package/src/modules/sdk.mjs +61 -9
- package/src/npm-launch.mjs +18 -0
- package/src/ollama.mjs +6 -5
- package/src/outbound.mjs +9 -1
- package/src/ports.mjs +8 -8
- package/src/room/control.mjs +1 -1
- package/src/room/prompt.mjs +13 -2
- package/src/room.mjs +14 -4
- package/src/router.mjs +1 -1
- package/src/runs.mjs +4 -1
- package/src/runtime-detection.mjs +9 -0
- package/src/salas.mjs +26 -0
- package/src/server-sentinel.mjs +7 -3
- package/src/server.mjs +617 -48
- package/src/setup.mjs +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
3
|
+
// «Your computer» (2026-10-16), the same block in a project (Modules and Settings › Local AI) and in
|
|
4
|
+
// Home (Settings › Local AI). Each page hands in its own row builders, so it reads like the rest.
|
|
5
|
+
import { t } from './i18n.js';
|
|
6
|
+
|
|
7
|
+
// What this computer can do with a local AI, measured on it (src/modules/local-models.mjs): which
|
|
8
|
+
// sizes run and train here, the one to use every day, and which features are worth turning on.
|
|
9
|
+
const LOCAL_FEATURE_NAMES = { search: 'Search by meaning', archivist: 'Archivist', agent: '@madre in the project', coordinator: 'The Coordinator with local AI', train: 'Train your own AI' };
|
|
10
|
+
export function hardwareLine(hw) {
|
|
11
|
+
if (!hw) return t('Not measured.');
|
|
12
|
+
if (hw.accelerator === 'apple') return t('{chip} · {gb} GB of shared memory', { chip: hw.chip ?? t('Apple chip'), gb: hw.memoryGb });
|
|
13
|
+
if (hw.accelerator === 'nvidia') return t('{gpu} · {vram} GB of video memory · {gb} GB of memory', { gpu: hw.gpus.map((gpu) => gpu.name).join(' + '), vram: hw.acceleratorGb, gb: hw.memoryGb });
|
|
14
|
+
return t('No compatible graphics card found · {gb} GB of memory', { gb: hw.memoryGb });
|
|
15
|
+
}
|
|
16
|
+
export function drawHardware(host, fit, { el, sxBlock, sxRow, sxChip }) {
|
|
17
|
+
const block = sxBlock(host, t('Your computer'), t('What it can do with a local AI, measured on it. Each model has a minimum to run it and another to train it.'));
|
|
18
|
+
sxRow(block, t('Hardware'), hardwareLine(fit?.hardware));
|
|
19
|
+
if (!fit?.models) return block;
|
|
20
|
+
const daily = fit.recommended?.run;
|
|
21
|
+
sxRow(block, t('Model for every day'), daily ? t('{model} · Ollama downloads it as {tag}', { model: daily.label, tag: daily.ollama }) : t('None runs well here: the project keeps using its providers.'));
|
|
22
|
+
for (const feature of fit.features ?? []) {
|
|
23
|
+
const tier = fit.models.find((model) => model.id === feature.needs);
|
|
24
|
+
const hint = feature.ok
|
|
25
|
+
? (feature.slow ? t('Yes, slowly: it runs on the processor.') : null)
|
|
26
|
+
: feature.id === 'train'
|
|
27
|
+
? (fit.accelerated ? t('Training starts at {gb} GB.', { gb: tier.trainGb }) : t('Training needs an Apple chip or an NVIDIA card.'))
|
|
28
|
+
: t('Needs at least {model}, from {gb} GB.', { model: tier.label, gb: tier.runGb });
|
|
29
|
+
sxRow(block, t(LOCAL_FEATURE_NAMES[feature.id]), hint, sxChip(feature.ok ? (feature.slow ? t('Slow') : t('Yes')) : t('No'), feature.ok ? (feature.slow ? 'warn' : 'on') : ''));
|
|
30
|
+
}
|
|
31
|
+
const more = el('details', 'sx-more');
|
|
32
|
+
more.append(el('summary', null, t('Every size, to run and to train')));
|
|
33
|
+
const wrap = el('div', 'sx-table');
|
|
34
|
+
const table = el('table', 'sx-models');
|
|
35
|
+
const head = el('tr');
|
|
36
|
+
for (const text of [t('Model'), t('To run'), t('To train'), t('Here')]) head.append(el('th', null, text));
|
|
37
|
+
table.append(head);
|
|
38
|
+
const verdict = (value) => (value === 'yes' ? t('runs') : value === 'slow' ? t('runs slowly') : null);
|
|
39
|
+
for (const model of fit.models) {
|
|
40
|
+
const row = el('tr', model.id === daily?.id ? 'pick' : null);
|
|
41
|
+
const here = [verdict(model.run), model.train === 'yes' ? t('trains') : null].filter(Boolean).join(' · ') || t('does not fit');
|
|
42
|
+
row.append(el('td', null, model.label), el('td', null, t('{gb} GB', { gb: model.runGb })), el('td', null, t('{gb} GB', { gb: model.trainGb })), el('td', model.run === 'no' ? 'no' : null, here));
|
|
43
|
+
table.append(row);
|
|
44
|
+
}
|
|
45
|
+
wrap.append(table);
|
|
46
|
+
more.append(wrap, el('p', 'sx-hint', t('Minimums for the whole computer, with the system and the browser open: a 4-bit model in Ollama to run it, a LoRA with mlx-lm to train it. On a Mac with an Apple chip the memory is shared; with an NVIDIA card what counts is the card\'s own.')));
|
|
47
|
+
block.append(more);
|
|
48
|
+
return block;
|
|
49
|
+
}
|
|
50
|
+
|
package/public/index.html
CHANGED
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
<button id="stop-all" class="stop-all" type="button" title="STOP ALL · halt every plan and every agent turn" hidden>
|
|
44
44
|
<svg class="bar-glyph" viewBox="0 0 20 20" aria-hidden="true"><rect class="ic-f" x="4" y="4" width="12" height="12" rx="3.4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>STOP ALL
|
|
45
45
|
</button>
|
|
46
|
+
<button id="code-button" class="mother-button code-button" type="button" title="Code · your files, the editor and the preview beside the conversation" aria-pressed="false">
|
|
47
|
+
<svg class="bar-glyph" viewBox="0 0 20 20" aria-hidden="true"><path d="M7.2 5.4 2.8 10l4.4 4.6M12.8 5.4l4.4 4.6-4.4 4.6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path class="code-slash" d="M11.3 5.6 8.7 14.4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" pathLength="10"/></svg>CODE
|
|
48
|
+
</button>
|
|
46
49
|
<button id="modules-button" class="mother-button" type="button" title="Modules · optional integrations for this project" aria-haspopup="dialog" aria-pressed="false">
|
|
47
50
|
<span class="mother-glyph modules-glyph" aria-hidden="true"></span><svg class="bar-glyph" viewBox="0 0 20 20" aria-hidden="true"><path class="ic-f" d="M2.75 8.45a2.2 2.2 0 0 1 2.2-2.2h2a1.9 1.9 0 1 1 2.6 0h2a2.2 2.2 0 0 1 2.2 2.2v2a1.9 1.9 0 1 1 0 2.6v2a2.2 2.2 0 0 1-2.2 2.2h-2a1.9 1.9 0 1 0-2.6 0h-2a2.2 2.2 0 0 1-2.2-2.2z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>MODULES<span id="modules-count" class="modules-count" hidden></span>
|
|
48
51
|
</button>
|
|
@@ -254,8 +257,9 @@
|
|
|
254
257
|
<span class="nostromo-actions">
|
|
255
258
|
<button id="nostromo-cold" class="mother-close cold" type="button" hidden aria-pressed="false" title="The memories nothing has ever reached for, that share a subject with nothing, and that have had their chances">UNUSED · 0</button>
|
|
256
259
|
<button id="nostromo-ask" class="mother-close ask" type="button" hidden aria-pressed="false" title="The questions this archive cannot answer yet, written from what it already holds">ASK · 0</button>
|
|
260
|
+
<button id="nostromo-neural" class="mother-close neural-key" type="button" aria-pressed="false">Neural</button>
|
|
257
261
|
<button id="nostromo-recenter" class="mother-close" type="button" hidden>RECENTER</button>
|
|
258
|
-
<button id="nostromo-close" class="
|
|
262
|
+
<span class="surface-modes"><button id="nostromo-close" class="surface-mode close" type="button" aria-label="Leave the Memory map" title="Leave the Memory map">×</button></span>
|
|
259
263
|
</span>
|
|
260
264
|
</header>
|
|
261
265
|
<canvas id="nostromo-canvas" class="nostromo-canvas" tabindex="-1" aria-label="Memories orbiting the core"></canvas>
|
package/public/macroverso.js
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
// for less motion gets one still frame redrawn when something changes. No line joins two universes
|
|
25
25
|
// until a link exists (phase 3c).
|
|
26
26
|
|
|
27
|
-
import { COLLAPSED, FOCUS_VEIL, MEMORY_COLORS, PITCH_LIMIT, TURN_PER_PIXEL, alongCurve, collapsedChip, createNostromo, currentAlong, dwarfChip, hexAlpha, hexMix, magmaMap, texturesReady } from './nostromo-engine.js';
|
|
27
|
+
import { COLLAPSED, FOCUS_VEIL, MEMORY_COLORS, NEURAL_AXON, NEURAL_CROSSFADE, NEURAL_IGNITION, PITCH_LIMIT, TURN_PER_PIXEL, alongCurve, collapsedChip, createNostromo, currentAlong, drawNeuralBulb, dwarfChip, hexAlpha, hexMix, magmaMap, neuralGround, neuronGlow, texturesReady } from './nostromo-engine.js';
|
|
28
|
+
import { createStrain, inviteNeural, neuralKey, paintNeuralKey } from './neural.js';
|
|
28
29
|
|
|
29
30
|
const KINDS = Object.keys(MEMORY_COLORS);
|
|
30
31
|
const TILT = 0.42; // how flat the orbits read: the plane seen a little from above
|
|
@@ -329,7 +330,7 @@ function farMemories(data) {
|
|
|
329
330
|
|
|
330
331
|
// One universe painted once: its scene placed, settled and drawn into a sprite by the engine. A
|
|
331
332
|
// room that is asleep, sealed, outdated or unreachable is MOTHER alone: cold, or caged.
|
|
332
|
-
function paintUniverse(one, dpr, shown = null) {
|
|
333
|
+
function paintUniverse(one, dpr, shown = null, neural = false) {
|
|
333
334
|
const side = Math.min(SPRITE_MAX, Math.round(Math.max(SIDE_MIN, shown ?? one.reach * 2) * dpr));
|
|
334
335
|
const css = side / dpr;
|
|
335
336
|
const canvas = canvasOf(side, side);
|
|
@@ -345,6 +346,8 @@ function paintUniverse(one, dpr, shown = null) {
|
|
|
345
346
|
}
|
|
346
347
|
scene.cam = { x: 0, y: 0, scale: (css / 2 - 4) / BODY_REACH, manual: true };
|
|
347
348
|
if (data?.sealed) scene.cage = { at: 0, closed: true };
|
|
349
|
+
// Neural mode (public/neural.js): the universe drawn as a cluster of neurons round its bulb.
|
|
350
|
+
scene.neural = neural;
|
|
348
351
|
engine.projectNostromo();
|
|
349
352
|
engine.drawNostromo(0.7);
|
|
350
353
|
if (!one.room.open) {
|
|
@@ -408,7 +411,7 @@ function paintStars(seed, count, biggest, dpr) {
|
|
|
408
411
|
return { canvas, side };
|
|
409
412
|
}
|
|
410
413
|
|
|
411
|
-
export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bff66', accentInk = '#0a1405', bishop = null, words, actions }) {
|
|
414
|
+
export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bff66', accentInk = '#0a1405', bishop = null, words, actions, neural: neuralStart = false }) {
|
|
412
415
|
const reduced = reducedMotion();
|
|
413
416
|
const layer = document.createElement('section');
|
|
414
417
|
layer.className = 'mv';
|
|
@@ -454,12 +457,23 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
454
457
|
recenter.className = 'mother-close mv-recenter';
|
|
455
458
|
recenter.textContent = words.recenter ?? 'RECENTER';
|
|
456
459
|
recenter.hidden = true;
|
|
460
|
+
// Leaving: the room's close, a sunk track and the cross that turns (styles.css, .x-close), as
|
|
461
|
+
// every window closes; what it does is in its name.
|
|
462
|
+
const backTrack = document.createElement('span');
|
|
463
|
+
backTrack.className = 'surface-modes';
|
|
457
464
|
const back = document.createElement('button');
|
|
458
465
|
back.type = 'button';
|
|
459
|
-
back.className = '
|
|
460
|
-
back.
|
|
461
|
-
back.
|
|
462
|
-
|
|
466
|
+
back.className = 'surface-mode close x-close x-only mv-back';
|
|
467
|
+
back.setAttribute('aria-label', words.back);
|
|
468
|
+
back.title = words.back;
|
|
469
|
+
back.insertAdjacentHTML('beforeend', '<span class="x-icon"><svg viewBox="0 0 20 20" focusable="false" aria-hidden="true"><path d="M5.5 5.5l9 9M14.5 5.5l-9 9" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>'); // trusted: our own SVG
|
|
470
|
+
backTrack.append(back);
|
|
471
|
+
// Neural mode (public/neural.js): the same space drawn as neurons, chosen here or in a project.
|
|
472
|
+
const neuralButton = document.createElement('button');
|
|
473
|
+
neuralButton.type = 'button';
|
|
474
|
+
neuralButton.className = 'mother-close neural-key';
|
|
475
|
+
neuralKey(neuralButton);
|
|
476
|
+
keys.append(neuralButton, recenter, backTrack);
|
|
463
477
|
head.append(title, sub, legend, keys);
|
|
464
478
|
const card = document.createElement('aside');
|
|
465
479
|
card.className = 'mv-card';
|
|
@@ -488,6 +502,9 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
488
502
|
let raf = 0;
|
|
489
503
|
let last = performance.now();
|
|
490
504
|
let clock = 0;
|
|
505
|
+
let neural = Boolean(neuralStart); // drawn as neurons (setNeuralMode)
|
|
506
|
+
let neuralSince = null; // when it came on, for the light running out from the centre
|
|
507
|
+
let fade = null; // the space as it was, fading over the new drawing
|
|
491
508
|
let queued = false;
|
|
492
509
|
let resizeTimer = 0;
|
|
493
510
|
const drag = { active: false, moved: false, turning: false, link: null, x: 0, y: 0, x0: 0, y0: 0 };
|
|
@@ -497,6 +514,36 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
497
514
|
let orb = null;
|
|
498
515
|
const gaze = { x: 0, y: 0, open: 0, at: 0, jumpAt: 0, jx: 0, jy: 0, blinkAt: -1, nextBlink: 3 + Math.random() * 4, twice: false };
|
|
499
516
|
let pointer = null;
|
|
517
|
+
// Neural mode, switched from its key or because a project switched it: the space as it was fades
|
|
518
|
+
// over the new drawing, the light runs out from the centre, every universe is drawn again its new
|
|
519
|
+
// way (a sleeping one, painted once, is painted again), and the choice is kept (actions.neural).
|
|
520
|
+
paintNeuralKey(neuralButton, neural);
|
|
521
|
+
function setNeuralMode(on) {
|
|
522
|
+
if (neural === on) return;
|
|
523
|
+
if (!reduced && canvas.width && canvas.height) {
|
|
524
|
+
const held = document.createElement('canvas');
|
|
525
|
+
held.width = canvas.width;
|
|
526
|
+
held.height = canvas.height;
|
|
527
|
+
held.getContext('2d').drawImage(canvas, 0, 0);
|
|
528
|
+
fade = { canvas: held, at: clock };
|
|
529
|
+
}
|
|
530
|
+
neural = on;
|
|
531
|
+
neuralSince = on && !reduced ? clock : null;
|
|
532
|
+
paintNeuralKey(neuralButton, on);
|
|
533
|
+
layer.querySelector('.neural-invite')?.remove();
|
|
534
|
+
const now = performance.now() / 1000;
|
|
535
|
+
for (const one of universes) {
|
|
536
|
+
if (!one.sprite?.scene) continue;
|
|
537
|
+
if (one.room.open && !reducedMotion()) one.sprite.engine.setNeural(on, now);
|
|
538
|
+
else one.dirty = true;
|
|
539
|
+
}
|
|
540
|
+
live?.engine.setNeural(on, now);
|
|
541
|
+
actions.neural?.(on);
|
|
542
|
+
redraw();
|
|
543
|
+
}
|
|
544
|
+
neuralButton.addEventListener('click', () => setNeuralMode(!neural));
|
|
545
|
+
// When this machine does not give the space its frames, the key invites to try the mode.
|
|
546
|
+
const strain = createStrain({ onStrain: () => { if (!neural) inviteNeural({ key: neuralButton, host: layer, map: 'projects', onTry: () => setNeuralMode(true) }); } });
|
|
500
547
|
const motes = Array.from({ length: ORB_MOTES }, (_, i) => { const rnd = random(hash(`mote${i}`)); return { phase: rnd() * Math.PI * 2, reach: 1.3 + rnd() * 0.65, speed: 0.12 + rnd() * 0.2, tilt: (rnd() - 0.5) * 0.5, size: 0.35 + rnd() * 0.6 }; });
|
|
501
548
|
|
|
502
549
|
function layout(all) {
|
|
@@ -595,7 +642,7 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
595
642
|
const next = [focus, ...universes].find((one) => one && one.dirty);
|
|
596
643
|
if (!next) return false;
|
|
597
644
|
if (!next.data) { if (!next.loading?.data) void load(next); return false; }
|
|
598
|
-
next.sprite = paintUniverse(next, view.dpr, next.shown ?? null);
|
|
645
|
+
next.sprite = paintUniverse(next, view.dpr, next.shown ?? null, neural);
|
|
599
646
|
next.dirty = false;
|
|
600
647
|
return true;
|
|
601
648
|
}
|
|
@@ -650,6 +697,7 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
650
697
|
}
|
|
651
698
|
engine.placeNostromo();
|
|
652
699
|
scene.words = words;
|
|
700
|
+
scene.neural = neural;
|
|
653
701
|
if (data.sealed) scene.cage = { at: 0, closed: true };
|
|
654
702
|
// The pointer reaches it through its own canvas, which is nowhere on the page: it answers
|
|
655
703
|
// from the origin, and the pointer is moved into its frame before it is asked.
|
|
@@ -713,7 +761,9 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
713
761
|
}
|
|
714
762
|
|
|
715
763
|
function frame(now) {
|
|
764
|
+
const started = performance.now();
|
|
716
765
|
advance(now);
|
|
766
|
+
strain.frame(performance.now() - started, now);
|
|
717
767
|
raf = reduced && !camera.to ? 0 : requestAnimationFrame(frame);
|
|
718
768
|
}
|
|
719
769
|
// One frame's worth of everything, without asking for the next: what frame() does, and what
|
|
@@ -766,10 +816,13 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
766
816
|
ctx.setTransform(view.dpr, 0, 0, view.dpr, 0, 0);
|
|
767
817
|
ctx.globalCompositeOperation = 'source-over';
|
|
768
818
|
ctx.globalAlpha = 1;
|
|
769
|
-
|
|
819
|
+
const beat = reduced ? 0.4 : 0.5 + 0.5 * Math.sin(clock * 5.46);
|
|
820
|
+
// Neural mode: no nebula and no stars, the dark the neurons stand on.
|
|
821
|
+
if (neural) neuralGround(ctx, view.w, view.h, beat);
|
|
822
|
+
else if (nebula) ctx.drawImage(nebula, 0, 0, view.w, view.h);
|
|
770
823
|
// Stars: the near ones move with the camera, the far ones barely. The tiles repeat under any
|
|
771
824
|
// movement, so the space has no edge.
|
|
772
|
-
stars.forEach((tile, depth) => {
|
|
825
|
+
if (!neural) stars.forEach((tile, depth) => {
|
|
773
826
|
const speed = [0.04, 0.1, 0.22][depth] * camera.zoom;
|
|
774
827
|
const ox = (((-camera.x * speed - clock * (reduced ? 0 : 1.5 * (depth + 1))) % tile.side) + tile.side) % tile.side;
|
|
775
828
|
const oy = (((-camera.y * speed) % tile.side) + tile.side) % tile.side;
|
|
@@ -783,14 +836,13 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
783
836
|
const span = Math.min(view.w, view.h * 1.7);
|
|
784
837
|
ctx.lineWidth = 1;
|
|
785
838
|
for (const orbit of [INNER, OUTER]) {
|
|
786
|
-
ctx.strokeStyle = 'rgba(242, 241, 238, .11)';
|
|
839
|
+
ctx.strokeStyle = neural ? `rgba(${NEURAL_AXON}, .1)` : 'rgba(242, 241, 238, .11)';
|
|
787
840
|
ctx.setLineDash(orbit === OUTER ? [3, 7] : []);
|
|
788
841
|
ctx.beginPath();
|
|
789
842
|
ctx.ellipse(centre.x, centre.y, span * orbit * camera.zoom, span * orbit * TILT * camera.zoom, 0, 0, Math.PI * 2);
|
|
790
843
|
ctx.stroke();
|
|
791
844
|
}
|
|
792
845
|
ctx.setLineDash([]);
|
|
793
|
-
const beat = reduced ? 0.4 : 0.5 + 0.5 * Math.sin(clock * 5.46);
|
|
794
846
|
drawVeins(ctx);
|
|
795
847
|
drawCentre(ctx, beat);
|
|
796
848
|
|
|
@@ -807,6 +859,12 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
807
859
|
if (veil > 0.004) { ctx.fillStyle = `rgba(2, 1, 4, ${veil})`; ctx.fillRect(0, 0, view.w, view.h); }
|
|
808
860
|
const top = order.find((entry) => entry.one === lifted);
|
|
809
861
|
if (top) drawUniverse(ctx, top.one, top.at, beat);
|
|
862
|
+
// Changing the drawing: the space as it was fades over the new one.
|
|
863
|
+
if (fade) {
|
|
864
|
+
const k = (clock - fade.at) / NEURAL_CROSSFADE;
|
|
865
|
+
if (k >= 1 || k < 0) fade = null;
|
|
866
|
+
else { ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.globalAlpha = 1 - k * (2 - k); ctx.drawImage(fade.canvas, 0, 0); ctx.restore(); }
|
|
867
|
+
}
|
|
810
868
|
}
|
|
811
869
|
|
|
812
870
|
// One universe on screen: MOTHER's warmth, the live scene or its own living canvas, its rings
|
|
@@ -830,7 +888,7 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
830
888
|
const away = at.light * at.light * (0.55 + 0.45 * Math.min(1, camera.zoom));
|
|
831
889
|
const reach = r * (2.3 + 0.2 * beat);
|
|
832
890
|
const halo = ctx.createRadialGradient(p.x, p.y, r * 0.6, p.x, p.y, reach);
|
|
833
|
-
halo.addColorStop(0, awake ? `rgba(226, 40, 22, ${(0.16 + 0.05 * beat) * away})` : `rgba(120, 150, 200, ${0.08 * away})`);
|
|
891
|
+
halo.addColorStop(0, neural ? (awake ? `rgba(${NEURAL_AXON}, ${(0.12 + 0.04 * beat) * away})` : `rgba(120, 150, 200, ${0.05 * away})`) : awake ? `rgba(226, 40, 22, ${(0.16 + 0.05 * beat) * away})` : `rgba(120, 150, 200, ${0.08 * away})`);
|
|
834
892
|
halo.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
835
893
|
ctx.fillStyle = halo;
|
|
836
894
|
ctx.beginPath(); ctx.arc(p.x, p.y, reach, 0, Math.PI * 2); ctx.fill();
|
|
@@ -844,8 +902,8 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
844
902
|
} else {
|
|
845
903
|
// Not read yet: an ember where it will be.
|
|
846
904
|
const ember = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r);
|
|
847
|
-
ember.addColorStop(0, awake ? '#ff7a3a' : '#56637a');
|
|
848
|
-
ember.addColorStop(1, awake ? 'rgba(168, 23, 12, 0)' : 'rgba(27, 33, 48, 0)');
|
|
905
|
+
ember.addColorStop(0, neural ? (awake ? '#9fdcff' : '#56637a') : awake ? '#ff7a3a' : '#56637a');
|
|
906
|
+
ember.addColorStop(1, neural ? 'rgba(20, 60, 110, 0)' : awake ? 'rgba(168, 23, 12, 0)' : 'rgba(27, 33, 48, 0)');
|
|
849
907
|
ctx.fillStyle = ember;
|
|
850
908
|
ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI * 2); ctx.fill();
|
|
851
909
|
}
|
|
@@ -899,85 +957,91 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
899
957
|
const centre = toScreen(0, 0);
|
|
900
958
|
const p = { x: centre.x, y: centre.y + float * Math.max(0.6, camera.zoom) };
|
|
901
959
|
const r = 30 * clamp(camera.zoom, 0.75, 1.8);
|
|
902
|
-
core.screen = { x: p.x, y: p.y, r: r * 1.3 };
|
|
903
|
-
//
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
gaze.
|
|
920
|
-
gaze.
|
|
921
|
-
gaze.
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
orb.paintedAt = clock;
|
|
928
|
-
}
|
|
929
|
-
const life = awake ? 1 : 0.5;
|
|
930
|
-
// The atmosphere: violet into blue, breathing slowly.
|
|
931
|
-
ctx.globalCompositeOperation = 'lighter';
|
|
932
|
-
const air = ctx.createRadialGradient(p.x, p.y, r * 0.8, p.x, p.y, r * (2.7 + 0.12 * beat));
|
|
933
|
-
air.addColorStop(0, `rgba(122, 58, 238, ${0.3 * life})`);
|
|
934
|
-
air.addColorStop(0.45, `rgba(72, 112, 255, ${0.12 * life})`);
|
|
935
|
-
air.addColorStop(1, 'rgba(40, 20, 120, 0)');
|
|
936
|
-
ctx.fillStyle = air;
|
|
937
|
-
ctx.beginPath(); ctx.arc(p.x, p.y, r * (2.7 + 0.12 * beat), 0, Math.PI * 2); ctx.fill();
|
|
938
|
-
// The wisps, turning two ways at once, as gas does.
|
|
939
|
-
const span = r * orb.reach * 2;
|
|
940
|
-
for (const [speed, alpha] of [[0.05, 0.8], [-0.033, 0.5]]) {
|
|
941
|
-
ctx.save();
|
|
942
|
-
ctx.translate(p.x, p.y);
|
|
943
|
-
ctx.rotate(reduced ? 0 : clock * speed);
|
|
944
|
-
ctx.globalAlpha = alpha * life;
|
|
945
|
-
ctx.drawImage(orb.wisps, -span / 2, -span / 2, span, span);
|
|
946
|
-
ctx.restore();
|
|
947
|
-
}
|
|
948
|
-
// The motes behind it, then the sphere, then the motes in front.
|
|
949
|
-
const drawMotes = (front) => {
|
|
950
|
-
for (const mote of motes) {
|
|
951
|
-
const a = mote.phase + (reduced ? 0 : clock * mote.speed);
|
|
952
|
-
const ahead = Math.sin(a) > 0;
|
|
953
|
-
if (ahead !== front) continue;
|
|
954
|
-
const mx = p.x + Math.cos(a) * r * mote.reach;
|
|
955
|
-
const my = p.y + Math.sin(a) * r * mote.reach * 0.3 + mote.tilt * r * Math.cos(a);
|
|
956
|
-
const size = mote.size * (ahead ? 1 : 0.7) * clamp(camera.zoom, 0.7, 1.4);
|
|
957
|
-
const glow = ctx.createRadialGradient(mx, my, 0, mx, my, size * 3);
|
|
958
|
-
glow.addColorStop(0, `rgba(214, 206, 255, ${(ahead ? 0.6 : 0.25) * life})`);
|
|
959
|
-
glow.addColorStop(1, 'rgba(120, 140, 255, 0)');
|
|
960
|
-
ctx.fillStyle = glow;
|
|
961
|
-
ctx.beginPath(); ctx.arc(mx, my, size * 3, 0, Math.PI * 2); ctx.fill();
|
|
960
|
+
core.screen = { x: p.x, y: p.y, r: r * (neural ? 1.8 : 1.3) };
|
|
961
|
+
// Neural mode: the centre of the brain, its bulb, in the Coordinator's violet; no eye, no gas.
|
|
962
|
+
if (neural) {
|
|
963
|
+
const ignite = neuralSince === null ? 1 : clamp((clock - neuralSince) / NEURAL_IGNITION, 0, 1);
|
|
964
|
+
drawNeuralBulb(ctx, p.x, p.y, r * 1.6, clock, { beat, light: awake ? 1 : 0.6, ignite, tone: 'bishop', still: reduced });
|
|
965
|
+
} else {
|
|
966
|
+
// Where it looks: at the person (the pointer) first; at the universe being carried, or the
|
|
967
|
+
// chosen one, when the pointer is away; wandering slowly when nothing is there. A living eye
|
|
968
|
+
// never holds still: small jumps every second or so.
|
|
969
|
+
const seen = linking?.from ?? (focus && !focus.centre ? focus : null);
|
|
970
|
+
let target = pointer ?? seen?.screen ?? null;
|
|
971
|
+
if (!target && !reduced) target = { x: p.x + Math.cos(clock * 0.21) * 160, y: p.y + Math.sin(clock * 0.34) * 90 };
|
|
972
|
+
if (!reduced && clock > gaze.jumpAt) { gaze.jumpAt = clock + 0.6 + Math.random() * 1.2; gaze.jx = (Math.random() - 0.5) * 0.07; gaze.jy = (Math.random() - 0.5) * 0.05; }
|
|
973
|
+
const toward = target ? { x: target.x - p.x, y: target.y - p.y } : { x: 0, y: 0 };
|
|
974
|
+
const far = Math.hypot(toward.x, toward.y) + 180;
|
|
975
|
+
const step = reduced ? 1 : clamp((clock - gaze.at) * 6, 0, 1);
|
|
976
|
+
gaze.at = clock;
|
|
977
|
+
gaze.x += (clamp(toward.x / far + gaze.jx, -1, 1) - gaze.x) * step;
|
|
978
|
+
gaze.y += (clamp(toward.y / far + gaze.jy, -1, 1) - gaze.y) * step;
|
|
979
|
+
gaze.open += ((linking?.over === core ? 1 : hover === core ? 0.45 : 0) - gaze.open) * step;
|
|
980
|
+
// A blink now and then, sometimes two; the lids close fast and open a little slower.
|
|
981
|
+
if (!reduced && awake && clock > gaze.nextBlink) {
|
|
982
|
+
gaze.blinkAt = clock;
|
|
983
|
+
gaze.twice = !gaze.twice && Math.random() < 0.25;
|
|
984
|
+
gaze.nextBlink = clock + (gaze.twice ? 0.32 : 4 + Math.random() * 6);
|
|
962
985
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
986
|
+
const since = clock - gaze.blinkAt;
|
|
987
|
+
const lid = gaze.blinkAt < 0 || since > 0.26 ? 0 : since < 0.09 ? since / 0.09 : 1 - (since - 0.09) / 0.17;
|
|
988
|
+
if (clock - orb.paintedAt >= 1 / ORB_FPS || orb.paintedAt < 0 || lid > 0) {
|
|
989
|
+
orb.paint(reduced ? 0 : clock, { x: gaze.x, y: gaze.y, open: gaze.open, lid: clamp(lid, 0, 1) * 0.94, asleep: !awake });
|
|
990
|
+
orb.paintedAt = clock;
|
|
991
|
+
}
|
|
992
|
+
const life = awake ? 1 : 0.5;
|
|
993
|
+
// The atmosphere: violet into blue, breathing slowly.
|
|
994
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
995
|
+
const air = ctx.createRadialGradient(p.x, p.y, r * 0.8, p.x, p.y, r * (2.7 + 0.12 * beat));
|
|
996
|
+
air.addColorStop(0, `rgba(122, 58, 238, ${0.3 * life})`);
|
|
997
|
+
air.addColorStop(0.45, `rgba(72, 112, 255, ${0.12 * life})`);
|
|
998
|
+
air.addColorStop(1, 'rgba(40, 20, 120, 0)');
|
|
999
|
+
ctx.fillStyle = air;
|
|
1000
|
+
ctx.beginPath(); ctx.arc(p.x, p.y, r * (2.7 + 0.12 * beat), 0, Math.PI * 2); ctx.fill();
|
|
1001
|
+
// The wisps, turning two ways at once, as gas does.
|
|
1002
|
+
const span = r * orb.reach * 2;
|
|
1003
|
+
for (const [speed, alpha] of [[0.05, 0.8], [-0.033, 0.5]]) {
|
|
1004
|
+
ctx.save();
|
|
1005
|
+
ctx.translate(p.x, p.y);
|
|
1006
|
+
ctx.rotate(reduced ? 0 : clock * speed);
|
|
1007
|
+
ctx.globalAlpha = alpha * life;
|
|
1008
|
+
ctx.drawImage(orb.wisps, -span / 2, -span / 2, span, span);
|
|
1009
|
+
ctx.restore();
|
|
1010
|
+
}
|
|
1011
|
+
// The motes behind it, then the sphere, then the motes in front.
|
|
1012
|
+
const drawMotes = (front) => {
|
|
1013
|
+
for (const mote of motes) {
|
|
1014
|
+
const a = mote.phase + (reduced ? 0 : clock * mote.speed);
|
|
1015
|
+
const ahead = Math.sin(a) > 0;
|
|
1016
|
+
if (ahead !== front) continue;
|
|
1017
|
+
const mx = p.x + Math.cos(a) * r * mote.reach;
|
|
1018
|
+
const my = p.y + Math.sin(a) * r * mote.reach * 0.3 + mote.tilt * r * Math.cos(a);
|
|
1019
|
+
const size = mote.size * (ahead ? 1 : 0.7) * clamp(camera.zoom, 0.7, 1.4);
|
|
1020
|
+
const glow = ctx.createRadialGradient(mx, my, 0, mx, my, size * 3);
|
|
1021
|
+
glow.addColorStop(0, `rgba(214, 206, 255, ${(ahead ? 0.6 : 0.25) * life})`);
|
|
1022
|
+
glow.addColorStop(1, 'rgba(120, 140, 255, 0)');
|
|
1023
|
+
ctx.fillStyle = glow;
|
|
1024
|
+
ctx.beginPath(); ctx.arc(mx, my, size * 3, 0, Math.PI * 2); ctx.fill();
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
drawMotes(false);
|
|
1028
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
1029
|
+
ctx.globalAlpha = awake ? 1 : 0.8;
|
|
1030
|
+
ctx.drawImage(orb.canvas, p.x - r, p.y - r, r * 2, r * 2);
|
|
1031
|
+
ctx.globalAlpha = 1;
|
|
1032
|
+
// Its own thin atmosphere, brighter on the side the light comes from.
|
|
1033
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
1034
|
+
const limb = ctx.createRadialGradient(p.x - r * 0.1, p.y - r * 0.12, r * 0.9, p.x, p.y, r * 1.14);
|
|
1035
|
+
limb.addColorStop(0, 'rgba(120, 150, 255, 0)');
|
|
1036
|
+
limb.addColorStop(0.55, `rgba(130, 160, 255, ${0.28 * life})`);
|
|
1037
|
+
limb.addColorStop(1, 'rgba(90, 60, 220, 0)');
|
|
1038
|
+
ctx.fillStyle = limb;
|
|
1039
|
+
ctx.beginPath(); ctx.arc(p.x, p.y, r * 1.14, 0, Math.PI * 2); ctx.fill();
|
|
1040
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
1041
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
1042
|
+
drawMotes(true);
|
|
1043
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
1044
|
+
}
|
|
981
1045
|
// A ring in the accent while a universe is carried onto it, and while it waits for the person;
|
|
982
1046
|
// a white one while it is pointed at or chosen.
|
|
983
1047
|
const target2 = linking?.over === core;
|
|
@@ -985,14 +1049,15 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
985
1049
|
ctx.strokeStyle = accent;
|
|
986
1050
|
ctx.globalAlpha = target2 ? 0.95 : reduced ? 0.6 : 0.45 + 0.35 * Math.sin(clock * 2.4);
|
|
987
1051
|
ctx.lineWidth = 2;
|
|
988
|
-
ctx.beginPath(); ctx.arc(p.x, p.y, r * 1.5, 0, Math.PI * 2); ctx.stroke();
|
|
1052
|
+
ctx.beginPath(); ctx.arc(p.x, p.y, r * (neural ? 2.2 : 1.5), 0, Math.PI * 2); ctx.stroke();
|
|
989
1053
|
ctx.globalAlpha = 1;
|
|
990
1054
|
} else if (core === hover || core === focus) {
|
|
991
1055
|
ctx.strokeStyle = core === focus ? 'rgba(255, 255, 255, .85)' : 'rgba(255, 255, 255, .4)';
|
|
992
1056
|
ctx.lineWidth = core === focus ? 1.4 : 1;
|
|
993
|
-
ctx.beginPath(); ctx.arc(p.x, p.y, r * 1.5, 0, Math.PI * 2); ctx.stroke();
|
|
1057
|
+
ctx.beginPath(); ctx.arc(p.x, p.y, r * (neural ? 2.2 : 1.5), 0, Math.PI * 2); ctx.stroke();
|
|
994
1058
|
}
|
|
995
|
-
|
|
1059
|
+
// The bulb's stem reaches lower than the orb did: its name stands under the stem.
|
|
1060
|
+
const below = p.y + r * (neural ? 3.25 : 1.62) + 8;
|
|
996
1061
|
ctx.textAlign = 'center';
|
|
997
1062
|
ctx.textBaseline = 'top';
|
|
998
1063
|
ctx.lineJoin = 'round';
|
|
@@ -1033,6 +1098,32 @@ export function openMacroverso({ host, rooms, here, enlaces = [], accent = '#9bf
|
|
|
1033
1098
|
const from = back ? pb : pa, to = back ? pa : pb;
|
|
1034
1099
|
// MOTHER's red leaves the room that serves and the accent arrives where it is read; both
|
|
1035
1100
|
// ways, red at both ends with the accent between them.
|
|
1101
|
+
// Neural mode: an axon between two clusters, in one cold line, and what crosses it a light
|
|
1102
|
+
// with the stretch it has just run lit behind it.
|
|
1103
|
+
if (neural) {
|
|
1104
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
1105
|
+
ctx.strokeStyle = `rgba(${NEURAL_AXON}, ${0.18 + 0.22 * lately})`;
|
|
1106
|
+
ctx.lineWidth = 0.9 + 0.6 * lately;
|
|
1107
|
+
ctx.beginPath(); ctx.moveTo(pa.x, pa.y); ctx.quadraticCurveTo(cx, cy, pb.x, pb.y); ctx.stroke();
|
|
1108
|
+
vein.pulses = vein.pulses.filter((pulse) => clock - pulse.born < VEIN_LIFE);
|
|
1109
|
+
for (const pulse of vein.pulses) {
|
|
1110
|
+
const u = Math.min(1, (clock - pulse.born) / VEIN_LIFE);
|
|
1111
|
+
const along = (k) => alongCurve(pa.x, pa.y, cx, cy, pb.x, pb.y, pulse.back ? 1 - k : k);
|
|
1112
|
+
let behind = along(u);
|
|
1113
|
+
const head = behind;
|
|
1114
|
+
for (let step = 1; step <= 6; step += 1) {
|
|
1115
|
+
const back = along(Math.max(0, u - step * 0.03));
|
|
1116
|
+
ctx.strokeStyle = hexAlpha(accent, 0.5 - step * 0.07);
|
|
1117
|
+
ctx.lineWidth = 2 - step * 0.22;
|
|
1118
|
+
ctx.beginPath(); ctx.moveTo(behind.x, behind.y); ctx.lineTo(back.x, back.y); ctx.stroke();
|
|
1119
|
+
behind = back;
|
|
1120
|
+
}
|
|
1121
|
+
const size = (6 + 4 * Math.sin(u * Math.PI)) * Math.max(0.7, Math.min(1.4, camera.zoom));
|
|
1122
|
+
ctx.drawImage(neuronGlow(accent), head.x - size, head.y - size, size * 2, size * 2);
|
|
1123
|
+
}
|
|
1124
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1036
1127
|
const red = '#e22816';
|
|
1037
1128
|
const arriving = enlace.direction === 'both' ? red : accent;
|
|
1038
1129
|
const head = reduced ? 0.5 : ((clock * (0.06 + 0.2 * recent) + vein.seed) % 1 + 1) % 1;
|