mozilla

Drawing shapes

격자

그리기를 시작하기 전에 캔버스 격자 또는 좌표 공간 에 대해 얘기할 필요가 있겠습니다. 이전 페이지의 HTML 템플릿은 너비와 높이가 각각 150픽셀인 캔버스 엘리먼트가 들어 있었습니다. 이 이미지에 제가 기본 격자를 겹쳐 그려 봤습니다. 보통 격자의 한 단위는 캔버스의 1픽셀에 해당합니다. 이 격자의 원점은 왼쪽 위 꼭지점(좌표 (0,0))에 있습니다. 모든 요소들은 이 원점을 기준으로 상대적인 위치에 놓입니다. 따라서 파란 정사각형의 왼쪽 위 꼭지점은 왼쪽으로부터 x픽셀, 위로부터 y픽셀 떨어져 있게 됩니다. (좌표 (x,y)) 이 입문서의 뒷쪽에서는 어떻게 원점을 다른 위치로 이동하고, 격자를 회전하거나 심지어 확대 및 축소를 하는지 살펴봅니다. 지금은 기본 격자를 그대로 사용하겠습니다.

도형 그리기

SVG와는 달리, 캔버스는 단 하나의 기본 도형, 즉 (직)사각형만을 지원합니다. 모든 다른 도형들은 하나 이상의 패스(path)를 합쳐서 만들어져야 합니다. 다행히 패스를 그리는 다양한 함수들이 제공되므로 이를 사용하여 매우 복잡한 도형도 만들어낼 수 있습니다.

사각형

먼저 사각형을 살펴 봅시다. 캔버스에 사각형을 그리는 함수는 세 개가 있습니다:

fillRect(x,y,width,height) : 속이 찬 사각형을 그립니다.
strokeRect(x,y,width,height) : 사각형의 테두리를 그립니다.
clearRect(x,y,width,height) : 지정한 영역을 지워서 완전히 투명하게 만듭니다.

세 함수는 모두 같은 인자를 받습니다. xy는 캔버스에서 사각형의 왼쪽 위 꼭지점이 어디에 있는지 (원점에 상대적인) 좌표를 지정합니다. widthheight는 당연히 사각형의 너비와 높이를 가리킵니다. 역주: 영어를 모르는 독자도 있다고 가정합니다. 이 함수들의 동작을 직접 살펴 보겠습니다.

아래는 이전 페이지에 있던 draw() 함수에 위의 세 함수들을 덧붙인 것입니다.

사각형 예제

직접 보기

function draw(){
  var canvas = document.getElementById('tutorial');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    ctx.fillRect(25,25,100,100);
    ctx.clearRect(45,45,60,60);
    ctx.strokeRect(50,50,50,50);
  }
}

오른쪽에 있는 이미지와 비슷한 결과가 나와야 할 것입니다. fillRect 함수는 100x100 픽셀 크기의 커다란 검은 사각형을 그립니다. clearRect 함수는 중간에서 60x60 픽셀 크기의 사각형을 지워 내고, 마지막으로 strokeRect는 지워진 사각형 안에 50x50 크기의 사각형 테두리를 그립니다. 다음 페이지에서는 clearRect 함수 대신 쓸 수 있는 두 가지 방법을 설명하며, 그려진 도형의 색깔과 선 모양을 어떻게 바꾸는 지도 설명합니다.

다음 절에서 살펴볼 패스 함수와는 달리, 세 사각형 함수 모두 캔버스에 도형을 바로 그립니다.

패스 그리기

패스를 사용하여 도형을 만들려면, 몇 가지 과정이 더 필요합니다.

beginPath()
closePath()
stroke()
fill()

패스를 만드는 첫번째 단계는 먼저 beginPath 메소드를 호출하는 것입니다. 내부적으로 패스는 합쳐져서 도형을 이루는 부분 패스(직선, 호 등)의 목록으로 저장됩니다. 이 메소드가 호출될 때마다, 목록은 초기화되고 새로운 도형을 그릴 수 있게 됩니다.

