在路径表面外绘制轮廓

时间:2015-04-15 19:09:45

标签: javascript html5 canvas

我有以下代码来绘制形状(主要用于矩形),但HTML5绘图功能似乎绘制边框,其厚度以指定的线为中心。我希望在形状表面之外有一个边框,我不知所措。

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和getStretchedY在将形状应用于设置元素的宽度,高度和偏移位置后返回第n个顶点的坐标。


感谢Ken Fyrstenberg的回答我已经让它适用于矩形,但是这个解决方案可能不适用于其他形状。

http://jsfiddle.net/0zq9mrch/

failed triangles

在这里,我画了两个&#34;宽&#34;边框,一个减去每个位置的lineWidth的一半,另一个添加。它没有工作(正如预期的那样),因为它只会在一个案例中将粗线放在上面和左边,在另一个案例中放在右边 - 而不是&#34;外面&#34;形状。您还可以看到斜坡周围的白色区域。


我试着弄清楚如何让顶点手动绘制粗边框的路径(使用fill()代替stroke())。

thick triangle with vertices shown

但事实证明我仍然遇到同样的问题:如何以编程方式确定边缘是在内部还是外部。这将需要一些三角法和重算法。出于我目前工作的目的,这太麻烦了。我想用它来绘制建筑物的地图。房间的墙壁需要在给定的尺寸之外绘制,但我现在仍然坚持独立的倾斜墙壁。

2 个答案:

答案 0 :(得分:3)

解决方案

您可以通过绘制两行来解决此问题:

  • 第一行的线条粗细
  • 第二条线收缩了外线宽度的50%

收缩时,向x和y添加50%,从宽度和高度减去线宽(或2x50%)。

实施例

snap

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>

复杂形状

snap complex

对于更复杂的形状,您必须手动计算路径。这有点复杂,也许对于SO来说太宽泛了。你必须考虑切线,弯道角度,交叉点等等。

“作弊”的一种方法是:

  • 将全线画到画布
  • 然后使用重用路径作为剪贴蒙版
  • 将复合模式更改为destination-atop
  • 沿各个方向绘制形状偏移
  • 恢复剪辑
  • 为主线再次更改颜色并重复使用路径。

下面的offset值将决定内线的粗细,而directions将决定分辨率。

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, comp. mode, transforms

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

答案 1 :(得分:2)

我已经迟到了,但是这是另一种方式来解决这个问题。&#34;一条复杂的道路。

它使用PathObject来简化创建外部笔划的过程。

PathObject保存用于定义复杂路径的所有命令和参数。

此PathObject还可以重放命令 - 从而可以重新定义/重绘已保存的路径。

PathObject类可重用。您可以使用它来保存需要重绘的任何路径(简单或复杂)。

Html5 Canvas很快将在上下文中内置自己的Path2D对象,但下面的示例中有一个跨浏览器的polyfill,可以在实现Path2D对象之前使用。

使用外部笔划应用银色衬里的云的插图。

enter image description here

&#34;在这里&#39;它是如何完成的......&#34;

  • 创建一个PathObject,可以保存用于定义复杂路径的所有命令和参数。此PathObject也可以重放命令 - 从而可以重新定义保存的路径。 Html5 Canvas很快就会在上下文中内置自己的Path2D对象,但下面的示例是一个跨浏览器的polyfill,可以在Path2D对象实现之前使用。

  • 使用PathObject保存复杂路径。

  • 在主画布上播放路径命令并根据需要填充/描边。

  • 在临时的内存中画布上播放路径命令。

  • 在临时画布上:

    • 设置所需外部笔画宽度两倍的context.lineWidth并进行笔画。

    • 设置globalCompositeOperation='destination-out'并填写。这将导致复杂路径的内部被清除并变得透明。

  • 将临时画布绘制到主画布上。这会导致主画布上现有的复杂路径获得&#34;外部笔画&#34;来自内存中的画布。

以下是示例代码和演示:

&#13;
&#13;
        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 
// commands & 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 commands=[];
    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'){
                        commands.length=0;
                        args.length=0;
                    }
                    commands.push(m);
                    args.push(arguments);
                    return(this);
            };}(m));
        }
        
        
    };

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

    //
    PathObject.prototype.show=function(){
        for(var i=0;i<commands.length;i++){
            log(commands[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);
&#13;
body{ background-color: ivory; }
canvas{border:1px solid red;}
&#13;
<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>
&#13;
&#13;
&#13;