From 2b44b68362b39d8378bb4ec3a60e4eedf24c5fae Mon Sep 17 00:00:00 2001 From: Mike Harris Date: Sun, 16 Nov 2025 22:58:34 -0600 Subject: [PATCH] Feature - Improve Structure Color Contrast (#2454) If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #2447 ## Description: This PR updates the logic used to generate structure fill and border colors. Currently, (v0.26.16 and earlier), some light territory colors have structures that are difficult to see and identify. This PR ensures that all territory colors have structures that are easily visible. Instead of using `Colord.lighten()` and `Colord.darken()` to generate structure colors, the logic now: - queries the territory color and border color of the structure owner - Converts these colors to the [LAB color space](https://en.wikipedia.org/wiki/CIELAB_color_space) (which is a human-perception-uniform color space). - Darkens the border color (by decreasing LAB luminance) and sometimes lightens the territory color (by increasing LAB luminance) until a specific `Color Delta` is achieved (currently `delta > 0.5`) - This ensures contrast between the structure and the territory background. Additionally, this PR re-organizes colors in the `Colors.ts` file for better visibility and removes redundant colors from the `nationColors` list. This PR is an implementation of the proposed mock-up posted on imgur in issue #2447. Screenshots of the original, final, and side-by-side comparison of structure colors (for all available player colors) are in the [imgur album](https://imgur.com/a/openfront-color-playground-4cxSbbj). I'd recommend inclusion as a feature/fix for v27. ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: GlacialDrift --- .../graphics/layers/StructureDrawingUtils.ts | 32 +- src/core/configuration/Colors.ts | 328 ++++++++---------- src/core/configuration/Config.ts | 2 + src/core/configuration/PastelTheme.ts | 55 ++- src/core/game/GameView.ts | 11 + 5 files changed, 218 insertions(+), 210 deletions(-) diff --git a/src/client/graphics/layers/StructureDrawingUtils.ts b/src/client/graphics/layers/StructureDrawingUtils.ts index 09ac75814..ad1496dfa 100644 --- a/src/client/graphics/layers/StructureDrawingUtils.ts +++ b/src/client/graphics/layers/StructureDrawingUtils.ts @@ -253,26 +253,13 @@ export class SpriteFactory { structureCanvas.height = Math.ceil(iconSize); const context = structureCanvas.getContext("2d")!; - const tc = owner.territoryColor(); - const bc = owner.borderColor(); - - // Potentially change logic here. Some TC/BC combinations do not provide good color contrast. - const darker = bc.luminance() < tc.luminance() ? bc : tc; - const lighter = bc.luminance() < tc.luminance() ? tc : bc; - - let borderColor: string; - if (isConstruction) { - context.fillStyle = "rgb(198, 198, 198)"; - borderColor = "rgb(128, 127, 127)"; - } else { - context.fillStyle = lighter - .lighten(0.13) - .alpha(renderIcon ? 0.65 : 1) - .toRgbString(); - const darken = darker.isLight() ? 0.17 : 0.15; - borderColor = darker.darken(darken).toRgbString(); - } - context.strokeStyle = borderColor; + // Use structureColors defined from the PlayerView. + context.fillStyle = isConstruction + ? "rgb(198,198,198)" + : owner.structureColors().light.toRgbString(); + context.strokeStyle = isConstruction + ? "rgb(127,127, 127)" + : owner.structureColors().dark.toRgbString(); context.lineWidth = 1; const halfIconSize = iconSize / 2; @@ -400,7 +387,10 @@ export class SpriteFactory { }; const [offsetX, offsetY] = SHAPE_OFFSETS[shape] || [0, 0]; context.drawImage( - this.getImageColored(structureInfo.image, borderColor), + this.getImageColored( + structureInfo.image, + owner.structureColors().dark.toRgbString(), + ), offsetX, offsetY, ); diff --git a/src/core/configuration/Colors.ts b/src/core/configuration/Colors.ts index 7de52c3b0..f90b034fb 100644 --- a/src/core/configuration/Colors.ts +++ b/src/core/configuration/Colors.ts @@ -42,222 +42,174 @@ function generateTeamColors(baseColor: Colord): Colord[] { } export const nationColors: Colord[] = [ + colord("rgb(210,210,100)"), // Lime Yellow + colord("rgb(180,210,120)"), // Light Green + colord("rgb(170,190,100)"), // Yellow Green + colord("rgb(80,200,120)"), // Emerald Green + colord("rgb(130,200,130)"), // Light Sea Green + colord("rgb(140,180,140)"), // Dark Sea Green + colord("rgb(160,190,160)"), // Pale Green + colord("rgb(160,180,140)"), // Dark Olive Green + colord("rgb(100,160,80)"), // Olive Green + colord("rgb(100,140,110)"), // Sea Green + colord("rgb(100,180,160)"), // Aquamarine + colord("rgb(130,180,170)"), // Medium Aquamarine + colord("rgb(170,190,180)"), // Pale Blue Green + colord("rgb(100,130,150)"), // Steel Blue + colord("rgb(120,160,200)"), // Cornflower Blue + colord("rgb(140,150,180)"), // Light Slate Gray + colord("rgb(100,210,210)"), // Turquoise + colord("rgb(140,180,220)"), // Light Blue + colord("rgb(130,170,190)"), // Cadet Blue + colord("rgb(100,180,230)"), // Sky Blue + colord("rgb(80,130,190)"), // Navy Blue + colord("rgb(120,120,190)"), // Periwinkle + colord("rgb(150,110,190)"), // Lavender + colord("rgb(160,120,160)"), // Purple Gray + colord("rgb(170,140,190)"), // Medium Purple + colord("rgb(180,130,180)"), // Plum + colord("rgb(190,140,150)"), // Puce + colord("rgb(180,100,230)"), // Purple + colord("rgb(180,160,180)"), // Mauve + colord("rgb(170,150,170)"), // Dusty Rose + colord("rgb(150,130,150)"), // Thistle + colord("rgb(230,180,180)"), // Light Pink + colord("rgb(210,160,200)"), // Orchid + colord("rgb(230,130,180)"), // Pink + colord("rgb(210,100,160)"), // Hot Pink + colord("rgb(190,100,130)"), // Maroon + colord("rgb(220,120,120)"), // Coral + colord("rgb(200,130,110)"), // Dark Salmon + colord("rgb(230,140,140)"), // Salmon colord("rgb(230,100,100)"), // Bright Red - colord("rgb(100,180,230)"), // Sky Blue - colord("rgb(230,180,80)"), // Golden Yellow - colord("rgb(180,100,230)"), // Purple - colord("rgb(80,200,120)"), // Emerald Green - colord("rgb(230,130,180)"), // Pink - colord("rgb(100,160,80)"), // Olive Green colord("rgb(230,150,100)"), // Peach - colord("rgb(80,130,190)"), // Navy Blue - colord("rgb(210,210,100)"), // Lime Yellow - colord("rgb(190,100,130)"), // Maroon - colord("rgb(100,210,210)"), // Turquoise colord("rgb(210,140,80)"), // Light Orange - colord("rgb(150,110,190)"), // Lavender - colord("rgb(180,210,120)"), // Light Green - colord("rgb(210,100,160)"), // Hot Pink - colord("rgb(100,140,110)"), // Sea Green - colord("rgb(230,180,180)"), // Light Pink - colord("rgb(120,120,190)"), // Periwinkle - colord("rgb(190,170,100)"), // Sand - colord("rgb(100,180,160)"), // Aquamarine - colord("rgb(210,160,200)"), // Orchid - colord("rgb(170,190,100)"), // Yellow Green - colord("rgb(100,130,150)"), // Steel Blue - colord("rgb(230,140,140)"), // Salmon - colord("rgb(140,180,220)"), // Light Blue - colord("rgb(200,160,110)"), // Tan - colord("rgb(180,130,180)"), // Plum - colord("rgb(130,200,130)"), // Light Sea Green - colord("rgb(220,120,120)"), // Coral - colord("rgb(120,160,200)"), // Cornflower Blue - colord("rgb(200,200,140)"), // Khaki - colord("rgb(160,120,160)"), // Purple Gray - colord("rgb(140,180,140)"), // Dark Sea Green - colord("rgb(200,130,110)"), // Dark Salmon - colord("rgb(130,170,190)"), // Cadet Blue - colord("rgb(190,180,160)"), // Tan Gray - colord("rgb(170,140,190)"), // Medium Purple - colord("rgb(160,190,160)"), // Pale Green - colord("rgb(190,150,130)"), // Rosy Brown - colord("rgb(140,150,180)"), // Light Slate Gray - colord("rgb(180,170,140)"), // Dark Khaki - colord("rgb(150,130,150)"), // Thistle - colord("rgb(170,190,180)"), // Pale Blue Green - colord("rgb(190,140,150)"), // Puce - colord("rgb(130,180,170)"), // Medium Aquamarine - colord("rgb(180,160,180)"), // Mauve - colord("rgb(160,180,140)"), // Dark Olive Green - colord("rgb(170,150,170)"), // Dusty Rose - colord("rgb(100,180,230)"), // Sky Blue colord("rgb(230,180,80)"), // Golden Yellow - colord("rgb(180,100,230)"), // Purple - colord("rgb(80,200,120)"), // Emerald Green - colord("rgb(230,130,180)"), // Pink - colord("rgb(100,160,80)"), // Olive Green - colord("rgb(230,150,100)"), // Peach - colord("rgb(80,130,190)"), // Navy Blue - colord("rgb(210,210,100)"), // Lime Yellow - colord("rgb(190,100,130)"), // Maroon - colord("rgb(100,210,210)"), // Turquoise - colord("rgb(210,140,80)"), // Light Orange - colord("rgb(150,110,190)"), // Lavender - colord("rgb(180,210,120)"), // Light Green - colord("rgb(210,100,160)"), // Hot Pink - colord("rgb(100,140,110)"), // Sea Green - colord("rgb(230,180,180)"), // Light Pink - colord("rgb(120,120,190)"), // Periwinkle - colord("rgb(190,170,100)"), // Sand - colord("rgb(100,180,160)"), // Aquamarine - colord("rgb(210,160,200)"), // Orchid - colord("rgb(170,190,100)"), // Yellow Green - colord("rgb(100,130,150)"), // Steel Blue - colord("rgb(230,140,140)"), // Salmon - colord("rgb(140,180,220)"), // Light Blue colord("rgb(200,160,110)"), // Tan - colord("rgb(180,130,180)"), // Plum - colord("rgb(130,200,130)"), // Light Sea Green - colord("rgb(220,120,120)"), // Coral - colord("rgb(120,160,200)"), // Cornflower Blue - colord("rgb(200,200,140)"), // Khaki - colord("rgb(160,120,160)"), // Purple Gray - colord("rgb(140,180,140)"), // Dark Sea Green - colord("rgb(200,130,110)"), // Dark Salmon - colord("rgb(130,170,190)"), // Cadet Blue - colord("rgb(190,180,160)"), // Tan Gray - colord("rgb(170,140,190)"), // Medium Purple - colord("rgb(160,190,160)"), // Pale Green colord("rgb(190,150,130)"), // Rosy Brown - colord("rgb(140,150,180)"), // Light Slate Gray + colord("rgb(190,180,160)"), // Tan Gray colord("rgb(180,170,140)"), // Dark Khaki - colord("rgb(150,130,150)"), // Thistle - colord("rgb(170,190,180)"), // Pale Blue Green - colord("rgb(190,140,150)"), // Puce - colord("rgb(130,180,170)"), // Medium Aquamarine - colord("rgb(180,160,180)"), // Mauve - colord("rgb(160,180,140)"), // Dark Olive Green - colord("rgb(170,150,170)"), // Dusty Rose + colord("rgb(200,200,140)"), // Khaki + colord("rgb(190,170,100)"), // Sand ]; // Bright pastel theme with 64 colors export const humanColors: Colord[] = [ - colord("rgb(16,185,129)"), // Sea Green - colord("rgb(34,197,94)"), // Emerald - colord("rgb(45,212,191)"), // Turquoise - colord("rgb(48,178,180)"), // Teal - colord("rgb(52,211,153)"), // Spearmint - colord("rgb(56,189,248)"), // Light Blue - colord("rgb(59,130,246)"), // Royal Blue - colord("rgb(67,190,84)"), // Fresh Green - colord("rgb(74,222,128)"), // Mint - colord("rgb(79,70,229)"), // Indigo - colord("rgb(82,183,136)"), // Jade - colord("rgb(96,165,250)"), // Sky Blue - colord("rgb(99,202,253)"), // Azure - colord("rgb(110,231,183)"), // Seafoam - colord("rgb(124,58,237)"), // Royal Purple - colord("rgb(125,211,252)"), // Crystal Blue - colord("rgb(132,204,22)"), // Lime - colord("rgb(133,77,14)"), // Chocolate - colord("rgb(134,239,172)"), // Light Green - colord("rgb(147,51,234)"), // Bright Purple - colord("rgb(147,197,253)"), // Powder Blue - colord("rgb(151,255,187)"), // Fresh Mint colord("rgb(163,230,53)"), // Yellow Green - colord("rgb(167,139,250)"), // Periwinkle - colord("rgb(168,85,247)"), // Vibrant Purple - colord("rgb(179,136,255)"), // Light Purple + colord("rgb(132,204,22)"), // Lime + colord("rgb(16,185,129)"), // Sea Green + colord("rgb(52,211,153)"), // Spearmint + colord("rgb(45,212,191)"), // Turquoise + colord("rgb(74,222,128)"), // Mint + colord("rgb(110,231,183)"), // Seafoam + colord("rgb(134,239,172)"), // Light Green + colord("rgb(151,255,187)"), // Fresh Mint colord("rgb(186,255,201)"), // Pale Emerald + colord("rgb(230,250,210)"), // Pastel Lime + colord("rgb(34,197,94)"), // Emerald + colord("rgb(67,190,84)"), // Fresh Green + colord("rgb(82,183,136)"), // Jade + colord("rgb(48,178,180)"), // Teal + colord("rgb(230,255,250)"), // Mint Whisper + colord("rgb(220,240,250)"), // Ice Blue + colord("rgb(233,213,255)"), // Light Lilac + colord("rgb(204,204,255)"), // Soft Lavender Blue + colord("rgb(220,220,255)"), // Meringue Blue + colord("rgb(202,225,255)"), // Baby Blue + colord("rgb(147,197,253)"), // Powder Blue + colord("rgb(125,211,252)"), // Crystal Blue + colord("rgb(99,202,253)"), // Azure + colord("rgb(56,189,248)"), // Light Blue + colord("rgb(96,165,250)"), // Sky Blue + colord("rgb(59,130,246)"), // Royal Blue + colord("rgb(79,70,229)"), // Indigo + colord("rgb(124,58,237)"), // Royal Purple + colord("rgb(147,51,234)"), // Bright Purple + colord("rgb(179,136,255)"), // Light Purple + colord("rgb(167,139,250)"), // Periwinkle + colord("rgb(217,70,239)"), // Fuchsia + colord("rgb(168,85,247)"), // Vibrant Purple colord("rgb(190,92,251)"), // Amethyst colord("rgb(192,132,252)"), // Lavender - colord("rgb(202,138,4)"), // Rich Gold - colord("rgb(202,225,255)"), // Baby Blue - colord("rgb(204,204,255)"), // Soft Lavender Blue - colord("rgb(217,70,239)"), // Fuchsia - colord("rgb(220,38,38)"), // Ruby - colord("rgb(220,220,255)"), // Meringue Blue - colord("rgb(220,240,250)"), // Ice Blue - colord("rgb(230,250,210)"), // Pastel Lime - colord("rgb(230,255,250)"), // Mint Whisper - colord("rgb(233,213,255)"), // Light Lilac - colord("rgb(234,88,12)"), // Burnt Orange - colord("rgb(234,179,8)"), // Sunflower - colord("rgb(235,75,75)"), // Bright Red - colord("rgb(236,72,153)"), // Deep Pink - colord("rgb(239,68,68)"), // Crimson colord("rgb(240,171,252)"), // Orchid - colord("rgb(240,240,200)"), // Light Khaki colord("rgb(244,114,182)"), // Rose + colord("rgb(236,72,153)"), // Deep Pink + colord("rgb(220,38,38)"), // Ruby + colord("rgb(239,68,68)"), // Crimson + colord("rgb(235,75,75)"), // Bright Red colord("rgb(245,101,101)"), // Coral - colord("rgb(245,158,11)"), // Amber colord("rgb(248,113,113)"), // Warm Red - colord("rgb(249,115,22)"), // Tangerine - colord("rgb(250,215,225)"), // Cotton Candy - colord("rgb(250,250,210)"), // Pastel Lemon colord("rgb(251,113,133)"), // Watermelon - colord("rgb(251,146,60)"), // Light Orange - colord("rgb(251,191,36)"), // Marigold - colord("rgb(251,235,245)"), // Rose Powder - colord("rgb(252,165,165)"), // Peach - colord("rgb(252,211,77)"), // Golden colord("rgb(253,164,175)"), // Salmon Pink + colord("rgb(252,165,165)"), // Peach colord("rgb(255,204,229)"), // Blush Pink - colord("rgb(255,223,186)"), // Apricot Cream + colord("rgb(250,215,225)"), // Cotton Candy + colord("rgb(251,235,245)"), // Rose Powder + colord("rgb(240,240,200)"), // Light Khaki + colord("rgb(250,250,210)"), // Pastel Lemon colord("rgb(255,240,200)"), // Vanilla + colord("rgb(255,223,186)"), // Apricot Cream + colord("rgb(252,211,77)"), // Golden + colord("rgb(251,191,36)"), // Marigold + colord("rgb(234,179,8)"), // Sunflower + colord("rgb(202,138,4)"), // Rich Gold + colord("rgb(245,158,11)"), // Amber + colord("rgb(251,146,60)"), // Light Orange + colord("rgb(249,115,22)"), // Tangerine + colord("rgb(234,88,12)"), // Burnt Orange + colord("rgb(133,77,14)"), // Chocolate ]; export const botColors: Colord[] = [ - colord("rgb(190,120,120)"), // Muted Red - colord("rgb(120,160,190)"), // Muted Sky Blue - colord("rgb(190,160,100)"), // Muted Golden Yellow - colord("rgb(160,120,190)"), // Muted Purple - colord("rgb(100,170,130)"), // Muted Emerald Green - colord("rgb(190,130,160)"), // Muted Pink - colord("rgb(120,150,100)"), // Muted Olive Green - colord("rgb(190,140,120)"), // Muted Peach - colord("rgb(100,120,160)"), // Muted Navy Blue - colord("rgb(170,170,120)"), // Muted Lime Yellow - colord("rgb(160,120,130)"), // Muted Maroon - colord("rgb(120,170,170)"), // Muted Turquoise - colord("rgb(170,140,100)"), // Muted Light Orange - colord("rgb(140,120,160)"), // Muted Lavender - colord("rgb(150,170,130)"), // Muted Light Green - colord("rgb(170,120,140)"), // Muted Hot Pink - colord("rgb(120,140,120)"), // Muted Sea Green - colord("rgb(180,160,160)"), // Muted Light Pink - colord("rgb(130,130,160)"), // Muted Periwinkle - colord("rgb(160,150,120)"), // Muted Sand - colord("rgb(120,160,150)"), // Muted Aquamarine - colord("rgb(170,150,170)"), // Muted Orchid - colord("rgb(150,160,120)"), // Muted Yellow Green - colord("rgb(120,130,140)"), // Muted Steel Blue - colord("rgb(180,140,140)"), // Muted Salmon - colord("rgb(140,160,170)"), // Muted Light Blue - colord("rgb(170,150,130)"), // Muted Tan - colord("rgb(160,130,160)"), // Muted Plum - colord("rgb(130,170,130)"), // Muted Light Sea Green - colord("rgb(170,130,130)"), // Muted Coral - colord("rgb(130,150,170)"), // Muted Cornflower Blue - colord("rgb(170,170,140)"), // Muted Khaki - colord("rgb(150,130,150)"), // Muted Purple Gray - colord("rgb(140,160,140)"), // Muted Dark Sea Green - colord("rgb(170,130,120)"), // Muted Dark Salmon - colord("rgb(130,150,160)"), // Muted Cadet Blue - colord("rgb(160,160,150)"), // Muted Tan Gray - colord("rgb(150,140,160)"), // Muted Medium Purple - colord("rgb(150,170,150)"), // Muted Pale Green - colord("rgb(160,140,130)"), // Muted Rosy Brown - colord("rgb(140,150,160)"), // Muted Light Slate Gray - colord("rgb(160,150,140)"), // Muted Dark Khaki - colord("rgb(140,130,140)"), // Muted Thistle - colord("rgb(150,160,160)"), // Muted Pale Blue Green - colord("rgb(160,140,150)"), // Muted Puce - colord("rgb(130,160,150)"), // Muted Medium Aquamarine - colord("rgb(160,150,160)"), // Muted Mauve colord("rgb(150,160,140)"), // Muted Dark Olive Green + colord("rgb(160,160,150)"), // Muted Tan Gray + colord("rgb(170,170,140)"), // Muted Khaki + colord("rgb(170,170,120)"), // Muted Lime Yellow + colord("rgb(150,160,120)"), // Muted Yellow Green + colord("rgb(150,170,130)"), // Muted Light Green + colord("rgb(150,170,150)"), // Muted Pale Green + colord("rgb(130,170,130)"), // Muted Light Sea Green + colord("rgb(140,160,140)"), // Muted Dark Sea Green + colord("rgb(120,150,100)"), // Muted Olive Green + colord("rgb(120,140,120)"), // Muted Sea Green + colord("rgb(100,170,130)"), // Muted Emerald Green + colord("rgb(120,160,150)"), // Muted Aquamarine + colord("rgb(130,160,150)"), // Muted Medium Aquamarine + colord("rgb(120,170,170)"), // Muted Turquoise + colord("rgb(120,160,190)"), // Muted Sky Blue + colord("rgb(130,150,170)"), // Muted Cornflower Blue + colord("rgb(130,150,160)"), // Muted Cadet Blue + colord("rgb(140,150,160)"), // Muted Light Slate Gray + colord("rgb(140,160,170)"), // Muted Light Blue + colord("rgb(150,160,160)"), // Muted Pale Blue Green + colord("rgb(100,120,160)"), // Muted Navy Blue + colord("rgb(120,130,140)"), // Muted Steel Blue + colord("rgb(130,130,160)"), // Muted Periwinkle + colord("rgb(140,130,140)"), // Muted Thistle + colord("rgb(140,120,160)"), // Muted Lavender + colord("rgb(150,130,150)"), // Muted Purple Gray + colord("rgb(150,140,160)"), // Muted Medium Purple + colord("rgb(160,130,160)"), // Muted Plum + colord("rgb(170,150,170)"), // Muted Orchid + colord("rgb(160,120,190)"), // Muted Purple + colord("rgb(160,120,130)"), // Muted Maroon + colord("rgb(170,120,140)"), // Muted Hot Pink + colord("rgb(170,130,120)"), // Muted Dark Salmon + colord("rgb(170,130,130)"), // Muted Coral + colord("rgb(180,140,140)"), // Muted Salmon + colord("rgb(190,130,160)"), // Muted Pink + colord("rgb(190,120,120)"), // Muted Red + colord("rgb(190,140,120)"), // Muted Peach + colord("rgb(190,160,100)"), // Muted Golden Yellow + colord("rgb(170,140,100)"), // Muted Light Orange + colord("rgb(160,140,130)"), // Muted Rosy Brown + colord("rgb(170,150,130)"), // Muted Tan + colord("rgb(160,150,120)"), // Muted Sand + colord("rgb(160,150,140)"), // Muted Dark Khaki + colord("rgb(160,140,150)"), // Muted Puce + colord("rgb(160,150,160)"), // Muted Mauve colord("rgb(150,140,150)"), // Muted Dusty Rose + colord("rgb(180,160,160)"), // Muted Light Pink ]; // Fallback colors for when the color palette is exhausted. diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 696bc8d26..f1f1b03c6 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -190,6 +190,8 @@ export interface Theme { // Don't call directly, use PlayerView territoryColor(playerInfo: PlayerView): Colord; // Don't call directly, use PlayerView + structureColors(territoryColor: Colord): { light: Colord; dark: Colord }; + // Don't call directly, use PlayerView borderColor(territoryColor: Colord): Colord; // Don't call directly, use PlayerView defendedBorderColors(territoryColor: Colord): { light: Colord; dark: Colord }; diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts index 028fec8bf..b798423fa 100644 --- a/src/core/configuration/PastelTheme.ts +++ b/src/core/configuration/PastelTheme.ts @@ -1,4 +1,4 @@ -import { Colord, colord } from "colord"; +import { Colord, colord, LabaColor } from "colord"; import { PseudoRandom } from "../PseudoRandom"; import { PlayerType, Team, TerrainType } from "../game/Game"; import { GameMap, TileRef } from "../game/GameMap"; @@ -65,6 +65,59 @@ export class PastelTheme implements Theme { return this.nationColorAllocator.assignColor(player.id()); } + structureColors(territoryColor: Colord): { light: Colord; dark: Colord } { + // Convert territory color to LAB color space. Territory color is rendered in game with alpha = 150/255, use that here. + const lightLAB = territoryColor.alpha(150 / 255).toLab(); + // Get "border color" from territory color & convert to LAB color space + const darkLAB = this.borderColor(territoryColor).toLab(); + // Calculate the contrast of the two provided colors + let contrast = this.contrast(lightLAB, darkLAB); + + // Don't want excessive contrast, so incrementally increase contrast within a loop. + // Define target values, looping limits, and loop counter + const loopLimit = 10; // Switch from darkening border to lightening fill if loopLimit is reached + const maxIterations = 50; // maximum number of loops allowed, throw error above this limit + const contrastTarget = 0.5; + let loopCount = 0; + + // Adjust luminance by 5 in each iteration. This is a balance between speed and not overdoing contrast changes. + const luminanceChange = 5; + + while (contrast < contrastTarget) { + if (loopCount > maxIterations) { + // Prevent runaway loops + console.warn(`Infinite loop detected during structure color calculation. + Light color: ${colord(lightLAB).toRgbString()}, + Dark color: ${colord(darkLAB).toRgbString()}, + Contrast: ${contrast}`); + break; + + // Increase the light color if the "loop limit" has been reach + // (probably due to the dark color already being as dark as it can be) + } else if (loopCount > loopLimit) { + lightLAB.l = this.clamp(lightLAB.l + luminanceChange); + + // Decrease the dark color first to keep the light color as close + // to the territory color as possible + } else { + darkLAB.l = this.clamp(darkLAB.l - luminanceChange); + } + + // re-calculate contrast and increment loop counter + contrast = this.contrast(lightLAB, darkLAB); + loopCount++; + } + return { light: colord(lightLAB), dark: colord(darkLAB) }; + } + + private contrast(first: LabaColor, second: LabaColor): number { + return colord(first).delta(colord(second)); + } + + private clamp(num: number, low: number = 0, high: number = 100): number { + return Math.min(Math.max(low, num), high); + } + // Don't call directly, use PlayerView borderColor(territoryColor: Colord): Colord { return territoryColor.darken(0.125); diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index ccceacef9..471555751 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -184,6 +184,8 @@ export class PlayerView { private _territoryColor: Colord; private _borderColor: Colord; + // Update here to include structure light and dark colors + private _structureColors: { light: Colord; dark: Colord }; private _defendedBorderColors: { light: Colord; dark: Colord }; constructor( @@ -229,6 +231,11 @@ export class PlayerView { this._territoryColor = defaultTerritoryColor; } + this._structureColors = this.game + .config() + .theme() + .structureColors(this._territoryColor); + const maybeFocusedBorderColor = this.game.myClientID() === this.data.clientID ? this.game.config().theme().focusedBorderColor() @@ -262,6 +269,10 @@ export class PlayerView { return isPrimary ? this._territoryColor : this._borderColor; } + structureColors(): { light: Colord; dark: Colord } { + return this._structureColors; + } + borderColor(tile?: TileRef, isDefended: boolean = false): Colord { if (tile === undefined || !isDefended) { return this._borderColor;