본문 바로가기
Font-end/Javascript

[그림판] HTML, CSS, JS로 그림판 만들기

by 허도치 2020. 3. 3.

 [ 예제 캡쳐화면 ]

 

 

0. 서론

 어제 Javascript를 이용해서 Element를 드로그앤드롭으로 움직이는 예제를 만들었는데, 이 재밌는 기능을 다른데 활용하고 싶어졌다. 생각을 하다가 그림판을 만들어보면 좋을것 같아서 만들어보았다. 그림판이라면 선을 그을 수 있어야하지만, 이 그림판은 도형과 텍스트만 지원한다. 간단하게 만든거라 완성도는 떨어지지만 재밌는 예제라고 생각한다. 어려운 로직이 포함된 것이 간단한 설명과 함께 정리해보았다.

 그럼 예제를 통해서 어떻게 그림판을 만드는지 알아보자.

 

 

 

1. 프로젝트 준비
1-1. Chrome Browser

1) IE와 같은 구형브라우저에서는 ES를 적용하려면 BABEL을 이용해야하기 때문에 Chrome Browser를 사용.

2) 다운로드
   - https://www.google.com/intl/ko/chrome/

 

1-2. 드로그 앤 드롭 기능

1) Element를 자유자재로 움직일 수 있는 기능
   : [Drag&Drop] 드래그 앤 드롭으로 Element 움직이기 포스트 참조

 

1-3. 프로젝트 구성

1) [ css/common.css ]
   : 공통으로 적용할 stylesheet 파일.

2) [ css/paint.css ]
   : 그림판에 관련된 stylesheet 파일.

3) [ js/common.js ]
   : Element에 이벤트를 바인딩하는 javascript 파일.

4) [ js/event_handler.js ]
   : Element에 적용할 이벤트를 작성하는 javascript 파일.

5) [ js/function.js ]
   : Element 생성 및 기타 함수들을 작성하는 javascript 파일.

6) [ index.html ]
   : 그림판을 그려줄 HTML 파일.

 

 

 

2. css/common.css
2-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
/* css/common.js */
 
/* 기본 설정 */
{ padding: 0; margin: 0; }
html, body { width: 100%; height: 100%; }
 
/* 수평 정렬 */
.vertical > div { float: left; }
 
/* 폰트 정렬 */
.align-left { text-align: left; }
.align-right { text-align: right; }
.align-center { text-align: center; }
cs

 

 

 

3. css/paint.css
3-2. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/* css/paint.css */
 
/* 배경 색상 */
.bg-green { background-color: lightgreen; }
.bg-blue { background-color: lightblue; }
.bg-pink { background-color: lightpink; }
.bg-white { background-color: white; }
.bg-black { background-color: black; }
 
/* 폰트 색상 */
.green { color: lightgreen; }
.blue { color: lightblue; }
.pink { color: lightpink; }
.white { color: white; }
.black { color: black; }
 
/* 도형 모양 */
.circle { border-radius: 50%; }
.rect { }
.text { }
 
/* 그림판 */
#paint { width: 100%; height: 100%; }
 
/* 도형 설정 */
#palette { width: 20%; height: 100%; padding: 5px; box-sizing: border-box; border: 1px solid darkgray; border-right: none; }
 
#palette dl { width: 100%; height: auto; margin-bottom: 10px; box-sizing: border-box; border: 1px dotted darkgray; padding: 3px; }
#palette dt { background-color: lightgray; font-weight: bold; }
#palette dd + dt { margin-top: 25px; }
 
/* 도형 모양 옵션 */
#palette .object-shape label { opacity: 30%; }
#palette .object-shape label:hover { cursor: pointer; font-size: 1.2em; opacity: 80%;}
#palette .object-shape input[type="radio"] { display: none; }
#palette .object-shape input[type="radio"]:checked + label { font-size: 1.5em; font-weight: bold; opacity: 100%; }
 
/* 도형 사이즈 옵션 */
#palette .object-size input[type="text"] { width: 50px; }
 
