├── .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 | 
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키 등의 이벤트가 발생하면 해당 위치에 `