first commit

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
HouYunFei
2026-05-19 07:53:53 +08:00
commit 472bf8b732
133 changed files with 16869 additions and 0 deletions
@@ -0,0 +1,90 @@
"use client";
export type ImageCropRect = {
x: number;
y: number;
width: number;
height: number;
};
export type ImageAngleTransform = {
horizontalAngle: number;
pitchAngle: number;
cameraDistance: number;
wideAngle: boolean;
};
export async function cropDataUrl(dataUrl: string, crop?: ImageCropRect) {
const image = await loadImage(dataUrl);
if (crop) {
return drawCrop(
image,
Math.floor(crop.x * image.width),
Math.floor(crop.y * image.height),
Math.ceil(crop.width * image.width),
Math.ceil(crop.height * image.height),
);
}
const size = Math.min(image.width, image.height);
const sx = Math.max(0, Math.floor((image.width - size) / 2));
const sy = Math.max(0, Math.floor((image.height - size) / 2));
return drawCrop(image, sx, sy, size, size);
}
export async function transformAngleDataUrl(dataUrl: string, params: ImageAngleTransform) {
const image = await loadImage(dataUrl);
const canvas = document.createElement("canvas");
const padding = Math.round(Math.max(image.width, image.height) * 0.18);
canvas.width = image.width + padding * 2;
canvas.height = image.height + padding * 2;
const context = canvas.getContext("2d");
if (!context) return dataUrl;
context.clearRect(0, 0, canvas.width, canvas.height);
const horizontal = params.horizontalAngle / 60;
const pitch = params.pitchAngle / 45;
const distanceScale = 1.12 - params.cameraDistance * 0.035;
const wideScale = params.wideAngle ? 0.88 : 1;
const scale = Math.max(0.64, Math.min(1.1, distanceScale * wideScale));
const width = image.width * scale * (1 - Math.abs(horizontal) * 0.28);
const height = image.height * scale * (1 - Math.abs(pitch) * 0.18);
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const skewX = horizontal * image.width * 0.18;
const skewY = pitch * image.height * 0.12;
const x = cx - width / 2 + horizontal * padding * 0.5;
const y = cy - height / 2 + pitch * padding * 0.45;
context.save();
context.setTransform(1, pitch * 0.08, horizontal * -0.1, 1, 0, 0);
context.drawImage(image, x + skewX, y + skewY, width, height);
context.restore();
if (params.wideAngle) {
const gradient = context.createRadialGradient(cx, cy, Math.min(canvas.width, canvas.height) * 0.2, cx, cy, Math.max(canvas.width, canvas.height) * 0.62);
gradient.addColorStop(0, "rgba(255,255,255,0)");
gradient.addColorStop(1, "rgba(0,0,0,0.18)");
context.fillStyle = gradient;
context.fillRect(0, 0, canvas.width, canvas.height);
}
return canvas.toDataURL("image/png");
}
function drawCrop(image: HTMLImageElement, sx: number, sy: number, sw: number, sh: number) {
const canvas = document.createElement("canvas");
canvas.width = Math.max(1, sw);
canvas.height = Math.max(1, sh);
const context = canvas.getContext("2d");
if (!context) return image.src;
context.drawImage(image, sx, sy, sw, sh, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL("image/png");
}
function loadImage(dataUrl: string) {
return new Promise<HTMLImageElement>((resolve) => {
const image = new Image();
image.onload = () => resolve(image);
image.src = dataUrl;
});
}