bo_knows
bo_knows

Reputation: 866

Understanding how canvas clip() affects multiple dynamically created shapes

I've been working on my first real HTML5 canvas project for awhile now, and I can't seem to get things to line up as I'd wish. I recently came across the context.clip() function, and it seems to help, but has other unintended consequences, so I thought I'd put my problem out there.

My project is a map of hexagonal tiles, with borders representing continents and travel routes. Here is a good photo of what my map looks like: main map

What I noticed, is that if I use the clip() function before drawing each hex, the inner light green hexes don't seem so off-centered. Here is a close-up comparison: compare

The problem is, when I use this clip() function, I seem to lose my borders/routes overlay. My code draws all the hexes, THEN draws the thick borders so that they don't get drawn on top of. clipping kills my borders

I'm not 100% sure I understand what is happening here. Why is clip() killing my borders? My entire list of code is on this github repo: https://github.com/boknows/hex-map-game, but the 2 main functions I'll list here. drawHexGrid() does the math to place each individual hex, drawHex() draws the hex with the proper fill, and drawHexBorders() draws the borders/routes after.

HexagonGrid.prototype.drawHexGrid = function (rows, cols, originX, originY, isDebug) {
this.canvasOriginX = originX;
this.canvasOriginY = originY;
this.rows = rows;
this.cols = cols;
var currentHexX;
var currentHexY;
var debugText = "";
var offsetColumn = false;
var hexNum = 1;

for (var col = 0; col < cols; col++) {
    for (var row = 0; row < rows; row++) {
        if (!offsetColumn) {
            currentHexX = (col * this.side) + originX;
            currentHexY = (row * this.height) + originY;
        } else {
            currentHexX = col * this.side + originX;
            currentHexY = (row * this.height) + originY + (this.height * 0.5);
        }
        if (isDebug) {
            debugText = hexNum;
            hexNum++;
        }
        if(map.data[row][col].type=="land"){  
            this.drawHex(currentHexX, currentHexY, "#99CC66", debugText, false, map.data[row][col].owner);
        }else if(map.data[row][col].type=="water"){
            this.drawHex(currentHexX, currentHexY, "#3333FF", "", false, map.data[row][col].owner);
        }else if(map.data[row][col].type=="forest"){
            this.drawHex(currentHexX, currentHexY, "#009900", debugText, false, map.data[row][col].owner);
        }else if(map.data[row][col].type=="desert"){
            this.drawHex(currentHexX, currentHexY, "#F5E8C1", debugText, false, map.data[row][col].owner);
        }else if(map.data[row][col].type=="mountains"){
            this.drawHex(currentHexX, currentHexY, "#996600", debugText, false, map.data[row][col].owner);
        }   
    }
    offsetColumn = !offsetColumn;

}
var offsetColumn = false;
for (var col = 0; col < cols; col++) { //Draw borders separately so they don't get overlapped by other graphics. 
    for (var row = 0; row < rows; row++) {
        if (!offsetColumn) {
            currentHexX = (col * this.side) + originX;
            currentHexY = (row * this.height) + originY;
        } else {
            currentHexX = col * this.side + originX;
            currentHexY = (row * this.height) + originY + (this.height * 0.5);
        }
        this.drawHexBorders(currentHexX, currentHexY);
    }
    offsetColumn = !offsetColumn;
}
};

HexagonGrid.prototype.drawHex = function (x0, y0, fillColor, debugText, highlight, highlightColor, owner) {  
this.context.font="bold 12px Helvetica";
this.owner = owner;
this.context.strokeStyle = "#000000";
this.context.lineWidth = 1;
this.context.lineCap='round';

this.context.restore();
var tile = this.getSelectedTile(x0 + this.width - this.side, y0);
var numberOfSides = 6,
size = this.radius,
Xcenter = x0 + (this.width / 2),
Ycenter = y0 + (this.height / 2);
this.context.beginPath();
this.context.lineWidth = 1;
this.context.moveTo (Xcenter +  size * Math.cos(0), Ycenter +  size *  Math.sin(0));          
for (var i = 1; i <= numberOfSides;i += 1) {
    this.context.lineTo (Xcenter + size * Math.cos(i * 2 * Math.PI / numberOfSides), Ycenter + size * Math.sin(i * 2 * Math.PI / numberOfSides));
}

if(typeof(map.data[tile.row][tile.column]) != "undefined"){
    if (fillColor && highlight == false && map.data[tile.row][tile.column].type =="land") {
        this.context.fillStyle = map.data[tile.row][tile.column].color;
    }else{
        this.context.fillStyle = fillColor;
    }
}


if (highlight == true){
    this.context.fillStyle = highlightColor;
}
this.context.fill();
this.context.closePath();
this.context.save();
this.context.clip();
this.context.lineWidth *= 2;
this.context.stroke();


if(map.data[tile.row][tile.column].type != "water"){
    //Draw smaller hex inside bigger hex - v2
    var numberOfSides = 6,
    size = this.radius*0.7,
    Xcenter = x0 + (this.width / 2),
    Ycenter = y0 + (this.height / 2);
    this.context.fillStyle = fillColor;
    this.context.strokeStyle = map.data[tile.row][tile.column].color;
    this.context.beginPath();
    this.context.lineWidth = .5;
    this.context.moveTo (Xcenter +  size * Math.cos(0), Ycenter +  size *  Math.sin(0));          
    for (var i = 1; i <= numberOfSides;i += 1) {
        this.context.lineTo (Xcenter + size * Math.cos(i * 2 * Math.PI / numberOfSides), Ycenter + size * Math.sin(i * 2 * Math.PI / numberOfSides));
    }
    this.context.fill();
    this.context.closePath();
    this.context.stroke();

    //if defensive boost active, draw grey dotted hex inside of owners colored hex.
    var index = 0;
    for(var i=0;i<map.dataProp.users.length;i++){
        if(map.dataProp.users[i]==map.data[tile.row][tile.column].owner){
            index = i;
        }
    }
    var defTrigger = false;
    for(var i=0;i<map.dataProp.turnModifiers[index].length;i++){
        if(map.dataProp.turnModifiers[index][i].type=="defensiveBoost"){
            defTrigger = true;
        }
    }
    if(defTrigger == true){
        var numberOfSides = 6,
        size = this.radius-12,
        Xcenter = x0 + (this.width / 2),
        Ycenter = y0 + (this.height / 2);
        this.context.strokeStyle = "#929292"
        this.context.beginPath();
        this.context.lineWidth = 5;
        this.context.moveTo (Xcenter +  size * Math.cos(0), Ycenter +  size *  Math.sin(0));          
        for (var i = 1; i <= numberOfSides;i += 1) {
            this.context.lineTo (Xcenter + size * Math.cos(i * 2 * Math.PI / numberOfSides), Ycenter + size * Math.sin(i * 2 * Math.PI / numberOfSides));
        }
        this.context.fill();
        this.context.closePath();
        this.context.stroke();
    }


    //Print number of units
    this.context.textAlign="center"; 
    this.context.textBaseline = "middle";
    this.context.font = 'bold 13pt Arial';
    //Code for contrasting text with background color
    /*var clr = getContrastYIQ(map.data[tile.row][tile.column].color); //contrast against player color 
    var clr = getContrastYIQ(fillColor); //contrast against land color (fillColor)
    this.context.fillStyle = clr;
    */
    this.context.fillStyle = "#000000";
    this.context.fillText(map.data[tile.row][tile.column].units, x0 + (this.width / 2) , y0 + (this.height / 2));
    this.context.fillStyle = "";
}
};

