sagadeck 1.2.0
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/LICENSE +21 -0
- package/README.md +305 -0
- package/SKILL.md +148 -0
- package/bin/sagadeck.js +511 -0
- package/docs/ANDAMENTO.md +1255 -0
- package/docs/EXPERIENCIAS.md +39 -0
- package/docs/PLANO-EXPERIENCIAS.md +39 -0
- package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
- package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
- package/docs/REFERENCIA.md +1131 -0
- package/package.json +59 -0
- package/src/adaptive-layouts.js +18 -0
- package/src/ai/art-direction.js +8 -0
- package/src/ai/commands.js +130 -0
- package/src/ai/context.js +245 -0
- package/src/ai/deck-ai.js +1135 -0
- package/src/ai/directions.js +74 -0
- package/src/ai/document-materials.js +49 -0
- package/src/ai/document-visuals.js +215 -0
- package/src/ai/evaluation.js +31 -0
- package/src/ai/ground.js +55 -0
- package/src/ai/llm.js +292 -0
- package/src/ai/quality.js +51 -0
- package/src/ai/relay.js +45 -0
- package/src/ai/staged-generation.js +24 -0
- package/src/ai/transform.js +858 -0
- package/src/ai/usage.js +17 -0
- package/src/ai/variety.js +80 -0
- package/src/ai/video-frames.js +24 -0
- package/src/ai/video-generation.js +126 -0
- package/src/ai/web-operation.js +31 -0
- package/src/algo-catalog.js +153 -0
- package/src/algo-trace.js +139 -0
- package/src/api-client.js +514 -0
- package/src/api-collections.js +14 -0
- package/src/api-demo.js +222 -0
- package/src/aspect.js +40 -0
- package/src/build.js +323 -0
- package/src/carousel.js +56 -0
- package/src/chrome.js +65 -0
- package/src/code-highlight.js +68 -0
- package/src/code-lab-browser.js +22 -0
- package/src/code-lab.js +3 -0
- package/src/code-language.js +52 -0
- package/src/csv.js +74 -0
- package/src/decision-lab.js +20 -0
- package/src/deck-file.js +119 -0
- package/src/diagram/napkin-examples.js +13 -0
- package/src/diagram/napkin.js +539 -0
- package/src/docx.js +128 -0
- package/src/dynamics/git-sim.js +200 -0
- package/src/dynamics/layouts.js +112 -0
- package/src/elements.js +554 -0
- package/src/experiences.js +124 -0
- package/src/exploration.js +31 -0
- package/src/export/browser.js +53 -0
- package/src/export/estudo.js +67 -0
- package/src/export/pptx.js +475 -0
- package/src/export/roteiro.js +40 -0
- package/src/export/shots.js +182 -0
- package/src/figures/charts.js +383 -0
- package/src/figures/diagrams.js +117 -0
- package/src/figures/icons.js +166 -0
- package/src/figures/material-pictos.js +20 -0
- package/src/figures/pictos.js +255 -0
- package/src/figures/points.js +106 -0
- package/src/figures/qr.js +29 -0
- package/src/figures/ufmap.js +48 -0
- package/src/fiscal/autofix.js +377 -0
- package/src/fiscal/normalize.js +115 -0
- package/src/graph-lab.js +24 -0
- package/src/hologram.js +30 -0
- package/src/identity.js +86 -0
- package/src/import/crop.js +59 -0
- package/src/import/index.js +132 -0
- package/src/import/legacy-word.js +42 -0
- package/src/import/merge.js +45 -0
- package/src/import/office.js +123 -0
- package/src/import/omml.js +69 -0
- package/src/import/pdf-render.js +56 -0
- package/src/import/pptx.js +701 -0
- package/src/import/xml.js +42 -0
- package/src/infographic.js +401 -0
- package/src/layouts.js +723 -0
- package/src/lessons.js +171 -0
- package/src/library.js +399 -0
- package/src/markup.js +116 -0
- package/src/master.js +214 -0
- package/src/mcp/server.js +371 -0
- package/src/meeting.js +42 -0
- package/src/motion.js +56 -0
- package/src/package.js +116 -0
- package/src/playground.js +19 -0
- package/src/portal-scene.js +20 -0
- package/src/preferences.js +61 -0
- package/src/protect.js +51 -0
- package/src/purpose.js +24 -0
- package/src/pytrace.js +1092 -0
- package/src/research/duckduckgo.js +73 -0
- package/src/research/research.js +202 -0
- package/src/research/svg-image.js +17 -0
- package/src/research/visuals.js +52 -0
- package/src/runtime/api-collection-ui.js +5 -0
- package/src/runtime/api-collections.js +28 -0
- package/src/runtime/api-core.js +530 -0
- package/src/runtime/api-ui.js +966 -0
- package/src/runtime/base.css +1774 -0
- package/src/runtime/calc.js +139 -0
- package/src/runtime/code-lab.js +371 -0
- package/src/runtime/decision-lab.js +49 -0
- package/src/runtime/diagram.js +341 -0
- package/src/runtime/fit.js +268 -0
- package/src/runtime/fonts/caveat.css +2 -0
- package/src/runtime/fonts/comic-neue.css +3 -0
- package/src/runtime/fonts/fredoka.css +2 -0
- package/src/runtime/fonts/index.json +10 -0
- package/src/runtime/fonts/inter.css +2 -0
- package/src/runtime/fonts/patrick-hand.css +2 -0
- package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
- package/src/runtime/fonts/press-start-2p.css +2 -0
- package/src/runtime/fonts/vt323.css +2 -0
- package/src/runtime/formula.js +148 -0
- package/src/runtime/graph-lab.js +72 -0
- package/src/runtime/meeting.css +7 -0
- package/src/runtime/meeting.js +10 -0
- package/src/runtime/motion.js +15 -0
- package/src/runtime/playground.js +11 -0
- package/src/runtime/points.js +19 -0
- package/src/runtime/portal-scene.css +6 -0
- package/src/runtime/portal-scene.js +22 -0
- package/src/runtime/runtime.js +827 -0
- package/src/runtime/science.js +89 -0
- package/src/runtime/skins/aluminio.css +31 -0
- package/src/runtime/skins/arcade.css +25 -0
- package/src/runtime/skins/aurora.css +17 -0
- package/src/runtime/skins/bauhaus.css +19 -0
- package/src/runtime/skins/editorial.css +19 -0
- package/src/runtime/skins/jornal.css +17 -0
- package/src/runtime/skins/manual-noite.css +14 -0
- package/src/runtime/skins/manual.css +14 -0
- package/src/runtime/skins/noite.css +21 -0
- package/src/runtime/skins/oceano.css +17 -0
- package/src/runtime/skins/pop.css +23 -0
- package/src/runtime/skins/prata.css +18 -0
- package/src/runtime/skins/prisma.css +28 -0
- package/src/runtime/skins/rabisco.css +18 -0
- package/src/runtime/skins/relevo.css +33 -0
- package/src/runtime/skins/sinal.css +16 -0
- package/src/runtime/skins/terminal.css +23 -0
- package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
- package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
- package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
- package/src/runtime/vendor/katex.css +1 -0
- package/src/runtime/vendor/mermaid.min.js +3636 -0
- package/src/runtime/vendor/plotly.min.js +3620 -0
- package/src/runtime/video-player.js +35 -0
- package/src/science.js +162 -0
- package/src/studio/ai-response.js +36 -0
- package/src/studio/api-routes.js +152 -0
- package/src/studio/assets/contexto/leia-me.md +12 -0
- package/src/studio/assets/contexto/vendas.csv +7 -0
- package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
- package/src/studio/assets/imagens/colecao-cor.png +0 -0
- package/src/studio/assets/imagens/colecao-equipe.png +0 -0
- package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
- package/src/studio/code-routes.js +32 -0
- package/src/studio/command-approval.js +48 -0
- package/src/studio/demo-decks.js +98 -0
- package/src/studio/direction-routes.js +29 -0
- package/src/studio/errors.js +65 -0
- package/src/studio/exploration-demo.js +17 -0
- package/src/studio/exporting.js +105 -0
- package/src/studio/generation-report.js +10 -0
- package/src/studio/instance.js +48 -0
- package/src/studio/layout-samples.js +152 -0
- package/src/studio/project.js +371 -0
- package/src/studio/public/api-collections.js +28 -0
- package/src/studio/public/app.js +5265 -0
- package/src/studio/public/art-preview.js +10 -0
- package/src/studio/public/calc-fields.js +11 -0
- package/src/studio/public/chat-progress.js +47 -0
- package/src/studio/public/developer-fields.js +22 -0
- package/src/studio/public/explorer.js +443 -0
- package/src/studio/public/history.js +58 -0
- package/src/studio/public/index.html +867 -0
- package/src/studio/public/inspector.js +270 -0
- package/src/studio/public/lab-fields.js +5 -0
- package/src/studio/public/library.css +275 -0
- package/src/studio/public/library.html +55 -0
- package/src/studio/public/library.js +471 -0
- package/src/studio/public/merge-decks.js +155 -0
- package/src/studio/public/review-ui.js +100 -0
- package/src/studio/public/screenshot-editor.js +140 -0
- package/src/studio/public/share-ui.js +72 -0
- package/src/studio/public/slide-clipboard.js +23 -0
- package/src/studio/public/slide-form.js +1263 -0
- package/src/studio/public/studio-awareness.js +154 -0
- package/src/studio/public/studio-next.css +303 -0
- package/src/studio/public/style.css +1262 -0
- package/src/studio/public/ui-icons.js +2 -0
- package/src/studio/public/viewers.js +372 -0
- package/src/studio/public/visual-editor.js +326 -0
- package/src/studio/public/workspace.js +18 -0
- package/src/studio/public-files.js +1 -0
- package/src/studio/request-helpers.js +41 -0
- package/src/studio/server.js +1583 -0
- package/src/studio/share-routes.js +146 -0
- package/src/studio/slide-copy-routes.js +26 -0
- package/src/studio/slide-copy.js +30 -0
- package/src/studio/snapshot.js +160 -0
- package/src/studio/style-routes.js +103 -0
- package/src/studio/template-collections.js +97 -0
- package/src/studio/versions.js +95 -0
- package/src/studio/video-download.js +29 -0
- package/src/studio/visual-keys.js +52 -0
- package/src/studio/workspaces.js +39 -0
- package/src/table.js +93 -0
- package/src/templates/scaffold.js +372 -0
- package/src/themes.js +604 -0
- package/src/trace-view.js +450 -0
- package/src/uid.js +19 -0
- package/src/video-player.js +40 -0
- package/src/visual-edits.js +99 -0
- package/src/ws.js +134 -0
- package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
- package/templates/cenario/imagens/cidade.svg +30 -0
- package/templates/cenario/imagens/pessoa.svg +15 -0
- package/templates/cenario-e-ciencia.yaml +47 -0
- package/templates/ensaio-api.yaml +160 -0
- package/templates/exemplo-alegre.yaml +75 -0
- package/templates/exemplo-keynote.yaml +90 -0
- package/templates/exemplo-napkin.yaml +139 -0
- package/templates/exemplo.yaml +228 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// XML mínimo para ler Office Open XML (pptx/docx/xlsx): vira árvore { name, attrs, children, text }.
|
|
2
|
+
// Sem DTD, sem entidades externas, sem nada que execute: só elementos, atributos, texto e CDATA.
|
|
3
|
+
const ENT = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
|
|
4
|
+
const decode = (s) => s.replace(/&(#x[0-9a-f]+|#\d+|lt|gt|amp|quot|apos);/gi, (m, e) => (e[0] === "#" ? String.fromCodePoint(e[1].toLowerCase() === "x" ? parseInt(e.slice(2), 16) : +e.slice(1)) : ENT[e.toLowerCase()] ?? m));
|
|
5
|
+
|
|
6
|
+
export function parseXML(src) {
|
|
7
|
+
const root = { name: "#root", attrs: {}, children: [] };
|
|
8
|
+
const stack = [root];
|
|
9
|
+
const re = /<!\[CDATA\[([\s\S]*?)\]\]>|<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<!DOCTYPE[^>]*>|<(\/?)([\w:.-]+)((?:\s+[\w:.-]+\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/?)>|([^<]+)/g;
|
|
10
|
+
let m;
|
|
11
|
+
while ((m = re.exec(src))) {
|
|
12
|
+
const top = stack[stack.length - 1];
|
|
13
|
+
if (m[1] != null) { top.children.push({ name: "#text", text: m[1] }); continue; }
|
|
14
|
+
if (m[6] != null) { if (top !== root) top.children.push({ name: "#text", text: decode(m[6]) }); continue; }
|
|
15
|
+
if (!m[3]) continue;
|
|
16
|
+
if (m[2]) { // fecha
|
|
17
|
+
while (stack.length > 1 && stack.pop().name !== m[3]);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const attrs = {};
|
|
21
|
+
for (const a of m[4].matchAll(/([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) attrs[a[1]] = decode(a[2] ?? a[3]);
|
|
22
|
+
const node = { name: m[3], attrs, children: [] };
|
|
23
|
+
top.children.push(node);
|
|
24
|
+
if (!m[5]) stack.push(node);
|
|
25
|
+
}
|
|
26
|
+
return root.children.find((c) => c.name !== "#text") || root;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// consultas simples: filhos por nome (com ou sem prefixo), primeiro, texto
|
|
30
|
+
export const kids = (n, name) => (n?.children || []).filter((c) => c.name !== "#text" && (name == null || c.name === name || (!name.includes(":") && c.name.split(":").pop() === name)));
|
|
31
|
+
export const kid = (n, name) => kids(n, name)[0] || null;
|
|
32
|
+
export const path = (n, ...names) => names.reduce((a, nm) => (a ? kid(a, nm) : null), n);
|
|
33
|
+
export function all(n, name, out = []) {
|
|
34
|
+
for (const c of n?.children || []) {
|
|
35
|
+
if (c.name === "#text") continue;
|
|
36
|
+
if (c.name === name || (!name.includes(":") && c.name.split(":").pop() === name)) out.push(c);
|
|
37
|
+
all(c, name, out);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
export const textOf = (n) => (n ? (n.children || []).map((c) => (c.name === "#text" ? c.text : textOf(c))).join("") : "");
|
|
42
|
+
export const num = (v, d = 0) => (v == null || v === "" || Number.isNaN(Number(v)) ? d : Number(v));
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// Infográficos (layout infographic): as formas clássicas de slide de consultoria, desenhadas na hora para qualquer
|
|
2
|
+
// quantidade de itens. A geometria (arcos, trilhas, anéis, linhas de metrô) é SVG; os textos são HTML de verdade
|
|
3
|
+
// por cima, no mesmo sistema de coordenadas (viram caixas de texto editáveis no PowerPoint) e encolhem para caber
|
|
4
|
+
// (data-fit). Uma cor por item, tirada do tema: numa paleta de família, os parentes dela; senão, tons harmônicos
|
|
5
|
+
// ao destaque. Neutros (fundo, linhas, texto) são as variáveis do tom do slide, então funciona em claro e escuro.
|
|
6
|
+
//
|
|
7
|
+
// shape: arco | ramos | lados | trilhas | metro | ciclo | pista | camadas
|
|
8
|
+
// center: "Título" ou { title, text, icon }
|
|
9
|
+
// items: [{ title, text, icon, steps: [...] (só trilhas) }]
|
|
10
|
+
import { md } from "./markup.js";
|
|
11
|
+
import { iconSVG } from "./figures/icons.js";
|
|
12
|
+
|
|
13
|
+
const W = 1680, H = 700;
|
|
14
|
+
export const INFOGRAPHIC_SHAPES = {
|
|
15
|
+
arco: { max: 8, label: "Arco: centro com anel colorido e itens numerados em pílulas ao longo de um arco" },
|
|
16
|
+
ramos: { max: 6, label: "Ramos: centro e cartões numerados com título e texto, ligados por um arco" },
|
|
17
|
+
lados: { max: 8, label: "Lados: peça central e itens com ícone em anel, metade de cada lado" },
|
|
18
|
+
trilhas: { max: 4, label: "Trilhas: um objetivo e linhas de etapas encadeadas (estratégia, tática…), uma cor por coluna" },
|
|
19
|
+
metro: { max: 7, label: "Metrô: centro e linhas que se abrem até cada item, com ícone e legenda" },
|
|
20
|
+
ciclo: { max: 8, label: "Ciclo: etapas em volta de um círculo, cada uma ligada à seguinte por uma seta curva (ciclo da água, PDCA, ciclo de vida)" },
|
|
21
|
+
pista: { max: 8, label: "Pista: um circuito de corrida visto de cima (estilo Mario Kart), com os itens como marcos ao longo da volta (uma história, uma jornada, as fases de um projeto)" },
|
|
22
|
+
camadas: { max: 6, label: "Camadas: um perfil físico de cima para baixo como na realidade (atmosfera, superfície, solo, lençol, rocha), com os fluxos entre as camadas como setas (flows)" },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ---- cores ----------------------------------------------------------------------------------------------------
|
|
26
|
+
const rgb = (h) => [0, 2, 4].map((i) => parseInt(String(h).replace("#", "").slice(i, i + 2), 16));
|
|
27
|
+
const toHex = (a) => a.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
28
|
+
const mix = (a, b, p) => toHex(rgb(a).map((v, i) => v * p + rgb(b)[i] * (1 - p)));
|
|
29
|
+
const lum = (h) => { const [r, g, b] = rgb(h).map((v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; }); return 0.2126 * r + 0.7152 * g + 0.0722 * b; };
|
|
30
|
+
const contrast = (a, b) => { const [x, y] = [lum(a), lum(b)].sort((m, n) => n - m); return (x + 0.05) / (y + 0.05); };
|
|
31
|
+
function toHsl(h) {
|
|
32
|
+
const [r, g, b] = rgb(h).map((v) => v / 255), mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn, l = (mx + mn) / 2;
|
|
33
|
+
const hh = !d ? 0 : mx === r ? ((g - b) / d) % 6 : mx === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
34
|
+
return { h: (hh * 60 + 360) % 360, s: d ? d / (1 - Math.abs(2 * l - 1)) : 0, l };
|
|
35
|
+
}
|
|
36
|
+
function fromHsl(h, s, l) {
|
|
37
|
+
const k = (n) => (n + h / 30) % 12, a = s * Math.min(l, 1 - l);
|
|
38
|
+
return toHex([0, 8, 4].map((n) => 255 * (l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))))));
|
|
39
|
+
}
|
|
40
|
+
const dist = (a, b) => { const d = Math.abs(a - b) % 360; return Math.min(d, 360 - d); };
|
|
41
|
+
const CANDIDATOS = [212, 272, 145, 28, 335, 175, 48, 300, 100, 195];
|
|
42
|
+
|
|
43
|
+
// n cores de item para o tema (hex sem #)
|
|
44
|
+
export function itemColors(theme, n) {
|
|
45
|
+
const c = theme.colors || {}, paper = c.paper || "FFFFFF", ink = c.ink || "222222", accent = c.accent || "0F6CBD";
|
|
46
|
+
// cor que aparece sobre o fundo e aceita letra clara por cima (contraste 3): clara demais escurece mantendo o matiz
|
|
47
|
+
const visible = (x) => {
|
|
48
|
+
if (contrast(x, paper) >= 3) return x;
|
|
49
|
+
const { h, s, l } = toHsl(x);
|
|
50
|
+
for (let L = l; L > 0.18; L -= 0.02) { const y = fromHsl(h, Math.max(s, 0.45), L); if (contrast(y, paper) >= 3) return y; }
|
|
51
|
+
return mix(ink, x, 0.6);
|
|
52
|
+
};
|
|
53
|
+
const out = [visible(accent)];
|
|
54
|
+
const differs = (x) => out.every((y) => contrast(x, y) > 1.25 || dist(toHsl(x).h, toHsl(y).h) > 14);
|
|
55
|
+
const fam = Object.keys(c).filter((k) => /^f\d+$/.test(k)).sort((a, b) => a.slice(1) - b.slice(1)).map((k) => c[k]);
|
|
56
|
+
if (fam.length >= 2) {
|
|
57
|
+
for (const f of fam.map(visible)) if (differs(f)) out.push(f);
|
|
58
|
+
const { h, s, l } = toHsl(out[0]);
|
|
59
|
+
for (const [dh, dl] of [[16, 0], [-16, 0], [0, -0.12], [0, 0.1], [16, -0.1], [-16, 0.1]]) { if (out.length >= Math.max(n, 4)) break; const y = visible(fromHsl((h + dh + 360) % 360, Math.max(s, 0.4), Math.min(0.7, Math.max(0.25, l + dl)))); if (differs(y)) out.push(y); }
|
|
60
|
+
} else {
|
|
61
|
+
const { h, s, l } = toHsl(out[0]);
|
|
62
|
+
const S = Math.min(0.7, Math.max(0.45, s)), L = Math.min(0.56, Math.max(0.42, l));
|
|
63
|
+
for (const hh of CANDIDATOS) { if (out.length >= Math.max(n, 6)) break; if (dist(hh, h) > 30 && out.every((y) => dist(toHsl(y).h, hh) > 30)) out.push(visible(fromHsl(hh, S, L))); }
|
|
64
|
+
}
|
|
65
|
+
return Array.from({ length: n }, (_, i) => out[i % out.length]);
|
|
66
|
+
}
|
|
67
|
+
// letra sobre uma cor: a do papel (clara) se lê bem; senão, a tinta
|
|
68
|
+
const onColor = (theme, x) => { const p = theme.colors?.paper || "FFFFFF", k = theme.colors?.ink || "222222"; return contrast(x, p) >= 3 ? p : k; };
|
|
69
|
+
|
|
70
|
+
// ---- peças ------------------------------------------------------------------------------------------------------
|
|
71
|
+
const F = (n) => +n.toFixed(1);
|
|
72
|
+
const FG = "color-mix(in srgb,var(--fg) 86%,var(--bg))"; // texto: grafite, nunca a cor cheia do tema
|
|
73
|
+
const LINE = "color-mix(in srgb,var(--fg) 45%,var(--bg))";
|
|
74
|
+
const icon = (name, size, color) => {
|
|
75
|
+
if (!name) return "";
|
|
76
|
+
try { return `<span class="ig-ic" style="color:${color};width:${size}px;height:${size}px">${iconSVG(name, { size, stroke: 1.9 })}</span>`; } catch { return ""; }
|
|
77
|
+
};
|
|
78
|
+
// Caixa de texto posicionada. Quem encolhe para caber é a caixa (data-fit, altura fixa): título e texto dentro
|
|
79
|
+
// dela têm o tamanho em em (base 20px), então encolhem juntos, na mesma proporção. Só pelo próprio tamanho
|
|
80
|
+
// (data-fit-self): é posicionada, encolher não abre espaço para o que vaza noutro canto do slide.
|
|
81
|
+
// Alinhamento "safe": se não couber, alinha pelo topo e o excesso vai para baixo, onde a medida (scrollHeight) o vê;
|
|
82
|
+
// centralizado ou no pé, o excesso subiria por cima da caixa sem ninguém perceber (e o título sairia cortado).
|
|
83
|
+
function box(x, y, w, h, inner, { align = "left", valign = "center", cls = "" } = {}) {
|
|
84
|
+
return `<div class="ig-box ${cls}" data-fit data-fit-self style="font-size:20px;left:${F(x)}px;top:${F(y)}px;width:${F(w)}px;height:${F(h)}px;text-align:${align};justify-content:${valign === "top" ? "flex-start" : valign === "bottom" ? "safe flex-end" : "safe center"};align-items:${align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start"}">${inner}</div>`;
|
|
85
|
+
}
|
|
86
|
+
const em = (px) => `${F(px / 20)}em`;
|
|
87
|
+
const title = (t, size, color, extra = "") => (t ? `<div class="t f-heading ig-t" style="font-size:${em(size)};color:${color};${extra}">${md(String(t))}</div>` : "");
|
|
88
|
+
const body = (t, size, color) => (t ? `<div class="t f-body ig-x" style="font-size:${em(size)};color:${color}">${md(String(t))}</div>` : "");
|
|
89
|
+
const svg = (inner) => `<svg class="ig-svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" aria-hidden="true">${inner}</svg>`;
|
|
90
|
+
const arcPath = (cx, cy, r, a0, a1) => {
|
|
91
|
+
const p = (a) => [cx + r * Math.cos(a), cy + r * Math.sin(a)];
|
|
92
|
+
const [x0, y0] = p(a0), [x1, y1] = p(a1);
|
|
93
|
+
return `M${F(x0)} ${F(y0)} A${F(r)} ${F(r)} 0 ${Math.abs(a1 - a0) > Math.PI ? 1 : 0} 1 ${F(x1)} ${F(y1)}`;
|
|
94
|
+
};
|
|
95
|
+
// Sombra e ponta de seta com nome próprio por slide: com o mesmo nome em todos, o navegador usaria a definição do
|
|
96
|
+
// primeiro slide, que fica invisível enquanto outro está na tela (e a seta sumiria junto).
|
|
97
|
+
let ID = "ig";
|
|
98
|
+
const shadow = () => `<defs><filter id="${ID}s" x="-20%" y="-20%" width="140%" height="140%"><feDropShadow dx="0" dy="4" stdDeviation="6" flood-opacity=".16"/></filter><marker id="${ID}a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0L10 5L0 10z" style="fill:${LINE}"/></marker></defs>`;
|
|
99
|
+
const hash = (t) => { let h = 5381; for (let i = 0; i < t.length; i++) h = ((h << 5) + h + t.charCodeAt(i)) >>> 0; return h.toString(36); };
|
|
100
|
+
// cada item numa camada: com build, aparece no seu clique
|
|
101
|
+
const layer = (inner, step) => `<div class="ig-layer"${step ? ` data-step="${step}"` : ""}>${inner}</div>`;
|
|
102
|
+
const norm = (x) => (typeof x === "string" ? { title: x } : x && typeof x === "object" ? x : {});
|
|
103
|
+
const centerOf = (s) => norm(s.center ?? s.hub ?? "");
|
|
104
|
+
|
|
105
|
+
// ---- formas -----------------------------------------------------------------------------------------------------
|
|
106
|
+
function arco(s, items, cols, theme) {
|
|
107
|
+
const n = items.length, cx = 400, cy = H / 2, R = 205;
|
|
108
|
+
const ph = Math.min(104, (H - 16) / n - 14);
|
|
109
|
+
// pílulas em alturas iguais (sem encavalar nas pontas); o ponto de cada uma no arco sai da altura
|
|
110
|
+
const span = n > 1 ? Math.min(H - ph - 12, (ph + 16) * (n - 1) * 1.25) : 0, ys = items.map((_, i) => (n > 1 ? cy - span / 2 + (i * span) / (n - 1) : cy));
|
|
111
|
+
const R2 = Math.max(R + 130, span / 2 / Math.sin((62 * Math.PI) / 180));
|
|
112
|
+
const thm = n > 1 ? Math.asin(Math.min(1, span / 2 / R2)) : 0;
|
|
113
|
+
const c = centerOf(s);
|
|
114
|
+
const seg = (2 * Math.PI) / n, gap = 0.06;
|
|
115
|
+
let base = svg(`${shadow()}<path d="${arcPath(cx, cy, R2, -thm - 0.26, thm + 0.26)}" style="fill:none;stroke:${LINE}" stroke-width="5" stroke-linecap="round"/>
|
|
116
|
+
${[-thm - 0.26, thm + 0.26].map((a) => `<circle cx="${F(cx + R2 * Math.cos(a))}" cy="${F(cy + R2 * Math.sin(a))}" r="8" style="fill:${LINE}"/>`).join("")}
|
|
117
|
+
${cols.map((col, i) => `<path d="${arcPath(cx, cy, R + 26, -Math.PI / 2 + i * seg + gap / 2, -Math.PI / 2 + (i + 1) * seg - gap / 2)}" style="fill:none;stroke:#${col}" stroke-width="22"/>`).join("")}
|
|
118
|
+
<circle cx="${cx}" cy="${cy}" r="${R}" style="fill:var(--bg);stroke:${LINE}" stroke-width="1.5" filter="url(#${ID}s)"/>`);
|
|
119
|
+
base += box(cx - R * 0.74, cy - R * 0.66, R * 1.48, R * 1.32, `${icon(c.icon, 64, FG)}${title(c.title, 50, FG, "font-weight:800;line-height:1.05")}${body(c.text, 22, FG)}`, { align: "center", cls: "ig-center" });
|
|
120
|
+
const layers = items.map((it, i) => {
|
|
121
|
+
const dy = ys[i], dx = cx + Math.sqrt(R2 * R2 - (dy - cy) ** 2), col = cols[i], on = onColor(theme, col);
|
|
122
|
+
const bx = dx + 92, br = ph / 2 + 4;
|
|
123
|
+
const g = svg(`<line x1="${F(dx)}" y1="${F(dy)}" x2="${F(bx - br)}" y2="${F(dy)}" style="stroke:${LINE}" stroke-width="3"/>
|
|
124
|
+
<circle cx="${F(dx)}" cy="${F(dy)}" r="17" style="fill:#${col};stroke:var(--bg)" stroke-width="6" filter="url(#${ID}s)"/>
|
|
125
|
+
<rect x="${F(bx)}" y="${F(dy - ph / 2)}" width="${F(W - 6 - bx)}" height="${F(ph)}" rx="${F(ph / 2)}" style="fill:#${col}" filter="url(#${ID}s)"/>
|
|
126
|
+
<circle cx="${F(bx)}" cy="${F(dy)}" r="${F(br)}" style="fill:var(--bg)" filter="url(#${ID}s)"/>`);
|
|
127
|
+
const num = box(bx - br, dy - br, br * 2, br * 2, it.icon ? icon(it.icon, br * 0.95, `#${col}`) : title(String(i + 1).padStart(2, "0"), Math.min(40, br * 0.9), `#${col}`, "font-weight:800"), { align: "center" });
|
|
128
|
+
const tx = bx + br + 22;
|
|
129
|
+
return layer(g + num + box(tx, dy - ph / 2 + 6, W - 34 - tx, ph - 12, `${title(it.title, Math.min(30, ph * 0.34), `#${on}`, "font-weight:700")}${body(it.text, Math.min(21, ph * 0.24), `#${on}`)}`), s.build ? i + 1 : 0);
|
|
130
|
+
});
|
|
131
|
+
return base + layers.join("");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ramos(s, items, cols, theme) {
|
|
135
|
+
const n = items.length, cx = 300, cy = H / 2, R = 172, Rs = R + 105;
|
|
136
|
+
const c = centerOf(s), dark = "color-mix(in srgb,var(--fg) 88%,var(--bg))";
|
|
137
|
+
let base = svg(`${shadow()}<circle cx="${cx}" cy="${cy}" r="${R + 62}" style="fill:none;stroke:${LINE}" stroke-width="5" stroke-dasharray="14 16"/>
|
|
138
|
+
<circle cx="${cx}" cy="${cy}" r="${R + 24}" style="fill:var(--bg);stroke:color-mix(in srgb,var(--fg) 12%,var(--bg))" stroke-width="3" filter="url(#${ID}s)"/>
|
|
139
|
+
<circle cx="${cx}" cy="${cy}" r="${R}" style="fill:${dark}"/>
|
|
140
|
+
<path d="${arcPath(cx, cy, Rs, -1.2, 1.2)}" style="fill:none;stroke:${LINE}" stroke-width="4"/>`);
|
|
141
|
+
base += box(cx - R * 0.78, cy - R * 0.7, R * 1.56, R * 1.4, `${icon(c.icon, 56, "var(--bg)")}${title(c.title, 40, "var(--bg)", "font-weight:700;line-height:1.1")}${body(c.text, 21, "var(--bg)")}`, { align: "center", cls: "ig-center" });
|
|
142
|
+
const x0 = 820, cw = W - 8 - x0, gap = 26, ch = Math.min(188, (H - gap * (n - 1)) / n), y0 = (H - (n * ch + (n - 1) * gap)) / 2;
|
|
143
|
+
const layers = items.map((it, i) => {
|
|
144
|
+
const yc = y0 + i * (ch + gap) + ch / 2, col = cols[i];
|
|
145
|
+
const dyc = Math.max(-Rs * 0.9, Math.min(Rs * 0.9, yc - cy)), dx = cx + Math.sqrt(Rs * Rs - dyc * dyc);
|
|
146
|
+
const br = Math.min(ch * 0.3, 48), bxc = x0 + ch / 2 + 4;
|
|
147
|
+
const g = svg(`<path d="M${F(dx)} ${F(cy + dyc)} H 730 V ${F(yc)} H ${x0}" style="fill:none;stroke:${LINE}" stroke-width="3.5"/>
|
|
148
|
+
<circle cx="${F(dx)}" cy="${F(cy + dyc)}" r="13" style="fill:#${col};stroke:var(--bg)" stroke-width="4"/>
|
|
149
|
+
<rect x="${x0}" y="${F(yc - ch / 2)}" width="${F(cw)}" height="${F(ch)}" rx="${F(ch / 2)}" style="fill:var(--bg);stroke:#${col}" stroke-width="6"/>
|
|
150
|
+
<circle cx="${F(bxc)}" cy="${F(yc)}" r="${F(br)}" style="fill:#${col}"/>`);
|
|
151
|
+
const num = box(bxc - br, yc - br, br * 2, br * 2, it.icon ? icon(it.icon, br, `#${onColor(theme, col)}`) : title(String(i + 1), br * 0.9, `#${onColor(theme, col)}`, "font-weight:700"), { align: "center" });
|
|
152
|
+
const tx = bxc + br + 24;
|
|
153
|
+
return layer(g + num + box(tx, yc - ch / 2 + 12, x0 + cw - ch / 3 - tx, ch - 24, `${title(it.title, Math.min(34, ch * 0.22), FG, "font-weight:700")}${body(it.text, Math.min(22, ch * 0.15), "color-mix(in srgb,var(--fg) 72%,var(--bg))")}`), s.build ? i + 1 : 0);
|
|
154
|
+
});
|
|
155
|
+
return base + layers.join("");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function lados(s, items, cols, theme) {
|
|
159
|
+
const n = items.length, cy = H / 2, c = centerOf(s);
|
|
160
|
+
const pw = 290, ph = 150, px0 = W / 2 - pw / 2;
|
|
161
|
+
const nl = Math.ceil(n / 2), nr = n - nl;
|
|
162
|
+
let base = svg(`${shadow()}<rect x="${px0}" y="${cy - ph / 2}" width="${pw}" height="${ph}" rx="${ph / 2}" style="fill:var(--bg);stroke:${LINE}" stroke-width="10" filter="url(#${ID}s)"/>`);
|
|
163
|
+
base += box(px0 + 20, cy - ph / 2 + 12, pw - 40, ph - 24, c.icon ? `${icon(c.icon, 76, FG)}` : title(c.title, 36, FG, "font-weight:800"), { align: "center", cls: "ig-center" });
|
|
164
|
+
const side = (list, off, left) => {
|
|
165
|
+
const k = list.length; if (!k) return "";
|
|
166
|
+
const sp = H / k, r = Math.min(72, sp * 0.36), ccx = left ? 520 : W - 520, spine = left ? 640 : W - 640, edge = left ? px0 : px0 + pw;
|
|
167
|
+
const ys = list.map((_, j) => sp * (j + 0.5));
|
|
168
|
+
let out = svg(`<path d="M${edge} ${cy} H ${spine} M${spine} ${F(Math.min(cy, ys[0]))} V ${F(Math.max(cy, ys[k - 1]))}" style="fill:none;stroke:${LINE}" stroke-width="2.5"/>`);
|
|
169
|
+
list.forEach((it, j) => {
|
|
170
|
+
const i = off + j, y = ys[j], col = cols[i], bw = left ? ccx - r - 44 : W - (ccx + r + 44), bh = Math.min(sp - 14, 180);
|
|
171
|
+
const g = svg(`<path d="M${spine} ${F(y)} H ${F(left ? ccx + r + 10 : ccx - r - 10)}" style="fill:none;stroke:${LINE}" stroke-width="2.5" marker-end="url(#${ID}a)"/>
|
|
172
|
+
<circle cx="${ccx}" cy="${F(y)}" r="${F(r)}" style="fill:var(--bg);stroke:#${col}" stroke-width="${F(r * 0.2)}" filter="url(#${ID}s)"/>`);
|
|
173
|
+
const ic = box(ccx - r, y - r, r * 2, r * 2, it.icon ? icon(it.icon, r * 0.85, FG) : title(String(i + 1), r * 0.8, `#${col}`, "font-weight:800"), { align: "center" });
|
|
174
|
+
const tx = left ? 10 : ccx + r + 34;
|
|
175
|
+
out += layer(g + ic + box(tx, y - bh / 2, bw, bh, `${title(it.title, Math.min(32, sp * 0.18), `#${col}`, "font-weight:700")}${body(it.text, Math.min(22, sp * 0.12), FG)}`, { align: left ? "right" : "left" }), s.build ? i + 1 : 0);
|
|
176
|
+
});
|
|
177
|
+
return out;
|
|
178
|
+
};
|
|
179
|
+
return base + side(items.slice(0, nl), 0, true) + side(items.slice(nl), nl, false);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function trilhas(s, items, cols, theme) {
|
|
183
|
+
const rows = items.map((it) => [it, ...(Array.isArray(it.steps) ? it.steps.map(norm) : [])].slice(0, 4));
|
|
184
|
+
const n = rows.length, m = Math.max(...rows.map((r) => r.length)), c = centerOf(s), cy = H / 2;
|
|
185
|
+
const goalCol = (theme.colors?.alert || "D33A2C"), onGoal = onColor(theme, goalCol);
|
|
186
|
+
const R = Math.min(150, H * 0.22), gx = R + 20, spine = gx + R + 50, x0 = spine + 44;
|
|
187
|
+
const g = 90, bw = (W - x0 - (m - 1) * g) / m, gy = 46, bh = Math.min(290, (H - (n - 1) * gy) / n), y0 = (H - (n * bh + (n - 1) * gy)) / 2;
|
|
188
|
+
const ys = rows.map((_, i) => y0 + i * (bh + gy) + bh / 2);
|
|
189
|
+
const colColors = itemColors(theme, m);
|
|
190
|
+
let base = svg(`${shadow()}<circle cx="${F(gx)}" cy="${cy}" r="${F(R)}" style="fill:#${goalCol}" filter="url(#${ID}s)"/>
|
|
191
|
+
<path d="M${F(gx + R)} ${cy} H ${F(spine)} M${F(spine)} ${F(Math.min(cy, ys[0]))} V ${F(Math.max(cy, ys[n - 1]))}" style="fill:none;stroke:${LINE}" stroke-width="2.5"/>`);
|
|
192
|
+
base += box(gx - R * 0.75, cy - R * 0.7, R * 1.5, R * 1.4, `${icon(c.icon, 64, `#${onGoal}`)}${title(c.title, 40, `#${onGoal}`, "font-weight:800")}${body(c.text, 20, `#${onGoal}`)}`, { align: "center", cls: "ig-center" });
|
|
193
|
+
let step = 0;
|
|
194
|
+
const layers = rows.map((row, i) => {
|
|
195
|
+
const y = ys[i];
|
|
196
|
+
let lines = `<path d="M${F(spine)} ${F(y)} H ${F(x0 - 8)}" style="fill:none;stroke:${LINE}" stroke-width="2.5" marker-end="url(#${ID}a)"/>`;
|
|
197
|
+
let texts = "";
|
|
198
|
+
row.forEach((b, j) => {
|
|
199
|
+
const x = x0 + j * (bw + g), col = colColors[j], on = `#${onColor(theme, col)}`;
|
|
200
|
+
lines += `<rect x="${F(x)}" y="${F(y - bh / 2)}" width="${F(bw)}" height="${F(bh)}" rx="28" style="fill:#${col}" filter="url(#${ID}s)"/>`;
|
|
201
|
+
if (j < row.length - 1) lines += `<path d="M${F(x + bw + 6)} ${F(y)} H ${F(x + bw + g - 8)}" style="fill:none;stroke:${LINE}" stroke-width="2.5" marker-end="url(#${ID}a)"/>`;
|
|
202
|
+
texts += box(x + 18, y - bh / 2 + 14, bw - 36, bh - 28, `${icon(b.icon, Math.min(56, bh * 0.22), on)}${title(b.title, Math.min(30, bh * 0.13), on, "font-weight:700")}${body(b.text, Math.min(21, bh * 0.09), on)}`, { align: "center" });
|
|
203
|
+
});
|
|
204
|
+
return layer(svg(lines) + texts, s.build ? ++step : 0);
|
|
205
|
+
});
|
|
206
|
+
return base + layers.join("");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function metro(s, items, cols, theme) {
|
|
210
|
+
const n = items.length, cy = H / 2, c = centerOf(s);
|
|
211
|
+
const R = Math.min(180, H * 0.27), hx = R + 30, hi = theme.colors?.accent || "0F6CBD", onHi = `#${onColor(theme, hi)}`;
|
|
212
|
+
const x0 = hx + R + 40, sp = H / n, mid = (n - 1) / 2, r = Math.min(64, sp * 0.4), end = W - 26;
|
|
213
|
+
let base = svg(`${shadow()}<circle cx="${F(hx)}" cy="${cy}" r="${F(R + 16)}" style="fill:none;stroke:#${hi}" stroke-width="2" opacity=".6"/>
|
|
214
|
+
<circle cx="${F(hx)}" cy="${cy}" r="${F(R)}" style="fill:#${hi}" filter="url(#${ID}s)"/>`);
|
|
215
|
+
base += box(hx - R * 0.74, cy - R * 0.62, R * 1.48, R * 1.24, `${icon(c.icon, 56, onHi)}${title(c.title, 44, onHi, "font-weight:800")}${body(c.text, 21, onHi)}`, { align: "center", cls: "ig-center" });
|
|
216
|
+
const layers = items.map((it, i) => {
|
|
217
|
+
const col = cols[i], t = mid ? Math.abs(i - mid) / mid : 0, y0 = cy + (i - mid) * 24, y = sp * (i + 0.5);
|
|
218
|
+
const xb = x0 + 60 + 50 * (1 - t), xe = xb + Math.abs(y - y0) * 0.55, cxi = 1160 + 200 * (1 - t);
|
|
219
|
+
const up = y < cy - 1 || (n === 1);
|
|
220
|
+
const g = svg(`<path d="M${F(x0)} ${F(y0)} H ${F(xb)} L ${F(xe)} ${F(y)} H ${F(end)}" style="fill:none;stroke:#${col}" stroke-width="5" stroke-linejoin="round"/>
|
|
221
|
+
<circle cx="${F(x0)}" cy="${F(y0)}" r="9" style="fill:var(--bg);stroke:#${col}" stroke-width="4"/>
|
|
222
|
+
<circle cx="${F(end)}" cy="${F(y)}" r="11" style="fill:var(--bg);stroke:#${col}" stroke-width="4"/>
|
|
223
|
+
${[0, 1, 2].map((k) => `<circle cx="${F(cxi - r - 56 + k * 16)}" cy="${F(y)}" r="5" style="fill:#${col}"/>`).join("")}
|
|
224
|
+
<circle cx="${F(cxi)}" cy="${F(y)}" r="${F(r + 10)}" style="fill:none;stroke:#${col}" stroke-width="2" opacity=".55"/>
|
|
225
|
+
<circle cx="${F(cxi)}" cy="${F(y)}" r="${F(r)}" style="fill:#${col}" filter="url(#${ID}s)"/>`);
|
|
226
|
+
const ic = box(cxi - r, y - r, r * 2, r * 2, it.icon ? icon(it.icon, r * 0.9, `#${onColor(theme, col)}`) : title(String(i + 1), r * 0.8, `#${onColor(theme, col)}`, "font-weight:800"), { align: "center" });
|
|
227
|
+
// legenda dentro da faixa do próprio item (acima da linha na metade de cima, abaixo na de baixo): nunca sai do
|
|
228
|
+
// palco nem invade a faixa do vizinho
|
|
229
|
+
const bx = xe + 24, bw = cxi - r - 70 - bx, bh = Math.min(sp / 2 - 6, 150);
|
|
230
|
+
const cap = box(bx, up ? y - 6 - bh : y + 8, bw, bh, `${title(it.title, Math.min(28, sp * 0.2), `#${col}`, "font-weight:700")}${body(it.text, Math.min(20, sp * 0.13), FG)}`, { align: "right", valign: up ? "bottom" : "top" });
|
|
231
|
+
return layer(g + ic + cap, s.build ? i + 1 : 0);
|
|
232
|
+
});
|
|
233
|
+
return base + layers.join("");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Ciclo: as etapas numa elipse, em sentido horário a partir de cima, cada uma ligada à seguinte por uma seta curva na
|
|
237
|
+
// cor dela (a última volta para a primeira); o nome do ciclo no centro. O texto de cada etapa fica do lado de fora,
|
|
238
|
+
// afastado da seta: acima/abaixo nas etapas do alto e de baixo, ao lado nas outras (subindo ou descendo para longe do
|
|
239
|
+
// arco).
|
|
240
|
+
function ciclo(s, items, cols, theme) {
|
|
241
|
+
const n = items.length, cx = W / 2, cy = H / 2, c = centerOf(s);
|
|
242
|
+
const Rx = 440, Ry = 172, r = 46;
|
|
243
|
+
const ang = items.map((_, i) => -Math.PI / 2 + (i * 2 * Math.PI) / n);
|
|
244
|
+
const pt = (a) => [cx + Rx * Math.cos(a), cy + Ry * Math.sin(a)];
|
|
245
|
+
const gap = (r + 16) / ((Rx + Ry) / 2); // o arco começa e termina fora do círculo da etapa
|
|
246
|
+
const markers = cols.map((col, i) => `<marker id="${ID}c${i}" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="4.5" markerHeight="4.5" orient="auto"><path d="M0 0L10 5L0 10z" style="fill:#${col}"/></marker>`).join("");
|
|
247
|
+
let base = svg(`${shadow()}<defs>${markers}</defs><ellipse cx="${cx}" cy="${cy}" rx="${Rx}" ry="${Ry}" style="fill:none;stroke:${LINE}" stroke-width="1.5" stroke-dasharray="3 9" opacity=".6"/>
|
|
248
|
+
<ellipse cx="${cx}" cy="${cy}" rx="${Rx * 0.42}" ry="${Ry * 0.62}" style="fill:var(--bg);stroke:color-mix(in srgb,var(--fg) 12%,var(--bg))" stroke-width="3" filter="url(#${ID}s)"/>`);
|
|
249
|
+
base += box(cx - Rx * 0.36, cy - Ry * 0.52, Rx * 0.72, Ry * 1.04, `${icon(c.icon, 52, FG)}${title(c.title, 36, FG, "font-weight:800;line-height:1.1")}${body(c.text, 19, FG)}`, { align: "center", cls: "ig-center" });
|
|
250
|
+
const layers = items.map((it, i) => {
|
|
251
|
+
const a = ang[i], [x, y] = pt(a), col = cols[i], on = `#${onColor(theme, col)}`;
|
|
252
|
+
let arrow = "";
|
|
253
|
+
if (n > 1) {
|
|
254
|
+
const [x1, y1] = pt(a + gap), [x2, y2] = pt(a + (2 * Math.PI) / n - gap);
|
|
255
|
+
arrow = `<path d="M${F(x1)} ${F(y1)} A${Rx} ${Ry} 0 0 1 ${F(x2)} ${F(y2)}" style="fill:none;stroke:#${col}" stroke-width="7" stroke-linecap="round" marker-end="url(#${ID}c${i})"/>`;
|
|
256
|
+
}
|
|
257
|
+
const g = svg(`${arrow}<circle cx="${F(x)}" cy="${F(y)}" r="${r + 9}" style="fill:var(--bg)" filter="url(#${ID}s)"/><circle cx="${F(x)}" cy="${F(y)}" r="${r}" style="fill:#${col}"/>`);
|
|
258
|
+
const ic = box(x - r, y - r, r * 2, r * 2, it.icon ? icon(it.icon, r * 0.95, on) : title(String(i + 1), r * 0.8, on, "font-weight:800"), { align: "center" });
|
|
259
|
+
const ux = Math.cos(a), uy = Math.sin(a), inner = `${title(it.title, 27, `#${col}`, "font-weight:700")}${body(it.text, 19, FG)}`;
|
|
260
|
+
let cap;
|
|
261
|
+
if (Math.abs(uy) > 0.9) { // no alto ou embaixo: acima/abaixo da etapa, centrado
|
|
262
|
+
const bw = 420, bh = Math.min(116, uy < 0 ? y - r - 18 : H - 4 - (y + r + 12));
|
|
263
|
+
cap = box(x - bw / 2, uy < 0 ? y - r - 14 - bh : y + r + 12, bw, bh, inner, { align: "center", valign: uy < 0 ? "bottom" : "top" });
|
|
264
|
+
} else { // ao lado, para fora, subindo (metade de cima) ou descendo (de baixo) para longe do arco
|
|
265
|
+
const bw = ux > 0 ? Math.min(380, W - 6 - (x + r + 18)) : Math.min(380, x - r - 18 - 6), bh = 130;
|
|
266
|
+
const bx = ux > 0 ? x + r + 18 : x - r - 18 - bw, by = uy < -0.15 ? y - bh + r * 0.6 : uy > 0.15 ? y - r * 0.6 : y - bh / 2;
|
|
267
|
+
cap = box(bx, Math.max(4, Math.min(H - 4 - bh, by)), bw, bh, inner, { align: ux > 0 ? "left" : "right", valign: uy < -0.15 ? "bottom" : uy > 0.15 ? "top" : "center" });
|
|
268
|
+
}
|
|
269
|
+
return layer(g + ic + cap, s.build ? i + 1 : 0);
|
|
270
|
+
});
|
|
271
|
+
return base + layers.join("");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Pista: um circuito de corrida visto de cima (estilo Mario Kart), com grama, zebra, asfalto, faixa do meio, largada
|
|
275
|
+
// quadriculada e caixas de item; os itens são os marcos da volta, a distâncias iguais a partir da largada, cada um com
|
|
276
|
+
// a sua placa (o cartão vai para fora da pista, ou para dentro quando a borda do palco não deixa); o centro é a placa
|
|
277
|
+
// do meio do circuito. As cores da cena (grama, asfalto, zebra) são as de uma pista; os marcos, as do tema.
|
|
278
|
+
// achatada no meio do palco: em cima e embaixo sobra uma faixa para as placas
|
|
279
|
+
const TRACK = [[190, 470], [200, 210], [400, 105], [610, 235], [820, 120], [1110, 105], [1430, 150], [1535, 360], [1390, 575], [1010, 600], [700, 520], [430, 610]]
|
|
280
|
+
.map(([x, y]) => [Math.round(140 + (x - 190) * 1.04), Math.round(350 + (y - 357) * 0.5)]);
|
|
281
|
+
function trackModel() {
|
|
282
|
+
const n = TRACK.length, P = (i) => TRACK[(i + n) % n];
|
|
283
|
+
const segs = [];
|
|
284
|
+
for (let i = 0; i < n; i++) {
|
|
285
|
+
const p0 = P(i - 1), p1 = P(i), p2 = P(i + 1), p3 = P(i + 2);
|
|
286
|
+
segs.push([p1, [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6], [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6], p2]);
|
|
287
|
+
}
|
|
288
|
+
const d = `M${F(segs[0][0][0])} ${F(segs[0][0][1])}` + segs.map(([, a, b, c]) => ` C${F(a[0])} ${F(a[1])} ${F(b[0])} ${F(b[1])} ${F(c[0])} ${F(c[1])}`).join("") + " Z";
|
|
289
|
+
const pts = [];
|
|
290
|
+
for (const [a, b, c, e] of segs) for (let k = 0; k < 40; k++) {
|
|
291
|
+
const t = k / 40, u = 1 - t;
|
|
292
|
+
pts.push([u * u * u * a[0] + 3 * u * u * t * b[0] + 3 * u * t * t * c[0] + t * t * t * e[0], u * u * u * a[1] + 3 * u * u * t * b[1] + 3 * u * t * t * c[1] + t * t * t * e[1]]);
|
|
293
|
+
}
|
|
294
|
+
const len = [0];
|
|
295
|
+
for (let i = 1; i <= pts.length; i++) len.push(len[i - 1] + Math.hypot(pts[i % pts.length][0] - pts[i - 1][0], pts[i % pts.length][1] - pts[i - 1][1]));
|
|
296
|
+
const total = len[len.length - 1];
|
|
297
|
+
// ponto e direção a uma fração da volta
|
|
298
|
+
const at = (f) => {
|
|
299
|
+
const L = ((f % 1) + 1) % 1 * total;
|
|
300
|
+
let i = len.findIndex((v) => v > L) - 1; if (i < 0) i = 0;
|
|
301
|
+
const p = pts[i % pts.length], q = pts[(i + 1) % pts.length], dx = q[0] - p[0], dy = q[1] - p[1], m = Math.hypot(dx, dy) || 1;
|
|
302
|
+
return { x: p[0], y: p[1], tx: dx / m, ty: dy / m };
|
|
303
|
+
};
|
|
304
|
+
const cx = TRACK.reduce((a, p) => a + p[0], 0) / n, cy = TRACK.reduce((a, p) => a + p[1], 0) / n;
|
|
305
|
+
return { d, at, cx, cy };
|
|
306
|
+
}
|
|
307
|
+
function pista(s, items, cols, theme) {
|
|
308
|
+
const n = items.length, c = centerOf(s), T = trackModel(), RW = 86;
|
|
309
|
+
const grass = "#5DAE4F", grass2 = "#4E9A43", asphalt = "#5B6068";
|
|
310
|
+
// árvores e arbustos fixos (a mesma pista em toda exportação)
|
|
311
|
+
const trees = [[90, 90], [130, 640], [700, 60], [980, 260], [1250, 300], [1600, 90], [1610, 640], [560, 400], [1180, 420], [300, 330], [850, 660], [1560, 520]];
|
|
312
|
+
const start = T.at(0), ang = (Math.atan2(start.ty, start.tx) * 180) / Math.PI;
|
|
313
|
+
const boxes = [0.18, 0.47, 0.79].map((f) => T.at(f));
|
|
314
|
+
let base = svg(`${shadow()}<defs><pattern id="${ID}k" width="20" height="20" patternUnits="userSpaceOnUse"><rect width="20" height="20" fill="#fff"/><rect width="10" height="10" fill="#111"/><rect x="10" y="10" width="10" height="10" fill="#111"/></pattern>
|
|
315
|
+
<linearGradient id="${ID}q" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#FFD54A"/><stop offset=".5" stop-color="#FF6FB5"/><stop offset="1" stop-color="#5BC0FF"/></linearGradient></defs>
|
|
316
|
+
<rect x="0" y="0" width="${W}" height="${H}" rx="36" fill="${grass}"/>
|
|
317
|
+
${trees.map(([x, y], i) => `<circle cx="${x}" cy="${y}" r="${18 + (i % 3) * 6}" fill="${grass2}"/><circle cx="${x - 6}" cy="${y - 6}" r="${8 + (i % 3) * 3}" fill="#6BC25C"/>`).join("")}
|
|
318
|
+
<path d="${T.d}" fill="none" stroke="#fff" stroke-width="${RW + 22}" stroke-linejoin="round"/>
|
|
319
|
+
<path d="${T.d}" fill="none" stroke="#E53935" stroke-width="${RW + 22}" stroke-dasharray="26 26" stroke-linejoin="round"/>
|
|
320
|
+
<path d="${T.d}" fill="none" stroke="${asphalt}" stroke-width="${RW}" stroke-linejoin="round"/>
|
|
321
|
+
<path d="${T.d}" fill="none" stroke="#fff" stroke-width="4" stroke-dasharray="26 30" opacity=".85"/>
|
|
322
|
+
<rect x="${F(start.x - 12)}" y="${F(start.y - RW / 2)}" width="24" height="${RW}" fill="url(#${ID}k)" transform="rotate(${F(ang)} ${F(start.x)} ${F(start.y)})"/>
|
|
323
|
+
${boxes.map((b, i) => `<rect x="${F(b.x - 15)}" y="${F(b.y - 15)}" width="30" height="30" rx="6" fill="url(#${ID}q)" stroke="#fff" stroke-width="3" opacity=".95" transform="rotate(${20 + i * 25} ${F(b.x)} ${F(b.y)})"/>`).join("")}
|
|
324
|
+
${c.title || c.text ? `<rect x="${F(T.cx - 170)}" y="${F(T.cy - 36)}" width="340" height="72" rx="16" style="fill:var(--bg);stroke:color-mix(in srgb,var(--fg) 20%,var(--bg))" stroke-width="3" filter="url(#${ID}s)"/>` : ""}`);
|
|
325
|
+
if (c.title || c.text) base += box(T.cx - 158, T.cy - 31, 316, 62, `${title(c.title, 26, FG, "font-weight:800;line-height:1.05")}${body(c.text, 15, FG)}`, { align: "center", cls: "ig-center" });
|
|
326
|
+
// cada marco vai para a faixa de cima ou de baixo (a mais perto); na faixa, as placas em fila, na ordem de x, sem
|
|
327
|
+
// encavalar; um tracejado liga o marco à placa
|
|
328
|
+
const marks = items.map((it, i) => ({ it, i, p: T.at((i + 0.5) / n) }));
|
|
329
|
+
const bands = [marks.filter((m) => m.p.y < T.cy), marks.filter((m) => m.p.y >= T.cy)];
|
|
330
|
+
const bh = 104, layers = [];
|
|
331
|
+
bands.forEach((band, up) => {
|
|
332
|
+
const k = band.length; if (!k) return;
|
|
333
|
+
band.sort((a, b) => a.p.x - b.p.x);
|
|
334
|
+
const gap = 14, bw = Math.min(330, (W - 12 - gap * (k - 1)) / k);
|
|
335
|
+
// a placa fica embaixo do x do marco quando dá; senão, empurra para os lados
|
|
336
|
+
let xs = band.map((m) => Math.max(6, Math.min(W - 6 - bw, m.p.x - bw / 2)));
|
|
337
|
+
for (let r = 0; r < 4; r++) for (let j = 1; j < k; j++) if (xs[j] < xs[j - 1] + bw + gap) xs[j] = xs[j - 1] + bw + gap;
|
|
338
|
+
const over = xs[k - 1] + bw - (W - 6); if (over > 0) xs = xs.map((x) => x - over);
|
|
339
|
+
for (let j = k - 2; j >= 0; j--) if (xs[j] + bw + gap > xs[j + 1]) xs[j] = xs[j + 1] - bw - gap;
|
|
340
|
+
const by = up ? H - 6 - bh : 6;
|
|
341
|
+
band.forEach((m, j) => {
|
|
342
|
+
const { it, i, p } = m, col = cols[i], on = `#${onColor(theme, col)}`, bx = xs[j];
|
|
343
|
+
const g = svg(`<path d="M${F(p.x)} ${F(p.y)} L${F(bx + bw / 2)} ${F(up ? by : by + bh)}" stroke="#fff" stroke-width="3" stroke-dasharray="2 7" stroke-linecap="round" fill="none"/>
|
|
344
|
+
<rect x="${F(bx)}" y="${F(by)}" width="${F(bw)}" height="${bh}" rx="18" style="fill:var(--bg)" filter="url(#${ID}s)"/><rect x="${F(bx)}" y="${F(by)}" width="10" height="${bh}" rx="5" style="fill:#${col}"/>
|
|
345
|
+
<circle cx="${F(p.x)}" cy="${F(p.y)}" r="31" fill="#fff" filter="url(#${ID}s)"/><circle cx="${F(p.x)}" cy="${F(p.y)}" r="25" style="fill:#${col}"/>`);
|
|
346
|
+
const num = box(p.x - 25, p.y - 25, 50, 50, it.icon ? icon(it.icon, 26, on) : title(String(i + 1), 24, on, "font-weight:800"), { align: "center" });
|
|
347
|
+
layers[i] = layer(g + num + box(bx + 22, by + 8, bw - 32, bh - 16, `${title(it.title, 24, `#${col}`, "font-weight:800")}${body(it.text, 17, FG)}`), s.build ? i + 1 : 0);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
return base + layers.join("");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Camadas: um perfil físico de cima para baixo como na realidade (atmosfera, superfície, solo, lençol, rocha; ou as
|
|
354
|
+
// camadas de um sistema), cada item uma faixa com o nome, o texto e as etiquetas dela; flows: [{from, to, title}]
|
|
355
|
+
// (números das camadas, de cima para baixo) viram setas verticais com o nome do fluxo — descendo (infiltração) ou
|
|
356
|
+
// subindo (evaporação). Num fluxograma, o Mermaid ordena pelas setas e a atmosfera ia parar embaixo.
|
|
357
|
+
function camadas(s, items, cols, theme) {
|
|
358
|
+
const n = items.length, gap = 12, bh = (H - gap * (n - 1)) / n, lw = 380, x0 = lw + 30, xr = W - 8;
|
|
359
|
+
const flows = (Array.isArray(s.flows) ? s.flows : []).map((f) => ({ from: Number(f.from ?? f.de) - 1, to: Number(f.to ?? f.para) - 1, title: String(f.title ?? f.label ?? "") })).filter((f) => f.from >= 0 && f.to >= 0 && f.from < n && f.to < n && f.from !== f.to);
|
|
360
|
+
// as setas na metade direita (as etiquetas ficam na esquerda, sem cruzar)
|
|
361
|
+
const xa = flows.length ? x0 + (xr - x0) * 0.5 : xr, fx = (k) => xa + 30 + ((xr - xa - 60) * (k + 0.5)) / Math.max(1, flows.length);
|
|
362
|
+
const top = (i) => i * (bh + gap);
|
|
363
|
+
const markers = cols.map((col, i) => `<marker id="${ID}l${i}" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto"><path d="M0 0L10 5L0 10z" style="fill:#${col}"/></marker>`).join("");
|
|
364
|
+
let base = svg(`${shadow()}<defs>${markers}</defs>${items.map((it, i) => `<rect x="0" y="${F(top(i))}" width="${W}" height="${F(bh)}" rx="18" style="fill:color-mix(in srgb,#${cols[i]} 16%,var(--bg));stroke:color-mix(in srgb,#${cols[i]} 55%,var(--bg))" stroke-width="2"/><rect x="0" y="${F(top(i))}" width="10" height="${F(bh)}" rx="5" style="fill:#${cols[i]}"/>`).join("")}`);
|
|
365
|
+
items.forEach((it, i) => {
|
|
366
|
+
base += box(28, top(i) + 8, lw - 30, bh - 16, `${icon(it.icon, Math.min(40, bh * 0.3), `#${cols[i]}`)}${title(it.title, Math.min(30, bh * 0.26), `#${cols[i]}`, "font-weight:800")}${body(it.text, Math.min(18, bh * 0.15), FG)}`, { align: "left" });
|
|
367
|
+
const chips = (Array.isArray(it.items) ? it.items : []).map(norm).filter((c) => c.title);
|
|
368
|
+
if (chips.length) base += `<div class="ig-box ig-chips" data-fit data-fit-self style="font-size:20px;left:${F(x0)}px;top:${F(top(i) + 10)}px;width:${F(xa - x0 - 20)}px;height:${F(bh - 20)}px;display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;align-content:center;gap:8px 10px;justify-content:flex-start">${chips.map((c) => `<span class="t f-body" style="font-size:${em(Math.min(19, bh * 0.16))};padding:.25em .7em;border-radius:999px;background:color-mix(in srgb,#${cols[i]} 22%,var(--bg));color:${FG};border:1px solid color-mix(in srgb,#${cols[i]} 50%,var(--bg))">${md(c.title)}</span>`).join("")}</div>`;
|
|
369
|
+
});
|
|
370
|
+
// setas por cima das faixas, cada uma numa coluna; o nome ao lado, no meio do caminho
|
|
371
|
+
const layers = flows.map((f, k) => {
|
|
372
|
+
const x = fx(k), down = f.to > f.from, col = cols[f.from];
|
|
373
|
+
// de dentro da camada de origem até dentro da de destino (só no vão entre as faixas, a seta mal aparecia)
|
|
374
|
+
const y1 = down ? top(f.from) + bh * 0.35 : top(f.from) + bh * 0.65, y2 = down ? top(f.to) + bh * 0.65 : top(f.to) + bh * 0.35;
|
|
375
|
+
// duas setas entre as mesmas camadas (percolação desce, fluxo ascendente sobe): os nomes em alturas diferentes
|
|
376
|
+
const same = flows.slice(0, k).filter((o) => Math.min(o.from, o.to) === Math.min(f.from, f.to) && Math.max(o.from, o.to) === Math.max(f.from, f.to)).length;
|
|
377
|
+
const ym = (y1 + y2) / 2 + (same % 2 ? 34 : 0) - (flows.some((o, j) => j > k && Math.min(o.from, o.to) === Math.min(f.from, f.to) && Math.max(o.from, o.to) === Math.max(f.from, f.to)) && !same ? 34 : 0);
|
|
378
|
+
const g = svg(`<path d="M${F(x)} ${F(y1)} L${F(x)} ${F(y2)}" style="fill:none;stroke:#${col}" stroke-width="6" stroke-linecap="round" marker-end="url(#${ID}l${f.from})"/>`); // sem sombra: filtro numa linha reta (caixa de largura zero) apaga a linha
|
|
379
|
+
// o nome à direita da seta, na largura até a próxima (vizinhos encostados contam como texto sobre texto e o slide
|
|
380
|
+
// inteiro encolhia); perto da borda, à esquerda
|
|
381
|
+
const lw2 = Math.max(90, Math.min(200, (xr - xa - 60) / Math.max(1, flows.length) - 22)), right = x + 12 + lw2 <= W - 4;
|
|
382
|
+
return layer(g + box(right ? x + 12 : x - 12 - lw2, ym - 30, lw2, 60, `<span class="t f-label" style="font-size:${em(15)};padding:.2em .55em;border-radius:6px;background:var(--bg);color:#${col};font-weight:700;white-space:normal;display:inline-block;max-width:100%">${md(f.title)}</span>`, { align: right ? "left" : "right" }), s.build ? k + 1 : 0);
|
|
383
|
+
});
|
|
384
|
+
return base + layers.join("");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const SHAPES = { arco, ramos, lados, trilhas, metro, ciclo, pista, camadas };
|
|
388
|
+
|
|
389
|
+
export function infographicHTML(s, ctx) {
|
|
390
|
+
const shape = SHAPES[s.shape] ? s.shape : "arco";
|
|
391
|
+
if (s.shape && !SHAPES[s.shape]) ctx?.warnings?.push(`infográfico: forma "${s.shape}" não existe (use ${Object.keys(SHAPES).join(", ")}); saiu como arco`);
|
|
392
|
+
let items = (Array.isArray(s.items) ? s.items : []).map(norm);
|
|
393
|
+
const max = INFOGRAPHIC_SHAPES[shape].max;
|
|
394
|
+
if (items.length > max) { ctx?.warnings?.push(`infográfico ${shape}: ${items.length} itens, cabem até ${max}; os demais ficaram de fora (divida em dois slides)`); items = items.slice(0, max); }
|
|
395
|
+
if (!items.length) items = [{ title: "Item" }];
|
|
396
|
+
const theme = ctx?.theme || { colors: {} };
|
|
397
|
+
ID = "ig" + hash(JSON.stringify([s.shape, s.center, s.items, s.title]));
|
|
398
|
+
const cols = itemColors(theme, items.length);
|
|
399
|
+
return `<div class="ig-stage ig-${shape} e" style="--d:2;">${SHAPES[shape](s, items, cols, theme)}</div>`;
|
|
400
|
+
}
|
|
401
|
+
|