golem-flask-backend / src /components /image-editor-pane.tsx
mememechez's picture
Deploy final cleaned source code
ca28016
raw
history blame
5.49 kB
"use client";
import React, { useEffect, useMemo, useRef, useState } from 'react';
type ImageEditorPaneProps = {
imageBase64: string;
onUpdate: (base64: string) => void;
onClose: () => void;
prompt?: string;
};
function loadImage(base64: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = `data:image/png;base64,${base64}`;
});
}
function canvasToBase64(canvas: HTMLCanvasElement): string {
return canvas.toDataURL("image/png").split(",")[1] || "";
}
export default function ImageEditorPane({ imageBase64, onUpdate, onClose, prompt }: ImageEditorPaneProps) {
const [working, setWorking] = useState(false);
const [brightness, setBrightness] = useState(0); // -100..100
const [contrast, setContrast] = useState(0); // -100..100
const previewRef = useRef<HTMLImageElement>(null);
const applyAndUpdate = async (fn: (img: HTMLImageElement, ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => void | Promise<void>) => {
setWorking(true);
try {
const img = await loadImage(imageBase64);
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d", { willReadFrequently: true });
if (!ctx) return;
ctx.drawImage(img, 0, 0);
await fn(img, ctx, canvas);
const b64 = canvasToBase64(canvas);
onUpdate(b64);
} finally {
setWorking(false);
}
};
const cropCenter = (aspect: number) => applyAndUpdate((img, ctx, canvas) => {
const w = img.naturalWidth; const h = img.naturalHeight;
let cw = w; let ch = Math.round(w / aspect);
if (ch > h) { ch = h; cw = Math.round(h * aspect); }
const sx = Math.round((w - cw) / 2);
const sy = Math.round((h - ch) / 2);
const out = document.createElement("canvas");
out.width = cw; out.height = ch;
const octx = out.getContext("2d")!;
octx.imageSmoothingEnabled = true;
octx.drawImage(img, sx, sy, cw, ch, 0, 0, cw, ch);
canvas.width = cw; canvas.height = ch;
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.drawImage(out, 0, 0);
});
const applyAdjust = () => applyAndUpdate((img, ctx, canvas) => {
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imgData.data;
const b = brightness / 100 * 255; // shift
const c = Math.pow((contrast + 100) / 100, 2); // gamma-like
for (let i = 0; i < data.length; i += 4) {
for (let k = 0; k < 3; k++) {
let v = data[i + k];
v = v + b; // brightness
v = ((v - 128) * c) + 128; // contrast
data[i + k] = Math.max(0, Math.min(255, v));
}
}
ctx.putImageData(imgData, 0, 0);
});
const upscale2x = () => applyAndUpdate((img, ctx, canvas) => {
const outW = img.naturalWidth * 2;
const outH = img.naturalHeight * 2;
const out = document.createElement("canvas");
out.width = outW; out.height = outH;
const octx = out.getContext("2d")!;
octx.imageSmoothingEnabled = true;
octx.imageSmoothingQuality = 'high';
octx.drawImage(img, 0, 0, outW, outH);
canvas.width = outW; canvas.height = outH;
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.drawImage(out, 0, 0);
});
return (
<div className="h-full w-full p-4">
<div className="relative h-full rounded-2xl overflow-hidden bg-white/5 dark:bg-white/5 backdrop-blur-xl border border-white/10 shadow-2xl">
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10">
<div className="text-sm text-muted-foreground truncate">{prompt || 'Image Editor'}</div>
<div className="flex items-center gap-2">
<button disabled={working} onClick={() => cropCenter(1)} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">1:1</button>
<button disabled={working} onClick={() => cropCenter(16/9)} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">16:9</button>
<button disabled={working} onClick={() => cropCenter(4/3)} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">4:3</button>
<div className="flex items-center gap-2 pl-3">
<span className="text-xs opacity-70">B</span>
<input disabled={working} type="range" min={-100} max={100} value={brightness} onChange={(e) => setBrightness(parseInt(e.target.value))} />
<span className="text-xs opacity-70">C</span>
<input disabled={working} type="range" min={-100} max={100} value={contrast} onChange={(e) => setContrast(parseInt(e.target.value))} />
<button disabled={working} onClick={applyAdjust} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">Apply</button>
</div>
<button disabled={working} onClick={upscale2x} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">Upscale 2×</button>
<button disabled={working} onClick={onClose} className="px-2 py-1 rounded bg-white/10 hover:bg-white/20">Close</button>
</div>
</div>
<div className="p-4 h-[calc(100%-56px)] overflow-auto flex items-center justify-center">
<img ref={previewRef} src={`data:image/png;base64,${imageBase64}`} alt="Editor" className="max-w-full max-h-full rounded-lg shadow" />
</div>
</div>
</div>
);
}