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

How to draw parallel lines with canvas PaperJS? (CanvasJavascript) - Stack Overflow

programmeradmin0浏览0评论

Sorry for my poor knowledge in mathematics.

How can I draw parallel lines like this:

Here is my current code:

<canvas id='canvas' resize></canvas>

I'm using PaperJS () :

<script type='text/javascript' src='.js'></script>

And this is my script:

<script type='text/paperscript' canvas='canvas'>
    var path1    = new Path();
    var path2    = new Path();
    var path3    = new Path();
    var distance = 20;

    path1.strokeWidth = 2.0;
    path1.strokeColor = 'black';
    path2.strokeWidth = 2.0;
    path2.strokeColor = 'black';
    path2.dashArray   = [4, 4];
    path3.strokeWidth = 2.0;
    path3.strokeColor = 'black';

    function onMouseDown (event) {
       path2.add(event.point);
       path1.add(event.point - distance);
       path3.add(event.point + distance);
    };
</script>

This is my bad result (I've rounded by red circle):

Sorry for my poor knowledge in mathematics.

How can I draw parallel lines like this:

Here is my current code:

<canvas id='canvas' resize></canvas>

I'm using PaperJS (http://paperjs) :

<script type='text/javascript' src='http://paperjs/assets/js/paper.js'></script>

And this is my script:

<script type='text/paperscript' canvas='canvas'>
    var path1    = new Path();
    var path2    = new Path();
    var path3    = new Path();
    var distance = 20;

    path1.strokeWidth = 2.0;
    path1.strokeColor = 'black';
    path2.strokeWidth = 2.0;
    path2.strokeColor = 'black';
    path2.dashArray   = [4, 4];
    path3.strokeWidth = 2.0;
    path3.strokeColor = 'black';

    function onMouseDown (event) {
       path2.add(event.point);
       path1.add(event.point - distance);
       path3.add(event.point + distance);
    };
</script>

This is my bad result (I've rounded by red circle):

Share Improve this question edited Nov 17, 2015 at 12:28 dphans asked Nov 16, 2015 at 14:35 dphansdphans 1,70320 silver badges20 bronze badges 5
  • 2 please post your existing code code – aurelius Commented Nov 16, 2015 at 14:40
  • 1 Could you please explain further what you are trying to achieve? – Sean_A91 Commented Nov 16, 2015 at 14:45
  • @aurelius I've updated. Thanks for your notice. – dphans Commented Nov 16, 2015 at 17:05
  • This is a plex problem. [Clipper](www.angusj./delphi/clipper.php) is a c++ open source library which enables to offset polygons. And here is an article which deals with a similar problem with OpenGL. – arthur.sw Commented Nov 16, 2015 at 18:01
  • The technical term for this is polygon offsetting. – nicholaswmin Commented Nov 23, 2015 at 12:16
Add a ment  | 

3 Answers 3

Reset to default 6

Your need to create extruding and beveling paths to a source path reminds me of this on-point blog post by Hans Muller.

Attribution Note:

Hans Muller wrote several blog posts about the work done to provide CSS shape-margin and shape-padding in Webkit and Blink.

http://hansmuller-webkit.blogspot./2014/03/a-simpler-algorithm-for-css-shapes.html

http://hansmuller-webkit.blogspot./2013/04/growing-and-shrinking-polygons-round-one.html

This same code that calculates the CSS margin path outside a shape and the CSS padding path inside a shape can be used to create your parallel paths.

Here is a demo from the post that shows "parallel" paths inside and outside a given path:

var shapeMargin = 10;
var shapePadding = 10;
var polygon;
var marginPolygon;
var paddingPolygon;

var dragVertexIndex = null;
var hoverLocation = null;
var polygonVertexRadius = 9;

function getCanvas() { return document.getElementById("demo-canvas"); }

function drawPolygonVertexLabels(g, p)
{
  for (var i = 0; i < p.vertices.length; i++) {
    var vertex = p.vertices[i];
    if (vertex.hidden)
      continue;
    g.fillText(vertex.label, vertex.x - 3, vertex.y + 4);
  }
}

function drawPolygonVertices(g, p, r)
{
  g.strokeStyle = "none";

  for (var i = 0; i < p.vertices.length; i++) {
    var vertex = p.vertices[i];
    if (vertex.hidden)
      return;
    g.beginPath();
    g.arc(vertex.x, vertex.y, r, 0, Math.PI*2, false)
    g.fill();

    /*
                if (vertex.isReflex) {
                    g.strokeStyle = "rgb(238,236,230)";
                    g.lineWidth = 1;
                    g.arc(vertex.x, vertex.y, polygonVertexRadius+2, 0, Math.PI*2, false);
                    g.stroke();
                }
                */

    g.closePath();
  }
}

function drawPolygonEdges(g, p)
{
  if (p.vertices.length == 0)
    return;

  g.beginPath();

  for (var i = 0; i < p.vertices.length; i++) {
    var vertex = p.vertices[i];
    if (i == 0) 
      g.moveTo(vertex.x, vertex.y);
    else
      g.lineTo(vertex.x, vertex.y);
  }
  if (polygon.closed)
    g.lineTo(p.vertices[0].x, p.vertices[0].y);

  g.stroke();
  g.closePath();
}

function drawPolygonOffsetEdges(g, p)
{
  var edges = p.offsetEdges;
  if (!edges || edges.length == 0)
    return;

  g.beginPath();
  for (var i = 0; i < edges.length; i++) {
    var edge = edges[i];
    g.moveTo(edge.vertex1.x, edge.vertex1.y);
    g.lineTo(edge.vertex2.x, edge.vertex2.y);
  }
  g.stroke();
  g.closePath();

}

function draw() {
  var canvas = getCanvas();
  var g = canvas.getContext("2d");

  g.clearRect(0, 0, canvas.width, canvas.height);

  // marginPolygon
  g.fillStyle = "none";
  g.strokeStyle = "rgba(238,236,230,0.5)";
  g.lineWidth = "1";
  drawPolygonOffsetEdges(g, marginPolygon);

  g.strokeStyle = "rgb(79,129,189)";
  g.lineWidth = "2";
  g.fillStyle = "none";
  drawPolygonEdges(g, marginPolygon);

  g.fillStyle = "rgb(79,129,189)";
  drawPolygonVertices(g, marginPolygon, polygonVertexRadius - 4);

  // paddingPolygon

  g.strokeStyle = "rgba(238,236,230,0.5)"
  g.lineWidth = "1";
  drawPolygonOffsetEdges(g, paddingPolygon);

  g.strokeStyle = "rgb(119,146,60)";
  g.lineWidth = "2";
  g.fillStyle = "none";
  drawPolygonEdges(g, paddingPolygon);

  g.fillStyle = "rgb(119,146,60)";
  drawPolygonVertices(g, paddingPolygon, polygonVertexRadius - 4);

  // polygon

  g.strokeStyle = "rgb(238,236,230)";
  g.fillStyle = "none";
  g.lineWidth = "1";
  drawPolygonEdges(g, polygon);

  g.fillStyle = "rgb(255,161,0)";
  drawPolygonVertices(g, polygon, polygonVertexRadius);

  g.font = "12px Arial";
  g.fillStyle = "black";
  drawPolygonVertexLabels(g, polygon);
}

// See http://paulbourke/geometry/pointlineplane/

function distanceToEdgeSquared(p1, p2, p3)
{
  var dx = p2.x - p1.x;
  var dy = p2.y - p1.y;

  if (dx == 0 || dy == 0) 
    return Number.POSITIVE_INFNITY;

  var u = ((p3.x - p1.x) * dx + (p3.y - p1.y) * dy) / (dx * dx + dy * dy);

  if (u < 0 || u > 1)
    return Number.POSITIVE_INFINITY;

  var x = p1.x + u * dx;  // closest point on edge p1,p2 to p3
  var y = p1.y + u * dy;

  return Math.pow(p3.x - x, 2) + Math.pow(p3.y - y, 2);

}

function polygonVertexNear(p)
{
  var thresholdDistanceSquared = polygonVertexRadius * polygonVertexRadius * 2;
  for (var i = 0; i < polygon.vertices.length; i++) {
    var vertex = polygon.vertices[i];
    var dx = vertex.x - p.x;
    var dy = vertex.y - p.y;
    if (dx*dx + dy*dy < thresholdDistanceSquared)
      return i;
  }
  return null;
}

function polygonEdgeNear(p)
{
  var thresholdDistanceSquared = polygonVertexRadius * polygonVertexRadius * 2;
  for (var i = 0; i < polygon.vertices.length; i++) {
    var v0 = polygon.vertices[i];
    var v1 = polygon.vertices[(i + 1) % polygon.vertices.length];
    if (distanceToEdgeSquared(v0, v1, p) < thresholdDistanceSquared)
      return {index0: i, index1: (i + 1) % polygon.vertices.length};
  }
  return null;
}

// See http://hansmuller-webkit.blogspot./2013/02/where-is-mouse.html
function canvasEventLocation(event)
{
  var canvas = getCanvas();
  var style = document.defaultView.getComputedStyle(canvas, null);

  function styleValue(property) {
    return parseInt(style.getPropertyValue(property), 10) || 0;
  }

  var scaleX = canvas.width / styleValue("width");
  var scaleY = canvas.height / styleValue("height");

  var canvasRect = canvas.getBoundingClientRect();
  var canvasX = scaleX * (event.clientX - canvasRect.left - canvas.clientLeft - styleValue("padding-left"));
  var canvasY = scaleY * (event.clientY - canvasRect.top - canvas.clientTop - styleValue("padding-top"))

  return {x: canvasX, y: canvasY};
}


function handleMouseDown(event)
{
  var eventXY = canvasEventLocation(event);
  getCanvas().addEventListener("mousemove", handleMouseMove, false); 

  if (polygon.closed) {
    dragVertexIndex = polygonVertexNear(eventXY);
    if (dragVertexIndex == null) {
      var edge = polygonEdgeNear(canvasEventLocation(event));
      if (edge != null) {
        polygon.vertices.splice(edge.index1, 0, eventXY);
        puteAll();
      }
    }
  }
  else
  {
    polygon.closed = polygonVertexNear(eventXY) != null;
    if (!polygon.closed)
      polygon.vertices.push(eventXY);
    else 
      puteAll();
  }

  // The following appears to be the only way to prevent Chrome from showing the text select cursor.
  // For the record: hacks based on -webkit-user-select: none, or #canvas:focus,#canvas:active do not 
  // currently work.

  event.preventDefault();
  event.stopPropagation();

  draw();
}

function handleMouseMove(event)
{
  if (dragVertexIndex != null) {
    var eventXY = canvasEventLocation(event);
    polygon.vertices[dragVertexIndex].x = eventXY.x;
    polygon.vertices[dragVertexIndex].y = eventXY.y;
    puteAll();
    draw();
  }
}

function handleMouseUp(event)
{
  getCanvas().removeEventListener("mousemove", handleMouseMove);
  dragVertexIndex = null;
  draw();
}

function handleSliderChange()
{
  function $(id) { return document.getElementById(id); }

  shapeMargin = parseInt($("slider.shapeMargin").value);
  $("value.shapeMargin").innerHTML = shapeMargin;

  shapePadding = parseInt($("slider.shapePadding").value);
  $("value.shapePadding").innerHTML = shapePadding;

  puteAll();
  draw();
}

function inwardEdgeNormal(edge)
{
  // Assuming that polygon vertices are in clockwise order
  var dx = edge.vertex2.x - edge.vertex1.x;
  var dy = edge.vertex2.y - edge.vertex1.y;
  var edgeLength = Math.sqrt(dx*dx + dy*dy);
  return {x: -dy/edgeLength, y: dx/edgeLength};
}

function outwardEdgeNormal(edge)
{
  var n = inwardEdgeNormal(edge);
  return {x: -n.x, y: -n.y};
}

// If the slope of line vertex1,vertex2 greater than the slope of vertex1,p then p is on the left side of vertex1,vertex2 and the return value is > 0.
// If p is colinear with vertex1,vertex2 then return 0, otherwise return a value < 0.

function leftSide(vertex1, vertex2, p)
{
  return ((p.x - vertex1.x) * (vertex2.y - vertex1.y)) - ((vertex2.x - vertex1.x) * (p.y - vertex1.y));
}

function isReflexVertex(polygon, vertexIndex)
{
  // Assuming that polygon vertices are in clockwise order
  var thisVertex = polygon.vertices[vertexIndex];
  var nextVertex = polygon.vertices[(vertexIndex + 1) % polygon.vertices.length];
  var prevVertex = polygon.vertices[(vertexIndex + polygon.vertices.length - 1) % polygon.vertices.length];
  if (leftSide(prevVertex, nextVertex, thisVertex) < 0)
    return true;  // TBD: return true if thisVertex is inside polygon when thisVertex isn't included

  return false;
}

function createPolygon(vertices)
{
  var polygon = {vertices: vertices};

  var edges = [];
  var minX = (vertices.length > 0) ? vertices[0].x : undefined;
  var minY = (vertices.length > 0) ? vertices[0].y : undefined;
  var maxX = minX;
  var maxY = minY;

  for (var i = 0; i < polygon.vertices.length; i++) {
    vertices[i].label = String(i);
    vertices[i].isReflex = isReflexVertex(polygon, i);
    var edge = {
      vertex1: vertices[i], 
      vertex2: vertices[(i + 1) % vertices.length], 
      polygon: polygon, 
      index: i
    };
    edge.outwardNormal = outwardEdgeNormal(edge);
    edge.inwardNormal = inwardEdgeNormal(edge);
    edges.push(edge);
    var x = vertices[i].x;
    var y = vertices[i].y;
    minX = Math.min(x, minX);
    minY = Math.min(y, minY);
    maxX = Math.max(x, maxX);
    maxY = Math.max(y, maxY);
  }                       

  polygon.edges = edges;
  polygon.minX = minX;
  polygon.minY = minY;
  polygon.maxX = maxX;
  polygon.maxY = maxY;
  polygon.closed = true;

  return polygon;
}

// based on http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/, edgeA => "line a", edgeB => "line b"

function edgesIntersection(edgeA, edgeB)
{
  var den = (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex2.x - edgeA.vertex1.x) - (edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex2.y - edgeA.vertex1.y);
  if (den == 0)
    return null;  // lines are parallel or conincident

  var ua = ((edgeB.vertex2.x - edgeB.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeB.vertex2.y - edgeB.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;
  var ub = ((edgeA.vertex2.x - edgeA.vertex1.x) * (edgeA.vertex1.y - edgeB.vertex1.y) - (edgeA.vertex2.y - edgeA.vertex1.y) * (edgeA.vertex1.x - edgeB.vertex1.x)) / den;

  if (ua < 0 || ub < 0 || ua > 1 || ub > 1)
    return null;

  return {x: edgeA.vertex1.x + ua * (edgeA.vertex2.x - edgeA.vertex1.x),  y: edgeA.vertex1.y + ua * (edgeA.vertex2.y - edgeA.vertex1.y)};
}

function appendArc(vertices, center, radius, startVertex, endVertex, isPaddingBoundary)
{
  const twoPI = Math.PI * 2;
  var startAngle = Math.atan2(startVertex.y - center.y, startVertex.x - center.x);
  var endAngle = Math.atan2(endVertex.y - center.y, endVertex.x - center.x);
  if (startAngle < 0)
    startAngle += twoPI;
  if (endAngle < 0)
    endAngle += twoPI;
  var arcSegmentCount = 5; // An odd number so that one arc vertex will be eactly arcRadius from center.
  var angle = ((startAngle > endAngle) ? (startAngle - endAngle) : (startAngle + twoPI - endAngle));
  var angle5 =  ((isPaddingBoundary) ? -angle : twoPI - angle) / arcSegmentCount;

  vertices.push(startVertex);
  for (var i = 1; i < arcSegmentCount; ++i) {
    var angle = startAngle + angle5 * i;
    var vertex = {
      x: center.x + Math.cos(angle) * radius,
      y: center.y + Math.sin(angle) * radius,
    };
    vertices.push(vertex);
  }
  vertices.push(endVertex);
}

function createOffsetEdge(edge, dx, dy)
{
  return {
    vertex1: {x: edge.vertex1.x + dx, y: edge.vertex1.y + dy},
    vertex2: {x: edge.vertex2.x + dx, y: edge.vertex2.y + dy}
  };
}

function createMarginPolygon(polygon)
{
  var offsetEdges = [];
  for (var i = 0; i < polygon.edges.length; i++) {
    var edge = polygon.edges[i];
    var dx = edge.outwardNormal.x * shapeMargin;
    var dy = edge.outwardNormal.y * shapeMargin;
    offsetEdges.push(createOffsetEdge(edge, dx, dy));
  }

  var vertices = [];
  for (var i = 0; i < offsetEdges.length; i++) {
    var thisEdge = offsetEdges[i];
    var prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length];
    var vertex = edgesIntersection(prevEdge, thisEdge);
    if (vertex)
      vertices.push(vertex);
    else {
      var arcCenter = polygon.edges[i].vertex1;
      appendArc(vertices, arcCenter, shapeMargin, prevEdge.vertex2, thisEdge.vertex1, false);
    }
  }

  var marginPolygon = createPolygon(vertices);
  marginPolygon.offsetEdges = offsetEdges;
  return marginPolygon;
}

function createPaddingPolygon(polygon)
{
  var offsetEdges = [];
  for (var i = 0; i < polygon.edges.length; i++) {
    var edge = polygon.edges[i];
    var dx = edge.inwardNormal.x * shapePadding;
    var dy = edge.inwardNormal.y * shapePadding;
    offsetEdges.push(createOffsetEdge(edge, dx, dy));
  }

  var vertices = [];
  for (var i = 0; i < offsetEdges.length; i++) {
    var thisEdge = offsetEdges[i];
    var prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length];
    var vertex = edgesIntersection(prevEdge, thisEdge);
    if (vertex)
      vertices.push(vertex);
    else {
      var arcCenter = polygon.edges[i].vertex1;
      appendArc(vertices, arcCenter, shapePadding, prevEdge.vertex2, thisEdge.vertex1, true);
    }
  }

  var paddingPolygon = createPolygon(vertices);
  paddingPolygon.offsetEdges = offsetEdges;
  return paddingPolygon;
}

function puteAll()
{
  polygon = createPolygon(polygon.vertices);
  marginPolygon = createMarginPolygon(polygon);
  paddingPolygon = createPaddingPolygon(polygon);
}

function init() 
{
  var polygonVertices =  [{x: 143, y: 327}, {x: 80, y: 236}, {x: 151, y: 148}, {x: 454, y: 69}, {x: 560, y: 320}];
  polygon = createPolygon(polygonVertices);

  var canvas = getCanvas();
  canvas.addEventListener("mousedown", handleMouseDown, false);
  canvas.addEventListener("mouseup", handleMouseUp, false);

  var sliderNames = ["slider.shapeMargin", "slider.shapePadding"];
  for (var i = 0; i < sliderNames.length; i++) {
    var slider = document.getElementById(sliderNames[i]);
    slider.onchange = handleSliderChange;
  } 

  puteAll();
  draw();
}

init();
#demo-canvas {
  border: solid black 4px;
  margin: 10px;
  cursor: default;
  background-color: #636363;
}
.gui {
  display: table;
}
.gui-row {
  display: table-row;
}
.gui-label {
  display: table-cell;
  text-align: end;
  margin: 1em;
  width: 200px;
}
.gui-input {
  display: table-cell;
  margin: 1em;
}
.gui-value {
  display: table-cell;
  margin: 1em;
}
<h4>Drag the numbered path vertices and the parallel lines adjust.</h4>
<canvas id="demo-canvas" width="650" height="400"></canvas>
<div class="gui">
  <div class="gui-row">
    <label class="gui-label" for="slider.shapeMargin">Shape Margin</label>
    <input class="gui-input" id="slider.shapeMargin" value="10" min="0" max="50" type="range" />
    <label class="gui-value" id="value.shapeMargin">10</label>
  </div>
  <div class="gui-row">
    <label class="gui-label" for="slider.shapePadding">Shape Padding</label>
    <input class="gui-input" id="slider.shapePadding" value="10" min="0" max="50" type="range" />
    <label class="gui-value" id="value.shapePadding">10</label>
  </div>
</div>

This is a more plicated problem than it initially seems. Put to one side the technicalities of drawing on the canvas and consider a line defined like this as an array of points

var line = [P(100, 400), P(200, 300), P(300, 300), P(300, 200), P(400, 200), P(400, 300)];

where P is just a function that turns a pair of coordinates into an object with x and y properties

function P(x, y) {
    return {x: x, y: y}
}

A first attempt would be to draw the lines parallel to each segment of your original path. You could use a function like this one (based on this answer to get points perpendicular to the original line)

function getParallelSegment(A, B, d, side) {
    // --- Return a line segment parallel to AB, d pixels away
    var dx = A.x - B.x,
        dy = A.y - B.y,
        dist = Math.sqrt(dx*dx + dy*dy) / 2;
    side = side || 1;
    dx *= side * d / dist;
    dy *= side * d / dist;
    return [P(A.x + dy, A.y - dx), P(B.x + dy, B.y - dx)];
}

The problem is that these line segments don't meet up and sometimes overlap (see JSFiddle), so you get something like this.

To make the segments join up, we have to extend each segment to the point of intersection with the following segment.

function getIntersection(A, B, C, D) {
    // --- Get intersection between lines AB and CD
    // See https://en.wikipedia/wiki/Line%E2%80%93line_intersection
    var ABdx = A.x - B.x,
        ABdy = A.y - B.y,
        CDdx = C.x - D.x,
        CDdy = C.y - D.y,
        ABd = A.x * B.y - A.y * B.x,
        CDd = C.x * D.y - C.y * D.x,
        den = ABdx * CDdy - ABdy * CDdx;
    return P((ABd * CDdx - ABdx * CDd) / den, (ABd * CDdy - ABdy * CDd) / den);
}
function getParallelPolyline(poly, distance, side) {
    // For a path [{x: x1, y: y2}, {x: x2, y: y2}, etc.] returns a parallel path
    var i, nextSegment,
        segment = getParallelSegment(poly[0], poly[1], distance, side),
        r = [segment[0]];
    for (i = 1; i < poly.length - 1; i++) {
        nextSegment = getParallelSegment(poly[i], poly[i + 1], distance, side);
        r.push(getIntersection(segment[0], segment[1], nextSegment[0], nextSegment[1]));
        segment = nextSegment;
    }
    r.push(segment[1]);
    return r;
}

This works with many but not all shapes (JSFiddle). For shapes like the one below (attempted parallel line in blue, original in black) you may have to define the expected behaviour more precisely. The problem is that for any shape there are 2 potential lines parallel to each segment. You need to define a way of deciding which side each segment should be on, perhaps by choosing whichever segment does not cause the parallel line to intersect the original line.

You have here a small example of two parallel lines. And here you have different tips to help on coloring and different aspects. It should be enough to get you started :)

<!DOCTYPE html>
<html>
<body>

<canvas id="myCanvas" width="400" height="400" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>

<script>

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.moveTo(0,0);
ctx.lineTo(200,100);
ctx.stroke();

ctx.moveTo(0,100);
ctx.lineTo(200,200);
ctx.stroke();

</script>

</body>
</html>
发布评论

评论列表(0)

  1. 暂无评论