두번째 단계는 그려질 패스를 실제로 지정하는 메소드를 호출하는 것입니다. 이들은 조금 후에 살펴보겠습니다.

세번째 단계는 closePath 메소드를 호출하는 것으로, 생략할 수 있습니다. 이 메소드는 현재 점에서 시작점으로 직선을 그려서 도형을 닫습니다. 만약 도형이 이미 닫혀 있거나 목록에 점이 하나 뿐이라면, 이 함수는 아무 일도 하지 않습니다.

마지막 단계는 stroke 메소드나 fill 메소드, 또는 둘 다를 호출하는 것입니다. 이들 중 하나를 호출하면 실제로 도형이 캔버스에 그려집니다. stroke는 도형의 테두리를 그리는 데 사용하며, fill은 도형의 안쪽을 채우는 데 사용합니다.

참고: fill 메소드를 호출하면 모든 열린 도형은 자동으로 닫히며 closePath 메소드를 사용할 필요가 없습니다.

간단한 도형(삼각형)을 그리는 코드는 다음과 같을 것입니다:

ctx.beginPath();
ctx.moveTo(75,50);
ctx.lineTo(100,75);
ctx.lineTo(100,25);
ctx.fill();

moveTo

매우 유용하며 실제로 아무 것도 그리진 않지만 위에서 말한 패스 목록의 일부인 함수가 하나 있는데, 바로 moveTo 함수입니다. 이 함수가 하는 일은 종이 한 편에서 펜이나 연필을 들어 올렸다가 다른 편에 내려 놓는 것으로 생각할 수 있습니다.

moveTo(x, y)

moveTo 함수는 인자 두 개, 즉 xy를 받는데, 이들은 새 시작점의 좌표입니다.

캔버스가 초기화되거나 beginPath 메소드가 호출되면, 시작점은 좌표 (0,0)으로 설정됩니다. 대부분의 경우 moveTo 메소드는 시작점을 어딘가 다른 곳으로 정할 때 쓰입니다. 또한 moveTo 메소드를 연결되지 않은 패스를 그릴 때 쓸 수도 있습니다. 오른쪽의 웃는 얼굴에서, 저는 moveTo 메소드를 쓴 부분을 (빨간 선으로) 표시해 뒀습니다.

여러분이 이걸 직접 해 보려면 다음 코드 조각을 쓰면 됩니다. 이를 이전에 봤던 draw 함수에 붙여 넣으십시오.

moveTo 예제

직접 보기

ctx.beginPath();
ctx.arc(75,75,50,0,Math.PI*2,true); // 바깥쪽 원
ctx.moveTo(110,75);
ctx.arc(75,75,35,0,Math.PI,false);  // 입 (시계 방향)
ctx.moveTo(65,65);
ctx.arc(60,65,5,0,Math.PI*2,true);  // 왼쪽 눈
ctx.moveTo(95,65);
ctx.arc(90,65,5,0,Math.PI*2,true);  // 오른쪽 눈
ctx.stroke();

참고: moveTo 메소드들을 빼면 연결된 선들을 볼 수 있습니다.
참고: arc 함수의 설명과 인자에 대해서는 아래를 보십시오.

선을 그리려면 lineTo 메소드를 사용합니다.

lineTo(x, y)

이 메소드는 인자 두 개, 즉 xy를 받는데, 이들은 선의 끝점의 좌표입니다. 시작점은 이전에 그려진 패스에 따라 결정되는데, 이전 패스의 끝점이 다음 패스의 시작점이 되고 하는 식입니다. 시작점은 moveTo 메소드로도 바꿀 수 있습니다.

lineTo 예제

아래 예제에는 두 개의 삼각형을 그리는데, 하나는 채워져 있고 하나는 테두리만 그립니다. (결과는 오른쪽의 이미지에서 볼 수 있습니다) 먼저 beginPath 메소드를 호출해서 새 도형 패스를 시작합니다. 그리고 moveTo 메소드로 시작점을 원하는 위치에 옮깁니다. 그 아래에서는 삼각형의 두 변을 이루는 선 두 개를 그립니다.

