30개의 프로젝트로 배우는 프론트엔드 with VanillaJS (7-2) 그림판

productuidev·2022년 10월 12일
0

FE Study

목록 보기
62/67
post-thumbnail

[fastcampus] 30개의 프로젝트로 배우는 프론트엔드 with VanillaJS (7-2)

(7) 그림판 구현

class와 id를 따로 준 이유 (분리)

  • id는 이벤트 적용 시 활용되고, class는 스타일 적용 시 활용
  • 스타일을 적용하면서 수정/변경이 잦음
  • 협업 시 마크업 개발자가 별도로 있을 경우 class가 변경될 수 있음
  • 수정된 class에 이벤트 바인딩이 될 경우, 이벤트가 적용되지 않거나 동작하지 않는 사이드 이펙트 발생 가능성이 있으므로 강의에서는 class는 스타일 적용에만 활용
  • 상황이나 협업을 고려한 방식 (방식에 정답은 없음)

04) 선 그리기 기능

캔버스에 그릴 때 path는 점들의 집합이며 서로 연결되면서 도형을 만들 수 있다.

  • 브러시 모드 : NONE BRUSH ERASER
  • 마우스를 눌렀는지 여부 : True/False (default는 false)

04-1) 요소 탐색

컨테이너 내의 캔버스, 툴바, 브러시, 컬러피커, 브러시패널, 브러시사이즈, 브러시팁 미리보기

04-2) 초기상태

context 파라미터로 2d를 줘서 2d 캔버스 구현

참고자료

04-3) 이벤트

  • 브러시를 클릭했을 때
  • 캔버스에서 마우스를 눌렀을 때
  • 캔버스에서 마우스를 움직일 때
  • 캔버스에서 마우스를 뗐을 때
  • 캔버스에서 마우스가 떠날 때
  • 브러시 슬라이더에서 브러시 사이즈가 변경될 때
  • 컬러피커에서 색상 변경될 때

04-4) 실행

DrawingBoard 인스턴스 생성

04-5) Code

src/index.js

class DrawingBoard {
  MODE = 'NONE'; // 브러시 모드 : NONE BRUSH ERASER
  IsMouseDown = false; // T/F

  constructor() {
    this.assignElement(); // 요소 탐색
    this.initContext(); // 초기상태
    this.addEvent(); // 이벤트
  }

  assignElement() {
    this.containerEl = document.getElementById('container');
    this.canvasEl = this.containerEl.querySelector('#canvas');
    this.toolbarEl = this.containerEl.querySelector('#toolbar');
    this.brushEl = this.containerEl.querySelector('#brush');
    this.colorPickerEl = this.containerEl.querySelector('#colorPicker');
    this.brushPanelEl = this.containerEl.querySelector('#brushPanel');
    this.brushSliderEl = this.brushPanelEl.querySelector('#brushSize');
    this.brushSizePreviewEl =
      this.brushPanelEl.querySelector('#brushSizePreview');
  }

