Canvas API란?

HTML5 Canvas API는 JavaScript로 2D 그래픽을 그리고 이미지를 픽셀 단위로 조작할 수 있는 브라우저 내장 API입니다. 별도의 라이브러리 없이도 이미지 편집, 필터 적용, 데이터 시각화 등 다양한 그래픽 작업이 가능합니다.

Canvas API가 유용한 경우

기본 설정

Canvas 요소 생성

// HTML에서 Canvas 사용
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// JavaScript로 동적 생성
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

이미지 로드

function loadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = src;
  });
}

async function processImage(src) {
  const img = await loadImage(src);

  canvas.width = img.width;
  canvas.height = img.height;
  ctx.drawImage(img, 0, 0);

  // 픽셀 데이터 접근
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const pixels = imageData.data; // Uint8ClampedArray [R,G,B,A, R,G,B,A, ...]
}

픽셀 데이터 구조

getImageData()가 반환하는 데이터는 1차원 배열로, 4바이트씩 하나의 픽셀을 나타냅니다.

// 픽셀 데이터 구조
// [R0, G0, B0, A0, R1, G1, B1, A1, R2, G2, B2, A2, ...]

function getPixel(imageData, x, y) {
  const index = (y * imageData.width + x) * 4;
  return {
    r: imageData.data[index],
    g: imageData.data[index + 1],
    b: imageData.data[index + 2],
    a: imageData.data[index + 3],
  };
}

function setPixel(imageData, x, y, r, g, b, a = 255) {
  const index = (y * imageData.width + x) * 4;
  imageData.data[index] = r;
  imageData.data[index + 1] = g;
  imageData.data[index + 2] = b;
  imageData.data[index + 3] = a;
}

이미지 필터 구현

1. 그레이스케일 변환

function grayscale(imageData) {
  const data = imageData.data;

  for (let i = 0; i < data.length; i += 4) {
    // 가중 평균법 (인간의 눈이 녹색에 더 민감)
    const gray = data[i] * 0.299 + data[i+1] * 0.587 + data[i+2] * 0.114;
    data[i] = gray;     // R
    data[i+1] = gray;   // G
    data[i+2] = gray;   // B
  }

  return imageData;
}

2. 밝기 조절

function brightness(imageData, value) {
  const data = imageData.data;

  for (let i = 0; i < data.length; i += 4) {
    data[i] = Math.min(255, Math.max(0, data[i] + value));
    data[i+1] = Math.min(255, Math.max(0, data[i+1] + value));
    data[i+2] = Math.min(255, Math.max(0, data[i+2] + value));
  }

  return imageData;
}

3. 색상 반전

function invert(imageData) {
  const data = imageData.data;

  for (let i = 0; i < data.length; i += 4) {
    data[i] = 255 - data[i];
    data[i+1] = 255 - data[i+1];
    data[i+2] = 255 - data[i+2];
  }

  return imageData;
}

4. 블러 효과 (Box Blur)

function boxBlur(imageData, radius) {
  const width = imageData.width;
  const height = imageData.height;
  const src = new Uint8ClampedArray(imageData.data);
  const dst = imageData.data;
  const size = (radius * 2 + 1) ** 2;

  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let r = 0, g = 0, b = 0;

      for (let dy = -radius; dy <= radius; dy++) {
        for (let dx = -radius; dx <= radius; dx++) {
          const nx = Math.min(width - 1, Math.max(0, x + dx));
          const ny = Math.min(height - 1, Math.max(0, y + dy));
          const idx = (ny * width + nx) * 4;

          r += src[idx];
          g += src[idx + 1];
          b += src[idx + 2];
        }
      }

      const idx = (y * width + x) * 4;
      dst[idx] = r / size;
      dst[idx + 1] = g / size;
      dst[idx + 2] = b / size;
    }
  }

  return imageData;
}

색상 공간 변환

RGB → LAB 변환

색상 비교나 매칭 알고리즘에서는 LAB 색상 공간이 인간의 색 인지와 더 가깝습니다.

