Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

draw outer and inner border around any canvas shape

How to draw outer and inner border around any canvas shape?

I'm drawing several stroke-only shapes on an html canvas, and I would like to draw an inner and outer border around them.

draft example: shapes with and without inner/outer border

Is there a generic why to do it for any shape (assuming it's a closed stroke-only shape)?

like image 919
avivr Avatar asked Dec 11 '15 22:12

avivr


1 Answers

Two methods

There is no inbuilt way to do this and there are two programmatic ways that I use. The first is complicated and involves expanding and contracting the path then drawing along that path. This works for most situations but will fail in complex situation, and the solution has many variables and options to account for these complications and how to handle them.

The better of the two

The second and easiest way that I present below is by using the ctx.globalCompositeOperation setting to mask out what you want drawn or not. As the stroke is drawn along the center and the fill fills up to the center you can draw the stroke at twice the desired width and then either mask in or mask out the inner or outer part.

This does become problematic when you start to create very complex images as the masking (Global Composite Operation) will interfere with what has already been drawn.

To simplify the process you can create a second canvas the same size as the original as a scratch space. You can then draw the shape on he scratch canvas do the masking and then draw the scratch canvas onto the working one.

Though this method is not as fast as computing the expanded or shrunk path, it does not suffer from the ambiguities faced by moving points in the path. Nor does this method create the lines with the correct line join or mitering for the inside or outside edges, for that you must use a the other method. For most purposes the masking it is a good solution.

Below is a demo of the masking method to draw an inner or outer path. If you modify the mask by including drawing a stroke along with the fill you can also set an offset so that the outline or inline will be offset by a number of pixels. I have left that for you. (hint add stroke and set the line width to twice the offset distance when drawing the mask).

var demo = function(){
    
    /** fullScreenCanvas.js begin **/
    var canvas = ( function () {
        canvas = document.getElementById("canv");
        if(canvas !== null){
            document.body.removeChild(canvas);
        }
        // creates a blank image with 2d context
        canvas = document.createElement("canvas"); 
        canvas.id = "canv";    
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight; 
        canvas.style.position = "absolute";
        canvas.style.top = "0px";
        canvas.style.left = "0px";
        canvas.style.zIndex = 1000;
        canvas.ctx = canvas.getContext("2d"); 
        document.body.appendChild(canvas);
        return canvas;
    })();
    var ctx = canvas.ctx;
    /** fullScreenCanvas.js end **/
    
    
    
    /** CreateImage.js begin **/
    // creates a blank image with 2d context
    var createImage = function(w,h){
        var image = document.createElement("canvas");  
        image.width = w;
        image.height =h; 
        image.ctx = image.getContext("2d"); 
        return image;
    }  
    /** CreateImage.js end **/
    
    
    
    // define a shape for demo
    var shape = [0.1,0.1,0.9,0.1,0.5,0.5,0.8,0.9,0.1,0.9];
    
    // draws the shape as a stroke
    var strokeShape = function (ctx) {
        var w, h, i;
        w = canvas.width;
        h = canvas.height;
        ctx.beginPath();
        ctx.moveTo(shape[0]  *w, shape[1]  *h)
        for (i = 2; i < shape.length; i += 2) {
            ctx.lineTo(shape[i] * w, shape[i + 1] * h);
        }
        ctx.closePath();
        ctx.stroke();
    }
    // draws the shape as filled
    var fillShape = function (ctx) {
        var w, h, i;       
        w = canvas.width;
        h = canvas.height;
        
        ctx.beginPath();
        ctx.moveTo(shape[0] * w,shape[1] * h)
        for (i = 2; i < shape.length; i += 2) {
            ctx.lineTo(shape[i]*w,shape[i+1]*h);
        }
        ctx.closePath();
        ctx.fill();
    }
    
    var drawInOutStroke = function(width,style,where){
        //  clear the workspace
        workCtx.ctx.globalCompositeOperation ="source-over";
        workCtx.ctx.clearRect(0, 0, workCtx.width, workCtx.height);
      
        // set the width to double 
        workCtx.ctx.lineWidth = width*2;
        workCtx.ctx.strokeStyle = style;
      
        // fill colour does not matter here as its not seen
        workCtx.ctx.fillStyle = "white";
      
        // can use any join type
        workCtx.ctx.lineJoin = "round";
      
        // draw the shape outline at double width
        strokeShape(workCtx.ctx);
      
        // set comp to in. 
        // in means leave only pixel that are both in the source and destination
        if (where.toLowerCase() === "in") {
            workCtx.ctx.globalCompositeOperation ="destination-in";
        } else {
            // out means only pixels on the destination that are not part of the source
            workCtx.ctx.globalCompositeOperation ="destination-out";
        }
        fillShape(workCtx.ctx);
        ctx.drawImage(workCtx, 0, 0);
    }
    
    // clear in case of resize
    ctx.globalCompositeOperation ="source-over";
    ctx.clearRect(0,0,canvas.width,canvas.height);
    
    // create the workspace canvas
    var workCtx = createImage(canvas.width, canvas.height);
    
    // draw the outer stroke
    drawInOutStroke((canvas.width + canvas.height) / 45, "black", "out");
  
    // draw the inner stroke
    drawInOutStroke((canvas.width + canvas.height) / 45, "red", "in");
    
    // draw the shape outline just to highlight the effect
    ctx.strokeStyle = "white";
    ctx.lineJoin = "round";
    ctx.lineWidth = (canvas.width + canvas.height) / 140;

    strokeShape(ctx);
    
};
// run the demo
demo();
// incase fullscreen redraw it all
window.addEventListener("resize",demo)
like image 160
Blindman67 Avatar answered Sep 23 '22 21:09

Blindman67