Thanks a lot, Jim! This sheds light on the subject and really sounds
promising.
Anton.
On 10/6/2016 9:31 PM, Jim Graham wrote:
Ah, I see.
There are a lot of mistaken assumptions in the rendering there. It's
not just line thickness, it is assumptions about stroke control and
positioning of strokes.
The biggest issue seems to be that developers (including our own
internal Swing developers who heard this lecture from me decades ago,
but ignored it because it was inconvenient and not yet relevant at the
time) who use the integer-based draw commands assume that their lines
are centered on the pixel that they are naming. In other words "if I
draw a line along x=5 then I am telling the system to fill all the
pixels with an X coordinate of 5", but that is not what that drawing
request is asking for. The coordinates are at pixel edges and it is
an artifact of our fill/stroke rules and stroke control hinting that
has made this work out at 1:1 scaling. As soon as you scale you can
see the issues. We used to only scale for printing, but now we are
starting to scale for the screen.
g.drawRect(0,0,w-1,h-1) is a completely disfunctional way to outline a
component with any settings other than 1:1 coordinates and
STROKE_CONTROL on. I've been mentioning this for years (going on
decades now), but Swing was addicted to that boilerplate for drawing a
border. Thankfully, FX and its CSS-focused skinning has gone with a
different mechanism (primarily most FX components are outlined using
successive fills - optimized in implementation to avoid overdrawing -
rather than using strokes based on mistaken assumptions about where
the line is drawn).
In particular, the line in that example g.drawRect(0,0,w-1,h-1)
technically occurs at the following coordinates:
outer part of outline goes from 0.0-0.5,0.0-0.5 to w-1+0.5,h-1+0.5
(i.e. -0.5,-0.5 to w-0.5,h-0.5)
inner part of outline goes from 0.0+0.5,0.0+0.5 to w-1-0.5,h-1-0.5
(i.e. +0.5,+0.5 to w-1.5,h-1.5)
At a high enough scale you can see the stroke starting to separate
from the fill on the right and bottom edges where the closest it gets
to the edge is 0.5 scaled coordiantes. That rounds to 0 for 1:1 and
with the biasing from STROKE_CONTROL, but at higher resolutions it
becomes a non-zero number of pixels.
Even at 1:1 scale if you follow our filling rules explicitly (which
would require setting STROKE_CONTROL to PURE) then that would fill the
rows at y=-1 and y=h-2 and the columns at x=-1 and x=w-2, which is not
what you want at all. Setting STROKE_CONTROL on allows us to bias the
location of the stroke a little and it ends up filling the correct
pixels as a side effect (though STROKE_CONTROL is meant to achieve
consistency in strokes, it also ends up shifting the line by enough
that the fill rules choose different pixels in this case). The
STROKE_CONTROL=on version of the path is assumed to be
(0.25,0.25,w-0.75,h-0.75) because we snap all coordinates in a
stroke-controlled non-AA path to the nearest location that is
0.25,0.25 within a pixel. This snapping to a consistent sub-pixel
location biasing at 0.25 was chosen because line widths grow more
evenly at that offset, but it offsets the outline enough so that the
outline considered for filling becomes:
outer part of outline goes from 0.25-0.5,0.25-0.5 to
0.25+w-1+0.5,0.25+h-1+0.5
(i.e. -0.25,-0.25 to w-0.25,h-0.25)
inner part of outline goes from 0.25+0.5,0.25+0.5 to
0.25+w-1-0.5,0.25+h-1-0.5
(i.e. +0.75,+0.75 to w-1.25,h-1.25)
which renders the rows columns at 0 and wh-1 at a 1:1 scale using our
fill rules. Note that if you scale high enough you can still see
separation between fill and outline, but the gap is only 0.25 scaled
coordinates so it would take a scale of at least 4x to see it.
The technically accurate way to render the first/last
pixel/1-unit-coordinate boundary of a component would be to
drawRect(0.5,0.5,w-1,h-1) (with no stroke control set) which would
place the rectangle at the following coordinates:
outer part of outline goes from 0.5-0.5,0.5-0.5 to
0.5+w-1+0.5,0.5+h-1+0.5
(i.e. 0,0, to w,h)
inner part of outline goes form 0.5+0.5,0.5+0.5 to
0.5+w-1-0.5,0.5+h-1-0.5
(i.e. 1,1 to w-1,h-1)
which completely encloses the first/last row/column of pixels on a 1:1
coordinate system and accurately covers the first/last N pixels in any
arbitrary N-scaled coordinate system. The rounding for scales like
1.5 still might not work out the way you wanted, but at least the
exact geometry is consistent with respect to the placement of pixels.
With AA you will get a consistent border all around if w,h are snapped
to a pixel size, but with non-AA then rounding error might lead to an
extra pixel on one pair of sides. I haven't done the analysis to see
how the above technique would be affected by STROKE_CONTROL because
really what you are looking for is to render the a consistent edge
around the component and so successive fills as is done with most of
our CSS skinning files in FX is a better solution overall. There are
just too many considerations in filling to make it worthwhile for
simple rectangular regions...
...jim
On 10/4/16 1:46 PM, Anton Tarasov wrote:
On 10/4/2016 11:30 PM, Jim Graham wrote:
I wasn't familiar with the test code for this particular case. Is
it in a bug report somewhere?
Jim, I'm re-attaching it (it was in my first e-mail).
Thanks,
Anton.
...jim
On 10/4/16 1:01 PM, Anton Tarasov wrote:
Also, the problem with primitives rendering
(http://cr.openjdk.java.net/%7Eant/hidpi_pics/Scaling-150-percent.png)
is
still there. But it seems to relate to line-thikness
(border-thickness) rounding inaccuracy. What we can do with
that?...