#구현

[html/css/js] 노이즈 효과 만들기

노새두마리 2024. 1. 23. 04:46

 우연한 계기로 canvas를 활용하여 그리드에 흰색 혹은 검은색을 칠하는 간단한 작업을 수행하였는데, 얼마 전 신기하게 봤던 노이즈 효과를 도전해 봐도 좋을 것 같습니다.

당장 코드가 급하신 분들은 이 글이 아니라 이쪽의 출처로

 

html5 canvas noise generation

...

codepen.io


로직

생각한 로직은 다음과 같습니다.

  1. canvas 생성
  2. 렌더링 함수 정의
    1. width * height 크기의 배열 생성 및 값 할당
    2. 배열의 값에 따라 픽셀 단위로 칠하기(fillRect 활용)
  3. 렌더링 함수 반복 호출

옵션은 다음과 같습니다.

  • window 크기 변경 시 canvas 크기도 조절되는 로직(resize)

구현

주요 로직

1. canvas 생성

  • html에 canvas 태그 추가
  • js로 canvas에 대한 참조 확보
  • 스크립트 최초 로드 시 사용자에게 보여지는 화면 크기와 일치하도록 canvas 크기 조정
const canvas = document.getElementById("noise");

let height = window.innerHeight;
let width = window.innerWidth;

if (canvas) {
  canvas.width = width;
  canvas.height = height;
}

 

2. 렌더링 함수 정의

canvas에 노이즈를 칠하는 drawNoise 함수를 정의합니다.

1. width * height 크기의 배열 생성 및 값 할당

1차원 배열로 생성하였습니다. 배열에 접근할 때에는 따로 인덱스를 구하는 함수를 활용합니다.

const getIdx = (row, col) => {
  return row * width + col;
};

const drawNoise = () => {
    const arr = new Array(width * height).fill().map(_ => Math.random() < 0.3);
    
    // ...
}

arr의 각 인덱스의 값은 생성된 난수가 0.3 미만인 경우 true, 0.3 이상인 경우 false로 할당됩니다.


2. 배열의 값에 따라 픽셀 단위로 칠하기

const getIdx = (row, col) => {
  return row * width + col;
};

// 색상 상수 추가
const BLACK = "#000";
const WHITE = "#FFF";

// canvas context
const ctx = canvas.getContext("2d");

const drawNoise = () => {
    const arr = new Array(width * height).fill().map(_ => Math.random() < 0.3);
    
    ctx.beginPath();

    for (let row = 0; row < height; row++) {
       for (let col = 0; col < width; col++) {
         const idx = getIdx(row, col);
         
         ctx.fillStyle = arr[idx] ? WHITE : BLACK;
         ctx.fillRect(col, row, 1, 1);
       }
    }
    ctx.stroke();
}

true인 경우 흰색, false인 경우 검은색으로 칠합니다.

 

3. 렌더링 함수 반복 호출

requestAnimationFrame을 활용하여 drawNoise 함수를 반복 호출합니다.

function renderLoop() {
  drawNoise();
  requestAnimationFrame(renderLoop);
}
requestAnimationFrame(renderLoop);

requestAnimationFrame 없이 재귀적으로 renderLoop를 호출하면 무한 루프에 빠지면서 프로그램이 멈춥니다. requestAnimationFrame은 이전 렌더링이 끝난 뒤에 다음 렌더링이 수행되도록 순서를 보장하고, 렌더링이 수행되는 동안  다른 스크립트의 실행을 멈추지 않습니다.


옵션

resize

let height, width; // 아까 선언한 변수 그대로 활용

function resize(){
  height = window.innerHeight;
  width = window.innerWidth;
  canvas.height = height;
  canvas.width = width;
}

window.addEventListener('resize', resize);

 

window 크기가 변경될 때 canvas 크기도 같이 조정됩니다.


결과물

 

1090 x 695 화면에서 Array를 사용하든 Uint8Array를 사용하든 초당 1프레임 정도 나오는 것 같습니다.

망했습니다. 이대로는 절대 사용할 수 없습니다.


최적화

