@ignix-social/editor 0.1.22 → 0.1.23

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ignix-social/editor",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Ignix Editor: render HTML video compositions (reels, edits, clips) on your own computer. Based on HyperFrames.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -114,7 +114,7 @@ Decide the zones once per video (e.g. header y 270–440, stage y 460–1300, ca
114
114
 
115
115
  Ready-made pieces that draw themselves from `t`, like everything else. They are options, not a format: use one when the idea is better told that way, never because it exists. `ignix-editor block` lists them; `ignix-editor block <name> <project>` copies `<name>.js` into the project; load it right after `ignix-frame.js`.
116
116
 
117
- **`stickman`** — stick-figure characters that walk, gesture, talk and react (explainers, "you vs. the smart way" sketches, a two-person dialogue, a story told by a character). Poses: `idle talk wave point think shrug cheer arms_crossed present sad sit jump_crouch jump_air`. Faces: `neutral happy sad surprised thinking angry`. Look: `color` (use the brand colour), `hair` (`short spiky curly long bun ponytail none`), `hairColor`, `accessory` (`none glasses cap headphones bow tie`), `theme` (`light`, or `dark` on a dark background).
117
+ **`stickman`** — stick-figure characters that walk, gesture, talk and react (explainers, "you vs. the smart way" sketches, a two-person dialogue, a story told by a character). Poses: `idle talk wave point think shrug cheer arms_crossed present sad sit jump_crouch jump_air`. Faces: `neutral happy sad surprised thinking angry`. Look: `color` (use the brand colour), `hair` (`short spiky curly long bun ponytail none`), `hairColor`, `accessory` (`none glasses cap headphones bow tie`), `theme` (`light`, or `dark` on a dark background; pass `theme` in the state to `draw()` to switch it per frame, e.g. dark scene → white whiteboard, with the same figure).
118
118
 
119
119
  ```html
120
120
  <svg id="stage" viewBox="0 0 1080 1920" style="position:absolute;inset:0;width:1080px;height:1920px"></svg>
@@ -159,7 +159,7 @@ const chat = IgnixChat.create(document.getElementById('screen'), {
159
159
  run({ id, duration, fps }, (t) => chat.draw(t));
160
160
  ```
161
161
 
162
- **`whiteboard`** — a whiteboard explanation: shapes and words drawn by hand while a marker follows the stroke. Hand-drawn helpers (same wobble every frame): `IgnixBoard.line`, `rect`, `circle`, `arrow`, `underline`, `strike`; or any SVG path `d`. Each `shape(d, at, dur, { color, stroke, fill })` draws itself from `at` over `dur`; `write(text, x, y, at, dur, { size })` writes text left to right. One thing at a time, timed to the words, and big: on a 1080×1920 reel words of 70–110 px and shapes that use most of the width.
162
+ **`whiteboard`** — a whiteboard explanation: shapes and words drawn by hand while a marker follows the stroke. Hand-drawn helpers (same wobble every frame): `IgnixBoard.line`, `rect`, `circle`, `arrow`, `underline`, `strike`; or any SVG path `d`. Each `shape(d, at, dur, { color, stroke, fill })` draws itself from `at` over `dur`; `write(text, x, y, at, dur, { size })` writes text left to right. `create(target, { paper })` also draws the board itself (`paper: '#F7F5F0'` or `{ color, x, y, w, h, radius, frame }`); `target` may be a `<g>` inside the `<svg>`, so scaling/translating that `<g>` moves a camera over the board. One thing at a time, timed to the words, and big: on a 1080×1920 reel words of 70–110 px and shapes that use most of the width.
163
163
 
164
164
  ```js
165
165
  const B = IgnixBoard;
@@ -259,7 +259,7 @@ work/ transcripts, edit decisions (JSON)
259
259
  build.mjs reads work/ + durations (ignix-editor ffprobe) → writes assets/timing.js and the <video>/<audio> tags between markers in index.html
260
260
  ```
261
261
 
262
- Re-run it whenever the voice or the cuts change; the animation code reads `window.R` from `assets/timing.js` and never hardcodes seconds.
262
+ Re-run it whenever the voice or the cuts change; the animation code reads `window.R` from `assets/timing.js` and never hardcodes seconds. Keep any HTML template the script reads in `work/`, never next to `index.html`: a second root HTML with `data-composition-id` fails `check` (`multiple_root_compositions`).
263
263
 
264
264
  ## 6. Traps that do not error
