feat: Add colorblind mode

This commit introduces a colorblind mode for red-green color blindness.

- Adds a new color palette with colorblind-friendly colors.

- Adds a user setting to toggle colorblind mode.

- Modifies the color allocation logic to use the new palette when the setting is enabled.

- Adds a button to the settings modal to toggle the feature.
This commit is contained in:
Restart2008
2025-10-26 17:01:17 -07:00
parent 9f1fc12b6b
commit c79e8022b9
9 changed files with 113 additions and 13 deletions
+2
View File
@@ -306,6 +306,8 @@
"tab_keybinds": "Keybinds",
"dark_mode_label": "Dark Mode",
"dark_mode_desc": "Toggle the sites appearance between light and dark themes",
"user_setting.colorblind_mode_label": "Colorblind Mode",
"user_setting.colorblind_mode_desc": "Adjusts colors for red-green color blindness.",
"emojis_label": "Emojis",
"emojis_desc": "Toggle whether emojis are shown in game",
"alert_frame_label": "Alert Frame",
@@ -136,6 +136,12 @@ export class SettingsModal extends LitElement implements Layer {
this.requestUpdate();
}
private onToggleColorblindModeButtonClick() {
this.userSettings.toggleColorblindMode();
this.eventBus.emit(new RefreshGraphicsEvent());
this.requestUpdate();
}
private onToggleRandomNameModeButtonClick() {
this.userSettings.toggleRandomName();
this.requestUpdate();
@@ -321,6 +327,31 @@ export class SettingsModal extends LitElement implements Layer {
</div>
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleColorblindModeButtonClick}"
>
<img
src=${darkModeIcon}
alt="colorblindModeIcon"
width="20"
height="20"
/>
<div class="flex-1">
<div class="font-medium">
${translateText("user_setting.colorblind_mode_label")}
</div>
<div class="text-sm text-slate-400">
${translateText("user_setting.colorblind_mode_desc")}
</div>
</div>
<div class="text-sm text-slate-400">
${this.userSettings.colorblindMode()
? translateText("user_setting.on")
: translateText("user_setting.off")}
</div>
</button>
<button
class="flex gap-3 items-center w-full text-left p-3 hover:bg-slate-700 rounded text-white transition-colors"
@click="${this.onToggleSpecialEffectsButtonClick}"
+14 -3
View File
@@ -3,14 +3,18 @@ import labPlugin from "colord/plugins/lab";
import lchPlugin from "colord/plugins/lch";
import Color from "colorjs.io";
import { ColoredTeams, Team } from "../game/Game";
import { UserSettings } from "../game/UserSettings";
import { PseudoRandom } from "../PseudoRandom";
import { simpleHash } from "../Util";
import {
blueTeamColors,
botTeamColors,
generateTeamColors,
greenColorblind,
greenTeamColors,
orangeTeamColors,
purpleTeamColors,
redColorblind,
redTeamColors,
tealTeamColors,
yellowTeamColors,
@@ -24,17 +28,22 @@ export class ColorAllocator {
private assigned = new Map<string, Colord>();
private teamPlayerColors = new Map<string, Colord>();
constructor(colors: Colord[], fallback: Colord[]) {
constructor(
colors: Colord[],
fallback: Colord[],
private userSettings: UserSettings,
) {
this.availableColors = [...colors];
this.fallbackColors = [...colors, ...fallback];
}
private getTeamColorVariations(team: Team): Colord[] {
const isColorblind = this.userSettings.colorblindMode();
switch (team) {
case ColoredTeams.Blue:
return blueTeamColors;
case ColoredTeams.Red:
return redTeamColors;
return isColorblind ? generateTeamColors(redColorblind) : redTeamColors;
case ColoredTeams.Teal:
return tealTeamColors;
case ColoredTeams.Purple:
@@ -44,7 +53,9 @@ export class ColorAllocator {
case ColoredTeams.Orange:
return orangeTeamColors;
case ColoredTeams.Green:
return greenTeamColors;
return isColorblind
? generateTeamColors(greenColorblind)
: greenTeamColors;
case ColoredTeams.Bot:
return botTeamColors;
default:
+4 -1
View File
@@ -14,6 +14,9 @@ export const orange = colord({ h: 25, s: 95, l: 53 });
export const green = colord({ h: 128, s: 49, l: 50 });
export const botColor = colord({ h: 36, s: 10, l: 80 });
export const redColorblind = colord({ h: 30, s: 100, l: 50 }); // Orange
export const greenColorblind = colord({ h: 210, s: 100, l: 50 }); // Blue
export const redTeamColors: Colord[] = generateTeamColors(red);
export const blueTeamColors: Colord[] = generateTeamColors(blue);
export const tealTeamColors: Colord[] = generateTeamColors(teal);
@@ -23,7 +26,7 @@ export const orangeTeamColors: Colord[] = generateTeamColors(orange);
export const greenTeamColors: Colord[] = generateTeamColors(green);
export const botTeamColors: Colord[] = [colord(botColor)];
function generateTeamColors(baseColor: Colord): Colord[] {
export function generateTeamColors(baseColor: Colord): Colord[] {
const { h: baseHue, s: baseSaturation, l: baseLightness } = baseColor.toHsl();
const colorCount = 64;
+6 -3
View File
@@ -220,14 +220,17 @@ export abstract class DefaultServerConfig implements ServerConfig {
}
export class DefaultConfig implements Config {
private pastelTheme: PastelTheme = new PastelTheme();
private pastelThemeDark: PastelThemeDark = new PastelThemeDark();
private pastelTheme: PastelTheme;
private pastelThemeDark: PastelThemeDark;
constructor(
private _serverConfig: ServerConfig,
private _gameConfig: GameConfig,
private _userSettings: UserSettings | null,
private _isReplay: boolean,
) {}
) {
this.pastelTheme = new PastelTheme(this.userSettings());
this.pastelThemeDark = new PastelThemeDark(this.userSettings());
}
stripePublishableKey(): string {
return process.env.STRIPE_PUBLISHABLE_KEY ?? "";
+28 -4
View File
@@ -3,6 +3,7 @@ import { PseudoRandom } from "../PseudoRandom";
import { PlayerType, Team, TerrainType } from "../game/Game";
import { GameMap, TileRef } from "../game/GameMap";
import { PlayerView } from "../game/GameView";
import { UserSettings } from "../game/UserSettings";
import { ColorAllocator } from "./ColorAllocator";
import { botColors, fallbackColors, humanColors, nationColors } from "./Colors";
import { Theme } from "./Config";
@@ -12,10 +13,33 @@ type ColorCache = Map<string, Colord>;
export class PastelTheme implements Theme {
private borderColorCache: ColorCache = new Map<string, Colord>();
private rand = new PseudoRandom(123);
private humanColorAllocator = new ColorAllocator(humanColors, fallbackColors);
private botColorAllocator = new ColorAllocator(botColors, botColors);
private teamColorAllocator = new ColorAllocator(humanColors, fallbackColors);
private nationColorAllocator = new ColorAllocator(nationColors, nationColors);
private humanColorAllocator: ColorAllocator;
private botColorAllocator: ColorAllocator;
private teamColorAllocator: ColorAllocator;
private nationColorAllocator: ColorAllocator;
constructor(private userSettings: UserSettings) {
this.humanColorAllocator = new ColorAllocator(
humanColors,
fallbackColors,
userSettings,
);
this.botColorAllocator = new ColorAllocator(
botColors,
botColors,
userSettings,
);
this.teamColorAllocator = new ColorAllocator(
humanColors,
fallbackColors,
userSettings,
);
this.nationColorAllocator = new ColorAllocator(
nationColors,
nationColors,
userSettings,
);
}
private background = colord({ r: 60, g: 60, b: 60 });
private shore = colord({ r: 204, g: 203, b: 158 });
@@ -1,6 +1,7 @@
import { Colord, colord } from "colord";
import { TerrainType } from "../game/Game";
import { GameMap, TileRef } from "../game/GameMap";
import { UserSettings } from "../game/UserSettings";
import { PastelTheme } from "./PastelTheme";
export class PastelThemeDark extends PastelTheme {
@@ -9,6 +10,10 @@ export class PastelThemeDark extends PastelTheme {
private darkWater = colord({ r: 14, g: 11, b: 30 });
private darkShorelineWater = colord({ r: 50, g: 50, b: 50 });
constructor(userSettings: UserSettings) {
super(userSettings);
}
terrainColor(gm: GameMap, tile: TileRef): Colord {
const mag = gm.magnitude(tile);
if (gm.isShore(tile)) {
+8
View File
@@ -128,6 +128,14 @@ export class UserSettings {
}
}
colorblindMode() {
return this.get("settings.colorblindMode", false);
}
toggleColorblindMode() {
this.set("settings.colorblindMode", !this.colorblindMode());
}
// For development only. Used for testing patterns, set in the console manually.
getDevOnlyPattern(): PlayerPattern | undefined {
const data = localStorage.getItem("dev-pattern") ?? undefined;
+15 -2
View File
@@ -14,6 +14,7 @@ import {
yellow,
} from "../src/core/configuration/Colors";
import { ColoredTeams } from "../src/core/game/Game";
import { UserSettings } from "../src/core/game/UserSettings";
const mockColors: Colord[] = [
colord({ r: 255, g: 0, b: 0 }),
@@ -28,11 +29,19 @@ const fallbackMockColors: Colord[] = [
const fallbackColors = [...fallbackMockColors, ...mockColors];
const mockUserSettings = {
colorblindMode: () => false,
} as UserSettings;
describe("ColorAllocator", () => {
let allocator: ColorAllocator;
beforeEach(() => {
allocator = new ColorAllocator(mockColors, fallbackMockColors);
allocator = new ColorAllocator(
mockColors,
fallbackMockColors,
mockUserSettings,
);
});
test("returns a unique color for each new ID", () => {
@@ -67,7 +76,11 @@ describe("ColorAllocator", () => {
});
test("assignBotColor returns deterministic color from botColors", () => {
const allocator = new ColorAllocator(mockColors, mockColors);
const allocator = new ColorAllocator(
mockColors,
mockColors,
mockUserSettings,
);
const id1 = "bot123";
const id2 = "bot456";