Draw a “squiggly line” in SVG

微笑、不失礼 提交于 2021-01-21 10:18:39

问题


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:

Squiggly Line Example

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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!