最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Draw a "squiggly line" in SVG - Stack Overflow

programmeradmin2浏览0评论

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.

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.

Share Improve this question asked Feb 24, 2017 at 14:45 will-hartwill-hart 3,8312 gold badges40 silver badges49 bronze badges 0
Add a comment  | 

4 Answers 4

Reset to default 15

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>

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

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.

I am glad I found this question, I was trying to draw a wavy line in SVG for a long time. Based on Paul's answer I created small reusable function for my needs:

/**
 * Creates a "wavy" path following the given path with a specified amplitude
 * and stepLength values.
 */
export function createWavyPath(path: string, stepLength: number, amplitude: number) {
    const referencePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
    referencePath.setAttribute("d", path);

    const pathLength = referencePath.getTotalLength();
    const stepCount = Math.round(pathLength / stepLength);

    let pos = referencePath.getPointAtLength(0);
    const resultPath = [ `M ${pos.x},${pos.y}` ];
    let orientation = -1;

    for (let i = 1; i <= stepCount; i++) {
        const last = pos;
        pos = referencePath.getPointAtLength(i * pathLength / stepCount);

        // 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 'orientation' (-1 or +1).
        // This point will be the control point of the quadratic curve forming the
        // squiggle stepLength.
        
        // The vector from the last point to this one
        const vector = {
            x: pos.x - last.x,
            y: pos.y - last.y
        };

        const vectorLength = Math.sqrt(vector.x * vector.x + vector.y * vector.y);

        const mid = {
            x: last.x + vector.x / 2,
            y: last.y + vector.y / 2
        };

        const normalVector = {
            x: -amplitude * vector.y / vectorLength,
            y: amplitude * vector.x / vectorLength
        };

        const controlPoint = {
            x: mid.x + normalVector.x * orientation,
            y: mid.y + normalVector.y * orientation
        };

        resultPath.push(`Q ${controlPoint.x},${controlPoint.y},${pos.x},${pos.y}`);

        orientation *= -1;
    }

    return resultPath.join(" ");
}
发布评论

评论列表(0)

  1. 暂无评论