Put the text and boxes of the interface on device pixels
On a screen scaled to 125 % or 150 % the editor looked soft beside a page the browser draws itself. The layout counts in whole CSS pixels, and most of those fall between two device pixels there. The GPU averages both: text loses its edges, and a 1 px line becomes a pixel and a half of mixed colour. Now text and boxes are rounded to device pixels, and a line is one device pixel wide. - ui/pixelGrid.ts holds the rule. crispText() makes a text with Pixi's roundPixels, crisp() does the same for a Graphics of upright boxes, and hairline() is the width of one device pixel. - Every text of the interface is made with crispText() instead of new Text(). - crisp() only rounds where the scale is not a whole number. At 100 % and 200 % nothing changes. - drawControlBox, drawHeaderBar and drawSectionBand draw their lines a hairline wide and flush with the edge. The widgets that draw lines of their own do the same. - Two bands share exactly the line they draw, and a column still ends on a whole pixel. - The stage is nudged by 1/32 px where the scale is not whole. At 150 % every odd pixel is an exact half, and without the nudge one band came out a row shorter than its neighbours. - Curves, icons and everything of the game stay unrounded. Rounding their points would bend them. - The harness takes deviceScaleFactor as an option, and the hooks deviceColumn, edgeContrast and pixelGrid read the frame as the screen gets it. Not in here: the subpixel smoothing a browser gives its own text. Pixi draws a text into a transparent picture first, and there only greyscale smoothing is possible. Measured at 150 %, edge contrast before and after: top bar 31.5 to 47.8, the lists on the left 68.6 to 86.4, inspector 27.6 to 38.4. At 100 % it is 46.5, 90.1 and 37.9, and the picture there is the same as before pixel for pixel, apart from the fps display. Tests: - Gate on melektaus-atx: 206 files, 3114 tests (3104 + 10), exit 0 - Full run on melektaus-atx: 1655 of 1656 checks passed, smokes 24/19/21. The one that failed was a condition of the new step 62 itself: it scrolled an inspector that had nothing to scroll. The step now makes the window lower for that reading. - e2e 61,62 after that fix: 14 checks. The full run was not repeated, and CI is switched off for now. - Ten unit tests, nine in tests/ui/pixel-grid.test.ts - Eight e2e checks in the new step 62, which runs at 150 % - Eleven unit counter-probes fail tests - Four browser counter-probes fail their checks. They ran before the fix to step 62 and were not repeated for it. - Screenshots looked at for 125 % and 150 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
275e50af01
commit
32a0af6e2c
49 files changed
+582
-134
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { Container, Graphics, Sprite, Text } from 'pixi.js';
|
||||
import { Container, Graphics, Sprite } from 'pixi.js';
|
||||
import type { StoredTexture } from '../../core/store/TextureStore';
|
||||
import { formatBytes, type Size, textStyle, truncate } from '../../ui';
|
||||
import { formatBytes, type Size, textStyle, truncate, crispText } from '../../ui';
|
||||
import { textureLibrary } from '../../viewport/TextureLibrary';
|
||||
import {
|
||||
currentPicker,
|
||||
@@ -63,7 +63,7 @@ class ImageGrid extends PickerList<ImageItem, Tile> {
|
||||
const box = new Container();
|
||||
const background = new Graphics();
|
||||
const sprite = new Sprite();
|
||||
const caption = new Text({ style: textStyle('small') });
|
||||
const caption = crispText({ style: textStyle('small') });
|
||||
box.addChild(background, sprite, caption);
|
||||
return { box, background, sprite, caption };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Container, type FederatedPointerEvent, type FederatedWheelEvent, Graphics, Text } from 'pixi.js';
|
||||
import { Container, type FederatedPointerEvent, type FederatedWheelEvent, Graphics } from 'pixi.js';
|
||||
import type { Vec2 } from '../../core/model/Properties';
|
||||
import { type Size, Theme, textStyle, ui, Widget, zoomFactor } from '../../ui';
|
||||
import { type Size, Theme, textStyle, ui, Widget, zoomFactor, crispText } from '../../ui';
|
||||
import { imageToLocal, type SheetView, zoomAbout } from './sheetView';
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@ export abstract class SheetCanvas<A> extends Widget {
|
||||
/** Everything the view moves; the checker board is its bottom layer. */
|
||||
protected readonly content = new Container();
|
||||
protected readonly checker = new Graphics();
|
||||
protected readonly hint = new Text({ text: '', style: textStyle('muted') });
|
||||
protected readonly hint = crispText({ text: '', style: textStyle('muted') });
|
||||
protected readonly clip = new Graphics();
|
||||
protected view: SheetView = { panX: 0, panY: 0, zoom: 1 };
|
||||
protected assetId = '';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Container, type FederatedPointerEvent, Graphics, Sprite, Text } from 'pixi.js';
|
||||
import { Container, type FederatedPointerEvent, Graphics, Sprite, type Text } from 'pixi.js';
|
||||
import {
|
||||
AddFrameCommand,
|
||||
SetFrameCollisionCommand,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
setFrameSlice,
|
||||
setFrameSortLine,
|
||||
} from '../../core/model/texture';
|
||||
import { pressCount, Theme, textStyle, type Widget } from '../../ui';
|
||||
import { pressCount, Theme, textStyle, type Widget, crispText } from '../../ui';
|
||||
import {
|
||||
gripAt,
|
||||
insideOutline,
|
||||
@@ -885,7 +885,7 @@ export class TextureCanvas extends SheetCanvas<TextureAsset> {
|
||||
return;
|
||||
}
|
||||
if (!label) {
|
||||
label = new Text({ style: textStyle('viewportLabel') });
|
||||
label = crispText({ style: textStyle('viewportLabel') });
|
||||
this.labels.set(frame.id, label);
|
||||
this.labelLayer.addChild(label);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user