채워진 삼각형과 삼각형 테두리의 차이가 보일 것입니다. 이는 앞에서 말했듯이, 패스 안쪽이 채워지면 도형은 자동으로 닫히기 때문입니다. 만약 삼각형 테두리를 그릴 때도 채워진 삼각형과 똑같이 한다면 완전한 삼각형이 아닌 선 두 개만이 그려질 것입니다.

직접 보기

// 채워진 삼각형
ctx.beginPath();
ctx.moveTo(25,25);
ctx.lineTo(105,25);
ctx.lineTo(25,105);
ctx.fill();

// 삼각형 테두리
ctx.beginPath();
ctx.moveTo(125,125);
ctx.lineTo(125,45);
ctx.lineTo(45,125);
ctx.closePath();
ctx.stroke();

호나 원을 그릴 때는 arc 메소드를 사용합니다. 명세에는 arcTo 메소드도 들어 있으며, 이는 Safari는 지원하지만 현재의 Gecko 브라우저에는 아직 구현되어 있지 않습니다.

arc(x, y, radius, startAngle, endAngle, anticlockwise)

이 메소드는 다섯 개의 인자를 받습니다: xy는 원의 중심의 좌표입니다. radius는 원의 반지름을 나타냅니다. startAngleendAngle은 호의 시작점과 끝점을 라디안(호도법)으로 지정합니다. 시작 각도와 끝 각도는 x축을 기준으로 계산됩니다. anticlockwise 인자는 불 값으로, true일 때는 호를 시계 반대 방향으로 그리고, 아니면 시계 방향으로 그리게 됩니다.

주의: Firefox 베타 빌드에서 마지막 인자는 clockwise입니다. (역주: 즉 true일 때 시계 방향으로 그립니다. 번역하는 시점에서 최신 버전인 Firefox 2.0은 위에 설명된 대로 동작합니다.) 최종 릴리스는 위에서 설명된 그대로 이 함수를 지원할 것입니다. 이 메소드의 현재 형태를 사용하고 있는 모든 스크립트는 최종 버전이 발표된 뒤에 수정되어야 할 필요가 있습니다.

참고: arc 함수의 각도는 그냥 각도가 아닌 라디안(호도법)으로 지정되어야 합니다. 보통 각도를 라디안으로 바꾸려면 다음과 같은 JavaScript 식을 쓰면 됩니다: var radians = (Math.PI/180)*degrees

arc 예제

다음 예제는 지금까지 봐 왔던 것보다 좀 더 복잡합니다. 저는 서로 다른 각도와 채움 방법을 쓰는 12개의 호를 그렸습니다. 만약 제가 이 예제를 위의 웃는 얼굴과 같이 만들었다면, 첫번째로 매우 많은 문장들이 필요할 것이며 둘째로 호를 그릴 때 모든 시작점을 알 필요가 있습니다. 여기서 사용했듯이 90도, 180도, 270도가 시작점인 호라면 별 문제가 되지 않겠지만, 그렇지 않다면 너무 어려운 문제가 되어 버립니다.

두 개의 for 루프는 호들의 행과 열에 대해 반복됩니다. 각 호에 대해서 저는 beginPath로 새 패스를 시작합니다. 그 아래에는 의미를 파악하기 쉽도록 모든 인자들이 변수로 쓰여져 있습니다. 보통 이 부분은 실제로 한 문장으로 끝나겠죠. xy 좌표는 충분히 명확할 것입니다. radiusstartAngle은 고정되어 있습니다. endAngle은 180도(첫 열)로 시작해서 한 번에 90도씩 증가하여 완전한 원(마지막 열)이 됩니다. anticlockwise 인수에 해당하는 문장은 첫번째 줄과 세번째 줄에서 시계 방향으로, 그리고 둘째 줄과 네번째 줄에서는 시계 반대 방향으로 그리도록 합니다. 역주: 원문은 옛날 명세에 따라 거꾸로 되어 있습니다. 소스 코드는 정상입니다. 마지막으로 if 문은 윗쪽 반은 테두리만 그리도록 하고 나머지 반은 채워진 호를 그리도록 합니다.

