Skip to content

Commit 9ad0476

Browse files
committed
Calculate the size of the cropper selection in relation to the window size
1 parent 983dc4d commit 9ad0476

File tree

3 files changed

+151
-126
lines changed

3 files changed

+151
-126
lines changed

ts/WoltLabSuite/Core/Component/Image/Cropper.ts

Lines changed: 80 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ function inSelection(selection: Selection, maxSelection: Selection): boolean {
2929
return (
3030
selection.x >= maxSelection.x &&
3131
selection.y >= maxSelection.y &&
32-
selection.x + selection.width <= maxSelection.x + maxSelection.width &&
33-
selection.y + selection.height <= maxSelection.y + maxSelection.height
32+
Math.ceil(selection.x + selection.width) <= Math.ceil(maxSelection.x + maxSelection.width) &&
33+
Math.ceil(selection.y + selection.height) <= Math.ceil(maxSelection.y + maxSelection.height)
3434
);
3535
}
3636

@@ -85,7 +85,7 @@ abstract class ImageCropper {
8585

8686
return new Promise<File>((resolve, reject) => {
8787
this.dialog!.addEventListener("primary", () => {
88-
this.cropperSelection!.$toCanvas()
88+
void this.getCanvas()
8989
.then((canvas) => {
9090
this.resizer
9191
.saveFile(
@@ -107,6 +107,10 @@ abstract class ImageCropper {
107107
});
108108
}
109109

110+
protected getCanvas(): Promise<HTMLCanvasElement> {
111+
return this.cropperSelection!.$toCanvas();
112+
}
113+
110114
public async loadImage() {
111115
const { image, exif } = await this.resizer.loadFile(this.file);
112116
this.image = image;
@@ -138,11 +142,9 @@ abstract class ImageCropper {
138142
this.cropperCanvas!.style.aspectRatio = `${this.width}/${this.height}`;
139143

140144
if (this.width >= this.height) {
141-
this.cropperCanvas!.style.width = `min(70vw, ${this.width}px)`;
142-
this.cropperCanvas!.style.height = "auto";
145+
this.cropperCanvas!.style.maxHeight = "100%";
143146
} else {
144-
this.cropperCanvas!.style.height = `min(60vh, ${this.height}px)`;
145-
this.cropperCanvas!.style.width = "auto";
147+
this.cropperCanvas!.style.maxWidth = "100%";
146148
}
147149

148150
this.cropperSelection!.aspectRatio = this.configuration.aspectRatio;
@@ -171,12 +173,11 @@ abstract class ImageCropper {
171173
const cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect();
172174
const selection = event.detail as Selection;
173175

174-
const cropperImageRect = this.cropperImage!.getBoundingClientRect();
175176
const maxSelection: Selection = {
176-
x: Math.round(cropperImageRect.left - cropperCanvasRect.left),
177-
y: Math.round(cropperImageRect.top - cropperCanvasRect.top),
178-
width: Math.round(cropperImageRect.width),
179-
height: Math.round(cropperImageRect.height),
177+
x: 0,
178+
y: 0,
179+
width: cropperCanvasRect.width,
180+
height: cropperCanvasRect.height,
180181
};
181182

182183
if (!inSelection(selection, maxSelection)) {
@@ -247,17 +248,15 @@ class ExactImageCropper extends ImageCropper {
247248
}
248249

249250
protected getCropperTemplate(): string {
250-
return `<div class="cropperContainer">
251-
<cropper-canvas background>
252-
<cropper-image rotatable></cropper-image>
253-
<cropper-shade hidden></cropper-shade>
254-
<cropper-selection movable outlined keyboard>
255-
<cropper-grid role="grid" bordered covered></cropper-grid>
256-
<cropper-crosshair centered></cropper-crosshair>
257-
<cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
258-
</cropper-selection>
259-
</cropper-canvas>
260-
</div>`;
251+
return `<cropper-canvas background>
252+
<cropper-image rotatable></cropper-image>
253+
<cropper-shade hidden></cropper-shade>
254+
<cropper-selection movable outlined keyboard>
255+
<cropper-grid role="grid" bordered covered></cropper-grid>
256+
<cropper-crosshair centered></cropper-crosshair>
257+
<cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
258+
</cropper-selection>
259+
</cropper-canvas>`;
261260
}
262261

263262
protected setCropperStyle() {
@@ -273,6 +272,7 @@ class ExactImageCropper extends ImageCropper {
273272
}
274273

275274
class MinMaxImageCropper extends ImageCropper {
275+
#cropperCanvasRect?: DOMRect;
276276
constructor(element: WoltlabCoreFileUploadElement, file: File, configuration: CropperConfiguration) {
277277
super(element, file, configuration);
278278
if (configuration.sizes.length !== 2) {
@@ -292,39 +292,40 @@ class MinMaxImageCropper extends ImageCropper {
292292
return getPhrase("wcf.global.button.reset");
293293
}
294294

295-
protected getCropperTemplate(): string {
296-
return `<div class="cropperContainer">
297-
<cropper-canvas background scale-step="0.0">
298-
<cropper-image skewable scalable translatable rotatable></cropper-image>
299-
<cropper-shade hidden></cropper-shade>
300-
<cropper-handle action="scale" hidden disabled></cropper-handle>
301-
<cropper-selection movable resizable outlined>
302-
<cropper-grid role="grid" bordered covered></cropper-grid>
303-
<cropper-crosshair centered></cropper-crosshair>
304-
<cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
305-
<cropper-handle action="n-resize"></cropper-handle>
306-
<cropper-handle action="e-resize"></cropper-handle>
307-
<cropper-handle action="s-resize"></cropper-handle>
308-
<cropper-handle action="w-resize"></cropper-handle>
309-
<cropper-handle action="ne-resize"></cropper-handle>
310-
<cropper-handle action="nw-resize"></cropper-handle>
311-
<cropper-handle action="se-resize"></cropper-handle>
312-
<cropper-handle action="sw-resize"></cropper-handle>
313-
</cropper-selection>
314-
</cropper-canvas>
315-
</div>`;
316-
}
317-
318-
protected setCropperStyle() {
319-
super.setCropperStyle();
295+
public async loadImage(): Promise<void> {
296+
await super.loadImage();
320297

321-
if (this.width >= this.height) {
322-
this.cropperCanvas!.style.width = `${Math.min(this.maxSize.width, this.width)}px`;
323-
} else {
324-
this.cropperCanvas!.style.height = `${Math.min(this.maxSize.height, this.height)}px`;
298+
if (this.image!.width < this.minSize.width || this.image!.height < this.minSize.height) {
299+
throw new Error(
300+
getPhrase("wcf.upload.error.image.tooSmall", {
301+
width: this.minSize.width,
302+
height: this.minSize.height,
303+
}),
304+
);
325305
}
326306
}
327307

308+
protected getCropperTemplate(): string {
309+
return `<cropper-canvas background scale-step="0.0">
310+
<cropper-image skewable scalable translatable rotatable></cropper-image>
311+
<cropper-shade hidden></cropper-shade>
312+
<cropper-handle action="scale" hidden disabled></cropper-handle>
313+
<cropper-selection precise movable resizable outlined>
314+
<cropper-grid role="grid" bordered covered></cropper-grid>
315+
<cropper-crosshair centered></cropper-crosshair>
316+
<cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
317+
<cropper-handle action="n-resize"></cropper-handle>
318+
<cropper-handle action="e-resize"></cropper-handle>
319+
<cropper-handle action="s-resize"></cropper-handle>
320+
<cropper-handle action="w-resize"></cropper-handle>
321+
<cropper-handle action="ne-resize"></cropper-handle>
322+
<cropper-handle action="nw-resize"></cropper-handle>
323+
<cropper-handle action="se-resize"></cropper-handle>
324+
<cropper-handle action="sw-resize"></cropper-handle>
325+
</cropper-selection>
326+
</cropper-canvas>`;
327+
}
328+
328329
protected createCropper() {
329330
super.createCropper();
330331

@@ -335,31 +336,46 @@ class MinMaxImageCropper extends ImageCropper {
335336
// Limit the selection to the min/max size
336337
this.cropperSelection!.addEventListener("change", (event: CustomEvent) => {
337338
const selection = event.detail as Selection;
339+
this.#cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect();
340+
341+
const maxImageWidth = Math.min(this.image!.width, this.maxSize.width);
342+
const widthRatio = this.#cropperCanvasRect.width / maxImageWidth;
343+
344+
const minWidth = this.minSize.width * widthRatio;
345+
const maxWidth = this.maxSize.width * widthRatio;
346+
const minHeight = minWidth / this.configuration.aspectRatio;
347+
const maxHeight = maxWidth / this.configuration.aspectRatio;
338348

339349
if (
340-
selection.width < this.minSize.width ||
341-
selection.height < this.minSize.height ||
342-
selection.width > this.maxSize.width ||
343-
selection.height > this.maxSize.height
350+
selection.width < minWidth ||
351+
selection.height < minHeight ||
352+
selection.width > maxWidth ||
353+
selection.height > maxHeight
344354
) {
345355
event.preventDefault();
346356
}
347357
});
348358
}
349359

360+
protected getCanvas(): Promise<HTMLCanvasElement> {
361+
// Calculate the size of the image in relation to the window size
362+
const maxImageWidth = Math.min(this.image!.width, this.maxSize.width);
363+
const widthRatio = this.#cropperCanvasRect!.width / maxImageWidth;
364+
const width = this.cropperSelection!.width / widthRatio;
365+
const height = width / this.configuration.aspectRatio;
366+
367+
return this.cropperSelection!.$toCanvas({
368+
width: Math.max(Math.min(Math.ceil(width), this.maxSize.width), this.minSize.width),
369+
height: Math.max(Math.min(Math.ceil(height), this.maxSize.height), this.minSize.height),
370+
});
371+
}
372+
350373
protected centerSelection(): void {
351374
this.cropperImage!.$center("contain");
352375

353376
const { width: imageWidth } = this.cropperImage!.getBoundingClientRect();
354377

355-
this.cropperSelection!.$change(
356-
0,
357-
0,
358-
imageWidth,
359-
0,
360-
this.configuration.aspectRatio,
361-
true,
362-
);
378+
this.cropperSelection!.$change(0, 0, imageWidth, 0, this.configuration.aspectRatio, true);
363379
this.cropperSelection!.$center();
364380
this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" });
365381
}

0 commit comments

Comments
 (0)