diff options
author | eleanor-park <eleanor_park@brown.edu> | 2024-10-28 23:10:24 -0400 |
---|---|---|
committer | eleanor-park <eleanor_park@brown.edu> | 2024-10-28 23:10:24 -0400 |
commit | e8661840c39219fd848e664c990943450c41bbee (patch) | |
tree | 0b511436e47c5cf225b37a36ba7d2771e12b648f | |
parent | d0569fcb79389b3c4f3d60e2a84f6f9220ca9cf3 (diff) |
basic structure of 4 cut modes implemented, many bugs to fix
4 files changed, 180 insertions, 149 deletions
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.scss b/src/client/views/nodes/imageEditor/ImageEditor.scss index 49146aa23..c691e6a18 100644 --- a/src/client/views/nodes/imageEditor/ImageEditor.scss +++ b/src/client/views/nodes/imageEditor/ImageEditor.scss @@ -75,38 +75,47 @@ $scale: 0.5; } } - .iconContainer { + .sideControlsContainer { + width: 160px; position: absolute; - top: 3rem; - left: 2rem; - display: flex; - flex-direction: column; - gap: 4rem; + left: 0; + height: 100%; - .imageToolsContainer { + .sideControls { + position: absolute; + width: 120px; + top: 3rem; + left: 2rem; display: flex; flex-direction: column; - gap: 10px; - } + gap: 1rem; - .cutToolsContainer { - display: grid; - gap: 5px; - grid-template-columns: 1fr 1fr; - } + .imageToolsContainer { + display: flex; + flex-direction: column; + gap: 10px; + } - .undoRedoContainer { - justify-content: center; - display: flex; - flex-direction: row; - } + .cutToolsContainer { + display: grid; + gap: 5px; + grid-template-columns: 1fr 1fr; + } - .sliderContainer { - height: 225px; - width: 100%; - display: flex; - justify-content: center; - cursor: pointer; + .undoRedoContainer { + justify-content: center; + display: flex; + flex-direction: row; + } + + .sliderContainer { + margin: 3rem 0; + height: 225px; + width: 100%; + display: flex; + justify-content: center; + cursor: pointer; + } } } diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx index d9f46876e..980f3e566 100644 --- a/src/client/views/nodes/imageEditor/ImageEditor.tsx +++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx @@ -4,7 +4,7 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable react/function-component-definition */ import { Checkbox, FormControlLabel, Slider, TextField } from '@mui/material'; -import { IconButton } from 'browndash-components'; +import { Button, IconButton, Type } from 'browndash-components'; import * as React from 'react'; import { useEffect, useRef, useState } from 'react'; import { CgClose } from 'react-icons/cg'; @@ -85,7 +85,6 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc // constants for image cutting const cutPts = useRef<Point[]>([]); - /** * * @param type The new tool type we are changing to @@ -356,7 +355,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc setLoading(false); }; - const cutImage = async () => { + const cutImage = async (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[]) => { const img = currImg.current; const canvas = canvasRef.current; if (!canvas || !img) return; @@ -381,21 +380,22 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc maxX = Math.max(cutPts.current[i].x, maxX); maxY = Math.max(cutPts.current[i].y, maxY); } - ctx.closePath(); - switch (cutType) { // may need to move this before the drawing at all for the line cases + switch (currCutType) { case BrushMode.IN: + ctx.closePath(); ctx.globalCompositeOperation = 'destination-in'; ctx.fill(); break; case BrushMode.OUT: + ctx.closePath(); ctx.globalCompositeOperation = 'destination-out'; ctx.fill(); break; - case BrushMode.LINE_OUT: - ctx.globalCompositeOperation = 'destination-out'; - break; case BrushMode.LINE_IN: ctx.globalCompositeOperation = 'destination-in'; + ctx.lineWidth = brushWidth + 20; + ctx.stroke(); + break; } } @@ -423,31 +423,41 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc const image = new Image(); image.src = url; image.onload = async () => { - const croppedCanvas = document.createElement('canvas'); - const croppedCtx = croppedCanvas.getContext('2d'); - if (!croppedCtx) return; - croppedCanvas.width = maxX - minX; - croppedCanvas.height = maxY - minY; - croppedCtx.globalCompositeOperation = 'source-over'; - croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height); - croppedCtx.drawImage(image, -minX, -minY); - const croppedURL = croppedCanvas.toDataURL(); - const croppedImage = new Image(); - croppedImage.src = croppedURL; - - currImg.current = croppedImage; - const newImgDoc = await createNewImgDoc(croppedImage, isFirstDoc.current); - if (isFirstDoc.current) isFirstDoc.current = false; + let finalImg: HTMLImageElement | undefined = image; + let finalImgURL: string = url; + if (currCutType == BrushMode.IN) { + const croppedData = cropImage(image, minX, maxY, minY, maxY); + finalImg = croppedData; + finalImgURL = croppedData.src; + } + currImg.current = finalImg; + const newImgDoc = await createNewImgDoc(finalImg, isFirstDoc.current); if (newImgDoc) { const docData = newImgDoc[DocData]; docData.backgroundColor = 'transparent'; + if (isFirstDoc.current) isFirstDoc.current = false; + setEdits([...prevEdits, { url: finalImgURL, saveRes: undefined }]); } - setEdits(prevEdits => [...prevEdits, { url: croppedURL, saveRes: undefined }]); setLoading(false); cutPts.current.length = 0; }; }; + const cropImage = (image: HTMLImageElement, minX: number, maxX: number, minY: number, maxY: number) => { + const croppedCanvas = document.createElement('canvas'); + const croppedCtx = croppedCanvas.getContext('2d'); + if (!croppedCtx) return image; + croppedCanvas.width = maxX - minX; + croppedCanvas.height = maxY - minY; + croppedCtx.globalCompositeOperation = 'source-over'; + croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height); + croppedCtx.drawImage(image, -minX, -minY); + const croppedURL = croppedCanvas.toDataURL(); + const croppedImage = new Image(); + croppedImage.src = croppedURL; + return croppedImage; + }; + // adjusts all the img positions to be aligned const adjustImgPositions = () => { if (!parentDoc.current) return; @@ -577,7 +587,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc labelPlacement="end" sx={{ whiteSpace: 'nowrap' }} /> - <ApplyFuncButtons onClick={currTool.applyFunc} loading={loading} onReset={handleReset} btnText={currTool.btnText} /> + <ApplyFuncButtons onClick={() => currTool.applyFunc(cutType, cursorData.width, edits)} loading={loading} onReset={handleReset} btnText={currTool.btnText} /> <IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} /> </div> </div> @@ -587,109 +597,120 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc // the side icons including tool type, the slider, and undo/redo function renderSideIcons() { return ( - <div className="iconContainer"> - <div className="imageToolsContainer"> - {imageEditTools.map(tool => { - return ImageToolButton(tool, tool.type === currTool.type, changeTool); - })} - </div> - {currTool.type == ImageToolType.Cut && - <div className="cutToolContainer"> - <Button - text="Keep in" - type={Type.TERT} - color={cutType == BrushMode.IN ? SettingsManager.userVariantColor : bgColor} - onClick={() => { - setCutType(BrushMode.IN); - }}/> - <Button - text="Keep out" - type={Type.TERT} - color={cutType == BrushMode.OUT ? SettingsManager.userVariantColor : bgColor} - onClick={() => { - setCutType(BrushMode.OUT); - }}/> - <Button - text="Draw in" - type={Type.TERT} - color={cutType == BrushMode.LINE_IN ? SettingsManager.userVariantColor : bgColor} - onClick={() => { - setCutType(BrushMode.LINE_IN); - }}/> - <Button - text="Erase" - type={Type.TERT} - color={cutType == BrushMode.LINE_OUT ? SettingsManager.userVariantColor : bgColor} - onClick={() => { - setCutType(BrushMode.LINE_OUT); - }}/> - </div>} - <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}> - {currTool.type === ImageToolType.GenerativeFill && ( - <Slider - sx={{ - '& input[type="range"]': { - WebkitAppearance: 'slider-vertical', - }, + <div className="sideControlsContainer" style={{ backgroundColor: bgColor }}> + <div className="sideControls"> + <div className="imageToolsContainer"> + {imageEditTools.map(tool => { + return ImageToolButton(tool, tool.type === currTool.type, changeTool); + })} + </div> + {currTool.type == ImageToolType.Cut && ( + <div className="cutToolsContainer"> + <Button + style={{ width: '100%' }} + text="Keep in" + type={Type.TERT} + color={cutType == BrushMode.IN ? SettingsManager.userColor : bgColor} + onClick={() => { + setCutType(BrushMode.IN); + }} + /> + <Button + style={{ width: '100%' }} + text="Keep out" + type={Type.TERT} + color={cutType == BrushMode.OUT ? SettingsManager.userColor : bgColor} + onClick={() => { + setCutType(BrushMode.OUT); + }} + /> + <Button + style={{ width: '100%' }} + text="Draw in" + type={Type.TERT} + color={cutType == BrushMode.LINE_IN ? SettingsManager.userColor : bgColor} + onClick={() => { + setCutType(BrushMode.LINE_IN); + }} + /> + <Button + style={{ width: '100%' }} + text="Erase" + type={Type.TERT} + color={cutType == BrushMode.LINE_OUT ? SettingsManager.userColor : bgColor} + onClick={() => { + setCutType(BrushMode.LINE_OUT); + }} + /> + </div> + )} + <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}> + {currTool.type === ImageToolType.GenerativeFill && ( + <Slider + sx={{ + '& input[type="range"]': { + WebkitAppearance: 'slider-vertical', + }, + }} + orientation="vertical" + min={genFillTool.sliderMin} + max={genFillTool.sliderMax} + defaultValue={genFillTool.sliderDefault} + size="small" + valueLabelDisplay="auto" + onChange={(e, val) => { + setCursorData(prev => ({ ...prev, width: val as number })); + }} + /> + )} + {currTool.type === ImageToolType.Cut && ( + <Slider + sx={{ + '& input[type="range"]': { + WebkitAppearance: 'slider-vertical', + }, + }} + orientation="vertical" + min={cutTool.sliderMin} + max={cutTool.sliderMax} + defaultValue={cutTool.sliderDefault} + size="small" + valueLabelDisplay="auto" + onChange={(e, val) => { + setCursorData(prev => ({ ...prev, width: val as number })); + }} + /> + )} + </div> + {/* Undo and Redo */} + <div className="undoRedoContainer"> + <IconButton + style={{ cursor: 'pointer' }} + onPointerDown={e => { + e.stopPropagation(); + handleUndo(); }} - orientation="vertical" - min={genFillTool.sliderMin} - max={genFillTool.sliderMax} - defaultValue={genFillTool.sliderDefault} - size="small" - valueLabelDisplay="auto" - onChange={(e, val) => { - setCursorData(prev => ({ ...prev, width: val as number })); + onPointerUp={e => { + e.stopPropagation(); }} + color={activeColor} + tooltip="Undo" + icon={<IoMdUndo />} /> - )} - {currTool.type === ImageToolType.Cut && ( - <Slider - sx={{ - '& input[type="range"]': { - WebkitAppearance: 'slider-vertical', - }, + <IconButton + style={{ cursor: 'pointer' }} + onPointerDown={e => { + e.stopPropagation(); + handleRedo(); }} - orientation="vertical" - min={cutTool.sliderMin} - max={cutTool.sliderMax} - defaultValue={cutTool.sliderDefault} - size="small" - valueLabelDisplay="auto" - onChange={(e, val) => { - setCursorData(prev => ({ ...prev, width: val as number })); + onPointerUp={e => { + e.stopPropagation(); }} + color={activeColor} + tooltip="Redo" + icon={<IoMdRedo />} /> - )} - </div> - {/* Undo and Redo */} - <div className="undoRedoContainer"> - <IconButton - style={{ cursor: 'pointer' }} - onPointerDown={e => { - e.stopPropagation(); - handleUndo(); - }} - onPointerUp={e => { - e.stopPropagation(); - }} - color={activeColor} - tooltip="Undo" - icon={<IoMdUndo />} - /> - <IconButton - style={{ cursor: 'pointer' }} - onPointerDown={e => { - e.stopPropagation(); - handleRedo(); - }} - onPointerUp={e => { - e.stopPropagation(); - }} - color={activeColor} - tooltip="Redo" - icon={<IoMdRedo />} - /> + </div> </div> </div> ); diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx index e90babf9b..cb963616b 100644 --- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx +++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx @@ -55,7 +55,7 @@ export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTo return ( <div className="imageEditorButtonContainer"> <Button - style={{ width: '100%', border: '1px' }} + style={{ width: '100%' }} text={tool.name} type={Type.TERT} color={isActive ? SettingsManager.userVariantColor : bgColor} diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts index bd2fac775..660af7c42 100644 --- a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts +++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorInterfaces.ts @@ -1,4 +1,5 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { Doc } from '../../../../../fields/Doc'; export interface CursorData { x: number; @@ -21,7 +22,7 @@ export interface ImageEditTool { name: string; btnText: string; icon: IconProp; - applyFunc: () => Promise<void>; + applyFunc: (currCutType: BrushMode, brushWidth: number, prevEdits: { url: string; saveRes: Doc | undefined }[]) => Promise<void>; sliderMin?: number; sliderMax?: number; sliderDefault?: number; |