Canvas 아날로그 시계
Canvas 를 활용하여 구성할 수 있는 다양한 모듈이 있습니다.
Canvas 는 Web 에서 Graphics 자원을 어느정도 사용할 수 있다는 장점이 있습니다.
일반적이진 않지만, Web 화면을 Application 처럼 구성할 수 있다는 장점 때문에 개인적으로 선호하는 Web 모듈 이기도 합니다.
시계는 Canvas 를 어떻게 활용할 수 있는지 확인해 볼 수 있는 좋은 소품이란 생각이 들어 간단히 구성해 보았습니다.
초침의 삼각형, 원에서 표현할 위치 등을 위해서는, 간단한 삼각함수를 활용하여야 하기 때문에 정리하기도 좋을것 같아서 선택해 보았습니다.
결과는 이곳 에서 확인해 보실 수 있습니다.
시계만들기
시계 외곽 만들기
- 시계 외곽은 원입니다.
원을 구성하기 위해서 반지름과 , javascript 함수중 Math.sin, Math.cos 을 사용하면 원을 그릴 수 있습니다.
물론 canvas 에서 제공해 주는 arc 함수를 사용하면 더 쉽게 원을 그릴 수 있습니다.
arc 함수에서 시작각도, 종료각도 입력값은 radian 입니다. 호의 길이를 기준으로 설정한 단위입니다.
(일반각도 x Math.PI) / 180 이면 radian 값을 얻을 수 있고, (radian x 180)/Math.PI 면 우리가 일반적으로 사용하는 각도를 얻을 수 있습니다.
외곽을 구하는 코드 입니다.
function setCurrentOptions(ctx, sOptions) {
if ( !ctx ) {
return;
}
if ( !sOptions ) {
return;
}
ctx.strokeStyle = sOptions.strokeStyle;
ctx.lineWidth = sOptions.lineWidth;
ctx.fillStyle = sOptions.fillStyle;
ctx.lineCap = sOptions.lineCap ? sOptions.lineCap : 'round';
ctx.joinCap = sOptions.joinCap ? sOptions.joinCap : 'round';
}
function drawClockLayers(ctx,cx,cy,radious, sOptions ) {
ctx.save();
ctx.beginPath();
setCurrentOptions(ctx, sOptions);
ctx.arc(cx,cy,radious,0, Math.PI*2);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
- 소스에 대한 간단 설명
setCurrentOptions 함수는 무엇인가를 그릴때 채우기색상과, 그리는 색, 그리고 기본적인 설정을 유틸리티 함수 처럼 구성한 것입니다.
더 detail 하게 구성할 수 있으나, 일단 시계를 그리는 용도 정도로 구성한 내용입니다.
참고로 IE 에서도 작동하도록 작성하였습니다.
save, retore => beginPath, closePath 구문이 쌍으로 구성되어 있습니다.
함수를 요청한 상태를 그대로 유지해 주기 위해 설정을 되돌리는 역활을 담당합니다.
시간을 알려주는 숫자 표현하기
- 시간을 알려 주는 숫자는 30도 각도로 움직입니다.
원은 360도 입니다. radian 으로 표현 하면 ( 2 * Math.PI ) 입니다.
360 / 12 = 30 각 시간의 간격은 30 도 입니다.
Math.sin(0) = 0, Math.cos(0) = 1 입니다. 시작점이 3시 부터 입니다.
Math.sin(90) = 1, Math.cos(90) = 0 입니다. 여기서 90도는 표현을 위한 값이고 실제로는 (Math.PI x 90) / 180 의 radian 값입니다. 값은 논리적으로는 12시 방향이지만, canvas 좌표는 왼쪽 상단이 0, 0 으로 y 좌표가 아래방향입니다. 굳이 변화가 필요없어, 실질적으로 가리키는 방향은 6시 방향입니다.
위 내용을 바탕으로 구성한 코드 입니다.
function drawClockTextArea(ctx, cx, cy, txtRadious, markRadious, innerLineWidth ) {
ctx.save();
ctx.beginPath();
ctx.font = "12px consolas";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.lineCap = "butt";
ctx.lineWidth = 1;
ctx.beginPath();
for ( let i = 0 ; i < 12 ; i++ ) {
ctx.lineWidth = 1;
ctx.strokeStyle = "#000080";
let rd = i*Math.PI/6;
let tx = txtRadious*Math.cos(rd)+cx;
let ty = txtRadious*Math.sin(rd)+cy;
let numStr = ((3+i)%12);
if ( numStr == 0 ) {
numStr = 12;
}
ctx.strokeText(numStr, tx,ty);
ctx.lineWidth = 2;
ctx.strokeStyle = "#00FFFF";
let tsx = (markRadious)*Math.cos(rd)+cx;
let tex = (markRadious+innerLineWidth)*Math.cos(rd)+cx;
let tsy = (markRadious)*Math.sin(rd)+cy;
let tey = (markRadious+innerLineWidth)*Math.sin(rd)+cy;
ctx.moveTo(tsx,tsy);
ctx.lineTo(tex,tey);
ctx.stroke();
}
ctx.closePath();
ctx.restore();
}
- 소스에 대한 간단 설명
let rd = i x Math.PI/6;
라디안 값을 만드는 과정입니다. ( 각 x Math.P ) / 180 인데 매 30도마다 변경되기 때문에 약분하여 구성하였습니다.
let tx = txtRadious x Math.cos(rd)+cx; 해당 각도의 x 를 구하는 값입니다. txtRadious 는 반지름이고, cx 는 중심점이니 cos 값으로 x 위치를 가져왔다 정도로 생각하시면 될것 같습니다. let ty = txtRadious x Math.sin(rd)+cy; => y 좌표 위치입니다.
let numStr = ((3+i)%12);
if ( numStr == 0 ) {
numStr = 12;
}
숫자를 표현한 부분입니다. 3시에서 시작해서 아래로 진행하는 숫자를 표현하기 위한 부분입니다.
시침만들기
- 시침은 분, 초의 진행에 영향을 받습니다.
시간을 알려 주는 시침은 한시간이 30도 각도로 움직입니다.
아날로그시계는 분, 초의 변화가 시침에 영향을 미치게 됩니다.
60분이 한시간이니 분은 60분동안 30도 정도 회전하게 됩니다. 즉 30/60 => 1/2 x 분 정도의 영향을 끼칩니다.
3600 초가 한시간이니 3600 초가 지나야 30도 회전 합니다. 30/3600 => 1/120 x 초 정도의 영향을 끼칩니다.
코드 입니다.
function drawClockHour(ctx, cx, cy, radious , now, sOptions) {
ctx.save();
setCurrentOptions(ctx, sOptions);
let s = now.getSeconds();
let m = now.getMinutes();
let h = now.getHours();
let rd = (((h-3))*30+m/2+s/120)*Math.PI/180;
let tx = Math.cos(rd)*radious+cx
let ty = Math.sin(rd)*radious+cy;
ctx.beginPath();
ctx.moveTo(cx,cy);
ctx.lineTo(tx,ty);
ctx.stroke();
ctx.closePath();
ctx.lineWidth = 2;
ctx.fillStyle = "#00FFFF";
ctx.strokeStyle = "#0000FF";
ctx.beginPath();
ctx.arc(cx,cy,6,0,Math.PI*2);
ctx.fill();
ctx.closePath();
ctx.beginPath();
ctx.arc(cx,cy,6,0,Math.PI*2);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
- 소스에 대한 간단 설명
let rd = (((h-3))*30+m/2+s/120)*Math.PI/180;
앞서 설명한 시, 분, 초 에따른 영향과 변경 각도를 구하는 부분입니다.
나머지는 tx, ty 로 위치를 구성하고, 꾸미기 위한 동그라미 그리는 부분입니다.
분침만들기
- 분침은 초의 진행에 영향을 받습니다.
한번 회전할 때 60분이 걸리기 때문에 90도 회전할 때 15분의 시간이 필요합니다.
1분이면 6/360 => 1/6 이니, 6도 회전하게 됩니다. 1분이 60초이니 1초에 6/60 => 1/10 도 회전합니다.
코드 입니다.
function drawClockMinute(ctx, cx, cy, radious , now, sOptions) {
ctx.save();
setCurrentOptions(ctx, sOptions);
let s = now.getSeconds();
let m = now.getMinutes();
let rd = ((m-15)*6+s/10)*Math.PI/180;
let tx = Math.cos(rd)*radious+cx
let ty = Math.sin(rd)*radious+cy;
ctx.beginPath();
ctx.moveTo(cx,cy);
ctx.lineTo(tx,ty);
ctx.stroke();
ctx.closePath();
ctx.restore();
}
- 소스에 대한 간단 설명
let rd = ((m-15)*6+s/10)*Math.PI/180;
(m - 15) 는 12시 부터 시작하기 위해서 15분 (90 도 왼쪽으로 시작지점을 옮김) 이동하였습니다.
분에 의해 회전한 각과 초의 의해 회전한 각을 더해서 회전각을 구성합니다.
해당 각( radian ) 에 cos, sin 값으로 위치를 만들고 분침을 그려 주고 있습니다.
초침만들기
- 초침의 화살표는 방향이 중요합니다.
초침은 60초에 360 도를 회전하니, 시침 분침에 비해서 간단히 계산 할 수 있습니다.
다만, 화살표가 각 방향에 따라 자연스럽게 보이기 위해서는 약간씩 회전을 구성해 주어야 합니다.
여기서는 중심점을 이동한 후 화살표 방향을 회전하는 방법으로 구성해 보았습니다.
코드 입니다.
function drawArrowPoints(ctx, tx, ty, radians,pw,ph,sOptions ) {
ctx.save();
setCurrentOptions(ctx, sOptions);
ctx.beginPath();
ctx.translate(tx,ty);
ctx.rotate(radians);
ctx.moveTo(ph/2,0);
ctx.lineTo(-ph/2,-pw/2);
ctx.lineTo(-ph/2,pw/2);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
}
function drawClockSecond(ctx, cx, cy, radious ,now, sOptions) {
ctx.save();
setCurrentOptions(ctx, sOptions);
let s = now.getSeconds();
let rd = ((s-15)*Math.PI*6)/180;
let tx = Math.cos(rd)*radious+cx
let ty = Math.sin(rd)*radious+cy;
ctx.beginPath();
ctx.moveTo(cx,cy);
ctx.lineTo(tx,ty);
ctx.stroke();
ctx.closePath();
if ( !sOptions ) {
sOptions = getDefaultOptions();
}
sOptions.strokeStyle = "blue";
sOptions.fillStyle = "#008080";
sOptions.lineWidth = 1;
sOptions.lineCap = 'butt';
sOptions.joinCap = 'butt';
drawArrowPoints(ctx, tx,ty, rd, 6,12, sOptions);
ctx.restore();
}
- 소스에 대한 간단 설명
초침이 회전한 양입니다.
let rd = ((s-15)*Math.PI*6)/180;
화살표 모양을 그리기 위해 이동후, 다시 회전한 내용입니다.
ctx.translate(tx,ty);
ctx.rotate(radians);
전체를 구성하는 부분입니다.
function makeClocks(ctx, cw, ch, radious) {
let cx = cw/2;
let cy = ch/2;
ctx.clearRect(0,0,cw,ch);
var innerLineWidth = 12;
var outerLineWidth = 4;
var sOptions = getDefaultOptions();
sOptions.strokeStyle = "#008080";
sOptions.lineWidth = innerLineWidth;
drawClockLayers(ctx, cx, cy, radious, sOptions);
sOptions.lineWidth = outerLineWidth;
sOptions.strokeStyle = "#000080";
drawClockLayers(ctx, cx, cy, (radious+innerLineWidth/2+outerLineWidth+1), sOptions);
var markRadious = radious-innerLineWidth/2;
var txtRadious = markRadious-8;
drawClockTextArea(ctx, cx,cy, txtRadious, markRadious, innerLineWidth);
var now = new Date();
sOptions.strokeStyle = "#008080";
sOptions.lineCap = 'butt';
sOptions.joinCap = 'butt';
sOptions.lineWidth = 2;
drawClockSecond(ctx, cx, cy, txtRadious-4, now, sOptions);
sOptions.strokeStyle = "#000080";
sOptions.lineWidth = 4;
drawClockMinute(ctx, cx, cy, txtRadious-8, now, sOptions);
sOptions.strokeStyle = "#000080";
sOptions.lineWidth = 8;
drawClockHour(ctx, cx, cy, txtRadious-12, now, sOptions);
}
function main() {
const width = 500;
const height = 500;
const canvas = CanvasUtils.makeCanvasObject("testID", undefined, width, height);
const ctx = canvas.getContext("2d");
function setRepeatClocks(time) {
makeClocks(ctx, width, height, 200);
requestAnimationFrame(setRepeatClocks);
}
requestAnimationFrame(setRepeatClocks);
}
main();
앞서 언급한 함수들을 순차적으로 호출함으로써 시계를 그리고 있습니다.
requestAnimationFrame 는 animation 을 효과를 주기 위해 사용하는 내장 함수로 , 시작과 멈춤을 구성 할 수도 있습니다.
몇몇 기능은 코드를 조정해서 util 로 구성할 부분도 있고, 성능을 최적화 하기 위해 테스트가 필요할 부분도 있습니다.
IE 를 염두에 두고 최신 문법을 사용하지 않았습니다. ^^