document.elementFromPoint returning different elements for same input
I am using document.elementFromPoint
to figure out what svg shape is at a certain point. I was experimenting with elementFromPoint
on this image when I noticed that I was getting different outputs for the same input.
Here is exactly what I did for reproducibility.
- I cut and pasted the svg tag from the linked svg and inserted it into the body of an html file. The html file was set up using the single ! emmet shortcut in vscode. Nothing else was in the body. The only css was
body{margin:0;}
. - In chrome I went around calling
elementFromPoint
. Mostly at .25,50 but sometimes just a little to the right or a little to the left. Sometimes calling with the same arguments over and over again to see if it would change. - There was no scrolling done during this time. There wasn't even an option as there was no scroll bar present.
My question is why does this happen? Is there a pattern to it? Is there a way to prevent it? Thanks.
1 answer
-
answered 2022-01-25 16:29
ccprog
While I can't tell you why this happens, or how to prevent it, the "pattern", or let's better say the reason for the inconsistent behavior can be narrowed down.
Let's look at the paths that are returned by your calls to
elementFromPoint
. There are two of them, and if you leave out the ids and classes, both look identical, even taking into consideration their parent elements:<g transform="translate(-1.775,-1.575)"> <path d="M 1.9,551.3 V 1.7 H 1102 V 551.3 H 2.3" /> </g>
If you rewrite the path such that the
transform
is resolved, you get a path in (browser) viewport coordinates - for clarity, I have rewritten theV
andH
commands asL
:<path d="M 0.125,549.725 L 0.125,0.125 L 1100.125,0.125 L 1100.125,549.725 L 0.525,549.725" />
The SVG contains a stylesheet that describes the filling and stroking of these elements. This is the relevant excerpt:
.seabase { fill: #C6ECFF; stroke: none; } .mapborder { fill: none; stroke: #0978AB; } path, circle { stroke-width: 0.25; }
So what should be returned from
document.elementFromPoint(0.25,50)
? The topmost element to be found at the given coordinates, provided thepointer-events
property allows the element to be the target of a hit-test.pointer-events
is not explicitely set for the elements, so the default value ofvisiblePainted
applies. This means that an (at least partially) opaque fill or stroke both can be hit.The bottom element of the two candidates,
<path class="seabase"/>
, has a visible fill, but no border. Point(0.25,50)
is inside that fill.The topmost element of the two candidates,
<path class="mapborder"/>
, has no fill but a visible border of width 0.25, extending half to the inside and half to the outside of the path outline. At the left side, there is a vertical subpathM 0.125,549.725 L 0.125,0.125
. Not going into the details of corners, the stroke could be drawn equivalently as a filled path with a definitionM 0,549.725 L 0,0.125 L 0.25,0.125 L 0.25,549.725 Z
In other words, the point
(0.25,50)
is exactly on the outline of the stroke. The spec is quite clear about the consequences:The zero-width geometric outline of a shape is included in the area to be painted.
The point should hit the
mapborder
element, as the point is part of its visible, painted stroke. If, as you describe, the browser sometimes returns theseabase
element, it is in error.I could go on and speculate about the reasons for this behavior, but that is moot, I think.
Can you prevent it? Only if you would be able to consistently avoid the exact outline of all paths, or if they have a stroke, the outline of that stroke. That's hardly practical, since you would have to find out if the browser missed something it should have hit, while you don't know what was missed.
Finally, there is a second interface that can be used. It is part of the SVG specification, while
elementFromPoint
is part of the CSSOM spec. Whether that makes a difference in the browser implementation, I cannot say.document.querySelector('.seabase').isPointInFill(new DOMPoint(2.025,48.425)); document.querySelector('.mapborder').isPointInStroke(new DOMPoint(2.025,48.425));
Note that you need to provide an element to test against, that you get no information which element is on top, and that the coordinates are expressed in local userspace (before
transform
is applied).
do you know?
how many words do you know