본문 바로가기

Javascript/응용프로젝트

자바스크립트로 그림판 만들기 (+ 다운로드 기능)


강의를 보고 난 후 혼자 정리하는 느낌이라.. 조금 조잡하고 설명이 정확하지 않을 수 있습니다
HTML,CSS는 강의자료로 받았고, 기능만 구현했습니다.


완성본


기능 설명

1. 그림 그리는 기능

  • 붓 크기 조절 가능
  • color-picker 기능

2. 지우개 

3. 네비게이터

4. 되돌아가기

5. 휴지통 기능 (초기화 기능)

6. 그린 그림 다운로드


HTML 과 CSS

강의 자료이기때문에, 참고용으로 보시라고 캡쳐본으로 올립니다

index.html
style.scss


기초 작업

 

스크립트는 ES6 클래스 문법을 사용했습니다. ( 저도 아직 ES6 문법에 익숙치 않아서 기능 구현에 중점을 맞춰서 설명합니다 )

 

사용할 엘리먼트 변수 지정

  MODE = "NONE"; // 클릭한 메뉴를 알아내기위한 변수
  IsMouseDown = false; // 마우스를 누르고 있는지 알아내기 위한 변수
  eraserColor = "#ffffff"; // 지우개 색상 (많이 쓰이기때문에 변수로 지정)
  backgroundColor = "#ffffff"; // 캔버스 배경 색상 (배경이 투명으로 되어있기때문에)
  IsNavigatorVisible = false; // 네비게이터가 활성화되어있는지에 대한 변수
  undoArray = []; // 되돌아가기 위해, 그린 데이터들을 저장해놓을 공간

  containerEl; // 전체를 묶고 있는 부모 컨테이너
  canvasEl; // 그리는 공간인 캔버스
  toolbarEl; // 캔버스 옆에 tool 들을 묶고 있는 부모요소
  brushEl; // 브러쉬 툴
  colorPickerEl; // 컬러 피커 툴
  brushPanelEl; // 브러쉬를 선택했을 때 나오는 브러쉬 크기 지정 패널
  brushSliderEl; // 브러쉬 크기를 조절해주는 아이 (패널)
  brushSizePreviewEl; // 브러쉬 크기를 미리보게 해주는 아이 (패널)
  eraserEl; // 지우개 툴
  navigatorEl; // 네비게이터 툴
  navigatorImageContainerEl; // 네비게이터의 이미지를 감싸고 있는 부모
  navigatorImageEl; // 네비게이터의 이미지 태그
  undoEl; // 되돌아가기 툴
  clearEl; // 초기화 툴
  downloadLinkEl; // 다운로드 툴

 

Element 가져오기

  assignElement() {
    this.containerEl = document.getElementById("container");
    this.canvasEl = this.containerEl.querySelector("#canvas");
    this.toolbarEl = this.containerEl.querySelector("#toolbar");
    this.brushEl = this.toolbarEl.querySelector("#brush");
    this.colorPickerEl = this.toolbarEl.querySelector("#colorPicker");
    this.brushPanelEl = this.containerEl.querySelector("#brushPanel");
    this.brushSliderEl = this.brushPanelEl.querySelector("#brushSize");
    this.brushSizePreviewEl =
      this.brushPanelEl.querySelector("#brushSizePreview");
    this.eraserEl = this.toolbarEl.querySelector("#eraser");
    this.navigatorEl = this.toolbarEl.querySelector("#navigator");
    this.navigatorImageContainerEl = this.containerEl.querySelector("#imgNav");
    this.navigatorImageEl =
      this.navigatorImageContainerEl.querySelector("#canvasImg");
    this.undoEl = this.toolbarEl.querySelector("#undo");
    this.clearEl = this.toolbarEl.querySelector("#clear");
    this.downloadLinkEl = this.toolbarEl.querySelector("#download");
  }

 

addEventListener 함수를 사용하는 코드 묶기

addEvent(){ 추후에 들어갈 addEventListener 함수 )

 

캔버스의 기본 배경 색상 넣어주는 함수

  initCanvasBackgroundColor() {
    this.context.fillStyle = this.backgroundColor;
    this.context.fillRect(0, 0, this.canvasEl.width, this.canvasEl.height); // 캔버스 기준으로 0,0 좌표에서 직사각형을 그려줌
  }