/* 도형 색상 옵션 */
#palette .object-color label { border: 1px solid darkgray; }
#palette .object-color label:hover { cursor: pointer; font-size: 1.2em; }
#palette .object-color input[type="radio"] { display: none; }
#palette .object-color input[type="radio"]:checked + label { font-size: 1.5em; font-weight: bold; }
 
/* 도형 투명도 옵션 */
#palette .object-opacity input:disabled { background-color: inherit; border: none; }
#palette .object-opacity input[type="text"] { width: 32px }
#palette .object-opacity input[type="range"] { width: 70%; }
 
/* 도화지 설정 */
#canvas { width: 80%; height: 100%; box-sizing: border-box; border: 1px solid darkgray; }
#canvas > .wrapper { width: 100%; height: 100%; max-width: 100%; max-height: 100%; position: relative; overflow: hidden; }
 
/* 도형이 텍스트인 경우 */
#canvas input.text { border: none; }
 
/* 도형의 위치를 절대값으로 적용하도록 설정 */
#canvas .object { position: absolute; }
 
/* 도형을 클릭했을 때, 그림자가 생기도록 설정 */
#canvas .object:hover { box-shadow: 0 0 10px 0 darkgray; cursor: pointer; }
cs

1) [ 53 ln ] - 도형이 도화지를 벗어나면 잘리도록 설정
   : "{overflow: hidden}"

 

 

 

4. js/functions.js
4-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// js/functions.js
 
// 속성에서 최대값 구하기
function getMaxAttr(objects, key, _default){
  return (
    objects.length > 0
      ? Math.max.apply(nullArray.from(objects).map(object=>object.getAttribute(key))) 
      : _default
  )+1;
}
 