직접 보기

for (i=0;i<4;i++){
  for(j=0;j<3;j++){
    ctx.beginPath();
    var x              = 25+j*50;               // x 좌표
    var y              = 25+i*50;               // y 좌표
    var radius         = 20;                    // 호의 반지름
    var startAngle     = 0;                     // 시작점
    var endAngle       = Math.PI+(Math.PI*j)/2; // 끝점
    var anticlockwise  = i%2==0 ? false : true; // 시계 반대 방향인가?

    ctx.arc(x,y,radius,startAngle,endAngle, anticlockwise);

    if (i>1){
      ctx.fill();
    } else {
      ctx.stroke();
    }
  }
}

Bezier and quadratic curves

다음 유형의 경로는 베지어 곡선(Bézier curves)으로 2차와 3차 곡선을 그릴 수 있습니다. 이들은 보통 복잡한 구조의 모양을 그리기 위해 사용됩니다.

quadraticCurveTo(cp1x, cp1y, x, y) // BROKEN in Firefox 1.5 (see work around below)
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)


둘의 차이는 오른쪽 그림에 잘 설명되어 있습니다. 2차 베지어 곡선은 시작과 끝 점(파란 점) 그리고 한 개의 조절점(control point) 만 있지만, 3차 베지어 곡선에서는 2개의 조절점을 사용합니다.

두 메소드 모두에 있는 x, y 인자는 끝 점의 좌표입니다. cp1x, cp1y는 첫 번째 조절점의 좌표이며, cp2x, cp2y는 두 번째 조절점의 좌표 입니다.

2차 또는 3차 베지어 곡선을 이용한다고 해서 완전히 새로운 것을 그릴 수 있는 것은 아닌데, 이는 베지어 곡선을 사용할 경우 벡터 드로잉 소프트웨어인 Adobe Illustrator과는 달리 자신이 하는 작업에 대한 즉각적인 시각 피드백을 얻을 수 없기 때문입니다. 이는 복잡한 모양을 그리기 어렵게 만들기도 합니다. 다음 예제에서 우리는 간단한 구조의 모양을 그려볼 것입니다. 하지만 대부분 그렇겠지만 여러분이 시간과 인내력이 있다면 보다 더 복잡한 모양도 그릴 수 있을 것입니다.

다음 예제들에서 특별히 어려움 점은 없습니다. 2가지 경우 모두가 완전한 모양을 가지는 연속된 곡선이 그려지는 것을 보게 될 것입니다.

quadraticCurveTo 예제

예제 보기

// 2차 베지어 곡선 예제
ctx.beginPath();
ctx.moveTo(75,25);
ctx.quadraticCurveTo(25,25,25,62.5);
ctx.quadraticCurveTo(25,100,50,100);
ctx.quadraticCurveTo(50,120,30,125);
ctx.quadraticCurveTo(60,120,65,100);
ctx.quadraticCurveTo(125,100,125,62.5);
ctx.quadraticCurveTo(125,25,75,25);
ctx.stroke();

2차 베지어 곡선의 단일 조절점을 3차 베지어 곡선의 2개 조절점으로 정확하게 계산할 수 있는 경우에는 2차 곡선을 3차 곡선으로 변환할 수 있습니다. 하지만 반대는 불가능 합니다. 3차 베지어 곡선의 3차 요소가 0인 경우에만 정확한 2차 베지어 곡선으로 변환할 수 있으며, 일반적으로는 여러개의 2차 베지어 곡선을 사용하여 3차 곡선을 추정하는 분할 기법이 사용됩니다.

