Why are text measurement APIs exposing `IntSize` f...
# compose
l
Why are text measurement APIs exposing
IntSize
from a
ceilToIntPx()
call rather than keep the decimals in a
Float
? When text is animated (and therefore has the subpixel flag), it leads to non-pixel perfect things.
💯 1
r
You can animate rendering using floats, that’s unrelated to the measurement
l
My use case it drawing text on a path, and splitting it to multiple calls to
drawTextOnPath
(Android's Canvas platform API) for each span.
r
I haven’t tried
drawTextOnPath
in a while but it was horribly broken in some cases
(and in some cases you can’t do better anyway, drawing text on a path is fundamentally problematic)
l
This is from a test along an arc. Magenta is all in one call, light gray is when drawing character per character, and offsetting from previous characters. The one with green is the same as the other one, but the light gray is behind because I removed
1
, essentially turning the
ceil
into a
floor
.
Is
drawTextOnPath
as broken as what this would produce? https://www.pushing-pixels.org/2022/02/10/drawing-text-on-a-path-in-compose-desktop-with-skia.html No other way on Android anyway, just curious
r
yeah
l
I could also leverage
PathMeasure
to draw text character per character on a path, would it be as broken?
r
look at the blue text
it’s on a concave curve
so depending on the height of the glyphs, they can overlap for instance
cusps are also an issue, etc.
l
I've seen that for overlapping on concave
r
splitting the measurement is going to be an issue because of the nature of text (ligatures, etc.)
l
My use case is mostly to draw on convex paths though
r
but even without splitting, there are problems
even a convex path can look horrible
take a small circle and draw text on the circle using a large font
l
That's intrinsic to drawing text on a path
r
it looks silly
etc.
l
You can draw text on a triangle path, it'll also look not nice
r
Skia has/had a way to “morph” the text along the path, but that also looked strange in some cases
You can actually do that using public Compose APIs, by drawing the text as a stamped Path on a Path
l
And that, kids, is why the Apple Watch is square.
r
No it’s not 🙂
l
Haha, yes, it has corner radius
r
(esp. since they have a bunch of circular watch faces)
l
True
Was just kidding
Drawing text as a stamped path? What do you mean?
r

https://www.youtube.com/watch?v=gtQsC1bav5M

16:30
l
I think I was there in person at your talk 😄 Recall that fun animation, but didn't realize it could be hacked to draw text on a path. I'm not sure I see how to get the path of a glyph from
TextLayoutResult
though…
My current workaround for my initial question is to call
getBoundingBox
on the
TextLayoutResult
and passing the last index of the previously drawn chunks to it. It's still not pixel perfect, but the difference is negligible
It's sad that this
ceil
operation is applied though, I can see the code of
getBoundingBox
is non trivial, certainly much more expensive to compute something that was already known but couldn't be retrived because of inpenetrable API boundaries.
r
I wouldn't worry about being pixel perfect
l
You would just subtract 1 to the ceiled result to avoid making longer than necessary?
r
No i wouldn't do anything
l
So you would let the text be a little longer than necessary? Like on my previous screenshot with magenta text where the gray one goes a bit farther?
r
Text on a path won't be "correct" anyway so 🤷‍♂️
Like you assume that drawTextOnPath is "correct". It does its best but... :)
l
I know, I'd also like to help it do its best, and I'm fine with the caveats
h
if you are going in
TextLayoutResult
+`getBoundingBox` direction, you might want to take a look at https://gist.github.com/halilozercan/cf09d8c1ea6ec68264c031731f8eeb38
👍🏼 1
l
Interesting! I see this doesn't use Android specific code, which would enable me to avoid relying on Android-only APIs, letting me make an iOS or WASM app for something I have in mind
About my almost pixel perfect match when using
getBoundingBox
, I noticed that when I have
skewX
set to something other than
0f
(e.g.
Float.MIN_VALUE
, aka
1.4E-45f
), the per-character split and non-split text render (with
drawTextOnPath
called for whole string vs for each char) match up perfectly. I'm not sure why, but it's good to know for me. I still plan to look into Halil's per-glyph rendering solution because I believe it'll work best with emojis, exotic unicode stuff, and non latin text (like arabic or japanese).
r
I wouldn't rely on this especially across platforms and API levels
1
The main reason it does that is probably the rendering pipeline disabling pixel snapping when there's any transform set on text
h
I also wouldn't recommend that approach if you are going to use it in an uncontrolled environment in production, e.g. the text is set by the user.
getBoundingBox
is not perfect. You may see some pixels of glyphs being left out especially for tall ascents and deep descents.
getBoundingBox
is not meant to be pixel perfect though. Its main purpose is to give a good approximation of where a character/glyph is in the text layout rectangle for focus, semantics, and gestures. In those cases missing a pixel or two wouldn't hurt. However when drawing it's not acceptable to leave a pixel behind while translating, rotation a glyph.
l
I have been able to combine your gist and your DrawTextOnPathDemo both linked above to get a nice API that renders text on a path, while supporting all Compose text features, and without breaking emojis, as you can see on the screenshot! Thank you so much Halil for the help, and thanks Romain for outlining the limits of drawing a text on a path. BTW, while working on that, I noticed that
nextContour()
isn't exposed in Compose's
PathMeasure
API. I'm not sure why, but I think it could be helpful to have it, so I filed a feature request: https://issuetracker.google.com/issues/334977827
r
PathMeasure
has other issues anyway, I would like to swap out its implementation eventually to not rely on the platform’s
👀 1
Anyway you can work around this problem by using the new API in Compose 1.7 that lets you divide a
Path
into a list of contours (as
Path
instances) and measuring them individually
👍🏼 1
l
Hum, what are the other issues
PathMeasure
has?
r
It doesn’t let you specify the error metric, which means it’s basically useless for any path that’s not expressed in pixels
(for instance, if your path is in the 0..1 range, the length will always be the same,
sqrt(2)
)
👀 1
l
"error"?
There’s a similar error metric used when measuring the length of a path
And by default it’s set to 0.5 (or 0.25 I don’t remember which one)
Anyway, now we have more robust primitives in Compose we could use to improve
PathMeasure
(and probably making it faster too)
👍🏼 1
l
Interesting
I'm doing everything in pixels at the moment, right at render time, so it's not so much of a problem for now, but it's good to know those subtle caveats nonetheless.
I'm now wondering is there is a way to morph the glyphs, with the Compose DrawScope and text APIs. It might not always be what I want, but it'd be cool to have the choice
r
I’ve been playing with that recently and it’s complex problem to solve generally, esp. if you morph across typefaces. You won’t get any morphing for text out of the box though
(some of the APIs added recently — divide, reverse, path iterator etc. — are precisely to be able to build this capability)