瀏覽代碼

refactor(module): optimization freehand tool and shape tool

* optimization toolbar display mode
RoyLiu 4 年之前
父節點
當前提交
434324e7d1

+ 5 - 4
components/AnnotationList/index.tsx

@@ -19,8 +19,9 @@ const AnnotationList: React.FC<Props> = ({
   const [list, setList] = useState<AnnotationType[]>([]);
 
   useEffect(() => {
-    if (isActive && annotations.length) {
-      annotations.sort((a, b) => {
+    if (isActive) {
+      const tmpArray = [...annotations];
+      tmpArray.sort((a, b) => {
         if (a.obj_attr.page > b.obj_attr.page) {
           return 1;
         }
@@ -29,7 +30,7 @@ const AnnotationList: React.FC<Props> = ({
         }
         return 0;
       });
-      setList(annotations);
+      setList(tmpArray);
     }
   }, [isActive, annotations]);
 
@@ -47,7 +48,7 @@ const AnnotationList: React.FC<Props> = ({
               obj_attr: { page, title, date, style },
             } = ele;
             const actualPage = page + 1;
-            const prevAnnot = annotations[index - 1];
+            const prevAnnot = list[index - 1];
             const prevPage =
               index > 0 && prevAnnot ? prevAnnot.obj_attr.page + 1 : -1;
 

+ 28 - 71
components/Ink/index.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
 
 import OuterRect from '../OuterRect';
 import {
@@ -7,6 +7,7 @@ import {
   controlPoint,
   line,
 } from '../../helpers/svgBezierCurve';
+import { pointCalc, rectCalcWithPoint, calcViewBox } from '../../helpers/brush';
 
 import { AnnotationContainer } from '../../global/otherStyled';
 import { SVG } from './styled';
@@ -20,54 +21,13 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
   id,
 }: AnnotationElementPropsType) => {
   let points: PointType[][] = [];
-  let rect: HTMLCoordinateType = { top: 0, left: 0, width: 0, height: 0 };
+  let tempRect: HTMLCoordinateType = { top: 0, left: 0, width: 0, height: 0 };
   const borderWidth = bdwidth * scale;
 
-  const pointCalc = (
-    _points: PointType[][],
-    h: number,
-    s: number,
-  ): PointType[][] => {
-    const reducer = _points.reduce(
-      (acc: PointType[][], cur: PointType[]): PointType[][] => {
-        const p = cur.map((point: PointType) => ({
-          x: point.x * s,
-          y: h - point.y * s,
-        }));
-        acc.push(p);
-        return acc;
-      },
-      [],
-    );
-
-    return reducer;
-  };
-
-  const rectCalcWithPoint = (
-    pointsGroup: PointType[][],
-  ): HTMLCoordinateType => {
-    const xArray: number[] = [];
-    const yArray: number[] = [];
-    pointsGroup[0].forEach((point: PointType) => {
-      xArray.push(point.x);
-      yArray.push(point.y);
-    });
-    const top = Math.min(...yArray);
-    const left = Math.min(...xArray);
-    const bottom = Math.max(...yArray);
-    const right = Math.max(...xArray);
-    return {
-      top,
-      left,
-      width: right - left + borderWidth,
-      height: bottom - top + borderWidth,
-    };
-  };
-
   points = pointCalc(position as PointType[][], viewport.height, scale);
-  rect = rectCalcWithPoint(points);
+  tempRect = rectCalcWithPoint(points, borderWidth);
 
-  const [newRect, setRect] = useState({ top: 0, left: 0, width: 0, height: 0 });
+  const [rect, setRect] = useState({ top: 0, left: 0, width: 0, height: 0 });
 
   const handleScaleOrMove = ({
     top,
@@ -75,17 +35,16 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
     width = 0,
     height = 0,
   }: CoordType): void => {
-    const xScaleRate = width / rect.width;
-    const yScaleRate = height / rect.height;
-    const xDistance = left - rect.left;
-    const yDistance = rect.top - top;
+    const xScaleRate = width / tempRect.width;
+    const yScaleRate = height / tempRect.height;
+    const xDistance = left - tempRect.left;
+    const yDistance = tempRect.top - top;
 
     const newPosition = (position as PointType[][])[0].map((ele) => ({
       x: (ele.x + xDistance) * (xScaleRate || 1),
       y: (ele.y + yDistance) * (yScaleRate || 1),
     }));
 
-    rect = { top, left, width, height };
     setRect({
       top,
       left,
@@ -98,28 +57,26 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
     });
   };
 
-  const calcViewBox = (
-    { top, left, width, height }: HTMLCoordinateType,
-    _borderWidth: number,
-  ): string => {
-    const distance = _borderWidth / 2;
-    return `
-    ${left - distance} ${top - distance} ${width + distance} ${
-      height + distance
-    }
-  `;
-  };
+  useEffect(() => {
+    const newPoints = pointCalc(
+      position as PointType[][],
+      viewport.height,
+      scale,
+    );
+    const newRect = rectCalcWithPoint(newPoints, borderWidth);
+    setRect(newRect);
+  }, [viewport, scale]);
 
   return (
     <>
       <AnnotationContainer
         id={id}
-        top={`${newRect.top || rect.top}px`}
-        left={`${newRect.left || rect.left}px`}
-        width={`${newRect.width || rect.width}px`}
-        height={`${newRect.height || rect.height}px`}
+        top={`${rect.top}px`}
+        left={`${rect.left}px`}
+        width={`${rect.width}px`}
+        height={`${rect.height}px`}
       >
-        <SVG viewBox={calcViewBox(rect, bdwidth * scale)}>
+        <SVG viewBox={calcViewBox(tempRect, borderWidth)}>
           {points.map((ele: PointType[], index: number) => {
             const key = `${id}_path_${index}`;
             return (
@@ -131,7 +88,7 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
                 )}
                 fill="none"
                 stroke={bdcolor}
-                strokeWidth={bdwidth * scale}
+                strokeWidth={borderWidth}
                 strokeOpacity={transparency}
               />
             );
@@ -140,10 +97,10 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
       </AnnotationContainer>
       {!isCollapse ? (
         <OuterRect
-          top={newRect.top || rect.top}
-          left={newRect.left || rect.left}
-          width={newRect.width || rect.width}
-          height={newRect.height || rect.height}
+          top={rect.top}
+          left={rect.left}
+          width={rect.width}
+          height={rect.height}
           onMove={handleScaleOrMove}
           onScale={handleScaleOrMove}
         />

+ 4 - 2
components/OuterRect/index.tsx

@@ -39,7 +39,7 @@ const initState = {
   clickY: 0,
 };
 
-const index: React.FC<Props> = ({
+const OuterRect: React.FC<Props> = ({
   left,
   top,
   width,
@@ -84,6 +84,8 @@ const index: React.FC<Props> = ({
   ): CoordType => ({
     left: currentPosition.x - (startPosition.x - objPosition.left),
     top: currentPosition.y - (startPosition.y - objPosition.top),
+    width: objPosition.width,
+    height: objPosition.height,
   });
 
   const calcScaleResult = (
@@ -177,4 +179,4 @@ const index: React.FC<Props> = ({
   );
 };
 
-export default index;
+export default OuterRect;

+ 2 - 2
components/OuterRectForLine/index.tsx

@@ -27,7 +27,7 @@ const initState = {
   clickY: 0,
 };
 
-const index: React.FC<Props> = ({
+const OuterRect: React.FC<Props> = ({
   top,
   left,
   width,
@@ -136,4 +136,4 @@ const index: React.FC<Props> = ({
   );
 };
 
-export default index;
+export default OuterRect;

+ 19 - 8
components/Page/index.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState, useRef } from 'react';
+import React, { useEffect, useRef } from 'react';
 
 import Watermark from '../Watermark';
 import { renderPdfPage } from '../../helpers/pdf';
@@ -11,10 +11,9 @@ import {
   TextLayer,
   WatermarkLayer,
   Inner,
+  Canvas,
 } from './styled';
 
-let pdfPage: PdfPageType | null = null;
-
 type Props = {
   pageNum: number;
   renderingState: RenderingStateType;
@@ -42,8 +41,14 @@ const PageView: React.FC<Props> = ({
   matchesMap,
   toolState,
 }: Props) => {
+  let pdfPage: any = null;
+  let renderTask: any = null;
+
   const rootEle = useRef<HTMLDivElement | null>(null);
-  const [renderTask, setRenderTask] = useState<RenderTaskType | null>(null);
+
+  const setRenderTask = (task: any) => {
+    renderTask = task;
+  };
 
   const renderPage = async (): Promise<void> => {
     if (getPage) {
@@ -75,11 +80,13 @@ const PageView: React.FC<Props> = ({
   };
 
   useEffect(() => {
-    if (renderingState === 'LOADING' && pdfPage) {
-      pdfPage.cleanup();
-    }
-    if (renderingState === 'RENDERING' && renderTask) {
+    if (renderTask) {
       renderTask.cancel();
+      renderTask = null;
+    }
+    if (pdfPage) {
+      pdfPage.cleanup();
+      pdfPage = null;
     }
     if (renderingState === 'RENDERING') {
       renderPage();
@@ -125,6 +132,10 @@ const PageView: React.FC<Props> = ({
           <AnnotationLayer data-id="annotation-layer">
             {annotations}
           </AnnotationLayer>
+          <Canvas
+            className="canvas"
+            viewBox={`0 0 ${viewport.width} ${viewport.height}`}
+          />
         </>
       )}
     </PageWrapper>

+ 7 - 0
components/Page/styled.ts

@@ -82,3 +82,10 @@ export const Inner = styled.div`
   height: 100%;
   font-size: 1.5rem;
 `;
+
+export const Canvas = styled.svg`
+  display: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+`;

+ 6 - 17
components/Toolbar/styled.ts

@@ -1,14 +1,4 @@
-import styled, { css, keyframes } from 'styled-components';
-
-const closeToolbar = keyframes`
-  from {
-    pointer-events: auto;
-    opacity: 1;
-  }
-  to {
-    opacity: 0;
-  }
-`;
+import styled from 'styled-components';
 
 export const Container = styled('div')<{
   displayMode: string;
@@ -33,12 +23,11 @@ export const Container = styled('div')<{
 
   transition: all 225ms ease-in-out;
 
-  ${(props) =>
-    props.hidden
-      ? css`
-          animation: ${closeToolbar} 3s forwards;
-        `
-      : ''}
+  opacity: ${(props) => (props.hidden ? 0 : 1)};
+
+  &:hover {
+    opacity: 1;
+  }
 `;
 
 export default Container;

+ 103 - 107
containers/FreehandTool.tsx

@@ -1,6 +1,5 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import { v4 as uuidv4 } from 'uuid';
-import MobileDetect from 'mobile-detect';
 
 import { ANNOTATION_TYPE } from '../constants';
 import Icon from '../components/Icon';
@@ -9,9 +8,13 @@ import ExpansionPanel from '../components/ExpansionPanel';
 import InkOption from '../components/InkOption';
 
 import {
-  getAbsoluteCoordinate,
-  parsePositionForBackend,
-} from '../helpers/position';
+  svgPath,
+  bezierCommand,
+  controlPoint,
+  line,
+} from '../helpers/svgBezierCurve';
+
+import { getAbsoluteCoordinate } from '../helpers/position';
 import {
   parseAnnotationObject,
   appendUserIdAndDate,
@@ -28,18 +31,21 @@ type Props = {
   onClick: () => void;
 };
 
+let pathEle: any = null;
+
 const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
-  const [cursorPosition, setRef] = useCursorPosition(20);
+  const [cursorPosition, setRef] = useCursorPosition(30);
 
-  const [uuid, setUuid] = useState('');
+  const [path, setPath] = useState<PointType[]>([]);
   const [data, setData] = useState({
+    page: 0,
     type: 'pen',
     opacity: 100,
     color: '#FF0000',
     width: 3,
   });
-  const [{ viewport, scale, annotations }, dispatch] = useStore();
-  const { addAnnots, updateAnnots } = useActions(dispatch);
+  const [{ viewport, scale }, dispatch] = useStore();
+  const { addAnnots } = useActions(dispatch);
 
   const setDataState = (obj: OptionPropsType): void => {
     setData((prev) => ({
@@ -48,122 +54,112 @@ const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
     }));
   };
 
-  const handleMouseDown = useCallback(
-    (event: MouseEvent | TouchEvent): void => {
-      const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
-      switchPdfViewerScrollState('hidden');
-
-      if (pageEle.hasAttribute('data-page-num')) {
-        setRef(pageEle);
-        const pageNum = pageEle.getAttribute('data-page-num') || 0;
-        const coordinate = getAbsoluteCoordinate(pageEle, event);
-        const id = uuidv4();
-
-        setUuid(id);
-
-        const annotData = {
-          id,
-          obj_type: ANNOTATION_TYPE.ink,
-          obj_attr: {
-            page: pageNum as number,
-            bdcolor: data.color,
-            bdwidth: data.width,
-            position: [[coordinate]],
-            transparency: data.opacity,
-          },
-        };
-        const freehand = appendUserIdAndDate(
-          parseAnnotationObject(annotData, viewport.height, scale),
-        );
-
-        addAnnots([freehand]);
-      }
-    },
-    [data, viewport, scale],
-  );
+  const handleMouseDown = (event: MouseEvent): void => {
+    const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
+    switchPdfViewerScrollState('hidden');
+
+    if (pageEle.hasAttribute('data-page-num')) {
+      setRef(pageEle);
+      const pageNum = pageEle.getAttribute('data-page-num') || '0';
+      const coordinate = getAbsoluteCoordinate(pageEle, event);
+      setData((current) => ({
+        ...current,
+        page: parseInt(pageNum, 10),
+      }));
+      setPath([coordinate]);
+    }
+  };
 
   const handleMouseUp = useCallback((): void => {
     switchPdfViewerScrollState('scroll');
-
-    const index = annotations.length - 1;
-    if (annotations[index]) {
-      const position = annotations[index].obj_attr.position as PointType[][];
-
-      if (!position[0]) return;
-
-      if (position[0].length === 1 && annotations[index].id === uuid) {
-        const point = position[0][0];
-        annotations[index].obj_attr.position = [
-          [
-            { x: point.x - 3, y: point.y - 3 },
-            { x: point.x + 3, y: point.y + 3 },
-          ],
-        ];
-        annotations[index] = appendUserIdAndDate(annotations[index]);
-        updateAnnots([...annotations]);
-      }
-
+    const id = uuidv4();
+
+    if (path.length) {
+      const defaultPoints = [
+        { x: path[0].x - 3, y: path[0].y - 3 },
+        { x: path[0].x + 3, y: path[0].y + 3 },
+      ];
+
+      const annotData = {
+        id,
+        obj_type: ANNOTATION_TYPE.ink,
+        obj_attr: {
+          page: data.page as number,
+          bdcolor: data.color,
+          bdwidth: data.width,
+          position: path.length === 1 ? [defaultPoints] : [path],
+          transparency: data.opacity,
+        },
+      };
+      const freehand = appendUserIdAndDate(
+        parseAnnotationObject(annotData, viewport.height, scale),
+      );
+
+      addAnnots([freehand]);
       setRef(null);
-      setUuid('');
+      setPath([]);
     }
-  }, [annotations, uuid]);
+  }, [path, data, viewport, scale]);
 
   useEffect(() => {
-    const index = annotations.length - 1;
-
-    if (
-      annotations[index] &&
-      annotations[index].id === uuid &&
-      cursorPosition.x &&
-      cursorPosition.y
-    ) {
-      const type = annotations[index].obj_type;
-      const position = annotations[index].obj_attr.position as PointType[][];
-      const coordinates = parsePositionForBackend(
-        type,
-        { x: cursorPosition.x, y: cursorPosition.y },
-        viewport.height,
-        scale,
-      ) as PointType;
-
-      const lastPosition = position[0].slice(-1)[0];
-
-      if (
-        coordinates.x !== lastPosition.x &&
-        coordinates.y !== lastPosition.y
-      ) {
-        position[0].push(coordinates);
-        annotations[index].obj_attr.position = position;
-        annotations[index] = appendUserIdAndDate(annotations[index]);
-        updateAnnots([...annotations]);
+    if (cursorPosition.x && cursorPosition.y) {
+      const coordinates = {
+        x: cursorPosition.x,
+        y: cursorPosition.y,
+      } as PointType;
+
+      setPath((current) => {
+        return [...current, coordinates];
+      });
+    }
+  }, [cursorPosition]);
+
+  /**
+   * 1. draw to canvas when mouse move
+   * 2. trigger mouse up to remove path element
+   */
+  useEffect(() => {
+    const pageEle = document.getElementById(`page_${data.page}`);
+    const canvas = pageEle?.getElementsByClassName('canvas')[0] as HTMLElement;
+
+    if (path.length) {
+      if (pageEle && canvas) {
+        canvas.style.display = 'block';
+
+        if (pathEle) {
+          const d = svgPath(path, bezierCommand(controlPoint(line, 0.2)));
+          pathEle.setAttribute('d', d);
+        } else {
+          pathEle = document.createElementNS(
+            'http://www.w3.org/2000/svg',
+            'path',
+          );
+          pathEle.setAttribute('fill', 'none');
+          pathEle.setAttribute('stroke', data.color);
+          pathEle.setAttribute('stroke-width', data.width * scale);
+          pathEle.setAttribute('stroke-opacity', data.opacity * 0.01);
+          canvas.appendChild(pathEle);
+        }
       }
+    } else if (canvas && pathEle) {
+      canvas.style.display = 'none';
+      canvas.removeChild(pathEle);
+      pathEle = null;
     }
-  }, [annotations, cursorPosition, uuid]);
+  }, [path, data, scale]);
 
   const subscribeEvent = (): void => {
-    const md = new MobileDetect(window.navigator.userAgent);
     const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
 
-    if (md.mobile() || md.tablet()) {
-      pdfViewer.addEventListener('touchstart', handleMouseDown);
-      pdfViewer.addEventListener('touchend', handleMouseUp);
-    } else {
-      pdfViewer.addEventListener('mousedown', handleMouseDown);
-      pdfViewer.addEventListener('mouseup', handleMouseUp);
-    }
+    pdfViewer.addEventListener('mousedown', handleMouseDown);
+    pdfViewer.addEventListener('mouseup', handleMouseUp);
   };
 
   const unsubscribeEvent = (): void => {
-    const md = new MobileDetect(window.navigator.userAgent);
     const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
 
-    if (md.mobile() || md.tablet()) {
-      pdfViewer.removeEventListener('touchstart', handleMouseDown);
-      pdfViewer.removeEventListener('touchend', handleMouseUp);
-    } else {
-      pdfViewer.removeEventListener('mousedown', handleMouseDown);
-      pdfViewer.removeEventListener('mouseup', handleMouseUp);
-    }
+    pdfViewer.removeEventListener('mousedown', handleMouseDown);
+    pdfViewer.removeEventListener('mouseup', handleMouseUp);
   };
 
   useEffect(() => {
@@ -178,7 +174,7 @@ const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
         unsubscribeEvent();
       }
     };
-  }, [isActive, handleMouseDown, handleMouseUp]);
+  }, [isActive, handleMouseUp]);
 
   const Label = (
     <Button

+ 2 - 2
containers/MarkupTools.tsx

@@ -8,7 +8,7 @@ import HighlightTools from './HighlightTools';
 import FreehandTool from './FreehandTool';
 import TextTool from './FreeTextTool';
 import StickyNoteTool from './StickyNoteTool';
-import ShapeTools from './ShapeTools';
+import ShapeTool from './ShapeTool';
 
 import useActions from '../actions';
 import useStore from '../store';
@@ -86,7 +86,7 @@ const MarkupTools: React.FC<Props> = ({
           onClickTool('sticky');
         }}
       />
-      <ShapeTools
+      <ShapeTool
         title={t('shape')}
         isActive={toolState === 'shape'}
         onClick={(): void => {

+ 255 - 0
containers/ShapeTool.tsx

@@ -0,0 +1,255 @@
+import React, { useEffect, useState, useCallback } from 'react';
+import { v4 as uuidv4 } from 'uuid';
+
+import { ANNOTATION_TYPE } from '../constants';
+import Button from '../components/Button';
+import ExpansionPanel from '../components/ExpansionPanel';
+import Icon from '../components/Icon';
+import ShapeOption from '../components/ShapeOption';
+
+import { getAbsoluteCoordinate } from '../helpers/position';
+import {
+  parseAnnotationObject,
+  appendUserIdAndDate,
+} from '../helpers/annotation';
+import { switchPdfViewerScrollState } from '../helpers/pdf';
+import useCursorPosition from '../hooks/useCursorPosition';
+
+import useActions from '../actions';
+import useStore from '../store';
+
+type Props = {
+  title: string;
+  isActive: boolean;
+  onClick: () => void;
+};
+
+let shapeEle: SVGElement | null = null;
+
+const ShapeTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
+  const [startPoint, setStartPoint] = useState({ x: 0, y: 0 });
+  const [endPoint, setEndPoint] = useState({ x: 0, y: 0 });
+
+  const [data, setData] = useState({
+    page: 0,
+    shape: 'square',
+    type: 'fill',
+    color: '#FBB705',
+    opacity: 35,
+    width: 0,
+  });
+  const [cursorPosition, setRef] = useCursorPosition(30);
+
+  const [{ viewport, scale }, dispatch] = useStore();
+  const { addAnnots } = useActions(dispatch);
+
+  const setDataState = (obj: OptionPropsType): void => {
+    setData((prev) => ({
+      ...prev,
+      ...obj,
+    }));
+  };
+
+  const convertPosition = (
+    type: string,
+    x1: number,
+    y1: number,
+    x2: number,
+    y2: number,
+  ): PositionType | LinePositionType => {
+    switch (type) {
+      case 'Line':
+        return {
+          start: {
+            x: x1,
+            y: y1,
+          },
+          end: {
+            x: x2,
+            y: y2,
+          },
+        };
+      default:
+        return {
+          top: y2 > y1 ? y1 : y2,
+          left: x2 > x1 ? x1 : x2,
+          right: x2 > x1 ? x2 : x1,
+          bottom: y2 > y1 ? y2 : y1,
+        };
+    }
+  };
+
+  const handleMouseDown = (event: MouseEvent): void => {
+    switchPdfViewerScrollState('hidden');
+
+    const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
+
+    if (pageEle.hasAttribute('data-page-num')) {
+      setRef(pageEle);
+      const pageNum = pageEle.getAttribute('data-page-num') || '0';
+      const coordinate = getAbsoluteCoordinate(pageEle, event);
+      setData((current) => ({
+        ...current,
+        page: parseInt(pageNum, 10),
+      }));
+      setStartPoint(coordinate);
+    }
+  };
+
+  const handleMouseUp = useCallback((): void => {
+    switchPdfViewerScrollState('scroll');
+    const shapeType = ANNOTATION_TYPE[data.shape];
+    const id = uuidv4();
+
+    if (startPoint.x !== endPoint.x && endPoint.y) {
+      const position = convertPosition(
+        shapeType,
+        startPoint.x,
+        startPoint.y,
+        endPoint.x,
+        endPoint.y,
+      );
+
+      const annoteData = {
+        id,
+        obj_type: shapeType,
+        obj_attr: {
+          page: data.page as number,
+          position,
+          bdcolor: data.color,
+          fcolor: data.type === 'fill' ? data.color : undefined,
+          transparency: data.opacity,
+          ftransparency: data.type === 'fill' ? data.opacity : undefined,
+          bdwidth: data.width,
+          is_arrow: data.shape === 'arrow',
+        },
+      };
+      const shapeAnnotation = appendUserIdAndDate(
+        parseAnnotationObject(annoteData, viewport.height, scale),
+      );
+
+      addAnnots([shapeAnnotation]);
+
+      setRef(null);
+      setStartPoint({ x: 0, y: 0 });
+      setEndPoint({ x: 0, y: 0 });
+    }
+  }, [startPoint, endPoint, viewport, data, scale]);
+
+  const subscribeEvent = (): void => {
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+    pdfViewer.addEventListener('mousedown', handleMouseDown);
+    pdfViewer.addEventListener('mouseup', handleMouseUp);
+  };
+
+  const unsubscribeEvent = (): void => {
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+    pdfViewer.removeEventListener('mousedown', handleMouseDown);
+    pdfViewer.removeEventListener('mouseup', handleMouseUp);
+  };
+
+  useEffect(() => {
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+
+    if (isActive && pdfViewer) {
+      subscribeEvent();
+    }
+
+    return (): void => {
+      if (pdfViewer) {
+        unsubscribeEvent();
+      }
+    };
+  }, [isActive, handleMouseUp]);
+
+  useEffect(() => {
+    if (cursorPosition.x && cursorPosition.y) {
+      setEndPoint({ x: cursorPosition.x, y: cursorPosition.y });
+    }
+  }, [cursorPosition]);
+
+  const checkSvgChild = (type: string) => {
+    switch (type) {
+      case 'square':
+        return 'rect';
+      case 'circle':
+        return 'ellipse';
+      default:
+        return 'line';
+    }
+  };
+
+  useEffect(() => {
+    const pageEle = document.getElementById(`page_${data.page}`);
+    const canvas = pageEle?.getElementsByClassName('canvas')[0] as HTMLElement;
+
+    if (endPoint.x && endPoint.y) {
+      canvas.style.display = 'block';
+
+      if (shapeEle) {
+        const { top, left, right, bottom } = convertPosition(
+          ANNOTATION_TYPE[data.shape],
+          startPoint.x,
+          startPoint.y,
+          endPoint.x,
+          endPoint.y,
+        ) as PositionType;
+
+        if (data.shape === 'square') {
+          shapeEle.setAttribute('x', `${left}`);
+          shapeEle.setAttribute('y', `${top}`);
+          shapeEle.setAttribute('width', `${right - left}`);
+          shapeEle.setAttribute('height', `${bottom - top}`);
+        } else if (data.shape === 'circle') {
+          const xRadius = (right - left) / 2;
+          const yRadius = (bottom - top) / 2;
+          shapeEle.setAttribute('cx', `${left + xRadius}`);
+          shapeEle.setAttribute('cy', `${top + yRadius}`);
+          shapeEle.setAttribute('rx', `${xRadius}`);
+          shapeEle.setAttribute('ry', `${yRadius}`);
+        } else {
+          const actualWidth = data.width * scale;
+          shapeEle.setAttribute('x1', `${startPoint.x + actualWidth}`);
+          shapeEle.setAttribute('y1', `${startPoint.y + actualWidth}`);
+          shapeEle.setAttribute('x2', `${endPoint.x + actualWidth}`);
+          shapeEle.setAttribute('y2', `${endPoint.y + actualWidth}`);
+        }
+      } else {
+        shapeEle = document.createElementNS(
+          'http://www.w3.org/2000/svg',
+          checkSvgChild(data.shape),
+        );
+        shapeEle.setAttribute('fill', data.color);
+        shapeEle.setAttribute('fill-opacity', `${data.opacity * 0.01}`);
+        shapeEle.setAttribute('stroke', data.color);
+        shapeEle.setAttribute('stroke-width', `${data.width * scale}`);
+        shapeEle.setAttribute('stroke-opacity', `${data.opacity * 0.01}`);
+        canvas.appendChild(shapeEle);
+      }
+    } else if (canvas && shapeEle) {
+      canvas.style.display = 'none';
+      canvas.removeChild(shapeEle);
+      shapeEle = null;
+    }
+  }, [startPoint, endPoint, data, scale]);
+
+  const Label = (
+    <Button
+      shouldFitContainer
+      align="left"
+      onClick={onClick}
+      isActive={isActive}
+    >
+      <Icon glyph="shape" style={{ marginRight: '10px' }} />
+      {title}
+    </Button>
+  );
+
+  return (
+    <ExpansionPanel isActive={isActive} label={Label}>
+      <ShapeOption {...data} setDataState={setDataState} />
+    </ExpansionPanel>
+  );
+};
+
+export default ShapeTool;

+ 0 - 228
containers/ShapeTools.tsx

@@ -1,228 +0,0 @@
-import React, { useEffect, useState, useCallback } from 'react';
-import { v4 as uuidv4 } from 'uuid';
-
-import { ANNOTATION_TYPE } from '../constants';
-import Button from '../components/Button';
-import ExpansionPanel from '../components/ExpansionPanel';
-import Icon from '../components/Icon';
-import ShapeOption from '../components/ShapeOption';
-
-import {
-  getAbsoluteCoordinate,
-  parsePositionForBackend,
-} from '../helpers/position';
-import {
-  parseAnnotationObject,
-  appendUserIdAndDate,
-} from '../helpers/annotation';
-import { switchPdfViewerScrollState } from '../helpers/pdf';
-import useCursorPosition from '../hooks/useCursorPosition';
-
-import useActions from '../actions';
-import useStore from '../store';
-
-type Props = {
-  title: string;
-  isActive: boolean;
-  onClick: () => void;
-};
-
-const Shape: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
-  const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
-  const [uuid, setUuid] = useState('');
-  const [data, setData] = useState({
-    shape: 'square',
-    type: 'fill',
-    color: '#FBB705',
-    opacity: 35,
-    width: 0,
-  });
-  const [cursorPosition, setRef] = useCursorPosition(20);
-
-  const [{ viewport, scale, annotations }, dispatch] = useStore();
-  const { addAnnots, updateAnnots } = useActions(dispatch);
-
-  const setDataState = (obj: OptionPropsType): void => {
-    setData((prev) => ({
-      ...prev,
-      ...obj,
-    }));
-  };
-
-  const convertPosition = (
-    type: string,
-    x1: number,
-    y1: number,
-    x2: number,
-    y2: number,
-  ): PositionType | LinePositionType => {
-    switch (type) {
-      case 'Line':
-        return {
-          start: {
-            x: x1,
-            y: y1,
-          },
-          end: {
-            x: x2,
-            y: y2,
-          },
-        };
-      default:
-        return {
-          top: y2 > y1 ? y1 : y2,
-          left: x2 > x1 ? x1 : x2,
-          right: x2 > x1 ? x2 : x1,
-          bottom: y2 > y1 ? y2 : y1,
-        };
-    }
-  };
-
-  const addShape = useCallback(
-    (
-      pageEle: HTMLElement,
-      event: MouseEvent | TouchEvent,
-      attributes: OptionPropsType,
-    ): void => {
-      const { shape = '', type, opacity, color, width = 0 } = attributes;
-      const pageNum = pageEle.getAttribute('data-page-num') || 0;
-      const coordinate = getAbsoluteCoordinate(pageEle, event);
-      const id = uuidv4();
-
-      setUuid(id);
-      setStartPosition(coordinate);
-
-      const shapeType = ANNOTATION_TYPE[shape];
-      const position = convertPosition(
-        shapeType,
-        coordinate.x - 8,
-        coordinate.y - 8,
-        coordinate.x + 8,
-        coordinate.y + 8,
-      );
-      const annoteData = {
-        id,
-        obj_type: shapeType,
-        obj_attr: {
-          page: pageNum as number,
-          position,
-          bdcolor: color,
-          fcolor: type === 'fill' ? color : undefined,
-          transparency: opacity,
-          ftransparency: type === 'fill' ? opacity : undefined,
-          bdwidth: width,
-          is_arrow: shape === 'arrow',
-        },
-      };
-      const shapeAnnotation = appendUserIdAndDate(
-        parseAnnotationObject(annoteData, viewport.height, scale),
-      );
-
-      addAnnots([shapeAnnotation]);
-    },
-    [viewport, scale, data],
-  );
-
-  const handleMouseDown = (event: MouseEvent | TouchEvent): void => {
-    switchPdfViewerScrollState('hidden');
-
-    const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
-
-    if (pageEle.hasAttribute('data-page-num')) {
-      addShape(pageEle, event, data);
-      setRef(pageEle);
-    }
-  };
-
-  const handleMouseUp = (): void => {
-    switchPdfViewerScrollState('scroll');
-    setRef(null);
-    setUuid('');
-    setStartPosition({ x: 0, y: 0 });
-  };
-
-  const subscribeEvent = (): void => {
-    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
-    pdfViewer.addEventListener('mousedown', handleMouseDown);
-    pdfViewer.addEventListener('mouseup', handleMouseUp);
-    pdfViewer.addEventListener('touchstart', handleMouseDown);
-    pdfViewer.addEventListener('touchend', handleMouseUp);
-  };
-
-  const unsubscribeEvent = (): void => {
-    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
-    pdfViewer.removeEventListener('mousedown', handleMouseDown);
-    pdfViewer.removeEventListener('mouseup', handleMouseUp);
-    pdfViewer.removeEventListener('touchstart', handleMouseDown);
-    pdfViewer.removeEventListener('touchend', handleMouseUp);
-  };
-
-  useEffect(() => {
-    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
-
-    if (isActive && pdfViewer) {
-      subscribeEvent();
-    }
-
-    return (): void => {
-      if (pdfViewer) {
-        unsubscribeEvent();
-      }
-    };
-  }, [isActive, addShape]);
-
-  const handleUpdate = useCallback(
-    (start: PointType, end: PointType): void => {
-      const index = annotations.length - 1;
-      const { x: x1, y: y1 } = start;
-      const { x: x2, y: y2 } = end;
-
-      if (annotations[index] && annotations[index].id === uuid) {
-        const type = annotations[index].obj_type;
-        const position = convertPosition(type, x1, y1, x2, y2);
-
-        annotations[index].obj_attr.position = parsePositionForBackend(
-          type,
-          position,
-          viewport.height,
-          scale,
-        );
-        annotations[index] = appendUserIdAndDate(annotations[index]);
-
-        updateAnnots([...annotations]);
-      }
-    },
-    [annotations, viewport, scale, uuid],
-  );
-
-  useEffect(() => {
-    if (
-      startPosition.x &&
-      startPosition.y &&
-      cursorPosition.x &&
-      cursorPosition.y
-    ) {
-      handleUpdate(startPosition, cursorPosition as PointType);
-    }
-  }, [startPosition, cursorPosition]);
-
-  const Label = (
-    <Button
-      shouldFitContainer
-      align="left"
-      onClick={onClick}
-      isActive={isActive}
-    >
-      <Icon glyph="shape" style={{ marginRight: '10px' }} />
-      {title}
-    </Button>
-  );
-
-  return (
-    <ExpansionPanel isActive={isActive} label={Label}>
-      <ShapeOption {...data} setDataState={setDataState} />
-    </ExpansionPanel>
-  );
-};
-
-export default Shape;

+ 54 - 0
helpers/brush.ts

@@ -40,3 +40,57 @@ export const completePath: CompletePathType = (pathElement, penCoordinates) => {
     pathElement.setAttribute('points', points);
   }
 };
+
+export const pointCalc = (
+  _points: PointType[][],
+  h: number,
+  s: number,
+): PointType[][] => {
+  const reducer = _points.reduce(
+    (acc: PointType[][], cur: PointType[]): PointType[][] => {
+      const p = cur.map((point: PointType) => ({
+        x: point.x * s,
+        y: h - point.y * s,
+      }));
+      acc.push(p);
+      return acc;
+    },
+    [],
+  );
+
+  return reducer;
+};
+
+export const rectCalcWithPoint = (
+  pointsGroup: PointType[][],
+  borderWidth: number,
+): HTMLCoordinateType => {
+  const xArray: number[] = [];
+  const yArray: number[] = [];
+
+  pointsGroup[0].forEach((point: PointType) => {
+    xArray.push(point.x);
+    yArray.push(point.y);
+  });
+
+  const top = Math.min(...yArray);
+  const left = Math.min(...xArray);
+  const bottom = Math.max(...yArray);
+  const right = Math.max(...xArray);
+  return {
+    top,
+    left,
+    width: right - left + borderWidth,
+    height: bottom - top + borderWidth,
+  };
+};
+
+export const calcViewBox = (
+  { top, left, width, height }: HTMLCoordinateType,
+  borderWidth: number,
+): string => {
+  const distance = borderWidth / 2;
+  return `
+  ${left - distance} ${top - distance} ${width + distance} ${height + distance}
+`;
+};