8

I'm trying to make a sort of a camera in HTML5 Canvas using 2D rendering context. As you see in the picture I've drawn below, here's what I'm trying to achieve:

  1. Say that the black one is the eye of the camera, I want it to be able to move around (edit:) WITH the canvas (as the green arrows in the picture) and able to look like AS IF it is travel around the objects, like the red one (I believe this is parallax stuff).
  2. Whenever I travel around the objects, when I make a rotation of the camera, I want it to rotate by the center of the camera (see the blue rotation).

I have done this up to where the red box can rotate on the center of the camera whenever I move the camera around, [EDIT] and here's a simplified a example:

*Within the requestAnimationFrame (game loop)*

...

ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);

ctx.save();
    // draw camera eye.
    ctx.lineWidth = 3;
    ctx.fillStyle = "black";
    ctx.beginPath();
    ctx.moveTo(ctx.canvas.width / 2 - 50, ctx.canvas.height / 2);
    ctx.lineTo(ctx.canvas.width / 2 + 50, ctx.canvas.height / 2);
    ctx.moveTo(ctx.canvas.width / 2, ctx.canvas.height / 2 - 50);
    ctx.lineTo(ctx.canvas.width / 2, ctx.canvas.height / 2 + 50);
    ctx.stroke();

    // Rotate by camera's center.
    ctx.translate(ctx.canvas.width / 2, ctx.canvas.height / 2);
    ctx.rotate(worldRotate);
    ctx.translate(worldPos.x, worldPos.y);

    //
    // ADD WORLD ENTITIES BELOW to be viewed by the camera.
    //

    ctx.fillStyle = "red";
    ctx.fillRect(-5, -5, 10, 10);
ctx.restore();

...

(EDIT: changed transform object into ctx to further simplify the code.)

variable worldRotate and worldPos are changeable via input for a test.

EDIT: Here's the real live example with the problem as drawn to make it even clearer on what I'm trying to achieve.

Try moving it to right or left and see what happens. And try to rotate right the camera so that the red box aligned with x-axis, then move the camera and see how it perfectly works like a camera I wanted.

(Note: Recommended browser to test it is IE10, latest chrome, latest firefox, latest opera, and desktop safari 5+).

The problem is, when I move the camera (by changing the worldPos, makes the red box move away from the camera), make a rotation (using worldRotate), and then move the camera again, the red object always move towards the purple arrows instead of the orange arrows. I want the red box to move toward the orange arrows regardless of any rotation by the camera's center.

Edit: As far as I can tell, it is obvious that the rotation causes the translation to mess up because the rotation changes the coordinate system for the red box, but I still don't know how to deal with it, at least to let the translation goes like the orange arrows regardless of its current rotated coordinate system for the red box.

Any solution on this? Thank you.

Sample picture

  • Some code would be enlightening, but absent that sounds like you need to context.save() before doing transforms (move,rotate,etc) and then context.restore() after the transforms are complete. – markE Mar 16 '13 at 16:51
  • Pardon me markE, I've updated the code per your request to consider. Yes it's related to those stuff. –  Mar 17 '13 at 02:58

3 Answers3

4

[Edited given further clarification by OP]

OK, I have better understanding of your question now. You want to give your camera lens perspective on the objects in your world.

If your camera looks only in the X directions in relation to your world objects, you can simply use parallax. You do this by applying 2 layers to your canvas that are scrolling at different speeds in X directions. This is panorama parallax. So for every frame, draw 2 layers like this pseudo-code:

// Move the back layer
backLayerX += 3;
ctx.save();
ctx.translate(backLayerX,backLayerY);
// draw the objects in your back layer now
ctx.translate(-backLayerX,-backLayerY);
ctx.restore();

// Move the front layer faster than the back layer
frontLayerX += 10;
ctx.save();
ctx.translate(frontLayerX,frontLayerY);
// draw the objects in your front layer now
ctx.translate(frontLayerX,frontLayerY);
ctx.restore();

And if you move the camera itself in an X direction, you must also alter the speed at which the front and back layers are translated.

If your camera also looks in the Z directions (camera closer) in relation to your world objects, the process is much more complicated.

For example, if you move closer to a statue, the statue appears to get bigger—ctx.scale(2,2). The background scenery also gets bigger, but not as fast—ctx.scale(1.25,1.25).

Then if you also take a step to the left, then the left side of the statue is slightly larger than the right side of the statue—the context skews. The background skews also, but very, very slightly.

Then if you also stand on a chair (change the Y direction) you get a different kind of skewing.

Your camera can never move even with the statue because your statue is 2D—just a cardboard cutout of the statue. From the camera’s perspective, the statue will actually disappear!

I hope I’ve given you a start to your project, but a full answer is waaaaaaaaaay beyond a Stackoverflow answer—it’s a college course on computer graphics.

