mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-26 06:44:37 +00:00
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:
@@ -306,6 +306,8 @@
|
||||
"tab_keybinds": "Keybinds",
|
||||
"dark_mode_label": "Dark Mode",
|
||||
"dark_mode_desc": "Toggle the site’s 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}"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 ?? "";
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user