function rgbToLab(r, g, b) {
  // Step 1: RGB → Linear RGB (감마 보정 제거)
  let rLin = r / 255;
  let gLin = g / 255;
  let bLin = b / 255;

  rLin = rLin > 0.04045
    ? Math.pow((rLin + 0.055) / 1.055, 2.4)
    : rLin / 12.92;
  gLin = gLin > 0.04045
    ? Math.pow((gLin + 0.055) / 1.055, 2.4)
    : gLin / 12.92;
  bLin = bLin > 0.04045
    ? Math.pow((bLin + 0.055) / 1.055, 2.4)
    : bLin / 12.92;

  // Step 2: Linear RGB → XYZ (D65 illuminant)
  const x = rLin * 0.4124564 + gLin * 0.3575761 + bLin * 0.1804375;
  const y = rLin * 0.2126729 + gLin * 0.7151522 + bLin * 0.0721750;
  const z = rLin * 0.0193339 + gLin * 0.1191920 + bLin * 0.9503041;

  // Step 3: XYZ → LAB
  const xRef = 0.95047, yRef = 1.00000, zRef = 1.08883;

  const f = (t) => t > 0.008856
    ? Math.pow(t, 1/3)
    : (7.787 * t) + 16/116;

  const L = 116 * f(y / yRef) - 16;
  const a = 500 * (f(x / xRef) - f(y / yRef));
  const bVal = 200 * (f(y / yRef) - f(z / zRef));

  return { L, a, b: bVal };
}

색상 거리 계산 (Delta E)

function deltaE(lab1, lab2) {
  const dL = lab1.L - lab2.L;
  const da = lab1.a - lab2.a;
  const db = lab1.b - lab2.b;

  return Math.sqrt(dL * dL + da * da + db * db);
}

// 사용 예시: 가장 유사한 색상 찾기
function findClosestColor(targetRgb, palette) {
  const targetLab = rgbToLab(targetRgb.r, targetRgb.g, targetRgb.b);
  let minDist = Infinity;
  let closest = null;

  for (const color of palette) {
    const colorLab = rgbToLab(color.r, color.g, color.b);
    const dist = deltaE(targetLab, colorLab);

    if (dist < minDist) {
      minDist = dist;
      closest = color;
    }
  }

  return closest;
}

이미지 다운로드

처리된 이미지를 파일로 저장하는 기능을 구현합니다.

function downloadCanvas(canvas, filename = 'output.png') {
  const link = document.createElement('a');
  link.download = filename;
  link.href = canvas.toDataURL('image/png');
  link.click();
}

// JPEG 품질 조절
function downloadAsJpeg(canvas, quality = 0.9) {
  const link = document.createElement('a');
  link.download = 'output.jpg';
  link.href = canvas.toDataURL('image/jpeg', quality);
  link.click();
}

성능 최적화

1. 대용량 이미지 처리

// 이미지 크기 제한
function resizeIfNeeded(img, maxSize = 2000) {
  const canvas = document.createElement('canvas');
  let { width, height } = img;

  if (width > maxSize || height > maxSize) {
    const ratio = Math.min(maxSize / width, maxSize / height);
    width *= ratio;
    height *= ratio;
  }

  canvas.width = width;
  canvas.height = height;
  canvas.getContext('2d').drawImage(img, 0, 0, width, height);

  return canvas;
}

2. Web Worker로 메인 스레드 차단 방지

// worker.js
self.onmessage = function(e) {
  const { imageData, filter } = e.data;
  const data = imageData.data;

  if (filter === 'grayscale') {
    for (let i = 0; i < data.length; i += 4) {
      const gray = data[i] * 0.299 + data[i+1] * 0.587 + data[i+2] * 0.114;
      data[i] = data[i+1] = data[i+2] = gray;
    }
  }

  self.postMessage({ imageData });
};

// main.js
const worker = new Worker('worker.js');
worker.postMessage({ imageData, filter: 'grayscale' });
worker.onmessage = (e) => {
  ctx.putImageData(e.data.imageData, 0, 0);
};

결론

Canvas API는 서버 없이 브라우저에서 강력한 이미지 처리를 가능하게 합니다. 픽셀 단위 접근, 색상 공간 변환, 필터 적용 등 대부분의 이미지 처리 작업을 클라이언트 사이드에서 수행할 수 있습니다.

실제로 Diastr 프로젝트에서 Canvas API와 Delta E 2000 알고리즘을 활용하여 보석 십자수 도안 변환기를 구현했습니다.