◀ 이전: Drawing shapes | 다음: Drawing text ▶
도형 그리기에 대한 챕터에서는 기본 선과 채우기 스타일만 사용했어요. 여기서는 우리가 사용할 수 있는 canvas 옵션들을 살펴보고 그림을 좀 더 매력적으로 만들어볼 거예요. 그림에 다양한 색상, 선 스타일, 그라데이션, 패턴, 그림자를 추가하는 방법을 배우게 될 거예요.
참고:
Canvas 콘텐츠는 스크린 리더에서 접근할 수 없어요. Canvas가 순전히 장식용이라면,<canvas>여는 태그에role="presentation"을 포함하세요. 그렇지 않으면, canvas 요소 자체에 직접aria-label속성의 값으로 설명 텍스트를 포함하거나, canvas 여는 태그와 닫는 태그 사이에 배치된 대체 콘텐츠를 포함하세요. Canvas 콘텐츠는 DOM의 일부가 아니지만, 중첩된 대체 콘텐츠는 DOM의 일부예요.
💡 강사 팁: 드디어 Canvas를 예쁘게 꾸미는 단계예요! 기본 도형만 그릴 줄 알면 기능적으로는 충분하지만, 스타일과 색상을 적용할 줄 알아야 진짜 멋진 그래픽을 만들 수 있답니다.
색상과 스타일의 핵심 개념:
fillStyle vs strokeStyle:
fillStyle: 도형 내부를 채우는 색strokeStyle: 도형 테두리(윤곽선) 색- 둘 다 색상, 그라데이션, 패턴을 받을 수 있어요!
상태 저장이 중요해요: Canvas는 "상태 머신"처럼 작동해요. 한번 설정한 스타일은 다시 바꾸기 전까지 계속 유지돼요.
실무에서 자주 사용하는 패턴:
// 색상 설정 - 여러 방식 가능 ctx.fillStyle = '#FF0000'; // HEX ctx.fillStyle = 'rgb(255, 0, 0)'; // RGB ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; // 투명도 포함 ctx.fillStyle = 'red'; // 색상 이름 // 여러 도형에 다른 색 적용하기 ctx.fillStyle = 'blue'; ctx.fillRect(0, 0, 50, 50); // 파란 사각형 ctx.fillStyle = 'red'; ctx.fillRect(60, 0, 50, 50); // 빨간 사각형제가 실무에서 자주 쓰는 기법들:
그라데이션: 차트나 그래프를 만들 때 그라데이션을 쓰면 훨씬 프로페셔널해 보여요. Linear gradient(선형)와 Radial gradient(원형) 두 종류가 있는데, 버튼이나 배경에 깊이감을 주기 좋아요.
투명도 활용:
globalAlpha나rgba()로 투명도를 조절하면 레이어 효과를 낼 수 있어요. 저는 데이터 시각화할 때 겹치는 영역을 표현하는데 많이 썼어요.선 스타일:
lineWidth,lineCap,lineJoin설정으로 선의 느낌이 완전히 달라져요. 손글씨 느낌을 낼 때는lineCap = 'round'를 꼭 쓰세요!흔한 실수와 해결법:
색이 안 바뀌어요!: 스타일은 그리기 전에 설정해야 해요.
fillRect()후에fillStyle을 바꿔봤자 소용없어요.그라데이션이 이상해요: 그라데이션 좌표는 Canvas 전체 기준이에요. 도형 크기가 바뀌어도 그라데이션은 같은 위치에 있어요. 도형마다 새로 만들어야 해요.
성능 문제: 매 프레임마다 복잡한 그라데이션을 새로 만들면 느려져요. 가능하면 한 번 만들어서 재사용하세요.
접근성 관련 중요한 팁:
위의 참고 사항은 정말 중요해요! Canvas로 의미있는 콘텐츠를 만들 때는 반드시 접근성을 고려해야 해요. 예를 들어:
<!-- 장식용 --> <canvas role="presentation"></canvas> <!-- 데이터 차트 --> <canvas aria-label="2024년 월별 매출 차트"> <p>1월: 100만원, 2월: 150만원...</p> </canvas>실제로 프로젝트에서 Canvas 차트를 만들 때, 시각 장애인을 위해 데이터를 텍스트로도 제공했더니 SEO에도 도움이 되더라고요!
안녕하세요! 이번에는 캔버스의 생명이라고 할 수 있는 '색상(Colors)'과 '선 스타일(Line styles)'에 대해 알아보겠습니다.
앞서 도형을 그리는 방법을 배웠다면, 이제는 그 도형들에 예쁜 색을 칠하고 선의 두께나 모양을 디테일하게 조절해 볼 차례예요. 웹 프로필 사이트를 꾸미거나 데이터를 시각화할 때 이 설정들이 작품의 완성도를 크게 좌우한답니다. 강사의 팁과 함께 자세히 살펴볼까요?
지금까지는 그림을 그리기 위한(경로를 만드는) 컨텍스트의 메서드들만 살펴보았습니다. 우리가 그린 도형에 색을 입히고 싶다면, 다음의 두 가지 아주 중요한 속성을 사용할 수 있습니다: fillStyle과 strokeStyle.
fillStyle = color
도형의 내부를 채울 때 사용할 색상이나 스타일을 설정합니다.
strokeStyle = color
도형의 테두리(윤곽선)에 사용할 색상이나 스타일을 설정합니다.
여기서 color는 CSS의 <color> 값, 그라디언트(gradient) 객체, 또는 패턴(pattern) 객체를 나타내는 문자열입니다. 그라디언트와 패턴 객체에 대해서는 나중에 다시 다루도록 하겠습니다. 기본적으로 윤곽선과 채우기 색상은 모두 검은색(CSS 색상 값 #000000)으로 설정되어 있습니다.
참고:
strokeStyle이나fillStyle속성의 값을 변경하면, 그 시점 이후로 그려지는 모든 도형들은 새로 설정된 이 값을 기본값으로 사용하게 됩니다. 만약 각기 다른 색상으로 여러 도형을 그리고 싶다면, 도형을 그릴 때마다fillStyle이나strokeStyle속성을 새로운 색상으로 다시 지정해 주어야 합니다.
💡 강사의 핵심 팁:
캔버스는 마치 '화가'와 같습니다! 화가가 빨간색 물감을 붓에 묻히면(fillStyle = 'red'), 붓을 씻고 다른 색을 묻히기 전까지는 계속 빨간색으로만 그림을 그리게 됩니다. 코드를 작성하실 때 이 '상태 유지(Stateful)' 특성을 꼭 기억하세요!
명세(specification)에 따르면, 이 속성들에 입력할 수 있는 유효한 문자열은 CSS <color> 값이어야 합니다. 아래의 예제들은 모두 완벽하게 동일한 색상(주황색)을 설정하는 코드입니다.
// 아래 코드는 모두 fillStyle을 'orange'로 설정합니다.
ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255 165 0)";
ctx.fillStyle = "rgb(255 165 0 / 100%)";
fillStyle 예제이 예제에서는 두 개의 for 루프를 다시 한번 사용하여 여러 가지 색상으로 채워진 직사각형 그리드를 그려보겠습니다. 결과 이미지는 하단의 스크린샷과 비슷할 것입니다. 코드가 엄청나게 특별한 것은 아닙니다. i와 j라는 두 변수를 사용해서 각 사각형마다 고유한 RGB 색상을 생성하는데, 이때 빨간색(red)과 초록색(green) 값만 수정하고 파란색(blue) 채널은 0으로 고정했습니다. 이런 식으로 각 채널 값을 수정하면 온갖 종류의 팔레트를 만들어낼 수 있죠. 반복문의 단계(step)를 늘리면 포토샵(Photoshop)에서 사용하는 색상 팔레트와 비슷한 멋진 결과물도 얻을 수 있습니다.
<canvas id="canvas" width="150" height="150" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 6; j++) {
ctx.fillStyle = `rgb(${Math.floor(255 - 42.5 * i)} ${Math.floor(
255 - 42.5 * j,
)} 0)`;
ctx.fillRect(j * 25, i * 25, 25, 25);
}
}
}
strokeStyle 예제이 예제는 바로 위의 예제와 매우 비슷하지만, 이번에는 도형 내부를 채우는 대신 strokeStyle 속성을 사용해서 테두리의 색상을 변경합니다. 또한 사각형 대신 arc() 메서드를 사용해서 동그라미(원) 그리드를 그립니다.
<canvas id="canvas" width="150" height="150" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 6; j++) {
ctx.strokeStyle = `rgb(0 ${Math.floor(255 - 42.5 * i)} ${Math.floor(
255 - 42.5 * j,
)})`;
ctx.beginPath();
ctx.arc(12.5 + j * 25, 12.5 + i * 25, 10, 0, 2 * Math.PI, true);
ctx.stroke();
}
}
}
캔버스에 불투명한 도형을 그리는 것 외에도, 반투명한(혹은 투명한) 도형도 그릴 수 있습니다. 이 작업은 globalAlpha 속성을 설정하거나, 테두리 및 채우기 스타일에 반투명한 색상 값을 직접 할당하는 방식으로 이루어집니다.
globalAlpha = transparencyValue
이 속성을 설정한 이후에 캔버스에 그려지는 모든 도형에 지정된 투명도 값이 적용됩니다. 값은 반드시 0.0 (완전 투명)에서 1.0 (완전 불투명) 사이여야 합니다. 기본값은 1.0 (완전 불투명)입니다.
globalAlpha 속성은 캔버스 위에 일괄적으로 비슷한 투명도를 가진 도형들을 아주 많이 그릴 때 유용할 수 있습니다. 하지만 일반적으로는 개별 도형의 색상을 설정할 때 직접 투명도를 함께 지정하는 방식이 훨씬 더 활용도가 높습니다.
strokeStyle과 fillStyle 속성은 CSS rgb 색상 값을 허용하기 때문에, 아래와 같은 표기법을 사용해서 투명한 색상을 직접 할당할 수 있습니다.
// 테두리와 채우기 스타일에 투명한 색상 할당하기
ctx.strokeStyle = "rgb(255 0 0 / 50%)"; // 반투명한 빨간색 윤곽선
ctx.fillStyle = "rgb(255 0 0 / 50%)"; // 반투명한 빨간색 채우기
rgb() 함수에는 선택적으로 추가할 수 있는 매개변수가 하나 더 있습니다. 슬래시(/) 뒤에 오는 이 마지막 매개변수는 해당 색상의 투명도(알파) 값을 설정합니다. 유효한 범위는 0%(완전 투명)에서 100%(완전 불투명) 사이의 백분율로 지정하거나, 0.0(0%와 동일)에서 1.0(100%와 동일) 사이의 숫자로 지정할 수 있습니다.
💡 강사의 실무 팁:
과거에는rgba(255, 0, 0, 0.5)형태로 많이 작성했지만, 최신 CSS 색상 모듈 규격에 따라rgb(255 0 0 / 50%)처럼 쉼표 없이 띄어쓰기와 슬래시를 사용하는 문법이 표준으로 권장되고 있습니다!
globalAlpha 예제이 예제에서는 바탕에 각기 다른 4가지 색상의 정사각형을 먼저 그리겠습니다. 그런 다음 그 위에 반투명한 원들을 겹쳐서 그릴 거예요. globalAlpha 속성을 0.2로 설정했기 때문에, 이 시점 이후에 그려지는 모든 도형(원들)은 이 투명도를 사용하게 됩니다.
for 루프가 한 번씩 돌 때마다 반지름이 점점 커지는 원들이 그려집니다. 최종 결과물은 방사형 그라디언트(radial gradient) 같은 느낌을 줍니다. 수많은 원들을 겹겹이 쌓아 올리면서, 이미 그려진 원들의 투명도가 점진적으로 줄어드는(불투명해지는) 효과를 내는 것이죠. 루프의 횟수(step count)를 늘려서 더 많은 원을 그리게 되면, 이미지 중앙 부분의 배경색은 완전히 가려져서 보이지 않게 될 것입니다.
<canvas id="canvas" width="150" height="150" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 배경 사각형 4개 그리기
ctx.fillStyle = "#ffdd00";
ctx.fillRect(0, 0, 75, 75);
ctx.fillStyle = "#66cc00";
ctx.fillRect(75, 0, 75, 75);
ctx.fillStyle = "#0099ff";
ctx.fillRect(0, 75, 75, 75);
ctx.fillStyle = "#ff3300";
ctx.fillRect(75, 75, 75, 75);
ctx.fillStyle = "white";
// 전역 투명도(globalAlpha) 설정
ctx.globalAlpha = 0.2;
// 반투명한 원들 그리기
for (let i = 0; i < 7; i++) {
ctx.beginPath();
ctx.arc(75, 75, 10 + 10 * i, 0, Math.PI * 2, true);
ctx.fill();
}
}
rgb()를 활용한 알파 투명도 예제이 두 번째 예제에서는 바로 위와 비슷한 작업을 하지만, 겹쳐진 원 대신 점점 불투명해지는 작은 직사각형들을 그려보겠습니다. rgb() 함수를 사용하면 도형을 칠할 때(fill)와 테두리를 그릴 때(stroke)의 스타일을 개별적으로 설정할 수 있기 때문에 제어하기가 훨씬 수월하고 유연해집니다.
<canvas id="canvas" width="150" height="150" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 배경 그리기 (4개의 가로 띠)
ctx.fillStyle = "rgb(255 221 0)";
ctx.fillRect(0, 0, 150, 37.5);
ctx.fillStyle = "rgb(102 204 0)";
ctx.fillRect(0, 37.5, 150, 37.5);
ctx.fillStyle = "rgb(0 153 255)";
ctx.fillRect(0, 75, 150, 37.5);
ctx.fillStyle = "rgb(255 51 0)";
ctx.fillRect(0, 112.5, 150, 37.5);
// 반투명한 사각형들 그리기
for (let i = 0; i < 10; i++) {
// i가 증가할수록 투명도가 0.1, 0.2... 1.0으로 짙어집니다.
ctx.fillStyle = `rgb(255 255 255 / ${(i + 1) / 10})`;
for (let j = 0; j < 4; j++) {
ctx.fillRect(5 + i * 14, 5 + j * 37.5, 14, 27.5);
}
}
}
캔버스에는 선의 모양(스타일)을 디테일하게 지정할 수 있는 여러 가지 속성들이 준비되어 있습니다.
lineWidth = value
앞으로 그려질 선의 두께를 설정합니다.
lineCap = type
선의 양쪽 끝부분 모양을 설정합니다.
lineJoin = type
두 개의 선이 만나는 "모서리(corners)" 부분의 모양을 설정합니다.
miterLimit = value
두 선이 예각(sharp angle)으로 만날 때 뾰족하게 튀어나오는 부분(miter)의 한계치를 설정하여, 모서리 부분이 얼마나 두껍게 연장될지를 제어합니다.
getLineDash()
현재 설정된 점선(dash) 패턴의 배열(짝수 개의 음이 아닌 숫자로 구성됨)을 반환합니다.
setLineDash(segments)
현재 선의 점선 패턴을 설정합니다.
lineDashOffset = value
선에서 점선 패턴이 시작될 오프셋(위치)을 지정합니다.
이 속성들이 정확히 어떤 역할을 하는지는 아래의 예제들을 직접 확인해 보시면 훨씬 빠르고 명확하게 이해하실 수 있을 겁니다.
lineWidth 예제이 속성은 현재 펜의 굵기를 설정합니다. 값은 반드시 양수(positive numbers)여야 합니다. 기본적으로 이 값은 1.0 유닛으로 설정되어 있습니다.
선의 두께란 주어진 경로(path)를 정중앙에 두고 양옆으로 확장되는 두께를 의미합니다. 다시 말해, 실제 그려지는 픽셀 영역은 경로를 기준으로 양쪽에 선 두께의 절반씩 확장됩니다. 캔버스의 좌표계는 화면의 픽셀과 1:1로 직접 맵핑되지 않기 때문에, 깔끔하고 선명한 수평선이나 수직선을 얻으려면 세심한 주의가 필요합니다.
아래 예제에서는 점점 굵어지는 10개의 직선을 그립니다. 가장 왼쪽에 있는 선의 굵기는 1.0 유닛입니다. 하지만 가장 왼쪽의 선과 그 외 홀수 굵기를 가진 선들의 모서리를 가만히 살펴보면 완전히 선명(crisp)하지 않고 살짝 흐릿해 보일 텐데, 이는 경로의 위치가 픽셀 사이사이에 걸쳐 있기 때문입니다.
<canvas id="canvas" width="150" height="150" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
for (let i = 0; i < 10; i++) {
ctx.lineWidth = 1 + i;
ctx.beginPath();
ctx.moveTo(5 + i * 14, 5);
ctx.lineTo(5 + i * 14, 140);
ctx.stroke();
}
}
참고:
검은색 선인데 왜 가장자리가 약간 회색처럼 흐릿하게 보이는지 궁금하시다면, 이전 챕터의 모서리가 흐릿하게 보이나요? (Seeing blurry edges?) 섹션을 꼭 확인해 보세요! (0.5 픽셀 보정 트릭을 배우실 수 있습니다.)
lineCap 예제lineCap 속성은 모든 선의 양 끝점(end points)을 어떤 모양으로 그릴지 결정합니다. 설정할 수 있는 값은 butt, round, square 이렇게 세 가지가 있습니다. 기본값은 butt입니다.
butt
선의 양 끝이 지정된 끝점 좌표에서 직각으로 싹둑 잘린 모양으로 끝납니다.
round
선의 양 끝에 반원 모양이 부드럽게 둥글게 추가됩니다.
square
선의 양 끝에 선 두께의 절반만큼 길이를 가진 사각형 캡이 추가되어 끝납니다.
이 속성은 경로의 '시작점'과 '최종 끝점'에만 영향을 미칩니다. 만약 closePath()를 통해 경로가 닫혀 버렸다면, 시작점과 끝점이 아예 존재하지 않게 됩니다. 대신 경로 안의 모든 점들은 이전에 그려진 선분과 다음 선분에 하나로 연결되며, 이때는 모서리 처리를 담당하는 lineJoin 속성의 설정을 따르게 됩니다.
이 예제에서는 lineCap 속성값을 각각 다르게 적용하여 3개의 선을 그려보겠습니다. 세 가지 옵션의 차이를 정확하게 보여주기 위해 파란색 가이드라인 두 개를 함께 그렸습니다. 세 개의 검은 선은 모두 정확히 이 가이드라인 좌표에서 시작하고 끝납니다.
butt를 사용했습니다. 가이드라인에 완전히 딱 맞아떨어지게 그려진 것을 볼 수 있죠.round 옵션을 사용했습니다. 끝점에 선 두께의 절반을 반지름으로 하는 예쁜 반원이 추가되었습니다.square 옵션을 사용했습니다. 끝점에 선 두께와 동일한 너비를 가지고 높이는 두께의 절반인 사각형 상자가 추가되어 가이드라인 밖으로 튀어나와 있습니다.<canvas id="canvas" width="150" height="150" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 파란색 가이드라인 먼저 그리기
ctx.strokeStyle = "#0099ff";
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(140, 10);
ctx.moveTo(10, 140);
ctx.lineTo(140, 140);
ctx.stroke();
// 선 그리기
ctx.strokeStyle = "black";
["butt", "round", "square"].forEach((lineCap, i) => {
ctx.lineWidth = 15;
ctx.lineCap = lineCap;
ctx.beginPath();
ctx.moveTo(25 + i * 50, 10);
ctx.lineTo(25 + i * 50, 140);
ctx.stroke();
});
}
lineJoin 예제lineJoin 속성은 하나의 도형 안에서 길이가 0이 아닌 두 개의 연결된 선분(직선, 호, 곡선 등)이 서로 만나는 곳(모서리)을 어떻게 이어서 처리할지 결정합니다. (길이가 0인 선분들, 즉 지정된 끝점과 제어점이 완전히 같은 위치에 있는 선분들은 무시됩니다.)
이 속성에도 세 가지 값이 존재합니다: round, bevel, miter. 기본값은 뾰족하게 이어지는 miter입니다. (참고로 두 개의 연결된 선분이 완전히 같은 방향을 향하고 있다면, 접합부가 꺾이지 않으므로 이 lineJoin 설정은 아무런 시각적 효과를 내지 못합니다.)
round
두 선분이 만나는 공통 끝점을 중심으로, 원반 모양을 추가하여 도형의 모서리를 부드럽고 둥글게 깎아냅니다. 이 둥근 모서리의 반경은 선 두께의 절반과 같습니다.
bevel
두 선분이 만나는 공통 끝점과 각 선분의 바깥쪽 직각 모서리 사이의 남는 공간을 추가적인 삼각형 모양으로 평평하게 채웁니다. (모서리가 비스듬하게 잘린 모양이 됩니다.)
miter
연결된 두 선분의 바깥쪽 가장자리를 한 지점에서 만날 때까지 연장하여, 마치 마름모꼴 모양의 영역이 추가로 채워지는 듯한 뾰족한 효과를 냅니다. 이 설정은 앞서 언급한 miterLimit 속성의 수치값에 의해 뾰족함의 한계가 제어됩니다.
아래 예제에서는 이 세 가지 lineJoin 속성값을 각각 적용한 3개의 서로 다른 경로(꺾은선)를 그립니다.
<canvas id="canvas" width="150" height="150" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
ctx.lineWidth = 10;
["round", "bevel", "miter"].forEach((lineJoin, i) => {
ctx.lineJoin = lineJoin;
ctx.beginPath();
ctx.moveTo(-5, 5 + i * 40);
ctx.lineTo(35, 45 + i * 40);
ctx.lineTo(75, 5 + i * 40);
ctx.lineTo(115, 45 + i * 40);
ctx.lineTo(155, 5 + i * 40);
ctx.stroke();
});
}
miterLimit 속성 데모 (A demo of the miterLimit property)이전 예제에서 보셨듯이, miter 옵션(기본값)을 사용해서 두 선을 연결할 때, 두 선의 바깥쪽 가장자리는 두 선이 만나는 꼭짓점까지 뾰족하게 연장됩니다. 두 선이 이루는 각도가 큰(완만한) 경우에는 이 뾰족한 끝점이 안쪽 연결점에서 그리 멀지 않습니다. 하지만 두 선 사이의 각도가 좁아질수록(예각이 될수록), 이 두 점 사이의 거리(이를 miter length, 즉 마이터 길이라고 부릅니다)는 기하급수적으로 늘어나게 됩니다! (마치 아주 뾰족한 바늘 끝처럼 길게 튀어나가게 되죠.)
miterLimit 속성은 이 바깥쪽 꼭짓점이 안쪽 연결점으로부터 '얼마나 멀리까지' 떨어질 수 있는지를 결정합니다. 만약 두 선이 만나서 생기는 마이터 길이가 이 miterLimit이 허용하는 값을 초과해 버린다면, 뾰족하게 그리는 것을 포기하고 끝을 뭉툭하게 깎아버리는 bevel 조인으로 대신 그리게 됩니다.
참고로, 허용되는 최대 마이터 길이는 현재 좌표계에서 측정된 '선 두께(lineWidth)'에 이 miterLimit 값을 곱한 값입니다. (HTML <canvas>에서 miterLimit의 기본값은 10.0입니다.)
따라서 miterLimit은 현재 화면의 확대/축소 비율(display scale)이나 경로에 적용된 다른 변형(affine transforms)과는 완전히 독립적으로 설정될 수 있습니다. 오직 렌더링 된 선의 끝부분 모양 자체에만 영향을 줍니다.
수학적으로 조금 더 정확하게 말하자면, miter limit은 연장선 길이(HTML 캔버스에서는, 경로에 지정된 연결 세그먼트들의 공통 끝점부터 선의 결합된 가장자리의 바깥쪽 모서리까지 측정됨) 대 '선 두께의 절반'의 비율이 가질 수 있는 최대 허용치입니다. 다르게 말하면, 가장자리의 안쪽과 바깥쪽 교차점 사이의 거리 대 전체 선 두께의 비율에 대한 최대 허용치라고도 할 수 있습니다. 이 값은 bevel 조인 대신 miter 조인이 렌더링 되기 위한 두 선 사이의 최소 내부 각도(minimum inner angle) 절반의 코시컨트(cosecant) 값과 같습니다.
miterLimit = max miterLength / lineWidth = 1 / sin ( min θ / 2 )miterLimit을 √2 ≈ 1.4142136(올림)으로 설정하면, 모든 예각(90도 미만)에 대해 마이터를 잘라내고, 오직 직각이거나 둔각일 때만 마이터 조인을 유지합니다.miterLimit을 1.0으로 설정하는 것도 가능하지만, 이렇게 하면 사실상 모든 마이터 조인을 비활성화해버립니다 (항상 뭉툭하게 깎임).miterLimit으로 사용할 수 없으며 무효 처리됩니다.백문이 불여일견이죠! 아래에 miterLimit을 동적으로 바꿔가면서 캔버스의 도형에 어떤 영향을 미치는지 직접 눈으로 확인할 수 있는 작은 데모를 준비했습니다. 화면의 옅은 파란색 선들은 지그재그 패턴을 이루는 각각의 선이 정확히 어디서 시작하고 끝나는지를 보여주는 가이드라인입니다.
만약 이 데모에서 miterLimit 값을 4.2 아래로 입력하면, 눈에 보이는 어떤 모서리도 뾰족한 마이터로 연결되지 못하고 파란 선 근처에서 뭉툭하게 잘린 베벨(bevel) 모양으로 연결되는 것을 볼 수 있습니다. 반면 10보다 큰 값을 주면, 대부분의 모서리가 파란 선 밖으로 아주 멀리까지 길쭉하고 뾰족하게 튀어나온 마이터로 연결될 것입니다. 이때 왼쪽에서 오른쪽으로 갈수록 각도가 점점 커지므로(완만해지므로) 튀어나오는 뾰족한 부분의 높이도 점점 줄어드는 걸 볼 수 있습니다. 중간 정도의 값을 주면 왼쪽의 날카로운 모서리들은 베벨로 깎이고, 오른쪽의 상대적으로 완만한 모서리들만 마이터로 뾰족하게 연결되는 것을 확인할 수 있습니다.
<canvas id="canvas" width="150" height="150" role="presentation"></canvas>
<div>
Change the <code>miterLimit</code> by entering a new value below and clicking
the redraw button.<br /><br />
<label for="miterLimit">Miter limit</label>
<input type="number" id="miterLimit" min="1" />
<button id="redraw">Redraw</button>
</div>
body {
display: flex;
}
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 캔버스 초기화
ctx.clearRect(0, 0, 150, 150);
// 파란색 가이드라인 그리기
ctx.strokeStyle = "#0099ff";
ctx.lineWidth = 2;
ctx.strokeRect(-5, 50, 160, 50);
// 실제 지그재그 선 스타일 설정
ctx.strokeStyle = "black";
ctx.lineWidth = 10;
// 사용자가 입력한 miterLimit 값이 유효한지 확인하고 캔버스에 적용
if (document.getElementById("miterLimit").checkValidity()) {
ctx.miterLimit = parseFloat(document.getElementById("miterLimit").value);
}
// 지그재그 선 그리기 시작!
ctx.beginPath();
ctx.moveTo(0, 100);
for (let i = 0; i < 24; i++) {
// 짝수 번째는 위로(25), 홀수 번째는 아래로(-25) 꺾습니다.
const dy = i % 2 === 0 ? 25 : -25;
// x 좌표를 i의 1.5제곱에 2를 곱해 점점 간격이 넓어지게 만듭니다. (각도가 완만해짐)
ctx.lineTo(i ** 1.5 * 2, 75 + dy);
}
ctx.stroke();
return false;
}
// 최초 로딩 시 현재 캔버스의 miterLimit(기본값 10)을 인풋창에 표시하고 그려줍니다.
document.getElementById("miterLimit").value = document
.getElementById("canvas")
.getContext("2d").miterLimit;
draw();
const redraw = document.getElementById("redraw");
redraw.addEventListener("click", draw);
💡 강사의 팁: 실무에서 별(★) 모양이나 차트의 날카로운 꼭짓점을 두꺼운 선으로 그릴 때, 뾰족한 끝이 화면 밖을 뚫고 나가는(마치 바늘처럼 길게 튀어나가는) 현상을 종종 겪게 됩니다. 이럴 때 캔버스 자체를 줄이려고 하지 마시고, 이
miterLimit을 적절히 조절해서 예쁘게 깎아주거나, 선 연결 방식을 아예ctx.lineJoin = 'round'나'bevel'로 바꿔버리는 센스를 발휘해 보세요!
setLineDash 메서드와 lineDashOffset 속성을 사용하면 점선(dash) 패턴을 지정할 수 있습니다. setLineDash 메서드는 선을 그리고 띄우는 거리(간격)를 번갈아 가며 지정하는 숫자들의 배열(list of numbers)을 인자로 받습니다. 그리고 lineDashOffset 속성은 그 패턴이 어디서부터 시작할지 오프셋(offset) 위치를 설정합니다.
이 예제에서는 이 기능들을 활용해 소위 "행진하는 개미(marching ants)" 효과를 만들어 볼 겁니다. 이 효과는 포토샵 같은 컴퓨터 그래픽 프로그램의 선택(Selection) 도구에서 아주 흔하게 볼 수 있는 애니메이션 기법이죠! 점선 테두리가 스멀스멀 움직이도록 애니메이션을 주면, 사용자가 복잡한 이미지 배경 속에서도 선택된 영역의 테두리를 아주 쉽게 구별할 수 있게 도와줍니다. 나중에 이 튜토리얼의 뒷부분에서 이런 식의 기본 애니메이션을 어떻게 만드는지 더 자세히 배우실 수 있을 겁니다.
<canvas id="canvas" width="111" height="111" role="presentation"></canvas>
const ctx = document.getElementById("canvas").getContext("2d");
let offset = 0;
function draw() {
// 이전 프레임의 잔상을 지워줍니다.
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 점선의 패턴을 설정합니다. (4픽셀 그리고, 2픽셀 쉬고)
ctx.setLineDash([4, 2]);
// 점선이 시작되는 위치를 offset 변수를 사용해 뒤로 당깁니다.
ctx.lineDashOffset = -offset;
// 사각형 테두리를 그립니다.
ctx.strokeRect(10, 10, 100, 100);
}
function march() {
offset++;
// 패턴이 [4, 2] 이므로 전체 주기는 6입니다.
// 따라서 오프셋이 5를 넘어가면(즉 6이 되면) 다시 0으로 돌려줍니다.
if (offset > 5) {
offset = 0;
}
draw();
// 20ms마다 재귀적으로 호출하여 애니메이션을 만듭니다. (1초에 약 50프레임)
setTimeout(march, 20);
}
// 행진 시작!
march();
💡 강사의 팁:
requestAnimationFrame을 사용하지 않고setTimeout을 사용한 귀여운 옛날 방식의 애니메이션이네요! 실무에서 부드러운 애니메이션을 원하신다면 무조건requestAnimationFrame(march)패턴을 사용하시는 것이 렌더링 성능과 배터리 효율에 훨씬 좋습니다. 이 개념은 나중 튜토리얼에서 확실히 잡고 넘어갑시다!
일반적인 드로잉 프로그램들과 마찬가지로, 캔버스에서도 선형(linear), 방사형(radial), 원뿔형(conic) 그레이디언트를 사용해서 도형의 속을 채우거나 테두리를 그릴 수 있습니다. 우리는 다음 메서드 중 하나를 사용해 CanvasGradient 객체를 먼저 생성합니다. 그러고 나서 이 객체를 fillStyle이나 strokeStyle 속성에 할당(대입)해 주면 되는 것이죠.
createLinearGradient(x1, y1, x2, y2)
시작점 (x1, y1)과 끝점 (x2, y2)을 연결하는 선을 따라 색이 변하는 선형(linear) 그레이디언트 객체를 생성합니다.
createRadialGradient(x1, y1, r1, x2, y2, r2)
두 개의 원을 기준으로 퍼져나가는 방사형(radial) 그레이디언트를 생성합니다. 매개변수들은 두 개의 원을 정의합니다. 하나는 (x1, y1)가 중심이고 반지름이 r1인 원이고, 다른 하나는 (x2, y2)가 중심이고 반지름이 r2인 원입니다.
createConicGradient(angle, x, y)
특정 위치 (x, y)를 중심으로 하고, 시작 각도인 angle(라디안 단위)부터 빙글 돌면서 색이 변하는 원뿔형(conic) 그레이디언트 객체를 생성합니다.
예를 들면 이렇게 생성합니다:
// (0,0)에서 (150, 150) 대각선 방향으로 흐르는 선형 그레이디언트
const lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
// 중심이 (75, 75)이고 크기가 다른 두 원이 만드는 방사형 그레이디언트
const radialgradient = ctx.createRadialGradient(75, 75, 0, 75, 75, 100);
이렇게 CanvasGradient 객체를 뼈대만 하나 만들어두고 나면, 그다음엔 addColorStop() 메서드를 사용해서 이 뼈대 위에 원하는 색상들을 '물감 짜듯' 배치해 줄 수 있습니다.
gradient.addColorStop(position, color)
생성해 둔 gradient 객체에 새로운 색상 정지점(color stop)을 추가합니다. position 인자는 0.0부터 1.0 사이의 숫자여야 하며, 그레이디언트 진행 방향을 기준으로 이 색상이 어디쯤 위치할지를 상대적으로 정의합니다. color 인자는 CSS의 <color> 값을 나타내는 문자열(예: '#fff', 'red', 'rgba(0,0,0,0.5)')이어야 하며, 짚어준 위치(offset)에 도달했을 때 그레이디언트가 띄어야 할 정확한 색상을 지시합니다.
여러분은 그레이디언트에 필요한 만큼 얼마든지 여러 개의 색상 정지점을 추가할 수 있습니다. 아래 코드는 하얀색에서 시작해서 검은색으로 끝나는 아주 심플한 선형 그레이디언트를 만드는 예시입니다.
const lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
lineargradient.addColorStop(0, "white"); // 시작 지점(0%)은 흰색
lineargradient.addColorStop(1, "black"); // 끝 지점(100%)은 검은색
createLinearGradient 예제 (A createLinearGradient example)이 예제에서는 두 가지 서로 다른 선형 그레이디언트를 만들어 보겠습니다. 아래 코드를 보시면, strokeStyle과 fillStyle 속성 모두 이렇게 만들어진 canvasGradient 객체를 훌륭한 입력값으로 받아들일 수 있다는 사실을 알 수 있습니다!
<canvas id="canvas" width="150" height="150" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 그레이디언트 생성하기
const linGrad = ctx.createLinearGradient(0, 0, 0, 150); // 수직 방향
linGrad.addColorStop(0, "#00ABEB"); // 하늘색 시작
linGrad.addColorStop(0.5, "white"); // 중간(50%)에 흰색
linGrad.addColorStop(0.5, "#26C000"); // 앗? 50%에 녹색을 또 넣었네요!
linGrad.addColorStop(1, "white"); // 끝은 다시 흰색
const linGrad2 = ctx.createLinearGradient(0, 50, 0, 95); // 더 짧은 수직 방향
linGrad2.addColorStop(0.5, "black"); // 중간 지점을 까맣게
linGrad2.addColorStop(1, "transparent"); // 끝은 투명하게
// 만들어둔 그레이디언트들을 채우기 색과 선 색에 할당합니다.
ctx.fillStyle = linGrad;
ctx.strokeStyle = linGrad2;
// 도형을 그립니다!
ctx.fillRect(10, 10, 130, 130);
ctx.strokeRect(50, 50, 50, 50);
}
첫 번째는 배경에 깔린 채우기용 그레이디언트(linGrad)입니다. 눈치채셨나요? 0.5라는 완전히 똑같은 위치에 서로 다른 두 개의 색상(하얀색과 녹색)을 연달아 할당했습니다. 이렇게 하면 흰색에서 녹색으로 스르륵 부드럽게 변하는 게 아니라, 칼로 자른 듯 아주 날카롭고 선명한 경계선이 만들어지게 됩니다! 평소에는 색상 정지점들을 어떤 순서로 선언하든 크게 상관없지만, 이렇게 동일한 위치를 여러 번 사용할 때는 선언하는 순서가 매우 중요하게 작용합니다. 코드가 의도한 순서대로 실행되도록 신경 써서 작성해 주세요.
두 번째 테두리용 그레이디언트(linGrad2)에서는 시작 색상(위치 0.0)을 아예 지정하지 않았습니다. 왜냐하면 꼭 필요하지 않기 때문이죠! 만약 시작 색상이 없다면, 브라우저는 가장 먼저 선언된 다음 색상 정지점의 색깔을 시작점까지 그대로 끌어다 씁니다. 따라서 0.5 위치에 검은색을 지정해 둔 덕분에, 시작점부터 0.5 지점까지는 쭈욱 새카만 색으로 자동 렌더링 된답니다.
createRadialGradient 예제 (A createRadialGradient example)이번 예제에서는 무려 4개의 서로 다른 방사형(원형) 그레이디언트를 정의해 볼 겁니다. 캔버스에서는 그레이디언트가 퍼져나가는 시작원(점)과 끝원의 위치와 크기를 우리 마음대로 조절할 수 있기 때문에, 포토샵에서나 볼 법한 '클래식한' 방사형 그레이디언트(그저 하나의 중심점에서 원 모양으로 바깥으로 퍼져나가는 방식)보다 훨씬 더 복잡하고 입체적인 효과를 만들어낼 수 있습니다.
<canvas id="canvas" width="150" height="150" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 그레이디언트 4형제 생성!
// 첫 번째: 중심이 살짝 어긋난 두 원으로 3D 구슬 느낌 내기
const radGrad = ctx.createRadialGradient(45, 45, 10, 52, 50, 30);
radGrad.addColorStop(0, "#A7D30C");
radGrad.addColorStop(0.9, "#019F62");
radGrad.addColorStop(1, "transparent");
const radGrad2 = ctx.createRadialGradient(105, 105, 20, 112, 120, 50);
radGrad2.addColorStop(0, "#FF5F98");
radGrad2.addColorStop(0.75, "#FF0188");
radGrad2.addColorStop(1, "transparent");
const radGrad3 = ctx.createRadialGradient(95, 15, 15, 102, 20, 40);
radGrad3.addColorStop(0, "#00C9FF");
radGrad3.addColorStop(0.8, "#00B5E2");
radGrad3.addColorStop(1, "transparent");
const radGrad4 = ctx.createRadialGradient(0, 150, 50, 0, 140, 90);
radGrad4.addColorStop(0, "#F4F201");
radGrad4.addColorStop(0.8, "#E4C700");
radGrad4.addColorStop(1, "transparent");
// 도형을 그립니다. 투명도를 살리기 위해 겹치게 그립니다.
ctx.fillStyle = radGrad4;
ctx.fillRect(0, 0, 150, 150);
ctx.fillStyle = radGrad3;
ctx.fillRect(0, 0, 150, 150);
ctx.fillStyle = radGrad2;
ctx.fillRect(0, 0, 150, 150);
ctx.fillStyle = radGrad;
ctx.fillRect(0, 0, 150, 150);
}
이 코드를 보면 시작점(안쪽 원)을 끝점(바깥쪽 원)의 정중앙에 두지 않고 살짝 빗겨나가게(offset) 배치해서, 마치 구슬에 빛이 반사되는 듯한 입체적인 3D 구형 효과(spherical 3D effect)를 멋지게 구현해 냈습니다. 여기서 주의할 점은, 안쪽 원과 바깥쪽 원이 서로 교차하거나 겹치게 그리는 것은 가급적 피해야 한다는 것입니다. 그렇게 하면 예측하기 어렵고 꽤 기괴한(?) 시각적 효과가 발생하기 십상이거든요.
또한 이 4개의 그레이디언트 모두 마지막 색상 정지점(1 위치)에는 완전히 투명한 색(transparent)을 사용했습니다. 이 투명한 영역이 그 직전의 색상 정지점과 아주 자연스럽고 예쁘게 섞이길 원한다면, 두 위치의 색상 자체는 똑같고 투명도(Alpha)만 다르게 주어야 합니다. 위 코드에서는 직관적인 이해를 돕기 위해 HEX 색상 코드와 transparent 키워드를 섞어 썼지만, 사실 첫 번째 그레이디언트의 #019F62는 CSS rgb() 문법으로 표현하면 rgb(1 159 98 / 100%)와 같습니다. 즉, 자연스럽게 사라지게 하려면 끝점을 rgb(1 159 98 / 0%)로 주면 더 완벽한 그라데이션을 얻을 수 있습니다.
createConicGradient 예제 (A createConicGradient example)이번에는 가장 최근에 추가된 두 개의 원뿔형(conic) 그레이디언트를 정의해 보겠습니다. 원뿔형 그레이디언트는 방사형 그레이디언트처럼 동심원을 그리는 대신, 어떤 한 점을 중심으로 각도(angle)를 따라 빙글빙글 돌아가면서 색이 변한다는 점이 특징입니다!
<canvas id="canvas" width="250" height="150" role="presentation"
>A conic gradient</canvas
>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 그레이디언트 생성
// 첫 번째: (62, 75)를 중심으로 2 라디안 각도에서 시작
const conicGrad1 = ctx.createConicGradient(2, 62, 75);
conicGrad1.addColorStop(0, "#A7D30C"); // 연두색에서 시작해서
conicGrad1.addColorStop(1, "white"); // 하얀색으로 한 바퀴 돕니다.
// 두 번째: (187, 75)를 중심으로 0 라디안에서 시작하는 체커보드 패턴
const conicGrad2 = ctx.createConicGradient(0, 187, 75);
// (각도(degree)를 라디안(radian)으로 바꾸려면 보통 Math.PI/180 을 곱합니다!)
// 0% ~ 25% 구간은 블랙
conicGrad2.addColorStop(0, "black");
conicGrad2.addColorStop(0.25, "black");
// 25%에서 하얀색으로 날카롭게 교체 후 50%까지 유지
conicGrad2.addColorStop(0.25, "white");
conicGrad2.addColorStop(0.5, "white");
// 50%에서 다시 블랙으로 교체 후 75%까지 유지
conicGrad2.addColorStop(0.5, "black");
conicGrad2.addColorStop(0.75, "black");
// 75%에서 하얀색으로 교체 후 끝까지 유지
conicGrad2.addColorStop(0.75, "white");
conicGrad2.addColorStop(1, "white");
// 준비된 그레이디언트로 사각형 색칠하기
ctx.fillStyle = conicGrad1;
ctx.fillRect(12, 25, 100, 100);
ctx.fillStyle = conicGrad2;
ctx.fillRect(137, 25, 100, 100);
}
첫 번째 그레이디언트는 왼쪽 사각형의 정중앙에 중심을 두고 있으며, 초록색에서 시작해 하얀색으로 부드럽게 한 바퀴를 돕니다. 시작 각도가 2 라디안(약 114도, 우측 하단 방향)으로 설정되어 있기 때문에, 시작점과 끝점이 만나는 뚜렷한 경계선이 남동쪽을 향하고 있는 것을 확연히 볼 수 있습니다.
두 번째 그레이디언트 역시 오른쪽 사각형의 정중앙에 위치해 있습니다. 하지만 이 녀석은 앞서 배운 '동일 위치에 두 가지 색상 넣기' 기법을 활용해서 여러 개의 색상 정지점을 겹쳐 두었습니다. 회전하는 매 1/4 바퀴(25%)마다 검은색과 흰색이 칼같이 번갈아 나타나게 설정했기 때문에, 마치 체스판 같은 멋진 체크무늬(checkered) 효과가 원형으로 나타납니다.
앞선 페이지의 예제들 중 하나에서, 우리는 복잡한 for 루프(반복문)를 돌려서 이미지를 패턴처럼 화면에 깔아본 적이 있습니다. 하지만 사실 이런 번거로운 짓을 할 필요 없이 훨씬 더 간단하게 해결할 수 있는 비장의 무기가 있습니다! 바로 createPattern() 메서드입니다.
createPattern(image, type)
새로운 캔버스 패턴 객체를 뚝딱 만들어서 반환합니다. image 인자로는 패턴의 소스가 될 이미지를 통째로 넘깁니다. (이때 사용할 수 있는 소스는 평범한 HTMLImageElement(img 태그)부터 시작해서, SVGImageElement, 다른 캔버스인 HTMLCanvasElement 나 OffscreenCanvas, 심지어 동영상인 HTMLVideoElement의 한 프레임이나 ImageBitmap까지 아주 다양합니다!) type 인자에는 이 이미지를 어떻게 이어 붙일지(타일링할지) 지시하는 문자열을 넣어줍니다.
이 type 문자열은 반드시 다음 중 하나의 값을 가져야만 합니다:
repeat
이미지를 가로(수평)와 세로(수직) 양쪽 방향 모두로 무한히 타일링(바둑판식 배열)합니다.
repeat-x
이미지를 오직 가로 방향으로만 반복합니다. (세로로는 반복하지 않음)
repeat-y
이미지를 오직 세로 방향으로만 반복합니다. (가로로는 반복하지 않음)
no-repeat
이미지를 전혀 타일링하지 않습니다. 딱 한 번만 렌더링하고 끝냅니다.
우리는 이 메서드를 사용해서 CanvasPattern 객체를 생성합니다. 눈치채셨겠지만, 방금 위에서 실컷 다뤄본 그레이디언트 생성 메서드들과 사용법이 소름 돋게 똑같습니다! 패턴을 만들고 나면, 그레이디언트 때와 마찬가지로 이 패턴 객체를 fillStyle이나 strokeStyle 속성에 살포시 얹어주기만 하면 됩니다.
const img = new Image();
img.src = "some-image.png"; // 소스 이미지 지정
// 'repeat' 속성으로 패턴 객체 생성!
const pattern = ctx.createPattern(img, "repeat");
참고 (매우 중요!): 캔버스의
drawImage()메서드를 사용할 때와 마찬가지로, 이createPattern메서드를 호출하기 전에는 반드시 여러분이 사용하려는 소스 이미지가 브라우저에 완전히 로드(load)되어 있어야만 합니다. 그렇지 않으면 패턴이 그려지지 않거나 텅 빈 화면만 나오게 됩니다.
createPattern 예제 (A createPattern example)이번 마지막 패턴 예제에서는, 이미지를 불러와 패턴을 만든 다음 fillStyle 속성에 할당하여 배경을 꽉 채워 보겠습니다. 여기서 유일하게 주의 깊게 보셔야 할 부분은 바로 이미지 객체의 onload 핸들러(이벤트 리스너)를 사용했다는 점입니다. 이미지가 브라우저 메모리에 완벽하게 올라간 상태가 된 후에 패턴으로 지정되도록 확실하게 보장하기 위한 안전장치입니다.
<canvas id="canvas" width="150" height="150" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 패턴으로 사용할 새 이미지 객체 생성
const img = new Image();
img.src = "canvas_create_pattern.png";
// 이미지가 완벽하게 로드된 시점에 실행될 함수 지정!
img.onload = () => {
// 이미지가 다 불러와졌으니 이제 패턴을 생성합니다.
const pattern = ctx.createPattern(img, "repeat");
// 채우기 스타일에 패턴을 장착!
ctx.fillStyle = pattern;
// 캔버스 가득 사각형을 그리면 패턴으로 꽉 채워집니다.
ctx.fillRect(0, 0, 150, 150);
};
}
💡 강사의 팁: 캔버스로 게임이나 화려한 대시보드를 만들 때, 배경 텍스처(나무 재질, 흙바닥, 우주 배경 등)를 타일 형태로 채워 넣어야 하는 경우가 무조건 생깁니다. 이때
createPattern을 쓰면 성능 저하 없이 정말 손쉽게 타일 맵을 구현할 수 있답니다!
캔버스에서 그림자를 다루는 건 복잡해 보이지만, 사실 딱 네 가지 속성만 기억하시면 됩니다!
shadowOffsetX = float
그림자가 원본 객체로부터 가로(수평) 방향으로 얼마나 떨어져서 늘어날지를 나타냅니다. 다행히 이 값은 현재 캔버스에 적용된 변환 행렬(transformation matrix)의 영향을 받지 않습니다. 기본값은 0입니다.
shadowOffsetY = float
그림자가 세로(수직) 방향으로 얼마나 떨어질지를 나타냅니다. 역시 변환 행렬의 영향을 무시합니다. 기본값은 0입니다.
shadowBlur = float
그림자가 흩어지는 '흐림 효과(blur)'의 크기를 지시합니다. 주의할 점은 이 값은 특정한 '픽셀 숫자'를 의미하는 것이 아니며(가우시안 블러 공식의 파라미터로 쓰임), 현재 변환 행렬에 의해 스케일이 커지거나 작아지지 않습니다. 기본값은 0(전혀 흐리지 않고 또렷함)입니다.
shadowColor = color
그림자 효과의 색상을 지정하는 표준 CSS 색상 값입니다. 기본값은 '완전히 투명한 검은색(fully-transparent black)'이므로, 색을 지정하지 않으면 그림자가 안 보입니다.
shadowOffsetX와 shadowOffsetY 속성은 그림자가 객체로부터 X와 Y 축 방향으로 도대체 얼마나 멀리 뻗어나갈지를 정합니다. 재미있는 점은 이 값들이 현재 적용된 스케일(확대)이나 회전 같은 변환 행렬을 깡그리 무시한다는 사실입니다!
만약 음수(-) 값을 주게 되면 그림자는 원본보다 위쪽이나 왼쪽으로 뻗어나가고, 양수(+) 값을 주면 아래쪽이나 오른쪽으로 내려갑니다. (기본값은 둘 다 0이라 겹쳐 있습니다.)
shadowBlur 속성은 그림자 가장자리가 얼마나 퍼지고 흐려질지를 결정합니다. 이 역시 픽셀을 의미하지 않으며, 확대/축소 변환의 영향도 받지 않습니다. (기본값 0)
마지막으로 shadowColor 속성은 CSS에서 흔히 쓰던 그 색상 값을 넣어 그림자 색을 입혀줍니다. 투명한 검은색이 기본값이기 때문에, 이 값을 불투명한 색으로 설정해야 비로소 그림자가 눈에 띄기 시작할 겁니다.
참고: 그림자는 현재 캔버스의 합성 연산(compositing operations) 모드가 기본값인
source-over일 때만 그려집니다. 합성 모드를 건드리신다면 그림자가 안 나타날 수도 있으니 주의하세요!
이 예제에서는 허공에 떠 있는 듯한 입체적인 텍스트 문자열에 아주 부드러운 그림자 효과를 입혀보겠습니다.
<canvas id="canvas" width="150" height="80" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
// 그림자 설정 세팅
ctx.shadowOffsetX = 2; // 우측으로 2 떨어짐
ctx.shadowOffsetY = 2; // 아래로 2 떨어짐
ctx.shadowBlur = 2; // 살짝 흐리게 뭉갬
ctx.shadowColor = "rgb(0 0 0 / 50%)"; // 반투명한 검은색 그림자!
// 폰트와 텍스트 세팅
ctx.font = "20px Times New Roman";
ctx.fillStyle = "Black"; // 글자 자체는 완전한 검은색
// 텍스트를 그리는 순간, 위에서 설정한 그림자가 자동으로 밑에 깔립니다.
ctx.fillText("Sample String", 5, 30);
}
이 코드에서 등장한 font 속성과 fillText 메서드는 도대체 무얼 하는 녀석일까요? 걱정 마세요! 바로 다음 챕터인 텍스트 그리기(drawing text)에서 아주 상세하게 파헤쳐 볼 예정이랍니다!
우리가 캔버스에서 경로의 속을 채우는 fill 메서드를 사용할 때 (또는 clip을 통해 마스크를 씌우거나 isPointInPath로 특정 점이 경로 안에 있는지 수학적으로 판별할 때), 어떤 점이 경로의 '내부(안쪽)'인지 아니면 '외부(바깥쪽)'인지를 판단하여 색을 칠할지 말지 결정하는 알고리즘을 추가 옵션으로 제공할 수 있습니다.
단순한 네모난 상자라면 안과 밖이 명확하겠지만, 별 모양처럼 선이 자기 자신을 마구 교차(intersect)하는 꼬인 경로이거나, 여러 경로가 러시아 인형처럼 중첩(nested)되어 있는 상황에서는 이 규칙이 정말 유용하고 중요하게 쓰입니다.
알고리즘에는 다음 두 가지 값이 가능합니다:
nonzero
논제로 와인딩 규칙(non-zero winding rule)입니다. 아무것도 입력하지 않았을 때 적용되는 기본(default) 규칙입니다.
evenodd
짝홀수 와인딩 규칙(even-odd winding rule)입니다.
이 두 규칙의 수학적 원리가 궁금하시다면 링크를 눌러보셔도 좋지만, 간단히 요약하자면!
nonzero(기본값)는 선이 그려진 '방향(시계방향/반시계방향)'까지 꼼꼼히 따져서 겹치는 부분을 한 덩어리로 보고 모두 채워버립니다.evenodd는 겹치는 횟수가 홀수 번(1번, 3번) 겹치면 '안쪽'이라 판단해 색을 칠하고, 짝수 번(2번, 4번) 겹치면 '바깥쪽'이라 판단해 구멍을 뻥 뚫어버리는 재밌는 녀석입니다.이 예제에서는 중첩된 두 개의 원을 그린 다음 evenodd 규칙을 써서 도넛 모양을 만들어 보겠습니다!
<canvas id="canvas" width="100" height="100" role="presentation"></canvas>
function draw() {
const ctx = document.getElementById("canvas").getContext("2d");
ctx.beginPath();
// 바깥쪽 큰 원을 그립니다. (반지름 30)
ctx.arc(50, 50, 30, 0, Math.PI * 2, true);
// 안쪽에 작은 원을 겹쳐서 그립니다. (반지름 15)
ctx.arc(50, 50, 15, 0, Math.PI * 2, true);
// 'evenodd' 규칙으로 채우기를 실행합니다!
ctx.fill("evenodd");
}
안쪽 원이 위치한 곳은 바깥쪽 원과 두 번(짝수 번) 겹치게 되므로, evenodd 규칙에 의해 색이 칠해지지 않고 뻥 뚫린 도넛(O 링) 모양이 완성되는 것을 확인하실 수 있습니다!
자, 지금까지 캔버스의 기초적인 도형 그리기부터 스타일 적용하기까지 꽤 많은 내용을 달려왔습니다. 정말 수고 많으셨습니다! 이제 이전 내용이 가물가물하시다면 이전(도형 그리기)으로 돌아가 복습하시거나, 준비가 되셨다면 다음 단계인 다음(텍스트 그리기) 챕터로 호기롭게 넘어가 볼까요?