scanic 1.1.1 → 1.2.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/dist/scanic.js CHANGED
@@ -1174,15 +1174,6 @@ function createDefaultCorners(imageWidth, imageHeight, insetRatio = 0.08) {
1174
1174
  bottomLeft: { x: inset, y: imageHeight - inset }
1175
1175
  };
1176
1176
  }
1177
- function drawHandle(ctx, point, radius, color, stroke, lineWidth) {
1178
- ctx.beginPath();
1179
- ctx.arc(point.x, point.y, radius, 0, Math.PI * 2);
1180
- ctx.fillStyle = color;
1181
- ctx.fill();
1182
- ctx.lineWidth = lineWidth;
1183
- ctx.strokeStyle = stroke;
1184
- ctx.stroke();
1185
- }
1186
1177
  function normalizeImageToCanvas(image) {
1187
1178
  if (!image) {
1188
1179
  throw new Error("No image provided");
@@ -1214,30 +1205,205 @@ function normalizeImageToCanvas(image) {
1214
1205
  }
1215
1206
  return { canvas, width, height };
1216
1207
  }
1208
+ const STYLE_ID = "scanic-corner-editor-styles";
1209
+ const EDITOR_CSS = `
1210
+ .scanic-corner-editor {
1211
+ --scanic-accent: #6366f1;
1212
+ --scanic-mask: rgba(15, 23, 42, 0.45);
1213
+ --scanic-edge-color: var(--scanic-accent);
1214
+ --scanic-edge-width: 2.5;
1215
+ --scanic-handle-size: 20px;
1216
+ --scanic-handle-hit: 44px;
1217
+ --scanic-handle-color: #ffffff;
1218
+ --scanic-handle-ring: 2px;
1219
+ --scanic-handle-ring-color: var(--scanic-accent);
1220
+ --scanic-handle-shadow: 0 1px 4px rgba(15, 23, 42, 0.45);
1221
+ --scanic-handle-active-shadow: 0 8px 22px rgba(15, 23, 42, 0.5);
1222
+ --scanic-handle-active-scale: 1.28;
1223
+ --scanic-surface: rgba(15, 23, 42, 0.92);
1224
+ --scanic-surface-fg: #e2e8f0;
1225
+ --scanic-surface-radius: 12px;
1226
+ }
1227
+ .scanic-handle {
1228
+ position: absolute;
1229
+ box-sizing: border-box;
1230
+ width: var(--scanic-handle-size);
1231
+ height: var(--scanic-handle-size);
1232
+ margin: 0;
1233
+ padding: 0;
1234
+ border: var(--scanic-handle-ring) solid var(--scanic-handle-ring-color);
1235
+ border-radius: 50%;
1236
+ background: var(--scanic-handle-color);
1237
+ box-shadow: var(--scanic-handle-shadow);
1238
+ transform: translate(-50%, -50%);
1239
+ cursor: grab;
1240
+ touch-action: none;
1241
+ z-index: 2;
1242
+ transition: transform 0.12s ease, box-shadow 0.12s ease,
1243
+ background 0.12s ease, border-color 0.12s ease;
1244
+ }
1245
+ /* Enlarges the pointer/touch target without changing the visual size. */
1246
+ .scanic-handle::after {
1247
+ content: '';
1248
+ position: absolute;
1249
+ left: 50%;
1250
+ top: 50%;
1251
+ width: var(--scanic-handle-hit);
1252
+ height: var(--scanic-handle-hit);
1253
+ transform: translate(-50%, -50%);
1254
+ border-radius: 50%;
1255
+ }
1256
+ /* Soft accent halo that blooms when a handle is grabbed — the "depth" layer. */
1257
+ .scanic-handle::before {
1258
+ content: '';
1259
+ position: absolute;
1260
+ left: 50%;
1261
+ top: 50%;
1262
+ width: 0;
1263
+ height: 0;
1264
+ border-radius: 50%;
1265
+ background: var(--scanic-accent);
1266
+ opacity: 0;
1267
+ transform: translate(-50%, -50%);
1268
+ transition: opacity 0.15s ease, width 0.15s ease, height 0.15s ease;
1269
+ pointer-events: none;
1270
+ }
1271
+ .scanic-handle:hover {
1272
+ transform: translate(-50%, -50%) scale(1.12);
1273
+ }
1274
+ .scanic-handle:focus-visible {
1275
+ outline: none;
1276
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--scanic-accent) 45%, transparent),
1277
+ var(--scanic-handle-shadow);
1278
+ }
1279
+ /* The currently selected corner — the target of nudges / keyboard. */
1280
+ .scanic-handle.is-selected {
1281
+ border-color: var(--scanic-accent);
1282
+ transform: translate(-50%, -50%) scale(1.12);
1283
+ }
1284
+ .scanic-handle.is-selected::before {
1285
+ opacity: 0.16;
1286
+ width: calc(var(--scanic-handle-size) * 2);
1287
+ height: calc(var(--scanic-handle-size) * 2);
1288
+ }
1289
+ .scanic-handle.is-active {
1290
+ cursor: grabbing;
1291
+ background: var(--scanic-accent);
1292
+ border-color: #ffffff;
1293
+ transform: translate(-50%, -50%) scale(var(--scanic-handle-active-scale));
1294
+ box-shadow: var(--scanic-handle-active-shadow);
1295
+ }
1296
+ .scanic-handle.is-active::before {
1297
+ opacity: 0.22;
1298
+ width: calc(var(--scanic-handle-size) * 2.8);
1299
+ height: calc(var(--scanic-handle-size) * 2.8);
1300
+ }
1301
+ .scanic-toolbar,
1302
+ .scanic-nudges {
1303
+ position: absolute;
1304
+ z-index: 3;
1305
+ display: flex;
1306
+ gap: 4px;
1307
+ padding: 5px;
1308
+ background: var(--scanic-surface);
1309
+ border-radius: var(--scanic-surface-radius);
1310
+ -webkit-backdrop-filter: blur(8px);
1311
+ backdrop-filter: blur(8px);
1312
+ box-shadow: 0 6px 20px rgba(15, 23, 42, 0.35);
1313
+ }
1314
+ .scanic-toolbar {
1315
+ left: 50%;
1316
+ bottom: 12px;
1317
+ transform: translateX(-50%);
1318
+ }
1319
+ .scanic-nudges {
1320
+ top: 10px;
1321
+ right: 10px;
1322
+ display: grid;
1323
+ grid-template-columns: repeat(4, auto);
1324
+ gap: 3px;
1325
+ }
1326
+ .scanic-toolbar button,
1327
+ .scanic-nudges button {
1328
+ display: grid;
1329
+ place-items: center;
1330
+ margin: 0;
1331
+ padding: 0;
1332
+ font-family: inherit;
1333
+ font-size: 15px;
1334
+ font-weight: 600;
1335
+ line-height: 1;
1336
+ color: var(--scanic-surface-fg);
1337
+ background: transparent;
1338
+ border: 0;
1339
+ border-radius: 8px;
1340
+ cursor: pointer;
1341
+ transition: background 0.12s ease, color 0.12s ease, filter 0.12s ease;
1342
+ }
1343
+ .scanic-toolbar button {
1344
+ width: 34px;
1345
+ height: 34px;
1346
+ }
1347
+ .scanic-nudges button {
1348
+ width: 30px;
1349
+ height: 30px;
1350
+ }
1351
+ .scanic-toolbar button svg {
1352
+ width: 18px;
1353
+ height: 18px;
1354
+ display: block;
1355
+ }
1356
+ .scanic-toolbar button:hover,
1357
+ .scanic-nudges button:hover {
1358
+ background: rgba(255, 255, 255, 0.14);
1359
+ }
1360
+ .scanic-toolbar button:focus-visible,
1361
+ .scanic-nudges button:focus-visible {
1362
+ outline: 2px solid var(--scanic-accent);
1363
+ outline-offset: 1px;
1364
+ }
1365
+ .scanic-toolbar .scanic-btn-apply {
1366
+ background: var(--scanic-accent);
1367
+ color: #ffffff;
1368
+ }
1369
+ .scanic-toolbar .scanic-btn-apply:hover {
1370
+ background: var(--scanic-accent);
1371
+ filter: brightness(1.1);
1372
+ }
1373
+ .scanic-toolbar .scanic-btn-expert.is-on {
1374
+ background: color-mix(in srgb, var(--scanic-accent) 32%, transparent);
1375
+ color: #ffffff;
1376
+ }
1377
+ `;
1378
+ const ICONS = {
1379
+ reset: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 4 3 10 9 10"/><path d="M3.5 14a8.5 8.5 0 1 0 2-7.4L3 10"/></svg>',
1380
+ cancel: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
1381
+ apply: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>',
1382
+ expert: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg>'
1383
+ };
1384
+ function injectStyles(doc) {
1385
+ if (!doc || typeof doc.getElementById !== "function") return;
1386
+ if (doc.getElementById(STYLE_ID)) return;
1387
+ const style = doc.createElement("style");
1388
+ style.id = STYLE_ID;
1389
+ style.textContent = EDITOR_CSS;
1390
+ (doc.head || doc.documentElement).appendChild(style);
1391
+ }
1217
1392
  function createCornerEditor(options = {}) {
1218
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
1393
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v;
1219
1394
  const container = options.container;
1220
1395
  if (!container || typeof container.appendChild !== "function") {
1221
1396
  throw new Error("createCornerEditor requires a valid container element");
1222
1397
  }
1223
- const { canvas: sourceCanvas, width: imageWidth, height: imageHeight } = normalizeImageToCanvas(options.image);
1224
- const editorCanvas = document.createElement("canvas");
1225
- editorCanvas.style.position = "absolute";
1226
- editorCanvas.style.top = "0";
1227
- editorCanvas.style.left = "0";
1228
- editorCanvas.style.display = "block";
1229
- editorCanvas.style.boxSizing = "border-box";
1230
- editorCanvas.style.touchAction = "none";
1231
- editorCanvas.style.userSelect = "none";
1232
- editorCanvas.style.webkitUserSelect = "none";
1233
- editorCanvas.style.cursor = "crosshair";
1234
- editorCanvas.style.outline = "none";
1235
- const keyboardEnabled = options.keyboard !== false;
1236
- if (keyboardEnabled) {
1237
- editorCanvas.tabIndex = 0;
1238
- editorCanvas.setAttribute("role", "application");
1239
- editorCanvas.setAttribute("aria-label", "Document corner editor. Use arrow keys to adjust the selected corner.");
1398
+ const doc = container.ownerDocument || (typeof document !== "undefined" ? document : null);
1399
+ const runtimeGlobal = typeof window !== "undefined" ? window : globalThis;
1400
+ if (options.injectStyles !== false) {
1401
+ injectStyles(doc);
1240
1402
  }
1403
+ const { canvas: sourceCanvas, width: imageWidth, height: imageHeight } = normalizeImageToCanvas(options.image);
1404
+ const addedRootClass = !container.classList.contains("scanic-corner-editor");
1405
+ container.classList.add("scanic-corner-editor");
1406
+ if ((_a = options.classNames) == null ? void 0 : _a.root) container.classList.add(options.classNames.root);
1241
1407
  const restoreContainerStyle = {
1242
1408
  position: container.style.position,
1243
1409
  minHeight: container.style.minHeight
@@ -1248,40 +1414,123 @@ function createCornerEditor(options = {}) {
1248
1414
  container.style.position = "relative";
1249
1415
  changedContainerPosition = true;
1250
1416
  }
1417
+ applyThemeOption(options.theme);
1418
+ const editorCanvas = doc.createElement("canvas");
1419
+ editorCanvas.style.position = "absolute";
1420
+ editorCanvas.style.top = "0";
1421
+ editorCanvas.style.left = "0";
1422
+ editorCanvas.style.display = "block";
1423
+ editorCanvas.style.touchAction = "none";
1424
+ editorCanvas.style.userSelect = "none";
1425
+ editorCanvas.style.webkitUserSelect = "none";
1426
+ editorCanvas.style.cursor = "default";
1251
1427
  container.appendChild(editorCanvas);
1252
1428
  const ctx = editorCanvas.getContext("2d");
1253
1429
  if (!ctx) {
1254
1430
  throw new Error("Failed to create 2D canvas context for corner editor");
1255
1431
  }
1256
1432
  const magnifier = {
1257
- enabled: ((_a = options.magnifier) == null ? void 0 : _a.enabled) !== false,
1258
- size: ((_b = options.magnifier) == null ? void 0 : _b.size) || 110,
1259
- zoom: ((_c = options.magnifier) == null ? void 0 : _c.zoom) || 2,
1260
- margin: ((_d = options.magnifier) == null ? void 0 : _d.margin) || 16,
1261
- borderColor: ((_e = options.magnifier) == null ? void 0 : _e.borderColor) || "#ffffff",
1262
- borderWidth: ((_f = options.magnifier) == null ? void 0 : _f.borderWidth) || 2,
1263
- crosshairColor: ((_g = options.magnifier) == null ? void 0 : _g.crosshairColor) || "#ffffff",
1264
- crosshairSize: ((_h = options.magnifier) == null ? void 0 : _h.crosshairSize) || 18
1433
+ enabled: ((_b = options.magnifier) == null ? void 0 : _b.enabled) !== false,
1434
+ size: ((_c = options.magnifier) == null ? void 0 : _c.size) || 120,
1435
+ zoom: ((_d = options.magnifier) == null ? void 0 : _d.zoom) || 2,
1436
+ margin: ((_e = options.magnifier) == null ? void 0 : _e.margin) || 8,
1437
+ borderColor: ((_f = options.magnifier) == null ? void 0 : _f.borderColor) || "#ffffff",
1438
+ borderWidth: ((_g = options.magnifier) == null ? void 0 : _g.borderWidth) || 2,
1439
+ crosshairColor: ((_h = options.magnifier) == null ? void 0 : _h.crosshairColor) || "#ffffff",
1440
+ crosshairSize: ((_i = options.magnifier) == null ? void 0 : _i.crosshairSize) || 18
1265
1441
  };
1266
1442
  const nudges = {
1267
- enabled: !!((_i = options.nudges) == null ? void 0 : _i.enabled),
1268
- steps: (((_j = options.nudges) == null ? void 0 : _j.steps) && options.nudges.steps.length ? options.nudges.steps : [1, 5]).map((v) => Math.max(1, Math.round(v)))
1443
+ enabled: !!((_j = options.nudges) == null ? void 0 : _j.enabled),
1444
+ steps: (((_k = options.nudges) == null ? void 0 : _k.steps) && options.nudges.steps.length ? options.nudges.steps : [1, 10]).map((v) => Math.max(1, Math.round(v)))
1445
+ };
1446
+ const toolbar = {
1447
+ enabled: ((_l = options.toolbar) == null ? void 0 : _l.enabled) !== false,
1448
+ reset: ((_m = options.toolbar) == null ? void 0 : _m.reset) !== false,
1449
+ cancel: ((_n = options.toolbar) == null ? void 0 : _n.cancel) !== false,
1450
+ apply: ((_o = options.toolbar) == null ? void 0 : _o.apply) !== false,
1451
+ labels: {
1452
+ reset: ((_q = (_p = options.toolbar) == null ? void 0 : _p.labels) == null ? void 0 : _q.reset) || "Reset",
1453
+ cancel: ((_s = (_r = options.toolbar) == null ? void 0 : _r.labels) == null ? void 0 : _s.cancel) || "Cancel",
1454
+ apply: ((_u = (_t = options.toolbar) == null ? void 0 : _t.labels) == null ? void 0 : _u.apply) || "Apply"
1455
+ }
1269
1456
  };
1457
+ const keyboardEnabled = options.keyboard !== false;
1270
1458
  const defaultCorners = createDefaultCorners(imageWidth, imageHeight);
1271
1459
  const requestedCorners = options.corners ? cloneCorners(options.corners) : defaultCorners;
1272
1460
  let corners = cornersAreFiniteAndDistinct$1(requestedCorners) && isConvexQuadrilateral$1(requestedCorners) ? requestedCorners : defaultCorners;
1273
1461
  const initialCorners = cloneCorners(corners);
1274
1462
  let isDestroyed = false;
1275
1463
  let activeCornerKey = null;
1464
+ let focusedCornerKey = "topLeft";
1276
1465
  let dragPointerId = null;
1277
1466
  let lastPointerPosition = null;
1278
- const handleHitArea = Math.max(24, options.handleHitArea || 48);
1279
- const handleRadius = Math.max(8, Math.min(16, handleHitArea * 0.3));
1467
+ Math.max(24, options.handleHitArea || 44);
1280
1468
  const cornerOrder = ["topLeft", "topRight", "bottomRight", "bottomLeft"];
1469
+ const cornerLabels = {
1470
+ topLeft: "Top-left corner",
1471
+ topRight: "Top-right corner",
1472
+ bottomRight: "Bottom-right corner",
1473
+ bottomLeft: "Bottom-left corner"
1474
+ };
1281
1475
  let view = { scale: 1, offsetX: 0, offsetY: 0, width: 1, height: 1 };
1282
- let nudgeControls = null;
1283
- let activeNudgeCorner = "topLeft";
1284
- const runtimeGlobal = typeof window !== "undefined" ? window : globalThis;
1476
+ const resolved = { mask: "rgba(15,23,42,0.45)", edgeColor: "#6366f1", edgeWidth: 2.5 };
1477
+ const handleEls = {};
1478
+ for (const key of cornerOrder) {
1479
+ const el = doc.createElement("button");
1480
+ el.type = "button";
1481
+ el.className = "scanic-handle" + (((_v = options.classNames) == null ? void 0 : _v.handle) ? " " + options.classNames.handle : "");
1482
+ el.dataset.corner = key;
1483
+ el.setAttribute("aria-label", cornerLabels[key]);
1484
+ if (!keyboardEnabled) el.tabIndex = -1;
1485
+ el.addEventListener("pointerdown", (e) => onHandlePointerDown(key, e));
1486
+ el.addEventListener("pointermove", (e) => onHandlePointerMove(key, e));
1487
+ el.addEventListener("pointerup", (e) => onHandlePointerUp(key, e));
1488
+ el.addEventListener("pointercancel", (e) => onHandlePointerUp(key, e));
1489
+ el.addEventListener("focus", () => {
1490
+ focusedCornerKey = key;
1491
+ updateSelected();
1492
+ });
1493
+ if (keyboardEnabled) el.addEventListener("keydown", (e) => onHandleKeyDown(key, e));
1494
+ container.appendChild(el);
1495
+ handleEls[key] = el;
1496
+ }
1497
+ function resolveColor(expr, fallback) {
1498
+ const prev = editorCanvas.style.color;
1499
+ editorCanvas.style.color = `var(${expr}, ${fallback})`;
1500
+ const value = getComputedStyle(editorCanvas).color || fallback;
1501
+ editorCanvas.style.color = prev;
1502
+ return value;
1503
+ }
1504
+ function resolveTheme() {
1505
+ resolved.mask = resolveColor("--scanic-mask", "rgba(15, 23, 42, 0.45)");
1506
+ resolved.edgeColor = resolveColor("--scanic-edge-color", "#6366f1");
1507
+ const widthRaw = getComputedStyle(container).getPropertyValue("--scanic-edge-width");
1508
+ const parsed = parseFloat(widthRaw);
1509
+ resolved.edgeWidth = Number.isFinite(parsed) && parsed > 0 ? parsed : 2.5;
1510
+ }
1511
+ function applyThemeOption(theme) {
1512
+ if (!theme || typeof theme !== "object") return;
1513
+ const pxKeys = /* @__PURE__ */ new Set(["handleSize", "handleHit"]);
1514
+ const map = {
1515
+ accent: "--scanic-accent",
1516
+ mask: "--scanic-mask",
1517
+ edgeColor: "--scanic-edge-color",
1518
+ edgeWidth: "--scanic-edge-width",
1519
+ handleSize: "--scanic-handle-size",
1520
+ handleHit: "--scanic-handle-hit",
1521
+ handleColor: "--scanic-handle-color",
1522
+ handleRingColor: "--scanic-handle-ring-color",
1523
+ surface: "--scanic-surface",
1524
+ surfaceColor: "--scanic-surface-fg",
1525
+ radius: "--scanic-surface-radius"
1526
+ };
1527
+ for (const [key, cssVar] of Object.entries(map)) {
1528
+ const value = theme[key];
1529
+ if (value == null) continue;
1530
+ const cssValue = typeof value === "number" && pxKeys.has(key) ? `${value}px` : String(value);
1531
+ container.style.setProperty(cssVar, cssValue);
1532
+ }
1533
+ }
1285
1534
  function emitChange() {
1286
1535
  if (typeof options.onChange === "function") {
1287
1536
  options.onChange(cloneCorners(corners));
@@ -1326,6 +1575,7 @@ function createCornerEditor(options = {}) {
1326
1575
  width,
1327
1576
  height
1328
1577
  };
1578
+ resolveTheme();
1329
1579
  }
1330
1580
  function imageToView(point) {
1331
1581
  return {
@@ -1351,55 +1601,27 @@ function createCornerEditor(options = {}) {
1351
1601
  y: event.clientY - rect.top
1352
1602
  };
1353
1603
  }
1354
- function hitTestCorner(canvasX, canvasY) {
1355
- let hit = null;
1356
- let bestDistance = Infinity;
1357
- for (const key of cornerOrder) {
1358
- const p = imageToView(corners[key]);
1359
- const d = Math.hypot(canvasX - p.x, canvasY - p.y);
1360
- if (d <= handleHitArea && d < bestDistance) {
1361
- bestDistance = d;
1362
- hit = key;
1363
- }
1364
- }
1365
- return hit;
1366
- }
1367
- function drawPolygonOverlay() {
1604
+ function drawOverlay() {
1368
1605
  const points = cornerOrder.map((key) => imageToView(corners[key]));
1369
1606
  ctx.save();
1370
- ctx.fillStyle = "rgba(15, 23, 42, 0.40)";
1607
+ ctx.fillStyle = resolved.mask;
1371
1608
  ctx.beginPath();
1372
1609
  ctx.rect(0, 0, view.width, view.height);
1373
1610
  ctx.moveTo(points[0].x, points[0].y);
1374
- for (let i = 1; i < points.length; i++) {
1375
- ctx.lineTo(points[i].x, points[i].y);
1376
- }
1611
+ for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
1377
1612
  ctx.closePath();
1378
1613
  ctx.fill("evenodd");
1379
1614
  ctx.restore();
1380
1615
  ctx.save();
1381
- ctx.strokeStyle = "#22c55e";
1382
- ctx.lineWidth = 3;
1616
+ ctx.strokeStyle = resolved.edgeColor;
1617
+ ctx.lineWidth = resolved.edgeWidth;
1618
+ ctx.lineJoin = "round";
1383
1619
  ctx.beginPath();
1384
1620
  ctx.moveTo(points[0].x, points[0].y);
1385
- for (let i = 1; i < points.length; i++) {
1386
- ctx.lineTo(points[i].x, points[i].y);
1387
- }
1621
+ for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
1388
1622
  ctx.closePath();
1389
1623
  ctx.stroke();
1390
1624
  ctx.restore();
1391
- for (const key of cornerOrder) {
1392
- const p = imageToView(corners[key]);
1393
- const isActive = key === activeCornerKey;
1394
- drawHandle(
1395
- ctx,
1396
- p,
1397
- isActive ? handleRadius + 1 : handleRadius,
1398
- isActive ? "#f59e0b" : "#ffffff",
1399
- isActive ? "#7c2d12" : "#0f172a",
1400
- 2
1401
- );
1402
- }
1403
1625
  }
1404
1626
  function drawMagnifier() {
1405
1627
  if (!magnifier.enabled || !activeCornerKey || !lastPointerPosition) {
@@ -1445,6 +1667,19 @@ function createCornerEditor(options = {}) {
1445
1667
  ctx.stroke();
1446
1668
  ctx.restore();
1447
1669
  }
1670
+ function positionHandles() {
1671
+ for (const key of cornerOrder) {
1672
+ const p = imageToView(corners[key]);
1673
+ const el = handleEls[key];
1674
+ el.style.left = p.x + "px";
1675
+ el.style.top = p.y + "px";
1676
+ }
1677
+ }
1678
+ function updateSelected() {
1679
+ for (const key of cornerOrder) {
1680
+ handleEls[key].classList.toggle("is-selected", key === focusedCornerKey);
1681
+ }
1682
+ }
1448
1683
  function render() {
1449
1684
  if (isDestroyed) return;
1450
1685
  ctx.clearRect(0, 0, view.width, view.height);
@@ -1459,8 +1694,9 @@ function createCornerEditor(options = {}) {
1459
1694
  imageWidth * view.scale,
1460
1695
  imageHeight * view.scale
1461
1696
  );
1462
- drawPolygonOverlay();
1697
+ drawOverlay();
1463
1698
  drawMagnifier();
1699
+ positionHandles();
1464
1700
  }
1465
1701
  const raf = typeof runtimeGlobal.requestAnimationFrame === "function" ? runtimeGlobal.requestAnimationFrame.bind(runtimeGlobal) : null;
1466
1702
  const caf = typeof runtimeGlobal.cancelAnimationFrame === "function" ? runtimeGlobal.cancelAnimationFrame.bind(runtimeGlobal) : null;
@@ -1487,110 +1723,58 @@ function createCornerEditor(options = {}) {
1487
1723
  return false;
1488
1724
  }
1489
1725
  corners = candidate;
1490
- activeNudgeCorner = nextCornerKey;
1726
+ focusedCornerKey = nextCornerKey;
1727
+ updateSelected();
1491
1728
  emitChange();
1492
1729
  scheduleRender();
1493
1730
  return true;
1494
1731
  }
1495
- function buildNudgeControls() {
1496
- if (!nudges.enabled) {
1497
- return;
1498
- }
1499
- nudgeControls = document.createElement("div");
1500
- nudgeControls.style.position = "absolute";
1501
- nudgeControls.style.right = "8px";
1502
- nudgeControls.style.bottom = "8px";
1503
- nudgeControls.style.background = "rgba(15, 23, 42, 0.9)";
1504
- nudgeControls.style.border = "1px solid rgba(148, 163, 184, 0.5)";
1505
- nudgeControls.style.borderRadius = "10px";
1506
- nudgeControls.style.padding = "8px";
1507
- nudgeControls.style.display = "grid";
1508
- nudgeControls.style.gridTemplateColumns = "repeat(4, auto)";
1509
- nudgeControls.style.gap = "6px";
1510
- nudgeControls.style.zIndex = "2";
1511
- const makeButton = (glyph, ariaLabel, dx, dy, step) => {
1512
- const btn = document.createElement("button");
1513
- btn.type = "button";
1514
- btn.textContent = glyph + (step > 1 ? " " + step : "");
1515
- btn.setAttribute("aria-label", ariaLabel);
1516
- btn.style.border = "1px solid #475569";
1517
- btn.style.background = "#0f172a";
1518
- btn.style.color = "#e2e8f0";
1519
- btn.style.borderRadius = "6px";
1520
- btn.style.padding = "4px 8px";
1521
- btn.style.fontSize = "13px";
1522
- btn.style.lineHeight = "1";
1523
- btn.style.cursor = "pointer";
1524
- btn.addEventListener("click", () => {
1525
- const current = corners[activeNudgeCorner];
1526
- setCorner(activeNudgeCorner, {
1527
- x: current.x + dx * step,
1528
- y: current.y + dy * step
1529
- });
1530
- });
1531
- return btn;
1532
- };
1533
- for (const step of nudges.steps) {
1534
- nudgeControls.appendChild(makeButton("←", `Move left ${step}px`, -1, 0, step));
1535
- nudgeControls.appendChild(makeButton("→", `Move right ${step}px`, 1, 0, step));
1536
- nudgeControls.appendChild(makeButton("↑", `Move up ${step}px`, 0, -1, step));
1537
- nudgeControls.appendChild(makeButton("↓", `Move down ${step}px`, 0, 1, step));
1538
- }
1539
- container.appendChild(nudgeControls);
1540
- }
1541
- function handlePointerDown(event) {
1732
+ function onHandlePointerDown(key, event) {
1542
1733
  if (isDestroyed) return;
1543
- const point = getEventCanvasPoint(event);
1544
- const hitCorner = hitTestCorner(point.x, point.y);
1545
- if (!hitCorner) return;
1546
- if (typeof event.preventDefault === "function") {
1547
- event.preventDefault();
1548
- }
1549
- activeCornerKey = hitCorner;
1550
- activeNudgeCorner = hitCorner;
1734
+ if (typeof event.preventDefault === "function") event.preventDefault();
1735
+ activeCornerKey = key;
1736
+ focusedCornerKey = key;
1737
+ updateSelected();
1551
1738
  dragPointerId = event.pointerId;
1552
- lastPointerPosition = point;
1553
- editorCanvas.style.cursor = "grabbing";
1554
- if (keyboardEnabled && typeof editorCanvas.focus === "function") {
1739
+ lastPointerPosition = getEventCanvasPoint(event);
1740
+ handleEls[key].classList.add("is-active");
1741
+ if (keyboardEnabled && typeof handleEls[key].focus === "function") {
1555
1742
  try {
1556
- editorCanvas.focus({ preventScroll: true });
1743
+ handleEls[key].focus({ preventScroll: true });
1557
1744
  } catch (_) {
1558
- editorCanvas.focus();
1745
+ handleEls[key].focus();
1559
1746
  }
1560
1747
  }
1561
- if (editorCanvas.setPointerCapture && dragPointerId !== void 0) {
1562
- editorCanvas.setPointerCapture(dragPointerId);
1748
+ if (handleEls[key].setPointerCapture && event.pointerId != null) {
1749
+ try {
1750
+ handleEls[key].setPointerCapture(event.pointerId);
1751
+ } catch (_) {
1752
+ }
1563
1753
  }
1564
1754
  scheduleRender();
1565
1755
  }
1566
- function handlePointerMove(event) {
1567
- if (isDestroyed) return;
1568
- const point = getEventCanvasPoint(event);
1569
- if (!activeCornerKey) {
1570
- editorCanvas.style.cursor = hitTestCorner(point.x, point.y) ? "grab" : "crosshair";
1571
- return;
1572
- }
1756
+ function onHandlePointerMove(key, event) {
1757
+ if (isDestroyed || activeCornerKey !== key) return;
1573
1758
  if (dragPointerId !== null && event.pointerId !== dragPointerId) return;
1574
- lastPointerPosition = point;
1575
- const imagePoint = viewToImage(point.x, point.y);
1576
- setCorner(activeCornerKey, imagePoint);
1759
+ lastPointerPosition = getEventCanvasPoint(event);
1760
+ setCorner(key, viewToImage(lastPointerPosition.x, lastPointerPosition.y));
1577
1761
  }
1578
- function handlePointerUp(event) {
1579
- if (!activeCornerKey) return;
1762
+ function onHandlePointerUp(key, event) {
1763
+ if (activeCornerKey !== key) return;
1580
1764
  if (dragPointerId !== null && event.pointerId !== dragPointerId) return;
1581
- if (editorCanvas.releasePointerCapture && dragPointerId !== null && dragPointerId !== void 0) {
1765
+ if (handleEls[key].releasePointerCapture && dragPointerId != null) {
1582
1766
  try {
1583
- editorCanvas.releasePointerCapture(dragPointerId);
1767
+ handleEls[key].releasePointerCapture(dragPointerId);
1584
1768
  } catch (_) {
1585
1769
  }
1586
1770
  }
1771
+ handleEls[key].classList.remove("is-active");
1587
1772
  activeCornerKey = null;
1588
1773
  dragPointerId = null;
1589
1774
  lastPointerPosition = null;
1590
- editorCanvas.style.cursor = "crosshair";
1591
1775
  scheduleRender();
1592
1776
  }
1593
- function handleKeyDown(event) {
1777
+ function onHandleKeyDown(key, event) {
1594
1778
  if (isDestroyed || !keyboardEnabled) return;
1595
1779
  if (event.key === "Enter") {
1596
1780
  event.preventDefault();
@@ -1611,85 +1795,78 @@ function createCornerEditor(options = {}) {
1611
1795
  else return;
1612
1796
  event.preventDefault();
1613
1797
  const step = event.shiftKey ? nudges.steps[nudges.steps.length - 1] || 10 : 1;
1614
- const current = corners[activeNudgeCorner];
1615
- setCorner(activeNudgeCorner, {
1616
- x: current.x + dx * step,
1617
- y: current.y + dy * step
1618
- });
1798
+ setCorner(key, { x: corners[key].x + dx * step, y: corners[key].y + dy * step });
1619
1799
  }
1620
- function handleMouseDown(event) {
1621
- handlePointerDown({
1622
- clientX: event.clientX,
1623
- clientY: event.clientY,
1624
- pointerId: 1
1625
- });
1800
+ let toolbarEl = null;
1801
+ let nudgeControls = null;
1802
+ let expertBtn = null;
1803
+ let expertVisible = false;
1804
+ const hasExpertToggle = toolbar.enabled && nudges.enabled;
1805
+ function makeButton({ html, text, title, className, onClick }) {
1806
+ const btn = doc.createElement("button");
1807
+ btn.type = "button";
1808
+ if (html != null) btn.innerHTML = html;
1809
+ else btn.textContent = text;
1810
+ if (title) {
1811
+ btn.title = title;
1812
+ btn.setAttribute("aria-label", title);
1813
+ }
1814
+ if (className) btn.className = className;
1815
+ btn.addEventListener("click", onClick);
1816
+ return btn;
1817
+ }
1818
+ function setExpert(on) {
1819
+ expertVisible = on;
1820
+ if (nudgeControls) nudgeControls.style.display = on ? "" : "none";
1821
+ if (expertBtn) {
1822
+ expertBtn.classList.toggle("is-on", on);
1823
+ expertBtn.setAttribute("aria-pressed", String(on));
1824
+ }
1825
+ }
1826
+ function buildToolbar() {
1827
+ var _a2;
1828
+ if (!toolbar.enabled) return;
1829
+ if (!toolbar.reset && !toolbar.cancel && !toolbar.apply && !hasExpertToggle) return;
1830
+ toolbarEl = doc.createElement("div");
1831
+ toolbarEl.className = "scanic-toolbar" + (((_a2 = options.classNames) == null ? void 0 : _a2.toolbar) ? " " + options.classNames.toolbar : "");
1832
+ if (toolbar.reset) {
1833
+ toolbarEl.appendChild(makeButton({ html: ICONS.reset, title: toolbar.labels.reset, className: "scanic-btn-reset", onClick: () => publicReset() }));
1834
+ }
1835
+ if (hasExpertToggle) {
1836
+ expertBtn = makeButton({ html: ICONS.expert, title: "Precision nudge (expert)", className: "scanic-btn-expert", onClick: () => setExpert(!expertVisible) });
1837
+ expertBtn.setAttribute("aria-pressed", "false");
1838
+ toolbarEl.appendChild(expertBtn);
1839
+ }
1840
+ if (toolbar.cancel) {
1841
+ toolbarEl.appendChild(makeButton({ html: ICONS.cancel, title: toolbar.labels.cancel, className: "scanic-btn-cancel", onClick: () => cancelEditor() }));
1842
+ }
1843
+ if (toolbar.apply) {
1844
+ toolbarEl.appendChild(makeButton({ html: ICONS.apply, title: toolbar.labels.apply, className: "scanic-btn-apply", onClick: () => confirmEditor() }));
1845
+ }
1846
+ container.appendChild(toolbarEl);
1626
1847
  }
1627
- function handleMouseMove(event) {
1628
- handlePointerMove({
1629
- clientX: event.clientX,
1630
- clientY: event.clientY,
1631
- pointerId: 1
1848
+ function buildNudgeControls() {
1849
+ var _a2;
1850
+ if (!nudges.enabled) return;
1851
+ nudgeControls = doc.createElement("div");
1852
+ nudgeControls.className = "scanic-nudges" + (((_a2 = options.classNames) == null ? void 0 : _a2.nudges) ? " " + options.classNames.nudges : "");
1853
+ const makeNudge = (glyph, label, dx, dy, step) => makeButton({
1854
+ text: glyph,
1855
+ title: label,
1856
+ className: "scanic-btn-nudge",
1857
+ onClick: () => {
1858
+ const current = corners[focusedCornerKey];
1859
+ setCorner(focusedCornerKey, { x: current.x + dx * step, y: current.y + dy * step });
1860
+ }
1632
1861
  });
1633
- }
1634
- function handleTouchStart(event) {
1635
- const touch = event.touches[0];
1636
- if (!touch) return;
1637
- const point = getEventCanvasPoint(touch);
1638
- if (hitTestCorner(point.x, point.y)) {
1639
- event.preventDefault();
1862
+ for (const step of nudges.steps) {
1863
+ nudgeControls.appendChild(makeNudge("←", `Move left ${step}px`, -1, 0, step));
1864
+ nudgeControls.appendChild(makeNudge("↑", `Move up ${step}px`, 0, -1, step));
1865
+ nudgeControls.appendChild(makeNudge("↓", `Move down ${step}px`, 0, 1, step));
1866
+ nudgeControls.appendChild(makeNudge("→", `Move right ${step}px`, 1, 0, step));
1640
1867
  }
1641
- handlePointerDown({
1642
- clientX: touch.clientX,
1643
- clientY: touch.clientY,
1644
- pointerId: 2
1645
- });
1646
- }
1647
- function handleTouchMove(event) {
1648
- const touch = event.touches[0];
1649
- if (!touch) return;
1650
- event.preventDefault();
1651
- handlePointerMove({
1652
- clientX: touch.clientX,
1653
- clientY: touch.clientY,
1654
- pointerId: 2
1655
- });
1656
- }
1657
- function attachEvents() {
1658
- if (typeof runtimeGlobal.PointerEvent !== "undefined") {
1659
- editorCanvas.addEventListener("pointerdown", handlePointerDown);
1660
- editorCanvas.addEventListener("pointermove", handlePointerMove);
1661
- editorCanvas.addEventListener("pointerup", handlePointerUp);
1662
- editorCanvas.addEventListener("pointercancel", handlePointerUp);
1663
- } else {
1664
- editorCanvas.addEventListener("mousedown", handleMouseDown);
1665
- if (typeof runtimeGlobal.addEventListener === "function") {
1666
- runtimeGlobal.addEventListener("mousemove", handleMouseMove);
1667
- runtimeGlobal.addEventListener("mouseup", handlePointerUp);
1668
- }
1669
- editorCanvas.addEventListener("touchstart", handleTouchStart, { passive: false });
1670
- editorCanvas.addEventListener("touchmove", handleTouchMove, { passive: false });
1671
- editorCanvas.addEventListener("touchend", handlePointerUp);
1672
- editorCanvas.addEventListener("touchcancel", handlePointerUp);
1673
- }
1674
- if (keyboardEnabled) {
1675
- editorCanvas.addEventListener("keydown", handleKeyDown);
1676
- }
1677
- }
1678
- function detachEvents() {
1679
- editorCanvas.removeEventListener("pointerdown", handlePointerDown);
1680
- editorCanvas.removeEventListener("pointermove", handlePointerMove);
1681
- editorCanvas.removeEventListener("pointerup", handlePointerUp);
1682
- editorCanvas.removeEventListener("pointercancel", handlePointerUp);
1683
- editorCanvas.removeEventListener("mousedown", handleMouseDown);
1684
- if (typeof runtimeGlobal.removeEventListener === "function") {
1685
- runtimeGlobal.removeEventListener("mousemove", handleMouseMove);
1686
- runtimeGlobal.removeEventListener("mouseup", handlePointerUp);
1687
- }
1688
- editorCanvas.removeEventListener("touchstart", handleTouchStart);
1689
- editorCanvas.removeEventListener("touchmove", handleTouchMove);
1690
- editorCanvas.removeEventListener("touchend", handlePointerUp);
1691
- editorCanvas.removeEventListener("touchcancel", handlePointerUp);
1692
- editorCanvas.removeEventListener("keydown", handleKeyDown);
1868
+ container.appendChild(nudgeControls);
1869
+ if (hasExpertToggle) nudgeControls.style.display = "none";
1693
1870
  }
1694
1871
  let dprCleanup = null;
1695
1872
  function watchDevicePixelRatio() {
@@ -1725,6 +1902,11 @@ function createCornerEditor(options = {}) {
1725
1902
  options.onCancel();
1726
1903
  }
1727
1904
  }
1905
+ function publicReset() {
1906
+ corners = cloneCorners(initialCorners);
1907
+ emitChange();
1908
+ scheduleRender();
1909
+ }
1728
1910
  const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => {
1729
1911
  if (isDestroyed) return;
1730
1912
  const next = computeDisplaySize();
@@ -1735,12 +1917,13 @@ function createCornerEditor(options = {}) {
1735
1917
  scheduleRender();
1736
1918
  }) : null;
1737
1919
  updateCanvasSize();
1920
+ buildToolbar();
1738
1921
  buildNudgeControls();
1739
- attachEvents();
1740
1922
  watchDevicePixelRatio();
1741
1923
  if (resizeObserver) {
1742
1924
  resizeObserver.observe(container);
1743
1925
  }
1926
+ updateSelected();
1744
1927
  render();
1745
1928
  return {
1746
1929
  getCorners() {
@@ -1756,9 +1939,7 @@ function createCornerEditor(options = {}) {
1756
1939
  return true;
1757
1940
  },
1758
1941
  reset() {
1759
- corners = cloneCorners(initialCorners);
1760
- emitChange();
1761
- scheduleRender();
1942
+ publicReset();
1762
1943
  },
1763
1944
  nudge(cornerKey, dx, dy, step = 1) {
1764
1945
  if (!cornerOrder.includes(cornerKey)) {
@@ -1769,6 +1950,12 @@ function createCornerEditor(options = {}) {
1769
1950
  y: corners[cornerKey].y + dy * step
1770
1951
  });
1771
1952
  },
1953
+ /** Re-read CSS variables into the canvas layer after a runtime theme change. */
1954
+ refreshTheme(theme) {
1955
+ applyThemeOption(theme);
1956
+ resolveTheme();
1957
+ scheduleRender();
1958
+ },
1772
1959
  confirm() {
1773
1960
  return confirmEditor();
1774
1961
  },
@@ -1776,6 +1963,7 @@ function createCornerEditor(options = {}) {
1776
1963
  cancelEditor();
1777
1964
  },
1778
1965
  destroy() {
1966
+ var _a2;
1779
1967
  if (isDestroyed) return;
1780
1968
  isDestroyed = true;
1781
1969
  if (rafId !== null && caf) {
@@ -1789,13 +1977,15 @@ function createCornerEditor(options = {}) {
1789
1977
  if (resizeObserver) {
1790
1978
  resizeObserver.disconnect();
1791
1979
  }
1792
- detachEvents();
1793
- if (nudgeControls && nudgeControls.parentNode) {
1794
- nudgeControls.parentNode.removeChild(nudgeControls);
1795
- }
1796
- if (editorCanvas.parentNode) {
1797
- editorCanvas.parentNode.removeChild(editorCanvas);
1980
+ for (const key of cornerOrder) {
1981
+ const el = handleEls[key];
1982
+ if (el && el.parentNode) el.parentNode.removeChild(el);
1798
1983
  }
1984
+ if (toolbarEl && toolbarEl.parentNode) toolbarEl.parentNode.removeChild(toolbarEl);
1985
+ if (nudgeControls && nudgeControls.parentNode) nudgeControls.parentNode.removeChild(nudgeControls);
1986
+ if (editorCanvas.parentNode) editorCanvas.parentNode.removeChild(editorCanvas);
1987
+ if (addedRootClass) container.classList.remove("scanic-corner-editor");
1988
+ if ((_a2 = options.classNames) == null ? void 0 : _a2.root) container.classList.remove(options.classNames.root);
1799
1989
  if (changedContainerPosition) {
1800
1990
  container.style.position = restoreContainerStyle.position;
1801
1991
  }