问题
I'm trying to work out how to draw a squiggly line on an arbitrary SVG path
element. The path is generated by a React Component. For example, I'm trying to replicate the line in this question:
Is there an easy way to do this in SVG and/or JavaScript generated paths?
I've considered joining up a series of curves using the s
path command, but then I would need to calculate points along the curve. I've also considered some sort of displacement filter but I'm not really sure where to start.
回答1:
Seems to me the easiest way would be step along the path. Then, at each step, insert a quadratic bezier curve with a control point that is half way between them and perpendicular to the curve. Then for the next step switch the side that the control point is on.
function makeSquiggle(squigglePathId, followPathId, squiggleStep, squiggleAmplitude)
{
var followPath = document.getElementById(followPathId);
var pathLen = followPath.getTotalLength();
// Adjust step so that there are a whole number of steps along the path
var numSteps = Math.round(pathLen / squiggleStep);
var pos = followPath.getPointAtLength(0);
var newPath = "M" + [pos.x, pos.y].join(',');
var side = -1;
for (var i=1; i<=numSteps; i++)
{
var last = pos;
var pos = followPath.getPointAtLength(i * pathLen / numSteps);
// Find a point halfway between last and pos. Then find the point that is
// perpendicular to that line segment, and is squiggleAmplitude away from
// it on the side of the line designated by 'side' (-1 or +1).
// This point will be the control point of the quadratic curve forming the
// squiggle step.
// The vector from the last point to this one
var vector = {x: (pos.x - last.x),
y: (pos.y - last.y)};
// The length of this vector
var vectorLen = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
// The point halfwasy between last point and tis one
var half = {x: (last.x + vector.x/2),
y: (last.y + vector.y/2)};
// The vector that is perpendicular to 'vector'
var perpVector = {x: -(squiggleAmplitude * vector.y / vectorLen),
y: (squiggleAmplitude * vector.x / vectorLen)};
// No calculate the control point position
var controlPoint = {x: (half.x + perpVector.x * side),
y: (half.y + perpVector.y * side)};
newPath += ("Q" + [controlPoint.x, controlPoint.y, pos.x, pos.y].join(','));
// Switch the side (for next step)
side = -side;
}
var squigglePath = document.getElementById(squigglePathId);
squigglePath.setAttribute("d", newPath);
}
makeSquiggle("squiggle", "follow", 25, 20);
#follow {
fill: none;
stroke: grey;
stroke-width: 2;
}
#squiggle {
fill: none;
stroke: red;
stroke-width: 2;
}
<svg width="500" height="400">
<path id="follow" d="M 50,300 C 100,100 300,0, 350,250 L 450,200"/>
<path id="squiggle" d="M0,0"/>
</svg>
回答2:
Best way would probably just join up Bézier curves and just offset and invert the values for every subsequent curve you wanted to create until you reached the desired length.
回答3:
Functional like solution
ATTENTION: f(x) is the curve that your squiggly line should follow and x0 is where you start to draw the squiggly line and xn is where you end this example assume that f(x) between x0 and xn is constantly growing
If you have a rasterised curve and the squiggly line that you want to draw is cos(x), you can find the points for draw the line as below:
- squiggly line the line that you want to draw
- curve the curve that squiggly line should follow
- point0 is the point where curve start
- pointN is the point in curve for x == N
- lenN is the length of the curve from point0 to pointN
- h (in the image) is the distance between curve and squiggly line in pointN and is cos(lenN)
- alphaN is the angle between the tangent of curve for x == n and the x-axis
- a (in the image) is -cos(alphaN)
- b (in the image) is sin(alphaN)
- squigglyPointN is pointN.x + a, pointN.y + b
'use strict'
//
// point = [int, int], point[0] = x, point[1] = y
// rasterizedCurve = [point0, ...,pointN]
//
// int -> [int,...,int]
function rangeFrom1ToN(N) {
return Array(N).fill(0).map((x, index) => index).slice(1);
}
// [int, ...,int] -> [float, ..., float]
function expandRange(Range, precision) {
return Range.map(x => rangeFrom1ToN(precision).map((y, index) => x + 1/precision * index))
.reduce((acc, val) => acc.concat(val));
}
function formatForSvg(points) {
return points.map(x => x.toString()).reduce((acc, val) => {return acc + ' ' + val})
}
// rasterizedCurve, index -> int
function derivative(curve, index){
//
// return dx' curve(x)
//
if (index === 0) {
return 0;
}
const point1 = curve[index - 1];
const point2 = curve[index];
return (point2[1] - point1[1]) / (point2[0] - point1[0]);
}
// rasterizedCurve -> rasterizedCurve
function squiggleAroundCurve(x, y, curve, index) {
const len = lenCurve(curve, index);
const h = Math.sin(len);
const b = Math.sin(Math.atan2(1, derivative(curve, index))) * h;
const a = Math.cos(Math.atan2(1, derivative(curve, index))) * h;
x -= a;
y += b;
return [x, y];
}
function pow2(x) {
return Math.pow(x,2);
}
function dist(point1, point2) {
return Math.sqrt(pow2(point2[0] - point1[0]) + pow2(point2[1] - point1[1]))
}
// rasterizedCurve, int -> int
function lenCurve(rasterizedCurve, index) {
const curve = rasterizedCurve.slice(0, index);
return curve.reduce((sum, point, index) => {
let len = 0;
if (index > 0) {
len = dist(point, curve[index - 1]);
}
return sum + len;
}, 0);
}
const Curve = expandRange(rangeFrom1ToN(90),50).map(x => [x, (Math.log(x) * 15)]);
const SquiggledCurve = Curve.map((point, index) => squiggleAroundCurve(point[0], point[1], Curve, index))
function zoom(curve, w) {
return curve.map(point => [point[0] * w, point[1] * w]);
}
function getNode(n, v) {
n = document.createElementNS("http://www.w3.org/2000/svg", n);
for (var p in v)
n.setAttributeNS(null, p.replace(/[A-Z]/g, function(m, p, o, s) { return "-" + m.toLowerCase(); }), v[p]);
return n
}
var svg = getNode("svg");
setTimeout(function() {
document.body.appendChild(svg);
const r = getNode('polyline', { points:formatForSvg(zoom(SquiggledCurve, 10)), fill:'none', stroke:'black'});
const c = getNode('polyline', { points:formatForSvg(zoom(Curve, 10)), fill:'none', stroke:'black'});
svg.appendChild(r);
svg.appendChild(c);
}, 1000);
svg {
width: 1100px;
height: 900px;
}
来源:https://stackoverflow.com/questions/42441472/draw-a-squiggly-line-in-svg