I do have an online tutorial recommendation for you. There is an excellent lecture on Perspective by Wolfgang Hurst. This is lecture#7 in his 13 part online college course. You can view his lecture two ways. View broadly to get the concepts involved in giving your camera perspective. Or view deeply and get the mathematical algorithms: http://www.youtube.com/watch?v=q_HA-x5AujI&list=PLDFA8FCF0017504DE

Good Luck and Learning… :)

[What follows addresses the OP's original question]

Here’s a quick tutorial on how to successfully rotate anything in canvas

The whole world rotates around an X/Y “registration point”.

You set the registration point by doing ctx.translate(X,Y) just before the ctx.rotate(angle).

After you draw your “world” objects in this rotated space, you must re-set the registration point by doing translate(-X,-Y).

So world rotation goes like this:

  • Context.save();
  • Context.translate( registrationX, registrationY );
  • Context.rotate(angle);
  • Draw stuff.
  • Context.translate( -registrationX, -registrationY );
  • Context.restore();

Here’s an excellent way of understanding rotation and registration points:

  • Get a piece of paper and draw a single letter on the page.
  • Get a pencil and hold it down at any point on the page.
  • --The pencil point is the registration point.
  • --The pencil point is the translate(X,Y).
  • Rotate the paper around the pencil point.
  • --The rotation of the paper is rotate(angle)
  • --Notice how the letter rotates as the paper moves.
  • --Your red box will rotate just like the letter (around your camera registration point)
  • Reset the paper to straight-up rotation.
  • --This “un-rotation” is like the translate(-X, -Y)
  • Move the pencil to another point on the paper
  • --Again, this is like translate(newX,newY)
  • Rotate the paper again and notice how the letter rotates in a different pattern.

Here is code and a Fiddle: http://jsfiddle.net/m1erickson/nj29x/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>

<style>
    body{ background-color: ivory; padding:30px; }
    canvas{border:1px solid red;}
</style>

<script>
$(function(){

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");

    var camX=100;
    var camY=100;
    var camRadius=40;
    var camDegrees=0;

    draw();

    function draw(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
        drawWorldRect(75,75,30,20,0,"blue","A");
        drawWorldRect(100,100,30,20,15,"red","B");
        drawCamera();
    }

    function drawWorldRect(x,y,width,height,angleDegrees,fillstyle,letter){
        var cx=x+width/2;
        var cy=y+height/2;
        ctx.save();
        ctx.translate(cx, cy);
        ctx.rotate( (Math.PI / 180) * angleDegrees);
        ctx.fillStyle = fillstyle;
        ctx.fillRect(-width/2, -height/2, width, height);
        ctx.fillStyle="white";
        ctx.font = '18px Verdana';
        ctx.fillText(letter, -6, 7);
        ctx.restore();
    }


    function drawCamera(){
        ctx.save();
        ctx.lineWidth = 3;
        ctx.beginPath();
        // draw camera cross-hairs
        ctx.strokeStyle = "#dddddd";
        ctx.moveTo(camX,camY-camRadius);
        ctx.lineTo(camX,camY+camRadius);
        ctx.moveTo(camX-camRadius,camY);
        ctx.lineTo(camX+camRadius,camY);
        ctx.arc(camX, camY, camRadius, 0 , 2 * Math.PI, false);
        ctx.stroke();
        // draw camera site
        ctx.fillStyle = "#222222";
        ctx.beginPath();
        ctx.arc(camX, camY-camRadius, 3, 0 , 2 * Math.PI, false);
        ctx.fill()
        ctx.restore();
    }

    function rotate(){
        ctx.save();
        ctx.translate(camX,camY);
        ctx.rotate(Math.PI/180*camDegrees);
        ctx.translate(-camX,-camY);
        draw();
        ctx.restore();
    }

    $("#rotateCW").click(function(){ camDegrees+=30; rotate(); });
    $("#rotateCCW").click(function(){ camDegrees-=30; rotate(); });
    $("#camUp").click(function(){ camY-=20; draw(); });
    $("#camDown").click(function(){ camY+=20; draw(); });
    $("#camLeft").click(function(){ camX-=20; draw(); });
    $("#camRight").click(function(){ camX+=20; draw(); });

}); // end $(function(){});
</script>

</head>

<body>

    <canvas id="canvas" width=400 height=300></canvas><br/>
    <button id="camLeft">Camera Left</button>
    <button id="camRight">Camera Right</button>
    <button id="camUp">Camera Up.</button>
    <button id="camDown">Camera Down</button><br/>
    <button id="rotateCW">Rotate Clockwise</button>
    <button id="rotateCCW">Rotate CounterClockwise</button>

</body>
</html>
markE
  • 102,905
  • 11
  • 164
  • 176
  • Hi markE, thank you for your answer and I appreciate that made a code for it. I believe your answer is closer to what I'm trying to achieve, but I think I haven't made it much clearer with the code so I've made a real live example of the snippets here so you can get the idea of what the camera is: https://dl.dropbox.com/u/53141171/samples/stackoverflow/index.html ... (I've also updated the question with the link). Try moving it to right or left and see what happens. And try to rotate right the camera so that the red box aligned with x-axis and see how it perfectly works like a camera I wanted. –  Mar 18 '13 at 08:19
  • Hi again markE, I've also rephrased and updated the point no. 1 of my question; the camera should move WITH the canvas, so it means its a parallax where the world entities (which contains the red box) should move around instead, and the pivot would be the center of the camera. I think this is what has confuses everyone, sorry about this. –  Mar 18 '13 at 09:12
  • Hi, I appreciate your understanding and effort in answering my question, and I think your answer is really getting into what I've been struggle about. Your answer is obviously a great start for me (and others who want to solve this problem) therefore I upvoted your answer first. Thanks markE. :) –  Mar 18 '13 at 16:29
  • About the scale, I've achieved it before the rotation issue using the same code as I've posted, its just that instead of transform.rotate() I do transform.scale(). Since scale doesn't care about angles, it works very well that the pivot stays on center of the camera even when I move it around. But of course its just a scale instead of real Z parameter, which I don't really need it for now; I need the scale for a matter of camera effects, not 3D. Once again thanks. –  Mar 18 '13 at 16:30
