@ignix-social/editor 0.1.20 → 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 +116 -0
- package/src/cli.mjs +26 -1
- package/src/runtime/blocks/beforeafter.js +53 -0
- package/src/runtime/blocks/character.js +55 -0
- package/src/runtime/blocks/chat.js +86 -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/kinetic.js +78 -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 +310 -0
- package/src/runtime/blocks/whiteboard.js +147 -0
- package/src/update-check.mjs +81 -0
package/package.json
CHANGED
|
@@ -24,6 +24,8 @@ Commands. `ignix-editor` means the copy the Ignix playbook installed: `~/.ignix-
|
|
|
24
24
|
|
|
25
25
|
One project = one folder with `index.html` at its root and everything it uses inside `assets/`. Never work inside the user's code repository.
|
|
26
26
|
|
|
27
|
+
**Version first.** If any `ignix-editor` command prints `⬆ Ignix Editor … is available`, stop and run the two update commands it prints (the user approves them), then read this guide again before going on: an old version can hang or lack fixes the guide counts on.
|
|
28
|
+
|
|
27
29
|
## 1. The composition contract
|
|
28
30
|
|
|
29
31
|
```html
|
|
@@ -108,6 +110,120 @@ Position the elements with the SAME numbers in CSS. Cursor paths, click ripples
|
|
|
108
110
|
|
|
109
111
|
Decide the zones once per video (e.g. header y 270–440, stage y 460–1300, captions y 1335–1465 on a 1080×1920 reel) and keep them. Put zoomable content (an app window, a phone) inside a stage element with `overflow:hidden`, and move a camera by transforming the content inside it: zooming in then never covers the header or the captions. A soft mask on the stage edges hides the crop.
|
|
110
112
|
|
|
113
|
+
### Optional blocks (only when they fit the script)
|
|
114
|
+
|
|
115
|
+
Ready-made pieces that draw themselves from `t`, like everything else. They are options, not a format: use one when the idea is better told that way, never because it exists. `ignix-editor block` lists them; `ignix-editor block <name> <project>` copies `<name>.js` into the project; load it right after `ignix-frame.js`.
|
|
116
|
+
|
|
117
|
+
**`stickman`** — stick-figure characters that walk, gesture, talk and react (explainers, "you vs. the smart way" sketches, a two-person dialogue, a story told by a character). Poses: `idle talk wave point think shrug cheer arms_crossed present sad sit jump_crouch jump_air`. Faces: `neutral happy sad surprised thinking angry`. Look: `color` (use the brand colour), `hair` (`short spiky curly long bun ponytail none`), `hairColor`, `accessory` (`none glasses cap headphones bow tie`), `theme` (`light`, or `dark` on a dark background).
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<svg id="stage" viewBox="0 0 1080 1920" style="position:absolute;inset:0;width:1080px;height:1920px"></svg>
|
|
121
|
+
```
|
|
122
|
+
```js
|
|
123
|
+
const S = IgnixStickman;
|
|
124
|
+
const ana = S.create(document.getElementById('stage'), { color: '#D24D1E', hair: 'ponytail' });
|
|
125
|
+
run({ id, duration, fps }, (t) => {
|
|
126
|
+
const w = S.walk(t, [0, -150], [1.6, 380]); // walks in from the left, then stops
|
|
127
|
+
const at = ana.draw({
|
|
128
|
+
x: w.x, ground: 1300, scale: 1.5, facing: 1, lift: w.lift ?? 0,
|
|
129
|
+
joints: w.walking ? w.joints : S.poseAt(t, [[0, 'idle'], [1.7, 'wave'], [3, 'talk']]),
|
|
130
|
+
face: S.faceAt(t, [[0, 'neutral'], [1.7, 'happy']]),
|
|
131
|
+
mouth: S.talkAt(t, [[1.8, 2.9]]), // the spans where THIS character's voice line plays
|
|
132
|
+
eyesOpen: S.blinkAt(t, 1), // a different seed per character
|
|
133
|
+
});
|
|
134
|
+
// `at.rightHand`, `at.leftHand`, `at.headTop` are scene coordinates: hang a prop or a speech bubble there.
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- Big: `scale` 1.4–1.6 on a 1080×1920 reel (a figure about 800 px tall); small figures leave the frame empty. Two characters face each other (`facing` 1 and −1).
|
|
139
|
+
- The mouth moves only while its own voice line plays (`talkAt` with that line's start and end); everyone else keeps their mouth shut.
|
|
140
|
+
- Change pose or face on the word that motivates it, as with any overlay. Put the brand's world around them (a desk, a phone, a chart) as simple shapes in the same SVG, drawn first so the characters stand in front.
|
|
141
|
+
|
|
142
|
+
**`kinetic`** — kinetic typography: the words of a line pop in on the exact moment they are said (a hook, a punchline, a key promise). You decide the line breaks (no measuring); each word has its `at` (from the voice timings), and optionally `color`, `size` (relative, e.g. 1.4) or `box` (a filled label behind it). Styles: `pop` (default), `slide`, `punch` (a stamp from big to normal), `type` (typed letter by letter).
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
const kt = IgnixKinetic.create(document.getElementById('kt'), {
|
|
146
|
+
lines: [[{ text: 'Stop', at: 0.2 }, { text: 'losing', at: 0.45 }], [{ text: 'bookings', at: 0.8, color: '#FF5A1F' }]],
|
|
147
|
+
style: 'pop', font: "900 130px 'Montserrat'", stroke: '#000', strokeWidth: 12,
|
|
148
|
+
});
|
|
149
|
+
run({ id, duration, fps }, (t) => kt.draw(t, { out: 3.0 })); // out: when the whole block leaves
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**`chat`** — a story told as a chat on a phone (a customer asking, the business answering, a booking made). Messages appear at their `at`; the other side shows "typing…" just before each of theirs. Themes: `imessage`, `whatsapp`, `instagram` (or your own `colors`). The container is the phone SCREEN; size it and draw the phone frame around it in CSS. Messages may carry an `image` (a local file in `assets/`). Invented conversations are examples: label them as such if they could pass for a real customer.
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
const chat = IgnixChat.create(document.getElementById('screen'), {
|
|
156
|
+
theme: 'whatsapp', title: 'Laura', subtitle: 'online', fontSize: 46,
|
|
157
|
+
messages: [{ from: 'them', text: 'Are you open today?', at: 0.6 }, { from: 'me', text: 'Until 8 pm 😊', at: 1.8 }],
|
|
158
|
+
});
|
|
159
|
+
run({ id, duration, fps }, (t) => chat.draw(t));
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**`whiteboard`** — a whiteboard explanation: shapes and words drawn by hand while a marker follows the stroke. Hand-drawn helpers (same wobble every frame): `IgnixBoard.line`, `rect`, `circle`, `arrow`, `underline`, `strike`; or any SVG path `d`. Each `shape(d, at, dur, { color, stroke, fill })` draws itself from `at` over `dur`; `write(text, x, y, at, dur, { size })` writes text left to right. One thing at a time, timed to the words, and big: on a 1080×1920 reel words of 70–110 px and shapes that use most of the width.
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
const B = IgnixBoard;
|
|
166
|
+
const board = B.create(document.getElementById('board'), { ink: '#1F1D1B', marker: true });
|
|
167
|
+
board.write('Comments', 300, 640, 0.4, 0.6, { size: 90 });
|
|
168
|
+
board.shape(B.circle(300, 610, 260, 120), 1.0, 0.7);
|
|
169
|
+
board.shape(B.arrow(420, 780, 660, 1040), 1.8, 0.5, { color: '#E0452B' });
|
|
170
|
+
run({ id, duration, fps }, (t) => board.draw(t));
|
|
171
|
+
```
|
|
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
|
+
|
|
111
227
|
## 3. Recorded video (edits and clips)
|
|
112
228
|
|
|
113
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.
|
package/src/cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Los mensajes van en inglés, como el manual y la skill: los lee sobre todo el agente.
|
|
4
4
|
import './win-fs-preload.mjs'; // Windows: fs.cpSync con rutas no ASCII (win-fs.mjs)
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
@@ -22,6 +22,7 @@ import { measureStyle } from './analysis/style.mjs';
|
|
|
22
22
|
import { reviewVideo } from './analysis/review.mjs';
|
|
23
23
|
import { pickAudio } from './pick-audio.mjs';
|
|
24
24
|
import { importGiphy, openGiphySearch } from './giphy.mjs';
|
|
25
|
+
import { warnIfOutdated } from './update-check.mjs';
|
|
25
26
|
|
|
26
27
|
const PKG_DIR = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
28
|
const PKG = JSON.parse(readFileSync(join(PKG_DIR, 'package.json'), 'utf8'));
|
|
@@ -255,6 +256,24 @@ const commands = {
|
|
|
255
256
|
|
|
256
257
|
// GIFs, stickers y clips de GIPHY elegidos por la persona (src/giphy.mjs): abre la
|
|
257
258
|
// búsqueda en su navegador, o trae el enlace que ha copiado.
|
|
259
|
+
// Bloques opcionales (src/runtime/blocks): plantillas sencillas que se copian al proyecto
|
|
260
|
+
// y se usan desde run(), como IgnixFrame. Sin nombre, lista los que hay.
|
|
261
|
+
async block(argv) {
|
|
262
|
+
const { pos } = parseArgs(argv, {});
|
|
263
|
+
const dir = join(PKG_DIR, 'src', 'runtime', 'blocks');
|
|
264
|
+
const all = readdirSync(dir).filter((f) => f.endsWith('.js')).map((f) => f.slice(0, -3)).sort();
|
|
265
|
+
const name = pos[0];
|
|
266
|
+
if (!name) {
|
|
267
|
+
console.log(all.join('\n'));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (!all.includes(name)) fail(`Unknown block "${name}". Blocks: ${all.join(', ')}`);
|
|
271
|
+
const project = resolve(pos[1] ?? '.');
|
|
272
|
+
if (!existsSync(join(project, 'index.html'))) fail(`${project} is not an Ignix Editor project (no index.html). Run it inside the project or pass its folder.`);
|
|
273
|
+
copyFileSync(join(dir, `${name}.js`), join(project, `${name}.js`));
|
|
274
|
+
console.log(`✓ ${name}.js copied to ${project}. Load it after ignix-frame.js: <script src="${name}.js"></script>`);
|
|
275
|
+
},
|
|
276
|
+
|
|
258
277
|
async giphy(argv) {
|
|
259
278
|
const { pos, opts } = parseArgs(argv, { search: 'str', kind: 'str', open: 'bool', json: 'bool' });
|
|
260
279
|
const usage = 'Usage: ignix-editor giphy --search "thumbs up" [--kind gifs|stickers|clips] or ignix-editor giphy <link> [folder]';
|
|
@@ -420,6 +439,7 @@ const commands = {
|
|
|
420
439
|
ignix-editor faces <video> [--every 0.2] [-o faces.json] where the face is, smoothed
|
|
421
440
|
ignix-editor pick-audio [folder] --kind music|sfx --query "inspiring" [--order trending]
|
|
422
441
|
opens the Pixabay search in the browser, waits for the person's download, copies it into assets/
|
|
442
|
+
ignix-editor block [name] [folder] lists the optional blocks, or copies one into the project (stickman…)
|
|
423
443
|
ignix-editor giphy --search "thumbs up" [--kind gifs|stickers|clips] opens the GIPHY search in the browser
|
|
424
444
|
ignix-editor giphy <link> [folder] brings the GIF (mp4), sticker (transparent frame sheet) or clip (mp4 with audio) into assets/
|
|
425
445
|
ignix-editor style measure <video>… [--speech] [--stills dir] [-o style.json] someone's editing style, measured on 2–3 of their videos
|
|
@@ -442,6 +462,11 @@ export async function main(argv) {
|
|
|
442
462
|
const [cmd = 'help', ...rest] = argv;
|
|
443
463
|
const fn = commands[cmd === '--version' || cmd === '-v' ? 'version' : cmd === '--help' || cmd === '-h' ? 'help' : cmd];
|
|
444
464
|
if (!fn) fail(`Unknown command: ${cmd}. See: ignix-editor help`);
|
|
465
|
+
// Aviso de versión nueva (src/update-check.mjs): no en `help`/`--version`, que el
|
|
466
|
+
// agente usa justo para comprobar qué tiene.
|
|
467
|
+
if (!['help', 'version', '--version', '-v', '--help', '-h'].includes(cmd)) {
|
|
468
|
+
await warnIfOutdated({ current: PKG.version, pkgDir: PKG_DIR }).catch(() => {});
|
|
469
|
+
}
|
|
445
470
|
try {
|
|
446
471
|
await fn(rest);
|
|
447
472
|
} catch (e) {
|
|
@@ -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,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* chat.js — bloque de Ignix Editor: una historia contada como conversación de chat en un
|
|
3
|
+
* móvil. Los mensajes aparecen a su hora, con «escribiendo…» antes de los del otro lado.
|
|
4
|
+
* Función pura del tiempo; el navegador apila los mensajes (los nuevos empujan arriba a
|
|
5
|
+
* los viejos) y aquí solo se decide cuáles se ven y cómo entran: nunca se mide la página.
|
|
6
|
+
*
|
|
7
|
+
* const C = IgnixChat;
|
|
8
|
+
* const chat = C.create(document.getElementById('phone'), {
|
|
9
|
+
* theme: 'imessage', // imessage | whatsapp | instagram
|
|
10
|
+
* title: 'Laura', subtitle: 'online',
|
|
11
|
+
* messages: [
|
|
12
|
+
* { from: 'them', text: 'Are you still open today?', at: 0.6 },
|
|
13
|
+
* { from: 'me', text: 'Yes! Until 8 pm 😊', at: 2.0 },
|
|
14
|
+
* { from: 'them', text: 'Perfect, booking now', at: 3.6 },
|
|
15
|
+
* ],
|
|
16
|
+
* });
|
|
17
|
+
* run({ id, duration, fps }, (t) => chat.draw(t));
|
|
18
|
+
*
|
|
19
|
+
* El contenedor es la PANTALLA del móvil (p. ej. 820×1500): su tamaño lo pones tú en CSS.
|
|
20
|
+
*/
|
|
21
|
+
(function () {
|
|
22
|
+
const clamp01 = (x) => Math.min(1, Math.max(0, x));
|
|
23
|
+
const outBack = (x) => 1 + 2.4 * (x - 1) ** 3 + 1.4 * (x - 1) ** 2;
|
|
24
|
+
const THEMES = {
|
|
25
|
+
imessage: { bg: '#FFFFFF', head: '#F6F6F6', ink: '#111111', me: '#0A84FF', meInk: '#FFFFFF', them: '#E9E9EB', themInk: '#111111', font: "-apple-system, 'Helvetica Neue', Arial, sans-serif", radius: 38 },
|
|
26
|
+
whatsapp: { bg: '#EFE7DE', head: '#075E54', ink: '#FFFFFF', me: '#DCF8C6', meInk: '#111111', them: '#FFFFFF', themInk: '#111111', font: "'Helvetica Neue', Arial, sans-serif", radius: 22 },
|
|
27
|
+
instagram: { bg: '#FFFFFF', head: '#FFFFFF', ink: '#111111', me: 'linear-gradient(135deg,#7B2FF7,#F107A3)', meInk: '#FFFFFF', them: '#EFEFEF', themInk: '#111111', font: "-apple-system, 'Helvetica Neue', Arial, sans-serif", radius: 40 },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function create(screen, { theme = 'imessage', title = '', subtitle = '', messages = [], fontSize = 46, typing = 0.9, colors = {} } = {}) {
|
|
31
|
+
const T = { ...THEMES[theme], ...colors };
|
|
32
|
+
screen.innerHTML = '';
|
|
33
|
+
Object.assign(screen.style, { background: T.bg, display: 'flex', flexDirection: 'column', overflow: 'hidden', fontFamily: T.font });
|
|
34
|
+
const head = document.createElement('div');
|
|
35
|
+
Object.assign(head.style, { background: T.head, color: T.ink, padding: `${fontSize * 0.9}px ${fontSize}px ${fontSize * 0.6}px`, textAlign: 'center', borderBottom: '1px solid rgba(0,0,0,.08)', flex: '0 0 auto' });
|
|
36
|
+
head.innerHTML = `<div style="font-weight:700;font-size:${fontSize * 1.05}px">${esc(title)}</div>${subtitle ? `<div style="opacity:.6;font-size:${fontSize * 0.62}px;margin-top:4px">${esc(subtitle)}</div>` : ''}`;
|
|
37
|
+
screen.appendChild(head);
|
|
38
|
+
const list = document.createElement('div');
|
|
39
|
+
Object.assign(list.style, { flex: '1 1 auto', display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', gap: `${fontSize * 0.35}px`, padding: `${fontSize * 0.6}px ${fontSize * 0.55}px`, overflow: 'hidden' });
|
|
40
|
+
screen.appendChild(list);
|
|
41
|
+
|
|
42
|
+
const items = [];
|
|
43
|
+
for (const m of messages) {
|
|
44
|
+
const mine = m.from === 'me';
|
|
45
|
+
if (!mine && typing > 0) {
|
|
46
|
+
const dots = document.createElement('div');
|
|
47
|
+
Object.assign(dots.style, { alignSelf: 'flex-start', background: T.them, borderRadius: `${T.radius}px`, padding: `${fontSize * 0.42}px ${fontSize * 0.6}px`, display: 'none', gap: `${fontSize * 0.18}px` });
|
|
48
|
+
dots.innerHTML = [0, 1, 2].map(() => `<span style="display:inline-block;width:${fontSize * 0.3}px;height:${fontSize * 0.3}px;border-radius:50%;background:#8E8E93;margin:0 ${fontSize * 0.07}px"></span>`).join('');
|
|
49
|
+
list.appendChild(dots);
|
|
50
|
+
items.push({ kind: 'typing', el: dots, from: Math.max(0, m.at - typing), to: m.at });
|
|
51
|
+
}
|
|
52
|
+
const b = document.createElement('div');
|
|
53
|
+
Object.assign(b.style, {
|
|
54
|
+
alignSelf: mine ? 'flex-end' : 'flex-start', maxWidth: '78%', background: mine ? T.me : T.them, color: mine ? T.meInk : T.themInk,
|
|
55
|
+
borderRadius: `${T.radius}px`, padding: `${fontSize * 0.42}px ${fontSize * 0.62}px`, fontSize: `${fontSize}px`, lineHeight: '1.25',
|
|
56
|
+
transformOrigin: mine ? '100% 100%' : '0% 100%', display: 'none', whiteSpace: 'pre-wrap',
|
|
57
|
+
});
|
|
58
|
+
if (m.image) b.innerHTML = `<img src="${m.image}" style="display:block;width:100%;border-radius:${T.radius * 0.6}px;margin-bottom:${m.text ? 10 : 0}px">${esc(m.text ?? '')}`;
|
|
59
|
+
else b.textContent = m.text;
|
|
60
|
+
list.appendChild(b);
|
|
61
|
+
items.push({ kind: 'msg', el: b, at: m.at });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function draw(t) {
|
|
65
|
+
for (const it of items) {
|
|
66
|
+
if (it.kind === 'typing') {
|
|
67
|
+
const on = t >= it.from && t < it.to;
|
|
68
|
+
it.el.style.display = on ? 'flex' : 'none';
|
|
69
|
+
if (on) [...it.el.children].forEach((d, i) => (d.style.opacity = (0.35 + 0.65 * Math.max(0, Math.sin((t - it.from) * 9 - i * 0.9))).toFixed(3)));
|
|
70
|
+
} else {
|
|
71
|
+
const k = clamp01((t - it.at) / 0.28);
|
|
72
|
+
it.el.style.display = t >= it.at ? 'block' : 'none';
|
|
73
|
+
it.el.style.transform = `scale(${(0.6 + 0.4 * outBack(k)).toFixed(4)})`;
|
|
74
|
+
it.el.style.opacity = clamp01(k * 3).toFixed(3);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { draw };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function esc(s) {
|
|
82
|
+
return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
window.IgnixChat = { create };
|
|
86
|
+
})();
|
|
@@ -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
|
+
})();
|