import { type FederatedPointerEvent, Graphics } from 'pixi.js'; import { type Size, Theme, Widget } from '../../ui'; const CELL = 20; const GAP = 2; export interface PaletteLayout { columns: number; /** The side of a swatch: at least `CELL`, grown so the row ends flush with the width. */ cell: number; rows: number; } /** As many swatches to a row as the width holds (#201); a width not known yet lays out eight. */ export function paletteLayout(width: number, swatches: number): PaletteLayout { const columns = width > 0 ? Math.max(1, Math.floor((width + GAP) / (CELL + GAP))) : 8; const cell = width > 0 ? Math.max(CELL, Math.floor((width - (columns - 1) * GAP) / columns)) : CELL; return { columns, cell, rows: Math.max(1, Math.ceil(swatches / columns)) }; } /** * The colours of a palette as swatches across the whole width (#130, #201). The first swatch is * index 0 – transparent, drawn as a checkerboard – and the others follow the palette in order, so a * swatch's place is its index. A click picks it; the picked one is outlined. */ export class PaletteGrid extends Widget { private readonly swatches = new Graphics(); private colors: readonly number[] = []; private picked = 1; onPick: (index: number) => void = () => {}; /** Told when the swatches need another number of rows, so the column around lays out again. */ onReflow: () => void = () => {}; constructor() { super(); this.addChild(this.swatches); this.cursor = Theme.hoverCursor; this.on('pointerdown', (e: FederatedPointerEvent) => { const local = this.toLocal(e.global); const { columns, cell } = this.grid(); const column = Math.floor(local.x / (cell + GAP)); const row = Math.floor(local.y / (cell + GAP)); const index = row * columns + column; if (column < columns && index <= this.colors.length) this.onPick(index); e.stopPropagation(); }); } private grid(): PaletteLayout { return paletteLayout(this.w, this.colors.length + 1); } setColors(colors: readonly number[], picked: number): void { const before = this.grid().rows; this.colors = colors; this.picked = picked; if (this.grid().rows !== before) this.onReflow(); this.invalidate(); } override measure(): Size { const { columns, cell, rows } = this.grid(); return { w: columns * (cell + GAP) - GAP, h: rows * (cell + GAP) }; } /** A new width can mean another number of rows, and so another height than the one given. */ override layout(): void { if (this.measure().h !== this.h) queueMicrotask(() => this.onReflow()); } /** Where a swatch's centre sits on screen – the check scripts click it. */ swatchGlobal(index: number): { x: number; y: number } { const p = this.getGlobalPosition(); const { cell } = this.grid(); const at = this.cellAt(index); return { x: p.x + at.x + cell / 2, y: p.y + at.y + cell / 2 }; } private cellAt(index: number): { x: number; y: number } { const { columns, cell } = this.grid(); return { x: (index % columns) * (cell + GAP), y: Math.floor(index / columns) * (cell + GAP), }; } override redraw(): void { const g = this.swatches.clear(); const { cell } = this.grid(); for (let index = 0; index <= this.colors.length; index++) { const { x, y } = this.cellAt(index); if (index === 0) { g.rect(x, y, cell, cell).fill(0x9aa0aa); g.rect(x, y, cell / 2, cell / 2) .rect(x + cell / 2, y + cell / 2, cell / 2, cell / 2) .fill(0x5a5f69); } else g.rect(x, y, cell, cell).fill(this.colors[index - 1] ?? 0); g.rect(x + 0.5, y + 0.5, cell - 1, cell - 1).stroke({ width: 1, color: Theme.colors.border }); } if (this.picked >= 0 && this.picked <= this.colors.length) { const { x, y } = this.cellAt(this.picked); g.rect(x - 1, y - 1, cell + 2, cell + 2).stroke({ width: 2, color: Theme.colors.accent }); } } }