@jossuealcala/madre 1.0.1 → 1.0.3
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/ADDITIONAL-TERMS.md +93 -0
- package/CHANGELOG.en.md +2013 -0
- package/CHANGELOG.md +102 -1
- package/CONTRIBUTING.md +1 -1
- package/LICENSE +661 -202
- package/NOTICE +26 -14
- package/README.en.md +241 -0
- package/README.md +21 -12
- package/TRADEMARKS.en.md +50 -0
- package/TRADEMARKS.md +33 -16
- package/bin/madre.cjs +40 -0
- package/bin/madre.mjs +7 -0
- package/docs/INTERNALS.md +6 -0
- package/docs/REFERENCE.md +40 -5
- package/docs/SDK.md +4 -2
- package/docs/en/INTERNALS.md +103 -0
- package/docs/en/REFERENCE.md +365 -0
- package/docs/en/SDK.md +308 -0
- package/docs/en/training/README.md +59 -0
- package/docs/training/README.md +48 -9
- package/docs/training/train.sh +7 -2
- package/package.json +13 -5
- package/public/abduction.js +2 -0
- package/public/app.js +3154 -539
- package/public/attribution.js +114 -0
- package/public/brands.js +23 -6
- package/public/code-tools.js +143 -0
- package/public/editor.js +22 -4
- package/public/es.js +1004 -416
- package/public/hardware.js +50 -0
- package/public/i18n.js +25 -1
- package/public/index.html +18 -11
- package/public/inquiry.js +2 -0
- package/public/loader.js +2 -0
- package/public/macroverso.js +189 -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 +414 -47
- package/public/power.js +100 -0
- package/public/resay.js +11 -0
- package/public/review-ask.js +2 -0
- package/public/routing.js +23 -1
- package/public/salas-mac.css +119 -18
- package/public/salas-mac.html +14 -2
- package/public/salas-mac.js +358 -56
- package/public/salas.css +2 -0
- package/public/salas.html +1 -0
- package/public/salas.js +4 -2
- package/public/styles.css +1793 -77
- package/public/syntax.js +2 -0
- package/public/touch.js +2 -0
- package/public/tour-chapters.js +2 -0
- package/public/tour.js +84 -5
- package/public/troubleshooting.js +24 -17
- package/public/wallpaper.js +275 -0
- package/src/adapters/claude.mjs +2 -0
- package/src/adapters/codex.mjs +2 -0
- package/src/adapters/cursor.mjs +96 -0
- package/src/adapters/gemini.mjs +13 -4
- package/src/adapters/madre.mjs +5 -2
- package/src/adapters/opencode.mjs +7 -3
- package/src/adapters/process.mjs +2 -0
- package/src/asking.mjs +2 -0
- package/src/auth-probe.mjs +145 -12
- package/src/bridge/devices.mjs +2 -0
- package/src/bridge/handoff.mjs +2 -0
- package/src/bridge/host.mjs +2 -0
- package/src/bridge/network.mjs +2 -0
- package/src/bridge/policy.mjs +3 -1
- package/src/bridge/qr.mjs +2 -0
- package/src/bridge/server.mjs +2 -0
- package/src/capabilities.mjs +11 -0
- package/src/chats.mjs +2 -0
- package/src/checkpoint.mjs +18 -1
- package/src/choices.mjs +2 -0
- package/src/cli-args.mjs +2 -0
- package/src/cold.mjs +2 -0
- package/src/commands.mjs +2 -0
- package/src/config.mjs +2 -0
- package/src/conversation-context.mjs +15 -1
- package/src/credentials.mjs +35 -3
- package/src/dataset.mjs +78 -4
- package/src/directives.mjs +2 -0
- package/src/distiller.mjs +2 -0
- package/src/embeddings.mjs +2 -0
- package/src/enlaces.mjs +214 -6
- package/src/event-store.mjs +2 -0
- package/src/exam.mjs +6 -4
- package/src/extensions.mjs +13 -3
- package/src/eyecat-watch.mjs +2 -0
- package/src/eyecat.mjs +2 -0
- package/src/file-ops.mjs +2 -0
- package/src/files.mjs +32 -0
- package/src/github.mjs +2 -0
- package/src/heartbeat.mjs +2 -0
- package/src/i18n.mjs +31 -1
- package/src/image-studio.mjs +2 -0
- package/src/json-file.mjs +2 -0
- package/src/launch.mjs +2 -0
- package/src/lease.mjs +5 -1
- package/src/lobby-work.mjs +2 -0
- package/src/local-ai.mjs +2 -0
- package/src/looks.mjs +127 -2
- package/src/market-key.mjs +8 -0
- package/src/market-remote.mjs +239 -0
- package/src/maturity.mjs +3 -1
- package/src/mcp/connect.mjs +345 -0
- package/src/mcp/github-server.mjs +2 -0
- package/src/mcp/image-server.mjs +2 -0
- package/src/mcp/memory-server.mjs +2 -0
- package/src/mcp/proxy.mjs +153 -0
- package/src/mcp/smtp-server.mjs +2 -0
- package/src/memory-dashboard.mjs +2 -0
- package/src/memory-tools.mjs +2 -0
- package/src/memory.mjs +2 -0
- package/src/models.mjs +49 -3
- package/src/module-scope.mjs +68 -0
- package/src/modules/ahp.mjs +104 -7
- package/src/modules/ash-policy.mjs +2 -0
- package/src/modules/ash.mjs +3 -1
- package/src/modules/bishop-exam.mjs +122 -0
- package/src/modules/bishop-plan.mjs +5 -3
- package/src/modules/bishop.mjs +84 -23
- package/src/modules/choices.mjs +2 -0
- package/src/modules/correo.mjs +2 -0
- package/src/modules/enlaces.mjs +89 -6
- package/src/modules/git-pulse.mjs +2 -0
- package/src/modules/github.mjs +2 -0
- package/src/modules/helpers.mjs +2 -0
- package/src/modules/image-studio.mjs +13 -2
- package/src/modules/index.mjs +4 -1
- package/src/modules/intents-calibration.mjs +2 -0
- package/src/modules/intents-desk.mjs +4 -2
- package/src/modules/intents-observe.mjs +2 -0
- package/src/modules/intents-policy.mjs +4 -1
- package/src/modules/intents-try.mjs +2 -0
- package/src/modules/intents.mjs +11 -7
- package/src/modules/local-models.mjs +124 -0
- package/src/modules/mcp-connect.mjs +293 -0
- package/src/modules/ollama.mjs +15 -4
- package/src/modules/playwright.mjs +88 -15
- package/src/modules/ripley.mjs +2 -0
- package/src/modules/runs.mjs +5 -3
- package/src/modules/sdk.mjs +63 -9
- package/src/modules/updates.mjs +2 -0
- package/src/mother.mjs +2 -0
- package/src/npm-launch.mjs +18 -0
- package/src/ollama.mjs +8 -5
- package/src/outbound.mjs +16 -6
- package/src/override.mjs +2 -0
- package/src/ports.mjs +6 -2
- package/src/prices.mjs +2 -0
- package/src/privacy.mjs +2 -0
- package/src/quota-monitor.mjs +2 -0
- package/src/quota-sources.mjs +2 -0
- package/src/request-guard.mjs +2 -0
- package/src/review-ask.mjs +2 -0
- package/src/room/archivist.mjs +2 -0
- package/src/room/attachments.mjs +2 -0
- package/src/room/budget.mjs +2 -0
- package/src/room/context.mjs +2 -0
- package/src/room/control.mjs +7 -1
- package/src/room/copilot.mjs +76 -0
- package/src/room/economy.mjs +2 -0
- package/src/room/escalation.mjs +2 -0
- package/src/room/ghost.mjs +2 -0
- package/src/room/guard.mjs +2 -0
- package/src/room/prompt.mjs +25 -5
- package/src/room/returns.mjs +82 -0
- package/src/room/runs.mjs +4 -0
- package/src/room/vectors.mjs +2 -0
- package/src/room.mjs +223 -35
- package/src/router.mjs +3 -1
- package/src/runs.mjs +6 -1
- package/src/runtime-detection.mjs +11 -0
- package/src/salas.mjs +28 -0
- package/src/sentinel-errors.mjs +2 -0
- package/src/server-sentinel.mjs +2 -0
- package/src/server.mjs +757 -81
- package/src/setup.mjs +3 -1
- package/src/smtp.mjs +2 -0
- package/src/tour.mjs +2 -0
- package/src/updates.mjs +2 -0
- package/src/usage-sentinel.mjs +2 -0
- package/src/vault.mjs +2 -0
- package/src/verdict.mjs +3 -1
- package/src/whats-new.mjs +2 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// NOSTROMO's engine: the physics and the painters of the archive as a body in space, lifted out
|
|
2
4
|
// of the room's page so the Macroverso can draw every room the way its own NOSTROMO is drawn —
|
|
3
5
|
// with these very functions, not a copy of them (docs/MACROVERSO.md, phase 3a).
|
|
@@ -897,6 +899,130 @@ export function hexMix(hex, other, amount) {
|
|
|
897
899
|
// Everything below closes over `nostromo`, the scene: the room's page hands its own object, with
|
|
898
900
|
// its canvas and its DOM beside the state; the Macroverso hands one scene per NOSTROMO it draws
|
|
899
901
|
// live. Nothing here reads the page.
|
|
902
|
+
|
|
903
|
+
// ---- NEURAL MODE (1.0.3) ---------------------------------------------------------------------
|
|
904
|
+
// The same scene drawn as the neurons of a brain: every memory a point of light in its kind's
|
|
905
|
+
// colour, the wires between them thin axons in one cold stroke, and light only where something
|
|
906
|
+
// travels — a pulse lights the path it runs and the neuron it reaches. The core is the central
|
|
907
|
+
// bulb of that brain: cells packed into a body with a short stem, tied to their nearest, beating.
|
|
908
|
+
// It shows everything the full drawing shows and costs a fraction of it: no surface is shaded
|
|
909
|
+
// pixel by pixel, no nebula, no stars; every glow is one small texture made once and stamped.
|
|
910
|
+
export const NEURAL_AXON = '86, 200, 255';
|
|
911
|
+
export const NEURAL_IGNITION = 1.9; // seconds the light takes to run out from the core when the mode comes on
|
|
912
|
+
export const NEURAL_CROSSFADE = 0.55; // seconds the drawing it replaces takes to fade out
|
|
913
|
+
const NEURON_GLOWS = new Map();
|
|
914
|
+
// A point of light of one colour: white at its heart, its colour round that, gone at its edge.
|
|
915
|
+
export function neuronGlow(hex) {
|
|
916
|
+
let canvas = NEURON_GLOWS.get(hex);
|
|
917
|
+
if (canvas) return canvas;
|
|
918
|
+
canvas = document.createElement('canvas');
|
|
919
|
+
canvas.width = 64;
|
|
920
|
+
canvas.height = 64;
|
|
921
|
+
const paint = canvas.getContext('2d');
|
|
922
|
+
const glow = paint.createRadialGradient(32, 32, 0, 32, 32, 32);
|
|
923
|
+
glow.addColorStop(0, 'rgba(255, 255, 255, 1)');
|
|
924
|
+
glow.addColorStop(0.1, hexAlpha(hexMix(hex, '#ffffff', 0.5), 0.95));
|
|
925
|
+
glow.addColorStop(0.24, hexAlpha(hex, 0.55));
|
|
926
|
+
glow.addColorStop(0.5, hexAlpha(hex, 0.14));
|
|
927
|
+
glow.addColorStop(1, hexAlpha(hex, 0));
|
|
928
|
+
paint.fillStyle = glow;
|
|
929
|
+
paint.fillRect(0, 0, 64, 64);
|
|
930
|
+
NEURON_GLOWS.set(hex, canvas);
|
|
931
|
+
return canvas;
|
|
932
|
+
}
|
|
933
|
+
// The bulb's cells, laid out once: a golden-angle spiral through a body a little wider above,
|
|
934
|
+
// most of them near its skin so its shape reads, a warm few deep inside, and a stem below.
|
|
935
|
+
const BULB_TONES = {
|
|
936
|
+
mother: { heart: '#ffe3b8', skin: '#7fd3ff', deep: '#ffc98a', stem: '#5fc2ff', air: '255, 214, 160', haze: '90, 190, 255', alarm: '#ff4a3a' },
|
|
937
|
+
bishop: { heart: '#efe6ff', skin: '#8fa6ff', deep: '#c9b4ff', stem: '#7f8cff', air: '214, 196, 255', haze: '110, 120, 255', alarm: '#ff4a3a' },
|
|
938
|
+
};
|
|
939
|
+
export const BULB = (() => {
|
|
940
|
+
const cells = [];
|
|
941
|
+
const noise = (i, k) => (((Math.sin(i * 12.9898 + k * 78.233) * 43758.5453) % 1) + 1) % 1;
|
|
942
|
+
const body = 190;
|
|
943
|
+
for (let i = 0; i < body; i += 1) {
|
|
944
|
+
const along = 1 - (2 * (i + 0.5)) / body;
|
|
945
|
+
const ring = Math.sqrt(Math.max(0, 1 - along * along));
|
|
946
|
+
const turn = i * 2.39996;
|
|
947
|
+
const deep = noise(i, 1) < 0.16;
|
|
948
|
+
const out = deep ? 0.18 + 0.42 * noise(i, 2) : 0.72 + 0.28 * Math.sqrt(noise(i, 3));
|
|
949
|
+
const wide = 1 + 0.14 * Math.max(0, along);
|
|
950
|
+
cells.push({ x: ring * Math.cos(turn) * out * wide, y: -along * out * 0.92, z: ring * Math.sin(turn) * out * 0.88 * wide, out, tone: deep ? 'deep' : out < 0.82 ? 'heart' : 'skin' });
|
|
951
|
+
}
|
|
952
|
+
for (let i = 0; i < 26; i += 1) {
|
|
953
|
+
const down = i / 25;
|
|
954
|
+
const turn = i * 2.39996;
|
|
955
|
+
const girth = 0.26 - 0.15 * down;
|
|
956
|
+
cells.push({ x: 0.16 * down * down + Math.cos(turn) * girth * 0.6, y: 0.82 + 0.95 * down, z: Math.sin(turn) * girth * 0.6, out: 1, tone: 'stem' });
|
|
957
|
+
}
|
|
958
|
+
const links = [];
|
|
959
|
+
const seen = new Set();
|
|
960
|
+
cells.forEach((cell, i) => {
|
|
961
|
+
const near = cells.map((other, j) => ({ j, d: (other.x - cell.x) ** 2 + (other.y - cell.y) ** 2 + (other.z - cell.z) ** 2 }))
|
|
962
|
+
.filter((one) => one.j !== i).sort((a, b) => a.d - b.d).slice(0, 2);
|
|
963
|
+
for (const { j } of near) { const key = i < j ? `${i}-${j}` : `${j}-${i}`; if (!seen.has(key)) { seen.add(key); links.push([i, j]); } }
|
|
964
|
+
});
|
|
965
|
+
return { cells, links };
|
|
966
|
+
})();
|
|
967
|
+
// The bulb, drawn at (x, y) with radius R in whatever space the context is in; `px` is one css
|
|
968
|
+
// pixel in that space. It turns slowly on its own besides the scene's turn, beats with the heart,
|
|
969
|
+
// answers when pulses come home (`lit`), flushes red in an alarm, and flashes as the mode comes on.
|
|
970
|
+
export function drawNeuralBulb(ctx, x, y, R, t, { yaw = 0, pitch = 0, beat = 0, lit = 0, alarm = 0, px = 1, light = 1, ignite = 1, tone = 'mother', still = false } = {}) {
|
|
971
|
+
const colours = BULB_TONES[tone] ?? BULB_TONES.mother;
|
|
972
|
+
const turn = yaw + (still ? 0 : t * 0.07);
|
|
973
|
+
const cy = Math.cos(turn), sy = Math.sin(turn), cp = Math.cos(pitch * 0.6), sp = Math.sin(pitch * 0.6);
|
|
974
|
+
const flash = ignite < 0.3 ? 1 - ignite / 0.3 : 0;
|
|
975
|
+
const energy = Math.min(1.6, (0.62 + 0.38 * beat + 0.45 * Math.min(1, lit) + 1.2 * flash) * light);
|
|
976
|
+
const reach = R * 1.12;
|
|
977
|
+
const at = BULB.cells.map((cell) => {
|
|
978
|
+
const x1 = cell.x * cy + cell.z * sy;
|
|
979
|
+
const z1 = cell.z * cy - cell.x * sy;
|
|
980
|
+
const y2 = cell.y * cp - z1 * sp;
|
|
981
|
+
const z2 = cell.y * sp + z1 * cp;
|
|
982
|
+
return { x: x + x1 * reach, y: y + y2 * reach, front: (z2 + 1) / 2, cell };
|
|
983
|
+
});
|
|
984
|
+
const before = ctx.globalCompositeOperation;
|
|
985
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
986
|
+
// Its air: warm at the heart, cold at the edge, breathing with the beat.
|
|
987
|
+
const air = ctx.createRadialGradient(x, y, 0, x, y, R * 2.5);
|
|
988
|
+
air.addColorStop(0, `rgba(${alarm ? '255, 90, 70' : colours.air}, ${0.3 * energy})`);
|
|
989
|
+
air.addColorStop(0.32, `rgba(${colours.haze}, ${0.1 * energy})`);
|
|
990
|
+
air.addColorStop(1, `rgba(${colours.haze}, 0)`);
|
|
991
|
+
ctx.fillStyle = air;
|
|
992
|
+
ctx.beginPath(); ctx.arc(x, y, R * 2.5, 0, Math.PI * 2); ctx.fill();
|
|
993
|
+
// The fibres between neighbouring cells, in one stroke.
|
|
994
|
+
ctx.strokeStyle = `rgba(${colours.haze}, ${Math.min(0.5, 0.2 * energy)})`;
|
|
995
|
+
ctx.lineWidth = 0.6 * px;
|
|
996
|
+
ctx.beginPath();
|
|
997
|
+
for (const [i, j] of BULB.links) { ctx.moveTo(at[i].x, at[i].y); ctx.lineTo(at[j].x, at[j].y); }
|
|
998
|
+
ctx.stroke();
|
|
999
|
+
// The cells, the near side brighter, the heart swelling on the beat.
|
|
1000
|
+
for (const point of at) {
|
|
1001
|
+
const inner = 1 - point.cell.out;
|
|
1002
|
+
const size = (3 + 2.6 * point.front + 2.4 * inner * beat) * px * (1 + 0.6 * flash);
|
|
1003
|
+
const hex = alarm ? colours.alarm : colours[point.cell.tone];
|
|
1004
|
+
ctx.globalAlpha = Math.min(1, (0.3 + 0.55 * point.front) * energy);
|
|
1005
|
+
ctx.drawImage(neuronGlow(hex), point.x - size, point.y - size, size * 2, size * 2);
|
|
1006
|
+
}
|
|
1007
|
+
// The nucleus: one wide soft light at the centre.
|
|
1008
|
+
const heart = R * (0.62 + 0.08 * beat + 0.4 * flash);
|
|
1009
|
+
ctx.globalAlpha = Math.min(1, (0.55 + 0.35 * beat) * energy);
|
|
1010
|
+
ctx.drawImage(neuronGlow(alarm ? colours.alarm : colours.heart), x - heart, y - heart, heart * 2, heart * 2);
|
|
1011
|
+
ctx.globalAlpha = 1;
|
|
1012
|
+
ctx.globalCompositeOperation = before;
|
|
1013
|
+
}
|
|
1014
|
+
// The ground the neurons stand on: almost black, a cold depth at the middle.
|
|
1015
|
+
export function neuralGround(ctx, w, h, beat = 0) {
|
|
1016
|
+
ctx.fillStyle = '#010309';
|
|
1017
|
+
ctx.fillRect(0, 0, w, h);
|
|
1018
|
+
const depth = ctx.createRadialGradient(w / 2, h / 2, 0, w / 2, h / 2, Math.max(w, h) * 0.72);
|
|
1019
|
+
depth.addColorStop(0, `rgba(16, 52, 96, ${0.42 + 0.04 * beat})`);
|
|
1020
|
+
depth.addColorStop(0.55, 'rgba(6, 18, 38, .3)');
|
|
1021
|
+
depth.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
1022
|
+
ctx.fillStyle = depth;
|
|
1023
|
+
ctx.fillRect(0, 0, w, h);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
900
1026
|
export function createNostromo(nostromo) {
|
|
901
1027
|
// What belongs to this scene and no other: the budget of faces painted this frame, and MOTHER's
|
|
902
1028
|
// painted face at its current size.
|
|
@@ -1556,6 +1682,8 @@ export function createNostromo(nostromo) {
|
|
|
1556
1682
|
}
|
|
1557
1683
|
|
|
1558
1684
|
function drawNostromo(t) {
|
|
1685
|
+
// The neural drawing of the same scene, when it is asked for (setNeural).
|
|
1686
|
+
if (nostromo.neural) { drawNeural(t); return; }
|
|
1559
1687
|
const ctx = nostromo.canvas.getContext('2d');
|
|
1560
1688
|
const { w, h, dpr } = nostromo.size;
|
|
1561
1689
|
const cam = nostromo.cam;
|
|
@@ -2099,38 +2227,7 @@ export function createNostromo(nostromo) {
|
|
|
2099
2227
|
if (nostromo.dust?.length || nostromo.fading?.length) drawDust(ctx, t, cam.scale);
|
|
2100
2228
|
|
|
2101
2229
|
// CODE000: the safety box. Bars fall from above and lock around the core.
|
|
2102
|
-
|
|
2103
|
-
const age = t - nostromo.cage.at;
|
|
2104
|
-
const p = nostromo.cage.closed ? 1 : Math.min(1, age / 1.1);
|
|
2105
|
-
const ease = 1 - Math.pow(1 - p, 3);
|
|
2106
|
-
const B = CORE_R * 2.05;
|
|
2107
|
-
const drop = (1 - ease) * 900;
|
|
2108
|
-
ctx.fillStyle = `rgba(0, 0, 0, ${0.35 * ease})`;
|
|
2109
|
-
ctx.fillRect(-B - 8, -B - 8 - drop, B * 2 + 16, B * 2 + 16);
|
|
2110
|
-
const steel = ctx.createLinearGradient(-B, 0, B, 0);
|
|
2111
|
-
steel.addColorStop(0, '#2a2c30'); steel.addColorStop(0.45, '#9aa0a8'); steel.addColorStop(0.55, '#d8dde3'); steel.addColorStop(1, '#3a3d42');
|
|
2112
|
-
ctx.strokeStyle = steel;
|
|
2113
|
-
ctx.lineCap = 'butt';
|
|
2114
|
-
ctx.shadowColor = 'rgba(0, 0, 0, .9)'; ctx.shadowBlur = 14;
|
|
2115
|
-
ctx.lineWidth = 7;
|
|
2116
|
-
for (let i = 0; i <= 6; i += 1) {
|
|
2117
|
-
const x = -B + (i * 2 * B) / 6;
|
|
2118
|
-
ctx.beginPath(); ctx.moveTo(x, -B - drop); ctx.lineTo(x, B - drop); ctx.stroke();
|
|
2119
|
-
}
|
|
2120
|
-
ctx.lineWidth = 9;
|
|
2121
|
-
for (const y of [-B, 0, B]) { ctx.beginPath(); ctx.moveTo(-B - 4, y - drop); ctx.lineTo(B + 4, y - drop); ctx.stroke(); }
|
|
2122
|
-
ctx.shadowBlur = 0;
|
|
2123
|
-
// Rivets at the joints.
|
|
2124
|
-
ctx.fillStyle = '#e8ecf0';
|
|
2125
|
-
for (let i = 0; i <= 6; i += 1) for (const y of [-B, 0, B]) { ctx.beginPath(); ctx.arc(-B + (i * 2 * B) / 6, y - drop, 2.6, 0, Math.PI * 2); ctx.fill(); }
|
|
2126
|
-
// The lock plate.
|
|
2127
|
-
if (ease > 0.98) {
|
|
2128
|
-
ctx.fillStyle = '#1b1d20'; ctx.fillRect(-B * 0.32, B - 14, B * 0.64, 28);
|
|
2129
|
-
ctx.strokeStyle = '#c4c9cf'; ctx.lineWidth = 2; ctx.strokeRect(-B * 0.32, B - 14, B * 0.64, 28);
|
|
2130
|
-
ctx.fillStyle = '#ff2a1f'; ctx.font = `700 ${Math.round(B * 0.1)}px ${getComputedStyle(nostromo.canvas).getPropertyValue('--mono') || 'monospace'}`; ctx.textAlign = 'center'; ctx.fillText('CODE000', 0, B + 6); ctx.textAlign = 'start';
|
|
2131
|
-
}
|
|
2132
|
-
if (!nostromo.cage.closed && p >= 1 && !nostromo.cage.clanged) { nostromo.cage.clanged = true; nostromo.onClang?.(); }
|
|
2133
|
-
}
|
|
2230
|
+
drawCage(ctx, t);
|
|
2134
2231
|
ctx.restore();
|
|
2135
2232
|
|
|
2136
2233
|
// Vignette that tightens on the beat; red wash while MOTHER is angry.
|
|
@@ -2144,21 +2241,291 @@ export function createNostromo(nostromo) {
|
|
|
2144
2241
|
}
|
|
2145
2242
|
|
|
2146
2243
|
// The hovered memory, named on screen; not while another one is held, which is the one being read.
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2244
|
+
drawHoverLabel(ctx, w);
|
|
2245
|
+
fadeOver(ctx, t);
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// ---- NEURAL MODE: the same scene, drawn as neurons (see NEURAL_AXON above). -----------------
|
|
2249
|
+
// Changing the drawing: what was on the canvas is kept a moment and fades over the new one, and
|
|
2250
|
+
// coming on, the light runs out from the core and reaches every neuron in turn.
|
|
2251
|
+
function setNeural(on, t) {
|
|
2252
|
+
const want = Boolean(on);
|
|
2253
|
+
if (Boolean(nostromo.neural) === want) return;
|
|
2254
|
+
const canvas = nostromo.canvas;
|
|
2255
|
+
let held = null;
|
|
2256
|
+
if (canvas?.width && canvas.height && typeof document !== 'undefined') {
|
|
2257
|
+
held = document.createElement('canvas');
|
|
2258
|
+
held.width = canvas.width;
|
|
2259
|
+
held.height = canvas.height;
|
|
2260
|
+
held.getContext('2d').drawImage(canvas, 0, 0);
|
|
2261
|
+
}
|
|
2262
|
+
nostromo.neural = want;
|
|
2263
|
+
nostromo.neuralSince = want && !nostromo.reduced ? t : null;
|
|
2264
|
+
nostromo.fadeFrom = held && !nostromo.reduced ? { canvas: held, at: t } : null;
|
|
2265
|
+
}
|
|
2266
|
+
function fadeOver(ctx, t) {
|
|
2267
|
+
const from = nostromo.fadeFrom;
|
|
2268
|
+
if (!from) return;
|
|
2269
|
+
const k = (t - from.at) / NEURAL_CROSSFADE;
|
|
2270
|
+
if (k >= 1 || k < 0) { nostromo.fadeFrom = null; return; }
|
|
2271
|
+
ctx.save();
|
|
2272
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
2273
|
+
ctx.globalAlpha = 1 - k * (2 - k);
|
|
2274
|
+
ctx.drawImage(from.canvas, 0, 0);
|
|
2275
|
+
ctx.restore();
|
|
2276
|
+
}
|
|
2277
|
+
function drawNeural(t) {
|
|
2278
|
+
const ctx = nostromo.canvas.getContext('2d');
|
|
2279
|
+
const { w, h, dpr } = nostromo.size;
|
|
2280
|
+
const cam = nostromo.cam;
|
|
2281
|
+
const { beat } = heartbeat(t);
|
|
2282
|
+
const alarm = nostromo.alarm ? Math.max(0, 1 - (t - nostromo.alarm) / 3.6) : 0;
|
|
2283
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2284
|
+
ctx.clearRect(0, 0, w, h);
|
|
2285
|
+
if (!nostromo.bare) neuralGround(ctx, w, h, beat);
|
|
2286
|
+
ctx.save();
|
|
2287
|
+
ctx.translate(w / 2, h / 2);
|
|
2288
|
+
ctx.scale(cam.scale, cam.scale);
|
|
2289
|
+
ctx.translate(-cam.x, -cam.y);
|
|
2290
|
+
const px = 1 / cam.scale;
|
|
2291
|
+
const nearness = Math.min(1, Math.max(0, (cam.scale - 0.1) / 0.3));
|
|
2292
|
+
const farLight = nostromo.bare ? 0.45 + 0.55 * nearness : 1;
|
|
2293
|
+
const farSize = nostromo.bare ? 0.5 + 0.5 * nearness : 1;
|
|
2294
|
+
const R = CORE_R * (1 + 0.05 * beat + 0.1 * alarm);
|
|
2295
|
+
const nodes = nostromo.nodes;
|
|
2296
|
+
const byId = new Map(nodes.map((node) => [node.memory.id, node]));
|
|
2297
|
+
// Coming on: a front of light leaves the core and every neuron appears as it is reached,
|
|
2298
|
+
// brightest at the moment it is crossed.
|
|
2299
|
+
const since = nostromo.neuralSince;
|
|
2300
|
+
const ignite = since === null || since === undefined ? 1 : Math.min(1, Math.max(0, (t - since) / NEURAL_IGNITION));
|
|
2301
|
+
let span = 1;
|
|
2302
|
+
if (ignite < 1) for (const node of nodes) span = Math.max(span, Math.hypot(node.x, node.y));
|
|
2303
|
+
const front = ignite < 1 ? (ignite * (2 - ignite)) * (span + 160) : Infinity;
|
|
2304
|
+
const shown = (node) => (front === Infinity ? 1 : Math.min(1, Math.max(0, (front - Math.hypot(node.x, node.y)) / 90)));
|
|
2305
|
+
const crossed = (node) => (front === Infinity ? 0 : Math.max(0, 1 - Math.abs(front - Math.hypot(node.x, node.y)) / 110));
|
|
2306
|
+
const lightOf = (node) => Math.min(1.6, (node.lit ?? 0) + 0.5 * (node.charge ?? 0) + 1.3 * crossed(node));
|
|
2307
|
+
// Where the wires run: the full drawing's own curves, bent less, so the pulses still ride them.
|
|
2308
|
+
for (const link of nostromo.links) {
|
|
2309
|
+
const a = byId.get(link.a);
|
|
2310
|
+
const b = byId.get(link.b);
|
|
2311
|
+
if (!a || !b) continue;
|
|
2312
|
+
const dx = b.x - a.x, dy = b.y - a.y;
|
|
2313
|
+
const d = Math.hypot(dx, dy) || 1;
|
|
2314
|
+
const bow = Math.sin(t * 0.7 + a.seed + b.seed) * Math.min(22, d * 0.07);
|
|
2315
|
+
link.cx = (a.x + b.x) / 2 - (dy / d) * bow;
|
|
2316
|
+
link.cy = (a.y + b.y) / 2 + (dx / d) * bow;
|
|
2317
|
+
}
|
|
2318
|
+
for (const node of nodes) {
|
|
2319
|
+
const d = Math.hypot(node.x, node.y) || 1;
|
|
2320
|
+
const bow = Math.sin(t * 0.9 + node.seed) * Math.min(26, d * 0.08);
|
|
2321
|
+
node.cx = node.x / 2 + (-node.y / d) * bow; node.cy = node.y / 2 + (node.x / d) * bow;
|
|
2322
|
+
node.rimX = (node.x / d) * R * 0.92; node.rimY = (node.y / d) * R * 0.92;
|
|
2161
2323
|
}
|
|
2324
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
2325
|
+
ctx.lineCap = 'round';
|
|
2326
|
+
// The axons from the core, then the synapses between memories: each set in one stroke.
|
|
2327
|
+
const reveal = front === Infinity ? 1 : ignite;
|
|
2328
|
+
ctx.strokeStyle = `rgba(${NEURAL_AXON}, ${0.13 * farLight * reveal})`;
|
|
2329
|
+
ctx.lineWidth = 0.8 * px;
|
|
2330
|
+
ctx.beginPath();
|
|
2331
|
+
for (const node of nodes) if (!node.forgetting && shown(node) > 0) { ctx.moveTo(node.rimX, node.rimY); ctx.quadraticCurveTo(node.cx, node.cy, node.x, node.y); }
|
|
2332
|
+
ctx.stroke();
|
|
2333
|
+
ctx.strokeStyle = `rgba(${NEURAL_AXON}, ${0.24 * farLight * reveal})`;
|
|
2334
|
+
ctx.lineWidth = 0.9 * px;
|
|
2335
|
+
ctx.beginPath();
|
|
2336
|
+
for (const link of nostromo.links) {
|
|
2337
|
+
const a = byId.get(link.a);
|
|
2338
|
+
const b = byId.get(link.b);
|
|
2339
|
+
if (!a || !b || shown(a) <= 0 || shown(b) <= 0) continue;
|
|
2340
|
+
ctx.moveTo(a.x, a.y); ctx.quadraticCurveTo(link.cx, link.cy, b.x, b.y);
|
|
2341
|
+
}
|
|
2342
|
+
ctx.stroke();
|
|
2343
|
+
// A synapse whose neurons are lit answers in their colours.
|
|
2344
|
+
for (const link of nostromo.links) {
|
|
2345
|
+
const a = byId.get(link.a);
|
|
2346
|
+
const b = byId.get(link.b);
|
|
2347
|
+
if (!a || !b) continue;
|
|
2348
|
+
const glow = Math.max(lightOf(a), lightOf(b)) * Math.min(shown(a), shown(b));
|
|
2349
|
+
if (glow < 0.12) continue;
|
|
2350
|
+
ctx.strokeStyle = hexAlpha(hexMix(a.color, b.color, 0.5), Math.min(0.7, 0.32 * glow) * farLight);
|
|
2351
|
+
ctx.lineWidth = (0.8 + 0.6 * Math.min(1, glow)) * px;
|
|
2352
|
+
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.quadraticCurveTo(link.cx, link.cy, b.x, b.y); ctx.stroke();
|
|
2353
|
+
}
|
|
2354
|
+
// The pulses: a head of light and the stretch of path it has just lit, fading behind it.
|
|
2355
|
+
for (const pulse of nostromo.pulses ?? []) {
|
|
2356
|
+
const u = Math.min(1, (t - pulse.born) / pulse.life);
|
|
2357
|
+
let path;
|
|
2358
|
+
let color;
|
|
2359
|
+
if (pulse.kind === 'link') {
|
|
2360
|
+
const { from, to, link } = pulse;
|
|
2361
|
+
path = (k) => alongCurve(from.x, from.y, link.cx ?? (from.x + to.x) / 2, link.cy ?? (from.y + to.y) / 2, to.x, to.y, k);
|
|
2362
|
+
color = to.color;
|
|
2363
|
+
} else {
|
|
2364
|
+
const node = pulse.node;
|
|
2365
|
+
const outward = pulse.to === node;
|
|
2366
|
+
const rim = { x: node.rimX ?? 0, y: node.rimY ?? 0 };
|
|
2367
|
+
const from = outward ? rim : node;
|
|
2368
|
+
const to = outward ? node : rim;
|
|
2369
|
+
path = (k) => alongCurve(from.x, from.y, node.cx ?? node.x / 2, node.cy ?? node.y / 2, to.x, to.y, k);
|
|
2370
|
+
color = outward ? '#bfe6ff' : node.color;
|
|
2371
|
+
}
|
|
2372
|
+
const head = path(u);
|
|
2373
|
+
let behind = head;
|
|
2374
|
+
for (let step = 1; step <= 6; step += 1) {
|
|
2375
|
+
const back = path(Math.max(0, u - step * 0.035));
|
|
2376
|
+
ctx.strokeStyle = hexAlpha(color, (0.55 - step * 0.08) * farLight);
|
|
2377
|
+
ctx.lineWidth = (1.6 - step * 0.18) * px * farSize;
|
|
2378
|
+
ctx.beginPath(); ctx.moveTo(behind.x, behind.y); ctx.lineTo(back.x, back.y); ctx.stroke();
|
|
2379
|
+
behind = back;
|
|
2380
|
+
}
|
|
2381
|
+
const size = (7 + 4 * Math.sin(u * Math.PI)) * px * farSize;
|
|
2382
|
+
ctx.globalAlpha = 0.95 * farLight;
|
|
2383
|
+
ctx.drawImage(neuronGlow(color), head.x - size, head.y - size, size * 2, size * 2);
|
|
2384
|
+
ctx.globalAlpha = 1;
|
|
2385
|
+
}
|
|
2386
|
+
// The beat leaves the bulb as a cold ring.
|
|
2387
|
+
for (const ring of nostromo.rings ?? []) {
|
|
2388
|
+
const age = t - ring.born;
|
|
2389
|
+
if (age < 0) continue;
|
|
2390
|
+
ctx.strokeStyle = `rgba(${NEURAL_AXON}, ${Math.max(0, 0.14 - age * 0.06) * farLight})`;
|
|
2391
|
+
ctx.lineWidth = Math.max(0.4, 1.6 - age * 0.7) * px * farSize;
|
|
2392
|
+
ctx.beginPath(); ctx.arc(0, 0, R + age * WAVE_SPEED * farSize, 0, Math.PI * 2); ctx.stroke();
|
|
2393
|
+
}
|
|
2394
|
+
// The front of light while the mode comes on.
|
|
2395
|
+
if (front !== Infinity) {
|
|
2396
|
+
ctx.strokeStyle = `rgba(200, 236, 255, ${0.5 * (1 - ignite)})`;
|
|
2397
|
+
ctx.lineWidth = 1.4 * px;
|
|
2398
|
+
ctx.beginPath(); ctx.arc(0, 0, front, 0, Math.PI * 2); ctx.stroke();
|
|
2399
|
+
}
|
|
2400
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
2401
|
+
// The central bulb.
|
|
2402
|
+
drawNeuralBulb(ctx, 0, 0, R, t, { yaw: (nostromo.spin ?? 0) * 0.2 - (nostromo.yaw ?? 0), pitch: nostromo.pitch ?? 0, beat, lit: nostromo.coreLit ?? 0, alarm, px, light: farLight, ignite, still: nostromo.reduced });
|
|
2403
|
+
// The neurons, each a point of light: brighter the more the room leans on it and the more is
|
|
2404
|
+
// reaching it now; one out of circulation stays a cold ember; a false claim is a dark ring.
|
|
2405
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
2406
|
+
for (const node of nodes) {
|
|
2407
|
+
if (node.forgetting || node.scale <= 0.01) continue;
|
|
2408
|
+
const seen = shown(node);
|
|
2409
|
+
if (seen <= 0) continue;
|
|
2410
|
+
const near = 0.55 + 0.45 * Math.min(1, Math.max(0, ((node.depth ?? 0) + 700) / 1400));
|
|
2411
|
+
if (node.memory.kind === COLLAPSED) {
|
|
2412
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
2413
|
+
ctx.strokeStyle = hexAlpha(hexMix(node.color, '#a8ccff', 0.4), 0.7 * seen * near * farLight);
|
|
2414
|
+
ctx.lineWidth = 1.1 * px;
|
|
2415
|
+
ctx.beginPath(); ctx.arc(node.x, node.y, 4.2 * px * farSize, 0, Math.PI * 2); ctx.stroke();
|
|
2416
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
const light = lightOf(node);
|
|
2420
|
+
const cold = !travels(node.memory);
|
|
2421
|
+
const colour = cold ? hexMix(node.color, '#2a3346', 0.6) : node.color;
|
|
2422
|
+
const size = (10 + 6 * (node.activity ?? 0) + 12 * Math.min(1, light)) * px * farSize * (0.6 + 0.4 * node.scale);
|
|
2423
|
+
ctx.globalAlpha = Math.min(1, (cold ? 0.4 : 0.66 + 0.26 * (node.activity ?? 0)) + 0.5 * Math.min(1, light)) * seen * near * farLight;
|
|
2424
|
+
ctx.drawImage(neuronGlow(colour), node.x - size, node.y - size, size * 2, size * 2);
|
|
2425
|
+
}
|
|
2426
|
+
ctx.globalAlpha = 1;
|
|
2427
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
2428
|
+
for (const node of nodes) if (node === nostromo.hover && !nostromo.selected) drawSight(ctx, node.x, node.y, 9 * px, t, cam.scale);
|
|
2429
|
+
// A held memory: everything else falls back; it, its synapses and its sight come forward.
|
|
2430
|
+
const lock = nostromo.lock;
|
|
2431
|
+
if (lock?.node && lock.fade > 0.01) {
|
|
2432
|
+
const held = lock.node;
|
|
2433
|
+
ctx.save();
|
|
2434
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2435
|
+
if (nostromo.bare) ctx.globalCompositeOperation = 'source-atop';
|
|
2436
|
+
ctx.fillStyle = `rgba(1, 3, 9, ${FOCUS_VEIL * lock.fade})`;
|
|
2437
|
+
ctx.fillRect(0, 0, w, h);
|
|
2438
|
+
ctx.restore();
|
|
2439
|
+
const id = held.memory.id;
|
|
2440
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
2441
|
+
for (const link of nostromo.links) {
|
|
2442
|
+
if (link.a !== id && link.b !== id) continue;
|
|
2443
|
+
const a = byId.get(link.a);
|
|
2444
|
+
const b = byId.get(link.b);
|
|
2445
|
+
if (!a || !b) continue;
|
|
2446
|
+
ctx.strokeStyle = hexAlpha(hexMix(a.color, b.color, 0.5), 0.7 * lock.fade);
|
|
2447
|
+
ctx.lineWidth = 1.2 * px;
|
|
2448
|
+
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.quadraticCurveTo(link.cx ?? (a.x + b.x) / 2, link.cy ?? (a.y + b.y) / 2, b.x, b.y); ctx.stroke();
|
|
2449
|
+
const other = a === held ? b : a;
|
|
2450
|
+
const size = 9 * px;
|
|
2451
|
+
ctx.globalAlpha = 0.8 * lock.fade;
|
|
2452
|
+
ctx.drawImage(neuronGlow(other.color), other.x - size, other.y - size, size * 2, size * 2);
|
|
2453
|
+
ctx.globalAlpha = 1;
|
|
2454
|
+
}
|
|
2455
|
+
const size = 16 * px;
|
|
2456
|
+
ctx.globalAlpha = lock.fade;
|
|
2457
|
+
ctx.drawImage(neuronGlow(held.color), held.x - size, held.y - size, size * 2, size * 2);
|
|
2458
|
+
ctx.globalAlpha = 1;
|
|
2459
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
2460
|
+
if (held === nostromo.selected) drawSight(ctx, held.x, held.y, 12 * px, t, cam.scale, { locked: true, age: t - lock.at, label: nostromo.words?.locked?.(held.memory) ?? '', still: nostromo.reduced });
|
|
2461
|
+
}
|
|
2462
|
+
for (const node of nodes) if (node.forgetting) drawImplosion(ctx, node, t, cam.scale, nostromo.reduced);
|
|
2463
|
+
if (nostromo.dust?.length || nostromo.fading?.length) drawDust(ctx, t, cam.scale);
|
|
2464
|
+
drawCage(ctx, t);
|
|
2465
|
+
ctx.restore();
|
|
2466
|
+
// The dark closes round the edges, a little on the beat; red while the core is angry.
|
|
2467
|
+
if (!nostromo.bare) {
|
|
2468
|
+
const vig = ctx.createRadialGradient(w / 2, h / 2, Math.min(w, h) * (0.38 - 0.02 * beat), w / 2, h / 2, Math.max(w, h) * 0.75);
|
|
2469
|
+
vig.addColorStop(0, 'rgba(0, 0, 0, 0)');
|
|
2470
|
+
vig.addColorStop(1, `rgba(${alarm ? 60 : 0}, 0, ${alarm ? 0 : 6}, ${0.6 + 0.08 * beat})`);
|
|
2471
|
+
ctx.fillStyle = vig;
|
|
2472
|
+
ctx.fillRect(0, 0, w, h);
|
|
2473
|
+
if (alarm) { ctx.fillStyle = `rgba(255, 30, 20, ${0.08 * alarm * (0.6 + 0.4 * Math.sin(t * 18))})`; ctx.fillRect(0, 0, w, h); }
|
|
2474
|
+
}
|
|
2475
|
+
drawHoverLabel(ctx, w);
|
|
2476
|
+
fadeOver(ctx, t);
|
|
2477
|
+
}
|
|
2478
|
+
// CODE000: the safety box, in the scene's world. Shared by both drawings.
|
|
2479
|
+
function drawCage(ctx, t) {
|
|
2480
|
+
if (!nostromo.cage) return;
|
|
2481
|
+
const age = t - nostromo.cage.at;
|
|
2482
|
+
const p = nostromo.cage.closed ? 1 : Math.min(1, age / 1.1);
|
|
2483
|
+
const ease = 1 - Math.pow(1 - p, 3);
|
|
2484
|
+
const B = CORE_R * 2.05;
|
|
2485
|
+
const drop = (1 - ease) * 900;
|
|
2486
|
+
ctx.fillStyle = `rgba(0, 0, 0, ${0.35 * ease})`;
|
|
2487
|
+
ctx.fillRect(-B - 8, -B - 8 - drop, B * 2 + 16, B * 2 + 16);
|
|
2488
|
+
const steel = ctx.createLinearGradient(-B, 0, B, 0);
|
|
2489
|
+
steel.addColorStop(0, '#2a2c30'); steel.addColorStop(0.45, '#9aa0a8'); steel.addColorStop(0.55, '#d8dde3'); steel.addColorStop(1, '#3a3d42');
|
|
2490
|
+
ctx.strokeStyle = steel;
|
|
2491
|
+
ctx.lineCap = 'butt';
|
|
2492
|
+
ctx.shadowColor = 'rgba(0, 0, 0, .9)'; ctx.shadowBlur = 14;
|
|
2493
|
+
ctx.lineWidth = 7;
|
|
2494
|
+
for (let i = 0; i <= 6; i += 1) {
|
|
2495
|
+
const x = -B + (i * 2 * B) / 6;
|
|
2496
|
+
ctx.beginPath(); ctx.moveTo(x, -B - drop); ctx.lineTo(x, B - drop); ctx.stroke();
|
|
2497
|
+
}
|
|
2498
|
+
ctx.lineWidth = 9;
|
|
2499
|
+
for (const y of [-B, 0, B]) { ctx.beginPath(); ctx.moveTo(-B - 4, y - drop); ctx.lineTo(B + 4, y - drop); ctx.stroke(); }
|
|
2500
|
+
ctx.shadowBlur = 0;
|
|
2501
|
+
// Rivets at the joints.
|
|
2502
|
+
ctx.fillStyle = '#e8ecf0';
|
|
2503
|
+
for (let i = 0; i <= 6; i += 1) for (const y of [-B, 0, B]) { ctx.beginPath(); ctx.arc(-B + (i * 2 * B) / 6, y - drop, 2.6, 0, Math.PI * 2); ctx.fill(); }
|
|
2504
|
+
// The lock plate.
|
|
2505
|
+
if (ease > 0.98) {
|
|
2506
|
+
ctx.fillStyle = '#1b1d20'; ctx.fillRect(-B * 0.32, B - 14, B * 0.64, 28);
|
|
2507
|
+
ctx.strokeStyle = '#c4c9cf'; ctx.lineWidth = 2; ctx.strokeRect(-B * 0.32, B - 14, B * 0.64, 28);
|
|
2508
|
+
ctx.fillStyle = '#ff2a1f'; ctx.font = `700 ${Math.round(B * 0.1)}px ${getComputedStyle(nostromo.canvas).getPropertyValue('--mono') || 'monospace'}`; ctx.textAlign = 'center'; ctx.fillText('CODE000', 0, B + 6); ctx.textAlign = 'start';
|
|
2509
|
+
}
|
|
2510
|
+
if (!nostromo.cage.closed && p >= 1 && !nostromo.cage.clanged) { nostromo.cage.clanged = true; nostromo.onClang?.(); }
|
|
2511
|
+
}
|
|
2512
|
+
// The hovered memory, named on screen. Shared by both drawings.
|
|
2513
|
+
function drawHoverLabel(ctx, w) {
|
|
2514
|
+
if (!nostromo.hover || nostromo.hover.scale <= 0.5 || nostromo.selected) return;
|
|
2515
|
+
const cam = nostromo.cam;
|
|
2516
|
+
const node = nostromo.hover;
|
|
2517
|
+
const p = toScreen(node.x, node.y);
|
|
2518
|
+
const label = `${nostromo.words?.kind?.(node.memory.kind) ?? node.memory.kind} · ${node.memory.text.length > 72 ? `${node.memory.text.slice(0, 71)}…` : node.memory.text}`;
|
|
2519
|
+
ctx.font = '11px ' + (getComputedStyle(nostromo.canvas).getPropertyValue('--mono') || 'monospace');
|
|
2520
|
+
const width = ctx.measureText(label).width + 16;
|
|
2521
|
+
const lx = Math.min(w - width - 8, Math.max(8, p.x - width / 2));
|
|
2522
|
+
const ly = p.y + node.r * cam.scale + 14;
|
|
2523
|
+
ctx.fillStyle = 'rgba(3, 2, 3, .85)';
|
|
2524
|
+
ctx.fillRect(lx, ly, width, 22);
|
|
2525
|
+
ctx.strokeStyle = hexAlpha(node.color, 0.6);
|
|
2526
|
+
ctx.strokeRect(lx + 0.5, ly + 0.5, width - 1, 21);
|
|
2527
|
+
ctx.fillStyle = '#eef1ea';
|
|
2528
|
+
ctx.fillText(label, lx + 8, ly + 15);
|
|
2162
2529
|
}
|
|
2163
2530
|
|
|
2164
2531
|
// Whether there is a camera to answer the pointer with. The canvas takes events from the moment
|
|
@@ -2216,7 +2583,7 @@ export function createNostromo(nostromo) {
|
|
|
2216
2583
|
}
|
|
2217
2584
|
|
|
2218
2585
|
return {
|
|
2219
|
-
buildNodes, sizeNostromo, placeNostromo, stepNostromo, focusNostromo, projectNostromo, fitNostromo, drawNostromo, drawNebula, heartbeat,
|
|
2586
|
+
buildNodes, sizeNostromo, placeNostromo, stepNostromo, focusNostromo, projectNostromo, fitNostromo, drawNostromo, drawNebula, heartbeat, setNeural,
|
|
2220
2587
|
toScreen, toWorld, nostromoLive, nostromoAt, beginImplosion, drawImplosion, drawDust, drawSight, dwarfSprite, magmaFace,
|
|
2221
2588
|
};
|
|
2222
2589
|
}
|
package/public/power.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
// Turning MADRE off from the page (2026-10-16): one key in the top bar of Home and of every
|
|
4
|
+
// project, a question before anything happens, and then the server closes every open project and
|
|
5
|
+
// itself the clean way (src/server.mjs, /api/madre/shutdown). What is left on screen says MADRE is
|
|
6
|
+
// off, so a page that no longer answers is never mistaken for one that broke.
|
|
7
|
+
import { t } from './i18n.js';
|
|
8
|
+
|
|
9
|
+
export const POWER_ICON = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 2.9v6.3" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M6.2 5.3a6.1 6.1 0 1 0 7.6 0" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>';
|
|
10
|
+
|
|
11
|
+
const node = (tag, className, text) => {
|
|
12
|
+
const made = document.createElement(tag);
|
|
13
|
+
if (className) made.className = className;
|
|
14
|
+
if (text !== undefined && text !== null) made.textContent = text;
|
|
15
|
+
return made;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// The key itself, in the classes of the bar it sits in.
|
|
19
|
+
export function powerKey(className) {
|
|
20
|
+
const key = node('button', `${className} power-key`);
|
|
21
|
+
key.type = 'button';
|
|
22
|
+
key.innerHTML = POWER_ICON; // trusted: our own SVG string
|
|
23
|
+
labelPowerKey(key);
|
|
24
|
+
key.addEventListener('click', () => void askPowerOff());
|
|
25
|
+
return key;
|
|
26
|
+
}
|
|
27
|
+
export function labelPowerKey(key) {
|
|
28
|
+
key.title = t('Turn MADRE off');
|
|
29
|
+
key.setAttribute('aria-label', t('Turn MADRE off'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// How many projects are open besides this page, to say what closes.
|
|
33
|
+
async function openProjects() {
|
|
34
|
+
try {
|
|
35
|
+
const data = await fetch('/api/salas', { cache: 'no-store' }).then((response) => (response.ok ? response.json() : null));
|
|
36
|
+
return (data?.rooms ?? []).filter((room) => room.open && room.id !== data.salas).length;
|
|
37
|
+
} catch { return null; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function askPowerOff() {
|
|
41
|
+
if (document.querySelector('dialog.power-ask')) return;
|
|
42
|
+
const open = await openProjects();
|
|
43
|
+
const dialog = node('dialog', 'room-ask power-ask');
|
|
44
|
+
dialog.setAttribute('aria-labelledby', 'power-ask-title');
|
|
45
|
+
const card = node('div', 'room-ask-card');
|
|
46
|
+
const icon = node('span', 'room-ask-icon');
|
|
47
|
+
icon.innerHTML = POWER_ICON; // trusted: our own SVG string
|
|
48
|
+
const title = node('h2', 'room-ask-title', t('Turn MADRE off?'));
|
|
49
|
+
title.id = 'power-ask-title';
|
|
50
|
+
const what = open === null ? t('Home and every open project close.') : open === 1 ? t('Home and the one open project close.') : t('Home and the {n} open projects close.', { n: open });
|
|
51
|
+
const body = node('p', 'room-ask-body', `${what} ${t('If an agent is working, it stops and the conversation says so; everything else is kept as it is.')}`);
|
|
52
|
+
const again = node('p', 'key-safe', t('To use it again, open MADRE the way you always do. Your projects, conversations and settings will be there.'));
|
|
53
|
+
const reply = node('p', 'key-reply');
|
|
54
|
+
reply.setAttribute('aria-live', 'polite');
|
|
55
|
+
const actions = node('div', 'room-ask-actions');
|
|
56
|
+
const cancel = node('button', 'room-ask-choice', t('Cancel'));
|
|
57
|
+
cancel.type = 'button';
|
|
58
|
+
const go = node('button', 'room-ask-choice danger', t('Turn off'));
|
|
59
|
+
go.type = 'button';
|
|
60
|
+
actions.append(cancel, go);
|
|
61
|
+
card.append(icon, title, body, again, reply, actions);
|
|
62
|
+
dialog.append(card);
|
|
63
|
+
document.body.append(dialog);
|
|
64
|
+
dialog.addEventListener('close', () => dialog.remove());
|
|
65
|
+
cancel.addEventListener('click', () => dialog.close());
|
|
66
|
+
go.addEventListener('click', async () => {
|
|
67
|
+
go.disabled = true;
|
|
68
|
+
cancel.disabled = true;
|
|
69
|
+
go.textContent = t('Turning off…');
|
|
70
|
+
reply.textContent = '';
|
|
71
|
+
let answer = null;
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch('/api/madre/shutdown', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
|
|
74
|
+
answer = await response.json().catch(() => ({}));
|
|
75
|
+
if (!response.ok) throw new Error(answer.error ?? `HTTP ${response.status}`);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
reply.textContent = error.message;
|
|
78
|
+
go.disabled = false;
|
|
79
|
+
cancel.disabled = false;
|
|
80
|
+
go.textContent = t('Turn off');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
dialog.close();
|
|
84
|
+
showOff(answer?.stuck ?? []);
|
|
85
|
+
});
|
|
86
|
+
dialog.showModal();
|
|
87
|
+
cancel.focus();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// What stays on screen once MADRE is off.
|
|
91
|
+
function showOff(stuck) {
|
|
92
|
+
const screen = node('div', 'madre-off');
|
|
93
|
+
screen.setAttribute('role', 'status');
|
|
94
|
+
const mark = node('span', 'madre-off-mark');
|
|
95
|
+
mark.innerHTML = POWER_ICON; // trusted: our own SVG string
|
|
96
|
+
screen.append(mark, node('h1', null, t('MADRE is off')), node('p', null, t('You can close this tab. To use it again, open MADRE the way you always do.')));
|
|
97
|
+
if (stuck.length) screen.append(node('p', 'madre-off-stuck', t('These projects did not answer and may still be open: {list}. Open MADRE again and close them from Home.', { list: stuck.join(', ') })));
|
|
98
|
+
document.body.append(screen);
|
|
99
|
+
document.documentElement.classList.add('madre-is-off');
|
|
100
|
+
}
|
package/public/resay.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// What MADRE said in another language, said again.
|
|
2
4
|
//
|
|
3
5
|
// A failure is recorded in the ledger with the sentence MADRE wrote at the time, and a room that
|
|
@@ -41,6 +43,7 @@ const SAID = [
|
|
|
41
43
|
'{n} agents are working at once. If this is not what you asked for, type STOPALL.',
|
|
42
44
|
'@{agent} is already answering another turn; answers may cross.',
|
|
43
45
|
' STOPALL halts everything.',
|
|
46
|
+
'The copilot ran commands by itself {n} times in a row; the next ones wait for you. Write to it to go on.',
|
|
44
47
|
'The plan by @{agent} has been running for {n} min.',
|
|
45
48
|
' STOPALL halts it.',
|
|
46
49
|
'@{holder} still holds EDIT; @{agent} answers read-only this turn.',
|
|
@@ -55,11 +58,19 @@ const SAID = [
|
|
|
55
58
|
'@{agent} is capped at #{n} {label}; raise its MAX MODE to #{wanted} in CONNECTIONS to arm {mode}.',
|
|
56
59
|
'@{agent} already holds {mode} of this project; one holder at a time. STOPALL revokes it.',
|
|
57
60
|
'@{agent} left the project: Ollama is not running. Start Ollama, or open Local AI in Modules.',
|
|
61
|
+
'@madre is in the project · {version}',
|
|
62
|
+
'{agent} is not installed on this computer.',
|
|
63
|
+
'@madre left the project: Ollama has no chat model running',
|
|
58
64
|
'@{agent} left the room: Ollama is not running. Start Ollama, or open Ollama in Modules.',
|
|
59
65
|
'Google reported the model as unavailable (HTTP 503) and the CLI kept retrying.',
|
|
60
66
|
'Try again shortly or choose another model.',
|
|
61
67
|
'Google says the AI Studio project behind this Gemini key has no prepaid credits left; every request is refused (HTTP 429) until it is topped up.',
|
|
62
68
|
'Add credits at https://ai.studio/projects, or switch the Gemini CLI to another key.',
|
|
69
|
+
'Google no longer accepts this Gemini sign-in (IneligibleTierError): the free personal Google account is refused before it reaches a model.',
|
|
70
|
+
'Paste a Gemini API key from Google AI Studio in Settings › Agents › Gemini, or run `gemini`, type /auth and pick "Use Gemini API key".',
|
|
71
|
+
'Wait a minute, or pick an explicit model such as gemini-3-flash-preview to skip the router; check the key\'s quota at aistudio.google.com.',
|
|
72
|
+
'Gemini tried to hand this task to one of its own subagents. MADRE does not allow that: a subagent answers outside the permission MADRE controls, so nothing it wrote would be checked, shown or undoable.',
|
|
73
|
+
'Ask again saying it should do the work itself, without subagents.',
|
|
63
74
|
'Google rejected the Gemini credentials.',
|
|
64
75
|
'Run `gemini` and use /auth, or check GEMINI_API_KEY.',
|
|
65
76
|
'exhausted',
|
package/public/review-ask.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
1
3
|
// Asking for a star or a review (2026-10-10), in the room and in Home: the server keeps when to
|
|
2
4
|
// ask (src/review-ask.mjs); this page says once a minute that it is in view, and when a moment is
|
|
3
5
|
// due it asks — only when nothing is going on: no turn running, nothing half written, no other
|