Created
March 15, 2021 00:08
-
-
Save nasser/254768e2cab1d2404f2f2f536751b9f2 to your computer and use it in GitHub Desktop.
SVG Hatch Fill Generator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const distance = (x1, y1, x2, y2) => | |
Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)) | |
const approx = (a, b) => | |
Math.abs(a - b) < 0.00001 | |
/** | |
* Compute the intersection of a line segment and a line | |
* | |
* @param x1 x coordinate of the first endpoint of the line segment | |
* @param y1 y coordinate of the first endpoint of the line segment | |
* @param x2 x coordinate of the second endpoint of the line segment | |
* @param y2 y coordinate of the second endpoint of the line segment | |
* @param x3 x coordinate of the first endpoint of the line | |
* @param y3 y coordinate of the first endpoint of the line | |
* @param x4 x coordinate of the second endpoint of the line | |
* @param y4 y coordinate of the second endpoint of the line | |
* @returns an { x, y } object if an intersection exists, null otherwisex | |
* @see https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection | |
*/ | |
function intersection(x1, y1, x2, y2, x3, y3, x4, y4) { | |
const d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) | |
if (d === 0) return null | |
const x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d | |
const y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d | |
if (approx(distance(x1, y1, x2, y2), distance(x1, y1, x, y) + distance(x2, y2, x, y))) | |
return { x, y } | |
return null | |
} | |
/** | |
* Generate a hatch fill pattern for a polygon described by `path` | |
* | |
* @param {SVGPathElement} path the bounding polygon of the hatch fill. requires | |
* getPathData API which needs to be polyfilled on chrome. | |
* @param {number} angle the angle of the hatch lines in degrees | |
* @param {number} dist the distance between the hatch lines | |
* @returns am array of {x, y} objects representing points that make up the | |
* hatch lines. each pair of points in the array represents one line. | |
*/ | |
function hatch(path, angle, dist) { | |
let result = [] | |
// normalize angle in case its negative or > 360 | |
angle = angle % 360 | |
if (angle < 0) angle = 360 + angle | |
const theta = angle * (Math.PI / 180) | |
// build list of line segments and x/y min/max from the path data | |
const pathData = path.getPathData() | |
const segments = [] | |
let lastx = 0, lasty = 0 | |
let xmin = Infinity, xmax = -Infinity | |
let ymin = Infinity, ymax = -Infinity | |
for (const p of pathData) { | |
const [x, y] = p.values | |
switch (p.type) { | |
case "m": | |
lastx += x | |
lasty += y | |
break; | |
case "l": | |
segments.push([[lastx, lasty], [lastx + x, lasty + y]]) | |
lastx += x | |
lasty += y | |
break; | |
case "L": | |
segments.push([[lastx, lasty], [x, + y]]) | |
lastx = x | |
lasty = y | |
break; | |
// TODO Z ? | |
} | |
if (lastx > xmax) xmax = lastx | |
if (lastx < xmin) xmin = lastx | |
if (lasty > ymax) ymax = lasty | |
if (lasty < ymin) ymin = lasty | |
} | |
// pick corners of bounding box to use as start and end | |
// depends on provided angle | |
let xstart, ystart, xend, yend | |
if ((theta >= 0 && theta < Math.PI / 2) | |
|| (theta >= Math.PI && theta < Math.PI + Math.PI / 2)) { | |
xstart = xmin | |
ystart = ymax | |
xend = xmax | |
yend = ymin | |
} else if ((theta >= Math.PI / 2 && theta < Math.PI) | |
|| (theta >= Math.PI + Math.PI / 2 && theta < Math.PI * 2)) { | |
xstart = xmin | |
ystart = ymin | |
xend = xmax | |
yend = ymax | |
} | |
// iterate across diagonal formed by bounding box corners | |
// project infinite line from each point and gather intersections | |
const diagonal = distance(xstart, ystart, xend, yend); | |
for (let d = 0; d < diagonal; d += dist) { | |
const t = d / diagonal | |
let x = xstart * (1 - t) + t * xend | |
let y = ystart * (1 - t) + t * yend | |
result = result.concat(intersections(x, y, theta)) | |
} | |
return result | |
function intersections(sx, sy, theta) { | |
const ret = [] | |
const x3 = sx - Math.cos(theta) * diagonal | |
const y3 = sy - Math.sin(theta) * diagonal | |
const x4 = x3 + Math.cos(theta) | |
const y4 = y3 + Math.sin(theta) | |
for (const [[x1, y1], [x2, y2]] of segments) { | |
const hit = intersection(x1, y1, x2, y2, x3, y3, x4, y4) | |
if (hit) | |
ret.push(hit) | |
} | |
ret.sort((a, b) => | |
distance(a.x, a.y, x3, y3) - distance(b.x, b.y, x3, y3)) | |
return ret | |
} | |
} | |
function demo() { | |
const svg = document.querySelector("svg") | |
const path = document.querySelector("some path element") | |
let lines = hatch(path, 45, 10) | |
for (let i = 0; i < lines.length - 1; i += 2) { | |
const a = lines[i]; | |
const b = lines[i + 1]; | |
const line = document.createElementNS("http://www.w3.org/2000/svg", "line") | |
line.style = "stroke: black;" | |
line.setAttribute("x1", a.x) | |
line.setAttribute("y1", a.y) | |
line.setAttribute("x2", b.x) | |
line.setAttribute("y2", b.y) | |
svg.appendChild(line) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdn.jsdelivr.net/gh/jarek-foksa/path-data-polyfill@b0c846f/path-data-polyfill.js"></script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment