tdc
tdc

Reputation: 5464

How can I rotate a segment of <canvas>, not the entire element?

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

Answers (2)

user1693593
user1693593

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).

Clock snapshot

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:

  • Get their angles based on time (milliseconds included in this example for smooth animation)
  • Render the lines based on angles

That's it.

Demo

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):

Demo

Clock with face as self-rendered background

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

markE
markE

Reputation: 105015

You can use context.save & context.restore to temporarily rotate part of your clock (like the second hand)

The idea is:

  • save the unrotated context: context.save();
  • rotate to the desired angle: context.rotate(angle);
  • restore the context to its unrotated state for further drawing: context.restore();

A Demo: http://jsfiddle.net/m1erickson/PjZz3/

enter image description here

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

Related Questions