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,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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user