  // 2D 캔버스 구현
  initContext() {
    this.context = this.canvasEl.getContext('2d');
  }

  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));
    this.brushSliderEl.addEventListener(
      'input',
      this.onChangeBrushSize.bind(this),
    );
    this.colorPickerEl.addEventListener('input', this.onChangeColor.bind(this));
  }

  onMouseOut() {
    if (this.MODE === 'NONE') return; // 브러시 모드가 NONE이면 진입 불가 (반환)
    this.IsMouseDown = false;
  }

  // 브러시 패널에 선택한 컬러피커의 색상 적용
  onChangeColor(event) {
    this.brushSizePreviewEl.style.backgroundColor = event.target.value;
  }

  // 브러시 패널에서 슬라이더로 브러시 사이즈 조정
  onChangeBrushSize(event) {
    this.brushSizePreviewEl.style.width = `${event.target.value}px`;
    this.brushSizePreviewEl.style.height = `${event.target.value}px`;
  }

  // 마우스를 누를 때
  onMouseDown(event) {
    if (this.MODE === 'NONE') return; // 브러시 모드가 NONE이면 진입 불가 (반환)
    this.IsMouseDown = true;
    const currentPosition = this.getMousePosition(event);

    // 2D 캔버스 그리기
    this.context.beginPath(); // 경로 시작
    this.context.moveTo(currentPosition.x, currentPosition.y); // 현재 좌표로 이동
    this.context.lineCap = 'round'; // 펜팁

    // this.context.strokeStyle = '#000000'; // 선 색상
    // this.context.lineWidth = 10; // 두께
    // this.context.lineTo(400, 400); // 캔버스 기준 x:400, y:400
    // this.context.stroke(); // 그리기

    this.context.strokeStyle = this.colorPickerEl.value; // 컬러피커의 값
    this.context.lineWidth = this.brushSliderEl.value; // 브러시슬라이더의 값
  }

  // 마우스를 움직일 때
  onMouseMove(event) {
    if (!this.IsMouseDown) return; // 마우스를 누른 게 아니면 진입 불가 (반환)

    const currentPosition = this.getMousePosition(event);
    this.context.lineTo(currentPosition.x, currentPosition.y); // 현재 좌표로 이동
    this.context.stroke(); // 그리기
  }

  // 마우스를 뗐을 때 = 마우스를 누른 게 아닌 상태
  onMouseUp() {
    if (this.MODE === 'NONE') return; // 브러시 모드가 NONE이면 진입 불가 (반환)
    this.IsMouseDown = false;
  }

  // 마우스 좌표
  getMousePosition(event) {
    const boundaries = this.canvasEl.getBoundingClientRect(); // 좌표 값 구하기
    return {
      x: event.clientX - boundaries.left, // 현재 캔버스기준 가로 시작점 부터 엘리먼트 왼쪽변 까지의 거리
      y: event.clientY - boundaries.top, // 현재 캔버스기준 세로 시작점 부터 엘리먼트 윗변 까지의 거리
    };
  }

  // 브러시를 클릭했을 때 이벤트 핸들러
  // 브러시를 눌렀을 때 상태 변경
  // (공통) 툴 클릭 시 active 클래스 추가 (활성화 상태)
  onClickBrush(event) {
    // this.MODE = 'BRUSH';
    const IsActive = event.currentTarget.classList.contains('active'); // 반복 코드 정리
    this.MODE = IsActive ? 'NONE' : 'BRUSH';
    this.canvasEl.style.cursor = IsActive ? 'default' : 'crosshair';

    this.brushPanelEl.classList.toggle('hide'); // 브러시 패널 활성화

    this.brushEl.classList.toggle('active');
  }
}

// 인스턴스 생성
new DrawingBoard();

npm run build

getBoundingClientRec 참고자료

CanvasRenderingContext2D 참고자료

<canvas> 요소의 드로잉 표면에 2D 렌더링 컨텍스트를 제공한다. 도형, 텍스트, 이미지 및 기타 객체를 그리는데 사용된다.

참고자료

더 알아보기

SVG : 미려하고 복잡한 벡터 이미지
Canvas : 빠른 처리속도를 바탕으로 한 비트맵으로 지도표기와 같은 실시간 데이터 표현에 특화

중간결과

😁😁😁


💬 선 그리기만 48분 정도 됐는데 중간에 끊어서 정리하면 코드 정리가 안 될 거 같아서 그냥 전체로 정리...

💬 시작 시 class와 id 분리 이유를 보다보니 재사용하는 경우 저 방식을 많이 썼던 것 같다. 공통으로 지정된 것만 사용하도록 설계되어 있어서 결국에는 큰 틀은 안 바뀌고 세부사항 적용을 위해서 class만 추가되는 식으로... (id 사용을 지양하는 곳에서는 class로 이벤트 바인딩을 하고 있어서 class도 바꾸면 안된다. 이로 인해 케이스 스타일도 엄격하게 적용하기도 한다) 그러다보면 문득 HTML은 사계절에 따라 나뭇잎이 달라지는 나무 같고, 리액트 컴포넌트는 레고조각으로 쌓은 성같다 뭐 그런 생각... 잡생각..

💬 선그리기 기능 구현 하다가 문득 찾아본...

