@ignix-social/editor 0.1.20 → 0.1.22
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/package.json +1 -1
- package/skills/ignix-editor/SKILL.md +116 -0
- package/src/cli.mjs +26 -1
- package/src/runtime/blocks/beforeafter.js +53 -0
- package/src/runtime/blocks/character.js +55 -0
- package/src/runtime/blocks/chat.js +86 -0
- package/src/runtime/blocks/headline.js +47 -0
- package/src/runtime/blocks/infographic.js +74 -0
- package/src/runtime/blocks/ink.js +2254 -0
- package/src/runtime/blocks/kinetic.js +78 -0
- package/src/runtime/blocks/map.js +68 -0
- package/src/runtime/blocks/menu.js +69 -0
- package/src/runtime/blocks/offer.js +51 -0
- package/src/runtime/blocks/photos.js +48 -0
- package/src/runtime/blocks/quiz.js +64 -0
- package/src/runtime/blocks/reviews.js +61 -0
- package/src/runtime/blocks/slots.js +69 -0
- package/src/runtime/blocks/steps.js +49 -0
- package/src/runtime/blocks/stickman.js +310 -0
- package/src/runtime/blocks/whiteboard.js +147 -0
- package/src/update-check.mjs +81 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* stickman.js — bloque de Ignix Editor: un muñeco de palitos que habla, gesticula y anda,
|
|
3
|
+
* dibujado en SVG como función pura del tiempo (igual que IgnixFrame).
|
|
4
|
+
*
|
|
5
|
+
* Portado de dos proyectos MIT, cuyos avisos se conservan:
|
|
6
|
+
* - stickman-explainers, Copyright (c) 2026 21cabbagee (esqueleto, poses como ángulos).
|
|
7
|
+
* - stickman-ia, Copyright (c) 2026 David García (IA Que Trabaja) y colaboradores
|
|
8
|
+
* (poses think/shrug/cheer/arms_crossed/present/sad, caras paramétricas, peinados,
|
|
9
|
+
* accesorios, ciclo de andar).
|
|
10
|
+
* MIT License: se permite usar, copiar, modificar y distribuir con este aviso incluido.
|
|
11
|
+
*
|
|
12
|
+
* Uso (en el run() de IgnixFrame):
|
|
13
|
+
* const S = IgnixStickman;
|
|
14
|
+
* const hero = S.create(document.getElementById('stage-svg'), { color: '#D24D1E', hair: 'short' });
|
|
15
|
+
* run({ id, duration, fps }, (t) => {
|
|
16
|
+
* hero.draw({
|
|
17
|
+
* x: 540, ground: 1500, scale: 1.1, facing: 1,
|
|
18
|
+
* joints: S.poseAt(t, [[0, 'idle'], [1.2, 'wave'], [3, 'point']]),
|
|
19
|
+
* face: S.faceAt(t, [[0, 'neutral'], [1.2, 'happy']]),
|
|
20
|
+
* mouth: S.talkAt(t, [[0.4, 2.6], [3.1, 5.0]]), // tramos en los que habla (su voz)
|
|
21
|
+
* eyesOpen: S.blinkAt(t),
|
|
22
|
+
* });
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
(function () {
|
|
26
|
+
const DEG = Math.PI / 180;
|
|
27
|
+
const BONES = { torso: 178, neck: 20, head_r: 60, upper_arm: 98, forearm: 92, thigh: 118, shin: 112, shoulder_drop: 16, foot: 26, shoulder_w: 12 };
|
|
28
|
+
const LINE = 11;
|
|
29
|
+
const DEFAULT = { spine: -90, neck: 0, l_sh: 104, l_el: 8, r_sh: 76, r_el: -8, l_hip: 97, l_knee: 0, r_hip: 83, r_knee: 0 };
|
|
30
|
+
const JOINT_KEYS = Object.keys(DEFAULT);
|
|
31
|
+
|
|
32
|
+
const proj = (o, a, len) => [o[0] + Math.cos(a * DEG) * len, o[1] + Math.sin(a * DEG) * len];
|
|
33
|
+
|
|
34
|
+
// Cinemática directa: cadera en (0,0), mirando a la derecha. Ángulos en grados (0 = derecha, 90 = abajo).
|
|
35
|
+
function forward(j) {
|
|
36
|
+
const g = (k) => (j[k] ?? DEFAULT[k]);
|
|
37
|
+
const hip = [0, 0];
|
|
38
|
+
const spine = g('spine');
|
|
39
|
+
const chest = proj(hip, spine, BONES.torso);
|
|
40
|
+
const shoulder = proj(hip, spine, BONES.torso - BONES.shoulder_drop);
|
|
41
|
+
const neckA = spine + g('neck');
|
|
42
|
+
const headBase = proj(chest, neckA, BONES.neck);
|
|
43
|
+
const head = proj(headBase, neckA, BONES.head_r);
|
|
44
|
+
const p = { hip, chest, shoulder, head_base: headBase, head, head_tilt: neckA + 90 };
|
|
45
|
+
const ux = Math.cos(spine * DEG), uy = Math.sin(spine * DEG);
|
|
46
|
+
for (const [side, sgn] of [['l', -1], ['r', 1]]) {
|
|
47
|
+
const sp = [shoulder[0] - uy * BONES.shoulder_w * sgn, shoulder[1] + ux * BONES.shoulder_w * sgn];
|
|
48
|
+
const sh = g(`${side}_sh`);
|
|
49
|
+
const el = proj(sp, sh, BONES.upper_arm);
|
|
50
|
+
const wr = proj(el, sh + g(`${side}_el`), BONES.forearm);
|
|
51
|
+
const hp = g(`${side}_hip`);
|
|
52
|
+
const kn = proj(hip, hp, BONES.thigh);
|
|
53
|
+
const an = proj(kn, hp + g(`${side}_knee`), BONES.shin);
|
|
54
|
+
Object.assign(p, { [`${side}_shoulder`]: sp, [`${side}_elbow`]: el, [`${side}_wrist`]: wr, [`${side}_knee`]: kn, [`${side}_ankle`]: an });
|
|
55
|
+
}
|
|
56
|
+
return p;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// IK analítica de dos huesos: [ángulo del hombro, ángulo relativo del codo].
|
|
60
|
+
function ik(target, l1, l2, bend) {
|
|
61
|
+
const [tx, ty] = target;
|
|
62
|
+
let d = Math.hypot(tx, ty);
|
|
63
|
+
d = Math.max(1e-6, Math.min(d, l1 + l2 - 1e-6, Math.max(Math.abs(l1 - l2) + 1e-6, d)));
|
|
64
|
+
const base = Math.atan2(ty, tx);
|
|
65
|
+
const a = Math.acos(Math.max(-1, Math.min(1, (l1 * l1 + d * d - l2 * l2) / (2 * l1 * d))));
|
|
66
|
+
const sh = base - bend * a;
|
|
67
|
+
const el = [Math.cos(sh) * l1, Math.sin(sh) * l1];
|
|
68
|
+
const fa = Math.atan2(ty - el[1], tx - el[0]);
|
|
69
|
+
let rel = (fa - sh) / DEG;
|
|
70
|
+
rel = ((rel + 180) % 360 + 360) % 360 - 180;
|
|
71
|
+
return [sh / DEG, rel];
|
|
72
|
+
}
|
|
73
|
+
function arm(side, target, elbow) {
|
|
74
|
+
let best = null;
|
|
75
|
+
for (const bend of [1, -1]) {
|
|
76
|
+
const [sh, el] = ik(target, BONES.upper_arm, BONES.forearm, bend);
|
|
77
|
+
const ex = Math.cos(sh * DEG) * BONES.upper_arm, ey = Math.sin(sh * DEG) * BONES.upper_arm;
|
|
78
|
+
const score = elbow === 'down' ? ey : side === 'r' ? ex : -ex;
|
|
79
|
+
if (!best || score > best[0]) best = [score, sh, el];
|
|
80
|
+
}
|
|
81
|
+
return { [`${side}_sh`]: best[1], [`${side}_el`]: best[2] };
|
|
82
|
+
}
|
|
83
|
+
const pose = (kw) => ({ ...DEFAULT, ...kw });
|
|
84
|
+
|
|
85
|
+
// El brazo/pierna «r» es el de delante cuando mira a la derecha.
|
|
86
|
+
const POSES = {
|
|
87
|
+
idle: pose({}),
|
|
88
|
+
talk: pose({ r_sh: 62, r_el: -58, l_sh: 108, l_el: 14 }),
|
|
89
|
+
wave: pose({ r_sh: -28, r_el: -62, l_sh: 106, l_el: 10, neck: -4 }),
|
|
90
|
+
point: pose({ r_sh: -12, r_el: -4, l_sh: 108, l_el: 12 }),
|
|
91
|
+
think: pose({ neck: 6, ...arm('r', [36, -30], 'down'), ...arm('l', [34, 70], 'out') }),
|
|
92
|
+
shrug: pose({ neck: -7, l_sh: 148, l_el: 78, r_sh: 32, r_el: -78 }),
|
|
93
|
+
cheer: pose({ l_sh: -134, l_el: 14, r_sh: -46, r_el: -14, spine: -91 }),
|
|
94
|
+
arms_crossed: pose({ ...arm('r', [-40, 52], 'out'), ...arm('l', [44, 60], 'out') }),
|
|
95
|
+
present: pose({ r_sh: 18, r_el: -48, l_sh: 110, l_el: 12 }),
|
|
96
|
+
sad: pose({ spine: -88, neck: 20, l_sh: 96, l_el: 2, r_sh: 84, r_el: -2, l_hip: 95, r_hip: 85 }),
|
|
97
|
+
sit: pose({ spine: -94, l_sh: 74, l_el: -30, r_sh: 64, r_el: -40, l_hip: -6, l_knee: 92, r_hip: 4, r_knee: 84 }),
|
|
98
|
+
jump_crouch: pose({ spine: -80, l_sh: 128, l_el: -16, r_sh: 52, r_el: 18, l_hip: 62, l_knee: 78, r_hip: 48, r_knee: 80 }),
|
|
99
|
+
jump_air: pose({ spine: -92, l_sh: -118, l_el: 12, r_sh: -62, r_el: -12, l_hip: 112, l_knee: 26, r_hip: 70, r_knee: 34 }),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Caras paramétricas (interpolables). eye: escala de ojos; brow_in: + preocupado, − enfado;
|
|
103
|
+
// curve: + sonrisa; mw: ancho de boca; mo: apertura; px/py: mirada; blush; brow_tilt: ceja levantada.
|
|
104
|
+
const EXPRESSIONS = {
|
|
105
|
+
neutral: { eye: 1, brow_in: 0, brow_y: 0, curve: 0.25, mw: 22, mo: 0, mdx: 0, px: 0, py: 0, blush: 0, brow_tilt: 0 },
|
|
106
|
+
happy: { eye: 0.92, brow_in: 6, brow_y: -3, curve: 1, mw: 28, mo: 0.18, mdx: 0, px: 0, py: 0, blush: 1, brow_tilt: 0 },
|
|
107
|
+
sad: { eye: 0.9, brow_in: 22, brow_y: 2, curve: -0.85, mw: 20, mo: 0, mdx: 0, px: 0, py: 0.35, blush: 0, brow_tilt: 0 },
|
|
108
|
+
surprised: { eye: 1.28, brow_in: 4, brow_y: -9, curve: 0, mw: 13, mo: 0.55, mdx: 0, px: 0, py: -0.1, blush: 0, brow_tilt: 0 },
|
|
109
|
+
thinking: { eye: 0.95, brow_in: 0, brow_y: -2, curve: -0.15, mw: 16, mo: 0, mdx: 6, px: 0.55, py: -0.75, blush: 0, brow_tilt: 9 },
|
|
110
|
+
angry: { eye: 0.95, brow_in: -26, brow_y: 4, curve: -0.6, mw: 22, mo: 0, mdx: 0, px: 0, py: 0, blush: 0, brow_tilt: 0 },
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
114
|
+
const easeInOut = (x) => (x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2);
|
|
115
|
+
const mix = (a, b, k) => {
|
|
116
|
+
const o = {};
|
|
117
|
+
for (const key of Object.keys(a)) o[key] = a[key] + ((b[key] ?? a[key]) - a[key]) * k;
|
|
118
|
+
return o;
|
|
119
|
+
};
|
|
120
|
+
const resolve = (table, v) => (typeof v === 'string' ? table[v] ?? Object.values(table)[0] : { ...Object.values(table)[0], ...v });
|
|
121
|
+
|
|
122
|
+
// Valor por tramos con mezcla suave: keys = [[t, 'pose' | {ángulos}], …]; pasa a cada una en `dur` s.
|
|
123
|
+
function keyedMix(table, t, keys, dur) {
|
|
124
|
+
let cur = resolve(table, keys[0][1]);
|
|
125
|
+
for (let i = 1; i < keys.length; i++) {
|
|
126
|
+
const [tk, v] = keys[i];
|
|
127
|
+
if (t < tk) break;
|
|
128
|
+
cur = mix(cur, resolve(table, v), easeInOut(clamp01((t - tk) / dur)));
|
|
129
|
+
}
|
|
130
|
+
return cur;
|
|
131
|
+
}
|
|
132
|
+
const poseAt = (t, keys, dur = 0.35) => {
|
|
133
|
+
const j = keyedMix(POSES, t, keys, dur);
|
|
134
|
+
// respiración: un vaivén mínimo para que nunca esté congelado del todo
|
|
135
|
+
return { ...j, spine: j.spine + Math.sin(t * 2.1) * 0.8, l_sh: j.l_sh + Math.sin(t * 2.1 + 1) * 1.2, r_sh: j.r_sh - Math.sin(t * 2.1 + 1) * 1.2 };
|
|
136
|
+
};
|
|
137
|
+
const faceAt = (t, keys, dur = 0.25) => keyedMix(EXPRESSIONS, t, keys, dur);
|
|
138
|
+
|
|
139
|
+
// Boca al hablar: dentro de cada tramo [inicio, fin] se abre y cierra a ritmo de sílabas
|
|
140
|
+
// (determinista: solo depende de t). 0 = cerrada, 1 = abierta del todo.
|
|
141
|
+
function talkAt(t, spans) {
|
|
142
|
+
for (const [a, b] of spans) {
|
|
143
|
+
if (t < a || t > b) continue;
|
|
144
|
+
const edge = Math.min(1, (t - a) / 0.08, (b - t) / 0.08);
|
|
145
|
+
const s = Math.abs(Math.sin(t * 13.7)) * 0.65 + Math.abs(Math.sin(t * 7.3 + 1.3)) * 0.35;
|
|
146
|
+
return Math.max(0, edge) * (0.25 + 0.6 * s);
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
// Parpadeo cada ~3,4 s (la semilla separa a varios personajes).
|
|
151
|
+
function blinkAt(t, seed = 0) {
|
|
152
|
+
const period = 3.4 + (seed % 5) * 0.37;
|
|
153
|
+
const p = ((t + seed * 1.13) % period + period) % period;
|
|
154
|
+
return p < 0.07 ? 1 - p / 0.07 : p < 0.14 ? (p - 0.07) / 0.07 : 1;
|
|
155
|
+
}
|
|
156
|
+
// Andar de x0 (en t0) a x1 (en t1): devuelve { x, facing, joints } para mezclar con la pose.
|
|
157
|
+
function walk(t, [t0, x0], [t1, x1], base = 'idle') {
|
|
158
|
+
const k = clamp01((t - t0) / (t1 - t0));
|
|
159
|
+
const x = x0 + (x1 - x0) * k;
|
|
160
|
+
const facing = x1 >= x0 ? 1 : -1;
|
|
161
|
+
if (k <= 0 || k >= 1) return { x, facing, joints: POSES[base] ?? POSES.idle, walking: false };
|
|
162
|
+
const ph = (t - t0) * 9;
|
|
163
|
+
const s = Math.sin(ph), c = Math.cos(ph);
|
|
164
|
+
const j = {
|
|
165
|
+
...(POSES[base] ?? POSES.idle),
|
|
166
|
+
l_hip: 90 + 26 * s, r_hip: 90 - 26 * s,
|
|
167
|
+
l_knee: Math.max(0, -c) * 38 * (s < 0.3 ? 1 : 0.6),
|
|
168
|
+
r_knee: Math.max(0, c) * 38 * (s > -0.3 ? 1 : 0.6),
|
|
169
|
+
l_sh: 90 - 24 * s, r_sh: 90 + 24 * s, l_el: -18, r_el: -18, spine: -87,
|
|
170
|
+
};
|
|
171
|
+
return { x, facing, joints: j, walking: true, lift: Math.abs(Math.sin(ph)) * 4 };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const f1 = (n) => Math.round(n * 10) / 10;
|
|
175
|
+
const pt = (p) => `${f1(p[0])},${f1(p[1])}`;
|
|
176
|
+
const lum = (h) => {
|
|
177
|
+
const x = h.replace('#', '');
|
|
178
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(x.slice(i, i + 2), 16) / 255);
|
|
179
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// look: { color, hair: short|spiky|curly|long|bun|ponytail|none, hairColor, accessory: none|glasses|cap|headphones|bow|tie, theme: 'light'|'dark' }
|
|
183
|
+
function create(svg, look = {}) {
|
|
184
|
+
const L = { color: '#D24D1E', hair: 'short', hairColor: '#22201E', accessory: 'none', theme: 'light', ...look };
|
|
185
|
+
const dark = L.theme === 'dark';
|
|
186
|
+
const ink = dark ? '#F3F0EA' : '#1F1D1B';
|
|
187
|
+
const headFill = dark ? '#2A3038' : '#FFFFFF';
|
|
188
|
+
const hairFill = dark && lum(L.hairColor) <= 0.25 ? '#D9D4CC' : L.hairColor;
|
|
189
|
+
const tie = lum(L.color) > 0.2 ? '#2B2F3A' : '#E8E2D8';
|
|
190
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
191
|
+
svg.appendChild(g);
|
|
192
|
+
const R = BONES.head_r;
|
|
193
|
+
|
|
194
|
+
function hairBack() {
|
|
195
|
+
if (L.hair === 'long') return `<rect x="${-R - 9}" y="${-R - 6}" width="${2 * R + 18}" height="${R * 2.45 + 6}" rx="${R}" ry="${R * 0.6}" fill="${hairFill}"/>`;
|
|
196
|
+
if (L.hair === 'bun') return `<circle cx="${-R * 0.25}" cy="${-R * 1.22}" r="${R * 0.38}" fill="${hairFill}"/>`;
|
|
197
|
+
if (L.hair === 'ponytail') return `<g transform="translate(${-R * 0.95},${-R * 0.15}) rotate(25)"><ellipse cx="0" cy="${R * 0.375}" rx="${R * 0.32}" ry="${R * 0.575}" fill="${hairFill}"/></g>`;
|
|
198
|
+
return '';
|
|
199
|
+
}
|
|
200
|
+
function hairFront() {
|
|
201
|
+
const h = L.hair;
|
|
202
|
+
if (h === 'none' || (L.accessory === 'cap' && ['short', 'spiky', 'curly'].includes(h))) return '';
|
|
203
|
+
if (h === 'curly') {
|
|
204
|
+
let s = '';
|
|
205
|
+
for (let i = 0; i < 9; i++) {
|
|
206
|
+
const a = (192 + i * 19.5) * DEG;
|
|
207
|
+
s += `<circle cx="${f1(Math.cos(a) * R * 0.9)}" cy="${f1(Math.sin(a) * R * 0.9 - 2)}" r="${f1(R * 0.3)}" fill="${hairFill}"/>`;
|
|
208
|
+
}
|
|
209
|
+
return s;
|
|
210
|
+
}
|
|
211
|
+
const r = R + 5;
|
|
212
|
+
const a0 = 188 * DEG, a1 = 352 * DEG;
|
|
213
|
+
let s = `<path d="M${f1(Math.cos(a0) * r)},${f1(Math.sin(a0) * r)} A${r},${r} 0 0 1 ${f1(Math.cos(a1) * r)},${f1(Math.sin(a1) * r)} C${R * 0.55},${-R * 0.62} ${R * 0.05},${-R * 0.38} ${-R * 0.42},${-R * 0.52} Q${-R * 0.8},${-R * 0.45} ${-R - 4},${-R * 0.12} Z" fill="${hairFill}"/>`;
|
|
214
|
+
if (h === 'spiky') {
|
|
215
|
+
for (let i = 0; i < 5; i++) {
|
|
216
|
+
const b0 = (205 + i * 27) * DEG, bm = (218 + i * 27) * DEG, b1 = (232 + i * 27) * DEG;
|
|
217
|
+
s += `<path d="M${f1(Math.cos(b0) * (R + 2))},${f1(Math.sin(b0) * (R + 2))} L${f1(Math.cos(bm) * (R + 24))},${f1(Math.sin(bm) * (R + 24))} L${f1(Math.cos(b1) * (R + 2))},${f1(Math.sin(b1) * (R + 2))} Z" fill="${hairFill}"/>`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return s;
|
|
221
|
+
}
|
|
222
|
+
function mouth(mx, my, f, talk) {
|
|
223
|
+
const curve = f.curve, o = Math.min(1, f.mo + talk);
|
|
224
|
+
const w = f.mw * (1 - 0.25 * Math.min(1, talk)), hw = w / 2, cy = curve * 11;
|
|
225
|
+
const st = `stroke="${ink}" stroke-width="6.5" stroke-linecap="round" stroke-linejoin="round"`;
|
|
226
|
+
if (o < 0.06) return `<path d="M${f1(mx - hw)},${f1(my - cy * 0.5)} Q${f1(mx)},${f1(my + cy)} ${f1(mx + hw)},${f1(my - cy * 0.5)}" fill="none" ${st}/>`;
|
|
227
|
+
const depth = 6 + o * 26;
|
|
228
|
+
return `<path d="M${f1(mx - hw)},${f1(my - cy * 0.5)} Q${f1(mx)},${f1(my + cy * 0.55)} ${f1(mx + hw)},${f1(my - cy * 0.5)} C${f1(mx + hw)},${f1(my + depth * 0.9)} ${f1(mx - hw)},${f1(my + depth * 0.9)} ${f1(mx - hw)},${f1(my - cy * 0.5)} Z" fill="${ink}" ${st}/>`;
|
|
229
|
+
}
|
|
230
|
+
function head(f, eyesOpen, talk) {
|
|
231
|
+
const fx = 9, ex = 19, ey = -2;
|
|
232
|
+
const thin = `stroke="${ink}" stroke-width="5" fill="none" stroke-linecap="round"`;
|
|
233
|
+
let s = `<circle r="${R}" fill="${headFill}" stroke="${ink}" stroke-width="${LINE}"/>` + hairFront();
|
|
234
|
+
const open = Math.max(0.08, eyesOpen);
|
|
235
|
+
for (const sx of [-1, 1]) {
|
|
236
|
+
const cx = fx + sx * ex;
|
|
237
|
+
s += `<ellipse cx="${cx}" cy="${ey}" rx="${f1(6.6 * f.eye)}" ry="${f1(8.8 * f.eye * open)}" fill="${ink}"/>`;
|
|
238
|
+
if (open > 0.5) s += `<circle cx="${f1(cx + 2.2 + f.px * 1.5)}" cy="${f1(ey - 3 + f.py * 1.5)}" r="${f1(2.3 * f.eye)}" fill="#FFFFFF"/>`;
|
|
239
|
+
}
|
|
240
|
+
const by = -23 + f.brow_y;
|
|
241
|
+
for (const sx of [-1, 1]) {
|
|
242
|
+
const cx = fx + sx * ex;
|
|
243
|
+
const a = (f.brow_in + (sx === 1 ? f.brow_tilt : 0)) * DEG;
|
|
244
|
+
const dy = Math.sin(a) * 9;
|
|
245
|
+
s += `<line x1="${f1(cx + sx * 9)}" y1="${f1(by + dy)}" x2="${f1(cx - sx * 9)}" y2="${f1(by - dy - (sx === 1 ? f.brow_tilt * 0.35 : 0))}" stroke="${ink}" stroke-width="6.5" stroke-linecap="round"/>`;
|
|
246
|
+
}
|
|
247
|
+
if (f.blush > 0.02) for (const sx of [-1, 1]) s += `<ellipse cx="${fx + sx * 31}" cy="13.5" rx="8" ry="3.5" fill="#F08F7A" opacity="${f1(0.4 * Math.min(1, f.blush) * 100) / 100}"/>`;
|
|
248
|
+
s += mouth(fx + f.mdx, 25, f, talk);
|
|
249
|
+
if (L.accessory === 'glasses') {
|
|
250
|
+
for (const sx of [-1, 1]) s += `<circle cx="${fx + sx * ex}" cy="${ey}" r="13.5" ${thin}/>`;
|
|
251
|
+
s += `<line x1="${fx - ex + 13.5}" y1="${ey - 1}" x2="${fx + ex - 13.5}" y2="${ey - 1}" ${thin}/><line x1="${fx + ex + 13.5}" y1="${ey - 3}" x2="${R - 4}" y2="${ey - 6}" ${thin}/>`;
|
|
252
|
+
} else if (L.accessory === 'cap') {
|
|
253
|
+
const r = R + 4;
|
|
254
|
+
s += `<path d="M${f1(Math.cos(182 * DEG) * r)},${f1(Math.sin(182 * DEG) * r - 2)} A${r},${r} 0 0 1 ${f1(Math.cos(358 * DEG) * r)},${f1(Math.sin(358 * DEG) * r - 2)} Q0,${-R * 0.42} ${-R - 3},${-R * 0.24} Z" fill="${L.color}" ${thin.replace('fill="none"', '')}/>`;
|
|
255
|
+
s += `<path d="M${R * 0.2},${-R * 0.36} Q${R * 1.1},${-R * 0.42} ${R * 1.42},${-R * 0.2} Q${R},${-R * 0.12} ${R * 0.2},${-R * 0.2} Z" fill="${L.color}" stroke="${ink}" stroke-width="5"/>`;
|
|
256
|
+
} else if (L.accessory === 'headphones') {
|
|
257
|
+
const r = R + 9;
|
|
258
|
+
s += `<path d="M${f1(Math.cos(190 * DEG) * r)},${f1(Math.sin(190 * DEG) * r - 2)} A${r},${r} 0 0 1 ${f1(Math.cos(350 * DEG) * r)},${f1(Math.sin(350 * DEG) * r - 2)}" fill="none" stroke="${ink}" stroke-width="9" stroke-linecap="round"/>`;
|
|
259
|
+
for (const sx of [-1, 1]) s += `<rect x="${sx * (R + 2) - 10}" y="-20" width="20" height="38" rx="8" fill="${L.color}" stroke="${ink}" stroke-width="5"/>`;
|
|
260
|
+
} else if (L.accessory === 'bow') {
|
|
261
|
+
s += `<g transform="translate(${-R * 0.45},${-R * 0.9}) rotate(-20)"><path d="M0,0 L-20,-12 L-20,12 Z M0,0 L20,-12 L20,12 Z" fill="${L.color}" stroke="${ink}" stroke-width="5" stroke-linejoin="round"/><circle r="6" fill="${L.color}" stroke="${ink}" stroke-width="5"/></g>`;
|
|
262
|
+
}
|
|
263
|
+
return s;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// state: { x, ground, scale, facing, joints, face, mouth, eyesOpen, alpha, pop, lift }
|
|
267
|
+
function draw(state) {
|
|
268
|
+
const st = { scale: 1, facing: 1, mouth: 0, eyesOpen: 1, alpha: 1, pop: 1, lift: 0, face: 'neutral', joints: 'idle', ...state };
|
|
269
|
+
const joints = typeof st.joints === 'string' ? POSES[st.joints] ?? POSES.idle : { ...DEFAULT, ...st.joints };
|
|
270
|
+
const face = typeof st.face === 'string' ? EXPRESSIONS[st.face] ?? EXPRESSIONS.neutral : { ...EXPRESSIONS.neutral, ...st.face };
|
|
271
|
+
const p = forward(joints);
|
|
272
|
+
const dy = -Math.max(p.l_ankle[1], p.r_ankle[1]) - st.lift;
|
|
273
|
+
const s = st.scale * st.pop;
|
|
274
|
+
const line = `stroke="${ink}" stroke-width="${LINE}" stroke-linecap="round" stroke-linejoin="round" fill="none"`;
|
|
275
|
+
const shW = 70 * st.scale * Math.max(0.45, 1 - st.lift / 260);
|
|
276
|
+
let out = `<ellipse cx="${f1(st.x)}" cy="${f1(st.ground)}" rx="${f1(shW)}" ry="${f1(9 * st.scale)}" fill="#000" opacity="0.1"/>`;
|
|
277
|
+
out += `<g transform="translate(${f1(st.x)},${f1(st.ground)}) scale(${f1(s * st.facing * 1000) / 1000},${f1(s * 1000) / 1000}) translate(0,${f1(dy)})">`;
|
|
278
|
+
for (const side of ['l', 'r']) {
|
|
279
|
+
const a = p[`${side}_ankle`];
|
|
280
|
+
out += `<polyline points="${pt(p.hip)} ${pt(p[`${side}_knee`])} ${pt(a)}" ${line}/><line x1="${f1(a[0])}" y1="${f1(a[1])}" x2="${f1(a[0] + BONES.foot)}" y2="${f1(a[1] + 1)}" ${line}/>`;
|
|
281
|
+
}
|
|
282
|
+
out += `<g transform="translate(${pt(p.head)}) rotate(${f1(p.head_tilt)})">${hairBack()}</g>`;
|
|
283
|
+
const v = [p.chest[0] - p.hip[0], p.chest[1] - p.hip[1]];
|
|
284
|
+
const n = Math.hypot(v[0], v[1]) || 1;
|
|
285
|
+
const u = [v[0] / n, v[1] / n];
|
|
286
|
+
out += `<line x1="${f1(p.chest[0])}" y1="${f1(p.chest[1])}" x2="${f1(p.head_base[0])}" y2="${f1(p.head_base[1])}" ${line}/>`;
|
|
287
|
+
out += `<line x1="${f1(p.hip[0] + u[0] * 4)}" y1="${f1(p.hip[1] + u[1] * 4)}" x2="${f1(p.chest[0] - u[0] * 12)}" y2="${f1(p.chest[1] - u[1] * 12)}" stroke="${L.color}" stroke-width="44" stroke-linecap="round"/>`;
|
|
288
|
+
if (L.accessory === 'tie') {
|
|
289
|
+
const q = [-u[1], u[0]];
|
|
290
|
+
const top = [p.chest[0] - u[0] * 22, p.chest[1] - u[1] * 22];
|
|
291
|
+
const P = (k, w) => `${f1(top[0] - u[0] * k + q[0] * w)},${f1(top[1] - u[1] * k + q[1] * w)}`;
|
|
292
|
+
out += `<polygon points="${P(0, 6)} ${P(0, -6)} ${P(62, -9)} ${P(74, 0)} ${P(62, 9)}" fill="${tie}"/>`;
|
|
293
|
+
}
|
|
294
|
+
out += `<g transform="translate(${pt(p.head)}) rotate(${f1(p.head_tilt)})">${head(face, st.eyesOpen, st.mouth)}</g>`;
|
|
295
|
+
for (const side of ['l', 'r']) {
|
|
296
|
+
const w = p[`${side}_wrist`];
|
|
297
|
+
out += `<polyline points="${pt(p[`${side}_shoulder`])} ${pt(p[`${side}_elbow`])} ${pt(w)}" ${line}/><circle cx="${f1(w[0])}" cy="${f1(w[1])}" r="9.5" fill="${ink}"/>`;
|
|
298
|
+
}
|
|
299
|
+
out += '</g>';
|
|
300
|
+
g.setAttribute('opacity', String(f1(st.alpha * 1000) / 1000));
|
|
301
|
+
g.innerHTML = out;
|
|
302
|
+
// Dónde quedan las manos y la cabeza en la escena (para colgarles un objeto o un bocadillo).
|
|
303
|
+
const toScene = (q) => [st.x + st.facing * s * q[0], st.ground + s * (q[1] + dy)];
|
|
304
|
+
return { leftHand: toScene(p.l_wrist), rightHand: toScene(p.r_wrist), head: toScene(p.head), headTop: toScene([p.head[0], p.head[1] - R]) };
|
|
305
|
+
}
|
|
306
|
+
return { draw, look: L };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
window.IgnixStickman = { create, poseAt, faceAt, talkAt, blinkAt, walk, forward, POSES, EXPRESSIONS, JOINT_KEYS };
|
|
310
|
+
})();
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* whiteboard.js — bloque de Ignix Editor: pizarra blanca. Trazos que se dibujan solos con
|
|
3
|
+
* un rotulador que los sigue, formas «a mano» (rectángulo, círculo, flecha, subrayado,
|
|
4
|
+
* tachado) y texto que se escribe. Función pura del tiempo.
|
|
5
|
+
*
|
|
6
|
+
* Las formas a mano llevan un temblor con semilla fija: el mismo dibujo en cada fotograma.
|
|
7
|
+
* La longitud de cada trazo es geometría del SVG (getTotalLength), no medida de la página.
|
|
8
|
+
*
|
|
9
|
+
* const B = IgnixBoard;
|
|
10
|
+
* const board = B.create(document.getElementById('board'), { ink: '#1F1D1B', marker: true });
|
|
11
|
+
* board.shape(B.circle(540, 700, 180), 0.5, 0.8); // (path, empieza, dura)
|
|
12
|
+
* board.shape(B.arrow(200, 1100, 470, 860), 1.4, 0.5, { color: '#E0452B' });
|
|
13
|
+
* board.write('2× more bookings', 540, 1180, 1.9, 1.0, { size: 84 });
|
|
14
|
+
* run({ id, duration, fps }, (t) => board.draw(t));
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
18
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
19
|
+
const easeInOut = (x) => (x < 0.5 ? 2 * x * x : 1 - (-2 * x + 2) ** 2 / 2);
|
|
20
|
+
// Ruido con semilla: el mismo «pulso» de mano en cada fotograma.
|
|
21
|
+
function rng(seed) {
|
|
22
|
+
let s = seed >>> 0 || 1;
|
|
23
|
+
return () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296) * 2 - 1;
|
|
24
|
+
}
|
|
25
|
+
let seedN = 7;
|
|
26
|
+
const f1 = (n) => Math.round(n * 10) / 10;
|
|
27
|
+
|
|
28
|
+
// Una línea a mano: ligera curva y temblor.
|
|
29
|
+
function line(x1, y1, x2, y2, wobble = 4, seed = seedN++) {
|
|
30
|
+
const r = rng(seed);
|
|
31
|
+
const mx = (x1 + x2) / 2 + r() * wobble * 2, my = (y1 + y2) / 2 + r() * wobble * 2;
|
|
32
|
+
return `M${f1(x1 + r() * wobble)},${f1(y1 + r() * wobble)} Q${f1(mx)},${f1(my)} ${f1(x2 + r() * wobble)},${f1(y2 + r() * wobble)}`;
|
|
33
|
+
}
|
|
34
|
+
function rect(x, y, w, h, wobble = 5, seed = seedN++) {
|
|
35
|
+
const r = rng(seed);
|
|
36
|
+
const j = () => r() * wobble;
|
|
37
|
+
// Un solo trazo continuo que se pasa un poco al cerrar, como a mano.
|
|
38
|
+
return `M${f1(x + j())},${f1(y + j())} L${f1(x + w + j())},${f1(y + j())} L${f1(x + w + j())},${f1(y + h + j())} L${f1(x + j())},${f1(y + h + j())} L${f1(x + j())},${f1(y - h * 0.04 + j())} L${f1(x + w * 0.12 + j())},${f1(y + j())}`;
|
|
39
|
+
}
|
|
40
|
+
function circle(cx, cy, rx, ry = rx, wobble = 0.05, seed = seedN++) {
|
|
41
|
+
const r = rng(seed);
|
|
42
|
+
const pts = [];
|
|
43
|
+
const n = 28, a0 = -Math.PI / 2 + r() * 0.3;
|
|
44
|
+
for (let i = 0; i <= n + 3; i++) {
|
|
45
|
+
const a = a0 + (i / n) * Math.PI * 2;
|
|
46
|
+
const k = 1 + r() * wobble;
|
|
47
|
+
pts.push([cx + Math.cos(a) * rx * k, cy + Math.sin(a) * ry * k]);
|
|
48
|
+
}
|
|
49
|
+
return 'M' + pts.map((p) => `${f1(p[0])},${f1(p[1])}`).join(' L');
|
|
50
|
+
}
|
|
51
|
+
function arrow(x1, y1, x2, y2, head = 46, seed = seedN++) {
|
|
52
|
+
const a = Math.atan2(y2 - y1, x2 - x1);
|
|
53
|
+
const h1 = [x2 - Math.cos(a - 0.45) * head, y2 - Math.sin(a - 0.45) * head];
|
|
54
|
+
const h2 = [x2 - Math.cos(a + 0.45) * head, y2 - Math.sin(a + 0.45) * head];
|
|
55
|
+
return `${line(x1, y1, x2, y2, 4, seed)} M${f1(h1[0])},${f1(h1[1])} L${f1(x2)},${f1(y2)} L${f1(h2[0])},${f1(h2[1])}`;
|
|
56
|
+
}
|
|
57
|
+
const underline = (x, y, w, seed = seedN++) => line(x, y, x + w, y + 6, 5, seed);
|
|
58
|
+
const strike = (x, y, w, seed = seedN++) => `${line(x, y, x + w, y - 10, 4, seed)} ${line(x + w * 0.05, y + 14, x + w * 1.02, y + 2, 4, seed + 1)}`;
|
|
59
|
+
|
|
60
|
+
function create(svg, { ink = '#1F1D1B', width = 9, marker = true, markerColor = '#2B6CB0' } = {}) {
|
|
61
|
+
const items = [];
|
|
62
|
+
const layer = document.createElementNS(NS, 'g');
|
|
63
|
+
svg.appendChild(layer);
|
|
64
|
+
let pen = null;
|
|
65
|
+
if (marker) {
|
|
66
|
+
pen = document.createElementNS(NS, 'g');
|
|
67
|
+
pen.innerHTML = `<g transform="rotate(-35)"><rect x="-10" y="-150" width="20" height="120" rx="6" fill="${markerColor}"/><rect x="-12" y="-38" width="24" height="26" rx="4" fill="#E8E8E8"/><path d="M-8,-12 L0,6 L8,-12 Z" fill="${ink}"/></g>`;
|
|
68
|
+
pen.style.display = 'none';
|
|
69
|
+
svg.appendChild(pen);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Un trazo que se dibuja de `at` a `at + dur`.
|
|
73
|
+
function shape(d, at, dur, { color = ink, stroke = width, fill = 'none' } = {}) {
|
|
74
|
+
const p = document.createElementNS(NS, 'path');
|
|
75
|
+
p.setAttribute('d', d);
|
|
76
|
+
p.setAttribute('fill', 'none');
|
|
77
|
+
p.setAttribute('stroke', color);
|
|
78
|
+
p.setAttribute('stroke-width', String(stroke));
|
|
79
|
+
p.setAttribute('stroke-linecap', 'round');
|
|
80
|
+
p.setAttribute('stroke-linejoin', 'round');
|
|
81
|
+
layer.appendChild(p);
|
|
82
|
+
const len = p.getTotalLength();
|
|
83
|
+
p.style.strokeDasharray = `${len} ${len}`;
|
|
84
|
+
let fillEl = null;
|
|
85
|
+
if (fill !== 'none') {
|
|
86
|
+
fillEl = p.cloneNode();
|
|
87
|
+
fillEl.setAttribute('fill', fill);
|
|
88
|
+
fillEl.setAttribute('stroke', 'none');
|
|
89
|
+
layer.insertBefore(fillEl, p);
|
|
90
|
+
}
|
|
91
|
+
items.push({ kind: 'path', el: p, fillEl, at, dur, len });
|
|
92
|
+
return p;
|
|
93
|
+
}
|
|
94
|
+
// Texto que se escribe de izquierda a derecha (se descubre con una máscara).
|
|
95
|
+
function write(text, x, y, at, dur, { size = 80, color = ink, font = "800 1em 'Montserrat'", anchor = 'middle' } = {}) {
|
|
96
|
+
const id = `wb-clip-${items.length}-${Math.random().toString(36).slice(2, 7)}`;
|
|
97
|
+
const clip = document.createElementNS(NS, 'clipPath');
|
|
98
|
+
clip.setAttribute('id', id);
|
|
99
|
+
const r = document.createElementNS(NS, 'rect');
|
|
100
|
+
clip.appendChild(r);
|
|
101
|
+
svg.appendChild(clip);
|
|
102
|
+
const tx = document.createElementNS(NS, 'text');
|
|
103
|
+
tx.textContent = text;
|
|
104
|
+
tx.setAttribute('x', String(x));
|
|
105
|
+
tx.setAttribute('y', String(y));
|
|
106
|
+
tx.setAttribute('fill', color);
|
|
107
|
+
tx.setAttribute('text-anchor', anchor);
|
|
108
|
+
tx.setAttribute('clip-path', `url(#${id})`);
|
|
109
|
+
tx.style.font = font;
|
|
110
|
+
tx.style.fontSize = `${size}px`;
|
|
111
|
+
layer.appendChild(tx);
|
|
112
|
+
// Ancho estimado sin medir, por lo alto (~0,8 em por carácter en una negrita): la
|
|
113
|
+
// máscara solo descubre, así que pasarse no corta nada; al acabar se quita.
|
|
114
|
+
const w = text.length * size * 0.8;
|
|
115
|
+
const x0 = anchor === 'middle' ? x - w / 2 : anchor === 'end' ? x - w : x;
|
|
116
|
+
items.push({ kind: 'text', el: tx, rect: r, at, dur, x0, w, y: y - size, h: size * 1.4 });
|
|
117
|
+
return tx;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function draw(t) {
|
|
121
|
+
let tip = null;
|
|
122
|
+
for (const it of items) {
|
|
123
|
+
const k = easeInOut(clamp01((t - it.at) / it.dur));
|
|
124
|
+
if (it.kind === 'path') {
|
|
125
|
+
it.el.style.strokeDashoffset = String(it.len * (1 - k));
|
|
126
|
+
it.el.style.opacity = t >= it.at ? '1' : '0';
|
|
127
|
+
if (it.fillEl) it.fillEl.style.opacity = String(clamp01((t - it.at - it.dur) / 0.3));
|
|
128
|
+
if (k > 0 && k < 1) tip = it.el.getPointAtLength(it.len * k);
|
|
129
|
+
} else {
|
|
130
|
+
it.rect.setAttribute('y', String(it.y));
|
|
131
|
+
it.rect.setAttribute('height', String(it.h));
|
|
132
|
+
// Terminado, sin máscara: que ninguna letra se quede fuera por la estimación.
|
|
133
|
+
it.rect.setAttribute('x', String(k >= 1 ? -1e5 : it.x0 - 10));
|
|
134
|
+
it.rect.setAttribute('width', String(k >= 1 ? 2e5 : Math.max(0, (it.w + 20) * k)));
|
|
135
|
+
if (k > 0 && k < 1) tip = { x: it.x0 + it.w * k, y: it.y + it.h * 0.75 };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (pen) {
|
|
139
|
+
pen.style.display = tip ? 'block' : 'none';
|
|
140
|
+
if (tip) pen.setAttribute('transform', `translate(${f1(tip.x)},${f1(tip.y)})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { shape, write, draw };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
window.IgnixBoard = { create, line, rect, circle, arrow, underline, strike };
|
|
147
|
+
})();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// AVISO DE VERSIÓN NUEVA (2026-10-08, Fran: aviso, NO actualización automática).
|
|
2
|
+
//
|
|
3
|
+
// Ignix publica la versión que recomienda (la misma que fija su MCP, ya probada en CI):
|
|
4
|
+
// https://ignix.social/api/editor/version. Como mucho una vez al día se consulta (2 s
|
|
5
|
+
// de tope, sin red no pasa nada) y, si la instalada es más vieja, cada comando dice en
|
|
6
|
+
// stderr cuál hay y el comando EXACTO para actualizar. Lo ejecuta el agente con el OK
|
|
7
|
+
// de la persona; nunca se actualiza solo a mitad de un proyecto.
|
|
8
|
+
//
|
|
9
|
+
// Existe porque un agente puede usar la guía instalada (`skills add`) sin pasar por el
|
|
10
|
+
// MCP, y entonces nadie comprobaba la versión (prueba en Windows, 2026-10-08).
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
13
|
+
import { HOME } from './binaries.mjs';
|
|
14
|
+
|
|
15
|
+
const URL_VERSION = process.env.IGNIX_EDITOR_VERSION_URL || 'https://ignix.social/api/editor/version';
|
|
16
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
/** a > b en versiones x.y.z. */
|
|
19
|
+
export function newer(a, b) {
|
|
20
|
+
const pa = String(a).split('.').map(Number), pb = String(b).split('.').map(Number);
|
|
21
|
+
for (let i = 0; i < 3; i++) {
|
|
22
|
+
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) > (pb[i] || 0);
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** La carpeta `--prefix` de esta instalación, si está instalada con npm (no en el repo). */
|
|
28
|
+
export function installPrefix(pkgDir) {
|
|
29
|
+
const scope = dirname(pkgDir);
|
|
30
|
+
const nm = dirname(scope);
|
|
31
|
+
if (basename(scope) !== '@ignix-social' || basename(nm) !== 'node_modules') return null;
|
|
32
|
+
return dirname(nm);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Los comandos para actualizar en este sistema. */
|
|
36
|
+
export function updateCommands(prefix, version, platform = process.platform) {
|
|
37
|
+
const pkg = `@ignix-social/editor@${version}`;
|
|
38
|
+
if (platform === 'win32') {
|
|
39
|
+
const bin = join(prefix, 'node_modules', '.bin', 'ignix-editor.cmd');
|
|
40
|
+
return [`npm.cmd install --prefix "${prefix}" ${pkg}`, `& "${bin}" skills add`];
|
|
41
|
+
}
|
|
42
|
+
const bin = join(prefix, 'node_modules', '.bin', 'ignix-editor');
|
|
43
|
+
return [`npm install --prefix "${prefix}" ${pkg}`, `"${bin}" skills add`];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function recommended() {
|
|
47
|
+
const cache = join(HOME, 'update-check.json');
|
|
48
|
+
try {
|
|
49
|
+
const c = JSON.parse(readFileSync(cache, 'utf8'));
|
|
50
|
+
if (Date.now() - c.checkedAt < DAY_MS && c.version) return c.version;
|
|
51
|
+
} catch {
|
|
52
|
+
/* sin caché */
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const r = await fetch(URL_VERSION, { signal: AbortSignal.timeout(2000) });
|
|
56
|
+
if (!r.ok) return null;
|
|
57
|
+
const { version } = await r.json();
|
|
58
|
+
if (!/^\d+\.\d+\.\d+$/.test(String(version))) return null;
|
|
59
|
+
mkdirSync(HOME, { recursive: true });
|
|
60
|
+
writeFileSync(cache, JSON.stringify({ checkedAt: Date.now(), version }));
|
|
61
|
+
return version;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Avisa (stderr) si Ignix recomienda una versión más nueva. Nunca lanza ni bloquea más de 2 s. */
|
|
68
|
+
export async function warnIfOutdated({ current, pkgDir, write = (s) => process.stderr.write(s) }) {
|
|
69
|
+
if (process.env.IGNIX_EDITOR_NO_UPDATE_CHECK === '1' || process.env.CI) return null;
|
|
70
|
+
const prefix = installPrefix(resolve(pkgDir));
|
|
71
|
+
if (!prefix || !existsSync(prefix)) return null;
|
|
72
|
+
const latest = await recommended();
|
|
73
|
+
if (!latest || !newer(latest, current)) return null;
|
|
74
|
+
const [install, skills] = updateCommands(prefix, latest);
|
|
75
|
+
write(
|
|
76
|
+
`⬆ Ignix Editor ${latest} is available (this is ${current}). Update before going on — ask the user to approve these two commands:\n` +
|
|
77
|
+
` ${install}\n ${skills}\n` +
|
|
78
|
+
` Then read the guide again (it may have changed).\n`,
|
|
79
|
+
);
|
|
80
|
+
return latest;
|
|
81
|
+
}
|