@project-gridmap/library 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +1 -5
  2. package/package.json +1 -1
  3. package/src/gridmap.js +111 -12
package/README.md CHANGED
@@ -3,14 +3,10 @@
3
3
  Project Gridmap is a content-agnostic hierarchical grid map renderer. It knows
4
4
  about layers, groups, items, and cells. It does not know what those things mean.
5
5
 
6
- The current root `index.html` is still the standalone Bible demo for GitHub
7
- Pages. The importable package in `src/` is deliberately generic so it can power
8
- that demo, The Bible Game, and other ordered datasets.
9
-
10
6
  ## Install
11
7
 
12
8
  ```bash
13
- npm install @project-gridmap/core
9
+ npm install @project-gridmap/library
14
10
  ```
15
11
 
16
12
  ## Usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@project-gridmap/library",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A content-agnostic hierarchical grid map renderer.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/gridmap.js CHANGED
@@ -201,6 +201,8 @@ export class Gridmap {
201
201
  this.medianValue = median(values);
202
202
  const sortedValues = values.filter((value) => Number.isFinite(value)).sort((a, b) => a - b);
203
203
  this.referenceValue = sortedValues[Math.floor(Math.max(0, sortedValues.length - 1) * 0.99)] ?? this.medianValue;
204
+ const sizes = this.model.cells.map((cell) => this.numberWorldSize(cell)).sort((a, b) => a - b);
205
+ this.medianNumberSize = sizes[sizes.length >> 1] || this.model.cellSide;
204
206
  }
205
207
 
