@sarmal/core 0.34.0 → 0.35.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/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,
@@ -1457,7 +1456,12 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1457
1456
  let scale = 1;
1458
1457
  let offsetX = 0;
1459
1458
  let offsetY = 0;
1460
- let bgCanvas = null;
1459
+ let pixelMaskStarts = new Uint32Array(0);
1460
+ let pixelMaskLengths = new Uint32Array(0);
1461
+ let pixelMaskIndices = new Uint32Array(0);
1462
+ let pixelMaskCoverages = new Float32Array(0);
1463
+ let bgImageData = null;
1464
+ let frameImageData = null;
1461
1465
  let animationId = null;
1462
1466
  let lastTime = 0;
1463
1467
  let pausedByVisibility = false;
@@ -1465,20 +1469,73 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1465
1469
  let morphReject = null;
1466
1470
  let morphDurationMs = DEFAULT_MORPH_DURATION_MS;
1467
1471
  let morphProgress = 0;
1468
- function buildBgCanvas() {
1469
- bgCanvas = new OffscreenCanvas(W, H);
1470
- const bgCtx = bgCanvas.getContext("2d");
1471
- const bg = gradientRgb ? gradientRgb[0] : colorRgb;
1472
- bgCtx.fillStyle = `rgba(${bg.r},${bg.g},${bg.b},0.05)`;
1473
- bgCtx.beginPath();
1472
+ function computePixelMask() {
1473
+ const starts = new Uint32Array(cols * rows);
1474
+ const lengths = new Uint32Array(cols * rows);
1475
+ const allIndices = [];
1476
+ const allCoverages = [];
1477
+ const cornerR = roundness * dotR;
1478
+ const cornerR2 = cornerR * cornerR;
1479
+ const SSAA = 4;
1480
+ const SSAA2 = SSAA * SSAA;
1474
1481
  for (let row = 0; row < rows; row++) {
1475
1482
  for (let col = 0; col < cols; col++) {
1483
+ const dotIdx = row * cols + col;
1476
1484
  const cx = (col + 0.5) * cellW;
1477
1485
  const cy = (row + 0.5) * cellH;
1478
- bgCtx.roundRect(cx - dotR, cy - dotR, dotR * 2, dotR * 2, roundness * dotR);
1486
+ const x0 = Math.max(0, Math.floor(cx - dotR - 1));
1487
+ const x1 = Math.min(W - 1, Math.ceil(cx + dotR + 1));
1488
+ const y0 = Math.max(0, Math.floor(cy - dotR - 1));
1489
+ const y1 = Math.min(H - 1, Math.ceil(cy + dotR + 1));
1490
+ starts[dotIdx] = allIndices.length;
1491
+ let count = 0;
1492
+ for (let py = y0; py <= y1; py++) {
1493
+ for (let px = x0; px <= x1; px++) {
1494
+ let hits = 0;
1495
+ for (let sy = 0; sy < SSAA; sy++) {
1496
+ const spyCenter = py + (sy + 0.5) / SSAA;
1497
+ for (let sx = 0; sx < SSAA; sx++) {
1498
+ const spxCenter = px + (sx + 0.5) / SSAA;
1499
+ const dx = Math.max(Math.abs(spxCenter - cx) - (dotR - cornerR), 0);
1500
+ const dy = Math.max(Math.abs(spyCenter - cy) - (dotR - cornerR), 0);
1501
+ if (dx * dx + dy * dy <= cornerR2) {
1502
+ hits++;
1503
+ }
1504
+ }
1505
+ }
1506
+ if (hits > 0) {
1507
+ allIndices.push((py * W + px) * 4);
1508
+ allCoverages.push(hits / SSAA2);
1509
+ count++;
1510
+ }
1511
+ }
1512
+ }
1513
+ lengths[dotIdx] = count;
1514
+ }
1515
+ }
1516
+ pixelMaskStarts = starts;
1517
+ pixelMaskLengths = lengths;
1518
+ pixelMaskIndices = new Uint32Array(allIndices);
1519
+ pixelMaskCoverages = new Float32Array(allCoverages);
1520
+ }
1521
+ function buildBgImageData() {
1522
+ bgImageData = new ImageData(W, H);
1523
+ const bg = gradientRgb ? gradientRgb[0] : colorRgb;
1524
+ const baseAlpha = 0.05 * 255;
1525
+ const { data } = bgImageData;
1526
+ const n = cols * rows;
1527
+ for (let dotIdx = 0; dotIdx < n; dotIdx++) {
1528
+ const start = pixelMaskStarts[dotIdx];
1529
+ const len = pixelMaskLengths[dotIdx];
1530
+ for (let k = 0; k < len; k++) {
1531
+ const px = pixelMaskIndices[start + k];
1532
+ const coverage = pixelMaskCoverages[start + k];
1533
+ data[px] = bg.r;
1534
+ data[px + 1] = bg.g;
1535
+ data[px + 2] = bg.b;
1536
+ data[px + 3] = Math.round(baseAlpha * coverage);
1479
1537
  }
1480
1538
  }
1481
- bgCtx.fill();
1482
1539
  }
1483
1540
  function sampleGradientRgb(stops, t) {
1484
1541
  const n = stops.length;
@@ -1540,43 +1597,38 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1540
1597
  }
1541
1598
  }
