@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/dist/index.d.cts
CHANGED
|
@@ -104,10 +104,10 @@ interface DotMatrixSarmalOptions extends Pick<BaseRendererOptions, "autoStart" |
|
|
|
104
104
|
* Grid geometry is derived from `cols` and `rows`.
|
|
105
105
|
* For example, a 240x240 canvas with `cols: 32, rows: 32` produces 1024 dots with cells approximately 7.5x7.5 px each.
|
|
106
106
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
107
|
+
* At init, a pixel mask is computed that records which canvas pixels belong to each dot.
|
|
108
|
+
* Each frame, RGBA values are written directly into a typed array (one entry per lit pixel)
|
|
109
|
+
* and flushed to the canvas with a single `ctx.putImageData` call.
|
|
110
|
+
* Frame cost is flat regardless of how many dots are lit or what grid size is used.
|
|
111
111
|
*
|
|
112
112
|
* @param canvas - The canvas element to draw into.
|
|
113
113
|
* Its `width` and `height` HTML attributes determine the rendering area.
|
package/dist/index.d.ts
CHANGED
|
@@ -104,10 +104,10 @@ interface DotMatrixSarmalOptions extends Pick<BaseRendererOptions, "autoStart" |
|
|
|
104
104
|
* Grid geometry is derived from `cols` and `rows`.
|
|
105
105
|
* For example, a 240x240 canvas with `cols: 32, rows: 32` produces 1024 dots with cells approximately 7.5x7.5 px each.
|
|
106
106
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
107
|
+
* At init, a pixel mask is computed that records which canvas pixels belong to each dot.
|
|
108
|
+
* Each frame, RGBA values are written directly into a typed array (one entry per lit pixel)
|
|
109
|
+
* and flushed to the canvas with a single `ctx.putImageData` call.
|
|
110
|
+
* Frame cost is flat regardless of how many dots are lit or what grid size is used.
|
|
111
111
|
*
|
|
112
112
|
* @param canvas - The canvas element to draw into.
|
|
113
113
|
* Its `width` and `height` HTML attributes determine the rendering area.
|
package/dist/index.js
CHANGED
|
@@ -1416,7 +1416,6 @@ function createSarmalSVG(container, curveDef, options) {
|
|
|
1416
1416
|
}
|
|
1417
1417
|
|
|
1418
1418
|
// src/renderer-dot-matrix.ts
|
|
1419
|
-
var NUM_BUCKETS = 8;
|
|
1420
1419
|
function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
1421
1420
|
const {
|
|
1422
1421
|
cols = 32,
|
|
@@ -1440,14 +1439,8 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1440
1439
|
const cellW = W / cols;
|
|
1441
1440
|
const cellH = H / rows;
|
|
1442
1441
|
const dotR = Math.min(cellW, cellH) * 0.36;
|
|
1443
|
-
let
|
|
1444
|
-
|
|
1445
|
-
validateBaseRenderOptions({ trailColor: initialColor });
|
|
1446
|
-
gradientRgb = initialColor.map(colorToRgb);
|
|
1447
|
-
} else {
|
|
1448
|
-
gradientRgb = null;
|
|
1449
|
-
}
|
|
1450
|
-
let colorRgb = gradientRgb ? gradientRgb[0] : colorToRgb(initialColor);
|
|
1442
|
+
let gradientOklab = null;
|
|
1443
|
+
let colorRgb = { r: 255, g: 255, b: 255 };
|
|
1451
1444
|
let currentTrailStyle = initialTrailStyle;
|
|
1452
1445
|
let animTime = 0;
|
|
1453
1446
|
const ANIM_PERIOD = 6;
|
|
@@ -1455,7 +1448,12 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1455
1448
|
let scale = 1;
|
|
1456
1449
|
let offsetX = 0;
|
|
1457
1450
|
let offsetY = 0;
|
|
1458
|
-
let
|
|
1451
|
+
let pixelMaskStarts = new Uint32Array(0);
|
|
1452
|
+
let pixelMaskLengths = new Uint32Array(0);
|
|
1453
|
+
let pixelMaskIndices = new Uint32Array(0);
|
|
1454
|
+
let pixelMaskCoverages = new Float32Array(0);
|
|
1455
|
+
let bgImageData = null;
|
|
1456
|
+
let frameImageData = null;
|
|
1459
1457
|
let animationId = null;
|
|
1460
1458
|
let lastTime = 0;
|
|
1461
1459
|
let pausedByVisibility = false;
|
|
@@ -1463,33 +1461,82 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1463
1461
|
let morphReject = null;
|
|
1464
1462
|
let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
|
|
1465
1463
|
let morphProgress = 0;
|
|
1466
|
-
function
|
|
1467
|
-
|
|
1468
|
-
const
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
|
|
1464
|
+
function computePixelMask() {
|
|
1465
|
+
const starts = new Uint32Array(cols * rows);
|
|
1466
|
+
const lengths = new Uint32Array(cols * rows);
|
|
1467
|
+
const allIndices = [];
|
|
1468
|
+
const allCoverages = [];
|
|
1469
|
+
const cornerR = roundness * dotR;
|
|
1470
|
+
const cornerR2 = cornerR * cornerR;
|
|
1471
|
+
const SSAA = 4;
|
|
1472
|
+
const SSAA2 = SSAA * SSAA;
|
|
1472
1473
|
for (let row = 0; row < rows; row++) {
|
|
1473
1474
|
for (let col = 0; col < cols; col++) {
|
|
1475
|
+
const dotIdx = row * cols + col;
|
|
1474
1476
|
const cx = (col + 0.5) * cellW;
|
|
1475
1477
|
const cy = (row + 0.5) * cellH;
|
|
1476
|
-
|
|
1478
|
+
const x0 = Math.max(0, Math.floor(cx - dotR - 1));
|
|
1479
|
+
const x1 = Math.min(W - 1, Math.ceil(cx + dotR + 1));
|
|
1480
|
+
const y0 = Math.max(0, Math.floor(cy - dotR - 1));
|
|
1481
|
+
const y1 = Math.min(H - 1, Math.ceil(cy + dotR + 1));
|
|
1482
|
+
starts[dotIdx] = allIndices.length;
|
|
1483
|
+
let count = 0;
|
|
1484
|
+
for (let py = y0; py <= y1; py++) {
|
|
1485
|
+
for (let px = x0; px <= x1; px++) {
|
|
1486
|
+
let hits = 0;
|
|
1487
|
+
for (let sy = 0; sy < SSAA; sy++) {
|
|
1488
|
+
const spyCenter = py + (sy + 0.5) / SSAA;
|
|
1489
|
+
for (let sx = 0; sx < SSAA; sx++) {
|
|
1490
|
+
const spxCenter = px + (sx + 0.5) / SSAA;
|
|
1491
|
+
const dx = Math.max(Math.abs(spxCenter - cx) - (dotR - cornerR), 0);
|
|
1492
|
+
const dy = Math.max(Math.abs(spyCenter - cy) - (dotR - cornerR), 0);
|
|
1493
|
+
if (dx * dx + dy * dy <= cornerR2) {
|
|
1494
|
+
hits++;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
if (hits > 0) {
|
|
1499
|
+
allIndices.push((py * W + px) * 4);
|
|
1500
|
+
allCoverages.push(hits / SSAA2);
|
|
1501
|
+
count++;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
lengths[dotIdx] = count;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
pixelMaskStarts = starts;
|
|
1509
|
+
pixelMaskLengths = lengths;
|
|
1510
|
+
pixelMaskIndices = new Uint32Array(allIndices);
|
|
1511
|
+
pixelMaskCoverages = new Float32Array(allCoverages);
|
|
1512
|
+
}
|
|
1513
|
+
function buildBgImageData() {
|
|
1514
|
+
bgImageData = new ImageData(W, H);
|
|
1515
|
+
const bg = colorRgb;
|
|
1516
|
+
const baseAlpha = 0.05 * 255;
|
|
1517
|
+
const { data } = bgImageData;
|
|
1518
|
+
const n = cols * rows;
|
|
1519
|
+
for (let dotIdx = 0; dotIdx < n; dotIdx++) {
|
|
1520
|
+
const start = pixelMaskStarts[dotIdx];
|
|
1521
|
+
const len = pixelMaskLengths[dotIdx];
|
|
1522
|
+
for (let k = 0; k < len; k++) {
|
|
1523
|
+
const px = pixelMaskIndices[start + k];
|
|
1524
|
+
const coverage = pixelMaskCoverages[start + k];
|
|
1525
|
+
data[px] = bg.r;
|
|
1526
|
+
data[px + 1] = bg.g;
|
|
1527
|
+
data[px + 2] = bg.b;
|
|
1528
|
+
data[px + 3] = Math.round(baseAlpha * coverage);
|
|
1477
1529
|
}
|
|
1478
1530
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
return {
|
|
1489
|
-
r: Math.round(a.r + (bStop.r - a.r) * mix),
|
|
1490
|
-
g: Math.round(a.g + (bStop.g - a.g) * mix),
|
|
1491
|
-
b: Math.round(a.b + (bStop.b - a.b) * mix)
|
|
1492
|
-
};
|
|
1531
|
+
}
|
|
1532
|
+
function applyColor(color) {
|
|
1533
|
+
if (Array.isArray(color)) {
|
|
1534
|
+
gradientOklab = color.map((c) => parseColorToOklab(c));
|
|
1535
|
+
colorRgb = oklabToRgb(gradientOklab[0]);
|
|
1536
|
+
} else {
|
|
1537
|
+
gradientOklab = null;
|
|
1538
|
+
colorRgb = colorToRgb(color);
|
|
1539
|
+
}
|
|
1493
1540
|
}
|
|
1494
1541
|
function calculateBoundaries(skel) {
|
|
1495
1542
|
const b = computeBoundaries(skel, W, H);
|
|
@@ -1538,43 +1585,44 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1538
1585
|
}
|
|
1539
1586
|
}
|
|
1540
1587
|
function draw() {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
ctx.drawImage(bgCanvas, 0, 0);
|
|
1588
|
+
if (!bgImageData || !frameImageData) {
|
|
1589
|
+
return;
|
|
1544
1590
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
const { r, g, b } = colorRgb;
|
|
1571
|
-
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
|
1572
|
-
}
|
|
1573
|
-
ctx.globalAlpha = alpha;
|
|
1574
|
-
ctx.fill();
|
|
1591
|
+
frameImageData.data.set(bgImageData.data);
|
|
1592
|
+
const { data } = frameImageData;
|
|
1593
|
+
const timeOffset = currentTrailStyle === "gradient-animated" ? animTime / ANIM_PERIOD : 0;
|
|
1594
|
+
const n = cols * rows;
|
|
1595
|
+
for (let dotIdx = 0; dotIdx < n; dotIdx++) {
|
|
1596
|
+
const intensity = grid[dotIdx];
|
|
1597
|
+
if (intensity <= 0) {
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
let r, g, b;
|
|
1601
|
+
if (gradientOklab !== null) {
|
|
1602
|
+
({ r, g, b } = oklabToRgb(getPaletteColor(gradientOklab, intensity, timeOffset)));
|
|
1603
|
+
} else {
|
|
1604
|
+
({ r, g, b } = colorRgb);
|
|
1605
|
+
}
|
|
1606
|
+
const baseA = (0.08 + intensity * 0.92) * 255;
|
|
1607
|
+
const start = pixelMaskStarts[dotIdx];
|
|
1608
|
+
const len = pixelMaskLengths[dotIdx];
|
|
1609
|
+
for (let k = 0; k < len; k++) {
|
|
1610
|
+
const px = pixelMaskIndices[start + k];
|
|
1611
|
+
const coverage = pixelMaskCoverages[start + k];
|
|
1612
|
+
data[px] = r;
|
|
1613
|
+
data[px + 1] = g;
|
|
1614
|
+
data[px + 2] = b;
|
|
1615
|
+
data[px + 3] = Math.round(baseA * coverage);
|
|
1575
1616
|
}
|
|
1576
1617
|
}
|
|
1577
|
-
ctx.
|
|
1618
|
+
ctx.putImageData(frameImageData, 0, 0);
|
|
1619
|
+
}
|
|
1620
|
+
function completeMorphNow() {
|
|
1621
|
+
engine.completeMorph();
|
|
1622
|
+
morphResolve?.();
|
|
1623
|
+
morphResolve = null;
|
|
1624
|
+
morphReject = null;
|
|
1625
|
+
morphProgress = 0;
|
|
1578
1626
|
}
|
|
1579
1627
|
function renderFrame(deltaTime) {
|
|
1580
1628
|
if (engine.morphAlpha !== null) {
|
|
@@ -1582,11 +1630,7 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1582
1630
|
engine.setMorphAlpha(morphProgress);
|
|
1583
1631
|
calculateBoundaries(engine.getSarmalSkeleton());
|
|
1584
1632
|
if (morphProgress >= 1) {
|
|
1585
|
-
|
|
1586
|
-
morphResolve?.();
|
|
1587
|
-
morphResolve = null;
|
|
1588
|
-
morphReject = null;
|
|
1589
|
-
morphProgress = 0;
|
|
1633
|
+
completeMorphNow();
|
|
1590
1634
|
calculateBoundaries(engine.getSarmalSkeleton());
|
|
1591
1635
|
}
|
|
1592
1636
|
} else if (engine.isLiveSkeleton) {
|
|
@@ -1604,8 +1648,12 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1604
1648
|
renderFrame(deltaTime);
|
|
1605
1649
|
animationId = requestAnimationFrame(loop);
|
|
1606
1650
|
}
|
|
1651
|
+
validateBaseRenderOptions({ trailColor: initialColor });
|
|
1652
|
+
applyColor(initialColor);
|
|
1607
1653
|
calculateBoundaries(engine.getSarmalSkeleton());
|
|
1608
|
-
|
|
1654
|
+
computePixelMask();
|
|
1655
|
+
frameImageData = new ImageData(W, H);
|
|
1656
|
+
buildBgImageData();
|
|
1609
1657
|
if (initialPhase !== void 0) {
|
|
1610
1658
|
engine.seek(initialPhase);
|
|
1611
1659
|
}
|
|
@@ -1650,11 +1698,7 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1650
1698
|
*/
|
|
1651
1699
|
morphTo(target, opts) {
|
|
1652
1700
|
if (morphResolve !== null) {
|
|
1653
|
-
|
|
1654
|
-
morphResolve();
|
|
1655
|
-
morphResolve = null;
|
|
1656
|
-
morphReject = null;
|
|
1657
|
-
morphProgress = 0;
|
|
1701
|
+
completeMorphNow();
|
|
1658
1702
|
}
|
|
1659
1703
|
morphDurationMs = opts?.duration ?? DEFAULT_MORPH_DURATION_MS;
|
|
1660
1704
|
morphProgress = 0;
|
|
@@ -1675,13 +1719,7 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1675
1719
|
validateBaseRenderOptions(partial);
|
|
1676
1720
|
let needsRebuildBg = false;
|
|
1677
1721
|
if (partial.trailColor !== void 0) {
|
|
1678
|
-
|
|
1679
|
-
gradientRgb = partial.trailColor.map(colorToRgb);
|
|
1680
|
-
colorRgb = gradientRgb[0];
|
|
1681
|
-
} else {
|
|
1682
|
-
gradientRgb = null;
|
|
1683
|
-
colorRgb = colorToRgb(partial.trailColor);
|
|
1684
|
-
}
|
|
1722
|
+
applyColor(partial.trailColor);
|
|
1685
1723
|
needsRebuildBg = true;
|
|
1686
1724
|
}
|
|
1687
1725
|
if (partial.trailStyle !== void 0) {
|
|
@@ -1691,11 +1729,15 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
|
|
|
1691
1729
|
}
|
|
1692
1730
|
}
|
|
1693
1731
|
if (needsRebuildBg) {
|
|
1694
|
-
|
|
1732
|
+
buildBgImageData();
|
|
1695
1733
|
}
|
|
1696
|
-
if (currentTrailStyle !== "default" &&
|
|
1734
|
+
if (currentTrailStyle !== "default" && gradientOklab === null) {
|
|
1735
|
+
console.warn(
|
|
1736
|
+
`[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.`
|
|
1737
|
+
);
|
|
1738
|
+
} else if (currentTrailStyle === "default" && gradientOklab !== null) {
|
|
1697
1739
|
console.warn(
|
|
1698
|
-
|
|
1740
|
+
'[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.'
|
|
1699
1741
|
);
|
|
1700
1742
|
}
|
|
1701
1743
|
}
|