canvas-globe 1.1.1 → 1.3.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.
@@ -640,6 +640,76 @@ const withAlpha = (color, a) => {
640
640
  return color;
641
641
  };
642
642
 
643
+ /**
644
+ * Aggregates projected points into a pointy-top hexagonal grid.
645
+ *
646
+ * The input is deliberately renderer-shaped (`{ x, y, depth, m }`) so the
647
+ * same helper works after either globe or flat-map projection. Each returned
648
+ * bin preserves its source markers for tooltips, clicks, and custom details.
649
+ */
650
+ const hexBinPoints = (points = [], radius = 18) => {
651
+ const size = Math.max(1, Number(radius) || 18);
652
+ const sqrt3 = Math.sqrt(3);
653
+ const cells = new Map();
654
+
655
+ const roundAxial = (q, r) => {
656
+ let x = q;
657
+ let z = r;
658
+ let y = -x - z;
659
+ let rx = Math.round(x);
660
+ let ry = Math.round(y);
661
+ let rz = Math.round(z);
662
+ const dx = Math.abs(rx - x);
663
+ const dy = Math.abs(ry - y);
664
+ const dz = Math.abs(rz - z);
665
+ if (dx > dy && dx > dz) rx = -ry - rz;
666
+ else if (dy > dz) ry = -rx - rz;
667
+ else rz = -rx - ry;
668
+ return [rx, rz];
669
+ };
670
+
671
+ for (const point of points) {
672
+ if (!Number.isFinite(point?.x) || !Number.isFinite(point?.y)) continue;
673
+ const q = (sqrt3 / 3 * point.x - point.y / 3) / size;
674
+ const r = (2 * point.y / 3) / size;
675
+ const [hq, hr] = roundAxial(q, r);
676
+ const key = `${hq}:${hr}`;
677
+ let cell = cells.get(key);
678
+ if (!cell) {
679
+ cell = {
680
+ q: hq,
681
+ r: hr,
682
+ x: size * sqrt3 * (hq + hr / 2),
683
+ y: size * 1.5 * hr,
684
+ count: 0,
685
+ value: 0,
686
+ depth: 0,
687
+ lon: 0,
688
+ lat: 0,
689
+ weight: 0,
690
+ markers: [],
691
+ };
692
+ cells.set(key, cell);
693
+ }
694
+ const marker = point.m || {};
695
+ const value = Number(marker.count);
696
+ const weight = Number.isFinite(value) && value > 0 ? value : 1;
697
+ cell.count += 1;
698
+ cell.value += weight;
699
+ cell.depth = Math.max(cell.depth, Number(point.depth) || 0);
700
+ cell.lon += (Number(marker.lon) || 0) * weight;
701
+ cell.lat += (Number(marker.lat) || 0) * weight;
702
+ cell.weight += weight;
703
+ cell.markers.push(marker);
704
+ }
705
+
706
+ return [...cells.values()].map((cell) => ({
707
+ ...cell,
708
+ lon: cell.weight ? cell.lon / cell.weight : 0,
709
+ lat: cell.weight ? cell.lat / cell.weight : 0,
710
+ }));
711
+ };
712
+
643
713
  const parseRGB = (color) => {
644
714
  if (color.startsWith("#")) {
645
715
  const hex = color.length === 4 ? color.replace(/#(.)(.)(.)/, "#$1$1$2$2$3$3") : color;
@@ -1314,7 +1384,7 @@ function drawFitted(ctx, media, box) {
1314
1384
  }
1315
1385
 
1316
1386
  // Keep in sync with package.json. Release checks enforce this value.
1317
- const CANVAS_GLOBE_VERSION = "1.1.1";
1387
+ const CANVAS_GLOBE_VERSION = "1.3.0";
1318
1388
 
1319
1389
  /** Local license-key checks and production-use presentation helpers. */
1320
1390
 
@@ -1477,6 +1547,7 @@ const DEFAULTS = {
1477
1547
  timeline: null,
1478
1548
  transparentBackground: false,
1479
1549
  heatmap: false,
1550
+ hexBins: false,
1480
1551
  spikes: false,
1481
1552
  labels: false,
1482
1553
  legend: null,
@@ -1520,13 +1591,28 @@ const DEFAULTS = {
1520
1591
  onCountryHover: null,
1521
1592
  onCountryClick: null,
1522
1593
  onRender: null,
1594
+ effects: null,
1523
1595
  };
1524
1596
 
1525
1597
  const coord = (v) => (Array.isArray(v) ? [v[0], v[1]] : [v.lon ?? v.lng ?? v.longitude, v.lat ?? v.latitude]);
1526
1598
 
1599
+ const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
1600
+
1601
+ /**
1602
+ * An effect's own 0→1 progress at a point on the shared clock. Derived purely
1603
+ * from `ms`, so the same millisecond always renders the same frame.
1604
+ */
1605
+ const phaseOf = (fx, ms) => {
1606
+ const duration = Math.max(1, fx.duration ?? 1000);
1607
+ const total = duration + (fx.hold ?? 0);
1608
+ const elapsed = fx.loop === false ? Math.min(ms, total) : ((ms % total) + total) % total;
1609
+ return Math.min(1, elapsed / duration);
1610
+ };
1611
+
1527
1612
  const defaultTooltip = (target, kind) => {
1528
1613
  if (kind === "country") return target.name || String(target.id ?? "");
1529
1614
  if (kind === "cluster") return `${target.count} in this area`;
1615
+ if (kind === "hex-bin") return `${target.markerCount} markers, value ${target.value}`;
1530
1616
  const name = target.city || target.name || target.label;
1531
1617
  const count = target.count != null ? `: ${target.count}` : "";
1532
1618
  return name ? `${name}${count}` : `${target.lat.toFixed(2)}, ${target.lon.toFixed(2)}${count}`;
@@ -1578,6 +1664,11 @@ class GeoGlobe {
1578
1664
  this._licenseHits = [];
1579
1665
  this._licenseDismissed = false;
1580
1666
  this._licenseHovered = false;
1667
+ this._fx = [];
1668
+ this._clock = null;
1669
+ this._t0 = now();
1670
+ this._spinBase = this.lon;
1671
+ this.pointer = { x: 0, y: 0, lon: 0, lat: 0, over: false };
1581
1672
 
1582
1673
  this._applyWorld();
1583
1674
  this._applyMarkers(this.o.markers);
@@ -1589,12 +1680,138 @@ class GeoGlobe {
1589
1680
  this.resize();
1590
1681
  if (this.o.focus) this.focusOn(this.o.focus, { instant: true });
1591
1682
  if (this.o.showViewer) this._initViewer();
1683
+ for (const fx of this.o.effects || []) this.use(fx);
1592
1684
  this._loop = this._loop.bind(this);
1593
1685
  this._raf = requestAnimationFrame(this._loop);
1594
1686
  }
1595
1687
 
1596
1688
  /* ------------------------------ public API ------------------------------ */
1597
1689
 
1690
+ /* --------------------------------- effects ------------------------------- */
1691
+
1692
+ /** Installs an effect. Returns `this`, so calls chain. */
1693
+ use(effect) {
1694
+ if (!effect || typeof effect.frame !== "function") return this;
1695
+ this._fx.push({ fx: effect, state: effect.setup?.(this) ?? {} });
1696
+ this._fx.sort((a, b) => (a.fx.z ?? 0) - (b.fx.z ?? 0));
1697
+ return this.invalidate();
1698
+ }
1699
+
1700
+ /** Removes an effect by reference or by name. */
1701
+ remove(effect) {
1702
+ const i = this._fx.findIndex((e) => e.fx === effect || e.fx.name === effect);
1703
+ if (i < 0) return this;
1704
+ const [entry] = this._fx.splice(i, 1);
1705
+ entry.fx.dispose?.(entry.state, this);
1706
+ return this.invalidate();
1707
+ }
1708
+
1709
+ clearEffects() {
1710
+ while (this._fx.length) this.remove(this._fx[0].fx);
1711
+ return this;
1712
+ }
1713
+
1714
+ get effects() {
1715
+ return this._fx.map((e) => e.fx);
1716
+ }
1717
+
1718
+ /**
1719
+ * Pins the effect clock to `ms`. Rendering becomes reproducible, which is
1720
+ * what frame-exact export and seamless loops need. `play()` releases it.
1721
+ */
1722
+ seek(ms) {
1723
+ // Capture where auto-rotation started from, so longitude can be derived
1724
+ // from the clock instead of accumulated per frame. Only on the way in:
1725
+ // seeking again while already seeked must not shift the origin.
1726
+ if (this._clock == null) this._spinBase = wrapLon(this.lon - this._spinRate() * this._fxTime());
1727
+ this._clock = Math.max(0, ms);
1728
+ if (this._autoSpin()) this.lon = wrapLon(this._spinBase + this._spinRate() * this._clock);
1729
+ return this.invalidate();
1730
+ }
1731
+
1732
+ play() {
1733
+ // Resume the timeline where it was paused rather than snapping back to 0.
1734
+ if (this._clock != null) this._t0 = now() - this._clock;
1735
+ this._clock = null;
1736
+ return this.invalidate();
1737
+ }
1738
+
1739
+ /** Draws the single frame that belongs at `ms` on the effect clock. */
1740
+ renderFrame(ms) {
1741
+ return this.seek(ms).render();
1742
+ }
1743
+
1744
+ _fxTime() {
1745
+ return this._clock != null ? this._clock : now() - this._t0;
1746
+ }
1747
+
1748
+ /**
1749
+ * Wall clock for looping scene animation. Pinned while seeking, so arcs,
1750
+ * orbits and pulses land on the same phase every time a frame is redrawn.
1751
+ */
1752
+ _animTime() {
1753
+ return this._clock != null ? this._clock : Date.now();
1754
+ }
1755
+
1756
+ /** Degrees of auto-rotation per millisecond. */
1757
+ _spinRate() {
1758
+ return ((this.o.rotateSpeed || 0) * (this.o.fps || 30)) / 1000;
1759
+ }
1760
+
1761
+ _runPasses(stage) {
1762
+ if (!this._fx.length) return;
1763
+ const ms = this._fxTime();
1764
+ const { ctx } = this;
1765
+ for (const entry of this._fx) {
1766
+ if ((entry.fx.stage || "above") !== stage) continue;
1767
+ ctx.save();
1768
+ try {
1769
+ entry.fx.frame(ctx, this, phaseOf(entry.fx, ms), entry.state);
1770
+ } catch (err) {
1771
+ // One bad effect must not take the render loop down with it.
1772
+ if (!entry.failed) {
1773
+ entry.failed = true;
1774
+ console.error(`canvas-globe: effect "${entry.fx.name || "anonymous"}" failed`, err);
1775
+ }
1776
+ }
1777
+ ctx.restore();
1778
+ }
1779
+ }
1780
+
1781
+ /** Land sample points as `[lon, lat]` pairs — what the dot matrix draws. */
1782
+ landPoints(spacing) {
1783
+ const prev = this.o.dotSpacing;
1784
+ if (spacing != null) this.o.dotSpacing = spacing;
1785
+ const flat = this._dotPoints();
1786
+ this.o.dotSpacing = prev;
1787
+ const out = [];
1788
+ for (let i = 0; i < flat.length; i += 2) out.push([flat[i], flat[i + 1]]);
1789
+ return out;
1790
+ }
1791
+
1792
+ /** Traces a country outline into a path using the current projection. */
1793
+ tracePath(shape, ctx = this.ctx, transform) {
1794
+ const geom = shape?.geometry || shape;
1795
+ if (!geom?.coordinates) return this;
1796
+ const polys = geom.type === "Polygon" ? [geom.coordinates] : geom.coordinates;
1797
+ for (const poly of polys) {
1798
+ for (const ring of poly) {
1799
+ let open = false;
1800
+ for (const c of ring) {
1801
+ const src = transform ? transform(c) : c;
1802
+ const p = this.project(src[0], src[1]);
1803
+ if (!p) {
1804
+ open = false;
1805
+ continue;
1806
+ }
1807
+ open ? ctx.lineTo(p.x, p.y) : (ctx.moveTo(p.x, p.y), (open = true));
1808
+ }
1809
+ if (open) ctx.closePath();
1810
+ }
1811
+ }
1812
+ return this;
1813
+ }
1814
+
1598
1815
  setMarkers(markers = []) {
1599
1816
  this._applyMarkers(markers);
1600
1817
  this._focus = -1;
@@ -2151,9 +2368,12 @@ class GeoGlobe {
2151
2368
  const { w, h } = this._size();
2152
2369
  ctx.setTransform(this._dpr, 0, 0, this._dpr, 0, 0);
2153
2370
  ctx.clearRect(0, 0, w, h);
2371
+ this._runPasses("beneath");
2154
2372
  this.hits = this.o.mode === "map" ? this._paintMap(w, h) : this._paintGlobe(w, h);
2155
2373
  this._dirty = false;
2374
+ this._runPasses("above");
2156
2375
  this.o.onRender?.(this);
2376
+ this._runPasses("post");
2157
2377
  this._paintLicenseNotice(w, h);
2158
2378
  return this;
2159
2379
  }
@@ -2171,6 +2391,7 @@ class GeoGlobe {
2171
2391
  destroy() {
2172
2392
  this._destroyed = true;
2173
2393
  cancelAnimationFrame(this._raf);
2394
+ this.clearEffects();
2174
2395
  this.stopTour();
2175
2396
  this.stopStory();
2176
2397
  this.stopTimeline();
@@ -2625,6 +2846,15 @@ class GeoGlobe {
2625
2846
  this._onMove = (e) => {
2626
2847
  const [x, y] = this._local(e);
2627
2848
  this._pointer = { x: e.clientX, y: e.clientY };
2849
+ this.pointer.x = x;
2850
+ this.pointer.y = y;
2851
+ this.pointer.over = true;
2852
+ const at = this.unproject(x, y);
2853
+ if (at) {
2854
+ this.pointer.lon = at[0];
2855
+ this.pointer.lat = at[1];
2856
+ }
2857
+ if (this._fx.length) this._dirty = true;
2628
2858
  const licenseHovered = this._licenseHitAt(x, y);
2629
2859
  if (licenseHovered !== this._licenseHovered) {
2630
2860
  this._licenseHovered = licenseHovered;
@@ -2678,7 +2908,7 @@ class GeoGlobe {
2678
2908
  this._cursor();
2679
2909
  this._dirty = true;
2680
2910
  const target = marker || this._hoveredCountry;
2681
- this._showTip(marker ? (marker.cluster ? "cluster" : "marker") : this._hoveredCountry ? "country" : null, target);
2911
+ this._showTip(marker ? (marker.hexBin ? "hex-bin" : marker.cluster ? "cluster" : "marker") : this._hoveredCountry ? "country" : null, target);
2682
2912
  } else if (this._tipVisible) {
2683
2913
  this._placeTip();
2684
2914
  }
@@ -2695,6 +2925,7 @@ class GeoGlobe {
2695
2925
  this._onLeave = (e) => {
2696
2926
  this._onUp(e);
2697
2927
  this._pointer = null;
2928
+ this.pointer.over = false;
2698
2929
  this._licenseHovered = false;
2699
2930
  if (this._hovered || this._hoveredCountry) {
2700
2931
  if (this._hovered) this.o.onHover?.(null, null);
@@ -2875,13 +3106,20 @@ class GeoGlobe {
2875
3106
 
2876
3107
  /* --------------------------------- loop --------------------------------- */
2877
3108
 
2878
- _spinning() {
3109
+ /** Auto-rotation is configured and permitted, regardless of the clock. */
3110
+ _autoSpin() {
2879
3111
  return !!this.o.autoRotate && !this._reducedMotion() && !this._drag && !this._vel && this.o.mode === "globe";
2880
3112
  }
2881
3113
 
3114
+ /** Auto-rotation should advance on its own this frame. */
3115
+ _spinning() {
3116
+ return this._autoSpin() && this._clock == null;
3117
+ }
3118
+
2882
3119
  /** True while something on screen still needs to move. */
2883
3120
  _animating() {
2884
3121
  if (this._target || this._drag || this._vel) return true;
3122
+ if (this._fx.length && this._clock == null) return true;
2885
3123
  if (this._spinning()) return true;
2886
3124
  if (this._reducedMotion()) return false;
2887
3125
  if (this._hasLive || this._pings.length) return true;
@@ -3155,7 +3393,7 @@ class GeoGlobe {
3155
3393
  const list = this._orbitList();
3156
3394
  if (!list.length) return;
3157
3395
  const { ctx } = this;
3158
- const spin = this._reducedMotion() ? 0 : (Date.now() / 1000) % 3600;
3396
+ const spin = this._reducedMotion() ? 0 : (this._animTime() / 1000) % 3600;
3159
3397
  ctx.lineCap = "round";
3160
3398
  for (const orbit of list) {
3161
3399
  const inc = (orbit.inclination ?? 30) * D2R;
@@ -3709,7 +3947,7 @@ class GeoGlobe {
3709
3947
  const arcs = this.o.arcs;
3710
3948
  if (!arcs || !arcs.length) return;
3711
3949
  const { ctx } = this;
3712
- const now = Date.now();
3950
+ const now = this._animTime();
3713
3951
  const still = this._reducedMotion();
3714
3952
  ctx.lineCap = "round";
3715
3953
  ctx.lineJoin = "round";
@@ -3858,6 +4096,87 @@ class GeoGlobe {
3858
4096
  ctx.restore();
3859
4097
  }
3860
4098
 
4099
+ /** Aggregate projected markers into an interactive hexagonal density layer. */
4100
+ _paintHexBins(pts, t, cx, cy, globeRadius, w, h) {
4101
+ const o = this.o.hexBins === true ? {} : this.o.hexBins;
4102
+ const { ctx } = this;
4103
+ const radius = Math.max(4, o.radius ?? 18);
4104
+ const padding = clamp(o.padding ?? 1.5, 0, radius * 0.45);
4105
+ const drawRadius = radius - padding;
4106
+ const minValue = Math.max(0, o.minValue ?? 1);
4107
+ const metric = o.value === "count" ? "count" : "value";
4108
+ const bins = hexBinPoints(pts, radius).filter((bin) => {
4109
+ if (bin[metric] < minValue) return false;
4110
+ if (this.o.mode === "globe") return Math.hypot(bin.x - cx, bin.y - cy) <= globeRadius + drawRadius;
4111
+ return bin.x >= -drawRadius && bin.x <= w + drawRadius && bin.y >= -drawRadius && bin.y <= h + drawRadius;
4112
+ });
4113
+ const max = Math.max(1, ...bins.map((bin) => bin[metric]));
4114
+ const range = Array.isArray(o.colorRange) && o.colorRange.length > 1
4115
+ ? o.colorRange
4116
+ : [t.ocean[0], o.color || t.marker];
4117
+ const domain = range.map((_, index) => max * index / (range.length - 1));
4118
+ const scale = colorScale(domain, range);
4119
+ const hits = [];
4120
+
4121
+ ctx.save();
4122
+ if (this.o.mode === "globe") {
4123
+ ctx.beginPath();
4124
+ ctx.arc(cx, cy, globeRadius, 0, TAU);
4125
+ ctx.clip();
4126
+ } else {
4127
+ ctx.beginPath();
4128
+ ctx.rect(0, 0, w, h);
4129
+ ctx.clip();
4130
+ }
4131
+
4132
+ ctx.lineJoin = "round";
4133
+ for (const bin of bins) {
4134
+ const value = bin[metric];
4135
+ ctx.beginPath();
4136
+ for (let i = 0; i < 6; i++) {
4137
+ const angle = (60 * i - 30) * D2R;
4138
+ const x = bin.x + drawRadius * Math.cos(angle);
4139
+ const y = bin.y + drawRadius * Math.sin(angle);
4140
+ i ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
4141
+ }
4142
+ ctx.closePath();
4143
+ ctx.globalAlpha = clamp(o.opacity ?? 0.82, 0, 1);
4144
+ ctx.fillStyle = scale(value) || o.color || t.marker;
4145
+ ctx.fill();
4146
+ if ((o.strokeWidth ?? 0.8) > 0) {
4147
+ ctx.globalAlpha = 1;
4148
+ ctx.strokeStyle = o.stroke || withAlpha(t.label, 0.3);
4149
+ ctx.lineWidth = o.strokeWidth ?? 0.8;
4150
+ ctx.stroke();
4151
+ }
4152
+ if (o.showCount && drawRadius >= 10) {
4153
+ ctx.globalAlpha = 1;
4154
+ ctx.fillStyle = o.labelColor || t.label;
4155
+ ctx.font = `600 ${Math.max(9, Math.min(13, drawRadius * 0.7))}px Inter,system-ui,sans-serif`;
4156
+ ctx.textAlign = "center";
4157
+ ctx.textBaseline = "middle";
4158
+ ctx.fillText(String(value), bin.x, bin.y);
4159
+ }
4160
+ hits.push({
4161
+ marker: {
4162
+ hexBin: true,
4163
+ count: bin.count,
4164
+ value: bin.value,
4165
+ markerCount: bin.count,
4166
+ markers: bin.markers,
4167
+ lon: bin.lon,
4168
+ lat: bin.lat,
4169
+ },
4170
+ x: bin.x,
4171
+ y: bin.y,
4172
+ r: drawRadius,
4173
+ });
4174
+ }
4175
+ ctx.restore();
4176
+ this._lastHexBins = bins;
4177
+ return hits;
4178
+ }
4179
+
3861
4180
  /** Spike height for a marker, as a fraction of the globe radius. */
3862
4181
  _spikeLift(m) {
3863
4182
  if (!this.o.spikes) return 0;
@@ -4238,9 +4557,11 @@ class GeoGlobe {
4238
4557
  pts.push({ m, x: cx + x, y: cy + y, depth: 0.65 + c * 0.35 });
4239
4558
  }
4240
4559
  if (this.o.heatmap) this._paintHeatmap(pts, t);
4560
+ const binHits = this.o.hexBins ? this._paintHexBins(pts, t, cx, cy, r, w, h) : [];
4241
4561
  if (this.o.spikes) this._paintSpikes(t, cx, cy, r, w, h, null);
4242
4562
  this._paintViewerAccuracy(t, cx, cy, r, null);
4243
- const hits = this._paintMarkers(pts, t);
4563
+ const showMarkers = !this.o.hexBins || (this.o.hexBins !== true && this.o.hexBins.hideMarkers === false);
4564
+ const hits = [...binHits, ...(showMarkers ? this._paintMarkers(pts, t) : [])];
4244
4565
  if (this.o.labels) this._paintLabels(pts, t, cx, cy, r, w, h);
4245
4566
  this._paintAnnotations(t);
4246
4567
  this._paintPings(t);
@@ -4315,9 +4636,11 @@ class GeoGlobe {
4315
4636
  pts.push({ m, x, y, depth: 1 });
4316
4637
  }
4317
4638
  if (this.o.heatmap) this._paintHeatmap(pts, t);
4639
+ const binHits = this.o.hexBins ? this._paintHexBins(pts, t, 0, 0, 0, w, h) : [];
4318
4640
  if (this.o.spikes) this._paintSpikes(t, 0, 0, 0, w, h, fwd);
4319
4641
  this._paintViewerAccuracy(t, 0, 0, 0, fwd);
4320
- const hits = this._paintMarkers(pts, t);
4642
+ const showMarkers = !this.o.hexBins || (this.o.hexBins !== true && this.o.hexBins.hideMarkers === false);
4643
+ const hits = [...binHits, ...(showMarkers ? this._paintMarkers(pts, t) : [])];
4321
4644
  if (this.o.labels) this._paintLabels(pts, t, 0, 0, 0, w, h);
4322
4645
  this._paintAnnotations(t);
4323
4646
  this._paintPings(t);
@@ -4438,7 +4761,7 @@ class GeoGlobe {
4438
4761
  }
4439
4762
 
4440
4763
  if (m.live && !this._reducedMotion()) {
4441
- const pulse = (Date.now() % 2200) / 2200;
4764
+ const pulse = (this._animTime() % 2200) / 2200;
4442
4765
  ctx.globalAlpha = 1 - pulse;
4443
4766
  ctx.strokeStyle = t.live;
4444
4767
  ctx.lineWidth = 1.4 * scale;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvas-globe",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "Build interactive 3D globes and flat world maps in JavaScript, React, Vue, Angular, or Svelte with Canvas 2D and no WebGL.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": {
@@ -66,6 +66,36 @@
66
66
  "import": "./src/react.js",
67
67
  "default": "./src/react.js"
68
68
  },
69
+ "./fx": {
70
+ "types": "./types/fx.d.ts",
71
+ "import": "./src/fx/index.js",
72
+ "default": "./src/fx/index.js"
73
+ },
74
+ "./fx/interaction": {
75
+ "types": "./types/fx-interaction.d.ts",
76
+ "import": "./src/fx/effects/interaction.js",
77
+ "default": "./src/fx/effects/interaction.js"
78
+ },
79
+ "./recipes": {
80
+ "types": "./types/recipes.d.ts",
81
+ "import": "./src/recipes.js",
82
+ "default": "./src/recipes.js"
83
+ },
84
+ "./charts": {
85
+ "types": "./types/charts.d.ts",
86
+ "import": "./src/charts.js",
87
+ "default": "./src/charts.js"
88
+ },
89
+ "./controls": {
90
+ "types": "./types/controls.d.ts",
91
+ "import": "./src/controls.js",
92
+ "default": "./src/controls.js"
93
+ },
94
+ "./places": {
95
+ "types": "./types/places.d.ts",
96
+ "import": "./src/places.js",
97
+ "default": "./src/places.js"
98
+ },
69
99
  "./data/world": {
70
100
  "types": "./types/data/world.d.ts",
71
101
  "import": "./src/data/world.js",
@@ -95,7 +125,9 @@
95
125
  "scripts": {
96
126
  "start": "node scripts/serve.mjs",
97
127
  "build": "node scripts/build.mjs",
98
- "data": "node scripts/generate-data.mjs && node scripts/generate-timezones.mjs",
128
+ "data": "node scripts/generate-data.mjs && node scripts/generate-timezones.mjs && node scripts/generate-places.mjs",
129
+ "places": "node scripts/generate-places.mjs",
130
+ "audit:docs": "node scripts/audit-docs.mjs",
99
131
  "example": "node scripts/serve.mjs",
100
132
  "storybook": "storybook dev -p 6006",
101
133
  "build-storybook": "storybook build",