1542
1599
  function draw() {
1543
- ctx.clearRect(0, 0, W, H);
1544
- if (bgCanvas) {
1545
- ctx.drawImage(bgCanvas, 0, 0);
1600
+ if (!bgImageData || !frameImageData) {
1601
+ return;
1546
1602
  }
1547
- const animOffset = currentTrailStyle === "gradient-animated" ? Math.abs(animTime / ANIM_PERIOD % 2 - 1) * 0.35 : 0;
1548
- for (let bucket = 0; bucket < NUM_BUCKETS; bucket++) {
1549
- const lo = bucket / NUM_BUCKETS;
1550
- const hi = (bucket + 1) / NUM_BUCKETS;
1551
- const midpoint = (lo + hi) / 2;
1552
- const alpha = 0.08 + midpoint * 0.92;
1553
- let hasLit = false;
1554
- ctx.beginPath();
1555
- for (let row = 0; row < rows; row++) {
1556
- for (let col = 0; col < cols; col++) {
1557
- const intensity = grid[row * cols + col];
1558
- if (intensity > lo && intensity <= hi) {
1559
- const cx = (col + 0.5) * cellW;
1560
- const cy = (row + 0.5) * cellH;
1561
- ctx.roundRect(cx - dotR, cy - dotR, dotR * 2, dotR * 2, roundness * dotR);
1562
- hasLit = true;
1563
- }
1564
- }
1565
- }
1566
- if (hasLit) {
1567
- if (gradientRgb !== null) {
1568
- const t = ((midpoint + animOffset) % 1 + 1) % 1;
1569
- const { r, g, b } = sampleGradientRgb(gradientRgb, t);
1570
- ctx.fillStyle = `rgb(${r},${g},${b})`;
1571
- } else {
1572
- const { r, g, b } = colorRgb;
1573
- ctx.fillStyle = `rgb(${r},${g},${b})`;
1574
- }
1575
- ctx.globalAlpha = alpha;
1576
- ctx.fill();
1603
+ frameImageData.data.set(bgImageData.data);
1604
+ const { data } = frameImageData;
1605
+ const sineOffset = currentTrailStyle === "gradient-animated" ? 0.15 * Math.sin(animTime / ANIM_PERIOD * 2 * Math.PI) : 0;
1606
+ const n = cols * rows;
1607
+ for (let dotIdx = 0; dotIdx < n; dotIdx++) {
1608
+ const intensity = grid[dotIdx];
1609
+ if (intensity <= 0) {
1610
+ continue;
1611
+ }
1612
+ let r, g, b;
1613
+ if (gradientRgb !== null) {
1614
+ const t = Math.max(0, Math.min(1, intensity + sineOffset));
1615
+ ({ r, g, b } = sampleGradientRgb(gradientRgb, t));
1616
+ } else {
1617
+ ({ r, g, b } = colorRgb);
1618
+ }
1619
+ const baseA = (0.08 + intensity * 0.92) * 255;
1620
+ const start = pixelMaskStarts[dotIdx];
1621
+ const len = pixelMaskLengths[dotIdx];
1622
+ for (let k = 0; k < len; k++) {
1623
+ const px = pixelMaskIndices[start + k];
1624
+ const coverage = pixelMaskCoverages[start + k];
1625
+ data[px] = r;
1626
+ data[px + 1] = g;
1627
+ data[px + 2] = b;
1628
+ data[px + 3] = Math.round(baseA * coverage);
1577
1629
  }
1578
1630
  }
1579
- ctx.globalAlpha = 1;
1631
+ ctx.putImageData(frameImageData, 0, 0);
1580
1632
  }
1581
1633
  function renderFrame(deltaTime) {
1582
1634
  if (engine.morphAlpha !== null) {
@@ -1607,7 +1659,9 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1607
1659
  animationId = requestAnimationFrame(loop);
1608
1660
  }
1609
1661
  calculateBoundaries(engine.getSarmalSkeleton());
1610
- buildBgCanvas();
1662
+ computePixelMask();
1663
+ frameImageData = new ImageData(W, H);
1664
+ buildBgImageData();
1611
1665
  if (initialPhase !== void 0) {
1612
1666
  engine.seek(initialPhase);
1613
1667
  }
@@ -1693,7 +1747,7 @@ function createSarmalDotMatrix(canvas, curveDef, options) {
1693
1747
  }
1694
1748
  }
1695
1749
  if (needsRebuildBg) {
1696
- buildBgCanvas();
1750
+ buildBgImageData();
1697
1751
  }
1698
1752
  if (currentTrailStyle !== "default" && gradientRgb === null) {
1699
1753
  console.warn(