HexagonGrid.prototype.drawHexBorders = function (x0, y0) {  
var tile = this.getSelectedTile(x0 + this.width - this.side, y0);
if(map.data[tile.row][tile.column].s != ""){
    this.context.beginPath();
    this.context.lineWidth = 5;
    this.context.strokeStyle=map.data[tile.row][tile.column].s;
    this.context.moveTo(x0 + this.side, y0 + this.height);
    this.context.lineTo(x0 + this.width - this.side, y0 + this.height);
    this.context.stroke();
}
if(map.data[tile.row][tile.column].n != ""){

    this.context.beginPath();
    this.context.lineWidth = 5;
    this.context.strokeStyle=map.data[tile.row][tile.column].n;
    this.context.moveTo(x0 + this.side, y0);
    this.context.lineTo(x0 + this.width - this.side, y0);
    this.context.stroke();
}
if(map.data[tile.row][tile.column].ne != ""){
    this.context.beginPath();
    this.context.lineWidth = 5;
    this.context.strokeStyle=map.data[tile.row][tile.column].ne;
    this.context.moveTo(x0 + this.side, y0);
    this.context.lineTo(x0 + this.width, y0 + (this.height / 2));
    this.context.stroke();
}
if(map.data[tile.row][tile.column].se != ""){
    this.context.beginPath();
    this.context.lineWidth = 5;
    this.context.strokeStyle=map.data[tile.row][tile.column].se;
    this.context.moveTo(x0 + this.width, y0 + (this.height / 2));
    this.context.lineTo(x0 + this.side, y0 + this.height);
    this.context.stroke();
}
if(map.data[tile.row][tile.column].sw != ""){
    this.context.beginPath();
    this.context.lineWidth = 5;
    this.context.strokeStyle=map.data[tile.row][tile.column].sw;
    this.context.moveTo(x0 + this.width - this.side, y0 + this.height);
    this.context.lineTo(x0, y0 + (this.height/2));
    this.context.stroke();
}
if(map.data[tile.row][tile.column].nw != ""){
    this.context.beginPath();
    this.context.lineWidth = 5;
    this.context.strokeStyle=map.data[tile.row][tile.column].nw;
    this.context.moveTo(x0, y0 + (this.height/2));
    this.context.lineTo(x0 + this.width - this.side, y0);
    this.context.stroke();
}
};

//Recusivly step up to the body to calculate canvas offset.
HexagonGrid.prototype.getRelativeCanvasOffset = function() {
var x = 0, y = 0;
var layoutElement = this.canvas;
var bound = layoutElement.getBoundingClientRect();
if (layoutElement.offsetParent) {
    do {
        x += layoutElement.offsetLeft;
        y += layoutElement.offsetTop;
    } while (layoutElement = layoutElement.offsetParent);

    return { x: bound.left, y: bound.top };
}
}

Upvotes: 0

Views: 156

Answers (1)

markE
markE

Reputation: 105015

Disclaimer: I haven't looked at your code--you post a lot of code! (Stackoverflow encourages you to post a reduced snippet that illustrates your issue).

But could your issue be due to the way strokes are done?

Strokes are done half-inside & half-outside their defined path. So if you stroke a rect and then clearRect that same rect, you will still see part of the stroke--you will see the half-outside part of the stroke.

// draw a stroked rect
context.strokeRect(10,10,30,30);

// clear the same rect
context.clearRect(10,10,30,30);

// the outside half of the stroke is still visible

Inversely, clipping will prevent the half-outside part of the stroke from being drawn. Perhaps your missing hex border is the half-outside stroke that's not being drawn.

Upvotes: 1

Related Questions