## Description:

Replace the HTML element pattern grid with a PNG image.


![image](https://github.com/user-attachments/assets/557d1f35-1a5e-4bb2-bd6e-4ba4ad1593f9)


![image](https://github.com/user-attachments/assets/426194ef-237d-42d7-a775-d91a7cc161f4)

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors
This commit is contained in:
Scott Anderson
2025-07-01 15:45:18 -04:00
committed by GitHub
parent a604d11b92
commit 47ccbc0473
5 changed files with 86 additions and 105 deletions
+8 -8
View File
@@ -10,6 +10,14 @@
"ABMIDw8": {
"name": "stripes_h"
},
"AAEYAwA": {
"name": "horizontal_stripes",
"role_group": "donor"
},
"AAoACQ": {
"name": "vertical_bars",
"role_group": "donor"
},
"ABMIpaU": {
"name": "checkerboard"
},
@@ -28,10 +36,6 @@
"name": "mini_cross",
"role_group": "donor"
},
"AHE4__8AAAAAAAAAAAAAAAAAAP__AAAAAAAAAAAAAAAAAAA": {
"name": "horizontal_stripes",
"role_group": "donor"
},
"AHI4AOAAkACIAEQAIgARjAhUBCgCKAHQACgB1AILAwUABwA": {
"name": "sword",
"role_group": "donor"
@@ -60,10 +64,6 @@
"name": "circuit_board",
"role_group": "donor"
},
"AHEYSZJJkkmSSZJJkkmSSZJJkg": {
"name": "vertical_bars",
"role_group": "donor"
},
"AHEgzGfznzu43XPoL2fMn_O4O3fdL-g": {
"name": "shells",
"role_group": "donor"
+4 -4
View File
@@ -496,14 +496,15 @@
"pattern": {
"default": "Default",
"custom": "Custom",
"stripes_v": "Vertical Stripes",
"stripes_h": "Horizontal Stripes",
"stripes_v": "Vertical",
"stripes_h": "Horizontal",
"horizontal_stripes": "Horizontal (Alt)",
"vertical_bars": "Vertical (Alt)",
"checkerboard": "Checkerboard",
"choco": "Choco",
"diagonal": "Diagonal",
"cross": "Cross",
"mini_cross": "Mini Cross",
"horizontal_stripes": "Horizontal Stripes (Alt)",
"sword": "Sword",
"sparse_dots": "Sparse Dots",
"evan": "Evan",
@@ -511,7 +512,6 @@
"mountain_ridge": "Mountain Ridge",
"scattered_dots": "Scattered Dots",
"circuit_board": "Circuit Board",
"vertical_bars": "Vertical Bars",
"shells": "Shells",
"-w-": ".w.",
"white_rabbit": "White Rabbit",
+54 -65
View File
@@ -17,7 +17,7 @@ export class TerritoryPatternsModal extends LitElement {
};
public previewButton: HTMLElement | null = null;
public buttonWidth: number = 100;
public buttonWidth: number = 150;
@state() private selectedPattern: string | undefined;
@@ -267,68 +267,12 @@ export class TerritoryPatternsModal extends LitElement {
}
private renderPatternPreview(
pattern: string,
width: number,
height: number,
pattern?: string,
width?: number,
height?: number,
): TemplateResult {
const decoder = new PatternDecoder(pattern);
const cellCountX = decoder.getTileWidth();
const cellCountY = decoder.getTileHeight();
const cellSize =
cellCountX > 0 && cellCountY > 0
? Math.min(height / cellCountY, width / cellCountX)
: 1;
return html`
<div
style="
display: flex;
align-items: center;
justify-content: center;
height: ${height}px;
width: ${width}px;
background-color: #f0f0f0;
border-radius: 4px;
box-sizing: border-box;
overflow: hidden;
position: relative;
"
>
<div
style="
display: grid;
grid-template-columns: repeat(${cellCountX}, ${cellSize}px);
grid-template-rows: repeat(${cellCountY}, ${cellSize}px);
background-color: #ccc;
padding: 2px;
border-radius: 4px;
"
>
${(() => {
const tiles: TemplateResult[] = [];
for (let py = 0; py < cellCountY; py++) {
for (let px = 0; px < cellCountX; px++) {
const x = px << decoder.getScale();
const y = py << decoder.getScale();
const bit = decoder.isSet(x, y);
tiles.push(html`
<div
style="
background-color: ${bit ? "#000" : "transparent"};
border: 1px solid rgba(0, 0, 0, 0.1);
width: ${cellSize}px;
height: ${cellSize}px;
border-radius: 1px;
"
></div>
`);
}
}
return tiles;
})()}
</div>
</div>
<img src="${generatePreviewDataUrl(pattern, width, height)}"></img>
`;
}
@@ -376,10 +320,7 @@ export class TerritoryPatternsModal extends LitElement {
public updatePreview() {
if (this.previewButton === null) return;
const preview =
this.selectedPattern === undefined
? this.renderBlankPreview(48, 48)
: this.renderPatternPreview(this.selectedPattern, 48, 48);
const preview = this.renderPatternPreview(this.selectedPattern, 48, 48);
render(preview, this.previewButton);
}
@@ -418,3 +359,51 @@ export class TerritoryPatternsModal extends LitElement {
this.hoveredPattern = null;
}
}
const DEFAULT_PATTERN_B64 = "AAAAAA"; // Empty 2x2 pattern
const COLOR_SET = [0, 0, 0, 255]; // Black
const COLOR_UNSET = [255, 255, 255, 255]; // White
export function generatePreviewDataUrl(
pattern?: string,
width?: number,
height?: number,
): string {
// Calculate canvas size
const decoder = new PatternDecoder(pattern ?? DEFAULT_PATTERN_B64);
const scaledWidth = decoder.scaledWidth();
const scaledHeight = decoder.scaledHeight();
width =
width === undefined
? scaledWidth
: Math.max(1, Math.floor(width / scaledWidth)) * scaledWidth;
height =
height === undefined
? scaledHeight
: Math.max(1, Math.floor(height / scaledHeight)) * scaledHeight;
// Create the canvas
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2D context not supported");
// Create an image
const imageData = ctx.createImageData(width, height);
const data = imageData.data;
let i = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const rgba = decoder.isSet(x, y) ? COLOR_SET : COLOR_UNSET;
data[i++] = rgba[0]; // Red
data[i++] = rgba[1]; // Green
data[i++] = rgba[2]; // Blue
data[i++] = rgba[3]; // Alpha
}
}
// Create a data URL
ctx.putImageData(imageData, 0, 0);
return canvas.toDataURL("image/png");
}
+2 -7
View File
@@ -317,13 +317,8 @@ export class TerritoryLayer implements Layer {
const baseColor = this.theme.territoryColor(owner);
const decoder = owner.patternDecoder();
if (decoder !== undefined) {
const bit = decoder.isSet(x, y) ? 1 : 0;
const colorToUse = bit ? baseColor.darken(0.2) : baseColor;
this.paintTile(tile, colorToUse, 150);
} else {
this.paintTile(tile, baseColor, 150);
}
const color = decoder?.isSet(x, y) ? baseColor.darken(0.2) : baseColor;
this.paintTile(tile, color, 150);
}
}
}
+18 -21
View File
@@ -2,9 +2,10 @@ import { base64url } from "jose";
export class PatternDecoder {
private bytes: Uint8Array;
private tileWidth: number;
private tileHeight: number;
private scale: number;
readonly height: number;
readonly width: number;
readonly scale: number;
constructor(base64: string) {
this.bytes = base64url.decode(base64);
@@ -24,10 +25,10 @@ export class PatternDecoder {
const byte2 = this.bytes[2];
this.scale = byte1 & 0x07;
this.tileWidth = (((byte2 & 0x03) << 5) | ((byte1 >> 3) & 0x1f)) + 2;
this.tileHeight = ((byte2 >> 2) & 0x3f) + 2;
this.width = (((byte2 & 0x03) << 5) | ((byte1 >> 3) & 0x1f)) + 2;
this.height = ((byte2 >> 2) & 0x3f) + 2;
const expectedBits = this.tileWidth * this.tileHeight;
const expectedBits = this.width * this.height;
const expectedBytes = (expectedBits + 7) >> 3; // Equivalent to: ceil(expectedBits / 8);
if (this.bytes.length - 3 < expectedBytes) {
throw new Error(
@@ -36,26 +37,22 @@ export class PatternDecoder {
}
}
getTileWidth(): number {
return this.tileWidth;
}
getTileHeight(): number {
return this.tileHeight;
}
getScale(): number {
return this.scale;
}
isSet(x: number, y: number): boolean {
const px = (x >> this.scale) % this.tileWidth;
const py = (y >> this.scale) % this.tileHeight;
const idx = py * this.tileWidth + px;
const px = (x >> this.scale) % this.width;
const py = (y >> this.scale) % this.height;
const idx = py * this.width + px;
const byteIndex = idx >> 3;
const bitIndex = idx & 7;
const byte = this.bytes[3 + byteIndex];
if (byte === undefined) throw new Error("Invalid pattern");
return (byte & (1 << bitIndex)) !== 0;
}
scaledHeight(): number {
return this.height << this.scale;
}
scaledWidth(): number {
return this.width << this.scale;
}
}