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

3

很抱歉我数学知识不太好。

我该如何画出这样的平行线:

这是我的当前代码:

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

这是我的糟糕结果(我用红圈标记了):

输入图片描述


2
请仅返回翻译后的文本:请发布您现有的代码 - aurelius
1
请问您能否进一步解释您想要实现的目标? - Sean_A91
@aurelius 我已经更新了。感谢您的提醒。 - dphans
这是一个复杂的问题。Clipper 是一个 C++ 开源库,可以实现多边形偏移。而这篇文章则介绍了使用 OpenGL 解决类似问题的方法。 - arthur.sw
这个的技术术语是多边形偏移。 - nicholaswmin
3个回答

6

enter image description here

您需要创建凸出和倾斜路径以源路径为基础,这让我想起了Hans Muller的一篇相关博客文章。

注明来源:

Hans Muller撰写了多篇关于在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填充路径,从而创建所需的平行路径。

这是来自该文章的演示,显示了给定路径内外的“平行”路径:

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>


1
我按照你的方式进行了操作,现在一切都正常了。非常感谢你! - dphans

2
这是一个比起一开始看起来更为复杂的问题。暂时不考虑在画布上绘制线条的技术细节,将其视为一组点定义的线条。
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),因此你会得到类似于这样的东西。

First attempt at parallel lines

为了使线段相连,我们必须将每个线段延伸到与其后续线段相交的点。
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)。对于像下面的形状(蓝色尝试平行线,黑色为原始线),您可能需要更精确地定义期望的行为。问题在于对于任何形状,每个线段都有两条潜在的平行线。您需要定义一种决定每个线段应该在哪一侧的方法,也许是通过选择不会导致平行线与原始线相交的线段。

Parallel line function not working


1

您在这里看到了两条平行线的小例子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>

抱歉我的解释不够清楚。谢谢你,但这不是我期望的答案。请查看我的更新。 - dphans

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接