@pixerate/flair 0.32.0 → 0.34.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/CHANGELOG.md +15 -0
- package/README.md +3 -3
- package/dist/catalog/index.d.ts +4 -2
- package/dist/catalog/index.js +6 -2
- package/dist/{chunk-VLJPCY54.js → chunk-2O3RMSOE.js} +3 -3
- package/dist/{chunk-RS2S2K5C.js → chunk-IHUNCBMF.js} +1083 -33
- package/dist/chunk-IHUNCBMF.js.map +1 -0
- package/dist/{chunk-DA4UUSDO.js → chunk-JETRHCP3.js} +9 -277
- package/dist/chunk-JETRHCP3.js.map +1 -0
- package/dist/{chunk-AUH3PEFW.js → chunk-MOLJPWME.js} +5 -3
- package/dist/{chunk-AUH3PEFW.js.map → chunk-MOLJPWME.js.map} +1 -1
- package/dist/{chunk-X3C2VFTM.js → chunk-N5HXMQOK.js} +7 -7
- package/dist/{chunk-3NPBCKKO.js → chunk-UU4PHRVF.js} +6 -6
- package/dist/{chunk-ORAFCHX3.js → chunk-VKIDRHBG.js} +278 -2
- package/dist/chunk-VKIDRHBG.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/editor/index.d.ts +1 -1
- package/dist/editor/index.js +13 -5
- package/dist/editor/index.js.map +1 -1
- package/dist/{effect-GGT2BUW2.js → effect-6VQS2CA5.js} +8 -8
- package/dist/embed/index.d.ts +1 -1
- package/dist/embed/index.js +4 -4
- package/dist/index.js +19 -19
- package/dist/node.d.ts +1 -1
- package/dist/node.js +7 -7
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +5 -5
- package/dist/svelte/index.d.ts +1 -1
- package/dist/svelte/index.js +5 -5
- package/dist/{types-c8cnPzuE.d.ts → types-B4Wjzde0.d.ts} +10 -1
- package/dist/vector/index.js +14 -14
- package/package.json +1 -1
- package/dist/chunk-DA4UUSDO.js.map +0 -1
- package/dist/chunk-ORAFCHX3.js.map +0 -1
- package/dist/chunk-RS2S2K5C.js.map +0 -1
- /package/dist/{chunk-VLJPCY54.js.map → chunk-2O3RMSOE.js.map} +0 -0
- /package/dist/{chunk-X3C2VFTM.js.map → chunk-N5HXMQOK.js.map} +0 -0
- /package/dist/{chunk-3NPBCKKO.js.map → chunk-UU4PHRVF.js.map} +0 -0
- /package/dist/{effect-GGT2BUW2.js.map → effect-6VQS2CA5.js.map} +0 -0
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
rgbToHex
|
|
11
11
|
} from "./chunk-VYOMXE6I.js";
|
|
12
12
|
import {
|
|
13
|
+
formatCount,
|
|
14
|
+
lerpAttr,
|
|
13
15
|
morphPaths,
|
|
14
16
|
morphSequence
|
|
15
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-VKIDRHBG.js";
|
|
16
18
|
|
|
17
19
|
// src/catalog/inputs.ts
|
|
18
20
|
var EFFECT_INPUTS = ["image", "video", "svg", "text", "mask", "path", "audio"];
|
|
@@ -1545,10 +1547,10 @@ var numberTicker = {
|
|
|
1545
1547
|
const chars = Array.from(String(v.value)).slice(0, TICKER_SLOTS);
|
|
1546
1548
|
const widths = chars.map((c) => (/[0-9]/.test(c) ? 0.62 : Math.max(0.3, textWidthEm(c))) * TICKER_SIZE);
|
|
1547
1549
|
const total = widths.reduce((a, b) => a + b, 0);
|
|
1548
|
-
const
|
|
1550
|
+
const fit8 = Math.min(1, 296 / Math.max(1, total));
|
|
1549
1551
|
const duration = Math.round(Math.min(5, Math.max(0.2, Number(v.duration) || 1.6)) * 1e3);
|
|
1550
1552
|
const stagger = Math.round(Math.min(0.5, Math.max(0, Number(v.stagger) || 0)) * 1e3);
|
|
1551
|
-
const out = { fit: +
|
|
1553
|
+
const out = { fit: +fit8.toFixed(4), ms: duration };
|
|
1552
1554
|
let x = 160 - total / 2;
|
|
1553
1555
|
let digit = 0;
|
|
1554
1556
|
for (let i = 0; i < TICKER_SLOTS; i++) {
|
|
@@ -1679,6 +1681,19 @@ var EFFECT_CATEGORIES = [
|
|
|
1679
1681
|
"data",
|
|
1680
1682
|
"icon"
|
|
1681
1683
|
];
|
|
1684
|
+
var EFFECT_CATEGORY_GROUPS = [
|
|
1685
|
+
{ name: "Backgrounds", categories: ["gradient", "organic", "pattern", "space", "3d", "weather", "water", "atmosphere"] },
|
|
1686
|
+
{ name: "Light", categories: ["light", "lighting", "glow", "shadow", "electric"] },
|
|
1687
|
+
{ name: "Pictures", categories: ["image", "distortion", "retro", "comic", "timewarp", "melt"] },
|
|
1688
|
+
{ name: "Particles & FX", categories: ["overlay", "splatter", "bubble", "particles", "trail", "debris", "impact", "pyrotechnics"] },
|
|
1689
|
+
{ name: "Type & titles", categories: ["text", "morph", "broadcast", "annotation", "sticker"] },
|
|
1690
|
+
{ name: "Inputs", categories: ["interaction", "audio", "scroll"] },
|
|
1691
|
+
{ name: "Interface", categories: ["ui", "loader", "icon", "data"] },
|
|
1692
|
+
{ name: "Transitions", categories: ["transition"] }
|
|
1693
|
+
];
|
|
1694
|
+
function categoryGroup(category) {
|
|
1695
|
+
return EFFECT_CATEGORY_GROUPS.find((g) => g.categories.includes(category));
|
|
1696
|
+
}
|
|
1682
1697
|
var VIDEO_USES = ["filter", "transition", "interactive", "stills", "none"];
|
|
1683
1698
|
var VECTOR_CONTROL_TYPES = ["float", "int", "bool", "color", "text"];
|
|
1684
1699
|
var PLACEHOLDER = /\{\{\s*([A-Za-z_]\w*)\s*(?::\s*([A-Za-z0-9]*)([^}]*))?\}\}/g;
|
|
@@ -5954,8 +5969,8 @@ function packGlyph(rows) {
|
|
|
5954
5969
|
return `ivec2(${a}, ${b})`;
|
|
5955
5970
|
}
|
|
5956
5971
|
var vec3Hex = (hex) => {
|
|
5957
|
-
const
|
|
5958
|
-
const c = [
|
|
5972
|
+
const n17 = parseInt(hex.slice(1), 16);
|
|
5973
|
+
const c = [n17 >> 16, n17 >> 8 & 255, n17 & 255].map((v) => +(v / 255).toFixed(3));
|
|
5959
5974
|
return `vec3(${c.map((v) => Number.isInteger(v) ? `${v}.0` : String(v)).join(", ")})`;
|
|
5960
5975
|
};
|
|
5961
5976
|
var paletteArray = (name, hexes) => `const vec3 ${name}[${hexes.length}] = vec3[${hexes.length}](
|
|
@@ -7013,7 +7028,7 @@ var chompLoader = {
|
|
|
7013
7028
|
var pixelEffects = [pixelText, coinSpin, spaceInvader, pixelHeartHealth, chompLoader];
|
|
7014
7029
|
|
|
7015
7030
|
// src/catalog/pyro.ts
|
|
7016
|
-
var round = (
|
|
7031
|
+
var round = (n17) => Math.round(n17 * 10) / 10;
|
|
7017
7032
|
var fireworkBurst = {
|
|
7018
7033
|
kind: "vector",
|
|
7019
7034
|
id: "firework-burst",
|
|
@@ -7068,7 +7083,7 @@ var fireworkBurst = {
|
|
|
7068
7083
|
var pyroEffects = [fireworkBurst];
|
|
7069
7084
|
|
|
7070
7085
|
// src/catalog/timewarp.ts
|
|
7071
|
-
var r111 = (
|
|
7086
|
+
var r111 = (n17) => Math.round(n17 * 10) / 10;
|
|
7072
7087
|
var at2 = (degrees, radius) => {
|
|
7073
7088
|
const a = degrees * Math.PI / 180;
|
|
7074
7089
|
return [r111(100 + radius * Math.sin(a)), r111(100 - radius * Math.cos(a))];
|
|
@@ -7202,16 +7217,16 @@ function around(c, r, from, to, avoid) {
|
|
|
7202
7217
|
if (((avoid - from) % 360 + 360) % 360 < delta) delta -= 360;
|
|
7203
7218
|
return arc2(c[0], c[1], r, r, from, from + delta);
|
|
7204
7219
|
}
|
|
7205
|
-
function crescent(c1,
|
|
7220
|
+
function crescent(c1, r116, c2, r2_) {
|
|
7206
7221
|
const [dx, dy] = [c2[0] - c1[0], c2[1] - c1[1]];
|
|
7207
7222
|
const d = Math.hypot(dx, dy);
|
|
7208
|
-
const a = (
|
|
7209
|
-
const h = Math.sqrt(Math.max(0,
|
|
7223
|
+
const a = (r116 * r116 - r2_ * r2_ + d * d) / (2 * d);
|
|
7224
|
+
const h = Math.sqrt(Math.max(0, r116 * r116 - a * a));
|
|
7210
7225
|
const [px2, py2] = [c1[0] + a * dx / d, c1[1] + a * dy / d];
|
|
7211
7226
|
const i1 = [px2 - h * dy / d, py2 + h * dx / d];
|
|
7212
7227
|
const i2 = [px2 + h * dy / d, py2 - h * dx / d];
|
|
7213
7228
|
const angle = (c, p) => Math.atan2(p[1] - c[1], p[0] - c[0]) / DEG2;
|
|
7214
|
-
const outer = around(c1,
|
|
7229
|
+
const outer = around(c1, r116, angle(c1, i1), angle(c1, i2), angle(c1, c2));
|
|
7215
7230
|
const inner = around(c2, r2_, angle(c2, i2), angle(c2, i1), angle(c2, c1) + 180);
|
|
7216
7231
|
return polyline3([...outer, ...inner.slice(1, -1)], true);
|
|
7217
7232
|
}
|
|
@@ -19738,26 +19753,26 @@ float coverage(vec2 p, float radius) {
|
|
|
19738
19753
|
}
|
|
19739
19754
|
return sum / 16.0;
|
|
19740
19755
|
}`.trim();
|
|
19741
|
-
function goldenTaps(
|
|
19742
|
-
const taps = Array.from({ length:
|
|
19743
|
-
const r = Math.sqrt((i + 0.5) /
|
|
19756
|
+
function goldenTaps(n17) {
|
|
19757
|
+
const taps = Array.from({ length: n17 }, (_, i) => {
|
|
19758
|
+
const r = Math.sqrt((i + 0.5) / n17);
|
|
19744
19759
|
const a = i * 2.39996;
|
|
19745
19760
|
return `vec2(${(r * Math.cos(a)).toFixed(5)}, ${(r * Math.sin(a)).toFixed(5)})`;
|
|
19746
19761
|
});
|
|
19747
|
-
return `const vec2 GOLDEN${
|
|
19762
|
+
return `const vec2 GOLDEN${n17}[${n17}] = vec2[${n17}](${taps.join(", ")});`;
|
|
19748
19763
|
}
|
|
19749
|
-
function coverageTaps(name,
|
|
19764
|
+
function coverageTaps(name, n17, blur) {
|
|
19750
19765
|
return `
|
|
19751
|
-
${goldenTaps(
|
|
19766
|
+
${goldenTaps(n17)}
|
|
19752
19767
|
float ${name}(vec2 p, float radius) {
|
|
19753
19768
|
float sum = 0.0;
|
|
19754
19769
|
if (gMaskPlace.w < 0.5) {
|
|
19755
|
-
for (int i = 0; i < ${
|
|
19770
|
+
for (int i = 0; i < ${n17}; i++) sum += shapeAt(p + radius * GOLDEN${n17}[i], radius * ${blur.toFixed(2)});
|
|
19756
19771
|
} else {
|
|
19757
19772
|
float lod = log2(max(radius * ${blur.toFixed(2)} * gMaskPlace.z, 1.0));
|
|
19758
|
-
for (int i = 0; i < ${
|
|
19773
|
+
for (int i = 0; i < ${n17}; i++) sum += shapeLod(p + radius * GOLDEN${n17}[i], lod);
|
|
19759
19774
|
}
|
|
19760
|
-
return sum / ${
|
|
19775
|
+
return sum / ${n17}.0;
|
|
19761
19776
|
}`.trim();
|
|
19762
19777
|
}
|
|
19763
19778
|
var LITE_TAPS = 10;
|
|
@@ -24982,7 +24997,7 @@ void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
|
24982
24997
|
var waterShaders = [oceanWaves, underwaterScene, pondRipples, waterfall];
|
|
24983
24998
|
|
|
24984
24999
|
// src/catalog/text.ts
|
|
24985
|
-
var r112 = (
|
|
25000
|
+
var r112 = (n17) => Math.round(n17 * 10) / 10;
|
|
24986
25001
|
function fit5(text, width, max, ems = textWidthEm(text)) {
|
|
24987
25002
|
const e = Math.max(0.6, ems);
|
|
24988
25003
|
const size = Math.min(max, width / e);
|
|
@@ -25446,7 +25461,7 @@ var glitchText = {
|
|
|
25446
25461
|
};
|
|
25447
25462
|
},
|
|
25448
25463
|
motion: (() => {
|
|
25449
|
-
const word = (
|
|
25464
|
+
const word = (paint2) => `<text x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" ${paint2}>{{text}}</text>`;
|
|
25450
25465
|
const jumps = (id, value) => GLITCH.map((key, k) => ({ id, attrs: { transform: [value(k), value(k)] }, delay: key.at, ms: 0 }));
|
|
25451
25466
|
return {
|
|
25452
25467
|
svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='GLITCH' label='Text'}} {{background: color default=#07070B label='Background'}} {{ink: color default=#F5F5F5 label='Text colour'}} {{left: color default=#FF0040 label='Red copy'}} {{right: color default=#00F0FF label='Cyan copy'}} {{amount: float min=0 max=3 default=1 step=0.1 label='Intensity'}} ${typeface(HEAVY2, 900)} --><defs>${[0, 1, 2, 3].map((b) => `<clipPath id="slice${b}"><rect x="-5000" y="{{cut${b}}}" width="10400" height="{{band${b}}}"/></clipPath>`).join("")}</defs><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/><g id="red" style="mix-blend-mode:screen">${word(`fill="{{left}}"`)}</g><g id="cyan" style="mix-blend-mode:screen">${word(`fill="{{right}}"`)}</g>${[0, 1, 2, 3].map((b) => `<g clip-path="url(#slice${b})"><g id="s${b}">${word(`fill="{{ink}}"`)}</g></g>`).join("")}</svg>`,
|
|
@@ -25500,7 +25515,7 @@ var extrudedText = {
|
|
|
25500
25515
|
};
|
|
25501
25516
|
},
|
|
25502
25517
|
motion: (() => {
|
|
25503
|
-
const word = (
|
|
25518
|
+
const word = (paint2) => `x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="{{size}}" font-weight="{{weight}}" stroke-linejoin="round" ${paint2}`;
|
|
25504
25519
|
const layers = Array.from({ length: LAYERS }, (_, k) => LAYERS - k);
|
|
25505
25520
|
return {
|
|
25506
25521
|
svg: `<svg viewBox="{{viewBox}}" overflow="{{overflow}}" data-overhang="{{overhang}}"><!-- {{text: text default='RETRO' label='Text'}} {{background: color default=#FFE8C2 label='Background'}} {{face: color default=#FFD23F label='Face'}} {{outline: color default=#1A1423 label='Outline'}} {{side: color default=#EE4266 label='Extrude'}} {{shade: color default=#3B1F2B label='Shadow'}} {{depth: float min=0.2 max=2 default=1 step=0.1 label='Depth'}} ${typeface(HEAVY2, 900)} --><rect width="400" height="200" fill="{{background}}" opacity="{{backdrop}}"/>${layers.map((i) => `<text id="e${i}" transform="translate(0 0)" ${word(`fill="{{fill${i}}}" stroke="{{fill${i}}}" stroke-width="2"`)}>{{text}}</text>`).join("")}<text ${word(`fill="{{face}}" stroke="{{outline}}" stroke-width="2.5" paint-order="stroke"`)}>{{text}}</text></svg>`,
|
|
@@ -27181,7 +27196,7 @@ var dripText = {
|
|
|
27181
27196
|
const left = 200 - width / 2;
|
|
27182
27197
|
const runs = Math.min(DRIP_RUNS, Math.max(1, Math.round(width / DRIP_RUN)));
|
|
27183
27198
|
const run = width / runs;
|
|
27184
|
-
const
|
|
27199
|
+
const r116 = (n17) => Math.round(n17 * 10) / 10;
|
|
27185
27200
|
const edge = Number(v.stroke) / 2 / DRIP_SIZE;
|
|
27186
27201
|
const tip = (DRIP_TOP + Math.max(...DRIPS2.map((d) => d.length)) * Number(v.drip) + 13 * 0.72 - DRIP_BASELINE) / DRIP_SIZE;
|
|
27187
27202
|
const overhang = [edge, edge, Math.max(0.05, tip) + edge, edge].map((e) => Math.round(e * 100) / 100).join(" ");
|
|
@@ -27195,15 +27210,15 @@ var dripText = {
|
|
|
27195
27210
|
overhang,
|
|
27196
27211
|
...Object.fromEntries(
|
|
27197
27212
|
DRIP_SLOTS.flatMap((d) => {
|
|
27198
|
-
const x =
|
|
27199
|
-
const length =
|
|
27213
|
+
const x = r116(left + (d.run + d.at) * run);
|
|
27214
|
+
const length = r116(d.length * Number(v.drip));
|
|
27200
27215
|
return [
|
|
27201
27216
|
[`show${d.i}`, d.run < runs ? "inline" : "none"],
|
|
27202
27217
|
[`x${d.i}`, x],
|
|
27203
|
-
[`left${d.i}`,
|
|
27204
|
-
[`shine${d.i}`,
|
|
27218
|
+
[`left${d.i}`, r116(x - d.width / 2)],
|
|
27219
|
+
[`shine${d.i}`, r116(x - d.width * 0.25)],
|
|
27205
27220
|
[`len${d.i}`, length],
|
|
27206
|
-
[`tip${d.i}`,
|
|
27221
|
+
[`tip${d.i}`, r116(DRIP_TOP + length)]
|
|
27207
27222
|
];
|
|
27208
27223
|
})
|
|
27209
27224
|
)
|
|
@@ -27211,8 +27226,8 @@ var dripText = {
|
|
|
27211
27226
|
},
|
|
27212
27227
|
motion: (() => {
|
|
27213
27228
|
const word = `<text x="200" y="${DRIP_BASELINE}" text-anchor="middle" textLength="{{width}}" lengthAdjust="spacingAndGlyphs" font-family="{{font}}" font-size="${DRIP_SIZE}" font-weight="{{weight}}" stroke-linejoin="round"`;
|
|
27214
|
-
const drips = (layer,
|
|
27215
|
-
(d) => `<g display="{{show${d.i}}}"><rect id="${layer}r${d.i}" x="{{left${d.i}}}" y="${DRIP_TOP}" width="${d.width}" height="0" rx="${d.width / 2}" ${
|
|
27229
|
+
const drips = (layer, paint2) => DRIP_SLOTS.map(
|
|
27230
|
+
(d) => `<g display="{{show${d.i}}}"><rect id="${layer}r${d.i}" x="{{left${d.i}}}" y="${DRIP_TOP}" width="${d.width}" height="0" rx="${d.width / 2}" ${paint2}/><circle id="${layer}c${d.i}" cx="{{x${d.i}}}" cy="${DRIP_TOP}" r="${d.width * 0.72}" ${paint2}/></g>`
|
|
27216
27231
|
).join("");
|
|
27217
27232
|
const outline4 = `fill="{{outline}}" stroke="{{outline}}" stroke-width="{{stroke}}"`;
|
|
27218
27233
|
const pop2 = (from, to) => [`translate(200 100) scale(${from}) translate(-200 -100)`, `translate(200 100) scale(${to}) translate(-200 -100)`];
|
|
@@ -27346,6 +27361,1038 @@ var vectorEffects = [
|
|
|
27346
27361
|
uploadCard
|
|
27347
27362
|
];
|
|
27348
27363
|
|
|
27364
|
+
// src/catalog/data.ts
|
|
27365
|
+
var r114 = (v) => Math.round(v * 10) / 10;
|
|
27366
|
+
var r212 = (v) => Math.round(v * 100) / 100;
|
|
27367
|
+
var n15 = (name) => `{{${name}}}`;
|
|
27368
|
+
var SANS12 = `system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif`;
|
|
27369
|
+
function fit7(text, width, max) {
|
|
27370
|
+
const e = Math.max(0.6, textWidthEm(String(text)));
|
|
27371
|
+
const size = Math.min(max, width / e);
|
|
27372
|
+
return { size: r114(size), length: r114(e * size) };
|
|
27373
|
+
}
|
|
27374
|
+
function fitLabel(text, width, max, min) {
|
|
27375
|
+
const whole = String(text ?? "");
|
|
27376
|
+
const f = fit7(whole, width, max);
|
|
27377
|
+
if (f.size >= min) return { text: whole, ...f };
|
|
27378
|
+
const letters = Array.from(whole);
|
|
27379
|
+
while (letters.length > 1 && Math.max(0.6, textWidthEm(`${letters.join("").trimEnd()}\u2026`)) * min > width) letters.pop();
|
|
27380
|
+
const cut = `${letters.join("").trimEnd()}\u2026`;
|
|
27381
|
+
return { text: cut, ...fit7(cut, width, max) };
|
|
27382
|
+
}
|
|
27383
|
+
var paint = (v) => isCurrentColor(v) ? "currentColor" : Array.isArray(v) ? rgbToHex(v) : "#000000";
|
|
27384
|
+
var formatSafe = (v) => String(v ?? "").replace(/[{}]/g, "");
|
|
27385
|
+
var DATA_PALETTE = ["#6366F1", "#22C55E", "#F59E0B", "#EC4899", "#0EA5E9", "#A855F7"];
|
|
27386
|
+
var palette = (count = DATA_PALETTE.length) => DATA_PALETTE.slice(0, count).map((hex, i) => `{{c${i + 1}: color default=${hex} label='Colour ${i + 1}'}}`).join(" ");
|
|
27387
|
+
var PALETTE_PRESETS = [
|
|
27388
|
+
{ name: "Ocean", values: { c1: [0.03, 0.4, 0.62], c2: [0.05, 0.65, 0.91], c3: [0.22, 0.74, 0.97], c4: [0.49, 0.83, 0.99], c5: [0.08, 0.72, 0.65], c6: [0.37, 0.92, 0.83] } },
|
|
27389
|
+
{ name: "Sunset", values: { c1: [0.94, 0.27, 0.27], c2: [0.98, 0.45, 0.09], c3: [0.98, 0.75, 0.14], c4: [0.93, 0.29, 0.6], c5: [0.66, 0.33, 0.97], c6: [0.4, 0.4, 0.95] } }
|
|
27390
|
+
];
|
|
27391
|
+
function parseNumberList(text, max) {
|
|
27392
|
+
return String(text ?? "").split(/[,;\s]+/).filter((t) => t !== "").slice(0, max).map((token) => {
|
|
27393
|
+
const cleaned = token.replace(/[^0-9.eE+-]/g, "");
|
|
27394
|
+
const value = /\d/.test(cleaned) ? Number(cleaned) : NaN;
|
|
27395
|
+
return Number.isFinite(value) ? Math.max(-1e9, Math.min(1e9, value)) : NaN;
|
|
27396
|
+
});
|
|
27397
|
+
}
|
|
27398
|
+
function parseLabels(text, max) {
|
|
27399
|
+
return String(text ?? "").split(/[,;\n]/).map((s) => s.trim()).slice(0, max);
|
|
27400
|
+
}
|
|
27401
|
+
function parseShares(text, max, fallback) {
|
|
27402
|
+
const values = parseNumberList(text, max).map((v) => Number.isFinite(v) && v > 0 ? v : 0);
|
|
27403
|
+
return values.some((v) => v > 0) ? values : [...fallback];
|
|
27404
|
+
}
|
|
27405
|
+
function decimalsOf(values) {
|
|
27406
|
+
let most = 0;
|
|
27407
|
+
for (const v of values) {
|
|
27408
|
+
if (!Number.isFinite(v) || Number.isInteger(v)) continue;
|
|
27409
|
+
const s = String(v);
|
|
27410
|
+
most = Math.max(most, s.includes("e") ? 2 : s.split(".")[1]?.length ?? 0);
|
|
27411
|
+
}
|
|
27412
|
+
return Math.min(2, most);
|
|
27413
|
+
}
|
|
27414
|
+
function parseRace(text, maxRows, maxStates, fallback) {
|
|
27415
|
+
const rows = [];
|
|
27416
|
+
for (const line of String(text ?? "").split(/[;\n]+/)) {
|
|
27417
|
+
if (!line.trim()) continue;
|
|
27418
|
+
const colon = line.indexOf(":");
|
|
27419
|
+
const name = colon >= 0 ? line.slice(0, colon).trim() : `#${rows.length + 1}`;
|
|
27420
|
+
const raw = parseNumberList(colon >= 0 ? line.slice(colon + 1) : line, maxStates);
|
|
27421
|
+
if (raw.some((v) => Number.isFinite(v))) rows.push({ name, raw });
|
|
27422
|
+
if (rows.length === maxRows) break;
|
|
27423
|
+
}
|
|
27424
|
+
if (!rows.length) return fallback.map((r) => ({ name: r.name, values: [...r.values] }));
|
|
27425
|
+
const states = Math.max(...rows.map((r) => r.raw.length));
|
|
27426
|
+
return rows.map(({ name, raw }) => {
|
|
27427
|
+
let last = raw.find((v) => Number.isFinite(v)) ?? 0;
|
|
27428
|
+
const values = Array.from({ length: states }, (_, k) => {
|
|
27429
|
+
const v = raw[k];
|
|
27430
|
+
if (v !== void 0 && Number.isFinite(v)) last = Math.max(0, v);
|
|
27431
|
+
return last;
|
|
27432
|
+
});
|
|
27433
|
+
return { name, values };
|
|
27434
|
+
});
|
|
27435
|
+
}
|
|
27436
|
+
function parseSeries(text, maxRows, maxPoints, fallback) {
|
|
27437
|
+
const rows = String(text ?? "").split(/[;\n]+/).map((line) => parseNumberList(line, maxPoints)).filter((row) => row.some((v) => Number.isFinite(v))).slice(0, maxRows).map((row) => row.map((v) => Number.isFinite(v) && v > 0 ? v : 0));
|
|
27438
|
+
if (!rows.length || rows.every((r) => r.every((v) => v === 0))) return fallback.map((r) => [...r]);
|
|
27439
|
+
const points = Math.max(2, ...rows.map((r) => r.length));
|
|
27440
|
+
return rows.map((r) => Array.from({ length: points }, (_, j) => r[Math.min(j, r.length - 1)]));
|
|
27441
|
+
}
|
|
27442
|
+
var SLOTS3 = 6;
|
|
27443
|
+
var LEGEND = { swatch: 158, label: 174, right: 250, pitch: 22 };
|
|
27444
|
+
function legendValues(labels, fractions, at4, cy) {
|
|
27445
|
+
const shown = fractions.map((f) => f > 0);
|
|
27446
|
+
const count = shown.filter(Boolean).length;
|
|
27447
|
+
const out = {};
|
|
27448
|
+
let row = 0;
|
|
27449
|
+
for (let i = 0; i < SLOTS3; i++) {
|
|
27450
|
+
const f = fractions[i] ?? 0;
|
|
27451
|
+
const label = labels[i] ?? "";
|
|
27452
|
+
const { text, size, length } = fitLabel(label, LEGEND.right - LEGEND.label - 32, 11, 8);
|
|
27453
|
+
out[`show${i}`] = f > 0 ? "inline" : "none";
|
|
27454
|
+
out[`ly${i}`] = r114(cy + ((f > 0 ? row : 0) - (count - 1) / 2) * LEGEND.pitch);
|
|
27455
|
+
out[`l${i}`] = text;
|
|
27456
|
+
out[`ls${i}`] = size;
|
|
27457
|
+
out[`ll${i}`] = length;
|
|
27458
|
+
out[`pct${i}`] = Math.round(f * 100);
|
|
27459
|
+
out[`at${i}`] = at4[i] ?? 0;
|
|
27460
|
+
if (f > 0) row++;
|
|
27461
|
+
}
|
|
27462
|
+
return out;
|
|
27463
|
+
}
|
|
27464
|
+
var legendMarkup = (prefix) => Array.from(
|
|
27465
|
+
{ length: SLOTS3 },
|
|
27466
|
+
(_, i) => `<g id="${prefix}Row${i}" display="{{show${i}}}" opacity="0" transform="translate(8 {{ly${i}}})"><rect x="${LEGEND.swatch}" y="-5" width="10" height="10" rx="3" fill="{{c${i + 1}}}"/><text x="${LEGEND.label}" y="4" font-size="{{ls${i}}}" textLength="{{ll${i}}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}">{{l${i}}}</text><text id="${prefix}Pct${i}" x="${LEGEND.right}" y="4" text-anchor="end" font-size="11" font-weight="700" fill="{{ink}}">0%</text></g>`
|
|
27467
|
+
).join("");
|
|
27468
|
+
var legendAnimations = (prefix) => Array.from({ length: SLOTS3 }, (_, i) => [
|
|
27469
|
+
{ id: `${prefix}Row${i}`, attrs: { opacity: [0, 1], transform: [`translate(8 {{ly${i}}})`, `translate(0 {{ly${i}}})`] }, delay: n15(`at${i}`), ms: 320, ease: "out" },
|
|
27470
|
+
{ id: `${prefix}Pct${i}`, count: [0, n15(`pct${i}`)], format: "{n}%", delay: n15(`at${i}`), ms: 600, ease: "out" }
|
|
27471
|
+
]).flat();
|
|
27472
|
+
var DONUT = { cx: 80, cy: 80, r: 50, width: 22, start: 150, sweep: 1400 };
|
|
27473
|
+
var DONUT_DEFAULT = [42, 28, 18, 12];
|
|
27474
|
+
var donutChart = {
|
|
27475
|
+
kind: "vector",
|
|
27476
|
+
id: "donut-chart",
|
|
27477
|
+
name: "Donut chart",
|
|
27478
|
+
description: "Segments sweeping round a ring one after another, a legend giving each share, and the total counting up in the middle.",
|
|
27479
|
+
category: "data",
|
|
27480
|
+
tags: ["chart", "donut", "share", "dashboard", "report", "explainer"],
|
|
27481
|
+
hold: 1600,
|
|
27482
|
+
presets: [...PALETTE_PRESETS, { name: "Budget", values: { values: "1200,800,450,300,250", labels: "Rent,Food,Travel,Fun,Other", caption: "per month" } }],
|
|
27483
|
+
derived: (v) => {
|
|
27484
|
+
const values = parseShares(v.values, SLOTS3, DONUT_DEFAULT);
|
|
27485
|
+
const total = values.reduce((a, b) => a + b, 0);
|
|
27486
|
+
const fractions = values.map((x) => x / total);
|
|
27487
|
+
const gap = fractions.filter((f) => f > 0).length > 1 ? 5e-3 : 0;
|
|
27488
|
+
const out = {};
|
|
27489
|
+
const at4 = [];
|
|
27490
|
+
let cum = 0;
|
|
27491
|
+
for (let i = 0; i < SLOTS3; i++) {
|
|
27492
|
+
const f = fractions[i] ?? 0;
|
|
27493
|
+
at4.push(Math.round(DONUT.start + cum * DONUT.sweep));
|
|
27494
|
+
out[`rot${i}`] = r212(-90 + cum * 360);
|
|
27495
|
+
out[`frac${i}`] = Math.min(1, Math.max(1e-3, +(f - gap).toFixed(4)));
|
|
27496
|
+
out[`ms${i}`] = Math.max(60, Math.round(f * DONUT.sweep));
|
|
27497
|
+
cum += f;
|
|
27498
|
+
}
|
|
27499
|
+
const dec = decimalsOf(values);
|
|
27500
|
+
const totalSize = fit7(formatCount(total, { decimals: dec }), 64, 26).size;
|
|
27501
|
+
const caption = fitLabel(v.caption, 58, 9.5, 7.5);
|
|
27502
|
+
return {
|
|
27503
|
+
...legendValues(parseLabels(v.labels, SLOTS3), fractions, at4, DONUT.cy),
|
|
27504
|
+
...out,
|
|
27505
|
+
total: +total.toFixed(dec),
|
|
27506
|
+
dec,
|
|
27507
|
+
totalSize,
|
|
27508
|
+
totalY: r114(DONUT.cy + totalSize * 0.35 - (String(v.caption).trim() ? 5 : 0)),
|
|
27509
|
+
captionY: r114(DONUT.cy + totalSize * 0.35 + 9),
|
|
27510
|
+
captionText: caption.text,
|
|
27511
|
+
captionSize: caption.size,
|
|
27512
|
+
captionLength: caption.length,
|
|
27513
|
+
countMs: DONUT.start + DONUT.sweep
|
|
27514
|
+
};
|
|
27515
|
+
},
|
|
27516
|
+
motion: {
|
|
27517
|
+
svg: `<svg viewBox="0 0 260 160" font-family="${SANS12}"><!-- {{values: text default='42,28,18,12' label='Values (comma-separated)'}} {{labels: text default='Direct,Social,Search,Email' label='Labels (comma-separated)'}} {{caption: text default='visits' label='Caption'}} {{ink: color default=#1E293B label='Text'}} {{track: color default=#E2E8F0 label='Track'}} ${palette()} --><circle cx="${DONUT.cx}" cy="${DONUT.cy}" r="${DONUT.r}" fill="none" stroke="{{track}}" stroke-width="${DONUT.width}"/>${Array.from(
|
|
27518
|
+
{ length: SLOTS3 },
|
|
27519
|
+
(_, i) => `<g display="{{show${i}}}"><circle id="donutChartSeg${i}" cx="${DONUT.cx}" cy="${DONUT.cy}" r="${DONUT.r}" fill="none" stroke="{{c${i + 1}}}" stroke-width="${DONUT.width}" transform="rotate({{rot${i}}} ${DONUT.cx} ${DONUT.cy})"/></g>`
|
|
27520
|
+
).join(
|
|
27521
|
+
""
|
|
27522
|
+
)}<text id="donutChartTotal" x="${DONUT.cx}" y="{{totalY}}" text-anchor="middle" font-size="{{totalSize}}" font-weight="800" fill="{{ink}}">0</text><text x="${DONUT.cx}" y="{{captionY}}" text-anchor="middle" font-size="{{captionSize}}" textLength="{{captionLength}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}" fill-opacity="0.6">{{captionText}}</text>${legendMarkup("donutChart")}</svg>`,
|
|
27523
|
+
animate: [
|
|
27524
|
+
...Array.from({ length: SLOTS3 }, (_, i) => ({ id: `donutChartSeg${i}`, draw: [0, n15(`frac${i}`)], delay: n15(`at${i}`), ms: n15(`ms${i}`), ease: "linear" })),
|
|
27525
|
+
{ id: "donutChartTotal", count: [0, n15("total")], decimals: n15("dec"), ms: n15("countMs"), ease: "out" },
|
|
27526
|
+
...legendAnimations("donutChart")
|
|
27527
|
+
]
|
|
27528
|
+
}
|
|
27529
|
+
};
|
|
27530
|
+
var PIE = { cx: 80, cy: 80, r: 58, pop: 7, start: 150, stagger: 230 };
|
|
27531
|
+
var PIE_DEFAULT = [38, 26, 20, 16];
|
|
27532
|
+
var pieMove = (dx, dy, s) => `translate(${dx} ${dy}) translate(${PIE.cx} ${PIE.cy}) scale(${s}) translate(${-PIE.cx} ${-PIE.cy})`;
|
|
27533
|
+
function wedge(a, b) {
|
|
27534
|
+
const { cx, cy, r } = PIE;
|
|
27535
|
+
if (b - a <= 0) return `M${cx} ${cy}Z`;
|
|
27536
|
+
if (b - a >= 0.9999) return `M${cx} ${cy - r} A${r} ${r} 0 1 1 ${cx} ${cy + r} A${r} ${r} 0 1 1 ${cx} ${cy - r}Z`;
|
|
27537
|
+
const at4 = (f) => `${r212(cx + r * Math.sin(f * 2 * Math.PI))} ${r212(cy - r * Math.cos(f * 2 * Math.PI))}`;
|
|
27538
|
+
return `M${cx} ${cy} L${at4(a)} A${r} ${r} 0 ${b - a > 0.5 ? 1 : 0} 1 ${at4(b)}Z`;
|
|
27539
|
+
}
|
|
27540
|
+
var pieChart = {
|
|
27541
|
+
kind: "vector",
|
|
27542
|
+
id: "pie-chart",
|
|
27543
|
+
name: "Pie chart",
|
|
27544
|
+
description: "Pie slices popping out one at a time and settling into place, each with its percentage, beside a legend.",
|
|
27545
|
+
category: "data",
|
|
27546
|
+
tags: ["chart", "pie", "share", "percentage", "dashboard", "report"],
|
|
27547
|
+
hold: 1600,
|
|
27548
|
+
presets: [...PALETTE_PRESETS, { name: "Survey", values: { values: "54,31,15", labels: "Yes,No,Not sure" } }],
|
|
27549
|
+
derived: (v) => {
|
|
27550
|
+
const values = parseShares(v.values, SLOTS3, PIE_DEFAULT);
|
|
27551
|
+
const total = values.reduce((a, b) => a + b, 0);
|
|
27552
|
+
const fractions = values.map((x) => x / total);
|
|
27553
|
+
const out = {};
|
|
27554
|
+
const at4 = [];
|
|
27555
|
+
const ink = paint(v.ink);
|
|
27556
|
+
let cum = 0;
|
|
27557
|
+
for (let i = 0; i < SLOTS3; i++) {
|
|
27558
|
+
const f = fractions[i] ?? 0;
|
|
27559
|
+
const mid = (cum + f / 2) * 2 * Math.PI;
|
|
27560
|
+
const inside = f >= 0.08;
|
|
27561
|
+
out[`tshow${i}`] = f >= 0.04 ? "inline" : "none";
|
|
27562
|
+
const radius = inside ? PIE.r * 0.62 : PIE.r + 14;
|
|
27563
|
+
at4.push(PIE.start + i * PIE.stagger);
|
|
27564
|
+
out[`d${i}`] = wedge(cum, cum + f);
|
|
27565
|
+
out[`ox${i}`] = r212(PIE.pop * Math.sin(mid));
|
|
27566
|
+
out[`oy${i}`] = r212(-PIE.pop * Math.cos(mid));
|
|
27567
|
+
out[`tx${i}`] = r114(PIE.cx + radius * Math.sin(mid));
|
|
27568
|
+
out[`ty${i}`] = r114(PIE.cy - radius * Math.cos(mid) + 4);
|
|
27569
|
+
out[`tf${i}`] = inside ? paint(v.onSlice) : ink;
|
|
27570
|
+
out[`tl${i}`] = PIE.start + i * PIE.stagger + 260;
|
|
27571
|
+
cum += f;
|
|
27572
|
+
}
|
|
27573
|
+
return { ...legendValues(parseLabels(v.labels, SLOTS3), fractions, at4, PIE.cy), ...out };
|
|
27574
|
+
},
|
|
27575
|
+
motion: {
|
|
27576
|
+
svg: `<svg viewBox="0 0 260 160" font-family="${SANS12}"><!-- {{values: text default='38,26,20,16' label='Values (comma-separated)'}} {{labels: text default='Product,Services,Support,Other' label='Labels (comma-separated)'}} {{ink: color default=#1E293B label='Text'}} {{onSlice: color default=#FFFFFF label='Labels on slices'}} {{edge: color default=#FFFFFF label='Slice edges'}} ${palette()} -->${Array.from(
|
|
27577
|
+
{ length: SLOTS3 },
|
|
27578
|
+
(_, i) => `<g id="pieChartSlice${i}" display="{{show${i}}}" transform="${pieMove(0, 0, 0)}"><path d="{{d${i}}}" fill="{{c${i + 1}}}" stroke="{{edge}}" stroke-width="2" stroke-linejoin="round"/></g>`
|
|
27579
|
+
).join("")}${Array.from(
|
|
27580
|
+
{ length: SLOTS3 },
|
|
27581
|
+
(_, i) => `<text id="pieChartLabel${i}" display="{{tshow${i}}}" x="{{tx${i}}}" y="{{ty${i}}}" text-anchor="middle" font-size="11" font-weight="700" fill="{{tf${i}}}" opacity="0">0%</text>`
|
|
27582
|
+
).join("")}${legendMarkup("pieChart")}</svg>`,
|
|
27583
|
+
animate: [
|
|
27584
|
+
...Array.from({ length: SLOTS3 }, (_, i) => [
|
|
27585
|
+
{ id: `pieChartSlice${i}`, attrs: { transform: [pieMove(0, 0, 0), pieMove(`{{ox${i}}}`, `{{oy${i}}}`, 1.06)] }, delay: n15(`at${i}`), ms: 260, ease: "out" },
|
|
27586
|
+
{ id: `pieChartSlice${i}`, attrs: { transform: [pieMove(`{{ox${i}}}`, `{{oy${i}}}`, 1.06), pieMove(0, 0, 1)] }, delay: n15(`tl${i}`), ms: 300, ease: "in-out" },
|
|
27587
|
+
{ id: `pieChartLabel${i}`, attrs: { opacity: [0, 1] }, delay: n15(`tl${i}`), ms: 200 },
|
|
27588
|
+
{ id: `pieChartLabel${i}`, count: [0, n15(`pct${i}`)], format: "{n}%", delay: n15(`tl${i}`), ms: 500, ease: "out" }
|
|
27589
|
+
]).flat(),
|
|
27590
|
+
...legendAnimations("pieChart")
|
|
27591
|
+
]
|
|
27592
|
+
}
|
|
27593
|
+
};
|
|
27594
|
+
var GAUGE = { cx: 120, cy: 112, r: 84, band: 14 };
|
|
27595
|
+
var gaugeAt = (f, r) => `${r212(GAUGE.cx - r * Math.cos(f * Math.PI))} ${r212(GAUGE.cy - r * Math.sin(f * Math.PI))}`;
|
|
27596
|
+
var gaugeArc = (a, b) => b - a <= 5e-4 ? `M${gaugeAt(a, GAUGE.r)}` : `M${gaugeAt(a, GAUGE.r)} A${GAUGE.r} ${GAUGE.r} 0 0 1 ${gaugeAt(b, GAUGE.r)}`;
|
|
27597
|
+
var needle = (deg) => `rotate(${deg} ${GAUGE.cx} ${GAUGE.cy})`;
|
|
27598
|
+
var gauge = {
|
|
27599
|
+
kind: "vector",
|
|
27600
|
+
id: "gauge",
|
|
27601
|
+
name: "Gauge",
|
|
27602
|
+
description: "A speedometer gauge: its coloured band draws on, the needle sweeps past the value and settles, and the reading counts up beneath.",
|
|
27603
|
+
category: "data",
|
|
27604
|
+
tags: ["gauge", "speedometer", "meter", "kpi", "dashboard", "score"],
|
|
27605
|
+
hold: 1600,
|
|
27606
|
+
presets: [
|
|
27607
|
+
{ name: "Speed", values: { value: 88, max: 160, unit: " km/h", label: "Speed", warn: 70, danger: 85 } },
|
|
27608
|
+
{ name: "Dark", values: { ink: [0.94, 0.95, 0.97], needleColor: [0.94, 0.95, 0.97], hub: [0.06, 0.07, 0.1] } }
|
|
27609
|
+
],
|
|
27610
|
+
derived: (v) => {
|
|
27611
|
+
const max = Math.max(1e-3, Number(v.max) || 100);
|
|
27612
|
+
const value = Math.min(max, Math.max(0, Number(v.value) || 0));
|
|
27613
|
+
const f = value / max;
|
|
27614
|
+
const warn = Math.min(1, Math.max(0, Number(v.warn) / 100));
|
|
27615
|
+
const danger = Math.min(1, Math.max(warn, Number(v.danger) / 100));
|
|
27616
|
+
const dec = decimalsOf([value]);
|
|
27617
|
+
const unit = formatSafe(v.unit);
|
|
27618
|
+
const reading = fit7(`${formatCount(value, { decimals: dec })}${unit}`, 150, 26);
|
|
27619
|
+
const label = fitLabel(v.label, 150, 10, 8);
|
|
27620
|
+
const ends = [0, max].map((x) => fit7(formatCount(x, { decimals: decimalsOf([x]) }), 30, 10).size);
|
|
27621
|
+
return {
|
|
27622
|
+
band0: gaugeArc(0, warn),
|
|
27623
|
+
band1: gaugeArc(warn, danger),
|
|
27624
|
+
band2: gaugeArc(danger, 1),
|
|
27625
|
+
over: needle(r114(180 * Math.min(1, f + 0.06))),
|
|
27626
|
+
angle: needle(r114(180 * f)),
|
|
27627
|
+
reading: +value.toFixed(dec),
|
|
27628
|
+
dec,
|
|
27629
|
+
unit,
|
|
27630
|
+
readingSize: reading.size,
|
|
27631
|
+
labelText: label.text,
|
|
27632
|
+
labelSize: label.size,
|
|
27633
|
+
labelLength: label.length,
|
|
27634
|
+
maxText: formatCount(max, { decimals: decimalsOf([max]) }),
|
|
27635
|
+
maxSize: ends[1]
|
|
27636
|
+
};
|
|
27637
|
+
},
|
|
27638
|
+
motion: {
|
|
27639
|
+
svg: `<svg viewBox="0 0 240 170" font-family="${SANS12}"><!-- {{value: float min=0 max=10000 default=72 step=1 label='Value'}} {{max: float min=1 max=10000 default=100 step=1 label='Scale maximum'}} {{unit: text default='%' label='Unit'}} {{label: text default='Performance' label='Label'}} {{warn: float min=0 max=100 default=60 step=1 label='Amber from (% of scale)'}} {{danger: float min=0 max=100 default=85 step=1 label='Red from (% of scale)'}} {{low: color default=#22C55E label='Low band'}} {{mid: color default=#F59E0B label='Middle band'}} {{high: color default=#EF4444 label='High band'}} {{needleColor: color default=#0F172A label='Needle'}} {{hub: color default=#FFFFFF label='Hub centre'}} {{ink: color default=#1E293B label='Text'}} --><path id="gaugeBand0" d="{{band0}}" fill="none" stroke="{{low}}" stroke-width="${GAUGE.band}"/><path id="gaugeBand1" d="{{band1}}" fill="none" stroke="{{mid}}" stroke-width="${GAUGE.band}"/><path id="gaugeBand2" d="{{band2}}" fill="none" stroke="{{high}}" stroke-width="${GAUGE.band}"/><g stroke="{{ink}}" stroke-opacity="0.35" stroke-linecap="round">${Array.from({ length: 11 }, (_, k) => `<path d="M${gaugeAt(k / 10, 70)} L${gaugeAt(k / 10, k % 5 ? 65 : 61)}" stroke-width="${k % 5 ? 1.5 : 2.5}"/>`).join("")}</g><text x="${GAUGE.cx - GAUGE.r}" y="130" text-anchor="middle" font-size="10" fill="{{ink}}" fill-opacity="0.6">0</text><text x="${GAUGE.cx + GAUGE.r}" y="130" text-anchor="middle" font-size="{{maxSize}}" fill="{{ink}}" fill-opacity="0.6">{{maxText}}</text><g id="gaugeNeedle" transform="${needle(0)}"><path d="M${GAUGE.cx} ${GAUGE.cy - 4} L${GAUGE.cx - GAUGE.r + 12} ${GAUGE.cy} L${GAUGE.cx} ${GAUGE.cy + 4}Z" fill="{{needleColor}}"/></g><circle cx="${GAUGE.cx}" cy="${GAUGE.cy}" r="8" fill="{{needleColor}}"/><circle cx="${GAUGE.cx}" cy="${GAUGE.cy}" r="3" fill="{{hub}}"/><text id="gaugeValue" x="${GAUGE.cx}" y="148" text-anchor="middle" font-size="{{readingSize}}" font-weight="800" fill="{{ink}}">0</text><text x="${GAUGE.cx}" y="164" text-anchor="middle" font-size="{{labelSize}}" textLength="{{labelLength}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}" fill-opacity="0.6">{{labelText}}</text></svg>`,
|
|
27640
|
+
animate: [
|
|
27641
|
+
...[0, 1, 2].map((k) => ({ id: `gaugeBand${k}`, draw: true, delay: k * 140, ms: 320, ease: k === 0 ? "out" : "linear" })),
|
|
27642
|
+
{ id: "gaugeNeedle", attrs: { transform: [needle(0), "{{over}}"] }, delay: 300, ms: 900, ease: "out" },
|
|
27643
|
+
{ id: "gaugeNeedle", attrs: { transform: ["{{over}}", "{{angle}}"] }, delay: 1200, ms: 450, ease: "in-out" },
|
|
27644
|
+
{ id: "gaugeValue", count: [0, n15("reading")], decimals: n15("dec"), format: "{n}{{unit}}", delay: 300, ms: 1300, ease: "out" }
|
|
27645
|
+
]
|
|
27646
|
+
}
|
|
27647
|
+
};
|
|
27648
|
+
var RACE_ROWS = 6;
|
|
27649
|
+
var RACE_STATES = 5;
|
|
27650
|
+
var RACE = { left: 74, top: 12, pitch: 28, height: 22, width: 320, value: 10, intro: 900, move: 1e3, rest: 700 };
|
|
27651
|
+
var RACE_DEFAULT = [
|
|
27652
|
+
{ name: "Lumen", values: [12, 28, 41, 66] },
|
|
27653
|
+
{ name: "Pixel", values: [30, 36, 48, 54] },
|
|
27654
|
+
{ name: "Vertex", values: [22, 40, 39, 71] },
|
|
27655
|
+
{ name: "Shader", values: [8, 20, 52, 60] },
|
|
27656
|
+
{ name: "Raster", values: [18, 24, 30, 35] },
|
|
27657
|
+
{ name: "Vector", values: [26, 30, 44, 49] }
|
|
27658
|
+
];
|
|
27659
|
+
var raceTime = (k) => RACE.intro + RACE.rest + (k - 1) * (RACE.move + RACE.rest);
|
|
27660
|
+
var barRace = {
|
|
27661
|
+
kind: "vector",
|
|
27662
|
+
id: "bar-race",
|
|
27663
|
+
name: "Bar race",
|
|
27664
|
+
description: "A bar chart race: bars stretch and overtake each other, swapping places as the period label ticks through the years.",
|
|
27665
|
+
category: "data",
|
|
27666
|
+
tags: ["chart", "race", "ranking", "bars", "timeline", "explainer"],
|
|
27667
|
+
hold: 1800,
|
|
27668
|
+
presets: [
|
|
27669
|
+
...PALETTE_PRESETS,
|
|
27670
|
+
{ name: "Browsers", values: { rows: "Chrome:20,45,62,65;Firefox:28,30,22,8;Safari:5,8,15,19;Edge:0,2,4,5;Opera:3,2,2,2", periods: "2009,2012,2016,2020" } }
|
|
27671
|
+
],
|
|
27672
|
+
derived: (v) => {
|
|
27673
|
+
const rows = parseRace(v.rows, RACE_ROWS, RACE_STATES, RACE_DEFAULT);
|
|
27674
|
+
const states = Math.max(1, rows[0].values.length);
|
|
27675
|
+
const all = rows.flatMap((r) => r.values);
|
|
27676
|
+
const dec = decimalsOf(all);
|
|
27677
|
+
const valueWidth = Math.max(...all.map((x) => textWidthEm(formatCount(x, { decimals: dec })))) * RACE.value;
|
|
27678
|
+
const barWidth = Math.max(40, RACE.width - RACE.left - 10 - valueWidth);
|
|
27679
|
+
const periods = parseLabels(v.periods, RACE_STATES);
|
|
27680
|
+
const end = states > 1 ? raceTime(states - 1) + RACE.move : RACE.intro;
|
|
27681
|
+
const out = { dec };
|
|
27682
|
+
const ranks = Array.from({ length: RACE_STATES }, (_, k) => {
|
|
27683
|
+
const s = Math.min(k, states - 1);
|
|
27684
|
+
const order = rows.map((r, i) => [i, r.values[s]]).sort((a, b) => b[1] - a[1] || a[0] - b[0]);
|
|
27685
|
+
const rank = new Map(order.map(([i], place) => [i, place]));
|
|
27686
|
+
const top = Math.max(1e-9, order[0][1]);
|
|
27687
|
+
return { s, rank, top };
|
|
27688
|
+
});
|
|
27689
|
+
for (let i = 0; i < RACE_ROWS; i++) {
|
|
27690
|
+
const row = rows[i];
|
|
27691
|
+
const name = fitLabel(row?.name ?? "", RACE.left - 12, 11, 8);
|
|
27692
|
+
out[`show${i}`] = row ? "inline" : "none";
|
|
27693
|
+
out[`nm${i}`] = name.text;
|
|
27694
|
+
out[`ns${i}`] = name.size;
|
|
27695
|
+
out[`nl${i}`] = name.length;
|
|
27696
|
+
out[`in${i}`] = i * 70;
|
|
27697
|
+
for (let k = 0; k < RACE_STATES; k++) {
|
|
27698
|
+
const { s, rank, top } = ranks[k];
|
|
27699
|
+
const value = row ? row.values[s] : 0;
|
|
27700
|
+
const w = r114(value / top * barWidth);
|
|
27701
|
+
out[`y${i}_${k}`] = RACE.top + (rank.get(i) ?? i) * RACE.pitch;
|
|
27702
|
+
out[`w${i}_${k}`] = w;
|
|
27703
|
+
out[`x${i}_${k}`] = r114(RACE.left + w + 5);
|
|
27704
|
+
out[`v${i}_${k}`] = value;
|
|
27705
|
+
}
|
|
27706
|
+
}
|
|
27707
|
+
for (let k = 1; k < RACE_STATES; k++) {
|
|
27708
|
+
out[`t${k}`] = k < states ? raceTime(k) : end;
|
|
27709
|
+
out[`m${k}`] = k < states ? RACE.move : 0;
|
|
27710
|
+
out[`tick${k}`] = k < states ? raceTime(k) + RACE.move / 2 : end;
|
|
27711
|
+
out[`solo${k}`] = Math.min(k, states - 1);
|
|
27712
|
+
}
|
|
27713
|
+
for (let k = 0; k < RACE_STATES; k++) {
|
|
27714
|
+
const p = fit7(periods[k] ?? "", 120, 30);
|
|
27715
|
+
out[`p${k}`] = periods[k] ?? "";
|
|
27716
|
+
out[`ps${k}`] = p.size;
|
|
27717
|
+
}
|
|
27718
|
+
return out;
|
|
27719
|
+
},
|
|
27720
|
+
motion: {
|
|
27721
|
+
svg: `<svg viewBox="0 0 ${RACE.width} 210" font-family="${SANS12}"><!-- {{rows: text default='Lumen:12,28,41,66;Pixel:30,36,48,54;Vertex:22,40,39,71;Shader:8,20,52,60;Raster:18,24,30,35;Vector:26,30,44,49' label='Rows (Name:v1,v2,\u2026; \u2026)'}} {{periods: text default='2021,2022,2023,2024' label='Periods (comma-separated)'}} {{ink: color default=#1E293B label='Text'}} ${palette()} --><g id="barRacePeriod" font-weight="800" fill="{{ink}}" fill-opacity="0.22" text-anchor="end">${Array.from(
|
|
27722
|
+
{ length: RACE_STATES },
|
|
27723
|
+
(_, k) => `<text x="${RACE.width - 8}" y="202" font-size="{{ps${k}}}"${k ? ` display="none"` : ""}>{{p${k}}}</text>`
|
|
27724
|
+
).join("")}</g>${Array.from(
|
|
27725
|
+
{ length: RACE_ROWS },
|
|
27726
|
+
(_, i) => `<g id="barRaceRow${i}" display="{{show${i}}}" opacity="0" transform="translate(0 {{y${i}_0}})"><text x="${RACE.left - 6}" y="15" text-anchor="end" font-size="{{ns${i}}}" textLength="{{nl${i}}}" lengthAdjust="spacingAndGlyphs" font-weight="600" fill="{{ink}}">{{nm${i}}}</text><rect id="barRaceBar${i}" x="${RACE.left}" y="0" width="0" height="${RACE.height}" rx="4" fill="{{c${i + 1}}}"/><text id="barRaceValue${i}" x="${RACE.left + 5}" y="15" font-size="${RACE.value}" font-weight="700" fill="{{ink}}">0</text></g>`
|
|
27727
|
+
).join("")}</svg>`,
|
|
27728
|
+
animate: [
|
|
27729
|
+
{ id: "barRacePeriod", solo: 0 },
|
|
27730
|
+
...Array.from({ length: RACE_STATES - 1 }, (_, j) => ({ id: "barRacePeriod", solo: n15(`solo${j + 1}`), delay: n15(`tick${j + 1}`) })),
|
|
27731
|
+
...Array.from({ length: RACE_ROWS }, (_, i) => [
|
|
27732
|
+
{ id: `barRaceRow${i}`, attrs: { opacity: [0, 1] }, delay: n15(`in${i}`), ms: 300 },
|
|
27733
|
+
{ id: `barRaceBar${i}`, attrs: { width: [0, n15(`w${i}_0`)] }, delay: n15(`in${i}`), ms: 800, ease: "out" },
|
|
27734
|
+
{ id: `barRaceValue${i}`, attrs: { x: [RACE.left + 5, n15(`x${i}_0`)] }, count: [0, n15(`v${i}_0`)], decimals: n15("dec"), delay: n15(`in${i}`), ms: 800, ease: "out" },
|
|
27735
|
+
...Array.from({ length: RACE_STATES - 1 }, (_2, j) => {
|
|
27736
|
+
const [a, b] = [j, j + 1];
|
|
27737
|
+
const at4 = { delay: n15(`t${b}`), ms: n15(`m${b}`), ease: "in-out" };
|
|
27738
|
+
return [
|
|
27739
|
+
{ id: `barRaceRow${i}`, attrs: { transform: [`translate(0 {{y${i}_${a}}})`, `translate(0 {{y${i}_${b}}})`] }, ...at4 },
|
|
27740
|
+
{ id: `barRaceBar${i}`, attrs: { width: [n15(`w${i}_${a}`), n15(`w${i}_${b}`)] }, ...at4 },
|
|
27741
|
+
{ id: `barRaceValue${i}`, attrs: { x: [n15(`x${i}_${a}`), n15(`x${i}_${b}`)] }, count: [n15(`v${i}_${a}`), n15(`v${i}_${b}`)], decimals: n15("dec"), ...at4 }
|
|
27742
|
+
];
|
|
27743
|
+
}).flat()
|
|
27744
|
+
]).flat()
|
|
27745
|
+
]
|
|
27746
|
+
}
|
|
27747
|
+
};
|
|
27748
|
+
var SPARK_DEFAULT = [4, 6, 5, 8, 7, 10, 9, 13];
|
|
27749
|
+
var SPARK_DRAW = 1200;
|
|
27750
|
+
function linePath(values, x0, x1, top, bottom) {
|
|
27751
|
+
const lo = Math.min(...values);
|
|
27752
|
+
const hi = Math.max(...values);
|
|
27753
|
+
const span = hi - lo || 1;
|
|
27754
|
+
const pts = values.map((v, j) => [r114(x0 + j * (x1 - x0) / (values.length - 1)), r114(hi === lo ? (top + bottom) / 2 : bottom - (v - lo) / span * (bottom - top))]);
|
|
27755
|
+
const line = pts.map(([x, y], j) => `${j ? "L" : "M"}${x} ${y}`).join(" ");
|
|
27756
|
+
return { line, area: `${line} L${pts.at(-1)[0]} ${bottom + 3} L${pts[0][0]} ${bottom + 3}Z`, end: pts.at(-1) };
|
|
27757
|
+
}
|
|
27758
|
+
function series(text, max, fallback) {
|
|
27759
|
+
const values = parseNumberList(text, max).filter((x) => Number.isFinite(x));
|
|
27760
|
+
return values.length >= 2 ? values : values.length === 1 ? [values[0], values[0]] : fallback;
|
|
27761
|
+
}
|
|
27762
|
+
var sparkline = {
|
|
27763
|
+
kind: "vector",
|
|
27764
|
+
id: "sparkline",
|
|
27765
|
+
name: "Sparkline",
|
|
27766
|
+
description: "A tiny trend line drawing in with a dot riding its tip, which then pulses beside the latest value; small enough to sit in a line of text.",
|
|
27767
|
+
category: "data",
|
|
27768
|
+
tags: ["chart", "sparkline", "trend", "inline", "kpi", "dashboard"],
|
|
27769
|
+
hold: 600,
|
|
27770
|
+
presets: [
|
|
27771
|
+
{ name: "Falling", values: { values: "14,13,11,12,9,8,9,6", accent: [0.86, 0.15, 0.15] } },
|
|
27772
|
+
{ name: "Violet", values: { accent: [0.55, 0.36, 0.96] } }
|
|
27773
|
+
],
|
|
27774
|
+
derived: (v) => {
|
|
27775
|
+
const values = series(v.values, 32, SPARK_DEFAULT);
|
|
27776
|
+
const dec = decimalsOf(values);
|
|
27777
|
+
const e = Math.max(0.6, textWidthEm(formatCount(values.at(-1), { decimals: dec })));
|
|
27778
|
+
const size = r114(Math.min(13, 44 / e));
|
|
27779
|
+
const right = r114(120 - 3 - e * size - 6);
|
|
27780
|
+
const { line, area } = linePath(values, 4, right, 6, 26);
|
|
27781
|
+
return { line, area, size, valueX: r114(right + 6), first: values[0], last: values.at(-1), dec };
|
|
27782
|
+
},
|
|
27783
|
+
motion: {
|
|
27784
|
+
svg: `<svg viewBox="0 0 120 32" font-family="${SANS12}"><!-- {{values: text default='4,6,5,8,7,10,9,13' label='Values (comma-separated)'}} {{accent: color default=#10B981 label='Line'}} {{ink: color default=#1E293B label='Value'}} --><defs><linearGradient id="sparklineFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="{{accent}}" stop-opacity="0.3"/><stop offset="1" stop-color="{{accent}}" stop-opacity="0"/></linearGradient></defs><path id="sparklineArea" d="{{area}}" fill="url(#sparklineFill)" opacity="0"/><path id="sparklineLine" d="{{line}}" fill="none" stroke="{{accent}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><g id="sparklineDot"><circle id="sparklinePulse" r="2.6" fill="none" stroke="{{accent}}" stroke-width="1.5" opacity="0"/><circle r="2.8" fill="{{accent}}"/></g><text id="sparklineValue" x="{{valueX}}" y="20.5" font-size="{{size}}" font-weight="700" fill="{{ink}}">0</text></svg>`,
|
|
27785
|
+
animate: [
|
|
27786
|
+
{ id: "sparklineLine", draw: true, ms: SPARK_DRAW, ease: "in-out" },
|
|
27787
|
+
{ id: "sparklineDot", along: "sparklineLine", ms: SPARK_DRAW, ease: "in-out" },
|
|
27788
|
+
{ id: "sparklineArea", attrs: { opacity: [0, 1] }, delay: 700, ms: 600 },
|
|
27789
|
+
{ id: "sparklineValue", count: [n15("first"), n15("last")], decimals: n15("dec"), ms: SPARK_DRAW, ease: "in-out" },
|
|
27790
|
+
...[0, 1, 2].map((k) => ({ id: "sparklinePulse", attrs: { r: [2.6, 9], opacity: [0.8, 0] }, delay: SPARK_DRAW + 50 + k * 750, ms: 700, ease: "out" }))
|
|
27791
|
+
]
|
|
27792
|
+
}
|
|
27793
|
+
};
|
|
27794
|
+
var BOARD = { rows: 6, top: 8, pitch: 34, height: 30, left: 8, width: 244, slide: 130 };
|
|
27795
|
+
var leaderboard = {
|
|
27796
|
+
kind: "vector",
|
|
27797
|
+
id: "leaderboard",
|
|
27798
|
+
name: "Leaderboard",
|
|
27799
|
+
description: "Ranked rows sliding into place from the bottom up, each with an initials avatar and a score counting up, then a highlight on the winner.",
|
|
27800
|
+
category: "data",
|
|
27801
|
+
tags: ["leaderboard", "ranking", "scores", "game", "dashboard", "competition"],
|
|
27802
|
+
hold: 1800,
|
|
27803
|
+
presets: [
|
|
27804
|
+
{ name: "Dark", values: { card: [0.12, 0.14, 0.19], ink: [0.94, 0.95, 0.97] } },
|
|
27805
|
+
{ name: "Sales", values: { names: "North team,South team,East team,West team", scores: "128400,119250,98100,87600", accent: [0.13, 0.77, 0.37] } }
|
|
27806
|
+
],
|
|
27807
|
+
derived: (v) => {
|
|
27808
|
+
const names = parseLabels(v.names, BOARD.rows);
|
|
27809
|
+
const scores = parseNumberList(v.scores, BOARD.rows).map((x) => Number.isFinite(x) ? x : 0);
|
|
27810
|
+
const count = Math.min(BOARD.rows, Math.max(1, names.filter((s) => s !== "").length, scores.length));
|
|
27811
|
+
const rows = Array.from({ length: count }, (_, i) => ({ name: names[i] || `Player ${i + 1}`, score: scores[i] ?? 0 }));
|
|
27812
|
+
const order = rows.map((r, i) => [i, r.score]).sort((a, b) => b[1] - a[1] || a[0] - b[0]);
|
|
27813
|
+
const rank = new Map(order.map(([i], place) => [i, place]));
|
|
27814
|
+
const dec = decimalsOf(rows.map((r) => r.score));
|
|
27815
|
+
const crown = (count - 1) * BOARD.slide + 450;
|
|
27816
|
+
const out = { dec, crown, settle: crown + 200 };
|
|
27817
|
+
for (let i = 0; i < BOARD.rows; i++) {
|
|
27818
|
+
const row = rows[i];
|
|
27819
|
+
const place = rank.get(i) ?? i;
|
|
27820
|
+
const wait = Math.max(0, count - 1 - place) * BOARD.slide;
|
|
27821
|
+
const scoreWidth = textWidthEm(formatCount(row?.score ?? 0, { decimals: dec })) * 12;
|
|
27822
|
+
const name = fitLabel(row?.name ?? "", Math.max(20, BOARD.left + BOARD.width - 12 - scoreWidth - 8 - 72), 12, 9);
|
|
27823
|
+
const initials = (row?.name ?? "").split(/\s+/).filter(Boolean).slice(0, 2).map((w) => Array.from(w)[0]).join("").toUpperCase() || "?";
|
|
27824
|
+
out[`show${i}`] = row ? "inline" : "none";
|
|
27825
|
+
out[`y${i}`] = BOARD.top + place * BOARD.pitch;
|
|
27826
|
+
out[`at${i}`] = wait;
|
|
27827
|
+
out[`land${i}`] = wait + 300;
|
|
27828
|
+
out[`rank${i}`] = place + 1;
|
|
27829
|
+
out[`nm${i}`] = name.text;
|
|
27830
|
+
out[`ns${i}`] = name.size;
|
|
27831
|
+
out[`nl${i}`] = name.length;
|
|
27832
|
+
out[`ini${i}`] = initials;
|
|
27833
|
+
out[`is${i}`] = r114(Math.min(10, 15 / Math.max(0.6, textWidthEm(initials))));
|
|
27834
|
+
out[`sc${i}`] = row?.score ?? 0;
|
|
27835
|
+
}
|
|
27836
|
+
return out;
|
|
27837
|
+
},
|
|
27838
|
+
motion: {
|
|
27839
|
+
svg: `<svg viewBox="0 0 260 214" font-family="${SANS12}"><!-- {{names: text default='Ava Stone,Ben Ito,Cleo Park,Dev Shah,Eli Moss' label='Names (comma-separated)'}} {{scores: text default='870,980,640,820,510' label='Scores (comma-separated)'}} {{card: color default=#F1F5F9 label='Rows'}} {{ink: color default=#0F172A label='Text'}} {{accent: color default=#F59E0B label='Winner highlight'}} ${palette()} -->${Array.from(
|
|
27840
|
+
{ length: BOARD.rows },
|
|
27841
|
+
(_, i) => `<g id="leaderboardRow${i}" display="{{show${i}}}" opacity="0" transform="translate(280 {{y${i}}})"><rect x="${BOARD.left}" y="0" width="${BOARD.width}" height="${BOARD.height}" rx="8" fill="{{card}}"/><text x="26" y="19.5" text-anchor="middle" font-size="12" font-weight="700" fill="{{ink}}" fill-opacity="0.55">{{rank${i}}}</text><circle cx="52" cy="15" r="11" fill="{{c${i + 1}}}"/><text x="52" y="18.5" text-anchor="middle" font-size="{{is${i}}}" font-weight="700" fill="#FFFFFF">{{ini${i}}}</text><text x="70" y="19.5" font-size="{{ns${i}}}" textLength="{{nl${i}}}" lengthAdjust="spacingAndGlyphs" font-weight="600" fill="{{ink}}">{{nm${i}}}</text><text id="leaderboardScore${i}" x="${BOARD.left + BOARD.width - 12}" y="19.5" text-anchor="end" font-size="12" font-weight="700" fill="{{ink}}">0</text></g>`
|
|
27842
|
+
).join(
|
|
27843
|
+
""
|
|
27844
|
+
)}<rect id="leaderboardRing" x="${BOARD.left}" y="${BOARD.top}" width="${BOARD.width}" height="${BOARD.height}" rx="8" fill="none" stroke="{{accent}}" stroke-width="2" opacity="0"/><rect id="leaderboardTop" x="${BOARD.left}" y="${BOARD.top}" width="${BOARD.width}" height="${BOARD.height}" rx="8" fill="{{accent}}" fill-opacity="0.12" stroke="{{accent}}" stroke-width="2" opacity="0"/><g id="leaderboardBadge" transform="translate(26 23) scale(0)"><circle r="10" fill="{{accent}}"/><path d="M-5 -3.5 L-2.5 -0.5 L0 -5 L2.5 -0.5 L5 -3.5 L4 3.5 H-4Z" fill="#FFFFFF"/></g></svg>`,
|
|
27845
|
+
animate: [
|
|
27846
|
+
...Array.from({ length: BOARD.rows }, (_, i) => [
|
|
27847
|
+
{ id: `leaderboardRow${i}`, attrs: { transform: [`translate(280 {{y${i}}})`, `translate(0 {{y${i}}})`] }, delay: n15(`at${i}`), ms: 450, ease: "out" },
|
|
27848
|
+
{ id: `leaderboardRow${i}`, attrs: { opacity: [0, 1] }, delay: n15(`at${i}`), ms: 200 },
|
|
27849
|
+
{ id: `leaderboardScore${i}`, count: [0, n15(`sc${i}`)], decimals: n15("dec"), delay: n15(`land${i}`), ms: 800, ease: "out" }
|
|
27850
|
+
]).flat(),
|
|
27851
|
+
{ id: "leaderboardTop", attrs: { opacity: [0, 1] }, delay: n15("crown"), ms: 250 },
|
|
27852
|
+
{ id: "leaderboardRing", attrs: { opacity: [0, 0.9] }, delay: n15("crown"), ms: 0 },
|
|
27853
|
+
{ id: "leaderboardRing", attrs: { x: [BOARD.left, BOARD.left - 5], y: [BOARD.top, BOARD.top - 5], width: [BOARD.width, BOARD.width + 10], height: [BOARD.height, BOARD.height + 10], opacity: [0.9, 0] }, delay: n15("crown"), ms: 650, ease: "out" },
|
|
27854
|
+
{ id: "leaderboardBadge", attrs: { transform: ["translate(26 23) scale(0)", "translate(26 23) scale(1.25)"] }, delay: n15("crown"), ms: 200, ease: "out" },
|
|
27855
|
+
{ id: "leaderboardBadge", attrs: { transform: ["translate(26 23) scale(1.25)", "translate(26 23) scale(1)"] }, delay: n15("settle"), ms: 200, ease: "in-out" }
|
|
27856
|
+
]
|
|
27857
|
+
}
|
|
27858
|
+
};
|
|
27859
|
+
var AREA = { series: 4, points: 12, x0: 34, x1: 268, top: 40, base: 150, sweep: 2400, rise: 1100 };
|
|
27860
|
+
var AREA_DEFAULT = [
|
|
27861
|
+
[12, 18, 15, 24, 28, 34],
|
|
27862
|
+
[8, 10, 14, 12, 18, 22],
|
|
27863
|
+
[5, 6, 9, 8, 11, 14]
|
|
27864
|
+
];
|
|
27865
|
+
var areaChart = {
|
|
27866
|
+
kind: "vector",
|
|
27867
|
+
id: "area-chart",
|
|
27868
|
+
name: "Area chart",
|
|
27869
|
+
description: "A stacked area chart whose layers rise together from the baseline, then a tooltip line glides across reading the total.",
|
|
27870
|
+
category: "data",
|
|
27871
|
+
tags: ["chart", "area", "stacked", "trend", "dashboard", "report"],
|
|
27872
|
+
hold: 1400,
|
|
27873
|
+
presets: [...PALETTE_PRESETS.map((p) => ({ ...p, values: Object.fromEntries(Object.entries(p.values).slice(0, AREA.series)) })), { name: "Two series", values: { series: "5,9,7,12,15,14,18,22;3,4,6,5,8,9,8,11", labels: "Paid,Organic", points: "W1,W2,W3,W4,W5,W6,W7,W8" } }],
|
|
27874
|
+
derived: (v) => {
|
|
27875
|
+
const rows = parseSeries(v.series, AREA.series, AREA.points, AREA_DEFAULT);
|
|
27876
|
+
const points = rows[0].length;
|
|
27877
|
+
const cum = rows.map((_, s) => Array.from({ length: points }, (_2, j) => rows.slice(0, s + 1).reduce((a, r) => a + r[j], 0)));
|
|
27878
|
+
const top = Math.max(1e-9, ...cum.at(-1));
|
|
27879
|
+
const x = (j) => r114(AREA.x0 + j * (AREA.x1 - AREA.x0) / (points - 1));
|
|
27880
|
+
const y = (value) => r114(AREA.base - value / top * (AREA.base - AREA.top));
|
|
27881
|
+
const edge = (values, back) => {
|
|
27882
|
+
const pts = values.map((val, j) => `${x(j)} ${y(val)}`);
|
|
27883
|
+
return (back ? pts.reverse() : pts).join(" L");
|
|
27884
|
+
};
|
|
27885
|
+
const zero = new Array(points).fill(0);
|
|
27886
|
+
const labels = parseLabels(v.labels, AREA.series);
|
|
27887
|
+
const xLabels = parseLabels(v.points, AREA.points);
|
|
27888
|
+
const dec = decimalsOf(rows.flat());
|
|
27889
|
+
const totals = cum.at(-1);
|
|
27890
|
+
const spacing = (AREA.x1 - AREA.x0) / (points - 1);
|
|
27891
|
+
const seg = AREA.sweep / (points - 1);
|
|
27892
|
+
const tip = AREA.rise + 200;
|
|
27893
|
+
const end = tip + AREA.sweep;
|
|
27894
|
+
const tipSize = r114(Math.min(9, 34 / Math.max(0.6, ...totals.map((t) => textWidthEm(formatCount(t, { decimals: dec }))))));
|
|
27895
|
+
const out = { dec, tip, tipSize, tx0: x(0), total0: +totals[0].toFixed(dec) };
|
|
27896
|
+
for (let s = 0; s < AREA.series; s++) {
|
|
27897
|
+
const shown = s < rows.length;
|
|
27898
|
+
const below = s > 0 && shown ? cum[s - 1] : zero;
|
|
27899
|
+
const above = shown ? cum[s] : zero;
|
|
27900
|
+
out[`show${s}`] = shown ? "inline" : "none";
|
|
27901
|
+
out[`flat${s}`] = `M${edge(zero, false)} L${edge(zero, true)}Z`;
|
|
27902
|
+
out[`full${s}`] = `M${edge(above, false)} L${edge(below, true)}Z`;
|
|
27903
|
+
const lab = fitLabel(labels[s] ?? "", 50, 9, 7.5);
|
|
27904
|
+
out[`lab${s}`] = lab.text;
|
|
27905
|
+
out[`labs${s}`] = lab.size;
|
|
27906
|
+
out[`labl${s}`] = lab.length;
|
|
27907
|
+
out[`dy${s}_0`] = y(above[0]);
|
|
27908
|
+
}
|
|
27909
|
+
for (let j = 0; j < AREA.points; j++) {
|
|
27910
|
+
const shown = j < points;
|
|
27911
|
+
const label = fitLabel(xLabels[j] ?? "", spacing * 0.9, 9, 7);
|
|
27912
|
+
out[`xs${j}`] = shown ? "inline" : "none";
|
|
27913
|
+
out[`xx${j}`] = shown ? x(j) : AREA.x1;
|
|
27914
|
+
out[`xl${j}`] = label.text;
|
|
27915
|
+
out[`xz${j}`] = label.size;
|
|
27916
|
+
}
|
|
27917
|
+
for (let j = 0; j < AREA.points - 1; j++) {
|
|
27918
|
+
const real = j < points - 1;
|
|
27919
|
+
const [a, b] = real ? [j, j + 1] : [points - 1, points - 1];
|
|
27920
|
+
out[`sd${j}`] = real ? Math.round(tip + j * seg) : end;
|
|
27921
|
+
out[`sm${j}`] = real ? Math.round(seg) : 0;
|
|
27922
|
+
out[`ta${j}`] = x(a);
|
|
27923
|
+
out[`tb${j}`] = x(b);
|
|
27924
|
+
out[`va${j}`] = +totals[a].toFixed(dec);
|
|
27925
|
+
out[`vb${j}`] = +totals[b].toFixed(dec);
|
|
27926
|
+
for (let s = 0; s < AREA.series; s++) {
|
|
27927
|
+
const c = s < rows.length ? cum[s] : zero;
|
|
27928
|
+
out[`ya${s}_${j}`] = y(c[a]);
|
|
27929
|
+
out[`yb${s}_${j}`] = y(c[b]);
|
|
27930
|
+
}
|
|
27931
|
+
}
|
|
27932
|
+
return out;
|
|
27933
|
+
},
|
|
27934
|
+
motion: {
|
|
27935
|
+
svg: `<svg viewBox="0 0 300 176" font-family="${SANS12}"><!-- {{series: text default='12,18,15,24,28,34;8,10,14,12,18,22;5,6,9,8,11,14' label='Series (values, rows split by ;)'}} {{labels: text default='Web,Mobile,API' label='Series labels (comma-separated)'}} {{points: text default='Jan,Feb,Mar,Apr,May,Jun' label='Point labels (comma-separated)'}} {{ink: color default=#1E293B label='Text'}} {{grid: color default=#E2E8F0 label='Grid'}} ${palette(AREA.series)} --><g stroke="{{grid}}" stroke-width="1">${[0, 1, 2, 3].map((k) => `<line x1="${AREA.x0}" x2="${AREA.x1}" y1="${AREA.top + k * (AREA.base - AREA.top) / 3}" y2="${AREA.top + k * (AREA.base - AREA.top) / 3}"/>`).join("")}</g>${Array.from(
|
|
27936
|
+
{ length: AREA.series },
|
|
27937
|
+
(_, s) => `<path id="areaChartArea${s}" display="{{show${s}}}" d="{{flat${s}}}" fill="{{c${s + 1}}}" fill-opacity="0.78" stroke="{{c${s + 1}}}" stroke-width="1.5" stroke-linejoin="round"/>`
|
|
27938
|
+
).join("")}${Array.from(
|
|
27939
|
+
{ length: AREA.series },
|
|
27940
|
+
(_, s) => `<g display="{{show${s}}}" transform="translate(${AREA.x0 + s * 64} 0)"><rect x="0" y="5" width="8" height="8" rx="2" fill="{{c${s + 1}}}"/><text x="12" y="12.5" font-size="{{labs${s}}}" textLength="{{labl${s}}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}">{{lab${s}}}</text></g>`
|
|
27941
|
+
).join("")}<g font-size="9" fill="{{ink}}" fill-opacity="0.6" text-anchor="middle">${Array.from(
|
|
27942
|
+
{ length: AREA.points },
|
|
27943
|
+
(_, j) => `<text display="{{xs${j}}}" x="{{xx${j}}}" y="166" font-size="{{xz${j}}}">{{xl${j}}}</text>`
|
|
27944
|
+
).join("")}</g><g id="areaChartTip" opacity="0" transform="translate({{tx0}} 0)"><line x1="0" x2="0" y1="34" y2="${AREA.base}" stroke="{{ink}}" stroke-opacity="0.5" stroke-width="1.2" stroke-dasharray="3 2"/><rect x="-20" y="18" width="40" height="16" rx="4" fill="{{ink}}"/><text id="areaChartTipValue" x="0" y="29" text-anchor="middle" font-size="{{tipSize}}" font-weight="700" fill="#FFFFFF">0</text>${Array.from(
|
|
27945
|
+
{ length: AREA.series },
|
|
27946
|
+
(_, s) => `<circle id="areaChartDot${s}" display="{{show${s}}}" cx="0" cy="{{dy${s}_0}}" r="3" fill="#FFFFFF" stroke="{{c${s + 1}}}" stroke-width="2"/>`
|
|
27947
|
+
).join("")}</g></svg>`,
|
|
27948
|
+
animate: [
|
|
27949
|
+
...Array.from({ length: AREA.series }, (_, s) => ({ id: `areaChartArea${s}`, attrs: { d: [`{{flat${s}}}`, `{{full${s}}}`] }, ms: AREA.rise, ease: "out" })),
|
|
27950
|
+
{ id: "areaChartTip", attrs: { opacity: [0, 1] }, delay: n15("tip"), ms: 200 },
|
|
27951
|
+
{ id: "areaChartTipValue", count: [n15("total0"), n15("total0")], decimals: n15("dec"), delay: n15("tip"), ms: 0 },
|
|
27952
|
+
...Array.from({ length: AREA.points - 1 }, (_, j) => {
|
|
27953
|
+
const at4 = { delay: n15(`sd${j}`), ms: n15(`sm${j}`), ease: "linear" };
|
|
27954
|
+
return [
|
|
27955
|
+
{ id: "areaChartTip", attrs: { transform: [`translate({{ta${j}}} 0)`, `translate({{tb${j}}} 0)`] }, ...at4 },
|
|
27956
|
+
{ id: "areaChartTipValue", count: [n15(`va${j}`), n15(`vb${j}`)], decimals: n15("dec"), ...at4 },
|
|
27957
|
+
...Array.from({ length: AREA.series }, (_2, s) => ({ id: `areaChartDot${s}`, attrs: { cy: [n15(`ya${s}_${j}`), n15(`yb${s}_${j}`)] }, ...at4 }))
|
|
27958
|
+
];
|
|
27959
|
+
}).flat()
|
|
27960
|
+
]
|
|
27961
|
+
}
|
|
27962
|
+
};
|
|
27963
|
+
var KPI_DEFAULT = [3, 4, 3.5, 5, 4.8, 6, 7.2];
|
|
27964
|
+
var kpiTile = {
|
|
27965
|
+
kind: "vector",
|
|
27966
|
+
id: "kpi-tile",
|
|
27967
|
+
name: "KPI tile",
|
|
27968
|
+
description: "A dashboard tile: the headline figure counts up, a delta arrow shows the change in green or red by its sign, and a mini sparkline draws in.",
|
|
27969
|
+
category: "data",
|
|
27970
|
+
tags: ["kpi", "dashboard", "metric", "card", "sparkline", "report"],
|
|
27971
|
+
hold: 1600,
|
|
27972
|
+
presets: [
|
|
27973
|
+
{ name: "Dark", values: { card: [0.09, 0.11, 0.15], ink: [0.95, 0.96, 0.97] } },
|
|
27974
|
+
{ name: "Churn", values: { label: "Churn rate", value: 2.4, prefix: "", suffix: "%", delta: -0.6, trend: "3.4,3.3,3.1,3.0,2.8,2.6,2.4", caption: "vs last quarter" } }
|
|
27975
|
+
],
|
|
27976
|
+
derived: (v) => {
|
|
27977
|
+
const value = Number(v.value) || 0;
|
|
27978
|
+
const dec = Math.min(3, Math.max(0, Math.round(Number(v.decimals) || 0)));
|
|
27979
|
+
const pre = formatSafe(v.prefix);
|
|
27980
|
+
const suf = formatSafe(v.suffix);
|
|
27981
|
+
const big = fit7(`${pre}${formatCount(value, { decimals: dec })}${suf}`, 204, 34);
|
|
27982
|
+
const delta = Number(v.delta) || 0;
|
|
27983
|
+
const up = delta >= 0;
|
|
27984
|
+
const trend = series(v.trend, 24, KPI_DEFAULT);
|
|
27985
|
+
const { line, area, end } = linePath(trend, 148, 222, 90, 120);
|
|
27986
|
+
const label = fitLabel(v.label, 204, 12, 10);
|
|
27987
|
+
const caption = fitLabel(v.caption, 120, 10, 8);
|
|
27988
|
+
return {
|
|
27989
|
+
pre,
|
|
27990
|
+
suf,
|
|
27991
|
+
dec,
|
|
27992
|
+
value: +value.toFixed(dec),
|
|
27993
|
+
size: big.size,
|
|
27994
|
+
delta: +Math.abs(delta).toFixed(1),
|
|
27995
|
+
deltaFormat: up ? "+{n}%" : "\u2212{n}%",
|
|
27996
|
+
deltaColor: paint(up ? v.up : v.down),
|
|
27997
|
+
arrow: up ? "M24 92 L30 100 H18Z" : "M24 101 L30 93 H18Z",
|
|
27998
|
+
line,
|
|
27999
|
+
area,
|
|
28000
|
+
endX: end[0],
|
|
28001
|
+
endY: end[1],
|
|
28002
|
+
labelText: label.text,
|
|
28003
|
+
captionText: caption.text,
|
|
28004
|
+
labelSize: label.size,
|
|
28005
|
+
labelLength: label.length,
|
|
28006
|
+
captionSize: caption.size,
|
|
28007
|
+
captionLength: caption.length
|
|
28008
|
+
};
|
|
28009
|
+
},
|
|
28010
|
+
motion: {
|
|
28011
|
+
svg: `<svg viewBox="0 0 240 140" font-family="${SANS12}"><!-- {{label: text default='Monthly revenue' label='Label'}} {{value: float min=0 max=1000000 default=48.2 step=0.1 label='Value'}} {{decimals: int min=0 max=3 default=1 label='Decimals'}} {{prefix: text default='$' label='Prefix'}} {{suffix: text default='k' label='Suffix'}} {{delta: float min=-100 max=100 default=12.4 step=0.1 label='Change (%)'}} {{caption: text default='vs last month' label='Caption'}} {{trend: text default='3,4,3.5,5,4.8,6,7.2' label='Trend (comma-separated)'}} {{card: color default=#FFFFFF label='Card'}} {{ink: color default=#0F172A label='Text'}} {{accent: color default=#6366F1 label='Sparkline'}} {{up: color default=#16A34A label='Rise'}} {{down: color default=#DC2626 label='Fall'}} --><defs><linearGradient id="kpiTileFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="{{accent}}" stop-opacity="0.28"/><stop offset="1" stop-color="{{accent}}" stop-opacity="0"/></linearGradient></defs><rect x="2" y="2" width="236" height="136" rx="14" fill="{{card}}" stroke="{{ink}}" stroke-opacity="0.1"/><text x="18" y="30" font-size="{{labelSize}}" textLength="{{labelLength}}" lengthAdjust="spacingAndGlyphs" font-weight="600" fill="{{ink}}" fill-opacity="0.6">{{labelText}}</text><text id="kpiTileValue" x="18" y="72" font-size="{{size}}" font-weight="800" fill="{{ink}}">0</text><g id="kpiTileDelta" opacity="0"><path d="{{arrow}}" fill="{{deltaColor}}"/><text id="kpiTileDeltaValue" x="34" y="101" font-size="12" font-weight="700" fill="{{deltaColor}}">0%</text></g><text id="kpiTileCaption" x="18" y="124" font-size="{{captionSize}}" textLength="{{captionLength}}" lengthAdjust="spacingAndGlyphs" fill="{{ink}}" fill-opacity="0.55" opacity="0">{{captionText}}</text><path id="kpiTileArea" d="{{area}}" fill="url(#kpiTileFill)" opacity="0"/><path id="kpiTileLine" d="{{line}}" fill="none" stroke="{{accent}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle id="kpiTileEnd" cx="{{endX}}" cy="{{endY}}" r="0" fill="{{accent}}"/></svg>`,
|
|
28012
|
+
animate: [
|
|
28013
|
+
{ id: "kpiTileValue", count: [0, n15("value")], decimals: n15("dec"), format: "{{pre}}{n}{{suf}}", ms: 1400, ease: "out" },
|
|
28014
|
+
{ id: "kpiTileDelta", attrs: { opacity: [0, 1], transform: ["translate(0 6)", "translate(0 0)"] }, delay: 500, ms: 400, ease: "out" },
|
|
28015
|
+
{ id: "kpiTileDeltaValue", count: [0, n15("delta")], decimals: 1, format: "{{deltaFormat}}", delay: 500, ms: 900, ease: "out" },
|
|
28016
|
+
{ id: "kpiTileCaption", attrs: { opacity: [0, 1] }, delay: 800, ms: 400 },
|
|
28017
|
+
{ id: "kpiTileLine", draw: true, delay: 300, ms: 1100, ease: "in-out" },
|
|
28018
|
+
{ id: "kpiTileArea", attrs: { opacity: [0, 1] }, delay: 900, ms: 500 },
|
|
28019
|
+
{ id: "kpiTileEnd", attrs: { r: [0, 4.5] }, delay: 1400, ms: 180, ease: "out" },
|
|
28020
|
+
{ id: "kpiTileEnd", attrs: { r: [4.5, 3] }, delay: 1580, ms: 200, ease: "in-out" }
|
|
28021
|
+
]
|
|
28022
|
+
}
|
|
28023
|
+
};
|
|
28024
|
+
var dataEffects = [donutChart, pieChart, gauge, barRace, sparkline, leaderboard, areaChart, kpiTile];
|
|
28025
|
+
|
|
28026
|
+
// src/catalog/icons.ts
|
|
28027
|
+
var r115 = (v) => Math.round(v * 10) / 10;
|
|
28028
|
+
var r213 = (v) => Math.round(v * 100) / 100;
|
|
28029
|
+
var n16 = (name) => `{{${name}}}`;
|
|
28030
|
+
var SANS13 = `system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif`;
|
|
28031
|
+
var about6 = (cx, cy, s) => `translate(${cx} ${cy}) scale(${r213(s)}) translate(${-cx} ${-cy})`;
|
|
28032
|
+
function cycle(id, from, to, phase, period, ease = "linear") {
|
|
28033
|
+
const at4 = (u) => Object.fromEntries(Object.keys(from).map((k) => [k, lerpAttr(from[k], to[k], u)]));
|
|
28034
|
+
const mid = at4(phase);
|
|
28035
|
+
const pairs = (a, b) => Object.fromEntries(Object.keys(from).map((k) => [k, [a[k], b[k]]]));
|
|
28036
|
+
const first = Math.round((1 - phase) * period);
|
|
28037
|
+
const out = [{ id, attrs: pairs(mid, to), ms: first, ease }];
|
|
28038
|
+
if (phase > 0) out.push({ id, attrs: pairs(from, mid), delay: first, ms: period - first, ease });
|
|
28039
|
+
return out;
|
|
28040
|
+
}
|
|
28041
|
+
var swing = (id, attr, a, b, period, delay = 0) => [
|
|
28042
|
+
{ id, attrs: { [attr]: [a, b] }, delay, ms: period / 2, ease: "in-out" },
|
|
28043
|
+
{ id, attrs: { [attr]: [b, a] }, delay: delay + period / 2, ms: period / 2, ease: "in-out" }
|
|
28044
|
+
];
|
|
28045
|
+
var picks = (name, count) => (v) => {
|
|
28046
|
+
const pick = Math.min(count - 1, Math.max(0, Math.round(Number(v[name]) || 0)));
|
|
28047
|
+
return Object.fromEntries(Array.from({ length: count }, (_, k) => [`show${k}`, k === pick ? "inline" : "none"]));
|
|
28048
|
+
};
|
|
28049
|
+
var WEATHER_MS = 2400;
|
|
28050
|
+
var CLOUD = "M-22 14 H22 A12 12 0 0 0 20 -9 A17 17 0 0 0 -12 -10 A12 12 0 0 0 -22 14Z";
|
|
28051
|
+
var DROP = "M0 -5 C2 -1 3 1 3 3 A3 3 0 0 1 -3 3 C-3 1 -2 -1 0 -5Z";
|
|
28052
|
+
var FLAKE = "M0 -4 V4 M-3.5 -2 L3.5 2 M-3.5 2 L3.5 -2";
|
|
28053
|
+
var sunRays = (cx, cy, from, to) => Array.from({ length: 8 }, (_, k) => {
|
|
28054
|
+
const a = k * Math.PI / 4;
|
|
28055
|
+
return `M${r115(cx + from * Math.cos(a))} ${r115(cy + from * Math.sin(a))} L${r115(cx + to * Math.cos(a))} ${r115(cy + to * Math.sin(a))}`;
|
|
28056
|
+
}).join(" ");
|
|
28057
|
+
var cloud2 = (id, x, y, s, fill, extra = "") => `<g id="${id}" transform="translate(${x} ${y}) scale(${s})"><path d="${CLOUD}" fill="${fill}"${extra}/></g>`;
|
|
28058
|
+
var falling = (prefix, shape, xs, top, bottom, spin = 0) => ({
|
|
28059
|
+
markup: xs.map((x, i) => `<g id="${prefix}${i}" transform="translate(${x} ${top}) rotate(0)" opacity="1">${shape(i)}</g>`).join(""),
|
|
28060
|
+
animate: xs.flatMap(
|
|
28061
|
+
(x, i) => cycle(`${prefix}${i}`, { transform: `translate(${x} ${top}) rotate(0)`, opacity: 1 }, { transform: `translate(${x + spin / 30} ${bottom}) rotate(${spin})`, opacity: 0 }, (i * 0.37 + 0.15) % 1, WEATHER_MS)
|
|
28062
|
+
)
|
|
28063
|
+
});
|
|
28064
|
+
var rain = falling("weatherIconsDrop", () => `<path d="${DROP}" fill="{{accent}}"/>`, [36, 50, 64, 43, 57], 50, 86);
|
|
28065
|
+
var storm = falling("weatherIconsStormDrop", () => `<path d="${DROP}" fill="{{accent}}"/>`, [34, 66], 50, 86);
|
|
28066
|
+
var snow = falling("weatherIconsFlake", () => `<path d="${FLAKE}" stroke="{{accent}}" stroke-width="1.8" stroke-linecap="round"/>`, [36, 50, 64, 43, 58], 50, 88, 120);
|
|
28067
|
+
var WIND = ["M14 38 H58 C68 38 70 26 62 24 C56 23 54 29 58 32", "M10 52 H76 C86 52 88 64 80 66 C74 67 72 61 76 58", "M20 66 H50"];
|
|
28068
|
+
var weatherIcons = {
|
|
28069
|
+
kind: "vector",
|
|
28070
|
+
id: "weather-icons",
|
|
28071
|
+
name: "Weather icons",
|
|
28072
|
+
description: "An animated weather icon: sun with turning rays, partly cloudy, drifting clouds, falling rain, a flashing thunderbolt, snow or gusting wind.",
|
|
28073
|
+
category: "icon",
|
|
28074
|
+
tags: ["weather", "forecast", "sun", "rain", "snow", "loop"],
|
|
28075
|
+
hold: 0,
|
|
28076
|
+
presets: [
|
|
28077
|
+
{ name: "Rain", values: { kind: 3 } },
|
|
28078
|
+
{ name: "Snow", values: { kind: 5, cloud: [0.8, 0.85, 0.92], accent: [0.55, 0.75, 0.95] } },
|
|
28079
|
+
{ name: "Thunder", values: { kind: 4, cloud: [0.39, 0.45, 0.55] } }
|
|
28080
|
+
],
|
|
28081
|
+
derived: picks("kind", 7),
|
|
28082
|
+
motion: {
|
|
28083
|
+
svg: `<svg viewBox="0 0 100 100"><!-- {{kind: int min=0 max=6 default=0 label='Weather (sun, partly cloudy, cloudy, rain, thunder, snow, wind)'}} {{sun: color default=#FBBF24 label='Sun'}} {{cloud: color default=#94A3B8 label='Cloud'}} {{accent: color default=#38BDF8 label='Rain, snow and wind'}} {{bolt: color default=#FACC15 label='Lightning'}} --><g display="{{show0}}"><g id="weatherIconsRays" transform="rotate(0 50 50)"><path d="${sunRays(50, 50, 26, 36)}" stroke="{{sun}}" stroke-width="5" stroke-linecap="round"/></g><circle id="weatherIconsSun" cx="50" cy="50" r="17" fill="{{sun}}"/></g><g display="{{show1}}"><g id="weatherIconsPartRays" transform="rotate(0 38 38)"><path d="${sunRays(38, 38, 19, 27)}" stroke="{{sun}}" stroke-width="4" stroke-linecap="round"/></g><circle cx="38" cy="38" r="13" fill="{{sun}}"/>${cloud2("weatherIconsPartCloud", 56, 60, 1, "{{cloud}}")}</g><g display="{{show2}}">${cloud2("weatherIconsBackCloud", 62, 40, 0.75, "{{cloud}}", ` opacity="0.55"`)}${cloud2("weatherIconsFrontCloud", 46, 56, 1.05, "{{cloud}}")}</g><g display="{{show3}}">${rain.markup}${cloud2("weatherIconsRainCloud", 50, 38, 1.1, "{{cloud}}")}</g><g display="{{show4}}">${storm.markup}<g transform="translate(50 36) scale(1.1)"><path d="${CLOUD}" fill="{{cloud}}"/><path d="${CLOUD}" fill="#000000" opacity="0.22"/></g><path id="weatherIconsBolt" d="M54 50 L42 70 H51 L45 90 L63 64 H54 L60 50Z" fill="{{bolt}}" opacity="0"/></g><g display="{{show5}}">${snow.markup}${cloud2("weatherIconsSnowCloud", 50, 38, 1.1, "{{cloud}}")}</g><g display="{{show6}}" fill="none" stroke="{{accent}}" stroke-width="5" stroke-linecap="round">${WIND.map((d, k) => `<path id="weatherIconsWind${k}" d="${d}"/>`).join("")}</g></svg>`,
|
|
28084
|
+
animate: [
|
|
28085
|
+
...cycle("weatherIconsRays", { transform: "rotate(0 50 50)" }, { transform: "rotate(45 50 50)" }, 0, WEATHER_MS),
|
|
28086
|
+
...swing("weatherIconsSun", "r", 17, 18.5, WEATHER_MS),
|
|
28087
|
+
...cycle("weatherIconsPartRays", { transform: "rotate(0 38 38)" }, { transform: "rotate(45 38 38)" }, 0, WEATHER_MS),
|
|
28088
|
+
...swing("weatherIconsPartCloud", "transform", "translate(54 60) scale(1)", "translate(60 60) scale(1)", WEATHER_MS),
|
|
28089
|
+
...swing("weatherIconsBackCloud", "transform", "translate(66 40) scale(0.75)", "translate(58 40) scale(0.75)", WEATHER_MS),
|
|
28090
|
+
...swing("weatherIconsFrontCloud", "transform", "translate(43 56) scale(1.05)", "translate(50 56) scale(1.05)", WEATHER_MS),
|
|
28091
|
+
...swing("weatherIconsRainCloud", "transform", "translate(49 38) scale(1.1)", "translate(51 38) scale(1.1)", WEATHER_MS),
|
|
28092
|
+
...swing("weatherIconsSnowCloud", "transform", "translate(49 38) scale(1.1)", "translate(51 38) scale(1.1)", WEATHER_MS),
|
|
28093
|
+
...rain.animate,
|
|
28094
|
+
...storm.animate,
|
|
28095
|
+
...snow.animate,
|
|
28096
|
+
// Two quick flashes, then dark until the loop comes round.
|
|
28097
|
+
{ id: "weatherIconsBolt", attrs: { opacity: [0, 1] }, delay: 700, ms: 60 },
|
|
28098
|
+
{ id: "weatherIconsBolt", attrs: { opacity: [1, 0.2] }, delay: 760, ms: 90 },
|
|
28099
|
+
{ id: "weatherIconsBolt", attrs: { opacity: [0.2, 1] }, delay: 850, ms: 60 },
|
|
28100
|
+
{ id: "weatherIconsBolt", attrs: { opacity: [1, 0] }, delay: 1100, ms: 400 },
|
|
28101
|
+
// Gusts draw on in turn and fade; undrawn again by the time the loop restarts.
|
|
28102
|
+
...WIND.flatMap((_, k) => [
|
|
28103
|
+
{ id: `weatherIconsWind${k}`, draw: true, delay: k * 220, ms: 900, ease: "in-out" },
|
|
28104
|
+
{ id: `weatherIconsWind${k}`, attrs: { opacity: [1, 0] }, delay: 1300 + k * 220, ms: 450 }
|
|
28105
|
+
])
|
|
28106
|
+
]
|
|
28107
|
+
}
|
|
28108
|
+
};
|
|
28109
|
+
var BELL_PIVOT = "50 16";
|
|
28110
|
+
var bellTurn = (deg) => `rotate(${deg} ${BELL_PIVOT})`;
|
|
28111
|
+
var BELL_SWING = [0, 18, -15, 11, -7, 3, 0];
|
|
28112
|
+
var CLAPPER_SWING = [0, -12, 14, -9, 6, -2, 0];
|
|
28113
|
+
var BADGE = [72, 26];
|
|
28114
|
+
var notificationBell = {
|
|
28115
|
+
kind: "vector",
|
|
28116
|
+
id: "notification-bell",
|
|
28117
|
+
name: "Notification bell",
|
|
28118
|
+
description: "A bell ringing with a damped swing and its clapper lagging behind, as a badge pops on with the unread count.",
|
|
28119
|
+
category: "icon",
|
|
28120
|
+
tags: ["notification", "bell", "alert", "badge", "ui", "loop"],
|
|
28121
|
+
hold: 300,
|
|
28122
|
+
presets: [
|
|
28123
|
+
{ name: "Dark", values: { bell: [0.9, 0.91, 0.94], ring: [0.06, 0.07, 0.1] } },
|
|
28124
|
+
{ name: "Many", values: { count: 128 } }
|
|
28125
|
+
],
|
|
28126
|
+
derived: (v) => {
|
|
28127
|
+
const count = Math.max(0, Math.round(Number(v.count) || 0));
|
|
28128
|
+
const many = count > 99;
|
|
28129
|
+
const digits = many ? 3 : String(count).length;
|
|
28130
|
+
return {
|
|
28131
|
+
from: many ? 99 : Math.max(0, count - 1),
|
|
28132
|
+
to: many ? 99 : count,
|
|
28133
|
+
format: many ? "{n}+" : "{n}",
|
|
28134
|
+
size: digits >= 3 ? 9 : digits === 2 ? 11 : 13,
|
|
28135
|
+
badgeR: digits >= 3 ? 15 : 13
|
|
28136
|
+
};
|
|
28137
|
+
},
|
|
28138
|
+
motion: {
|
|
28139
|
+
svg: `<svg viewBox="0 0 100 100" font-family="${SANS13}"><!-- {{count: int min=0 max=999 default=3 label='Count'}} {{bell: color default=#334155 label='Bell'}} {{badge: color default=#EF4444 label='Badge'}} {{ring: color default=#FFFFFF label='Badge outline'}} --><g fill="none" stroke="{{bell}}" stroke-width="3.5" stroke-linecap="round"><path id="notificationBellWaveL" d="M20 30 C14 38 14 50 20 58" opacity="0"/><path id="notificationBellWaveR" d="M80 30 C86 38 86 50 80 58" opacity="0"/></g><g id="notificationBellClapper" transform="${bellTurn(0)}"><circle cx="50" cy="80" r="7" fill="{{bell}}"/></g><g id="notificationBellBody" transform="${bellTurn(0)}"><circle cx="50" cy="15" r="5" fill="{{bell}}"/><path d="M50 18 C36 18 30 30 30 44 V58 L23 68 V72 H77 V68 L70 58 V44 C70 30 64 18 50 18Z" fill="{{bell}}"/></g><g id="notificationBellBadge" transform="${about6(BADGE[0], BADGE[1], 0)}"><circle cx="${BADGE[0]}" cy="${BADGE[1]}" r="{{badgeR}}" fill="{{badge}}" stroke="{{ring}}" stroke-width="3"/><text id="notificationBellCount" x="${BADGE[0]}" y="${BADGE[1] + 4.5}" text-anchor="middle" font-size="{{size}}" font-weight="800" fill="#FFFFFF">0</text></g></svg>`,
|
|
28140
|
+
animate: [
|
|
28141
|
+
...BELL_SWING.slice(1).map((deg, k) => ({ id: "notificationBellBody", attrs: { transform: [bellTurn(BELL_SWING[k]), bellTurn(deg)] }, delay: k * 120, ms: 120, ease: "in-out" })),
|
|
28142
|
+
...CLAPPER_SWING.slice(1).map((deg, k) => ({ id: "notificationBellClapper", attrs: { transform: [bellTurn(CLAPPER_SWING[k]), bellTurn(deg)] }, delay: 50 + k * 120, ms: 120, ease: "in-out" })),
|
|
28143
|
+
...["L", "R"].flatMap((side, k) => [
|
|
28144
|
+
{ id: `notificationBellWave${side}`, attrs: { opacity: [0, 1] }, delay: 80 + k * 120, ms: 120 },
|
|
28145
|
+
{ id: `notificationBellWave${side}`, attrs: { opacity: [1, 0] }, delay: 420 + k * 120, ms: 260 }
|
|
28146
|
+
]),
|
|
28147
|
+
{ id: "notificationBellBadge", attrs: { transform: [about6(BADGE[0], BADGE[1], 0), about6(BADGE[0], BADGE[1], 1.25)] }, delay: 300, ms: 180, ease: "out" },
|
|
28148
|
+
{ id: "notificationBellBadge", attrs: { transform: [about6(BADGE[0], BADGE[1], 1.25), about6(BADGE[0], BADGE[1], 1)] }, delay: 480, ms: 200, ease: "in-out" },
|
|
28149
|
+
{ id: "notificationBellCount", count: [n16("from"), n16("to")], format: "{{format}}", delay: 300, ms: 350, ease: "out" },
|
|
28150
|
+
// The badge goes before the loop starts over, so it can pop on again.
|
|
28151
|
+
{ id: "notificationBellBadge", attrs: { transform: [about6(BADGE[0], BADGE[1], 1), about6(BADGE[0], BADGE[1], 0)] }, delay: 2400, ms: 220, ease: "in-out" }
|
|
28152
|
+
]
|
|
28153
|
+
}
|
|
28154
|
+
};
|
|
28155
|
+
var ARROW_MS = 1200;
|
|
28156
|
+
var refreshArc = (() => {
|
|
28157
|
+
const r = 26;
|
|
28158
|
+
const end = -40 * Math.PI / 180;
|
|
28159
|
+
const [ex, ey] = [50 + r * Math.cos(end), 50 + r * Math.sin(end)];
|
|
28160
|
+
const [tx, ty] = [-Math.sin(end), Math.cos(end)];
|
|
28161
|
+
const [nx, ny] = [Math.cos(end), Math.sin(end)];
|
|
28162
|
+
const tip = `${r115(ex + tx * 7)} ${r115(ey + ty * 7)}`;
|
|
28163
|
+
const head = `M${r115(ex + nx * 7)} ${r115(ey + ny * 7)} L${tip} L${r115(ex - nx * 7)} ${r115(ey - ny * 7)}Z`;
|
|
28164
|
+
return { arc: `M24 50 A${r} ${r} 0 0 1 ${r115(ex)} ${r115(ey)}`, head };
|
|
28165
|
+
})();
|
|
28166
|
+
var animatedArrows = {
|
|
28167
|
+
kind: "vector",
|
|
28168
|
+
id: "animated-arrows",
|
|
28169
|
+
name: "Animated arrows",
|
|
28170
|
+
description: "An arrow icon set: an arrow nudging right, chevrons cascading down, refresh arrows turning, or an external-link arrow leaving its box.",
|
|
28171
|
+
category: "icon",
|
|
28172
|
+
tags: ["arrow", "chevron", "refresh", "link", "ui", "loop"],
|
|
28173
|
+
// Every arrow's motion is over by 1000 ms: each rests the same before it repeats.
|
|
28174
|
+
hold: ARROW_MS - 1e3,
|
|
28175
|
+
presets: [
|
|
28176
|
+
{ name: "Scroll hint", values: { kind: 1, color: [0.39, 0.4, 0.95] } },
|
|
28177
|
+
{ name: "Refresh", values: { kind: 2 } }
|
|
28178
|
+
],
|
|
28179
|
+
derived: picks("kind", 4),
|
|
28180
|
+
motion: {
|
|
28181
|
+
svg: `<svg viewBox="0 0 100 100"><!-- {{kind: int min=0 max=3 default=0 label='Arrow (right, chevrons, refresh, external)'}} {{color: color default=#0F172A label='Colour'}} {{width: float min=2 max=12 default=7 step=0.5 label='Stroke width'}} --><g fill="none" stroke="{{color}}" stroke-width="{{width}}" stroke-linecap="round" stroke-linejoin="round"><g display="{{show0}}"><g id="animatedArrowsRight" transform="translate(0 0)"><path d="M20 50 H76 M56 30 L76 50 L56 70"/></g></g><g display="{{show1}}">${[0, 1, 2].map((k) => `<path id="animatedArrowsChevron${k}" d="M32 ${24 + k * 18} L50 ${38 + k * 18} L68 ${24 + k * 18}" opacity="0.2"/>`).join("")}</g><g display="{{show2}}"><g id="animatedArrowsRefresh" transform="rotate(0 50 50)">${[0, 180].map((deg) => `<g transform="rotate(${deg} 50 50)"><path d="${refreshArc.arc}"/><path d="${refreshArc.head}" fill="{{color}}" stroke-width="2"/></g>`).join("")}</g></g><g display="{{show3}}"><path d="M54 22 H32 C26 22 22 26 22 32 V68 C22 74 26 78 32 78 H68 C74 78 78 74 78 68 V46"/><g id="animatedArrowsExternal" transform="translate(0 0)"><path d="M46 54 L76 24 M56 24 H76 V44"/></g></g></g></svg>`,
|
|
28182
|
+
animate: [
|
|
28183
|
+
{ id: "animatedArrowsRight", attrs: { transform: ["translate(0 0)", "translate(9 0)"] }, ms: 380, ease: "out" },
|
|
28184
|
+
{ id: "animatedArrowsRight", attrs: { transform: ["translate(9 0)", "translate(0 0)"] }, delay: 380, ms: 520, ease: "in-out" },
|
|
28185
|
+
...[0, 1, 2].flatMap((k) => [
|
|
28186
|
+
{ id: `animatedArrowsChevron${k}`, attrs: { opacity: [0.2, 1], transform: ["translate(0 0)", "translate(0 3)"] }, delay: k * 180, ms: 260, ease: "out" },
|
|
28187
|
+
{ id: `animatedArrowsChevron${k}`, attrs: { opacity: [1, 0.2], transform: ["translate(0 3)", "translate(0 0)"] }, delay: k * 180 + 300, ms: 420, ease: "in-out" }
|
|
28188
|
+
]),
|
|
28189
|
+
// Two arrows round the circle: half a turn brings it back to how it started.
|
|
28190
|
+
{ id: "animatedArrowsRefresh", attrs: { transform: ["rotate(0 50 50)", "rotate(180 50 50)"] }, ms: 1e3, ease: "in-out" },
|
|
28191
|
+
{ id: "animatedArrowsExternal", attrs: { transform: ["translate(0 0)", "translate(10 -10)"], opacity: [1, 0] }, delay: 100, ms: 360, ease: "in-out" },
|
|
28192
|
+
{ id: "animatedArrowsExternal", attrs: { transform: ["translate(-10 10)", "translate(0 0)"], opacity: [0, 1] }, delay: 520, ms: 420, ease: "out" }
|
|
28193
|
+
]
|
|
28194
|
+
}
|
|
28195
|
+
};
|
|
28196
|
+
var EMOJI_Y = 40;
|
|
28197
|
+
var emojiX = (i) => 30 + i * 44;
|
|
28198
|
+
var emojiPose = (i, dy, s) => `translate(${emojiX(i)} ${EMOJI_Y}) translate(0 ${dy}) scale(${s})`;
|
|
28199
|
+
var FACES = [
|
|
28200
|
+
// Like: a thumb up on a blue disc.
|
|
28201
|
+
`<circle r="17" fill="{{like}}"/><path d="M-8 -1 H-4 V9 H-8Z M-2 -1 L2 -9 C3.5 -10.5 6.5 -9 5.8 -6.3 L4.8 -2.5 H8.5 C10.4 -2.5 11.4 -0.6 10.8 1.2 L8.8 7.5 C8.3 9 7.2 9.6 5.8 9.6 H-2Z" fill="#FFFFFF"/>`,
|
|
28202
|
+
// Love: a heart on a red disc.
|
|
28203
|
+
`<circle r="17" fill="{{love}}"/><path d="M0 9 C0 9 -9.5 3 -9.5 -2.8 C-9.5 -6 -7.3 -8.2 -4.6 -8.2 C-2.6 -8.2 -0.9 -7.1 0 -5.5 C0.9 -7.1 2.6 -8.2 4.6 -8.2 C7.3 -8.2 9.5 -6 9.5 -2.8 C9.5 3 0 9 0 9Z" fill="#FFFFFF"/>`,
|
|
28204
|
+
// Haha: shut eyes and a wide open laugh.
|
|
28205
|
+
`<circle r="17" fill="{{face}}"/><path d="M-10 -6 L-5 -3.5 L-10 -1 M10 -6 L5 -3.5 L10 -1" fill="none" stroke="{{features}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M-9 2 H9 C9 8.5 5 12 0 12 C-5 12 -9 8.5 -9 2Z" fill="{{features}}"/>`,
|
|
28206
|
+
// Wow: raised brows, round eyes and mouth.
|
|
28207
|
+
`<circle r="17" fill="{{face}}"/><path d="M-9 -9 Q-6 -11.5 -3 -9.5 M9 -9 Q6 -11.5 3 -9.5" fill="none" stroke="{{features}}" stroke-width="1.8" stroke-linecap="round"/><ellipse cx="-5.5" cy="-3.5" rx="2.2" ry="3" fill="{{features}}"/><ellipse cx="5.5" cy="-3.5" rx="2.2" ry="3" fill="{{features}}"/><ellipse cx="0" cy="7.5" rx="4" ry="5" fill="{{features}}"/>`,
|
|
28208
|
+
// Sad: a frown and a tear.
|
|
28209
|
+
`<circle r="17" fill="{{face}}"/><circle cx="-5.5" cy="-3" r="2.2" fill="{{features}}"/><circle cx="5.5" cy="-3" r="2.2" fill="{{features}}"/><path d="M-6 9.5 Q0 3.5 6 9.5" fill="none" stroke="{{features}}" stroke-width="2" stroke-linecap="round"/><path d="M-6 1 C-4.5 3.5 -4 4.8 -4 5.8 A2 2 0 0 1 -8 5.8 C-8 4.8 -7.5 3.5 -6 1Z" fill="#38BDF8"/>`,
|
|
28210
|
+
// Angry: a red-orange face with knitted brows.
|
|
28211
|
+
`<circle r="17" fill="{{angry}}"/><path d="M-10 -8 L-3 -4.5 M10 -8 L3 -4.5" stroke="{{features}}" stroke-width="2.2" stroke-linecap="round"/><circle cx="-5" cy="-1" r="2" fill="{{features}}"/><circle cx="5" cy="-1" r="2" fill="{{features}}"/><path d="M-6 10 Q0 5.5 6 10" fill="none" stroke="{{features}}" stroke-width="2" stroke-linecap="round"/>`
|
|
28212
|
+
];
|
|
28213
|
+
var emojiReactions = {
|
|
28214
|
+
kind: "vector",
|
|
28215
|
+
id: "emoji-reactions",
|
|
28216
|
+
name: "Emoji reactions",
|
|
28217
|
+
description: "A reaction picker: on hover the bar opens and like, love, haha, wow, sad and angry faces bounce up one after another, drawn as vectors.",
|
|
28218
|
+
category: "icon",
|
|
28219
|
+
tags: ["interactive", "hover", "emoji", "reactions", "social", "ui"],
|
|
28220
|
+
trigger: "hover",
|
|
28221
|
+
hold: 600,
|
|
28222
|
+
presets: [{ name: "Dark", values: { pill: [0.13, 0.14, 0.19], button: [0.6, 0.63, 0.7] } }],
|
|
28223
|
+
motion: {
|
|
28224
|
+
svg: `<svg viewBox="0 0 280 80"><!-- {{pill: color default=#FFFFFF label='Bar'}} {{button: color default=#64748B label='Button icon'}} {{face: color default=#FBBF24 label='Faces'}} {{features: color default=#5B3A00 label='Eyes and mouths'}} {{like: color default=#2563EB label='Like'}} {{love: color default=#F43F5E label='Love'}} {{angry: color default=#F97316 label='Angry'}} --><rect id="emojiReactionsShadow" x="8" y="17" width="52" height="52" rx="26" fill="#000000" opacity="0.1"/><rect id="emojiReactionsPill" x="6" y="14" width="52" height="52" rx="26" fill="{{pill}}" stroke="#000000" stroke-opacity="0.08"/><g id="emojiReactionsButton" fill="none" stroke="{{button}}" stroke-width="2.2" stroke-linecap="round"><circle cx="30" cy="40" r="11"/><circle cx="26" cy="37" r="0.8" fill="{{button}}"/><circle cx="34" cy="37" r="0.8" fill="{{button}}"/><path d="M25.5 43.5 Q30 47 34.5 43.5"/></g>${FACES.map(
|
|
28225
|
+
(face, i) => `<g id="emojiReactionsFace${i}" transform="${emojiPose(i, 14, 0)}">${face}</g>`
|
|
28226
|
+
).join("")}</svg>`,
|
|
28227
|
+
animate: [
|
|
28228
|
+
{ id: "emojiReactionsPill", attrs: { width: [52, 268] }, ms: 280, ease: "out" },
|
|
28229
|
+
{ id: "emojiReactionsShadow", attrs: { width: [52, 268] }, ms: 280, ease: "out" },
|
|
28230
|
+
{ id: "emojiReactionsButton", attrs: { opacity: [1, 0] }, ms: 120 },
|
|
28231
|
+
...FACES.flatMap((_, i) => [
|
|
28232
|
+
{ id: `emojiReactionsFace${i}`, attrs: { transform: [emojiPose(i, 14, 0), emojiPose(i, -9, 1.15)] }, delay: 90 + i * 70, ms: 200, ease: "out" },
|
|
28233
|
+
{ id: `emojiReactionsFace${i}`, attrs: { transform: [emojiPose(i, -9, 1.15), emojiPose(i, 0, 1)] }, delay: 290 + i * 70, ms: 200, ease: "in-out" }
|
|
28234
|
+
])
|
|
28235
|
+
]
|
|
28236
|
+
}
|
|
28237
|
+
};
|
|
28238
|
+
var STARS3 = 5;
|
|
28239
|
+
var STAR = { y: 32, r: 16, inner: 7, pitch: 38, first: 22 };
|
|
28240
|
+
var starX = (i) => STAR.first + i * STAR.pitch;
|
|
28241
|
+
var starPath2 = (cx) => `M${Array.from({ length: 10 }, (_, k) => {
|
|
28242
|
+
const a = k * Math.PI / 5 - Math.PI / 2;
|
|
28243
|
+
const r = k % 2 ? STAR.inner : STAR.r;
|
|
28244
|
+
return `${r115(cx + r * Math.cos(a))} ${r115(STAR.y + 1.5 + r * Math.sin(a))}`;
|
|
28245
|
+
}).join(" L")}Z`;
|
|
28246
|
+
var bookmarkStar = {
|
|
28247
|
+
kind: "vector",
|
|
28248
|
+
id: "bookmark-star",
|
|
28249
|
+
name: "Star rating",
|
|
28250
|
+
description: "Five stars that fill one after another on hover, each bouncing as it fills (halves too), while the rating counts up beside them.",
|
|
28251
|
+
category: "icon",
|
|
28252
|
+
tags: ["interactive", "hover", "rating", "stars", "review", "ui"],
|
|
28253
|
+
trigger: "hover",
|
|
28254
|
+
hold: 600,
|
|
28255
|
+
presets: [
|
|
28256
|
+
{ name: "Half", values: { rating: 3.5 } },
|
|
28257
|
+
{ name: "Dark", values: { empty: [0.28, 0.31, 0.37], ink: [0.94, 0.95, 0.97] } }
|
|
28258
|
+
],
|
|
28259
|
+
derived: (v) => {
|
|
28260
|
+
const rating = Math.min(STARS3, Math.max(0, Math.round((Number(v.rating) || 0) * 2) / 2));
|
|
28261
|
+
return {
|
|
28262
|
+
rating,
|
|
28263
|
+
...Object.fromEntries(
|
|
28264
|
+
Array.from({ length: STARS3 }, (_, i) => {
|
|
28265
|
+
const f = Math.min(1, Math.max(0, rating - i));
|
|
28266
|
+
return [
|
|
28267
|
+
[`w${i}`, r115(f * STAR.r * 2)],
|
|
28268
|
+
[`pop${i}`, about6(starX(i), STAR.y, f > 0 ? 1.22 : 1)]
|
|
28269
|
+
];
|
|
28270
|
+
}).flat()
|
|
28271
|
+
)
|
|
28272
|
+
};
|
|
28273
|
+
},
|
|
28274
|
+
motion: {
|
|
28275
|
+
svg: `<svg viewBox="0 0 260 64" font-family="${SANS13}"><!-- {{rating: float min=0 max=5 default=4.5 step=0.5 label='Rating'}} {{star: color default=#FBBF24 label='Filled'}} {{empty: color default=#CBD5E1 label='Empty'}} {{ink: color default=#0F172A label='Text'}} --><defs>${Array.from(
|
|
28276
|
+
{ length: STARS3 },
|
|
28277
|
+
(_, i) => `<clipPath id="bookmarkStarClip${i}"><rect id="bookmarkStarFill${i}" x="${starX(i) - STAR.r}" y="${STAR.y - STAR.r - 2}" width="0" height="${STAR.r * 2 + 6}"/></clipPath>`
|
|
28278
|
+
).join("")}</defs>${Array.from(
|
|
28279
|
+
{ length: STARS3 },
|
|
28280
|
+
(_, i) => `<g id="bookmarkStarStar${i}" transform="${about6(starX(i), STAR.y, 1)}"><path d="${starPath2(starX(i))}" fill="{{empty}}" stroke="{{empty}}" stroke-width="2" stroke-linejoin="round"/><path d="${starPath2(starX(i))}" fill="{{star}}" stroke="{{star}}" stroke-width="2" stroke-linejoin="round" clip-path="url(#bookmarkStarClip${i})"/></g>`
|
|
28281
|
+
).join("")}<text id="bookmarkStarValue" x="202" y="38.5" font-size="16" font-weight="700" fill="{{ink}}" opacity="0">0.0</text></svg>`,
|
|
28282
|
+
animate: [
|
|
28283
|
+
...Array.from({ length: STARS3 }, (_, i) => [
|
|
28284
|
+
{ id: `bookmarkStarFill${i}`, attrs: { width: [0, n16(`w${i}`)] }, delay: i * 110, ms: 160, ease: "linear" },
|
|
28285
|
+
{ id: `bookmarkStarStar${i}`, attrs: { transform: [about6(starX(i), STAR.y, 1), `{{pop${i}}}`] }, delay: i * 110 + 60, ms: 130, ease: "out" },
|
|
28286
|
+
{ id: `bookmarkStarStar${i}`, attrs: { transform: [`{{pop${i}}}`, about6(starX(i), STAR.y, 1)] }, delay: i * 110 + 190, ms: 180, ease: "in-out" }
|
|
28287
|
+
]).flat(),
|
|
28288
|
+
{ id: "bookmarkStarValue", attrs: { opacity: [0, 1] }, ms: 200 },
|
|
28289
|
+
{ id: "bookmarkStarValue", count: [0, n16("rating")], decimals: 1, format: "{n} / 5", ms: STARS3 * 110 + 100, ease: "out" }
|
|
28290
|
+
]
|
|
28291
|
+
}
|
|
28292
|
+
};
|
|
28293
|
+
var LID_HINGE = "76 30";
|
|
28294
|
+
var lid = (deg) => `rotate(${deg} ${LID_HINGE})`;
|
|
28295
|
+
var paperAt = (x, y, deg) => `translate(${x} ${y}) rotate(${deg})`;
|
|
28296
|
+
var canTilt = (deg) => `rotate(${deg} 50 88)`;
|
|
28297
|
+
var trashDelete = {
|
|
28298
|
+
kind: "vector",
|
|
28299
|
+
id: "trash-delete",
|
|
28300
|
+
name: "Trash delete",
|
|
28301
|
+
description: "A trash can whose lid flips open as a crumpled paper drops in, then snaps shut with a little wobble; replays on every click.",
|
|
28302
|
+
category: "icon",
|
|
28303
|
+
tags: ["interactive", "click", "delete", "trash", "remove", "ui"],
|
|
28304
|
+
trigger: "click",
|
|
28305
|
+
hold: 600,
|
|
28306
|
+
presets: [
|
|
28307
|
+
{ name: "Danger", values: { color: [0.86, 0.15, 0.15] } },
|
|
28308
|
+
{ name: "Dark", values: { color: [0.9, 0.91, 0.94], paper: [0.6, 0.63, 0.7] } }
|
|
28309
|
+
],
|
|
28310
|
+
motion: {
|
|
28311
|
+
svg: `<svg viewBox="0 0 100 100"><!-- {{color: color default=#334155 label='Can'}} {{paper: color default=#F8FAFC label='Paper'}} {{width: float min=2 max=8 default=5 step=0.5 label='Stroke width'}} --><g id="trashDeletePaper" transform="${paperAt(38, 12, 0)}" opacity="0"><path d="M-9 -4 L-4 -10 L3 -8 L9 -3 L8 5 L2 9 L-6 8 L-10 2Z" fill="{{paper}}" stroke="{{color}}" stroke-width="2" stroke-linejoin="round"/><path d="M-4 -3 L2 1 M-2 5 L4 -2" stroke="{{color}}" stroke-width="1.4" stroke-linecap="round"/></g><g id="trashDeleteCan" transform="${canTilt(0)}" fill="none" stroke="{{color}}" stroke-width="{{width}}" stroke-linecap="round" stroke-linejoin="round"><path d="M30 36 L34 82 C34.4 86 36 88 40 88 H60 C64 88 65.6 86 66 82 L70 36"/><path d="M43 46 V76 M57 46 V76"/><g id="trashDeleteLid" transform="${lid(0)}"><path d="M24 30 H76 M42 30 V23 H58 V30"/></g></g></svg>`,
|
|
28312
|
+
animate: [
|
|
28313
|
+
{ id: "trashDeleteLid", step: 1, attrs: { transform: [lid(0), lid(32)] }, ms: 220, ease: "out" },
|
|
28314
|
+
{ id: "trashDeletePaper", step: 1, attrs: { opacity: [0, 1] }, delay: 100, ms: 100 },
|
|
28315
|
+
{ id: "trashDeletePaper", step: 1, attrs: { transform: [paperAt(38, 12, 0), paperAt(48, 62, 160)] }, delay: 120, ms: 420, ease: "in-out" },
|
|
28316
|
+
{ id: "trashDeletePaper", step: 1, attrs: { opacity: [1, 0] }, delay: 430, ms: 110 },
|
|
28317
|
+
{ id: "trashDeleteLid", step: 1, attrs: { transform: [lid(32), lid(0)] }, delay: 540, ms: 170, ease: "in-out" },
|
|
28318
|
+
{ id: "trashDeleteLid", step: 1, attrs: { transform: [lid(0), lid(5)] }, delay: 710, ms: 80, ease: "out" },
|
|
28319
|
+
{ id: "trashDeleteLid", step: 1, attrs: { transform: [lid(5), lid(0)] }, delay: 790, ms: 120, ease: "in-out" },
|
|
28320
|
+
{ id: "trashDeleteCan", step: 1, attrs: { transform: [canTilt(0), canTilt(-5)] }, delay: 700, ms: 90, ease: "out" },
|
|
28321
|
+
{ id: "trashDeleteCan", step: 1, attrs: { transform: [canTilt(-5), canTilt(3)] }, delay: 790, ms: 120, ease: "in-out" },
|
|
28322
|
+
{ id: "trashDeleteCan", step: 1, attrs: { transform: [canTilt(3), canTilt(0)] }, delay: 910, ms: 140, ease: "in-out" },
|
|
28323
|
+
// The paper, gone, goes back to the top for the next click.
|
|
28324
|
+
{ id: "trashDeletePaper", step: 1, attrs: { transform: [paperAt(38, 12, 0), paperAt(38, 12, 0)] }, delay: 1100, ms: 0 }
|
|
28325
|
+
]
|
|
28326
|
+
}
|
|
28327
|
+
};
|
|
28328
|
+
var shackle = (y) => `translate(0 ${y})`;
|
|
28329
|
+
var keyPose = (deg, s) => `translate(50 66) rotate(${deg}) scale(${s})`;
|
|
28330
|
+
var lockUnlock = {
|
|
28331
|
+
kind: "vector",
|
|
28332
|
+
id: "lock-unlock",
|
|
28333
|
+
name: "Lock and unlock",
|
|
28334
|
+
description: "A padlock: a click turns a key and pops the shackle open as the lock turns green; the next click turns it back and snaps it shut.",
|
|
28335
|
+
category: "icon",
|
|
28336
|
+
tags: ["interactive", "click", "lock", "security", "toggle", "ui"],
|
|
28337
|
+
trigger: "click",
|
|
28338
|
+
hold: 600,
|
|
28339
|
+
presets: [{ name: "Dark", values: { locked: [0.85, 0.87, 0.9], hole: [0.06, 0.07, 0.1] } }],
|
|
28340
|
+
motion: {
|
|
28341
|
+
svg: `<svg viewBox="0 0 100 100"><!-- {{locked: color default=#334155 label='Locked'}} {{open: color default=#16A34A label='Unlocked'}} {{hole: color default=#FFFFFF label='Keyhole'}} {{key: color default=#F59E0B label='Key'}} --><g id="lockUnlockShackle" transform="${shackle(0)}"><path id="lockUnlockLoop" d="M34 48 V34 C34 25 41 18 50 18 C59 18 66 25 66 34 V48" fill="none" stroke="{{locked}}" stroke-width="8"/></g><rect id="lockUnlockBody" x="22" y="44" width="56" height="44" rx="9" fill="{{locked}}"/><g id="lockUnlockHole" transform="rotate(0 50 66)"><circle cx="50" cy="62" r="5.5" fill="{{hole}}"/><path d="M47.5 64 H52.5 L53.5 75 H46.5Z" fill="{{hole}}"/></g><g id="lockUnlockKey" transform="${keyPose(0, 0.4)}" opacity="0"><rect x="-2.5" y="-6" width="5" height="10" fill="{{key}}"/><circle cy="-13" r="8" fill="none" stroke="{{key}}" stroke-width="4.5"/></g></svg>`,
|
|
28342
|
+
animate: [
|
|
28343
|
+
// Step 1, unlock: the key goes in and turns, the shackle springs up, and the lock goes green.
|
|
28344
|
+
{ id: "lockUnlockKey", step: 1, attrs: { opacity: [0, 1], transform: [keyPose(0, 0.4), keyPose(0, 1)] }, ms: 160, ease: "out" },
|
|
28345
|
+
{ id: "lockUnlockKey", step: 1, attrs: { transform: [keyPose(0, 1), keyPose(90, 1)] }, delay: 180, ms: 200, ease: "in-out" },
|
|
28346
|
+
{ id: "lockUnlockHole", step: 1, attrs: { transform: ["rotate(0 50 66)", "rotate(90 50 66)"] }, delay: 180, ms: 200, ease: "in-out" },
|
|
28347
|
+
{ id: "lockUnlockShackle", step: 1, attrs: { transform: [shackle(0), shackle(-16)] }, delay: 380, ms: 160, ease: "out" },
|
|
28348
|
+
{ id: "lockUnlockShackle", step: 1, attrs: { transform: [shackle(-16), shackle(-12)] }, delay: 540, ms: 180, ease: "in-out" },
|
|
28349
|
+
{ id: "lockUnlockBody", step: 1, attrs: { fill: ["{{locked}}", "{{open}}"] }, delay: 380, ms: 220 },
|
|
28350
|
+
{ id: "lockUnlockLoop", step: 1, attrs: { stroke: ["{{locked}}", "{{open}}"] }, delay: 380, ms: 220 },
|
|
28351
|
+
{ id: "lockUnlockKey", step: 1, attrs: { opacity: [1, 0] }, delay: 650, ms: 200 },
|
|
28352
|
+
// Step 2, lock: the key turns back, the shackle drops shut with a bump.
|
|
28353
|
+
{ id: "lockUnlockKey", step: 2, attrs: { opacity: [0, 1], transform: [keyPose(90, 0.4), keyPose(90, 1)] }, ms: 160, ease: "out" },
|
|
28354
|
+
{ id: "lockUnlockKey", step: 2, attrs: { transform: [keyPose(90, 1), keyPose(0, 1)] }, delay: 180, ms: 200, ease: "in-out" },
|
|
28355
|
+
{ id: "lockUnlockHole", step: 2, attrs: { transform: ["rotate(90 50 66)", "rotate(0 50 66)"] }, delay: 180, ms: 200, ease: "in-out" },
|
|
28356
|
+
{ id: "lockUnlockShackle", step: 2, attrs: { transform: [shackle(-12), shackle(2)] }, delay: 380, ms: 150, ease: "in-out" },
|
|
28357
|
+
{ id: "lockUnlockShackle", step: 2, attrs: { transform: [shackle(2), shackle(0)] }, delay: 530, ms: 120, ease: "out" },
|
|
28358
|
+
{ id: "lockUnlockBody", step: 2, attrs: { fill: ["{{open}}", "{{locked}}"] }, delay: 400, ms: 220 },
|
|
28359
|
+
{ id: "lockUnlockLoop", step: 2, attrs: { stroke: ["{{open}}", "{{locked}}"] }, delay: 400, ms: 220 },
|
|
28360
|
+
{ id: "lockUnlockKey", step: 2, attrs: { opacity: [1, 0] }, delay: 650, ms: 200 },
|
|
28361
|
+
{ id: "lockUnlockKey", step: 2, attrs: { transform: [keyPose(0, 0.4), keyPose(0, 0.4)] }, delay: 900, ms: 0 }
|
|
28362
|
+
]
|
|
28363
|
+
}
|
|
28364
|
+
};
|
|
28365
|
+
var WIFI = { cx: 50, cy: 80 };
|
|
28366
|
+
var wifiArc = (r) => {
|
|
28367
|
+
const d = r * Math.SQRT1_2;
|
|
28368
|
+
return `M${r115(WIFI.cx - d)} ${r115(WIFI.cy - d)} A${r} ${r} 0 0 1 ${r115(WIFI.cx + d)} ${r115(WIFI.cy - d)}`;
|
|
28369
|
+
};
|
|
28370
|
+
var WIFI_ARCS = [20, 36, 52];
|
|
28371
|
+
var wifiSignal = {
|
|
28372
|
+
kind: "vector",
|
|
28373
|
+
id: "wifi-signal",
|
|
28374
|
+
name: "Wi-Fi signal",
|
|
28375
|
+
description: "Wi-Fi bars filling one after another from the dot outward, pulsing twice at full strength, then fading to start again.",
|
|
28376
|
+
category: "icon",
|
|
28377
|
+
tags: ["wifi", "signal", "connection", "network", "status", "loop"],
|
|
28378
|
+
hold: 200,
|
|
28379
|
+
presets: [
|
|
28380
|
+
{ name: "Dark", values: { track: [0.2, 0.22, 0.27], color: [0.38, 0.65, 0.98] } }
|
|
28381
|
+
],
|
|
28382
|
+
motion: {
|
|
28383
|
+
svg: `<svg viewBox="0 0 100 100"><!-- {{color: color default=#2563EB label='Signal'}} {{track: color default=#E2E8F0 label='Empty bars'}} {{width: float min=4 max=12 default=9 step=0.5 label='Bar width'}} --><g fill="none" stroke="{{track}}" stroke-width="{{width}}" stroke-linecap="round">${WIFI_ARCS.map((r) => `<path d="${wifiArc(r)}"/>`).join("")}</g><circle cx="${WIFI.cx}" cy="${WIFI.cy}" r="6.5" fill="{{track}}"/><g id="wifiSignalOn"><circle id="wifiSignalDot" cx="${WIFI.cx}" cy="${WIFI.cy}" r="6.5" fill="{{color}}" opacity="0"/><g fill="none" stroke="{{color}}" stroke-width="{{width}}" stroke-linecap="round">${WIFI_ARCS.map((r, k) => `<path id="wifiSignalArc${k}" d="${wifiArc(r)}" opacity="0"/>`).join("")}</g></g></svg>`,
|
|
28384
|
+
animate: [
|
|
28385
|
+
{ id: "wifiSignalDot", attrs: { opacity: [0, 1], r: [3, 6.5] }, ms: 220, ease: "out" },
|
|
28386
|
+
...WIFI_ARCS.map((_, k) => ({ id: `wifiSignalArc${k}`, attrs: { opacity: [0, 1] }, delay: 250 + k * 250, ms: 220, ease: "out" })),
|
|
28387
|
+
...[0, 1].flatMap((k) => swing("wifiSignalOn", "opacity", 1, 0.45, 600, 1150 + k * 600)),
|
|
28388
|
+
// Fades out together, so the loop starts from empty again.
|
|
28389
|
+
{ id: "wifiSignalDot", attrs: { opacity: [1, 0], r: [6.5, 3] }, delay: 2600, ms: 300 },
|
|
28390
|
+
...WIFI_ARCS.map((_, k) => ({ id: `wifiSignalArc${k}`, attrs: { opacity: [1, 0] }, delay: 2600, ms: 300 }))
|
|
28391
|
+
]
|
|
28392
|
+
}
|
|
28393
|
+
};
|
|
28394
|
+
var iconEffects = [weatherIcons, notificationBell, animatedArrows, emojiReactions, bookmarkStar, trashDelete, lockUnlock, wifiSignal];
|
|
28395
|
+
|
|
27349
28396
|
// src/catalog/index.ts
|
|
27350
28397
|
var SHADER_EFFECTS = [
|
|
27351
28398
|
meshGradient,
|
|
@@ -27422,7 +28469,7 @@ var SHADER_EFFECTS = [
|
|
|
27422
28469
|
...waterShaders,
|
|
27423
28470
|
...transitions
|
|
27424
28471
|
];
|
|
27425
|
-
var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects, ...glowEffects, ...shadowEffects, ...lightingEffects, ...scrollEffects, ...broadcastEffects, ...annotationEffects, ...uiEffects, ...loaderEffects].sort(
|
|
28472
|
+
var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects, ...pyroEffects, ...timewarpEffects, ...meltEffects, ...electricEffects, ...pixelEffects, ...splatterEffects, ...bubbleEffects, ...stickerEffects, ...trailEffects, ...impactEffects, ...comicEffects, ...glowEffects, ...shadowEffects, ...lightingEffects, ...scrollEffects, ...broadcastEffects, ...annotationEffects, ...uiEffects, ...loaderEffects, ...dataEffects, ...iconEffects].sort(
|
|
27426
28473
|
(a, b) => EFFECT_CATEGORIES.indexOf(a.category) - EFFECT_CATEGORIES.indexOf(b.category)
|
|
27427
28474
|
);
|
|
27428
28475
|
var byId = new Map(EFFECTS.map((e) => [e.id, e]));
|
|
@@ -27439,6 +28486,7 @@ function findEffects(query = {}) {
|
|
|
27439
28486
|
const words = (query.text ?? "").toLowerCase().split(/\s+/).filter(Boolean);
|
|
27440
28487
|
return EFFECTS.filter((e) => {
|
|
27441
28488
|
if (query.category && e.category !== query.category) return false;
|
|
28489
|
+
if (query.group && !EFFECT_CATEGORY_GROUPS.find((g) => g.name.toLowerCase() === query.group.toLowerCase())?.categories.includes(e.category)) return false;
|
|
27442
28490
|
if (query.kind && e.kind !== query.kind) return false;
|
|
27443
28491
|
if (query.tags && !query.tags.every((t) => e.tags.includes(t))) return false;
|
|
27444
28492
|
if (query.video && !(e.kind === "shader" && e.video && query.video.includes(e.video))) return false;
|
|
@@ -27479,6 +28527,8 @@ export {
|
|
|
27479
28527
|
SAMPLE_IMAGES,
|
|
27480
28528
|
textImage,
|
|
27481
28529
|
EFFECT_CATEGORIES,
|
|
28530
|
+
EFFECT_CATEGORY_GROUPS,
|
|
28531
|
+
categoryGroup,
|
|
27482
28532
|
VIDEO_USES,
|
|
27483
28533
|
VECTOR_CONTROL_TYPES,
|
|
27484
28534
|
vectorControls,
|
|
@@ -27498,4 +28548,4 @@ export {
|
|
|
27498
28548
|
effectControls,
|
|
27499
28549
|
effectDefaults
|
|
27500
28550
|
};
|
|
27501
|
-
//# sourceMappingURL=chunk-
|
|
28551
|
+
//# sourceMappingURL=chunk-IHUNCBMF.js.map
|