// 도형 element 생성
function createObject(setting){
  const canvas = document.querySelector("#canvas > .wrapper");
  const objects = canvas.querySelectorAll(".object");
  
  let classList = ["object"];
  let object = document.createElement("div");  
  if( setting.shape === "text" ){
    object = document.createElement("input");
    object.type = "text";
  }
  
  // 도형 모양 설정
  if( setting.shape ){
    classList.push(setting.shape);
  }
  
  // 배경 색상 설정
  if( setting.bg_color ){
    classList.push(setting.bg_color);
  }
  
  // 폰트 색상 설정
  if( setting.font_color ){
    classList.push(setting.font_color);
  }
  
  // 우선순위 설정
  const priority = getMaxAttr(objects, "priority"0);
  object.setAttribute("priority", priority);
  object.style["z-index"= priority;
  
  // 도형 사이즈 설정
  if( setting.width && setting.height ){
    object.style.width = setting.width+"px";
    object.style.height = setting.height+"px";
  }
  
  // 폰트 사이즈 설정
  if( setting.font_size ){
    object.style["font-size"= setting.font_size+"px";
  }
  
  // 투명도 설정
  if( setting.opacity ){
    object.style["opacity"= setting.opacity+"%";
  }
  
  // class 설정
  object.className = classList.join(" ");
  
  // 이벤트 바인딩
  object.addEventListener("mousedown", handleObjectHold);
  object.addEventListener("dblclick", handleObjectRemove);
  
  return object;
}
cs

1) [ 4-10 ln ] - getMaxAttr(objects, key, _default)
   : element의 속성에서 최대값을 구하는 함수.
   : 우선순위(priority)를 구할 때 사용됨.

2) [ 13-68 ln ] - createObject(setting)
   : 선택한 옵션을 적용한 도형을 생성하는 함수.
   : [ 18-22 ln ] - 도형은 '<div>', 텍스트는 '<input>' 태그를 사용.
   : [ 64 ln ] - 도형을 선택했을 때, 도형을 holding시키는 이벤트를 바인딩.
   : [ 65 ln ] - 도형을 더블클릭했을 때, 도형을 삭제하는 이벤트를 바인딩.

 

 

 

5. js/event_handler.js
5-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// js/event_handler.js
 
// 도형 생성 이벤트 핸들러
function handleCreateObject(event){
  event.preventDefault();
  
  const canvas = document.querySelector("#canvas > .wrapper");
  const palette = document.getElementById("palette_form");
  const object_state = palette.querySelectorAll('input[type="text"], input[type="radio"]:checked');
  
  // 도형 설정값을 JSON형태로 치환.
  const settings = Array.from(object_state).reduce(function(prev, crnt, idx){
    if( idx === 1 ) {
      const _setting = {}
      _setting[prev.name= prev.value;
      _setting[crnt.name= crnt.value;
      return _setting;
    }
    prev[crnt.name=crnt.value;
    
    return prev;
  });
  
  // 도형 생성
  const object = createObject(settings);
  
  // 도형을 도화지에 추가
  canvas.appendChild(object);
  
  // 도형이 텍스트인 경우, 바로 글을 작성할 수 있도록 포커싱.
  if( settings.shape === "text" ){
    object.focus(); 
  }
}
 
// 도형 삭제 이벤트 핸들러
function handleObjectRemove(event){
  event.preventDefault();
  event.target.remove();
}
 
// 도형 선택 이벤트 핸들러
function handleObjectHold(event){
  event.preventDefault();
  
  event.target.focus();
  
  const canvas = document.querySelector("#canvas > .wrapper");
  const objects = canvas.querySelectorAll(".object");
  let seledted_object = event.target;
  let classList = seledted_object.classList;
  
  if!classList.contains("hold") ){
    // 마우스 커서의 위치 (왼쪽 상단 모서리 기준)
    const mouseX = event.clientX;
    const mouseY = event.clientY;
    
    // 선택한 도형의 위치 (왼쪽 상단 모서리 기준)
    const objectPos = seledted_object.getBoundingClientRect();
    const objectX = objectPos.x;
    const objectY = objectPos.y;
    
    // 도형과 마우스의 위치 차이
    const gapX = mouseX - objectX;
    const gapY = mouseY - objectY;
    
    // 도형과 마우스의 위치 차이를 속성에 저장
    seledted_object.setAttribute("gap-x", gapX);
    seledted_object.setAttribute("gap-y", gapY);
    
    // 선택한 도형을 맨 앞으로 보내기
    const priority = getMaxAttr(objects, "priority"0);
    seledted_object.setAttribute("priority", priority);
    seledted_object.style["z-index"= priority;
    
    // 선택한 도형에 'hold' class를 추가
    classList.add("hold");
  }
}
 
// 도형 움직임 이벤트 핸들러
function handleObjectDrag(event){
  event.preventDefault();
    
  const canvas = document.querySelector("#canvas > .wrapper");
  const object = canvas.querySelector(".object.hold");
  if( object ){
    // 마우스 커서의 위치. (왼쪽 상단 모서리 기준)
    const mouseX = event.clientX;
    const mouseY = event.clientY;
    
    // 도화지의 위치. (왼쪽 상단 모서리 기준)
    const canvasPos = canvas.getBoundingClientRect();
    const canvasX = canvasPos.x;
    const canvasY = canvasPos.y;
    
    // 도형과 마우스의 위치 차이
    const gapX = object.getAttribute("gap-x");
    const gapY = object.getAttribute("gap-y");
    
    // 도형이 이동할 위치
    const objectX = mouseX - gapX - canvasX;
    const objectY = mouseY - gapY - canvasY;
    
    object.style.left = objectX+"px";
    object.style.top = objectY+"px";
  }
}
 
// 도형 놓기 이벤트 핸들러
function handleObjectDrop(event){
  event.preventDefault();
    
  const canvas = document.querySelector("#canvas > .wrapper");
  const object = canvas.querySelector(".object.hold");
  if( object ){
    // 속성 및 class를 삭제
    object.removeAttribute("gap-x")
    object.removeAttribute("gap-y")
    
    object.classList.remove("hold");
  }
}
cs

1) [ 4-34 ln ] - handleCreateObject(event)
   :
'생성' 버튼을 '클릭'했을 때, 도형을 생성하는 이벤트 핸들러.
   : [ 9 ln ] - 설정한 속성값들이 있는 input 태그들을 탐색.
   : [ 12-22 ln ] - input태그의 'name'속성과 'value'속성으로 JSON형태로 치환.

2) [ 37-40 ln ] - handleObjectRemove(event)
   : 도형을 '더블클릭'했을 때, 도형을 삭제하는 이벤트 핸들러.

3) [ 43-79 ln ] - handleObjectHold(event)
   : 도형을 선택했을 때, 잡고있는 상태를 적용하는 이벤트 핸들러.

4) [ 82-108 ln ] - handleObjectDrag(event)
   : 마우스를 움직였을 때, 도형을 이동시킬 위치를 구하고 적용하는 이벤트 핸들러.