this.context.fillStyle
- 도형을 채우는 색을 설정
this.context.fillRect
- fillRect (x,y,width,height)
- 색이 채워진 사각형을 그려줌

출처 : https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=javaking75&logNo=140169690495

 

캔버스의 위치를 정의하는 함수

  initContext() {
    this.context = this.canvasEl.getContext("2d");
  }
getContext("2d")
- 캔버스에 2d 좌표로 위치를 지정할 수 있게 됨.

 

constructor 에 클래스 필드를 선언하고 초기화 해주기

  constructor() {
    this.assignElement();
    this.initContext();
    this.initCanvasBackgroundColor();
    this.addEvent();
  }

그리기 기능

 

이벤트리스너 함수

  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));
    this.canvasEl.addEventListener("mouseout", this.onMouseOut.bind(this));
  }

 

onClickBrush () - 브러쉬 툴 선택 함수

  onClickBrush(event) {
    const IsActive = event.currentTarget.classList.contains("active");
    this.MODE = IsActive ? "NONE" : "BRUSH";
    this.canvasEl.style.cursor = IsActive ? "default" : "crosshair";
    this.brushPanelEl.classList.toggle("hide");
    event.currentTarget.classList.toggle("active");
    this.eraserEl.classList.remove("active");
  }
1. 브러쉬 탭을 클릭하면 "active" 라는 클래스가 toggle로 추가되고 제거될 수 있도록 한다.
2. 처음 눌렀을 때 (즉 , "active" 라는 클래스가 없을 때) 클릭하면 false가 나온다
3. 툴 모드를 IsActive 변수의 값을 활용해서 "NONE" : "BRUSH" 로 정한다.
4. 커서 모양도 IsActive 변수의 값을 활용해서 "default" : "crosshair" 로 정한다.
5. 브러쉬 모드가 활성화된다면, 브러쉬 패널을 보여준다 (toggle 을 활용해서 브러쉬 모드를 끄면 없어지도록 함)
6. 지우개 모드가 활성화된다면, 브러쉬 모드가 비활성화 된 모습을 보여주기 위해 "active" 클래스를 지워주도록 한다.

 

onMouseDown() - 캔버스에 마우스를 눌렀을 때 함수

  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";
    if (this.MODE === "BRUSH") {
      this.context.strokeStyle = this.colorPickerEl.value;
      this.context.lineWidth = this.brushSliderEl.value;
    } else if (this.MODE === "ERASER") {
      this.context.strokeStyle = this.eraserColor;
      this.context.lineWidth = 50;
    }
  }
