最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Draw an outline outside the path surface - Stack Overflow

programmeradmin2浏览0评论

I have the following code to draw shapes (mainly used for rectangles) but the HTML5 drawing functions seem to draw borders with their thickness centered on the lines specified. I would like to have a border outside the surface of the shape and I'm at a loss.

Path.prototype.trace = function(elem, closePath) {
  sd.context.beginPath();
  sd.context.moveTo(this.getStretchedX(0, elem.width), this.getStretchedY(0, elem.height));
  sd.context.lineCap = "square";

  for(var i=1; i<this.points.length; ++i) {
    sd.context.lineTo(this.getStretchedX(i, elem.width), this.getStretchedY(i, elem.height));
  }

  if(closePath) {
    sd.context.lineTo(this.getStretchedX(0, elem.width), this.getStretchedY(0, elem.height));
  }
}

getStrechedX and getStretchedY return the coordinates of the nth vertex once the shape is applied to a set element width, height and offset position.


Thanks to Ken Fyrstenberg's answer I've got it working for a rectangle, but this solution can sadly not apply to other shapes.

/

Here I drew two "wide" borders, one subtracting half the lineWidth to every position, another one adding. It doesn't work (as expected) because it's only going to put the thick lines above and to the left in one case, under and to the right in another - not "outside" the shape. You can also see a white area around the slope.


I tried working out how I could get the vertices to manually draw the path for the thick border (using fill() instead of stroke()).

But it turns out I still end up with the same problem: how to programatically determine if an edge is inside or outside. This would require some trigonometry and a heavy algorithm. For the purpose of my current work, this is too much trouble. I wanted to use this to draw a map of a building. The room walls need to be drawn outside the given dimensions, but I'll stick to standalone sloped walls for now.

I have the following code to draw shapes (mainly used for rectangles) but the HTML5 drawing functions seem to draw borders with their thickness centered on the lines specified. I would like to have a border outside the surface of the shape and I'm at a loss.

Path.prototype.trace = function(elem, closePath) {
  sd.context.beginPath();
  sd.context.moveTo(this.getStretchedX(0, elem.width), this.getStretchedY(0, elem.height));
  sd.context.lineCap = "square";

  for(var i=1; i<this.points.length; ++i) {
    sd.context.lineTo(this.getStretchedX(i, elem.width), this.getStretchedY(i, elem.height));
  }

  if(closePath) {
    sd.context.lineTo(this.getStretchedX(0, elem.width), this.getStretchedY(0, elem.height));
  }
}

getStrechedX and getStretchedY return the coordinates of the nth vertex once the shape is applied to a set element width, height and offset position.


Thanks to Ken Fyrstenberg's answer I've got it working for a rectangle, but this solution can sadly not apply to other shapes.

http://jsfiddle/0zq9mrch/

Here I drew two "wide" borders, one subtracting half the lineWidth to every position, another one adding. It doesn't work (as expected) because it's only going to put the thick lines above and to the left in one case, under and to the right in another - not "outside" the shape. You can also see a white area around the slope.


I tried working out how I could get the vertices to manually draw the path for the thick border (using fill() instead of stroke()).

But it turns out I still end up with the same problem: how to programatically determine if an edge is inside or outside. This would require some trigonometry and a heavy algorithm. For the purpose of my current work, this is too much trouble. I wanted to use this to draw a map of a building. The room walls need to be drawn outside the given dimensions, but I'll stick to standalone sloped walls for now.

Share Improve this question edited Apr 15, 2015 at 20:09 Domino asked Apr 15, 2015 at 19:09 DominoDomino 6,7881 gold badge36 silver badges61 bronze badges 1
  • You're just drawing lines, there is no concept of "inside" or "outside" for a line. I believe you'll just have to deal with this yourself and offset the line by .5 the width of the line in whatever direction you want. – James Montagne Commented Apr 15, 2015 at 19:11
Add a ment  | 

2 Answers 2

Reset to default 5

Solution

You can solve this by drawing two lines:

  • First line with line thickness as intended
  • Second line contracted with 50% of the outer line width

To contract, add 50% to x and y, subtract line-width (or 2x 50%) from width and height.

Example

var ctx = document.querySelector("canvas").getContext("2d");
var lineWidth = 20;
var lw50 = lineWidth * 0.5;

// outer line
ctx.lineWidth = lineWidth;         // intended line width
ctx.strokeStyle = "#975";          // color for main line
ctx.strokeRect(40, 40, 100, 100);  // full line

// inner line
ctx.lineWidth = 2;                 // inner line width
ctx.strokeStyle = "#000";          // color for inner line

ctx.strokeRect(40 + lw50, 40 + lw50, 100 - lineWidth, 100 - lineWidth);
<canvas></canvas>

Complex shapes

For more plex shapes you will have to calculate the path manually. This is a little bit more plex and perhaps too broad for SO. You have to consider things like tangents, angle at bends, intersections and so forth.

One way to "cheat" is to:

  • draw the main line at full thickness to canvas
  • then use reuse the path as a clipping mask
  • change posite mode to destination-atop
  • draw the shape offset in various direction
  • restore clipping
  • change color and reuse path again for the main line.

The offset value below will determine the thickness of the inner line while the directions will determine resolution.

var ctx = document.querySelector("canvas").getContext("2d");
var lineWidth = 20;
var offset = 0.5;                                   // line "thickness"
var directions = 8;                                 // increase to increase details
var angleStep = 2 * Math.PI / 8;

