@ignix-social/editor 0.1.24 → 0.1.26
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
|
@@ -222,6 +222,25 @@ const ba = IgnixBeforeAfter.create(document.getElementById('ba'), {
|
|
|
222
222
|
|
|
223
223
|
Every block draws itself with `x.draw(t)` inside `run()`.
|
|
224
224
|
|
|
225
|
+
**`captions`** — word-by-word captions straight from `words.json` (the output of `ignix-editor transcribe`): groups of 2–3 words that never cross the end of a sentence or a pause, the word being said highlighted, numbers and your `emphasis` words in a second colour, hard cuts between groups (a fade leaves an empty frame). Styles: `bold_pop` (uppercase with a thick outline, the current word pops), `karaoke` (words fill with colour as they are said), `clean` (sentence case with a soft shadow), `box` (on a dark box, the current word on a coloured pill). `IgnixCaptions.clip(words, from, to)` takes a clip's words and starts them at 0; `IgnixCaptions.remap(words, cuts)` removes the cut stretches (`[[start, end], …]` in the same time as the words — the same list you cut the video with) and moves the rest earlier. Load the words as a script (`assets/words.js`: `window.WORDS = {…}`), never with `fetch`. The container is the caption band (e.g. `left:90px; right:90px; top:1180px; height:260px` on a reel, under the face); never give it `id="caption"`.
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
const C = IgnixCaptions;
|
|
229
|
+
const words = C.remap(C.clip(WORDS.words, 452.1, 511.5), [[12.3, 13.1]]);
|
|
230
|
+
const cap = C.create(document.getElementById('subs'), { words, pauses: WORDS.pauses, style: 'bold_pop', accent: '#FF6A00', emphasis: ['YouTube'] });
|
|
231
|
+
run({ id, duration, fps }, (t) => cap.draw(t));
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**`overlays`** — the on-screen labels of an edit, each on its word: `title` (big, `*asterisks*` in the accent colour), `number` (counts up from `from` to `to`, with `prefix`, `suffix`, `label`, `locale`), `list` (items tick in one by one, `step` seconds apart), `strike` (crossed out, with an optional `replace` that pops in under it), `label` (a pill with an arrow: `point: 'down'|'up'|'left'|'right'`, placed with `x`/`y` in %), `emoji`. Place each with `pos: 'top'|'upper'|'center'|'lower'|'bottom'` (bands inside the reel's safe area) or `x`/`y` in %. The container covers the frame (`position:absolute; inset:0`) above the video and under the captions.
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
const O = IgnixOverlays.create(document.getElementById('ov'), { accent: '#FF6A00', items: [
|
|
238
|
+
{ type: 'number', from: 800, to: 1600, suffix: ' €', label: 'per video', at: 3.1, dur: 2.2 },
|
|
239
|
+
{ type: 'strike', text: 'YOUR VIDEO', replace: 'ANOTHER VIDEO', at: 6.0, dur: 2.5 },
|
|
240
|
+
] });
|
|
241
|
+
run({ id, duration, fps }, (t) => O.draw(t));
|
|
242
|
+
```
|
|
243
|
+
|
|
225
244
|
**Mixing them.** Blocks combine with each other and with the normal direction (recreated screens, captions, the camera): one block per stretch (open with a stickman sketch, explain on the whiteboard, then the usual screens), or several at once (the stickman pointing at what the whiteboard draws, a chat inside a scene with captions over it). Give each its own container, show it only during its stretch (toggle `display` from `t`) and change on the word that motivates it, with a transition (a quick slide, wipe or scale) rather than a hard swap.
|
|
226
245
|
|
|
227
246
|
## 3. Recorded video (edits and clips)
|
package/src/analysis/style.mjs
CHANGED
|
@@ -324,7 +324,12 @@ export function describe(s, n) {
|
|
|
324
324
|
const lead = n > 1 ? `Measured on ${n} videos.` : 'Measured on 1 video (2–3 give a steadier style).';
|
|
325
325
|
out.push(lead);
|
|
326
326
|
if (s.format.aspect) out.push(`Format ${s.format.aspect}${s.durationSec ? `, about ${Math.round(s.durationSec)} s long` : ''}.`);
|
|
327
|
-
|
|
327
|
+
// Sin cortes secos pero con el texto cambiando sin parar: la escena se transforma desde
|
|
328
|
+
// dentro, y «un corte cada 31 s» describía lo contrario de lo que se ve (2026-10-09).
|
|
329
|
+
const morph = s.shots.cutsPerMinute != null && s.shots.cutsPerMinute < 3 && (s.captions.changesPerMinute ?? 0) >= 10;
|
|
330
|
+
if (morph)
|
|
331
|
+
out.push(`Hardly any hard cuts (${s.shots.cutsPerMinute} per minute): the scene changes from within, the text on screen changes every ${Math.round(600 / s.captions.changesPerMinute) / 10} s. Keep it one continuous scene that builds and morphs, not a cut every few seconds.`);
|
|
332
|
+
else if (s.shots.medianSec != null)
|
|
328
333
|
out.push(`A cut every ${s.shots.medianSec} s (${s.shots.cutsPerMinute} per minute)${s.shots.firstCutSec != null ? `; the first cut at ${s.shots.firstCutSec} s` : ''}.`);
|
|
329
334
|
if (s.captions.coverage != null)
|
|
330
335
|
out.push(
|
|
@@ -350,7 +355,9 @@ export function describe(s, n) {
|
|
|
350
355
|
const types = s.audio.sfxTypes?.length ? s.audio.sfxTypes.join(', ') : null;
|
|
351
356
|
out.push(
|
|
352
357
|
!types
|
|
353
|
-
?
|
|
358
|
+
? s.audio.wordsPerMinute != null || (s.audio.musicShare ?? 0) >= 0.2
|
|
359
|
+
? 'Sound effects: not measurable here — one mixed under a continuous voice and music cannot be told apart. Ask the person whether they use them before assuming none.'
|
|
360
|
+
: 'No sound effects heard.'
|
|
354
361
|
: s.audio.sfxPerMinute > 0
|
|
355
362
|
? `Sound effects heard: ${types} — about ${s.audio.sfxPerMinute} a minute${s.audio.sfxOnCutsShare ? `, on ${Math.round(s.audio.sfxOnCutsShare * 100)} % of the cuts` : ''}.`
|
|
356
363
|
: `Sound effects in only ${s.audio.sfxVideos} of ${n} videos (${types}); mostly without.`,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* captions.js — bloque de Ignix Editor: subtítulos palabra a palabra a partir de words.json
|
|
3
|
+
* (`ignix-editor transcribe`). Agrupa 2-3 palabras sin cruzar el final de una frase ni una
|
|
4
|
+
* pausa, resalta la palabra que suena y destaca números y palabras clave. Función pura del
|
|
5
|
+
* tiempo: el navegador coloca las palabras (CSS) y aquí solo se anima.
|
|
6
|
+
*
|
|
7
|
+
* const C = IgnixCaptions;
|
|
8
|
+
* const data = window.WORDS; // { words, pauses } de words.json
|
|
9
|
+
* const words = C.remap(C.clip(data.words, 452.1, 511.5), [[12.3, 13.1]]); // clip + cortes
|
|
10
|
+
* const cap = C.create(document.getElementById('cap'), {
|
|
11
|
+
* words, pauses: data.pauses, style: 'bold_pop', // bold_pop | karaoke | clean | box
|
|
12
|
+
* accent: '#FF6A00', accent2: '#FFB020', emphasis: ['YouTube', 'gratis'],
|
|
13
|
+
* });
|
|
14
|
+
* run({ id, duration, fps }, (t) => cap.draw(t));
|
|
15
|
+
*
|
|
16
|
+
* Cortes (`remap`): tramos [inicio, fin] QUITADOS, en el tiempo del original; las palabras de
|
|
17
|
+
* dentro desaparecen y las de después se adelantan lo quitado. Usa la misma lista para el vídeo.
|
|
18
|
+
*/
|
|
19
|
+
(function () {
|
|
20
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
21
|
+
const outBack = (x) => 1 + 2.4 * (x - 1) ** 3 + 1.4 * (x - 1) ** 2;
|
|
22
|
+
const norm = (s) => String(s).toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^\p{L}\p{N}]/gu, '');
|
|
23
|
+
const ENDS = /[.!?…]["»”)]?$/;
|
|
24
|
+
|
|
25
|
+
/** Palabras de un tramo [from, to) del original, con el tiempo empezando en 0. */
|
|
26
|
+
function clip(words, from, to = Infinity) {
|
|
27
|
+
return words.filter((w) => w.s >= from - 0.02 && w.s < to).map((w) => ({ ...w, s: +(w.s - from).toFixed(3), e: +(Math.min(w.e, to) - from).toFixed(3) }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Quita los tramos `cuts` ([[s, e], …] en el tiempo de `words`) y adelanta lo que viene después. */
|
|
31
|
+
function remap(words, cuts = []) {
|
|
32
|
+
const cs = [...cuts].map(([a, b]) => [Math.min(a, b), Math.max(a, b)]).sort((x, y) => x[0] - y[0]);
|
|
33
|
+
const map = (x) => {
|
|
34
|
+
let off = 0;
|
|
35
|
+
for (const [a, b] of cs) {
|
|
36
|
+
if (x >= b) off += b - a;
|
|
37
|
+
else if (x > a) return a - off; // dentro de un corte: al borde
|
|
38
|
+
}
|
|
39
|
+
return x - off;
|
|
40
|
+
};
|
|
41
|
+
return words
|
|
42
|
+
.filter((w) => !cs.some(([a, b]) => w.s >= a && w.e <= b))
|
|
43
|
+
.map((w) => ({ ...w, s: +map(w.s).toFixed(3), e: +map(w.e).toFixed(3) }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Grupos de hasta `maxWords` palabras / `maxChars` letras; corta en fin de frase y en pausas. */
|
|
47
|
+
function group(words, { maxWords = 3, maxChars = 18, pauses = [], breakGap = 0.35 } = {}) {
|
|
48
|
+
const out = [];
|
|
49
|
+
let cur = null;
|
|
50
|
+
for (let i = 0; i < words.length; i++) {
|
|
51
|
+
const w = words[i];
|
|
52
|
+
const prev = words[i - 1];
|
|
53
|
+
const chars = cur ? cur.words.reduce((n, x) => n + x.t.length + 1, 0) + w.t.length : 0;
|
|
54
|
+
const pauseBetween = prev && (w.s - prev.e > breakGap || pauses.some(([a, b]) => b - a >= breakGap && a >= prev.s && a < w.s + 0.05));
|
|
55
|
+
if (!cur || cur.words.length >= maxWords || chars > maxChars || ENDS.test(prev.t) || pauseBetween) {
|
|
56
|
+
cur = { words: [], s: w.s, e: w.e };
|
|
57
|
+
out.push(cur);
|
|
58
|
+
}
|
|
59
|
+
cur.words.push(w);
|
|
60
|
+
cur.e = w.e;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Puntuación de final que sobra en pantalla («hola,» → «hola»); ¿ y ! se quedan.
|
|
66
|
+
const tidy = (t) => t.replace(/[.,;:]+$/, '').replace(/^["«“(]+|["»”)]+$/g, '');
|
|
67
|
+
|
|
68
|
+
function create(box, { words = [], pauses = [], style = 'bold_pop', maxWords, maxChars, accent = '#FF6A00', accent2 = '#FFB020', emphasis = [], numbers = true, uppercase, fontSize = 88, font = "'Montserrat', Arial, sans-serif", color = '#FFFFFF', stroke = '#000000', hold = 0.35 } = {}) {
|
|
69
|
+
const st = {
|
|
70
|
+
bold_pop: { up: true, weight: 900, maxW: 3, maxC: 14 },
|
|
71
|
+
karaoke: { up: true, weight: 900, maxW: 4, maxC: 22 },
|
|
72
|
+
clean: { up: false, weight: 800, maxW: 5, maxC: 28 },
|
|
73
|
+
box: { up: false, weight: 800, maxW: 3, maxC: 18 },
|
|
74
|
+
}[style] ?? { up: true, weight: 900, maxW: 3, maxC: 16 };
|
|
75
|
+
const up = uppercase ?? st.up;
|
|
76
|
+
const groups = group(words, { maxWords: maxWords ?? st.maxW, maxChars: maxChars ?? st.maxC, pauses });
|
|
77
|
+
const emph = new Set(emphasis.map(norm));
|
|
78
|
+
box.innerHTML = '';
|
|
79
|
+
// Rejilla de una celda: todos los grupos en el mismo sitio (si dos coinciden un instante,
|
|
80
|
+
// se superponen en vez de repartirse el ancho).
|
|
81
|
+
Object.assign(box.style, { position: box.style.position || 'absolute', display: 'grid', placeItems: 'center', pointerEvents: 'none' });
|
|
82
|
+
const els = groups.map((g, gi) => {
|
|
83
|
+
const row = document.createElement('div');
|
|
84
|
+
// Hueco amplio entre palabras: con contorno grueso, 0,3 em las pega («VÍDEOYOUTUBE»); 0,55 em deja sitio al crecer la palabra activa.
|
|
85
|
+
Object.assign(row.style, {
|
|
86
|
+
display: 'none', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'baseline', columnGap: '0.55em', rowGap: '0.06em',
|
|
87
|
+
gridArea: '1 / 1', maxWidth: '100%', textAlign: 'center', fontFamily: font, fontWeight: String(st.weight), fontSize: `${fontSize}px`, lineHeight: '1.08', transformOrigin: '50% 60%',
|
|
88
|
+
});
|
|
89
|
+
if (style === 'box') Object.assign(row.style, { background: 'rgba(14,14,16,.78)', padding: '0.22em 0.5em', borderRadius: '0.32em' });
|
|
90
|
+
const spans = g.words.map((w) => {
|
|
91
|
+
const el = document.createElement('span');
|
|
92
|
+
const text = tidy(w.t);
|
|
93
|
+
el.textContent = up ? text.toUpperCase() : text;
|
|
94
|
+
const key = numbers && /\d/.test(w.t) ? 'num' : emph.has(norm(w.t)) ? 'emph' : '';
|
|
95
|
+
Object.assign(el.style, { display: 'inline-block', color, transformOrigin: '50% 70%', borderRadius: '0.18em', padding: style === 'box' ? '0 0.12em' : '0' });
|
|
96
|
+
if (style === 'bold_pop' || style === 'karaoke') Object.assign(el.style, { WebkitTextStroke: `0.15em ${stroke}`, paintOrder: 'stroke fill' });
|
|
97
|
+
if (style === 'clean') el.style.textShadow = '0 4px 14px rgba(0,0,0,.65), 0 1px 2px rgba(0,0,0,.8)';
|
|
98
|
+
row.appendChild(el);
|
|
99
|
+
return { el, w, key };
|
|
100
|
+
});
|
|
101
|
+
box.appendChild(row);
|
|
102
|
+
const next = groups[gi + 1];
|
|
103
|
+
// Entra 0,04 s antes de su palabra; el anterior sale justo entonces (sin solaparse).
|
|
104
|
+
return { row, spans, s: g.s, end: next ? Math.min(next.s - 0.04, g.e + hold) : g.e + hold };
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
groups,
|
|
109
|
+
draw(t) {
|
|
110
|
+
for (const g of els) {
|
|
111
|
+
const on = t >= g.s - 0.04 && t < g.end;
|
|
112
|
+
g.row.style.display = on ? 'flex' : 'none';
|
|
113
|
+
if (!on) continue;
|
|
114
|
+
const k = clamp01((t - g.s + 0.04) / 0.14);
|
|
115
|
+
g.row.style.transform = style === 'clean' || style === 'box' ? `translateY(${((1 - k) * 18).toFixed(1)}px)` : `scale(${(0.86 + 0.14 * outBack(k)).toFixed(4)})`;
|
|
116
|
+
g.row.style.opacity = '1'; // corte seco entre grupos: un fundido deja un fotograma vacío
|
|
117
|
+
for (const { el, w, key } of g.spans) {
|
|
118
|
+
const now = t >= w.s && t < Math.max(w.e, w.s + 0.12);
|
|
119
|
+
const said = t >= w.s;
|
|
120
|
+
const base = key === 'num' || key === 'emph' ? accent2 : color;
|
|
121
|
+
if (style === 'box') {
|
|
122
|
+
el.style.background = now ? accent : 'transparent';
|
|
123
|
+
el.style.color = now ? '#111' : base;
|
|
124
|
+
} else if (style === 'karaoke') {
|
|
125
|
+
el.style.color = said ? accent : base;
|
|
126
|
+
} else {
|
|
127
|
+
el.style.color = now ? accent : base;
|
|
128
|
+
}
|
|
129
|
+
const kw = clamp01((t - w.s) / 0.09);
|
|
130
|
+
// Crece desde su centro y se come el hueco con la vecina («NOMACHINES»): poco, y menos
|
|
131
|
+
// cuanto más larga es la palabra.
|
|
132
|
+
el.style.transform = now && style === 'bold_pop' ? `scale(${(1 + Math.min(0.07, 0.35 / Math.max(5, w.t.length)) * kw).toFixed(4)})` : 'scale(1)';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
window.IgnixCaptions = { create, group, clip, remap };
|
|
140
|
+
})();
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* overlays.js — bloque de Ignix Editor: rótulos sobre el vídeo, cada uno anclado a su
|
|
3
|
+
* palabra: título, número que cuenta, lista que se va marcando, tachado (con su reemplazo),
|
|
4
|
+
* etiqueta con flecha y emoji. Función pura del tiempo.
|
|
5
|
+
*
|
|
6
|
+
* const O = IgnixOverlays.create(document.getElementById('ov'), { // #ov: inset 0 sobre el vídeo
|
|
7
|
+
* accent: '#FF6A00',
|
|
8
|
+
* items: [
|
|
9
|
+
* { type: 'title', text: 'YouTube ya no te *enseña*', at: 0.2, dur: 2.4, pos: 'top' },
|
|
10
|
+
* { type: 'number', to: 1600, prefix: '', suffix: ' €', at: 3.1, dur: 2.2, from: 800, label: 'por vídeo' },
|
|
11
|
+
* { type: 'list', title: 'Antes de publicar', items: ['Gancho', 'Prueba', 'CTA'], at: 6, step: 0.7, dur: 3.5 },
|
|
12
|
+
* { type: 'strike', text: 'TU VÍDEO', replace: 'OTRO VÍDEO', at: 10, dur: 2.5 },
|
|
13
|
+
* { type: 'label', text: 'Esto', at: 13, dur: 1.5, x: 62, y: 40, point: 'down' },
|
|
14
|
+
* { type: 'emoji', char: '🔥', at: 15, dur: 1.2, x: 78, y: 22 },
|
|
15
|
+
* ],
|
|
16
|
+
* });
|
|
17
|
+
* run({ id, duration, fps }, (t) => O.draw(t));
|
|
18
|
+
*
|
|
19
|
+
* pos: top | upper | center | lower | bottom (franjas dentro de la zona segura del reel) o
|
|
20
|
+
* { x, y } en % del cuadro. Texto con *asteriscos* = esa parte en el color de acento.
|
|
21
|
+
*/
|
|
22
|
+
(function () {
|
|
23
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
24
|
+
const outBack = (x) => 1 + 2.4 * (x - 1) ** 3 + 1.4 * (x - 1) ** 2;
|
|
25
|
+
const outCubic = (x) => 1 - (1 - x) ** 3;
|
|
26
|
+
const POS = { top: 15, upper: 28, center: 46, lower: 60, bottom: 70 };
|
|
27
|
+
|
|
28
|
+
// «Hola *mundo*» → nodos con la parte entre asteriscos en el acento (sin innerHTML).
|
|
29
|
+
function rich(el, text, accent) {
|
|
30
|
+
String(text)
|
|
31
|
+
.split(/(\*[^*]+\*)/)
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.forEach((part) => {
|
|
34
|
+
const s = document.createElement('span');
|
|
35
|
+
const hot = part.startsWith('*') && part.endsWith('*');
|
|
36
|
+
s.textContent = hot ? part.slice(1, -1) : part;
|
|
37
|
+
if (hot) s.style.color = accent;
|
|
38
|
+
el.appendChild(s);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fmt(n, { decimals = 0, locale = 'es-ES' } = {}) {
|
|
43
|
+
return new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(n);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function create(box, { items = [], accent = '#FF6A00', ink = '#FFFFFF', panel = 'rgba(14,14,16,.82)', font = "'Montserrat', Arial, sans-serif", size = 1, locale = 'es-ES' } = {}) {
|
|
47
|
+
box.innerHTML = '';
|
|
48
|
+
Object.assign(box.style, { pointerEvents: 'none', fontFamily: font });
|
|
49
|
+
const base = 1080 * size; // tamaños pensados para 1080 de ancho
|
|
50
|
+
const px = (n) => `${((n * base) / 1080).toFixed(1)}px`;
|
|
51
|
+
|
|
52
|
+
const made = items.map((it) => {
|
|
53
|
+
const el = document.createElement('div');
|
|
54
|
+
const at = it.at ?? 0;
|
|
55
|
+
const dur = it.dur ?? 2;
|
|
56
|
+
const p = typeof it.pos === 'object' && it.pos ? it.pos : { x: it.x ?? 50, y: it.y ?? POS[it.pos ?? (it.type === 'title' ? 'top' : 'upper')] ?? 28 };
|
|
57
|
+
Object.assign(el.style, { position: 'absolute', left: `${p.x}%`, top: `${p.y}%`, transform: 'translate(-50%,-50%)', opacity: '0', color: ink, textAlign: 'center' });
|
|
58
|
+
const o = { it, el, at, end: at + dur };
|
|
59
|
+
|
|
60
|
+
if (it.type === 'title') {
|
|
61
|
+
Object.assign(el.style, { width: '80%', fontWeight: '900', fontSize: px(it.size ?? 92), lineHeight: '1.04', textTransform: 'uppercase', WebkitTextStroke: `${px(10)} #000`, paintOrder: 'stroke fill' });
|
|
62
|
+
rich(el, it.text, accent);
|
|
63
|
+
} else if (it.type === 'number' || it.type === 'counter') {
|
|
64
|
+
Object.assign(el.style, { background: panel, borderRadius: px(36), padding: `${px(26)} ${px(46)}`, fontWeight: '900' });
|
|
65
|
+
const num = document.createElement('div');
|
|
66
|
+
Object.assign(num.style, { fontSize: px(it.size ?? 150), lineHeight: '1', color: accent, fontVariantNumeric: 'tabular-nums' });
|
|
67
|
+
el.appendChild(num);
|
|
68
|
+
if (it.label) {
|
|
69
|
+
const l = document.createElement('div');
|
|
70
|
+
Object.assign(l.style, { fontSize: px(46), fontWeight: '800', marginTop: px(8), textTransform: 'uppercase' });
|
|
71
|
+
l.textContent = it.label;
|
|
72
|
+
el.appendChild(l);
|
|
73
|
+
}
|
|
74
|
+
o.num = num;
|
|
75
|
+
} else if (it.type === 'list') {
|
|
76
|
+
Object.assign(el.style, { width: '76%', background: panel, borderRadius: px(32), padding: `${px(34)} ${px(40)}`, textAlign: 'left' });
|
|
77
|
+
if (it.title) {
|
|
78
|
+
const h = document.createElement('div');
|
|
79
|
+
Object.assign(h.style, { fontSize: px(44), fontWeight: '900', color: accent, textTransform: 'uppercase', marginBottom: px(18) });
|
|
80
|
+
h.textContent = it.title;
|
|
81
|
+
el.appendChild(h);
|
|
82
|
+
}
|
|
83
|
+
o.rows = (it.items ?? []).map((txt, i) => {
|
|
84
|
+
const r = document.createElement('div');
|
|
85
|
+
Object.assign(r.style, { display: 'flex', alignItems: 'center', gap: px(22), fontSize: px(it.size ?? 54), fontWeight: '800', margin: `${px(12)} 0`, opacity: '0' });
|
|
86
|
+
const dot = document.createElement('div');
|
|
87
|
+
Object.assign(dot.style, { flex: '0 0 auto', width: px(62), height: px(62), borderRadius: '50%', background: accent, color: '#111', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: px(36), fontWeight: '900' });
|
|
88
|
+
dot.textContent = it.check === false ? String(i + 1) : '✓';
|
|
89
|
+
const tx = document.createElement('div');
|
|
90
|
+
tx.textContent = txt;
|
|
91
|
+
r.append(dot, tx);
|
|
92
|
+
el.appendChild(r);
|
|
93
|
+
return { r, dot, at: at + 0.25 + i * (it.step ?? 0.6) };
|
|
94
|
+
});
|
|
95
|
+
} else if (it.type === 'strike') {
|
|
96
|
+
Object.assign(el.style, { fontWeight: '900', fontSize: px(it.size ?? 96), textTransform: 'uppercase', whiteSpace: 'nowrap' });
|
|
97
|
+
const old = document.createElement('div');
|
|
98
|
+
Object.assign(old.style, { position: 'relative', display: 'inline-block', WebkitTextStroke: `${px(9)} #000`, paintOrder: 'stroke fill' });
|
|
99
|
+
old.textContent = it.text;
|
|
100
|
+
const line = document.createElement('div');
|
|
101
|
+
Object.assign(line.style, { position: 'absolute', left: '-6%', width: '112%', top: '48%', height: px(16), background: it.lineColor ?? '#E0252B', borderRadius: px(8), transformOrigin: '0 50%', transform: 'scaleX(0) rotate(-4deg)' });
|
|
102
|
+
old.appendChild(line);
|
|
103
|
+
el.appendChild(old);
|
|
104
|
+
o.old = old;
|
|
105
|
+
o.line = line;
|
|
106
|
+
if (it.replace) {
|
|
107
|
+
const nw = document.createElement('div');
|
|
108
|
+
Object.assign(nw.style, { color: accent, marginTop: px(10), WebkitTextStroke: `${px(9)} #000`, paintOrder: 'stroke fill', opacity: '0' });
|
|
109
|
+
nw.textContent = it.replace;
|
|
110
|
+
el.appendChild(nw);
|
|
111
|
+
o.nw = nw;
|
|
112
|
+
}
|
|
113
|
+
} else if (it.type === 'label') {
|
|
114
|
+
Object.assign(el.style, { background: accent, color: '#111', fontWeight: '900', fontSize: px(it.size ?? 52), padding: `${px(14)} ${px(30)}`, borderRadius: px(999), whiteSpace: 'nowrap' });
|
|
115
|
+
el.textContent = it.text;
|
|
116
|
+
if (it.point) {
|
|
117
|
+
const a = document.createElement('div');
|
|
118
|
+
const s = px(22);
|
|
119
|
+
const side = { down: { top: '100%', left: '50%', bd: `border-top:${s} solid ${accent}`, tf: 'translateX(-50%)' }, up: { bottom: '100%', left: '50%', bd: `border-bottom:${s} solid ${accent}`, tf: 'translateX(-50%)' }, left: { right: '100%', top: '50%', bd: `border-right:${s} solid ${accent}`, tf: 'translateY(-50%)' }, right: { left: '100%', top: '50%', bd: `border-left:${s} solid ${accent}`, tf: 'translateY(-50%)' } }[it.point];
|
|
120
|
+
if (side) {
|
|
121
|
+
a.style.cssText = `position:absolute;width:0;height:0;border:${s} solid transparent;${side.bd};transform:${side.tf}`;
|
|
122
|
+
for (const k of ['top', 'bottom', 'left', 'right']) if (side[k]) a.style[k] = side[k];
|
|
123
|
+
el.appendChild(a);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} else if (it.type === 'emoji') {
|
|
127
|
+
Object.assign(el.style, { fontSize: px(it.size ?? 150), lineHeight: '1' });
|
|
128
|
+
el.textContent = it.char ?? '🔥';
|
|
129
|
+
}
|
|
130
|
+
box.appendChild(el);
|
|
131
|
+
return o;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
draw(t) {
|
|
136
|
+
for (const o of made) {
|
|
137
|
+
const { it, el, at, end } = o;
|
|
138
|
+
const on = t >= at && t < end;
|
|
139
|
+
el.style.display = on ? 'block' : 'none';
|
|
140
|
+
if (!on) continue;
|
|
141
|
+
const kin = clamp01((t - at) / 0.28);
|
|
142
|
+
const kout = clamp01((t - (end - 0.2)) / 0.2);
|
|
143
|
+
el.style.opacity = (clamp01(kin * 1.8) * (1 - kout)).toFixed(3);
|
|
144
|
+
const pop = it.type === 'emoji' ? 0.3 + 0.7 * outBack(kin) : 0.82 + 0.18 * outBack(kin);
|
|
145
|
+
const wob = it.type === 'emoji' ? `rotate(${(Math.sin((t - at) * 6) * 6).toFixed(2)}deg)` : '';
|
|
146
|
+
el.style.transform = `translate(-50%,-50%) translateY(${((1 - outCubic(kin)) * 30 + kout * -20).toFixed(1)}px) scale(${pop.toFixed(4)}) ${wob}`;
|
|
147
|
+
if (o.num) {
|
|
148
|
+
const k = outCubic(clamp01((t - at - 0.15) / (it.count ?? 0.9)));
|
|
149
|
+
const v = (it.from ?? 0) + ((it.to ?? 0) - (it.from ?? 0)) * k;
|
|
150
|
+
o.num.textContent = `${it.prefix ?? ''}${fmt(v, { decimals: it.decimals ?? 0, locale: it.locale ?? locale })}${it.suffix ?? ''}`;
|
|
151
|
+
}
|
|
152
|
+
if (o.rows) {
|
|
153
|
+
for (const r of o.rows) {
|
|
154
|
+
const k = clamp01((t - r.at) / 0.22);
|
|
155
|
+
r.r.style.opacity = k.toFixed(3);
|
|
156
|
+
r.r.style.transform = `translateX(${((1 - outCubic(k)) * -30).toFixed(1)}px)`;
|
|
157
|
+
r.dot.style.transform = `scale(${(0.5 + 0.5 * outBack(k)).toFixed(4)})`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (o.line) {
|
|
161
|
+
const k = outCubic(clamp01((t - at - (it.strikeAt ?? 0.45)) / 0.3));
|
|
162
|
+
o.line.style.transform = `scaleX(${k.toFixed(4)}) rotate(-4deg)`;
|
|
163
|
+
o.old.style.opacity = o.nw ? (1 - 0.45 * clamp01((t - at - 0.8) / 0.3)).toFixed(3) : '1';
|
|
164
|
+
if (o.nw) {
|
|
165
|
+
const kn = clamp01((t - at - (it.replaceAt ?? 0.85)) / 0.25);
|
|
166
|
+
o.nw.style.opacity = kn.toFixed(3);
|
|
167
|
+
o.nw.style.transform = `scale(${(0.7 + 0.3 * outBack(kn)).toFixed(4)})`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
window.IgnixOverlays = { create, format: fmt };
|
|
176
|
+
})();
|