1. 캔버스에 마우스를 누르는데, 모드가 "NONE" 이라면 아무런 동작을 하지 않도록 함.
2. IsMouseDown 변수에 true 로 값을 바꿔준다. (누르고 있는 중이라는 것을 알기 위해서)
3. currentPosition 을 구해준다. (밑에서 자세한 기능 설명)
4. context.beginPath()를 사용해, 새로운 경로를 만들어, 그리기 명령들에게 경로를 구성하고 만드는데 사용할 수 있도록 함.
5. getMousePosition() 에서 구한 좌표를 토대로 그린 선의 경로를 시작하고 위치를 좌표로 이동할 수 있도록 함.
6. context.lineCap 을 "round" 로 지정해 둥근 선을 그릴 수 있도록 함.
7. 그리기 모드라면 선 색상을 color-picker에서 정한 값으로 바꿀 수 있도록 해줌.
7-1. 선 굵기는 브러쉬 패널에서 지정한 굵기로 그릴 수 있도록 해줌.
8. 지우개 모드라면 지우개 컬러로 선 색상을 바꿔줌. ( #fff ) 
8-1. 지우개 모드일 때 선 굵기는 임의로 지정해줌 ( 저는 50px )

 

getMousePosition()

  getMousePosition(event) {
    const boundaries = this.canvasEl.getBoundingClientRect();
    return {
      x: event.clientX - boundaries.left,
      y: event.clientY - boundaries.top,
    };
  }
1. 먼저 캔버스의 크기와 캔버스의 위치를 구함.
2. 얻고 있는 마우스 위치가 clientX, clientY
3. 마우스 위치가 클라이언트 창에 상대적이므로 요소 자체에 상대적으로 변환하려면 캔버스 요소의 위치를 빼야함.
4. x 좌표 y 좌표를 반환한다.

 

getBoundingClientReact() 메소드에 대해서
- 엘리먼트의 크기 (즉 캔버스) 와 뷰포트에 상대적인 위치 정보를 제공하는 DOMRect 객체를 반환
- 반환값 : padding, border-width 를 포함한 전체 엘리먼트가 들어 있는 가장 작은 사각형인 DOMRect 객체
"left" 와 "top"
left : 현재 창기준 가로 시작점부터 엘리먼트 왼쪽변까지의 거리
top : 화면 상단 부터 대상의 처음 위치 값
event.clientX , event.clientY
- 브라우저에서 사용자에게 웹페이지가 보여지는 영역을 기준으로 좌표를 표시함, 스크롤바가 움직이더라도 특정 지점의 값은 같다.

boundaries 와 clientX, clientY 를 콘솔에 찍은 모습

 

onMouseMove () - 캔버스에 마우스를 움직일 때 이벤트

 

  onMouseMove(event) {
    if (!this.IsMouseDown) return;
    const currentPosition = this.getMousePosition(event);
    this.context.lineTo(currentPosition.x, currentPosition.y);
    this.context.stroke();
  }
1. 마우스를 누르고 있는 상태가 아니라면 아무런 실행도 하지않는다.
2. 마우스를 움직일 때 좌표를 구해준다 (getMousePosition 함수 사용)
3. lineTo 메서드에 좌표 x, y 값을 넣어줘서 마우스 이동 경로에 맞게 직선이 추가될 수 있도록 한다.
4. stroke() 메서드를 호출해, 현재 지정된 경로를 윤곽선으로 그려질 수 있도록 해준다.
lineTo()
- 하위 경로의 마지막 지점을 지정된 좌표에 연결하여 현재 하위 경로에 직선을 추가함 (x, y)
- 이 메서드는 아무 것도 직접 렌더링하지 않음.
- 캔버스에 경로를 그리려면 fill() 또는 stroke() 메서드를 사용해야함.
stroke()
- 현재 지정된 경로를 그려주는 메서드

 

onMouseUp() - 캔버스에 마우스를 뗐을 때 이벤트

  onMouseUp() {
    if (this.MODE === "NONE") return;
    this.IsMouseDown = false;
  }
1. 모드를 아무것도 선택하지 않았다면 아무런 실행도 하지않는다.
2. 마우스를 뗐을 때, 마우스 다운 변수를 false 로 바꿔주어 마우스가 뗀 상태임을 알려준다.

 

onMouseOut() - 캔버스에서 마우스가 나갔을 때 이벤트

  onMouseOut() {
    if (this.MODE === "NONE") return;
    this.IsMouseDown = false;
  }
onMouseUp() 함수와 같음.

 

그리기 기능에서 정리할 부분

ctx.beginPath(); // Start a new path
ctx.moveTo(30, 50); // Move the pen to (x, y)
ctx.lineTo(150, 100); // Draw a line to (x, y)
ctx.stroke(); // Render the path
onMouseDown() 에서 beginPath 메서드 와 moveTo(x, y) 메서드를 사용했다.
onMouseMove() 에서 lineTo(x,y) 메서드 와 stroke() 메서드를 사용했다.
-> 이 4가지는 거의 이 순서로 사용된다고 보면 될거같다.

브러쉬 미리보기 크기,색상 보여주기

 

이벤트리스너 함수

  addEvent() {
	...
    this.brushSliderEl.addEventListener(
      "input",
      this.onChangeBrushSize.bind(this)
    );
    this.colorPickerEl.addEventListener("input", this.onChangeColor.bind(this));
  }

 

onChangeBrushSize() - 브러쉬 미리보기에서 크기 보여주기

  onChangeBrushSize(event) {
    this.brushSizePreviewEl.style.width = `${event.target.value}px`;
    this.brushSizePreviewEl.style.height = `${event.target.value}px`;
  }

console.log(event.target.value)

1. 브러쉬 크기 조절은 이미 mouseDown() 에서 했음.
2. 조절한 value 값을 previewEl에게 style 주면 됨 . (px 붙여야함)

 

onChangeColor() - 브러쉬 미리보기에서 색상 보여주기

  onChangeColor(event) {
    this.brushSizePreviewEl.style.backgroundColor = event.target.value;
  }

console.log(event.target.value)

1. 이런식으로 컬러픽커에서 색상을 고르면, 색상코드를 value로 줌.
2. value 를 미리보기 엘리먼트에게 backgroundColor 로 주면 끝

지우개 기능

 

이벤트리스너 함수

  addEvent() {
	...
    this.eraserEl.addEventListener("click", this.onClickEraser.bind(this));
  }

 

onClickEraser() - 지우개 툴을 클릭할 때 이벤트

  onClickEraser(event) {
    const IsActive = event.currentTarget.classList.contains("active");
    this.MODE = IsActive ? "NONE" : "ERASER";
    this.canvasEl.style.cursor = IsActive ? "default" : "crosshair";
    this.brushPanelEl.classList.add("hide");
    event.currentTarget.classList.toggle("active");
    this.brushEl.classList.remove("active");
  }
1. 브러쉬 탭과 동일하게 이루어져있음.
2. onMouseDown() 에서 조건문으로 모드가 지우개 모드일 때 , 선 색상과 선 굵기를 다 지정해놨음.

네비게이터 기능

 

이벤트리스너 함수

  addEvent() {
	...
    this.navigatorEl.addEventListener(
      "click",
      this.onClickNavigator.bind(this)
    );
  }

 

onClickNavigator() - 네비게이터 툴을 클릭했을 때 이벤트

  onClickNavigator(event) {
    this.IsNavigatorVisible = !event.currentTarget.classList.contains("active");
    event.currentTarget.classList.toggle("active");
    this.navigatorImageContainerEl.classList.toggle("hide");
    // console.log(this.canvasEl.toDataURL())
    this.updateNavigator();
  }
1. 네비게이터의 활성화를 알려주기 위해서 네비게이터 툴에 "active" 클래스의 유무로 값을 넣어준다.
2. 네비게이터를 눌렀을 때 네비게이터를 보여주도록 한다. ( "hide" 클래스 )
3. 네비게이터를 눌렀을 때 네비게이터의 내용을 업데이트 해주도록 함. (updateNavigator())

 

updateNavigator() - 네비게이터의 내용을 업데이트 해주는 함수

  updateNavigator() {
    if (!this.IsNavigatorVisible) return;
    this.navigatorImageEl.src = this.canvasEl.toDataURL();
  }
1. 네비게이터가 활성화되지않은 상태라면 실행하지 않는다. ( 비효율적이기때문에 )
2. navigator 안에는 image 태그가 있는데 , 그 이미지 태그로 그린 내용을 보여줄 것임.
-> 캔버스 태그에는 이미지의 url 을 가져올 수 있음 ( canvas.toDataURL() )
toDataURL( type, encoderOptions )
- 매개변수로 지정된 형식의 이미지를 데이터 url 로 리턴해줌
- 원하는 파일 형식과 이미지 품질을 매개변수로 지정할 수 있음
- type : 이미지 형식을 나타내는 "문자열", 기본값은 "image/png"
- encoderOptions : 이미지 품질을 나타내는 "Number" , < 0, 1 > 이 있음.

console.log(this.canvasEl.toDataURL())

 

다른 이벤트에서도 네비게이터 update 해주기 ( 마우스 뗐을 때 )

- onMouseUp ()

  onMouseUp() {
    if (this.MODE === "NONE") return;
    this.IsMouseDown = false;
    this.updateNavigator();
  }

- onMouseOut()

  onMouseOut() {
    if (this.MODE === "NONE") return;
    this.IsMouseDown = false;
    this.updateNavigator();
  }

되돌리기 기능

 

이벤트리스너 함수

  addEvent() {
	...
    this.undoEl.addEventListener("click", this.onClickUndo.bind(this));
  }

 

saveState() - 그린 그림의 데이터 URL 을 undoArray (배열) 에 저장하는 함수

  saveState() {
    if (this.undoArray.length > 4) {
      this.undoArray.shift();
      this.undoArray.push(this.canvasEl.toDataURL());
    } else {
      this.undoArray.push(this.canvasEl.toDataURL());
    }
  }
1. 배열의 데이터가 5개보다 많을 경우에는, 첫번째 데이터를 뺀다
2. 캔버스의 데이터 URL 을 넣어준다.
3. 배열의 데이터가 5개보다 많지 않을 경우에도, 캔버스의 데이터 URL 을 넣어준다.

 

특정 이벤트에서 데이터 URL을 저장해주기

- onMouseDown() 마우스를 눌렀을 때

  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";
    if (this.MODE === "BRUSH") {
      this.context.strokeStyle = this.colorPickerEl.value;
      this.context.lineWidth = this.brushSliderEl.value;
    } else if (this.MODE === "ERASER") {
      this.context.strokeStyle = this.eraserColor;
      this.context.lineWidth = 50;
    }
    this.saveState();
  }

 

