2

I'm drawing lines on an HTML canvas, and use a less precise 2d-array (representing blocks of 10x10 pixels) in which I 'draw' lines with Bresenham's algorithm to store line-ids, so I can use that array to see which line is selected.

This works, but I would like it to be more accurate - not in the 10x10 size that I use (I like that I don't exactly have to click on the line), but when I draw a representation of that array over my actual canvas, I see that there are a lot of the 10x10 blocks not filled, even though the line is crossing them:

enter image description here

Is there a better solution to this? What I want is to catch ALL grid blocks that the actual line passes through.

Jongware
  • 22,200
  • 8
  • 54
  • 100
Eindbaas
  • 945
  • 1
  • 8
  • 16

2 Answers2

6

Without seeing your code, I think you made a rounding error while filling the lookup table using the Bresenham algorithm or you scaled the coordinates before running the algorithm.

This jsFiddle shows what I came up with and the squares are perfectly aligned.

enter image description here

HTML

<canvas id="myCanvas"></canvas>

CSS

#myCanvas {
    width: 250px;
    height: 250px;
}

JavaScript

var $canvas = $("#myCanvas"),
    ctx = $canvas[0].getContext("2d");

ctx.canvas.width = $canvas.width();
ctx.canvas.height = $canvas.height();

function Grid(ctx) {
    this._ctx = ctx;
    this._lines = [];
    this._table = [];
    this._tableScale = 10;
    this._createLookupTable();
}
Grid.prototype._createLookupTable = function() {
    this._table = [];
    for (var y = 0; y < Math.ceil(ctx.canvas.height / this._tableScale); y++) {
        this._table[y] = [];
        for (var x = 0; x < Math.ceil(ctx.canvas.width / this._tableScale); x++)
            this._table[y][x] = null;
    }
};
Grid.prototype._updateLookupTable = function(line) {
    var x0 = line.from[0],
        y0 = line.from[1],
        x1 = line.to[0],
        y1 = line.to[1],
        dx = Math.abs(x1 - x0),
        dy = Math.abs(y1 - y0),
        sx = (x0 < x1) ? 1 : -1,
        sy = (y0 < y1) ? 1 : -1,
        err = dx - dy;
    while(true) {
        this._table[Math.floor(y0 / 10)][Math.floor(x0 / 10)] = line;
        if ((x0 == x1) && (y0 == y1)) break;
        var e2 = 2 * err;
        if (e2 >- dy) { err -= dy; x0 += sx; }
        if (e2 < dx) { err += dx; y0 += sy; }
    }    
};
Grid.prototype.hitTest = function(x, y) {
    var ctx = this._ctx,
        hoverLine = this._table[Math.floor(y / 10)][Math.floor(x / 10)];
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    this._lines.forEach(function(line) {
        line.draw(ctx, line === hoverLine ? "red" : "black");
    });
};
Grid.prototype.drawLookupTable = function() {
    ctx.beginPath();
    for (var y = 0; y < this._table.length; y++)
        for (var x = 0; x < this._table[y].length; x++) {
            if (this._table[y][x])
                ctx.rect(x * 10, y * 10, 10, 10);
        }
    ctx.strokeStyle = "rgba(0, 0, 0, 0.2)";
    ctx.stroke();
};
Grid.prototype.addLine = function(line) {
    this._lines.push(line);
    this._updateLookupTable(line);
};
Grid.prototype.draw = function() {
    var ctx = this._ctx;
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    this._lines.forEach(function(line) {
        line.draw(ctx);
    });
};

function Line(x0, y0, x1, y1) {
    this.from = [ x0, y0 ];
    this.to = [ x1, y1];
}
Line.prototype.draw = function(ctx, style) {
    ctx.beginPath();
    ctx.moveTo(this.from[0], this.from[1]);
    ctx.lineTo(this.to[0], this.to[1]);
    ctx.strokeStyle = style || "black";
    ctx.stroke();
};

var grid = new Grid(ctx);
grid.addLine(new Line(80, 10, 240, 75));
grid.addLine(new Line(150, 200, 50, 45));
grid.addLine(new Line(240, 10, 20, 150));
grid.draw();
grid.drawLookupTable();

$canvas.on("mousemove", function(e) {
    grid.hitTest(e.offsetX, e.offsetY);
    grid.drawLookupTable();
});
huysentruitw
  • 27,376
  • 9
  • 90
  • 133
  • I think it was me downscaling when starting the Bresenham draw, instead of downscaling only right before storing the value in the table. Thanks! – Eindbaas Jan 19 '15 at 13:14
1

Your best option is to treat the mouse-cursor-position as a small circle (f.e. with a 5px radius) and check if the line intersects with the circle.

Use the math as explained in this Q&A

JavaScript

A simple function to detect intersection would be:

function lineCircleIntersects(x1, y1, x2, y2, cx, cy, cr) {
    var dx = x2 - x1,
        dy = y2 - y1,
        a = dx * dx + dy * dy,
        b = 2 * (dx * (x1 - cx) + dy * (y1 - cy)),
        c = cx * cx + cy * cy,
        bb4ac;

    c += x1 * x1 + y1 * y1;
    c -= 2 * (cx * x1 + cy * y1);
    c -= cr * cr;
    bb4ac = b * b - 4 * a * c;

    return bb4ac >= 0;  // true: collision, false: no collision
}

See it working in this jsFiddle, but note that this function will also return true if the cursor is on the slope of the line outside [x1, y1], [x2, y2]. I'll leave this up to you :)

You can also try line-circle-collision library on github which should give you what you want.

Community
  • 1
  • 1
huysentruitw
  • 27,376
  • 9
  • 90
  • 133
  • Thanks for that example, i will try it out. It is a lot more expensive though (especially since i call it on mousemove), i liked my approach of having just a simple and quick lookup-table without any calculations in it. – Eindbaas Jan 18 '15 at 19:00
  • 1
    If I read your answer, you only have to do the calculations on `click`. I just used `mousemove` for demonstration purposes. – huysentruitw Jan 18 '15 at 19:36