bezierCurveTo 예제

예제 보기

// 3차 베지어 곡선 예제
ctx.beginPath();
ctx.moveTo(75,40);
ctx.bezierCurveTo(75,37,70,25,50,25);
ctx.bezierCurveTo(20,25,20,62.5,20,62.5);
ctx.bezierCurveTo(20,80,40,102,75,120);
ctx.bezierCurveTo(110,102,130,80,130,62.5);
ctx.bezierCurveTo(130,62.5,130,25,100,25);
ctx.bezierCurveTo(85,25,75,37,75,40);
ctx.fill();

Firefox 1.5의 quadraticCurveTo() 버그 해결책

Firefox 1.5에 구현된 quadraticCurveTo()에는 버그가 있습니다. 해당 함수는 2차 곡선을 그리지 않고 3차 곡선 함수인 bezierCurveTo()를 호출하며, 2차식에 사용되는 한 개 제어점을 두 번 반복해서 사용합니다. 이 때문에 quadraticCurveTo()는 잘못된 결과를 출력합니다. quadraticCurveTo()를 사용하려면 반드시 2차 베지어 곡선을 3차 베지어 곡선으로 직접 변환한 후에 bezierCurveTo() 메소드를 사용해야 합니다.

var currentX, currentY;  // set to last x,y sent to lineTo/moveTo/bezierCurveTo or quadraticCurveToFixed()

function quadraticCurveToFixed( cpx, cpy, x, y ) {
  /*
   For the equations below the following variable name prefixes are used:
     qp0 is the quadratic curve starting point (you must keep this from your last point sent to moveTo(), lineTo(), or bezierCurveTo() ).
     qp1 is the quadatric curve control point (this is the cpx,cpy you would have sent to quadraticCurveTo() ).
     qp2 is the quadratic curve ending point (this is the x,y arguments you would have sent to quadraticCurveTo() ).
   We will convert these points to compute the two needed cubic control points (the starting/ending points are the same for both
   the quadratic and cubic curves.

   The equations for the two cubic control points are:
     cp0=qp0 and cp3=qp2
     cp1 = qp0 + 2/3 *(qp1-qp0)
     cp2 = cp1 + 1/3 *(qp2-qp0) 

   In the code below, we must compute both the x and y terms for each point separately. 

    cp1x = qp0x + 2.0/3.0*(qp1x - qp0x);
    cp1x = qp0y + 2.0/3.0*(qp1y - qp0y);
    cp2x = cp1x + (qp2x - qp0x)/3.0;
    cp2y = cp1y + (qp2y - qp0y)/3.0;

   We will now 
     a) replace the qp0x and qp0y variables with currentX and currentY (which *you* must store for each moveTo/lineTo/bezierCurveTo)
     b) replace the qp1x and qp1y variables with cpx and cpy (which we would have passed to quadraticCurveTo)
     c) replace the qp2x and qp2y variables with x and y.
   which leaves us with: 
  */
  var cp1x = currentX + 2.0/3.0*(cpx - currentX);
  var cp1y = currentY + 2.0/3.0*(cpy - currentY);
  var cp2x = cp1x + (x - currentX)/3.0;
  var cp2y = cp1y + (y - currentY)/3.0;

  // and now call cubic Bezier curve to function 
  bezierCurveTo( cp1x, cp1y, cp2x, cp2y, x, y );

  currentX = x;
  currentY = y;
}

사각형

앞서 살펴 본 사각형을 그리는 3가지 메소드 외에 사각형 경로를 그리는 rect 메소드가 있습니다.

rect(x, y, width, height)

이 메소드는 4개의 인자를 받습니다. xy 파라미터는 새롭게 그려질 사각형 경로의 우상단 좌표를 의미합니다. widthheight는 사각형의 너비와 높이를 의미합니다.

이 메소드가 실행되면 자동으로 (x, y) 파라미터를 받는 moveTo 메소드가 호출됩니다 (즉, 시작 위치로 이동합니다).