265
265
 
@@ -279,5 +279,5 @@ Re-run it whenever the voice or the cuts change; the animation code reads `windo
279
279
  Only if the app offers no way, give the user ONE line to paste into Terminal, e.g. `cd ~/ignix-videos/<name> && ~/.ignix-editor/cli/node_modules/.bin/ignix-editor render`, never the contents of a script file (a `#!` line breaks zsh), then continue when they say it finished.
280
280
 
281
281
  1. `ignix-editor check` → 0 errors.
282
- 2. `ignix-editor snapshot --at <every ~0.5 s and every cue>` and LOOK at the frames (or tile them: `ignix-editor ffmpeg -pattern_type glob -i 'snapshots/frame-*.png' -vf "scale=270:480,tile=8x2" sheet.jpg`). Check: hook readable at frame 0, nothing still for >1.5 s, every named thing pointed at on its word, captions never over what is pointed at, nothing outside the safe zone.
282
+ 2. `ignix-editor snapshot --at <every ~0.5 s and every cue>` and LOOK at the frames — it also writes them tiled as `snapshots/contact-sheet*.jpg`, the quickest to read. Check: hook readable at frame 0, nothing still for >1.5 s, every named thing pointed at on its word, captions never over what is pointed at, nothing outside the safe zone.
283
283
  3. `ignix-editor render` → it reviews the MP4 by itself and prints `✓ Review: ready to show` or `✗ Review: NOT READY` with what to fix and where (stretches where nothing on the stage moves, a mostly empty frame, silences between voice lines, no change of scene). NOT READY means it is not finished: fix every ✗, render again, and only then show it. The `!` lines are worth fixing too when it is quick. (`ignix-editor review <mp4>` runs the same check on any video.) Then open the MP4's first frame and a few cues again, and deliver as the playbook says.
@@ -182,10 +182,16 @@
182
182
  // look: { color, hair: short|spiky|curly|long|bun|ponytail|none, hairColor, accessory: none|glasses|cap|headphones|bow|tie, theme: 'light'|'dark' }
183
183
  function create(svg, look = {}) {
184
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;
185
+ // Colores según el tema; `draw` puede cambiar de tema en cada fotograma (escena oscura →
186
+ // pizarra blanca) sin crear otro monigote.
187
+ let ink, headFill, hairFill;
188
+ function palette(theme) {
189
+ const dark = theme === 'dark';
190
+ ink = dark ? '#F3F0EA' : '#1F1D1B';
191
+ headFill = dark ? '#2A3038' : '#FFFFFF';
192
+ hairFill = dark && lum(L.hairColor) <= 0.25 ? '#D9D4CC' : L.hairColor;
193
+ }
194
+ palette(L.theme);
189
195
  const tie = lum(L.color) > 0.2 ? '#2B2F3A' : '#E8E2D8';
190
196
  const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
191
197
  svg.appendChild(g);
@@ -263,9 +269,10 @@
263
269
  return s;
264
270
  }
265
271
 
266
- // state: { x, ground, scale, facing, joints, face, mouth, eyesOpen, alpha, pop, lift }
272
+ // state: { x, ground, scale, facing, joints, face, mouth, eyesOpen, alpha, pop, lift, theme }
267
273
  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 };
274
+ const st = { scale: 1, facing: 1, mouth: 0, eyesOpen: 1, alpha: 1, pop: 1, lift: 0, face: 'neutral', joints: 'idle', theme: L.theme, ...state };
275
+ palette(st.theme);
269
276
  const joints = typeof st.joints === 'string' ? POSES[st.joints] ?? POSES.idle : { ...DEFAULT, ...st.joints };
270
277
  const face = typeof st.face === 'string' ? EXPRESSIONS[st.face] ?? EXPRESSIONS.neutral : { ...EXPRESSIONS.neutral, ...st.face };
271
278
  const p = forward(joints);
@@ -57,8 +57,19 @@
57
57
  const underline = (x, y, w, seed = seedN++) => line(x, y, x + w, y + 6, 5, seed);
58
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
59
 
