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