2

The x and y points to move the center of the world to are calculated using the tangent of the angle of rotation. I have an example exhibiting the concept, it is not a final solution, but a starting point you can work from. Think of it as as a "rough cut" (don't have time to polish it completely). For instance as you can see, the pivot point of rotation is not moved to the point directly beneath the camera lenses, so the rotation doesn't occur directly beneath the camera.

Here is a diagram explaining how the geometry / trigonometry works:

enter image description here

Here is the code snippets and a link to the fully functional jsfiddle. The keyboard commands are the same as in your example.

http://jsfiddle.net/ricksuggs/X8rYF/

    //s (down)
    if (keycode === 83) {

        event.preventDefault();
        canvas.getContext('2d').clearRect(-200,-200,400,400);
        canvas.getContext('2d').translate(2*Math.tan(rotationSum), -2); 
        drawGrid();
        drawCamera();

    }

    // <-- (rotate left)
    if (keycode === 37) {

        event.preventDefault();
        canvas.getContext('2d').clearRect(-200,-200,400,400);
        rotationSum += rotation;
        console.log('rotationSum: ' + rotationSum);
        canvas.getContext('2d').rotate(-rotation);        
        drawGrid();
        drawCamera();

    }
Rick Suggs
  • 1,582
  • 1
  • 15
  • 30
  • Congratulations ricksuggs, you earned it. I never thought tan is working on this as I've been messing with this trigonometry thingy and it never worked out (Simply because my math is terrible). Thanks for the explanation and the live example, may this also helps others too. –  Mar 26 '13 at 15:02
  • Oh my I was too early on my judge, but still among the answers you're the closest, so let me help you straighten it up. Is there any way that your grid to keep rotating based on the middle of the crosshair even when it moves around? because the grid shown is rotating based on its own pivot instead the camera center (the crosshair). it's based on the 2nd point of the question, Thanks. –  Mar 26 '13 at 15:10
  • As I mentioned in the first paragraph, the pivot point of rotation is not moved to the point directly beneath the camera lens...yet. If I have time I will modify the solution to correct the issue. – Rick Suggs Mar 26 '13 at 15:51
  • Ah you did mention that, my bad. You already got my upvote for getting this far. Please take your time. :) –  Mar 26 '13 at 15:57
0

The order should be like this:

Ctx.save()
Ctx.translate(x, y) //to the center (Or top left of object to rotate)
Ctx.rotates(radian)
Ctx.translate(-x, -y) //important to move the context back!
Ctx.draw(...)
Ctx.restore()
Jarrod
  • 9,349
  • 5
  • 58
  • 73
  • I've tried the code you posted but when I make a dynamic translation the result goes the same as drawn. I've updated my question with code to make it a little bit clearer on what I've done. –  Mar 17 '13 at 02:59
  • It doesn't look like you're translating back the negative amount you made in the first translation to reset the context. – Jarrod Mar 17 '13 at 03:33
  • Based on my code, If what you mean is I should put transform.translate(-ctx.canvas.width / 2, -ctx.canvas.height / 2); after the transform.rotate(worldRotate), then it only moves the red box to the top left (which is of course half of the canvas size), and the problem still persist; the red box moves toward the purple arrows, even though its still rotates on the center of the camera. Maybe you can suggest me with code of where I should make the translation of the negative amount as solution based on my code? Thanks. –  Mar 17 '13 at 04:47