fix(text): position glyphs at their fractional coordinates - #461
Merged
Merged
Conversation
Every Font was built with Skia's defaults, which place glyphs on whole device pixels: subpixel positioning off, baseline snapping on. The box around a text moved by hundredths of a pixel, the glyphs inside it advanced in 1px jumps, so a label visibly slid against its own container under any slow motion — a wiggle of a few pixels, a float loop, a camera push-in. Measured on the reported scenario, the text centroid strayed 0.690px from a 3px linear travel while the bar sharing its translated box tracked it to the hundredth. `subpixel_font` is now the only constructor, and the 107 direct `Font::from_typeface` calls across 37 files go through it, so a new component cannot reintroduce the default by writing the obvious thing. The residue is 0.153px, which is Skia quantising subpixel positions to quarter-pixels rather than snapping. One test had to change its measurement. `centring_keeps_the_gradient _over_the_glyphs` sampled the rightmost ink column and compared premultiplied channels; that column is now an antialiased fringe rather than a hard edge, so the same violet read (33, 22, 59) through alpha 61 instead of (139, 92, 246). It now reads a covered column and unpremultiplies, which is what it meant to compare. Closes #456
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every
skia_safe::Fontin the engine was built with Skia's defaults, which place glyphs on whole device pixels: subpixel positioning off, baseline snapping on. Everything else on the canvas sits at its exact fractional position, so under any slow motion the box moved by hundredths of a pixel while the glyphs inside it advanced in 1px jumps. What that looks like is a label shivering inside a card that is standing still — awiggleof a few pixels, afloatloop, a camera push-in.Measured on the scenario from the issue, a text and a 4px bar in one column translated 0 → 3px linearly: the bar's ink centroid tracked the keyframe to the hundredth, the text's held for three frames and then jumped by exactly 1.00px.
renderer::subpixel_fontis now the only way to build a Font, and the 107 directFont::from_typefacecalls across 37 files go through it. A single constructor rather than 107 call sites to keep in line: a new component that writes the obvious thing gets the right behaviour, and reintroducing the snapping means deleting the constructor rather than forgetting three setters.The residue is 0.153px against 0.690px before, not zero, because Skia quantises subpixel positions to quarter-pixels. That is a quarter of the previous error and a fifth of a pixel of the glyph's own antialiasing, rather than a visible step.
centring_keeps_the_gradient_over_the_glyphshad to change what it measures, and it is worth being explicit about why, since a test edited alongside the change it guards deserves the scrutiny. It sampled the rightmost ink column and compared premultiplied channels. With glyph edges no longer snapped, that column is an antialiased fringe rather than a hard edge, so the identical violet read(33, 22, 59)through alpha 61 where the centred case read(139, 92, 246)at full coverage —33 × 255 / 61 = 138. The gradient was never wrong; the test was reading premultiplied values as if they were colours. It now walks in from the right to a column with real coverage and unpremultiplies before comparing.Verification — a new integration test renders the issue's scenario frame by frame, separates the text band from the bar band by their ink rows, and asserts the text centroid follows the travel and does not drift against the bar. Red at 0.690px with the constructor's three setters removed, green at 0.153px with them, tolerance 0.25px. Plus
cargo fmt --all --check,cargo clippy --workspace --all-targets --features rustmotion/studio -- -D warnings, andcargo test --workspace --features rustmotion/studio: 0 failed. A 40px and a 14px line were also rendered and looked at, since hinting is now off.Closes #456