The essence of the problem is that when I move an SVG element around, the widths of the lines varies.
I'm working in a browser environment and I want crisp lines (no anti-aliasing), so shape-rendering is set to crispEdges. For example, you might have
"d M 0 0 L 10 0 L 10 10 L 0 10 z"
to define a square, with style set to
'fill: none; stroke: black; stroke-width: 1px; shape-rendering: crispEdges'
Then use translate() to drag it around with the mouse. It works, but as the square moves, the thickness of the lines jumps around by a pixel, and I want the thickness to remain constant.
I think I know why it's happening, but I can't find any way to prevent it. It happens (I think) due to the way the coordinates relative to the viewBox are mapped to coordinates relative to the physical monitor's pixels. When a one pixel wide (in SVG terms) line is mapped to the monitor, it may or may not straddle several pixels, so it may end up a (screen) pixel thicker or thinner.
I've tried messing with the ratio of the viewBox size to the size of the drawing area, rounding the translation amount to be a whole number or to take the form integer + 0.50, but no solution like this seems likely to work because it comes down to the level of zoom in the browser. vector-effect: non-scaling-stroke doesn't fix it either.
Is there a way to tell SVG to treat all paths as 1d geometric shapes, and to use a particular pen for all of them? Put another way, I want the pen used to draw everything to "scale with the zoom", rather than having the lines scale after they've been drawn.
Please, vanilla JS only.
You can test what I mean by running the code below. Click on the square and drag it around (no touch devices). Note that at some levels of browser zoom, it may behave nicely and not do what I've described.
There's something odd going on. The svg element (the square) starts out looking uniform, with all edges of the same width. It only moves in response to a mousemove, and that event (presumably) happens when the mouse moves by a whole (never fractional) screen pixel. Therefore, one would expect the square to move by a whole screen pixel and remain aligned to the screen pixels if it started off that way.
Follow-up...The problem is not with the screen CTM. I tested that the inverse really is an inverse; that is, CTM x CTM^{-1} is the identity for every value I tried. And the values that matrixTransform(getScreenCTM().inverse()) provides are linear in the inputs (or as linear as floating-point numbers allow).
<html>
<body>
<svg xmlns="http://www.w3.org/2000/svg" id="svgtest"
style = "border: 1px solid; display: block; margin-left: auto; margin-right: auto; shape-rendering: crispEdges;"
width = "400" height = "400" viewBox = "0 0 100 100" >
</svg>
<script>
var theSquare = {
x : 10,
y : 10
};
var initialSquare = {
x : 10,
y : 10
};
var SideLength = 10;
var initialX = 0;
var initialY = 0;
var draggingSquare = 0;
function pointInRect(p, r) {
return p.x > r.xLeft && p.x < r.xRight && p.y > r.yTop && p.y < r.yBottom;
}
function mouseToLocal(theEvent) {
var screenPt = svgArea.createSVGPoint();
screenPt.x = theEvent.clientX;
screenPt.y = theEvent.clientY;
var svgPt = screenPt.matrixTransform(svgArea.getScreenCTM().inverse());
return {
x : svgPt.x,
y : svgPt.y
};
}
function doMouseDown(event) {
var localCoord = mouseToLocal(event);
initialX = localCoord.x;
initialY = localCoord.y;
var pt = {x: initialX, y: initialY};
var rect = {xLeft: theSquare.x, xRight: theSquare.x + SideLength,
yTop: theSquare.y, yBottom: theSquare.y + SideLength};
if (pointInRect(pt,rect))
{
draggingSquare = 1;
initialSquare.x = theSquare.x;
initialSquare.y = theSquare.y;
}
else
draggingSquare = 0;
}
function doMouseUp(event) {
draggingSquare = 0;
}
function doMouseMove(event) {
if (draggingSquare == 0)
return;
var localCoord = mouseToLocal(event);
zeroTranslate.setTranslate(initialSquare.x + localCoord.x - initialX,
+initialSquare.y + localCoord.y - initialY);
theSquare.x = initialSquare.x + localCoord.x - initialX;
theSquare.y = initialSquare.y + localCoord.y - initialY;
}
var svgArea = document.getElementById('svgtest');
var theSVGSquare = document.createElementNS("http://www.w3.org/2000/svg", 'path' );
theSVGSquare.setAttributeNS(null, "d", "M 0 0" +
" L " + SideLength + " 0" +
" L " + SideLength + " " + SideLength +
" L 0 " +SideLength +
" z");
theSVGSquare.setAttributeNS(null, 'style', 'fill: none; stroke: black; stroke-width: 0.5px; shape-rendering: crispEdges' );
var tforms = theSVGSquare.transform;
var zeroTranslate = svgArea.createSVGTransform();
zeroTranslate.setTranslate(initialSquare.x,initialSquare.y);
theSVGSquare.transform.baseVal.insertItemBefore(zeroTranslate,0);
svgArea.appendChild(theSVGSquare);
svgArea.addEventListener("mousedown",doMouseDown,false);
svgArea.addEventListener("mouseup",doMouseUp,false);
svgArea.addEventListener("mousemove",doMouseMove,false);
</script>
</body>
</html>
I'm posting this "answer" as a way to officially close the question, and to thank Robert Longson for his attention.
In short, what I want to do can't be done in a browser, which is surprising since all I want to do is draw a line. Using the HTML canvas tag produces something blurry due to anti-aliasing, and SVG produces jittery lines. It comes down to the fact that the browser doesn't provide the information needed to work at a device-pixel level of accuracy.
I posted a discussion of this problem, with examples, on my personal website: