@tsdraw/core 0.9.0 → 0.9.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/dist/index.cjs CHANGED
@@ -561,7 +561,11 @@ function getLineDash(dash, width) {
561
561
  var DEFAULT_SPACING = 20;
562
562
  var DEFAULT_LINE_WIDTH = 0.5;
563
563
  var DEFAULT_DOT_RADIUS = 1;
564
- var DEFAULT_OPACITY = 0.25;
564
+ var DEFAULT_OPACITY = 1;
565
+ var LINE_GAP_FADE_IN = 3;
566
+ var LINE_GAP_FADE_FULL = 8;
567
+ var DOT_GAP_FADE_IN = 2;
568
+ var DOT_GAP_FADE_FULL = 16;
565
569
  function resolvePresetPatternColor(colorLight, colorDark, theme) {
566
570
  if (theme === "dark") return colorDark ?? colorLight ?? "#888888";
567
571
  return colorLight ?? "#c0c0c0";
@@ -574,56 +578,116 @@ function visiblePageRect(viewport, canvasWidth, canvasHeight) {
574
578
  maxY: (canvasHeight - viewport.y) / viewport.zoom
575
579
  };
576
580
  }
577
- function drawHorizontalLines(ctx, visible, spacing, lineWidth, color, opacity) {
581
+ function isOnGrid(value, gridSpacing) {
582
+ const remainder = (value % gridSpacing + gridSpacing) % gridSpacing;
583
+ return remainder < 0.5 || remainder > gridSpacing - 0.5;
584
+ }
585
+ function fadeForScreenGap(screenGap, fadeIn, fadeFull) {
586
+ return Math.min(1, Math.max(0, (screenGap - fadeIn) / (fadeFull - fadeIn)));
587
+ }
588
+ function buildLevels(baseSpacing, zoom, fadeIn, fadeFull) {
589
+ let topSpacing = baseSpacing;
590
+ while (topSpacing * zoom < fadeFull) {
591
+ topSpacing *= 2;
592
+ }
593
+ const levels = [];
594
+ for (let s = topSpacing; s >= baseSpacing; s /= 2) {
595
+ const fade = fadeForScreenGap(s * zoom, fadeIn, fadeFull);
596
+ if (fade < 0.01) break;
597
+ levels.push({ spacing: s, fade });
598
+ }
599
+ return levels;
600
+ }
601
+ function drawDotTile(ctx, physicalWidth, physicalHeight, panXPx, panYPx, exactTileSize, radiusPx, color, alpha) {
602
+ if (alpha < 5e-3 || exactTileSize < 2) return;
603
+ const tilePixels = Math.ceil(exactTileSize);
604
+ const tileScale = exactTileSize / tilePixels;
605
+ const center = tilePixels / 2;
606
+ const tile = new OffscreenCanvas(tilePixels, tilePixels);
607
+ const tctx = tile.getContext("2d");
608
+ tctx.fillStyle = color;
609
+ tctx.beginPath();
610
+ tctx.arc(center, center, radiusPx / tileScale, 0, Math.PI * 2);
611
+ tctx.fill();
612
+ const pattern = ctx.createPattern(tile, "repeat");
613
+ if (!pattern) return;
614
+ pattern.setTransform(
615
+ new DOMMatrix().translateSelf(panXPx - exactTileSize / 2, panYPx - exactTileSize / 2).scaleSelf(tileScale, tileScale)
616
+ );
617
+ ctx.save();
618
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
619
+ ctx.globalAlpha = alpha;
620
+ ctx.fillStyle = pattern;
621
+ ctx.fillRect(0, 0, physicalWidth, physicalHeight);
622
+ ctx.restore();
623
+ }
624
+ function drawMergingDots(ctx, viewport, canvasWidth, canvasHeight, baseSpacing, dotRadius, color, opacity) {
625
+ const dpr = ctx.getTransform().a;
626
+ const levels = buildLevels(baseSpacing, viewport.zoom, DOT_GAP_FADE_IN, DOT_GAP_FADE_FULL);
627
+ if (levels.length === 0) return;
628
+ const physicalWidth = canvasWidth * dpr;
629
+ const physicalHeight = canvasHeight * dpr;
630
+ const panXPx = viewport.x * dpr;
631
+ const panYPx = viewport.y * dpr;
632
+ const radiusPx = dotRadius * dpr;
633
+ for (const level of levels) {
634
+ const tileSize = level.spacing * viewport.zoom * dpr;
635
+ drawDotTile(ctx, physicalWidth, physicalHeight, panXPx, panYPx, tileSize, radiusPx, color, opacity * level.fade);
636
+ }
637
+ }
638
+ function drawHorizontalLinesForLevel(ctx, visible, spacing, skipSpacing, lineWidth, color, alpha) {
578
639
  const startY = Math.floor(visible.minY / spacing) * spacing;
579
640
  ctx.save();
580
641
  ctx.strokeStyle = color;
581
- ctx.lineWidth = lineWidth / ctx.getTransform().a;
582
- ctx.globalAlpha = opacity;
642
+ ctx.lineWidth = lineWidth;
643
+ ctx.globalAlpha = alpha;
583
644
  ctx.beginPath();
584
645
  for (let y = startY; y <= visible.maxY; y += spacing) {
646
+ if (skipSpacing > 0 && isOnGrid(y, skipSpacing)) continue;
585
647
  ctx.moveTo(visible.minX, y);
586
648
  ctx.lineTo(visible.maxX, y);
587
649
  }
588
650
  ctx.stroke();
589
651
  ctx.restore();
590
652
  }
591
- function drawGridLines(ctx, visible, spacing, lineWidth, color, opacity) {
653
+ function drawMergingLines(ctx, visible, baseSpacing, lineWidth, color, opacity, zoom) {
654
+ const compensatedWidth = lineWidth / ctx.getTransform().a;
655
+ const levels = buildLevels(baseSpacing, zoom, LINE_GAP_FADE_IN, LINE_GAP_FADE_FULL);
656
+ for (let i = 0; i < levels.length; i++) {
657
+ const level = levels[i];
658
+ const coarserSpacing = i > 0 ? levels[i - 1].spacing : 0;
659
+ drawHorizontalLinesForLevel(ctx, visible, level.spacing, coarserSpacing, compensatedWidth, color, opacity * level.fade);
660
+ }
661
+ }
662
+ function drawGridForLevel(ctx, visible, spacing, skipSpacing, lineWidth, color, alpha) {
592
663
  const startX = Math.floor(visible.minX / spacing) * spacing;
593
664
  const startY = Math.floor(visible.minY / spacing) * spacing;
594
- const compensatedWidth = lineWidth / ctx.getTransform().a;
595
665
  ctx.save();
596
666
  ctx.strokeStyle = color;
597
- ctx.lineWidth = compensatedWidth;
598
- ctx.globalAlpha = opacity;
667
+ ctx.lineWidth = lineWidth;
668
+ ctx.globalAlpha = alpha;
599
669
  ctx.beginPath();
600
670
  for (let x = startX; x <= visible.maxX; x += spacing) {
671
+ if (skipSpacing > 0 && isOnGrid(x, skipSpacing)) continue;
601
672
  ctx.moveTo(x, visible.minY);
602
673
  ctx.lineTo(x, visible.maxY);
603
674
  }
604
675
  for (let y = startY; y <= visible.maxY; y += spacing) {
676
+ if (skipSpacing > 0 && isOnGrid(y, skipSpacing)) continue;
605
677
  ctx.moveTo(visible.minX, y);
606
678
  ctx.lineTo(visible.maxX, y);
607
679
  }
608
680
  ctx.stroke();
609
681
  ctx.restore();
610
682
  }
611
- function drawDotPattern(ctx, visible, spacing, dotRadius, color, opacity) {
612
- const startX = Math.floor(visible.minX / spacing) * spacing;
613
- const startY = Math.floor(visible.minY / spacing) * spacing;
614
- const compensatedRadius = dotRadius / ctx.getTransform().a;
615
- ctx.save();
616
- ctx.fillStyle = color;
617
- ctx.globalAlpha = opacity;
618
- ctx.beginPath();
619
- for (let x = startX; x <= visible.maxX; x += spacing) {
620
- for (let y = startY; y <= visible.maxY; y += spacing) {
621
- ctx.moveTo(x + compensatedRadius, y);
622
- ctx.arc(x, y, compensatedRadius, 0, Math.PI * 2);
623
- }
683
+ function drawMergingGrid(ctx, visible, baseSpacing, lineWidth, color, opacity, zoom) {
684
+ const compensatedWidth = lineWidth / ctx.getTransform().a;
685
+ const levels = buildLevels(baseSpacing, zoom, LINE_GAP_FADE_IN, LINE_GAP_FADE_FULL);
686
+ for (let i = 0; i < levels.length; i++) {
687
+ const level = levels[i];
688
+ const coarserSpacing = i > 0 ? levels[i - 1].spacing : 0;
689
+ drawGridForLevel(ctx, visible, level.spacing, coarserSpacing, compensatedWidth, color, opacity * level.fade);
624
690
  }
625
- ctx.fill();
626
- ctx.restore();
627
691
  }
628
692
  function renderCanvasBackground(ctx, viewport, canvasWidth, canvasHeight, options, theme) {
629
693
  if (!options || options.type === "blank") return;
@@ -631,23 +695,24 @@ function renderCanvasBackground(ctx, viewport, canvasWidth, canvasHeight, option
631
695
  options.render(ctx, viewport, canvasWidth, canvasHeight);
632
696
  return;
633
697
  }
634
- const spacing = options.spacing ?? DEFAULT_SPACING;
635
- if (spacing <= 0) return;
698
+ const baseSpacing = options.spacing ?? DEFAULT_SPACING;
699
+ if (baseSpacing <= 0) return;
636
700
  const color = resolvePresetPatternColor(options.color, options.colorDark, theme);
637
701
  const opacity = options.opacity ?? DEFAULT_OPACITY;
702
+ if (options.type === "dots") {
703
+ drawMergingDots(ctx, viewport, canvasWidth, canvasHeight, baseSpacing, options.size ?? DEFAULT_DOT_RADIUS, color, opacity);
704
+ return;
705
+ }
638
706
  const visible = visiblePageRect(viewport, canvasWidth, canvasHeight);
639
707
  ctx.save();
640
708
  ctx.translate(viewport.x, viewport.y);
641
709
  ctx.scale(viewport.zoom, viewport.zoom);
642
710
  switch (options.type) {
643
711
  case "lines":
644
- drawHorizontalLines(ctx, visible, spacing, options.size ?? DEFAULT_LINE_WIDTH, color, opacity);
712
+ drawMergingLines(ctx, visible, baseSpacing, options.size ?? DEFAULT_LINE_WIDTH, color, opacity, viewport.zoom);
645
713
  break;
646
714
  case "grid":
647
- drawGridLines(ctx, visible, spacing, options.size ?? DEFAULT_LINE_WIDTH, color, opacity);
648
- break;
649
- case "dots":
650
- drawDotPattern(ctx, visible, spacing, options.size ?? DEFAULT_DOT_RADIUS, color, opacity);
715
+ drawMergingGrid(ctx, visible, baseSpacing, options.size ?? DEFAULT_LINE_WIDTH, color, opacity, viewport.zoom);
651
716
  break;
652
717
  }
653
718
  ctx.restore();