├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── api │ └── route.ts ├── component │ ├── CommonInput.tsx │ ├── ControlNetInput.tsx │ ├── ExtraInput.tsx │ ├── ImageGallery.tsx │ ├── ImageMaskCanvas.module.css │ ├── ImageMaskCanvas.tsx │ ├── ImageUpload.tsx │ ├── Img2imgImageInput.tsx │ ├── PromptContainer.tsx │ └── page.tsx ├── favicon.ico ├── globals.css ├── hook │ ├── useExtra.hook.ts │ ├── useImg2img.hook.ts │ ├── useOptions.hook.ts │ ├── useProgress.hook.ts │ ├── useSelector.hook.ts │ └── useTxt2img.hook.ts ├── layout.tsx ├── page.tsx ├── redux │ ├── Features │ │ ├── ExtraState │ │ │ └── ExtraSlice.ts │ │ ├── Img2imgState │ │ │ └── Img2imgSlice.ts │ │ └── Txt2imgState │ │ │ └── Txt2imgSlice.ts │ ├── provider.tsx │ └── store.ts ├── sd │ ├── extra │ │ └── route.ts │ ├── img2img │ │ └── route.ts │ ├── options │ │ └── route.ts │ ├── page.tsx │ ├── progress │ │ └── route.ts │ └── txt2img │ │ └── route.ts ├── type │ ├── extra.ts │ ├── img2img.ts │ └── txt2img.ts └── utils │ ├── canvas.ts │ ├── controlModel.ts │ └── file.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | This project is a NEXTJS framework to coperate with stable diffusion webui api. 4 | 5 | ## Getting Started 6 | 7 | First, run the development server: 8 | 9 | ```bash 10 | npm run dev 11 | # or 12 | yarn dev 13 | # or 14 | pnpm dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /app/api/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | export async function GET() { 4 | const res = await fetch("https://data.mongodb-api.com/...", { 5 | headers: { 6 | "Content-Type": "application/json", 7 | }, 8 | }); 9 | const data = await res.json(); 10 | 11 | return NextResponse.json({ data }); 12 | } 13 | -------------------------------------------------------------------------------- /app/component/CommonInput.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch } from "react-redux"; 2 | import { useSelector } from "../hook/useSelector.hook"; 3 | import { setSettings as setTxt2imgSettings } from "../redux/Features/Txt2imgState/Txt2imgSlice"; 4 | import { setSettings as setImg2imgSettings } from "../redux/Features/Img2imgState/Img2imgSlice"; 5 | import { use, useEffect, useState } from "react"; 6 | 7 | const samplingMethodOptions = [ 8 | { 9 | name: "Euler a", 10 | aliases: ["k_euler_a", "k_euler_ancestral"], 11 | options: {}, 12 | }, 13 | { 14 | name: "Euler", 15 | aliases: ["k_euler"], 16 | options: {}, 17 | }, 18 | { 19 | name: "LMS", 20 | aliases: ["k_lms"], 21 | options: {}, 22 | }, 23 | { 24 | name: "Heun", 25 | aliases: ["k_heun"], 26 | options: {}, 27 | }, 28 | { 29 | name: "DPM2", 30 | aliases: ["k_dpm_2"], 31 | options: { 32 | discard_next_to_last_sigma: "True", 33 | }, 34 | }, 35 | { 36 | name: "DPM2 a", 37 | aliases: ["k_dpm_2_a"], 38 | options: { 39 | discard_next_to_last_sigma: "True", 40 | }, 41 | }, 42 | { 43 | name: "DPM++ 2S a", 44 | aliases: ["k_dpmpp_2s_a"], 45 | options: {}, 46 | }, 47 | { 48 | name: "DPM++ 2M", 49 | aliases: ["k_dpmpp_2m"], 50 | options: {}, 51 | }, 52 | { 53 | name: "DPM++ SDE", 54 | aliases: ["k_dpmpp_sde"], 55 | options: {}, 56 | }, 57 | { 58 | name: "DPM fast", 59 | aliases: ["k_dpm_fast"], 60 | options: {}, 61 | }, 62 | { 63 | name: "DPM adaptive", 64 | aliases: ["k_dpm_ad"], 65 | options: {}, 66 | }, 67 | { 68 | name: "LMS Karras", 69 | aliases: ["k_lms_ka"], 70 | options: { 71 | scheduler: "karras", 72 | }, 73 | }, 74 | { 75 | name: "DPM2 Karras", 76 | aliases: ["k_dpm_2_ka"], 77 | options: { 78 | scheduler: "karras", 79 | discard_next_to_last_sigma: "True", 80 | }, 81 | }, 82 | { 83 | name: "DPM2 a Karras", 84 | aliases: ["k_dpm_2_a_ka"], 85 | options: { 86 | scheduler: "karras", 87 | discard_next_to_last_sigma: "True", 88 | }, 89 | }, 90 | { 91 | name: "DPM++ 2S a Karras", 92 | aliases: ["k_dpmpp_2s_a_ka"], 93 | options: { 94 | scheduler: "karras", 95 | }, 96 | }, 97 | { 98 | name: "DPM++ 2M Karras", 99 | aliases: ["k_dpmpp_2m_ka"], 100 | options: { 101 | scheduler: "karras", 102 | }, 103 | }, 104 | { 105 | name: "DPM++ SDE Karras", 106 | aliases: ["k_dpmpp_sde_ka"], 107 | options: { 108 | scheduler: "karras", 109 | }, 110 | }, 111 | { 112 | name: "DDIM", 113 | aliases: [], 114 | options: {}, 115 | }, 116 | { 117 | name: "PLMS", 118 | aliases: [], 119 | options: {}, 120 | }, 121 | { 122 | name: "UniPC", 123 | aliases: [], 124 | options: {}, 125 | }, 126 | ]; 127 | 128 | export const CommonInput = ({ mode }: { mode: number }) => { 129 | const settings = 130 | mode == 0 131 | ? useSelector((state) => state.txt2img.settings) 132 | : useSelector((state) => state.img2img.settings); 133 | const setSettings = mode == 0 ? setTxt2imgSettings : setImg2imgSettings; 134 | const dispatch = useDispatch(); 135 | 136 | const [samplingMethod, setSamplingMethod] = useState("Eular a"); 137 | const [steps, setSteps] = useState(20); 138 | const [restoreFase, setRestoreFase] = useState(false); 139 | const [tiling, setTiling] = useState(false); 140 | const [height, setHeight] = useState(512); 141 | const [width, setWidth] = useState(512); 142 | const [batchCount, setBatchCount] = useState(1); 143 | const [batchSize, setBatchSize] = useState(1); 144 | const [cfg, setCfg] = useState(7); 145 | const [denoising, setDenoising] = useState(0.7); 146 | const [seeds, setSeeds] = useState(-1); 147 | 148 | useEffect(() => { 149 | if (settings.height && settings.height != height) { 150 | setHeight(settings.height); 151 | } 152 | if (settings.width && settings.width != width) { 153 | setWidth(settings.width); 154 | } 155 | }, [settings]); 156 | 157 | return ( 158 |
159 |
160 | 161 | 178 | 179 | { 185 | setSteps(parseInt(e.target.value)); 186 | dispatch( 187 | setSettings({ ...settings, steps: parseInt(e.target.value) }) 188 | ); 189 | }} 190 | /> 191 |
192 |
193 | { 199 | setRestoreFase(e.target.checked); 200 | dispatch( 201 | setSettings({ ...settings, restore_faces: e.target.checked }) 202 | ); 203 | }} 204 | /> 205 | 206 | { 212 | setTiling(e.target.checked); 213 | dispatch(setSettings({ ...settings, tiling: e.target.checked })); 214 | }} 215 | /> 216 | 217 |
218 |
219 | 220 | { 226 | setHeight(parseInt(e.target.value)); 227 | dispatch( 228 | setSettings({ ...settings, height: parseInt(e.target.value) }) 229 | ); 230 | }} 231 | /> 232 | 233 | { 239 | setWidth(parseInt(e.target.value)); 240 | dispatch( 241 | setSettings({ ...settings, width: parseInt(e.target.value) }) 242 | ); 243 | }} 244 | /> 245 |
246 |
247 | 248 | { 254 | setBatchCount(parseInt(e.target.value)); 255 | dispatch( 256 | setSettings({ 257 | ...settings, 258 | n_iter: parseInt(e.target.value), 259 | }) 260 | ); 261 | }} 262 | /> 263 | 264 | { 270 | setBatchSize(parseInt(e.target.value)); 271 | dispatch( 272 | setSettings({ ...settings, batch_size: parseInt(e.target.value) }) 273 | ); 274 | }} 275 | /> 276 |
277 | 278 | { 284 | setCfg(parseInt(e.target.value)); 285 | dispatch( 286 | setSettings({ 287 | ...settings, 288 | cfg_scale: parseInt(e.target.value), 289 | }) 290 | ); 291 | }} 292 | /> 293 |
294 | {mode == 1 && ( 295 |
296 | 297 | { 303 | setDenoising(parseFloat(e.target.value)); 304 | dispatch( 305 | setSettings({ 306 | ...settings, 307 | denoising_strength: parseFloat(e.target.value), 308 | }) 309 | ); 310 | }} 311 | /> 312 |
313 | )} 314 |
315 |
316 | 317 | { 323 | setSeeds(parseInt(e.target.value)); 324 | dispatch( 325 | setSettings({ 326 | ...settings, 327 | seed: parseInt(e.target.value), 328 | }) 329 | ); 330 | }} 331 | /> 332 |
333 |
334 | ); 335 | }; 336 | -------------------------------------------------------------------------------- /app/component/ControlNetInput.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import ImageMaskCanvas from "./ImageMaskCanvas"; 4 | import { useSelector } from "../hook/useSelector.hook"; 5 | import { setSettings as setTxt2imgSettings } from "../redux/Features/Txt2imgState/Txt2imgSlice"; 6 | import { setSettings as setImg2imgSettings } from "../redux/Features/Img2imgState/Img2imgSlice"; 7 | 8 | const modules = [ 9 | "none", 10 | "canny", 11 | "depth", 12 | "depth_leres", 13 | "depth_leres++", 14 | "hed", 15 | "hed_safe", 16 | "mediapipe_face", 17 | "mlsd", 18 | "normal_map", 19 | "openpose", 20 | "openpose_hand", 21 | "openpose_face", 22 | "openpose_faceonly", 23 | "openpose_full", 24 | "clip_vision", 25 | "color", 26 | "pidinet", 27 | "pidinet_safe", 28 | "pidinet_sketch", 29 | "pidinet_scribble", 30 | "scribble_xdog", 31 | "scribble_hed", 32 | "segmentation", 33 | "threshold", 34 | "depth_zoe", 35 | "normal_bae", 36 | "oneformer_coco", 37 | "oneformer_ade20k", 38 | "lineart", 39 | "lineart_coarse", 40 | "lineart_anime", 41 | "lineart_standard", 42 | "shuffle", 43 | "tile_resample", 44 | "inpaint", 45 | "invert", 46 | "lineart_anime_denoise", 47 | ]; 48 | 49 | const models = [ 50 | "control_v11e_sd15_ip2p [c4bb465c]", 51 | "control_v11e_sd15_shuffle [526bfdae]", 52 | "control_v11f1e_sd15_tile [a371b31b]", 53 | "control_v11f1p_sd15_depth [cfd03158]", 54 | "control_v11p_sd15_canny [d14c016b]", 55 | "control_v11p_sd15_inpaint [ebff9138]", 56 | "control_v11p_sd15_lineart [43d4be0d]", 57 | "control_v11p_sd15_mlsd [aca30ff0]", 58 | "control_v11p_sd15_normalbae [316696f1]", 59 | "control_v11p_sd15_openpose [cab727d4]", 60 | "control_v11p_sd15_scribble [d4ba51ff]", 61 | "control_v11p_sd15_seg [e1f51eb9]", 62 | "control_v11p_sd15_softedge [a8575a2a]", 63 | "control_v11p_sd15s2_lineart_anime [3825e83e]", 64 | ]; 65 | 66 | export const ControlNetInput = ({ mode }: { mode: number }) => { 67 | const dispatch = useDispatch(); 68 | const settings = 69 | mode == 0 70 | ? useSelector((state) => state.txt2img.settings) 71 | : useSelector((state) => state.img2img.settings); 72 | const setSettings = mode == 0 ? setTxt2imgSettings : setImg2imgSettings; 73 | const [isEnabled, setIsEnabled] = useState(false); 74 | const [moduleSelected, setModuleSelected] = useState("openpose_face"); 75 | const [modelSelected, setModelSelected] = useState( 76 | "control_v11p_sd15_openpose [cab727d4]" 77 | ); 78 | const [image, setImage] = useState(null); 79 | const [imageSize, setImageSize] = useState({ width: 0, height: 0 }); 80 | 81 | const imageHandler = (image: string | null) => { 82 | setImage(image); 83 | }; 84 | 85 | const imageSizeHandler = (imageSize: { width: number; height: number }) => { 86 | setImageSize(imageSize); 87 | }; 88 | 89 | useEffect(() => { 90 | if (isEnabled) { 91 | dispatch( 92 | setSettings({ 93 | ...settings, 94 | alwayson_scripts: { 95 | controlnet: { 96 | args: [ 97 | { 98 | module: moduleSelected, 99 | model: modelSelected, 100 | input_image: image, 101 | }, 102 | ], 103 | }, 104 | }, 105 | }) 106 | ); 107 | } 108 | }, [isEnabled, moduleSelected, modelSelected, image]); 109 | 110 | useEffect(() => { 111 | if (imageSize.width > 0 && imageSize.height > 0) { 112 | console.log("imageSize", imageSize); 113 | dispatch( 114 | setSettings({ 115 | ...settings, 116 | width: imageSize.width, 117 | height: imageSize.height, 118 | }) 119 | ); 120 | } 121 | }, [imageSize]); 122 | 123 | return ( 124 |
125 | ControlNetInput component: 126 | setIsEnabled(!isEnabled)} 130 | /> 131 | {isEnabled && ( 132 | <> 133 | 143 | 144 | 154 | 158 | 159 | )} 160 |
161 | ); 162 | }; 163 | -------------------------------------------------------------------------------- /app/component/ExtraInput.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import ImageMaskCanvas from "./ImageMaskCanvas"; 3 | import { useSelector } from "../hook/useSelector.hook"; 4 | import { useDispatch } from "react-redux"; 5 | import { setSettings } from "../redux/Features/ExtraState/ExtraSlice"; 6 | 7 | const upscalerOptions = [ 8 | { 9 | name: "None", 10 | model_name: null, 11 | model_path: null, 12 | model_url: null, 13 | scale: 4, 14 | }, 15 | { 16 | name: "Lanczos", 17 | model_name: null, 18 | model_path: null, 19 | model_url: null, 20 | scale: 4, 21 | }, 22 | { 23 | name: "Nearest", 24 | model_name: null, 25 | model_path: null, 26 | model_url: null, 27 | scale: 4, 28 | }, 29 | { 30 | name: "ESRGAN_4x", 31 | model_name: "ESRGAN_4x", 32 | model_path: 33 | "https://github.com/cszn/KAIR/releases/download/v1.0/ESRGAN.pth", 34 | model_url: null, 35 | scale: 4, 36 | }, 37 | { 38 | name: "R-ESRGAN 4x+", 39 | model_name: null, 40 | model_path: 41 | "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth", 42 | model_url: null, 43 | scale: 4, 44 | }, 45 | { 46 | name: "R-ESRGAN 4x+ Anime6B", 47 | model_name: null, 48 | model_path: 49 | "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth", 50 | model_url: null, 51 | scale: 4, 52 | }, 53 | ]; 54 | 55 | export const ExtraInput = () => { 56 | const [image, setImage] = useState(""); 57 | const [imageSize, setImageSize] = useState<{ width: number; height: number }>( 58 | { width: 0, height: 0 } 59 | ); 60 | const [resize, setResize] = useState<1 | 2 | 4>(2); 61 | const [upscaler1, setUpscaler1] = useState("None"); 62 | const [upscaler2, setUpscaler2] = useState("None"); 63 | 64 | const settings = useSelector((state) => state.extra.settings); 65 | 66 | const dispatch = useDispatch(); 67 | 68 | const imageHandler = (image: string | null) => { 69 | setImage(image); 70 | }; 71 | const imageSizeHandler = (imageSize: { width: number; height: number }) => { 72 | setImageSize(imageSize); 73 | }; 74 | 75 | useEffect(() => { 76 | if (image) { 77 | dispatch( 78 | setSettings({ 79 | ...settings, 80 | image, 81 | }) 82 | ); 83 | } 84 | }, [image]); 85 | 86 | useEffect(() => { 87 | if (resize) { 88 | dispatch( 89 | setSettings({ 90 | ...settings, 91 | upscaling_resize: resize, 92 | }) 93 | ); 94 | } 95 | }, [resize]); 96 | 97 | useEffect(() => { 98 | if (upscaler1) { 99 | dispatch( 100 | setSettings({ 101 | ...settings, 102 | upscaler_1: upscaler1, 103 | }) 104 | ); 105 | } 106 | }, [upscaler1]); 107 | 108 | return ( 109 |
110 | 114 |
115 | 116 | setResize(1)} 121 | checked={resize === 1} 122 | /> 123 | 124 | setResize(2)} 129 | checked={resize === 2} 130 | /> 131 | 132 | setResize(4)} 137 | checked={resize === 4} 138 | /> 139 | 140 |
141 |
142 | 143 | 153 |
154 |
155 | ); 156 | }; 157 | -------------------------------------------------------------------------------- /app/component/ImageGallery.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { downloadBase64Image } from "../utils/file"; 3 | 4 | interface ImageGalleryProps { 5 | result: any; 6 | } 7 | 8 | export const ImageGallery = (props: ImageGalleryProps) => { 9 | const { result } = props; 10 | const [selectedImage, setSelectedImage] = useState(null); 11 | const [selectedImageIndex, setSelectedImageIndex] = useState(0); 12 | 13 | const handleImageClick = (image: string, index: number) => { 14 | // setSelectedImage(image); 15 | setSelectedImageIndex(index); 16 | }; 17 | 18 | useEffect(() => { 19 | if (result) { 20 | setSelectedImage(result["images"][0]); 21 | const parameters = JSON.parse(result["info"]); 22 | console.log(parameters); 23 | } 24 | }, []); 25 | 26 | useEffect(() => { 27 | if (result) { 28 | setSelectedImage(result["images"][selectedImageIndex]); 29 | } 30 | }, [selectedImageIndex]); 31 | 32 | return ( 33 |
34 | {selectedImage && ( 35 |
36 |
37 | 61 | 62 | 86 |
87 |
88 | 96 |
97 |
98 | )} 99 |
100 | {result && 101 | result["images"].map((image: string, index: number) => ( 102 |
103 | handleImageClick(image, index)} 106 | className="cursor-pointer w-20" 107 | /> 108 | {selectedImage == image && ( 109 |
110 | )} 111 |
112 | ))} 113 |
114 |
115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /app/component/ImageMaskCanvas.module.css: -------------------------------------------------------------------------------- 1 | .canvas { 2 | position: relative; 3 | } 4 | 5 | .canvas::after { 6 | content: ""; 7 | position: absolute; 8 | z-index: 2; 9 | left: 0; 10 | top: 0; 11 | width: 10px; 12 | height: 10px; 13 | background-color: red; 14 | border-radius: 50%; 15 | transform: translate(-50%, -50%); 16 | pointer-events: none; 17 | } 18 | -------------------------------------------------------------------------------- /app/component/ImageMaskCanvas.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useRef, useEffect, useState, use } from "react"; 2 | import { contain, cover } from "../utils/canvas"; 3 | 4 | interface ImageMaskCanvasProps { 5 | imageHandler: (image: string | null, width?: number, height?: number) => void; 6 | imageSizeHandler?: (imageSize: { width: number; height: number }) => void; 7 | maskImageHandler?: (image: string | null) => void; 8 | containCrop?: boolean; 9 | } 10 | 11 | const ImageMaskCanvas: FC = ( 12 | props: ImageMaskCanvasProps 13 | ) => { 14 | const [isPainting, setIsPainting] = useState(false); 15 | const { imageHandler, maskImageHandler, imageSizeHandler, containCrop } = 16 | props; 17 | const [mousePosition, setMousePosition] = useState< 18 | { x: number; y: number } | undefined 19 | >(undefined); 20 | const [image, setImage] = useState(null); 21 | const [maskImage, setMaskImage] = useState(null); 22 | const canvasRef = useRef(null); 23 | const maskCanvasRef = useRef(null); 24 | const contextRef = useRef(null); 25 | const maskContextRef = useRef(null); 26 | 27 | const startPaint = (event: MouseEvent) => { 28 | const coordinates = getCoordinates(event); 29 | if (coordinates) { 30 | setIsPainting(true); 31 | setMousePosition(coordinates); 32 | } 33 | }; 34 | 35 | const paint = (event: MouseEvent) => { 36 | if (isPainting) { 37 | const newMousePosition = getCoordinates(event); 38 | if (mousePosition && newMousePosition) { 39 | drawLine(mousePosition, newMousePosition); 40 | setMousePosition(newMousePosition); 41 | } 42 | } 43 | }; 44 | 45 | const exitPaint = () => { 46 | if (isPainting) { 47 | saveMask(); 48 | } 49 | setIsPainting(false); 50 | setMousePosition(undefined); 51 | }; 52 | 53 | const getCoordinates = ( 54 | event: MouseEvent 55 | ): { x: number; y: number } | undefined => { 56 | if (!canvasRef.current) { 57 | return; 58 | } 59 | 60 | const canvas: HTMLCanvasElement = canvasRef.current; 61 | return { 62 | x: event.pageX - canvas.offsetLeft, 63 | y: event.pageY - canvas.offsetTop, 64 | }; 65 | }; 66 | 67 | const drawLine = ( 68 | originalMousePosition: { x: number; y: number }, 69 | newMousePosition: { x: number; y: number } 70 | ) => { 71 | if (!contextRef.current || !maskContextRef.current) { 72 | return; 73 | } 74 | 75 | const context: CanvasRenderingContext2D = contextRef.current; 76 | const maskContext: CanvasRenderingContext2D = maskContextRef.current; 77 | 78 | context.strokeStyle = "black"; 79 | context.lineJoin = "round"; 80 | context.lineWidth = 20; 81 | maskContext.strokeStyle = "white"; 82 | maskContext.lineJoin = "round"; 83 | maskContext.lineWidth = 20; 84 | 85 | context.beginPath(); 86 | context.moveTo(originalMousePosition.x, originalMousePosition.y); 87 | context.lineTo(newMousePosition.x, newMousePosition.y); 88 | context.closePath(); 89 | 90 | context.stroke(); 91 | 92 | maskContext.beginPath(); 93 | maskContext.moveTo(originalMousePosition.x, originalMousePosition.y); 94 | maskContext.lineTo(newMousePosition.x, newMousePosition.y); 95 | maskContext.closePath(); 96 | 97 | maskContext.stroke(); 98 | }; 99 | 100 | const resetCanvas = () => { 101 | const context = contextRef.current; 102 | const canvas = canvasRef.current; 103 | const maskContext = maskContextRef.current; 104 | const maskCanvas = maskCanvasRef.current; 105 | 106 | if (context && canvas && maskContext && maskCanvas) { 107 | context.clearRect(0, 0, canvas.width, canvas.height); 108 | maskContext.clearRect(0, 0, maskCanvas.width, maskCanvas.height); 109 | 110 | if (image) { 111 | const { offsetX, offsetY, width, height } = contain( 112 | canvas.width, 113 | canvas.height, 114 | image.width, 115 | image.height 116 | ); 117 | 118 | context.drawImage(image, offsetX, offsetY, width, height); 119 | } 120 | setMaskImage(null); 121 | if (maskImageHandler) { 122 | maskImageHandler(null); 123 | } 124 | } 125 | }; 126 | 127 | const clearCanvas = () => { 128 | const context = contextRef.current; 129 | const canvas = canvasRef.current; 130 | const maskContext = maskContextRef.current; 131 | const maskCanvas = maskCanvasRef.current; 132 | 133 | if (context && canvas && maskContext && maskCanvas) { 134 | context.clearRect(0, 0, canvas.width, canvas.height); 135 | maskContext.clearRect(0, 0, maskCanvas.width, maskCanvas.height); 136 | setImage(null); 137 | setMaskImage(null); 138 | imageHandler(null); 139 | if (maskImageHandler) { 140 | maskImageHandler(null); 141 | } 142 | } 143 | }; 144 | 145 | const uploadImage = (event: React.ChangeEvent) => { 146 | if (event.target.files && event.target.files[0]) { 147 | const img = new Image(); 148 | img.src = URL.createObjectURL(event.target.files[0]); 149 | img.onload = function () { 150 | const context = contextRef.current; 151 | const canvas = canvasRef.current; 152 | const maskContext = maskContextRef.current; 153 | const maskCanvas = maskCanvasRef.current; 154 | if (context && canvas && maskContext && maskCanvas) { 155 | context.clearRect(0, 0, canvas.width, canvas.height); 156 | maskContext.clearRect(0, 0, maskCanvas.width, maskCanvas.height); 157 | if (img) { 158 | const { offsetX, offsetY, width, height } = contain( 159 | canvas.width, 160 | canvas.height, 161 | img.width, 162 | img.height 163 | ); 164 | context.drawImage(img, offsetX, offsetY, width, height); 165 | const dataURL = canvas.toDataURL(); 166 | if (containCrop) { 167 | imageHandler(dataURL); 168 | if (imageSizeHandler) { 169 | imageHandler(dataURL, canvas.width, canvas.height); 170 | } 171 | } 172 | } 173 | } 174 | }; 175 | setImage(img); 176 | 177 | if (!containCrop) { 178 | var reader = new FileReader(); 179 | reader.readAsDataURL(event.target.files[0]); 180 | reader.onload = function () { 181 | const csv: string | ArrayBuffer | null = reader.result; 182 | var finalCsv = ""; 183 | if (typeof csv === "string") { 184 | finalCsv = csv; 185 | } else if (csv instanceof ArrayBuffer) { 186 | finalCsv = csv.toString(); 187 | } 188 | imageHandler(finalCsv); 189 | if (imageSizeHandler) { 190 | console.log( 191 | "about to call imageSizeHandler", 192 | img.width, 193 | img.height 194 | ); 195 | imageHandler(finalCsv, img.width, img.height); 196 | } 197 | }; 198 | } 199 | } 200 | }; 201 | 202 | const saveMask = () => { 203 | const context = contextRef.current; 204 | const maskContext = maskContextRef.current; 205 | const canvas = canvasRef.current; 206 | 207 | if (!context || !maskContext || !image || !canvas) { 208 | return; 209 | } 210 | 211 | const maskCanvas = document.createElement("canvas"); 212 | 213 | var maskImageData = maskContext.getImageData( 214 | 0, 215 | 0, 216 | canvas.width, 217 | canvas.height 218 | ); 219 | 220 | maskCanvas.width = canvas.width; 221 | maskCanvas.height = canvas.height; 222 | 223 | if (!containCrop) { 224 | const { offsetX, offsetY, width, height } = contain( 225 | canvas.width, 226 | canvas.height, 227 | image.width, 228 | image.height 229 | ); 230 | 231 | maskImageData = maskContext.getImageData(offsetX, offsetY, width, height); 232 | 233 | maskCanvas.width = image.width; 234 | maskCanvas.height = image.height; 235 | } 236 | 237 | const maskCanvasContext = maskCanvas.getContext("2d"); 238 | if (!maskCanvasContext) { 239 | return; 240 | } 241 | 242 | maskCanvasContext.drawImage( 243 | maskContext.canvas, 244 | 0, 245 | 0, 246 | maskCanvas.width, 247 | maskCanvas.height 248 | ); 249 | 250 | const maskDataURL = maskCanvas.toDataURL(); 251 | 252 | // console.log(maskDataURL); 253 | setMaskImage(maskDataURL); 254 | if (maskImageHandler) { 255 | console.log("about to call maskImageHandler"); 256 | maskImageHandler(maskDataURL); 257 | } 258 | }; 259 | 260 | useEffect(() => { 261 | if (!canvasRef.current || !maskCanvasRef.current) { 262 | return; 263 | } 264 | const canvas: HTMLCanvasElement = canvasRef.current; 265 | const context = canvas.getContext("2d"); 266 | const maskCanvas: HTMLCanvasElement = maskCanvasRef.current; 267 | const maskContext = maskCanvas.getContext("2d"); 268 | if (!context || !maskContext) { 269 | return; 270 | } 271 | contextRef.current = context; 272 | maskContextRef.current = maskContext; 273 | 274 | canvas.addEventListener("mousedown", startPaint); 275 | canvas.addEventListener("mousemove", paint); 276 | canvas.addEventListener("mouseup", exitPaint); 277 | canvas.addEventListener("mouseleave", exitPaint); 278 | 279 | return () => { 280 | canvas.removeEventListener("mousedown", startPaint); 281 | canvas.removeEventListener("mousemove", paint); 282 | canvas.removeEventListener("mouseup", exitPaint); 283 | canvas.removeEventListener("mouseleave", exitPaint); 284 | }; 285 | }, [startPaint, paint, exitPaint]); 286 | 287 | return ( 288 |
289 | 290 | 296 | 302 | 303 | 304 | {/* */} 305 |
306 | ); 307 | }; 308 | 309 | export default ImageMaskCanvas; 310 | -------------------------------------------------------------------------------- /app/component/ImageUpload.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, FC, useState } from "react"; 2 | 3 | const ImageUpload: FC = () => { 4 | const [selectedImage, setSelectedImage] = useState(null); 5 | const [base64Image, setBase64Image] = useState(null); 6 | 7 | const handleImageUpload = (event: ChangeEvent) => { 8 | if (event.target.files && event.target.files[0]) { 9 | const reader = new FileReader(); 10 | const file = event.target.files[0]; 11 | 12 | reader.onloadend = () => { 13 | setSelectedImage(file); 14 | setBase64Image(reader.result as string); 15 | }; 16 | 17 | reader.readAsDataURL(file); 18 | } 19 | }; 20 | 21 | return ( 22 |
23 | 24 | {selectedImage &&

{selectedImage.name}

} 25 | {base64Image &&