@pixerate/flair 0.8.0 → 0.10.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 +21 -0
- package/README.md +13 -6
- package/dist/catalog/index.d.ts +9 -3
- package/dist/catalog/index.js +2 -1
- package/dist/chunk-OHW7UG6D.js +336 -0
- package/dist/chunk-OHW7UG6D.js.map +1 -0
- package/dist/chunk-P5TSANZY.js +1 -0
- package/dist/{chunk-EQS4EXNM.js → chunk-SH66CNLC.js} +52 -6
- package/dist/chunk-SH66CNLC.js.map +1 -0
- package/dist/{chunk-FHXOMIQY.js → chunk-STGGN4MR.js} +2185 -209
- package/dist/chunk-STGGN4MR.js.map +1 -0
- package/dist/{chunk-TDMD5XAU.js → chunk-YACVME3D.js} +75 -4
- package/dist/{chunk-TDMD5XAU.js.map → chunk-YACVME3D.js.map} +1 -1
- package/dist/{chunk-EZRRKQFV.js → chunk-YLJWC4N2.js} +96 -1
- package/dist/chunk-YLJWC4N2.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/editor/index.d.ts +3 -4
- package/dist/editor/index.js +5 -4
- package/dist/editor/index.js.map +1 -1
- package/dist/index.d.ts +65 -6
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/{renderer-0_Ap1tyi.d.ts → renderer-SNYZu__R.d.ts} +17 -0
- package/dist/svg-CpeanaE_.d.ts +375 -0
- package/dist/{types-CNPIrnTf.d.ts → types-D8uhzDyN.d.ts} +8 -2
- package/dist/vector/index.d.ts +21 -112
- package/dist/vector/index.js +8 -2
- package/package.json +1 -1
- package/dist/chunk-CFCLT2XL.js +0 -1
- package/dist/chunk-EQS4EXNM.js.map +0 -1
- package/dist/chunk-EZRRKQFV.js.map +0 -1
- package/dist/chunk-FHXOMIQY.js.map +0 -1
- package/dist/clock-BhdXWH-n.d.ts +0 -47
- package/dist/motion-Ce0lmzm3.d.ts +0 -179
- /package/dist/{chunk-CFCLT2XL.js.map → chunk-P5TSANZY.js.map} +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
morphPaths,
|
|
3
|
+
morphSequence
|
|
4
|
+
} from "./chunk-OHW7UG6D.js";
|
|
1
5
|
import {
|
|
2
6
|
getTransitionGLSL
|
|
3
7
|
} from "./chunk-EHDJ3CPJ.js";
|
|
@@ -295,10 +299,33 @@ var PORTRAIT = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360">
|
|
|
295
299
|
<circle cx="295" cy="150" r="6" fill="#2b1d16"/><circle cx="345" cy="150" r="6" fill="#2b1d16"/>
|
|
296
300
|
<path d="M300 185 Q320 198 340 185" stroke="#7a3e2c" stroke-width="5" fill="none"/>
|
|
297
301
|
</svg>`;
|
|
302
|
+
var PORTRAIT_2 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360" width="640" height="360">
|
|
303
|
+
<rect width="640" height="360" fill="#d3e4e6"/>
|
|
304
|
+
<path d="M255 360 Q268 300 430 297 Q592 300 605 360Z" fill="#a4372f"/>
|
|
305
|
+
<rect x="408" y="250" width="44" height="52" fill="#d9a07c"/>
|
|
306
|
+
<circle cx="430" cy="175" r="95" fill="#e8b48f"/>
|
|
307
|
+
<path d="M332 172 Q330 66 430 64 Q530 66 528 172 Q520 112 470 100 Q420 128 352 116 Q336 140 332 172Z" fill="#8a3b1e"/>
|
|
308
|
+
<circle cx="396" cy="175" r="8" fill="#3a2418"/><circle cx="464" cy="175" r="8" fill="#3a2418"/>
|
|
309
|
+
<path d="M424 186 Q430 206 436 186" stroke="#c48262" stroke-width="5" fill="none"/>
|
|
310
|
+
<path d="M403 225 Q430 244 457 225" stroke="#9c4a35" stroke-width="7" fill="none" stroke-linecap="round"/>
|
|
311
|
+
</svg>`;
|
|
312
|
+
var LOGO = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
|
313
|
+
<path fill="#6366f1" fill-rule="evenodd" d="M256 56a200 200 0 1 1 0 400a200 200 0 1 1 0-400zm0 92a108 108 0 1 0 0 216a108 108 0 1 0 0-216z"/>
|
|
314
|
+
<circle cx="256" cy="256" r="52" fill="#6366f1"/>
|
|
315
|
+
</svg>`;
|
|
316
|
+
var HEART = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
|
317
|
+
<path fill="#f43f5e" d="M256 452C120 352 40 272 40 172C40 102 95 58 160 58C205 58 238 84 256 118C274 84 307 58 352 58C417 58 472 102 472 172C472 272 392 352 256 452Z"/>
|
|
318
|
+
</svg>`;
|
|
298
319
|
var SAMPLE_IMAGES = {
|
|
299
320
|
landscape: svgDataUrl(LANDSCAPE),
|
|
300
321
|
city: svgDataUrl(CITY),
|
|
301
|
-
portrait: svgDataUrl(PORTRAIT)
|
|
322
|
+
portrait: svgDataUrl(PORTRAIT),
|
|
323
|
+
/** Another face, larger and to the right: morph it with `portrait`. */
|
|
324
|
+
portrait2: svgDataUrl(PORTRAIT_2),
|
|
325
|
+
/** A ring-and-dot mark, indigo on transparent. */
|
|
326
|
+
logo: svgDataUrl(LOGO),
|
|
327
|
+
/** A heart, rose on transparent. */
|
|
328
|
+
heart: svgDataUrl(HEART)
|
|
302
329
|
};
|
|
303
330
|
function charWidth(c) {
|
|
304
331
|
if (/[iIjl.,:;'!|`]/.test(c)) return 0.3;
|
|
@@ -1290,162 +1317,2107 @@ uniform float uSpeed; // @control min=0 max=2 default=0.5 label="Speed"`,
|
|
|
1290
1317
|
col = mix(col, uWall, wall);
|
|
1291
1318
|
fragColor = vec4(col, 1.0);
|
|
1292
1319
|
}`
|
|
1293
|
-
)
|
|
1320
|
+
)
|
|
1321
|
+
};
|
|
1322
|
+
var kaleidoscope = {
|
|
1323
|
+
kind: "shader",
|
|
1324
|
+
id: "kaleidoscope",
|
|
1325
|
+
name: "Kaleidoscope",
|
|
1326
|
+
description: "Mirrored wedges of shifting colour turning around the centre.",
|
|
1327
|
+
category: "pattern",
|
|
1328
|
+
tags: ["psychedelic", "music", "symmetry"],
|
|
1329
|
+
source: glsl(
|
|
1330
|
+
`// Kaleidoscope: mirrored wedges of shifting colour.
|
|
1331
|
+
uniform int uSegments; // @control min=2 max=16 default=8 label="Segments"
|
|
1332
|
+
uniform float uZoom; // @control min=0.5 max=8 default=3 label="Zoom"
|
|
1333
|
+
uniform float uSpeed; // @control min=0 max=2 default=0.4 label="Speed"
|
|
1334
|
+
uniform vec3 uTint; // @control color default=#FFFFFF label="Tint"`,
|
|
1335
|
+
ROTATE,
|
|
1336
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
1337
|
+
vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
|
|
1338
|
+
float t = iTime * uSpeed;
|
|
1339
|
+
float n = float(uSegments);
|
|
1340
|
+
float a = atan(p.y, p.x) + t * 0.3;
|
|
1341
|
+
float r = length(p);
|
|
1342
|
+
float wedge = 6.28318 / n;
|
|
1343
|
+
a = mod(a, wedge);
|
|
1344
|
+
a = abs(a - wedge * 0.5);
|
|
1345
|
+
vec2 q = vec2(cos(a), sin(a)) * r * uZoom;
|
|
1346
|
+
q = rot(t * 0.2) * q + t;
|
|
1347
|
+
vec3 col = 0.5 + 0.5 * cos(vec3(0.0, 2.0, 4.0) + q.x * 2.0 + sin(q.y * 3.0) + t);
|
|
1348
|
+
col *= 0.6 + 0.4 * sin(q.x * 5.0) * sin(q.y * 5.0);
|
|
1349
|
+
col *= smoothstep(1.2, 0.2, r);
|
|
1350
|
+
fragColor = vec4(col * uTint, 1.0);
|
|
1351
|
+
}`
|
|
1352
|
+
)
|
|
1353
|
+
};
|
|
1354
|
+
var neonCircuit = {
|
|
1355
|
+
kind: "shader",
|
|
1356
|
+
id: "neon-circuit",
|
|
1357
|
+
name: "Neon circuit",
|
|
1358
|
+
description: "Glowing neon traces routed up a circuit board, with pulses of light racing along them.",
|
|
1359
|
+
category: "pattern",
|
|
1360
|
+
tags: ["background", "neon", "circuit", "tech", "80s", "vaporwave"],
|
|
1361
|
+
source: glsl(
|
|
1362
|
+
`// Neon circuit: glowing traces climbing a board in rounded jogs, with pulses running along them.
|
|
1363
|
+
uniform vec3 uBackground; // @control color default=#170A2E label="Background"
|
|
1364
|
+
uniform vec3 uColorA; // @control color default=#FF5FA2 label="Trace A"
|
|
1365
|
+
uniform vec3 uColorB; // @control color default=#43E5FF label="Trace B"
|
|
1366
|
+
uniform vec3 uColorC; // @control color default=#FFD23F label="Trace C"
|
|
1367
|
+
uniform vec3 uColorD; // @control color default=#9D7BFF label="Trace D"
|
|
1368
|
+
uniform float uLanes; // @control min=4 max=40 default=14 label="Lanes"
|
|
1369
|
+
uniform float uDensity; // @control min=0.1 max=1 default=0.7 label="Density"
|
|
1370
|
+
uniform float uWidth; // @control min=0.04 max=0.4 default=0.16 label="Line width"
|
|
1371
|
+
uniform float uGlow; // @control min=0 max=2 default=1 label="Glow"
|
|
1372
|
+
uniform float uSpeed; // @control min=0 max=3 default=1 label="Pulse speed"
|
|
1373
|
+
uniform float uPulses; // @control min=0 max=1 default=0.5 label="Pulses"
|
|
1374
|
+
uniform float uScroll; // @control min=-2 max=2 default=0.4 label="Scroll"
|
|
1375
|
+
uniform float uGrid; // @control min=0 max=1 default=0.3 label="Grid"`,
|
|
1376
|
+
HASH,
|
|
1377
|
+
`// Each trace k owns lanes 3k..3k+2 and climbs in runs of RUN lanes, jogging
|
|
1378
|
+
// sideways between them on rounded corners, so traces never cross.
|
|
1379
|
+
const float RUN = 2.5;
|
|
1380
|
+
const float RADIUS = 0.35;
|
|
1381
|
+
|
|
1382
|
+
float laneOf(float k, float j) {
|
|
1383
|
+
return 3.0 * k + floor(hash12(vec2(k, j) + 0.37) * 3.0) + 0.5;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
vec3 traceColor(float k) {
|
|
1387
|
+
float h = hash12(vec2(k, 7.3));
|
|
1388
|
+
return h < 0.25 ? uColorA : h < 0.5 ? uColorB : h < 0.75 ? uColorC : uColorD;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Distance to a quarter circle of RADIUS round c, in the quadrant whose signs are q.
|
|
1392
|
+
float arcDist(vec2 p, vec2 c, vec2 q) {
|
|
1393
|
+
vec2 d = p - c;
|
|
1394
|
+
return d.x * q.x >= 0.0 && d.y * q.y >= 0.0 ? abs(length(d) - RADIUS) : 1e5;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Trace k's run j: distance from p to its climb (x) and how far up it p is (y), then
|
|
1398
|
+
// the same for its jog into run j + 1 (z, w), measured along the run.
|
|
1399
|
+
vec4 run(vec2 p, float k, float j) {
|
|
1400
|
+
float x0 = laneOf(k, j - 1.0);
|
|
1401
|
+
float x1 = laneOf(k, j);
|
|
1402
|
+
float x2 = laneOf(k, j + 1.0);
|
|
1403
|
+
float y0 = j * RUN;
|
|
1404
|
+
float y1 = y0 + RUN;
|
|
1405
|
+
float lo = y0 + (x0 == x1 ? 0.0 : RADIUS);
|
|
1406
|
+
float hi = y1 - (x2 == x1 ? 0.0 : RADIUS);
|
|
1407
|
+
vec4 r = vec4(length(p - vec2(x1, clamp(p.y, lo, hi))), p.y - y0, 1e5, 0.0);
|
|
1408
|
+
if (x2 != x1) {
|
|
1409
|
+
float sx = sign(x2 - x1);
|
|
1410
|
+
float h = length(p - vec2(clamp(p.x, min(x1, x2) + RADIUS, max(x1, x2) - RADIUS), y1));
|
|
1411
|
+
h = min(h, arcDist(p, vec2(x1 + sx * RADIUS, y1 - RADIUS), vec2(-sx, 1.0)));
|
|
1412
|
+
h = min(h, arcDist(p, vec2(x2 - sx * RADIUS, y1 + RADIUS), vec2(sx, -1.0)));
|
|
1413
|
+
r.zw = vec2(h, RUN + abs(p.x - x1));
|
|
1414
|
+
}
|
|
1415
|
+
return r;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// How bright a pulse is at s along run j of trace k: a bright head with a fading tail.
|
|
1419
|
+
// A pulse crosses one run per cycle and carries on into the next with the next cycle.
|
|
1420
|
+
float pulseAt(float k, float j, float s) {
|
|
1421
|
+
float cycle = iTime * uSpeed * 1.5 + hash12(vec2(k, 8.8));
|
|
1422
|
+
float behind = fract(cycle) * (RUN + abs(laneOf(k, j + 1.0) - laneOf(k, j))) - s;
|
|
1423
|
+
float lit = step(hash12(vec2(k, floor(cycle) - j)), uPulses);
|
|
1424
|
+
return lit * (behind >= 0.0 ? exp(-behind * 0.8) : exp(behind * 30.0));
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
1428
|
+
float px = uLanes / iResolution.y;
|
|
1429
|
+
vec2 p = fragCoord * px;
|
|
1430
|
+
p.y += iTime * uScroll;
|
|
1431
|
+
|
|
1432
|
+
vec2 g = abs(fract(p / 1.5) - 0.5);
|
|
1433
|
+
float grid = 1.0 - smoothstep(0.0, px * 1.5, (0.5 - max(g.x, g.y)) * 1.5);
|
|
1434
|
+
vec3 light = uColorD * 0.12 * uGrid * grid;
|
|
1435
|
+
|
|
1436
|
+
float home = floor(p.x / 3.0);
|
|
1437
|
+
for (int i = -1; i <= 1; i++) {
|
|
1438
|
+
float k = home + float(i);
|
|
1439
|
+
if (hash12(vec2(k, 1.9)) > uDensity) continue;
|
|
1440
|
+
vec2 q = vec2(p.x, p.y + hash12(vec2(k, 4.1)) * RUN);
|
|
1441
|
+
float jq = floor(q.y / RUN);
|
|
1442
|
+
float w = uWidth * 0.5;
|
|
1443
|
+
// The trace's nearest point lights it; each piece's pulse lights only round that piece.
|
|
1444
|
+
float d = 1e5;
|
|
1445
|
+
float pulse = 0.0;
|
|
1446
|
+
for (int b = -1; b <= 1; b++) {
|
|
1447
|
+
float j = jq + float(b);
|
|
1448
|
+
vec4 r = run(q, k, j);
|
|
1449
|
+
d = min(d, min(r.x, r.z));
|
|
1450
|
+
pulse = max(pulse, pulseAt(k, j, r.y) * exp(-r.x / uWidth));
|
|
1451
|
+
if (r.z < 1e4) pulse = max(pulse, pulseAt(k, j, r.w) * exp(-r.z / uWidth));
|
|
1452
|
+
}
|
|
1453
|
+
float core = 1.0 - smoothstep(w - px, w + px, d);
|
|
1454
|
+
float glow = uGlow * (0.5 * exp(-d / uWidth) + 0.2 * exp(-d / (uWidth * 5.0)));
|
|
1455
|
+
vec3 c = traceColor(k);
|
|
1456
|
+
light += c * (core * 0.8 + glow + uGlow * pulse) + mix(c, vec3(1.0), 0.7) * core * pulse * 3.0;
|
|
1457
|
+
}
|
|
1458
|
+
fragColor = vec4(uBackground + (1.0 - exp(-light * 1.4)), 1.0);
|
|
1459
|
+
}`
|
|
1460
|
+
),
|
|
1461
|
+
presets: [
|
|
1462
|
+
{ name: "Mainframe", values: { uBackground: [0, 0.04, 0.02], uColorA: [0.2, 1, 0.45], uColorB: [0.1, 0.75, 0.35], uColorC: [0.6, 1, 0.7], uColorD: [0.1, 0.5, 0.25] } },
|
|
1463
|
+
{ name: "Blueprint", values: { uBackground: [0.02, 0.06, 0.16], uColorA: [0.35, 0.75, 1], uColorB: [0.6, 0.9, 1], uColorC: [1, 1, 1], uColorD: [0.25, 0.45, 0.9], uGlow: 0.6, uPulses: 0.3 } },
|
|
1464
|
+
{ name: "Dense", values: { uLanes: 26, uDensity: 1, uWidth: 0.12, uPulses: 0.8 } }
|
|
1465
|
+
]
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
// src/catalog/interaction.ts
|
|
1469
|
+
var r1 = (v) => Math.round(v * 10) / 10;
|
|
1470
|
+
var r2 = (v) => Math.round(v * 100) / 100;
|
|
1471
|
+
var n = (name) => `{{${name}}}`;
|
|
1472
|
+
var mixHex = (a, b, t) => {
|
|
1473
|
+
const x = Array.isArray(a) ? a : [1, 1, 1];
|
|
1474
|
+
const y = Array.isArray(b) ? b : [1, 1, 1];
|
|
1475
|
+
return rgbToHex([0, 1, 2].map((i) => x[i] + (y[i] - x[i]) * t));
|
|
1476
|
+
};
|
|
1477
|
+
function fit(text, width, max) {
|
|
1478
|
+
const e = Math.max(0.6, textWidthEm(String(text)));
|
|
1479
|
+
const size = Math.min(max, width / e);
|
|
1480
|
+
return { size: r1(size), length: r1(e * size) };
|
|
1481
|
+
}
|
|
1482
|
+
var about = (cx, cy, s) => `translate(${cx} ${cy}) scale(${r2(s)}) translate(${-cx} ${-cy})`;
|
|
1483
|
+
var snap = (id, attr, value, delay, step = 1) => ({
|
|
1484
|
+
id,
|
|
1485
|
+
step,
|
|
1486
|
+
attrs: { [attr]: [value, value] },
|
|
1487
|
+
delay,
|
|
1488
|
+
ms: 0
|
|
1489
|
+
});
|
|
1490
|
+
var SANS = `system-ui, -apple-system, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif`;
|
|
1491
|
+
var HEART2 = "M12 20.5 C12 20.5 3 14.8 3 8.8 C3 6 5.2 3.8 8 3.8 C9.7 3.8 11.2 4.7 12 6.1 C12.8 4.7 14.3 3.8 16 3.8 C18.8 3.8 21 6 21 8.8 C21 14.8 12 20.5 12 20.5 Z";
|
|
1492
|
+
var PARTICLES = 8;
|
|
1493
|
+
var BURST_FROM = 22;
|
|
1494
|
+
var BURST_TO = 46;
|
|
1495
|
+
var HEART_X = 72;
|
|
1496
|
+
var HEART_Y = 60;
|
|
1497
|
+
var heartPose = (k) => `translate(${HEART_X} ${HEART_Y}) scale(${r2(2.2 * k)}) translate(-12 -12)`;
|
|
1498
|
+
var likeButton = {
|
|
1499
|
+
kind: "vector",
|
|
1500
|
+
id: "like-button",
|
|
1501
|
+
name: "Like button",
|
|
1502
|
+
description: "A heart that fills with a springy pop, a ring and a burst of particles as its like count ticks up, and empties again on the next click.",
|
|
1503
|
+
category: "interaction",
|
|
1504
|
+
tags: ["interactive", "ui", "click", "like", "heart", "toggle", "social", "micro-interaction"],
|
|
1505
|
+
trigger: "click",
|
|
1506
|
+
hold: 900,
|
|
1507
|
+
presets: [
|
|
1508
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], chip: [0.13, 0.14, 0.19], outline: [0.6, 0.63, 0.7], ink: [0.9, 0.91, 0.95] } },
|
|
1509
|
+
{ name: "Purple", values: { heart: [0.6, 0.3, 1], accent: [1, 0.4, 0.75], count: 1204 } }
|
|
1510
|
+
],
|
|
1511
|
+
derived: (v) => ({ liked: Number(v.count) + 1 }),
|
|
1512
|
+
motion: {
|
|
1513
|
+
svg: `<svg viewBox="0 0 240 120"><!-- {{count: int min=0 max=99999 default=42 label='Start count'}} {{background: color default=#FFFFFF label='Background'}} {{chip: color default=#F3F4F6 label='Button'}} {{outline: color default=#6B7280 label='Outline'}} {{heart: color default=#F43F5E label='Heart'}} {{accent: color default=#FB923C label='Sparks'}} {{ink: color default=#111827 label='Count colour'}} --><rect width="240" height="120" fill="{{background}}"/><rect x="20" y="24" width="200" height="72" rx="36" fill="{{chip}}"/><circle id="ring" cx="${HEART_X}" cy="${HEART_Y}" r="14" fill="none" stroke="{{heart}}" stroke-width="0"/>${Array.from(
|
|
1514
|
+
{ length: PARTICLES },
|
|
1515
|
+
(_, k) => {
|
|
1516
|
+
const a = (k * 360 / PARTICLES - 90) * (Math.PI / 180);
|
|
1517
|
+
return `<circle id="p${k}" cx="${r1(HEART_X + BURST_FROM * Math.cos(a))}" cy="${r1(HEART_Y + BURST_FROM * Math.sin(a))}" r="0" fill="${k % 2 ? "{{accent}}" : "{{heart}}"}"/>`;
|
|
1518
|
+
}
|
|
1519
|
+
).join("")}<g id="hg" transform="${heartPose(1)}"><path id="heartShape" d="${HEART2}" fill="{{heart}}" fill-opacity="0" stroke="{{outline}}" stroke-width="1.8" stroke-linejoin="round"/></g><text id="tally" x="106" y="70.5" font-family="${SANS}" font-size="30" font-weight="700" fill="{{ink}}">{{count}}</text></svg>`,
|
|
1520
|
+
animate: [
|
|
1521
|
+
// Step 1, like: squash, pop past full size and settle while it fills.
|
|
1522
|
+
{ id: "hg", step: 1, attrs: { transform: [heartPose(1), heartPose(0.78)] }, ms: 110, ease: "out" },
|
|
1523
|
+
{ id: "hg", step: 1, attrs: { transform: [heartPose(0.78), heartPose(1.22)] }, delay: 110, ms: 170, ease: "out" },
|
|
1524
|
+
{ id: "hg", step: 1, attrs: { transform: [heartPose(1.22), heartPose(1)] }, delay: 280, ms: 220, ease: "in-out" },
|
|
1525
|
+
{ id: "heartShape", step: 1, attrs: { "fill-opacity": [0, 1], stroke: ["{{outline}}", "{{heart}}"] }, delay: 110, ms: 120 },
|
|
1526
|
+
// The ring and sparks burst out, then snap back once they are gone.
|
|
1527
|
+
snap("ring", "stroke-width", 0, 0),
|
|
1528
|
+
{ id: "ring", step: 1, attrs: { r: [14, 44], "stroke-width": [10, 0] }, delay: 130, ms: 480, ease: "out" },
|
|
1529
|
+
snap("ring", "r", 14, 700),
|
|
1530
|
+
...Array.from({ length: PARTICLES }, (_, k) => {
|
|
1531
|
+
const a = (k * 360 / PARTICLES - 90) * (Math.PI / 180);
|
|
1532
|
+
const [x0, y0] = [r1(HEART_X + BURST_FROM * Math.cos(a)), r1(HEART_Y + BURST_FROM * Math.sin(a))];
|
|
1533
|
+
const [x1, y1] = [r1(HEART_X + BURST_TO * Math.cos(a)), r1(HEART_Y + BURST_TO * Math.sin(a))];
|
|
1534
|
+
return [
|
|
1535
|
+
snap(`p${k}`, "r", 0, 0),
|
|
1536
|
+
{ id: `p${k}`, step: 1, attrs: { cx: [x0, x1], cy: [y0, y1], r: [4.5, 0] }, delay: 150, ms: 520, ease: "out" },
|
|
1537
|
+
snap(`p${k}`, "cx", x0, 700),
|
|
1538
|
+
snap(`p${k}`, "cy", y0, 700)
|
|
1539
|
+
];
|
|
1540
|
+
}).flat(),
|
|
1541
|
+
{ id: "tally", step: 1, count: [n("count"), n("liked")], attrs: { fill: ["{{ink}}", "{{heart}}"] }, delay: 150, ms: 260, ease: "out" },
|
|
1542
|
+
// Step 2, unlike: a small dip as it empties and the count goes back.
|
|
1543
|
+
{ id: "hg", step: 2, attrs: { transform: [heartPose(1), heartPose(0.86)] }, ms: 120, ease: "out" },
|
|
1544
|
+
{ id: "hg", step: 2, attrs: { transform: [heartPose(0.86), heartPose(1)] }, delay: 120, ms: 200, ease: "in-out" },
|
|
1545
|
+
{ id: "heartShape", step: 2, attrs: { "fill-opacity": [1, 0], stroke: ["{{heart}}", "{{outline}}"] }, ms: 180 },
|
|
1546
|
+
{ id: "tally", step: 2, count: [n("liked"), n("count")], attrs: { fill: ["{{heart}}", "{{ink}}"] }, ms: 220, ease: "out" }
|
|
1547
|
+
]
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
var toggleSwitch = {
|
|
1551
|
+
kind: "vector",
|
|
1552
|
+
id: "toggle-switch",
|
|
1553
|
+
name: "Toggle switch",
|
|
1554
|
+
description: "A switch whose knob slides across, squashing a little mid-slide, as the track blends to its on colour; the next click slides it back.",
|
|
1555
|
+
category: "interaction",
|
|
1556
|
+
tags: ["interactive", "ui", "click", "toggle", "switch", "settings", "micro-interaction"],
|
|
1557
|
+
trigger: "click",
|
|
1558
|
+
hold: 900,
|
|
1559
|
+
presets: [
|
|
1560
|
+
{ name: "iOS", values: { on: [0.2, 0.78, 0.35] } },
|
|
1561
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], off: [0.22, 0.24, 0.3], on: [0.55, 0.45, 1], knob: [0.96, 0.96, 1] } }
|
|
1562
|
+
],
|
|
1563
|
+
motion: (() => {
|
|
1564
|
+
const slide = (from, to, step) => [
|
|
1565
|
+
{ id: "slide", step, attrs: { transform: [`translate(${from} 0)`, `translate(${to} 0)`] }, ms: 320, ease: "in-out" },
|
|
1566
|
+
// Stretched along the slide and flattened halfway, round again at the end.
|
|
1567
|
+
{ id: "knob", step, attrs: { rx: [19, 24], ry: [19, 16.5] }, ms: 160, ease: "out" },
|
|
1568
|
+
{ id: "knob", step, attrs: { rx: [24, 19], ry: [16.5, 19] }, delay: 160, ms: 160, ease: "in-out" }
|
|
1569
|
+
];
|
|
1570
|
+
return {
|
|
1571
|
+
svg: `<svg viewBox="0 0 200 120"><!-- {{background: color default=#FFFFFF label='Background'}} {{off: color default=#D1D5DB label='Track off'}} {{on: color default=#6366F1 label='Track on'}} {{knob: color default=#FFFFFF label='Knob'}} --><rect width="200" height="120" fill="{{background}}"/><rect id="track" x="40" y="35" width="120" height="50" rx="25" fill="{{off}}"/><g id="slide" transform="translate(0 0)"><ellipse cx="65" cy="62.5" rx="18" ry="18" fill="#000000" opacity="0.16"/><ellipse id="knob" cx="65" cy="60" rx="19" ry="19" fill="{{knob}}"/></g></svg>`,
|
|
1572
|
+
animate: [
|
|
1573
|
+
...slide(0, 70, 1),
|
|
1574
|
+
{ id: "track", step: 1, attrs: { fill: ["{{off}}", "{{on}}"] }, ms: 320, ease: "in-out" },
|
|
1575
|
+
...slide(70, 0, 2),
|
|
1576
|
+
{ id: "track", step: 2, attrs: { fill: ["{{on}}", "{{off}}"] }, ms: 320, ease: "in-out" }
|
|
1577
|
+
]
|
|
1578
|
+
};
|
|
1579
|
+
})()
|
|
1580
|
+
};
|
|
1581
|
+
var checkbox = {
|
|
1582
|
+
kind: "vector",
|
|
1583
|
+
id: "checkbox",
|
|
1584
|
+
name: "Checkbox",
|
|
1585
|
+
description: "A labelled checkbox that fills with a press and draws its tick on, then undraws it and empties on the next click.",
|
|
1586
|
+
category: "interaction",
|
|
1587
|
+
tags: ["interactive", "ui", "click", "checkbox", "form", "toggle", "todo", "micro-interaction"],
|
|
1588
|
+
trigger: "click",
|
|
1589
|
+
hold: 900,
|
|
1590
|
+
presets: [
|
|
1591
|
+
{ name: "Green", values: { accent: [0.13, 0.7, 0.4] } },
|
|
1592
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], border: [0.4, 0.43, 0.5], ink: [0.92, 0.93, 0.96], accent: [0.95, 0.6, 0.15], tick: [0.06, 0.07, 0.1] } }
|
|
1593
|
+
],
|
|
1594
|
+
derived: (v) => {
|
|
1595
|
+
const { size, length } = fit(v.label, 216, 24);
|
|
1596
|
+
return { size, length, base: r1(60 + size * 0.35) };
|
|
1597
|
+
},
|
|
1598
|
+
motion: {
|
|
1599
|
+
svg: `<svg viewBox="0 0 320 120"><!-- {{label: text default='Remember me' label='Label'}} {{background: color default=#FFFFFF label='Background'}} {{border: color default=#9CA3AF label='Border'}} {{accent: color default=#6366F1 label='Checked'}} {{tick: color default=#FFFFFF label='Tick'}} {{ink: color default=#111827 label='Label colour'}} --><rect width="320" height="120" fill="{{background}}"/><g id="boxg" transform="${about(48, 60, 1)}"><rect id="box" x="30" y="42" width="36" height="36" rx="9" fill="{{accent}}" fill-opacity="0" stroke="{{border}}" stroke-width="3"/><path id="tick" d="M39 60.5 L45.5 67 L58 53.5" fill="none" stroke="{{tick}}" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round" opacity="0"/></g><text x="84" y="{{base}}" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{size}}" font-weight="500" fill="{{ink}}">{{label}}</text></svg>`,
|
|
1600
|
+
animate: [
|
|
1601
|
+
// Step 1, check: the box dips as it fills, then the tick draws on.
|
|
1602
|
+
{ id: "boxg", step: 1, attrs: { transform: [about(48, 60, 1), about(48, 60, 0.86)] }, ms: 100, ease: "out" },
|
|
1603
|
+
{ id: "boxg", step: 1, attrs: { transform: [about(48, 60, 0.86), about(48, 60, 1)] }, delay: 100, ms: 220, ease: "in-out" },
|
|
1604
|
+
{ id: "box", step: 1, attrs: { "fill-opacity": [0, 1], stroke: ["{{border}}", "{{accent}}"] }, ms: 160 },
|
|
1605
|
+
// Shown only once it starts drawing: a round cap would leave a dot.
|
|
1606
|
+
{ id: "tick", step: 1, attrs: { opacity: [0, 1] }, delay: 120, ms: 40 },
|
|
1607
|
+
{ id: "tick", step: 1, draw: true, delay: 120, ms: 260, ease: "out" },
|
|
1608
|
+
// Step 2, uncheck: the tick undraws, then the box empties.
|
|
1609
|
+
{ id: "tick", step: 2, draw: [1, 0], ms: 160, ease: "in-out" },
|
|
1610
|
+
{ id: "tick", step: 2, attrs: { opacity: [1, 0] }, delay: 140, ms: 30 },
|
|
1611
|
+
{ id: "box", step: 2, attrs: { "fill-opacity": [1, 0], stroke: ["{{accent}}", "{{border}}"] }, delay: 100, ms: 180 },
|
|
1612
|
+
{ id: "boxg", step: 2, attrs: { transform: [about(48, 60, 1), about(48, 60, 0.92)] }, delay: 100, ms: 90, ease: "out" },
|
|
1613
|
+
{ id: "boxg", step: 2, attrs: { transform: [about(48, 60, 0.92), about(48, 60, 1)] }, delay: 190, ms: 160, ease: "in-out" }
|
|
1614
|
+
]
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
function capsule(p, q, width, flatEnd = false) {
|
|
1618
|
+
const h = width / 2;
|
|
1619
|
+
const a = Math.atan2(q[1] - p[1], q[0] - p[0]);
|
|
1620
|
+
const pts = [];
|
|
1621
|
+
const cap = (c, from) => {
|
|
1622
|
+
for (let k = 0; k <= 8; k++) {
|
|
1623
|
+
const t = from + k * Math.PI / 8;
|
|
1624
|
+
pts.push([c[0] + h * Math.cos(t), c[1] + h * Math.sin(t)]);
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
if (flatEnd) pts.push([q[0] + h * Math.cos(a - Math.PI / 2), q[1] + h * Math.sin(a - Math.PI / 2)], [q[0] + h * Math.cos(a + Math.PI / 2), q[1] + h * Math.sin(a + Math.PI / 2)]);
|
|
1628
|
+
else cap(q, a - Math.PI / 2);
|
|
1629
|
+
cap(p, a + Math.PI / 2);
|
|
1630
|
+
return `M${pts.map((pt) => `${r2(pt[0])} ${r2(pt[1])}`).join(" L")} Z`;
|
|
1631
|
+
}
|
|
1632
|
+
var menuMorphs = /* @__PURE__ */ new Map();
|
|
1633
|
+
function menuMorph(weight) {
|
|
1634
|
+
if (!menuMorphs.has(weight)) {
|
|
1635
|
+
const bars = [7, 12, 17].map((y) => capsule([5, y], [19, y], weight)).join(" ");
|
|
1636
|
+
const meet = (weight / 2 + 0.05) / Math.SQRT2;
|
|
1637
|
+
const cross = [
|
|
1638
|
+
capsule([6.5, 6.5], [17.5, 17.5], weight),
|
|
1639
|
+
capsule([17.5, 6.5], [12 + meet, 12 - meet], weight, true),
|
|
1640
|
+
capsule([6.5, 17.5], [12 - meet, 12 + meet], weight, true)
|
|
1641
|
+
].join(" ");
|
|
1642
|
+
menuMorphs.set(weight, morphPaths(bars, cross, { points: 48 }));
|
|
1643
|
+
}
|
|
1644
|
+
return menuMorphs.get(weight);
|
|
1645
|
+
}
|
|
1646
|
+
var menuPose = (turn) => `translate(60 60) rotate(${turn}) scale(3) translate(-12 -12)`;
|
|
1647
|
+
var menuToggle = {
|
|
1648
|
+
kind: "vector",
|
|
1649
|
+
id: "menu-toggle",
|
|
1650
|
+
name: "Menu toggle",
|
|
1651
|
+
description: "A hamburger menu button whose three bars morph into a close cross as it spins half a turn, and back on the next click.",
|
|
1652
|
+
category: "interaction",
|
|
1653
|
+
tags: ["interactive", "ui", "click", "menu", "hamburger", "close", "morph", "navigation", "micro-interaction"],
|
|
1654
|
+
trigger: "click",
|
|
1655
|
+
hold: 900,
|
|
1656
|
+
presets: [
|
|
1657
|
+
{ name: "Light", values: { background: [1, 1, 1], button: [0.95, 0.96, 0.97], active: [0.07, 0.09, 0.15], ink: [0.07, 0.09, 0.15], activeInk: [1, 1, 1] } },
|
|
1658
|
+
{ name: "Bold", values: { weight: 3, button: [0.98, 0.8, 0.1], active: [0.95, 0.3, 0.2], ink: [0.1, 0.1, 0.1], activeInk: [1, 1, 1] } }
|
|
1659
|
+
],
|
|
1660
|
+
derived: (v) => {
|
|
1661
|
+
const [bars, cross] = menuMorph(Math.round(Number(v.weight) * 4) / 4);
|
|
1662
|
+
return { bars, cross };
|
|
1663
|
+
},
|
|
1664
|
+
motion: {
|
|
1665
|
+
svg: `<svg viewBox="0 0 120 120"><!-- {{weight: float min=1 max=3.5 default=2 step=0.25 label='Line weight'}} {{background: color default=#0F172A label='Background'}} {{button: color default=#1E293B label='Button'}} {{active: color default=#6366F1 label='Button open'}} {{ink: color default=#F8FAFC label='Icon'}} {{activeInk: color default=#FFFFFF label='Icon open'}} --><rect width="120" height="120" fill="{{background}}"/><rect id="bg" x="16" y="16" width="88" height="88" rx="24" fill="{{button}}"/><g id="icon" transform="${menuPose(0)}"><path id="glyph" d="{{bars}}" fill="{{ink}}" fill-rule="evenodd"/></g></svg>`,
|
|
1666
|
+
animate: [
|
|
1667
|
+
// Step 1, open: bars to cross while turning half a turn.
|
|
1668
|
+
{ id: "glyph", step: 1, attrs: { d: ["{{bars}}", "{{cross}}"], fill: ["{{ink}}", "{{activeInk}}"] }, ms: 450, ease: "in-out" },
|
|
1669
|
+
{ id: "icon", step: 1, attrs: { transform: [menuPose(0), menuPose(180)] }, ms: 450, ease: "in-out" },
|
|
1670
|
+
{ id: "bg", step: 1, attrs: { fill: ["{{button}}", "{{active}}"] }, ms: 300, ease: "in-out" },
|
|
1671
|
+
// Step 2, close: on round the same way (-180° looks like 180°), ending where it started.
|
|
1672
|
+
{ id: "glyph", step: 2, attrs: { d: ["{{cross}}", "{{bars}}"], fill: ["{{activeInk}}", "{{ink}}"] }, ms: 450, ease: "in-out" },
|
|
1673
|
+
{ id: "icon", step: 2, attrs: { transform: [menuPose(-180), menuPose(0)] }, ms: 450, ease: "in-out" },
|
|
1674
|
+
{ id: "bg", step: 2, attrs: { fill: ["{{active}}", "{{button}}"] }, ms: 300, ease: "in-out" }
|
|
1675
|
+
]
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
var BOOKMARK = "M41 22 H79 Q84 22 84 27 V98 L60 81 L36 98 V27 Q36 22 41 22 Z";
|
|
1679
|
+
var bounce = (y) => `translate(0 ${y})`;
|
|
1680
|
+
var bookmark = {
|
|
1681
|
+
kind: "vector",
|
|
1682
|
+
id: "bookmark",
|
|
1683
|
+
name: "Bookmark",
|
|
1684
|
+
description: "A bookmark that hops and fills from top to bottom when saved, then drains and dips when unsaved.",
|
|
1685
|
+
category: "interaction",
|
|
1686
|
+
tags: ["interactive", "ui", "click", "bookmark", "save", "toggle", "micro-interaction"],
|
|
1687
|
+
trigger: "click",
|
|
1688
|
+
hold: 900,
|
|
1689
|
+
presets: [
|
|
1690
|
+
{ name: "Gold", values: { fill: [0.98, 0.72, 0.1] } },
|
|
1691
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], outline: [0.6, 0.63, 0.7], fill: [0.4, 0.8, 1] } }
|
|
1692
|
+
],
|
|
1693
|
+
motion: {
|
|
1694
|
+
svg: `<svg viewBox="0 0 120 120"><!-- {{background: color default=#FFFFFF label='Background'}} {{outline: color default=#6B7280 label='Outline'}} {{fill: color default=#6366F1 label='Saved'}} --><defs><clipPath id="fillClip"><rect id="level" x="30" y="18" width="60" height="0"/></clipPath></defs><rect width="120" height="120" fill="{{background}}"/><g id="mark" transform="${bounce(0)}"><path d="${BOOKMARK}" fill="{{fill}}" clip-path="url(#fillClip)"/><path id="edge" d="${BOOKMARK}" fill="none" stroke="{{outline}}" stroke-width="4" stroke-linejoin="round"/></g></svg>`,
|
|
1695
|
+
animate: [
|
|
1696
|
+
// Step 1, save: a hop, a drop past rest and a settle, filling top down.
|
|
1697
|
+
{ id: "mark", step: 1, attrs: { transform: [bounce(0), bounce(-7)] }, ms: 130, ease: "out" },
|
|
1698
|
+
{ id: "mark", step: 1, attrs: { transform: [bounce(-7), bounce(4)] }, delay: 130, ms: 170, ease: "in-out" },
|
|
1699
|
+
{ id: "mark", step: 1, attrs: { transform: [bounce(4), bounce(0)] }, delay: 300, ms: 200, ease: "in-out" },
|
|
1700
|
+
{ id: "level", step: 1, attrs: { height: [0, 84] }, delay: 60, ms: 380, ease: "in-out" },
|
|
1701
|
+
{ id: "edge", step: 1, attrs: { stroke: ["{{outline}}", "{{fill}}"] }, ms: 200 },
|
|
1702
|
+
// Step 2, unsave: drains top down (then the empty clip snaps back up) with a small dip.
|
|
1703
|
+
{ id: "level", step: 2, attrs: { y: [18, 102], height: [84, 0] }, ms: 320, ease: "in-out" },
|
|
1704
|
+
snap("level", "y", 18, 320, 2),
|
|
1705
|
+
{ id: "mark", step: 2, attrs: { transform: [bounce(0), bounce(3)] }, ms: 120, ease: "out" },
|
|
1706
|
+
{ id: "mark", step: 2, attrs: { transform: [bounce(3), bounce(0)] }, delay: 120, ms: 180, ease: "in-out" },
|
|
1707
|
+
{ id: "edge", step: 2, attrs: { stroke: ["{{fill}}", "{{outline}}"] }, delay: 100, ms: 200 }
|
|
1708
|
+
]
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
var swapLabels = (outAt, back) => [
|
|
1712
|
+
{ id: "label", step: 1, attrs: { opacity: [1, 0], transform: ["translate(0 0)", "translate(0 -10)"] }, delay: outAt, ms: 180, ease: "out" },
|
|
1713
|
+
{ id: "done", step: 1, attrs: { opacity: [0, 1], transform: ["translate(0 10)", "translate(0 0)"] }, delay: outAt + 120, ms: 220, ease: "out" },
|
|
1714
|
+
{ id: "done", step: 1, attrs: { opacity: [1, 0], transform: ["translate(0 0)", "translate(0 -10)"] }, delay: back, ms: 180, ease: "out" },
|
|
1715
|
+
{ id: "label", step: 1, attrs: { opacity: [0, 1], transform: ["translate(0 10)", "translate(0 0)"] }, delay: back + 120, ms: 220, ease: "out" },
|
|
1716
|
+
snap("done", "transform", "translate(0 10)", back + 200)
|
|
1717
|
+
];
|
|
1718
|
+
var checkFlash = (at, back) => [
|
|
1719
|
+
{ id: "check", step: 1, attrs: { opacity: [0, 1] }, delay: at, ms: 40 },
|
|
1720
|
+
{ id: "check", step: 1, draw: true, delay: at, ms: 280, ease: "out" },
|
|
1721
|
+
{ id: "check", step: 1, attrs: { opacity: [1, 0] }, delay: back, ms: 160 },
|
|
1722
|
+
{ id: "check", step: 1, draw: [0, 0], delay: back + 200, ms: 0 }
|
|
1723
|
+
];
|
|
1724
|
+
var buttonLabels = (a, b, room, max) => {
|
|
1725
|
+
const ems = [a, b].map((t) => Math.max(0.6, textWidthEm(String(t))));
|
|
1726
|
+
const size = Math.min(max, room / Math.max(...ems));
|
|
1727
|
+
return { size: r1(size), base: r1(50 + size * 0.35), labelLen: r1(ems[0] * size), doneLen: r1(ems[1] * size) };
|
|
1728
|
+
};
|
|
1729
|
+
var labelTexts = (x) => `<text id="label" x="${x}" y="{{base}}" textLength="{{labelLen}}" lengthAdjust="spacingAndGlyphs" transform="translate(0 0)" font-family="${SANS}" font-size="{{size}}" font-weight="600" fill="{{ink}}">{{label}}</text><text id="done" x="${x}" y="{{base}}" textLength="{{doneLen}}" lengthAdjust="spacingAndGlyphs" transform="translate(0 10)" opacity="0" font-family="${SANS}" font-size="{{size}}" font-weight="600" fill="{{ink}}">{{doneText}}</text>`;
|
|
1730
|
+
var COPY_BACK = 1600;
|
|
1731
|
+
var copyPose = (s) => `translate(54 50) scale(${r2(1.2 * s)}) translate(-12 -12)`;
|
|
1732
|
+
var copyButton = {
|
|
1733
|
+
kind: "vector",
|
|
1734
|
+
id: "copy-button",
|
|
1735
|
+
name: "Copy button",
|
|
1736
|
+
description: "A copy button whose icon shrinks away for a drawn check and a \u201CCopied!\u201D label, turning back by itself a moment later.",
|
|
1737
|
+
category: "interaction",
|
|
1738
|
+
tags: ["interactive", "ui", "click", "copy", "clipboard", "feedback", "button", "micro-interaction"],
|
|
1739
|
+
trigger: "click",
|
|
1740
|
+
hold: 600,
|
|
1741
|
+
presets: [
|
|
1742
|
+
{ name: "Light", values: { background: [1, 1, 1], button: [0.95, 0.96, 0.97], success: [0.86, 0.97, 0.9], ink: [0.07, 0.09, 0.15] } },
|
|
1743
|
+
{ name: "Code", values: { label: "Copy code", doneText: "Copied to clipboard", button: [0.2, 0.22, 0.28] } }
|
|
1744
|
+
],
|
|
1745
|
+
derived: (v) => buttonLabels(v.label, v.doneText, 126, 19),
|
|
1746
|
+
motion: {
|
|
1747
|
+
svg: `<svg viewBox="0 0 240 100"><!-- {{label: text default='Copy' label='Label'}} {{doneText: text default='Copied!' label='Copied label'}} {{background: color default=#0F172A label='Background'}} {{button: color default=#1E293B label='Button'}} {{success: color default=#166534 label='Copied'}} {{ink: color default=#F8FAFC label='Text and icon'}} --><rect width="240" height="100" fill="{{background}}"/><rect id="btn" x="20" y="24" width="200" height="52" rx="14" fill="{{button}}"/><g id="copy" transform="${copyPose(1)}" opacity="1" fill="none" stroke="{{ink}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8.5" y="8.5" width="11.5" height="11.5" rx="2.5"/><path d="M4.5 15.5 V6.5 C4.5 5.4 5.4 4.5 6.5 4.5 H15.5"/></g><g transform="${copyPose(1)}"><path id="check" d="M5 12.5 L10 17.5 L19 7" fill="none" stroke="{{ink}}" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" opacity="0"/></g>${labelTexts(80)}</svg>`,
|
|
1748
|
+
animate: [
|
|
1749
|
+
// One step that returns by itself: each click replays it.
|
|
1750
|
+
{ id: "copy", step: 1, attrs: { transform: [copyPose(1), copyPose(0.3)], opacity: [1, 0] }, ms: 180, ease: "out" },
|
|
1751
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{button}}", "{{success}}"] }, ms: 200 },
|
|
1752
|
+
...checkFlash(150, COPY_BACK),
|
|
1753
|
+
...swapLabels(0, COPY_BACK),
|
|
1754
|
+
{ id: "copy", step: 1, attrs: { transform: [copyPose(0.3), copyPose(1)], opacity: [0, 1] }, delay: COPY_BACK + 80, ms: 260, ease: "out" },
|
|
1755
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{success}}", "{{button}}"] }, delay: COPY_BACK, ms: 300 }
|
|
1756
|
+
]
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
var SEND_BACK = 1800;
|
|
1760
|
+
var planeScale = (s) => `scale(${r2(s)}) translate(-12 -12)`;
|
|
1761
|
+
var sendButton = {
|
|
1762
|
+
kind: "vector",
|
|
1763
|
+
id: "send-button",
|
|
1764
|
+
name: "Send button",
|
|
1765
|
+
description: "A send button whose paper plane winds up and flies off up and to the right as the label turns to \u201CSent\u201D with a check, then it resets.",
|
|
1766
|
+
category: "interaction",
|
|
1767
|
+
tags: ["interactive", "ui", "click", "send", "submit", "message", "button", "micro-interaction"],
|
|
1768
|
+
trigger: "click",
|
|
1769
|
+
hold: 600,
|
|
1770
|
+
presets: [
|
|
1771
|
+
{ name: "Sky", values: { button: [0.05, 0.6, 0.9], success: [0.05, 0.45, 0.7] } },
|
|
1772
|
+
{ name: "Mail", values: { label: "Send email", doneText: "Delivered", button: [0.9, 0.3, 0.25], success: [0.13, 0.6, 0.35] } }
|
|
1773
|
+
],
|
|
1774
|
+
derived: (v) => buttonLabels(v.label, v.doneText, 120, 20),
|
|
1775
|
+
motion: {
|
|
1776
|
+
svg: `<svg viewBox="0 0 240 100"><!-- {{label: text default='Send' label='Label'}} {{doneText: text default='Sent' label='Sent label'}} {{background: color default=#FFFFFF label='Background'}} {{button: color default=#4F46E5 label='Button'}} {{success: color default=#16A34A label='Sent'}} {{ink: color default=#FFFFFF label='Text and icon'}} --><rect width="240" height="100" fill="{{background}}"/><rect id="btn" x="20" y="24" width="200" height="52" rx="26" fill="{{button}}"/><path id="flight" d="M56 50 C88 46 150 22 262 -36" fill="none"/><g transform="translate(56 50) scale(1.1) translate(-12 -12)"><path id="check" d="M5 12.5 L10 17.5 L19 7" fill="none" stroke="{{ink}}" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" opacity="0"/></g>${labelTexts(82)}<g id="plane" transform="translate(56 50)"><g id="planeIcon" transform="${planeScale(1.05)}" opacity="1"><path d="M22 2 L15 22 L11 13 L2 9 Z" fill="{{ink}}" stroke="{{ink}}" stroke-width="1.5" stroke-linejoin="round"/><path d="M22 2 L11 13" stroke="{{button}}" stroke-width="1.5" stroke-linecap="round"/></g></g></svg>`,
|
|
1777
|
+
animate: [
|
|
1778
|
+
// Wind up, then fly off along the curve, shrinking and fading.
|
|
1779
|
+
{ id: "planeIcon", step: 1, attrs: { transform: [planeScale(1.05), planeScale(0.88)] }, ms: 120, ease: "out" },
|
|
1780
|
+
{ id: "plane", step: 1, along: "flight", delay: 120, ms: 600, ease: "in-out" },
|
|
1781
|
+
{ id: "planeIcon", step: 1, attrs: { transform: [planeScale(0.88), planeScale(0.5)] }, delay: 120, ms: 600, ease: "in-out" },
|
|
1782
|
+
{ id: "planeIcon", step: 1, attrs: { opacity: [1, 0] }, delay: 520, ms: 200 },
|
|
1783
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{button}}", "{{success}}"] }, delay: 450, ms: 250 },
|
|
1784
|
+
...swapLabels(150, SEND_BACK),
|
|
1785
|
+
...checkFlash(500, SEND_BACK),
|
|
1786
|
+
// Back home: the plane pops in where it started.
|
|
1787
|
+
{ id: "plane", step: 1, along: "flight", at: [0, 0], delay: SEND_BACK, ms: 0 },
|
|
1788
|
+
{ id: "planeIcon", step: 1, attrs: { transform: [planeScale(0), planeScale(1.05)], opacity: [0, 1] }, delay: SEND_BACK + 100, ms: 300, ease: "out" },
|
|
1789
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{success}}", "{{button}}"] }, delay: SEND_BACK, ms: 300 }
|
|
1790
|
+
]
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
var [ARROW, RING] = morphPaths(
|
|
1794
|
+
"M10.6 4 H13.4 V11.6 H17.2 L12 17 L6.8 11.6 H10.6 Z M5 18.6 H19 V20.6 H5 Z",
|
|
1795
|
+
(() => {
|
|
1796
|
+
const circle2 = (r) => `M${Array.from({ length: 48 }, (_, k) => {
|
|
1797
|
+
const a = k / 48 * Math.PI * 2 - Math.PI / 2;
|
|
1798
|
+
return `${r2(12 + r * Math.cos(a))} ${r2(12 + r * Math.sin(a))}`;
|
|
1799
|
+
}).join(" L")} Z`;
|
|
1800
|
+
return `${circle2(9.6)} ${circle2(8)}`;
|
|
1801
|
+
})(),
|
|
1802
|
+
{ points: 56 }
|
|
1803
|
+
);
|
|
1804
|
+
var RING_R = r2(8.8 * 2.2);
|
|
1805
|
+
var DOWNLOAD_BACK = 2300;
|
|
1806
|
+
var iconAt = (y) => `translate(60 ${y}) scale(2.2) translate(-12 -12)`;
|
|
1807
|
+
var downloadButton = {
|
|
1808
|
+
kind: "vector",
|
|
1809
|
+
id: "download-button",
|
|
1810
|
+
name: "Download button",
|
|
1811
|
+
description: "A round download button whose arrow drops and morphs into a progress ring that fills, ends on a check, then resets.",
|
|
1812
|
+
category: "interaction",
|
|
1813
|
+
tags: ["interactive", "ui", "click", "download", "progress", "morph", "button", "micro-interaction"],
|
|
1814
|
+
trigger: "click",
|
|
1815
|
+
hold: 600,
|
|
1816
|
+
presets: [
|
|
1817
|
+
{ name: "Light", values: { background: [1, 1, 1], button: [0.93, 0.95, 1], ink: [0.31, 0.27, 0.9], track: [0.78, 0.8, 0.95], progress: [0.31, 0.27, 0.9], success: [0.86, 0.97, 0.9] } },
|
|
1818
|
+
{ name: "Ocean", values: { button: [0.03, 0.4, 0.6], progress: [0.4, 0.95, 0.85], success: [0.05, 0.55, 0.5] } }
|
|
1819
|
+
],
|
|
1820
|
+
motion: {
|
|
1821
|
+
svg: `<svg viewBox="0 0 120 120"><!-- {{background: color default=#0F172A label='Background'}} {{button: color default=#4F46E5 label='Button'}} {{success: color default=#16A34A label='Done'}} {{ink: color default=#FFFFFF label='Icon'}} {{track: color default=#818CF8 label='Ring track'}} {{progress: color default=#FFFFFF label='Progress'}} --><rect width="120" height="120" fill="{{background}}"/><circle id="btn" cx="60" cy="60" r="42" fill="{{button}}"/><g id="icon" transform="${iconAt(60)}"><path id="glyph" d="${ARROW}" fill="{{ink}}" fill-rule="evenodd"/></g><circle id="bar" cx="60" cy="60" r="${RING_R}" fill="none" stroke="{{progress}}" stroke-width="3.6" transform="rotate(-90 60 60)"/><g transform="translate(60 60) scale(1.5) translate(-12 -12)"><path id="check" d="M6 12.5 L10.5 17 L18.5 8" fill="none" stroke="{{ink}}" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" opacity="0"/></g></svg>`,
|
|
1822
|
+
animate: [
|
|
1823
|
+
// The arrow drops, then rises back as it morphs into the ring's track.
|
|
1824
|
+
{ id: "icon", step: 1, attrs: { transform: [iconAt(60), iconAt(67)] }, ms: 180, ease: "out" },
|
|
1825
|
+
{ id: "icon", step: 1, attrs: { transform: [iconAt(67), iconAt(60)] }, delay: 180, ms: 260, ease: "in-out" },
|
|
1826
|
+
{ id: "glyph", step: 1, attrs: { d: [ARROW, RING], fill: ["{{ink}}", "{{track}}"] }, delay: 180, ms: 340, ease: "in-out" },
|
|
1827
|
+
// Progress round the ring (a flat-ended line, so nothing shows at 0), then a check.
|
|
1828
|
+
{ id: "bar", step: 1, draw: true, delay: 520, ms: 900, ease: "in-out" },
|
|
1829
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{button}}", "{{success}}"] }, delay: 1400, ms: 250 },
|
|
1830
|
+
{ id: "check", step: 1, attrs: { opacity: [0, 1] }, delay: 1450, ms: 40 },
|
|
1831
|
+
{ id: "check", step: 1, draw: true, delay: 1450, ms: 300, ease: "out" },
|
|
1832
|
+
// Reset: check and ring fade, the track morphs back into the arrow.
|
|
1833
|
+
{ id: "check", step: 1, attrs: { opacity: [1, 0] }, delay: DOWNLOAD_BACK, ms: 150 },
|
|
1834
|
+
{ id: "bar", step: 1, attrs: { opacity: [1, 0] }, delay: DOWNLOAD_BACK, ms: 200 },
|
|
1835
|
+
{ id: "check", step: 1, draw: [0, 0], delay: DOWNLOAD_BACK + 200, ms: 0 },
|
|
1836
|
+
{ id: "bar", step: 1, draw: [0, 0], delay: DOWNLOAD_BACK + 200, ms: 0 },
|
|
1837
|
+
snap("bar", "opacity", 1, DOWNLOAD_BACK + 200),
|
|
1838
|
+
{ id: "glyph", step: 1, attrs: { d: [RING, ARROW], fill: ["{{track}}", "{{ink}}"] }, delay: DOWNLOAD_BACK + 100, ms: 420, ease: "in-out" },
|
|
1839
|
+
{ id: "btn", step: 1, attrs: { fill: ["{{success}}", "{{button}}"] }, delay: DOWNLOAD_BACK, ms: 300 }
|
|
1840
|
+
]
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
var linkHover = {
|
|
1844
|
+
kind: "vector",
|
|
1845
|
+
id: "link-hover",
|
|
1846
|
+
name: "Link hover",
|
|
1847
|
+
description: "A text link whose underline grows from left to right on hover, its arrow nudging right as the text changes colour.",
|
|
1848
|
+
category: "interaction",
|
|
1849
|
+
tags: ["interactive", "ui", "hover", "link", "underline", "navigation", "micro-interaction"],
|
|
1850
|
+
trigger: "hover",
|
|
1851
|
+
hold: 800,
|
|
1852
|
+
presets: [
|
|
1853
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], ink: [0.85, 0.87, 0.92], hover: [0.4, 0.85, 1] } },
|
|
1854
|
+
{ name: "Editorial", values: { text: "Continue reading", ink: [0.1, 0.1, 0.1], hover: [0.85, 0.2, 0.15] } }
|
|
1855
|
+
],
|
|
1856
|
+
// The text and its arrow centred together; the arrow is drawn round (0, 0) and placed.
|
|
1857
|
+
derived: (v) => {
|
|
1858
|
+
const { size, length } = fit(v.text, 230, 28);
|
|
1859
|
+
const total = length + size * 1.05;
|
|
1860
|
+
const left = 160 - total / 2;
|
|
1861
|
+
const base = 50 + size * 0.35;
|
|
1862
|
+
const ax = left + length + size * 0.35;
|
|
1863
|
+
const ay = base - size * 0.33;
|
|
1864
|
+
const s = size / 28;
|
|
1865
|
+
return {
|
|
1866
|
+
size,
|
|
1867
|
+
length,
|
|
1868
|
+
left: r1(left),
|
|
1869
|
+
base: r1(base),
|
|
1870
|
+
lineY: r1(base + size * 0.2),
|
|
1871
|
+
lineH: r1(Math.max(2, size * 0.08)),
|
|
1872
|
+
arrowAt: `translate(${r1(ax)} ${r1(ay)}) scale(${r2(s)})`,
|
|
1873
|
+
arrowTo: `translate(${r1(ax + size * 0.25)} ${r1(ay)}) scale(${r2(s)})`
|
|
1874
|
+
};
|
|
1875
|
+
},
|
|
1876
|
+
motion: {
|
|
1877
|
+
svg: `<svg viewBox="0 0 320 100"><!-- {{text: text default='Read the docs' label='Link text'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#1F2937 label='Text colour'}} {{hover: color default=#4F46E5 label='Hover colour'}} --><rect width="320" height="100" fill="{{background}}"/><text id="link" x="{{left}}" y="{{base}}" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{size}}" font-weight="600" fill="{{ink}}">{{text}}</text><rect id="under" x="{{left}}" y="{{lineY}}" width="0" height="{{lineH}}" rx="1" fill="{{hover}}"/><g id="arrow" transform="{{arrowAt}}"><path id="arrowPath" d="M0 0 H18 M11 -7 L18 0 L11 7" fill="none" stroke="{{ink}}" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></g></svg>`,
|
|
1878
|
+
animate: [
|
|
1879
|
+
{ id: "link", attrs: { fill: ["{{ink}}", "{{hover}}"] }, ms: 220 },
|
|
1880
|
+
{ id: "under", attrs: { width: [0, n("length")] }, ms: 320, ease: "out" },
|
|
1881
|
+
{ id: "arrow", attrs: { transform: ["{{arrowAt}}", "{{arrowTo}}"] }, ms: 300, ease: "out" },
|
|
1882
|
+
{ id: "arrowPath", attrs: { stroke: ["{{ink}}", "{{hover}}"] }, ms: 220 }
|
|
1883
|
+
]
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
var buttonHoverFill = {
|
|
1887
|
+
kind: "vector",
|
|
1888
|
+
id: "button-hover-fill",
|
|
1889
|
+
name: "Button hover fill",
|
|
1890
|
+
description: "An outlined pill button that fills with a slanted sweep from the left on hover, its label inverting colour as the fill passes.",
|
|
1891
|
+
category: "interaction",
|
|
1892
|
+
tags: ["interactive", "ui", "hover", "button", "fill", "cta", "micro-interaction"],
|
|
1893
|
+
trigger: "hover",
|
|
1894
|
+
hold: 800,
|
|
1895
|
+
presets: [
|
|
1896
|
+
{ name: "Dark", values: { background: [0.06, 0.07, 0.1], surface: [0.06, 0.07, 0.1], accent: [0.98, 0.8, 0.2] } },
|
|
1897
|
+
{ name: "Coral", values: { label: "Book a demo", accent: [0.96, 0.35, 0.3] } }
|
|
1898
|
+
],
|
|
1899
|
+
derived: (v) => {
|
|
1900
|
+
const { size, length } = fit(v.label, 190, 22);
|
|
1901
|
+
return { size, length, base: r1(60 + size * 0.35) };
|
|
1902
|
+
},
|
|
1903
|
+
motion: (() => {
|
|
1904
|
+
const sweep = (x) => `M10 26 H${x} L${x - 24} 94 H10 Z`;
|
|
1905
|
+
const label = (fill, clip = "") => `<text x="160" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{size}}" font-weight="700" letter-spacing="0.5" fill="${fill}"${clip}>{{label}}</text>`;
|
|
1906
|
+
return {
|
|
1907
|
+
svg: `<svg viewBox="0 0 320 120"><!-- {{label: text default='Get started' label='Label'}} {{background: color default=#FFFFFF label='Background'}} {{surface: color default=#FFFFFF label='Button'}} {{accent: color default=#4F46E5 label='Accent'}} --><defs><clipPath id="swept"><path id="cover" d="${sweep(40)}"/></clipPath></defs><rect width="320" height="120" fill="{{background}}"/><g id="btn" transform="translate(0 0)"><rect x="40" y="30" width="240" height="60" rx="30" fill="{{surface}}" stroke="{{accent}}" stroke-width="2.5"/><rect x="40" y="30" width="240" height="60" rx="30" fill="{{accent}}" stroke="{{accent}}" stroke-width="2.5" clip-path="url(#swept)"/>${label("{{accent}}")}${label("{{surface}}", ` clip-path="url(#swept)"`)}</g></svg>`,
|
|
1908
|
+
animate: [
|
|
1909
|
+
{ id: "cover", attrs: { d: [sweep(40), sweep(310)] }, ms: 380, ease: "in-out" },
|
|
1910
|
+
{ id: "btn", attrs: { transform: ["translate(0 0)", "translate(0 -2)"] }, ms: 240, ease: "out" }
|
|
1911
|
+
]
|
|
1912
|
+
};
|
|
1913
|
+
})()
|
|
1914
|
+
};
|
|
1915
|
+
var tipPose = (s, dy) => `translate(160 ${104 + dy}) scale(${r2(s)}) translate(-160 -104)`;
|
|
1916
|
+
var tooltipPop = {
|
|
1917
|
+
kind: "vector",
|
|
1918
|
+
id: "tooltip-pop",
|
|
1919
|
+
name: "Tooltip pop",
|
|
1920
|
+
description: "A small help badge that pops a tooltip bubble up above it on hover, rising and fading in with a slight overshoot.",
|
|
1921
|
+
category: "interaction",
|
|
1922
|
+
tags: ["interactive", "ui", "hover", "tooltip", "help", "popover", "micro-interaction"],
|
|
1923
|
+
trigger: "hover",
|
|
1924
|
+
hold: 800,
|
|
1925
|
+
presets: [
|
|
1926
|
+
{ name: "Light", values: { background: [0.97, 0.97, 0.98], bubble: [1, 1, 1], tipInk: [0.1, 0.1, 0.15], badge: [0.6, 0.63, 0.7] } },
|
|
1927
|
+
{ name: "Warning", values: { tip: "This can't be undone", accent: [0.95, 0.4, 0.15], bubble: [0.95, 0.4, 0.15], tipInk: [1, 1, 1] } }
|
|
1928
|
+
],
|
|
1929
|
+
// The bubble is as wide as its text, centred over the badge.
|
|
1930
|
+
derived: (v) => {
|
|
1931
|
+
const { size, length } = fit(v.tip, 250, 15);
|
|
1932
|
+
const w = length + 28;
|
|
1933
|
+
return { size, length, bx: r1(160 - w / 2), bw: r1(w), tipY: r1(79 + size * 0.35) };
|
|
1934
|
+
},
|
|
1935
|
+
motion: {
|
|
1936
|
+
svg: `<svg viewBox="0 0 320 180"><!-- {{tip: text default='Saved to your library' label='Tooltip'}} {{background: color default=#0F172A label='Background'}} {{badge: color default=#475569 label='Badge'}} {{accent: color default=#6366F1 label='Badge hovered'}} {{bubble: color default=#F8FAFC label='Bubble'}} {{tipInk: color default=#0F172A label='Tooltip text'}} --><rect width="320" height="180" fill="{{background}}"/><g id="tip" transform="${tipPose(0.5, 8)}" opacity="0"><rect x="{{bx}}" y="60" width="{{bw}}" height="38" rx="10" fill="{{bubble}}"/><path d="M151 96 L160 105 L169 96 Z" fill="{{bubble}}"/><text x="160" y="{{tipY}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{size}}" font-weight="500" fill="{{tipInk}}">{{tip}}</text></g><g id="badgeG" transform="${about(160, 134, 1)}"><circle id="badge" cx="160" cy="134" r="18" fill="{{badge}}"/><text x="160" y="141" text-anchor="middle" font-family="${SANS}" font-size="20" font-weight="700" fill="#FFFFFF">?</text></g></svg>`,
|
|
1937
|
+
animate: [
|
|
1938
|
+
{ id: "tip", attrs: { transform: [tipPose(0.5, 8), tipPose(1.07, -3)] }, ms: 190, ease: "out" },
|
|
1939
|
+
{ id: "tip", attrs: { transform: [tipPose(1.07, -3), tipPose(1, 0)] }, delay: 190, ms: 150, ease: "in-out" },
|
|
1940
|
+
{ id: "tip", attrs: { opacity: [0, 1] }, ms: 150 },
|
|
1941
|
+
{ id: "badge", attrs: { fill: ["{{badge}}", "{{accent}}"] }, ms: 200 },
|
|
1942
|
+
{ id: "badgeG", attrs: { transform: [about(160, 134, 1), about(160, 134, 1.08)] }, ms: 200, ease: "out" }
|
|
1943
|
+
]
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
var pressButton = {
|
|
1947
|
+
kind: "vector",
|
|
1948
|
+
id: "press-button",
|
|
1949
|
+
name: "Press button",
|
|
1950
|
+
description: "A chunky 3D button whose face pushes down onto its base while held, its shadow tightening, and springs back up on release.",
|
|
1951
|
+
category: "interaction",
|
|
1952
|
+
tags: ["interactive", "ui", "press", "button", "3d", "tactile", "micro-interaction"],
|
|
1953
|
+
trigger: "press",
|
|
1954
|
+
hold: 800,
|
|
1955
|
+
presets: [
|
|
1956
|
+
{ name: "Arcade red", values: { face: [0.94, 0.27, 0.27], background: [0.1, 0.09, 0.12] } },
|
|
1957
|
+
{ name: "Mint", values: { face: [0.3, 0.85, 0.6], ink: [0.05, 0.25, 0.15], label: "Let's go" } }
|
|
1958
|
+
],
|
|
1959
|
+
derived: (v) => {
|
|
1960
|
+
const { size, length } = fit(v.label, 170, 22);
|
|
1961
|
+
return { size, length, base: r1(67 + size * 0.35), side: mixHex(v.face, [0, 0, 0], 0.35), pressed: mixHex(v.face, [0, 0, 0], 0.08) };
|
|
1962
|
+
},
|
|
1963
|
+
// Played backwards on release, the brief lift at the start of the press becomes a spring past rest at the end.
|
|
1964
|
+
motion: {
|
|
1965
|
+
svg: `<svg viewBox="0 0 320 140"><!-- {{label: text default='Press me' label='Label'}} {{background: color default=#F1F5F9 label='Background'}} {{face: color default=#6366F1 label='Button'}} {{ink: color default=#FFFFFF label='Label colour'}} {{shadow: color default=#0F172A label='Shadow'}} --><rect width="320" height="140" fill="{{background}}"/><ellipse id="shadow" cx="160" cy="114" rx="108" ry="9" fill="{{shadow}}" opacity="0.3"/><rect x="60" y="50" width="200" height="58" rx="16" fill="{{side}}"/><g id="faceG" transform="translate(0 0)"><rect id="faceRect" x="60" y="38" width="200" height="58" rx="16" fill="{{face}}"/><text x="160" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS}" font-size="{{size}}" font-weight="800" fill="{{ink}}">{{label}}</text></g></svg>`,
|
|
1966
|
+
animate: [
|
|
1967
|
+
{ id: "faceG", attrs: { transform: ["translate(0 0)", "translate(0 -1.5)"] }, ms: 40, ease: "out" },
|
|
1968
|
+
{ id: "faceG", attrs: { transform: ["translate(0 -1.5)", "translate(0 9)"] }, delay: 40, ms: 90, ease: "out" },
|
|
1969
|
+
{ id: "shadow", attrs: { rx: [108, 96], ry: [9, 5], opacity: [0.3, 0.18] }, delay: 40, ms: 90, ease: "out" },
|
|
1970
|
+
{ id: "faceRect", attrs: { fill: ["{{face}}", "{{pressed}}"] }, delay: 40, ms: 90 }
|
|
1971
|
+
]
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
var interactionEffects = [
|
|
1975
|
+
likeButton,
|
|
1976
|
+
toggleSwitch,
|
|
1977
|
+
checkbox,
|
|
1978
|
+
menuToggle,
|
|
1979
|
+
bookmark,
|
|
1980
|
+
copyButton,
|
|
1981
|
+
sendButton,
|
|
1982
|
+
downloadButton,
|
|
1983
|
+
linkHover,
|
|
1984
|
+
buttonHoverFill,
|
|
1985
|
+
tooltipPop,
|
|
1986
|
+
pressButton
|
|
1987
|
+
];
|
|
1988
|
+
|
|
1989
|
+
// src/catalog/morph.ts
|
|
1990
|
+
var NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi;
|
|
1991
|
+
var DEG = Math.PI / 180;
|
|
1992
|
+
var r22 = (v) => {
|
|
1993
|
+
const r = Math.round(v * 100) / 100;
|
|
1994
|
+
return Object.is(r, -0) ? 0 : r;
|
|
1995
|
+
};
|
|
1996
|
+
var n2 = (name) => `{{${name}}}`;
|
|
1997
|
+
var pointsOf = (d) => {
|
|
1998
|
+
const nums = (d.match(NUMBER) ?? []).map(Number);
|
|
1999
|
+
return Array.from({ length: nums.length >> 1 }, (_, i) => [nums[2 * i], nums[2 * i + 1]]);
|
|
2000
|
+
};
|
|
2001
|
+
function mapPath(d, f) {
|
|
2002
|
+
const moved = pointsOf(d).map(f);
|
|
2003
|
+
let i = 0;
|
|
2004
|
+
return d.replace(NUMBER, () => {
|
|
2005
|
+
const k = i++;
|
|
2006
|
+
return String(r22(moved[k >> 1][k % 2]));
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
var polyline = (pts, close = false) => `M${pts.map((p) => `${r22(p[0])} ${r22(p[1])}`).join(" L")}${close ? " Z" : ""}`;
|
|
2010
|
+
function arc(cx, cy, rx, ry, from, to, every = 8) {
|
|
2011
|
+
const steps = Math.max(1, Math.ceil(Math.abs(to - from) / every));
|
|
2012
|
+
return Array.from({ length: steps + 1 }, (_, k) => {
|
|
2013
|
+
const a = (from + (to - from) * k / steps) * DEG;
|
|
2014
|
+
return [cx + rx * Math.cos(a), cy + ry * Math.sin(a)];
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
var circle = (cx, cy, r) => polyline(arc(cx, cy, r, r, 0, 360).slice(0, -1), true);
|
|
2018
|
+
var star = (cx, cy, tips, outer, inner) => polyline(
|
|
2019
|
+
Array.from({ length: tips * 2 }, (_, k) => {
|
|
2020
|
+
const r = k % 2 ? inner : outer;
|
|
2021
|
+
const a = (-90 + k * 180 / tips) * DEG;
|
|
2022
|
+
return [cx + r * Math.cos(a), cy + r * Math.sin(a)];
|
|
2023
|
+
}),
|
|
2024
|
+
true
|
|
2025
|
+
);
|
|
2026
|
+
function stroke(pts, width, flat = {}) {
|
|
2027
|
+
const h = width / 2;
|
|
2028
|
+
const normals = pts.slice(1).map((p, i) => {
|
|
2029
|
+
const q = pts[i];
|
|
2030
|
+
const l = Math.hypot(p[0] - q[0], p[1] - q[1]);
|
|
2031
|
+
return [-(p[1] - q[1]) / l, (p[0] - q[0]) / l];
|
|
2032
|
+
});
|
|
2033
|
+
const offsets = pts.map((_, i) => {
|
|
2034
|
+
const a = normals[Math.max(0, i - 1)];
|
|
2035
|
+
const b = normals[Math.min(normals.length - 1, i)];
|
|
2036
|
+
const l = Math.hypot(a[0] + b[0], a[1] + b[1]);
|
|
2037
|
+
const m = [(a[0] + b[0]) / l, (a[1] + b[1]) / l];
|
|
2038
|
+
const k = h / (m[0] * b[0] + m[1] * b[1]);
|
|
2039
|
+
return [m[0] * k, m[1] * k];
|
|
2040
|
+
});
|
|
2041
|
+
const left = pts.map((p, i) => [p[0] + offsets[i][0], p[1] + offsets[i][1]]);
|
|
2042
|
+
const right = pts.map((p, i) => [p[0] - offsets[i][0], p[1] - offsets[i][1]]).reverse();
|
|
2043
|
+
const angle = (v) => Math.atan2(v[1], v[0]) / DEG;
|
|
2044
|
+
const [first, last] = [pts[0], pts[pts.length - 1]];
|
|
2045
|
+
const endAt = angle(normals[normals.length - 1]);
|
|
2046
|
+
const startAt = angle(normals[0]) + 180;
|
|
2047
|
+
const endCap = flat.end ? [] : arc(last[0], last[1], h, h, endAt, endAt - 180, 15).slice(1, -1);
|
|
2048
|
+
const startCap = flat.start ? [] : arc(first[0], first[1], h, h, startAt, startAt - 180, 15).slice(1, -1);
|
|
2049
|
+
return polyline([...left, ...endCap, ...right, ...startCap], true);
|
|
2050
|
+
}
|
|
2051
|
+
function around(c, r, from, to, avoid) {
|
|
2052
|
+
let delta = ((to - from) % 360 + 360) % 360;
|
|
2053
|
+
if (((avoid - from) % 360 + 360) % 360 < delta) delta -= 360;
|
|
2054
|
+
return arc(c[0], c[1], r, r, from, from + delta);
|
|
2055
|
+
}
|
|
2056
|
+
function crescent(c1, r13, c2, r2_) {
|
|
2057
|
+
const [dx, dy] = [c2[0] - c1[0], c2[1] - c1[1]];
|
|
2058
|
+
const d = Math.hypot(dx, dy);
|
|
2059
|
+
const a = (r13 * r13 - r2_ * r2_ + d * d) / (2 * d);
|
|
2060
|
+
const h = Math.sqrt(Math.max(0, r13 * r13 - a * a));
|
|
2061
|
+
const [px, py] = [c1[0] + a * dx / d, c1[1] + a * dy / d];
|
|
2062
|
+
const i1 = [px - h * dy / d, py + h * dx / d];
|
|
2063
|
+
const i2 = [px + h * dy / d, py - h * dx / d];
|
|
2064
|
+
const angle = (c, p) => Math.atan2(p[1] - c[1], p[0] - c[0]) / DEG;
|
|
2065
|
+
const outer = around(c1, r13, angle(c1, i1), angle(c1, i2), angle(c1, c2));
|
|
2066
|
+
const inner = around(c2, r2_, angle(c2, i2), angle(c2, i1), angle(c2, c1) + 180);
|
|
2067
|
+
return polyline([...outer, ...inner.slice(1, -1)], true);
|
|
2068
|
+
}
|
|
2069
|
+
var HEART3 = "M12 20.5 C12 20.5 3 14.8 3 8.8 C3 6 5.2 3.8 8 3.8 C9.7 3.8 11.2 4.7 12 6.1 C12.8 4.7 14.3 3.8 16 3.8 C18.8 3.8 21 6 21 8.8 C21 14.8 12 20.5 12 20.5 Z";
|
|
2070
|
+
var placed = (d, cx, cy, s) => mapPath(d, (p) => [cx + (p[0] - 12) * s, cy + (p[1] - 12) * s]);
|
|
2071
|
+
function fitted(d, box, points) {
|
|
2072
|
+
const outline = morphSequence([d], { points })[0];
|
|
2073
|
+
const pts = pointsOf(outline);
|
|
2074
|
+
const xs = pts.map((p) => p[0]);
|
|
2075
|
+
const ys = pts.map((p) => p[1]);
|
|
2076
|
+
const [x0, y0] = [Math.min(...xs), Math.min(...ys)];
|
|
2077
|
+
const [w, h] = [Math.max(...xs) - x0, Math.max(...ys) - y0];
|
|
2078
|
+
const s = Math.min(box[2] / Math.max(w, 1e-6), box[3] / Math.max(h, 1e-6));
|
|
2079
|
+
const [ox, oy] = [box[0] + (box[2] - w * s) / 2, box[1] + (box[3] - h * s) / 2];
|
|
2080
|
+
return mapPath(outline, (p) => [ox + (p[0] - x0) * s, oy + (p[1] - y0) * s]);
|
|
2081
|
+
}
|
|
2082
|
+
function fittedOr(d, fallback, box, points) {
|
|
2083
|
+
try {
|
|
2084
|
+
return fitted(String(d), box, points);
|
|
2085
|
+
} catch {
|
|
2086
|
+
return fitted(fallback, box, points);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
var SHAPE_FROM = "M30 12 H70 A18 18 0 0 1 88 30 V70 A18 18 0 0 1 70 88 H30 A18 18 0 0 1 12 70 V30 A18 18 0 0 1 30 12 Z";
|
|
2090
|
+
var SHAPE_TO = "M50 6 L61 38 L95 38 L67 58 L78 92 L50 72 L22 92 L33 58 L5 38 L39 38 Z";
|
|
2091
|
+
var shapeMorph = {
|
|
2092
|
+
kind: "vector",
|
|
2093
|
+
id: "shape-morph",
|
|
2094
|
+
name: "Shape morph",
|
|
2095
|
+
description: "Any two SVG paths morphing into each other and back, their fill and outline colours blending along.",
|
|
2096
|
+
category: "morph",
|
|
2097
|
+
tags: ["morph", "shape", "path", "icon", "logo"],
|
|
2098
|
+
hold: 0,
|
|
2099
|
+
presets: [
|
|
2100
|
+
{ name: "Ink", values: { background: [0.98, 0.97, 0.94], fillFrom: [0.1, 0.1, 0.12], fillTo: [0.1, 0.1, 0.12], strokeFrom: [0.1, 0.1, 0.12], strokeTo: [0.1, 0.1, 0.12], width: 0 } },
|
|
2101
|
+
{
|
|
2102
|
+
name: "Heart to drop",
|
|
2103
|
+
values: {
|
|
2104
|
+
from: "M12 20.5 C12 20.5 3 14.8 3 8.8 C3 6 5.2 3.8 8 3.8 C9.7 3.8 11.2 4.7 12 6.1 C12.8 4.7 14.3 3.8 16 3.8 C18.8 3.8 21 6 21 8.8 C21 14.8 12 20.5 12 20.5 Z",
|
|
2105
|
+
to: "M12 2 C12 2 5 10 5 15 A7 7 0 0 0 19 15 C19 10 12 2 12 2 Z",
|
|
2106
|
+
fillFrom: [0.96, 0.25, 0.37],
|
|
2107
|
+
fillTo: [0.22, 0.6, 0.97]
|
|
2108
|
+
}
|
|
2109
|
+
},
|
|
2110
|
+
{ name: "Outline", values: { fillFrom: [0.06, 0.09, 0.16], fillTo: [0.06, 0.09, 0.16], strokeFrom: [0.2, 0.83, 0.6], strokeTo: [0.98, 0.8, 0.08], width: 2.5 } }
|
|
2111
|
+
],
|
|
2112
|
+
// Each path is fitted to the picture on its own, so paths in any coordinates fill it.
|
|
2113
|
+
derived: (v) => {
|
|
2114
|
+
const [a, b] = morphPaths(fittedOr(v.from, SHAPE_FROM, [12, 12, 76, 76], 96), fittedOr(v.to, SHAPE_TO, [12, 12, 76, 76], 96), { points: 80 });
|
|
2115
|
+
return { a, b, back: Number(v.pause) * 2 + Number(v.morph) };
|
|
2116
|
+
},
|
|
2117
|
+
motion: {
|
|
2118
|
+
svg: `<svg viewBox="0 0 100 100"><!-- {{from: text default='${SHAPE_FROM}' label='From path'}} {{to: text default='${SHAPE_TO}' label='To path'}} {{fillFrom: color default=#6366F1 label='Fill (from)'}} {{fillTo: color default=#F59E0B label='Fill (to)'}} {{strokeFrom: color default=#C7D2FE label='Outline (from)'}} {{strokeTo: color default=#FEF3C7 label='Outline (to)'}} {{background: color default=#0F172A label='Background'}} {{width: float min=0 max=6 default=1.5 step=0.25 label='Outline width'}} {{morph: float min=200 max=3000 default=1000 step=50 label='Morph ms'}} {{pause: float min=0 max=3000 default=700 step=50 label='Pause ms'}} --><rect width="100" height="100" fill="{{background}}"/><path id="shape" d="{{a}}" fill="{{fillFrom}}" fill-rule="evenodd" stroke="{{strokeFrom}}" stroke-width="{{width}}" stroke-linejoin="round"/></svg>`,
|
|
2119
|
+
animate: [
|
|
2120
|
+
{ id: "shape", attrs: { d: ["{{a}}", "{{b}}"], fill: ["{{fillFrom}}", "{{fillTo}}"], stroke: ["{{strokeFrom}}", "{{strokeTo}}"] }, delay: n2("pause"), ms: n2("morph"), ease: "in-out" },
|
|
2121
|
+
{ id: "shape", attrs: { d: ["{{b}}", "{{a}}"], fill: ["{{fillTo}}", "{{fillFrom}}"], stroke: ["{{strokeTo}}", "{{strokeFrom}}"] }, delay: n2("back"), ms: n2("morph"), ease: "in-out" }
|
|
2122
|
+
]
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
var LINE = 2.4;
|
|
2126
|
+
var ICON_PAIRS = (() => {
|
|
2127
|
+
const c = 12;
|
|
2128
|
+
const meet = LINE / 2 / Math.SQRT2;
|
|
2129
|
+
const sun = [circle(c, c, 4.6), ...Array.from({ length: 8 }, (_, k) => {
|
|
2130
|
+
const a = k * 45 * DEG;
|
|
2131
|
+
return stroke([[c + 7.6 * Math.cos(a), c + 7.6 * Math.sin(a)], [c + 10 * Math.cos(a), c + 10 * Math.sin(a)]], 2);
|
|
2132
|
+
})];
|
|
2133
|
+
return [
|
|
2134
|
+
[
|
|
2135
|
+
"M7.5 4.5 L13.5 8.32 L13.5 15.68 L7.5 19.5 Z M13.5 8.32 L19.3 12 L13.5 15.68 Z",
|
|
2136
|
+
[stroke([[8.5, 6.5], [8.5, 17.5]], 3.6), stroke([[15.5, 6.5], [15.5, 17.5]], 3.6)].join(" ")
|
|
2137
|
+
],
|
|
2138
|
+
[
|
|
2139
|
+
[7, 12, 17].map((y) => stroke([[5, y], [19, y]], LINE)).join(" "),
|
|
2140
|
+
[
|
|
2141
|
+
stroke([[6.5, 6.5], [17.5, 17.5]], LINE),
|
|
2142
|
+
stroke([[17.5, 6.5], [c + meet, c - meet]], LINE, { end: true }),
|
|
2143
|
+
stroke([[6.5, 17.5], [c - meet, c + meet]], LINE, { end: true })
|
|
2144
|
+
].join(" ")
|
|
2145
|
+
],
|
|
2146
|
+
["M10.7 5 H13.3 V10.7 H19 V13.3 H13.3 V19 H10.7 V13.3 H5 V10.7 H10.7 Z", stroke([[5, 12.5], [10, 17.5], [19, 7]], 2.6)],
|
|
2147
|
+
[sun.join(" "), crescent([11.5, 12.5], 8.5, [16.5, 7.5], 7)],
|
|
2148
|
+
[`${HEART3} ${placed(HEART3, 12, 12, 0.7)}`, HEART3]
|
|
2149
|
+
];
|
|
2150
|
+
})();
|
|
2151
|
+
var iconMorphs = /* @__PURE__ */ new Map();
|
|
2152
|
+
function iconMorph(pair2) {
|
|
2153
|
+
if (!iconMorphs.has(pair2)) iconMorphs.set(pair2, morphPaths(...ICON_PAIRS[pair2], { points: 64 }));
|
|
2154
|
+
return iconMorphs.get(pair2);
|
|
2155
|
+
}
|
|
2156
|
+
var pose = (turn, scale) => `translate(60 60) rotate(${turn}) scale(${scale}) translate(-12 -12)`;
|
|
2157
|
+
var toggle = (at, d, fill, turn) => [
|
|
2158
|
+
{ id: "glyph", attrs: { d }, delay: at, ms: 420, ease: "in-out" },
|
|
2159
|
+
{ id: "icon", attrs: { transform: [pose(0, 2.6), pose(turn, 2.15)] }, delay: at, ms: 170, ease: "out" },
|
|
2160
|
+
{ id: "icon", attrs: { transform: [pose(turn, 2.15), pose(-turn * 0.3, 2.85)] }, delay: at + 170, ms: 190, ease: "in-out" },
|
|
2161
|
+
{ id: "icon", attrs: { transform: [pose(-turn * 0.3, 2.85), pose(0, 2.6)] }, delay: at + 360, ms: 260, ease: "in-out" },
|
|
2162
|
+
{ id: "button", attrs: { fill }, delay: at, ms: 320, ease: "in-out" }
|
|
2163
|
+
];
|
|
2164
|
+
var iconToggle = {
|
|
2165
|
+
kind: "vector",
|
|
2166
|
+
id: "icon-toggle",
|
|
2167
|
+
name: "Icon toggle",
|
|
2168
|
+
description: "A round button whose icon morphs between two states (play and pause, menu and close, sun and moon) with a little pop.",
|
|
2169
|
+
category: "morph",
|
|
2170
|
+
tags: ["morph", "icon", "ui", "button", "toggle", "micro-interaction"],
|
|
2171
|
+
hold: 700,
|
|
2172
|
+
presets: [
|
|
2173
|
+
{ name: "Play / pause", values: { pair: 0 } },
|
|
2174
|
+
{ name: "Menu / close", values: { pair: 1, off: [0.98, 0.98, 0.98], on: [0.07, 0.07, 0.09], ink: [0.4, 0.4, 0.45], page: [0.9, 0.91, 0.93] } },
|
|
2175
|
+
{ name: "Plus / check", values: { pair: 2, on: [0.13, 0.77, 0.37] } },
|
|
2176
|
+
{ name: "Day / night", values: { pair: 3, off: [0.98, 0.75, 0.14], on: [0.19, 0.18, 0.5], page: [0.06, 0.07, 0.15] } },
|
|
2177
|
+
{ name: "Like", values: { pair: 4, off: [0.12, 0.12, 0.14], on: [0.94, 0.27, 0.4] } }
|
|
2178
|
+
],
|
|
2179
|
+
derived: (v) => {
|
|
2180
|
+
const [a, b] = iconMorph(Math.min(ICON_PAIRS.length - 1, Math.max(0, Math.round(Number(v.pair)) || 0)));
|
|
2181
|
+
return { a, b };
|
|
2182
|
+
},
|
|
2183
|
+
motion: {
|
|
2184
|
+
svg: `<svg viewBox="0 0 120 120"><!-- {{pair: int min=0 max=4 default=0 label='Icons (play, menu, plus, sun, heart)'}} {{ink: color default=#FFFFFF label='Icon'}} {{off: color default=#334155 label='Button'}} {{on: color default=#6366F1 label='Button (on)'}} {{page: color default=#0B1020 label='Background'}} --><rect width="120" height="120" fill="{{page}}"/><circle id="button" cx="60" cy="60" r="44" fill="{{off}}"/><g id="icon" transform="${pose(0, 2.6)}"><path id="glyph" d="{{a}}" fill="{{ink}}" fill-rule="evenodd" stroke="{{ink}}" stroke-width="0.3" stroke-linejoin="round"/></g></svg>`,
|
|
2185
|
+
animate: [...toggle(500, ["{{a}}", "{{b}}"], ["{{off}}", "{{on}}"], -16), ...toggle(2100, ["{{b}}", "{{a}}"], ["{{on}}", "{{off}}"], 16)]
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
var SEQUENCE = morphSequence(
|
|
2189
|
+
[circle(100, 100, 72), star(100, 104, 5, 82, 36), placed(HEART3, 100, 98, 8.4), star(100, 100, 3, 78, 78)],
|
|
2190
|
+
{ points: 96 }
|
|
2191
|
+
);
|
|
2192
|
+
var morphSequenceEffect = {
|
|
2193
|
+
kind: "vector",
|
|
2194
|
+
id: "morph-sequence",
|
|
2195
|
+
name: "Morph sequence",
|
|
2196
|
+
description: "One shape flowing from circle to star to heart to hexagon and round again, its colour changing with each.",
|
|
2197
|
+
category: "morph",
|
|
2198
|
+
tags: ["morph", "shape", "loop", "background", "playful"],
|
|
2199
|
+
hold: 0,
|
|
2200
|
+
presets: [
|
|
2201
|
+
{ name: "Pastel", values: { background: [0.99, 0.97, 0.94], c0: [0.65, 0.84, 0.98], c1: [0.99, 0.85, 0.55], c2: [0.98, 0.65, 0.7], c3: [0.75, 0.7, 0.98] } },
|
|
2202
|
+
{ name: "Mono", values: { background: [0.04, 0.04, 0.05], c0: [0.95, 0.95, 0.95], c1: [0.95, 0.95, 0.95], c2: [0.95, 0.95, 0.95], c3: [0.95, 0.95, 0.95], morph: 600, pause: 300 } }
|
|
2203
|
+
],
|
|
2204
|
+
derived: (v) => {
|
|
2205
|
+
const each = Number(v.morph) + Number(v.pause);
|
|
2206
|
+
return Object.fromEntries(SEQUENCE.map((_, i) => [`at${i}`, Number(v.pause) + i * each]));
|
|
2207
|
+
},
|
|
2208
|
+
motion: {
|
|
2209
|
+
svg: `<svg viewBox="0 0 200 200"><!-- {{c0: color default=#38BDF8 label='Circle'}} {{c1: color default=#FACC15 label='Star'}} {{c2: color default=#F43F5E label='Heart'}} {{c3: color default=#A78BFA label='Hexagon'}} {{background: color default=#0F172A label='Background'}} {{morph: float min=200 max=3000 default=900 step=50 label='Morph ms'}} {{pause: float min=0 max=3000 default=500 step=50 label='Pause ms'}} --><rect width="200" height="200" fill="{{background}}"/><path id="shape" d="${SEQUENCE[0]}" fill="{{c0}}" fill-rule="evenodd"/></svg>`,
|
|
2210
|
+
animate: SEQUENCE.map((d, i) => {
|
|
2211
|
+
const next = (i + 1) % SEQUENCE.length;
|
|
2212
|
+
return { id: "shape", attrs: { d: [d, SEQUENCE[next]], fill: [`{{c${i}}}`, `{{c${next}}}`] }, delay: n2(`at${i}`), ms: n2("morph"), ease: "in-out" };
|
|
2213
|
+
})
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
2216
|
+
var GOO = (() => {
|
|
2217
|
+
const cluster = [
|
|
2218
|
+
circle(100, 100, 25),
|
|
2219
|
+
...Array.from({ length: 5 }, (_, k) => circle(100 + 56 * Math.cos((-90 + 72 * k) * DEG), 100 + 56 * Math.sin((-90 + 72 * k) * DEG), 15))
|
|
2220
|
+
].join(" ");
|
|
2221
|
+
const blobStar = polyline(
|
|
2222
|
+
arc(100, 100, 1, 1, -90, 270, 4).slice(0, -1).map((p) => {
|
|
2223
|
+
const a = Math.atan2(p[1] - 100, p[0] - 100);
|
|
2224
|
+
const r = 50 + 20 * Math.cos(5 * (a + Math.PI / 2));
|
|
2225
|
+
return [100 + r * Math.cos(a), 100 + r * Math.sin(a)];
|
|
2226
|
+
}),
|
|
2227
|
+
true
|
|
2228
|
+
);
|
|
2229
|
+
const pair2 = `${circle(63, 100, 35)} ${circle(137, 100, 35)}`;
|
|
2230
|
+
const stages = morphSequence([cluster, blobStar, pair2], { points: 64 }).map((d) => d.split(/ (?=M)/));
|
|
2231
|
+
return stages[0].map((_, piece) => stages.map((stage) => stage[piece]));
|
|
2232
|
+
})();
|
|
2233
|
+
var gooeyMorph = {
|
|
2234
|
+
kind: "vector",
|
|
2235
|
+
id: "gooey-morph",
|
|
2236
|
+
name: "Gooey morph",
|
|
2237
|
+
description: "A cluster of liquid drops melting into a star, splitting into two blobs and beading back into drops.",
|
|
2238
|
+
category: "morph",
|
|
2239
|
+
tags: ["morph", "gooey", "liquid", "metaball", "blob", "loop"],
|
|
2240
|
+
hold: 0,
|
|
2241
|
+
presets: [
|
|
2242
|
+
{ name: "Lava", values: { colorA: [1, 0.85, 0.2], colorB: [0.95, 0.2, 0.1], background: [0.1, 0.02, 0.02] } },
|
|
2243
|
+
{ name: "Slime", values: { colorA: [0.75, 1, 0.3], colorB: [0.1, 0.7, 0.35], background: [0.03, 0.08, 0.05], goo: 9 } },
|
|
2244
|
+
{ name: "Ink", values: { colorA: [0.1, 0.1, 0.12], colorB: [0.1, 0.1, 0.12], background: [0.97, 0.96, 0.93] } }
|
|
2245
|
+
],
|
|
2246
|
+
derived: (v) => {
|
|
2247
|
+
const each = Number(v.morph) + Number(v.pause);
|
|
2248
|
+
return { ...Object.fromEntries(GOO[0].map((_, i) => [`at${i}`, Number(v.pause) + i * each])), threshold: -0.5 * Number(v.sharp) };
|
|
2249
|
+
},
|
|
2250
|
+
motion: {
|
|
2251
|
+
svg: `<svg viewBox="0 0 200 200"><!-- {{colorA: color default=#22D3EE label='Colour A'}} {{colorB: color default=#A855F7 label='Colour B'}} {{background: color default=#0B0B1A label='Background'}} {{goo: float min=1 max=14 default=7 step=0.5 label='Gooeyness'}} {{sharp: float min=8 max=40 default=20 step=1 label='Edge sharpness'}} {{morph: float min=300 max=3000 default=1200 step=50 label='Morph ms'}} {{pause: float min=0 max=3000 default=400 step=50 label='Pause ms'}} --><defs><linearGradient id="gooFill" gradientUnits="userSpaceOnUse" x1="30" y1="30" x2="170" y2="170"><stop offset="0" stop-color="{{colorA}}"/><stop offset="1" stop-color="{{colorB}}"/></linearGradient><filter id="goo" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur in="SourceGraphic" stdDeviation="{{goo}}" result="soft"/><feColorMatrix in="soft" type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 {{sharp}} {{threshold}}"/></filter></defs><rect width="200" height="200" fill="{{background}}"/><g filter="url(#goo)" fill="url(#gooFill)">${GOO.map((stages, i) => `<path id="goo${i}" d="${stages[0]}" fill-rule="evenodd"/>`).join("")}</g></svg>`,
|
|
2252
|
+
animate: GOO.flatMap(
|
|
2253
|
+
(stages, i) => stages.map((d, s) => ({ id: `goo${i}`, attrs: { d: [d, stages[(s + 1) % stages.length]] }, delay: n2(`at${s}`), ms: n2("morph"), ease: "in-out" }))
|
|
2254
|
+
)
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
var DOTS = 48;
|
|
2258
|
+
var DOT_STAGGER = 14;
|
|
2259
|
+
var PARTICLE_FROM = HEART3;
|
|
2260
|
+
var PARTICLE_TO = star(12, 12, 5, 10, 4.2);
|
|
2261
|
+
var noise = (i) => {
|
|
2262
|
+
const x = Math.sin(i * 12.9898 + 4.1414) * 43758.5453;
|
|
2263
|
+
return x - Math.floor(x);
|
|
2264
|
+
};
|
|
2265
|
+
var particleMorph = {
|
|
2266
|
+
kind: "vector",
|
|
2267
|
+
id: "particle-morph",
|
|
2268
|
+
name: "Particle morph",
|
|
2269
|
+
description: "A shape bursting into dots along its outline that swirl across and settle into a second shape, then back.",
|
|
2270
|
+
category: "morph",
|
|
2271
|
+
tags: ["morph", "particles", "dots", "shape", "logo", "reveal"],
|
|
2272
|
+
hold: 600,
|
|
2273
|
+
presets: [
|
|
2274
|
+
{ name: "Confetti", values: { background: [1, 0.98, 0.95], fromColor: [0.93, 0.27, 0.6], toColor: [0.15, 0.55, 0.95], ghost: 0.1, size: 3.2 } },
|
|
2275
|
+
{ name: "Embers", values: { background: [0.05, 0.02, 0.02], fromColor: [1, 0.85, 0.3], toColor: [1, 0.3, 0.1], scatter: 50, size: 2 } },
|
|
2276
|
+
{ name: "Circle to square", values: { from: "M12 2 A10 10 0 1 1 12 22 A10 10 0 1 1 12 2 Z", to: "M3 3 H21 V21 H3 Z" } }
|
|
2277
|
+
],
|
|
2278
|
+
// The dots sit on the morphed outlines: point k of one shape flies to point k of the other.
|
|
2279
|
+
derived: (v) => {
|
|
2280
|
+
const box = [40, 40, 120, 120];
|
|
2281
|
+
const [a, b] = morphPaths(fittedOr(v.from, PARTICLE_FROM, box, 96), fittedOr(v.to, PARTICLE_TO, box, 96), { points: DOTS });
|
|
2282
|
+
const [pa, pb] = [pointsOf(a), pointsOf(b)];
|
|
2283
|
+
const out = { a, b };
|
|
2284
|
+
const scatter = Number(v.scatter);
|
|
2285
|
+
for (let i = 0; i < DOTS; i++) {
|
|
2286
|
+
const k = Math.floor(i * pa.length / DOTS);
|
|
2287
|
+
const [p, q] = [pa[k], pb[k]];
|
|
2288
|
+
out[`ax${i}`] = r22(p[0]);
|
|
2289
|
+
out[`ay${i}`] = r22(p[1]);
|
|
2290
|
+
out[`bx${i}`] = r22(q[0]);
|
|
2291
|
+
out[`by${i}`] = r22(q[1]);
|
|
2292
|
+
for (const [name, seed] of [["s", i], ["t", i + DOTS]]) {
|
|
2293
|
+
const angle = seed * 2.39996;
|
|
2294
|
+
const far = scatter * (0.45 + 0.55 * noise(seed));
|
|
2295
|
+
out[`${name}x${i}`] = r22((p[0] + q[0]) / 2 + far * Math.cos(angle));
|
|
2296
|
+
out[`${name}y${i}`] = r22((p[1] + q[1]) / 2 + far * Math.sin(angle));
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
return out;
|
|
2300
|
+
},
|
|
2301
|
+
motion: (() => {
|
|
2302
|
+
const back = 3e3;
|
|
2303
|
+
const spread = DOTS * DOT_STAGGER;
|
|
2304
|
+
const leg = (i, at, from, mid, to, colours) => [
|
|
2305
|
+
{ id: `p${i}`, attrs: { cx: [n2(`${from}x${i}`), n2(`${mid}x${i}`)], cy: [n2(`${from}y${i}`), n2(`${mid}y${i}`)] }, delay: at + i * DOT_STAGGER, ms: 450, ease: "out" },
|
|
2306
|
+
{ id: `p${i}`, attrs: { cx: [n2(`${mid}x${i}`), n2(`${to}x${i}`)], cy: [n2(`${mid}y${i}`), n2(`${to}y${i}`)], fill: colours }, delay: at + 450 + i * DOT_STAGGER, ms: 650, ease: "in-out" }
|
|
2307
|
+
];
|
|
2308
|
+
const fade = (id, at, show) => ({ id, attrs: { opacity: show ? [0, n2("ghost")] : [n2("ghost"), 0] }, delay: at, ms: 450, ease: "in-out" });
|
|
2309
|
+
return {
|
|
2310
|
+
svg: `<svg viewBox="0 0 200 200"><!-- {{from: text default='${PARTICLE_FROM}' label='From path'}} {{to: text default='${PARTICLE_TO}' label='To path'}} {{fromColor: color default=#F472B6 label='From colour'}} {{toColor: color default=#FBBF24 label='To colour'}} {{background: color default=#0C0A1D label='Background'}} {{size: float min=0.8 max=6 default=2.6 step=0.1 label='Dot size'}} {{scatter: float min=0 max=80 default=34 step=1 label='Scatter'}} {{ghost: float min=0 max=1 default=0.16 label='Shape fill'}} --><rect width="200" height="200" fill="{{background}}"/><path id="ghostA" d="{{a}}" fill="{{fromColor}}" fill-rule="evenodd" opacity="{{ghost}}"/><path id="ghostB" d="{{b}}" fill="{{toColor}}" fill-rule="evenodd" opacity="0"/>${Array.from(
|
|
2311
|
+
{ length: DOTS },
|
|
2312
|
+
(_, i) => `<circle id="p${i}" cx="{{ax${i}}}" cy="{{ay${i}}}" r="{{size}}" fill="{{fromColor}}"/>`
|
|
2313
|
+
).join("")}</svg>`,
|
|
2314
|
+
animate: [
|
|
2315
|
+
fade("ghostA", 400, false),
|
|
2316
|
+
fade("ghostB", 400 + spread + 900, true),
|
|
2317
|
+
fade("ghostB", back, false),
|
|
2318
|
+
fade("ghostA", back + spread + 900, true),
|
|
2319
|
+
...Array.from({ length: DOTS }, (_, i) => [
|
|
2320
|
+
...leg(i, 400, "a", "s", "b", ["{{fromColor}}", "{{toColor}}"]),
|
|
2321
|
+
...leg(i, back, "b", "t", "a", ["{{toColor}}", "{{fromColor}}"])
|
|
2322
|
+
]).flat()
|
|
2323
|
+
]
|
|
2324
|
+
};
|
|
2325
|
+
})()
|
|
2326
|
+
};
|
|
2327
|
+
var DIGITS = (() => {
|
|
2328
|
+
const six = [...arc(17, 22, 14, 19, 245, 180), ...arc(10, 22, 7, 7, 180, 540).slice(1)];
|
|
2329
|
+
return [
|
|
2330
|
+
polyline(arc(10, 16, 7, 13, 0, 360).slice(0, -1), true),
|
|
2331
|
+
polyline([[5.5, 8], [11, 3], [11, 29]]),
|
|
2332
|
+
polyline([...arc(10, 9.5, 6.5, 6.5, 200, 380), [3.5, 29], [17, 29]]),
|
|
2333
|
+
polyline([...arc(10, 9, 6, 6, 215, 450), ...arc(10, 21.5, 7, 6.5, 270, 495).slice(1)]),
|
|
2334
|
+
polyline([[14, 29], [14, 3], [3, 20.5], [18, 20.5]]),
|
|
2335
|
+
polyline([[16.5, 3], [5.5, 3], [4.6, 14.6], ...arc(10.5, 21.2, 6.8, 7, 218, 500)]),
|
|
2336
|
+
polyline(six),
|
|
2337
|
+
polyline([[3, 3], [17, 3], [8, 29]]),
|
|
2338
|
+
polyline([...arc(10, 9.5, 5.5, 5.5, 90, 450), ...arc(10, 22, 7, 7, 270, 630).slice(1)]),
|
|
2339
|
+
polyline(six.map((p) => [20 - p[0], 32 - p[1]]))
|
|
2340
|
+
];
|
|
2341
|
+
})();
|
|
2342
|
+
var BLANK = "M10 16 L10 16";
|
|
2343
|
+
var SLOTS = 4;
|
|
2344
|
+
var MAX_STEPS = 8;
|
|
2345
|
+
var DIGIT_REST = 260;
|
|
2346
|
+
var digitMorph = {
|
|
2347
|
+
kind: "vector",
|
|
2348
|
+
id: "digit-morph",
|
|
2349
|
+
name: "Digit morph",
|
|
2350
|
+
description: "A number counting from one value to another, each digit's stroke bending smoothly into the next.",
|
|
2351
|
+
category: "morph",
|
|
2352
|
+
tags: ["morph", "number", "counter", "countdown", "kpi", "digits"],
|
|
2353
|
+
hold: 900,
|
|
2354
|
+
presets: [
|
|
2355
|
+
{ name: "Countdown", values: { from: 10, to: 0, ink: [1, 0.36, 0.3] } },
|
|
2356
|
+
{ name: "New year", values: { from: 2025, to: 2026, morph: 700, background: [0.04, 0.03, 0.1], ink: [1, 0.84, 0.4] } },
|
|
2357
|
+
{ name: "Thin", values: { weight: 1.4, background: [0.98, 0.98, 0.97], ink: [0.1, 0.1, 0.12] } }
|
|
2358
|
+
],
|
|
2359
|
+
derived: (v) => {
|
|
2360
|
+
const from = Math.round(Number(v.from)) || 0;
|
|
2361
|
+
const to = Math.round(Number(v.to)) || 0;
|
|
2362
|
+
const steps = Math.min(MAX_STEPS, Math.abs(to - from));
|
|
2363
|
+
const values = Array.from({ length: steps + 1 }, (_, k) => Math.round(from + (to - from) * k / Math.max(1, steps)));
|
|
2364
|
+
const used = Math.max(1, ...values.map((x) => String(x).length));
|
|
2365
|
+
const scale = Math.min(4.2, 340 / (used * 22 - 2));
|
|
2366
|
+
const left = 200 - (used * 22 - 2) * scale / 2;
|
|
2367
|
+
const interval = Number(v.morph) + DIGIT_REST;
|
|
2368
|
+
const lastEnd = Math.max(0, steps - 1) * interval + Number(v.morph);
|
|
2369
|
+
const out = { scale: r22(scale), y: r22(100 - 16 * scale), back: lastEnd + DIGIT_REST * 3 };
|
|
2370
|
+
for (let k = 1; k <= MAX_STEPS; k++) out[`t${k}`] = k <= steps ? (k - 1) * interval : lastEnd;
|
|
2371
|
+
for (let j = 0; j < SLOTS; j++) {
|
|
2372
|
+
const place = j - (SLOTS - used);
|
|
2373
|
+
out[`x${j}`] = r22(left + Math.max(0, place) * 22 * scale);
|
|
2374
|
+
const glyphs = values.map((x) => {
|
|
2375
|
+
const text = String(x);
|
|
2376
|
+
const at = text.length - (used - place);
|
|
2377
|
+
return place >= 0 && at >= 0 ? Number(text[at]) : -1;
|
|
2378
|
+
});
|
|
2379
|
+
const paths = morphSequence(glyphs.map((g) => g < 0 ? BLANK : DIGITS[g]), { points: 64 });
|
|
2380
|
+
for (let k = 0; k <= MAX_STEPS; k++) {
|
|
2381
|
+
const s = Math.min(k, steps);
|
|
2382
|
+
out[`d${j}_${k}`] = paths[s];
|
|
2383
|
+
out[`o${j}_${k}`] = glyphs[s] < 0 ? 0 : 1;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
return out;
|
|
2387
|
+
},
|
|
2388
|
+
motion: {
|
|
2389
|
+
svg: `<svg viewBox="0 0 400 200"><!-- {{from: int min=0 max=9999 default=12 label='From'}} {{to: int min=0 max=9999 default=20 label='To'}} {{ink: color default=#F8FAFC label='Digits'}} {{background: color default=#111827 label='Background'}} {{weight: float min=0.8 max=5 default=2.8 step=0.1 label='Weight'}} {{morph: float min=150 max=1500 default=420 step=10 label='Morph ms'}} --><rect width="400" height="200" fill="{{background}}"/>${Array.from(
|
|
2390
|
+
{ length: SLOTS },
|
|
2391
|
+
(_, j) => `<path id="s${j}" d="{{d${j}_0}}" opacity="{{o${j}_0}}" transform="translate({{x${j}}} {{y}}) scale({{scale}})" fill="none" fill-rule="evenodd" stroke="{{ink}}" stroke-width="{{weight}}" stroke-linecap="round" stroke-linejoin="round"/>`
|
|
2392
|
+
).join("")}</svg>`,
|
|
2393
|
+
animate: Array.from({ length: SLOTS }, (_, j) => [
|
|
2394
|
+
...Array.from({ length: MAX_STEPS }, (_2, i) => {
|
|
2395
|
+
const k = i + 1;
|
|
2396
|
+
return { id: `s${j}`, attrs: { d: [`{{d${j}_${k - 1}}}`, `{{d${j}_${k}}}`], opacity: [n2(`o${j}_${k - 1}`), n2(`o${j}_${k}`)] }, delay: n2(`t${k}`), ms: n2("morph"), ease: "in-out" };
|
|
2397
|
+
}),
|
|
2398
|
+
// Back to the first number in one morph, so it loops.
|
|
2399
|
+
{ id: `s${j}`, attrs: { d: [`{{d${j}_${MAX_STEPS}}}`, `{{d${j}_0}}`], opacity: [n2(`o${j}_${MAX_STEPS}`), n2(`o${j}_0`)] }, delay: n2("back"), ms: n2("morph"), ease: "in-out" }
|
|
2400
|
+
]).flat()
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
var morphEffects = [shapeMorph, iconToggle, morphSequenceEffect, gooeyMorph, particleMorph, digitMorph];
|
|
2404
|
+
|
|
2405
|
+
// src/catalog/shaders/interaction.ts
|
|
2406
|
+
var CLICKS = `
|
|
2407
|
+
vec4 clickAt(int i, bool demo) {
|
|
2408
|
+
if (!demo || iClicks[0].w > 0.5) return iClicks[i];
|
|
2409
|
+
float k = floor(iTime / 2.0) - float(i);
|
|
2410
|
+
vec2 h = hash22(vec2(k * 1.37 + 0.5, 3.7));
|
|
2411
|
+
return vec4((0.15 + 0.7 * h) * iResolution.xy, k * 2.0, 1.0);
|
|
2412
|
+
}`.trim();
|
|
2413
|
+
var HOVER = `
|
|
2414
|
+
vec3 hoverPoint(bool demo, float idle) {
|
|
2415
|
+
vec2 drift = iResolution.xy * (0.5 + vec2(0.28 * sin(iTime * 0.63), 0.22 * sin(iTime * 0.91 + 1.3)));
|
|
2416
|
+
float h = clamp(iPointer.z, 0.0, 1.0);
|
|
2417
|
+
return vec3(mix(drift, iPointer.xy, h), mix(demo ? idle : 0.0, 1.0, h));
|
|
2418
|
+
}`.trim();
|
|
2419
|
+
var GLOW = `
|
|
2420
|
+
vec4 glow(vec3 c) {
|
|
2421
|
+
c = 1.0 - exp(-max(c, 0.0));
|
|
2422
|
+
float a = max(c.r, max(c.g, c.b));
|
|
2423
|
+
return vec4(c / max(a, 1e-4), a);
|
|
2424
|
+
}`.trim();
|
|
2425
|
+
var OVER = `
|
|
2426
|
+
vec4 over(vec4 acc, vec3 c, float a) {
|
|
2427
|
+
return vec4(acc.rgb * (1.0 - a) + c * a, acc.a * (1.0 - a) + a);
|
|
2428
|
+
}
|
|
2429
|
+
vec4 unpremultiply(vec4 acc) {
|
|
2430
|
+
return vec4(acc.rgb / max(acc.a, 1e-4), acc.a);
|
|
2431
|
+
}`.trim();
|
|
2432
|
+
var clickRipple = {
|
|
2433
|
+
kind: "shader",
|
|
2434
|
+
id: "click-ripple",
|
|
2435
|
+
name: "Click ripple",
|
|
2436
|
+
description: "Rings of water spreading out from every click and fading as they go.",
|
|
2437
|
+
category: "interaction",
|
|
2438
|
+
tags: ["interactive", "click", "overlay", "water", "ripple"],
|
|
2439
|
+
transparent: true,
|
|
2440
|
+
source: glsl(
|
|
2441
|
+
`// Click ripple: water rings spreading from each click (up to 8 at once).
|
|
2442
|
+
uniform vec3 uColor; // @control color default=#BFE6FF label="Colour"
|
|
2443
|
+
uniform float uSpeed; // @control min=0.05 max=2 default=0.5 label="Speed"
|
|
2444
|
+
uniform float uWidth; // @control min=0.002 max=0.05 default=0.012 label="Ring width"
|
|
2445
|
+
uniform float uLife; // @control min=0.3 max=6 default=2 label="Lifetime"
|
|
2446
|
+
uniform int uRings; // @control min=1 max=4 default=3 label="Rings per click"
|
|
2447
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2448
|
+
HASH,
|
|
2449
|
+
CLICKS,
|
|
2450
|
+
OVER,
|
|
2451
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2452
|
+
float px = 1.0 / iResolution.y;
|
|
2453
|
+
vec4 acc = vec4(0.0);
|
|
2454
|
+
for (int i = 7; i >= 0; i--) {
|
|
2455
|
+
vec4 c = clickAt(i, uDemo);
|
|
2456
|
+
if (c.w < 0.5) continue;
|
|
2457
|
+
float age = iTime - c.z;
|
|
2458
|
+
if (age < 0.0 || age > uLife) continue;
|
|
2459
|
+
float life = age / uLife;
|
|
2460
|
+
float fade = (1.0 - life) * (1.0 - life);
|
|
2461
|
+
float d = length(fragCoord - c.xy) * px;
|
|
2462
|
+
for (int j = 0; j < 4; j++) {
|
|
2463
|
+
if (j >= uRings) break;
|
|
2464
|
+
float r = (age - float(j) * 0.16) * uSpeed;
|
|
2465
|
+
if (r <= 0.0) continue;
|
|
2466
|
+
// Rings widen and soften as they spread.
|
|
2467
|
+
float w = max(uWidth * (1.0 + life * 1.5), 1.5 * px);
|
|
2468
|
+
float x = (d - r) / w;
|
|
2469
|
+
float k = fade * (1.0 - 0.28 * float(j)) * smoothstep(0.0, 0.05, r);
|
|
2470
|
+
// A bright crest with a darker trough just inside it.
|
|
2471
|
+
float crest = exp(-x * x * 2.0);
|
|
2472
|
+
float trough = exp(-(x + 1.6) * (x + 1.6) * 2.0);
|
|
2473
|
+
acc = over(acc, uColor * 0.15, trough * k * 0.35);
|
|
2474
|
+
acc = over(acc, mix(uColor, vec3(1.0), 0.5 * crest), crest * k);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
fragColor = unpremultiply(acc);
|
|
2478
|
+
}`
|
|
2479
|
+
),
|
|
2480
|
+
presets: [
|
|
2481
|
+
{ name: "Sonar", values: { uColor: [0.3, 1, 0.6], uSpeed: 0.3, uWidth: 6e-3, uLife: 4, uRings: 4 } },
|
|
2482
|
+
{ name: "Raindrop", values: { uColor: [1, 1, 1], uSpeed: 0.8, uWidth: 0.02, uLife: 1.2, uRings: 2 } }
|
|
2483
|
+
]
|
|
2484
|
+
};
|
|
2485
|
+
var clickBurst = {
|
|
2486
|
+
kind: "shader",
|
|
2487
|
+
id: "click-burst",
|
|
2488
|
+
name: "Click burst",
|
|
2489
|
+
description: "Sparks or confetti bursting out of every click, falling under gravity as they fade.",
|
|
2490
|
+
category: "interaction",
|
|
2491
|
+
tags: ["interactive", "click", "overlay", "particles", "celebration", "confetti"],
|
|
2492
|
+
transparent: true,
|
|
2493
|
+
source: glsl(
|
|
2494
|
+
`// Click burst: sparks (or confetti) thrown out of each click, slowed by drag and pulled down by gravity.
|
|
2495
|
+
uniform vec3 uColorA; // @control color default=#FFD166 label="Colour 1"
|
|
2496
|
+
uniform vec3 uColorB; // @control color default=#EF476F label="Colour 2"
|
|
2497
|
+
uniform vec3 uColorC; // @control color default=#06D6A0 label="Colour 3"
|
|
2498
|
+
uniform int uCount; // @control min=4 max=48 default=24 label="Count"
|
|
2499
|
+
uniform float uSpread; // @control min=0.1 max=2 default=0.8 label="Spread"
|
|
2500
|
+
uniform float uGravity; // @control min=0 max=3 default=1 label="Gravity"
|
|
2501
|
+
uniform float uLife; // @control min=0.4 max=4 default=1.6 label="Lifetime"
|
|
2502
|
+
uniform bool uConfetti; // @control default=false label="Confetti"
|
|
2503
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2504
|
+
HASH,
|
|
2505
|
+
CLICKS,
|
|
2506
|
+
OVER,
|
|
2507
|
+
`float segment(vec2 p, vec2 a, vec2 b) {
|
|
2508
|
+
vec2 ab = b - a;
|
|
2509
|
+
float t = clamp(dot(p - a, ab) / max(dot(ab, ab), 1e-8), 0.0, 1.0);
|
|
2510
|
+
return length(p - a - ab * t);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2514
|
+
float px = 1.0 / iResolution.y;
|
|
2515
|
+
vec2 p = fragCoord * px;
|
|
2516
|
+
const float drag = 2.5;
|
|
2517
|
+
vec4 acc = vec4(0.0);
|
|
2518
|
+
for (int i = 7; i >= 0; i--) {
|
|
2519
|
+
vec4 c = clickAt(i, uDemo);
|
|
2520
|
+
if (c.w < 0.5) continue;
|
|
2521
|
+
float age = iTime - c.z;
|
|
2522
|
+
if (age < 0.0 || age > uLife) continue;
|
|
2523
|
+
float life = age / uLife;
|
|
2524
|
+
vec2 origin = c.xy * px;
|
|
2525
|
+
// A quick flash where it landed.
|
|
2526
|
+
float flash = exp(-age * 18.0) * exp(-dot(p - origin, p - origin) / 0.002);
|
|
2527
|
+
acc = over(acc, vec3(1.0), clamp(flash, 0.0, 1.0));
|
|
2528
|
+
float slow = (1.0 - exp(-drag * age)) / drag;
|
|
2529
|
+
for (int j = 0; j < 48; j++) {
|
|
2530
|
+
if (j >= uCount) break;
|
|
2531
|
+
vec2 h = hash22(vec2(float(j) * 7.13 + 1.0, c.z * 3.1 + float(i)));
|
|
2532
|
+
float h3 = hash12(h * 91.7 + c.z);
|
|
2533
|
+
float angle = h.x * 6.28318;
|
|
2534
|
+
vec2 v0 = vec2(cos(angle), sin(angle)) * uSpread * (0.3 + 0.7 * h.y) + vec2(0.0, uSpread * 0.35);
|
|
2535
|
+
vec2 pos = origin + v0 * slow - vec2(0.0, 0.5 * uGravity * 0.6 * age * age);
|
|
2536
|
+
if (abs(p.x - pos.x) > 0.06 || abs(p.y - pos.y) > 0.06) continue;
|
|
2537
|
+
vec3 col = h3 < 0.333 ? uColorA : (h3 < 0.667 ? uColorB : uColorC);
|
|
2538
|
+
float a;
|
|
2539
|
+
if (uConfetti) {
|
|
2540
|
+
// A tumbling paper rectangle.
|
|
2541
|
+
float spin = (h3 - 0.5) * 20.0 * age + angle;
|
|
2542
|
+
float cs = cos(spin);
|
|
2543
|
+
float sn = sin(spin);
|
|
2544
|
+
vec2 q = mat2(cs, sn, -sn, cs) * (p - pos);
|
|
2545
|
+
vec2 size = vec2(0.011, 0.006 * abs(cos(age * (4.0 + 6.0 * h.y) + h.x * 9.0)) + px);
|
|
2546
|
+
vec2 e = abs(q) - size;
|
|
2547
|
+
float sd = length(max(e, 0.0)) + min(max(e.x, e.y), 0.0);
|
|
2548
|
+
a = (1.0 - smoothstep(-px, px, sd)) * (1.0 - smoothstep(0.7, 1.0, life));
|
|
2549
|
+
col *= 0.75 + 0.25 * cs;
|
|
2550
|
+
} else {
|
|
2551
|
+
// A streak along its velocity, with a white-hot core.
|
|
2552
|
+
vec2 v = v0 * exp(-drag * age) - vec2(0.0, uGravity * 0.6 * age);
|
|
2553
|
+
float r = 0.004 * (1.0 - 0.6 * life) + px;
|
|
2554
|
+
float sd = segment(p, pos, pos - v * 0.05) - r;
|
|
2555
|
+
a = (1.0 - smoothstep(-px, px, sd)) * (1.0 - life);
|
|
2556
|
+
a += exp(-max(sd, 0.0) / (r * 2.0)) * 0.35 * (1.0 - life);
|
|
2557
|
+
col = mix(col, vec3(1.0), exp(-max(sd + r, 0.0) / (r * 0.5)) * 0.6);
|
|
2558
|
+
}
|
|
2559
|
+
acc = over(acc, col, clamp(a, 0.0, 1.0));
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
fragColor = unpremultiply(acc);
|
|
2563
|
+
}`
|
|
2564
|
+
),
|
|
2565
|
+
presets: [
|
|
2566
|
+
{ name: "Confetti", values: { uConfetti: true, uCount: 36, uSpread: 1, uGravity: 1.4, uLife: 2.5, uColorC: [0.3, 0.55, 1] } },
|
|
2567
|
+
{ name: "Embers", values: { uColorA: [1, 0.6, 0.15], uColorB: [1, 0.3, 0.1], uColorC: [1, 0.85, 0.5], uCount: 16, uSpread: 0.5, uGravity: 0.2 } }
|
|
2568
|
+
]
|
|
2569
|
+
};
|
|
2570
|
+
var cursorGlow = {
|
|
2571
|
+
kind: "shader",
|
|
2572
|
+
id: "cursor-glow",
|
|
2573
|
+
name: "Cursor glow",
|
|
2574
|
+
description: "A soft light following the pointer, with a halo ring that tightens while it is pressed.",
|
|
2575
|
+
category: "interaction",
|
|
2576
|
+
tags: ["interactive", "hover", "press", "overlay", "glow", "cursor"],
|
|
2577
|
+
transparent: true,
|
|
2578
|
+
source: glsl(
|
|
2579
|
+
`// Cursor glow: a soft light at the pointer, scaled by how hovered, with a halo that tightens while pressed.
|
|
2580
|
+
uniform vec3 uColor; // @control color default=#7DD3FC label="Colour"
|
|
2581
|
+
uniform float uRadius; // @control min=0.03 max=0.6 default=0.18 label="Radius"
|
|
2582
|
+
uniform float uIntensity; // @control min=0 max=3 default=1.2 label="Intensity"
|
|
2583
|
+
uniform float uHalo; // @control min=0 max=1 default=0.7 label="Halo"
|
|
2584
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2585
|
+
HOVER,
|
|
2586
|
+
GLOW,
|
|
2587
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2588
|
+
vec3 hp = hoverPoint(uDemo, 0.7);
|
|
2589
|
+
float d = length(fragCoord - hp.xy) / iResolution.y;
|
|
2590
|
+
// How pressed: eases in while held; the idle demo pulses it.
|
|
2591
|
+
float press = 1.0 - exp(-max(iPointer.w, 0.0) * 4.0);
|
|
2592
|
+
if (uDemo) press = max(press, (1.0 - iPointer.z) * pow(max(sin(iTime * 1.3), 0.0), 3.0));
|
|
2593
|
+
float light = exp(-pow(d / uRadius, 2.0) * 2.0) * uIntensity * hp.z * (1.0 + 0.6 * press);
|
|
2594
|
+
float ringR = uRadius * mix(1.3, 0.55, press);
|
|
2595
|
+
float ringW = max(uRadius * 0.05, 1.5 / iResolution.y);
|
|
2596
|
+
float ring = exp(-pow((d - ringR) / ringW, 2.0)) * uHalo * hp.z * (0.6 + 0.8 * press);
|
|
2597
|
+
fragColor = glow(uColor * light + mix(uColor, vec3(1.0), 0.5) * ring * 1.5);
|
|
2598
|
+
}`
|
|
2599
|
+
),
|
|
2600
|
+
presets: [
|
|
2601
|
+
{ name: "Spotlight", values: { uColor: [1, 0.95, 0.85], uRadius: 0.4, uIntensity: 0.8, uHalo: 0 } },
|
|
2602
|
+
{ name: "Neon", values: { uColor: [1, 0.2, 0.75], uRadius: 0.1, uIntensity: 2, uHalo: 1 } }
|
|
2603
|
+
]
|
|
2604
|
+
};
|
|
2605
|
+
var hoverLiquid = {
|
|
2606
|
+
kind: "shader",
|
|
2607
|
+
id: "hover-liquid",
|
|
2608
|
+
name: "Liquid hover",
|
|
2609
|
+
description: "An image that swells and ripples like liquid around the pointer while it hovers.",
|
|
2610
|
+
category: "interaction",
|
|
2611
|
+
tags: ["interactive", "hover", "image", "liquid", "distortion"],
|
|
2612
|
+
source: glsl(
|
|
2613
|
+
`// Liquid hover: an image bulging and rippling around the pointer, as strongly as it is hovered.
|
|
2614
|
+
uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.landscape} label="Image"
|
|
2615
|
+
uniform float uStrength; // @control min=0 max=1 default=0.6 label="Strength"
|
|
2616
|
+
uniform float uRadius; // @control min=0.05 max=0.8 default=0.3 label="Radius"
|
|
2617
|
+
uniform float uWobble; // @control min=0 max=1 default=0.5 label="Wobble"
|
|
2618
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2619
|
+
HASH,
|
|
2620
|
+
NOISE,
|
|
2621
|
+
HOVER,
|
|
2622
|
+
PICTURE,
|
|
2623
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2624
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
2625
|
+
vec3 hp = hoverPoint(uDemo, 0.5);
|
|
2626
|
+
vec2 d = (fragCoord - hp.xy) / iResolution.y;
|
|
2627
|
+
float r = length(d);
|
|
2628
|
+
float s = uStrength * hp.z;
|
|
2629
|
+
float fall = exp(-pow(r / uRadius, 2.0) * 1.5);
|
|
2630
|
+
// Swell: pull samples towards the pointer, so it magnifies like a drop.
|
|
2631
|
+
vec2 off = -d * fall * s * 0.6;
|
|
2632
|
+
// Wobble: rings running out from the pointer and a slow liquid churn.
|
|
2633
|
+
vec2 dir = d / max(r, 1e-5);
|
|
2634
|
+
float wave = sin(r / uRadius * 14.0 - iTime * 5.0) * fall;
|
|
2635
|
+
off += dir * wave * uWobble * s * 0.02;
|
|
2636
|
+
vec2 churn = vec2(vnoise(d * 6.0 + iTime * 0.7), vnoise(d * 6.0 - iTime * 0.6 + 5.0)) - 0.5;
|
|
2637
|
+
off += churn * fall * s * uWobble * 0.05;
|
|
2638
|
+
vec3 col = picture(uImage, uv + off * vec2(iResolution.y / iResolution.x, 1.0));
|
|
2639
|
+
// Light catching the swell's upper-left slope.
|
|
2640
|
+
float slope = dot(dir, normalize(vec2(-0.6, 0.8))) * r / uRadius;
|
|
2641
|
+
col += 0.12 * s * fall * slope + 0.06 * s * wave * uWobble;
|
|
2642
|
+
fragColor = vec4(col, 1.0);
|
|
2643
|
+
}`
|
|
2644
|
+
),
|
|
2645
|
+
presets: [
|
|
2646
|
+
{ name: "Syrup", values: { uStrength: 0.9, uRadius: 0.45, uWobble: 0.15 } },
|
|
2647
|
+
{ name: "Splash", values: { uStrength: 0.5, uRadius: 0.25, uWobble: 1 } }
|
|
2648
|
+
]
|
|
2649
|
+
};
|
|
2650
|
+
var hoverReveal = {
|
|
2651
|
+
kind: "shader",
|
|
2652
|
+
id: "hover-reveal",
|
|
2653
|
+
name: "X-ray reveal",
|
|
2654
|
+
description: "A second image showing through a soft lens around the pointer that grows as it hovers and pulses on click.",
|
|
2655
|
+
category: "interaction",
|
|
2656
|
+
tags: ["interactive", "hover", "click", "image", "reveal", "x-ray"],
|
|
2657
|
+
source: glsl(
|
|
2658
|
+
`// X-ray reveal: a second image seen through a soft lens at the pointer.
|
|
2659
|
+
uniform sampler2D uImage; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.landscape} label="Image"
|
|
2660
|
+
uniform sampler2D uReveal; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.city} label="Revealed image"
|
|
2661
|
+
uniform float uRadius; // @control min=0.05 max=0.6 default=0.22 label="Lens radius"
|
|
2662
|
+
uniform float uSoftness; // @control min=0 max=1 default=0.35 label="Softness"
|
|
2663
|
+
uniform vec3 uRim; // @control color default=#A5F3FC label="Rim"
|
|
2664
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2665
|
+
HASH,
|
|
2666
|
+
NOISE,
|
|
2667
|
+
CLICKS,
|
|
2668
|
+
HOVER,
|
|
2669
|
+
PICTURE,
|
|
2670
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2671
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
2672
|
+
float px = 1.0 / iResolution.y;
|
|
2673
|
+
vec3 hp = hoverPoint(uDemo, 0.85);
|
|
2674
|
+
// Each click swells the lens, which settles back.
|
|
2675
|
+
float pulse = 0.0;
|
|
2676
|
+
for (int i = 0; i < 8; i++) {
|
|
2677
|
+
vec4 c = clickAt(i, uDemo);
|
|
2678
|
+
float age = iTime - c.z;
|
|
2679
|
+
if (c.w < 0.5 || age < 0.0) continue;
|
|
2680
|
+
pulse += 0.6 * exp(-age * 4.0) * (1.0 - exp(-age * 30.0));
|
|
2681
|
+
}
|
|
2682
|
+
float R = uRadius * hp.z * (1.0 + pulse);
|
|
2683
|
+
float d = length(fragCoord - hp.xy) * px;
|
|
2684
|
+
// A wobbly edge, so the lens looks like it is melting through.
|
|
2685
|
+
d += (vnoise(fragCoord * px * 9.0 + iTime * 0.6) - 0.5) * 0.25 * R;
|
|
2686
|
+
float soft = max(uSoftness * R * 0.6, 1.5 * px);
|
|
2687
|
+
float m = 1.0 - smoothstep(R - soft, R, d);
|
|
2688
|
+
vec3 col = mix(picture(uImage, uv), picture(uReveal, uv), m);
|
|
2689
|
+
float rim = exp(-pow((d - R + soft * 0.5) / max(soft * 0.5, 2.0 * px), 2.0)) * step(0.001, R);
|
|
2690
|
+
col += uRim * rim * 0.6;
|
|
2691
|
+
fragColor = vec4(col, 1.0);
|
|
2692
|
+
}`
|
|
2693
|
+
),
|
|
2694
|
+
presets: [
|
|
2695
|
+
{ name: "Peephole", values: { uRadius: 0.12, uSoftness: 0.05, uRim: [1, 1, 1] } },
|
|
2696
|
+
{ name: "Fog lamp", values: { uRadius: 0.4, uSoftness: 1, uRim: [0, 0, 0] } }
|
|
2697
|
+
]
|
|
2698
|
+
};
|
|
2699
|
+
var magnifier = {
|
|
2700
|
+
kind: "shader",
|
|
2701
|
+
id: "magnifier",
|
|
2702
|
+
name: "Magnifier",
|
|
2703
|
+
description: "A round magnifying glass over an image at the pointer, with a bright rim and a fringe of colour at its edge.",
|
|
2704
|
+
category: "interaction",
|
|
2705
|
+
tags: ["interactive", "hover", "image", "lens", "zoom"],
|
|
2706
|
+
source: glsl(
|
|
2707
|
+
`// Magnifier: a round lens over an image at the pointer, showing while hovered.
|
|
2708
|
+
uniform sampler2D uImage; // @control filter=linear wrap=mirror default=${SAMPLE_IMAGES.city} label="Image"
|
|
2709
|
+
uniform float uZoom; // @control min=1 max=5 default=2 label="Zoom"
|
|
2710
|
+
uniform float uRadius; // @control min=0.05 max=0.5 default=0.18 label="Radius"
|
|
2711
|
+
uniform float uAberration; // @control min=0 max=1 default=0.35 label="Colour fringe"
|
|
2712
|
+
uniform vec3 uRim; // @control color default=#FFFFFF label="Rim"
|
|
2713
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2714
|
+
HOVER,
|
|
2715
|
+
PICTURE,
|
|
2716
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2717
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
2718
|
+
vec3 hp = hoverPoint(uDemo, 1.0);
|
|
2719
|
+
float s = hp.z;
|
|
2720
|
+
vec3 col = picture(uImage, uv);
|
|
2721
|
+
float R = uRadius * (0.75 + 0.25 * s) * iResolution.y;
|
|
2722
|
+
vec2 d = fragCoord - hp.xy;
|
|
2723
|
+
float r = length(d);
|
|
2724
|
+
float k = r / R;
|
|
2725
|
+
// A soft shadow under the glass.
|
|
2726
|
+
float shadow = 1.0 - smoothstep(R * 0.9, R * 1.3, length(d - vec2(R * 0.08, -R * 0.12)));
|
|
2727
|
+
col *= 1.0 - 0.35 * shadow * s;
|
|
2728
|
+
if (k < 1.05) {
|
|
2729
|
+
// More magnified at the centre, squeezed towards the rim like real glass.
|
|
2730
|
+
vec2 q = d / uZoom * (1.0 + 0.4 * k * k * k * k);
|
|
2731
|
+
float fr = uAberration * 0.08 * k * k * k;
|
|
2732
|
+
vec3 lens = vec3(
|
|
2733
|
+
picture(uImage, (hp.xy + q * (1.0 + fr)) / iResolution.xy).r,
|
|
2734
|
+
picture(uImage, (hp.xy + q) / iResolution.xy).g,
|
|
2735
|
+
picture(uImage, (hp.xy + q * (1.0 - fr)) / iResolution.xy).b
|
|
2736
|
+
);
|
|
2737
|
+
lens += 0.16 * exp(-pow(length(d / R - vec2(-0.35, 0.4)) * 2.6, 2.0));
|
|
2738
|
+
lens *= 1.0 - 0.25 * pow(k, 6.0);
|
|
2739
|
+
col = mix(col, lens, (1.0 - smoothstep(R - 1.5, R, r)) * s);
|
|
2740
|
+
}
|
|
2741
|
+
float rimW = max(R * 0.035, 2.0);
|
|
2742
|
+
float lit = 0.6 + 0.4 * dot(d / max(r, 1e-4), normalize(vec2(-1.0, 1.0)));
|
|
2743
|
+
float rim = exp(-pow((r - R) / rimW, 2.0));
|
|
2744
|
+
col = mix(col, uRim * lit, rim * s * 0.9);
|
|
2745
|
+
fragColor = vec4(col, 1.0);
|
|
2746
|
+
}`
|
|
2747
|
+
),
|
|
2748
|
+
presets: [
|
|
2749
|
+
{ name: "Loupe", values: { uZoom: 4, uRadius: 0.1, uAberration: 0.1 } },
|
|
2750
|
+
{ name: "Fishbowl", values: { uZoom: 1.5, uRadius: 0.35, uAberration: 1 } }
|
|
2751
|
+
]
|
|
2752
|
+
};
|
|
2753
|
+
var holoCard = {
|
|
2754
|
+
kind: "shader",
|
|
2755
|
+
id: "holo-card",
|
|
2756
|
+
name: "Holo card",
|
|
2757
|
+
description: "A holographic foil trading card whose rainbow sheen and glare shift as the pointer tilts it.",
|
|
2758
|
+
category: "interaction",
|
|
2759
|
+
tags: ["interactive", "hover", "card", "holographic", "foil", "iridescent"],
|
|
2760
|
+
transparent: true,
|
|
2761
|
+
source: glsl(
|
|
2762
|
+
`// Holo card: a rounded card with rainbow foil and glare that move as the pointer tilts it.
|
|
2763
|
+
uniform sampler2D uImage; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.portrait} label="Image"
|
|
2764
|
+
uniform float uFoil; // @control min=0 max=1 default=0.7 label="Foil"
|
|
2765
|
+
uniform float uGlare; // @control min=0 max=1 default=0.6 label="Glare"
|
|
2766
|
+
uniform float uTilt; // @control min=0 max=1 default=0.6 label="Tilt"
|
|
2767
|
+
uniform float uCorner; // @control min=0 max=0.25 default=0.06 label="Corner radius"
|
|
2768
|
+
uniform vec3 uBorder; // @control color default=#E2E8F0 label="Border"
|
|
2769
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2770
|
+
HASH,
|
|
2771
|
+
NOISE,
|
|
2772
|
+
HOVER,
|
|
2773
|
+
OVER,
|
|
2774
|
+
PICTURE,
|
|
2775
|
+
`float roundedBox(vec2 p, vec2 hs, float r) {
|
|
2776
|
+
vec2 e = abs(p) - hs + r;
|
|
2777
|
+
return length(max(e, 0.0)) + min(max(e.x, e.y), 0.0) - r;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2781
|
+
vec2 res = iResolution.xy;
|
|
2782
|
+
const float aspect = 0.716;
|
|
2783
|
+
float h = min(res.y * 0.84, res.x * 0.84 / aspect);
|
|
2784
|
+
vec2 hs = vec2(h * aspect, h) * 0.5;
|
|
2785
|
+
vec3 hp = hoverPoint(uDemo, 0.7);
|
|
2786
|
+
vec2 tilt = clamp((hp.xy - 0.5 * res) / hs, -1.0, 1.0) * hp.z * uTilt;
|
|
2787
|
+
vec2 s = (fragCoord - 0.5 * res) / hs;
|
|
2788
|
+
// Fake perspective: the side the pointer is on leans away.
|
|
2789
|
+
vec2 q = s / max(1.0 - 0.15 * dot(s, tilt), 0.2);
|
|
2790
|
+
float radius = uCorner * hs.x * 2.0;
|
|
2791
|
+
float sd = roundedBox(q * hs, hs, radius);
|
|
2792
|
+
float mask = 1.0 - smoothstep(-1.0, 1.0, sd);
|
|
2793
|
+
// A soft shadow, falling away from the tilt.
|
|
2794
|
+
float shadowD = roundedBox(fragCoord - 0.5 * res + tilt * hs * 0.06 + vec2(0.0, h * 0.03), hs, radius);
|
|
2795
|
+
float shadow = (1.0 - smoothstep(-h * 0.04, h * 0.06, shadowD)) * 0.35;
|
|
2796
|
+
vec4 acc = over(vec4(0.0), vec3(0.0), shadow * (1.0 - mask));
|
|
2797
|
+
if (mask > 0.0) {
|
|
2798
|
+
vec2 cuv = q * 0.5 + 0.5;
|
|
2799
|
+
// The picture fills the card, cropped to fit.
|
|
2800
|
+
vec2 ts = vec2(textureSize(uImage, 0));
|
|
2801
|
+
float imageAspect = ts.x / max(ts.y, 1.0);
|
|
2802
|
+
vec2 fit = imageAspect > aspect ? vec2(aspect / imageAspect, 1.0) : vec2(1.0, imageAspect / aspect);
|
|
2803
|
+
vec3 base = picture(uImage, 0.5 + (cuv - 0.5) * fit);
|
|
2804
|
+
// Foil: rainbow bands that slide with the tilt, strongest in a broad band facing the light.
|
|
2805
|
+
float n = vnoise(cuv * 5.0);
|
|
2806
|
+
float phase = dot(cuv, vec2(0.7, 1.1)) * 1.6 + tilt.x * 0.9 - tilt.y * 0.6 + n * 0.5;
|
|
2807
|
+
vec3 rainbow = 0.5 + 0.5 * cos(6.28318 * (phase + vec3(0.0, 0.33, 0.67)));
|
|
2808
|
+
float lines = 0.75 + 0.25 * sin((cuv.x - cuv.y) * 90.0 + tilt.x * 8.0);
|
|
2809
|
+
float band = exp(-pow((cuv.x + cuv.y) * 0.7 - 0.7 - (tilt.x + tilt.y) * 0.6, 2.0) * 6.0);
|
|
2810
|
+
float foil = uFoil * (0.25 + 0.75 * band) * lines;
|
|
2811
|
+
// Overlay blend, so the picture keeps its darks under the foil.
|
|
2812
|
+
vec3 lit = mix(2.0 * base * rainbow, 1.0 - 2.0 * (1.0 - base) * (1.0 - rainbow), step(0.5, base));
|
|
2813
|
+
vec3 col = mix(base, lit + rainbow * 0.12, foil * 0.75);
|
|
2814
|
+
float sparkle = step(0.985, hash12(floor(cuv * vec2(90.0, 126.0))));
|
|
2815
|
+
col += sparkle * uFoil * band * (0.5 + 0.5 * sin(iTime * 3.0 + n * 20.0 + tilt.x * 10.0));
|
|
2816
|
+
// Glare: a highlight where the light hits, following the pointer.
|
|
2817
|
+
vec2 g = cuv - (0.5 + tilt * 0.55);
|
|
2818
|
+
col += uGlare * 0.6 * exp(-dot(g, g) * 7.0);
|
|
2819
|
+
// A thin foil border.
|
|
2820
|
+
float border = smoothstep(-0.03 * h - 1.0, -0.03 * h, sd);
|
|
2821
|
+
col = mix(col, uBorder * (0.8 + 0.4 * rainbow * uFoil), border);
|
|
2822
|
+
acc = over(acc, clamp(col, 0.0, 1.0), mask);
|
|
2823
|
+
}
|
|
2824
|
+
fragColor = unpremultiply(acc);
|
|
2825
|
+
}`
|
|
2826
|
+
),
|
|
2827
|
+
presets: [
|
|
2828
|
+
{ name: "Subtle", values: { uFoil: 0.3, uGlare: 0.35, uTilt: 0.4 } },
|
|
2829
|
+
{ name: "Full foil", values: { uFoil: 1, uGlare: 0.8, uTilt: 1, uBorder: [0.98, 0.82, 0.35] } }
|
|
2830
|
+
]
|
|
2831
|
+
};
|
|
2832
|
+
var pressCharge = {
|
|
2833
|
+
kind: "shader",
|
|
2834
|
+
id: "press-charge",
|
|
2835
|
+
name: "Press to charge",
|
|
2836
|
+
description: "A ring that fills and glows while the pointer is held and drains when it is let go, bursting if it was fully charged.",
|
|
2837
|
+
category: "interaction",
|
|
2838
|
+
tags: ["interactive", "press", "overlay", "hold", "button", "ring"],
|
|
2839
|
+
transparent: true,
|
|
2840
|
+
source: glsl(
|
|
2841
|
+
`// Press to charge: a ring that fills while held (iPointer.w > 0), drains when let go (iPointer.w < 0)
|
|
2842
|
+
// and bursts if it was let go fully charged.
|
|
2843
|
+
uniform vec3 uColor; // @control color default=#A78BFA label="Colour"
|
|
2844
|
+
uniform float uHold; // @control min=0.3 max=5 default=1.2 label="Seconds to charge"
|
|
2845
|
+
uniform float uSize; // @control min=0.08 max=0.45 default=0.22 label="Size"
|
|
2846
|
+
uniform float uThickness; // @control min=0.005 max=0.08 default=0.025 label="Thickness"
|
|
2847
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2848
|
+
OVER,
|
|
2849
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
2850
|
+
float px = 1.0 / iResolution.y;
|
|
2851
|
+
// iPointer.w: seconds held while pressed, minus the seconds since letting go after, 0 before any press.
|
|
2852
|
+
float w = iPointer.w;
|
|
2853
|
+
// The press is the newest click, so this is how long it was held when let go.
|
|
2854
|
+
float heldFor = iTime + min(w, 0.0) - iClicks[0].z;
|
|
2855
|
+
// Until someone presses, a synthetic press: hold, charge, let go, burst, rest.
|
|
2856
|
+
if (uDemo && iClicks[0].w < 0.5 && w == 0.0) {
|
|
2857
|
+
float t = mod(iTime, uHold + 2.0) - 0.4;
|
|
2858
|
+
float hold = uHold + 0.4;
|
|
2859
|
+
w = t <= 0.0 ? 0.0 : (t < hold ? t : -(t - hold));
|
|
2860
|
+
heldFor = hold;
|
|
2861
|
+
}
|
|
2862
|
+
float held = max(w, 0.0);
|
|
2863
|
+
float since = max(-w, 0.0);
|
|
2864
|
+
float reached = w < 0.0 ? clamp(heldFor / uHold, 0.0, 1.0) : 0.0;
|
|
2865
|
+
// While held it fills; once let go it drains from where it got to, over 0.4 s.
|
|
2866
|
+
float drain = 1.0 - smoothstep(0.0, 0.4, since);
|
|
2867
|
+
float charge = w > 0.0 ? clamp(held / uHold, 0.0, 1.0) : reached * drain;
|
|
2868
|
+
float full = step(uHold, held);
|
|
2869
|
+
vec2 p = (fragCoord - 0.5 * iResolution.xy) * px;
|
|
2870
|
+
float r = length(p);
|
|
2871
|
+
float turn = fract(atan(p.x, p.y) / 6.28318 + 1.0);
|
|
2872
|
+
float R = uSize * (1.0 - 0.06 * charge + 0.03 * iPointer.z);
|
|
2873
|
+
float ringD = abs(r - R) - uThickness * 0.5;
|
|
2874
|
+
float ring = 1.0 - smoothstep(-px, px, ringD);
|
|
2875
|
+
vec4 acc = vec4(0.0);
|
|
2876
|
+
// The core fills faintly, and the track shows where the charge goes.
|
|
2877
|
+
acc = over(acc, uColor, (1.0 - smoothstep(R - px, R + px, r)) * 0.1 * charge);
|
|
2878
|
+
acc = over(acc, uColor * 0.6, ring * (0.22 + 0.1 * iPointer.z));
|
|
2879
|
+
// The charge: an arc clockwise from the top, glowing brighter as it fills.
|
|
2880
|
+
float arcEnd = (charge - turn) * 6.28318 * R / px;
|
|
2881
|
+
float arc = ring * clamp(arcEnd + 0.5, 0.0, 1.0) * step(0.001, charge);
|
|
2882
|
+
float pulse = 1.0 + full * 0.25 * sin(iTime * 9.0);
|
|
2883
|
+
float halo = exp(-max(ringD, 0.0) / (uThickness * 1.5)) * charge * charge * 0.6 * clamp(arcEnd + 0.5, 0.0, 1.0);
|
|
2884
|
+
acc = over(acc, uColor, clamp(halo * pulse, 0.0, 1.0));
|
|
2885
|
+
acc = over(acc, mix(uColor, vec3(1.0), 0.35 * charge * pulse), arc);
|
|
2886
|
+
// A ping when it fills, and a flash with a ring bursting out when let go fully charged.
|
|
2887
|
+
float flashAge = -1.0;
|
|
2888
|
+
float flashSize = 0.0;
|
|
2889
|
+
if (w > 0.0 && held >= uHold) { flashAge = held - uHold; flashSize = 0.5; }
|
|
2890
|
+
if (w < 0.0 && reached >= 1.0) { flashAge = since; flashSize = 1.0; }
|
|
2891
|
+
if (flashAge >= 0.0 && flashAge < 0.8) {
|
|
2892
|
+
float k = 1.0 - flashAge / 0.8;
|
|
2893
|
+
float burstR = R + flashAge * 0.6 * flashSize;
|
|
2894
|
+
float burst = exp(-pow((r - burstR) / (uThickness * (1.0 + flashAge * 3.0)), 2.0)) * k * k * flashSize;
|
|
2895
|
+
acc = over(acc, mix(uColor, vec3(1.0), 0.5), clamp(burst, 0.0, 1.0));
|
|
2896
|
+
acc = over(acc, vec3(1.0), exp(-r * r / (R * R * 0.5)) * exp(-flashAge * 6.0) * 0.8 * flashSize);
|
|
2897
|
+
}
|
|
2898
|
+
fragColor = unpremultiply(acc);
|
|
2899
|
+
}`
|
|
2900
|
+
),
|
|
2901
|
+
presets: [
|
|
2902
|
+
{ name: "Hold to confirm", values: { uColor: [0.95, 0.3, 0.3], uHold: 2, uSize: 0.15, uThickness: 0.015 } },
|
|
2903
|
+
{ name: "Power up", values: { uColor: [0.2, 0.9, 1], uHold: 0.8, uSize: 0.3, uThickness: 0.05 } }
|
|
2904
|
+
]
|
|
1294
2905
|
};
|
|
1295
|
-
var
|
|
2906
|
+
var hoverSwap = {
|
|
1296
2907
|
kind: "shader",
|
|
1297
|
-
id: "
|
|
1298
|
-
name: "
|
|
1299
|
-
description: "
|
|
1300
|
-
category: "
|
|
1301
|
-
tags: ["
|
|
2908
|
+
id: "hover-swap",
|
|
2909
|
+
name: "Hover swap",
|
|
2910
|
+
description: "One image burning away into another from the pointer as it hovers, along a glowing ragged edge.",
|
|
2911
|
+
category: "interaction",
|
|
2912
|
+
tags: ["interactive", "hover", "image", "dissolve", "transition"],
|
|
1302
2913
|
source: glsl(
|
|
1303
|
-
`//
|
|
1304
|
-
uniform
|
|
1305
|
-
uniform
|
|
1306
|
-
uniform
|
|
1307
|
-
uniform
|
|
1308
|
-
|
|
2914
|
+
`// Hover swap: image A dissolves into image B as hover goes 0 \u2192 1, spreading from the pointer.
|
|
2915
|
+
uniform sampler2D uFrom; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.landscape} label="Image"
|
|
2916
|
+
uniform sampler2D uTo; // @control filter=linear wrap=clamp default=${SAMPLE_IMAGES.city} label="Hover image"
|
|
2917
|
+
uniform vec3 uEdge; // @control color default=#FFB86B label="Edge glow"
|
|
2918
|
+
uniform float uEdgeWidth; // @control min=0 max=0.3 default=0.08 label="Edge width"
|
|
2919
|
+
uniform float uGrain; // @control min=1 max=20 default=6 label="Noise scale"
|
|
2920
|
+
uniform bool uDemo; // @control default=true label="Demo when idle"`,
|
|
2921
|
+
HASH,
|
|
2922
|
+
NOISE,
|
|
2923
|
+
FBM,
|
|
2924
|
+
HOVER,
|
|
2925
|
+
PICTURE,
|
|
1309
2926
|
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
1310
|
-
vec2
|
|
1311
|
-
|
|
1312
|
-
float
|
|
1313
|
-
|
|
1314
|
-
float
|
|
1315
|
-
float
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
vec2
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
2927
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
2928
|
+
vec3 hp = hoverPoint(uDemo, 0.0);
|
|
2929
|
+
float h = clamp(iPointer.z, 0.0, 1.0);
|
|
2930
|
+
// Idle, hover swings slowly in and out.
|
|
2931
|
+
float demo = uDemo ? smoothstep(0.1, 0.9, 0.5 - 0.5 * cos(iTime * 0.9)) : 0.0;
|
|
2932
|
+
float progress = mix(demo, 1.0, h);
|
|
2933
|
+
// Distance from the pointer, as a share of the way to the farthest corner.
|
|
2934
|
+
vec2 o = hp.xy / iResolution.y;
|
|
2935
|
+
vec2 size = iResolution.xy / iResolution.y;
|
|
2936
|
+
float far = max(max(length(o), length(o - vec2(size.x, 0.0))), max(length(o - vec2(0.0, size.y)), length(o - size)));
|
|
2937
|
+
float dist = length(fragCoord / iResolution.y - o) / far;
|
|
2938
|
+
float f = 0.65 * dist + 0.35 * fbm(fragCoord / iResolution.y * uGrain, 5);
|
|
2939
|
+
float w = max(uEdgeWidth, 0.005);
|
|
2940
|
+
float t = progress * (1.0 + w);
|
|
2941
|
+
float m = 1.0 - smoothstep(t - w, t, f);
|
|
2942
|
+
vec3 col = mix(picture(uFrom, uv), picture(uTo, uv), m);
|
|
2943
|
+
float edge = m * (1.0 - m) * 4.0 * step(0.001, uEdgeWidth);
|
|
2944
|
+
col = mix(col, uEdge, edge * 0.85);
|
|
2945
|
+
fragColor = vec4(col, 1.0);
|
|
1324
2946
|
}`
|
|
1325
|
-
)
|
|
2947
|
+
),
|
|
2948
|
+
presets: [
|
|
2949
|
+
{ name: "Clean cut", values: { uEdgeWidth: 0.01, uGrain: 2 } },
|
|
2950
|
+
{ name: "Embers", values: { uEdge: [1, 0.35, 0.1], uEdgeWidth: 0.2, uGrain: 14 } }
|
|
2951
|
+
]
|
|
1326
2952
|
};
|
|
1327
|
-
var
|
|
2953
|
+
var interactionShaders = [
|
|
2954
|
+
clickRipple,
|
|
2955
|
+
clickBurst,
|
|
2956
|
+
cursorGlow,
|
|
2957
|
+
hoverLiquid,
|
|
2958
|
+
hoverReveal,
|
|
2959
|
+
magnifier,
|
|
2960
|
+
holoCard,
|
|
2961
|
+
pressCharge,
|
|
2962
|
+
hoverSwap
|
|
2963
|
+
];
|
|
2964
|
+
|
|
2965
|
+
// src/catalog/shaders/morph.ts
|
|
2966
|
+
var pair = (from, to) => `uniform sampler2D uFrom; // @control filter=linear wrap=clamp default=${from} label="From image"
|
|
2967
|
+
uniform sampler2D uTo; // @control filter=linear wrap=clamp default=${to} label="To image"
|
|
2968
|
+
uniform bool uAnimate; // @control default=true label="Animate"
|
|
2969
|
+
uniform float uProgress; // @control min=0 max=1 default=0.5 label="Progress (when still)"`;
|
|
2970
|
+
var COMMON = `
|
|
2971
|
+
float progress() {
|
|
2972
|
+
return uAnimate ? smoothstep(0.1, 0.9, 0.5 - 0.5 * cos(iTime * 6.28318 / 4.0)) : uProgress;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
// The share of an image visible across and up when it covers the frame (the test card fits it).
|
|
2976
|
+
vec2 coverScale(sampler2D tex) {
|
|
2977
|
+
vec2 size = vec2(textureSize(tex, 0));
|
|
2978
|
+
float frameAspect = iResolution.x / iResolution.y;
|
|
2979
|
+
float imageAspect = size.x > 1.0 ? size.x / size.y : frameAspect;
|
|
2980
|
+
return frameAspect > imageAspect ? vec2(1.0, imageAspect / frameAspect) : vec2(frameAspect / imageAspect, 1.0);
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
vec3 cover(sampler2D tex, vec2 uv) {
|
|
2984
|
+
return picture(tex, 0.5 + (uv - 0.5) * coverScale(tex));
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
float luma(vec3 c) {
|
|
2988
|
+
return dot(c, vec3(0.2126, 0.7152, 0.0722));
|
|
2989
|
+
}`.trim();
|
|
2990
|
+
var featureMorph = {
|
|
1328
2991
|
kind: "shader",
|
|
1329
|
-
id: "
|
|
1330
|
-
name: "
|
|
1331
|
-
description: "
|
|
1332
|
-
category: "
|
|
1333
|
-
tags: ["
|
|
2992
|
+
id: "feature-morph",
|
|
2993
|
+
name: "Feature morph",
|
|
2994
|
+
description: "The classic face morph: two pictures warp so five matching points (eyes, mouth, chin, hairline) meet while they cross-fade.",
|
|
2995
|
+
category: "morph",
|
|
2996
|
+
tags: ["morph", "face", "warp", "image", "transition"],
|
|
2997
|
+
loop: 4,
|
|
1334
2998
|
source: glsl(
|
|
1335
|
-
`//
|
|
1336
|
-
|
|
1337
|
-
uniform
|
|
1338
|
-
uniform
|
|
1339
|
-
uniform
|
|
1340
|
-
uniform
|
|
1341
|
-
uniform
|
|
1342
|
-
uniform
|
|
1343
|
-
uniform
|
|
1344
|
-
uniform
|
|
1345
|
-
uniform
|
|
1346
|
-
uniform
|
|
1347
|
-
uniform
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
`//
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
2999
|
+
`// Feature morph: both images warp towards in-between positions of five matching points while they cross-fade.
|
|
3000
|
+
${pair(SAMPLE_IMAGES.portrait, SAMPLE_IMAGES.portrait2)}
|
|
3001
|
+
uniform vec2 uFromA; // @control min=0 max=1 default=0.461,0.583 label="From: left eye"
|
|
3002
|
+
uniform vec2 uFromB; // @control min=0 max=1 default=0.539,0.583 label="From: right eye"
|
|
3003
|
+
uniform vec2 uFromC; // @control min=0 max=1 default=0.5,0.472 label="From: mouth"
|
|
3004
|
+
uniform vec2 uFromD; // @control min=0 max=1 default=0.5,0.389 label="From: chin"
|
|
3005
|
+
uniform vec2 uFromE; // @control min=0 max=1 default=0.5,0.778 label="From: hairline"
|
|
3006
|
+
uniform vec2 uToA; // @control min=0 max=1 default=0.619,0.514 label="To: left eye"
|
|
3007
|
+
uniform vec2 uToB; // @control min=0 max=1 default=0.725,0.514 label="To: right eye"
|
|
3008
|
+
uniform vec2 uToC; // @control min=0 max=1 default=0.672,0.364 label="To: mouth"
|
|
3009
|
+
uniform vec2 uToD; // @control min=0 max=1 default=0.672,0.25 label="To: chin"
|
|
3010
|
+
uniform vec2 uToE; // @control min=0 max=1 default=0.672,0.778 label="To: hairline"
|
|
3011
|
+
uniform bool uShowPoints; // @control default=false label="Show points"`,
|
|
3012
|
+
PICTURE,
|
|
3013
|
+
COMMON,
|
|
3014
|
+
`// Points are given in image uv; the warp works in frame space (height 1, x scaled by the aspect).
|
|
3015
|
+
vec2 toFrame(vec2 imageUv, vec2 scale) {
|
|
3016
|
+
return (0.5 + (imageUv - 0.5) / scale) * vec2(iResolution.x / iResolution.y, 1.0);
|
|
3017
|
+
}
|
|
1354
3018
|
|
|
1355
|
-
|
|
1356
|
-
return
|
|
3019
|
+
vec2 toImage(vec2 q, vec2 scale) {
|
|
3020
|
+
return 0.5 + (q / vec2(iResolution.x / iResolution.y, 1.0) - 0.5) * scale;
|
|
1357
3021
|
}
|
|
1358
3022
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
return h < 0.25 ? uColorA : h < 0.5 ? uColorB : h < 0.75 ? uColorC : uColorD;
|
|
3023
|
+
vec2 cmul(vec2 a, vec2 b) {
|
|
3024
|
+
return vec2(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);
|
|
1362
3025
|
}
|
|
1363
3026
|
|
|
1364
|
-
//
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
3027
|
+
// Where frame point x, among the in-between points m, comes from in a picture whose points are s:
|
|
3028
|
+
// a best-fit similarity (move, scale, turn) carries m onto s, then inverse-distance weighting of
|
|
3029
|
+
// what is left over bends each point exactly onto its partner.
|
|
3030
|
+
vec2 warp(vec2 x, vec2 m[5], vec2 s[5]) {
|
|
3031
|
+
vec2 cm = vec2(0.0);
|
|
3032
|
+
vec2 cs = vec2(0.0);
|
|
3033
|
+
for (int i = 0; i < 5; i++) {
|
|
3034
|
+
cm += m[i] * 0.2;
|
|
3035
|
+
cs += s[i] * 0.2;
|
|
3036
|
+
}
|
|
3037
|
+
vec2 num = vec2(0.0);
|
|
3038
|
+
float den = 0.0;
|
|
3039
|
+
for (int i = 0; i < 5; i++) {
|
|
3040
|
+
vec2 a = m[i] - cm;
|
|
3041
|
+
num += cmul(vec2(a.x, -a.y), s[i] - cs);
|
|
3042
|
+
den += dot(a, a);
|
|
3043
|
+
}
|
|
3044
|
+
vec2 k = num / max(den, 1e-6);
|
|
3045
|
+
vec2 bend = vec2(0.0);
|
|
3046
|
+
float total = 0.0;
|
|
3047
|
+
for (int i = 0; i < 5; i++) {
|
|
3048
|
+
vec2 left = s[i] - (cs + cmul(k, m[i] - cm));
|
|
3049
|
+
float d2 = dot(x - m[i], x - m[i]);
|
|
3050
|
+
float w = 1.0 / (d2 * d2 + 1e-6);
|
|
3051
|
+
bend += w * left;
|
|
3052
|
+
total += w;
|
|
3053
|
+
}
|
|
3054
|
+
return cs + cmul(k, x - cm) + bend / total;
|
|
1368
3055
|
}
|
|
1369
3056
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
float
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
3057
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
3058
|
+
float t = progress();
|
|
3059
|
+
vec2 scaleA = coverScale(uFrom);
|
|
3060
|
+
vec2 scaleB = coverScale(uTo);
|
|
3061
|
+
vec2 a[5] = vec2[5](toFrame(uFromA, scaleA), toFrame(uFromB, scaleA), toFrame(uFromC, scaleA), toFrame(uFromD, scaleA), toFrame(uFromE, scaleA));
|
|
3062
|
+
vec2 b[5] = vec2[5](toFrame(uToA, scaleB), toFrame(uToB, scaleB), toFrame(uToC, scaleB), toFrame(uToD, scaleB), toFrame(uToE, scaleB));
|
|
3063
|
+
vec2 m[5];
|
|
3064
|
+
for (int i = 0; i < 5; i++) m[i] = mix(a[i], b[i], t);
|
|
3065
|
+
vec2 x = fragCoord / iResolution.y;
|
|
3066
|
+
vec3 colA = picture(uFrom, toImage(warp(x, m, a), scaleA));
|
|
3067
|
+
vec3 colB = picture(uTo, toImage(warp(x, m, b), scaleB));
|
|
3068
|
+
vec3 col = mix(colA, colB, smoothstep(0.2, 0.8, t));
|
|
3069
|
+
if (uShowPoints) {
|
|
3070
|
+
float px = 1.0 / iResolution.y;
|
|
3071
|
+
for (int i = 0; i < 5; i++) {
|
|
3072
|
+
float d = length(x - m[i]);
|
|
3073
|
+
col = mix(col, vec3(0.0), smoothstep(px * 1.5, 0.0, abs(d - 0.012)) * 0.6);
|
|
3074
|
+
col = mix(col, vec3(1.0, 0.85, 0.2), smoothstep(px, 0.0, abs(d - 0.012)));
|
|
3075
|
+
}
|
|
1387
3076
|
}
|
|
1388
|
-
|
|
3077
|
+
fragColor = vec4(col, 1.0);
|
|
3078
|
+
}`
|
|
3079
|
+
),
|
|
3080
|
+
presets: [{ name: "Show points", values: { uShowPoints: true, uAnimate: false, uProgress: 0.5 } }]
|
|
3081
|
+
};
|
|
3082
|
+
var silhouetteMorph = {
|
|
3083
|
+
kind: "shader",
|
|
3084
|
+
id: "silhouette-morph",
|
|
3085
|
+
name: "Silhouette morph",
|
|
3086
|
+
description: "One silhouette or logo flowing into another as a gooey blob, its colour blending from the first shape's to the second's.",
|
|
3087
|
+
category: "morph",
|
|
3088
|
+
tags: ["morph", "shape", "logo", "goo", "blob", "text"],
|
|
3089
|
+
loop: 4,
|
|
3090
|
+
source: glsl(
|
|
3091
|
+
`// Silhouette morph: blurred alpha masks of two shapes mixed by progress and thresholded, so one flows into the other.
|
|
3092
|
+
uniform sampler2D uFrom; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.logo} label="From shape"
|
|
3093
|
+
uniform sampler2D uTo; // @control filter=mipmap wrap=clamp default=${SAMPLE_IMAGES.heart} label="To shape"
|
|
3094
|
+
uniform bool uAnimate; // @control default=true label="Animate"
|
|
3095
|
+
uniform float uProgress; // @control min=0 max=1 default=0.5 label="Progress (when still)"
|
|
3096
|
+
uniform float uSize; // @control min=0.2 max=1 default=0.75 label="Shape size"
|
|
3097
|
+
uniform float uGoo; // @control min=0 max=0.3 default=0.1 label="Gooeyness"
|
|
3098
|
+
uniform float uEdge; // @control min=0 max=0.05 default=0.004 label="Edge softness"
|
|
3099
|
+
uniform bool uImageColours; // @control default=true label="Use the shapes' own colours"
|
|
3100
|
+
uniform vec3 uColourA; // @control color default=#6366F1 label="From colour"
|
|
3101
|
+
uniform vec3 uColourB; // @control color default=#F43F5E label="To colour"
|
|
3102
|
+
uniform vec3 uBackground; // @control color default=#0F172A label="Background"`,
|
|
3103
|
+
`// A shape's colour (premultiplied) and alpha at frame point p (height 1, centred), the image contained in
|
|
3104
|
+
// uSize of the frame, read from a mip level about as coarse as the spacing (in frame units), so sparse blur
|
|
3105
|
+
// taps still sum smoothly. With no image, a disc (fallback 0) or a rounded square (1).
|
|
3106
|
+
vec4 shapeAt(sampler2D tex, vec2 p, float spacing, int fallback) {
|
|
3107
|
+
vec2 size = vec2(textureSize(tex, 0));
|
|
3108
|
+
if (size.x <= 1.0) {
|
|
3109
|
+
float d = fallback == 0 ? length(p) - 0.3 * uSize : length(max(abs(p) - 0.2 * uSize, 0.0)) - 0.08 * uSize;
|
|
3110
|
+
float a = step(d, 0.0);
|
|
3111
|
+
return vec4(vec3(a), a);
|
|
3112
|
+
}
|
|
3113
|
+
float aspect = size.x / size.y;
|
|
3114
|
+
float h = min(uSize, iResolution.x / iResolution.y * uSize / aspect);
|
|
3115
|
+
vec2 uv = p / vec2(h * aspect, h) + 0.5;
|
|
3116
|
+
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) return vec4(0.0);
|
|
3117
|
+
vec4 c = textureLod(tex, uv, log2(max(spacing * size.y / h, 1.0)));
|
|
3118
|
+
// Clear texels upload black, so filtered colour is already about colour \xD7 alpha: divide it out, then weight by alpha.
|
|
3119
|
+
return vec4(clamp(c.rgb / max(c.a, 1e-3), 0.0, 1.0) * c.a, c.a);
|
|
1389
3120
|
}
|
|
1390
3121
|
|
|
1391
|
-
//
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
3122
|
+
// A disc blur of the shape: 28 taps on a golden-angle spiral.
|
|
3123
|
+
vec4 blurred(sampler2D tex, vec2 p, float radius, int fallback) {
|
|
3124
|
+
vec4 sum = vec4(0.0);
|
|
3125
|
+
for (int i = 0; i < 28; i++) {
|
|
3126
|
+
float r = radius * sqrt((float(i) + 0.5) / 28.0);
|
|
3127
|
+
float a = float(i) * 2.39996;
|
|
3128
|
+
sum += shapeAt(tex, p + r * vec2(cos(a), sin(a)), radius * 0.35, fallback);
|
|
3129
|
+
}
|
|
3130
|
+
return sum / 28.0;
|
|
1398
3131
|
}
|
|
1399
3132
|
|
|
1400
3133
|
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
1401
|
-
float
|
|
1402
|
-
vec2 p = fragCoord *
|
|
1403
|
-
|
|
3134
|
+
float t = uAnimate ? smoothstep(0.1, 0.9, 0.5 - 0.5 * cos(iTime * 6.28318 / 4.0)) : uProgress;
|
|
3135
|
+
vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
|
|
3136
|
+
// Shapes are sharp at either end and softest half way, where the blobs merge.
|
|
3137
|
+
float radius = uGoo * sin(3.14159 * t) + 1.0 / iResolution.y;
|
|
3138
|
+
vec4 a = blurred(uFrom, p, radius, 0);
|
|
3139
|
+
vec4 b = blurred(uTo, p, radius, 1);
|
|
3140
|
+
float field = mix(a.a, b.a, t);
|
|
3141
|
+
float w = uEdge + fwidth(field) * 0.75;
|
|
3142
|
+
float inside = smoothstep(0.5 - w, 0.5 + w, field);
|
|
3143
|
+
// Each shape's colour here (or the other's, where only one shape reaches), blended by progress.
|
|
3144
|
+
vec3 ca = uImageColours ? a.rgb / max(a.a, 1e-4) : uColourA;
|
|
3145
|
+
vec3 cb = uImageColours ? b.rgb / max(b.a, 1e-4) : uColourB;
|
|
3146
|
+
ca = mix(cb, ca, smoothstep(0.0, 0.05, a.a));
|
|
3147
|
+
cb = mix(ca, cb, smoothstep(0.0, 0.05, b.a));
|
|
3148
|
+
vec3 tint = mix(ca, cb, smoothstep(0.2, 0.8, t));
|
|
3149
|
+
// A little roundness: the thick middle of a blob is lighter than its rim.
|
|
3150
|
+
tint *= 0.82 + 0.25 * smoothstep(0.5, 1.0, field);
|
|
3151
|
+
fragColor = vec4(mix(uBackground, tint, inside), 1.0);
|
|
3152
|
+
}`
|
|
3153
|
+
),
|
|
3154
|
+
presets: [
|
|
3155
|
+
{
|
|
3156
|
+
name: "Hello \u2192 World",
|
|
3157
|
+
values: { uFrom: textImage("HELLO"), uTo: textImage("WORLD"), uImageColours: false, uSize: 0.9, uGoo: 0.05, uColourA: [0.98, 0.75, 0.14], uColourB: [0.13, 0.83, 0.93] }
|
|
3158
|
+
},
|
|
3159
|
+
{ name: "Blobby", values: { uGoo: 0.25, uBackground: [1, 0.97, 0.93] } }
|
|
3160
|
+
]
|
|
3161
|
+
};
|
|
3162
|
+
var transitionLiquidMorph = {
|
|
3163
|
+
kind: "shader",
|
|
3164
|
+
id: "transition-liquid-morph",
|
|
3165
|
+
name: "Liquid morph",
|
|
3166
|
+
description: "Both images swept along a flowing, swirling current that peaks half way, dissolving one into the other like ink in water.",
|
|
3167
|
+
category: "transition",
|
|
3168
|
+
tags: ["transition", "morph", "liquid", "image", "flow"],
|
|
3169
|
+
loop: 4,
|
|
3170
|
+
source: glsl(
|
|
3171
|
+
`// Liquid morph: both images advected along a domain-warped fbm current, strongest at mid-progress, while cross-fading.
|
|
3172
|
+
${pair(SAMPLE_IMAGES.landscape, SAMPLE_IMAGES.city)}
|
|
3173
|
+
uniform float uStrength; // @control min=0 max=0.5 default=0.1 label="Strength"
|
|
3174
|
+
uniform float uScale; // @control min=0.5 max=8 default=2.5 label="Scale"
|
|
3175
|
+
uniform float uFlow; // @control min=0 max=2 default=0.4 label="Flow speed"`,
|
|
3176
|
+
HASH,
|
|
3177
|
+
NOISE,
|
|
3178
|
+
FBM,
|
|
3179
|
+
PICTURE,
|
|
3180
|
+
COMMON,
|
|
3181
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
3182
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
3183
|
+
float t = progress();
|
|
3184
|
+
float s = sin(3.14159 * t);
|
|
3185
|
+
float time = iTime * uFlow;
|
|
3186
|
+
vec2 q = fragCoord / iResolution.y * uScale;
|
|
3187
|
+
vec2 first = vec2(fbm(q + vec2(0.0, time), 4), fbm(q + vec2(5.2, 1.3) - vec2(time, 0.0), 4)) - 0.5;
|
|
3188
|
+
vec2 flow = vec2(fbm(q + 4.0 * first + vec2(1.7, 9.2) + time * 0.5, 4), fbm(q + 4.0 * first + vec2(8.3, 2.8) - time * 0.5, 4)) - 0.5;
|
|
3189
|
+
vec2 d = flow * uStrength * s * 2.0;
|
|
3190
|
+
// From is carried downstream, To arrives from upstream; neither moves at the ends.
|
|
3191
|
+
vec3 a = cover(uFrom, uv + d);
|
|
3192
|
+
vec3 b = cover(uTo, uv - d);
|
|
3193
|
+
float k = smoothstep(0.25, 0.75, t + (flow.x + flow.y) * 0.6 * s);
|
|
3194
|
+
fragColor = vec4(mix(a, b, k), 1.0);
|
|
3195
|
+
}`
|
|
3196
|
+
),
|
|
3197
|
+
presets: [
|
|
3198
|
+
{ name: "Gentle", values: { uStrength: 0.08, uScale: 1.5, uFlow: 0.2 } },
|
|
3199
|
+
{ name: "Turbulent", values: { uStrength: 0.35, uScale: 4, uFlow: 1.2 } }
|
|
3200
|
+
]
|
|
3201
|
+
};
|
|
3202
|
+
var transitionDisplacement = {
|
|
3203
|
+
kind: "shader",
|
|
3204
|
+
id: "transition-displacement",
|
|
3205
|
+
name: "Displacement",
|
|
3206
|
+
description: "Each image's brightness pushes it out in one direction as the next slides in, warped by its own light and dark.",
|
|
3207
|
+
category: "transition",
|
|
3208
|
+
tags: ["transition", "morph", "displacement", "image", "warp"],
|
|
3209
|
+
loop: 4,
|
|
3210
|
+
source: glsl(
|
|
3211
|
+
`// Displacement: From's luminance drives a directional warp out, To's luminance the same warp in reverse.
|
|
3212
|
+
${pair(SAMPLE_IMAGES.city, SAMPLE_IMAGES.landscape)}
|
|
3213
|
+
uniform float uStrength; // @control min=0 max=1 default=0.4 label="Strength"
|
|
3214
|
+
uniform float uAngle; // @control min=0 max=360 default=0 label="Direction (\xB0)"`,
|
|
3215
|
+
PICTURE,
|
|
3216
|
+
COMMON,
|
|
3217
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
3218
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
3219
|
+
float t = progress();
|
|
3220
|
+
vec2 dir = vec2(cos(radians(uAngle)), sin(radians(uAngle))) * vec2(iResolution.y / iResolution.x, 1.0);
|
|
3221
|
+
float la = luma(cover(uFrom, uv));
|
|
3222
|
+
float lb = luma(cover(uTo, uv));
|
|
3223
|
+
vec3 a = cover(uFrom, uv + dir * t * uStrength * la);
|
|
3224
|
+
vec3 b = cover(uTo, uv - dir * (1.0 - t) * uStrength * lb);
|
|
3225
|
+
fragColor = vec4(mix(a, b, smoothstep(0.2, 0.8, t)), 1.0);
|
|
3226
|
+
}`
|
|
3227
|
+
),
|
|
3228
|
+
presets: [
|
|
3229
|
+
{ name: "Upward smear", values: { uStrength: 0.6, uAngle: 90 } },
|
|
3230
|
+
{ name: "Subtle", values: { uStrength: 0.12, uAngle: 30 } }
|
|
3231
|
+
]
|
|
3232
|
+
};
|
|
3233
|
+
var transitionLuma = {
|
|
3234
|
+
kind: "shader",
|
|
3235
|
+
id: "transition-luma",
|
|
3236
|
+
name: "Luma fade",
|
|
3237
|
+
description: "Pixels switch to the next image in order of brightness, shadows first or highlights first, along a soft edge.",
|
|
3238
|
+
category: "transition",
|
|
3239
|
+
tags: ["transition", "morph", "luma", "image", "dissolve"],
|
|
3240
|
+
loop: 4,
|
|
3241
|
+
source: glsl(
|
|
3242
|
+
`// Luma fade: each pixel switches when progress passes its brightness, with a soft edge.
|
|
3243
|
+
${pair(SAMPLE_IMAGES.landscape, SAMPLE_IMAGES.city)}
|
|
3244
|
+
uniform float uSoftness; // @control min=0.01 max=0.5 default=0.1 label="Softness"
|
|
3245
|
+
uniform bool uLightFirst; // @control default=false label="Light first"
|
|
3246
|
+
uniform int uSource; // @control min=0 max=1 default=0 label="Brightness of (0 From, 1 To)"`,
|
|
3247
|
+
PICTURE,
|
|
3248
|
+
COMMON,
|
|
3249
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
3250
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
3251
|
+
float t = progress();
|
|
3252
|
+
vec3 a = cover(uFrom, uv);
|
|
3253
|
+
vec3 b = cover(uTo, uv);
|
|
3254
|
+
float l = clamp(luma(uSource == 0 ? a : b), 0.0, 1.0);
|
|
3255
|
+
if (uLightFirst) l = 1.0 - l;
|
|
3256
|
+
// The front sweeps from below the darkest pixel to above the lightest, so both ends are clean.
|
|
3257
|
+
float front = mix(-uSoftness, 1.0 + uSoftness, t);
|
|
3258
|
+
fragColor = vec4(mix(a, b, smoothstep(l - uSoftness, l + uSoftness, front)), 1.0);
|
|
3259
|
+
}`
|
|
3260
|
+
),
|
|
3261
|
+
presets: [
|
|
3262
|
+
{ name: "Highlights first", values: { uLightFirst: true, uSoftness: 0.06 } },
|
|
3263
|
+
{ name: "Hard threshold", values: { uSoftness: 0.01, uSource: 1 } }
|
|
3264
|
+
]
|
|
3265
|
+
};
|
|
3266
|
+
var transitionParticleSwap = {
|
|
3267
|
+
kind: "shader",
|
|
3268
|
+
id: "transition-particle-swap",
|
|
3269
|
+
name: "Particle swap",
|
|
3270
|
+
description: "The image breaks into square particles that tumble along curved, staggered paths and land as the next image.",
|
|
3271
|
+
category: "transition",
|
|
3272
|
+
tags: ["transition", "morph", "particles", "image", "mosaic"],
|
|
3273
|
+
loop: 4,
|
|
3274
|
+
source: glsl(
|
|
3275
|
+
`// Particle swap: each cell of From flies to a neighbouring cell's place, shrinking and spinning on the way, and lands as To.
|
|
3276
|
+
${pair(SAMPLE_IMAGES.landscape, SAMPLE_IMAGES.city)}
|
|
3277
|
+
uniform float uParticle; // @control min=6 max=80 default=20 label="Particle size (px)"
|
|
3278
|
+
uniform float uScatter; // @control min=0 max=0.8 default=0.45 label="Scatter"
|
|
3279
|
+
uniform float uSpin; // @control min=0 max=2 default=0.6 label="Spin & curl"
|
|
3280
|
+
uniform vec3 uBackground; // @control color default=#0B0B12 label="Background"`,
|
|
3281
|
+
HASH,
|
|
3282
|
+
ROTATE,
|
|
3283
|
+
PICTURE,
|
|
3284
|
+
COMMON,
|
|
3285
|
+
`const vec2 RING[4] = vec2[4](vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 1.0));
|
|
1404
3286
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
3287
|
+
// Where cell c's particle lands: cells trade places around each 2 \xD7 2 block, turned 1\u20133 steps.
|
|
3288
|
+
vec2 landing(vec2 c) {
|
|
3289
|
+
vec2 block = floor(c * 0.5);
|
|
3290
|
+
vec2 local = c - block * 2.0;
|
|
3291
|
+
int i = local.y < 0.5 ? int(local.x) : 3 - int(local.x);
|
|
3292
|
+
int turn = 1 + int(hash12(block + 7.3) * 2.999);
|
|
3293
|
+
return block * 2.0 + RING[(i + turn) % 4];
|
|
3294
|
+
}
|
|
1408
3295
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
3296
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
3297
|
+
float t = progress();
|
|
3298
|
+
vec2 p = fragCoord / uParticle;
|
|
3299
|
+
vec2 grid = iResolution.xy / uParticle;
|
|
3300
|
+
vec2 base = floor(p);
|
|
3301
|
+
vec3 col = uBackground;
|
|
3302
|
+
float top = -1.0;
|
|
3303
|
+
vec2 hitFrom = vec2(0.0);
|
|
3304
|
+
vec2 hitTo = vec2(0.0);
|
|
3305
|
+
float hitT = 0.0;
|
|
3306
|
+
float hitCover = 0.0;
|
|
3307
|
+
// Particles travel at most about 1.5 cells, so the 5 \xD7 5 cells around are every candidate.
|
|
3308
|
+
for (int j = -2; j <= 2; j++) {
|
|
3309
|
+
for (int i = -2; i <= 2; i++) {
|
|
3310
|
+
vec2 c = base + vec2(float(i), float(j));
|
|
3311
|
+
vec2 h = hash22(c);
|
|
3312
|
+
// Staggered: a sweep from the left plus some randomness.
|
|
3313
|
+
float delay = 0.5 * (0.55 * clamp(c.x / grid.x, 0.0, 1.0) + 0.45 * h.x);
|
|
3314
|
+
float k = smoothstep(0.0, 1.0, clamp((t - delay) / 0.5, 0.0, 1.0));
|
|
3315
|
+
float lift = sin(3.14159 * k);
|
|
3316
|
+
vec2 from = c + 0.5;
|
|
3317
|
+
vec2 to = landing(c) + 0.5;
|
|
3318
|
+
vec2 path = to - from;
|
|
3319
|
+
vec2 side = normalize(vec2(-path.y, path.x) + (h - 0.5) * 0.8);
|
|
3320
|
+
vec2 pos = mix(from, to, k) + side * lift * (uScatter + uSpin * 0.3) * (h.y > 0.5 ? 0.6 : -0.6);
|
|
3321
|
+
float size = 1.0 - 0.3 * lift;
|
|
3322
|
+
float angle = (h.y - 0.5) * 6.28318 * uSpin * lift;
|
|
3323
|
+
vec2 l = rot(-angle) * (p - pos) / size;
|
|
3324
|
+
float m = max(abs(l.x), abs(l.y));
|
|
3325
|
+
float z = h.y + lift;
|
|
3326
|
+
if (m <= 0.5 && z > top) {
|
|
3327
|
+
top = z;
|
|
3328
|
+
hitFrom = from + l;
|
|
3329
|
+
hitTo = to + l;
|
|
3330
|
+
hitT = k;
|
|
3331
|
+
hitCover = 1.0 - smoothstep(0.5 - 1.5 / uParticle, 0.5, m) * min(lift * 8.0, 1.0);
|
|
3332
|
+
}
|
|
1425
3333
|
}
|
|
1426
|
-
float core = 1.0 - smoothstep(w - px, w + px, d);
|
|
1427
|
-
float glow = uGlow * (0.5 * exp(-d / uWidth) + 0.2 * exp(-d / (uWidth * 5.0)));
|
|
1428
|
-
vec3 c = traceColor(k);
|
|
1429
|
-
light += c * (core * 0.8 + glow + uGlow * pulse) + mix(c, vec3(1.0), 0.7) * core * pulse * 3.0;
|
|
1430
3334
|
}
|
|
1431
|
-
|
|
3335
|
+
if (top > -1.0) {
|
|
3336
|
+
vec3 a = cover(uFrom, hitFrom / grid);
|
|
3337
|
+
vec3 b = cover(uTo, hitTo / grid);
|
|
3338
|
+
vec3 particle = mix(a, b, hitT) * (1.0 + 0.15 * sin(3.14159 * hitT));
|
|
3339
|
+
col = mix(col, particle, hitCover);
|
|
3340
|
+
}
|
|
3341
|
+
fragColor = vec4(col, 1.0);
|
|
1432
3342
|
}`
|
|
1433
3343
|
),
|
|
1434
3344
|
presets: [
|
|
1435
|
-
{ name: "
|
|
1436
|
-
{ name: "
|
|
1437
|
-
|
|
3345
|
+
{ name: "Confetti", values: { uParticle: 10, uScatter: 0.8, uSpin: 1.6 } },
|
|
3346
|
+
{ name: "Tiles", values: { uParticle: 48, uScatter: 0.2, uSpin: 0.15 } }
|
|
3347
|
+
]
|
|
3348
|
+
};
|
|
3349
|
+
var transitionInkBleed = {
|
|
3350
|
+
kind: "shader",
|
|
3351
|
+
id: "transition-ink-bleed",
|
|
3352
|
+
name: "Ink bleed",
|
|
3353
|
+
description: "The next image bleeds through the first like wet ink on paper, spreading from a few seeds with dark, pooled edges.",
|
|
3354
|
+
category: "transition",
|
|
3355
|
+
tags: ["transition", "morph", "ink", "watercolour", "image", "organic"],
|
|
3356
|
+
loop: 4,
|
|
3357
|
+
source: glsl(
|
|
3358
|
+
`// Ink bleed: To spreads through From along an fbm threshold grown from seeds, with a wet, darkened fringe.
|
|
3359
|
+
${pair(SAMPLE_IMAGES.city, SAMPLE_IMAGES.landscape)}
|
|
3360
|
+
uniform float uScale; // @control min=0.5 max=8 default=3 label="Scale"
|
|
3361
|
+
uniform float uEdge; // @control min=0.005 max=0.2 default=0.05 label="Edge width"
|
|
3362
|
+
uniform vec3 uInk; // @control color default=#1B1F3B label="Ink"`,
|
|
3363
|
+
HASH,
|
|
3364
|
+
NOISE,
|
|
3365
|
+
FBM,
|
|
3366
|
+
PICTURE,
|
|
3367
|
+
COMMON,
|
|
3368
|
+
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
3369
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
3370
|
+
float t = progress();
|
|
3371
|
+
vec2 q = fragCoord / iResolution.y;
|
|
3372
|
+
// Distance to the nearest of four seeds spread across the frame, measured through a noisy warp so the blots are ragged.
|
|
3373
|
+
vec2 warped = q + (vec2(fbm(q * uScale, 5), fbm(q * uScale + 31.7, 5)) - 0.5) * 0.35;
|
|
3374
|
+
float aspect = iResolution.x / iResolution.y;
|
|
3375
|
+
float near = 1e9;
|
|
3376
|
+
for (int i = 0; i < 4; i++) {
|
|
3377
|
+
vec2 h = hash22(vec2(float(i), 3.7));
|
|
3378
|
+
vec2 seed = vec2((float(i) + 0.2 + 0.6 * h.x) / 4.0 * aspect, 0.2 + 0.6 * h.y);
|
|
3379
|
+
near = min(near, length(warped - seed));
|
|
3380
|
+
}
|
|
3381
|
+
// Fine noise adds capillary fingers to the front.
|
|
3382
|
+
float v = clamp(near / 0.5 * 0.75 + 0.25 * fbm(q * uScale * 4.0, 4) + 0.06 * (vnoise(q * uScale * 18.0) - 0.5), 0.0, 1.0);
|
|
3383
|
+
float front = mix(-2.0 * uEdge - 0.02, 1.0 + 2.0 * uEdge + 0.02, t);
|
|
3384
|
+
float gap = front - v;
|
|
3385
|
+
float wet = smoothstep(-uEdge * 0.3, uEdge * 0.3, gap);
|
|
3386
|
+
// Paper just ahead of the front darkens as it soaks; pigment pools just behind it.
|
|
3387
|
+
float soak = smoothstep(-2.0 * uEdge, 0.0, gap) * (1.0 - wet);
|
|
3388
|
+
float pool = wet * smoothstep(uEdge, 0.0, gap);
|
|
3389
|
+
vec2 bleed = vec2(soak - pool) * 0.004;
|
|
3390
|
+
vec3 a = cover(uFrom, uv + bleed);
|
|
3391
|
+
vec3 b = cover(uTo, uv - bleed);
|
|
3392
|
+
vec3 col = mix(a, b, wet);
|
|
3393
|
+
col = mix(col, col * 0.82, soak * 0.6);
|
|
3394
|
+
col = mix(col, uInk, pool * 0.85);
|
|
3395
|
+
fragColor = vec4(col, 1.0);
|
|
3396
|
+
}`
|
|
3397
|
+
),
|
|
3398
|
+
presets: [
|
|
3399
|
+
{ name: "Sepia wash", values: { uInk: [0.36, 0.23, 0.12], uEdge: 0.08, uScale: 2 } },
|
|
3400
|
+
{ name: "Crisp", values: { uEdge: 0.015, uScale: 5 } }
|
|
1438
3401
|
]
|
|
1439
3402
|
};
|
|
3403
|
+
var morphShaders = [
|
|
3404
|
+
featureMorph,
|
|
3405
|
+
silhouetteMorph,
|
|
3406
|
+
transitionLiquidMorph,
|
|
3407
|
+
transitionDisplacement,
|
|
3408
|
+
transitionLuma,
|
|
3409
|
+
transitionParticleSwap,
|
|
3410
|
+
transitionInkBleed
|
|
3411
|
+
];
|
|
1440
3412
|
|
|
1441
3413
|
// src/catalog/shaders/overlay.ts
|
|
1442
|
-
var
|
|
3414
|
+
var GLOW2 = `
|
|
1443
3415
|
vec4 glow(vec3 c) {
|
|
1444
3416
|
c = 1.0 - exp(-max(c, 0.0));
|
|
1445
3417
|
float a = max(c.r, max(c.g, c.b));
|
|
1446
3418
|
return vec4(c / max(a, 1e-4), a);
|
|
1447
3419
|
}`.trim();
|
|
1448
|
-
var
|
|
3420
|
+
var OVER2 = `
|
|
1449
3421
|
vec4 over(vec4 acc, vec3 c, float a) {
|
|
1450
3422
|
return vec4(acc.rgb * (1.0 - a) + c * a, acc.a * (1.0 - a) + a);
|
|
1451
3423
|
}
|
|
@@ -1467,7 +3439,7 @@ uniform float uSpeed; // @control min=0 max=3 default=1 label="Speed"
|
|
|
1467
3439
|
uniform float uSize; // @control min=0.5 max=2 default=1 label="Drop size"
|
|
1468
3440
|
uniform vec3 uTint; // @control color default=#CFE3F0 label="Tint"`,
|
|
1469
3441
|
HASH,
|
|
1470
|
-
|
|
3442
|
+
OVER2,
|
|
1471
3443
|
`// One drop of radius r at offset d: a faint tinted body, a darker rim and a bright highlight.
|
|
1472
3444
|
vec4 paintDrop(vec4 acc, vec2 d, float r, float px) {
|
|
1473
3445
|
float dist = length(d);
|
|
@@ -1601,7 +3573,7 @@ uniform vec3 uColor; // @control color default=#FFC98A label="Colour"
|
|
|
1601
3573
|
uniform float uStreak; // @control min=0 max=2 default=1 label="Streak length"
|
|
1602
3574
|
uniform float uIntensity; // @control min=0 max=2 default=1 label="Intensity"`,
|
|
1603
3575
|
HASH,
|
|
1604
|
-
|
|
3576
|
+
GLOW2,
|
|
1605
3577
|
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
1606
3578
|
vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
|
|
1607
3579
|
float aspect = iResolution.x / iResolution.y;
|
|
@@ -1651,7 +3623,7 @@ uniform float uSoftness; // @control min=0.05 max=1 default=0.5 label="Softness
|
|
|
1651
3623
|
uniform float uGrain; // @control min=0 max=0.3 default=0.08 label="Grain"
|
|
1652
3624
|
uniform float uGrainSize; // @control min=1 max=4 default=1.5 label="Grain size (px)"`,
|
|
1653
3625
|
HASH,
|
|
1654
|
-
|
|
3626
|
+
OVER2,
|
|
1655
3627
|
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
1656
3628
|
float aspect = iResolution.x / iResolution.y;
|
|
1657
3629
|
// 0 at the centre, 1 at the edges; rounder shapes measure across the frame's aspect.
|
|
@@ -1688,7 +3660,7 @@ uniform float uWidth; // @control min=0.001 max=0.05 default=0.006 label="Wi
|
|
|
1688
3660
|
uniform vec3 uColor; // @control color default=#4FF0FF label="Colour"
|
|
1689
3661
|
uniform float uInterval; // @control min=0 max=5 default=1 label="Pause (s)"
|
|
1690
3662
|
uniform float uTrail; // @control min=0 max=1 default=0.5 label="Trail"`,
|
|
1691
|
-
|
|
3663
|
+
GLOW2,
|
|
1692
3664
|
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
1693
3665
|
vec2 p = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
|
|
1694
3666
|
float aspect = iResolution.x / iResolution.y;
|
|
@@ -1762,7 +3734,7 @@ uniform vec3 uHeat; // @control color default=#FF7A1A label="Heat"
|
|
|
1762
3734
|
uniform float uSpeed; // @control min=0 max=3 default=1 label="Rise speed"
|
|
1763
3735
|
uniform float uSize; // @control min=0.3 max=3 default=1 label="Size"`,
|
|
1764
3736
|
HASH,
|
|
1765
|
-
|
|
3737
|
+
GLOW2,
|
|
1766
3738
|
`void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
1767
3739
|
vec2 p = fragCoord / iResolution.y;
|
|
1768
3740
|
float aspect = iResolution.x / iResolution.y;
|
|
@@ -2814,24 +4786,24 @@ var imageTreatments = [
|
|
|
2814
4786
|
];
|
|
2815
4787
|
|
|
2816
4788
|
// src/catalog/text.ts
|
|
2817
|
-
var
|
|
2818
|
-
function
|
|
4789
|
+
var r12 = (n4) => Math.round(n4 * 10) / 10;
|
|
4790
|
+
function fit2(text, width, max, ems = textWidthEm(text)) {
|
|
2819
4791
|
const e = Math.max(0.6, ems);
|
|
2820
4792
|
const size = Math.min(max, width / e);
|
|
2821
|
-
return { size:
|
|
4793
|
+
return { size: r12(size), length: r12(e * size), left: r12(200 - e * size / 2) };
|
|
2822
4794
|
}
|
|
2823
4795
|
var lettersOf = (text) => Array.from(String(text));
|
|
2824
|
-
var
|
|
4796
|
+
var mixHex2 = (a, b, t) => {
|
|
2825
4797
|
const x = Array.isArray(a) ? a : [1, 1, 1];
|
|
2826
4798
|
const y = Array.isArray(b) ? b : [1, 1, 1];
|
|
2827
4799
|
return rgbToHex([0, 1, 2].map((i) => x[i] + (y[i] - x[i]) * t));
|
|
2828
4800
|
};
|
|
2829
4801
|
var holds = (id, attr, keys) => keys.map(([delay, value]) => ({ id, attrs: { [attr]: [value, value] }, delay, ms: 0 }));
|
|
2830
|
-
var
|
|
4802
|
+
var n3 = (name) => `{{${name}}}`;
|
|
2831
4803
|
var MONO = `ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace`;
|
|
2832
4804
|
var MONO_EM = 0.6;
|
|
2833
4805
|
var HEAVY = `'Arial Black', 'Helvetica Neue', Impact, system-ui, sans-serif`;
|
|
2834
|
-
var
|
|
4806
|
+
var SANS2 = `system-ui, 'Helvetica Neue', Arial, sans-serif`;
|
|
2835
4807
|
var BLINKS = 7;
|
|
2836
4808
|
var typewriter = {
|
|
2837
4809
|
kind: "vector",
|
|
@@ -2848,14 +4820,14 @@ var typewriter = {
|
|
|
2848
4820
|
// Monospace, so the caret lands after the last letter however long the text is.
|
|
2849
4821
|
derived: (v) => {
|
|
2850
4822
|
const count = Math.max(1, lettersOf(v.text).length);
|
|
2851
|
-
const size =
|
|
2852
|
-
const left =
|
|
4823
|
+
const size = r12(Math.min(44, 340 / ((count + 1) * MONO_EM)));
|
|
4824
|
+
const left = r12(200 - (count + 1) * MONO_EM * size / 2);
|
|
2853
4825
|
const typing = Math.round(count * Number(v.pace));
|
|
2854
4826
|
return {
|
|
2855
4827
|
size,
|
|
2856
4828
|
left,
|
|
2857
|
-
base:
|
|
2858
|
-
end:
|
|
4829
|
+
base: r12(100 + size * 0.35),
|
|
4830
|
+
end: r12(left + count * MONO_EM * size),
|
|
2859
4831
|
typing,
|
|
2860
4832
|
...Object.fromEntries(Array.from({ length: BLINKS }, (_, k) => [`blink${k}`, typing + k * 500]))
|
|
2861
4833
|
};
|
|
@@ -2863,9 +4835,9 @@ var typewriter = {
|
|
|
2863
4835
|
motion: {
|
|
2864
4836
|
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Hello, world.' label='Text'}} {{paper: color default=#111827 label='Background'}} {{ink: color default=#F9FAFB label='Text colour'}} {{pace: float min=30 max=300 default=90 step=5 label='Ms per letter'}} --><rect width="400" height="200" fill="{{paper}}"/><text id="typed" x="{{left}}" y="{{base}}" font-family="${MONO}" font-size="{{size}}" fill="{{ink}}" xml:space="preserve">{{text}}</text><text id="caret" x="{{end}}" y="{{base}}" font-family="${MONO}" font-size="{{size}}" fill="{{ink}}" opacity="0">\u258C</text></svg>`,
|
|
2865
4837
|
animate: [
|
|
2866
|
-
{ id: "typed", write: "{{text}}", caret: "\u258C", ms:
|
|
4838
|
+
{ id: "typed", write: "{{text}}", caret: "\u258C", ms: n3("typing"), ease: "linear" },
|
|
2867
4839
|
// Hidden while the writer shows its own caret, then blinking where it stopped.
|
|
2868
|
-
...holds("caret", "opacity", [[0, 0], ...Array.from({ length: BLINKS }, (_, k) => [
|
|
4840
|
+
...holds("caret", "opacity", [[0, 0], ...Array.from({ length: BLINKS }, (_, k) => [n3(`blink${k}`), k % 2 ? 0 : 1])])
|
|
2869
4841
|
]
|
|
2870
4842
|
}
|
|
2871
4843
|
};
|
|
@@ -2883,21 +4855,21 @@ var scrambleText = {
|
|
|
2883
4855
|
],
|
|
2884
4856
|
derived: (v) => {
|
|
2885
4857
|
const count = Math.max(1, lettersOf(v.text).length);
|
|
2886
|
-
const size =
|
|
2887
|
-
const width =
|
|
2888
|
-
const base =
|
|
2889
|
-
return { size, base, barX:
|
|
4858
|
+
const size = r12(Math.min(60, 360 / (count * MONO_EM)));
|
|
4859
|
+
const width = r12(count * MONO_EM * size);
|
|
4860
|
+
const base = r12(100 + size * 0.35);
|
|
4861
|
+
return { size, base, barX: r12(200 - width / 2), barY: r12(base + size * 0.3), barW: width, decode: Math.round(count * Number(v.pace)), glowWide: Number(v.glow) * 2 };
|
|
2890
4862
|
},
|
|
2891
4863
|
motion: {
|
|
2892
4864
|
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='ACCESS GRANTED' label='Text'}} {{glyphs: text default='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@$*+' label='Scramble glyphs'}} {{background: color default=#05070D label='Background'}} {{ink: color default=#E2E8F0 label='Text colour'}} {{accent: color default=#22D3EE label='Accent'}} {{pace: float min=20 max=250 default=70 step=5 label='Ms per letter'}} {{glow: float min=0 max=6 default=2 step=0.5 label='Glow'}} --><defs><filter id="glow" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="{{glowWide}}" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><rect width="400" height="200" fill="{{background}}"/><text id="word" x="200" y="{{base}}" text-anchor="middle" font-family="${MONO}" font-size="{{size}}" font-weight="700" fill="{{ink}}" filter="url(#glow)" xml:space="preserve">{{text}}</text><rect id="bar" x="{{barX}}" y="{{barY}}" width="0" height="3" fill="{{accent}}"/></svg>`,
|
|
2893
4865
|
animate: [
|
|
2894
4866
|
// Pure noise for a moment (the scramble runs before the change starts), then it decodes.
|
|
2895
|
-
{ id: "word", write: "{{text}}", scramble: "{{glyphs}}", delay: 500, ms:
|
|
2896
|
-
{ id: "bar", attrs: { width: [0,
|
|
4867
|
+
{ id: "word", write: "{{text}}", scramble: "{{glyphs}}", delay: 500, ms: n3("decode"), ease: "linear" },
|
|
4868
|
+
{ id: "bar", attrs: { width: [0, n3("barW")] }, delay: 500, ms: n3("decode"), ease: "linear" }
|
|
2897
4869
|
]
|
|
2898
4870
|
}
|
|
2899
4871
|
};
|
|
2900
|
-
var
|
|
4872
|
+
var SLOTS2 = 20;
|
|
2901
4873
|
var kineticStagger = {
|
|
2902
4874
|
kind: "vector",
|
|
2903
4875
|
id: "kinetic-stagger",
|
|
@@ -2912,27 +4884,27 @@ var kineticStagger = {
|
|
|
2912
4884
|
],
|
|
2913
4885
|
// Up to SLOTS letters, each centred in its estimated advance; unused slots stay empty.
|
|
2914
4886
|
derived: (v) => {
|
|
2915
|
-
const letters = lettersOf(v.text).slice(0,
|
|
4887
|
+
const letters = lettersOf(v.text).slice(0, SLOTS2);
|
|
2916
4888
|
const widths = letters.map((c) => textWidthEm(c));
|
|
2917
4889
|
const ems = Math.max(0.6, widths.reduce((a, b) => a + b, 0));
|
|
2918
4890
|
const size = Math.min(92, 370 / ems);
|
|
2919
4891
|
const base = 100 + size * 0.35;
|
|
2920
4892
|
const stagger = Number(v.stagger);
|
|
2921
4893
|
const out = {
|
|
2922
|
-
size:
|
|
2923
|
-
base:
|
|
2924
|
-
rise:
|
|
2925
|
-
drop:
|
|
2926
|
-
lineX:
|
|
2927
|
-
lineY:
|
|
2928
|
-
lineW:
|
|
4894
|
+
size: r12(size),
|
|
4895
|
+
base: r12(base),
|
|
4896
|
+
rise: r12(base - size * 0.1),
|
|
4897
|
+
drop: r12(base + size * 0.6),
|
|
4898
|
+
lineX: r12(200 - ems * size / 2),
|
|
4899
|
+
lineY: r12(base + size * 0.2),
|
|
4900
|
+
lineW: r12(ems * size),
|
|
2929
4901
|
lineAt: Math.round(letters.length * stagger + 400)
|
|
2930
4902
|
};
|
|
2931
4903
|
let x = 200 - ems * size / 2;
|
|
2932
|
-
for (let i = 0; i <
|
|
4904
|
+
for (let i = 0; i < SLOTS2; i++) {
|
|
2933
4905
|
const w = (widths[i] ?? 0) * size;
|
|
2934
4906
|
out[`c${i}`] = letters[i]?.trim() ?? "";
|
|
2935
|
-
out[`x${i}`] =
|
|
4907
|
+
out[`x${i}`] = r12(x + w / 2);
|
|
2936
4908
|
out[`d${i}`] = Math.round(i * stagger);
|
|
2937
4909
|
out[`e${i}`] = Math.round(i * stagger + 380);
|
|
2938
4910
|
x += w;
|
|
@@ -2941,21 +4913,21 @@ var kineticStagger = {
|
|
|
2941
4913
|
},
|
|
2942
4914
|
motion: {
|
|
2943
4915
|
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Kinetic' label='Text'}} {{background: color default=#F8FAFC label='Background'}} {{ink: color default=#0F172A label='Text colour'}} {{accent: color default=#F43F5E label='Underline'}} {{stagger: float min=20 max=200 default=55 step=5 label='Ms between letters'}} --><rect width="400" height="200" fill="{{background}}"/>${Array.from(
|
|
2944
|
-
{ length:
|
|
4916
|
+
{ length: SLOTS2 },
|
|
2945
4917
|
(_, i) => `<text id="l${i}" text-anchor="middle" transform="translate({{x${i}}} {{drop}}) rotate(-28) scale(0.3)" opacity="0" font-family="${HEAVY}" font-size="{{size}}" font-weight="900" fill="{{ink}}">{{c${i}}}</text>`
|
|
2946
4918
|
).join("")}<rect id="line" x="{{lineX}}" y="{{lineY}}" width="0" height="6" rx="3" fill="{{accent}}"/></svg>`,
|
|
2947
4919
|
animate: [
|
|
2948
|
-
...Array.from({ length:
|
|
4920
|
+
...Array.from({ length: SLOTS2 }, (_, i) => [
|
|
2949
4921
|
{
|
|
2950
4922
|
id: `l${i}`,
|
|
2951
4923
|
attrs: { transform: [`translate({{x${i}}} {{drop}}) rotate(-28) scale(0.3)`, `translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`], opacity: [0, 1] },
|
|
2952
|
-
delay:
|
|
4924
|
+
delay: n3(`d${i}`),
|
|
2953
4925
|
ms: 380,
|
|
2954
4926
|
ease: "out"
|
|
2955
4927
|
},
|
|
2956
|
-
{ id: `l${i}`, attrs: { transform: [`translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`, `translate({{x${i}}} {{base}}) rotate(0) scale(1)`] }, delay:
|
|
4928
|
+
{ id: `l${i}`, attrs: { transform: [`translate({{x${i}}} {{rise}}) rotate(5) scale(1.12)`, `translate({{x${i}}} {{base}}) rotate(0) scale(1)`] }, delay: n3(`e${i}`), ms: 260, ease: "in-out" }
|
|
2957
4929
|
]).flat(),
|
|
2958
|
-
{ id: "line", attrs: { width: [0,
|
|
4930
|
+
{ id: "line", attrs: { width: [0, n3("lineW")] }, delay: n3("lineAt"), ms: 500, ease: "in-out" }
|
|
2959
4931
|
]
|
|
2960
4932
|
}
|
|
2961
4933
|
};
|
|
@@ -2976,16 +4948,16 @@ var neonSign = {
|
|
|
2976
4948
|
derived: (v) => {
|
|
2977
4949
|
const letters = lettersOf(v.text);
|
|
2978
4950
|
const k = Math.min(Math.max(0, Math.round(Number(v.faulty)) - 1), Math.max(0, letters.length - 1));
|
|
2979
|
-
const { size, length } =
|
|
4951
|
+
const { size, length } = fit2(String(v.text), 340, 84);
|
|
2980
4952
|
return {
|
|
2981
4953
|
head: letters.slice(0, k).join(""),
|
|
2982
4954
|
mid: letters[k] ?? "",
|
|
2983
4955
|
tail: letters.slice(k + 1).join(""),
|
|
2984
4956
|
size,
|
|
2985
4957
|
length,
|
|
2986
|
-
base:
|
|
4958
|
+
base: r12(100 + size * 0.3),
|
|
2987
4959
|
far: Number(v.glow) * 3,
|
|
2988
|
-
core:
|
|
4960
|
+
core: mixHex2(v.tube, [1, 1, 1], 0.6)
|
|
2989
4961
|
};
|
|
2990
4962
|
},
|
|
2991
4963
|
motion: (() => {
|
|
@@ -3017,14 +4989,14 @@ var strokeDrawText = {
|
|
|
3017
4989
|
],
|
|
3018
4990
|
// <text> takes no pathLength, so `draw` cannot measure it: the dash grows instead, longer than any letter's outline.
|
|
3019
4991
|
derived: (v) => {
|
|
3020
|
-
const { size, length } =
|
|
3021
|
-
return { size, length, base:
|
|
4992
|
+
const { size, length } = fit2(String(v.text), 360, 100);
|
|
4993
|
+
return { size, length, base: r12(100 + size * 0.35), dash: Math.round(size * 6) };
|
|
3022
4994
|
},
|
|
3023
4995
|
motion: {
|
|
3024
4996
|
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Draw' label='Text'}} {{background: color default=#FFFBF5 label='Background'}} {{outline: color default=#1E293B label='Outline'}} {{fill: color default=#F97316 label='Fill'}} {{width: float min=0.5 max=6 default=2 step=0.5 label='Outline width'}} {{fillOpacity: float min=0 max=1 default=1 label='Fill opacity'}} --><rect width="400" height="200" fill="{{background}}"/><text id="word" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${HEAVY}" font-size="{{size}}" font-weight="900" fill="{{fill}}" fill-opacity="0" stroke="{{outline}}" stroke-width="{{width}}" stroke-linejoin="round" stroke-dasharray="0 {{dash}}">{{text}}</text></svg>`,
|
|
3025
4997
|
animate: [
|
|
3026
4998
|
{ id: "word", attrs: { "stroke-dasharray": ["0 {{dash}}", "{{dash}} 0"] }, ms: 2e3, ease: "in-out" },
|
|
3027
|
-
{ id: "word", attrs: { "fill-opacity": [0,
|
|
4999
|
+
{ id: "word", attrs: { "fill-opacity": [0, n3("fillOpacity")] }, delay: 1500, ms: 700, ease: "out" }
|
|
3028
5000
|
]
|
|
3029
5001
|
}
|
|
3030
5002
|
};
|
|
@@ -3041,21 +5013,21 @@ var splitReveal = {
|
|
|
3041
5013
|
{ name: "Gold on black", values: { background: [0.02, 0.02, 0.02], ink: [0.96, 0.84, 0.55], line: [0.96, 0.84, 0.55] } }
|
|
3042
5014
|
],
|
|
3043
5015
|
derived: (v) => {
|
|
3044
|
-
const { size, length, left } =
|
|
5016
|
+
const { size, length, left } = fit2(String(v.text), 360, 88);
|
|
3045
5017
|
return {
|
|
3046
5018
|
size,
|
|
3047
5019
|
length,
|
|
3048
|
-
base:
|
|
3049
|
-
shift:
|
|
3050
|
-
lineX1:
|
|
3051
|
-
lineX2:
|
|
3052
|
-
captionY:
|
|
5020
|
+
base: r12(100 + size * 0.36),
|
|
5021
|
+
shift: r12(size * 0.8),
|
|
5022
|
+
lineX1: r12(left - 10),
|
|
5023
|
+
lineX2: r12(400 - left + 10),
|
|
5024
|
+
captionY: r12(100 + size * 0.36 + 30)
|
|
3053
5025
|
};
|
|
3054
5026
|
},
|
|
3055
5027
|
motion: (() => {
|
|
3056
|
-
const word = `<text x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${
|
|
5028
|
+
const word = `<text x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS2}" font-size="{{size}}" font-weight="800" fill="{{ink}}">{{text}}</text>`;
|
|
3057
5029
|
return {
|
|
3058
|
-
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Chapter One' label='Text'}} {{caption: text default='The beginning' label='Caption'}} {{background: color default=#0F172A label='Background'}} {{ink: color default=#F8FAFC label='Text colour'}} {{line: color default=#38BDF8 label='Line'}} --><defs><clipPath id="above"><rect width="400" height="100"/></clipPath><clipPath id="below"><rect y="100" width="400" height="100"/></clipPath></defs><rect width="400" height="200" fill="{{background}}"/><g clip-path="url(#above)"><g id="top" transform="translate(0 {{shift}})">${word}</g></g><g clip-path="url(#below)"><g id="bottom" transform="translate(0 -{{shift}})">${word}</g></g><path id="line" d="M{{lineX1}} 100 H{{lineX2}}" stroke="{{line}}" stroke-width="2.5" stroke-linecap="round"/><text id="caption" x="200" y="{{captionY}}" text-anchor="middle" font-family="${
|
|
5030
|
+
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Chapter One' label='Text'}} {{caption: text default='The beginning' label='Caption'}} {{background: color default=#0F172A label='Background'}} {{ink: color default=#F8FAFC label='Text colour'}} {{line: color default=#38BDF8 label='Line'}} --><defs><clipPath id="above"><rect width="400" height="100"/></clipPath><clipPath id="below"><rect y="100" width="400" height="100"/></clipPath></defs><rect width="400" height="200" fill="{{background}}"/><g clip-path="url(#above)"><g id="top" transform="translate(0 {{shift}})">${word}</g></g><g clip-path="url(#below)"><g id="bottom" transform="translate(0 -{{shift}})">${word}</g></g><path id="line" d="M{{lineX1}} 100 H{{lineX2}}" stroke="{{line}}" stroke-width="2.5" stroke-linecap="round"/><text id="caption" x="200" y="{{captionY}}" text-anchor="middle" font-family="${SANS2}" font-size="13" letter-spacing="3" fill="{{ink}}" opacity="0">{{caption}}</text></svg>`,
|
|
3059
5031
|
animate: [
|
|
3060
5032
|
{ id: "line", draw: true, ms: 600, ease: "in-out" },
|
|
3061
5033
|
{ id: "top", attrs: { transform: ["translate(0 {{shift}})", "translate(0 0)"] }, delay: 450, ms: 800, ease: "out" },
|
|
@@ -3081,7 +5053,7 @@ var highlighter = {
|
|
|
3081
5053
|
],
|
|
3082
5054
|
// The swipe runs a little past the phrase at both ends, across its lower half, with a ragged second pass.
|
|
3083
5055
|
derived: (v) => {
|
|
3084
|
-
const { size, length, left } =
|
|
5056
|
+
const { size, length, left } = fit2(String(v.text), 330, 56);
|
|
3085
5057
|
const base = 100 + size * 0.35;
|
|
3086
5058
|
const x0 = left - size * 0.25;
|
|
3087
5059
|
const x1 = 400 - left + size * 0.2;
|
|
@@ -3089,15 +5061,15 @@ var highlighter = {
|
|
|
3089
5061
|
return {
|
|
3090
5062
|
size,
|
|
3091
5063
|
length,
|
|
3092
|
-
base:
|
|
3093
|
-
swipe: `M${
|
|
3094
|
-
ragged: `M${
|
|
3095
|
-
thick:
|
|
3096
|
-
thin:
|
|
5064
|
+
base: r12(base),
|
|
5065
|
+
swipe: `M${r12(x0)} ${r12(y + size * 0.05)} Q${r12((x0 + x1) / 2)} ${r12(y - size * 0.06)} ${r12(x1)} ${r12(y - size * 0.02)}`,
|
|
5066
|
+
ragged: `M${r12(x0 + size * 0.15)} ${r12(y + size * 0.3)} L${r12(x1 - size * 0.1)} ${r12(y + size * 0.26)}`,
|
|
5067
|
+
thick: r12(size * 0.72),
|
|
5068
|
+
thin: r12(size * 0.16)
|
|
3097
5069
|
};
|
|
3098
5070
|
},
|
|
3099
5071
|
motion: {
|
|
3100
|
-
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Ship it faster' label='Phrase'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#111827 label='Text colour'}} {{marker: color default=#FDE047 label='Marker'}} --><rect width="400" height="200" fill="{{background}}"/><g transform="rotate(-1.2 200 100)" opacity="0.9"><path id="swipe" d="{{swipe}}" fill="none" stroke="{{marker}}" stroke-width="{{thick}}"/><path id="ragged" d="{{ragged}}" fill="none" stroke="{{marker}}" stroke-width="{{thin}}" stroke-linecap="round"/></g><text id="phrase" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${
|
|
5072
|
+
svg: `<svg viewBox="0 0 400 200"><!-- {{text: text default='Ship it faster' label='Phrase'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#111827 label='Text colour'}} {{marker: color default=#FDE047 label='Marker'}} --><rect width="400" height="200" fill="{{background}}"/><g transform="rotate(-1.2 200 100)" opacity="0.9"><path id="swipe" d="{{swipe}}" fill="none" stroke="{{marker}}" stroke-width="{{thick}}"/><path id="ragged" d="{{ragged}}" fill="none" stroke="{{marker}}" stroke-width="{{thin}}" stroke-linecap="round"/></g><text id="phrase" x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS2}" font-size="{{size}}" font-weight="700" fill="{{ink}}" opacity="0">{{text}}</text></svg>`,
|
|
3101
5073
|
animate: [
|
|
3102
5074
|
{ id: "phrase", attrs: { opacity: [0, 1] }, ms: 300, ease: "out" },
|
|
3103
5075
|
{ id: "swipe", draw: true, delay: 350, ms: 650, ease: "in-out" },
|
|
@@ -3132,36 +5104,36 @@ var wordCycle = {
|
|
|
3132
5104
|
const step = size * 1.3;
|
|
3133
5105
|
const wx = left + (lead + 0.3) * size;
|
|
3134
5106
|
return {
|
|
3135
|
-
size:
|
|
3136
|
-
base:
|
|
3137
|
-
px:
|
|
3138
|
-
plen:
|
|
3139
|
-
wx:
|
|
3140
|
-
clipX:
|
|
3141
|
-
clipY:
|
|
3142
|
-
clipW:
|
|
3143
|
-
clipH:
|
|
3144
|
-
underY:
|
|
5107
|
+
size: r12(size),
|
|
5108
|
+
base: r12(base),
|
|
5109
|
+
px: r12(left),
|
|
5110
|
+
plen: r12(Math.max(0.6, lead) * size),
|
|
5111
|
+
wx: r12(wx),
|
|
5112
|
+
clipX: r12(wx - 4),
|
|
5113
|
+
clipY: r12(base - size * 1),
|
|
5114
|
+
clipW: r12(Math.max(...ems) * size + 8),
|
|
5115
|
+
clipH: r12(size * 1.32),
|
|
5116
|
+
underY: r12(base + size * 0.18),
|
|
3145
5117
|
...Object.fromEntries(
|
|
3146
5118
|
[...words, words[0]].flatMap((_, k) => [
|
|
3147
|
-
[`y${k}`,
|
|
3148
|
-
[`o${k}`,
|
|
3149
|
-
[`len${k}`,
|
|
5119
|
+
[`y${k}`, r12(base + k * step)],
|
|
5120
|
+
[`o${k}`, r12(k * step)],
|
|
5121
|
+
[`len${k}`, r12(ems[k % CYCLE_WORDS] * size)]
|
|
3150
5122
|
])
|
|
3151
5123
|
),
|
|
3152
5124
|
...Object.fromEntries(words.map((w, k) => [`w${k}`, w]))
|
|
3153
5125
|
};
|
|
3154
5126
|
},
|
|
3155
5127
|
motion: {
|
|
3156
|
-
svg: `<svg viewBox="0 0 400 200"><!-- {{prefix: text default='We build' label='Lead-in'}} {{word1: text default=apps label='Word 1'}} {{word2: text default=brands label='Word 2'}} {{word3: text default=websites label='Word 3'}} {{word4: text default=games label='Word 4'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#0F172A label='Text colour'}} {{accent: color default=#6366F1 label='Accent'}} --><defs><clipPath id="window"><rect x="{{clipX}}" y="{{clipY}}" width="{{clipW}}" height="{{clipH}}"/></clipPath></defs><rect width="400" height="200" fill="{{background}}"/><text x="{{px}}" y="{{base}}" textLength="{{plen}}" lengthAdjust="spacingAndGlyphs" font-family="${
|
|
5128
|
+
svg: `<svg viewBox="0 0 400 200"><!-- {{prefix: text default='We build' label='Lead-in'}} {{word1: text default=apps label='Word 1'}} {{word2: text default=brands label='Word 2'}} {{word3: text default=websites label='Word 3'}} {{word4: text default=games label='Word 4'}} {{background: color default=#FFFFFF label='Background'}} {{ink: color default=#0F172A label='Text colour'}} {{accent: color default=#6366F1 label='Accent'}} --><defs><clipPath id="window"><rect x="{{clipX}}" y="{{clipY}}" width="{{clipW}}" height="{{clipH}}"/></clipPath></defs><rect width="400" height="200" fill="{{background}}"/><text x="{{px}}" y="{{base}}" textLength="{{plen}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS2}" font-size="{{size}}" font-weight="700" fill="{{ink}}">{{prefix}}</text><g clip-path="url(#window)"><g id="reel" transform="translate(0 -0)">${Array.from(
|
|
3157
5129
|
{ length: CYCLE_WORDS + 1 },
|
|
3158
|
-
(_, k) => `<text x="{{wx}}" y="{{y${k}}}" textLength="{{len${k}}}" lengthAdjust="spacingAndGlyphs" font-family="${
|
|
5130
|
+
(_, k) => `<text x="{{wx}}" y="{{y${k}}}" textLength="{{len${k}}}" lengthAdjust="spacingAndGlyphs" font-family="${SANS2}" font-size="{{size}}" font-weight="800" fill="{{accent}}">{{w${k % CYCLE_WORDS}}}</text>`
|
|
3159
5131
|
).join("")}</g></g><rect id="under" x="{{wx}}" y="{{underY}}" width="{{len0}}" height="4" rx="2" fill="{{accent}}"/></svg>`,
|
|
3160
5132
|
animate: Array.from({ length: CYCLE_WORDS }, (_, i) => {
|
|
3161
5133
|
const at = { delay: (i + 1) * SHOW_MS + i * ROLL_MS, ms: ROLL_MS, ease: "in-out" };
|
|
3162
5134
|
return [
|
|
3163
5135
|
{ id: "reel", attrs: { transform: [`translate(0 -{{o${i}}})`, `translate(0 -{{o${i + 1}}})`] }, ...at },
|
|
3164
|
-
{ id: "under", attrs: { width: [
|
|
5136
|
+
{ id: "under", attrs: { width: [n3(`len${i}`), n3(`len${i + 1}`)] }, ...at }
|
|
3165
5137
|
];
|
|
3166
5138
|
}).flat()
|
|
3167
5139
|
}
|
|
@@ -3182,9 +5154,9 @@ var liquidFill = {
|
|
|
3182
5154
|
],
|
|
3183
5155
|
// The surface starts under the letters and ends above their caps.
|
|
3184
5156
|
derived: (v) => {
|
|
3185
|
-
const { size, length } =
|
|
5157
|
+
const { size, length } = fit2(String(v.text), 360, 110);
|
|
3186
5158
|
const base = 100 + size * 0.36;
|
|
3187
|
-
return { size, length, base:
|
|
5159
|
+
return { size, length, base: r12(base), low: r12(base + 12), high: r12(base - size * 0.82 - 12) };
|
|
3188
5160
|
},
|
|
3189
5161
|
motion: (() => {
|
|
3190
5162
|
const word = `x="200" y="{{base}}" text-anchor="middle" textLength="{{length}}" lengthAdjust="spacingAndGlyphs" font-family="${HEAVY}" font-size="{{size}}" font-weight="900"`;
|
|
@@ -3231,17 +5203,17 @@ var glitchText = {
|
|
|
3231
5203
|
{ name: "Vapor", values: { left: [1, 0.3, 0.85], right: [0.4, 0.6, 1], background: [0.08, 0.02, 0.12], amount: 1.4 } }
|
|
3232
5204
|
],
|
|
3233
5205
|
derived: (v) => {
|
|
3234
|
-
const { size, length } =
|
|
5206
|
+
const { size, length } = fit2(String(v.text), 340, 100);
|
|
3235
5207
|
const base = 100 + size * 0.36;
|
|
3236
5208
|
const top = base - size * 0.78;
|
|
3237
|
-
const cuts = [0, ...BANDS.map((f) =>
|
|
5209
|
+
const cuts = [0, ...BANDS.map((f) => r12(top + f * (base - top))), 200];
|
|
3238
5210
|
const unit = size * Number(v.amount);
|
|
3239
|
-
const shift = ([x, y]) => `translate(${
|
|
5211
|
+
const shift = ([x, y]) => `translate(${r12(x * unit)} ${r12(y * unit)})`;
|
|
3240
5212
|
return {
|
|
3241
5213
|
size,
|
|
3242
5214
|
length,
|
|
3243
|
-
base:
|
|
3244
|
-
...Object.fromEntries([0, 1, 2, 3].flatMap((b) => [[`cut${b}`, cuts[b]], [`band${b}`,
|
|
5215
|
+
base: r12(base),
|
|
5216
|
+
...Object.fromEntries([0, 1, 2, 3].flatMap((b) => [[`cut${b}`, cuts[b]], [`band${b}`, r12(cuts[b + 1] - cuts[b])]])),
|
|
3245
5217
|
...Object.fromEntries(
|
|
3246
5218
|
GLITCH.flatMap((key, k) => [
|
|
3247
5219
|
[`red${k}`, shift(key.red)],
|
|
@@ -3281,21 +5253,21 @@ var extrudedText = {
|
|
|
3281
5253
|
],
|
|
3282
5254
|
// Each layer sits a step further back along the extrude direction, shaded from the side colour to the shadow.
|
|
3283
5255
|
derived: (v) => {
|
|
3284
|
-
const { size, length } =
|
|
5256
|
+
const { size, length } = fit2(String(v.text), 320, 96);
|
|
3285
5257
|
const step = size * 0.03 * Number(v.depth);
|
|
3286
5258
|
const at = (deg, i) => {
|
|
3287
5259
|
const a = deg * Math.PI / 180;
|
|
3288
|
-
return `translate(${
|
|
5260
|
+
return `translate(${r12(Math.cos(a) * step * i)} ${r12(Math.sin(a) * step * i)})`;
|
|
3289
5261
|
};
|
|
3290
5262
|
return {
|
|
3291
5263
|
size,
|
|
3292
5264
|
length,
|
|
3293
|
-
base:
|
|
5265
|
+
base: r12(92 + size * 0.35),
|
|
3294
5266
|
...Object.fromEntries(
|
|
3295
5267
|
Array.from({ length: LAYERS }, (_, k) => {
|
|
3296
5268
|
const i = LAYERS - k;
|
|
3297
5269
|
return [
|
|
3298
|
-
[`fill${i}`,
|
|
5270
|
+
[`fill${i}`, mixHex2(v.side, v.shade, i / LAYERS)],
|
|
3299
5271
|
[`a${i}`, at(EXTRUDE_FROM, i)],
|
|
3300
5272
|
[`b${i}`, at(EXTRUDE_TO, i)]
|
|
3301
5273
|
];
|
|
@@ -3341,6 +5313,8 @@ var EFFECT_CATEGORIES = [
|
|
|
3341
5313
|
"image",
|
|
3342
5314
|
"overlay",
|
|
3343
5315
|
"text",
|
|
5316
|
+
"morph",
|
|
5317
|
+
"interaction",
|
|
3344
5318
|
"transition",
|
|
3345
5319
|
"loader",
|
|
3346
5320
|
"data",
|
|
@@ -4022,9 +5996,11 @@ var SHADER_EFFECTS = [
|
|
|
4022
5996
|
filmGrain,
|
|
4023
5997
|
...overlayEffects,
|
|
4024
5998
|
...textShaders,
|
|
5999
|
+
...morphShaders,
|
|
6000
|
+
...interactionShaders,
|
|
4025
6001
|
...transitions
|
|
4026
6002
|
];
|
|
4027
|
-
var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects].sort(
|
|
6003
|
+
var EFFECTS = [...SHADER_EFFECTS, ...vectorEffects, ...textEffects, ...morphEffects, ...interactionEffects].sort(
|
|
4028
6004
|
(a, b) => EFFECT_CATEGORIES.indexOf(a.category) - EFFECT_CATEGORIES.indexOf(b.category)
|
|
4029
6005
|
);
|
|
4030
6006
|
var byId = new Map(EFFECTS.map((e) => [e.id, e]));
|
|
@@ -4077,4 +6053,4 @@ export {
|
|
|
4077
6053
|
effectControls,
|
|
4078
6054
|
effectDefaults
|
|
4079
6055
|
};
|
|
4080
|
-
//# sourceMappingURL=chunk-
|
|
6056
|
+
//# sourceMappingURL=chunk-STGGN4MR.js.map
|