2

I'm doing some work with the HTML canvas and I'm finding that when I have drawn a line I'm unable to clear or draw over it?

So what I'm doing: I have a canvas with a grid where each cell draws itself (a filled rectangle, done on page load), when the mouse is on the grid the cell the mouse is over should be outlined (draw four lines on the canvas no other redraws). When the mouse moves to a different cell the new cell should be outlined (as before) and the previous outlined cell should no longer be outlined (the intention was to redraw the cell, drawing over the lines). It's 'un-outlining' the cell that is proving to be the problem.

I assumed it worked with a painter's algorithm, that the last thing drawn on the canvas would be visible but the effects I'm getting suggest that filled shapes and lines are handled separately with lines on a higher layer?

Here's the source for a standalone page to demonstrate it:

<!DOCTYPE html>
<html>

<head>

<script type="text/javascript">


var CANVAS_DIMENSION = 200;
var CANVAS_WIDTH = 10;
var CANVAS_HEIGHT = 10;

var GRID_CELL_WIDTH = CANVAS_DIMENSION/CANVAS_WIDTH;
var GRID_CELL_HEIGHT = CANVAS_DIMENSION/CANVAS_HEIGHT;

var canvas;
var context;
var grid;
var mouseCellColumn = -1;
var mouseCellRow = -1;

function init()
{
    canvas = document.getElementById("myCanvas");

    context = canvas.getContext("2d");

    canvas.addEventListener('mousemove', updateMousePosition, false);

    grid = new Array(CANVAS_WIDTH);
    for (var i = 0; i < CANVAS_WIDTH; ++i)
    {
        grid[i] = new Array(CANVAS_HEIGHT);

        for (var j = 0; j < CANVAS_HEIGHT; ++j)
        {
            grid[i][j] = "#0000FF";
        }
    }

    renderScene()
}


function updateMousePosition(event)
{
    var initialColumn = mouseCellColumn;
    var initialRow = mouseCellRow;

    var objectPosition = findPos(this);

    var gridPosition = new Point(event.pageX - objectPosition.x, event.pageY - objectPosition.y);

    mouseCellColumn = getColumn(gridPosition.x);

    mouseCellRow = getRow(gridPosition.y);

    var cell_position = getCellPosition(mouseCellColumn, mouseCellRow);

    // outline the current cell
    drawRectangleOutlineWithColour(cell_position.x, 
                                   cell_position.y, 
                                   GRID_CELL_WIDTH, 
                                   GRID_CELL_HEIGHT, 
                                   "#FF0000");

    // if mouse has moved cell redraw
    if (((initialColumn != mouseCellColumn) ||
        (initialRow != mouseCellRow)) &&
        (initialColumn > -1) && 
        (initialRow > -1))
    {
        renderGridCell(initialColumn, initialRow);
    }

}


function renderScene()
{
    for (var i = 0; i < CANVAS_WIDTH; ++i)
    {
        for (var j = 0; j < CANVAS_HEIGHT; ++j)
        {
            renderGridCell(i, j);                
        }
    }
}


function renderGridCell(Column, Row)
{
    var position = getCellPosition(Column, Row);

    drawRectangleWithColour(position.x, 
                            position.y, 
                            GRID_CELL_WIDTH, 
                            GRID_CELL_HEIGHT, 
                            grid[Column][Row]);
}


function drawRectangleWithColour(minX, minY, width, height, colour)
{
    context.fillStyle = colour;

    context.fillRect(minX,
                     minY,
                     width,
                     height);
}


function drawRectangleOutlineWithColour(minX, minY, width, height, colour)
{
    context.strokeStyle = colour;

    context.moveTo(minX, minY);
    context.lineTo(minX + width, minY);
    context.moveTo(minX + width, minY);
    context.lineTo(minX + width, minY + height);
    context.moveTo(minX + width, minY + height);
    context.lineTo(minX, minY + height);
    context.moveTo(minX, minY + height);
    context.lineTo(minX, minY);

    context.stroke();
}


function Point(x, y)
{
    this.x = x;
    this.y = y;
}

function getColumn(xPosition)
{
    if (xPosition < 0)
    {
        xPosition = 0;
    }

    if (xPosition > CANVAS_DIMENSION)
    {
        xPosition = CANVAS_DIMENSION;
    }

    return Math.floor(xPosition/GRID_CELL_WIDTH);
}

function getRow(yPosition)
{
    if (yPosition < 0)
    {
        yPosition = 0;
    }

    if (yPosition > CANVAS_DIMENSION)
    {
        yPosition = CANVAS_DIMENSION;
    }

    return Math.floor(yPosition/GRID_CELL_HEIGHT);
}

