이제 대충 레이아웃은 만들어 졌으니 캔버스에 글릭하면 선을 그려지는 기능을 구현한다.
사용자가 brush 도구를 선택한다 -> 캔버스에 마우스 클릭을 하여 선을 그린다.
먼저 index.html 에 작성한 컴포넌트들을 불러온다.
src/index.js
assignElement() {
this.containerEl = document.getElementById("container");
this.canvasEl = this.containerEl.querySelector("#canvas");
this.toolbarEl = this.containerEl.querySelector("#toolbar");
this.brushEl = this.toolbarEl.querySelector("#brush");
}
무언가를 표시하기 위해서, 어떤 스크립트가 랜더링 컨텍스트에 접근하여 그리도록 할 필요가 있습니다. canvas 요소는 getContext() 메서드를 이용해서, 랜더링 컨텍스트와 (렌더링 컨텍스트의) 그리기 함수들을 사용할 수 있습니다. getContext() 메서드는 렌더링 컨텍스트 타입을 지정하는 하나의 파라메터를 가집니다.
캔버스에 무언가를 표시하기 위해서는 어떤 스크립트가 랜더링 컨텍스트에 접근해서 그리도록 할 필요가 있는데 canvas의 경우는 getContext()메서드를 사용해서 랜더링 컨텍스트와 그리기 함수들을 사용할 수 있다. getContext() 메서드의 경우에는 렌더링 컨텍스트 타입을 지정하는 파라미터를 가진다. 2d 혹은 3d 인데 이번 경우는 점과 선들로만 그려지기 때문에 2d로 사용한다.
initContext() {
this.context = this.canvasEl.getContext("2d");
}
제일 처음으로 해야할 일은 사용자가 왼쪽 toolbar에 있는 도구 선택 목록중에 brush 툴을 선택하면 해당 기능이 활성화 되었됨과 동시에 사용자에게 시각적으로 보여주는 것이다. 하지만 그 전에 추가해야할 변수가 존재하는데, 이것은 사용자가 선택할수 있는 도구는 brush 뿐만 아니라 그림을 지우는 지우개또한 마우스 클릭으로 선택이 된다. 따라서 어떤 도구를 선택했는지를 표시해줄 변수가 필요하므로 이를 위해 MODE
변수를 만든다.
MODE = "NONE"; //NONE BRUSH ERASER
이제 사용자가 brush 도구를 클릭하면 발생하는 이벤트를 추가한다
addEvent() {
this.brushEl.addEventListener("click", this.onClickBrush.bind(this));
}
사용자가 brush를 클릭하면 MODE
변수의 값을 BRUSH로 변경하고 brushEl에 active 클래스를 추가해서 진한 색으로 나타나게 해준다. active 클래스를 기본 툴바의 색깔보다 더 진한색으로 선언했기 때문에 색이 변하는 것이다. 사용자가 클릭하게 되면 다시 원래 색을 돌아온다.
onClickBrush(event) {
this.MODE = "BRUSH";
this.brushEl.classList.toggle("active");
}
요런 식으로.
시각적으로는 사용중인 여부를 파악할수는 있지만, brush 도구가 active된 상태에서 클릭을 한번 더 할 경우에 MODE의 값이 변동되지 않는다. 따라서 사용자가 도구를 클릭했을때 active 클래스가 존재여부에 따라 MODE의 값을 다르게 설정해줘야한다.
onClickBrush(event) {
this.MODE = event.currentTarget.classList.contains("active") ? "NONE" : "BRUSH";
this.brushEl.classList.toggle("active");
}
사용자가 brush를 클릭했을 때, 이미 active 클래스가 들어있다면 MODE의 값을 NONE으로 변경, 그 반대의 경우에 BRUSH로 바꿔줌으로써 MODE의 값을 변경시키게 해준다.
currentTarget
을 사용하는 이유는 사용자가 클릭하는 brush 도구의 html 구조는
<div class="tool brush" id="brush">
<i class="fas fa-paint-brush"></i>
</div>
이렇게 div와 i가 들어있는데, 클릭하는 것을 div로만 지정하고 싶기 때문에 사용하는 것이다.
그다음으로는 brush를 사용중일때 커서 포인터의 모양을 바꾸고 싶다.
this.canvasEl.style.cursor = event.currentTarget.classList.contains(
"active"
)
? "default"
: "crosshair";
active 클래스가 존재하는 상태에서 사용자가 brush를 선택하면 기본 포인터로, 그 반대의 경우에는 crosshair 형태의 포인터가 나타난다.
event.currentTarget.classList.contains("active")가 반복적으로 사용되므로 코드를 간단하게 작성한다.
onClickBrush(event) {
const IsActive = event.currentTarget.classList.contains("active");
this.MODE = IsActive ? "NONE" : "BRUSH";
this.canvasEl.style.cursor = IsActive ? "default" : "crosshair";
this.brushEl.classList.toggle("active");
}
자 그럼 이제 캔버스에 이벤트를 적용해서 선이 그려지게 해보자.
선이 그려지는 것은 캔버스에 마우스를 클릭하면 발생하는 이벤트이기 때문에 addEvent()에 onMouseDown
이벤트를 추가한다.
this.canvasEl.addEventListener("mousedown", this.onMouseDown.bind(this));
마우스 클릭의 여부를 판단하기 위한 IsMouseDown
변수를 생성한다.
MODE = "NONE"; //NONE BRUSH ERASER
IsMouseDown = false; // true false
만약 MODE의 값이 NONE일 경우에는 그냥 종료하고, 그게 아니라면 IsMouseDown
의 값을 true로 변경한뒤 현재 클릭한 좌표를 가져오는 변수인 currentPosition을 생성한다.
onMouseDown(event) {
if(this.MODE === "NONE") return;
this.IsMouseDown = true;
const currentPosition = this.getMousePosition(event);
}
getMousePosition(event)
메소드를 작성한다.
getMousePosition(event) {
const boundaries = this.canvasEl.getBoundingClientRect();
return {
x : clientX - boundaries.left,
y : clientY - boundaries.top,
}
}
getBoundingClientRect()에서는 페이지 최상단의 맨 왼쪽의 점을 (0,0)으로 두고 계산한다. 따라서 캔버스를 기준으로 좌표를 지정한 것이 아니기 때문에, 캔버스의 기준을 맞추기 위해서 사용자가 클릭한 좌표에서 left의 값을 빼줘야 제대로된 x좌표가 나타나는 것이다. y좌표 또한 이와 마찬가지로 계산한다.
이제 이전에 작성한 context
를 사용해서 직선이 그려지는 예시를 작성해보자.
onMouseDown(event) {
if (this.MODE === "NONE") return;
this.IsMouseDown = true;
const currentPosition = this.getMousePosition(event);
this.context.beginPath(); //시작한다.
this.context.moveTo(currentPosition.x, currentPosition.y); //사용자가 클릭한 위치에서부터
this.context.lineCap = "round"; //끝부분은 동그라미 모양으로
this.context.stokeStyle = "#000000"; //색깔은 검정색
this.context.lineWidth = 10; //선의 두께는 10으로
this.context.lineTo(400,400); // (400,400)좌표로 선이 그려지게끔
this.context.stroke(); // 그려라
}
이렇게 작성하고 난 다음에 실행해보면
캔버스 어느 곳을 클릭하던지 간에 일정한 좌표(400,400)으로 향하는 직선이 만들어지게 된다.
이런 방식을 활용하면 사용자가 클릭해서 움직이는 만큼 원하는 방식으로 그림을 그릴수 있게 될 것이다. 하지만 그렇게 하려면 lineTo
의 경우 좌표가 움직이기 때문에 고정값이 아닌 사용자가 움직이는 좌표를 사용해야한다.
addEvent()
에 onMouseMove 이벤트를 추가한다.
addEvent() {
this.brushEl.addEventListener("click", this.onClickBrush.bind(this));
this.canvasEl.addEventListener("mousedown", this.onMouseDown.bind(this));
this.canvasEl.addEventListener("mousemove", this.onMouseMove.bind(this));
}
onMouseMove(event) {
if (!this.IsMouseDown) return;
const currentPosition = this.getMousePosition(event);
this.context.lineTo(currentPosition.x, currentPosition.y);
this.context.stroke();
}
이렇게 해주면 사용자가 캔버스에 마우스를 클릭한 상태로 커서를 움직이면 경로에 따라 선이 그려지게 된다
이렇게만 보면 다 된것 같지만, 사용자가 마우스 클릭을 떼도 선이 계속 그려지게 된다.
마우스클릭후 다시 떼면 그림 그려지는 것을 멈추게 해야한다.
onMouseUp
이벤트를 추가한다.
addEvent() {
this.brushEl.addEventListener("click", this.onClickBrush.bind(this));
this.canvasEl.addEventListener("mousedown", this.onMouseDown.bind(this));
this.canvasEl.addEventListener("mousemove", this.onMouseMove.bind(this));
this.canvasEl.addEventListener("mouseup", this.onMouseUp.bind(this));
}
마우스를 클릭하다 떼면 IsMouseDown
의 값을 false로 바꿔서 onMouseDown을 종료 시킨다.
만약 MODE 값이 NONE일 때는 자동으로 종료된다.
onMouseUp() {
if (this.MODE === "NONE") return;
this.IsMouseDown = false;
}
사용자가 마우스를 떼면 그림 그려지는 것이 멈춘다.
아직 아니다. 사용자가 원하는 색상으로 선이 나타나야 하고, brush
도구를 선택 했을 때 brushSizePrevieContainer
가 나타나서 두께를 수정할 수 있어야 한다.
사용자가 원하는 색상으로 선이 그려지게 해보자.
먼저 색상을 선택하는 컴포넌트를 불러온다.
assignElement() {
...
this.colorPickerEl = this.toolbarEl.querySelector("#colorPicker");
}
onMouseDown
메서드에서 strokeStyle의 값을 지정된 값 대신에 this.colorPickerEl.value
로 지정한다. 이렇게 하면 사용자가 선택한 색상의 값을 그려지는 선의 색상으로 지정할 것이다.
onMouseDown(event) {
if (this.MODE === "NONE") return;
...
this.context.strokeStyle = this.colorPickerEl.value;
this.context.lineWidth = 10;
}
그리고 다시 실행해보면
원하는 색상으로 그림이 그려지는 것을 확인할 수 있다.
마지막으로 선의 두께를 설정하면 그대로 적용되는 기능을 추가해야한다.
먼저 브러시 패널을 클릭하게 되면 brushSizePrevieContainer
가 나타나게 설정해야한다.
컴포넌트를 불러오고,
assignElement() {
this.brushPanelEl = this.containerEl.querySelector("#brushPanel");
}
onClickBrush 메서드에 brushPanelEl
클래스에다가 hide
를 토글시키면 끝이다.
const IsActive = event.currentTarget.classList.contains("active");
...
this.brushPanelEl.classList.toggle("hide");
}
브러시 아이콘을 클릭할 때마다 brushSizePrevieContainer
가 나타나고 사라지게 된다.
사이즈 조정에 따라 오른쪽에 위치한 미리보기에도 변경된 크기가 나타나게 해야한다.
컴포넌트를 가져오기 -> 이벤트 추가
assignElement() {
this.brushSliderEl = this.brushPanelEl.querySelector("#brushSize");
this.brushSizePriviewEl =
this.brushPanelEl.querySelector("#brushSizePreview");
}
addEvent() {
this.brushSliderEl.addEventListener(
"input",
this.onChangeBrushSize.bind(this)
);
}
onChangeBrushSize(event) {
this.brushSizePriviewEl.style.width = `${event.target.value}px`;
this.brushSizePriviewEl.style.height = `${event.target.value}px`;
}
onChangeBrushSIze
를 통해서 정해진 burshSizePriviewEl 의 width 값을 onMouseDown
메서드의 this.context.lineWidth
에 추가시키면 된다.
onMouseDown(event) {
if (this.MODE === "NONE") return;
...
this.context.lineWidth = this.brushSliderEl.value;
}
이제 실행해보면
사용자가 선택한 너비에 따라 그려지는 선의 두께가 달라지는 모습을 볼 수 있다.
전반적인 기능은 끝났지만, 사용자가 색상을 고른뒤에 브러쉬 미리보기에서의 색깔이 변경 되지 않는 모습을 볼 수 있는데 이를 수정하려고 한다.
colorPickerEl
에다가 onChangeColor
이벤트를 추가시키고, 이는 brushSizePriviewEl
의 스타일 값중 background
의 값을 사용자가 선택한 색상으로 지정한다.
addEvent() {
this.brushEl.addEventListener("click", this.onClickBrush.bind(this));
...
this.colorPickerEl.addEventListener("input", this.onChangeColor.bind(this));
}
onChangeColor(event) {
this.brushSizePriviewEl.style.background = event.target.value;
}
그리고 다시 실행하면
사용자가 고른 색상을 미리보기에서도 보여주게 된다.
하나만 더 해보자. 사용자가 그림을 그리다가 캔버스를 벗어나면 onMouseUp
과 같은 이벤트가 발생하게 해서 캔버스에 커서를 가져와도 선이 그려지지 않게 할 것이다.
canvasEl
에 이벤트를 추가하고,
this.canvasEl.addEventListener("mouseout", this.onMouseOut.bind(this));
onMouseOut
메서드를 작성해준다.
onMouseOut() {
if (this.MODE === "NONE") return;
this.IsMouseDown = false;
}
이렇게 해주면 마우스 커서가 캔버스를 벗어난 뒤에 다시 돌아와도 선이 그려지지 않게 된다.
요런 식으로.
이제 그림판의 선그리기 기능은 완성됐다.