60
- function create(svg, { ink = '#1F1D1B', width = 9, marker = true, markerColor = '#2B6CB0' } = {}) {
60
+ // `svg` puede ser el <svg> o un <g> dentro de él (para mover una «cámara» sobre la
61
+ // pizarra: escala/traslada el <g>). `paper` dibuja la pizarra en sí: un color, o
62
+ // { color, x, y, w, h, radius, frame } (por defecto 0,0,1080,1920 sin marco).
63
+ function create(svg, { ink = '#1F1D1B', width = 9, marker = true, markerColor = '#2B6CB0', paper = null } = {}) {
61
64
  const items = [];
65
+ if (paper) {
66
+ const P = typeof paper === 'string' ? { color: paper } : paper;
67
+ const bg = document.createElementNS(NS, 'rect');
68
+ const attrs = { x: P.x ?? 0, y: P.y ?? 0, width: P.w ?? 1080, height: P.h ?? 1920, rx: P.radius ?? 0, fill: P.color ?? '#F7F5F0' };
69
+ if (P.frame) Object.assign(attrs, { stroke: P.frame === true ? '#C9C4BA' : P.frame, 'stroke-width': P.frameWidth ?? 14 });
70
+ for (const [k, v] of Object.entries(attrs)) bg.setAttribute(k, String(v));
71
+ svg.appendChild(bg);
72
+ }
62
73
  const layer = document.createElementNS(NS, 'g');
63
74
  svg.appendChild(layer);
64
75
  let pen = null;
@@ -109,11 +120,12 @@
109
120
  tx.style.font = font;
110
121
  tx.style.fontSize = `${size}px`;
111
122
  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 });
123
+ // El ancho se MIDE en cada fotograma (getComputedTextLength: geometría del texto con
124
+ // la fuente ya cargada, igual en todos los fotogramas del render) para que el
125
+ // rotulador acabe donde acaba la palabra. Si aún no se puede medir, estimación por lo
126
+ // alto (~0,8 em por carácter): la máscara solo descubre y al acabar se quita.
127
+ const est = text.length * size * 0.8;
128
+ items.push({ kind: 'text', el: tx, rect: r, at, dur, x, anchor, est, y: y - size, h: size * 1.4 });
117
129
  return tx;
118
130
  }
119
131
 
@@ -127,6 +139,14 @@
127
139
  if (it.fillEl) it.fillEl.style.opacity = String(clamp01((t - it.at - it.dur) / 0.3));
128
140
  if (k > 0 && k < 1) tip = it.el.getPointAtLength(it.len * k);
129
141
  } else {
142
+ let w = it.est;
143
+ try {
144
+ w = it.el.getComputedTextLength() || it.est;
145
+ } catch {
146
+ /* sin medida: estimación */
147
+ }
148
+ it.w = w;
149
+ it.x0 = it.anchor === 'middle' ? it.x - w / 2 : it.anchor === 'end' ? it.x - w : it.x;
130
150
  it.rect.setAttribute('y', String(it.y));
131
151
  it.rect.setAttribute('height', String(it.h));
132
152
  // Terminado, sin máscara: que ninguna letra se quede fuera por la estimación.
@@ -95,7 +95,7 @@ export function alignToScript(script, tokens, end) {
95
95
  let i = 0;
96
96
  for (let j = 0; j < tokens.length && i < sw.length; j++) {
97
97
  const tk = tokens[j];
98
- const tEnd = j + 1 < tokens.length ? tokens[j + 1].s : Math.max(tk.e, end);
98
+ const tEnd = j + 1 < tokens.length ? tokens[j + 1].s : end > 0 ? end : tk.e;
99
99
  // whisper juntó dos palabras en una («a DM» → «ADM»)
100
100
  if (i + 1 < sw.length && norm(sw[i] + sw[i + 1]) === norm(tk.t) && norm(sw[i]) !== norm(tk.t)) {
101
101
  const mid = tk.s + (tEnd - tk.s) * (sw[i].length / (sw[i].length + sw[i + 1].length));
@@ -118,7 +118,10 @@ export function alignToScript(script, tokens, end) {
118
118
  s = e;
119
119
  }
120
120
  }
121
- return out.map((w) => ({ t: w.t, s: +w.s.toFixed(3), e: +w.e.toFixed(3) }));
121
+ // whisper trabaja en ventanas de 30 s y a veces pone el final de la última palabra más
122
+ // allá del audio (una frase de 2,76 s con «Ignix» acabando en 3,6 s): nada pasa del final.
123
+ const cap = end > 0 ? (x) => Math.min(x, end) : (x) => x;
124
+ return out.map((w) => ({ t: w.t, s: +cap(w.s).toFixed(3), e: +cap(w.e).toFixed(3) }));
122
125
  }
123
126
 
124
127
  /**