function getCellPosition(column, row)
{
    if (row < 0)
    {
        row = 0;
    }

    if (row > CANVAS_HEIGHT)
    {
        row = CANVAS_HEIGHT - 1;
    }

    if (column < 0)
    {
        row = 0;
    }

    if (column > CANVAS_WIDTH)
    {
        column = CANVAS_WIDTH - 1;
    }

    var result = new Point(column * GRID_CELL_WIDTH, row * GRID_CELL_HEIGHT);
    return result;
}


function findPos(obj)
{
    var result = new Point(0, 0);

    if (obj.offsetParent)
    {
        do
        {
            result.x += obj.offsetLeft;
            result.y += obj.offsetTop;
        } while (obj = obj.offsetParent);
    }

    return result;
}


</script>
</head>

<body onload="init()">


<div id="test" style="width: 200px; height:200px; margin: 0px auto;">
    <canvas id="myCanvas" width="200" height="200">
    Your browser does not support the canvas element.
    </canvas>
</div>

</body>
</html>

The offending area is here:

// outline the current cell
drawRectangleOutlineWithColour(cell_position.x, 
                               cell_position.y, 
                               GRID_CELL_WIDTH, 
                               GRID_CELL_HEIGHT, 
                               "#FF0000");

// if mouse has moved cell redraw
if (((initialColumn != mouseCellColumn) ||
    (initialRow != mouseCellRow)) &&
    (initialColumn > -1) && 
    (initialRow > -1))
{
    renderGridCell(initialColumn, initialRow);
}

This has no effect, the outlines accumulate.

Some digging into canvas redraws suggests 'clearRect' but this doesn't seem to help, the outlines persist:

// outline the current cell
drawRectangleOutlineWithColour(cell_position.x, 
                               cell_position.y, 
                               GRID_CELL_WIDTH, 
                               GRID_CELL_HEIGHT, 
                               "#FF0000");

// if mouse has moved cell redraw
if (((initialColumn != mouseCellColumn) ||
    (initialRow != mouseCellRow)) &&
    (initialColumn > -1) && 
    (initialRow > -1))
{
    context.clearRect(position.x, 
                      position.y, 
                      GRID_CELL_WIDTH, 
                      GRID_CELL_HEIGHT);

    renderGridCell(initialColumn, initialRow);
}

Let's redraw the outline area specifically with a different colour?

// outline the current cell
drawRectangleOutlineWithColour(cell_position.x, 
                               cell_position.y, 
                               GRID_CELL_WIDTH, 
                               GRID_CELL_HEIGHT, 
                               "#FF0000");

// if mouse has moved cell redraw
if (((initialColumn != mouseCellColumn) ||
    (initialRow != mouseCellRow)) &&
    (initialColumn > -1) && 
    (initialRow > -1))
{
    var position = getCellPosition(initialColumn, initialRow);

    drawRectangleWithColour(position.x, 
                            position.y, 
                            GRID_CELL_WIDTH, 
                            GRID_CELL_HEIGHT, 
                            "#00FF00");
}

Nope, the grid redraws but it has no effect on the lines. Redraw the whole grid?

    renderScene();

    // outline the current cell
    drawRectangleOutlineWithColour(cell_position.x, 
                                   cell_position.y, 
                                   GRID_CELL_WIDTH, 
                                   GRID_CELL_HEIGHT, 
                                   "#FF0000");
/*
    // if mouse has moved cell redraw
    if (((initialColumn != mouseCellColumn) ||
        (initialRow != mouseCellRow)) &&
        (initialColumn > -1) && 
        (initialRow > -1))
    {
        var position = getCellPosition(initialColumn, initialRow);

        drawRectangleWithColour(position.x, 
                                position.y, 
                                GRID_CELL_WIDTH, 
                                GRID_CELL_HEIGHT, 
                                "#00FF00");
    }
*/

I can't seem to find a good explanation of how the canvas is handling this, I'm getting the same behaviour in Safari on MacOSX and Chrome and Firefox on Windows7.

Paddy
  • 79
  • 1
  • 3
  • 13

1 Answers1

4

There are two things going on here.

The first and most important is that you are never calling context.beginPath()

Every time you think you are drawing a new red outline you are actually adding to the context's current path and making it longer. The first time the context has just one rect, then it has two, then it has three, etc.

Every time you call stroke you are stroking every single rectangle you have ever drawn, because the context's current path includes them all. To remedy this you must reset the current path by calling beginPath.

The second problem is that your lines are not being drawn on perfect pixels, so when you go to erase them you will see graphical issues due to anti-aliasing. To fix this you need to draw on perfect pixels and be careful about the rect that you are erasing. For more on this see Loktar's answer here: HTML5 Canvas and Line Width

Here is your code, implemented with those two fixes:

http://jsfiddle.net/QjKAp/

Community
  • 1
  • 1
Simon Sarris
  • 62,212
  • 13
  • 141
  • 171