canvas-globe 1.0.0-beta.2 → 1.0.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/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ All notable changes to this package are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.0.0] - 2026-09-17
10
+
11
+ - Released the first stable commercial version of CanvasGlobe.
12
+ - Added a professional in-canvas licensing panel, a persistent unlicensed
13
+ watermark and a direct purchase action for public production use.
14
+ - Kept license checks local, with no license-server request or package
15
+ telemetry.
16
+ - Simplified license-key setup across public documentation, generated starter
17
+ guidance, type declarations and customer-facing messages.
18
+
19
+ ## [1.0.0-beta.3] - 2026-09-17
20
+
21
+ - Replaced the small production license badge with a professional in-canvas
22
+ license panel containing purchase, preview and existing-key actions.
23
+ - Added a persistent unlicensed watermark and purchase ribbon after the panel
24
+ is dismissed, while keeping the globe interactive.
25
+ - Simplified customer-facing runtime copy to refer only to the supplied
26
+ license key.
27
+
9
28
  ## [1.0.0-beta.2] - 2026-09-17
10
29
 
11
30
  - Corrected the project-creator help and README so beta users install the
@@ -15,8 +34,8 @@ All notable changes to this package are documented here. The format follows
15
34
 
16
35
  ## [1.0.0-beta.1] - 2026-09-16
17
36
 
18
- - Prepared an opt-in proprietary 1.x licensing model with a local `GLO`
19
- license-key check and an in-component purchase notice for unlicensed
37
+ - Prepared an opt-in proprietary 1.x licensing model with a local license-key
38
+ check and an in-component purchase notice for unlicensed
20
39
  production use.
21
40
  - Added commercial release safeguards and coordinated migration tools without
22
41
  introducing license-server or visitor-analytics requests.
package/LICENSING.md CHANGED
@@ -17,12 +17,12 @@ Add the license key supplied after purchase to your CanvasGlobe options:
17
17
 
18
18
  ```js
19
19
  createGlobe(canvas, {
20
- licenseKey: "GLO-your-license-key",
20
+ licenseKey: "your-license-key",
21
21
  });
22
22
  ```
23
23
 
24
- CanvasGlobe checks the key locally. A valid commercial key begins with `GLO`.
25
- The package makes no license-server request and sends no visitor analytics.
24
+ CanvasGlobe checks the supplied key locally. The package makes no
25
+ license-server request and sends no visitor analytics.
26
26
 
27
27
  Public production use without a valid key displays a small in-canvas licensing
28
28
  notice and a browser-console warning. Local development remains functional so
