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