@ignix-social/editor 0.1.21 → 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 +1 -1
- package/skills/ignix-editor/SKILL.md +58 -4
- package/src/runtime/blocks/beforeafter.js +53 -0
- package/src/runtime/blocks/character.js +55 -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/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 +13 -6
- package/src/runtime/blocks/whiteboard.js +26 -6
- package/src/transcribe.mjs +5 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* map.js — bloque de Ignix Editor: «estamos aquí». Un mapa (la captura del cliente de su
|
|
3
|
+
* ficha de Google Maps en assets/, o un plano de calles dibujado), una chincheta que cae,
|
|
4
|
+
* un pulso alrededor y la tarjeta con el nombre y la dirección. Función pura del tiempo.
|
|
5
|
+
*
|
|
6
|
+
* const MP = IgnixMap.create(document.getElementById('map'), {
|
|
7
|
+
* image: 'assets/map.png', // opcional: sin imagen se dibuja un plano genérico
|
|
8
|
+
* pin: [0.55, 0.42], // posición de la chincheta (fracción del ancho/alto)
|
|
9
|
+
* name: 'Studio Nova', address: 'Calle Mayor 12, Madrid', at: 0.3, accent: '#FF5A1F',
|
|
10
|
+
* });
|
|
11
|
+
* run({ id, duration, fps }, (t) => MP.draw(t));
|
|
12
|
+
*
|
|
13
|
+
* La dirección, la REAL del negocio (get_brand_profile / su ficha de Google).
|
|
14
|
+
*/
|
|
15
|
+
(function () {
|
|
16
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
17
|
+
const outBounce = (x) => {
|
|
18
|
+
const n = 7.5625, d = 2.75;
|
|
19
|
+
if (x < 1 / d) return n * x * x;
|
|
20
|
+
if (x < 2 / d) return n * (x -= 1.5 / d) * x + 0.75;
|
|
21
|
+
if (x < 2.5 / d) return n * (x -= 2.25 / d) * x + 0.9375;
|
|
22
|
+
return n * (x -= 2.625 / d) * x + 0.984375;
|
|
23
|
+
};
|
|
24
|
+
function rng(seed) {
|
|
25
|
+
let s = seed >>> 0 || 1;
|
|
26
|
+
return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296;
|
|
27
|
+
}
|
|
28
|
+
// Plano genérico: calles en rejilla torcida, un parque y un río (siempre el mismo).
|
|
29
|
+
function streets(w, h, seed) {
|
|
30
|
+
const r = rng(seed);
|
|
31
|
+
let s = `<rect width="${w}" height="${h}" fill="#EDE9E1"/>`;
|
|
32
|
+
s += `<path d="M${-50},${h * 0.72} C${w * 0.3},${h * 0.6} ${w * 0.6},${h * 0.9} ${w + 50},${h * 0.78}" stroke="#A9CDE6" stroke-width="${w * 0.05}" fill="none"/>`;
|
|
33
|
+
s += `<rect x="${w * 0.08}" y="${h * 0.1}" width="${w * 0.22}" height="${h * 0.16}" rx="18" fill="#CFE3C3"/>`;
|
|
34
|
+
for (let i = 0; i < 9; i++) {
|
|
35
|
+
const y = h * (i / 8) + (r() - 0.5) * 40, x = w * (i / 8) + (r() - 0.5) * 40;
|
|
36
|
+
s += `<line x1="0" y1="${y}" x2="${w}" y2="${y + (r() - 0.5) * 120}" stroke="#FFFFFF" stroke-width="${i % 3 === 0 ? 22 : 12}"/>`;
|
|
37
|
+
s += `<line x1="${x}" y1="0" x2="${x + (r() - 0.5) * 120}" y2="${h}" stroke="#FFFFFF" stroke-width="${i % 4 === 0 ? 22 : 12}"/>`;
|
|
38
|
+
}
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function create(box, { image, pin = [0.5, 0.45], name = '', address = '', at = 0, accent = '#E0452B', ink = '#1F1D1B', fontSize = 44, width = 900, height = 1100, seed = 4 } = {}) {
|
|
43
|
+
box.innerHTML = '';
|
|
44
|
+
Object.assign(box.style, { position: 'relative', overflow: 'hidden', width: `${width}px`, height: `${height}px` });
|
|
45
|
+
box.innerHTML =
|
|
46
|
+
(image ? `<img src="${image}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover">` : `<svg width="${width}" height="${height}" style="position:absolute;inset:0">${streets(width, height, seed)}</svg>`) +
|
|
47
|
+
`<div class="ring" style="position:absolute;left:${pin[0] * 100}%;top:${pin[1] * 100}%;width:${fontSize * 4}px;height:${fontSize * 4}px;margin:-${fontSize * 2}px 0 0 -${fontSize * 2}px;border-radius:50%;border:${Math.max(4, fontSize * 0.12)}px solid ${accent};opacity:0"></div>` +
|
|
48
|
+
`<svg class="pin" width="${fontSize * 2}" height="${fontSize * 2.6}" viewBox="0 0 40 52" style="position:absolute;left:${pin[0] * 100}%;top:${pin[1] * 100}%;margin:-${fontSize * 2.6}px 0 0 -${fontSize}px;transform-origin:50% 100%;opacity:0"><path d="M20 0C9 0 0 9 0 20c0 15 20 32 20 32s20-17 20-32C40 9 31 0 20 0z" fill="${accent}"/><circle cx="20" cy="20" r="8" fill="#fff"/></svg>` +
|
|
49
|
+
(name || address ? `<div class="card" style="position:absolute;left:${fontSize * 0.6}px;right:${fontSize * 0.6}px;bottom:${fontSize * 0.6}px;background:#fff;border-radius:${fontSize * 0.5}px;padding:${fontSize * 0.5}px ${fontSize * 0.6}px;box-shadow:0 14px 30px rgba(0,0,0,.2);color:${ink};opacity:0"><div style="font-size:${fontSize}px;font-weight:900">${name}</div><div style="font-size:${fontSize * 0.65}px;font-weight:600;opacity:.7;margin-top:4px">${address}</div></div>` : '');
|
|
50
|
+
const pinEl = box.querySelector('.pin'), ring = box.querySelector('.ring'), card = box.querySelector('.card');
|
|
51
|
+
return {
|
|
52
|
+
draw(t) {
|
|
53
|
+
const k = clamp01((t - at) / 0.6);
|
|
54
|
+
pinEl.style.opacity = t >= at ? '1' : '0';
|
|
55
|
+
pinEl.style.transform = `translateY(${((1 - outBounce(k)) * -300).toFixed(1)}px)`;
|
|
56
|
+
const p = t >= at + 0.6 ? ((t - at - 0.6) % 1.4) / 1.4 : -1;
|
|
57
|
+
ring.style.opacity = p >= 0 ? (1 - p).toFixed(3) : '0';
|
|
58
|
+
ring.style.transform = `scale(${(0.3 + 0.9 * Math.max(0, p)).toFixed(4)})`;
|
|
59
|
+
if (card) {
|
|
60
|
+
const kc = clamp01((t - at - 0.8) / 0.4);
|
|
61
|
+
card.style.opacity = kc.toFixed(3);
|
|
62
|
+
card.style.transform = `translateY(${((1 - kc) * 40).toFixed(1)}px)`;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
window.IgnixMap = { create };
|
|
68
|
+
})();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* menu.js — bloque de Ignix Editor: carta o catálogo. Los productos (nombre, precio, foto
|
|
3
|
+
* opcional) entran en cascada y uno se destaca. Función pura del tiempo.
|
|
4
|
+
*
|
|
5
|
+
* Productos y precios REALES (list_catalog o la tienda de Ignix): nunca inventados.
|
|
6
|
+
*
|
|
7
|
+
* const M = IgnixMenu.create(document.getElementById('menu'), {
|
|
8
|
+
* title: 'Today’s specials',
|
|
9
|
+
* items: [
|
|
10
|
+
* { name: 'Croissant', price: '€1.80', image: 'assets/croissant.jpg' },
|
|
11
|
+
* { name: 'Pain au chocolat', price: '€2.10', note: 'fresh at 8 am' },
|
|
12
|
+
* ],
|
|
13
|
+
* at: 0.3, step: 0.35, highlight: { index: 1, at: 2.4 },
|
|
14
|
+
* accent: '#FF5A1F',
|
|
15
|
+
* });
|
|
16
|
+
* run({ id, duration, fps }, (t) => M.draw(t));
|
|
17
|
+
*/
|
|
18
|
+
(function () {
|
|
19
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
20
|
+
const outCubic = (x) => 1 - (1 - x) ** 3;
|
|
21
|
+
|
|
22
|
+
function create(box, { title = '', items = [], at = 0, step = 0.3, highlight = null, accent = '#FF5A1F', ink = '#1F1D1B', card = '#FFFFFF', fontSize = 44 } = {}) {
|
|
23
|
+
box.innerHTML = '';
|
|
24
|
+
Object.assign(box.style, { display: 'flex', flexDirection: 'column', gap: `${fontSize * 0.4}px` });
|
|
25
|
+
const rows = [];
|
|
26
|
+
if (title) {
|
|
27
|
+
const h = document.createElement('div');
|
|
28
|
+
h.textContent = title;
|
|
29
|
+
Object.assign(h.style, { fontSize: `${fontSize * 1.3}px`, fontWeight: '900', color: ink, marginBottom: `${fontSize * 0.2}px`, opacity: '0' });
|
|
30
|
+
box.appendChild(h);
|
|
31
|
+
rows.push({ el: h, at });
|
|
32
|
+
}
|
|
33
|
+
items.forEach((it, i) => {
|
|
34
|
+
const r = document.createElement('div');
|
|
35
|
+
Object.assign(r.style, {
|
|
36
|
+
display: 'flex', alignItems: 'center', gap: `${fontSize * 0.5}px`, background: card, borderRadius: `${fontSize * 0.5}px`,
|
|
37
|
+
padding: `${fontSize * 0.4}px ${fontSize * 0.5}px`, boxShadow: '0 10px 28px rgba(0,0,0,.12)', opacity: '0', position: 'relative',
|
|
38
|
+
outline: `0px solid ${accent}`,
|
|
39
|
+
});
|
|
40
|
+
r.innerHTML =
|
|
41
|
+
(it.image ? `<img src="${it.image}" style="width:${fontSize * 2.4}px;height:${fontSize * 2.4}px;object-fit:cover;border-radius:${fontSize * 0.35}px;flex:0 0 auto">` : '') +
|
|
42
|
+
`<div style="flex:1 1 auto;min-width:0"><div style="font-size:${fontSize}px;font-weight:800;color:${ink};line-height:1.15">${esc(it.name)}</div>${it.note ? `<div style="font-size:${fontSize * 0.62}px;opacity:.6;margin-top:4px;color:${ink}">${esc(it.note)}</div>` : ''}</div>` +
|
|
43
|
+
`<div style="font-size:${fontSize * 1.05}px;font-weight:900;color:${accent};flex:0 0 auto">${esc(it.price ?? '')}</div>`;
|
|
44
|
+
box.appendChild(r);
|
|
45
|
+
rows.push({ el: r, at: at + (title ? 0.3 : 0) + i * step, index: i });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function draw(t) {
|
|
49
|
+
for (const r of rows) {
|
|
50
|
+
const k = clamp01((t - r.at) / 0.35);
|
|
51
|
+
r.el.style.opacity = clamp01(k * 2).toFixed(3);
|
|
52
|
+
let tr = `translateY(${((1 - outCubic(k)) * 60).toFixed(1)}px)`;
|
|
53
|
+
if (highlight && r.index === highlight.index) {
|
|
54
|
+
const kh = clamp01((t - highlight.at) / 0.3);
|
|
55
|
+
tr += ` scale(${(1 + 0.06 * outCubic(kh)).toFixed(4)})`;
|
|
56
|
+
r.el.style.outlineWidth = `${(Math.max(4, fontSize * 0.12) * kh).toFixed(1)}px`;
|
|
57
|
+
r.el.style.zIndex = '1';
|
|
58
|
+
}
|
|
59
|
+
r.el.style.transform = tr;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { draw };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function esc(s) {
|
|
66
|
+
return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
67
|
+
}
|
|
68
|
+
window.IgnixMenu = { create };
|
|
69
|
+
})();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* offer.js — bloque de Ignix Editor: oferta con cuenta atrás. Tarjeta con el precio de
|
|
3
|
+
* antes tachado, el de ahora que salta, una etiqueta de descuento, la cuenta atrás que
|
|
4
|
+
* corre y la llamada a la acción. Función pura del tiempo.
|
|
5
|
+
*
|
|
6
|
+
* Solo ofertas REALES de la marca, con su fecha de fin de verdad: una cuenta atrás falsa
|
|
7
|
+
* es engañosa (y en la UE, práctica desleal).
|
|
8
|
+
*
|
|
9
|
+
* const O = IgnixOffer.create(document.getElementById('offer'), {
|
|
10
|
+
* title: 'Autumn colour + cut', oldPrice: '€85', price: '€59', badge: '-30%',
|
|
11
|
+
* endsIn: 2 * 86400 + 3 * 3600 + 14 * 60, // segundos que quedan AL EMPEZAR el vídeo
|
|
12
|
+
* cta: 'Book from the link in bio', at: 0.3, accent: '#FF5A1F',
|
|
13
|
+
* });
|
|
14
|
+
* run({ id, duration, fps }, (t) => O.draw(t));
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
18
|
+
const outBack = (x) => 1 + 2.6 * (x - 1) ** 3 + 1.6 * (x - 1) ** 2;
|
|
19
|
+
const two = (n) => String(Math.max(0, Math.floor(n))).padStart(2, '0');
|
|
20
|
+
|
|
21
|
+
function create(box, { title = '', oldPrice = '', price = '', badge = '', endsIn = 0, cta = '', at = 0, accent = '#FF5A1F', ink = '#1F1D1B', card = '#FFFFFF', fontSize = 48, labels = ['DAYS', 'HRS', 'MIN', 'SEC'] } = {}) {
|
|
22
|
+
box.innerHTML = '';
|
|
23
|
+
Object.assign(box.style, { position: 'relative', background: card, borderRadius: `${fontSize * 0.8}px`, padding: `${fontSize * 1.1}px ${fontSize}px`, boxShadow: '0 30px 60px rgba(0,0,0,.18)', color: ink, textAlign: 'center', opacity: '0' });
|
|
24
|
+
const unit = (l) => `<div style="flex:1"><div class="d" style="font-size:${fontSize * 1.5}px;font-weight:900;font-variant-numeric:tabular-nums;background:${ink};color:#fff;border-radius:${fontSize * 0.3}px;padding:${fontSize * 0.2}px 0">00</div><div style="font-size:${fontSize * 0.45}px;font-weight:800;opacity:.6;margin-top:6px">${l}</div></div>`;
|
|
25
|
+
box.innerHTML =
|
|
26
|
+
(badge ? `<div class="b" style="position:absolute;top:${-fontSize * 0.7}px;right:${fontSize * 0.6}px;background:${accent};color:#fff;font-weight:900;font-size:${fontSize * 1.1}px;padding:${fontSize * 0.2}px ${fontSize * 0.5}px;border-radius:${fontSize * 0.4}px;transform:rotate(6deg) scale(0)">${badge}</div>` : '') +
|
|
27
|
+
`<div style="font-size:${fontSize}px;font-weight:800">${title}</div>` +
|
|
28
|
+
`<div style="display:flex;align-items:baseline;justify-content:center;gap:${fontSize * 0.5}px;margin:${fontSize * 0.4}px 0 ${fontSize * 0.7}px"><span class="o" style="position:relative;font-size:${fontSize * 1.2}px;font-weight:800;opacity:.5">${oldPrice}<span class="s" style="position:absolute;left:-4%;right:-4%;top:52%;height:${Math.max(4, fontSize * 0.1)}px;background:${accent};transform-origin:0 50%;transform:scaleX(0)"></span></span><span class="p" style="font-size:${fontSize * 2.4}px;font-weight:900;color:${accent};display:inline-block;transform:scale(0)">${price}</span></div>` +
|
|
29
|
+
(endsIn > 0 ? `<div style="display:flex;gap:${fontSize * 0.3}px">${labels.map(unit).join('')}</div>` : '') +
|
|
30
|
+
(cta ? `<div class="c" style="margin-top:${fontSize * 0.8}px;background:${ink};color:#fff;font-weight:900;font-size:${fontSize * 0.85}px;padding:${fontSize * 0.45}px;border-radius:${fontSize * 2}px;opacity:0">${cta}</div>` : '');
|
|
31
|
+
const q = (s) => box.querySelector(s);
|
|
32
|
+
const digits = [...box.querySelectorAll('.d')];
|
|
33
|
+
return {
|
|
34
|
+
draw(t) {
|
|
35
|
+
const k = clamp01((t - at) / 0.35);
|
|
36
|
+
box.style.opacity = clamp01(k * 2).toFixed(3);
|
|
37
|
+
box.style.transform = `translateY(${((1 - outBack(k)) * 80).toFixed(1)}px)`;
|
|
38
|
+
q('.s') && (q('.s').style.transform = `scaleX(${clamp01((t - at - 0.4) / 0.3).toFixed(3)})`);
|
|
39
|
+
q('.p') && (q('.p').style.transform = `scale(${outBack(clamp01((t - at - 0.7) / 0.35)).toFixed(4)})`);
|
|
40
|
+
q('.b') && (q('.b').style.transform = `rotate(6deg) scale(${outBack(clamp01((t - at - 1.0) / 0.3)).toFixed(4)})`);
|
|
41
|
+
q('.c') && (q('.c').style.opacity = clamp01((t - at - 1.4) / 0.3).toFixed(3));
|
|
42
|
+
if (digits.length) {
|
|
43
|
+
const left = Math.max(0, endsIn - Math.max(0, t - at));
|
|
44
|
+
const parts = [left / 86400, (left % 86400) / 3600, (left % 3600) / 60, left % 60];
|
|
45
|
+
digits.forEach((d, i) => (d.textContent = two(parts[i])));
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
window.IgnixOffer = { create };
|
|
51
|
+
})();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* photos.js — bloque de Ignix Editor: fotos con movimiento (Ken Burns). Cada foto entra
|
|
3
|
+
* con un fundido, se acerca o se desplaza despacio y deja paso a la siguiente; con pie de
|
|
4
|
+
* foto opcional. Función pura del tiempo. Fotos LOCALES (assets/), del cliente o de stock
|
|
5
|
+
* con licencia.
|
|
6
|
+
*
|
|
7
|
+
* const P = IgnixPhotos.create(document.getElementById('photos'), {
|
|
8
|
+
* photos: [
|
|
9
|
+
* { src: 'assets/salon-1.jpg', at: 0, dur: 2.6, from: [1.0, 0, 0], to: [1.15, -3, 2], caption: 'Saturday, 9 am' },
|
|
10
|
+
* { src: 'assets/salon-2.jpg', at: 2.4, dur: 2.6, from: [1.2, 4, 0], to: [1.05, 0, 0] },
|
|
11
|
+
* ],
|
|
12
|
+
* fade: 0.45,
|
|
13
|
+
* });
|
|
14
|
+
* run({ id, duration, fps }, (t) => P.draw(t));
|
|
15
|
+
*
|
|
16
|
+
* from/to = [escala, desplazamiento x %, desplazamiento y %]. Las fotos cubren el contenedor.
|
|
17
|
+
*/
|
|
18
|
+
(function () {
|
|
19
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
20
|
+
const inOut = (x) => (x < 0.5 ? 2 * x * x : 1 - (-2 * x + 2) ** 2 / 2);
|
|
21
|
+
|
|
22
|
+
function create(box, { photos = [], fade = 0.45, fontSize = 44 } = {}) {
|
|
23
|
+
box.innerHTML = '';
|
|
24
|
+
Object.assign(box.style, { position: 'relative', overflow: 'hidden' });
|
|
25
|
+
const items = photos.map((p) => {
|
|
26
|
+
const wrap = document.createElement('div');
|
|
27
|
+
Object.assign(wrap.style, { position: 'absolute', inset: '0', opacity: '0' });
|
|
28
|
+
wrap.innerHTML = `<img src="${p.src}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transform-origin:50% 50%">${p.caption ? `<div style="position:absolute;left:${fontSize * 0.7}px;bottom:${fontSize * 0.9}px;background:rgba(0,0,0,.55);color:#fff;font-weight:800;font-size:${fontSize}px;padding:${fontSize * 0.25}px ${fontSize * 0.5}px;border-radius:${fontSize * 0.3}px">${p.caption}</div>` : ''}`;
|
|
29
|
+
box.appendChild(wrap);
|
|
30
|
+
return { wrap, img: wrap.querySelector('img'), at: p.at, dur: p.dur, from: p.from ?? [1, 0, 0], to: p.to ?? [1.12, 0, 0] };
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
draw(t) {
|
|
34
|
+
for (const it of items) {
|
|
35
|
+
const local = t - it.at;
|
|
36
|
+
const on = local >= 0 && local <= it.dur;
|
|
37
|
+
const op = on ? Math.min(clamp01(local / fade), clamp01((it.dur - local) / fade) || (it === items[items.length - 1] ? 1 : 0)) : 0;
|
|
38
|
+
it.wrap.style.opacity = op.toFixed(3);
|
|
39
|
+
if (!on) continue;
|
|
40
|
+
const k = inOut(clamp01(local / it.dur));
|
|
41
|
+
const [s0, x0, y0] = it.from, [s1, x1, y1] = it.to;
|
|
42
|
+
it.img.style.transform = `translate(${(x0 + (x1 - x0) * k).toFixed(3)}%, ${(y0 + (y1 - y0) * k).toFixed(3)}%) scale(${(s0 + (s1 - s0) * k).toFixed(4)})`;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
window.IgnixPhotos = { create };
|
|
48
|
+
})();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* quiz.js — bloque de Ignix Editor: pregunta o reto. La pregunta, las opciones una a una,
|
|
3
|
+
* una cuenta atrás para pensar y la respuesta que se destaca. Función pura del tiempo.
|
|
4
|
+
* Para comentarios: «¿cuál dirías? Comenta A, B o C» antes de revelarla.
|
|
5
|
+
*
|
|
6
|
+
* const Q = IgnixQuiz.create(document.getElementById('quiz'), {
|
|
7
|
+
* question: 'When do most salon bookings happen?',
|
|
8
|
+
* options: ['9–11 am', 'Lunch time', 'After 10 pm'],
|
|
9
|
+
* correct: 2, at: 0.3, think: 3, accent: '#FF5A1F',
|
|
10
|
+
* });
|
|
11
|
+
* run({ id, duration, fps }, (t) => Q.draw(t));
|
|
12
|
+
*
|
|
13
|
+
* La respuesta correcta tiene que serlo de verdad (un dato de la marca o con fuente).
|
|
14
|
+
*/
|
|
15
|
+
(function () {
|
|
16
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
17
|
+
const outBack = (x) => 1 + 2.4 * (x - 1) ** 3 + 1.4 * (x - 1) ** 2;
|
|
18
|
+
|
|
19
|
+
function create(box, { question = '', options = [], correct = 0, at = 0, step = 0.35, think = 3, accent = '#FF5A1F', ink = '#1F1D1B', card = '#FFFFFF', fontSize = 48, letters = 'ABCDEF' } = {}) {
|
|
20
|
+
box.innerHTML = '';
|
|
21
|
+
Object.assign(box.style, { display: 'flex', flexDirection: 'column', gap: `${fontSize * 0.45}px` });
|
|
22
|
+
const q = document.createElement('div');
|
|
23
|
+
q.textContent = question;
|
|
24
|
+
Object.assign(q.style, { fontSize: `${fontSize * 1.15}px`, fontWeight: '900', color: ink, lineHeight: '1.15', marginBottom: `${fontSize * 0.3}px`, opacity: '0' });
|
|
25
|
+
box.appendChild(q);
|
|
26
|
+
const opts = options.map((o, i) => {
|
|
27
|
+
const el = document.createElement('div');
|
|
28
|
+
Object.assign(el.style, { display: 'flex', alignItems: 'center', gap: `${fontSize * 0.4}px`, background: card, color: ink, borderRadius: `${fontSize * 0.5}px`, padding: `${fontSize * 0.45}px ${fontSize * 0.5}px`, fontSize: `${fontSize}px`, fontWeight: '800', boxShadow: '0 10px 26px rgba(0,0,0,.12)', opacity: '0', border: `${Math.max(4, fontSize * 0.1)}px solid transparent` });
|
|
29
|
+
el.innerHTML = `<span style="flex:0 0 auto;width:${fontSize * 1.3}px;height:${fontSize * 1.3}px;border-radius:50%;background:${ink};color:#fff;display:flex;align-items:center;justify-content:center">${letters[i]}</span><span style="flex:1">${o}</span><span class="ok" style="opacity:0;font-size:${fontSize * 1.1}px">✓</span>`;
|
|
30
|
+
box.appendChild(el);
|
|
31
|
+
return { el, ok: el.querySelector('.ok'), at: at + 0.5 + i * step, right: i === correct };
|
|
32
|
+
});
|
|
33
|
+
const timer = document.createElement('div');
|
|
34
|
+
Object.assign(timer.style, { alignSelf: 'center', marginTop: `${fontSize * 0.4}px`, width: `${fontSize * 2.4}px`, height: `${fontSize * 2.4}px`, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: `${fontSize * 1.2}px`, fontWeight: '900', color: '#fff', background: accent, opacity: '0' });
|
|
35
|
+
box.appendChild(timer);
|
|
36
|
+
const thinkAt = at + 0.5 + options.length * step + 0.2;
|
|
37
|
+
const reveal = thinkAt + think;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
reveal,
|
|
41
|
+
draw(t) {
|
|
42
|
+
q.style.opacity = clamp01((t - at) / 0.3).toFixed(3);
|
|
43
|
+
for (const o of opts) {
|
|
44
|
+
const k = clamp01((t - o.at) / 0.3);
|
|
45
|
+
o.el.style.opacity = (clamp01(k * 2) * (t >= reveal && !o.right ? 0.35 : 1)).toFixed(3);
|
|
46
|
+
const kr = o.right ? clamp01((t - reveal) / 0.3) : 0;
|
|
47
|
+
o.el.style.transform = `translateX(${((1 - outBack(k)) * -80).toFixed(1)}px) scale(${(1 + 0.05 * kr).toFixed(4)})`;
|
|
48
|
+
o.el.style.borderColor = kr > 0 ? accent : 'transparent';
|
|
49
|
+
o.ok.style.opacity = kr.toFixed(3);
|
|
50
|
+
o.ok.style.color = accent;
|
|
51
|
+
}
|
|
52
|
+
const left = Math.ceil(reveal - t);
|
|
53
|
+
const showT = t >= thinkAt && t < reveal;
|
|
54
|
+
timer.style.opacity = showT ? '1' : '0';
|
|
55
|
+
if (showT) {
|
|
56
|
+
timer.textContent = String(left);
|
|
57
|
+
const frac = (reveal - t) % 1;
|
|
58
|
+
timer.style.transform = `scale(${(1 + 0.15 * clamp01((frac - 0.85) / 0.15)).toFixed(4)})`;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
window.IgnixQuiz = { create };
|
|
64
|
+
})();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* reviews.js — bloque de Ignix Editor: reseñas REALES que entran una tras otra mientras
|
|
3
|
+
* se rellenan sus estrellas. Función pura del tiempo; el navegador coloca las tarjetas
|
|
4
|
+
* (CSS) y aquí solo se anima con transform/opacity.
|
|
5
|
+
*
|
|
6
|
+
* Solo reseñas que existen (las de la marca en Ignix: list_reviews / list_booking_reviews),
|
|
7
|
+
* con el nombre como lo muestra la fuente y sin retocar el texto más allá de recortarlo.
|
|
8
|
+
*
|
|
9
|
+
* const R = IgnixReviews.create(document.getElementById('reviews'), {
|
|
10
|
+
* reviews: [
|
|
11
|
+
* { author: 'Marta G.', stars: 5, text: 'Best haircut I have had in years.', source: 'Google', at: 0.4 },
|
|
12
|
+
* { author: 'Leo P.', stars: 5, text: 'Booked at 11 pm from Instagram, done.', source: 'Google', at: 1.6 },
|
|
13
|
+
* ],
|
|
14
|
+
* accent: '#FF5A1F', fontSize: 40,
|
|
15
|
+
* });
|
|
16
|
+
* run({ id, duration, fps }, (t) => R.draw(t));
|
|
17
|
+
*/
|
|
18
|
+
(function () {
|
|
19
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
20
|
+
const outBack = (x) => 1 + 2.2 * (x - 1) ** 3 + 1.2 * (x - 1) ** 2;
|
|
21
|
+
const STAR = 'M12 2l2.9 6.6 7.1.6-5.4 4.7 1.6 7L12 17.3 5.8 20.9l1.6-7L2 9.2l7.1-.6z';
|
|
22
|
+
|
|
23
|
+
function create(box, { reviews = [], accent = '#FFB400', fontSize = 40, card = '#FFFFFF', ink = '#1F1D1B', gap = 28, max = 3 } = {}) {
|
|
24
|
+
box.innerHTML = '';
|
|
25
|
+
Object.assign(box.style, { display: 'flex', flexDirection: 'column', gap: `${gap}px` });
|
|
26
|
+
const items = reviews.map((r, i) => {
|
|
27
|
+
const el = document.createElement('div');
|
|
28
|
+
Object.assign(el.style, {
|
|
29
|
+
background: card, color: ink, borderRadius: `${fontSize * 0.6}px`, padding: `${fontSize * 0.7}px ${fontSize * 0.8}px`,
|
|
30
|
+
boxShadow: '0 18px 40px rgba(0,0,0,.18)', fontSize: `${fontSize}px`, lineHeight: '1.3', opacity: '0', display: 'none',
|
|
31
|
+
transformOrigin: i % 2 ? '100% 50%' : '0% 50%',
|
|
32
|
+
});
|
|
33
|
+
const stars = Array.from({ length: 5 }, (_, k) => `<svg viewBox="0 0 24 24" width="${fontSize * 1.05}" height="${fontSize * 1.05}" style="margin-right:4px"><path d="${STAR}" fill="#E4E1DB"/><path class="fill" data-k="${k}" d="${STAR}" fill="${accent}" style="transform-origin:12px 12px;transform:scale(0)"/></svg>`).join('');
|
|
34
|
+
el.innerHTML = `<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:${fontSize * 0.35}px"><div style="display:flex">${stars}</div>${r.source ? `<div style="font-size:${fontSize * 0.6}px;opacity:.55;font-weight:700">${esc(r.source)}</div>` : ''}</div><div style="font-weight:600">“${esc(r.text)}”</div><div style="margin-top:${fontSize * 0.35}px;font-size:${fontSize * 0.7}px;opacity:.65;font-weight:700">— ${esc(r.author)}</div>`;
|
|
35
|
+
box.appendChild(el);
|
|
36
|
+
return { el, at: r.at ?? i * 1.2, stars: Math.max(0, Math.min(5, Math.round(r.stars ?? 5))), fills: [...el.querySelectorAll('.fill')], side: i % 2 ? 1 : -1 };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function draw(t) {
|
|
40
|
+
// Las `max` más recientes a la vista: las viejas se van por arriba.
|
|
41
|
+
const shown = items.filter((it) => t >= it.at);
|
|
42
|
+
const visible = new Set(shown.slice(-max));
|
|
43
|
+
for (const it of items) {
|
|
44
|
+
const k = clamp01((t - it.at) / 0.45);
|
|
45
|
+
it.el.style.display = visible.has(it) ? 'block' : 'none';
|
|
46
|
+
it.el.style.opacity = clamp01(k * 2).toFixed(3);
|
|
47
|
+
it.el.style.transform = `translateX(${((1 - outBack(k)) * 120 * it.side).toFixed(1)}px) rotate(${((1 - k) * 3 * it.side).toFixed(2)}deg)`;
|
|
48
|
+
it.fills.forEach((f, s) => {
|
|
49
|
+
const ks = s < it.stars ? clamp01((t - it.at - 0.25 - s * 0.09) / 0.2) : 0;
|
|
50
|
+
f.style.transform = `scale(${outBack(ks).toFixed(3)})`;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { draw };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function esc(s) {
|
|
58
|
+
return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
59
|
+
}
|
|
60
|
+
window.IgnixReviews = { create };
|
|
61
|
+
})();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* slots.js — bloque de Ignix Editor: los huecos libres de la agenda (la semana o un día)
|
|
3
|
+
* en una rejilla que aparece y en la que algunos se van ocupando en directo. Función pura
|
|
4
|
+
* del tiempo.
|
|
5
|
+
*
|
|
6
|
+
* Huecos REALES (get_booking_availability de Ignix): no se inventa disponibilidad. Que
|
|
7
|
+
* «se ocupen» solo para enseñar cómo funciona la reserva, y dicho como ejemplo.
|
|
8
|
+
*
|
|
9
|
+
* const S = IgnixSlots.create(document.getElementById('slots'), {
|
|
10
|
+
* days: [
|
|
11
|
+
* { label: 'Thu 9', slots: ['10:00', '12:30', '17:00', '18:30'] },
|
|
12
|
+
* { label: 'Fri 10', slots: ['09:30', '11:00', '16:00'] },
|
|
13
|
+
* ],
|
|
14
|
+
* at: 0.3, // cuándo empieza a aparecer
|
|
15
|
+
* taken: [{ day: 0, slot: 2, at: 2.4 }], // huecos que se ocupan (opcional)
|
|
16
|
+
* accent: '#FF5A1F',
|
|
17
|
+
* });
|
|
18
|
+
* run({ id, duration, fps }, (t) => S.draw(t));
|
|
19
|
+
*/
|
|
20
|
+
(function () {
|
|
21
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
22
|
+
const outBack = (x) => 1 + 2.4 * (x - 1) ** 3 + 1.4 * (x - 1) ** 2;
|
|
23
|
+
|
|
24
|
+
function create(box, { days = [], at = 0, step = 0.07, taken = [], accent = '#FF5A1F', ink = '#1F1D1B', bg = '#FFFFFF', fontSize = 40, takenLabel = 'Booked' } = {}) {
|
|
25
|
+
box.innerHTML = '';
|
|
26
|
+
Object.assign(box.style, { display: 'grid', gridTemplateColumns: `repeat(${Math.max(1, days.length)}, 1fr)`, gap: `${fontSize * 0.5}px`, alignItems: 'start' });
|
|
27
|
+
const cells = [];
|
|
28
|
+
let n = 0;
|
|
29
|
+
days.forEach((d, di) => {
|
|
30
|
+
const col = document.createElement('div');
|
|
31
|
+
Object.assign(col.style, { display: 'flex', flexDirection: 'column', gap: `${fontSize * 0.35}px` });
|
|
32
|
+
const head = document.createElement('div');
|
|
33
|
+
head.textContent = d.label;
|
|
34
|
+
Object.assign(head.style, { fontSize: `${fontSize * 0.8}px`, fontWeight: '800', color: ink, textAlign: 'center', marginBottom: `${fontSize * 0.15}px`, opacity: '0' });
|
|
35
|
+
col.appendChild(head);
|
|
36
|
+
cells.push({ el: head, at: at + n++ * step, head: true });
|
|
37
|
+
d.slots.forEach((s, si) => {
|
|
38
|
+
const c = document.createElement('div');
|
|
39
|
+
Object.assign(c.style, {
|
|
40
|
+
position: 'relative', background: bg, color: accent, border: `${Math.max(3, fontSize * 0.08)}px solid ${accent}`, borderRadius: `${fontSize * 0.45}px`,
|
|
41
|
+
padding: `${fontSize * 0.35}px 0`, textAlign: 'center', fontSize: `${fontSize}px`, fontWeight: '800', opacity: '0', overflow: 'hidden',
|
|
42
|
+
});
|
|
43
|
+
c.innerHTML = `<span class="t">${s}</span><span class="b" style="position:absolute;inset:0;display:none;align-items:center;justify-content:center;background:${ink};color:#fff;font-size:${fontSize * 0.7}px;transform:translateY(100%)">${takenLabel}</span>`;
|
|
44
|
+
col.appendChild(c);
|
|
45
|
+
const tk = taken.find((x) => x.day === di && x.slot === si);
|
|
46
|
+
cells.push({ el: c, at: at + n++ * step, taken: tk?.at, band: c.querySelector('.b'), time: c.querySelector('.t') });
|
|
47
|
+
});
|
|
48
|
+
box.appendChild(col);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function draw(t) {
|
|
52
|
+
for (const c of cells) {
|
|
53
|
+
const k = clamp01((t - c.at) / 0.3);
|
|
54
|
+
c.el.style.opacity = clamp01(k * 2.5).toFixed(3);
|
|
55
|
+
c.el.style.transform = `scale(${(0.7 + 0.3 * outBack(k)).toFixed(4)})`;
|
|
56
|
+
if (c.band) {
|
|
57
|
+
// La franja «reservado» no existe hasta que entra, y la hora se va mientras
|
|
58
|
+
// tanto: nunca dos textos uno encima de otro (lo marca `check`).
|
|
59
|
+
const kb = c.taken != null ? clamp01((t - c.taken) / 0.3) : 0;
|
|
60
|
+
c.band.style.display = kb > 0 ? 'flex' : 'none';
|
|
61
|
+
c.band.style.transform = `translateY(${((1 - kb) * 100).toFixed(1)}%)`;
|
|
62
|
+
c.time.style.opacity = (1 - kb).toFixed(3);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { draw };
|
|
67
|
+
}
|
|
68
|
+
window.IgnixSlots = { create };
|
|
69
|
+
})();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* steps.js — bloque de Ignix Editor: paso a paso. Pasos numerados unidos por una línea
|
|
3
|
+
* que se dibuja; cada paso se enciende en su momento, el actual destaca y los hechos
|
|
4
|
+
* quedan marcados. Función pura del tiempo.
|
|
5
|
+
*
|
|
6
|
+
* const ST = IgnixSteps.create(document.getElementById('steps'), {
|
|
7
|
+
* steps: [
|
|
8
|
+
* { title: 'Pick a keyword', text: 'e.g. PRICE', at: 0.4 },
|
|
9
|
+
* { title: 'Write the DM', text: 'with your link as a button', at: 1.8 },
|
|
10
|
+
* { title: 'Turn it on', at: 3.2 },
|
|
11
|
+
* ],
|
|
12
|
+
* accent: '#FF5A1F',
|
|
13
|
+
* });
|
|
14
|
+
* run({ id, duration, fps }, (t) => ST.draw(t));
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
18
|
+
const outBack = (x) => 1 + 2.4 * (x - 1) ** 3 + 1.4 * (x - 1) ** 2;
|
|
19
|
+
|
|
20
|
+
function create(box, { steps = [], accent = '#FF5A1F', ink = '#1F1D1B', muted = '#B9B4AC', fontSize = 46, gap = 70 } = {}) {
|
|
21
|
+
box.innerHTML = '';
|
|
22
|
+
Object.assign(box.style, { position: 'relative', display: 'flex', flexDirection: 'column', gap: `${gap}px` });
|
|
23
|
+
const dot = fontSize * 1.6;
|
|
24
|
+
const rows = steps.map((s, i) => {
|
|
25
|
+
const r = document.createElement('div');
|
|
26
|
+
Object.assign(r.style, { position: 'relative', display: 'flex', alignItems: 'flex-start', gap: `${fontSize * 0.6}px`, opacity: '0.25' });
|
|
27
|
+
r.innerHTML =
|
|
28
|
+
(i < steps.length - 1 ? `<div class="ln" style="position:absolute;left:${dot / 2 - 4}px;top:${dot}px;width:8px;height:calc(100% + ${gap - 4}px);background:${muted};border-radius:4px;overflow:hidden"><div class="lf" style="width:100%;height:100%;background:${accent};transform-origin:50% 0;transform:scaleY(0)"></div></div>` : '') +
|
|
29
|
+
`<div class="n" style="position:relative;flex:0 0 auto;width:${dot}px;height:${dot}px;border-radius:50%;background:${muted};color:#fff;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:${fontSize * 0.85}px">${i + 1}</div>` +
|
|
30
|
+
`<div style="padding-top:${(dot - fontSize * 1.15) / 2}px"><div style="font-size:${fontSize}px;font-weight:900;color:${ink};line-height:1.15">${s.title}</div>${s.text ? `<div style="font-size:${fontSize * 0.68}px;font-weight:600;color:${ink};opacity:.65;margin-top:6px">${s.text}</div>` : ''}</div>`;
|
|
31
|
+
box.appendChild(r);
|
|
32
|
+
return { el: r, n: r.querySelector('.n'), lf: r.querySelector('.lf'), at: s.at, next: steps[i + 1]?.at, num: i + 1 };
|
|
33
|
+
});
|
|
34
|
+
return {
|
|
35
|
+
draw(t) {
|
|
36
|
+
rows.forEach((r) => {
|
|
37
|
+
const k = clamp01((t - r.at) / 0.3);
|
|
38
|
+
const done = r.next != null && t >= r.next;
|
|
39
|
+
r.el.style.opacity = (0.25 + 0.75 * k).toFixed(3);
|
|
40
|
+
r.n.style.background = k > 0 ? accent : muted;
|
|
41
|
+
r.n.style.transform = `scale(${(1 + 0.18 * (outBack(k) - 1) + (done ? 0 : 0.08 * k)).toFixed(4)})`;
|
|
42
|
+
r.n.textContent = done ? '✓' : String(r.num);
|
|
43
|
+
if (r.lf) r.lf.style.transform = `scaleY(${r.next != null ? clamp01((t - r.at - 0.3) / Math.max(0.2, r.next - r.at - 0.3)).toFixed(4) : 0})`;
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
window.IgnixSteps = { create };
|
|
49
|
+
})();
|
|
@@ -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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
package/src/transcribe.mjs
CHANGED
|
@@ -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 :
|
|
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
|
-
|
|
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
|
/**
|