├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .prettierrc ├── @types ├── InitialSetting.ts ├── ShapesHistory.ts └── index.ts ├── README.md ├── images ├── architecture.png ├── demo.gif ├── example.jpeg ├── logo.png └── paper.png ├── lib ├── bundle.js └── bundle.js.map ├── package.json ├── release.sh ├── reports ├── 304_정종윤(경희대학교).mp4 ├── CD1_6팀(최종보고서).docx └── HTML5_Canvas_기반_오픈소스_이미지_에디터_라이브러리_개발.pdf ├── rollup.config.js ├── src ├── assets │ ├── Rectangle.png │ └── circle.png ├── components │ ├── EditableText.tsx │ ├── ImageHandler.tsx │ ├── JsonHandler.tsx │ ├── TransformableCircle.tsx │ ├── TransformableImage.tsx │ ├── TransformableLine.tsx │ ├── TransformableRect.tsx │ └── index.ts ├── context │ ├── HistoryContext.tsx │ ├── ShapesContext.tsx │ └── index.ts ├── hooks │ ├── index.ts │ ├── useDraggable.ts │ ├── useDrawing.ts │ ├── useFilter.ts │ ├── useFocusable.ts │ ├── useIdCounter.ts │ ├── useResizer.ts │ ├── useShapeCache.ts │ ├── useShapes.ts │ ├── useStage.ts │ ├── useTransformer.ts │ └── useZoom.ts ├── index.ts ├── layout │ ├── Canvas.tsx │ ├── Editor.tsx │ ├── Panel.tsx │ ├── Toolbar.tsx │ └── index.ts └── types │ └── modules.d.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ['plugin:react/recommended', 'airbnb'], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | ecmaFeatures: { 10 | jsx: true, 11 | }, 12 | ecmaVersion: 12, 13 | sourceType: 'module', 14 | }, 15 | plugins: ['react', '@typescript-eslint', 'react-hooks'], 16 | rules: { 17 | 'no-alert': 'off', 18 | camelcase: 'off', 19 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 21 | 'global-require': 0, 22 | 'import/no-unresolved': 0, 23 | 'class-methods-use-this': 0, 24 | 'import/order': ['error', { 'newlines-between': 'always' }], 25 | 'react/jsx-filename-extension': 0, 26 | 'no-unused-vars': 'off', 27 | '@typescript-eslint/no-unused-vars': 'off', 28 | 'import/extensions': 0, 29 | '@typescript-eslint/no-use-before-define': ['error'], 30 | 'no-use-before-define': 'off', 31 | 'import/prefer-default-export': 'off', 32 | 'react/jsx-props-no-spreading': 'off', 33 | 'max-len': ['error', { code: 80 }], 34 | 'arrow-body-style': [ 35 | 'error', 36 | 'as-needed', 37 | { requireReturnForObjectLiteral: true }, 38 | ], 39 | 'implicit-arrow-linebreak': 'off', 40 | 'comma-dangle': [ 41 | 'error', 42 | { 43 | functions: 'never', 44 | arrays: 'always-multiline', 45 | objects: 'always-multiline', 46 | }, 47 | ], 48 | 'jsx-a11y/label-has-associated-control': 'off', 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /MEMO.md 4 | lib/bundle.js 5 | lib/bundle.js.map 6 | 7 | /tests/e2e/videos/ 8 | /tests/e2e/screenshots/ 9 | 10 | 11 | # local env files 12 | .env.local 13 | .env.*.local 14 | 15 | # Log files 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | pnpm-debug.log* 20 | 21 | # Editor directories and files 22 | .idea 23 | .vscode 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.22.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "htmlWhitespaceSensitivity": "ignore", 3 | "printWidth": 80, 4 | "trailingComma": "es5", 5 | "tabWidth": 2, 6 | "semi": true, 7 | "singleQuote": true, 8 | "useTabs": false, 9 | "arrowParens": "always" 10 | } 11 | -------------------------------------------------------------------------------- /@types/InitialSetting.ts: -------------------------------------------------------------------------------- 1 | export interface InitialSetting { 2 | width?: number; 3 | height?: number; 4 | maxWidth?: number; 5 | maxHeight?: number; 6 | responsive?: boolean; 7 | aspectRatio?: number; 8 | } 9 | -------------------------------------------------------------------------------- /@types/ShapesHistory.ts: -------------------------------------------------------------------------------- 1 | import Konva from 'konva'; 2 | 3 | export type ShapesHistory = Konva.ShapeConfig[]; 4 | -------------------------------------------------------------------------------- /@types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './InitialSetting'; 2 | export * from './ShapesHistory'; 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-konva-image-editor 2 | 3 |

4 | 5 |

6 | 7 | ## Subject 8 | 9 | > - (국문) HTML5 Canvas 기반 오픈소스 이미지 에디터 라이브러리 개발 10 | > - (영문) Opensource Image Editor Library Development based on HTML5 Canvas 11 | 12 | - 본 프로젝트는 2021-2학기 경희대학교 캡스톤 디자인 1 - 산업체 주제(모바일 앱개발 협동조합)를 바탕으로 개발했습니다. 13 | 14 | ## Members 15 | 16 | - 정종윤(@wormwlrm) 17 | 18 | ## Abstract 19 | 20 | HTML5부터 등장한 Canvas API를 이용하면 웹에서도 이미지 편집을 비롯한 다양한 그래픽 기술을 처리할 수 있다. 하지만 기본적으로 제공하는 API의 추상화 단계가 낮고, 그래픽 도메인에 대한 지식을 별 도로 학습해야 한다는 단점이 있다. 따라서 대부분의 경우에는 Canvas API를 직접 활용하기보다는 완성 된 라이브러리를 활용하는 방법을 선택한다. 21 | 22 | 본 프로젝트에서는 이미지 편집에 초점을 맞추어, 2D 그래픽 기반의 Canvas API를 활용한 오픈소스 이미지 에디터를 구현한다. 이미지 에디터에서 제공하는 다양한 편집 기능들을 어떠한 Canvas API와 디자인 패턴으로 구현 가능한지를 연구하고, 이 과정에서 마주치는 한계점들을 극복하는 방안에 대해 알아본다. 23 | 24 | ## Overview 25 | 26 |

27 | 28 |

29 | 30 | > 별도 제작한 [데모 페이지](https://wormwlrm.github.io/react-konva-image-editor-demo)에서 동작하는 예시를 확인할 수 있습니다. 31 | 32 | 본 프로젝트에서는 다음과 같은 기능을 지원합니다. 기능에 대한 자세한 설명은 아래에서 확인할 수 있습니다. 33 | 34 | - [x] 라이브러리 형태로 패키지 릴리스 35 | - [x] 호스트 옵션 제공 36 | - [x] 이미지/도형 컴포넌트 생성 및 편집 37 | - [x] 수정 가능한 텍스트 컴포넌트 제공 38 | - [x] 컴포넌트 복제/삭제 39 | - [x] 실행 취소/다시 실행 40 | - [x] 드로잉 41 | - [x] 이미지 저장 42 | - [x] 캔버스 확대 및 축소 43 | - [x] Z-index 조정 44 | - [x] 이미지 저장 45 | - [x] 직렬화를 이용한 수정 내역 저장 및 복원 기능 46 | 47 | ### Motivation 48 | 49 | 이 프로젝트는 산학 협력 프로젝트의 주제의 응용으로부터 시작하였습니다. 기존 주제는 이미 배포된 이미지 편집 라이브러리를 사용하는 웹 애플리케이션을 제작하는 구현 프로젝트였습니다. 하지만 이미 구현된 라이브러리를 단순히 이용하는 것은 큰 의미가 없다고 생각했고, 보다 도전적으로 접근해서 다른 사람이 사용할 수 있는 라이브러리 자체를 제작해보고 싶다는 생각이 들었습니다. 이 과정에서 산업체의 허락을 구했고, 웹 환경에서 이미지 편집 기능이 포함된 라이브러리를 직접 제작하기로 했습니다. 50 | 51 | ### Considerations 52 | 53 | - **추상화**: Canvas API에서 제공하는 기본 추상화 단계가 낮아, 실제 애플리케이션 레벨에서 기능을 쓰기에는 생산성이 낮습니다. 따라서 2D 그래픽에 대한 기본 추상화를 제공하는 라이브러리 Konva.js를 활용하였습니다. 54 | - **상태 관리**: Canvas 위에 그려진 이미지, 도형 및 수정 사항들을 객체지향적으로 관리하면서 데이터와 뷰를 일치시키고자 React.js를 활용하였습니다. 55 | - **라이브러리**: JavaScript 모듈 시스템에 적합한 형태여야 하고, 브라우저 및 패키지 관리자를 통해 다운로드 가능해야 하므로 Rollup.js 및 NPM을 사용함으로써 활용하였습니다. 56 | 57 | ### Architecture 58 | 59 | 본 프로젝트의 아키텍처는 다음과 같습니다. 60 | 61 | ![구조도](images/architecture.png) 62 | 63 | - **Shapes Layer**: 캔버스에 그려지는 모든 도형, 사용자 상태 등을 관리합니다. 또한 캔버스와 패널, 툴바 사이의 상호작용을 중재합니다. 64 | - **Snapshot**: 특정한 사용자 액션(도형 생성, 이동, 스타일 수정 등)이 발생하여 히스토리를 저장해야 할 경우, 현재 도형을 스냅샷으로 저장하고 히스토리에 추가합니다. 65 | - **History Layer**: 저장된 스냅샷 배열을 기반으로 실행 취소 및 다시 실행 기능을 지원합니다. 66 | - **Canvas Layer**: Shapes Layer에 저장된 도형들을 실제 2D 그래픽으로 표현하고, 사용자로부터 입력받은 상호작용을 Shapes Layer에 넘겨줍니다. 67 | - **Toolbar Layer**: 사용자 상호작용을 명시적으로 입력받는 영역입니다. 68 | - **Panel Layer**: 현재 선택된 도형의 속성을 표시하는 영역입니다. 69 | 70 | ### Features 71 | 72 | 본 프로젝트에서는 다음과 같은 기능을 지원합니다. 73 | 74 | #### 라이브러리 형태로 패키지 릴리스 75 | 76 | 데모 프로젝트 확인을 위해 현재까지의 개발 사항을 node.js 패키지 매니저인 [NPM](https://www.npmjs.com/package/react-konva-image-editor)에 배포한 상태입니다. 따라서 터미널에서 다음과 같은 명령어를 입력하면 설치가 가능합니다. 77 | 78 | ```bash 79 | # for this package 80 | $ npm install react-konva-image-editor 81 | 82 | # for peer dependencies 83 | $ npm install react react-dom 84 | ``` 85 | 86 | 호스트 측에서 다음과 같이 호출하여 사용할 수 있습니다. 87 | 88 | ```js 89 | // App.js 90 | import { Editor } from 'react-konva-image-editor'; 91 | 92 | return ( 93 | 94 | ); 95 | ``` 96 | 97 | #### 호스트 옵션 제공 98 | 99 | 라이브러리를 설치하여 사용하는 호스트에서 에디터 레이아웃에 대한 옵션값을 설정할 수 있습니다. 100 | 101 | ```js 102 | const props = { 103 | width = window.innerHeight, // Number, 명시적으로 너비 설정 104 | height = 500, // Number, 명시적으로 높이 설정 105 | responsive = false, // Boolean, 반응형 설정 106 | aspectRatio = 1, // Number, 반응형 설정 시 너비와 높이 비율 설정 107 | } 108 | 109 | return ( 110 | 111 | ); 112 | ``` 113 | 114 | #### 이미지/도형 컴포넌트 생성 및 편집 115 | 116 | 117 | 118 | 캔버스에 이미지와 도형 인스턴스를 생성할 수 있습니다. 각 도형은 드래그 가능하며(Draggable), 자체적으로 회전, 리사이징이 가능(Transformable)합니다. 119 | 120 | 이미지 및 도형 인스턴스는 Shape Layer에서 관리되며, 각 컴포넌트에 대한 속성 정의는 `components` 폴더에 정의되어 있습니다. 121 | 122 | 이미지를 불러올 때에는 이미지 파일을 Base64로 인코딩하여 사용합니다. 123 | 124 | #### 실행 취소/다시 실행 125 | 126 | 127 | 128 | 실행 취소와 다시 실행 기능을 위해 메멘토 패턴을 적용했습니다. 메멘토 패턴은 히스토리를 저장하는 Caretaker 역할, 그리고 히스토리를 가리키는 인덱스를 띄워주는 Originator 역할으로 구성됩니다. 129 | 130 | 여기서 Caretaker 역할을 하는 것이 History Layer입니다. 도형에 대한 사용자 상호작용(드래그, 회전, 리사이징 등)이 발생할 때마다 History Layer는 현재 Shapes Layer의 인스턴스를 스냅샷을 생성하고 배열로 관리합니다. 131 | 132 | History Layer에는 특정 스냅샷을 가리키는 인덱스가 정의되어 있습니다. 따라서 실행 취소 이벤트가 발생할 때에는 인덱스를 1만큼 감소시키며, 다시 실행 이벤트가 발생할 때에는 인덱스를 1만큼 증가시킵니다. 133 | 134 | Shapes Layer는 현재 History Layer에서 가리키는 스냅샷을 가져옵니다. Canvas Layer는 Shapes Layer의 인스턴스 배열에 종속적이기 때문에, 저장된 인스턴스를 캔버스에 표시합니다. 135 | 136 | #### 드로잉 137 | 138 | 139 | 140 | 드로잉은 기본적으로 드래그 앤 드롭과 동일한 입력을 받지만 다르게 동작해야 합니다. 따라서 툴바에서 드로잉 모드를 선택하게 되면, 캔버스에 있는 도형들은 더 이상 드래그 가능하지 않게 됩니다. 이는 도형 위에서 드로잉을 시작했을 때, 도형이 드래그 되는 현상을 막기 위해서입니다. 141 | 142 | 마우스 커서를 누르면 `onmousedown` 이벤트가 발생하게 되는데, 이때 x, y 좌표를 구할 수 있습니다. 따라서 이 좌표를 이산적으로 덧붙이면서 라인 컴포넌트의 경로를 표시할 수 있습니다. 143 | 144 | 라인 컴포넌트 역시 도형 인스턴스로 취급됩니다. 따라서 드래그와 변환이 가능합니다. 145 | 146 | 하지만 마우스를 떼기 전까지 라인 컴포넌트는 아직 생성된 상태가 아니지만, 현재까지의 경로를 캔버스에 표시해주어야 할 필요가 있습니다. 따라서 마우스를 떼기 전까지 좌표 배열을 임시로 저장하고, 마우스를 뗄 때 저장된 좌표 배열을 이용해 Shape Layer에 새 라인 컴포넌트를 생성합니다. 147 | 148 | #### 수정 가능한 텍스트 컴포넌트 제공 149 | 150 | 151 | 152 | HTML5 스펙에 따르면, 캔버스에서 텍스트를 렌더할 수는 있지만 캔버스 내에서 입력값을 직접 수정할 수 있는 인풋(input) 형태로는 제공되지 않습니다. 따라서 해당 기능이 필요한 경우가 있다면 Canvas API를 적절히 우회하여야 합니다. 153 | 154 | 수정 가능한 텍스트 컴포넌트를 만들기 위해서는 Canvas API와 외부 DOM 엘리먼트 간 스타일 및 데이터를 동기화했습니다. 155 | 우선 캔버스에 텍스트를 나타낼 수 있는 텍스트 컴포넌트를 정의합니다. 이 역시 도형 인스턴스로 취급되므로 드래그와 변환이 가능해야 합니다. 156 | 157 | 만약 텍스트 컴포넌트에 더블 클릭이나 enter키 등의 이벤트가 발생하면 해당 위치에 `