@sarmal/core 0.34.0 → 0.35.1
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/README.md +5 -3
- package/dist/index.cjs +128 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +128 -86
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
<strong>Parametric curve animations for loading/thinking indicators</strong>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
-
<
|
|
8
|
-
<a href="https://sarmal.art"
|
|
9
|
-
|
|
7
|
+
<div align="center">
|
|
8
|
+
<a href="https://sarmal.art">
|
|
9
|
+
<img src="../../docs/public/curves-light.gif" alt="Sarmal demo" width="100%" max-width="720">
|
|
10
|
+
</a>
|
|
11
|
+
</div>
|
|
10
12
|
|
|
11
13
|
---
|
|
12
14
|
|
package/dist/index.cjs
CHANGED
|
@@ -1418,7 +1418,6 @@ function createSarmalSVG(container, curveDef, options) {
|
|
|
1418
1418
|
}
|
|
1419
1419
|
|
|
1420
1420
|
// src/renderer-dot-matrix.ts
|
|
1421
|
-
var NUM_BUCKETS = 8;
|
|
1422
1421
|
function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
1423
1422
|
const {
|
|
1424
1423
|
cols = 32,
|
|
@@ -1442,14 +1441,8 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1442
1441
|
const cellW = W / cols;
|
|
1443
1442
|
const cellH = H / rows;
|
|
1444
1443
|
const dotR = Math.min(cellW, cellH) * 0.36;
|
|
1445
|
-
let
|
|
1446
|
-
|
|
1447
|
-
validateBaseRenderOptions({ trailColor: initialColor });
|
|
1448
|
-
gradientRgb = initialColor.map(colorToRgb);
|
|
1449
|
-
} else {
|
|
1450
|
-
gradientRgb = null;
|
|
1451
|
-
}
|
|
1452
|
-
let colorRgb = gradientRgb ? gradientRgb[0] : colorToRgb(initialColor);
|
|
1444
|
+
let gradientOklab = null;
|
|
1445
|
+
let colorRgb = { r: 255, g: 255, b: 255 };
|
|
1453
1446
|
let currentTrailStyle = initialTrailStyle;
|
|
1454
1447
|
let animTime = 0;
|
|
1455
1448
|
const ANIM_PERIOD = 6;
|
|
@@ -1457,7 +1450,12 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1457
1450
|
let scale = 1;
|
|
1458
1451
|
let offsetX = 0;
|
|
1459
1452
|
let offsetY = 0;
|
|
1460
|
-
let
|
|
1453
|
+
let pixelMaskStarts = new Uint32Array(0);
|
|
1454
|
+
let pixelMaskLengths = new Uint32Array(0);
|
|
1455
|
+
let pixelMaskIndices = new Uint32Array(0);
|
|
1456
|
+
let pixelMaskCoverages = new Float32Array(0);
|
|
1457
|
+
let bgImageData = null;
|
|
1458
|
+
let frameImageData = null;
|
|
1461
1459
|
let animationId = null;
|
|
1462
1460
|
let lastTime = 0;
|
|
1463
1461
|
let pausedByVisibility = false;
|
|
@@ -1465,33 +1463,82 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1465
1463
|
let morphReject = null;
|
|
1466
1464
|
let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
|
|
1467
1465
|
let morphProgress = 0;
|
|
1468
|
-
function
|
|
1469
|
-
|
|
1470
|
-
const
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1466
|
+
function computePixelMask() {
|
|
1467
|
+
const starts = new Uint32Array(cols * rows);
|
|
1468
|
+
const lengths = new Uint32Array(cols * rows);
|
|
1469
|
+
const allIndices = [];
|
|
1470
|
+
const allCoverages = [];
|
|
1471
|
+
const cornerR = roundness * dotR;
|
|
1472
|
+
const cornerR2 = cornerR * cornerR;
|
|
1473
|
+
const SSAA = 4;
|
|
1474
|
+
const SSAA2 = SSAA * SSAA;
|
|
1474
1475
|
for (let row = 0; row < rows; row++) {
|
|
1475
1476
|
for (let col = 0; col < cols; col++) {
|
|
1477
|
+
const dotIdx = row * cols + col;
|
|
1476
1478
|
const cx = (col + 0.5) * cellW;
|
|
1477
1479
|
const cy = (row + 0.5) * cellH;
|
|
1478
|
-
|
|
1480
|
+
const x0 = Math.max(0, Math.floor(cx - dotR - 1));
|
|
1481
|
+
const x1 = Math.min(W - 1, Math.ceil(cx + dotR + 1));
|
|
1482
|
+
const y0 = Math.max(0, Math.floor(cy - dotR - 1));
|
|
1483
|
+
const y1 = Math.min(H - 1, Math.ceil(cy + dotR + 1));
|
|
1484
|
+
starts[dotIdx] = allIndices.length;
|
|
1485
|
+
let count = 0;
|
|
1486
|
+
for (let py = y0; py <= y1; py++) {
|
|
1487
|
+
for (let px = x0; px <= x1; px++) {
|
|
1488
|
+
let hits = 0;
|
|
1489
|
+
for (let sy = 0; sy < SSAA; sy++) {
|
|
1490
|
+
const spyCenter = py + (sy + 0.5) / SSAA;
|
|
1491
|
+
for (let sx = 0; sx < SSAA; sx++) {
|
|
1492
|
+
const spxCenter = px + (sx + 0.5) / SSAA;
|
|
1493
|
+
const dx = Math.max(Math.abs(spxCenter - cx) - (dotR - cornerR), 0);
|
|
1494
|
+
const dy = Math.max(Math.abs(spyCenter - cy) - (dotR - cornerR), 0);
|
|
1495
|
+
if (dx * dx + dy * dy <= cornerR2) {
|
|
1496
|
+
hits++;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (hits > 0) {
|
|
1501
|
+
allIndices.push((py * W + px) * 4);
|
|
1502
|
+
allCoverages.push(hits / SSAA2);
|
|
1503
|
+
count++;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
lengths[dotIdx] = count;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
pixelMaskStarts = starts;
|
|
1511
|
+
pixelMaskLengths = lengths;
|
|
1512
|
+
pixelMaskIndices = new Uint32Array(allIndices);
|
|
1513
|
+
pixelMaskCoverages = new Float32Array(allCoverages);
|
|
1514
|
+
}
|
|
1515
|
+
function buildBgImageData() {
|
|
1516
|
+
bgImageData = new ImageData(W, H);
|
|
1517
|
+
const bg = colorRgb;
|
|
1518
|
+
const baseAlpha = 0.05 * 255;
|
|
1519
|
+
const { data } = bgImageData;
|
|
1520
|
+
const n = cols * rows;
|
|
1521
|
+
for (let dotIdx = 0; dotIdx < n; dotIdx++) {
|
|
1522
|
+
const start = pixelMaskStarts[dotIdx];
|
|
1523
|
+
const len = pixelMaskLengths[dotIdx];
|
|
1524
|
+
for (let k = 0; k < len; k++) {
|
|
1525
|
+
const px = pixelMaskIndices[start + k];
|
|
1526
|
+
const coverage = pixelMaskCoverages[start + k];
|
|
1527
|
+
data[px] = bg.r;
|
|
1528
|
+
data[px + 1] = bg.g;
|
|
1529
|
+
data[px + 2] = bg.b;
|
|
1530
|
+
data[px + 3] = Math.round(baseAlpha * coverage);
|
|
1479
1531
|
}
|
|
1480
1532
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
return {
|
|
1491
|
-
r: Math.round(a.r + (bStop.r - a.r) * mix),
|
|
1492
|
-
g: Math.round(a.g + (bStop.g - a.g) * mix),
|
|
1493
|
-
b: Math.round(a.b + (bStop.b - a.b) * mix)
|
|
1494
|
-
};
|
|
1533
|
+
}
|
|
1534
|
+
function applyColor(color) {
|
|
1535
|
+
if (Array.isArray(color)) {
|
|
1536
|
+
gradientOklab = color.map((c) => parseColorToOklab(c));
|
|
1537
|
+
colorRgb = oklabToRgb(gradientOklab[0]);
|
|
1538
|
+
} else {
|
|
1539
|
+
gradientOklab = null;
|
|
1540
|
+
colorRgb = colorToRgb(color);
|
|
1541
|
+
}
|
|
1495
1542
|
}
|
|
1496
1543
|
function calculateBoundaries(skel) {
|
|
1497
1544
|
const b = computeBoundaries(skel, W, H);
|
|
@@ -1540,43 +1587,44 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1540
1587
|
}
|
|
1541
1588
|
}
|
|
1542
1589
|
function draw() {
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
ctx.drawImage(bgCanvas, 0, 0);
|
|
1590
|
+
if (!bgImageData || !frameImageData) {
|
|
1591
|
+
return;
|
|
1546
1592
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
const { r, g, b } = colorRgb;
|
|
1573
|
-
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
|
1574
|
-
}
|
|
1575
|
-
ctx.globalAlpha = alpha;
|
|
1576
|
-
ctx.fill();
|
|
1593
|
+
frameImageData.data.set(bgImageData.data);
|
|
1594
|
+
const { data } = frameImageData;
|
|
1595
|
+
const timeOffset = currentTrailStyle === "gradient-animated" ? animTime / ANIM_PERIOD : 0;
|
|
1596
|
+
const n = cols * rows;
|
|
1597
|
+
for (let dotIdx = 0; dotIdx < n; dotIdx++) {
|
|
1598
|
+
const intensity = grid[dotIdx];
|
|
1599
|
+
if (intensity <= 0) {
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
let r, g, b;
|
|
1603
|
+
if (gradientOklab !== null) {
|
|
1604
|
+
({ r, g, b } = oklabToRgb(getPaletteColor(gradientOklab, intensity, timeOffset)));
|
|
1605
|
+
} else {
|
|
1606
|
+
({ r, g, b } = colorRgb);
|
|
1607
|
+
}
|
|
1608
|
+
const baseA = (0.08 + intensity * 0.92) * 255;
|
|
1609
|
+
const start = pixelMaskStarts[dotIdx];
|
|
1610
|
+
const len = pixelMaskLengths[dotIdx];
|
|
1611
|
+
for (let k = 0; k < len; k++) {
|
|
1612
|
+
const px = pixelMaskIndices[start + k];
|
|
1613
|
+
const coverage = pixelMaskCoverages[start + k];
|
|
1614
|
+
data[px] = r;
|
|
1615
|
+
data[px + 1] = g;
|
|
1616
|
+
data[px + 2] = b;
|
|
1617
|
+
data[px + 3] = Math.round(baseA * coverage);
|
|
1577
1618
|
}
|
|
1578
1619
|
}
|
|
1579
|
-
ctx.
|
|
1620
|
+
ctx.putImageData(frameImageData, 0, 0);
|
|
1621
|
+
}
|
|
1622
|
+
function completeMorphNow() {
|
|
1623
|
+
engine.completeMorph();
|
|
1624
|
+
morphResolve?.();
|
|
1625
|
+
morphResolve = null;
|
|
1626
|
+
morphReject = null;
|
|
1627
|
+
morphProgress = 0;
|
|
1580
1628
|
}
|
|
1581
1629
|
function renderFrame(deltaTime) {
|
|
1582
1630
|
if (engine.morphAlpha !== null) {
|
|
@@ -1584,11 +1632,7 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1584
1632
|
engine.setMorphAlpha(morphProgress);
|
|
1585
1633
|
calculateBoundaries(engine.getSarmalSkeleton());
|
|
1586
1634
|
if (morphProgress >= 1) {
|
|
1587
|
-
|
|
1588
|
-
morphResolve?.();
|
|
1589
|
-
morphResolve = null;
|
|
1590
|
-
morphReject = null;
|
|
1591
|
-
morphProgress = 0;
|
|
1635
|
+
completeMorphNow();
|
|
1592
1636
|
calculateBoundaries(engine.getSarmalSkeleton());
|
|
1593
1637
|
}
|
|
1594
1638
|
} else if (engine.isLiveSkeleton) {
|
|
@@ -1606,8 +1650,12 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1606
1650
|
renderFrame(deltaTime);
|
|
1607
1651
|
animationId = requestAnimationFrame(loop);
|
|
1608
1652
|
}
|
|
1653
|
+
validateBaseRenderOptions({ trailColor: initialColor });
|
|
1654
|
+
applyColor(initialColor);
|
|
1609
1655
|
calculateBoundaries(engine.getSarmalSkeleton());
|
|
1610
|
-
|
|
1656
|
+
computePixelMask();
|
|
1657
|
+
frameImageData = new ImageData(W, H);
|
|
1658
|
+
buildBgImageData();
|
|
1611
1659
|
if (initialPhase !== void 0) {
|
|
1612
1660
|
engine.seek(initialPhase);
|
|
1613
1661
|
}
|
|
@@ -1652,11 +1700,7 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1652
1700
|
*/
|
|
1653
1701
|
morphTo(target, opts) {
|
|
1654
1702
|
if (morphResolve !== null) {
|
|
1655
|
-
|
|
1656
|
-
morphResolve();
|
|
1657
|
-
morphResolve = null;
|
|
1658
|
-
morphReject = null;
|
|
1659
|
-
morphProgress = 0;
|
|
1703
|
+
completeMorphNow();
|
|
1660
1704
|
}
|
|
1661
1705
|
morphDurationMs = opts?.duration ?? DEFAULT_MORPH_DURATION_MS;
|
|
1662
1706
|
morphProgress = 0;
|
|
@@ -1677,13 +1721,7 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1677
1721
|
validateBaseRenderOptions(partial);
|
|
1678
1722
|
let needsRebuildBg = false;
|
|
1679
1723
|
if (partial.trailColor !== void 0) {
|
|
1680
|
-
|
|
1681
|
-
gradientRgb = partial.trailColor.map(colorToRgb);
|
|
1682
|
-
colorRgb = gradientRgb[0];
|
|
1683
|
-
} else {
|
|
1684
|
-
gradientRgb = null;
|
|
1685
|
-
colorRgb = colorToRgb(partial.trailColor);
|
|
1686
|
-
}
|
|
1724
|
+
applyColor(partial.trailColor);
|
|
1687
1725
|
needsRebuildBg = true;
|
|
1688
1726
|
}
|
|
1689
1727
|
if (partial.trailStyle !== void 0) {
|
|
@@ -1693,11 +1731,15 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1693
1731
|
}
|
|
1694
1732
|
}
|
|
1695
1733
|
if (needsRebuildBg) {
|
|
1696
|
-
|
|
1734
|
+
buildBgImageData();
|
|
1697
1735
|
}
|
|
1698
|
-
if (currentTrailStyle !== "default" &&
|
|
1736
|
+
if (currentTrailStyle !== "default" && gradientOklab === null) {
|
|
1737
|
+
console.warn(
|
|
1738
|
+
`[sarmal] dot matrix: trailColor is a single color but trailStyle is "${currentTrailStyle}"; the trail will render as a solid color. Pass an array of hex colors to use a real gradient.`
|
|
1739
|
+
);
|
|
1740
|
+
} else if (currentTrailStyle === "default" && gradientOklab !== null) {
|
|
1699
1741
|
console.warn(
|
|
1700
|
-
|
|
1742
|
+
'[sarmal] dot matrix: trailColor is an array but trailStyle is "default"; only the first color will be used. Pass a gradient trailStyle to use the whole palette.'
|
|
1701
1743
|
);
|
|
1702
1744
|
}
|
|
1703
1745
|
}
|