조합해서 만들기

본 페이지에 있는 모든 예제들은 하나의 모양을 만들때 한 가지 종류의 경로 함수만을 사용했습니다. 하지만 어떤 모양을 만들 때 사용하는 수 있는 경로의 종류나 양에 제한이 없습니다. 따라서 마지막 예제에서는 모든 종류의 경로 함수를 사용하려고 했고, 이를 이용해 아주 유명한 게임 캐릭터들을 만들어 보았습니다.

예제

I'm not going to run through this complete script, but the most important things to note are the function roundedRect and the use of the fillStyle property. It can be very usefull and time saving to define your own functions to draw more complex shapes. In this script it would have taken me twice as many lines of code as I have now.
We will look at the fillStyle property in greater depth later in this tutorial. Here I'm using it to change the fill color from the default black, to white, and back again.

예제 보기

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  roundedRect(ctx,12,12,150,150,15);
  roundedRect(ctx,19,19,150,150,9);
  roundedRect(ctx,53,53,49,33,10);
  roundedRect(ctx,53,119,49,16,6);
  roundedRect(ctx,135,53,49,33,10);
  roundedRect(ctx,135,119,25,49,10);

  ctx.beginPath();
  ctx.arc(37,37,13,Math.PI/7,-Math.PI/7,true);
  ctx.lineTo(31,37);
  ctx.fill();
  for(i=0;i<8;i++){
    ctx.fillRect(51+i*16,35,4,4);
  }
  for(i=0;i<6;i++){
    ctx.fillRect(115,51+i*16,4,4);
  }
  for(i=0;i<8;i++){
    ctx.fillRect(51+i*16,99,4,4);
  }
  ctx.beginPath();
  ctx.moveTo(83,116);
  ctx.lineTo(83,102);
  ctx.bezierCurveTo(83,94,89,88,97,88);
  ctx.bezierCurveTo(105,88,111,94,111,102);
  ctx.lineTo(111,116);
  ctx.lineTo(106.333,111.333);
  ctx.lineTo(101.666,116);
  ctx.lineTo(97,111.333);
  ctx.lineTo(92.333,116);
  ctx.lineTo(87.666,111.333);
  ctx.lineTo(83,116);
  ctx.fill();
  ctx.fillStyle = "white";
  ctx.beginPath();
  ctx.moveTo(91,96);
  ctx.bezierCurveTo(88,96,87,99,87,101);
  ctx.bezierCurveTo(87,103,88,106,91,106);
  ctx.bezierCurveTo(94,106,95,103,95,101);
  ctx.bezierCurveTo(95,99,94,96,91,96);
  ctx.moveTo(103,96);
  ctx.bezierCurveTo(100,96,99,99,99,101);
  ctx.bezierCurveTo(99,103,100,106,103,106);
  ctx.bezierCurveTo(106,106,107,103,107,101);
  ctx.bezierCurveTo(107,99,106,96,103,96);
  ctx.fill();
  ctx.fillStyle = "black";
  ctx.beginPath();
  ctx.arc(101,102,2,0,Math.PI*2,true);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(89,102,2,0,Math.PI*2,true);
  ctx.fill();
}
function roundedRect(ctx,x,y,width,height,radius){
  ctx.beginPath();
  ctx.moveTo(x,y+radius);
  ctx.lineTo(x,y+height-radius);
  ctx.quadraticCurveTo(x,y+height,x+radius,y+height);
  ctx.lineTo(x+width-radius,y+height);
  ctx.quadraticCurveTo(x+width,y+height,x+width,y+height-radius);
  ctx.lineTo(x+width,y+radius);
  ctx.quadraticCurveTo(x+width,y,x+width-radius,y);
  ctx.lineTo(x+radius,y);
  ctx.quadraticCurveTo(x,y,x,y+radius);
  ctx.stroke();
}

문서 태그 및 공헌자

Contributors to this page: teoli, Suguni, 토끼군
최종 변경: teoli,