canvas-globe 1.0.0-beta.2 → 1.0.0-beta.3

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,15 @@ All notable changes to this package are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.0.0-beta.3] - 2026-09-17
10
+
11
+ - Replaced the small production license badge with a professional in-canvas
12
+ license panel containing purchase, preview and existing-key actions.
13
+ - Added a persistent unlicensed watermark and purchase ribbon after the panel
14
+ is dismissed, while keeping the globe interactive.
15
+ - Simplified customer-facing runtime copy to refer to the license key without
16
+ exposing its technical prefix convention.
17
+
9
18
  ## [1.0.0-beta.2] - 2026-09-17
10
19
 
11
20
  - Corrected the project-creator help and README so beta users install the
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-beta.3",
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,7 +1314,7 @@ 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-beta.3";
1318
1318
 
1319
1319
  /** Local license-key checks and production-use presentation helpers. */
1320
1320
 
@@ -1322,6 +1322,8 @@ const DEFAULT_LICENSE_KEY = "0000-0000-000-0000";
1322
1322
  const LICENSE_KEY_PREFIX = "GLO";
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
@@ -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) {
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-beta.3",
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
@@ -20,6 +20,7 @@ export {
20
20
  DEFAULT_LICENSE_KEY,
21
21
  LICENSE_KEY_PREFIX,
22
22
  LICENSE_PAGE_URL,
23
+ LICENSE_SETUP_URL,
23
24
  inspectRuntime,
24
25
  inspectLicenseKey,
25
26
  verifyLicenseKey,
package/src/license.js CHANGED
@@ -4,6 +4,8 @@ export const DEFAULT_LICENSE_KEY = "0000-0000-000-0000";
4
4
  export const LICENSE_KEY_PREFIX = "GLO";
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
@@ -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-beta.3";
package/types/index.d.ts CHANGED
@@ -610,6 +610,7 @@ export declare const DEFAULT_LICENSE_KEY: "0000-0000-000-0000";
610
610
  export declare const LICENSE_KEY_PREFIX: "GLO";
611
611
  /** Canonical licensing page used by CanvasGlobe notices. */
612
612
  export declare const LICENSE_PAGE_URL: string;
613
+ export declare const LICENSE_SETUP_URL: string;
613
614
  /** Classifies a browser location without making a network request. */
614
615
  export declare function inspectRuntime(locationValue?: {
615
616
  protocol?: string;