import { Graphics } from 'pixi.js'; import { afterEach, describe, expect, it } from 'vitest'; import { edgeContrast } from '../../src/editor/debug/sharpness'; import { drawControlBox, drawHeaderBar } from '../../src/ui/draw'; import { crisp, crispText, hairline, hairlineFor, initPixelGrid, offDeviceGrid, tieBreakFor, } from '../../src/ui/pixelGrid'; import { drawSectionBand } from '../../src/ui/sections'; afterEach(() => initPixelGrid(1)); /** The rectangles a shape was drawn from, as `[x, y, w, h]`. */ function rects(g: Graphics): number[][] { return g.context.instructions.flatMap((instruction) => { const data = instruction.data as { path?: { instructions: { action: string; data: number[] }[] } }; return (data.path?.instructions ?? []) .filter((step) => step.action === 'rect' || step.action === 'roundRect') .map((step) => step.data.slice(0, 4)); }); } describe('the device pixel grid', () => { it('names the scales at which whole layout pixels miss the device grid', () => { expect([1, 2, 1.25, 1.5, 1.75].map(offDeviceGrid)).toEqual([false, false, true, true, true]); }); it('makes a hairline whole device pixels wide, and never thinner than one', () => { expect(hairlineFor(1)).toBe(1); expect(hairlineFor(2)).toBe(1); expect(hairlineFor(1.5)).toBeCloseTo(1 / 1.5, 10); expect(hairlineFor(1.25)).toBeCloseTo(1 / 1.25, 10); expect(hairlineFor(1.75) * 1.75).toBeCloseTo(1, 10); expect(hairlineFor(0.5) * 0.5).toBeCloseTo(1, 10); initPixelGrid(1.5); expect(hairline()).toBeCloseTo(2 / 3, 10); }); it('nudges the stage only where ties can happen, and by far less than a device pixel', () => { expect(tieBreakFor(1)).toBe(0); expect(tieBreakFor(2)).toBe(0); for (const res of [1.25, 1.5, 1.75]) { expect(tieBreakFor(res)).toBeGreaterThan(0.01); expect(tieBreakFor(res) * res).toBeLessThan(0.1); } }); it('rounds every text of the interface, at any scale', () => { expect(crispText({ text: 'a' }).roundPixels).toBe(true); initPixelGrid(1.5); expect(crispText().roundPixels).toBe(true); }); it('rounds a box only where the layout pixels miss the device grid', () => { expect(crisp(new Graphics()).roundPixels).toBe(false); initPixelGrid(2); expect(crisp(new Graphics()).roundPixels).toBe(false); initPixelGrid(1.5); expect(crisp(new Graphics()).roundPixels).toBe(true); }); it('draws the shared boxes with lines of one layout pixel where that is a device pixel', () => { const bar = new Graphics(); drawHeaderBar(bar, 200, 30); expect(rects(bar)).toEqual([ [0, 0, 200, 30], [0, 29, 200, 1], ]); const box = new Graphics(); drawControlBox(box, 100, 24, { fill: 0, border: 1 }); expect(rects(box)).toEqual([ [0, 0, 100, 24], [0.5, 0.5, 99, 23], ]); expect(box.roundPixels).toBe(false); }); it('draws them with lines of one device pixel at 150 %, flush with the edge they belong to', () => { initPixelGrid(1.5); const line = 2 / 3; const bar = new Graphics(); drawHeaderBar(bar, 200, 30); expect(bar.roundPixels).toBe(true); expect(rects(bar)[1]).toEqual([0, 30 - line, 200, line]); const box = new Graphics(); drawControlBox(box, 100, 24, { fill: 0, border: 1 }); expect(box.roundPixels).toBe(true); expect(rects(box)[1]).toEqual([line / 2, line / 2, 100 - line, 24 - line]); const band = new Graphics(); drawSectionBand(band, 0, 10, 80, 26); expect(band.roundPixels).toBe(true); expect(rects(band)).toEqual([ [0, 10, 80, 26], [0, 10, 80, line], [0, 36 - line, 80, line], ]); }); }); describe('edgeContrast', () => { /** One row of grey values, as RGBA, two rows high so every pixel has a neighbour below. */ const picture = (row: number[]) => { const rgba = [...row, ...row].flatMap((v) => [v, v, v, 255]); return edgeContrast(rgba, row.length, 2); }; it('scores a hard edge higher than the same edge averaged over two pixels', () => { const hard = picture([0, 0, 200, 200, 0, 0]); const soft = picture([0, 100, 200, 100, 0, 0]); expect(hard).toBeCloseTo(200, 5); expect(soft).toBeCloseTo(100, 5); }); it('is zero for a picture without an edge', () => { expect(picture([40, 40, 41, 40])).toBe(0); }); });