생각해보니 난 깊이 있게 안 했던 거 같다.. 쫓기듯 열심히는 하긴 했는데 돌이켜보면 기초가 빈약했던 거 같기도 😑🤔

💬 퍼블리싱을 하든 프론트엔드 개발자을 하든 클라이언트단이기 때문에 디자인과는 떼려야 뗄 수 없는 관계인데.. 이 강의 패키지를 들을 때 디자인 가이드나 디자인에 연관된 내용이 나오는 게 처음엔 솔직히 싫었다. 방향성이 달라져서라고 생각했는데... 생각해보면 내 과거의 경험들은 사실 무관한 게 아니라는 걸 깨닫게 되었다. 하지만 결국엔 컴퓨터 그래픽스를 브라우저에서 다루는 건데 비트맵인지 벡터인지 조차 구분 못하면 안되는 거니까.. 정말 점들이 연결되서 선이 되는 것처럼 conneticg the dots...

+ 근황 Talk : 생각보다 데이터는 우리 생활 가까이에 있다를 느끼는 요즘(?)...

💬 데이터 관련된 서비스를 하고 있으므로 회사에서 DB 관련 스터디를 하게 될 예정이다.. 신청자를 보니 서비스기획자/운영자, 디자이너 분들도 신청하신 거 같아서 나도:) 다른 회사 근무할 때 마케터 분들이 SQL을 다루시거나 애널리틱스, UX 설계 하는 분들이 Hotjar나 뷰저블을 사용하시기도 했었는데.. 간단한 수업이지만 가볍게 알아보면 좋을 거 같다. (난 DB에 대해서는 잘 모르니..) 그래도 명색이 마이데이터 서비스니까 (하핫) 수업 전에 간단히 구글링해 조금 알아보는 중... (옆팀에는 데이터분석하는 분들이 계시는데 뭔가 고심해서 모니터를 보는 것 같았는데.. 수업을 듣고나면 대강 어떤 것에 대해 고심하고 계시는구나 하고 흐름을 알 수 있을거 같다) 개발팀 생활을 해보니 통밥으로(?) 어느 정도 흐름이나 상황을 익히고 있다. 👀👂 (그래도 모르는 건 많아서 기억나는대로 서칭중...)

💬 생각보다 꽤 많은 업계에서 마이데이터 사업을 벌이고 있는 것 같다. 회사에서 마이데이터+IT 관련하여 온라인강의도 수강했는데(가명정보? 비식별처리?) 생각보다 알아두면 좋은 내용도 몇 개 있었어서 여력이 되면 나중에 velog에 요약해서 정리할 생각..

💬 요즘은 바쁘게 지내서 사실 좋다. 시간이 부족하다보니 중요한 일에 집중하느라 정신이 없다. 잘 안보던 캘린더 앱도 잘 저장해서 쓰고 있다. (가끔 예약도 놓치고, 어쩔땐 시간이 없어 점심을 포기하고 미뤄둔 개인 일을 하기도 한다) 최근까지 여러 일을 겪으면서 나에게 필요한 게 뭔지 의사표현을 분명히 하고, 관심있는 것에 시간을 쏟는 것이 얼마나 중요한 일이 깨닫는 중... (가족들과의 시간도 부족하고, 집에 와서 생각 정리할 시간도 부족하고.. 그러다보면 금새 시간이 흘러간다) 이러면 사실 주변 사람들한테 미안한 이야기지만 조금 관심이 없어진다.. 그래서인지 뭔가 더 사무적인 내가 된 거 같다. 감정적일 때도 있지만 에너지를 길게 쏟지 않으려고 하는 중이다. 할애할 여력이 없다는 표현이 더 정확한 거 같다. 이해심이나 포용력이 누구나 있지 않고 곡해하거나 오해하는 사람까지 생각해 줄 필요는 없는 거 같다. (소수라도 잘 관찰하거나 지켜보고 그럴 만한 사람인지 판단한 후 의견을 전달해야겠다는 걸 깨달은 요즘...)

profile
필요한 내용을 공부하고 저장합니다.

0개의 댓글