onClickUndo () - 되돌리기 툴을 클릭했을 때 발생하는 이벤트

onClickUndo() {
    if (this.undoArray.length === 0) {
      alert("더이상 실행취소 불가합니다");
      return;
    }
    let previousDataUrl = this.undoArray.pop();
    let previousImage = new Image();
    previousImage.onload = () => {
      this.context.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
      this.context.drawImage(
        previousImage,
        0,
        0,
        this.canvasEl.width,
        this.canvasEl.height,
        0,
        0,
        this.canvasEl.width,
        this.canvasEl.height
      );
    };
    previousImage.src = previousDataUrl;
  }
1.  undoArray 에 데이터가 없을 시에는 alert 메서드를 사용해, 사용자에게 실행 취소 안내해주고 함수 실행하지 않기
2. pop() 메서드를 사용해서, undoArray 의 마지막 데이터를 제거하고 그 요소를 previousDataUrl 변수에 저장해주기
3. previousImage 에 image 객체를 생성해준다.
4. 이미지가 성공적으로 로딩이 되었을 때, 캔버스에 이미지를 그려주는 메서드를 활용해서, 받아온 데이터 URL을 캔버스에 그려줍니다.
5. 만든 image 객체의 속성에는 undoArray 의 마지막 데이터 URL 을 대입해준다.

 

