Skip to content

Instantly share code, notes, and snippets.

@nasser
Created March 15, 2021 00:08
Show Gist options
  • Save nasser/254768e2cab1d2404f2f2f536751b9f2 to your computer and use it in GitHub Desktop.
Save nasser/254768e2cab1d2404f2f2f536751b9f2 to your computer and use it in GitHub Desktop.
SVG Hatch Fill Generator
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)
}
}
<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