@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.
- package/package.json +1 -1
- package/skills/ignix-editor/SKILL.md +54 -0
- 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/package.json
CHANGED
|
@@ -170,6 +170,60 @@ board.shape(B.arrow(420, 780, 660, 1040), 1.8, 0.5, { color: '#E0452B' });
|
|
|
170
170
|
run({ id, duration, fps }, (t) => board.draw(t));
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
**`reviews`** — REAL reviews (the brand's, from Ignix: `list_reviews` / `list_booking_reviews`) arriving one after another while their stars fill; the last `max` (3) stay on screen. Author as the source shows it, text untouched except trimmed.
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
const rv = IgnixReviews.create(document.getElementById('reviews'), {
|
|
177
|
+
reviews: [{ author: 'Marta G.', stars: 5, text: 'Best haircut in years.', source: 'Google', at: 0.4 }], accent: '#FFB400', fontSize: 40 });
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**`slots`** — the REAL free slots (`get_booking_availability`) as a grid by day; some can get "booked" live (`taken`) to show how booking works — say it is an example if they were not really taken.
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
const sl = IgnixSlots.create(document.getElementById('slots'), {
|
|
184
|
+
days: [{ label: 'Thu 9', slots: ['10:00', '12:30', '17:00'] }, { label: 'Fri 10', slots: ['09:30', '11:00'] }],
|
|
185
|
+
at: 0.3, taken: [{ day: 0, slot: 2, at: 2.4 }], accent: '#FF5A1F', takenLabel: 'Booked' });
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**`menu`** — a menu or catalogue: REAL products and prices (`list_catalog` or the Ignix store), cascading in, one `highlight`ed. Optional `image` (local file) and `note` per item.
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
const mn = IgnixMenu.create(document.getElementById('menu'), {
|
|
192
|
+
title: "Today's specials", items: [{ name: 'Croissant', price: '€1.80', note: 'fresh at 8 am' }],
|
|
193
|
+
at: 0.3, step: 0.35, highlight: { index: 0, at: 1.8 }, accent: '#FF5A1F' });
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**`beforeafter`** — before and after with a sliding curtain and handle. PHOTOS only (videos must be written in the HTML), the client's real ones, same place and framing; never a generated or retouched "before/after". `moves`: `[t, position 0–1]`.
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
const ba = IgnixBeforeAfter.create(document.getElementById('ba'), {
|
|
200
|
+
before: 'assets/before.jpg', after: 'assets/after.jpg', labels: ['BEFORE', 'AFTER'],
|
|
201
|
+
moves: [[0, 0.5], [1.2, 0.1], [2.4, 0.9], [3.4, 0.5]] });
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
**`infographic`** (`IgnixInfo`) — `bars(el, { items: [{ label, value, color }], unit, at, step })` that grow with their figure, `counter(el, { from, to, at, dur, prefix, suffix, decimals })` a big number counting up, `ring(el, { value 0–1, at, label })` a progress ring. Only REAL figures of the brand, or with their source on screen.
|
|
206
|
+
|
|
207
|
+
**`photos`** (`IgnixPhotos.create(el, { photos: [{ src, at, dur, from: [scale, x%, y%], to: [...], caption }], fade })`) — photos with slow push/pan (Ken Burns) and crossfades. Local files only (the client's, or licensed stock).
|
|
208
|
+
|
|
209
|
+
**`quiz`** (`IgnixQuiz.create(el, { question, options, correct, at, think })`) — a question, the options one by one, a countdown to think, then the right one highlighted (`.reveal` tells you when). Great before a "comment A, B or C". The right answer must really be right.
|
|
210
|
+
|
|
211
|
+
**`offer`** (`IgnixOffer.create(el, { title, oldPrice, price, badge, endsIn, cta, at })`) — old price struck, new price pops, a badge, a live countdown (`endsIn` = seconds left when the video starts) and the call to action. ONLY real offers with their real end date: a fake countdown is misleading (and unlawful in the EU).
|
|
212
|
+
|
|
213
|
+
**`map`** (`IgnixMap.create(el, { image, pin: [x, y], name, address, at, width, height })`) — "we are here": the client's map screenshot (or a drawn street plan if none), a pin that drops, a pulse and the address card. The business's real address.
|
|
214
|
+
|
|
215
|
+
**`steps`** (`IgnixSteps.create(el, { steps: [{ title, text, at }] })`) — a numbered step-by-step: each lights up on its word, a line runs to the next, done ones get a ✓.
|
|
216
|
+
|
|
217
|
+
**`headline`** (`IgnixHeadline.create(el, { style: 'tv' | 'paper', kicker, headline, source, ticker, at })`) — a news headline. Real news with its source visible, or the brand's own announcement said as such; never an invented headline dressed as a real outlet, never a real outlet's logo.
|
|
218
|
+
|
|
219
|
+
**`character`** (`IgnixCharacter.create(el, { image, at, talk: [[start, end]], bubbles: [{ text, at, dur }], bubbleSide })`) — the brand's character image (from `list_characters`, best with a transparent background) that enters, breathes, moves while its voice plays and speaks in bubbles.
|
|
220
|
+
|
|
221
|
+
**`ink`** (`IgnixInk.create(canvas)`) — the hand-drawn style of procedural-film on a `<canvas width="1080" height="1920">`: `ink.frame(t, (ctx, L) => { … })` with `L.paper(ctx)` or `L.blueprint(ctx)` as the background, `L.inkPath`/`L.inkLine`/`L.inkCircle` (inked lines that tremble like a hand drawing; `draw: 0–1` draws them on), `L.hatch`/`L.crossHatch`/`L.stipple` for tone, `L.hand` for handwritten words, and `L.glowDot`, `L.ticks`, `L.bracket`, `L.guideCircle`, `L.arcAnnotation` for blueprint diagrams. Each function documents its options inside `ink.js`. A whole explainer can live in this style, or one stretch of a video.
|
|
222
|
+
|
|
223
|
+
Every block draws itself with `x.draw(t)` inside `run()`.
|
|
224
|
+
|
|
225
|
+
**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
|
+
|
|
173
227
|
## 3. Recorded video (edits and clips)
|
|
174
228
|
|
|
175
229
|
- Transcribe on the machine: `ignix-editor transcribe assets/take.mp4 --lang es -o work/words.json`. Show the user the text and fix obvious mishearings (names, brands) without rewriting what they said.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* beforeafter.js — bloque de Ignix Editor: antes y después con una cortina que se
|
|
3
|
+
* desliza entre las dos FOTOS, con su asa y sus etiquetas. Función pura del tiempo. Solo
|
|
4
|
+
* fotos: un <video> creado desde JS el motor no lo ve (para vídeos, escríbelos en el HTML
|
|
5
|
+
* y recórtalos igual con clip-path).
|
|
6
|
+
*
|
|
7
|
+
* Fotos REALES del cliente (el mismo sitio, persona u objeto, mismo encuadre). Nunca un
|
|
8
|
+
* «antes/después» generado o retocado para exagerar: en salud, belleza o reformas eso es
|
|
9
|
+
* engañoso.
|
|
10
|
+
*
|
|
11
|
+
* const BA = IgnixBeforeAfter.create(document.getElementById('ba'), {
|
|
12
|
+
* before: 'assets/before.jpg', after: 'assets/after.jpg',
|
|
13
|
+
* labels: ['BEFORE', 'AFTER'],
|
|
14
|
+
* moves: [[0.4, 0.5], [1.6, 0.08], [3.0, 0.92], [4.2, 0.5]], // [t, posición 0-1 de la cortina]
|
|
15
|
+
* });
|
|
16
|
+
* run({ id, duration, fps }, (t) => BA.draw(t));
|
|
17
|
+
*
|
|
18
|
+
* El contenedor tiene el tamaño que le des en CSS; las fotos lo cubren (object-fit: cover).
|
|
19
|
+
*/
|
|
20
|
+
(function () {
|
|
21
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
22
|
+
const inOut = (x) => (x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2);
|
|
23
|
+
|
|
24
|
+
function create(box, { before, after, labels = ['BEFORE', 'AFTER'], moves = [[0, 0.5]], dur = 0.9, handle = '#FFFFFF', fontSize = 40 } = {}) {
|
|
25
|
+
box.innerHTML = '';
|
|
26
|
+
Object.assign(box.style, { position: 'relative', overflow: 'hidden' });
|
|
27
|
+
const media = (src) => `<img src="${src}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover">`;
|
|
28
|
+
const tag = (txt, side) => `<div style="position:absolute;top:${fontSize * 0.6}px;${side}:${fontSize * 0.6}px;background:rgba(0,0,0,.6);color:#fff;font-weight:900;font-size:${fontSize}px;padding:${fontSize * 0.2}px ${fontSize * 0.45}px;border-radius:${fontSize * 0.3}px;letter-spacing:.04em">${txt}</div>`;
|
|
29
|
+
box.innerHTML =
|
|
30
|
+
`<div class="a" style="position:absolute;inset:0">${media(after)}${tag(labels[1], 'right')}</div>` +
|
|
31
|
+
`<div class="b" style="position:absolute;inset:0;clip-path:inset(0 50% 0 0)">${media(before)}${tag(labels[0], 'left')}</div>` +
|
|
32
|
+
// El asa va en una capa del ancho del contenedor movida con transform (translateX en %
|
|
33
|
+
// es del propio ancho = el del contenedor): sin `left`, que salta de píxel en píxel.
|
|
34
|
+
`<div class="h" data-layout-allow-overflow style="position:absolute;inset:0;pointer-events:none;transform:translateX(50%)">` +
|
|
35
|
+
`<div style="position:absolute;top:0;bottom:0;left:0;width:${Math.max(4, fontSize * 0.14)}px;margin-left:-${Math.max(2, fontSize * 0.07)}px;background:${handle};box-shadow:0 0 18px rgba(0,0,0,.35)"></div>` +
|
|
36
|
+
`<div style="position:absolute;top:50%;left:0;width:${fontSize * 2}px;height:${fontSize * 2}px;margin:-${fontSize}px 0 0 -${fontSize}px;border-radius:50%;background:${handle};box-shadow:0 6px 18px rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;font-size:${fontSize * 0.9}px;font-weight:900;color:#333">⇆</div></div>`;
|
|
37
|
+
const b = box.querySelector('.b'), h = box.querySelector('.h');
|
|
38
|
+
|
|
39
|
+
function draw(t) {
|
|
40
|
+
let pos = moves[0][1];
|
|
41
|
+
for (let i = 1; i < moves.length; i++) {
|
|
42
|
+
const [tk, v] = moves[i];
|
|
43
|
+
if (t < tk) break;
|
|
44
|
+
pos = pos + (v - pos) * inOut(clamp01((t - tk) / dur));
|
|
45
|
+
}
|
|
46
|
+
const pct = (clamp01(pos) * 100).toFixed(2);
|
|
47
|
+
b.style.clipPath = `inset(0 ${(100 - pct).toFixed(2)}% 0 0)`;
|
|
48
|
+
h.style.transform = `translateX(${pct}%)`;
|
|
49
|
+
}
|
|
50
|
+
return { draw };
|
|
51
|
+
}
|
|
52
|
+
window.IgnixBeforeAfter = { create };
|
|
53
|
+
})();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* character.js — bloque de Ignix Editor: el personaje de la marca (una imagen: la del
|
|
3
|
+
* personaje de Ignix, list_characters, idealmente con fondo transparente) que entra,
|
|
4
|
+
* respira, se mueve al hablar y dice cosas en bocadillos. Función pura del tiempo.
|
|
5
|
+
*
|
|
6
|
+
* const CH = IgnixCharacter.create(document.getElementById('char'), {
|
|
7
|
+
* image: 'assets/fox.png', at: 0.2,
|
|
8
|
+
* talk: [[0.6, 2.4], [3.0, 4.6]], // tramos en los que habla (su voz)
|
|
9
|
+
* bubbles: [{ text: 'Hi! I answer your DMs at night.', at: 0.6, dur: 1.9 }],
|
|
10
|
+
* bubbleSide: 'right',
|
|
11
|
+
* });
|
|
12
|
+
* run({ id, duration, fps }, (t) => CH.draw(t));
|
|
13
|
+
*
|
|
14
|
+
* El contenedor fija el tamaño del personaje; el bocadillo sale por encima de él.
|
|
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, { image, at = 0, talk = [], bubbles = [], bubbleSide = 'right', fontSize = 46, ink = '#1F1D1B', bubble = '#FFFFFF' } = {}) {
|
|
21
|
+
box.innerHTML = '';
|
|
22
|
+
Object.assign(box.style, { position: 'relative' });
|
|
23
|
+
const img = document.createElement('img');
|
|
24
|
+
img.src = image;
|
|
25
|
+
Object.assign(img.style, { position: 'absolute', inset: '0', width: '100%', height: '100%', objectFit: 'contain', objectPosition: '50% 100%', transformOrigin: '50% 100%', opacity: '0' });
|
|
26
|
+
box.appendChild(img);
|
|
27
|
+
const bs = bubbles.map((b) => {
|
|
28
|
+
const el = document.createElement('div');
|
|
29
|
+
Object.assign(el.style, {
|
|
30
|
+
position: 'absolute', bottom: '92%', [bubbleSide === 'left' ? 'right' : 'left']: '42%', width: 'max-content', maxWidth: `${fontSize * 13}px`,
|
|
31
|
+
background: bubble, color: ink, fontSize: `${fontSize}px`, fontWeight: '800', lineHeight: '1.25', padding: `${fontSize * 0.45}px ${fontSize * 0.6}px`,
|
|
32
|
+
borderRadius: `${fontSize * 0.7}px`, boxShadow: '0 14px 30px rgba(0,0,0,.18)', opacity: '0', transformOrigin: bubbleSide === 'left' ? '100% 100%' : '0% 100%',
|
|
33
|
+
});
|
|
34
|
+
el.textContent = b.text;
|
|
35
|
+
box.appendChild(el);
|
|
36
|
+
return { el, at: b.at, end: b.at + (b.dur ?? 2) };
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
draw(t) {
|
|
40
|
+
const k = clamp01((t - at) / 0.45);
|
|
41
|
+
const speaking = talk.some(([a, b]) => t >= a && t <= b);
|
|
42
|
+
const breathe = Math.sin(t * 2.2) * 0.012;
|
|
43
|
+
const talkBob = speaking ? Math.abs(Math.sin(t * 11)) * 0.035 : 0;
|
|
44
|
+
img.style.opacity = clamp01(k * 2).toFixed(3);
|
|
45
|
+
img.style.transform = `translateY(${((1 - outBack(k)) * 120).toFixed(1)}px) scale(${(1 + breathe).toFixed(4)}, ${(1 - breathe + talkBob).toFixed(4)}) rotate(${(speaking ? Math.sin(t * 5) * 1.5 : 0).toFixed(2)}deg)`;
|
|
46
|
+
for (const b of bs) {
|
|
47
|
+
const kin = clamp01((t - b.at) / 0.25), kout = clamp01((t - b.end) / 0.2);
|
|
48
|
+
b.el.style.opacity = (kin * (1 - kout)).toFixed(3);
|
|
49
|
+
b.el.style.transform = `scale(${(0.6 + 0.4 * outBack(kin)).toFixed(4)})`;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
window.IgnixCharacter = { create };
|
|
55
|
+
})();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* headline.js — bloque de Ignix Editor: titular de noticia. Estilo TV (franja «ÚLTIMA
|
|
3
|
+
* HORA» con titular y rótulo que corre) o estilo periódico (cabecera, titular grande y
|
|
4
|
+
* fuente). Función pura del tiempo.
|
|
5
|
+
*
|
|
6
|
+
* Solo noticias REALES y con su fuente a la vista, o un anuncio de la propia marca dicho
|
|
7
|
+
* como tal. Nunca un titular inventado con aspecto de medio real ni el logo de un medio.
|
|
8
|
+
*
|
|
9
|
+
* const H = IgnixHeadline.create(document.getElementById('news'), {
|
|
10
|
+
* style: 'tv', // tv | paper
|
|
11
|
+
* kicker: 'BREAKING', headline: 'Instagram now lets you schedule DMs',
|
|
12
|
+
* source: 'Source: Instagram blog, 8 Oct 2026', ticker: 'Comment-to-DM · Auto replies · Bookings',
|
|
13
|
+
* at: 0.3, accent: '#E0252B',
|
|
14
|
+
* });
|
|
15
|
+
* run({ id, duration, fps }, (t) => H.draw(t));
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
19
|
+
const outCubic = (x) => 1 - (1 - x) ** 3;
|
|
20
|
+
|
|
21
|
+
function create(box, { style = 'tv', kicker = 'BREAKING', headline = '', source = '', ticker = '', masthead = 'THE DAILY', at = 0, accent = '#E0252B', ink = '#1F1D1B', fontSize = 56 } = {}) {
|
|
22
|
+
box.innerHTML = '';
|
|
23
|
+
let parts;
|
|
24
|
+
if (style === 'paper') {
|
|
25
|
+
Object.assign(box.style, { background: '#F4EFE4', color: ink, padding: `${fontSize}px ${fontSize * 0.8}px`, fontFamily: "Georgia, 'Times New Roman', serif", boxShadow: '0 30px 60px rgba(0,0,0,.25)', opacity: '0' });
|
|
26
|
+
box.innerHTML = `<div style="text-align:center;font-size:${fontSize * 1.1}px;font-weight:700;letter-spacing:.06em;border-bottom:4px double ${ink};padding-bottom:${fontSize * 0.3}px">${masthead}</div><div class="h" style="font-size:${fontSize * 1.25}px;font-weight:700;line-height:1.1;margin-top:${fontSize * 0.6}px"></div>${source ? `<div class="s" style="margin-top:${fontSize * 0.5}px;font-size:${fontSize * 0.45}px;font-style:italic;opacity:0">${source}</div>` : ''}`;
|
|
27
|
+
parts = { h: box.querySelector('.h'), s: box.querySelector('.s') };
|
|
28
|
+
} else {
|
|
29
|
+
Object.assign(box.style, { color: '#fff', fontFamily: "'Montserrat', Arial, sans-serif", opacity: '0' });
|
|
30
|
+
box.innerHTML = `<div class="k" style="display:inline-block;background:${accent};font-weight:900;font-size:${fontSize * 0.7}px;padding:${fontSize * 0.15}px ${fontSize * 0.45}px;letter-spacing:.05em;transform-origin:0 50%">${kicker}</div><div style="background:${ink};padding:${fontSize * 0.35}px ${fontSize * 0.45}px"><div class="h" style="font-size:${fontSize}px;font-weight:900;line-height:1.15"></div>${source ? `<div class="s" style="font-size:${fontSize * 0.42}px;opacity:0;margin-top:6px">${source}</div>` : ''}</div>${ticker ? `<div style="background:#fff;color:${ink};overflow:hidden;white-space:nowrap;font-weight:800;font-size:${fontSize * 0.5}px;padding:${fontSize * 0.15}px 0"><span class="tk" style="display:inline-block;padding-left:100%">${ticker} • ${ticker}</span></div>` : ''}`;
|
|
31
|
+
parts = { h: box.querySelector('.h'), s: box.querySelector('.s'), k: box.querySelector('.k'), tk: box.querySelector('.tk') };
|
|
32
|
+
}
|
|
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 - outCubic(k)) * 60).toFixed(1)}px)`;
|
|
38
|
+
if (parts.k) parts.k.style.transform = `scaleX(${outCubic(clamp01((t - at) / 0.3)).toFixed(4)})`;
|
|
39
|
+
const n = Math.round(clamp01((t - at - 0.3) / Math.max(0.4, headline.length * 0.035)) * headline.length);
|
|
40
|
+
parts.h.textContent = headline.slice(0, n) || '';
|
|
41
|
+
if (parts.s) parts.s.style.opacity = clamp01((t - at - 0.4 - headline.length * 0.035) / 0.3).toFixed(3);
|
|
42
|
+
if (parts.tk) parts.tk.style.transform = `translateX(${(-((t - at) * 120) % 4000).toFixed(1)}px)`;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
window.IgnixHeadline = { create };
|
|
47
|
+
})();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* infographic.js — bloque de Ignix Editor: infografía. Barras que crecen con su cifra,
|
|
3
|
+
* un número grande que cuenta y un anillo de progreso. Función pura del tiempo.
|
|
4
|
+
*
|
|
5
|
+
* Solo cifras REALES de la marca o con su fuente (dila en pantalla): nunca inventadas.
|
|
6
|
+
*
|
|
7
|
+
* const I = IgnixInfo;
|
|
8
|
+
* const bars = I.bars(document.getElementById('bars'), {
|
|
9
|
+
* items: [{ label: 'Before', value: 12 }, { label: 'With Ignix', value: 48, color: '#FF5A1F' }],
|
|
10
|
+
* unit: ' bookings', at: 0.4, step: 0.35 });
|
|
11
|
+
* const big = I.counter(document.getElementById('big'), { to: 312, at: 0.3, dur: 1.4, prefix: '+', suffix: '%' });
|
|
12
|
+
* const ring = I.ring(document.getElementById('ring'), { value: 0.82, at: 0.5, label: 'answered in < 1 min' });
|
|
13
|
+
* run({ id, duration, fps }, (t) => { bars.draw(t); big.draw(t); ring.draw(t); });
|
|
14
|
+
*/
|
|
15
|
+
(function () {
|
|
16
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
17
|
+
const outCubic = (x) => 1 - (1 - x) ** 3;
|
|
18
|
+
const fmt = (v, decimals, sep) => {
|
|
19
|
+
const s = v.toFixed(decimals);
|
|
20
|
+
const [i, d] = s.split('.');
|
|
21
|
+
return i.replace(/\B(?=(\d{3})+(?!\d))/g, sep) + (d ? `.${d}` : '');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function bars(box, { items = [], max, unit = '', at = 0, step = 0.3, dur = 0.9, accent = '#1F1D1B', ink = '#1F1D1B', track = 'rgba(0,0,0,.08)', fontSize = 42, decimals = 0, sep = ',' } = {}) {
|
|
25
|
+
const top = max ?? Math.max(...items.map((i) => i.value), 1);
|
|
26
|
+
box.innerHTML = '';
|
|
27
|
+
Object.assign(box.style, { display: 'flex', flexDirection: 'column', gap: `${fontSize * 0.7}px` });
|
|
28
|
+
const rows = items.map((it, i) => {
|
|
29
|
+
const r = document.createElement('div');
|
|
30
|
+
r.innerHTML = `<div style="display:flex;justify-content:space-between;font-size:${fontSize}px;font-weight:800;color:${ink};margin-bottom:${fontSize * 0.25}px"><span>${it.label}</span><span class="v"></span></div><div style="height:${fontSize * 0.9}px;border-radius:${fontSize}px;background:${track};overflow:hidden"><div class="f" style="height:100%;width:100%;border-radius:${fontSize}px;background:${it.color ?? accent};transform-origin:0 50%;transform:scaleX(0)"></div></div>`;
|
|
31
|
+
r.style.opacity = '0';
|
|
32
|
+
box.appendChild(r);
|
|
33
|
+
return { el: r, v: r.querySelector('.v'), f: r.querySelector('.f'), at: at + i * step, value: it.value };
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
draw(t) {
|
|
37
|
+
for (const r of rows) {
|
|
38
|
+
const k = outCubic(clamp01((t - r.at) / dur));
|
|
39
|
+
r.el.style.opacity = clamp01((t - r.at) / 0.2).toFixed(3);
|
|
40
|
+
r.f.style.transform = `scaleX(${((r.value / top) * k).toFixed(4)})`;
|
|
41
|
+
r.v.textContent = fmt(r.value * k, decimals, sep) + unit;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function counter(box, { from = 0, to, at = 0, dur = 1.2, prefix = '', suffix = '', decimals = 0, sep = ',', fontSize = 220, color = '#1F1D1B' } = {}) {
|
|
48
|
+
Object.assign(box.style, { fontSize: `${fontSize}px`, fontWeight: '900', color, lineHeight: '1', fontVariantNumeric: 'tabular-nums' });
|
|
49
|
+
return {
|
|
50
|
+
draw(t) {
|
|
51
|
+
const k = outCubic(clamp01((t - at) / dur));
|
|
52
|
+
box.style.opacity = t >= at ? '1' : '0';
|
|
53
|
+
box.textContent = prefix + fmt(from + (to - from) * k, decimals, sep) + suffix;
|
|
54
|
+
box.style.transform = `scale(${(0.85 + 0.15 * k).toFixed(4)})`;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ring(box, { value = 0.75, at = 0, dur = 1.2, size = 420, width = 44, accent = '#FF5A1F', track = 'rgba(0,0,0,.08)', label = '', ink = '#1F1D1B', suffix = '%' } = {}) {
|
|
60
|
+
const r = (size - width) / 2, c = 2 * Math.PI * r;
|
|
61
|
+
box.innerHTML = `<div style="position:relative;width:${size}px;height:${size}px;margin:0 auto"><svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="transform:rotate(-90deg)"><circle cx="${size / 2}" cy="${size / 2}" r="${r}" fill="none" stroke="${track}" stroke-width="${width}"/><circle class="p" cx="${size / 2}" cy="${size / 2}" r="${r}" fill="none" stroke="${accent}" stroke-width="${width}" stroke-linecap="round" stroke-dasharray="${c} ${c}" stroke-dashoffset="${c}"/></svg><div class="n" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:${size * 0.24}px;font-weight:900;color:${ink}"></div></div>${label ? `<div style="text-align:center;margin-top:${size * 0.06}px;font-size:${size * 0.09}px;font-weight:700;color:${ink}">${label}</div>` : ''}`;
|
|
62
|
+
const p = box.querySelector('.p'), n = box.querySelector('.n');
|
|
63
|
+
return {
|
|
64
|
+
draw(t) {
|
|
65
|
+
const k = outCubic(clamp01((t - at) / dur));
|
|
66
|
+
box.style.opacity = t >= at ? '1' : '0';
|
|
67
|
+
p.setAttribute('stroke-dashoffset', String(c * (1 - value * k)));
|
|
68
|
+
n.textContent = `${Math.round(value * 100 * k)}${suffix}`;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
window.IgnixInfo = { bars, counter, ring };
|
|
74
|
+
})();
|