pop ()
- 배열 함수로써, 배열에서 마지막 요소를 제거하고 그 요소를 반환함
new Image()
- img 객체 생성 및 동적으로 이미지 생성
- img 객체에는 HTML 문서내에 있는 이미지에 관한 정보를 담고 있다.
<img src="이미지파일" name="이름" width="가로너비" height="세로높이" alt="그림설명">​
clearRect(x, y, width, height)
- 사각형 영역의 픽셀을 투명한 검색으로 설정해서 지움. => 그냥 다 지워준다고 생각하면 편함
- 캔버스의 width 와, 캔버스의 height 만큼의 사각형의 영역을 지워준다고 생각
drawImage()
- 캔버스에 이미지를 그리는 다양한 방법을 제공하는 메서드
drawImage(image, dx, dy)
drawImage(image, dx, dy, dWidth, dHeight)
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

- 매개변수에 대해서는 너무나도 많아서 밑에 주소를 참고
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage


초기화 ( 휴지통 ) 기능

 

이벤트리스너 함수

  addEvent() {
	...
    this.clearEl.addEventListener("click", this.onClickClear.bind(this));
  }

 

onClickClear () - 초기화 툴을 클릭했을 때 발생하는 이벤트

  onClickClear() {
    this.context.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height);
    this.undoArray = [];
    this.updateNavigator();
    this.initCanvasBackgroundColor();
  }
1. clearRect() 메서드를 사용해, 캔버스에 있는 모든 것을 지워줌
2. undoArray 에 있는 배열 데이터들또한 모두 삭제해줌
3. 네비게이터의 내용 또한 업데이트 해준다.
4. 다시 캔버스의 배경을 흰색 (#fff) 컬러로 채워준다.

다운로드 기능

 

이벤트리스너 함수

  addEvent() {
	...
    this.downloadLinkEl.addEventListener(
      "click",
      this.onClickDownload.bind(this)
    );
  }

 

onClickDownload () - 다운로드 툴을 클릭했을 때 발생하는 이벤트

  onClickDownload() {
    this.downloadLinkEl.href = this.canvasEl.toDataURL("image/jpeg", 1);
    this.downloadLinkEl.download = "example.jpeg";
  }
* downloadLinkEl = a 태그 <a></a>
- a 태그에는 다운로드 속성이 있음.
- href의 경로에 있는 파일을 다운로드 받을 수 있음.
- 다운로드 속성에 이름을 정해주면, 그 이름으로 다운로드 받을 수 있도록 해줌

전체 기능 구현 코드