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 알고리즘을 활용하여 보석 십자수 도안 변환기를 구현했습니다.