anvilsign in

collin/mahjong · c1de3f15

Cut each face out of the sheet, so a turned tile stops wearing a blue line

Collin Richards · 2026-08-21 20:04 UTC · c1de3f155bd40675a20193a465ff3683cf1015df · parent 5d3e6b14 · browse files

modifiedREADME.md+7 −0
⋯ 446 unchanged lines
447447 the only way to know is to look at it and move something. (`tiles.html` is not
448448 one of the build's entry points, so it does not ship.)
449449
450+Each cell of the sheet is cut out into its own canvas the first time it is
451+wanted. `drawImage` with a source rectangle is allowed to sample past that
452+rectangle when it resamples, and the row under every face is the highlighted
453+variant, whose border is bright blue — so a tile at any angle other than a right
454+one wore a blue fringe along one edge. Square on it never showed, which is why
455+it looked like a rotation bug rather than a spritesheet one.
456+
450457 A tile used to be drawn as two passes — a slab of ivory offset down the screen
451458 with the face on top, so a sliver of the tile's own side showed along the bottom
452459 edge. At the size a discard is actually drawn that never read as thickness; it
⋯ 156 unchanged lines
modifiedsrc/ui/tileArt.ts+52 −6
⋯ 121 unchanged lines
122122 }
123123
124124 /**
125+ * One cell of the sheet, cut out and kept.
126+ *
127+ * `drawImage` with a source rectangle is allowed to sample *past* that
128+ * rectangle when it resamples, and the row under every face is the sheet's own
129+ * highlighted variant, whose border is bright blue. Square on and at a right
130+ * angle nothing resamples across that boundary and all is well; at any other
131+ * angle the blue is dragged in and every turned tile wears a blue fringe along
132+ * one edge. Measured, an arbitrary rotation took the worst blue on the tile
133+ * from 92 — which is the tile's own ink — to 255.
134+ *
135+ * Cut out once at native size there is nothing beside it to bleed, and it is
136+ * the same 92 at every angle and every size.
137+ *
138+ * 43 columns, two rows; the second is the highlighted variant, which is what a
139+ * lifted tile in a hand uses. The tile in your fingers and the one still to be
140+ * claimed get it here for the same reason.
141+ */
142+const cells = new Map<string, HTMLCanvasElement>();
143+let cellsFrom: HTMLImageElement | null = null;
144+
145+function cellOf(sheet: HTMLImageElement, tile: number, lit: boolean): HTMLCanvasElement {
146+ // A different sheet — a hot reload, in practice — is a different set of cells.
147+ if (cellsFrom !== sheet) {
148+ cells.clear();
149+ cellsFrom = sheet;
150+ }
151+ const key = `${tile}:${lit}`;
152+ const had = cells.get(key);
153+ if (had) return had;
154+
155+ const cw = sheet.naturalWidth / SPRITE_COLS;
156+ const ch = sheet.naturalHeight / 2;
157+ const c = document.createElement('canvas');
158+ c.width = Math.round(cw);
159+ c.height = Math.round(ch);
160+ c.getContext('2d')?.drawImage(
161+ sheet,
162+ SPRITE_COL[tile] * cw,
163+ lit ? ch : 0,
164+ cw,
165+ ch,
166+ 0,
167+ 0,
168+ c.width,
169+ c.height,
170+ );
171+ cells.set(key, c);
172+ return c;
173+}
174+
175+/**
125176 * A soft silhouette under a tile.
126177 *
127178 * The blur is baked into a sprite once per tile size rather than applied per
⋯ 109 unchanged lines
237288 }
238289
239290 if (art.faces) {
240- // 43 columns, two rows; the second is the sheet's own highlighted variant,
241- // which is what a lifted tile in a hand uses. The tile in your fingers and
242- // the one still to be claimed get it here for the same reason.
243- const cw = art.faces.naturalWidth / SPRITE_COLS;
244- const ch = art.faces.naturalHeight / 2;
245- ctx.drawImage(art.faces, SPRITE_COL[tile] * cw, lit ? ch : 0, cw, ch, -w / 2, -h / 2, w, h);
291+ ctx.drawImage(cellOf(art.faces, tile, lit), -w / 2, -h / 2, w, h);
246292 } else {
247293 tilePath(ctx, w, h);
248294 ctx.fillStyle = '#f6f0e2';
⋯ 20 unchanged lines