206
208
  viewport() {
@@ -564,7 +566,15 @@ export class Gridmap {
564
566
 
565
567
  const selected = this.state.selected;
566
568
  if (selected) {
569
+ const item = this.model.items[selected.itemIndex];
567
570
  const colour = this.colourOf(selected, theme.text);
571
+ ctx.strokeStyle = colour;
572
+ ctx.globalAlpha = 1;
573
+ ctx.lineWidth = 1.5;
574
+ ctx.beginPath();
575
+ rect(item);
576
+ ctx.stroke();
577
+
568
578
  ctx.strokeStyle = colour;
569
579
  ctx.fillStyle = colour;
570
580
  ctx.globalAlpha = 0.1;
@@ -598,34 +608,95 @@ export class Gridmap {
598
608
  this.renderLabels(X, Y);
599
609
  }
600
610
 
611
+ markScale(cell, floor = 0.4) {
612
+ return this.options.relativeMarkSize && cell.value
613
+ ? Math.max(floor, Math.sqrt(cell.value / this.medianValue))
614
+ : 1;
615
+ }
616
+
617
+ numberWorldSize(cell) {
618
+ const digits = String(cell.label).length;
619
+ const share = this.options.relativeMarkSize && cell.value
620
+ ? Math.min(1, Math.max(0.35, Math.sqrt(cell.value / this.referenceValue)))
621
+ : 0.7;
622
+ return Math.min(
623
+ Math.min(cell.width, cell.height) * this.options.markMaxSize * share,
624
+ cell.width * 0.7 / (digits * 0.6),
625
+ );
626
+ }
627
+
628
+ useNumbers() {
629
+ return this.options.markType === 'number' && this.medianNumberSize * this.camera.k >= this.options.numberMinPx;
630
+ }
631
+
601
632
  renderMarks(ctx, X, Y, onScreen) {
602
633
  const theme = this.options.theme;
603
- const useNumbers = this.options.markType === 'number' && this.model.cellSide * this.camera.k >= this.options.numberMinPx;
634
+ const useNumbers = this.useNumbers();
635
+ const worldK = this.frame(this.model.world).k;
636
+ const dot = Math.min(7, 3 * (this.camera.k / worldK) ** 0.28);
637
+ const damping = useNumbers
638
+ ? Math.min(1, 24 / (this.medianNumberSize * this.camera.k))
639
+ : 1;
640
+
604
641
  ctx.textAlign = 'center';
605
642
  ctx.textBaseline = 'middle';
606
643
  for (const item of this.model.items) {
607
644
  ctx.fillStyle = this.colourOf(item, theme.text);
608
645
  for (const cell of item.cells) {
609
646
  if (!onScreen(cell)) continue;
610
- const scale = this.options.relativeMarkSize && cell.value
611
- ? Math.max(0.35, Math.sqrt(cell.value / this.referenceValue))
612
- : 0.7;
613
- const size = Math.min(cell.width, cell.height) * this.options.markMaxSize * scale * this.camera.k;
614
- ctx.globalAlpha = cell === this.state.hover || cell === this.state.selected ? 1 : this.options.markOpacity;
647
+ const strong = cell === this.state.hover || cell === this.state.selected;
648
+ ctx.globalAlpha = strong ? 1 : this.options.markOpacity;
615
649
  if (useNumbers) {
616
- ctx.font = `${cell === this.state.hover || cell === this.state.selected ? 500 : 400} ${Math.max(2, Math.min(24, size))}px ${theme.font}`;
650
+ const size = this.numberWorldSize(cell) * this.camera.k * damping;
651
+ if (size < 2) continue;
652
+ ctx.font = `${strong ? 500 : 400} ${Math.max(2, Math.min(24, size))}px ${theme.font}`;
617
653
  ctx.fillText(cell.label, X(cell.centerX), Y(cell.centerY));
618
654
  } else {
619
- const radius = Math.min(4, Math.max(1.2, size / 8));
655
+ const radius = dot * this.markScale(cell) / 2;
620
656
  ctx.beginPath();
621
657
  ctx.arc(X(cell.centerX), Y(cell.centerY), radius, 0, Math.PI * 2);
622
658
  ctx.fill();
623
659
  }
624
660
  }
625
661
  }
662
+
663
+ this.renderMarkEmphasis(ctx, X, Y, useNumbers, dot, damping);
626
664
  ctx.globalAlpha = 1;
627
665
  }
628
666
 
667
+ renderMarkEmphasis(ctx, X, Y, useNumbers, dot, damping) {
668
+ const hover = this.state.hover;
669
+ const selected = this.state.selected;
670
+
671
+ if (!useNumbers && hover) {
672
+ const colour = this.colourOf(hover, this.options.theme.text);
673
+ const radius = dot * this.markScale(hover) / 2;
674
+ ctx.fillStyle = colour;
675
+ ctx.strokeStyle = colour;
676
+ ctx.globalAlpha = 1;
677
+ ctx.beginPath();
678
+ ctx.arc(X(hover.centerX), Y(hover.centerY), radius + 1.2, 0, Math.PI * 2);
679
+ ctx.fill();
680
+ ctx.globalAlpha = 0.35;
681
+ ctx.beginPath();
682
+ ctx.arc(X(hover.centerX), Y(hover.centerY), radius + 7, 0, Math.PI * 2);
683
+ ctx.stroke();
684
+ }
685
+
686
+ if (selected) {
687
+ const colour = this.colourOf(selected, this.options.theme.text);
688
+ const numberSize = this.numberWorldSize(selected) * this.camera.k * damping;
689
+ const numberRadius = Math.max(numberSize * 0.55, String(selected.label).length * 0.6 * numberSize / 2);
690
+ const starRadius = dot * this.markScale(selected) / 2;
691
+ ctx.strokeStyle = colour;
692
+ ctx.globalAlpha = 1;
693
+ ctx.lineWidth = 1;
694
+ ctx.beginPath();
695
+ ctx.arc(X(selected.centerX), Y(selected.centerY), useNumbers ? numberRadius + 5 : starRadius + 6, 0, Math.PI * 2);
696
+ ctx.stroke();
697
+ }
698
+ }
699
+
629
700
  renderVeil(ctx, rect) {
630
701
  const region = this.focusRegion();
631
702
  if (!region) return;
@@ -642,27 +713,55 @@ export class Gridmap {
642
713
  renderLabels(X, Y) {
643
714
  const labelItem = this.options.labels.item;
644
715
  const selected = this.state.selected;
716
+ const hover = this.state.hover;
645
717
  this.overlay.replaceChildren();
718
+
719
+ for (const layer of this.model.layers) {
720
+ const h = layer.height * this.camera.k;
721
+ const text = layer.label.toUpperCase();
722
+ if (h < text.length * 7.5) continue;
723
+ const el = createElement('div', { text: label }, this.overlay);
724
+ Object.assign(el.style, {
725
+ position: 'absolute',
726
+ left: `${X(layer.x) - 22}px`,
727
+ top: `${Y(layer.y + layer.height / 2)}px`,
728
+ transform: 'translate(-50%, -50%) rotate(-90deg)',
729
+ transformOrigin: 'center',
730
+ color: selected?.layerIndex === layer.index ? this.options.theme.text : this.options.theme.mutedText,
731
+ fontSize: '9.5px',
732
+ letterSpacing: '0.32em',
733
+ whiteSpace: 'nowrap',
734
+ });
735
+ }
736
+
646
737
  for (const item of this.model.items) {
647
738
  const w = item.width * this.camera.k;
648
739
  const h = item.height * this.camera.k;
649
- if (w < 28 || h < 14) continue;
650
- const text = typeof labelItem === 'function'
740
+ if (h < 22) continue;
741
+ const full = typeof labelItem === 'function'
651
742
  ? labelItem(item, this)
652
743
  : this.options.itemLabels === 'full' ? item.label : item.shortLabel;
744
+ const text = `${item.index + 1}.${full}`.toUpperCase();
745
+ const number = String(item.index + 1);
746
+ const label = text.length * 7.3 + 20 <= w ? text : number.length * 7.3 + 14 <= w ? number : null;
747
+ if (!label) continue;
748
+
653
749
  const el = createElement('div', { text }, this.overlay);
654
750
  Object.assign(el.style, {
655
751
  position: 'absolute',
656
752
  left: `${X(item.x) + 5}px`,
657
753
  top: `${Y(item.y) - 6}px`,
658
- maxWidth: `${Math.max(20, w - 10)}px`,
754
+ maxWidth: `${Math.max(20, w - 8)}px`,
659
755
  overflow: 'hidden',
660
756
  whiteSpace: 'nowrap',
661
757
  textOverflow: 'clip',
662
758
  color: this.colourOf(item, this.options.theme.mutedText),
663
759
  background: this.options.theme.background,
664
760
  padding: '0 4px',
665
- opacity: selected?.itemIndex === item.index ? '1' : '0.78',
761
+ fontSize: '9px',
762
+ letterSpacing: '0.14em',
763
+ lineHeight: '1.33',
764
+ opacity: selected?.itemIndex === item.index || hover?.itemIndex === item.index ? '1' : '0.72',
666
765
  });
667
766
  }
668
767
  }