File size: 5,491 Bytes
ca28016
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"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>
  );
}