개인 개발 프로젝트/그림판 앱

[그림판 앱] 4. app.js

종범2 2020. 4. 5. 22:35

프로젝트의 모든 javascript는 app.js에 작성하였다.

 

app.js

// Constant
const INITIAL_COLOR = '#2c2c2c'
const INITIAL_BG_COLOR = 'white';
const INITIAL_LINE_WIDTH = 2.5;
const BTN_CLICKED_CN = 'controls__color__clicked';
// Dom Element
const canvasParent = document.querySelector('#canvas');
const canvas = document.querySelector("#jsCanvas");
const ctx = canvas.getContext('2d');
const colors = document.querySelectorAll('.jsColor')
const myColorContorls = document.querySelector('#jsMyColorControl');
const myColor = document.querySelector('#jsMyColor')
const range = document.querySelector('#jsRange');
const mode = document.querySelector('#jsMode');
const saveBtn = document.querySelector('#jsSave');
const resetBtn = document.querySelector('#jsReset');
const resizeBtn = document.querySelector('#jsResize');
const widthControls = document.querySelector('#jsWidth');
const heightControls = document.querySelector('#jsHeight');
// Variable
let canvasWidth = 600;
let canvasHeight = 500;
let isPainting = false;
let isFilling = false;
// Init setting
const initSetting = () => {
  // Set width, height of canvas
  canvas.width = canvasWidth;
  canvas.height = canvasHeight;
  // Set background color, paint color, fill color, line width of canvas
  ctx.fillStyle = INITIAL_BG_COLOR;
  ctx.fillRect(0, 0, canvasWidth, canvasHeight);
  ctx.strokeStyle = INITIAL_COLOR;
  ctx.fillStyle = INITIAL_COLOR;
  ctx.lineWidth = INITIAL_LINE_WIDTH;
  // Set initial line width
  range.value = INITIAL_LINE_WIDTH;
  // Set initial mode to paint
  isFilling = false;
  mode.innerText = 'fill';
  // Set all button unclicked
  colors.forEach(color=>{
    color.classList.remove(BTN_CLICKED_CN);
  })
  // Set black button clicked
  colors[0].classList.add(BTN_CLICKED_CN);
}
// Init event
const initEvent = () => {
  // Add event to Canvas
  if (canvas) {
    canvas.addEventListener("mousemove", onMouseMove);
    canvas.addEventListener("mouseleave", stopPainting);
    canvas.addEventListener("mouseup", stopPainting);
    canvas.addEventListener("mousedown", startPainting);
    canvas.addEventListener("click", handleCanvasClick);
    canvas.addEventListener("contextmenu", handleContextMenu)
  }
  // Add event to color
  colors.forEach(color => {
    color.addEventListener('click', handleColorClick);
  })
  // Add event to range
  if (range) {
    range.addEventListener("input", handleRangeChange);
  }
  // Add event to mode button
  if (mode) {
    mode.addEventListener("click", hanldeModeClick);
  }
  // Add event to save button
  if (saveBtn) {
    saveBtn.addEventListener("click", handleSaveClick);
  }
  // Add event to reset button
  if (resetBtn) {
    resetBtn.addEventListener("click", handleResetClick);
  }
  // Add event to my color
  if (myColorContorls) {
    myColorContorls.addEventListener("change", handleMyColorChange);
  }
  // Add event to resize button
  if (resizeBtn) {
    resizeBtn.addEventListener("click", handleResizeClick);
  }
}
// Set start paint
const startPainting = () => {
  isPainting = true;
}
// Set stop paint
const stopPainting = () => {
  isPainting = false;
}
// Event of move mouse on canvas
const onMouseMove = (e) => {
  if (isFilling)
    return;
  const x = e.offsetX;
  const y = e.offsetY;
  if (!isPainting) {
    ctx.beginPath();
    ctx.moveTo(x, y);
  } else {
    ctx.lineTo(x, y);
    ctx.stroke();
  }
}
// Event of click color
const handleColorClick = (e) => {
  // set color of paint or fill
  const color = e.target.style.backgroundColor;
  ctx.strokeStyle = color;
  ctx.fillStyle = color;
  // Set all button unclicked
  colors.forEach(color=>{
    color.classList.remove(BTN_CLICKED_CN);
  })
  // Set clicked button clicked
  e.target.classList.add(BTN_CLICKED_CN);
}
// Event of change line width
const handleRangeChange = (e) => {
  const size = e.target.value;
  ctx.lineWidth = size;
}
// Event of change mode
const hanldeModeClick = () => {
  if (isFilling === true) {
    // If current mode is paint
    isFilling = false;
    mode.innerText = 'fill';
  } else {
    // If current mode is fill
    isFilling = true;
    mode.innerText = 'paint';
  }
}
// Event of click canvas
const handleCanvasClick = () => {
  if (isFilling === true) {
    ctx.fillRect(0, 0, canvasWidth, canvasHeight);
  }
}
// Prevent event of right click
const handleContextMenu = (e) => {
  e.preventDefault();
}
// Event of Click save button
const handleSaveClick = () => {
  const image = canvas.toDataURL();
  const link = document.createElement('a');
  link.href = image;
  link.download = 'PaintJS[Export]';
  link.click();
}
// Event of Click reset button
const handleResetClick = () => {
  initSetting();
}
// Event of Change my color
const handleMyColorChange = (e) => {
  const color = e.target.value;
  ctx.strokeStyle = color;
  ctx.fillStyle = color;
  myColor.style.backgroundColor = color;
}
// Event of Change size of canvas
const handleResizeClick = (e) => {
  if (widthControls.value>window.innerWidth){
    alert('Too Large');
  }else{
    canvasWidth = widthControls.value;
    canvasHeight = heightControls.value;
    initSetting();
  }
}
// init
initSetting();
initEvent();

 

