5

I was trying to do a perspective grid on my canvas and I've changed the function from another website with this result:

    function keystoneAndDisplayImage(ctx, img, x, y, pixelHeight, scalingFactor) {
    var h = img.height,
            w = img.width,

            numSlices = Math.abs(pixelHeight),

            sliceHeight = h / numSlices,

            polarity = (pixelHeight > 0) ? 1 : -1,
            heightScale = Math.abs(pixelHeight) / h,

            widthScale = (1 - scalingFactor) / numSlices;

    for(var n = 0; n < numSlices; n++) {

        var sy = sliceHeight * n,
                sx = 0,
                sHeight = sliceHeight,
                sWidth = w;

        var dy = y + (sliceHeight * n * heightScale * polarity),
                dx = x + ((w * widthScale * n) / 2),
                dHeight = sliceHeight * heightScale,
                dWidth = w * (1 - (widthScale * n));

        ctx.drawImage(img, sx, sy, sWidth, sHeight,
                dx, dy, dWidth, dHeight);
    }
}

It creates almost-good perspective grid, but it isn't scaling the Height, so every square has got the same height. Here's a working jsFiddle and how it should look like, just below the canvas. I can't think of any math formula to distort the height in proportion to the "perspective distance" (top). I hope you understand. Sorry for language errors.

Any help would be greatly appreciated

Regards
sinsuren
  • 1,745
  • 2
  • 23
  • 26
VixinG
  • 1,387
  • 1
  • 17
  • 31
  • If you want a perspective correct grid then there is a lot of 3D math involved. The most important construct is a [Matrix](https://en.wikipedia.org/wiki/Transformation_matrix). If you don't want to build your own 3D engine you can use an existing one like [three.js](http://threejs.org/). Look at the source from [this](http://threejs.org/examples/canvas_performance.html) example. – bitWorking Jul 22 '13 at 23:48
  • The only 3D thing I want in my canvas is that grid. Is there any way to do that without serious 3D math? I only need to add something what changes the distance between horizontal lines every row (something like that). – VixinG Jul 22 '13 at 23:54
  • Any ideas? I found nothing. – VixinG Jul 23 '13 at 16:03
  • what's wrong with using an image as background? – bitWorking Jul 23 '13 at 16:51
  • I'm learning, I like to know how to do such things like full perspective grid. – VixinG Jul 23 '13 at 19:40

1 Answers1

14

There is sadly no proper way besides using a 3D approach. But luckily it is not so complicated.

The following will produce a grid that is rotatable by the X axis (as in your picture) so we only need to focus on that axis.

To understand what goes on: We define the grid in Cartesian coordinate space. Fancy word for saying we are defining our points as vectors and not absolute coordinates. That is to say one grid cell can go from 0,0 to 1,1 instead of for example 10,20 to 45, 45 just to take some numbers.

At the projection stage we project these Cartesian coordinates into our screen coordinates.

The result will be like this:

snapshot 3d grid

ONLINE DEMO

Ok, lets dive into it - first we set up some variables that we need for projection etc:

fov = 512,         /// Field of view kind of the lense, smaller values = spheric
viewDist = 22,     /// view distance, higher values = further away
w = ez.width / 2,  /// center of screen
h = ez.height / 2,
angle = -27,       /// grid angle
i, p1, p2,         /// counter and two points (corners)
grid = 10;         /// grid size in Cartesian

To adjust the grid we don't adjust the loops (see below) but alter the fov and viewDist as well as modifying the grid to increase or decrease the number of cells.

Lets say you want a more extreme view - by setting fov to 128 and viewDist to 5 you will get this result using the same grid and angle:

enter image description here

The "magic" function doing all the math is as follows:

function rotateX(x, y) {

    var rd, ca, sa, ry, rz, f;

    rd = angle * Math.PI / 180; /// convert angle into radians
    ca = Math.cos(rd);
    sa = Math.sin(rd);

    ry = y * ca;   /// convert y value as we are rotating
    rz = y * sa;   /// only around x. Z will also change

    /// Project the new coords into screen coords
    f = fov / (viewDist + rz);
    x = x * f + w;
    y = ry * f + h;

    return [x, y];
}

And that's it. Worth to mention is that it is the combination of the new Y and Z that makes the lines smaller at the top (at this angle).

Now we can create a grid in Cartesian space like this and rotate those points directly into screen coordinate space:

/// create vertical lines
for(i = -grid; i <= grid; i++) {
    p1 = rotateX(i, -grid);
    p2 = rotateX(i, grid);
    ez.strokeLine(p1[0], p1[1], p2[0], p2[1]); //from easyCanvasJS, see demo
}

/// create horizontal lines
for(i = -grid; i <= grid; i++) {
    p1 = rotateX(-grid, i);
    p2 = rotateX(grid, i);
    ez.strokeLine(p1[0], p1[1], p2[0], p2[1]);
}

Also notice that position 0,0 is center of screen. This is why we use negative values to get out on the left side or upwards. You can see that the two center lines are straight lines.

And that's all there is to it. To color a cell you simply select the Cartesian coordinate and then convert it by calling rotateX() and you will have the coordinates you need for the corners.

For example - a random cell number is picked (between -10 and 10 on both X and Y axis):

c1 = rotateX(cx, cy);         /// upper left corner
c2 = rotateX(cx + 1, cy);     /// upper right corner
c3 = rotateX(cx + 1, cy + 1); /// bottom right corner
c4 = rotateX(cx, cy + 1);     /// bottom left corner

/// draw a polygon between the points
ctx.beginPath();
ctx.moveTo(c1[0], c1[1]);
ctx.lineTo(c2[0], c2[1]);
ctx.lineTo(c3[0], c3[1]);
ctx.lineTo(c4[0], c4[1]);
ctx.closePath();

/// fill the polygon
ctx.fillStyle = 'rgb(200,0,0)';
ctx.fill();

An animated version that can help see what goes on.

  • 1
    EasyCanvas is very powerful, I saw the documentation. I'll use it with a credit :) Also, one question: Script is creating the grid by adding cells on both sides, that's why it's rotating by X on the middle of the grid. Is there any way to rotate in the same way, but instead of middle, to rotate by last horizontal line? I'm trying to achieve that right now. – VixinG Jul 23 '13 at 21:36
  • 1
    @VixinG Certainly. Just "translate" the grid up, ie. add cells from -20 to 0 then 0 will be pivot point, or the base of rotation. If you want to move the whole grid after the rotation just add a value to produced Y value from rotateX(). –  Jul 23 '13 at 22:04
  • @Ken-AbdiasSoftware Yes, I translated it already by myself. Thanks again, have a wonderful day! – VixinG Jul 23 '13 at 22:17
  • 3
    404 links to jsfiddle – rofrol Jun 23 '21 at 19:20