지금 할 수 있는 행동은 크게 두 가지입니다.

  1. 칠하는 과정을 효율적으로 수행한다.
  2. 칠해야 하는 횟수를 줄인다.

1을 수행하려면 렌더링 자체를 빠르게 처리할 수 있는 방법이 필요하고,

2를 수행하려면 한 번에 칠해지는 픽셀 단위를 크게 만들어야 합니다. 현재 1 픽셀 단위로 칠하게 되어 있는데 이것을 n 픽셀 단위로 칠하면 반복되는 작업을 1 / n^2 로 줄일 수 있겠죠.

별 효과가 없던 부분들은 접어놓겠습니다.

2번 최적화

더보기

2번 최적화를 먼저 시도해 보죠. 현재 1 픽셀씩 칠할 때 한 번에 70만 번의 연산이 필요합니다. 2 픽셀로 늘려보겠습니다.

2 픽셀로 늘린 모습. 조금 빨라졌습니다.

 

5 픽셀로 늘린 모습. 빨라졌지만 느립니다. 여전히... 게다가 보기에도 좋지 않습니다.

 

흰색은 아예 칠하지 않고 넘어가도록 변경해 봅시다.

오, 미래가 보여요. 아무것도 보이지 않아요.

 

검은색으로 칠해졌던 부분이 다음 반복에도 그대로 유지되면서 결국 검은색으로 뒤덮이고 마는 모습입니다.

 

 

2번 최적화 방법인 픽셀 단위 늘리기로는 무리가 있다는 것을 알게 되었으니, 1번 방법으로 가 봅시다.

더보기

당장 떠오르는 방법은 Uint8Array의 하나의 정수에 8개의 상태를 담는 방법입니다. 0 이상 255 이하의 난수를 통해 8가지 픽셀의 값을 하나의 값으로 저장하였습니다. 

하지만 여전히 느립니다. 비트가 0인지 1인지로만 판별하면 무조건 50%를 기준으로 사용할 수밖에 없기도 하구요.

 

더욱 효율적인 방법을 찾기 위해 결국 가장 처음 제시했던 레퍼런스 코드를 참고하기로 합니다.

레퍼런스에 구현된 것이 비교조차 안될 정도로 빠른데, 차이는 fillRect가 아닌 createImageData, putImageData를 활용하는 것이었습니다.

둘의 가장 큰 차이는 색상의 표현으로 보입니다.

fillRect는 fillStyle을 문자열로 지정하므로, 칠하기 전에 주어진 문자열을 색상 값으로 파싱하는 과정을 거쳐야 합니다. 무려 영역을 칠하려는 횟수 만큼이나요.

반면, 이미지 데이터는 RGBA에 대응되는 값 자체를 저장하고 있으므로, 그대로 렌더링에 사용할 수 있습니다.

const drawNoise = () => {
  const image = ctx.createImageData(width, height);
  const arr = new Uint32Array(image.data.buffer);
  const len = arr.length;
  for (let i = 0; i < len; i++){
    if (Math.random() < 0.5){
      arr[i] = 0xFF000000;
    }
  }
  ctx.putImageData(image, 0, 0);
}

image.data.buffer에는 색상 정보가 8비트 단위로 들어가 있는데, 이것을 32비트 단위로 제어하면 한 픽셀에 대한 RGBA 값을 한번에 할당할 수 있습니다.

Uint32Array에 할당되는 16진수값은 하나씩 만져보니 앞에서부터 차례대로 투명도, Blue, Green, Red에 대응됩니다. 작은 자릿수의 비트부터 RGBA입니다.

레퍼런스에는 0xFFFFFFFF로 할당되어 있어서 흰색 노이즈가 들어가는데, RGB를 적절히 조절하면 다른 색깔의 노이즈를 입힐 수 있습니다.

10 프레임 제한이 걸려 있는 상태입니다. 실제로는 훨씬 빠릅니다.


후기

끝입니다. 이제 canvas 자체의 투명도 또는 색상에 포함된 투명도를 조절해서 사용할 수 있습니다.

웬만해서는 갖다 쓰는 게 제일이라는 것을 느낍니다...