Reputation: 5464
I'm trying to learn some of the <canvas>
API right now. I've tasked myself with creating a simple analog style clock with working clock hands (second, minute and hour).
The clock frame, face and hands are all drawn with the same canvas element. I've created a drawScene()
function which runs every second and redraws the entire clock. If you want to look more in-depth at the code I will post it into a jsbin linked at the bottom of this post.
The goal is for the drawScene()
method to call the drawClockFace()
method, which passes the current second / minute / hour to individual functions that draw the hand based on the passed in time (ie drawSecondHand(currentSecond)
).
question:
how do I rotate individual components of a canvas (ie the second hand on my clock) without rotating the entire canvas? I know I need to calculate where to draw the line from the center origin out, based on what the current second is. I'm just not sure the gemoetric calculation needed to determined "where" to draw the line.
Here's what I have so far -- note it's not super clean because I've been putzing with it. http://codepen.io/tconroy/pen/BcEbf
Upvotes: 2
Views: 2103
Reputation:
How do I rotate individual components of a canvas (ie the second hand on my clock) without rotating the entire canvas?
You can use the following approach to calculate the angles manually without rotating the canvas each time. The demos below produces a smooth running clock utilizing milliseconds (I'll show below how to drop this if not wanted).
Initially you would want to rotate the canvas -90 degrees to have 0 degrees point up and not to the right. This makes life easier in regards to the angles which will produce for example 0 degree for 12 o'clock. You can do this rotation after drawing the main face.
For each hand:
That's it.
At the core you can have a function which calculates current time into angles for the hour, minutes and seconds - this function will also get the smooth angles for "in-between" based on milliseconds (doesn't need to wait a whole second to change):
/// somewhere globally
var pi2 = Math.PI * 2;
function timeToAngles() {
var os = 1 / 60, /// mini-step
time = new Date(), /// get current time
h = time.getHours(), /// get current hour
m = time.getMinutes(), /// get current minutes
s = time.getSeconds(), /// get current seconds
ms = time.getMilliseconds(), /// get current milliseconds
sa, ma, ha; /// for calc. angles
sa = pi2 * ((s / 60) + (os * ms * 0.001)); /// second's angle
ma = pi2 * ((m / 60) + (os * s / 60)); /// minute's angle
ha = pi2 * (((h % 12) / 12) + (( 1 / 12) * m / 60)); /// hour's angle
return {
h: ha,
m: ma,
s: sa
}
}
Now it's simply a matter of feeding these angles to your render function:
(function loop() {
renderClock()
requestAnimationFrame(loop);
})();
If you want to update only per second replace rAF with (you could also remove the calculation from the timeToAngles()
but it will be microscopic in this context):
(function loop() {
setTimeout(loop, 1000);
renderClock()
})();
Then render lines based on the angles you get from current time:
function renderClock() {
var angles = timeToAngles(), /// get angles
cx = ctx.canvas.width * 0.5, /// center
cy = ctx.canvas.width * 0.5,
lh = cx * 0.5, /// length of hour's hand
lm = cx * 0.8, /// length of minute's hand
ls = cx * 0.9, /// length of second' hand
pos; /// end-point of hand
/// clear and render face
ctx.clearRect(0, 0, cx*2, cy*2);
ctx.beginPath();
ctx.arc(cx, cy, cx - ctx.lineWidth, 0, pi2);
/// hours
pos = lineToAngle(cx, cy, lh, angles.h);
ctx.moveTo(cx, cy);
ctx.lineTo(pos.x, pos.y);
/// minutes
pos = lineToAngle(cx, cy, lm, angles.m);
ctx.moveTo(cx, cy);
ctx.lineTo(pos.x, pos.y);
/// render hours and minutes
ctx.lineWidth = 5;
ctx.stroke();
ctx.beginPath();
/// seconds
pos = lineToAngle(cx, cy, ls, angles.s);
ctx.moveTo(cx, cy);
ctx.lineTo(pos.x, pos.y);
ctx.lineWidth = 2; /// create a variation for seconds hand
ctx.stroke();
}
This helper function calculates the end-point based on angle and trigonometry:
function lineToAngle(x, y, length, angle) {
return {
x: x + length * Math.cos(angle),
y: y + length * Math.sin(angle)
}
}
Additional tip for rendering the clock face:
If you are making a clock then this tip can be very helpful - instead of clearing and rendering the face each update you can instead render the face once, convert the canvas to a data-uri and set that as a background image to canvas (itself).
This way you only need to redraw the hands. Before rotating the canvas -90 degrees (as shown above):
var dots = 12, /// generate dots for each hour
dotPos = cx * 0.85, /// position of dots
step = pi2 / dots, /// calc step
a = 0; /// angle for loop
ctx.beginPath();
/// create body border
ctx.beginPath();
ctx.arc(cx, cy, cx - ctx.lineWidth - 2, 0, pi2);
ctx.fillStyle = '#000';
ctx.lineWidth = 5;
ctx.stroke();
/// color of hour dots
ctx.fillStyle = '#999';
/// draw the dots
for(; a < pi2; a += step) {
var pos = lineToAngle(cx, cy, dotPos, a);
ctx.beginPath();
ctx.arc(pos.x, pos.y, 3, 0, pi2);
ctx.fill();
}
/// create highlighted dots for every 3 hours
a = 0;
step = pi2 / 4;
ctx.fillStyle = '#777';
for(; a < pi2; a += step) {
var pos = lineToAngle(cx, cy, dotPos, a);
ctx.beginPath();
ctx.arc(pos.x, pos.y, 5, 0, pi2);
ctx.fill();
}
/// set as background
clock.style.backgroundImage = 'url(' + clock.toDataURL() + ')';
Then start the loop here.
Upvotes: 5
Reputation: 105015
You can use context.save & context.restore to temporarily rotate part of your clock (like the second hand)
The idea is:
A Demo: http://jsfiddle.net/m1erickson/PjZz3/
Example code:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" />
<script src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: white; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
ctx.strokeStyle="black"
ctx.fillStyle="ivory";
function animate() {
requestAnimFrame(animate);
// clear the canvas
ctx.clearRect(0,0,canvas.width,canvas.height);
// Draw the clock face
ctx.beginPath();
ctx.arc(150,150,50,0,Math.PI*2);
ctx.closePath();
ctx.lineWidth=3;
ctx.stroke();
ctx.fillStyle="ivory";
ctx.fill();
ctx.fillStyle="black";
ctx.fillText("12",145,115);
// Separately rotate the second hand
// using ctx.save and ctx.restore
// plus ctx.rotate
// calc the rotation angle
var d=new Date();
var radianAngle=(d.getSeconds()/60)*Math.PI*2;
ctx.save();
ctx.translate(150,150);
ctx.rotate(radianAngle);
ctx.moveTo(0,0);
ctx.lineTo(45,0);
ctx.stroke();
ctx.restore();
}
animate();
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="canvas" width=350 height=350></canvas>
</body>
</html>
Upvotes: 1