diff --git a/resources/cosmetics/cosmetics.json b/resources/cosmetics/cosmetics.json
index cd6023c60..0efe2818b 100644
--- a/resources/cosmetics/cosmetics.json
+++ b/resources/cosmetics/cosmetics.json
@@ -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"
diff --git a/resources/lang/en.json b/resources/lang/en.json
index 244d1f426..cd9f6aa41 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -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",
diff --git a/src/client/TerritoryPatternsModal.ts b/src/client/TerritoryPatternsModal.ts
index e2788d5fb..226242967 100644
--- a/src/client/TerritoryPatternsModal.ts
+++ b/src/client/TerritoryPatternsModal.ts
@@ -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`
-
-
- ${(() => {
- 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`
-
- `);
- }
- }
- return tiles;
- })()}
-
-
+
`;
}
@@ -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");
+}
diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts
index 6fdd7a48b..21fd3d150 100644
--- a/src/client/graphics/layers/TerritoryLayer.ts
+++ b/src/client/graphics/layers/TerritoryLayer.ts
@@ -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);
}
}
}
diff --git a/src/core/PatternDecoder.ts b/src/core/PatternDecoder.ts
index e489bceef..167732de3 100644
--- a/src/core/PatternDecoder.ts
+++ b/src/core/PatternDecoder.ts
@@ -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;
+ }
}