@masters-union/union-stack 0.3.12 → 0.3.14

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/picker.js CHANGED
@@ -660,8 +660,6 @@ var ImageEditor = class {
660
660
  constructor(opts) {
661
661
  this.opts = opts;
662
662
  this.cropBox = null;
663
- // What's currently rendered to the user, sized to fit MAX_DISPLAY.
664
- this.displayScale = 1;
665
663
  // Tracks whether the working canvas has transparency (post-circle), so
666
664
  // export() can pick PNG vs JPEG correctly.
667
665
  this.hasAlpha = false;
@@ -673,12 +671,13 @@ var ImageEditor = class {
673
671
  this.dragStart = null;
674
672
  this.onPointerMove = (e) => {
675
673
  if (!this.dragKind || !this.dragStart || !this.cropRect) return;
676
- const dx = e.clientX - this.dragStart.px;
677
- const dy = e.clientY - this.dragStart.py;
674
+ const m = this.canvasMetrics();
675
+ const dx = (e.clientX - this.dragStart.px) / (m.scaleX || 1);
676
+ const dy = (e.clientY - this.dragStart.py) / (m.scaleY || 1);
678
677
  const start = this.dragStart.rect;
679
- const maxW = this.displayCanvas.width;
680
- const maxH = this.displayCanvas.height;
681
- const min = MIN_CROP;
678
+ const maxW = this.working.width;
679
+ const maxH = this.working.height;
680
+ const min = this.minCropImg();
682
681
  if (this.aspect && this.dragKind !== "move") {
683
682
  switch (this.dragKind) {
684
683
  case "se":
@@ -874,7 +873,6 @@ var ImageEditor = class {
874
873
  const dh = Math.max(1, Math.round(wh * scale));
875
874
  this.displayCanvas.width = dw;
876
875
  this.displayCanvas.height = dh;
877
- this.displayScale = scale;
878
876
  const ctx = this.displayCanvas.getContext("2d");
879
877
  if (!ctx) return;
880
878
  ctx.clearRect(0, 0, dw, dh);
@@ -893,43 +891,44 @@ var ImageEditor = class {
893
891
  this.mode = "crop";
894
892
  this.cropTool.dataset.active = "true";
895
893
  this.cropTool.innerHTML = `${ICON.check} <span>Apply crop</span>`;
896
- const dw = this.displayCanvas.width;
897
- const dh = this.displayCanvas.height;
894
+ const iw = this.working.width;
895
+ const ih = this.working.height;
898
896
  if (this.aspect) {
899
897
  const a = this.aspect;
900
- let w = dw;
898
+ const minImg = this.minCropImg();
899
+ let w = iw;
901
900
  let h = w / a;
902
- if (h > dh) {
903
- h = dh;
901
+ if (h > ih) {
902
+ h = ih;
904
903
  w = h * a;
905
904
  }
906
905
  w = w * 0.9;
907
906
  h = w / a;
908
- if (w < MIN_CROP) {
909
- w = MIN_CROP;
907
+ if (w < minImg) {
908
+ w = minImg;
910
909
  h = w / a;
911
910
  }
912
- if (h < MIN_CROP) {
913
- h = MIN_CROP;
911
+ if (h < minImg) {
912
+ h = minImg;
914
913
  w = h * a;
915
914
  }
916
- if (w > dw) {
917
- w = dw;
915
+ if (w > iw) {
916
+ w = iw;
918
917
  h = w / a;
919
918
  }
920
- if (h > dh) {
921
- h = dh;
919
+ if (h > ih) {
920
+ h = ih;
922
921
  w = h * a;
923
922
  }
924
923
  this.cropRect = {
925
- x: Math.round((dw - w) / 2),
926
- y: Math.round((dh - h) / 2),
924
+ x: Math.round((iw - w) / 2),
925
+ y: Math.round((ih - h) / 2),
927
926
  w,
928
927
  h
929
928
  };
930
929
  } else {
931
- const inset = Math.round(Math.min(dw, dh) * 0.1);
932
- this.cropRect = { x: inset, y: inset, w: dw - inset * 2, h: dh - inset * 2 };
930
+ const inset = Math.round(Math.min(iw, ih) * 0.1);
931
+ this.cropRect = { x: inset, y: inset, w: iw - inset * 2, h: ih - inset * 2 };
933
932
  }
934
933
  this.renderCropBox();
935
934
  const locked = this.aspect !== null;
@@ -964,28 +963,28 @@ var ImageEditor = class {
964
963
  }
965
964
  // Push the current crop frame size (in image/output pixels) into the inputs,
966
965
  // skipping whichever the user is actively editing so we don't fight typing.
966
+ // cropRect is already in image px, so this is a direct read.
967
967
  syncCropDimsInputs() {
968
968
  if (!this.cropRect || !this.widthInput || !this.heightInput) return;
969
- const s = this.displayScale || 1;
970
- const wImg = Math.max(1, Math.round(this.cropRect.w / s));
971
- const hImg = Math.max(1, Math.round(this.cropRect.h / s));
969
+ const wImg = Math.max(1, Math.round(this.cropRect.w));
970
+ const hImg = Math.max(1, Math.round(this.cropRect.h));
972
971
  if (document.activeElement !== this.widthInput) this.widthInput.value = String(wImg);
973
972
  if (document.activeElement !== this.heightInput) this.heightInput.value = String(hImg);
974
973
  }
975
- // Free-form only: resize the crop frame from a typed pixel dimension. Anchored
976
- // at the frame's current top-left and clamped to the canvas + the min size.
974
+ // Free-form only: resize the crop frame from a typed output-pixel dimension.
975
+ // Anchored at the frame's current top-left and clamped to the image + min size.
977
976
  onDimInput(axis) {
978
977
  if (this.aspect !== null || !this.cropRect) return;
979
- const s = this.displayScale || 1;
980
978
  const input = axis === "w" ? this.widthInput : this.heightInput;
981
979
  const valImg = parseInt(input.value, 10);
982
980
  if (!Number.isFinite(valImg) || valImg <= 0) return;
981
+ const min = this.minCropImg();
983
982
  if (axis === "w") {
984
- const maxW = this.displayCanvas.width - this.cropRect.x;
985
- this.cropRect = { ...this.cropRect, w: clamp(Math.round(valImg * s), MIN_CROP, maxW) };
983
+ const maxW = this.working.width - this.cropRect.x;
984
+ this.cropRect = { ...this.cropRect, w: clamp(valImg, min, maxW) };
986
985
  } else {
987
- const maxH = this.displayCanvas.height - this.cropRect.y;
988
- this.cropRect = { ...this.cropRect, h: clamp(Math.round(valImg * s), MIN_CROP, maxH) };
986
+ const maxH = this.working.height - this.cropRect.y;
987
+ this.cropRect = { ...this.cropRect, h: clamp(valImg, min, maxH) };
989
988
  }
990
989
  this.updateCropBox();
991
990
  }
@@ -1008,16 +1007,35 @@ var ImageEditor = class {
1008
1007
  this.cropBox = box;
1009
1008
  this.updateCropBox();
1010
1009
  }
1010
+ // The crop rect is stored in IMAGE (working-canvas / output) pixels — the same
1011
+ // space we ultimately crop in — so the on-screen selection always matches the
1012
+ // pixels we extract, regardless of how CSS scales the displayed canvas. These
1013
+ // metrics convert between image px and the canvas's live on-screen size.
1014
+ canvasMetrics() {
1015
+ const c = this.displayCanvas.getBoundingClientRect();
1016
+ const w = this.canvasWrap.getBoundingClientRect();
1017
+ return {
1018
+ scaleX: c.width / (this.working.width || 1),
1019
+ // CSS px per image px
1020
+ scaleY: c.height / (this.working.height || 1),
1021
+ offX: c.left - w.left,
1022
+ // canvas offset inside wrap
1023
+ offY: c.top - w.top
1024
+ };
1025
+ }
1026
+ // Minimum crop size in IMAGE px, sized so the handles stay ~MIN_CROP px apart
1027
+ // on screen no matter how zoomed the canvas is.
1028
+ minCropImg() {
1029
+ const { scaleX } = this.canvasMetrics();
1030
+ return scaleX > 0 ? MIN_CROP / scaleX : MIN_CROP;
1031
+ }
1011
1032
  updateCropBox() {
1012
1033
  if (!this.cropBox || !this.cropRect) return;
1013
- const canvasRect = this.displayCanvas.getBoundingClientRect();
1014
- const wrapRect = this.canvasWrap.getBoundingClientRect();
1015
- const left = canvasRect.left - wrapRect.left + this.cropRect.x;
1016
- const top = canvasRect.top - wrapRect.top + this.cropRect.y;
1017
- this.cropBox.style.left = `${left}px`;
1018
- this.cropBox.style.top = `${top}px`;
1019
- this.cropBox.style.width = `${this.cropRect.w}px`;
1020
- this.cropBox.style.height = `${this.cropRect.h}px`;
1034
+ const m = this.canvasMetrics();
1035
+ this.cropBox.style.left = `${m.offX + this.cropRect.x * m.scaleX}px`;
1036
+ this.cropBox.style.top = `${m.offY + this.cropRect.y * m.scaleY}px`;
1037
+ this.cropBox.style.width = `${this.cropRect.w * m.scaleX}px`;
1038
+ this.cropBox.style.height = `${this.cropRect.h * m.scaleY}px`;
1021
1039
  this.syncCropDimsInputs();
1022
1040
  }
1023
1041
  // ---- drag handlers (crop) ----------------------------------------------
@@ -1035,12 +1053,13 @@ var ImageEditor = class {
1035
1053
  // the ratio exact; both are clamped so the rect stays on-canvas.
1036
1054
  lockedRectFromDrag(anchorX, anchorY, desiredW, signX, signY) {
1037
1055
  const a = this.aspect;
1038
- const maxW = this.displayCanvas.width;
1039
- const maxH = this.displayCanvas.height;
1040
- let w = Math.max(MIN_CROP, desiredW);
1056
+ const maxW = this.working.width;
1057
+ const maxH = this.working.height;
1058
+ const min = this.minCropImg();
1059
+ let w = Math.max(min, desiredW);
1041
1060
  let h = w / a;
1042
- if (h < MIN_CROP) {
1043
- h = MIN_CROP;
1061
+ if (h < min) {
1062
+ h = min;
1044
1063
  w = h * a;
1045
1064
  }
1046
1065
  const roomX = signX > 0 ? maxW - anchorX : anchorX;
@@ -1057,11 +1076,10 @@ var ImageEditor = class {
1057
1076
  this.exitCropMode();
1058
1077
  return;
1059
1078
  }
1060
- const s = this.displayScale;
1061
- const sx = Math.round(this.cropRect.x / s);
1062
- const sy = Math.round(this.cropRect.y / s);
1063
- const sw = Math.round(this.cropRect.w / s);
1064
- const sh = Math.round(this.cropRect.h / s);
1079
+ const sx = clamp(Math.round(this.cropRect.x), 0, this.working.width - 1);
1080
+ const sy = clamp(Math.round(this.cropRect.y), 0, this.working.height - 1);
1081
+ const sw = clamp(Math.round(this.cropRect.w), 1, this.working.width - sx);
1082
+ const sh = clamp(Math.round(this.cropRect.h), 1, this.working.height - sy);
1065
1083
  const next = document.createElement("canvas");
1066
1084
  next.width = sw;
1067
1085
  next.height = sh;
@@ -1126,6 +1144,7 @@ var ImageEditor = class {
1126
1144
  // ---- apply --------------------------------------------------------------
1127
1145
  async applyAndClose() {
1128
1146
  this.applyBtn.disabled = true;
1147
+ if (this.mode === "crop") this.applyCrop();
1129
1148
  try {
1130
1149
  const file = await this.exportFile();
1131
1150
  this.opts.onApply(file);
@@ -1384,10 +1403,10 @@ var Picker = class {
1384
1403
  this.$urlSource = this.renderUrlSource();
1385
1404
  body.appendChild(this.$urlSource);
1386
1405
  }
1387
- this.activateSource(sources[0] ?? "device");
1388
1406
  this.$list = el2("div", "us-file-list");
1389
1407
  body.appendChild(this.$list);
1390
1408
  panel.appendChild(body);
1409
+ this.activateSource(sources[0] ?? "device");
1391
1410
  const autoUpload = this.opts.autoUpload === true;
1392
1411
  const actions = el2("div", "us-actions");
1393
1412
  this.$summary = el2("div", "us-actions-summary", "");