
canvas api를 사용하면 자바스크립트와 html <canvas>를 사용해 그래픽을 그릴 수 있다. 특히 주로 2D 그래픽 요소를 그리기 위해 사용된다. 간단한 도형 뿐 아니라 이미지를 추가하거나 편집하는 용도로도 많이 사용되며, jpg, png, base64 등 다양한 형식으로 출력이 가능하다.
비슷하게 그래픽을 다루는 api 중에선 3D를 주로 다루는 WebGL이 있다.
WebGL(Web Graphics Library)은 플러그인을 사용하지 않고 웹 브라우저에서 상호작용 가능한 3D와 2D 그래픽을 표현하기 위한 JavaScript API다. WebGL은 HTML5 canvas 요소에서 사용할 수 있는, OpenGL ES 2.0을 대부분 충족하는 API를 제공한다.
HTML
ctx.beginPath();
ctx.arc(x좌표, y좌표, 반지름, 0(시작 각도), 2 * Math.PI(종료 각도));
ctx.fill();
이제 실제로는 어떤 일이 일어날까?
두 번째 원을 그릴 때? 역시 마찬가지로 위와 같은 작업을 동일하게 반복한다.
천 번 만 번 반복을 하더라도 위와 같은 작업은 동일하게 필요한다.
WebGL
const m4 = twgl.m4;
const gl = document.querySelector("canvas").getContext("webgl");
const vs = ` attribute vec4 position; uniform mat4 u_matrix; void main() { gl_Position = u_matrix * position; } `;
const fs = ` precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; } `;
const program = twgl.createProgram(gl, [vs, fs]);
const positionLoc = gl.getAttribLocation(program, "position");
const colorLoc = gl.getUniformLocation(program, "u_color");
const matrixLoc = gl.getUniformLocation(program, "u_matrix");
const positions = [];
const radius = 50;
const numEdgePoints = 64;
for (let i = 0; i < numEdgePoints; ++i) {
const angle0 = (i) * Math.PI * 2 / numEdgePoints;
const angle1 = (i + 1) * Math.PI * 2 / numEdgePoints;
positions.push(0, 0, Math.cos(angle0) * radius, Math.sin(angle0) * radius, Math.cos(angle1) * radius, Math.sin(angle1) * radius);
}
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
gl.useProgram(program);
const projection = m4.ortho(0, gl.canvas.width, 0, gl.canvas.height, -1, 1);
function drawCircle(x, y, color) {
const mat = m4.translate(projection, [x, y, 0]);
gl.uniform4fv(colorLoc, color);
gl.uniformMatrix4fv(matrixLoc, false, mat);
gl.drawArrays(gl.TRIANGLES, 0, numEdgePoints * 3);
}
drawCircle(50, 75, [1, 0, 0, 1]);
drawCircle(150, 75, [0, 1, 0, 1]);
drawCircle(250, 75, [0, 0, 1, 1]);
WebGL에서는 일종의 인스턴스화를 통해 이미 원을 그려놓거나, 원을 그리는데 필요한 반복 작업들(원의 크기를 계산하고 그 안을 채우고 버퍼 영역을 확보하는 등의 일)을 캐싱할 수 있다.
그 이유는 Canvas API에서 도형을 모양을 정하고 윤곽을 잡는데 사용되는 호출 함수와, 실제로 그 내부를 채우는 함수가 다르기 때문.
만약 arc 를 호출해서 호를 그렸다고 해도, 그 다음에 사용자가 다음 포인트로 이동을 할 지 moveTo , 다른 포인트로 이동하면서 해당 면적을 채울지 lineTo , 그 안을 채울지 fill, 외곽선을 그릴지 stroke 알 수 없기 때문이다.
뭐가 더 좋은 것일까?
요점은 WebGL이 Canvas API가 스킵할 수 없는 일부 단계를 스킵하거나, 재사용이 가능하게끔 더 낮은 레벨에서 제어할 수 있다는 점이다.
그러나 위에서 본 바와 같이, Canvas API는 원을 그리는 데 3줄이 필요한 반면 WebGL은 원을 그리는데 60줄의 코드가 필요하다.
따라서 WebGL / Canvas API는 편의성과 성능의 tradeoff가 있고, 상황에 맞게 사용을 하면 된다고 보면 된다.
Canvas API는 Canvas Element가 있어야 하기 때문에, Canvas Element를 하나 넣어준 html 파일을 만들어준다.
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>GAME</title>
</head>
<body>
<canvas></canvas>
</body>
</html>
💡 높이와 너비를 설정하지 않은 canvas의 크기는 기본적으로 w:300px h:150px
캔버스 위에 무엇인가 그리기 위해서는 다음과 같이 드로잉 컨텍스트를 가져와야 한다.
HTMLCanvasElement.getContext() - Web API | MDN
const contextType = "2d"
// "2d" | "webgl" | "webgl2" | "bitmaprenderer"
const canvas = document.querySelector('canvas')
const ctx = canvas.getContext(contextType);
드로잉 컨텍스트 중, 우리는 2차원 렌더링 컨텍스트를 나타내는 CanvasRenderingContext2D 객체를 사용한다.
드로잉 컨텍스트의 다양한 메소드를 통해 캔버스에 도형을 그릴 수 있다.
자주 사용되는 메소드
beginPath 새 경로를 생성closePath 경로 닫기. 마지막 경로에 있는 점과 경로의 시작점을 연결한다.stroke 경로의 윤곽선에 선을 그린다.fill 경로의 내부를 채운다moveTo 아무것도 그리지 않고 펜(현재 위치)의 좌표를 옮긴다.lineTo 현재 위치에서 특정 위치까지 직선을 그린다.arc 호/원을 그린다.rect 직사각형을 그린다. 원과 정사각형 그리기
ctx.rect(5, 5, 40, 40) // x, y, w, h
ctx.arc(100, 15, 20, 0, 2*Math.PI) // x, y, r, 시작 각, 끝 각
ctx.stroke();

📝 흐린 문제 발생
흐린 문제는 레티나 디스플레이와 같은 고해상도 디스플레이에서의 추가 픽셀이 필요해서 나타나는 현상으로, window 객체에 있는 devicePixelRatio를 통해 교정할 수 있다.


const scale = window.devicePixelRatio;
ctx.scale(scale, scale);
canvas.width = 300 * scale;
canvas.height = 150 * scale;
canvas.style.width = 300 + "px";
canvas.style.height = 150 + "px";
📝 선이 연결된 문제
해당 코드가 그림을 그리는 과정이라고 생각하면 붓을 떼지 않고 원을 그리러 간 셈이기 때문에 발생한 문제다. moveTo를 통해 붓의 위치를 원으로 옮기자
ctx.rect(5, 5, 40, 40);
ctx.moveTo(100,25); // 붓의 위치를 옮긴다.
ctx.arc(100, 25, 20, 0, 2*Math.PI);

++ 여기서 반지름은 x좌표에 원의 반지름을 더해 제거할 수 있지만
ctx.rect(5, 5, 40, 40);
ctx.stroke();
ctx.beginPath();
ctx.arc(100, 25, 20, 0, 2*Math.PI);
ctx.stroke();

✨beginPath를 사용해서 새 경롤르 만드는 방법은 각 경로의 스타일을 경리시켜 관리할 때 유용하게 사용된다.