mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 22:06:17 +00:00
fix: duplication
This commit is contained in:
@@ -58,9 +58,15 @@ export function ImageCropper({
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
if (containerRect.width === 0 || containerRect.height === 0) return null;
|
||||
|
||||
const computedStyle = globalThis.getComputedStyle(container);
|
||||
const paddingLeft = Number.parseFloat(computedStyle.paddingLeft) || 0;
|
||||
const paddingRight = Number.parseFloat(computedStyle.paddingRight) || 0;
|
||||
const paddingTop = Number.parseFloat(computedStyle.paddingTop) || 0;
|
||||
const paddingBottom = Number.parseFloat(computedStyle.paddingBottom) || 0;
|
||||
|
||||
return {
|
||||
maxWidth: containerRect.width - 40,
|
||||
maxHeight: containerRect.height - 200,
|
||||
maxWidth: containerRect.width - paddingLeft - paddingRight,
|
||||
maxHeight: containerRect.height - paddingTop - paddingBottom,
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -212,16 +218,17 @@ export function ImageCropper({
|
||||
};
|
||||
};
|
||||
|
||||
const adjustAspectRatio = useCallback(
|
||||
(width: number, height: number, ratio: number) => {
|
||||
const currentRatio = width / height;
|
||||
if (currentRatio > ratio) {
|
||||
return { width, height: width / ratio };
|
||||
}
|
||||
return { width: height * ratio, height };
|
||||
},
|
||||
[]
|
||||
);
|
||||
const enforceAspectRatio = (
|
||||
width: number,
|
||||
height: number,
|
||||
ratio: number
|
||||
): { width: number; height: number } => {
|
||||
const currentRatio = width / height;
|
||||
if (currentRatio > ratio) {
|
||||
return { width, height: width / ratio };
|
||||
}
|
||||
return { width: height * ratio, height };
|
||||
};
|
||||
|
||||
const calculateMinSize = useCallback(
|
||||
(effectiveAspectRatio: number | undefined) => {
|
||||
@@ -233,39 +240,78 @@ export function ImageCropper({
|
||||
[minCropSize]
|
||||
);
|
||||
|
||||
const getImageWrapperBounds = useCallback(() => {
|
||||
const imageWrapper = imageRef.current?.parentElement;
|
||||
if (!imageWrapper) return { width: 0, height: 0 };
|
||||
|
||||
const rect = imageWrapper.getBoundingClientRect();
|
||||
return { width: rect.width, height: rect.height };
|
||||
}, []);
|
||||
|
||||
const constrainCropArea = useCallback(
|
||||
(area: CropArea): CropArea => {
|
||||
const displaySize = getDisplaySize();
|
||||
const wrapperBounds = getImageWrapperBounds();
|
||||
const actualBounds = {
|
||||
width:
|
||||
wrapperBounds.width > 0 ? wrapperBounds.width : displaySize.width,
|
||||
height:
|
||||
wrapperBounds.height > 0 ? wrapperBounds.height : displaySize.height,
|
||||
};
|
||||
|
||||
let { x, y, width, height } = area;
|
||||
|
||||
const effectiveAspectRatio = circular ? 1 : aspectRatio;
|
||||
|
||||
if (effectiveAspectRatio) {
|
||||
({ width, height } = adjustAspectRatio(
|
||||
width,
|
||||
height,
|
||||
effectiveAspectRatio
|
||||
));
|
||||
}
|
||||
|
||||
const minSize = calculateMinSize(effectiveAspectRatio);
|
||||
width = Math.max(minSize, Math.min(width, displaySize.width));
|
||||
height = Math.max(minSize, Math.min(height, displaySize.height));
|
||||
|
||||
if (effectiveAspectRatio) {
|
||||
({ width, height } = adjustAspectRatio(
|
||||
({ width, height } = enforceAspectRatio(
|
||||
width,
|
||||
height,
|
||||
effectiveAspectRatio
|
||||
));
|
||||
|
||||
const maxWidth = Math.min(
|
||||
actualBounds.width,
|
||||
actualBounds.height * effectiveAspectRatio
|
||||
);
|
||||
const maxHeight = Math.min(
|
||||
actualBounds.height,
|
||||
actualBounds.width / effectiveAspectRatio
|
||||
);
|
||||
|
||||
width = Math.max(minSize, Math.min(width, maxWidth));
|
||||
height = Math.max(minSize, Math.min(height, maxHeight));
|
||||
|
||||
({ width, height } = enforceAspectRatio(
|
||||
width,
|
||||
height,
|
||||
effectiveAspectRatio
|
||||
));
|
||||
|
||||
width = Math.min(width, actualBounds.width);
|
||||
height = Math.min(height, actualBounds.height);
|
||||
({ width, height } = enforceAspectRatio(
|
||||
width,
|
||||
height,
|
||||
effectiveAspectRatio
|
||||
));
|
||||
} else {
|
||||
width = Math.max(minSize, Math.min(width, actualBounds.width));
|
||||
height = Math.max(minSize, Math.min(height, actualBounds.height));
|
||||
}
|
||||
|
||||
x = Math.max(0, Math.min(x, displaySize.width - width));
|
||||
y = Math.max(0, Math.min(y, displaySize.height - height));
|
||||
x = Math.max(0, Math.min(x, actualBounds.width - width));
|
||||
y = Math.max(0, Math.min(y, actualBounds.height - height));
|
||||
|
||||
return { x, y, width, height };
|
||||
},
|
||||
[getDisplaySize, circular, aspectRatio, adjustAspectRatio, calculateMinSize]
|
||||
[
|
||||
getDisplaySize,
|
||||
getImageWrapperBounds,
|
||||
circular,
|
||||
aspectRatio,
|
||||
calculateMinSize,
|
||||
]
|
||||
);
|
||||
|
||||
const getRelativeCoordinates = (
|
||||
@@ -275,10 +321,10 @@ export function ImageCropper({
|
||||
if (!imageWrapper) return null;
|
||||
|
||||
const rect = imageWrapper.getBoundingClientRect();
|
||||
return {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
};
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
const handleDrag = useCallback(
|
||||
@@ -393,11 +439,48 @@ export function ImageCropper({
|
||||
resizeHandle: string,
|
||||
aspectRatio: number
|
||||
) => {
|
||||
const { width: adjustedWidth, height: adjustedHeight } = adjustAspectRatio(
|
||||
newWidth,
|
||||
newHeight,
|
||||
aspectRatio
|
||||
const deltaX = Math.abs(newWidth - cropStart.width);
|
||||
const deltaY = Math.abs(newHeight - cropStart.height);
|
||||
|
||||
let adjustedWidth: number;
|
||||
let adjustedHeight: number;
|
||||
|
||||
if (deltaX > deltaY) {
|
||||
adjustedWidth = newWidth;
|
||||
adjustedHeight = newWidth / aspectRatio;
|
||||
} else {
|
||||
adjustedHeight = newHeight;
|
||||
adjustedWidth = newHeight * aspectRatio;
|
||||
}
|
||||
|
||||
const wrapperBounds = getImageWrapperBounds();
|
||||
const displaySize = getDisplaySize();
|
||||
const actualBounds = {
|
||||
width: wrapperBounds.width > 0 ? wrapperBounds.width : displaySize.width,
|
||||
height:
|
||||
wrapperBounds.height > 0 ? wrapperBounds.height : displaySize.height,
|
||||
};
|
||||
|
||||
const maxWidth = Math.min(
|
||||
actualBounds.width,
|
||||
actualBounds.height * aspectRatio
|
||||
);
|
||||
const maxHeight = Math.min(
|
||||
actualBounds.height,
|
||||
actualBounds.width / aspectRatio
|
||||
);
|
||||
|
||||
adjustedWidth = Math.min(adjustedWidth, maxWidth);
|
||||
adjustedHeight = Math.min(adjustedHeight, maxHeight);
|
||||
|
||||
const finalRatio = adjustedWidth / adjustedHeight;
|
||||
if (Math.abs(finalRatio - aspectRatio) > 0.001) {
|
||||
if (finalRatio > aspectRatio) {
|
||||
adjustedHeight = adjustedWidth / aspectRatio;
|
||||
} else {
|
||||
adjustedWidth = adjustedHeight * aspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
const { x, y } = adjustPositionForHandle(
|
||||
resizeHandle,
|
||||
@@ -575,7 +658,15 @@ export function ImageCropper({
|
||||
return (
|
||||
<div className="image-cropper">
|
||||
<div className="image-cropper__container" ref={containerRef}>
|
||||
<div className="image-cropper__image-wrapper">
|
||||
<div
|
||||
className="image-cropper__image-wrapper"
|
||||
style={{
|
||||
width: `${displaySize.width}px`,
|
||||
height: `${displaySize.height}px`,
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
}}
|
||||
>
|
||||
{imageLoaded && (
|
||||
<img
|
||||
ref={imageRef}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
/**
|
||||
* Crops an image using HTML5 Canvas API
|
||||
* @param imagePath - Path to the image file
|
||||
* @param cropArea - Crop area coordinates and dimensions
|
||||
* @param outputFormat - Output image format (default: 'image/png')
|
||||
* @returns Promise resolving to cropped image as Uint8Array
|
||||
*/
|
||||
export async function cropImage(
|
||||
import type { CropArea } from "@renderer/components";
|
||||
|
||||
type DrawImageCallback = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
img: HTMLImageElement,
|
||||
cropArea: CropArea
|
||||
) => void;
|
||||
|
||||
const loadImageAndProcess = async (
|
||||
imagePath: string,
|
||||
cropArea: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
},
|
||||
outputFormat: string = "image/png"
|
||||
): Promise<Uint8Array> {
|
||||
cropArea: CropArea,
|
||||
outputFormat: string,
|
||||
drawCallback: DrawImageCallback
|
||||
): Promise<Uint8Array> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
||||
@@ -27,35 +24,25 @@ export async function cropImage(
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.width = cropArea.width;
|
||||
canvas.height = cropArea.height;
|
||||
drawCallback(ctx, img, cropArea);
|
||||
|
||||
ctx.drawImage(
|
||||
img,
|
||||
cropArea.x,
|
||||
cropArea.y,
|
||||
cropArea.width,
|
||||
cropArea.height,
|
||||
0,
|
||||
0,
|
||||
cropArea.width,
|
||||
cropArea.height
|
||||
);
|
||||
const convertBlobToUint8Array = async (
|
||||
blob: Blob
|
||||
): Promise<Uint8Array> => {
|
||||
const buffer = await blob.arrayBuffer();
|
||||
return new Uint8Array(buffer);
|
||||
};
|
||||
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
reject(new Error("Failed to create blob from canvas"));
|
||||
return;
|
||||
}
|
||||
const handleBlob = (blob: Blob | null) => {
|
||||
if (!blob) {
|
||||
reject(new Error("Failed to create blob from canvas"));
|
||||
return;
|
||||
}
|
||||
|
||||
blob.arrayBuffer().then((buffer) => {
|
||||
resolve(new Uint8Array(buffer));
|
||||
});
|
||||
},
|
||||
outputFormat,
|
||||
0.95
|
||||
);
|
||||
convertBlobToUint8Array(blob).then(resolve).catch(reject);
|
||||
};
|
||||
|
||||
canvas.toBlob(handleBlob, outputFormat, 0.95);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
@@ -64,6 +51,73 @@ export async function cropImage(
|
||||
|
||||
img.src = imagePath.startsWith("local:") ? imagePath : `local:${imagePath}`;
|
||||
});
|
||||
};
|
||||
|
||||
const setCanvasDimensions = (
|
||||
canvas: HTMLCanvasElement,
|
||||
width: number,
|
||||
height: number
|
||||
): void => {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
};
|
||||
|
||||
type DrawImageParams = {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
sourceWidth: number;
|
||||
sourceHeight: number;
|
||||
destWidth: number;
|
||||
destHeight: number;
|
||||
};
|
||||
|
||||
const drawCroppedImage = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
img: HTMLImageElement,
|
||||
params: DrawImageParams
|
||||
): void => {
|
||||
ctx.drawImage(
|
||||
img,
|
||||
params.sourceX,
|
||||
params.sourceY,
|
||||
params.sourceWidth,
|
||||
params.sourceHeight,
|
||||
0,
|
||||
0,
|
||||
params.destWidth,
|
||||
params.destHeight
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Crops an image using HTML5 Canvas API
|
||||
* @param imagePath - Path to the image file
|
||||
* @param cropArea - Crop area coordinates and dimensions
|
||||
* @param outputFormat - Output image format (default: 'image/png')
|
||||
* @returns Promise resolving to cropped image as Uint8Array
|
||||
*/
|
||||
export async function cropImage(
|
||||
imagePath: string,
|
||||
cropArea: CropArea,
|
||||
outputFormat: string = "image/png"
|
||||
): Promise<Uint8Array> {
|
||||
return loadImageAndProcess(
|
||||
imagePath,
|
||||
cropArea,
|
||||
outputFormat,
|
||||
(ctx, img, area) => {
|
||||
const canvas = ctx.canvas;
|
||||
setCanvasDimensions(canvas, area.width, area.height);
|
||||
drawCroppedImage(ctx, img, {
|
||||
sourceX: area.x,
|
||||
sourceY: area.y,
|
||||
sourceWidth: area.width,
|
||||
sourceHeight: area.height,
|
||||
destWidth: area.width,
|
||||
destHeight: area.height,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,56 +129,30 @@ export async function cropImage(
|
||||
*/
|
||||
export async function cropImageToCircle(
|
||||
imagePath: string,
|
||||
cropArea: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
},
|
||||
cropArea: CropArea,
|
||||
outputFormat: string = "image/png"
|
||||
): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
if (!ctx) {
|
||||
reject(new Error("Failed to get canvas context"));
|
||||
return;
|
||||
}
|
||||
|
||||
const size = Math.min(cropArea.width, cropArea.height);
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
return loadImageAndProcess(
|
||||
imagePath,
|
||||
cropArea,
|
||||
outputFormat,
|
||||
(ctx, img, area) => {
|
||||
const size = Math.min(area.width, area.height);
|
||||
const canvas = ctx.canvas;
|
||||
setCanvasDimensions(canvas, size, size);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
|
||||
ctx.clip();
|
||||
|
||||
ctx.drawImage(img, cropArea.x, cropArea.y, size, size, 0, 0, size, size);
|
||||
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
reject(new Error("Failed to create blob from canvas"));
|
||||
return;
|
||||
}
|
||||
|
||||
blob.arrayBuffer().then((buffer) => {
|
||||
resolve(new Uint8Array(buffer));
|
||||
});
|
||||
},
|
||||
outputFormat,
|
||||
0.95
|
||||
);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error("Failed to load image"));
|
||||
};
|
||||
|
||||
img.src = imagePath.startsWith("local:") ? imagePath : `local:${imagePath}`;
|
||||
});
|
||||
drawCroppedImage(ctx, img, {
|
||||
sourceX: area.x,
|
||||
sourceY: area.y,
|
||||
sourceWidth: size,
|
||||
sourceHeight: size,
|
||||
destWidth: size,
|
||||
destHeight: size,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user