5) [ 111-123 ln ] - handleObjectDrop(event)
   : 도형을 놓았을 때, 잡고있는 상태를 해제하는 이벤트 핸들러.

 

 

 

6. js/common.js
6-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
// js/common.js
 
// '생성' 버튼에 클릭 이벤트 바인딩
const btn_create = document.getElementById("create_object");
btn_create.addEventListener("click", handleCreateObject);
 
// 마우스 이벤트 추가
document.addEventListener('mousemove', handleObjectDrag);
document.addEventListener('mouseup', handleObjectDrop);
cs

 

 

 

7. index.html
7-1. 스크립트 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<!-- index.html -->
 
<!DOCTYPE html>
<html>
  <head>
    <title>그림판</title>
    <link href="css/common.css" rel="stylesheet"/>
    <link href="css/paint.css" rel="stylesheet"/>
    <script src="js/functions.js"></script>
    <script src="js/event_handler.js"></script>
  </head>
  <body>
    <div id="paint" class="vertical">
      <div id="palette">
        <form id="palette_form" onsubmit="return false;">
          <dl class="object-setting">
            <dt>모양</dt>
            <dd class="object-shape">
              <input type="radio" id="shape_circle" name="shape" value="circle" checked>
              <label for="shape_circle"></label>
              <input type="radio" id="shape_rect" name="shape" value="rect">
              <label for="shape_rect"></label>
              <input type="radio" id="shape_text" name="shape" value="text" onchange="size_height.value=18; font_size.value=15; font_black.checked=true; bg_white.checked=true;">
              <label for="shape_text">T</label>
            </dd>
            <dt>사이즈</dt>
            <dd class="object-size">
              <label for="size_width">W</label>
              <input type="text" id="size_width" class="align-right" name="width"value="100"><a>px</a><br>
            </dd>
            <dd class="object-size">
              <label for="size_height">H</label>
              <input type="text" id="size_height" class="align-right" name="height" value="100"><a>px</a>
            </dd>
            <dt>글자 크기</dt>
            <dd class="object-size">
              <label for="size_font">Font</label>
              <input type="text" id="size_font" class="align-right" name="font_size"value="32"><a>px</a><br>
            </dd>
            <dt>배경 색상</dt>
            <dd class="object-color bg-color">
              <table>
                <tbody>
                  <tr>
                    <td>
                      <input type="radio" id="bg_black" name="bg_color" value="bg-black" checked>
                      <label for="bg_black" class="bg-black black"></label>
                    </td>
                    <td>
                      <input type="radio" id="bg_white" name="bg_color" value="bg-white">
                      <label for="bg_white" class="bg-white white"></label>
                    </td>
                    <td>
                      <input type="radio" id="bg_blue" name="bg_color" value="bg-blue">
                      <label for="bg_blue" class="bg-blue blue"></label>
                    </td>
                    <td>
                      <input type="radio" id="bg_pink" name="bg_color" value="bg-pink">
                      <label for="bg_pink" class="bg-pink pink"></label>
                    </td>
                    <td>
                      <input type="radio" id="bg_green" name="bg_color" value="bg-green">
                      <label for="bg_green" class="bg-green green"></label>
                    </td>
                  </tr>
                </tbody>              
              </table>
            </dd>
            <dt>글자 색상</dt>
            <dd class="object-color font-color">
              <table>
                <tbody>
                  <tr>
                    <td>
                      <input type="radio" id="font_black" name="font_color" value="black" checked>
                      <label for="font_black" class="bg-black black"></label>
                    </td>
                    <td>
                      <input type="radio" id="font_white" name="font_color" value="white">
                      <label for="font_white" class="bg-white white"></label>
                    </td>
                    <td>
                      <input type="radio" id="font_blue" name="font_color" value="blue">
                      <label for="font_blue" class="bg-blue blue"></label>
                    </td>
                    <td>
                      <input type="radio" id="font_pink" name="font_color" value="pink">
                      <label for="font_pink" class="bg-pink pink"></label>
                    </td>
                    <td>
                      <input type="radio" id="font_green" name="font_color" value="green">
                      <label for="font_green" class="bg-green green"></label>
                    </td>
                  </tr>
                </tbody>              
              </table>
            </dd>
            <dt>투명도</dt>
            <dd class="object-opacity"><input type="text" id="opacity" name="opacity" value="100" class="align-right" disabled>%</dd>
            <dd><input type="range" name="opacity_slider" min="1" max="100" value="100" onchange="opacity.value=this.value"></dd>
          </dl>
          <div class="buttons align-right">
            <button id="create_object">생성</button>
          </div>
        </form>
      </div>
      <div id="canvas">
        <div class="wrapper">
        </div>
      </div>
    </div>
    <script src="js/common.js"></script>
  </body>
