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.
Files changed (58) hide show
  1. package/CREDITS.md +80 -0
  2. package/LICENSE +21 -0
  3. package/README.md +289 -0
  4. package/apreton/README.md +54 -0
  5. package/apreton/handshake.md +33 -0
  6. package/apreton/headless.md +58 -0
  7. package/apreton/mcp.md +221 -0
  8. package/apreton/navegador.md +58 -0
  9. package/bin/instalacion.js +148 -0
  10. package/bin/ratacode.js +1251 -0
  11. package/fabrica/settings.yaml +247 -0
  12. package/mcp/README.md +253 -0
  13. package/mcp/bin/ratacode-mcp.js +215 -0
  14. package/mcp/lib/actividad.js +49 -0
  15. package/mcp/lib/casa.js +126 -0
  16. package/mcp/lib/claves.js +148 -0
  17. package/mcp/lib/espacios.js +117 -0
  18. package/mcp/lib/http.js +184 -0
  19. package/mcp/lib/lectura.js +210 -0
  20. package/mcp/lib/modelos.js +235 -0
  21. package/mcp/lib/nucleo.js +332 -0
  22. package/mcp/lib/registro.js +21 -0
  23. package/mcp/lib/seguridad.js +283 -0
  24. package/mcp/lib/servidor.js +397 -0
  25. package/mcp/lib/tareas.js +406 -0
  26. package/mcp/package.json +22 -0
  27. package/mcp/tunel.mjs +209 -0
  28. package/modos/arquitecto/agent.cordis.yml +120 -0
  29. package/modos/arquitecto/preset.yml +3 -0
  30. package/modos/capataz/agent.cordis.yml +120 -0
  31. package/modos/capataz/preset.yml +3 -0
  32. package/modos/faro/agent.cordis.yml +120 -0
  33. package/modos/faro/preset.yml +3 -0
  34. package/modos/gepeto/agent.cordis.yml +191 -0
  35. package/modos/gepeto/preset.yml +3 -0
  36. package/modos/hero/agent.cordis.yml +198 -0
  37. package/modos/hero/preset.yml +3 -0
  38. package/modos/modo-rata/agent.cordis.yml +198 -0
  39. package/modos/modo-rata/preset.yml +3 -0
  40. package/modos/nex/agent.cordis.yml +213 -0
  41. package/modos/nex/preset.yml +3 -0
  42. package/modos/nex/skills/cordis-plugin-development/SKILL.md +420 -0
  43. package/modos/nex/skills/editing-cordis-compositions/SKILL.md +165 -0
  44. package/modos/pix/agent.cordis.yml +207 -0
  45. package/modos/pix/preset.yml +3 -0
  46. package/modos/tirita/agent.cordis.yml +130 -0
  47. package/modos/tirita/preset.yml +3 -0
  48. package/package.json +49 -0
  49. package/piel/activos/ratacode-emblema.svg +14 -0
  50. package/piel/activos/ratacode-es.js +1331 -0
  51. package/piel/activos/ratacode-identidad.css +50 -0
  52. package/piel/activos/ratacode-piel.css +217 -0
  53. package/piel/activos/ratacode-piel.js +487 -0
  54. package/piel/activos/ratacode-vida.js +424 -0
  55. package/piel/cordis.patch.yml +6 -0
  56. package/piel/lib/cliente.js +1020 -0
  57. package/piel/lib/index.js +1230 -0
  58. 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
+ })();
@@ -0,0 +1,6 @@
1
+ # La capa de RATACODE sobre el perfil web de DSH.
2
+ # Una sola fila: la piel. Se monta sobre el árbol del motor y engancha en el
3
+ # index que sirve dsh-host-frontend-static (ver lib/index.js).
4
+ - insert:
5
+ - id: ratacode-piel
6
+ name: ratacode-piel