가장 중요한 기능은 canvas에 그림을 그리는 기능이다. 그 이외의 코드는 위젯에 이벤트를 바인딩하거나 style을 설정하는 코드이므로 설명을 생략하겠다. canvas 내에서 마우스가 눌러져 있는 상태에서 마우스가 이동하면 그 동선을 따라 canvas에 line을 그려야 한다. 이를 위해 canvas 태그를 선택하고 getContext 함수를 호출하여 line을 canvas에 그리는 기능을 가진 컨텍스트 객체를 불러온다.

 

우선 canvas 위에서 마우스를 클릭하면 그림을 그리기 시작해야 한다. 따라서 마우스를 클릭하면 startPainting라는 함수를 실행하여 isPainting이라는 변수를 true로 설정한다. 그리고 마우스가 움직이면 onMouseMove라는 함수를 실행한다. 마우스를 누르기 전에는 ctx.beginPath()와 ctx.moveTo(x, y) 함수를 호출한다. 두 함수를 연속으로 호출하면 x, y 위치에 그림을 그리기를 시작한다고 설정한다. 마우스를 누르기 전에는 항상 이 값이 변하여 새롭게 설정하게 된다. 마우스를 누른 상태에서 마우스가 움직이면 ctx.lineTo(xy), ctx.stroke() 함수를 호출한다. 두 함수를 연속으로 호출하면 이전에 ctx.moveTo(x, y)를 호출할 때의 x, y 값에서 ctx.lineTo(xy)를 호출할 때의 x, y값까지 canvas에 선으로 그린다. 마우스가 조금씩 움직일 때마다 이전의 위치에서 움직인 위치까지 선을 그리게 되고 이 선들이 모여서 그림을 구성하게 된다.

 

참고자료

https://www.w3schools.com/tags/canvas_lineto.asp

 

HTML canvas lineTo() Method

HTML canvas lineTo() Method ❮ HTML Canvas Reference Example Begin a path, move to position 0,0. Create a line to position 300,150: YourbrowserdoesnotsupporttheHTML5canvastag. JavaScript: var c = document.getElementById("myCanvas"); var ctx = c.getContext("

www.w3schools.com

 

그림 그리기가 끝나면 canvas에 그린 그림을 파일로 저장해야 한다. canvas에는 toDataURL() 함수가 있다. 이를 호출하면 다음과 같은 문자열이 반환된다.

 

"data:image/png;base64,iVBORw0KGgoAAAANSUhE..." (예시)

 

data:image/png;base64이후에 등장하는 문자열은 canvas에 그린 그림인 바이너리 데이터를 base64라는 형태의 문자열로 전환한 문자열이다. 반환된 문자열을 img 태그의 src 속성 값으로 두면 이미지를 보여줄 수도 있고 위의 handleSaveClick 함수처럼 a 태그의 href 속성으로 두고 a 태그를 클릭하여 이미지로 다운로드를 할 수도 있다.

 

참고자료

https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL

 

HTMLCanvasElement.toDataURL()

The HTMLCanvasElement.toDataURL() method returns a data URI containing a representation of the image in the format specified by the type parameter (defaults to PNG). The returned image is in a resolution of 96 dpi.

developer.mozilla.org

[전체 글]

[그림판 앱] 5. 마무리

[그림판 앱] 4. app.js

[그림판 앱] 3. reset.css, style.css

[그림판 앱] 2. index.html

[그림판 앱] 1. 프로젝트 소개