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(null, Array.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) 생성된 도형을 더블클릭하면 삭제
마치며
- 그림판을 만들다보니 생각보다 재밌어서 여러 기능들을 추가하느라 소스가 조금 길어졌다. 예제에는 글자 설정이나 색상 설정 부분에 미흡한 부분들이 많지만 직접 추가해보는 것을 추천한다.
'Font-end > Javascript' 카테고리의 다른 글
[Context Menu] 나만의 컨텍스트 메뉴 만들기 (1) | 2020.05.29 |
---|---|
[Redux] 무작정 시작하기 (1) - Redux란? (0) | 2020.04.23 |
[모듈] Prototype을 이용한 모듈 패턴으로 Element 모듈 만들기 (0) | 2020.03.19 |
[공적마스크API] 공적 마스크 판매 정보 조회 (2) | 2020.03.18 |
[Drag&Drop] 드래그 앤 드롭으로 Element 움직이기 (0) | 2020.03.02 |
댓글