Html Canvas 로 그려본 아날로그 시계

Canvas 기본 사용 방법

Canvas 아날로그 시계

Canvas 를 활용하여 구성할 수 있는 다양한 모듈이 있습니다.
Canvas 는 Web 에서 Graphics 자원을 어느정도 사용할 수 있다는 장점이 있습니다.
일반적이진 않지만, Web 화면을 Application 처럼 구성할 수 있다는 장점 때문에 개인적으로 선호하는 Web 모듈 이기도 합니다.

시계는 Canvas 를 어떻게 활용할 수 있는지 확인해 볼 수 있는 좋은 소품이란 생각이 들어 간단히 구성해 보았습니다.
초침의 삼각형, 원에서 표현할 위치 등을 위해서는, 간단한 삼각함수를 활용하여야 하기 때문에 정리하기도 좋을것 같아서 선택해 보았습니다.

결과는 이곳 에서 확인해 보실 수 있습니다. Clock

시계만들기

시계 외곽 만들기

  1. 시계 외곽은 원입니다.

원을 구성하기 위해서 반지름과 , 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();
 	}
  1. 소스에 대한 간단 설명

setCurrentOptions 함수는 무엇인가를 그릴때 채우기색상과, 그리는 색, 그리고 기본적인 설정을 유틸리티 함수 처럼 구성한 것입니다.
더 detail 하게 구성할 수 있으나, 일단 시계를 그리는 용도 정도로 구성한 내용입니다.
참고로 IE 에서도 작동하도록 작성하였습니다.
save, retore => beginPath, closePath 구문이 쌍으로 구성되어 있습니다.
함수를 요청한 상태를 그대로 유지해 주기 위해 설정을 되돌리는 역활을 담당합니다.

시간을 알려주는 숫자 표현하기

  1. 시간을 알려 주는 숫자는 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();
 	}
  1. 소스에 대한 간단 설명

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시에서 시작해서 아래로 진행하는 숫자를 표현하기 위한 부분입니다.

시침만들기

  1. 시침은 분, 초의 진행에 영향을 받습니다.

시간을 알려 주는 시침은 한시간이 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();
 	}
  1. 소스에 대한 간단 설명
 let rd = (((h-3))*30+m/2+s/120)*Math.PI/180;

앞서 설명한 시, 분, 초 에따른 영향과 변경 각도를 구하는 부분입니다.
나머지는 tx, ty 로 위치를 구성하고, 꾸미기 위한 동그라미 그리는 부분입니다.

분침만들기

  1. 분침은 초의 진행에 영향을 받습니다.

한번 회전할 때 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();
 	}
  1. 소스에 대한 간단 설명
 let rd = ((m-15)*6+s/10)*Math.PI/180;

(m - 15) 는 12시 부터 시작하기 위해서 15분 (90 도 왼쪽으로 시작지점을 옮김) 이동하였습니다.
분에 의해 회전한 각과 초의 의해 회전한 각을 더해서 회전각을 구성합니다.
해당 각( radian ) 에 cos, sin 값으로 위치를 만들고 분침을 그려 주고 있습니다.

초침만들기

  1. 초침의 화살표는 방향이 중요합니다.

초침은 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();
 	}
  1. 소스에 대한 간단 설명

초침이 회전한 양입니다.

     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 를 염두에 두고 최신 문법을 사용하지 않았습니다. ^^

 Share!