I'm trying to programatically create a svg node like this
const parser = new DOMParser()
const doc = parser.parseFromString(mySvgAsString, 'image/svg+xml')
const svg = doc.children[0]
Everything seems to work fine, however when I try to use the isPointInFill
method, it always returns false. Like this:
const pathNode = svgNode.children[0]
const point = svgNode.createSVGPoint()
point.x = 5
point.y = 5
pathNode.isPointInFill(p) // always false
I created a fiddle that loads a svg square when iterates throught a 10 * 10 matrix - they all return false.
Fiddle https://jsfiddle.net/9hyp51cg/
I noticed that also functions like getBBox() on the svg return 0 as width / height. I'd suppose this is connected to this behaviour
const $ul = document.getElementById("list")
// basically a square
const svgString = `<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 0.5H1V9.5H10V0.5Z" fill="#D9D9D9" stroke="black"/></svg>`
const parser = new DOMParser()
const doc = parser.parseFromString(svgString, 'image/svg+xml')
const svgNode = doc.children[0]
const pathNode = svgNode.children[0]
const point = svgNode.createSVGPoint()
for (let i = 0; i <= 10; i++) {
for (let j = 0; j <= 10; j++) {
point.x = i;
point.y = j;
$ul.innerHTML = $ul.innerHTML + `<li>${i},${j} => ${pathNode.isPointInFill(point)}</li>`;
}
}
<ul id="list"></ul>
Methods like isPointInFill()
, isPointInStroke()
or getBBox()
require elements to be rendered. Otherwise the browser can't calculate point/element intersections. (In fact Firefox can test isPointInFill()
for not rendered elements but that's obviously not enough for a cross-browser solution)
You can hide the svg by applying a style like so
position:absolute; height:0; width:0
.
This way you don't have to worry about layout shifts.
const $ul = document.getElementById("list");
// basically a square
const svgString = `<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 0.5 H1V9.5H10V0.5Z" fill="#D9D9D9" stroke="black"/></svg>`;
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, "image/svg+xml");
const svgNode = doc.children[0];
const pathNode = svgNode.children[0];
// append svg and hide it
svgNode.setAttribute("style", "position:absolute; height:0; width:0;");
document.body.append(svgNode);
const point = svgNode.createSVGPoint();
point.x = 0;
point.y = 1;
for (let i = 0; i <= 10; i++) {
for (let j = 0; j <= 10; j++) {
point.x = i;
point.y = j;
$ul.innerHTML =
$ul.innerHTML +
`<li>${i}, ${j} => ${pathNode.isPointInFill(point)} </li>`;
}
}
//remove svg
svgNode.remove();
<ul id="list"></ul>
isPointInPath()
The canvas drawing API provides a similar method that doesn't require a rendered element at all.
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
let d = pathNode.getAttribute("d");
let pathCanvas = new Path2D(d);
let pointInPathCanvas = ctx.isPointInPath(pathCanvas, x, y);
Another benefit of this approach is that it's usually significantly faster than it's SVG counterpart (especially on chromium/blink based browsers).
See also "I have a problem with Chrome's version of SVGGeometryElement.isPointInFill() (works in Firefox)"
const $ul = document.getElementById("list");
// basically a square
const svgString = `<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 0.5 H1V9.5H10V0.5Z" fill="#D9D9D9" stroke="black"/></svg>`;
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, "image/svg+xml");
const svgNode = doc.children[0];
const pathNode = svgNode.children[0];
// draw svg path to canvas
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
let d = pathNode.getAttribute("d");
let pathCanvas = new Path2D(d);
for (let i = 0; i <= 10; i++) {
for (let j = 0; j <= 10; j++) {
let pointInPathCanvas = ctx.isPointInPath(pathCanvas, i, j);
$ul.innerHTML =
$ul.innerHTML +
`<li>${i}, ${j} => ${pointInPathCanvas} </li>`;
}
}
<ul id="list"></ul>
This concept is explained in detail in this post "How can I determine whether a 2D Point is within a Polygon?" and may be an alternative for headless or virtual DOM environments.
To apply this ray-cast approach to SVG <path>
elements, we need some extra steps:
I've collected my tests in this github repository - not well tested and not ready for production.
let svg = document.querySelector("svg");
let path = document.querySelector("path");
let d = path.getAttribute("d");
//split bezier segments for polygon approximation
let precision = 12;
let pathData = parsePathDataNormalized(d, {
arcsToCubic: true
});
let compoundPoly = pathDataToCompoundPoly(pathData, precision)
/**
* test performance
* random points
*/
let maxPoints = 130 * 130;
let cols = 130;
let rows = 130;
let pts = [];
let x = 0
let y = 0
for (let i = 0; i <= maxPoints; i++) {
let pt = {
x: x,
y: y
};
x++
if (x >= cols) {
x = 0;
y++
}
pts.push(pt);
}
/**
* 1. raycast speed test
*/
let pixels = [];
t0 = performance.now();
pts.forEach((pt) => {
let pointInPoly = isPointInFillCompoundPolygon(compoundPoly, pt);
//let pointInPoly = isPointInFillPathData(d, pt, precision )
if (pointInPoly) pixels.push(pt);
});
t1 = performance.now() - t0;
console.log("points in poly raycast", t1, 'ms total points:', pts.length);
pixels.forEach((pt) => {
renderPoint(svg2, pt, "green", "0.25%");
});
// update transform matrix
let matrix = svg.getScreenCTM().inverse();
window.addEventListener("resize", (e) => {
matrix = svg.getScreenCTM().inverse();
});
document.addEventListener("mousemove", (e) => {
// move svg cursor
let svgCursor = screenToSVG({
x: e.clientX,
y: e.clientY
}, matrix);
cursor.setAttribute("cx", svgCursor.x);
cursor.setAttribute("cy", svgCursor.y);
// highlight
let pointInPoly = isPointInFillCompoundPolygon(compoundPoly, svgCursor);
if (pointInPoly) {
cursor.setAttribute("fill", "green");
} else {
cursor.setAttribute("fill", "red");
}
});
//just for illustration
function renderPoint(
svg,
coords,
fill = "red",
r = "2",
opacity = "1",
id = "",
className = ""
) {
//console.log(coords);
if (Array.isArray(coords)) {
coords = {
x: coords[0],
y: coords[1]
};
}
let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
<title>${coords.x} ${coords.y}</title></circle>`;
svg.insertAdjacentHTML("beforeend", marker);
}
/**
* helper function to translate between
* svg and HTML DOM coordinates:
* based on @Paul LeBeau's anser to
* "How to convert svg element coordinates to screen coordinates?"
* https://stackoverflow.com/questions/48343436/how-to-convert-svg-element-coordinates-to-screen-coordinates/48354404#48354404
*/
function screenToSVG(p, matrix) {
return new DOMPoint(p.x, p.y).matrixTransform(matrix);
}
.grd {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2em
}
<h1>Pointilize</h1>
<p>Move your mouse over the left svg. If it's in fill the fill changes to green.</p>
<div class="grd">
<svg id="svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130 130" overflow="visible">
<path fill="#ccc" class="glyph"
d="M28 99.6 L28 99.6 Q19.8 99.6 13.5 95.4 Q7.1 91.2 3.5 83.5 Q0 75.8 0 65.3 L0 65.3 Q0 54.8 3.5 47.3 Q7.1 39.7 13.5 35.7 Q19.8 31.6 28 31.6 L28 31.6 Q36.3 31.6 42.6 35.7 Q48.9 39.7 52.5 47.3 Q56.1 54.8 56.1 65.3 L56.1 65.3 Q56.1 75.8 52.5 83.5 Q48.9 91.2 42.6 95.4 Q36.3 99.6 28 99.6 ZM28 92.3 L28 92.3 Q33.9 92.3 38.3 89 Q42.6 85.6 45 79.6 Q47.4 73.5 47.4 65.3 L47.4 65.3 Q47.4 53.1 42.1 46 Q36.8 38.9 28 38.9 L28 38.9 Q19.2 38.9 13.9 46 Q8.6 53.1 8.6 65.3 L8.6 65.3 Q8.6 73.5 11 79.6 Q13.4 85.6 17.8 89 Q22.2 92.3 28 92.3 ZM85.8 120.8 L85.8 120.8 Q76.9 120.8 71.3 117.4 Q65.7 114 65.7 107.7 L65.7 107.7 Q65.7 104.6 67.6 101.8 Q69.5 98.9 72.8 96.7 L72.8 96.7 L72.8 96.3 Q71 95.2 69.8 93.2 Q68.5 91.2 68.5 88.4 L68.5 88.4 Q68.5 85.3 70.2 83 Q71.9 80.7 73.8 79.4 L73.8 79.4 L73.8 79 Q71.4 77 69.5 73.6 Q67.5 70.2 67.5 65.9 L67.5 65.9 Q67.5 60.6 70 56.7 Q72.5 52.8 76.7 50.7 Q80.9 48.6 85.8 48.6 L85.8 48.6 Q87.8 48.6 89.6 49 Q91.4 49.3 92.7 49.8 L92.7 49.8 L109.6 49.8 L109.6 56.1 L99.6 56.1 Q101.3 57.7 102.5 60.4 Q103.6 63 103.6 66.1 L103.6 66.1 Q103.6 71.3 101.2 75.1 Q98.8 78.9 94.8 81 Q90.8 83 85.8 83 L85.8 83 Q81.9 83 78.5 81.3 L78.5 81.3 Q77.2 82.4 76.3 83.8 Q75.4 85.1 75.4 87.1 L75.4 87.1 Q75.4 89.4 77.3 90.9 Q79.1 92.4 84 92.4 L84 92.4 L93.4 92.4 Q101.9 92.4 106.2 95.2 Q110.4 97.9 110.4 104 L110.4 104 Q110.4 108.5 107.4 112.3 Q104.4 116.1 98.9 118.5 Q93.4 120.8 85.8 120.8 ZM85.8 77.5 L85.8 77.5 Q90 77.5 93.1 74.4 Q96.1 71.2 96.1 65.9 L96.1 65.9 Q96.1 60.6 93.1 57.6 Q90.1 54.6 85.8 54.6 L85.8 54.6 Q81.5 54.6 78.5 57.6 Q75.5 60.6 75.5 65.9 L75.5 65.9 Q75.5 71.2 78.6 74.4 Q81.6 77.5 85.8 77.5 ZM87 115.1 L87 115.1 Q94 115.1 98.2 112.1 Q102.4 109 102.4 105.2 L102.4 105.2 Q102.4 101.8 99.9 100.5 Q97.3 99.2 92.6 99.2 L92.6 99.2 L84.2 99.2 Q82.8 99.2 81.2 99 Q79.5 98.8 77.9 98.4 L77.9 98.4 Q75.3 100.3 74.1 102.4 Q72.9 104.5 72.9 106.6 L72.9 106.6 Q72.9 110.5 76.7 112.8 Q80.4 115.1 87 115.1 Z ">
</path>
<circle id="cursor" cx="50%" cy="50%" r="1" fill="red" />
</svg>
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130 130" overflow="visible">
</svg>
</div>
<script src="https://cdn.jsdelivr.net/gh/herrstrietzel/point-in-fill-raycast@main/js/point-in-fill-raycast.js"></script>