ratacode 0.2.5
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/CREDITS.md +80 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/apreton/README.md +54 -0
- package/apreton/handshake.md +33 -0
- package/apreton/headless.md +58 -0
- package/apreton/mcp.md +221 -0
- package/apreton/navegador.md +58 -0
- package/bin/instalacion.js +148 -0
- package/bin/ratacode.js +1251 -0
- package/fabrica/settings.yaml +247 -0
- package/mcp/README.md +253 -0
- package/mcp/bin/ratacode-mcp.js +215 -0
- package/mcp/lib/actividad.js +49 -0
- package/mcp/lib/casa.js +126 -0
- package/mcp/lib/claves.js +148 -0
- package/mcp/lib/espacios.js +117 -0
- package/mcp/lib/http.js +184 -0
- package/mcp/lib/lectura.js +210 -0
- package/mcp/lib/modelos.js +235 -0
- package/mcp/lib/nucleo.js +332 -0
- package/mcp/lib/registro.js +21 -0
- package/mcp/lib/seguridad.js +283 -0
- package/mcp/lib/servidor.js +397 -0
- package/mcp/lib/tareas.js +406 -0
- package/mcp/package.json +22 -0
- package/mcp/tunel.mjs +209 -0
- package/modos/arquitecto/agent.cordis.yml +120 -0
- package/modos/arquitecto/preset.yml +3 -0
- package/modos/capataz/agent.cordis.yml +120 -0
- package/modos/capataz/preset.yml +3 -0
- package/modos/faro/agent.cordis.yml +120 -0
- package/modos/faro/preset.yml +3 -0
- package/modos/gepeto/agent.cordis.yml +191 -0
- package/modos/gepeto/preset.yml +3 -0
- package/modos/hero/agent.cordis.yml +198 -0
- package/modos/hero/preset.yml +3 -0
- package/modos/modo-rata/agent.cordis.yml +198 -0
- package/modos/modo-rata/preset.yml +3 -0
- package/modos/nex/agent.cordis.yml +213 -0
- package/modos/nex/preset.yml +3 -0
- package/modos/nex/skills/cordis-plugin-development/SKILL.md +420 -0
- package/modos/nex/skills/editing-cordis-compositions/SKILL.md +165 -0
- package/modos/pix/agent.cordis.yml +207 -0
- package/modos/pix/preset.yml +3 -0
- package/modos/tirita/agent.cordis.yml +130 -0
- package/modos/tirita/preset.yml +3 -0
- package/package.json +49 -0
- package/piel/activos/ratacode-emblema.svg +14 -0
- package/piel/activos/ratacode-es.js +1331 -0
- package/piel/activos/ratacode-identidad.css +50 -0
- package/piel/activos/ratacode-piel.css +217 -0
- package/piel/activos/ratacode-piel.js +487 -0
- package/piel/activos/ratacode-vida.js +424 -0
- package/piel/cordis.patch.yml +6 -0
- package/piel/lib/cliente.js +1020 -0
- package/piel/lib/index.js +1230 -0
- package/piel/package.json +36 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
RATACODE · «vida» (13-sep-2026 · chispa liviana)
|
|
3
|
+
La nube de chispas: un efecto propio de RATACODE (canvas fino, sin dependencias).
|
|
4
|
+
Va DETRÁS de las respuestas, nunca dentro de la caja de escribir: las chispas
|
|
5
|
+
forman el emblema de RATACODE, se apartan cuando pasa el ratón y vuelven a su
|
|
6
|
+
sitio con un muelle. No recoge clics: no estorba al escribir.
|
|
7
|
+
Va lo más liviana posible SIN perder la gracia:
|
|
8
|
+
· la mitad de chispas (objetivo 700, tope 900, sueltas 20 por cada 945 px)
|
|
9
|
+
· lienzo a dpr 1 y brillo sólo con la chispa revuelta
|
|
10
|
+
· 15 imágenes/s en reposo; 60 mientras hay uso (ratón o teclado) y 3 s más
|
|
11
|
+
· UN solo vigilante (un reloj de 500 ms) en vez de tres redes que se pisaban
|
|
12
|
+
· una sola rata: la de RATACODE (fuera nombres e identidades de la casa)
|
|
13
|
+
Se apaga con «reducir movimiento» y se pausa con la pestaña oculta.
|
|
14
|
+
============================================================ */
|
|
15
|
+
(() => {
|
|
16
|
+
const MARCA = 'mr-vida'; // lo esperan ratacode-piel.css:84-89
|
|
17
|
+
window.__mrVidaDispose?.();
|
|
18
|
+
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
|
19
|
+
const tactil = matchMedia('(hover: none)').matches;
|
|
20
|
+
const eventos = new AbortController();
|
|
21
|
+
const VELOCIDAD = 0.8; // Mismo movimiento, con el reloj un 20 % más lento.
|
|
22
|
+
/* (d) Dos frecuencias, una sola velocidad. Con uso (ratón moviéndose dentro o
|
|
23
|
+
tecla pulsada) se dibuja a 60; sin uso, a 15. El reloj de la animación sigue
|
|
24
|
+
contando tiempo de verdad, así que esto cambia la FRECUENCIA, no el
|
|
25
|
+
movimiento: lo que se aprobó no se toca. Los dos umbrales van un pelín POR
|
|
26
|
+
DEBAJO del salto del monitor a propósito: clavados en 16,67 ms el reloj
|
|
27
|
+
empata con el salto y se pierde un fotograma de cada dos (medido en el banco:
|
|
28
|
+
salía a 43,6 imágenes en vez de 60). Con 1000/17 = 58,8 ms el reposo cae en el
|
|
29
|
+
4.º salto de un monitor de 60 Hz (66,7 ms) y en el 9.º de uno de 144 → 15
|
|
30
|
+
imágenes por segundo en ambos. Y 3 s de cortesía tras el último uso, para que
|
|
31
|
+
el regreso con muelle se vea entero y no a saltos. */
|
|
32
|
+
const CUADRO = 1000 / 61, CUADRO_EN_REPOSO = 1000 / 17, USO = 3000, PASO_MAX = 1000 / 12;
|
|
33
|
+
|
|
34
|
+
/* ---------- los dos tintes: el rosa y el amarillo de la casa ---------- */
|
|
35
|
+
const raiz = getComputedStyle(document.documentElement);
|
|
36
|
+
function cssColor(nombre, defecto) {
|
|
37
|
+
const v = (raiz.getPropertyValue(nombre) || '').trim();
|
|
38
|
+
return /^#[0-9a-f]{6}$/i.test(v) ? v : defecto;
|
|
39
|
+
}
|
|
40
|
+
const TINTAS = {
|
|
41
|
+
personal: cssColor('--mr-pink', '#ff268e'),
|
|
42
|
+
amarillo: cssColor('--mr-yellow', '#e4f226')
|
|
43
|
+
};
|
|
44
|
+
const NOMBRE_RATA = 'RATACODE'; // la única rata: no hay personajes
|
|
45
|
+
|
|
46
|
+
/* ---------- la luz redonda que va detrás de cada chispa ---------- */
|
|
47
|
+
function brillo(rgb) {
|
|
48
|
+
const c = document.createElement('canvas'); c.width = c.height = 24;
|
|
49
|
+
const g = c.getContext('2d'), gr = g.createRadialGradient(12, 12, 0, 12, 12, 12);
|
|
50
|
+
gr.addColorStop(0, `rgba(${rgb},0.55)`); gr.addColorStop(0.35, `rgba(${rgb},0.18)`); gr.addColorStop(1, `rgba(${rgb},0)`);
|
|
51
|
+
g.fillStyle = gr; g.fillRect(0, 0, 24, 24); return c;
|
|
52
|
+
}
|
|
53
|
+
// Ratita entera vista desde el cielo: lomo, orejas, hocico y cola curva.
|
|
54
|
+
// Las 16 orientaciones se dibujan UNA vez; el movimiento reutiliza sellos.
|
|
55
|
+
function ratita(color, giro) {
|
|
56
|
+
const c = document.createElement('canvas'); c.width = c.height = 64;
|
|
57
|
+
const g = c.getContext('2d'); g.translate(32, 32); g.rotate(giro);
|
|
58
|
+
g.fillStyle = g.strokeStyle = color; g.lineCap = 'round';
|
|
59
|
+
g.lineWidth = 2.4;
|
|
60
|
+
g.beginPath(); g.moveTo(0, 11); g.bezierCurveTo(-9, 19, 13, 19, 4, 28); g.stroke();
|
|
61
|
+
g.beginPath(); g.ellipse(0, 2, 8, 12, 0, 0, Math.PI * 2); g.fill();
|
|
62
|
+
g.beginPath(); g.moveTo(-6, -7); g.quadraticCurveTo(-5, -15, 0, -23);
|
|
63
|
+
g.quadraticCurveTo(5, -15, 6, -7); g.closePath(); g.fill();
|
|
64
|
+
g.beginPath(); g.arc(-6, -9, 3.6, 0, Math.PI * 2); g.arc(6, -9, 3.6, 0, Math.PI * 2); g.fill();
|
|
65
|
+
g.lineWidth = 2;
|
|
66
|
+
g.beginPath(); g.moveTo(-6, -1); g.lineTo(-10, 2); g.moveTo(6, -1); g.lineTo(10, 2);
|
|
67
|
+
g.moveTo(-6, 9); g.lineTo(-10, 12); g.moveTo(6, 9); g.lineTo(10, 12); g.stroke();
|
|
68
|
+
return c;
|
|
69
|
+
}
|
|
70
|
+
/* Una sola rata, así que los sellos se hacen una vez al arrancar y ya está.
|
|
71
|
+
Antes había un `Map` con hasta 4 juegos (uno por personaje de la casa) y un
|
|
72
|
+
chequeo de identidad cada 150 ms que leía `localStorage` y la lista de la casa. */
|
|
73
|
+
const BRILLO = {}, RATITA = {};
|
|
74
|
+
for (const [tinte, hex] of Object.entries(TINTAS)) {
|
|
75
|
+
BRILLO[tinte] = brillo([1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16)).join(','));
|
|
76
|
+
RATITA[tinte] = Array.from({ length: 16 }, (_, i) => ratita(hex, i * Math.PI / 8));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let caja = null, lienzo = null, ctx = null;
|
|
80
|
+
let W = 0, H = 0, parts = [], ambiente = [], vivo = true, emblema = null, forma = 'RATACODE', ultimoCuadro = 0;
|
|
81
|
+
let tiempo = 0, cargaEmblema = 0, ultimoUso = -1e9, ultimaOla = 0, fotograma = 0;
|
|
82
|
+
const partesDelDia = []; // la lista de trabajo, reutilizada vuelta tras vuelta (c)
|
|
83
|
+
const raton = { x: -9999, y: -9999, px: -9999, py: -9999, activo: false, vel: 0 };
|
|
84
|
+
const vigia = new ResizeObserver(() => { clearTimeout(vigia._t); vigia._t = setTimeout(() => medir(true), 200); });
|
|
85
|
+
|
|
86
|
+
/* ---------- (a) si no se ve, no se trabaja; (b) si se ve a medias, sólo esa parte ----------
|
|
87
|
+
Tres formas de no verse: la pestaña oculta (`document.hidden`), el lienzo sin
|
|
88
|
+
caja o invisible (`checkVisibility`) y el lienzo fuera de la ventana. La
|
|
89
|
+
comprobación cara se hace desde EL UNICO vigilante, cada 500 ms, y no en cada
|
|
90
|
+
vuelta: leer estilos y rectángulos fuerza el cálculo de la página, y eso sí
|
|
91
|
+
que atasca. Parar es cancelar el fotograma Y no volver a pedirlo. */
|
|
92
|
+
let enPantalla = true, recorteVisible = null;
|
|
93
|
+
function puestoEnPantalla() {
|
|
94
|
+
if (!caja || !caja.isConnected || !lienzo || !lienzo.isConnected) return null;
|
|
95
|
+
if (typeof lienzo.checkVisibility === 'function' &&
|
|
96
|
+
!lienzo.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) return null;
|
|
97
|
+
const r = lienzo.getBoundingClientRect();
|
|
98
|
+
return r.width > 1 && r.height > 1 ? r : null;
|
|
99
|
+
}
|
|
100
|
+
/* R21 · el tema MINIMAL va sin ratitas de fondo: ni se pinta el lienzo (lo
|
|
101
|
+
esconde el CSS) ni se le da una vuelta de reloj (esto). Se mira el
|
|
102
|
+
documento cada vez, así que cambiar de tema en Ajustes para y arranca el
|
|
103
|
+
fotograma sin recargar nada. */
|
|
104
|
+
function conRatitas() { return document.documentElement.dataset.ratacodeTema !== 'minimal'; }
|
|
105
|
+
function puedeVerse() { return !document.hidden && enPantalla && conRatitas(); }
|
|
106
|
+
function parar() {
|
|
107
|
+
cancelAnimationFrame(fotograma); fotograma = 0; ultimoCuadro = 0;
|
|
108
|
+
}
|
|
109
|
+
/* Un solo vigilante para los tres oficios que antes iban en relojes sueltos
|
|
110
|
+
(250 ms al dormirse, 300 ms dentro del fotograma y 1 500 ms para el remonte,
|
|
111
|
+
más 150 ms de identidad): mirar si el hueco sigue en su sitio y remontarse si
|
|
112
|
+
la casa cambió, leer qué banda se ve, y arrancar o parar el fotograma. Con la
|
|
113
|
+
pestaña oculta no toca el DOM: `document.hidden` no fuerza maquetación. */
|
|
114
|
+
function vigilar() {
|
|
115
|
+
if (!vivo) return;
|
|
116
|
+
if (document.hidden) { enPantalla = false; recorteVisible = null; parar(); return; }
|
|
117
|
+
const destino = document.querySelector('[class*="_scrollBody"]')?.parentElement;
|
|
118
|
+
if (caja !== destino || (destino && !lienzo?.isConnected)) montar();
|
|
119
|
+
const r = puestoEnPantalla();
|
|
120
|
+
if (!r) { enPantalla = false; recorteVisible = null; parar(); return; }
|
|
121
|
+
enPantalla = true;
|
|
122
|
+
/* La banda que de verdad se ve, en coordenadas del lienzo. Si se ve entero,
|
|
123
|
+
no hay recorte que valga: ni una cuenta de más. */
|
|
124
|
+
const arriba = Math.max(r.top, 0), abajo = Math.min(r.bottom, window.innerHeight);
|
|
125
|
+
const izquierda = Math.max(r.left, 0), derecha = Math.min(r.right, window.innerWidth);
|
|
126
|
+
const completo = arriba <= r.top + 0.5 && abajo >= r.bottom - 0.5 &&
|
|
127
|
+
izquierda <= r.left + 0.5 && derecha >= r.right - 0.5;
|
|
128
|
+
recorteVisible = completo ? null : { x: izquierda - r.left, y: arriba - r.top, w: Math.max(0, derecha - izquierda), h: Math.max(0, abajo - arriba) };
|
|
129
|
+
if (!W || !H) medir(true);
|
|
130
|
+
programar();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sacarEmblema() {
|
|
134
|
+
const carga = ++cargaEmblema;
|
|
135
|
+
const valor = getComputedStyle(document.documentElement).getPropertyValue('--mr-emblema') || '';
|
|
136
|
+
// La identidad usa SVG percent-encoded, no necesariamente base64.
|
|
137
|
+
const m = valor.trim().match(/^url\(["']?(data:image\/svg\+xml[^"')]+)["']?\)$/);
|
|
138
|
+
if (!m) return;
|
|
139
|
+
const img = new Image();
|
|
140
|
+
img.onload = () => { if (vivo && carga === cargaEmblema) { emblema = img; medir(true); } };
|
|
141
|
+
img.onerror = () => { if (vivo && carga === cargaEmblema) { emblema = null; medir(true); } };
|
|
142
|
+
img.src = m[1];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* ---------- medir el hueco y sembrar las chispas ---------- */
|
|
146
|
+
function medir(forzar) {
|
|
147
|
+
if (!vivo || !caja || !lienzo || !caja.isConnected) return;
|
|
148
|
+
const r = caja.getBoundingClientRect();
|
|
149
|
+
const asiento = caja.querySelector('[class*="_composerSeat"]') || document.querySelector('[data-composer-card]');
|
|
150
|
+
const entrada = asiento?.getBoundingClientRect();
|
|
151
|
+
const alto = entrada && entrada.top > r.top ? Math.min(r.height, entrada.top - r.top) : r.height;
|
|
152
|
+
if (!r.width || alto < 24) { W = H = 0; return; }
|
|
153
|
+
if (!forzar && Math.abs(r.width - W) < 2 && Math.abs(alto - H) < 2 && parts.length) return;
|
|
154
|
+
const dpr = 1; // dpr 1: el 44 % de los píxeles que a 1,5 (1/1,5²); el navegador ya escala el CSS
|
|
155
|
+
W = r.width; H = alto;
|
|
156
|
+
lienzo.width = Math.round(W * dpr); lienzo.height = Math.round(H * dpr);
|
|
157
|
+
lienzo.style.width = W + 'px'; lienzo.style.height = H + 'px';
|
|
158
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
159
|
+
ambiente = [];
|
|
160
|
+
|
|
161
|
+
parts = [];
|
|
162
|
+
/* 1 · la nube con forma del emblema: se dibuja a escondidas y se mira qué píxel está lleno */
|
|
163
|
+
{
|
|
164
|
+
const lado = Math.min(H * 0.90, W * 0.78, 740);
|
|
165
|
+
const tmp = document.createElement('canvas');
|
|
166
|
+
tmp.width = Math.round(W); tmp.height = Math.round(H);
|
|
167
|
+
const tc = tmp.getContext('2d');
|
|
168
|
+
if (emblema) {
|
|
169
|
+
forma = 'logo';
|
|
170
|
+
tc.drawImage(emblema, (W - lado) / 2, (H - lado) / 2, lado, lado);
|
|
171
|
+
} else {
|
|
172
|
+
forma = 'RATACODE';
|
|
173
|
+
const marca = document.querySelector('.mr-dsh-word');
|
|
174
|
+
const familia = marca ? getComputedStyle(marca).fontFamily : '"Segoe UI", sans-serif';
|
|
175
|
+
tc.font = `italic 900 100px ${familia}`;
|
|
176
|
+
const tam = Math.min(H * 0.48, W * 0.9 / tc.measureText('RATACODE').width * 100);
|
|
177
|
+
tc.font = `italic 900 ${tam}px ${familia}`;
|
|
178
|
+
tc.textAlign = 'center'; tc.textBaseline = 'middle'; tc.fillStyle = '#ffffff';
|
|
179
|
+
tc.fillText('RATACODE', W / 2, H / 2);
|
|
180
|
+
}
|
|
181
|
+
const datos = tc.getImageData(0, 0, tmp.width, tmp.height).data;
|
|
182
|
+
// Sólo trazos luminosos: la cara negra del SVG no se convierte en una mancha.
|
|
183
|
+
const trazo = k => datos[k + 3] > 128 && Math.max(datos[k], datos[k + 1], datos[k + 2]) > 105;
|
|
184
|
+
let llenos = 0;
|
|
185
|
+
for (let i = 0; i < datos.length; i += 16) if (trazo(i)) llenos++;
|
|
186
|
+
const objetivo = tactil ? 500 : 700;
|
|
187
|
+
const paso = Math.max(3, Math.ceil(Math.sqrt((llenos * 4) / objetivo)));
|
|
188
|
+
for (let y = 0; y < tmp.height; y += paso) {
|
|
189
|
+
for (let x = 0; x < tmp.width; x += paso) {
|
|
190
|
+
const k = (y * tmp.width + x) * 4;
|
|
191
|
+
if (!trazo(k)) continue;
|
|
192
|
+
// Color propio a la izquierda; el MISMO amarillo de la casa a la derecha.
|
|
193
|
+
const color = x < W / 2 ? 'personal' : 'amarillo';
|
|
194
|
+
const hx = x + (Math.random() - 0.5) * paso * 0.9, hy = y + (Math.random() - 0.5) * paso * 0.9;
|
|
195
|
+
parts.push({
|
|
196
|
+
hx, hy, x: hx + (Math.random() - 0.5) * W * 0.5, y: hy + (Math.random() - 0.5) * H * 0.9, vx: 0, vy: 0,
|
|
197
|
+
masa: 0.45 + Math.random() * 2.2, muelle: 0.006 + Math.random() * 0.034,
|
|
198
|
+
roce: 0.86 + Math.random() * 0.1, fase: Math.random() * Math.PI * 2,
|
|
199
|
+
amp: 0.6 + Math.random() * 1.8, r: 0.7 + Math.random() * 0.8, color, grupo: 0, rumbo: Math.floor(Math.random() * 16)
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/* 2 · chispas sueltas, con más presencia en los lados: donde no hay texto, más nítidas */
|
|
205
|
+
if (parts.length > 900) parts = parts.filter((_, i) => i % Math.ceil(parts.length / 900) === 0);
|
|
206
|
+
const nSueltas = Math.min(40, Math.round((W / 945) * (tactil ? 14 : 20)));
|
|
207
|
+
for (let i = 0; i < nSueltas; i++) {
|
|
208
|
+
const enElLado = Math.random() < 0.7;
|
|
209
|
+
const hx = enElLado
|
|
210
|
+
? (Math.random() < 0.5 ? Math.random() * W * 0.17 : W * 0.83 + Math.random() * W * 0.17)
|
|
211
|
+
: Math.random() * W;
|
|
212
|
+
const hy = Math.random() * H;
|
|
213
|
+
ambiente.push({
|
|
214
|
+
hx, hy, x: hx, y: hy, vx: 0, vy: 0, grupo: -1, masa: 1, rumbo: Math.floor(Math.random() * 16),
|
|
215
|
+
fase: Math.random() * Math.PI * 2, amp: 3 + Math.random() * 10,
|
|
216
|
+
r: 0.7 + Math.random() * 1.1, alfa: 0.3 + Math.random() * 0.35,
|
|
217
|
+
color: Math.random() < 0.5 ? 'personal' : 'amarillo',
|
|
218
|
+
muelle: 0.01 + Math.random() * 0.02, roce: 0.9 + Math.random() * 0.06
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ---------- el empujón: el ratón abre un círculo que las aparta ---------- */
|
|
224
|
+
function empujar(p, mx, my, alcance, fuerza) {
|
|
225
|
+
const ddx = p.x - mx, ddy = p.y - my, d2 = ddx * ddx + ddy * ddy;
|
|
226
|
+
if (d2 >= alcance * alcance) return;
|
|
227
|
+
const d = Math.sqrt(d2) || 1, f = 1 - d / alcance;
|
|
228
|
+
const golpe = ((f * f * fuerza) / p.masa) * (0.35 + Math.random() * 1.5);
|
|
229
|
+
const ang = Math.atan2(ddy, ddx) + (Math.random() - 0.5) * 1.6;
|
|
230
|
+
p.vx += Math.cos(ang) * golpe; p.vy += Math.sin(ang) * golpe;
|
|
231
|
+
}
|
|
232
|
+
/* Teclear también revuelve la nube: un empujón suave desde un punto al azar,
|
|
233
|
+
como mucho uno cada 120 ms. Cuesta una pasada por tecla, no un cálculo en
|
|
234
|
+
cada fotograma, y es lo que hace que se NOTE que alguien escribe. */
|
|
235
|
+
function revolver(t) {
|
|
236
|
+
if (!W || !parts.length || t - ultimaOla < 120) return;
|
|
237
|
+
ultimaOla = t;
|
|
238
|
+
const mx = W * (0.3 + Math.random() * 0.4), my = H * (0.3 + Math.random() * 0.4);
|
|
239
|
+
for (const p of parts) empujar(p, mx, my, 190, 12);
|
|
240
|
+
for (const p of ambiente) empujar(p, mx, my, 190, 3);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Un solo dueño del próximo cuadro. Programarlo también en pasoDentro
|
|
244
|
+
// duplicaba la cola en cada vuelta hasta congelar toda la conversación.
|
|
245
|
+
function programar() {
|
|
246
|
+
if (!vivo || fotograma) return;
|
|
247
|
+
if (!puedeVerse()) { parar(); return; } // (a): sin verlo, ni una vuelta de reloj
|
|
248
|
+
fotograma = requestAnimationFrame(paso);
|
|
249
|
+
}
|
|
250
|
+
function paso(t) {
|
|
251
|
+
fotograma = 0;
|
|
252
|
+
if (!vivo) return;
|
|
253
|
+
if (!puedeVerse()) { parar(); return; }
|
|
254
|
+
// Física como la referencia a 60 Hz; no acelerar en monitores de 144/240 Hz.
|
|
255
|
+
// (d) Con uso (ratón moviéndose dentro o tecla pulsada) se va a 60 y durante
|
|
256
|
+
// los 3 s siguientes; sin uso, a 15. El ratón QUIETO dentro ya no sube el
|
|
257
|
+
// reloj: no hay nada que seguir.
|
|
258
|
+
const enUso = t - ultimoUso < USO;
|
|
259
|
+
if (t - ultimoCuadro < (enUso ? CUADRO : CUADRO_EN_REPOSO)) { programar(); return; }
|
|
260
|
+
const delta = Math.min(ultimoCuadro ? t - ultimoCuadro : CUADRO, PASO_MAX) * VELOCIDAD;
|
|
261
|
+
ultimoCuadro = t;
|
|
262
|
+
tiempo += delta;
|
|
263
|
+
try { pasoDentro(tiempo, delta / (1000 / 60), enUso); }
|
|
264
|
+
catch (e) { window.__mrVidaFallo = String(e && e.stack || e).slice(0, 400); }
|
|
265
|
+
programar();
|
|
266
|
+
}
|
|
267
|
+
function pasoDentro(t, dt, enUso) {
|
|
268
|
+
if (!lienzo || !lienzo.isConnected) montar();
|
|
269
|
+
if (!ctx || !W) { return; }
|
|
270
|
+
|
|
271
|
+
const vel = Math.hypot(raton.x - raton.px, raton.y - raton.py);
|
|
272
|
+
raton.px = raton.x; raton.py = raton.y;
|
|
273
|
+
raton.vel += (Math.min(vel, 40) - raton.vel) * (1 - Math.pow(0.85, dt));
|
|
274
|
+
const pulso = (0.5 + 0.5 * Math.sin(t / 620)) * (0.7 + 0.3 * Math.sin(t / 1730 + 1.3));
|
|
275
|
+
const ALCANCE = Math.min(250, 80 + 140 * pulso + raton.vel * 1.2);
|
|
276
|
+
const mx = raton.x, my = raton.y;
|
|
277
|
+
|
|
278
|
+
ctx.clearRect(0, 0, W, H);
|
|
279
|
+
|
|
280
|
+
/* (b) sólo se dibuja lo que está en la vista. La física la sigue haciendo
|
|
281
|
+
todo el mundo —una chispa de fuera tiene que estar en su sitio al
|
|
282
|
+
volver—, pero el pincel no gasta en lo que nadie mira. */
|
|
283
|
+
const vx0 = (recorteVisible ? recorteVisible.x : 0) - 34;
|
|
284
|
+
const vy0 = (recorteVisible ? recorteVisible.y : 0) - 34;
|
|
285
|
+
const vx1 = (recorteVisible ? recorteVisible.x + recorteVisible.w : W) + 34;
|
|
286
|
+
const vy1 = (recorteVisible ? recorteVisible.y + recorteVisible.h : H) + 34;
|
|
287
|
+
|
|
288
|
+
/* (c) la lista de trabajo se reutiliza: antes se hacía `parts.concat(ambiente)`
|
|
289
|
+
en cada vuelta, o sea un array nuevo de 1.200 huecos sesenta veces por
|
|
290
|
+
segundo. Eso no se ve, pero se paga en basura y en paradas del recolector,
|
|
291
|
+
y el recolector es el mismo que atiende al chat. */
|
|
292
|
+
partesDelDia.length = 0;
|
|
293
|
+
for (let i = 0; i < parts.length; i++) partesDelDia[i] = parts[i];
|
|
294
|
+
for (let i = parts.length, j = 0; j < ambiente.length; i++, j++) partesDelDia[i] = ambiente[j];
|
|
295
|
+
const todos = partesDelDia;
|
|
296
|
+
// Sin comparar cada chispa con todas las demás: coste lineal y acotado.
|
|
297
|
+
// El empujón sólo corre en los cuadros de uso: a 15 imágenes el golpe valdría
|
|
298
|
+
// por cuatro (lleva el `dt` dentro) y la nube temblaría bajo un ratón quieto.
|
|
299
|
+
const empuja = raton.activo && enUso;
|
|
300
|
+
for (const p of todos) {
|
|
301
|
+
if (empuja) empujar(p, mx, my, ALCANCE, (p.grupo < 0 ? 2.2 : 30) * dt);
|
|
302
|
+
const casaX = p.hx + Math.sin(t / 1100 + p.fase) * p.amp;
|
|
303
|
+
const casaY = p.hy + Math.cos(t / 1300 + p.fase) * p.amp;
|
|
304
|
+
p.vx += (casaX - p.x) * p.muelle * dt; p.vy += (casaY - p.y) * p.muelle * dt;
|
|
305
|
+
const roce = Math.pow(p.roce, dt);
|
|
306
|
+
p.vx *= roce; p.vy *= roce;
|
|
307
|
+
const v = Math.hypot(p.vx, p.vy);
|
|
308
|
+
if (v > 70) { p.vx *= 70 / v; p.vy *= 70 / v; }
|
|
309
|
+
p.x += p.vx * dt; p.y += p.vy * dt;
|
|
310
|
+
if (p.x < vx0 || p.x > vx1 || p.y < vy0 || p.y > vy1) continue;
|
|
311
|
+
const excitada = Math.min(1, v / 14);
|
|
312
|
+
ctx.globalAlpha = (p.alfa === undefined ? 0.5 : p.alfa) + excitada * 0.4;
|
|
313
|
+
// El halo sólo se pinta con la chispa revuelta: en reposo, un dibujo menos por chispa.
|
|
314
|
+
if (excitada > 0.04) {
|
|
315
|
+
const tam = 8 + excitada * 9;
|
|
316
|
+
ctx.drawImage(BRILLO[p.color], p.x - tam, p.y - tam, tam * 2, tam * 2);
|
|
317
|
+
}
|
|
318
|
+
// Corren de hocico, no de lado; al descansar conservan su orientación.
|
|
319
|
+
if (v > 0.8) p.rumbo = (Math.round((Math.atan2(p.vy, p.vx) + Math.PI / 2) / (Math.PI / 8)) + 16) % 16;
|
|
320
|
+
const talla = (p.grupo < 0 ? 11 : 7) + p.r * 1.3 + excitada * 10;
|
|
321
|
+
ctx.drawImage(RATITA[p.color][p.rumbo], p.x - talla / 2, p.y - talla / 2, talla, talla);
|
|
322
|
+
}
|
|
323
|
+
ctx.globalAlpha = 1;
|
|
324
|
+
/* (c) el cuaderno de a bordo no se rehace en cada vuelta: se escribe UNA vez
|
|
325
|
+
y luego sólo se refresca lo barato. El desplazamiento medio costaba
|
|
326
|
+
recorrer las mil y pico chispas en cada fotograma para un número que no se
|
|
327
|
+
ve en ninguna parte; ahora se calcula sólo si alguien lo lee (la puerta de
|
|
328
|
+
depuración, que es quien lo mira). */
|
|
329
|
+
const cuaderno = cuadernoDeABordo();
|
|
330
|
+
cuaderno.W = Math.round(W); cuaderno.H = Math.round(H);
|
|
331
|
+
cuaderno.nube = parts.length; cuaderno.sueltas = ambiente.length;
|
|
332
|
+
cuaderno.forma = forma; cuaderno.tiempo = Math.round(t);
|
|
333
|
+
cuaderno.dentro = !!(caja && caja.isConnected); cuaderno.recorte = recorteVisible;
|
|
334
|
+
cuaderno.cuadros++;
|
|
335
|
+
}
|
|
336
|
+
function cuadernoDeABordo() {
|
|
337
|
+
const guardado = window.__mrVida;
|
|
338
|
+
if (guardado && guardado.marca === MARCA) return guardado;
|
|
339
|
+
const cuaderno = {
|
|
340
|
+
W: Math.round(W), H: Math.round(H), nube: parts.length, sueltas: ambiente.length, forma,
|
|
341
|
+
particula: 'ratita-cenital', zona: 'salida', agente: NOMBRE_RATA,
|
|
342
|
+
colores: [TINTAS.personal, TINTAS.amarillo], velocidad: VELOCIDAD, tiempo: 0, cuadros: 0,
|
|
343
|
+
dentro: !!(caja && caja.isConnected), recorte: null
|
|
344
|
+
};
|
|
345
|
+
Object.defineProperty(cuaderno, 'marca', { value: MARCA, enumerable: false });
|
|
346
|
+
Object.defineProperty(cuaderno, 'desplazamiento', {
|
|
347
|
+
enumerable: true, configurable: true,
|
|
348
|
+
get() {
|
|
349
|
+
if (!parts.length) return 0;
|
|
350
|
+
let suma = 0;
|
|
351
|
+
for (const p of parts) suma += Math.hypot(p.x - p.hx, p.y - p.hy);
|
|
352
|
+
return Math.round(suma / parts.length);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
window.__mrVida = cuaderno;
|
|
356
|
+
return cuaderno;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* ---------- fondo fijo del panel de salida, separado del editor ---------- */
|
|
360
|
+
function montar() {
|
|
361
|
+
const nueva = document.querySelector('[class*="_scrollBody"]')?.parentElement;
|
|
362
|
+
if (!nueva) {
|
|
363
|
+
if (!montar.aviso) { console.warn('[ratacode] No encuentro el panel de salida: fondo pausado; revisar selectores del panel.'); montar.aviso = true; }
|
|
364
|
+
caja = null; ctx = null; W = H = 0; return;
|
|
365
|
+
}
|
|
366
|
+
montar.aviso = false;
|
|
367
|
+
const viejo = document.getElementById(MARCA);
|
|
368
|
+
if (viejo && viejo !== lienzo) viejo.remove();
|
|
369
|
+
if (caja !== nueva) caja?.classList.remove('mr-vida-salida');
|
|
370
|
+
caja = nueva; caja.classList.add('mr-vida-salida');
|
|
371
|
+
lienzo = document.getElementById(MARCA);
|
|
372
|
+
if (!lienzo || lienzo.parentElement !== caja) {
|
|
373
|
+
lienzo?.remove(); // One canvas: do not leave the previous resident's background behind.
|
|
374
|
+
lienzo = document.createElement('canvas');
|
|
375
|
+
lienzo.id = MARCA; lienzo.className = MARCA;
|
|
376
|
+
lienzo.setAttribute('aria-hidden', 'true');
|
|
377
|
+
caja.prepend(lienzo);
|
|
378
|
+
emblema = null; sacarEmblema();
|
|
379
|
+
}
|
|
380
|
+
ctx = lienzo.getContext('2d');
|
|
381
|
+
ambiente = [];
|
|
382
|
+
medir(true);
|
|
383
|
+
/* si la caja cambia de tamaño (o de forma), se siembra otra vez para que no quede torcido */
|
|
384
|
+
vigia.disconnect();
|
|
385
|
+
vigia.observe(caja);
|
|
386
|
+
const entrada = caja.querySelector('[class*="_composerSeat"]'); if (entrada) vigia.observe(entrada);
|
|
387
|
+
programar();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
addEventListener('resize', () => { clearTimeout(montar._t); montar._t = setTimeout(medir, 200); }, { signal: eventos.signal });
|
|
391
|
+
addEventListener('pointermove', e => {
|
|
392
|
+
if (!caja) return;
|
|
393
|
+
const r = caja.getBoundingClientRect();
|
|
394
|
+
if (e.clientX < r.left || e.clientX > r.left + W || e.clientY < r.top || e.clientY > r.top + H) { raton.activo = false; return; }
|
|
395
|
+
if (!raton.activo) { raton.px = e.clientX - r.left; raton.py = e.clientY - r.top; }
|
|
396
|
+
raton.x = e.clientX - r.left; raton.y = e.clientY - r.top; raton.activo = true;
|
|
397
|
+
ultimoUso = performance.now(); // (d): el ratón aquí es uso → 60 imágenes por segundo
|
|
398
|
+
}, { passive: true, signal: eventos.signal });
|
|
399
|
+
addEventListener('pointerleave', () => { raton.activo = false; }, { signal: eventos.signal });
|
|
400
|
+
addEventListener('blur', () => { raton.activo = false; }, { signal: eventos.signal });
|
|
401
|
+
addEventListener('keydown', () => { // escribir: sube el reloj y revuelve la nube
|
|
402
|
+
if (!puedeVerse()) return;
|
|
403
|
+
const t = performance.now(); ultimoUso = t; revolver(t);
|
|
404
|
+
}, { signal: eventos.signal });
|
|
405
|
+
document.addEventListener('visibilitychange', () => {
|
|
406
|
+
if (document.hidden) parar(); else vigilar();
|
|
407
|
+
}, { signal: eventos.signal });
|
|
408
|
+
|
|
409
|
+
window.__mrVidaDispose = () => {
|
|
410
|
+
vivo = false;
|
|
411
|
+
cancelAnimationFrame(fotograma); fotograma = 0;
|
|
412
|
+
clearTimeout(montar._t); clearTimeout(vigia._t); clearInterval(vigilante);
|
|
413
|
+
eventos.abort(); vigia.disconnect();
|
|
414
|
+
lienzo?.remove(); caja?.classList.remove('mr-vida-salida'); caja = lienzo = ctx = null;
|
|
415
|
+
parts = []; ambiente = [];
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
montar();
|
|
419
|
+
vigilar();
|
|
420
|
+
/* la casa se repinta sola: si la caja vuelve a montarse, el lienzo vuelve con
|
|
421
|
+
ella. Es EL UNICO reloj que queda (antes había tres). */
|
|
422
|
+
const VIGÍA_MS = 500;
|
|
423
|
+
const vigilante = setInterval(vigilar, VIGÍA_MS);
|
|
424
|
+
})();
|