This is the bug that the ORDERED cap was a guess at, and it is not the screen. The picture is upscaled by S CSS pixels per layout unit and composed at DENSITY buffer pixels per unit, so one buffer pixel covers S*dpr/DENSITY device ones. At S=2 on a phone with dpr 2.748 that is 1.832. Nearest-neighbour keeps five buffer rows out of every six and doubles the sixth, in a rhythm that walks down the screen. Measured rather than argued: a ground ruled every four buffer pixels came out seven device pixels apart two hundred and three times and eight apart a hundred. That is not a texture, it is a beat -- which is why it reads as the pattern collapsing into lines, and why there is a seam across the picture where the beat turns over. Every screen was doing it. The dispersed one was just the easiest to see it in, being the one whose ink is single pixels. So the candidate upscales are the ones where it lands whole: S = DENSITY*k/dpr for integer k, which is every scale at which a buffer pixel is exactly k device pixels. S stops being an integer and does not need to be. What has to be whole is the device mapping -- and this makes a layout unit exactly DENSITY*k device pixels, so the one-unit rules the whole design is drawn with are whole pixels too, which they never were on a fractional-dpr display. Same measurement after: 2.000 device pixels per buffer pixel, every rule eight apart, two hundred and seventy-five times out of two hundred and seventy-six. Preferred rather than forced, at about an eighth of the board's size, because which multiples exist depends on the display. At dpr 2.748 the crisp scale is larger than the integer one and there is nothing to trade. At dpr 2 with density 3 the only crisp scales are 1.5 and 3, and 3 leaves the column too narrow to hold -- so that display keeps a resampled board at full size rather than a crisp one at three quarters. Across eight devices the crisp choice cost nothing at all: a smaller upscale with a larger cell came out to the same board in millimetres. Worth naming, though: a unit is a multiple of DENSITY device pixels or it is not crisp, so the intensity setting and the display interact, and density 2 is crisp at full size on a dpr-2 screen where density 3 is not. Type follows the device grid now instead of a capped multiple of the upscale, for the same reason: it is not dithered and not upscaled from the buffer, so there was nothing for the cap to protect.
- commit
- d3b9638ff412cba9383b53b23fecf6a14691e2fb
- parent
- c08ce727c23409cbc00d8bdcdc3882633d132054
- author
- Dami <dami.etoile@gmail.com>
| M | mockup/ | 63 | ++++++++++++++++++++++++++++-- |
@@ -500,15 +500,67 @@ function metricsFor(mode, S, box) {
500500 * rather than a candidate -- it is what a keypad phone gets and nothing else.501501 * And where two candidates give the same board, the smaller upscale wins,502502 * because it is the one with more units left over for everything around it. */503+/* The upscales worth trying, in the order of how much they are worth.504+ *505+ * A buffer pixel has to land on a whole number of *device* pixels or the506+ * compositor resamples the one thing in this app that must never be resampled.507+ * The picture is upscaled by S CSS pixels per layout unit and composed at508+ * DENSITY buffer pixels per unit, so a buffer pixel covers S*dpr/DENSITY device509+ * ones -- and on the phone this was found on, S=2 with dpr 2.748 makes that510+ * 1.832. Nearest-neighbour then keeps five buffer rows out of every six and511+ * doubles the sixth, in a rhythm that walks down the screen: a ground ruled512+ * every four buffer pixels came out at seven device pixels two times in three513+ * and eight the rest, which is not a texture any more, it is a beat. It reads514+ * as the pattern collapsing into lines, and as a seam where the beat turns515+ * over.516+ *517+ * So the candidates are the scales where it lands whole: S = DENSITY*k/dpr for518+ * integer k, which is every scale at which one buffer pixel is exactly k device519+ * pixels. S is no longer an integer and does not need to be -- what has to be520+ * whole is the device mapping, and this makes a *layout unit* exactly DENSITY*k521+ * device pixels, so the one-unit rules the whole design is drawn with are whole522+ * pixels too, which they were not before.523+ *524+ * The integer upscales are still offered, and rank below any crisp one at the525+ * same board size. They are what a display with dpr 2 and density 3 gets: there526+ * is no k there that both lands whole and leaves the column wide enough, and a527+ * board at half the size is a worse answer than a board that resamples. Which528+ * is a real limit and worth naming -- a unit is a multiple of DENSITY device529+ * pixels or it is not crisp, and DENSITY is the setting, so the two interact. */530+function upscales(dpr) {531+ const out = [];532+ for (let k = 1; k <= 6; k++) {533+ const S = (DENSITY * k) / dpr;534+ if (S >= 1 && S <= 8) out.push({ S, crisp: true });535+ }536+ for (let S = 1; S <= 8; S++) out.push({ S, crisp: false });537+ return out;538+}539+503540 function chooseLayout(box) {541+ const dpr = window.devicePixelRatio || 1;504542 let best = null, bestCost = -1;505543 for (const mode of ['stack', 'split']) {506- for (let S = 1; S <= 8; S++) {544+ for (const { S, crisp } of upscales(dpr)) {507545 const m = metricsFor(mode, S, box);508546 if (m.VW < 150 || m.VW > 640 || m.VH < 120) continue;509547 const cell = fitCell(m, REF_W, REF_H);510548 if (!cell) continue;511- const cost = (S > 1 ? 1e6 : 0) + cell * S * 100 - S;549+ /* Crispness is worth about an eighth of the board's size and not more.550+ *551+ * It is not free: a whole mapping needs the layout unit to be a multiple552+ * of DENSITY device pixels, and which multiples exist depends on the553+ * display. At dpr 2.748 the crisp scale is *larger* than the integer one554+ * and there is nothing to trade. At dpr 2 with density 3 the only crisp555+ * scales are 1.5 and 3, and 3 leaves the column too narrow to hold, so556+ * crispness there would cost a quarter of the board -- which is the wrong557+ * trade, and a resampled board at full size is what that display gets.558+ * (Density 2 is crisp at full size on those screens; the setting and the559+ * display interact, and pretending otherwise would mean shrinking the560+ * board on hardware that never asked for it.)561+ *562+ * Both still outrank the tie-break that prefers the smaller upscale. */563+ const cost = (S > 1 ? 1e6 : 0) + cell * S * 100 * (crisp ? 1.125 : 1) - S;512564 if (cost > bestCost) { bestCost = cost; best = m; }513565 }514566 }@@ -529,12 +581,15 @@ export function layout() {
529581 530582 // The backing stores carry the density; the CSS size does not, so the531583 // picture occupies exactly the space it always did and simply has more532- // pixels in it.584+ // pixels in it. What the CSS size does have to do is land the buffer on585+ // whole device pixels, which is what chooseLayout picked S for.533586 cvs.width = dpx(VW); cvs.height = dpx(VH);534587 cvs.style.width = `${VW * S}px`; cvs.style.height = `${VH * S}px`;535588 scene.width = dpx(VW); scene.height = dpx(VH);536589 537- typeScale = S * Math.min(3, window.devicePixelRatio || 1);590+ // Type is not dithered and is not upscaled from the buffer, so it wants the591+ // device grid itself rather than a capped multiple of the upscale.592+ typeScale = S * (window.devicePixelRatio || 1);538593 typeCvs.width = Math.round(VW * typeScale);539594 typeCvs.height = Math.round(VH * typeScale);540595 typeCvs.style.width = `${VW * S}px`; typeCvs.style.height = `${VH * S}px`;