// shape
ctx.lineWidth = lineWidth;                          // intended line width
ctx.strokeStyle = "#000";                           // color for inner line
ctx.moveTo(50, 100);                                // some random shape
ctx.lineTo(100, 20);
ctx.lineTo(200, 100);
ctx.lineTo(300, 100);
ctx.lineTo(200, 200);
ctx.lineTo(50, 100);
ctx.closePath();
ctx.stroke();

ctx.save()

ctx.clip();                                         // set as clipping mask
ctx.globalCompositeOperation = "destination-atop";  // draws "behind" existing drawings

for(var a = 0; a < Math.PI * 2; a += angleStep) {
  ctx.setTransform(1,0,0,1, offset * Math.cos(a), offset * Math.sin(a));
  ctx.drawImage(ctx.canvas, 0, 0);
}

ctx.restore();                              // removes clipping, p. mode, transforms

// set new color and redraw same path as previous
ctx.strokeStyle = "#975";                           // color for inner line
ctx.stroke();
<canvas height=250></canvas>

I'm late to the party, but here's an alternate way to "outside stroke" a plex path.

It uses a PathObject to simplify the process of creating the outside stroke.

The PathObject saves all the mands and arguments used to define your plex path.

This PathObject can also replay the mands--and can thereby redefine/redraw the saved path.

The PathObject class is re-usable. You can use it to save any path (simple or plex) that you need to redraw.

Html5 Canvas will soon have its own Path2D object built into the context, but my example below has a cross-browser polyfill that can be used until the Path2D object is implemented.

An illustration of a cloud with a silver lining applied using an outside stroke.

"Here's how it's done..."

  • Create a PathObject that can save all the mands and arguments used to define your plex path. This PathObject can also replay the mands--and can thereby redefine the saved path. Html5 Canvas will soon have its own Path2D object built into the context, but my example below is a cross-browser polyfill that can be used until the Path2D object is implemented.

  • Save a plex path using the PathObject.

  • Play the path mands on the main canvas and fill/stroke as desired.

  • Play the path mands on a temporary in-memory canvas.

  • On the temporary canvas:

    • Set a context.lineWidth of twice your desired outside stroke width and do the stroke.

    • Set globalCompositeOperation='destination-out' and fill. This will cause the inside of the plex path to be cleared and made transparent.

  • Draw the temporary canvas onto the main canvas. This causes your existing plex path on the main canvas to get the "outside stroke" from the in-memory canvas.

Here's example code and a Demo:

        function log(){console.log.apply(console,arguments);}

        var canvas=document.getElementById("canvas");
        var ctx=canvas.getContext("2d");
        var canvas1=document.getElementById("canvas1");
        var ctx1=canvas1.getContext("2d");


// A "class" that remembers (and can replay) all the 
// mands & arguments used to define a context path
var PathObject=( function(){

    // Path-related context methods that don't return a value
    var methods = ['arc','beginPath','bezierCurveTo','clip','closePath',
      'lineTo','moveTo','quadraticCurveTo','rect','restore','rotate',
      'save','scale','setTransform','transform','translate','arcTo'];

    var mands=[];
    var args=[];

    function PathObject(){       
        // add methods plus logging
        for (var i=0;i<methods.length;i++){   
            var m = methods[i];
            this[m] = (function(m){
                return function () {
                    if(m=='beginPath'){
                        mands.length=0;
                        args.length=0;
                    }
                    mands.push(m);
                    args.push(arguments);
                    return(this);
            };}(m));
        }
        
        
    };

    // define/redefine the path by issuing all the saved
    //     path mands to the specified context
    PathObject.prototype.definePath=function(context){
        for(var i=0;i<mands.length;i++){
            context[mands[i]].apply(context, args[i]);            
        }
    }   

    //
    PathObject.prototype.show=function(){
        for(var i=0;i<mands.length;i++){
            log(mands[i],args[i]);
        }
    }

    //
    return(PathObject);
})();




var x=75;
var y=100;
var scale=0.50;

// define a cloud path
var path=new PathObject()
.beginPath()
.save()
.translate(x,y)
.scale(scale,scale)
.moveTo(0, 0)
.bezierCurveTo(-40,  20, -40,  70,  60,  70)
.bezierCurveTo(80,  100, 150, 100, 170,  70)
.bezierCurveTo(250,  70, 250,  40, 220,  20)
.bezierCurveTo(260, -40, 200, -50, 170, -30)         
.bezierCurveTo(150, -75,  80, -60,  80, -30)
.bezierCurveTo(30,  -75, -20, -60,   0,   0)
.restore();


// fill the blue sky on the main canvas
ctx.fillStyle='skyblue';
ctx.fillRect(0,0,canvas.width,canvas.height);

// draw the cloud on the main canvas
path.definePath(ctx);
ctx.fillStyle='white';
ctx.fill();
ctx.strokeStyle='black';
ctx.lineWidth=2;
ctx.stroke();

// draw the cloud's silver lining on the temp canvas
path.definePath(ctx1);
ctx1.lineWidth=20;
ctx1.strokeStyle='silver';
ctx1.stroke();
ctx1.globalCompositeOperation='destination-out';
ctx1.fill();

// draw the silver lining onto the main canvas
ctx.drawImage(canvas1,0,0);
body{ background-color: ivory; }
canvas{border:1px solid red;}
<h4>Main canvas with original white cloud + small black stroke<br>The "outside silver lining" is from the temp canvas</h4>
<canvas id="canvas" width=300 height=300></canvas>
<h4>Temporary canvas used to create the "outside stroke"</h4>
<canvas id="canvas1" width=300 height=300></canvas>

发布评论

评论列表(0)

  1. 暂无评论