@ignix-social/editor 0.1.21 → 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.
@@ -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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[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
+ })();