</html>
cs

1) [ 7-10 ln ] - css, javascript 파일 로드.

2) [ 17-25 ln ] - 도형의 모양을 선택하는 영역.
   : [ 23 ln ] - onchange를 통해 '텍스트'가 선택되었을때, 폰트 사이즈, 도형 높이, 색상 등의 초기값을 설정

3) [ 26-34 ln ] - 도형의 높이와 너비를 설정하는 영역.

4) [ 35-39 ln ] - 글자의 크기를 설정하는 영역.

5) [ 40-68 ln ] - 도형의 색상을 설정하는 영역.

6) [ 69-97 ln ] - 글자의 색상을 설정하는 영역.

7) [ 98-100 ln ] - 도형의 투명도를 설정하는 영역.
   : input태그의 'range' 타입을 이용하여 slider로 만듦.

8) [ 103 ln ] - '생성' 버튼.

9) [ 112 ln ] - Element가 모드 로드된 후 javascript 파일을 로드.
   : 'head'에서 로드할 경우 Element가 생성되기 전에 파일이 로드되어 Element를 참조할 수 없음.

 

 

 

 

8. 실행
8-1. index.html을 실행

1) 파일을 더블클릭하여 실행

2) 파일을 Chrome Browser로 드로그앤드롭하여 실행

3) 주소창에 파일의 위치를 입력하여 실행
   : file:///C:/work/javascript/paint/index.html

 

8-2. 실행 결과

[ 실행 화면 ]

1) 도형을 선택

2) 사이즈를 설정
   : 텍스트를 선택한 경우 높이는 적절하게 설정.
   : 글자 크기 + 3px 정도가 적당함.

3) 배경과 글자 색상 선택

4) 투명도 선택
   : 테스트했을 때, 버튼을 눌러서 좌우로 슬라이드하면 동작을 안하고 막대를 눌러줘야함.

5) '생성' 버튼 클릭

6) 생성된 도형을 더블클릭하면 삭제

 

 

 

마치며

- 그림판을 만들다보니 생각보다 재밌어서 여러 기능들을 추가하느라 소스가 조금 길어졌다. 예제에는 글자 설정이나 색상 설정 부분에 미흡한 부분들이 많지만 직접 추가해보는 것을 추천한다.

댓글