aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/generativeFill/GenerativeFill.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/generativeFill/GenerativeFill.tsx')
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.tsx308
1 files changed, 308 insertions, 0 deletions
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
new file mode 100644
index 000000000..6dd80a5d1
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -0,0 +1,308 @@
+import { useEffect, useRef, useState } from 'react';
+import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler';
+import { BrushHandler } from './generativeFillUtils/BrushHandler';
+import { IconButton, TextField } from '@mui/material';
+import { CursorData, Point } from './generativeFillUtils/generativeFillInterfaces';
+import { activeColor, canvasSize, eraserColor } from './generativeFillUtils/generativeFillConstants';
+import { PointerHandler } from './generativeFillUtils/PointerHandler';
+import { BsBrush, BsEraser } from 'react-icons/bs';
+import { AiOutlineUpload } from 'react-icons/ai';
+import { CiUndo, CiRedo } from 'react-icons/ci';
+import Buttons from './GenerativeFillButtons';
+import React from 'react';
+import './GenerativeFill.scss';
+
+/**
+ * For images not 1024x1024 fill in the rest in solid black, or a
+ * reflected version of the image.
+ */
+
+/**
+ * TODO: Look into img onload, sometimes the canvas doesn't update properly
+ */
+
+enum BrushStyle {
+ ADD,
+ SUBTRACT,
+ MARQUEE,
+}
+
+interface ImageEdit {
+ imgElement: HTMLImageElement;
+ parent: ImageEdit;
+ children: ImageEdit[];
+}
+
+const GenerativeFill = () => {
+ const canvasRef = useRef<HTMLCanvasElement>(null);
+ const canvasBackgroundRef = useRef<HTMLCanvasElement>(null);
+ const drawingAreaRef = useRef<HTMLDivElement>(null);
+ const fileRef = useRef<HTMLInputElement>(null);
+ const [cursorData, setCursorData] = useState<CursorData>({
+ x: 0,
+ y: 0,
+ width: 150,
+ });
+ const [isBrushing, setIsBrushing] = useState(false);
+ const [canvasScale, setCanvasScale] = useState(0.5);
+ const [edits, setEdits] = useState<string[]>([]);
+ const [brushStyle, setBrushStyle] = useState<BrushStyle>(BrushStyle.ADD);
+ const [input, setInput] = useState('');
+ const [loading, setLoading] = useState(false);
+ // used to store the current image loaded to the main canvas
+ const currImg = useRef<HTMLImageElement | null>(null);
+ // ref to store history
+ const undoStack = useRef<ImageData[]>([]);
+
+ // initiate brushing
+ const handlePointerDown = (e: React.PointerEvent) => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+
+ setIsBrushing(true);
+ const { x, y } = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale);
+
+ BrushHandler.brushCircleOverlay(x, y, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT);
+ };
+
+ // stop brushing, push to undo stack
+ const handlePointerUp = (e: React.PointerEvent) => {
+ const ctx = ImageUtility.getCanvasContext(canvasBackgroundRef);
+ if (!ctx) return;
+ if (!isBrushing) return;
+ setIsBrushing(false);
+ };
+
+ // handles brushing on pointer movement
+ useEffect(() => {
+ if (!isBrushing) return;
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+
+ const handlePointerMove = (e: PointerEvent) => {
+ const currPoint = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale);
+ const lastPoint: Point = {
+ x: currPoint.x - e.movementX / canvasScale,
+ y: currPoint.y - e.movementY / canvasScale,
+ };
+ BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT);
+ };
+
+ drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove);
+ return () => drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove);
+ }, [isBrushing]);
+
+ // first load
+ useEffect(() => {
+ const img = new Image();
+ img.src = '/assets/art.jpeg';
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ }, [canvasRef]);
+
+ useEffect(() => {
+ if (!canvasBackgroundRef.current) return;
+ const ctx = ImageUtility.getCanvasContext(canvasBackgroundRef);
+ if (!ctx) return;
+ }, [canvasBackgroundRef]);
+
+ // handles brush sizing
+ useEffect(() => {
+ const handleKeyPress = (e: KeyboardEvent) => {
+ if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ e.stopPropagation();
+ setCursorData(data => ({ ...data, width: data.width + 5 }));
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ e.stopPropagation();
+ setCursorData(data => (data.width >= 20 ? { ...data, width: data.width - 5 } : data));
+ }
+ };
+ window.addEventListener('keydown', handleKeyPress);
+ return () => window.removeEventListener('keydown', handleKeyPress);
+ }, []);
+
+ // handle pinch zoom
+ useEffect(() => {
+ const handlePinch = (e: WheelEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const delta = e.deltaY;
+ const scaleFactor = delta > 0 ? 0.98 : 1.02; // Adjust the scale factor as per your requirement
+ setCanvasScale(prevScale => prevScale * scaleFactor);
+ };
+
+ drawingAreaRef.current?.addEventListener('wheel', handlePinch, {
+ passive: false,
+ });
+ return () => drawingAreaRef.current?.removeEventListener('wheel', handlePinch);
+ }, [drawingAreaRef]);
+
+ // updates the current position of the cursor
+ const updateCursorData = (e: React.PointerEvent) => {
+ const drawingArea = drawingAreaRef.current;
+ if (!drawingArea) return;
+ const { x, y } = PointerHandler.getPointRelativeToElement(drawingArea, e, 1);
+ setCursorData(data => ({
+ ...data,
+ x,
+ y,
+ }));
+ };
+
+ // File upload
+ const uploadImg = (e: React.ChangeEvent<HTMLInputElement>) => {
+ if (e.target.files) {
+ const file = e.target.files[0];
+ const image = new Image();
+ const imgUrl = URL.createObjectURL(file);
+ image.src = imgUrl;
+ ImageUtility.drawImgToCanvas(image, canvasRef);
+ currImg.current = image;
+ }
+ };
+
+ // Get AI Edit
+ const getEdit = async () => {
+ const img = currImg.current;
+ if (!img) return;
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ setLoading(true);
+ try {
+ const maskBlob = await ImageUtility.canvasToBlob(canvas);
+ const imgBlob = await ImageUtility.canvasToBlob(ImageUtility.getCanvasImg(img));
+
+ // const res = await ImageUtility.getEdit(
+ // imgBlob,
+ // maskBlob,
+ // input !== ""
+ // ? input + " in the same style"
+ // : "Fill in the image in the same style",
+ // 1
+ // );
+
+ const res = await ImageUtility.mockGetEdit();
+ const { urls } = res as APISuccess;
+ const image = new Image();
+ image.src = urls[0];
+ setLoading(false);
+ setEdits(urls);
+ ImageUtility.drawImgToCanvas(image, canvasRef);
+ currImg.current = image;
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ return (
+ <div className="generativeFillContainer">
+ <div className="controls">
+ <h1>Generative Fill</h1>
+ <Buttons canvasRef={canvasRef} backgroundref={canvasBackgroundRef} currImg={currImg} getEdit={getEdit} undoStack={undoStack} loading={loading} />
+ </div>
+ {/* Main canvas for editing */}
+ <div
+ className="drawingArea" // this only works if pointerevents: none is set on the custom pointer
+ ref={drawingAreaRef}
+ onPointerOver={updateCursorData}
+ onPointerMove={updateCursorData}
+ onPointerDown={handlePointerDown}
+ onPointerUp={handlePointerUp}>
+ <canvas ref={canvasRef} width={canvasSize} height={canvasSize} style={{ transform: `scale(${canvasScale})` }} />
+ <canvas ref={canvasBackgroundRef} width={canvasSize} height={canvasSize} style={{ transform: `scale(${canvasScale})` }} />
+ <div
+ className="pointer"
+ style={{
+ left: cursorData.x,
+ top: cursorData.y,
+ width: cursorData.width,
+ height: cursorData.width,
+ }}>
+ <div className="innerPointer"></div>
+ </div>
+ {/* Icons */}
+ <div className="iconContainer">
+ <input ref={fileRef} type="file" accept="image/*" onChange={uploadImg} style={{ display: 'none' }} />
+ <IconButton
+ onClick={() => {
+ if (fileRef.current) {
+ fileRef.current.click();
+ }
+ }}>
+ <AiOutlineUpload />
+ </IconButton>
+ <IconButton
+ onClick={() => {
+ setBrushStyle(BrushStyle.ADD);
+ }}>
+ <BsBrush color={brushStyle === BrushStyle.ADD ? activeColor : 'inherit'} />
+ </IconButton>
+ <IconButton
+ onClick={() => {
+ setBrushStyle(BrushStyle.SUBTRACT);
+ }}>
+ <BsEraser color={brushStyle === BrushStyle.SUBTRACT ? activeColor : 'inherit'} />
+ </IconButton>
+ {/* Undo and Redo */}
+ {/* <IconButton
+ onPointerDown={e => {
+ e.stopPropagation();
+ console.log(undoStack.current);
+ }}
+ onPointerUp={e => {
+ e.stopPropagation();
+ }}>
+ <CiUndo />
+ </IconButton>
+ <IconButton onClick={() => {}}>
+ <CiRedo />
+ </IconButton> */}
+ </div>
+ {/* Edits box */}
+ <div className="editsBox">
+ {edits.map(edit => (
+ <img
+ key={edit}
+ width={100}
+ height={100}
+ src={edit}
+ onClick={() => {
+ const img = new Image();
+ img.src = edit;
+ ImageUtility.drawImgToCanvas(img, canvasRef);
+ currImg.current = img;
+ }}
+ />
+ ))}
+ </div>
+ </div>
+ <div>
+ <TextField
+ value={input}
+ onChange={e => setInput(e.target.value)}
+ disabled={isBrushing}
+ type="text"
+ label="Prompt"
+ placeholder="Prompt..."
+ sx={{
+ backgroundColor: '#ffffff',
+ position: 'absolute',
+ bottom: '1rem',
+ transform: 'translateX(calc(50vw - 50%))',
+ width: 'calc(100vw - 4rem)',
+ }}
+ />
+ </div>
+ </div>
+ );
+};
+
+export default GenerativeFill;