Skip to content

Instantly share code, notes, and snippets.

@nazikus
Forked from mbostock/.block
Last active March 30, 2017 21:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nazikus/707e716208427488ef94012780cdf290 to your computer and use it in GitHub Desktop.
Save nazikus/707e716208427488ef94012780cdf290 to your computer and use it in GitHub Desktop.
Gradient Along Stroke

Fork from Gradient Along Stroke;

Differences from the source:

  • substituded random storke path with circle path
  • circle is drawn with <path>, not <circle>. Attribute "d" is parametrized according to circle definition.
  • added circle completion ratio (percent), e.g. can be used as progress circle.
  • rainbow color interpolation substitude with custom multi-color interpolation ending with the same color.

Original readme

This example demonstrates how to create a gradient that follows a stroke. This technique is sometimes used to indicate directionality along a curved edge, such as with hierarchical edge bundling.

To start, take any SVG path element and uniformly sample points along the path using getPointAtLength. (This method can also be used for path tweening.) Then, for each segment between adjacent points, compute the miter joint via line-line intersection. Lastly fill each segment by interpolating the start and end colors, here green to red, using the normalized length t along the path. Although each segment is a constant color, there are many segments to give the appearance of a continuous gradient.

This example uses a thin stroke in addition to filling the segments. This avoids antialiasing artifacts due to most web browsers not implementing full-scene antialiasing.

<!DOCTYPE html>
<meta charset="utf-8">
<svg width="500" height="500">
<path fill="none" stroke-width="10"></path>
</svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var c = [250, 250]; // center
var r = 200; // radius
var complete = 0.8; // percent
var circlePath = `
M ${c[0]} ${c[1]-r}
a ${r},${r} 0 1,0 0, ${(r * 2)}
a ${r},${r} 0 1,0 0, -${(r * 2)}
Z
`;
var colorInterpolator = d3.interpolateRgbBasis(["rgb(225, 65, 118)", "rgb(108, 125, 255)", "rgb(225, 65, 118)"]);
var colorHandler = (d, i, nodes) => {
let color = d3.color(colorInterpolator(d.t));
color.opacity = i/nodes.length > 1-complete ? 1 : 0;
return color;
};
var path = d3.select("path").attr("d", circlePath).remove();
console.time("[gradient stroke performance]");
d3.select("svg").selectAll("path")
.data(quads(samples(path.node(), 8)))
.enter().append("path")
.style("fill", colorHandler)
.style("stroke", colorHandler)
.style("stroke-width", 30)
.style("stroke-linecap", (d, i, all) => i === 0 || i === all.length ? "round" : "round" )
.attr("d", d => lineJoin(d[0], d[1], d[2], d[3], 32) );
console.timeEnd("[gradient stroke performance]");
// Sample the SVG path uniformly with the specified precision.
function samples(path, precision) {
var n = path.getTotalLength(), t = [0], i = 0, dt = precision;
while ((i += dt) < n) t.push(i);
t.push(n);
return t.map(function(t) {
var p = path.getPointAtLength(t), a = [p.x, p.y];
a.t = t / n;
return a;
});
}
// Compute quads of adjacent points [p0, p1, p2, p3].
function quads(points) {
return d3.range(points.length - 1).map(function(i) {
var a = [points[i - 1], points[i], points[i + 1], points[i + 2]];
a.t = (points[i].t + points[i + 1].t) / 2;
return a;
});
}
// Compute stroke outline for segment p12.
function lineJoin(p0, p1, p2, p3, width) {
var u12 = perp(p1, p2),
r = width / 2,
a = [p1[0] + u12[0] * r, p1[1] + u12[1] * r],
b = [p2[0] + u12[0] * r, p2[1] + u12[1] * r],
c = [p2[0] - u12[0] * r, p2[1] - u12[1] * r],
d = [p1[0] - u12[0] * r, p1[1] - u12[1] * r];
if (p0) { // clip ad and dc using average of u01 and u12
var u01 = perp(p0, p1), e = [p1[0] + u01[0] + u12[0], p1[1] + u01[1] + u12[1]];
a = lineIntersect(p1, e, a, b);
d = lineIntersect(p1, e, d, c);
}
if (p3) { // clip ab and dc using average of u12 and u23
var u23 = perp(p2, p3), e = [p2[0] + u23[0] + u12[0], p2[1] + u23[1] + u12[1]];
b = lineIntersect(p2, e, a, b);
c = lineIntersect(p2, e, d, c);
}
return "M" + a + "L" + b + " " + c + " " + d + "Z";
}
// Compute intersection of two infinite lines ab and cd.
function lineIntersect(a, b, c, d) {
var x1 = c[0], x3 = a[0], x21 = d[0] - x1, x43 = b[0] - x3,
y1 = c[1], y3 = a[1], y21 = d[1] - y1, y43 = b[1] - y3,
ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21);
return [x1 + ua * x21, y1 + ua * y21];
}
// Compute unit vector perpendicular to p01.
function perp(p0, p1) {
var u01x = p0[1] - p1[1], u01y = p1[0] - p0[0],
u01d = Math.sqrt(u01x * u01x + u01y * u01y);
return [u01x / u01d, u01y / u01d];
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment