프로젝트 기록

[Dotting] paint bucket brush mode 구현하기

lerrybe 2023. 4. 30. 21:35

🥕 What is Dotting?

Dotting
Flexible Pixel Art Editor for React

Dotting은 '누구나 사용하기 편한 Pixel 아트'를 기조로 제작 중인 react용 라이브러리인데, 라이브러리인만큼 자유도가 높고 이를 기반으로 다양한 서비스를 만들 수도 있습니다.

 

(아래는 활용 예시,

Primer 주최 생성AI 해커톤 나갔던 'Dotting gen-ai'이다. 오른쪽 bar에서 키워드를 통해 픽셀 이미지를 불러와 edit할 수도 있고, 기존의 이미지를 import해 픽셀화하여 마찬가지로 edit할 수 있습니다.)

 

https://dotting-genai.vercel.app/

 

Dotting gen-ai

 

dotting-genai.vercel.app

 

 

더 자세한 내용은 레포지토리document, 혹은 레포의 issue를 살펴보며 다음 Todo를 구경해도 좋을 것 같습니다.

[Repo] https://github.com/hunkim98/dotting

[Docs] https://hunkim98.github.io/dotting/?path=/story/introduction--page

 

 

 

이번 포스팅은 Dotting에 간단하게 기여했던 첫 번째 PR, Create paint bucket brush mode에 대한 글입니다. 🚀

 


 

🥕 Published Issue

#3 issue

 

Create paint bucket brush mode · Issue #3 · hunkim98/dotting

Currently there are only two brush modes: Dot, Eraser. In many art editors there is a paint bucket brush mode that allows neighboring colors to be colored at once. It might be great if dotting had ...

github.com

 

이미 dot 모드 (하나씩 찍을 수 있는 모드), eraser 모드는 구현되어 있었고, 여기서 인접한 같은 픽셀들을 모두 하나의 색으로 탐색하여 칠할 수 있는 Paint bucket 모드가 필요해보였습니다.

 


🥕 Implements

일단 화면을 그려주는 Canvas 클래스가 정의되어 있고, 이 클래스 내부에는 다양한 변수가 정의되어 있습니다.

 

대표적인 것 몇 개를 살펴보면, brushMode는 현재 선택된 브러시 모드를 저장하는 멤버 변수이고, brushColor는 현재 선택된 브러시 색상을 저장하는 멤버 변수입니다. brushColor에는 사용자가 색상을 선택하면 해당 변수에 선택한 색상이 저장됩니다. data는 캔버스 상의 모든 셀 데이터를 저장하는 Map 객체인데 이 객체는 Map<number, Map<number, PixelData>> 타입으로 정의되어 있으며, 각각의 Map<number, PixelData> 객체는 캔버스의 한 열(col)에 해당하는 픽셀 데이터를 저장하고 있습니다.

 

이런 내용을 배경으로 일단 한 픽셀을 중심으로 근처의 같은 색상을 찾는 방법을 생각했을 때 크게 DFS, BFS가 생각났습니다. 처음에 재귀를 생각했다가 픽셀 수가 굉장히 많아졌을 때 콜스택이 쌓일 우려가 있어 인접한 그리드들을 먼저 보는 BFS로 틀었고, 주위를 탐색하는 모양도 인접한 4개를 보는 식으로 뻗어나가는 게 예쁘겠다 싶어서 넓이 중심 탐색으로 구현했습니다. (탐색 성능상 크게 유의미한지는 더 고려해보려 합니다.)

 

아래는 코드 구현부입니다.

 

📚 함수 실행부

Paint bucket 모드가 선택되었을 때 행해야하는 로직들을 작성해줍니다. 탐색과 관련된 부분은 따로 함수를 뺐습니다.

  // ... 

  if (this.brushMode === BrushMode.PAINT_BUCKET) {
      /* 🪣 this paints same color area / with selected brush color. */

      const gridIndices = this.getGridIndices();
      if (!isValidIndicesRange(rowIndex, columnIndex, gridIndices)) {
        return;
      }

      const initialSelectedColor = this.data
        .get(rowIndex)
        ?.get(columnIndex)?.color;
      if (initialSelectedColor === this.brushColor) {
        return;
      }

      this.paintPixelsWithBucket(initialSelectedColor, gridIndices, {
        rowIndex,
        columnIndex,
      });
      this.emit(CanvasEvents.DATA_CHANGE, this.data);
      this.emit(CanvasEvents.STROKE_END, this.strokedPixels, this.data);
  }
    this.render();
  }

 

📚 함수 구현부

paintPixelsWithBucket(
    initialColor: string,
    gridIndices: Indices,
    currentIndices: { rowIndex: number; columnIndex: number }
  ): void {
    const indicesQueue = new Queue<{ rowIndex: number; columnIndex: number }>();
    indicesQueue.enqueue(currentIndices);

    while (indicesQueue.size() > 0) {
      const { rowIndex, columnIndex } = indicesQueue.dequeue()!;
      if (!isValidIndicesRange(rowIndex, columnIndex, gridIndices)) {
        continue;
      }

      const currentPixel = this.data.get(rowIndex)?.get(columnIndex);
      if (!currentPixel || currentPixel?.color !== initialColor) {
        continue;
      }

      this.data.get(rowIndex)!.set(columnIndex, { color: this.brushColor });
      this.strokedPixels.push({
        rowIndex,
        columnIndex,
        color: this.brushColor,
      });
      [
        { rowIndex: rowIndex - 1, columnIndex },
        { rowIndex: rowIndex + 1, columnIndex },
        { rowIndex, columnIndex: columnIndex - 1 },
        { rowIndex, columnIndex: columnIndex + 1 },
      ].forEach(({ rowIndex, columnIndex }) => {
        indicesQueue.enqueue({ rowIndex, columnIndex });
      });
    }
  }

 

이 때 JS Array로는 enque 과정을 상수시간에 할 수 없어서 queue 까지 구현한 후, PR을 날렸습니다.

 

기능 screenshot

 

 

 

 


🥕 PR Merged

 

Fisrt release!