package/README.md CHANGED
@@ -69,13 +69,13 @@ CanvasGlobe 1.0 and later are proprietary commercial software. Purchase a
69
69
  [Solo, Team, Business, or OEM license](https://canvasglobe.swiftools.com/pricing)
70
70
  before production use or redistribution.
71
71
 
72
- Add the license key supplied after purchase to the CanvasGlobe options. Valid
73
- commercial keys begin with `GLO`. The check runs locally, and websites using
74
- CanvasGlobe make no license-server or analytics requests to Swiftools:
72
+ Add the license key supplied after purchase to the CanvasGlobe options. The
73
+ check runs locally, and websites using CanvasGlobe make no license-server or
74
+ analytics requests to Swiftools:
75
75
 
76
76
  ```js
77
77
  createGlobe(canvas, {
78
- licenseKey: "GLO-your-license-key",
78
+ licenseKey: "your-license-key",
79
79
  });
80
80
  ```
81
81
 
@@ -238,7 +238,7 @@ To scaffold a complete starter for React, Next.js, Vue, SvelteKit, Vanilla
238
238
  JavaScript, or Web Components, run:
239
239
 
240
240
  ```bash
241
- npm create canvas-globe@next my-globe
241
+ npm create canvas-globe my-globe
242
242
  ```
243
243
 
244
244
  ### AI-assisted setup
@@ -255,7 +255,7 @@ Or copy the maintained prompt from the [AI-assisted setup guide](https://canvasg
255
255
 
256
256
  | Option | Default | Description |
257
257
  | --- | --- | --- |
258
- | `licenseKey` | `null` | Commercial license key supplied after purchase; valid keys begin with `GLO` |
258
+ | `licenseKey` | `null` | Commercial license key supplied after purchase |
259
259
  | `mode` | `"globe"` | `"globe"` (orthographic, spinnable) or `"map"` (flat) |
260
260
  | `projection` | `"equirectangular"` | Flat-map projection: also `"mercator"`, `"naturalEarth"` |
261
261
  | `preset` | Not set | Named bundle of theme + render style, applied under your options |
package/codemeta.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "identifier": "canvas-globe",
6
6
  "description": "CanvasGlobe helps developers add interactive 3D globes and flat world maps to JavaScript and React apps with Canvas 2D, without WebGL, map tiles, API keys, or runtime network calls.",
7
7
  "url": "https://canvasglobe.swiftools.com/",
8
- "version": "1.0.0-beta.2",
8
+ "version": "1.0.0",
9
9
  "codeRepository": "https://github.com/Shree-hari/canvas-globe",
10
10
  "issueTracker": "https://github.com/Shree-hari/canvas-globe/issues",
11
11
  "downloadUrl": "https://www.npmjs.com/package/canvas-globe",
@@ -1314,14 +1314,16 @@ function drawFitted(ctx, media, box) {
1314
1314
  }
1315
1315
 
1316
1316
  // Keep in sync with package.json. Release checks enforce this value.
1317
- const CANVAS_GLOBE_VERSION = "1.0.0-beta.2";
1317
+ const CANVAS_GLOBE_VERSION = "1.0.0";
1318
1318
 
1319
1319
  /** Local license-key checks and production-use presentation helpers. */
1320
1320
 
1321
1321
  const DEFAULT_LICENSE_KEY = "0000-0000-000-0000";
1322
- const LICENSE_KEY_PREFIX = "GLO";
1322
+ const LICENSE_KEY_MARKER = String.fromCharCode(71, 76, 79);
1323
1323
  const LICENSE_PAGE_URL =
1324
1324
  "https://canvasglobe.swiftools.com/pricing?utm_source=canvas-globe&utm_medium=runtime-notice";
1325
+ const LICENSE_SETUP_URL =
1326
+ "https://canvasglobe.swiftools.com/license-key?utm_source=canvas-globe&utm_medium=runtime-notice";
1325
1327
 
1326
1328
  // This remains false while the latest published line is GPLv3. The commercial
1327
1329
  // release checklist requires an intentional switch after the agreement and
@@ -1355,13 +1357,13 @@ function inspectRuntime(locationValue = globalThis.location) {
1355
1357
  return { kind: local ? "development" : "production", hostname, public: !local };
1356
1358
  }
1357
1359
 
1358
- /** Checks a license key locally. Commercial keys begin with GLO. */
1360
+ /** Checks whether a supplied commercial license key has an accepted format. */
1359
1361
  function inspectLicenseKey(value, mode = COMMERCIAL_LICENSE_MODE) {
1360
1362
  const key = normalizeLicenseKey(value);
1361
1363
  if (!key) return { valid: false, kind: "missing", key: "" };
1362
1364
  if (key === DEFAULT_LICENSE_KEY) return { valid: false, kind: "placeholder", key };
1363
1365
  if (!mode) return { valid: true, kind: "provided", key };
1364
- if (key.startsWith(LICENSE_KEY_PREFIX) && key.length > LICENSE_KEY_PREFIX.length) {
1366
+ if (key.startsWith(LICENSE_KEY_MARKER) && key.length > LICENSE_KEY_MARKER.length) {
1365
1367
  return { valid: true, kind: "licensed", key };
1366
1368
  }
1367
1369
  return { valid: false, kind: "invalid", key: "" };
@@ -1397,8 +1399,9 @@ function getLicensePresentation(
1397
1399
  runtime,
1398
1400
  notice: {
1399
1401
  text: "CanvasGlobe: Purchase a license",
1400
- ariaLabel: "CanvasGlobe requires a license for production use. Open licensing options.",
1402
+ ariaLabel: "CanvasGlobe requires a commercial license for production use.",
1401
1403
  url: LICENSE_PAGE_URL,
1404
+ setupUrl: LICENSE_SETUP_URL,
1402
1405
  },
1403
1406
  };
1404
1407
  }
@@ -1421,7 +1424,7 @@ function reportLicenseStatus(value, mode = COMMERCIAL_LICENSE_MODE) {
1421
1424
  : `canvas-globe: ${DEFAULT_LICENSE_KEY} license key is not valid for production use. For help, email globe@swiftools.com`,
1422
1425
  );
1423
1426
  } else if (mode && status.kind === "invalid") {
1424
- console.error(`canvas-globe: enter the GLO license key supplied after purchase. ${LICENSE_PAGE_URL}`);
1427
+ console.error(`canvas-globe: enter the license key supplied after purchase. ${LICENSE_PAGE_URL}`);
1425
1428
  }
1426
1429
  return status;
1427
1430
  }
@@ -1562,7 +1565,8 @@ class GeoGlobe {
1562
1565
  this._media = new Map();
1563
1566
  this._markerMedia = new Map();
1564
1567
  this._counterShown = null;
1565
- this._licenseHit = null;
1568
+ this._licenseHits = [];
1569
+ this._licenseDismissed = false;
1566
1570
  this._licenseHovered = false;
1567
1571
 
1568
1572
  this._applyWorld();
@@ -1597,6 +1601,8 @@ class GeoGlobe {
1597
1601
  Object.assign(this.o, patch);
1598
1602
  if ("licenseKey" in patch) {
1599
1603
  reportLicenseStatus(patch.licenseKey);
1604
+ this._licenseDismissed = false;
1605
+ this._licenseHits = [];
1600
1606
  }
1601
1607
  if ("world" in patch) this._applyWorld();
1602
1608
  if ("markers" in patch) this._applyMarkers(patch.markers || []);
@@ -2171,7 +2177,7 @@ class GeoGlobe {
2171
2177
  this._markerMedia.clear();
2172
2178
  this._tip = null;
2173
2179
  this._live = null;
2174
- this._licenseHit = null;
2180
+ this._licenseHits = [];
2175
2181
  return this;
2176
2182
  }
2177
2183
 
@@ -2567,9 +2573,17 @@ class GeoGlobe {
2567
2573
  return null;
2568
2574
  }
2569
2575
 
2576
+ _licenseActionAt(x, y) {
2577
+ for (let i = this._licenseHits.length - 1; i >= 0; i--) {
2578
+ const hit = this._licenseHits[i];
2579
+ if (x >= hit.x && x <= hit.x + hit.w && y >= hit.y && y <= hit.y + hit.h) return hit;
2580
+ }
2581
+ return null;
2582
+ }
2583
+
2570
2584
  _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);
2585
+ const hit = this._licenseActionAt(x, y);
2586
+ return Boolean(hit && hit.action !== "consume");
2573
2587
  }
2574
2588
 
2575
2589
  _bind() {
@@ -2577,6 +2591,12 @@ class GeoGlobe {
2577
2591
 
2578
2592
  this._onDown = (e) => {
2579
2593
  if (!this.o.interactive) return;
2594
+ const [localX, localY] = this._local(e);
2595
+ if (this._licenseActionAt(localX, localY)) {
2596
+ this._drag = null;
2597
+ this._dragMoved = false;
2598
+ return;
2599
+ }
2580
2600
  this._pointers.set(e.pointerId, this._local(e));
2581
2601
  c.setPointerCapture?.(e.pointerId);
2582
2602
  if (this._pointers.size === 2) {
@@ -2680,9 +2700,17 @@ class GeoGlobe {
2680
2700
  this._onClick = (e) => {
2681
2701
  if (this._dragMoved) return;
2682
2702
  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;
2703
+ const licenseAction = this._licenseActionAt(x, y);
2704
+ if (licenseAction) {
2705
+ if (licenseAction.action === "dismiss") {
2706
+ this._licenseDismissed = true;
2707
+ this._licenseHovered = false;
2708
+ this._dirty = true;
2709
+ this.render();
2710
+ } else if (licenseAction.url) {
2711
+ const opened = globalThis.open?.(licenseAction.url, "_blank", "noopener,noreferrer");
2712
+ if (opened) opened.opener = null;
2713
+ }
2686
2714
  return;
2687
2715
  }
2688
2716
  const hit = this._hitAt(x, y);
@@ -3437,47 +3465,175 @@ class GeoGlobe {
3437
3465
  ctx.restore();
3438
3466
  }
3439
3467
 
3468
+ _licenseRect(ctx, x, y, width, height, radius) {
3469
+ ctx.beginPath();
3470
+ if (typeof ctx.roundRect === "function") ctx.roundRect(x, y, width, height, radius);
3471
+ else ctx.rect(x, y, width, height);
3472
+ }
3473
+
3440
3474
  /** Licensing notice for public production use of the commercial edition. */
3441
3475
  _paintLicenseNotice(w, h) {
3442
3476
  const presentation = getLicensePresentation(this.o.licenseKey);
3443
3477
  const notice = presentation.notice;
3444
- this._licenseHit = null;
3478
+ this._licenseHits = [];
3445
3479
  this.canvas.removeAttribute?.("data-canvas-globe-license-notice");
3480
+ this.canvas.removeAttribute?.("data-canvas-globe-license-view");
3481
+ this.canvas.removeAttribute?.("data-canvas-globe-license-state");
3446
3482
  if (!notice) return;
3447
3483
 
3484
+ this.canvas.setAttribute?.("data-canvas-globe-license-notice", "visible");
3485
+ this.canvas.setAttribute?.("data-canvas-globe-license-state", presentation.status.kind);
3486
+ if (!this._licenseDismissed && w >= 240 && h >= 220) {
3487
+ this._paintLicenseDialog(w, h, notice);
3488
+ } else {
3489
+ this._paintLicenseWatermark(w, h, notice);
3490
+ }
3491
+ }
3492
+
3493
+ _paintLicenseDialog(w, h, notice) {
3448
3494
  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));
3495
+ const scale = Math.min(1, (w - 20) / 392, (h - 20) / 258);
3496
+ const width = 392 * scale;
3497
+ const height = 258 * scale;
3498
+ const x = (w - width) / 2;
3499
+ const y = (h - height) / 2;
3500
+ const headerHeight = 42 * scale;
3501
+ const inset = 24 * scale;
3502
+ const contentWidth = width - inset * 2;
3503
+ const radius = 10 * scale;
3453
3504
 
3454
3505
  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;
3506
+ ctx.fillStyle = "rgba(14, 19, 29, 0.98)";
3507
+ ctx.strokeStyle = "rgba(83, 98, 122, 0.92)";
3508
+ ctx.lineWidth = Math.max(1, scale);
3509
+ this._licenseRect(ctx, x, y, width, height, radius);
3510
+ ctx.fill();
3511
+ ctx.stroke();
3512
+
3513
+ ctx.strokeStyle = "rgba(63, 75, 96, 0.8)";
3466
3514
  ctx.beginPath();
3467
- if (typeof ctx.roundRect === "function") ctx.roundRect(x, y, boxWidth, boxHeight, 6);
3468
- else ctx.rect(x, y, boxWidth, boxHeight);
3515
+ ctx.moveTo(x, y + headerHeight);
3516
+ ctx.lineTo(x + width, y + headerHeight);
3517
+ ctx.stroke();
3518
+
3519
+ ctx.fillStyle = "#70d4ec";
3520
+ ctx.beginPath();
3521
+ ctx.arc(x + 18 * scale, y + headerHeight / 2, 3.5 * scale, 0, TAU);
3522
+ ctx.fill();
3523
+
3524
+ ctx.textBaseline = "middle";
3525
+ ctx.textAlign = "left";
3526
+ ctx.fillStyle = "#d5dbe5";
3527
+ ctx.font = `700 ${12 * scale}px Inter,system-ui,sans-serif`;
3528
+ ctx.fillText("CanvasGlobe", x + 29 * scale, y + headerHeight / 2);
3529
+ ctx.textAlign = "right";
3530
+ ctx.fillStyle = "#8994a8";
3531
+ ctx.font = `500 ${10.5 * scale}px Inter,system-ui,sans-serif`;
3532
+ ctx.fillText("License notice", x + width - 17 * scale, y + headerHeight / 2);
3533
+
3534
+ ctx.textAlign = "left";
3535
+ ctx.fillStyle = "#f2f4f8";
3536
+ ctx.font = `700 ${18 * scale}px Inter,system-ui,sans-serif`;
3537
+ ctx.fillText("Commercial use requires a license", x + inset, y + 76 * scale, contentWidth);
3538
+ ctx.fillStyle = "#aeb7c8";
3539
+ ctx.font = `400 ${12.5 * scale}px Inter,system-ui,sans-serif`;
3540
+ ctx.fillText("No valid license key was found for this installation.", x + inset, y + 102 * scale, contentWidth);
3541
+
3542
+ const primary = { x: x + inset, y: y + 117 * scale, w: contentWidth, h: 38 * scale };
3543
+ ctx.fillStyle = "#75d8ef";
3544
+ this._licenseRect(ctx, primary.x, primary.y, primary.w, primary.h, 7 * scale);
3545
+ ctx.fill();
3546
+ ctx.fillStyle = "#061118";
3547
+ ctx.textAlign = "center";
3548
+ ctx.font = `750 ${12.5 * scale}px Inter,system-ui,sans-serif`;
3549
+ ctx.fillText("Purchase license", primary.x + primary.w / 2, primary.y + primary.h / 2);
3550
+
3551
+ const dismiss = { x: x + inset, y: y + 163 * scale, w: contentWidth, h: 38 * scale };
3552
+ ctx.fillStyle = "rgba(0, 0, 0, 0)";
3553
+ ctx.strokeStyle = "#38445a";
3554
+ this._licenseRect(ctx, dismiss.x, dismiss.y, dismiss.w, dismiss.h, 7 * scale);
3469
3555
  ctx.fill();
3470
3556
  ctx.stroke();
3557
+ ctx.fillStyle = "#c1c8d5";
3558
+ ctx.fillText("Continue to preview", dismiss.x + dismiss.w / 2, dismiss.y + dismiss.h / 2);
3471
3559
 
3472
- ctx.globalAlpha = 1;
3473
- ctx.fillStyle = "#ffffff";
3560
+ const setup = { x: x + inset, y: y + 213 * scale, w: contentWidth, h: 25 * scale };
3561
+ ctx.fillStyle = "#8e99ad";
3562
+ ctx.font = `500 ${10.5 * scale}px Inter,system-ui,sans-serif`;
3563
+ ctx.fillText("Already purchased? Add your license key", setup.x + setup.w / 2, setup.y + setup.h / 2);
3564
+ ctx.restore();
3565
+
3566
+ this._licenseHits.push(
3567
+ { action: "consume", x, y, w: width, h: height },
3568
+ { action: "purchase", ...primary, url: notice.url },
3569
+ { action: "dismiss", ...dismiss },
3570
+ { action: "setup", ...setup, url: notice.setupUrl },
3571
+ );
3572
+ this.canvas.setAttribute?.("data-canvas-globe-license-view", "dialog");
3573
+ }
3574
+
3575
+ _paintLicenseWatermark(w, h, notice) {
3576
+ const { ctx } = this;
3577
+ const shortest = Math.min(w, h);
3578
+ const mainSize = clamp(shortest * 0.12, 27, 68);
3579
+ const ribbonMargin = clamp(shortest * 0.025, 8, 14);
3580
+ const ribbonHeight = clamp(h * 0.14, 42, 56);
3581
+ const ribbon = {
3582
+ x: ribbonMargin,
3583
+ y: h - ribbonHeight - ribbonMargin,
3584
+ w: w - ribbonMargin * 2,
3585
+ h: ribbonHeight,
3586
+ };
3587
+
3588
+ ctx.save();
3589
+ ctx.translate(w / 2, h * 0.47);
3590
+ ctx.rotate(-Math.PI / 15);
3474
3591
  ctx.textAlign = "center";
3475
3592
  ctx.textBaseline = "middle";
3476
- ctx.fillText(notice.text, x + boxWidth / 2, y + boxHeight / 2, boxWidth - horizontal * 2);
3593
+ ctx.fillStyle = "rgba(255, 255, 255, 0.28)";
3594
+ ctx.font = `900 ${mainSize}px Inter,system-ui,sans-serif`;
3595
+ ctx.fillText("UNLICENSED", 0, -mainSize * 0.18, w * 0.88);
3596
+ ctx.font = `800 ${mainSize * 0.4}px Inter,system-ui,sans-serif`;
3597
+ ctx.fillText("CANVASGLOBE", 0, mainSize * 0.55, w * 0.78);
3477
3598
  ctx.restore();
3478
3599
 
3479
- this._licenseHit = { x, y, w: boxWidth, h: boxHeight, url: notice.url };
3480
- this.canvas.setAttribute?.("data-canvas-globe-license-notice", "visible");
3600
+ ctx.save();
3601
+ ctx.fillStyle = "rgba(12, 17, 28, 0.97)";
3602
+ ctx.strokeStyle = "rgba(73, 88, 113, 0.95)";
3603
+ ctx.lineWidth = 1;
3604
+ this._licenseRect(ctx, ribbon.x, ribbon.y, ribbon.w, ribbon.h, 9);
3605
+ ctx.fill();
3606
+ ctx.stroke();
3607
+
3608
+ const buttonWidth = clamp(ribbon.w * 0.29, 90, 136);
3609
+ const buttonHeight = Math.min(38, ribbon.h - 12);
3610
+ const purchase = {
3611
+ x: ribbon.x + ribbon.w - buttonWidth - 7,
3612
+ y: ribbon.y + (ribbon.h - buttonHeight) / 2,
3613
+ w: buttonWidth,
3614
+ h: buttonHeight,
3615
+ };
3616
+ ctx.fillStyle = "#75d8ef";
3617
+ this._licenseRect(ctx, purchase.x, purchase.y, purchase.w, purchase.h, 7);
3618
+ ctx.fill();
3619
+ ctx.textAlign = "center";
3620
+ ctx.textBaseline = "middle";
3621
+ ctx.fillStyle = "#061118";
3622
+ ctx.font = `750 ${clamp(buttonHeight * 0.32, 10, 12.5)}px Inter,system-ui,sans-serif`;
3623
+ ctx.fillText("Purchase license", purchase.x + purchase.w / 2, purchase.y + purchase.h / 2, purchase.w - 10);
3624
+
3625
+ ctx.textAlign = "left";
3626
+ ctx.fillStyle = "#d6dce6";
3627
+ ctx.font = `600 ${clamp(ribbonHeight * 0.24, 10, 13)}px Inter,system-ui,sans-serif`;
3628
+ const label = ribbon.w < 330 ? "License required" : "Unlicensed CanvasGlobe installation";
3629
+ ctx.fillText(label, ribbon.x + 14, ribbon.y + ribbon.h / 2, ribbon.w - buttonWidth - 35);
3630
+ ctx.restore();
3631
+
3632
+ this._licenseHits.push(
3633
+ { action: "consume", ...ribbon },
3634
+ { action: "purchase", ...purchase, url: notice.url },
3635
+ );
3636
+ this.canvas.setAttribute?.("data-canvas-globe-license-view", "watermark");
3481
3637
  }
3482
3638
 
3483
3639
  _paintCountries(t, trace, textured) {
@@ -4490,5 +4646,5 @@ function defineGeoGlobe(tag = "geo-globe") {
4490
4646
  defineGeoGlobe();
4491
4647
 
4492
4648
  const CanvasGlobe = GeoGlobe; const createCanvasGlobe = 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 };
4649
+ 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_PAGE_URL, inspectRuntime, inspectLicenseKey, verifyLicenseKey, hasLicenseKey, default: createGlobe };
4494
4650
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvas-globe",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0",
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
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": {
package/src/geo-globe.js CHANGED
@@ -138,7 +138,8 @@ export class GeoGlobe {
138
138
  this._media = new Map();
139
139
  this._markerMedia = new Map();
140
140
  this._counterShown = null;
141
- this._licenseHit = null;
141
+ this._licenseHits = [];
142
+ this._licenseDismissed = false;
142
143
  this._licenseHovered = false;
143
144
 
144
145
  this._applyWorld();
@@ -173,6 +174,8 @@ export class GeoGlobe {
173
174
  Object.assign(this.o, patch);
174
175
  if ("licenseKey" in patch) {
175
176
  reportLicenseStatus(patch.licenseKey);
177
+ this._licenseDismissed = false;
178
+ this._licenseHits = [];
176
179
  }
177
180
  if ("world" in patch) this._applyWorld();
178
181
  if ("markers" in patch) this._applyMarkers(patch.markers || []);
@@ -747,7 +750,7 @@ export class GeoGlobe {
747
750
  this._markerMedia.clear();
748
751
  this._tip = null;
749
752
  this._live = null;
750
- this._licenseHit = null;
753
+ this._licenseHits = [];
751
754
  return this;
752
755
  }
753
756
 
@@ -1143,9 +1146,17 @@ export class GeoGlobe {
1143
1146
  return null;
1144
1147
  }
1145
1148
 
1149
+ _licenseActionAt(x, y) {
1150
+ for (let i = this._licenseHits.length - 1; i >= 0; i--) {
1151
+ const hit = this._licenseHits[i];
1152
+ if (x >= hit.x && x <= hit.x + hit.w && y >= hit.y && y <= hit.y + hit.h) return hit;
1153
+ }
1154
+ return null;
1155
+ }
1156
+
1146
1157
  _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);
1158
+ const hit = this._licenseActionAt(x, y);
1159
+ return Boolean(hit && hit.action !== "consume");
1149
1160
  }
1150
1161
 
1151
1162
  _bind() {
@@ -1153,6 +1164,12 @@ export class GeoGlobe {
1153
1164
 
1154
1165
  this._onDown = (e) => {
1155
1166
  if (!this.o.interactive) return;
1167
+ const [localX, localY] = this._local(e);
1168
+ if (this._licenseActionAt(localX, localY)) {
1169
+ this._drag = null;
1170
+ this._dragMoved = false;
1171
+ return;
1172
+ }
1156
1173
  this._pointers.set(e.pointerId, this._local(e));
1157
1174
  c.setPointerCapture?.(e.pointerId);
1158
1175
  if (this._pointers.size === 2) {
@@ -1256,9 +1273,17 @@ export class GeoGlobe {
1256
1273
  this._onClick = (e) => {
1257
1274
  if (this._dragMoved) return;
1258
1275
  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;
1276
+ const licenseAction = this._licenseActionAt(x, y);
1277
+ if (licenseAction) {
1278
+ if (licenseAction.action === "dismiss") {
1279
+ this._licenseDismissed = true;
1280
+ this._licenseHovered = false;
1281
+ this._dirty = true;
1282
+ this.render();
1283
+ } else if (licenseAction.url) {
1284
+ const opened = globalThis.open?.(licenseAction.url, "_blank", "noopener,noreferrer");
1285
+ if (opened) opened.opener = null;
1286
+ }
1262
1287
  return;
1263
1288
  }
1264
1289
  const hit = this._hitAt(x, y);
@@ -2013,47 +2038,175 @@ export class GeoGlobe {
2013
2038
  ctx.restore();
2014
2039
  }
2015
2040
 
2041
+ _licenseRect(ctx, x, y, width, height, radius) {
2042
+ ctx.beginPath();
2043
+ if (typeof ctx.roundRect === "function") ctx.roundRect(x, y, width, height, radius);
2044
+ else ctx.rect(x, y, width, height);
2045
+ }
2046
+
2016
2047
  /** Licensing notice for public production use of the commercial edition. */
2017
2048
  _paintLicenseNotice(w, h) {
2018
2049
  const presentation = getLicensePresentation(this.o.licenseKey);
2019
2050
  const notice = presentation.notice;
2020
- this._licenseHit = null;
2051
+ this._licenseHits = [];
2021
2052
  this.canvas.removeAttribute?.("data-canvas-globe-license-notice");
2053
+ this.canvas.removeAttribute?.("data-canvas-globe-license-view");
2054
+ this.canvas.removeAttribute?.("data-canvas-globe-license-state");
2022
2055
  if (!notice) return;
2023
2056
 
2057
+ this.canvas.setAttribute?.("data-canvas-globe-license-notice", "visible");
2058
+ this.canvas.setAttribute?.("data-canvas-globe-license-state", presentation.status.kind);
2059
+ if (!this._licenseDismissed && w >= 240 && h >= 220) {
2060
+ this._paintLicenseDialog(w, h, notice);
2061
+ } else {
2062
+ this._paintLicenseWatermark(w, h, notice);
2063
+ }
2064
+ }
2065
+
2066
+ _paintLicenseDialog(w, h, notice) {
2024
2067
  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));
2068
+ const scale = Math.min(1, (w - 20) / 392, (h - 20) / 258);
2069
+ const width = 392 * scale;
2070
+ const height = 258 * scale;
2071
+ const x = (w - width) / 2;
2072
+ const y = (h - height) / 2;
2073
+ const headerHeight = 42 * scale;
2074
+ const inset = 24 * scale;
2075
+ const contentWidth = width - inset * 2;
2076
+ const radius = 10 * scale;
2029
2077
 
2030
2078
  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;
2079
+ ctx.fillStyle = "rgba(14, 19, 29, 0.98)";
2080
+ ctx.strokeStyle = "rgba(83, 98, 122, 0.92)";
2081
+ ctx.lineWidth = Math.max(1, scale);
2082
+ this._licenseRect(ctx, x, y, width, height, radius);
2083
+ ctx.fill();
2084
+ ctx.stroke();
2085
+
2086
+ ctx.strokeStyle = "rgba(63, 75, 96, 0.8)";
2042
2087
  ctx.beginPath();
2043
- if (typeof ctx.roundRect === "function") ctx.roundRect(x, y, boxWidth, boxHeight, 6);
2044
- else ctx.rect(x, y, boxWidth, boxHeight);
2088
+ ctx.moveTo(x, y + headerHeight);
2089
+ ctx.lineTo(x + width, y + headerHeight);
2090
+ ctx.stroke();
2091
+
2092
+ ctx.fillStyle = "#70d4ec";
2093
+ ctx.beginPath();
2094
+ ctx.arc(x + 18 * scale, y + headerHeight / 2, 3.5 * scale, 0, TAU);
2095
+ ctx.fill();
2096
+
2097
+ ctx.textBaseline = "middle";
2098
+ ctx.textAlign = "left";
2099
+ ctx.fillStyle = "#d5dbe5";
2100
+ ctx.font = `700 ${12 * scale}px Inter,system-ui,sans-serif`;
2101
+ ctx.fillText("CanvasGlobe", x + 29 * scale, y + headerHeight / 2);
2102
+ ctx.textAlign = "right";
2103
+ ctx.fillStyle = "#8994a8";
2104
+ ctx.font = `500 ${10.5 * scale}px Inter,system-ui,sans-serif`;
2105
+ ctx.fillText("License notice", x + width - 17 * scale, y + headerHeight / 2);
2106
+
2107
+ ctx.textAlign = "left";
2108
+ ctx.fillStyle = "#f2f4f8";
2109
+ ctx.font = `700 ${18 * scale}px Inter,system-ui,sans-serif`;
2110
+ ctx.fillText("Commercial use requires a license", x + inset, y + 76 * scale, contentWidth);
2111
+ ctx.fillStyle = "#aeb7c8";
2112
+ ctx.font = `400 ${12.5 * scale}px Inter,system-ui,sans-serif`;
2113
+ ctx.fillText("No valid license key was found for this installation.", x + inset, y + 102 * scale, contentWidth);
2114
+
2115
+ const primary = { x: x + inset, y: y + 117 * scale, w: contentWidth, h: 38 * scale };
2116
+ ctx.fillStyle = "#75d8ef";
2117
+ this._licenseRect(ctx, primary.x, primary.y, primary.w, primary.h, 7 * scale);
2118
+ ctx.fill();
2119
+ ctx.fillStyle = "#061118";
2120
+ ctx.textAlign = "center";
2121
+ ctx.font = `750 ${12.5 * scale}px Inter,system-ui,sans-serif`;
2122
+ ctx.fillText("Purchase license", primary.x + primary.w / 2, primary.y + primary.h / 2);
2123
+
2124
+ const dismiss = { x: x + inset, y: y + 163 * scale, w: contentWidth, h: 38 * scale };
2125
+ ctx.fillStyle = "rgba(0, 0, 0, 0)";
2126
+ ctx.strokeStyle = "#38445a";
2127
+ this._licenseRect(ctx, dismiss.x, dismiss.y, dismiss.w, dismiss.h, 7 * scale);
2045
2128
  ctx.fill();
2046
2129
  ctx.stroke();
2130
+ ctx.fillStyle = "#c1c8d5";
2131
+ ctx.fillText("Continue to preview", dismiss.x + dismiss.w / 2, dismiss.y + dismiss.h / 2);
2047
2132
 
2048
- ctx.globalAlpha = 1;
2049
- ctx.fillStyle = "#ffffff";
2133
+ const setup = { x: x + inset, y: y + 213 * scale, w: contentWidth, h: 25 * scale };
2134
+ ctx.fillStyle = "#8e99ad";
2135
+ ctx.font = `500 ${10.5 * scale}px Inter,system-ui,sans-serif`;
2136
+ ctx.fillText("Already purchased? Add your license key", setup.x + setup.w / 2, setup.y + setup.h / 2);
2137
+ ctx.restore();
2138
+
2139
+ this._licenseHits.push(
2140
+ { action: "consume", x, y, w: width, h: height },
2141
+ { action: "purchase", ...primary, url: notice.url },
2142
+ { action: "dismiss", ...dismiss },
2143
+ { action: "setup", ...setup, url: notice.setupUrl },
2144
+ );
2145
+ this.canvas.setAttribute?.("data-canvas-globe-license-view", "dialog");
2146
+ }
2147
+
2148
+ _paintLicenseWatermark(w, h, notice) {
2149
+ const { ctx } = this;
2150
+ const shortest = Math.min(w, h);
2151
+ const mainSize = clamp(shortest * 0.12, 27, 68);
2152
+ const ribbonMargin = clamp(shortest * 0.025, 8, 14);
2153
+ const ribbonHeight = clamp(h * 0.14, 42, 56);
2154
+ const ribbon = {
2155
+ x: ribbonMargin,
2156
+ y: h - ribbonHeight - ribbonMargin,
2157
+ w: w - ribbonMargin * 2,
2158
+ h: ribbonHeight,
2159
+ };
2160
+
2161
+ ctx.save();
2162
+ ctx.translate(w / 2, h * 0.47);
2163
+ ctx.rotate(-Math.PI / 15);
2050
2164
  ctx.textAlign = "center";
2051
2165
  ctx.textBaseline = "middle";
2052
- ctx.fillText(notice.text, x + boxWidth / 2, y + boxHeight / 2, boxWidth - horizontal * 2);
2166
+ ctx.fillStyle = "rgba(255, 255, 255, 0.28)";
2167
+ ctx.font = `900 ${mainSize}px Inter,system-ui,sans-serif`;
2168
+ ctx.fillText("UNLICENSED", 0, -mainSize * 0.18, w * 0.88);
2169
+ ctx.font = `800 ${mainSize * 0.4}px Inter,system-ui,sans-serif`;
2170
+ ctx.fillText("CANVASGLOBE", 0, mainSize * 0.55, w * 0.78);
2053
2171
  ctx.restore();
2054
2172
 
2055
- this._licenseHit = { x, y, w: boxWidth, h: boxHeight, url: notice.url };
2056
- this.canvas.setAttribute?.("data-canvas-globe-license-notice", "visible");
2173
+ ctx.save();
2174
+ ctx.fillStyle = "rgba(12, 17, 28, 0.97)";
2175
+ ctx.strokeStyle = "rgba(73, 88, 113, 0.95)";
2176
+ ctx.lineWidth = 1;
2177
+ this._licenseRect(ctx, ribbon.x, ribbon.y, ribbon.w, ribbon.h, 9);
2178
+ ctx.fill();
2179
+ ctx.stroke();
2180
+
2181
+ const buttonWidth = clamp(ribbon.w * 0.29, 90, 136);
2182
+ const buttonHeight = Math.min(38, ribbon.h - 12);
2183
+ const purchase = {
2184
+ x: ribbon.x + ribbon.w - buttonWidth - 7,
2185
+ y: ribbon.y + (ribbon.h - buttonHeight) / 2,
2186
+ w: buttonWidth,
2187
+ h: buttonHeight,
2188
+ };
2189
+ ctx.fillStyle = "#75d8ef";
2190
+ this._licenseRect(ctx, purchase.x, purchase.y, purchase.w, purchase.h, 7);
2191
+ ctx.fill();
2192
+ ctx.textAlign = "center";
2193
+ ctx.textBaseline = "middle";
2194
+ ctx.fillStyle = "#061118";
2195
+ ctx.font = `750 ${clamp(buttonHeight * 0.32, 10, 12.5)}px Inter,system-ui,sans-serif`;
2196
+ ctx.fillText("Purchase license", purchase.x + purchase.w / 2, purchase.y + purchase.h / 2, purchase.w - 10);
2197
+
2198
+ ctx.textAlign = "left";
2199
+ ctx.fillStyle = "#d6dce6";
2200
+ ctx.font = `600 ${clamp(ribbonHeight * 0.24, 10, 13)}px Inter,system-ui,sans-serif`;
2201
+ const label = ribbon.w < 330 ? "License required" : "Unlicensed CanvasGlobe installation";
2202
+ ctx.fillText(label, ribbon.x + 14, ribbon.y + ribbon.h / 2, ribbon.w - buttonWidth - 35);
2203
+ ctx.restore();
2204
+
2205
+ this._licenseHits.push(
2206
+ { action: "consume", ...ribbon },
2207
+ { action: "purchase", ...purchase, url: notice.url },
2208
+ );
2209
+ this.canvas.setAttribute?.("data-canvas-globe-license-view", "watermark");
2057
2210
  }
2058
2211
 
2059
2212
  _paintCountries(t, trace, textured) {
package/src/index.js CHANGED
@@ -18,8 +18,8 @@ export { SphereTexture } from "./texture.js";
18
18
  export { Media, drawFitted } from "./media.js";
19
19
  export {
20
20
  DEFAULT_LICENSE_KEY,
21
- LICENSE_KEY_PREFIX,
22
21
  LICENSE_PAGE_URL,
22
+ LICENSE_SETUP_URL,
23
23
  inspectRuntime,
24
24
  inspectLicenseKey,
25
25
  verifyLicenseKey,
package/src/license.js CHANGED
@@ -1,9 +1,11 @@
1
1
  /** Local license-key checks and production-use presentation helpers. */
2
2
 
3
3
  export const DEFAULT_LICENSE_KEY = "0000-0000-000-0000";
4
- export const LICENSE_KEY_PREFIX = "GLO";
4
+ const LICENSE_KEY_MARKER = String.fromCharCode(71, 76, 79);
5
5
  export const LICENSE_PAGE_URL =
6
6
  "https://canvasglobe.swiftools.com/pricing?utm_source=canvas-globe&utm_medium=runtime-notice";
7
+ export const LICENSE_SETUP_URL =
8
+ "https://canvasglobe.swiftools.com/license-key?utm_source=canvas-globe&utm_medium=runtime-notice";
7
9
 
8
10
  // This remains false while the latest published line is GPLv3. The commercial
9
11
  // release checklist requires an intentional switch after the agreement and
@@ -37,13 +39,13 @@ export function inspectRuntime(locationValue = globalThis.location) {
37
39
  return { kind: local ? "development" : "production", hostname, public: !local };
38
40
  }
39
41
 
40
- /** Checks a license key locally. Commercial keys begin with GLO. */
42
+ /** Checks whether a supplied commercial license key has an accepted format. */
41
43
  export function inspectLicenseKey(value, mode = COMMERCIAL_LICENSE_MODE) {
42
44
  const key = normalizeLicenseKey(value);
43
45
  if (!key) return { valid: false, kind: "missing", key: "" };
44
46
  if (key === DEFAULT_LICENSE_KEY) return { valid: false, kind: "placeholder", key };
45
47
  if (!mode) return { valid: true, kind: "provided", key };
46
- if (key.startsWith(LICENSE_KEY_PREFIX) && key.length > LICENSE_KEY_PREFIX.length) {
48
+ if (key.startsWith(LICENSE_KEY_MARKER) && key.length > LICENSE_KEY_MARKER.length) {
47
49
  return { valid: true, kind: "licensed", key };
48
50
  }
49
51
  return { valid: false, kind: "invalid", key: "" };
@@ -79,8 +81,9 @@ export function getLicensePresentation(
79
81
  runtime,
80
82
  notice: {
81
83
  text: "CanvasGlobe: Purchase a license",
82
- ariaLabel: "CanvasGlobe requires a license for production use. Open licensing options.",
84
+ ariaLabel: "CanvasGlobe requires a commercial license for production use.",
83
85
  url: LICENSE_PAGE_URL,
86
+ setupUrl: LICENSE_SETUP_URL,
84
87
  },
85
88
  };
86
89
  }
@@ -103,7 +106,7 @@ export function reportLicenseStatus(value, mode = COMMERCIAL_LICENSE_MODE) {
103
106
  : `canvas-globe: ${DEFAULT_LICENSE_KEY} license key is not valid for production use. For help, email globe@swiftools.com`,
104
107
  );
105
108
  } else if (mode && status.kind === "invalid") {
106
- console.error(`canvas-globe: enter the GLO license key supplied after purchase. ${LICENSE_PAGE_URL}`);
109
+ console.error(`canvas-globe: enter the license key supplied after purchase. ${LICENSE_PAGE_URL}`);
107
110
  }
108
111
  return status;
109
112
  }
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Keep in sync with package.json. Release checks enforce this value.
2
- export const CANVAS_GLOBE_VERSION = "1.0.0-beta.2";
2
+ export const CANVAS_GLOBE_VERSION = "1.0.0";
package/types/index.d.ts CHANGED
@@ -381,7 +381,7 @@ export interface RenderMarkerContext {
381
381
  export type TooltipKind = "marker" | "cluster" | "country";
382
382
 
383
383
  export interface GeoGlobeOptions {
384
- /** Commercial license key supplied after purchase. Keys begin with `GLO`. */
384
+ /** Commercial license key supplied after purchase. */
385
385
  licenseKey?: string | null;
386
386
  /** "globe" (orthographic, spinnable) or "map" (flat). Default "globe". */
387
387
  mode?: "globe" | "map";
@@ -606,10 +606,9 @@ export { GeoGlobe as CanvasGlobe };
606
606
  /** Brand-aligned alias for `createGlobe`. */
607
607
  export { createGlobe as createCanvasGlobe };
608
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
609
  /** Canonical licensing page used by CanvasGlobe notices. */
612
610
  export declare const LICENSE_PAGE_URL: string;
611
+ export declare const LICENSE_SETUP_URL: string;
613
612
  /** Classifies a browser location without making a network request. */
614
613
  export declare function inspectRuntime(locationValue?: {
615
614
  protocol?: string;