如何使用 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-margin
和 shape-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>
对不起,我的数学知识很差。
如何绘制平行线:
这是我当前的代码:
<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-margin
和 shape-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>