cropper: fix incorrect image positioning after zoom-out

This commit is contained in:
or-else
2025-08-21 10:13:43 +03:00
parent 5bb829a6bc
commit 89e9b85551
11 changed files with 52 additions and 43 deletions
+2 -2
View File
@@ -3103,8 +3103,8 @@ ul.tabbar li.active > a {
.cropper .cutout { .cropper .cutout {
position: absolute; position: absolute;
width: 17rem; width: 17.8rem;
height: 17rem; height: 17.8rem;
border: 1px dashed var(--clr-cropper-cutout-border); border: 1px dashed var(--clr-cropper-cutout-border);
margin: auto; margin: auto;
top: 0; top: 0;
+1 -1
View File
File diff suppressed because one or more lines are too long
+22 -18
View File
@@ -2,14 +2,14 @@
import React from 'react'; import React from 'react';
const DEFAULT_MAX_ZOOM = 2.5; const DEFAULT_MAX_ZOOM = 8;
export default class Cropper extends React.Component { export default class Cropper extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
// Coordinates of the top left corner of the image in container coordinates. // Coordinates of the top left corner of the image in bounding box (container) coordinates.
panX: 0, panX: 0,
panY: 0, panY: 0,
// Origin of the zoom in image coordinates. // Origin of the zoom in image coordinates.
@@ -32,7 +32,7 @@ export default class Cropper extends React.Component {
// The main container (static). // The main container (static).
this.boundingBox = React.createRef(); this.boundingBox = React.createRef();
// The original image width and height before any scaling. // The original image width and height before any scaling (does not change).
this.imageWidth = 0; this.imageWidth = 0;
this.imageHeight = 0; this.imageHeight = 0;
@@ -42,10 +42,10 @@ export default class Cropper extends React.Component {
// Length of the previous mouse move when dragging. // Length of the previous mouse move when dragging.
this.prevDistance = 0; this.prevDistance = 0;
// Bounding rectangles of static elements. // Bounding rectangles of static elements (does not change).
this.cutoutRect = {}; this.cutoutRect = {};
this.bBoxRect = {}; this.bBoxRect = {};
// Center of the bounding box. // Center of the bounding box (does not change).
this.originX = 0; this.originX = 0;
this.originY = 0; this.originY = 0;
@@ -70,7 +70,7 @@ export default class Cropper extends React.Component {
this.originX = this.bBoxRect.width / 2; this.originX = this.bBoxRect.width / 2;
this.originY = this.bBoxRect.height / 2; this.originY = this.bBoxRect.height / 2;
// The rectangle position is in viewport coordinates. // The cutout circle bounds in viewport coordinates.
this.cutoutRect = this.cutout.current.getBoundingClientRect(); this.cutoutRect = this.cutout.current.getBoundingClientRect();
} }
@@ -127,7 +127,7 @@ export default class Cropper extends React.Component {
const minZoom = Math.max(this.cutoutRect.width / imgRect.width, this.cutoutRect.height / imgRect.height); const minZoom = Math.max(this.cutoutRect.width / imgRect.width, this.cutoutRect.height / imgRect.height);
this.setState({ this.setState({
minZoom: minZoom, minZoom: minZoom,
maxZoom: Math.max(DEFAULT_MAX_ZOOM, minZoom + 1) maxZoom: minZoom * DEFAULT_MAX_ZOOM
}); });
// Initial zoom level fills the bounding box at the smallest image dimension and overflows the largest, i.e. "fill" not "fit". // Initial zoom level fills the bounding box at the smallest image dimension and overflows the largest, i.e. "fill" not "fit".
@@ -150,25 +150,29 @@ export default class Cropper extends React.Component {
let panY = this.state.panY; let panY = this.state.panY;
// Ensure that the image at the new zoom is not outside of the cutout boundaries. // Ensure that the image at the new zoom is not outside of the cutout boundaries.
// Calculate image coordinates at the new zoom. // Calculate image coordinates at the new zoom.
const imgLeft = this.originX - (this.originX - panX) * zoom; const imgLeft = this.originX - (this.originX - panX) * zoom;
const imgRight = imgLeft + this.imageWidth * zoom; const imgRight = imgLeft + this.imageWidth * zoom;
const imgTop = this.originY - (this.originY - panY) * zoom;
const imgBottom = imgTop + this.imageHeight * zoom;
// Cutout in the same coordinates as the image. // Cutout in the same coordinates as the image.
const coLeft = this.cutoutRect.left - this.bBoxRect.left; const coLeft = this.cutoutRect.left - this.bBoxRect.left;
const coRight = coLeft + this.cutoutRect.width; const coRight = coLeft + this.cutoutRect.width;
if (coLeft < imgLeft) {
panX -= imgLeft - coLeft;
} else if (coRight > imgRight) {
panX += coRight - imgRight;
}
const imgTop = this.originY - (this.originY - panY) * zoom;
const imgBottom = imgTop + this.imageHeight * zoom;
const coTop = this.cutoutRect.top - this.bBoxRect.top; const coTop = this.cutoutRect.top - this.bBoxRect.top;
const coBottom = coTop + this.cutoutRect.height; const coBottom = coTop + this.cutoutRect.height;
if (coLeft < imgLeft) {
panX -= (imgLeft - coLeft) / zoom;
} else if (coRight > imgRight) {
panX += (coRight - imgRight) / zoom;
}
if (coTop < imgTop) { if (coTop < imgTop) {
panY -= imgTop - coTop; panY -= (imgTop - coTop) / zoom;
} else if (coBottom > imgBottom) { } else if (coBottom > imgBottom) {
panY += coBottom - imgBottom; panY += (coBottom - imgBottom) /zoom;
} }
this.positionAll(panX, panY, zoom); this.positionAll(panX, panY, zoom);
@@ -203,9 +207,9 @@ export default class Cropper extends React.Component {
const imgRect = this.preview.current.getBoundingClientRect(); const imgRect = this.preview.current.getBoundingClientRect();
// Check if the new position is within the boundaries, and if not if it's closer to them. // Check if the new position is within the boundaries, and if not if it's closer to them.
let panX = Cropper.checkBound(this.state.panX, [imgRect.left, imgRect.right], const panX = Cropper.checkBound(this.state.panX, [imgRect.left, imgRect.right],
[this.cutoutRect.left, this.cutoutRect.right], dX); [this.cutoutRect.left, this.cutoutRect.right], dX);
let panY = Cropper.checkBound(this.state.panY, [imgRect.top, imgRect.bottom], const panY = Cropper.checkBound(this.state.panY, [imgRect.top, imgRect.bottom],
[this.cutoutRect.top, this.cutoutRect.bottom], dY); [this.cutoutRect.top, this.cutoutRect.bottom], dY);
this.positionAll(panX, panY, this.state.zoom); this.positionAll(panX, panY, this.state.zoom);
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+15 -15
View File
@@ -17168,7 +17168,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react"); /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
const DEFAULT_MAX_ZOOM = 2.5; const DEFAULT_MAX_ZOOM = 8;
class Cropper extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) { class Cropper extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -17250,7 +17250,7 @@ class Cropper extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) {
const minZoom = Math.max(this.cutoutRect.width / imgRect.width, this.cutoutRect.height / imgRect.height); const minZoom = Math.max(this.cutoutRect.width / imgRect.width, this.cutoutRect.height / imgRect.height);
this.setState({ this.setState({
minZoom: minZoom, minZoom: minZoom,
maxZoom: Math.max(DEFAULT_MAX_ZOOM, minZoom + 1) maxZoom: minZoom * DEFAULT_MAX_ZOOM
}); });
const zoom = Math.max(this.bBoxRect.width / imgRect.width, this.bBoxRect.height / imgRect.height); const zoom = Math.max(this.bBoxRect.width / imgRect.width, this.bBoxRect.height / imgRect.height);
const panX = this.cutoutRect.left - this.bBoxRect.left - (imgRect.width - this.cutoutRect.width) / 2; const panX = this.cutoutRect.left - this.bBoxRect.left - (imgRect.width - this.cutoutRect.width) / 2;
@@ -17265,21 +17265,21 @@ class Cropper extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) {
let panY = this.state.panY; let panY = this.state.panY;
const imgLeft = this.originX - (this.originX - panX) * zoom; const imgLeft = this.originX - (this.originX - panX) * zoom;
const imgRight = imgLeft + this.imageWidth * zoom; const imgRight = imgLeft + this.imageWidth * zoom;
const coLeft = this.cutoutRect.left - this.bBoxRect.left;
const coRight = coLeft + this.cutoutRect.width;
if (coLeft < imgLeft) {
panX -= imgLeft - coLeft;
} else if (coRight > imgRight) {
panX += coRight - imgRight;
}
const imgTop = this.originY - (this.originY - panY) * zoom; const imgTop = this.originY - (this.originY - panY) * zoom;
const imgBottom = imgTop + this.imageHeight * zoom; const imgBottom = imgTop + this.imageHeight * zoom;
const coLeft = this.cutoutRect.left - this.bBoxRect.left;
const coRight = coLeft + this.cutoutRect.width;
const coTop = this.cutoutRect.top - this.bBoxRect.top; const coTop = this.cutoutRect.top - this.bBoxRect.top;
const coBottom = coTop + this.cutoutRect.height; const coBottom = coTop + this.cutoutRect.height;
if (coLeft < imgLeft) {
panX -= (imgLeft - coLeft) / zoom;
} else if (coRight > imgRight) {
panX += (coRight - imgRight) / zoom;
}
if (coTop < imgTop) { if (coTop < imgTop) {
panY -= imgTop - coTop; panY -= (imgTop - coTop) / zoom;
} else if (coBottom > imgBottom) { } else if (coBottom > imgBottom) {
panY += coBottom - imgBottom; panY += (coBottom - imgBottom) / zoom;
} }
this.positionAll(panX, panY, zoom); this.positionAll(panX, panY, zoom);
} }
@@ -17306,13 +17306,13 @@ class Cropper extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) {
document.body.style['userSelect'] = 'none'; document.body.style['userSelect'] = 'none';
} }
translate(pageX, pageY) { translate(pageX, pageY) {
const dX = pageX - this.mouseX; const dX = (pageX - this.mouseX) / this.state.zoom;
const dY = pageY - this.mouseY; const dY = (pageY - this.mouseY) / this.state.zoom;
this.mouseX = pageX; this.mouseX = pageX;
this.mouseY = pageY; this.mouseY = pageY;
const imgRect = this.preview.current.getBoundingClientRect(); const imgRect = this.preview.current.getBoundingClientRect();
let panX = Cropper.checkBound(this.state.panX, [imgRect.left, imgRect.right], [this.cutoutRect.left, this.cutoutRect.right], dX); const panX = Cropper.checkBound(this.state.panX, [imgRect.left, imgRect.right], [this.cutoutRect.left, this.cutoutRect.right], dX);
let panY = Cropper.checkBound(this.state.panY, [imgRect.top, imgRect.bottom], [this.cutoutRect.top, this.cutoutRect.bottom], dY); const panY = Cropper.checkBound(this.state.panY, [imgRect.top, imgRect.bottom], [this.cutoutRect.top, this.cutoutRect.bottom], dY);
this.positionAll(panX, panY, this.state.zoom); this.positionAll(panX, panY, this.state.zoom);
} }
mouseMove(e) { mouseMove(e) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -1
View File
@@ -109,6 +109,12 @@ class CreateAccountView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Pu
newAvatar: img, newAvatar: img,
newAvatarMime: mime newAvatarMime: mime
}); });
if (!img) {
this.setState({
imageUrl: null,
uploadUrl: null
});
}
} }
handleToggleSaveToken() { handleToggleSaveToken() {
_lib_local_storage_js__WEBPACK_IMPORTED_MODULE_7__["default"].setObject('keep-logged-in', !this.state.saveToken); _lib_local_storage_js__WEBPACK_IMPORTED_MODULE_7__["default"].setObject('keep-logged-in', !this.state.saveToken);
@@ -126,7 +132,6 @@ class CreateAccountView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Pu
handleAvatarCropped(mime, blob, width, height) { handleAvatarCropped(mime, blob, width, height) {
const url = blob ? URL.createObjectURL(blob) : null; const url = blob ? URL.createObjectURL(blob) : null;
this.setState({ this.setState({
avatar: url,
newAvatar: null, newAvatar: null,
newAvatarMime: null newAvatarMime: null
}); });
File diff suppressed because one or more lines are too long