Closed Twinside closed 6 years ago
It feels like there's something fundamentally wrong here, not sure on what side. I've traced the example stroke_pixel.png
through a bit, this is what I see at one of the questionable pixels in Shading.hs:solidColor (after changing the background to white):
idx 124 cov 128 icov 127
old PixelRGBA8 255 255 255 255
new PixelRGBA8 127 127 127 255
idx 124 cov 128 icov 127
old PixelRGBA8 127 127 127 255
new PixelRGBA8 63 63 63 255
So it seems each half black pixel "doubles" the blackness. That's not what I want here of course.
But! Imagine you're drawing the same line twice, so that you get twice the same half of a pixel. In that case do you really want the pixel to be fully black? A 3/4 black pixel like we get here intuitively seems kind of sensible to me.
(For reference, here's the PR introducing that test: https://github.com/Twinside/Rasterific/pull/41)
Ok I've investigated, and the behavior is normal in this engine. This is part of the anti-aliasing, and anti-aliased edges are alpha blended hence the halving behavior you have seen, we don't accumulate sub-pixel coverage anywhere, but in internal structure while drawing.
So what it means, to get read of a non-full black pixel, you have (or manage to get Diagrams) draw the bunch of lines in a whole call, to reuse your unit test:
strokePixelTest :: (forall g. Stroker g) -> IO ()
strokePixelTest stroker =
produceImageAtSize 10 5 "stroke_pixel.png"
$ withTexture (uniformTexture black)
$ drawing
where
drawing = sequence_ $
[ stroker 1 JoinRound (CapStraight 0, CapStraight 0) (lineFromPath p)
| p <- ps ]
ps = -- line of 1, 2, 3 pixels
[ [ V2 1 1.5, V2 2 1.5 ]
, [ V2 3 1.5, V2 5 1.5 ]
, [ V2 6 1.5, V2 9 1.5 ]
-- line of halved lines
-- [ V2 1 3.5, V2 1.5 3.5 ]
-- , [ V2 1.5 3.5, V2 2 3.5 ]
, [ V2 1 3.5, V2 1.5 3.5, V2 2 3.5 ]
-- , [ V2 3 3.5, V2 4 3.5 ]
-- , [ V2 4 3.5, V2 5 3.5 ]
, [ V2 3 3.5, V2 4 3.5, V2 5 3.5 ]
-- , [ V2 6 3.5, V2 7.5 3.5 ]
-- , [ V2 7.5 3.5, V2 9 3.5 ]
, [ V2 6 3.5, V2 7.5 3.5, V2 9 3.5 ]
]
By stroking both lines beginning at half points in the same call, the engine will perform correct sub-pixel accumulation and provide correct result.
As a small test, I've tried to reproduce a similar test with the HTML5 Canvas API, to see the result:
<!DOCTYPE html>
<html>
<head><title>canvas</title></head>
<body><canvas id="canvas" width="30" height="30"></canvas></body>
<script>
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
const xOffset = 0.5;
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.lineWidth = 1;
ctx.moveTo(1 + xOffset, 3);
ctx.lineTo(1.5 + xOffset, 3);
ctx.stroke();
ctx.beginPath();
ctx.lineWidth = 1;
ctx.moveTo(1.5 + xOffset, 3);
ctx.lineTo(2 + xOffset, 3);
ctx.stroke();
const offset = 0.5;
ctx.beginPath();
ctx.moveTo(1 + xOffset, 10 + offset);
ctx.lineWidth = 1;
ctx.lineTo(1.5 + xOffset, 10 + offset);
ctx.moveTo(1.5 + xOffset, 10 + offset);
ctx.lineTo(2 + xOffset, 10 + offset);
ctx.stroke();
</script>
</html>
So with Firefox (I think it's Cairo for backend) and Chrome (Skia) all I could get was:
So clearly not black :/
Thanks for checking. Yes, I can reproduce this with firefox canvas.
On the other hand I get perfect black pixels with diagrams' cairo backend https://github.com/diagrams/diagrams-cairo. I don't immediately see diagrams-cairo doing anything fancy, so I'm really curious what's going on here...
I think I get it
Looking at diagrams-rasterific:
renderPath :: TypeableFloat n => Path V2 n -> [[R.Primitive]]
renderPath p = -- Implementation not important, the type is important
-- Stroke both dashed and solid lines.
mkStroke :: TypeableFloat n => n -> R.Join -> (R.Cap, R.Cap) -> Maybe (R.DashPattern, n)
-> [[R.Primitive]] -> RenderR ()
mkStroke (realToFrac -> l) j c d primList =
maybe (mapM_ (R.stroke l j c) primList)
(\(dsh, off) -> mapM_ (R.dashedStrokeWithOffset (realToFrac off) dsh l j c) primList)
What looks weird and completly fit the previous analysis is the mapM_
use in mkStroke
, we stroke every line separately instead of doing it "in group". Looking at diagrams-cairo path rendering:
instance Renderable (Path V2 Double) Cairo where
render _ p = C $ do
cairoPath p
f <- getStyleAttrib getFillTexture
s <- getStyleAttrib getLineTexture
ign <- use ignoreFill
setTexture f
when (isJust f && not ign) $ liftC C.fillPreserve
setTexture s
liftC C.stroke
We call stroke only once for the whole path. I guess there is a need to patch diagrams-rasterific :]
Pull request proposed to diagrams-rasterific
, closing this ticket
Great, thank you! (I can confirm this: If I split the offending path into separate paths, I get the same gray pixels with cairo.)
see diagrams/diagrams-rasterific#42 for more details