如何使用 canvas PaperJS 绘制平行线? (Canvas/Javascript)

How to draw parallel lines with canvas PaperJS? (Canvas/Javascript)

对不起,我的数学知识很差。

如何绘制平行线:

这是我当前的代码:

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

我正在使用 PaperJS (http://paperjs.org) :

<script type='text/javascript' src='http://paperjs.org/assets/js/paper.js'></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>

这是我不好的结果(我用红圈圈出来了):

你有 here 两条平行线的小例子。 here 你有不同的技巧来帮助着色和不同方面。 它应该足以让你开始:)

<!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>

这是一个比最初看起来更复杂的问题。将在 canvas 上绘图的技术放在一边,并考虑像这样定义为点数组的线

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

其中 P 只是一个将一对坐标转换为具有 x 和 y 属性的对象的函数

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

第一次尝试是绘制与原始路径的每一段平行的线。你可以使用像这样的函数(基于 this answer 得到垂直于原始线的点)

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)];
}

问题是这些线段不相交,有时会重叠(参见 JSFiddle),所以你会得到这样的结果。

为了使线段连接起来,我们必须将每个线段延伸到与下一条线段的交点。

function getIntersection(A, B, C, D) {
    // --- Get intersection between lines AB and CD
    // See https://en.wikipedia.org/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;
}

这适用于许多但并非所有形状 (JSFiddle)。对于像下面这样的形状(蓝色尝试平行线,黑色原始线),您可能需要更精确地定义预期行为。问题是对于任何形状,每个线段都有 2 条平行的潜在线。您需要定义一种方法来决定每个线段应该在哪一侧,也许是通过选择不会导致平行线与原始线相交的线段。

您需要为源路径创建拉伸和倒角路径,这让我想起了 Hans Muller 的这篇即时博客 post。

署名说明:

Hans Muller 撰写了多篇博客 post,介绍在 Webkit 和 Blink 中提供 CSS shape-marginshape-padding 所做的工作。

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

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

计算形状外部 CSS 边距路径和形状内部 CSS 填充路径的相同代码可用于创建平行路径。

这是来自 post 的演示,显示给定路径内外的 "parallel" 路径:

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.net/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.com/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);
        computeAll();
      }
    }
  }
  else
  {
    polygon.closed = polygonVertexNear(eventXY) != null;
    if (!polygon.closed)
      polygon.vertices.push(eventXY);
    else 
      computeAll();
  }

  // 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;
    computeAll();
    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;

  computeAll();
  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 computeAll()
{
  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;
  } 

  computeAll();
  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>