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:
adminandClaude Opus 5.5 committed 2026-10-02 08:42:26 +02:00
1 parent 275e50af01
commit 32a0af6e2c
49 files changed
+582 -134

No files matched your search

+3 -3
View File
@@ -1,5 +1,5 @@
import { CanvasTextMetrics, type FederatedPointerEvent, Rectangle, Text } from 'pixi.js';
import { centreY, Theme, textStyle } from '../ui';
import { CanvasTextMetrics, type FederatedPointerEvent, Rectangle, type Text } from 'pixi.js';
import { centreY, Theme, textStyle, crispText } from '../ui';
import { TabButton } from './TabButton';
const GAP = 4;
@@ -24,7 +24,7 @@ export class DocumentTab extends TabButton {
private readonly onClose: () => void,
) {
super(label, { align: 'left', icon, onClick: onPick });
this.closeText = new Text({ text: CLOSE, style: textStyle('muted') });
this.closeText = crispText({ text: CLOSE, style: textStyle('muted') });
this.closeText.eventMode = 'static';
this.closeText.cursor = Theme.hoverCursor;
// Stopped on the way down as well, or the tab under it would count the press as its own click.
+3 -2
View File
@@ -7,7 +7,7 @@
* The list differs per kind (tiles for pictures, rows with a sample line for typefaces, rows with a
* ▶ for sounds); the dialog around it does not.
*/
import { type Container, type FederatedPointerEvent, type Graphics, Text } from 'pixi.js';
import type { Container, FederatedPointerEvent, Graphics, Text } from 'pixi.js';
import type { AssetType } from '../core/model/AssetTypes';
import type { Project } from '../core/model/Project';
import type { StoredFile } from '../core/store/FileStore';
@@ -22,6 +22,7 @@ import {
Theme,
textStyle,
Widget,
crispText,
} from '../ui';
import { dialogButtons } from './FormDialog';
@@ -66,7 +67,7 @@ export abstract class PickerList<Item extends PickerItem, Row extends PickerRow>
protected items: Item[] = [];
protected rows: Row[] = [];
private selectedFile: string | null = null;
private readonly empty = new Text({ text: '', style: textStyle('muted') });
private readonly empty = crispText({ text: '', style: textStyle('muted') });
private readonly unwatch: (() => void)[] = [];
onPick?: (file: string) => void;
onActivate?: (file: string) => void;
+3 -3
View File
@@ -1,5 +1,5 @@
import { Graphics } from 'pixi.js';
import { Button, HStack, Menu, Theme } from '../ui';
import { Button, crisp, hairline, HStack, Menu, Theme } from '../ui';
import { DocumentTab } from './DocumentTab';
import { TabButton } from './TabButton';
import { tabRowLayout } from './tabRow';
@@ -44,7 +44,7 @@ export class TabStrip<T extends string> extends HStack {
private readonly icons = new Map<T, string>();
private readonly overflow: Button;
/** The bar the cards sit on, drawn behind every tab. */
private readonly bar = new Graphics();
private readonly bar = crisp(new Graphics());
private hiddenTabs: T[] = [];
private active: T | null;
@@ -165,7 +165,7 @@ export class TabStrip<T extends string> extends HStack {
this.bar.clear();
this.bar.rect(0, 0, this.w, this.h).fill(c.tabBar);
// The line the cards stand on; the one showing covers its own stretch of it.
this.bar.rect(0, this.h - 1, this.w, 1).fill(c.border);
this.bar.rect(0, this.h - hairline(), this.w, hairline()).fill(c.border);
}
private applyOverflow(): void {
+22 -5
View File
@@ -1,4 +1,10 @@
import { Container, type FederatedPointerEvent, type FederatedWheelEvent, Graphics, Text } from 'pixi.js';
import {
Container,
type FederatedPointerEvent,
type FederatedWheelEvent,
Graphics,
type Text,
} from 'pixi.js';
import {
clipboardKeyCount,
copyAnimationKeys,
@@ -24,7 +30,18 @@ import {
import type { AnimationAsset, AnimationTrack, KeyInterpolation } from '../../core/model/Asset';
import type { Vec2 } from '../../core/model/Properties';
import { findAssetOf } from '../../core/model/Project';
import { drawBand, Menu, pressCount, type Size, Theme, textStyle, ui, Widget, zoomFactor } from '../../ui';
import {
drawBand,
Menu,
pressCount,
type Size,
Theme,
textStyle,
ui,
Widget,
zoomFactor,
crispText,
} from '../../ui';
import { type EditorState, trackSelection } from '../state';
import { FramesDialog, openFramesDialog } from './FramesDialog';
import {
@@ -87,7 +104,7 @@ export class TimelineCanvas extends Widget {
private readonly rulerLayer = new Container();
private readonly labels = new Map<string, Text>();
private readonly rulerTexts: Text[] = [];
private readonly hint = new Text({ text: '', style: textStyle('muted') });
private readonly hint = crispText({ text: '', style: textStyle('muted') });
private readonly clip = new Graphics();
private view: TimelineView = { ...DEFAULT_VIEW };
private assetId = '';
@@ -604,7 +621,7 @@ export class TimelineCanvas extends Widget {
this.cells.rect(x, RULER_HEIGHT - 6, 1, 6).fill(c.textMuted);
let text = this.rulerTexts[used];
if (!text) {
text = new Text({ style: textStyle('small') });
text = crispText({ style: textStyle('small') });
this.rulerTexts.push(text);
this.rulerLayer.addChild(text);
}
@@ -667,7 +684,7 @@ export class TimelineCanvas extends Widget {
alive.add(track.id);
let label = this.labels.get(track.id);
if (!label) {
label = new Text({ style: textStyle('body') });
label = crispText({ style: textStyle('body') });
this.labels.set(track.id, label);
this.labelLayer.addChild(label);
}
+5 -3
View File
@@ -8,13 +8,13 @@
* what a key or a `Lerp` reads. A dot runs along it as a preview, and a second one beside the graph
* moves only up and down – the motion the curve gives a value.
*/
import { type FederatedPointerEvent, Graphics, Text } from 'pixi.js';
import { type FederatedPointerEvent, Graphics, type Text } from 'pixi.js';
import { SetCurvePointsCommand } from '../../core/commands/curveCommands';
import type { CurveAsset, VectorPoint, VectorRing } from '../../core/model/Asset';
import { curveExtent, curveValue, sanitizedCurve } from '../../core/model/curve';
import { findAssetOf } from '../../core/model/Project';
import type { Vec2 } from '../../core/model/Properties';
import { cancelFrameWork, onFrame, pressCount, Theme, textStyle } from '../../ui';
import { cancelFrameWork, onFrame, pressCount, Theme, textStyle, crispText } from '../../ui';
import { FramePreview } from '../framePreview';
import { deleteTakes, pickedIn, pointPress } from '../pointGestures';
import { DRAG_THRESHOLD, drawPointGrip, POINT_SIZE } from '../pointGrips';
@@ -75,7 +75,9 @@ export class CurveCanvas extends SheetCanvas<CurveAsset> {
private readonly guides = new Graphics();
private readonly line = new Graphics();
private readonly overlay = new Graphics();
private readonly labels = ['0', '1', '0', '1'].map((text) => new Text({ text, style: textStyle('muted') }));
private readonly labels = ['0', '1', '0', '1'].map((text) =>
crispText({ text, style: textStyle('muted') }),
);
private drag: CurveDrag | null = null;
/** The point last pressed or put down – the one the header's point type is about; -1 for none. */
private picked = -1;
+42 -1
View File
@@ -1,18 +1,59 @@
import { type FrameRect, isTextureFile, resolvePicture } from '../../core/model/texture';
import { effectiveScale, gameClock } from '../../core/time';
import { afterFrames, isRendering, renderCount, requestRender, stagePixel, stagePixels } from '../../ui';
import {
afterFrames,
hairline,
isRendering,
renderCount,
requestRender,
stagePixel,
stagePixels,
} from '../../ui';
import { particleLibrary } from '../../viewport/ParticleLibrary';
import { shaderLibrary } from '../../viewport/ShaderLibrary';
import { textureLibrary } from '../../viewport/TextureLibrary';
import { type Container, Mesh } from 'pixi.js';
import type { HookContext } from './hookHelpers';
import { edgeContrast } from './sharpness';
/** What was really drawn, and the clocks behind it: pixels, textures, frames, shaders and particles. */
export function renderHooks({ app, state, editor }: HookContext) {
const { viewport, gameView } = editor;
const pixelAt = (x: number, y: number): string => stagePixel(app, x, y);
/**
* A part of the frame as the screen gets it, device pixel for device pixel. `pixelAt` cannot say
* that: it draws the stage again into a target of its own, at the layout's size.
*/
const deviceFrame = (x: number, y: number, w: number, h: number): ImageData => {
const res = app.renderer.resolution;
const [sx, sy, sw, sh] = [x, y, w, h].map((v) => Math.round(v * res)) as [number, number, number, number];
const copy = document.createElement('canvas');
copy.width = Math.max(1, sw);
copy.height = Math.max(1, sh);
const context = copy.getContext('2d')!;
// In one go with the render: the drawing buffer is only promised to hold the frame until the task ends.
app.render();
context.drawImage(app.canvas, sx, sy, copy.width, copy.height, 0, 0, copy.width, copy.height);
return context.getImageData(0, 0, copy.width, copy.height);
};
return {
pixelAt,
/** The grid the interface rounds to: device pixels per layout pixel, and what a hairline measures. */
pixelGrid: () => ({ resolution: app.renderer.resolution, hairline: hairline() }),
/** How hard the edges are in a rectangle of the screen, given in layout pixels – see `edgeContrast`. */
edgeContrast: (x: number, y: number, w: number, h: number) => {
const frame = deviceFrame(x, y, w, h);
return edgeContrast(frame.data, frame.width, frame.height);
},
/** The device pixels down one column, from layout `y` over `h`, as `#rrggbb`. */
deviceColumn: (x: number, y: number, h: number) => {
const { data, height } = deviceFrame(x, y, 1 / app.renderer.resolution, h);
const hex = (v: number | undefined) => (v ?? 0).toString(16).padStart(2, '0');
return Array.from(
{ length: height },
(_, row) => `#${hex(data[row * 4])}${hex(data[row * 4 + 1])}${hex(data[row * 4 + 2])}`,
);
},
/** Many points from one render, each the colour `pixelAt` would give – for checks that sample hundreds. */
pixels: (points: readonly { x: number; y: number }[]) => stagePixels(app, points),
/**
+25
View File
@@ -0,0 +1,25 @@
/**
* How hard the edges in a picture are: the mean jump in brightness between neighbours, over the
* pixels that have one at all. Text and lines drawn between two device pixels are averaged across
* both and score lower – the one number that tells such a frame from a crisp one (see `pixelGrid.ts`).
*/
export function edgeContrast(rgba: ArrayLike<number>, width: number, height: number): number {
const lum = (x: number, y: number): number => {
const i = (y * width + x) * 4;
return 0.299 * (rgba[i] ?? 0) + 0.587 * (rgba[i + 1] ?? 0) + 0.114 * (rgba[i + 2] ?? 0);
};
let sum = 0;
let edges = 0;
for (let y = 0; y < height - 1; y++)
for (let x = 0; x < width - 1; x++) {
const here = lum(x, y);
const dx = Math.abs(lum(x + 1, y) - here);
const dy = Math.abs(lum(x, y + 1) - here);
sum += dx + dy;
if (dx > EDGE || dy > EDGE) edges += 1;
}
return edges ? sum / edges : 0;
}
/** Below this a difference is noise of the blending, not an edge. */
const EDGE = 2;
+4 -4
View File
@@ -1,6 +1,6 @@
import { Container, Graphics, Text, TextStyle } from 'pixi.js';
import { Container, Graphics, type Text, TextStyle } from 'pixi.js';
import type { StoredFont } from '../../core/store/FontStore';
import { formatBytes, Theme, textStyle, truncate } from '../../ui';
import { formatBytes, Theme, textStyle, truncate, crispText } from '../../ui';
import { fontLibrary } from '../../viewport/FontLibrary';
import {
currentPicker,
@@ -41,8 +41,8 @@ class FontList extends RowList<FontItem, FontRow> {
const background = new Graphics();
// Its own style object, never `textStyle('body')`: those are shared across the whole editor,
// so writing a family into one would restyle every label in the window.
const sample = new Text({ style: new TextStyle({ fontSize: SAMPLE_SIZE, fill: Theme.colors.text }) });
const caption = new Text({ style: textStyle('small') });
const sample = crispText({ style: new TextStyle({ fontSize: SAMPLE_SIZE, fill: Theme.colors.text }) });
const caption = crispText({ style: textStyle('small') });
box.addChild(background, sample, caption);
return { box, background, sample, caption };
}
+3 -3
View File
@@ -1,5 +1,5 @@
import { Text, TextStyle } from 'pixi.js';
import { Theme, truncate, type Size, Widget } from '../../ui';
import { TextStyle } from 'pixi.js';
import { Theme, truncate, type Size, Widget, crispText } from '../../ui';
import { fontLibrary } from '../../viewport/FontLibrary';
const SAMPLE_SIZE = 26;
@@ -10,7 +10,7 @@ const SAMPLE_TEXT = 'Aa Bb Cc 0123';
* hash. Reads in the editor's own typeface until the face has arrived, then redraws itself.
*/
export class FontPreview extends Widget {
private readonly sample = new Text({
private readonly sample = crispText({
style: new TextStyle({ fontSize: SAMPLE_SIZE, fill: Theme.colors.text }),
});
private readonly stopWatching: () => void;
+3 -3
View File
@@ -1,6 +1,6 @@
import { Graphics, Rectangle, Text, TextStyle } from 'pixi.js';
import { Graphics, Rectangle, type Text, TextStyle } from 'pixi.js';
import type { AnnotationKind, GraphAnnotation } from '../../core/graph';
import { readableTextColor, Theme, truncate, Widget } from '../../ui';
import { readableTextColor, Theme, truncate, Widget, crispText } from '../../ui';
/** Height of a group's title bar – the same as a node's, so the two line up in the eye. */
export const ANNOTATION_TITLE_HEIGHT = 24;
@@ -26,7 +26,7 @@ export class AnnotationView extends Widget {
) {
super();
this.addChild(this.box);
this.caption = new Text({ text: '', style: this.captionStyle(0xffffff) });
this.caption = crispText({ text: '', style: this.captionStyle(0xffffff) });
this.caption.position.set(PAD, kind === 'group' ? 0 : PAD);
this.addChild(this.caption);
this.cursor = Theme.hoverCursor;
+3 -3
View File
@@ -1,6 +1,6 @@
import { Container, Graphics, Text } from 'pixi.js';
import { Container, Graphics, type Text } from 'pixi.js';
import { type GraphEdge, portsCompatible } from '../../core/graph';
import { Theme, textStyle, truncate } from '../../ui';
import { Theme, textStyle, truncate, crispText } from '../../ui';
import { dashedLine } from '../../viewport/symbols';
import {
arrowCorners,
@@ -314,7 +314,7 @@ export class EdgeLayer extends Container {
private nextLabel(): Text {
let label = this.labels[this.labelCount];
if (!label) {
label = new Text({ text: '', style: textStyle('small') });
label = crispText({ text: '', style: textStyle('small') });
this.labels.push(label);
this.labelLayer.addChild(label);
}
+3 -2
View File
@@ -1,4 +1,4 @@
import { Container, type FederatedPointerEvent, type FederatedWheelEvent, Graphics, Text } from 'pixi.js';
import { Container, type FederatedPointerEvent, type FederatedWheelEvent, Graphics } from 'pixi.js';
import { textOf } from '../../core/model/Properties';
import { AddNodeCommand, CollapseNodesCommand } from '../../core/commands/graphCommands';
import { SetMachineStartCommand } from '../../core/commands/assetCommands';
@@ -42,6 +42,7 @@ import {
truncate,
Widget,
zoomFactor,
crispText,
} from '../../ui';
import {
annotationSelection,
@@ -104,7 +105,7 @@ export class GraphCanvas extends Widget {
private readonly tempEdge = new Graphics();
/** Drawn in screen space, so the band keeps a hairline border at any zoom. */
private readonly band = new Graphics();
private readonly hint = new Text({ text: '', style: textStyle('muted') });
private readonly hint = crispText({ text: '', style: textStyle('muted') });
private readonly views = new Map<string, NodeView>();
private assetId = '';
private pan = { x: 0, y: 0 };
+13 -4
View File
@@ -1,4 +1,4 @@
import { CanvasTextMetrics, Graphics, Rectangle, Text } from 'pixi.js';
import { CanvasTextMetrics, Graphics, Rectangle, type Text } from 'pixi.js';
import {
asNumber,
asString,
@@ -20,7 +20,16 @@ import { FIRST_TILE_LAYER } from '../../core/model/liveTilemap';
import type { Project } from '../../core/model/Project';
import { isList, type PropertyValue } from '../../core/model/Properties';
import type { PropertyDef } from '../../core/properties/PropertySchema';
import { formatNumber, Theme, textStyle, type TextStyleToken, toHex, truncate, Widget } from '../../ui';
import {
formatNumber,
Theme,
textStyle,
type TextStyleToken,
toHex,
truncate,
Widget,
crispText,
} from '../../ui';
import { keyLabel, referenceLabel } from '../inspector/fieldFactory';
import { collapsedNodeWidth } from './folding';
@@ -275,7 +284,7 @@ export class NodeView extends Widget {
for (const [key, value] of this.liveTexts) {
const slot = this.valueSlots.get(key);
if (!slot || slot.right - slot.left < 12) continue;
const text = new Text({ text: truncate(value, style, slot.right - slot.left), style });
const text = crispText({ text: truncate(value, style, slot.right - slot.left), style });
text.position.set(slot.right - text.width, slot.top);
this.addChild(text);
this.valueTexts.set(key, text);
@@ -339,7 +348,7 @@ export class NodeView extends Widget {
align: 'left' | 'right' = 'left',
maxWidth = NODE_WIDTH - PAD * 2,
) => {
const node = new Text({ text: truncate(text, textStyle(style), maxWidth), style: textStyle(style) });
const node = crispText({ text: truncate(text, textStyle(style), maxWidth), style: textStyle(style) });
node.position.set(align === 'left' ? x : x - node.width, y);
this.addChild(node);
this.texts.push(node);
+3 -3
View File
@@ -5,7 +5,7 @@ import type { PropertyBag } from '../../core/model/Properties';
import { resolveTexture } from '../../core/model/texture';
import { defaultsFor } from '../../core/properties/PropertySchema';
import { SHEET_EDGE } from '../../core/shader/nodes';
import { type Size, Theme, Widget } from '../../ui';
import { crisp, hairline, type Size, Theme, Widget } from '../../ui';
import { ParticleField } from '../../viewport/particles';
import { particleLibrary } from '../../viewport/ParticleLibrary';
import { shaderLibrary } from '../../viewport/ShaderLibrary';
@@ -21,7 +21,7 @@ const MIN_HEIGHT = 40;
* has no way to know which of them is meant.
*/
export class ParticlePreview extends Widget {
private readonly bg = new Graphics();
private readonly bg = crisp(new Graphics());
private readonly content = new Container();
private readonly clip = new Graphics();
private readonly field = new ParticleField();
@@ -80,7 +80,7 @@ export class ParticlePreview extends Widget {
// A ground of its own and a rule along the top: on the graph's own background the strip was
// invisible as a thing, and a particle standing still read as a node that had lost its box.
this.bg.clear().rect(0, 0, this.w, this.h).fill(c.panel);
this.bg.rect(0, 0, this.w, 1).fill(c.border);
this.bg.rect(0, 0, this.w, hairline()).fill(c.border);
this.clip.clear().rect(0, 0, this.w, this.h).fill(0xffffff);
this.field.container.visible = this.asset !== null;
if (!this.asset) return;
+3 -3
View File
@@ -3,7 +3,7 @@ import type { ShaderAsset } from '../../core/model/Asset';
import type { PropertyBag } from '../../core/model/Properties';
import { SHADER_KEY } from '../../core/properties/shaders';
import { type EdgeMode, SHEET_EDGE } from '../../core/shader/nodes';
import { type Size, Theme, Widget } from '../../ui';
import { crisp, hairline, type Size, Theme, Widget } from '../../ui';
import { shaderLibrary } from '../../viewport/ShaderLibrary';
import { syncShader } from '../../viewport/shaders';
import { disposeSpriteMesh, newSpriteMesh } from '../../viewport/symbols';
@@ -21,7 +21,7 @@ const MAX_ZOOM = 8;
* The shader is the scene's own (`syncShader`), so what the panel shows is what a sprite would.
*/
export class ShaderPreview extends Widget {
private readonly bg = new Graphics();
private readonly bg = crisp(new Graphics());
private readonly content = new Container();
private readonly clip = new Graphics();
private readonly mesh = newSpriteMesh();
@@ -87,7 +87,7 @@ export class ShaderPreview extends Widget {
override redraw(): void {
const c = Theme.colors;
this.bg.clear().rect(0, 0, this.w, this.h).fill(c.panel);
this.bg.rect(0, 0, this.w, 1).fill(c.border);
this.bg.rect(0, 0, this.w, hairline()).fill(c.border);
this.clip.clear().rect(0, 0, this.w, this.h).fill(0xffffff);
this.mesh.visible = this.shader !== null;
if (!this.shader) return;
+4 -4
View File
@@ -1,6 +1,6 @@
import { Container, type FederatedPointerEvent, Graphics, Text } from 'pixi.js';
import { Container, type FederatedPointerEvent, Graphics, type Text } from 'pixi.js';
import type { StoredSound } from '../../core/store/SoundStore';
import { drawSvg, formatBytes, textStyle, Theme, truncate } from '../../ui';
import { drawSvg, formatBytes, textStyle, Theme, truncate, crispText } from '../../ui';
import { soundLibrary } from '../../viewport/SoundLibrary';
import {
currentPicker,
@@ -48,8 +48,8 @@ class SoundList extends RowList<SoundItem, SoundRow> {
const background = new Graphics();
const play = new Graphics();
drawSvg(play, TOOLBAR_ICON_SVG.play, Theme.colors.accentText);
const caption = new Text({ style: textStyle('body') });
const detail = new Text({ style: textStyle('small') });
const caption = crispText({ style: textStyle('body') });
const detail = crispText({ style: textStyle('small') });
box.addChild(background, play, caption, detail);
return { box, background, play, caption, detail };
}
+3 -3
View File
@@ -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 };
}
+3 -3
View File
@@ -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 = '';
+3 -3
View File
@@ -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);
}
+5 -5
View File
@@ -1,4 +1,4 @@
import { Container, Graphics, Sprite, Text, type TextStyle } from 'pixi.js';
import { Container, Graphics, Sprite, type Text, type TextStyle } from 'pixi.js';
import type { TileAsset } from '../../core/model/Asset';
import {
autotileCombinations,
@@ -19,7 +19,7 @@ import {
tilePlayback,
} from '../../core/model/tile';
import { findAssetOf } from '../../core/model/Project';
import { type Size, Theme, textStyle, ui, Widget } from '../../ui';
import { type Size, Theme, textStyle, ui, Widget, crispText } from '../../ui';
import { textureLibrary } from '../../viewport/TextureLibrary';
import { nearestZoomStep, stepFreeZoom, wheelSteps, zoomStepBelow } from '../../viewport/view';
import type { Point } from '../framing';
@@ -77,7 +77,7 @@ export class TileCanvas extends Widget {
private readonly captionLayer = new Container();
private readonly clipped = new Container();
private readonly clip = new Graphics();
private readonly hint = new Text({ text: '', style: textStyle('muted') });
private readonly hint = crispText({ text: '', style: textStyle('muted') });
private readonly tiles: BoardTile[] = [];
private readonly captions: Caption[] = [];
/** Own copies, since the size follows the zoom here and nowhere else. */
@@ -421,9 +421,9 @@ export class TileCanvas extends Widget {
/** `NESW-ne,se` becomes two lines, so the long captions need no more width than a tile. */
private addCaption(label: string, x: number, y: number): void {
const [sides = label, notches] = label.split('-');
const sidesText = new Text({ text: sides, style: this.sidesStyle, anchor: { x: 0.5, y: 0 } });
const sidesText = crispText({ text: sides, style: this.sidesStyle, anchor: { x: 0.5, y: 0 } });
const notchesText = notches
? new Text({ text: notches, style: this.notchesStyle, anchor: { x: 0.5, y: 0 } })
? crispText({ text: notches, style: this.notchesStyle, anchor: { x: 0.5, y: 0 } })
: null;
this.captionLayer.addChild(sidesText);
if (notchesText) this.captionLayer.addChild(notchesText);
+4 -3
View File
@@ -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 { loneTileQuadrants } from '../../core/model/autotile';
import { findAssetOf } from '../../core/model/Project';
import { quadrantRef, readTile } from '../../core/model/tile';
@@ -11,6 +11,7 @@ import {
textStyle,
truncate,
Widget,
crispText,
} from '../../ui';
import { textureLibrary } from '../../viewport/TextureLibrary';
import { gridMetrics, type GridMetrics, tileAt, tileRect } from '../texture/imageGrid';
@@ -187,8 +188,8 @@ export class TilePalette extends Widget {
const box = new Container();
const background = new Graphics();
const sprites = [new Sprite(), new Sprite(), new Sprite(), new Sprite()] as Swatch['sprites'];
const empty = new Text({ text: '?', style: textStyle('muted') });
const caption = new Text({ style: textStyle('small') });
const empty = crispText({ text: '?', style: textStyle('muted') });
const caption = crispText({ style: textStyle('small') });
box.addChild(background, ...sprites, empty, caption);
box.eventMode = 'none';
this.addChild(box);
+3 -4
View File
@@ -2,11 +2,10 @@
* The scene seen through its active camera at the chosen scale, letterboxed: the running world while
* playing, the model as a still preview otherwise.
*/
import { Text } from 'pixi.js';
import { isSymbolType } from '../../core/model/AssetTypes';
import type { EditorState } from '../state';
import type { World } from '../../runtime/World';
import { requestRender, Theme, textStyle, ui } from '../../ui';
import { requestRender, Theme, textStyle, ui, crispText } from '../../ui';
import { type ActiveCamera, cameraOfItems, cameraOfWorld } from '../../viewport/activeCamera';
import { FullscreenCorner } from '../../viewport/FullscreenCorner';
import { collectHolderItems, collectWorldItems } from '../../viewport/items';
@@ -16,8 +15,8 @@ import { type Frame, WorldViewport } from '../../viewport/WorldViewport';
import { PerformanceOverlay } from './PerformanceOverlay';
export class GameViewport extends WorldViewport<EditorState> {
private readonly hint = new Text({ text: 'No active camera', style: textStyle('muted') });
private readonly editorRunCaption = new Text({
private readonly hint = crispText({ text: 'No active camera', style: textStyle('muted') });
private readonly editorRunCaption = crispText({
text: 'Behaviors run here · keys and clicks go to the game',
style: textStyle('muted'),
});
+3 -3
View File
@@ -1,11 +1,11 @@
import { Container, Graphics, Text } from 'pixi.js';
import { Theme, textStyle } from '../../ui';
import { Container, Graphics } from 'pixi.js';
import { Theme, textStyle, crispText } from '../../ui';
import { performanceLines, type PerformanceReading } from '../frameMeter';
/** Frame rate, entities and the time per frame by area over a running game (#275), top left. */
export class PerformanceOverlay extends Container {
private readonly bg = new Graphics();
private readonly text = new Text({ text: '', style: textStyle('mono') });
private readonly text = crispText({ text: '', style: textStyle('mono') });
constructor() {
super();
+13 -3
View File
@@ -3,7 +3,7 @@
* copy, so live positions are visible and stay editable. A container's template is the same
* work on the same shape of data, so the Container tab subclasses this and swaps `holder()`.
*/
import { Container, type FederatedPointerEvent, Graphics, Rectangle, Text } from 'pixi.js';
import { Container, type FederatedPointerEvent, Graphics, Rectangle, type Text } from 'pixi.js';
import { oneStep } from '../../core/commands/CompoundCommand';
import { MoveSceneObjectCommand, PlaceAssetCommand } from '../../core/commands/sceneObjectCommands';
import { hasSceneIcon, isSymbolType } from '../../core/model/AssetTypes';
@@ -28,7 +28,17 @@ import { FramePreview } from '../framePreview';
import { panelPolicy } from '../playThrottle';
import { type EditorState, objectSelection } from '../state';
import { paintedLayerId, paintedTilemap } from '../panels/TilemapPanel';
import { activeDrag, drawBand, drawGrid, endDrag, Menu, onDragChange, Theme, textStyle } from '../../ui';
import {
activeDrag,
drawBand,
drawGrid,
endDrag,
Menu,
onDragChange,
Theme,
textStyle,
crispText,
} from '../../ui';
import { canPasteObjects, pasteObjects } from '../clipboard';
import { collectHolderItems, type DrawItem, type TilemapSolids } from '../../viewport/items';
import { sortRowOf } from '../../viewport/drawOrder';
@@ -946,7 +956,7 @@ export class SceneViewport extends FreeViewport {
return;
}
if (!label) {
label = new Text({ text: '', style: textStyle('viewportLabel') });
label = crispText({ text: '', style: textStyle('viewportLabel') });
label.eventMode = 'none';
this.labels.set(item.id, label);
this.labelLayer.addChild(label);