canvas-globe 0.1.6 → 1.0.0-beta.2

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.
@@ -1,4 +1,4 @@
1
- /*! canvas-globe | Copyright (C) 2026 Harsh Jhunjhunuwala | GPL-3.0-only OR commercial | https://github.com/Shree-hari/canvas-globe */
1
+ /*! canvas-globe | Copyright (C) 2026 Harsh Jhunjhunuwala | Proprietary commercial software | https://github.com/Shree-hari/canvas-globe */
2
2
  (function (root, factory) {
3
3
  if (typeof exports === "object" && typeof module !== "undefined") module.exports = factory();
4
4
  else if (typeof define === "function" && define.amd) define(factory);
@@ -1313,39 +1313,115 @@ function drawFitted(ctx, media, box) {
1313
1313
  return true;
1314
1314
  }
1315
1315
 
1316
- /** License-key configuration helpers. */
1316
+ // Keep in sync with package.json. Release checks enforce this value.
1317
+ const CANVAS_GLOBE_VERSION = "1.0.0-beta.2";
1318
+
1319
+ /** Local license-key checks and production-use presentation helpers. */
1320
+
1317
1321
  const DEFAULT_LICENSE_KEY = "0000-0000-000-0000";
1322
+ const LICENSE_KEY_PREFIX = "GLO";
1323
+ const LICENSE_PAGE_URL =
1324
+ "https://canvasglobe.swiftools.com/pricing?utm_source=canvas-globe&utm_medium=runtime-notice";
1325
+
1326
+ // This remains false while the latest published line is GPLv3. The commercial
1327
+ // release checklist requires an intentional switch after the agreement and
1328
+ // website copy have been approved and deployed.
1329
+ const COMMERCIAL_LICENSE_MODE = true;
1330
+
1331
+ const PRIVATE_HOST_PATTERNS = [
1332
+ /^localhost$/,
1333
+ /\.localhost$/,
1334
+ /\.local$/,
1335
+ /\.test$/,
1336
+ /^127(?:\.\d{1,3}){3}$/,
1337
+ /^10(?:\.\d{1,3}){3}$/,
1338
+ /^192\.168(?:\.\d{1,3}){2}$/,
1339
+ /^172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}$/,
1340
+ /^::1$/,
1341
+ ];
1342
+
1343
+ const normalizeLicenseKey = (value) =>
1344
+ typeof value === "string" ? value.trim() : "";
1345
+
1346
+ /** Returns development, production or unknown for a browser location. */
1347
+ function inspectRuntime(locationValue = globalThis.location) {
1348
+ if (!locationValue) return { kind: "unknown", hostname: "", public: false };
1349
+ const protocol = String(locationValue.protocol || "").toLowerCase();
1350
+ const hostname = String(locationValue.hostname || "").toLowerCase().replace(/^\[|\]$/g, "");
1351
+ if (!hostname || (protocol && protocol !== "http:" && protocol !== "https:")) {
1352
+ return { kind: "unknown", hostname, public: false };
1353
+ }
1354
+ const local = PRIVATE_HOST_PATTERNS.some((pattern) => pattern.test(hostname));
1355
+ return { kind: local ? "development" : "production", hostname, public: !local };
1356
+ }
1318
1357
 
1319
- /** Returns the configured license-key status. */
1320
- function inspectLicenseKey(value) {
1321
- const key = typeof value === "string" ? value : "";
1358
+ /** Checks a license key locally. Commercial keys begin with GLO. */
1359
+ function inspectLicenseKey(value, mode = COMMERCIAL_LICENSE_MODE) {
1360
+ const key = normalizeLicenseKey(value);
1322
1361
  if (!key) return { valid: false, kind: "missing", key: "" };
1323
- if (key === DEFAULT_LICENSE_KEY) {
1324
- return { valid: false, kind: "placeholder", key };
1362
+ if (key === DEFAULT_LICENSE_KEY) return { valid: false, kind: "placeholder", key };
1363
+ if (!mode) return { valid: true, kind: "provided", key };
1364
+ if (key.startsWith(LICENSE_KEY_PREFIX) && key.length > LICENSE_KEY_PREFIX.length) {
1365
+ return { valid: true, kind: "licensed", key };
1325
1366
  }
1326
- return { valid: true, kind: "provided", key };
1367
+ return { valid: false, kind: "invalid", key: "" };
1368
+ }
1369
+
1370
+ /** Resolves the same local result for callers that already use this helper. */
1371
+ async function verifyLicenseKey(value, mode = COMMERCIAL_LICENSE_MODE) {
1372
+ return inspectLicenseKey(value, mode);
1373
+ }
1374
+
1375
+ /** Returns whether a valid local license key is available. */
1376
+ function hasLicenseKey(value, mode = COMMERCIAL_LICENSE_MODE) {
1377
+ return inspectLicenseKey(value, mode).valid;
1327
1378
  }
1328
1379
 
1329
- /** Returns whether a configured license key is available. */
1330
- function hasLicenseKey(value) {
1331
- return inspectLicenseKey(value).valid;
1380
+ /**
1381
+ * Returns the notice that the commercial release displays for public use.
1382
+ * The mode argument exists for release tooling and tests, not as a public
1383
+ * option that applications can use to suppress licensing UI.
1384
+ */
1385
+ function getLicensePresentation(
1386
+ value,
1387
+ locationValue = globalThis.location,
1388
+ mode = COMMERCIAL_LICENSE_MODE,
1389
+ ) {
1390
+ const status = inspectLicenseKey(value, mode);
1391
+ const runtime = inspectRuntime(locationValue);
1392
+ if (!mode || status.valid || !runtime.public) {
1393
+ return { status, runtime, notice: null };
1394
+ }
1395
+ return {
1396
+ status,
1397
+ runtime,
1398
+ notice: {
1399
+ text: "CanvasGlobe: Purchase a license",
1400
+ ariaLabel: "CanvasGlobe requires a license for production use. Open licensing options.",
1401
+ url: LICENSE_PAGE_URL,
1402
+ },
1403
+ };
1332
1404
  }
1333
1405
 
1334
- /** Reports missing or placeholder keys in the browser console. */
1335
- function reportLicenseStatus(value) {
1336
- const status = inspectLicenseKey(value);
1337
- // CanvasGlobe can be constructed in non-browser test and rendering
1338
- // environments. Console messaging applies when it is used in a browser.
1406
+ /** Reports local license-key problems in the browser console. */
1407
+ function reportLicenseStatus(value, mode = COMMERCIAL_LICENSE_MODE) {
1408
+ const presentation = getLicensePresentation(value, globalThis.location, mode);
1409
+ const { status } = presentation;
1339
1410
  if (typeof location === "undefined") return status;
1340
1411
  if (status.kind === "missing") {
1341
1412
  console.error(
1342
- "canvas-globe: please provide a valid license key. For help, email globe@swiftools.com",
1413
+ mode
1414
+ ? `canvas-globe: a license is required for production use. ${LICENSE_PAGE_URL}`
1415
+ : "canvas-globe: please provide a valid license key. For help, email globe@swiftools.com",
1343
1416
  );
1344
1417
  } else if (status.kind === "placeholder") {
1345
1418
  console.warn(
1346
- `canvas-globe: ${DEFAULT_LICENSE_KEY} license key is not valid for production use. ` +
1347
- "For help, email globe@swiftools.com",
1419
+ mode
1420
+ ? `canvas-globe: the placeholder is not a production license. ${LICENSE_PAGE_URL}`
1421
+ : `canvas-globe: ${DEFAULT_LICENSE_KEY} license key is not valid for production use. For help, email globe@swiftools.com`,
1348
1422
  );
1423
+ } else if (mode && status.kind === "invalid") {
1424
+ console.error(`canvas-globe: enter the GLO license key supplied after purchase. ${LICENSE_PAGE_URL}`);
1349
1425
  }
1350
1426
  return status;
1351
1427
  }
@@ -1366,7 +1442,7 @@ function reportLicenseStatus(value) {
1366
1442
 
1367
1443
 
1368
1444
  const DEFAULTS = {
1369
- licenseKey: DEFAULT_LICENSE_KEY,
1445
+ licenseKey: null,
1370
1446
  mode: "globe",
1371
1447
  projection: "equirectangular",
1372
1448
  theme: "atlas",
@@ -1486,6 +1562,8 @@ class GeoGlobe {
1486
1562
  this._media = new Map();
1487
1563
  this._markerMedia = new Map();
1488
1564
  this._counterShown = null;
1565
+ this._licenseHit = null;
1566
+ this._licenseHovered = false;
1489
1567
 
1490
1568
  this._applyWorld();
1491
1569
  this._applyMarkers(this.o.markers);
@@ -1517,7 +1595,9 @@ class GeoGlobe {
1517
1595
  setOptions(patch = {}) {
1518
1596
  patch = this._expandLooks(patch);
1519
1597
  Object.assign(this.o, patch);
1520
- if ("licenseKey" in patch) reportLicenseStatus(patch.licenseKey);
1598
+ if ("licenseKey" in patch) {
1599
+ reportLicenseStatus(patch.licenseKey);
1600
+ }
1521
1601
  if ("world" in patch) this._applyWorld();
1522
1602
  if ("markers" in patch) this._applyMarkers(patch.markers || []);
1523
1603
  if ("zoom" in patch) this._zoom = clamp(patch.zoom, this.o.minZoom, this.o.maxZoom);
@@ -2058,6 +2138,7 @@ class GeoGlobe {
2058
2138
  this.hits = this.o.mode === "map" ? this._paintMap(w, h) : this._paintGlobe(w, h);
2059
2139
  this._dirty = false;
2060
2140
  this.o.onRender?.(this);
2141
+ this._paintLicenseNotice(w, h);
2061
2142
  return this;
2062
2143
  }
2063
2144
 
@@ -2090,6 +2171,7 @@ class GeoGlobe {
2090
2171
  this._markerMedia.clear();
2091
2172
  this._tip = null;
2092
2173
  this._live = null;
2174
+ this._licenseHit = null;
2093
2175
  return this;
2094
2176
  }
2095
2177
 
@@ -2469,7 +2551,7 @@ class GeoGlobe {
2469
2551
  c.style.cursor = "default";
2470
2552
  return;
2471
2553
  }
2472
- c.style.cursor = this._hovered || this._hoveredCountry ? "pointer" : this._drag ? "grabbing" : "grab";
2554
+ c.style.cursor = this._licenseHovered || this._hovered || this._hoveredCountry ? "pointer" : this._drag ? "grabbing" : "grab";
2473
2555
  }
2474
2556
 
2475
2557
  _local(e) {
@@ -2485,6 +2567,11 @@ class GeoGlobe {
2485
2567
  return null;
2486
2568
  }
2487
2569
 
2570
+ _licenseHitAt(x, y) {
2571
+ const hit = this._licenseHit;
2572
+ return Boolean(hit && x >= hit.x && x <= hit.x + hit.w && y >= hit.y && y <= hit.y + hit.h);
2573
+ }
2574
+
2488
2575
  _bind() {
2489
2576
  const c = this.canvas;
2490
2577
 
@@ -2508,6 +2595,11 @@ class GeoGlobe {
2508
2595
  this._onMove = (e) => {
2509
2596
  const [x, y] = this._local(e);
2510
2597
  this._pointer = { x: e.clientX, y: e.clientY };
2598
+ const licenseHovered = this._licenseHitAt(x, y);
2599
+ if (licenseHovered !== this._licenseHovered) {
2600
+ this._licenseHovered = licenseHovered;
2601
+ this._cursor();
2602
+ }
2511
2603
  if (this._pointers.has(e.pointerId)) this._pointers.set(e.pointerId, [x, y]);
2512
2604
 
2513
2605
  if (this._pinch && this._pointers.size === 2) {
@@ -2573,6 +2665,7 @@ class GeoGlobe {
2573
2665
  this._onLeave = (e) => {
2574
2666
  this._onUp(e);
2575
2667
  this._pointer = null;
2668
+ this._licenseHovered = false;
2576
2669
  if (this._hovered || this._hoveredCountry) {
2577
2670
  if (this._hovered) this.o.onHover?.(null, null);
2578
2671
  if (this._hoveredCountry) this.o.onCountryHover?.(null, null);
@@ -2587,6 +2680,11 @@ class GeoGlobe {
2587
2680
  this._onClick = (e) => {
2588
2681
  if (this._dragMoved) return;
2589
2682
  const [x, y] = this._local(e);
2683
+ if (this._licenseHitAt(x, y)) {
2684
+ const opened = globalThis.open?.(this._licenseHit.url, "_blank", "noopener,noreferrer");
2685
+ if (opened) opened.opener = null;
2686
+ return;
2687
+ }
2590
2688
  const hit = this._hitAt(x, y);
2591
2689
  if (hit) {
2592
2690
  this.o.onClick?.(hit.marker, { x: hit.x, y: hit.y });
@@ -3339,6 +3437,49 @@ class GeoGlobe {
3339
3437
  ctx.restore();
3340
3438
  }
3341
3439
 
3440
+ /** Licensing notice for public production use of the commercial edition. */
3441
+ _paintLicenseNotice(w, h) {
3442
+ const presentation = getLicensePresentation(this.o.licenseKey);
3443
+ const notice = presentation.notice;
3444
+ this._licenseHit = null;
3445
+ this.canvas.removeAttribute?.("data-canvas-globe-license-notice");
3446
+ if (!notice) return;
3447
+
3448
+ const { ctx } = this;
3449
+ const fontSize = Math.max(10, Math.min(13, Math.round(Math.min(w, h) * 0.03)));
3450
+ const horizontal = 10;
3451
+ const vertical = 7;
3452
+ const margin = Math.max(8, Math.round(Math.min(w, h) * 0.025));
3453
+
3454
+ ctx.save();
3455
+ ctx.font = `600 ${fontSize}px Inter,system-ui,sans-serif`;
3456
+ const textWidth = ctx.measureText(notice.text).width;
3457
+ const boxWidth = Math.min(w - margin * 2, textWidth + horizontal * 2);
3458
+ const boxHeight = fontSize + vertical * 2;
3459
+ const x = Math.max(margin, w - boxWidth - margin);
3460
+ const y = Math.max(margin, h - boxHeight - margin);
3461
+
3462
+ ctx.globalAlpha = 0.94;
3463
+ ctx.fillStyle = "rgba(9, 18, 35, 0.92)";
3464
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
3465
+ ctx.lineWidth = 1;
3466
+ ctx.beginPath();
3467
+ if (typeof ctx.roundRect === "function") ctx.roundRect(x, y, boxWidth, boxHeight, 6);
3468
+ else ctx.rect(x, y, boxWidth, boxHeight);
3469
+ ctx.fill();
3470
+ ctx.stroke();
3471
+
3472
+ ctx.globalAlpha = 1;
3473
+ ctx.fillStyle = "#ffffff";
3474
+ ctx.textAlign = "center";
3475
+ ctx.textBaseline = "middle";
3476
+ ctx.fillText(notice.text, x + boxWidth / 2, y + boxHeight / 2, boxWidth - horizontal * 2);
3477
+ ctx.restore();
3478
+
3479
+ this._licenseHit = { x, y, w: boxWidth, h: boxHeight, url: notice.url };
3480
+ this.canvas.setAttribute?.("data-canvas-globe-license-notice", "visible");
3481
+ }
3482
+
3342
3483
  _paintCountries(t, trace, textured) {
3343
3484
  if (this.o.landStyle === "dots" || this.o.landStyle === "none") return;
3344
3485
  const { ctx } = this;
@@ -4349,5 +4490,5 @@ function defineGeoGlobe(tag = "geo-globe") {
4349
4490
  defineGeoGlobe();
4350
4491
 
4351
4492
  const CanvasGlobe = GeoGlobe; const createCanvasGlobe = createGlobe;
4352
- return { GeoGlobe, CanvasGlobe, createGlobe, createCanvasGlobe, GeoGlobeElement, defineGeoGlobe, themes, presets, scenes, countryPalette, exportPresets, exportSize, fromCSV, fromRows, parseCSV, geocode, countryPoint, locateViewer, locateViewerPrecise, timeZoneLocation, countryLocation, placeLocation, recordCanvas, downloadBlob, canRecord, supportedRecordingType, SphereTexture, Media, mapAspect, colorScale, subsolarPoint, greatCircle, angularDistance, pointInGeometry, geometryBounds, projections, world, DEFAULT_LICENSE_KEY, inspectLicenseKey, hasLicenseKey, default: createGlobe };
4493
+ return { GeoGlobe, CanvasGlobe, createGlobe, createCanvasGlobe, GeoGlobeElement, defineGeoGlobe, themes, presets, scenes, countryPalette, exportPresets, exportSize, fromCSV, fromRows, parseCSV, geocode, countryPoint, locateViewer, locateViewerPrecise, timeZoneLocation, countryLocation, placeLocation, recordCanvas, downloadBlob, canRecord, supportedRecordingType, SphereTexture, Media, mapAspect, colorScale, subsolarPoint, greatCircle, angularDistance, pointInGeometry, geometryBounds, projections, world, DEFAULT_LICENSE_KEY, LICENSE_KEY_PREFIX, LICENSE_PAGE_URL, inspectRuntime, inspectLicenseKey, verifyLicenseKey, hasLicenseKey, default: createGlobe };
4353
4494
  });
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "canvas-globe",
3
- "version": "0.1.6",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "Build interactive 3D globes and flat world maps in JavaScript or React with Canvas 2D, without WebGL, map tiles, API keys, or runtime network calls.",
5
- "license": "GPL-3.0-only",
5
+ "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": {
7
7
  "name": "Harsh Jhunjhunuwala"
8
8
  },
@@ -75,7 +75,7 @@
75
75
  "README.md",
76
76
  "CHANGELOG.md",
77
77
  "COPYRIGHT",
78
- "LICENSE",
78
+ "LICENSE.md",
79
79
  "LICENSING.md",
80
80
  "THIRD_PARTY_NOTICES.md",
81
81
  "codemeta.json",
@@ -101,6 +101,8 @@
101
101
  "typecheck": "tsc --noEmit -p tsconfig.json",
102
102
  "typecheck:ecosystem": "tsc --noEmit -p tsconfig.ecosystem.json",
103
103
  "smoke:package": "node scripts/package-smoke.mjs",
104
+ "commercial:check": "node scripts/commercial-release-check.mjs",
105
+ "commercial:prepare": "node scripts/prepare-commercial-release.mjs",
104
106
  "release:check": "npm run test && npm run typecheck && npm run typecheck:ecosystem && npm run test:ecosystem && npm run build && node scripts/release-check.mjs && npm run smoke:package",
105
107
  "prepublishOnly": "npm run test && npm run typecheck && npm run build && node scripts/release-check.mjs"
106
108
  },
package/src/geo-globe.js CHANGED
@@ -11,11 +11,14 @@ import { SphereTexture } from "./texture.js";
11
11
  import { Media, drawFitted } from "./media.js";
12
12
  import { scenes, sceneKeys } from "./scenes.js";
13
13
  import { exportSize } from "./export.js";
14
- import { DEFAULT_LICENSE_KEY, reportLicenseStatus } from "./license.js";
14
+ import {
15
+ getLicensePresentation,
16
+ reportLicenseStatus,
17
+ } from "./license.js";
15
18
  import { D2R, R2D, TAU, clamp, wrapLon, resolveProjection, projectionBounds, ortho, orthoInverse, greatCircle, circleAround, distanceMeters, subsolarPoint, pointInGeometry, geometryBounds, normalizeShapes, withAlpha } from "./geo.js";
16
19
 
17
20
  const DEFAULTS = {
18
- licenseKey: DEFAULT_LICENSE_KEY,
21
+ licenseKey: null,
19
22
  mode: "globe",
20
23
  projection: "equirectangular",
21
24
  theme: "atlas",
@@ -135,6 +138,8 @@ export class GeoGlobe {
135
138
  this._media = new Map();
136
139
  this._markerMedia = new Map();
137
140
  this._counterShown = null;
141
+ this._licenseHit = null;
142
+ this._licenseHovered = false;
138
143
 
139
144
  this._applyWorld();
140
145
  this._applyMarkers(this.o.markers);
@@ -166,7 +171,9 @@ export class GeoGlobe {
166
171
  setOptions(patch = {}) {
167
172
  patch = this._expandLooks(patch);
168
173
  Object.assign(this.o, patch);
169
- if ("licenseKey" in patch) reportLicenseStatus(patch.licenseKey);
174
+ if ("licenseKey" in patch) {
175
+ reportLicenseStatus(patch.licenseKey);
176
+ }
170
177
  if ("world" in patch) this._applyWorld();
171
178
  if ("markers" in patch) this._applyMarkers(patch.markers || []);
172
179
  if ("zoom" in patch) this._zoom = clamp(patch.zoom, this.o.minZoom, this.o.maxZoom);
@@ -707,6 +714,7 @@ export class GeoGlobe {
707
714
  this.hits = this.o.mode === "map" ? this._paintMap(w, h) : this._paintGlobe(w, h);
708
715
  this._dirty = false;
709
716
  this.o.onRender?.(this);
717
+ this._paintLicenseNotice(w, h);
710
718
  return this;
711
719
  }
712
720
 
@@ -739,6 +747,7 @@ export class GeoGlobe {
739
747
  this._markerMedia.clear();
740
748
  this._tip = null;
741
749
  this._live = null;
750
+ this._licenseHit = null;
742
751
  return this;
743
752
  }
744
753
 
@@ -1118,7 +1127,7 @@ export class GeoGlobe {
1118
1127
  c.style.cursor = "default";
1119
1128
  return;
1120
1129
  }
1121
- c.style.cursor = this._hovered || this._hoveredCountry ? "pointer" : this._drag ? "grabbing" : "grab";
1130
+ c.style.cursor = this._licenseHovered || this._hovered || this._hoveredCountry ? "pointer" : this._drag ? "grabbing" : "grab";
1122
1131
  }
1123
1132
 
1124
1133
  _local(e) {
@@ -1134,6 +1143,11 @@ export class GeoGlobe {
1134
1143
  return null;
1135
1144
  }
1136
1145
 
1146
+ _licenseHitAt(x, y) {
1147
+ const hit = this._licenseHit;
1148
+ return Boolean(hit && x >= hit.x && x <= hit.x + hit.w && y >= hit.y && y <= hit.y + hit.h);
1149
+ }
1150
+
1137
1151
  _bind() {
1138
1152
  const c = this.canvas;
1139
1153
 
@@ -1157,6 +1171,11 @@ export class GeoGlobe {
1157
1171
  this._onMove = (e) => {
1158
1172
  const [x, y] = this._local(e);
1159
1173
  this._pointer = { x: e.clientX, y: e.clientY };
1174
+ const licenseHovered = this._licenseHitAt(x, y);
1175
+ if (licenseHovered !== this._licenseHovered) {
1176
+ this._licenseHovered = licenseHovered;
1177
+ this._cursor();
1178
+ }
1160
1179
  if (this._pointers.has(e.pointerId)) this._pointers.set(e.pointerId, [x, y]);
1161
1180
 
1162
1181
  if (this._pinch && this._pointers.size === 2) {
@@ -1222,6 +1241,7 @@ export class GeoGlobe {
1222
1241
  this._onLeave = (e) => {
1223
1242
  this._onUp(e);
1224
1243
  this._pointer = null;
1244
+ this._licenseHovered = false;
1225
1245
  if (this._hovered || this._hoveredCountry) {
1226
1246
  if (this._hovered) this.o.onHover?.(null, null);
1227
1247
  if (this._hoveredCountry) this.o.onCountryHover?.(null, null);
@@ -1236,6 +1256,11 @@ export class GeoGlobe {
1236
1256
  this._onClick = (e) => {
1237
1257
  if (this._dragMoved) return;
1238
1258
  const [x, y] = this._local(e);
1259
+ if (this._licenseHitAt(x, y)) {
1260
+ const opened = globalThis.open?.(this._licenseHit.url, "_blank", "noopener,noreferrer");
1261
+ if (opened) opened.opener = null;
1262
+ return;
1263
+ }
1239
1264
  const hit = this._hitAt(x, y);
1240
1265
  if (hit) {
1241
1266
  this.o.onClick?.(hit.marker, { x: hit.x, y: hit.y });
@@ -1988,6 +2013,49 @@ export class GeoGlobe {
1988
2013
  ctx.restore();
1989
2014
  }
1990
2015
 
2016
+ /** Licensing notice for public production use of the commercial edition. */
2017
+ _paintLicenseNotice(w, h) {
2018
+ const presentation = getLicensePresentation(this.o.licenseKey);
2019
+ const notice = presentation.notice;
2020
+ this._licenseHit = null;
2021
+ this.canvas.removeAttribute?.("data-canvas-globe-license-notice");
2022
+ if (!notice) return;
2023
+
2024
+ const { ctx } = this;
2025
+ const fontSize = Math.max(10, Math.min(13, Math.round(Math.min(w, h) * 0.03)));
2026
+ const horizontal = 10;
2027
+ const vertical = 7;
2028
+ const margin = Math.max(8, Math.round(Math.min(w, h) * 0.025));
2029
+
2030
+ ctx.save();
2031
+ ctx.font = `600 ${fontSize}px Inter,system-ui,sans-serif`;
2032
+ const textWidth = ctx.measureText(notice.text).width;
2033
+ const boxWidth = Math.min(w - margin * 2, textWidth + horizontal * 2);
2034
+ const boxHeight = fontSize + vertical * 2;
2035
+ const x = Math.max(margin, w - boxWidth - margin);
2036
+ const y = Math.max(margin, h - boxHeight - margin);
2037
+
2038
+ ctx.globalAlpha = 0.94;
2039
+ ctx.fillStyle = "rgba(9, 18, 35, 0.92)";
2040
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
2041
+ ctx.lineWidth = 1;
2042
+ ctx.beginPath();
2043
+ if (typeof ctx.roundRect === "function") ctx.roundRect(x, y, boxWidth, boxHeight, 6);
2044
+ else ctx.rect(x, y, boxWidth, boxHeight);
2045
+ ctx.fill();
2046
+ ctx.stroke();
2047
+
2048
+ ctx.globalAlpha = 1;
2049
+ ctx.fillStyle = "#ffffff";
2050
+ ctx.textAlign = "center";
2051
+ ctx.textBaseline = "middle";
2052
+ ctx.fillText(notice.text, x + boxWidth / 2, y + boxHeight / 2, boxWidth - horizontal * 2);
2053
+ ctx.restore();
2054
+
2055
+ this._licenseHit = { x, y, w: boxWidth, h: boxHeight, url: notice.url };
2056
+ this.canvas.setAttribute?.("data-canvas-globe-license-notice", "visible");
2057
+ }
2058
+
1991
2059
  _paintCountries(t, trace, textured) {
1992
2060
  if (this.o.landStyle === "dots" || this.o.landStyle === "none") return;
1993
2061
  const { ctx } = this;
package/src/index.js CHANGED
@@ -16,6 +16,14 @@ export { locateViewer, locateViewerPrecise, timeZoneLocation, countryLocation, p
16
16
  export { recordCanvas, downloadBlob, canRecord, supportedRecordingType } from "./recorder.js";
17
17
  export { SphereTexture } from "./texture.js";
18
18
  export { Media, drawFitted } from "./media.js";
19
- export { DEFAULT_LICENSE_KEY, inspectLicenseKey, hasLicenseKey } from "./license.js";
19
+ export {
20
+ DEFAULT_LICENSE_KEY,
21
+ LICENSE_KEY_PREFIX,
22
+ LICENSE_PAGE_URL,
23
+ inspectRuntime,
24
+ inspectLicenseKey,
25
+ verifyLicenseKey,
26
+ hasLicenseKey,
27
+ } from "./license.js";
20
28
  export { mapAspect, colorScale, subsolarPoint, greatCircle, angularDistance, pointInGeometry, geometryBounds, projections } from "./geo.js";
21
29
  export { default } from "./geo-globe.js";
package/src/license.js CHANGED
@@ -1,36 +1,109 @@
1
- /** License-key configuration helpers. */
1
+ /** Local license-key checks and production-use presentation helpers. */
2
+
2
3
  export const DEFAULT_LICENSE_KEY = "0000-0000-000-0000";
4
+ export const LICENSE_KEY_PREFIX = "GLO";
5
+ export const LICENSE_PAGE_URL =
6
+ "https://canvasglobe.swiftools.com/pricing?utm_source=canvas-globe&utm_medium=runtime-notice";
7
+
8
+ // This remains false while the latest published line is GPLv3. The commercial
9
+ // release checklist requires an intentional switch after the agreement and
10
+ // website copy have been approved and deployed.
11
+ export const COMMERCIAL_LICENSE_MODE = true;
12
+
13
+ const PRIVATE_HOST_PATTERNS = [
14
+ /^localhost$/,
15
+ /\.localhost$/,
16
+ /\.local$/,
17
+ /\.test$/,
18
+ /^127(?:\.\d{1,3}){3}$/,
19
+ /^10(?:\.\d{1,3}){3}$/,
20
+ /^192\.168(?:\.\d{1,3}){2}$/,
21
+ /^172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}$/,
22
+ /^::1$/,
23
+ ];
24
+
25
+ const normalizeLicenseKey = (value) =>
26
+ typeof value === "string" ? value.trim() : "";
27
+
28
+ /** Returns development, production or unknown for a browser location. */
29
+ export function inspectRuntime(locationValue = globalThis.location) {
30
+ if (!locationValue) return { kind: "unknown", hostname: "", public: false };
31
+ const protocol = String(locationValue.protocol || "").toLowerCase();
32
+ const hostname = String(locationValue.hostname || "").toLowerCase().replace(/^\[|\]$/g, "");
33
+ if (!hostname || (protocol && protocol !== "http:" && protocol !== "https:")) {
34
+ return { kind: "unknown", hostname, public: false };
35
+ }
36
+ const local = PRIVATE_HOST_PATTERNS.some((pattern) => pattern.test(hostname));
37
+ return { kind: local ? "development" : "production", hostname, public: !local };
38
+ }
3
39
 
4
- /** Returns the configured license-key status. */
5
- export function inspectLicenseKey(value) {
6
- const key = typeof value === "string" ? value : "";
40
+ /** Checks a license key locally. Commercial keys begin with GLO. */
41
+ export function inspectLicenseKey(value, mode = COMMERCIAL_LICENSE_MODE) {
42
+ const key = normalizeLicenseKey(value);
7
43
  if (!key) return { valid: false, kind: "missing", key: "" };
8
- if (key === DEFAULT_LICENSE_KEY) {
9
- return { valid: false, kind: "placeholder", key };
44
+ if (key === DEFAULT_LICENSE_KEY) return { valid: false, kind: "placeholder", key };
45
+ if (!mode) return { valid: true, kind: "provided", key };
46
+ if (key.startsWith(LICENSE_KEY_PREFIX) && key.length > LICENSE_KEY_PREFIX.length) {
47
+ return { valid: true, kind: "licensed", key };
10
48
  }
11
- return { valid: true, kind: "provided", key };
49
+ return { valid: false, kind: "invalid", key: "" };
50
+ }
51
+
52
+ /** Resolves the same local result for callers that already use this helper. */
53
+ export async function verifyLicenseKey(value, mode = COMMERCIAL_LICENSE_MODE) {
54
+ return inspectLicenseKey(value, mode);
12
55
  }
13
56
 
14
- /** Returns whether a configured license key is available. */
15
- export function hasLicenseKey(value) {
16
- return inspectLicenseKey(value).valid;
57
+ /** Returns whether a valid local license key is available. */
58
+ export function hasLicenseKey(value, mode = COMMERCIAL_LICENSE_MODE) {
59
+ return inspectLicenseKey(value, mode).valid;
60
+ }
61
+
62
+ /**
63
+ * Returns the notice that the commercial release displays for public use.
64
+ * The mode argument exists for release tooling and tests, not as a public
65
+ * option that applications can use to suppress licensing UI.
66
+ */
67
+ export function getLicensePresentation(
68
+ value,
69
+ locationValue = globalThis.location,
70
+ mode = COMMERCIAL_LICENSE_MODE,
71
+ ) {
72
+ const status = inspectLicenseKey(value, mode);
73
+ const runtime = inspectRuntime(locationValue);
74
+ if (!mode || status.valid || !runtime.public) {
75
+ return { status, runtime, notice: null };
76
+ }
77
+ return {
78
+ status,
79
+ runtime,
80
+ notice: {
81
+ text: "CanvasGlobe: Purchase a license",
82
+ ariaLabel: "CanvasGlobe requires a license for production use. Open licensing options.",
83
+ url: LICENSE_PAGE_URL,
84
+ },
85
+ };
17
86
  }
18
87
 
19
- /** Reports missing or placeholder keys in the browser console. */
20
- export function reportLicenseStatus(value) {
21
- const status = inspectLicenseKey(value);
22
- // CanvasGlobe can be constructed in non-browser test and rendering
23
- // environments. Console messaging applies when it is used in a browser.
88
+ /** Reports local license-key problems in the browser console. */
89
+ export function reportLicenseStatus(value, mode = COMMERCIAL_LICENSE_MODE) {
90
+ const presentation = getLicensePresentation(value, globalThis.location, mode);
91
+ const { status } = presentation;
24
92
  if (typeof location === "undefined") return status;
25
93
  if (status.kind === "missing") {
26
94
  console.error(
27
- "canvas-globe: please provide a valid license key. For help, email globe@swiftools.com",
95
+ mode
96
+ ? `canvas-globe: a license is required for production use. ${LICENSE_PAGE_URL}`
97
+ : "canvas-globe: please provide a valid license key. For help, email globe@swiftools.com",
28
98
  );
29
99
  } else if (status.kind === "placeholder") {
30
100
  console.warn(
31
- `canvas-globe: ${DEFAULT_LICENSE_KEY} license key is not valid for production use. ` +
32
- "For help, email globe@swiftools.com",
101
+ mode
102
+ ? `canvas-globe: the placeholder is not a production license. ${LICENSE_PAGE_URL}`
103
+ : `canvas-globe: ${DEFAULT_LICENSE_KEY} license key is not valid for production use. For help, email globe@swiftools.com`,
33
104
  );
105
+ } else if (mode && status.kind === "invalid") {
106
+ console.error(`canvas-globe: enter the GLO license key supplied after purchase. ${LICENSE_PAGE_URL}`);
34
107
  }
35
108
  return status;
36
109
  }
package/src/version.js ADDED
@@ -0,0 +1,2 @@
1
+ // Keep in sync with package.json. Release checks enforce this value.
2
+ export const CANVAS_GLOBE_VERSION = "1.0.0-beta.2";
package/types/index.d.ts CHANGED
@@ -381,10 +381,7 @@ export interface RenderMarkerContext {
381
381
  export type TooltipKind = "marker" | "cluster" | "country";
382
382
 
383
383
  export interface GeoGlobeOptions {
384
- /**
385
- * License key supplied for a GPLv3-compatible project or with a commercial
386
- * order. Never sent over the network and never used to disable features.
387
- */
384
+ /** Commercial license key supplied after purchase. Keys begin with `GLO`. */
388
385
  licenseKey?: string | null;
389
386
  /** "globe" (orthographic, spinnable) or "map" (flat). Default "globe". */
390
387
  mode?: "globe" | "map";
@@ -609,13 +606,28 @@ export { GeoGlobe as CanvasGlobe };
609
606
  /** Brand-aligned alias for `createGlobe`. */
610
607
  export { createGlobe as createCanvasGlobe };
611
608
  export declare const DEFAULT_LICENSE_KEY: "0000-0000-000-0000";
609
+ /** Prefix used by CanvasGlobe commercial license keys. */
610
+ export declare const LICENSE_KEY_PREFIX: "GLO";
611
+ /** Canonical licensing page used by CanvasGlobe notices. */
612
+ export declare const LICENSE_PAGE_URL: string;
613
+ /** Classifies a browser location without making a network request. */
614
+ export declare function inspectRuntime(locationValue?: {
615
+ protocol?: string;
616
+ hostname?: string;
617
+ } | null): {
618
+ kind: "development" | "production" | "unknown";
619
+ hostname: string;
620
+ public: boolean;
621
+ };
612
622
  export interface LicenseKeyStatus {
613
623
  valid: boolean;
614
- kind: "missing" | "placeholder" | "provided";
624
+ kind: "missing" | "placeholder" | "provided" | "invalid" | "licensed";
615
625
  key: string;
616
626
  }
617
627
  /** Returns the configured license-key status. */
618
628
  export declare function inspectLicenseKey(value: unknown): LicenseKeyStatus;
629
+ /** Resolves the same local license-key result. */
630
+ export declare function verifyLicenseKey(value: unknown): Promise<LicenseKeyStatus>;
619
631
  /** Returns whether a configured license key is available. */
620
632
  export declare function hasLicenseKey(value: unknown): boolean;
621
633
  export declare const themes: Record<ThemeName, Theme>;