mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 07:54:20 +00:00
Feature/Move theme system from core to client-side ThemeProvider (#4108)
**Add approved & assigned issue number here:** Resolves #2549 ## Description: Themes are purely for the client's rendering, and the server doesn't need context on them. This PR moves `Theme.ts` from `src/core/configuration` to `src/client/theme` and moves affiliation colors to `render-settings.json`. This is to support the ability to add additional themes more quickly, such as colorblind-friendly themes. No visible changes occur from this refactor. ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: jetaviz --------- Co-authored-by: Josh Harris <josh@wickedsick.com>
This commit is contained in:
@@ -75,6 +75,7 @@ import {
|
||||
} from "./render/gl";
|
||||
import { ALL_UNIT_TYPES, UnitState } from "./render/types";
|
||||
import { SoundManager } from "./sound/SoundManager";
|
||||
import { themeProvider } from "./theme/ThemeProvider";
|
||||
|
||||
export interface LobbyConfig {
|
||||
cosmetics: PlayerCosmeticRefs;
|
||||
@@ -110,6 +111,7 @@ export function joinLobby(
|
||||
console.log(`joining lobby: gameID: ${lobbyConfig.gameID}`);
|
||||
|
||||
const userSettings: UserSettings = new UserSettings();
|
||||
themeProvider.reset(); // fresh colour allocators for this game
|
||||
startGame(lobbyConfig.gameID, lobbyConfig.gameStartInfo?.config ?? {});
|
||||
|
||||
const transport = new Transport(lobbyConfig, eventBus);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
import { PastelTheme } from "../../core/configuration/PastelTheme";
|
||||
import {
|
||||
ColoredTeams,
|
||||
Duos,
|
||||
@@ -17,6 +16,8 @@ import { assignTeamsLobbyPreview } from "../../core/game/TeamAssignment";
|
||||
import { UserSettings } from "../../core/game/UserSettings";
|
||||
import { ClientInfo, TeamCountConfig } from "../../core/Schemas";
|
||||
import { createRandomName, formatPlayerDisplayName } from "../../core/Util";
|
||||
import { Theme } from "../theme/Theme";
|
||||
import { themeProvider } from "../theme/ThemeProvider";
|
||||
import { getTranslatedPlayerTeamLabel, translateText } from "../Utils";
|
||||
|
||||
export interface TeamPreviewData {
|
||||
@@ -37,7 +38,9 @@ export class LobbyTeamView extends LitElement {
|
||||
@property({ type: Number }) nationCount: number = 0;
|
||||
@property({ type: Boolean }) isPublicGame: boolean = false;
|
||||
|
||||
private theme: PastelTheme = new PastelTheme();
|
||||
private get theme(): Theme {
|
||||
return themeProvider.current();
|
||||
}
|
||||
@state() private showTeamColors: boolean = false;
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Colord } from "colord";
|
||||
import { Theme } from "src/core/configuration/Theme";
|
||||
import { assetUrl } from "../../core/AssetUrls";
|
||||
import { TrainType, UnitType } from "../../core/game/Game";
|
||||
import { UnitView } from "../../core/game/GameView";
|
||||
import { Theme } from "../theme/Theme";
|
||||
const atomBombSprite = assetUrl("sprites/atombomb.png");
|
||||
const hydrogenBombSprite = assetUrl("sprites/hydrogenbomb.png");
|
||||
const mirvSprite = assetUrl("sprites/mirv2.png");
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "../../../core/game/GameUpdates";
|
||||
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
|
||||
import { Controller } from "../../Controller";
|
||||
import { themeProvider } from "../../theme/ThemeProvider";
|
||||
import {
|
||||
GoToPlayerEvent,
|
||||
GoToPositionEvent,
|
||||
@@ -166,7 +167,7 @@ export class AttacksDisplay extends LitElement implements Controller {
|
||||
const cached = this.spriteDataURLCache.get(key);
|
||||
if (cached) return cached;
|
||||
try {
|
||||
const canvas = getColoredSprite(unit, this.game.config().theme());
|
||||
const canvas = getColoredSprite(unit, themeProvider.current());
|
||||
const dataURL = canvas.toDataURL();
|
||||
this.spriteDataURLCache.set(key, dataURL);
|
||||
return dataURL;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { GameMode, Team } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { Controller } from "../../Controller";
|
||||
import { Platform } from "../../Platform";
|
||||
import { themeProvider } from "../../theme/ThemeProvider";
|
||||
import { getTranslatedPlayerTeamLabel, translateText } from "../../Utils";
|
||||
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
|
||||
import { SpawnBarVisibleEvent } from "./SpawnTimer";
|
||||
@@ -66,10 +67,7 @@ export class GameLeftSidebar extends LitElement implements Controller {
|
||||
if (!this.playerTeam && this.game.myPlayer()?.team()) {
|
||||
this.playerTeam = this.game.myPlayer()!.team();
|
||||
if (this.playerTeam) {
|
||||
this.playerColor = this.game
|
||||
.config()
|
||||
.theme()
|
||||
.teamColor(this.playerTeam);
|
||||
this.playerColor = themeProvider.current().teamColor(this.playerTeam);
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
MouseMoveEvent,
|
||||
TouchEvent,
|
||||
} from "../../InputHandler";
|
||||
import { themeProvider } from "../../theme/ThemeProvider";
|
||||
import { TransformHandler } from "../../TransformHandler";
|
||||
import {
|
||||
getTranslatedPlayerTeamLabel,
|
||||
@@ -360,9 +361,8 @@ export class PlayerInfoOverlay extends LitElement implements Controller {
|
||||
>
|
||||
<span class="text-xs font-normal text-gray-400"
|
||||
>[<span
|
||||
style="color: ${this.game
|
||||
.config()
|
||||
.theme()
|
||||
style="color: ${themeProvider
|
||||
.current()
|
||||
.teamColor(player.team()!)
|
||||
.toHex()}"
|
||||
>${playerTeam}</span
|
||||
|
||||
@@ -4,6 +4,7 @@ import { EventBus, GameEvent } from "../../../core/EventBus";
|
||||
import { GameMode, GameType, Team } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { Controller } from "../../Controller";
|
||||
import { themeProvider } from "../../theme/ThemeProvider";
|
||||
import { TransformHandler } from "../../TransformHandler";
|
||||
|
||||
export class SpawnBarVisibleEvent implements GameEvent {
|
||||
@@ -71,7 +72,7 @@ export class SpawnTimer extends LitElement implements Controller {
|
||||
teamTiles.set(team, tiles + player.numTilesOwned());
|
||||
}
|
||||
|
||||
const theme = this.game.config().theme();
|
||||
const theme = themeProvider.current();
|
||||
const total = sumIterator(teamTiles.values());
|
||||
if (total > 0) {
|
||||
for (const [team, count] of teamTiles) {
|
||||
|
||||
@@ -79,6 +79,27 @@ export interface RenderSettings {
|
||||
defensePostRange: number;
|
||||
embargoTintRatio: number;
|
||||
friendlyTintRatio: number;
|
||||
embargoTintR: number;
|
||||
embargoTintG: number;
|
||||
embargoTintB: number;
|
||||
friendlyTintR: number;
|
||||
friendlyTintG: number;
|
||||
friendlyTintB: number;
|
||||
};
|
||||
/** Alt-view affiliation colors (0–1 RGB). */
|
||||
affiliation: {
|
||||
selfR: number;
|
||||
selfG: number;
|
||||
selfB: number;
|
||||
allyR: number;
|
||||
allyG: number;
|
||||
allyB: number;
|
||||
neutralR: number;
|
||||
neutralG: number;
|
||||
neutralB: number;
|
||||
enemyR: number;
|
||||
enemyG: number;
|
||||
enemyB: number;
|
||||
};
|
||||
railroad: {
|
||||
railMinZoom: number;
|
||||
|
||||
@@ -478,7 +478,7 @@ export class GPURenderer {
|
||||
this.sceneTarget = { fbo: sceneFbo, tex: sceneTex, w: 1, h: 1 };
|
||||
|
||||
// --- Alt-view passes ---
|
||||
this.affiliationPalette = new AffiliationPalette(gl);
|
||||
this.affiliationPalette = new AffiliationPalette(gl, this.settings);
|
||||
const affTex = this.affiliationPalette.getTexture();
|
||||
this.borderStampPass.setAffiliationTex(affTex);
|
||||
this.unitPass.setAffiliationTex(affTex);
|
||||
|
||||
@@ -27,6 +27,8 @@ export class BorderStampPass {
|
||||
private uDefenseCheckerDarken: WebGLUniformLocation;
|
||||
private uEmbargoTintRatio: WebGLUniformLocation;
|
||||
private uFriendlyTintRatio: WebGLUniformLocation;
|
||||
private uEmbargoTint: WebGLUniformLocation;
|
||||
private uFriendlyTint: WebGLUniformLocation;
|
||||
private uAltView: WebGLUniformLocation;
|
||||
|
||||
private vao: WebGLVertexArrayObject;
|
||||
@@ -79,6 +81,8 @@ export class BorderStampPass {
|
||||
this.program,
|
||||
"uFriendlyTintRatio",
|
||||
)!;
|
||||
this.uEmbargoTint = gl.getUniformLocation(this.program, "uEmbargoTint")!;
|
||||
this.uFriendlyTint = gl.getUniformLocation(this.program, "uFriendlyTint")!;
|
||||
this.uAltView = gl.getUniformLocation(this.program, "uAltView")!;
|
||||
|
||||
gl.useProgram(this.program);
|
||||
@@ -109,6 +113,18 @@ export class BorderStampPass {
|
||||
gl.uniform1f(this.uDefenseCheckerDarken, mo.defenseCheckerDarken);
|
||||
gl.uniform1f(this.uEmbargoTintRatio, mo.embargoTintRatio);
|
||||
gl.uniform1f(this.uFriendlyTintRatio, mo.friendlyTintRatio);
|
||||
gl.uniform3f(
|
||||
this.uEmbargoTint,
|
||||
mo.embargoTintR,
|
||||
mo.embargoTintG,
|
||||
mo.embargoTintB,
|
||||
);
|
||||
gl.uniform3f(
|
||||
this.uFriendlyTint,
|
||||
mo.friendlyTintR,
|
||||
mo.friendlyTintG,
|
||||
mo.friendlyTintB,
|
||||
);
|
||||
gl.uniform1i(this.uAltView, this.altView ? 1 : 0);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
|
||||
@@ -75,7 +75,27 @@
|
||||
"highlightThicken": 2,
|
||||
"defensePostRange": 30,
|
||||
"embargoTintRatio": 0.35,
|
||||
"friendlyTintRatio": 0.35
|
||||
"friendlyTintRatio": 0.35,
|
||||
"embargoTintR": 1,
|
||||
"embargoTintG": 0,
|
||||
"embargoTintB": 0,
|
||||
"friendlyTintR": 0,
|
||||
"friendlyTintG": 1,
|
||||
"friendlyTintB": 0
|
||||
},
|
||||
"affiliation": {
|
||||
"selfR": 0,
|
||||
"selfG": 1,
|
||||
"selfB": 0,
|
||||
"allyR": 1,
|
||||
"allyG": 1,
|
||||
"allyB": 0,
|
||||
"neutralR": 0.502,
|
||||
"neutralG": 0.502,
|
||||
"neutralB": 0.502,
|
||||
"enemyR": 1,
|
||||
"enemyG": 0,
|
||||
"enemyB": 0
|
||||
},
|
||||
"railroad": {
|
||||
"railMinZoom": 4,
|
||||
|
||||
@@ -12,6 +12,8 @@ uniform float uHighlightBrighten;
|
||||
uniform float uDefenseCheckerDarken;
|
||||
uniform float uEmbargoTintRatio;
|
||||
uniform float uFriendlyTintRatio;
|
||||
uniform vec3 uEmbargoTint;
|
||||
uniform vec3 uFriendlyTint;
|
||||
|
||||
in vec2 vWorldPos;
|
||||
out vec4 fragColor;
|
||||
@@ -46,9 +48,9 @@ void main() {
|
||||
}
|
||||
// Relationship tint (applied BEFORE defense checkerboard, matching game)
|
||||
if (relation > 0.75) {
|
||||
bc = mix(bc, vec3(1.0, 0.0, 0.0), uEmbargoTintRatio);
|
||||
bc = mix(bc, uEmbargoTint, uEmbargoTintRatio);
|
||||
} else if (relation > 0.25) {
|
||||
bc = mix(bc, vec3(0.0, 1.0, 0.0), uFriendlyTintRatio);
|
||||
bc = mix(bc, uFriendlyTint, uFriendlyTintRatio);
|
||||
}
|
||||
// Defense bonus: checkerboard darken (applied AFTER tint, matching game)
|
||||
if (defense) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* Rebuilt when localPlayerID or relationship data changes.
|
||||
*/
|
||||
|
||||
import type { RenderSettings } from "../RenderSettings";
|
||||
import { getPaletteSize } from "./ColorUtils";
|
||||
import { createTexture2D } from "./GlUtils";
|
||||
|
||||
@@ -16,20 +17,6 @@ const RELATION_NEUTRAL = 0;
|
||||
const RELATION_FRIENDLY = 1;
|
||||
const RELATION_EMBARGO = 2;
|
||||
|
||||
// Affiliation RGB values (upstream PastelTheme)
|
||||
const SELF_R = 0,
|
||||
SELF_G = 255,
|
||||
SELF_B = 0;
|
||||
const ALLY_R = 255,
|
||||
ALLY_G = 255,
|
||||
ALLY_B = 0;
|
||||
const NEUTRAL_R = 128,
|
||||
NEUTRAL_G = 128,
|
||||
NEUTRAL_B = 128;
|
||||
const ENEMY_R = 255,
|
||||
ENEMY_G = 0,
|
||||
ENEMY_B = 0;
|
||||
|
||||
const TEX_W = getPaletteSize(); // 4096 — covers full 12-bit smallID range
|
||||
const TEX_H = 2;
|
||||
|
||||
@@ -44,7 +31,10 @@ export class AffiliationPalette {
|
||||
private relationData: Uint8Array | null = null;
|
||||
private relationSize = 0;
|
||||
|
||||
constructor(gl: WebGL2RenderingContext) {
|
||||
constructor(
|
||||
gl: WebGL2RenderingContext,
|
||||
private settings: RenderSettings,
|
||||
) {
|
||||
this.gl = gl;
|
||||
this.rebuild(); // initialize to spectator-mode defaults (gray borders, red units)
|
||||
this.tex = createTexture2D(gl, {
|
||||
@@ -100,6 +90,22 @@ export class AffiliationPalette {
|
||||
const rel = this.relationData;
|
||||
const rs = this.relationSize;
|
||||
|
||||
// Affiliation RGB values (0–1) from render-settings, expanded to 0–255.
|
||||
const a = this.settings.affiliation;
|
||||
const to255 = (v: number) => Math.round(v * 255);
|
||||
const SELF_R = to255(a.selfR),
|
||||
SELF_G = to255(a.selfG),
|
||||
SELF_B = to255(a.selfB);
|
||||
const ALLY_R = to255(a.allyR),
|
||||
ALLY_G = to255(a.allyG),
|
||||
ALLY_B = to255(a.allyB);
|
||||
const NEUTRAL_R = to255(a.neutralR),
|
||||
NEUTRAL_G = to255(a.neutralG),
|
||||
NEUTRAL_B = to255(a.neutralB);
|
||||
const ENEMY_R = to255(a.enemyR),
|
||||
ENEMY_G = to255(a.enemyG),
|
||||
ENEMY_B = to255(a.enemyB);
|
||||
|
||||
for (let owner = 0; owner < TEX_W; owner++) {
|
||||
// Determine relationship
|
||||
let relation = RELATION_NEUTRAL;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { colord, Colord, extend } from "colord";
|
||||
import labPlugin from "colord/plugins/lab";
|
||||
import lchPlugin from "colord/plugins/lch";
|
||||
import Color from "colorjs.io";
|
||||
import { ColoredTeams, Team } from "../../core/game/Game";
|
||||
import { PseudoRandom } from "../../core/PseudoRandom";
|
||||
import { simpleHash } from "../../core/Util";
|
||||
import {
|
||||
blueTeamColors,
|
||||
botTeamColors,
|
||||
greenTeamColors,
|
||||
orangeTeamColors,
|
||||
purpleTeamColors,
|
||||
redTeamColors,
|
||||
tealTeamColors,
|
||||
yellowTeamColors,
|
||||
} from "./Colors";
|
||||
extend([lchPlugin]);
|
||||
extend([labPlugin]);
|
||||
|
||||
export class ColorAllocator {
|
||||
private availableColors: Colord[];
|
||||
private fallbackColors: Colord[];
|
||||
private assigned = new Map<string, Colord>();
|
||||
private teamPlayerColors = new Map<string, Colord>();
|
||||
|
||||
constructor(colors: Colord[], fallback: Colord[]) {
|
||||
this.availableColors = [...colors];
|
||||
this.fallbackColors = [...colors, ...fallback];
|
||||
}
|
||||
|
||||
private getTeamColorVariations(team: Team): Colord[] {
|
||||
switch (team) {
|
||||
case ColoredTeams.Blue:
|
||||
return blueTeamColors;
|
||||
case ColoredTeams.Red:
|
||||
return redTeamColors;
|
||||
case ColoredTeams.Teal:
|
||||
return tealTeamColors;
|
||||
case ColoredTeams.Purple:
|
||||
return purpleTeamColors;
|
||||
case ColoredTeams.Yellow:
|
||||
return yellowTeamColors;
|
||||
case ColoredTeams.Orange:
|
||||
return orangeTeamColors;
|
||||
case ColoredTeams.Green:
|
||||
return greenTeamColors;
|
||||
case ColoredTeams.Bot:
|
||||
return botTeamColors;
|
||||
case ColoredTeams.Humans:
|
||||
return blueTeamColors;
|
||||
case ColoredTeams.Nations:
|
||||
return redTeamColors;
|
||||
default:
|
||||
return [this.assignColor(team)];
|
||||
}
|
||||
}
|
||||
|
||||
assignColor(id: string): Colord {
|
||||
if (this.assigned.has(id)) {
|
||||
return this.assigned.get(id)!;
|
||||
}
|
||||
|
||||
if (this.availableColors.length === 0) {
|
||||
this.availableColors = [...this.fallbackColors];
|
||||
}
|
||||
|
||||
let selectedIndex: number;
|
||||
|
||||
if (this.assigned.size === 0 || this.assigned.size > 50) {
|
||||
// Randomly pick the first color if no colors have been assigned yet.
|
||||
//
|
||||
// Or if more than 50 colors assigned just pick a random one for perf reasons,
|
||||
// as selecting a distinct color is O(n^2), and the color palette is mostly exhausted anyways.
|
||||
const rand = new PseudoRandom(simpleHash(id));
|
||||
selectedIndex = rand.nextInt(0, this.availableColors.length);
|
||||
} else {
|
||||
const assignedColors = Array.from(this.assigned.values());
|
||||
selectedIndex =
|
||||
selectDistinctColorIndex(this.availableColors, assignedColors) ?? 0;
|
||||
}
|
||||
|
||||
const color = this.availableColors.splice(selectedIndex, 1)[0];
|
||||
this.assigned.set(id, color);
|
||||
return color;
|
||||
}
|
||||
|
||||
assignTeamColor(team: Team): Colord {
|
||||
const teamColors = this.getTeamColorVariations(team);
|
||||
const rgb = teamColors[0].toRgb();
|
||||
rgb.r = Math.round(rgb.r);
|
||||
rgb.g = Math.round(rgb.g);
|
||||
rgb.b = Math.round(rgb.b);
|
||||
return colord(rgb);
|
||||
}
|
||||
|
||||
assignTeamPlayerColor(team: Team, playerId: string): Colord {
|
||||
if (this.teamPlayerColors.has(playerId)) {
|
||||
return this.teamPlayerColors.get(playerId)!;
|
||||
}
|
||||
|
||||
const teamColors = this.getTeamColorVariations(team);
|
||||
const hashValue = simpleHash(playerId);
|
||||
const colorIndex = hashValue % teamColors.length;
|
||||
const color = teamColors[colorIndex];
|
||||
|
||||
this.teamPlayerColors.set(playerId, color);
|
||||
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
// Select a distinct color index from the available colors that
|
||||
// is most different from the assigned colors
|
||||
export function selectDistinctColorIndex(
|
||||
availableColors: Colord[],
|
||||
assignedColors: Colord[],
|
||||
): number | null {
|
||||
if (assignedColors.length === 0) {
|
||||
throw new Error("No assigned colors");
|
||||
}
|
||||
|
||||
const assignedLabColors = assignedColors.map(toColor);
|
||||
|
||||
let maxDeltaE = 0;
|
||||
let maxIndex = 0;
|
||||
|
||||
for (let i = 0; i < availableColors.length; i++) {
|
||||
const color = availableColors[i];
|
||||
const deltaE = minDeltaE(toColor(color), assignedLabColors);
|
||||
if (deltaE > maxDeltaE) {
|
||||
maxDeltaE = deltaE;
|
||||
maxIndex = i;
|
||||
}
|
||||
}
|
||||
return maxIndex;
|
||||
}
|
||||
|
||||
function minDeltaE(lab1: Color, assignedLabColors: Color[]) {
|
||||
return assignedLabColors.reduce((min, assigned) => {
|
||||
return Math.min(min, deltaE2000(lab1, assigned));
|
||||
}, Infinity);
|
||||
}
|
||||
|
||||
function deltaE2000(c1: Color, c2: Color): number {
|
||||
return c1.deltaE(c2, "2000");
|
||||
}
|
||||
|
||||
function toColor(colord: Colord): Color {
|
||||
const lab = colord.toLab();
|
||||
return new Color("lab", [lab.l, lab.a, lab.b]);
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
import { colord, Colord, extend } from "colord";
|
||||
import labPlugin from "colord/plugins/lab";
|
||||
import lchPlugin from "colord/plugins/lch";
|
||||
|
||||
extend([lchPlugin]);
|
||||
extend([labPlugin]);
|
||||
|
||||
export const red = colord("rgb(235,51,51)");
|
||||
export const blue = colord("rgb(41,98,255)");
|
||||
export const teal = colord("rgb(43,212,189)");
|
||||
export const purple = colord("rgb(146,52,234)");
|
||||
export const yellow = colord("rgb(231,176,8)");
|
||||
export const orange = colord("rgb(249,116,21)");
|
||||
export const green = colord("rgb(65,190,82)");
|
||||
export const botColor = colord("rgb(209,205,199)");
|
||||
|
||||
export const redTeamColors: Colord[] = generateTeamColors(red);
|
||||
export const blueTeamColors: Colord[] = generateTeamColors(blue);
|
||||
export const tealTeamColors: Colord[] = generateTeamColors(teal);
|
||||
export const purpleTeamColors: Colord[] = generateTeamColors(purple);
|
||||
export const yellowTeamColors: Colord[] = generateTeamColors(yellow);
|
||||
export const orangeTeamColors: Colord[] = generateTeamColors(orange);
|
||||
export const greenTeamColors: Colord[] = generateTeamColors(green);
|
||||
export const botTeamColors: Colord[] = [botColor];
|
||||
|
||||
function generateTeamColors(baseColor: Colord): Colord[] {
|
||||
const lch = baseColor.toLch();
|
||||
const colorCount = 64;
|
||||
const goldenAngle = 137.508;
|
||||
|
||||
return Array.from({ length: colorCount }, (_, index) => {
|
||||
if (index === 0) return baseColor;
|
||||
|
||||
// Spread hues evenly across ±6° band using golden angle within that range
|
||||
const hueShift = ((index * goldenAngle) % 12) - 6;
|
||||
const h = (lch.h + hueShift + 360) % 360;
|
||||
|
||||
// Chroma oscillates ±10% around the base to add variety without washing out
|
||||
const chromaFactor = 1.0 + 0.1 * Math.sin(index * 0.7);
|
||||
const c = Math.max(10, Math.min(130, lch.c * chromaFactor));
|
||||
|
||||
// Lightness alternates above/below the base using golden angle spacing
|
||||
// Tighter range (±18) keeps teammates recognizable as the same team
|
||||
const lightOffset = 18 * Math.sin(index * goldenAngle * (Math.PI / 180));
|
||||
const l = Math.max(25, Math.min(80, lch.l + lightOffset));
|
||||
|
||||
return colord({ l, c, h });
|
||||
});
|
||||
}
|
||||
|
||||
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(230,150,100)"), // Peach
|
||||
colord("rgb(210,140,80)"), // Light Orange
|
||||
colord("rgb(230,180,80)"), // Golden Yellow
|
||||
colord("rgb(200,160,110)"), // Tan
|
||||
colord("rgb(190,150,130)"), // Rosy Brown
|
||||
colord("rgb(190,180,160)"), // Tan Gray
|
||||
colord("rgb(180,170,140)"), // Dark Khaki
|
||||
colord("rgb(200,200,140)"), // Khaki
|
||||
colord("rgb(190,170,100)"), // Sand
|
||||
];
|
||||
|
||||
// Bright pastel theme with 64 colors
|
||||
export const humanColors: Colord[] = [
|
||||
colord("rgb(163,230,53)"), // Yellow Green
|
||||
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(240,171,252)"), // Orchid
|
||||
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(248,113,113)"), // Warm Red
|
||||
colord("rgb(251,113,133)"), // Watermelon
|
||||
colord("rgb(253,164,175)"), // Salmon Pink
|
||||
colord("rgb(252,165,165)"), // Peach
|
||||
colord("rgb(255,204,229)"), // Blush Pink
|
||||
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(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.
|
||||
export const fallbackColors: Colord[] = [
|
||||
colord("rgb(35,0,0)"),
|
||||
colord("rgb(45,0,0)"),
|
||||
colord("rgb(55,0,0)"),
|
||||
colord("rgb(65,0,0)"),
|
||||
colord("rgb(75,0,0)"),
|
||||
colord("rgb(85,0,0)"),
|
||||
colord("rgb(95,0,0)"),
|
||||
colord("rgb(105,0,0)"),
|
||||
colord("rgb(115,0,0)"),
|
||||
colord("rgb(125,0,0)"),
|
||||
colord("rgb(135,0,0)"),
|
||||
colord("rgb(145,0,0)"),
|
||||
colord("rgb(155,0,0)"),
|
||||
colord("rgb(165,0,0)"),
|
||||
colord("rgb(175,0,0)"),
|
||||
colord("rgb(185,0,0)"),
|
||||
colord("rgb(195,0,5)"),
|
||||
colord("rgb(205,0,10)"),
|
||||
colord("rgb(215,0,15)"),
|
||||
colord("rgb(225,0,20)"),
|
||||
colord("rgb(235,0,25)"),
|
||||
colord("rgb(245,0,30)"),
|
||||
colord("rgb(255,0,35)"),
|
||||
colord("rgb(255,10,45)"),
|
||||
colord("rgb(255,20,55)"),
|
||||
colord("rgb(255,30,65)"),
|
||||
colord("rgb(255,40,75)"),
|
||||
colord("rgb(255,50,85)"),
|
||||
colord("rgb(255,60,95)"),
|
||||
colord("rgb(255,70,105)"),
|
||||
colord("rgb(255,80,115)"),
|
||||
colord("rgb(255,90,125)"),
|
||||
colord("rgb(255,100,135)"),
|
||||
colord("rgb(255,110,145)"),
|
||||
colord("rgb(255,120,155)"),
|
||||
colord("rgb(255,130,165)"),
|
||||
colord("rgb(255,140,175)"),
|
||||
colord("rgb(255,150,185)"),
|
||||
colord("rgb(255,160,195)"),
|
||||
colord("rgb(255,170,205)"),
|
||||
colord("rgb(255,180,215)"),
|
||||
colord("rgb(255,190,225)"),
|
||||
colord("rgb(255,200,235)"),
|
||||
colord("rgb(0,45,0)"),
|
||||
colord("rgb(0,55,0)"),
|
||||
colord("rgb(0,65,0)"),
|
||||
colord("rgb(0,75,0)"),
|
||||
colord("rgb(0,85,0)"),
|
||||
colord("rgb(0,95,0)"),
|
||||
colord("rgb(0,105,0)"),
|
||||
colord("rgb(0,115,0)"),
|
||||
colord("rgb(0,125,0)"),
|
||||
colord("rgb(0,135,0)"),
|
||||
colord("rgb(0,145,0)"),
|
||||
colord("rgb(0,155,0)"),
|
||||
colord("rgb(0,165,0)"),
|
||||
colord("rgb(0,175,0)"),
|
||||
colord("rgb(0,185,0)"),
|
||||
colord("rgb(0,195,5)"),
|
||||
colord("rgb(0,205,10)"),
|
||||
colord("rgb(0,215,15)"),
|
||||
colord("rgb(0,225,20)"),
|
||||
colord("rgb(0,235,25)"),
|
||||
colord("rgb(0,245,30)"),
|
||||
colord("rgb(0,255,35)"),
|
||||
colord("rgb(10,255,45)"),
|
||||
colord("rgb(20,255,55)"),
|
||||
colord("rgb(30,255,65)"),
|
||||
colord("rgb(40,255,75)"),
|
||||
colord("rgb(50,255,85)"),
|
||||
colord("rgb(60,255,95)"),
|
||||
colord("rgb(70,255,105)"),
|
||||
colord("rgb(80,255,115)"),
|
||||
colord("rgb(90,255,125)"),
|
||||
colord("rgb(100,255,135)"),
|
||||
colord("rgb(110,255,145)"),
|
||||
colord("rgb(120,255,155)"),
|
||||
colord("rgb(130,255,165)"),
|
||||
colord("rgb(140,255,175)"),
|
||||
colord("rgb(150,255,185)"),
|
||||
colord("rgb(160,255,195)"),
|
||||
colord("rgb(170,255,205)"),
|
||||
colord("rgb(180,255,215)"),
|
||||
colord("rgb(190,255,225)"),
|
||||
colord("rgb(200,255,235)"),
|
||||
colord("rgb(0,0,35)"),
|
||||
colord("rgb(0,0,45)"),
|
||||
colord("rgb(0,0,55)"),
|
||||
colord("rgb(0,0,65)"),
|
||||
colord("rgb(0,0,75)"),
|
||||
colord("rgb(0,0,85)"),
|
||||
colord("rgb(0,0,95)"),
|
||||
colord("rgb(0,0,105)"),
|
||||
colord("rgb(0,0,115)"),
|
||||
colord("rgb(0,0,125)"),
|
||||
colord("rgb(0,0,135)"),
|
||||
colord("rgb(0,0,145)"),
|
||||
colord("rgb(0,0,155)"),
|
||||
colord("rgb(0,0,165)"),
|
||||
colord("rgb(0,0,175)"),
|
||||
colord("rgb(0,0,185)"),
|
||||
colord("rgb(5,0,195)"),
|
||||
colord("rgb(10,0,205)"),
|
||||
colord("rgb(15,0,215)"),
|
||||
colord("rgb(20,0,225)"),
|
||||
colord("rgb(25,0,235)"),
|
||||
colord("rgb(30,0,245)"),
|
||||
colord("rgb(35,0,255)"),
|
||||
colord("rgb(45,10,255)"),
|
||||
colord("rgb(55,20,255)"),
|
||||
colord("rgb(65,30,255)"),
|
||||
colord("rgb(75,40,255)"),
|
||||
colord("rgb(85,50,255)"),
|
||||
colord("rgb(95,60,255)"),
|
||||
colord("rgb(105,70,255)"),
|
||||
colord("rgb(115,80,255)"),
|
||||
colord("rgb(125,90,255)"),
|
||||
colord("rgb(135,100,255)"),
|
||||
colord("rgb(145,110,255)"),
|
||||
colord("rgb(155,120,255)"),
|
||||
colord("rgb(165,130,255)"),
|
||||
colord("rgb(175,140,255)"),
|
||||
colord("rgb(185,150,255)"),
|
||||
colord("rgb(195,160,255)"),
|
||||
colord("rgb(205,170,255)"),
|
||||
colord("rgb(215,180,255)"),
|
||||
colord("rgb(225,190,255)"),
|
||||
colord("rgb(235,200,255)"),
|
||||
colord("rgb(35,0,35)"),
|
||||
colord("rgb(45,0,45)"),
|
||||
colord("rgb(55,0,55)"),
|
||||
colord("rgb(65,0,65)"),
|
||||
colord("rgb(75,0,75)"),
|
||||
colord("rgb(85,0,85)"),
|
||||
colord("rgb(95,0,95)"),
|
||||
colord("rgb(105,0,105)"),
|
||||
colord("rgb(115,0,115)"),
|
||||
colord("rgb(125,0,125)"),
|
||||
colord("rgb(135,0,135)"),
|
||||
colord("rgb(145,0,145)"),
|
||||
colord("rgb(155,0,155)"),
|
||||
colord("rgb(165,0,165)"),
|
||||
colord("rgb(175,0,175)"),
|
||||
colord("rgb(185,0,185)"),
|
||||
colord("rgb(195,5,195)"),
|
||||
colord("rgb(205,10,205)"),
|
||||
colord("rgb(215,15,215)"),
|
||||
colord("rgb(225,20,225)"),
|
||||
colord("rgb(235,25,235)"),
|
||||
colord("rgb(245,30,245)"),
|
||||
colord("rgb(255,35,255)"),
|
||||
colord("rgb(255,45,255)"),
|
||||
colord("rgb(255,55,255)"),
|
||||
colord("rgb(255,65,255)"),
|
||||
colord("rgb(255,75,255)"),
|
||||
colord("rgb(255,85,255)"),
|
||||
colord("rgb(255,95,255)"),
|
||||
colord("rgb(255,105,255)"),
|
||||
colord("rgb(255,115,255)"),
|
||||
colord("rgb(255,125,255)"),
|
||||
colord("rgb(255,135,255)"),
|
||||
colord("rgb(255,145,255)"),
|
||||
colord("rgb(255,155,255)"),
|
||||
colord("rgb(255,165,255)"),
|
||||
colord("rgb(255,175,255)"),
|
||||
colord("rgb(255,185,255)"),
|
||||
colord("rgb(255,195,255)"),
|
||||
colord("rgb(255,205,255)"),
|
||||
colord("rgb(255,215,255)"),
|
||||
colord("rgb(0,35,35)"),
|
||||
colord("rgb(0,45,45)"),
|
||||
colord("rgb(0,55,55)"),
|
||||
colord("rgb(0,65,65)"),
|
||||
colord("rgb(0,75,75)"),
|
||||
colord("rgb(0,85,85)"),
|
||||
colord("rgb(0,95,95)"),
|
||||
colord("rgb(0,105,105)"),
|
||||
colord("rgb(0,115,115)"),
|
||||
colord("rgb(0,125,125)"),
|
||||
colord("rgb(0,135,135)"),
|
||||
colord("rgb(0,145,145)"),
|
||||
colord("rgb(0,155,155)"),
|
||||
colord("rgb(0,165,165)"),
|
||||
colord("rgb(0,175,175)"),
|
||||
colord("rgb(0,185,185)"),
|
||||
colord("rgb(5,195,195)"),
|
||||
colord("rgb(10,205,205)"),
|
||||
colord("rgb(15,215,215)"),
|
||||
colord("rgb(20,225,225)"),
|
||||
colord("rgb(25,235,235)"),
|
||||
colord("rgb(30,245,245)"),
|
||||
colord("rgb(35,255,255)"),
|
||||
colord("rgb(45,255,255)"),
|
||||
colord("rgb(55,255,255)"),
|
||||
colord("rgb(65,255,255)"),
|
||||
colord("rgb(75,255,255)"),
|
||||
colord("rgb(85,255,255)"),
|
||||
colord("rgb(95,255,255)"),
|
||||
colord("rgb(105,255,255)"),
|
||||
colord("rgb(115,255,255)"),
|
||||
colord("rgb(125,255,255)"),
|
||||
colord("rgb(135,255,255)"),
|
||||
colord("rgb(145,255,255)"),
|
||||
colord("rgb(155,255,255)"),
|
||||
colord("rgb(165,255,255)"),
|
||||
colord("rgb(175,255,255)"),
|
||||
colord("rgb(185,255,255)"),
|
||||
colord("rgb(195,255,255)"),
|
||||
colord("rgb(205,255,255)"),
|
||||
colord("rgb(215,255,255)"),
|
||||
colord("rgb(35,35,0)"),
|
||||
colord("rgb(45,45,0)"),
|
||||
colord("rgb(55,55,0)"),
|
||||
colord("rgb(65,65,0)"),
|
||||
colord("rgb(75,75,0)"),
|
||||
colord("rgb(85,85,0)"),
|
||||
colord("rgb(95,95,0)"),
|
||||
colord("rgb(105,105,0)"),
|
||||
colord("rgb(115,115,0)"),
|
||||
colord("rgb(125,125,0)"),
|
||||
colord("rgb(135,135,0)"),
|
||||
colord("rgb(145,145,0)"),
|
||||
colord("rgb(155,155,0)"),
|
||||
colord("rgb(165,165,0)"),
|
||||
colord("rgb(175,175,0)"),
|
||||
colord("rgb(185,185,0)"),
|
||||
colord("rgb(195,195,5)"),
|
||||
colord("rgb(205,205,10)"),
|
||||
colord("rgb(215,215,15)"),
|
||||
colord("rgb(225,225,20)"),
|
||||
colord("rgb(235,235,25)"),
|
||||
colord("rgb(245,245,30)"),
|
||||
colord("rgb(255,255,35)"),
|
||||
colord("rgb(255,255,45)"),
|
||||
colord("rgb(255,255,55)"),
|
||||
colord("rgb(255,255,65)"),
|
||||
colord("rgb(255,255,75)"),
|
||||
colord("rgb(255,255,85)"),
|
||||
colord("rgb(255,255,95)"),
|
||||
colord("rgb(255,255,105)"),
|
||||
colord("rgb(255,255,115)"),
|
||||
colord("rgb(255,255,125)"),
|
||||
colord("rgb(255,255,135)"),
|
||||
colord("rgb(255,255,145)"),
|
||||
colord("rgb(255,255,155)"),
|
||||
colord("rgb(255,255,165)"),
|
||||
colord("rgb(255,255,175)"),
|
||||
colord("rgb(255,255,185)"),
|
||||
colord("rgb(255,255,195)"),
|
||||
colord("rgb(255,255,205)"),
|
||||
colord("rgb(255,255,215)"),
|
||||
colord("rgb(215,255,200)"), // Fresh Mint
|
||||
colord("rgb(225,255,175)"), // Soft Lime
|
||||
colord("rgb(240,250,160)"), // Citrus Wash
|
||||
colord("rgb(245,245,175)"), // Lemon Mist
|
||||
colord("rgb(150,200,255)"), // Cornflower Mist
|
||||
colord("rgb(160,215,255)"), // Powder Blue
|
||||
colord("rgb(170,225,255)"), // Baby Sky
|
||||
colord("rgb(180,235,250)"), // Aqua Pastel
|
||||
colord("rgb(190,245,240)"), // Ice Mint
|
||||
colord("rgb(210,255,245)"), // Sea Mist
|
||||
colord("rgb(220,255,255)"), // Pale Aqua
|
||||
colord("rgb(230,250,255)"), // Sky Haze
|
||||
colord("rgb(240,240,255)"), // Frosted Lilac
|
||||
colord("rgb(250,230,255)"), // Misty Mauve
|
||||
colord("rgb(170,190,255)"), // Periwinkle Ice
|
||||
colord("rgb(180,180,255)"), // Pale Indigo
|
||||
colord("rgb(200,170,255)"), // Lilac Bloom
|
||||
colord("rgb(190,140,195)"), // Fuchsia Tint
|
||||
colord("rgb(195,145,200)"), // Dusky Rose
|
||||
colord("rgb(200,150,205)"), // Plum Frost
|
||||
colord("rgb(205,155,210)"), // Berry Foam
|
||||
colord("rgb(210,160,215)"), // Grape Cloud
|
||||
colord("rgb(215,165,220)"), // Light Bloom
|
||||
colord("rgb(220,170,225)"), // Cherry Blossom
|
||||
colord("rgb(225,175,230)"), // Faded Rose
|
||||
colord("rgb(230,180,235)"), // Dreamy Mauve
|
||||
colord("rgb(235,185,240)"), // Powder Violet
|
||||
colord("rgb(240,190,245)"), // Pastel Violet
|
||||
colord("rgb(245,195,250)"), // Soft Magenta
|
||||
colord("rgb(250,200,255)"), // Lilac Cream
|
||||
colord("rgb(255,205,255)"), // Violet Bloom
|
||||
colord("rgb(255,210,255)"), // Orchid Mist
|
||||
colord("rgb(255,210,250)"), // Lavender Mist
|
||||
colord("rgb(255,205,245)"), // Pastel Orchid
|
||||
colord("rgb(255,215,245)"), // Rose Whisper
|
||||
colord("rgb(220,160,255)"), // Violet Mist
|
||||
colord("rgb(235,150,255)"), // Orchid Glow
|
||||
colord("rgb(245,160,240)"), // Rose Lilac
|
||||
colord("rgb(255,170,225)"), // Bubblegum Pink
|
||||
colord("rgb(255,185,215)"), // Blush Mist
|
||||
colord("rgb(255,195,235)"), // Faded Fuchsia
|
||||
colord("rgb(255,200,220)"), // Cotton Rose
|
||||
colord("rgb(255,210,230)"), // Pastel Blush
|
||||
colord("rgb(255,220,235)"), // Pink Mist
|
||||
colord("rgb(255,220,250)"), // Powder Petal
|
||||
colord("rgb(255,225,255)"), // Petal Mist
|
||||
colord("rgb(255,230,245)"), // Light Rose
|
||||
colord("rgb(255,235,235)"), // Blushed Petal
|
||||
colord("rgb(255,215,195)"), // Apricot Glow
|
||||
colord("rgb(255,225,180)"), // Butter Peach
|
||||
colord("rgb(255,230,190)"),
|
||||
colord("rgb(255,235,200)"), // Cream Peach
|
||||
colord("rgb(255,245,210)"), // Soft Banana
|
||||
colord("rgb(255,240,220)"), // Pastel Sand
|
||||
];
|
||||
@@ -0,0 +1,206 @@
|
||||
import { Colord, colord, LabaColor } from "colord";
|
||||
import { PseudoRandom } from "../../core/PseudoRandom";
|
||||
import { PlayerType, Team, TerrainType } from "../../core/game/Game";
|
||||
import { GameMap, TileRef } from "../../core/game/GameMap";
|
||||
import { PlayerView } from "../../core/game/GameView";
|
||||
import { ColorAllocator } from "./ColorAllocator";
|
||||
import { botColors, fallbackColors, humanColors, nationColors } from "./Colors";
|
||||
import { Theme } from "./Theme";
|
||||
|
||||
export class PastelTheme implements Theme {
|
||||
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 background = colord("rgb(60,60,60)");
|
||||
private shore = colord("rgb(204,203,158)");
|
||||
private falloutColors = [
|
||||
colord("rgb(120,255,71)"), // Original color
|
||||
colord("rgb(130,255,85)"), // Slightly lighter
|
||||
colord("rgb(110,245,65)"), // Slightly darker
|
||||
colord("rgb(125,255,75)"), // Warmer tint
|
||||
colord("rgb(115,250,68)"), // Cooler tint
|
||||
];
|
||||
private water = colord("rgb(70,132,180)");
|
||||
private shorelineWater = colord("rgb(100,143,255)");
|
||||
|
||||
/** Default spawn highlight colors for other players in FFA, yellow */
|
||||
private _spawnHighlightColor = colord("rgb(255,213,79)");
|
||||
/** Added non-default spawn highlight colors for self, full white */
|
||||
private _spawnHighlightSelfColor = colord("rgb(255,255,255)");
|
||||
/** Added non-default spawn highlight colors for teammates, green */
|
||||
private _spawnHighlightTeamColor = colord("rgb(0,255,0)");
|
||||
/** Added non-default spawn highlight colors for enemies, red */
|
||||
private _spawnHighlightEnemyColor = colord("rgb(255,0,0)");
|
||||
|
||||
teamColor(team: Team): Colord {
|
||||
return this.teamColorAllocator.assignTeamColor(team);
|
||||
}
|
||||
|
||||
territoryColor(player: PlayerView): Colord {
|
||||
const team = player.team();
|
||||
if (team !== null) {
|
||||
return this.teamColorAllocator.assignTeamPlayerColor(team, player.id());
|
||||
}
|
||||
if (player.type() === PlayerType.Human) {
|
||||
return this.humanColorAllocator.assignColor(player.id());
|
||||
}
|
||||
if (player.type() === PlayerType.Bot) {
|
||||
return this.botColorAllocator.assignColor(player.id());
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
defendedBorderColors(territoryColor: Colord): {
|
||||
light: Colord;
|
||||
dark: Colord;
|
||||
} {
|
||||
return {
|
||||
light: territoryColor.darken(0.2),
|
||||
dark: territoryColor.darken(0.4),
|
||||
};
|
||||
}
|
||||
|
||||
focusedBorderColor(): Colord {
|
||||
return colord("rgb(230,230,230)");
|
||||
}
|
||||
|
||||
textColor(player: PlayerView): string {
|
||||
return player.type() === PlayerType.Human ? "#000000" : "#4D4D4D";
|
||||
}
|
||||
|
||||
// | Terrain Type | Magnitude | Base Color Logic | Visual Description |
|
||||
// | :---------------- | :-------- | :---------------------------------------------- | :------------------------------------------------------------------- |
|
||||
// | **Shore (Land)** | N/A | Fixed: `rgb(204, 203, 158)` | Sandy beige. Overrides other land types if adjacent to water. |
|
||||
// | **Plains** | 0 - 9 | `rgb(190, 220, 138)` - `rgb(190, 202, 138)` | Light green. Gets slightly darker/less green as magnitude increases. |
|
||||
// | **Highland** | 10 - 19 | `rgb(220, 203, 158)` - `rgb(238, 221, 176)` | Tan/Beige. Gets lighter as magnitude increases. |
|
||||
// | **Mountain** | 20 - 30 | `rgb(240, 240, 240)` - `rgb(245, 245, 245)` | Grayscale (White/Grey). Represents snow caps or rocky peaks. |
|
||||
// | **Water (Shore)** | 0 | Fixed: `rgb(100, 143, 255)` | Light blue near land. |
|
||||
// | **Water (Deep)** | 1 - 10+ | `rgb(70, 132, 180)` - `rgb(61, 123, 171)` | Darker blue, adjusted slightly by distance to land. |
|
||||
terrainColor(gm: GameMap, tile: TileRef): Colord {
|
||||
const mag = gm.magnitude(tile);
|
||||
if (gm.isShore(tile)) {
|
||||
return this.shore;
|
||||
}
|
||||
switch (gm.terrainType(tile)) {
|
||||
case TerrainType.Ocean:
|
||||
case TerrainType.Lake: {
|
||||
const w = this.water.rgba;
|
||||
if (gm.isShoreline(tile) && gm.isWater(tile)) {
|
||||
return this.shorelineWater;
|
||||
}
|
||||
return colord({
|
||||
r: Math.max(w.r - 10 + (11 - Math.min(mag, 10)), 0),
|
||||
g: Math.max(w.g - 10 + (11 - Math.min(mag, 10)), 0),
|
||||
b: Math.max(w.b - 10 + (11 - Math.min(mag, 10)), 0),
|
||||
});
|
||||
}
|
||||
case TerrainType.Plains:
|
||||
return colord({
|
||||
r: 190,
|
||||
g: 220 - 2 * mag,
|
||||
b: 138,
|
||||
});
|
||||
case TerrainType.Highland:
|
||||
return colord({
|
||||
r: 200 + 2 * mag,
|
||||
g: 183 + 2 * mag,
|
||||
b: 138 + 2 * mag,
|
||||
});
|
||||
case TerrainType.Mountain:
|
||||
return colord({
|
||||
r: 230 + mag / 2,
|
||||
g: 230 + mag / 2,
|
||||
b: 230 + mag / 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
backgroundColor(): Colord {
|
||||
return this.background;
|
||||
}
|
||||
|
||||
falloutColor(): Colord {
|
||||
return this.rand.randElement(this.falloutColors);
|
||||
}
|
||||
|
||||
font(): string {
|
||||
return "Overpass, sans-serif";
|
||||
}
|
||||
|
||||
spawnHighlightColor(): Colord {
|
||||
return this._spawnHighlightColor;
|
||||
}
|
||||
/** Return spawn highlight color for self */
|
||||
spawnHighlightSelfColor(): Colord {
|
||||
return this._spawnHighlightSelfColor;
|
||||
}
|
||||
/** Return spawn highlight color for teammates */
|
||||
spawnHighlightTeamColor(): Colord {
|
||||
return this._spawnHighlightTeamColor;
|
||||
}
|
||||
/** Return spawn highlight color for enemies */
|
||||
spawnHighlightEnemyColor(): Colord {
|
||||
return this._spawnHighlightEnemyColor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Colord, colord } from "colord";
|
||||
import { TerrainType } from "../../core/game/Game";
|
||||
import { GameMap, TileRef } from "../../core/game/GameMap";
|
||||
import { PastelTheme } from "./PastelTheme";
|
||||
|
||||
export class PastelThemeDark extends PastelTheme {
|
||||
private darkShore = colord("rgb(134,133,88)");
|
||||
|
||||
private darkWater = colord("rgb(14,11,30)");
|
||||
private darkShorelineWater = colord("rgb(50,50,50)");
|
||||
|
||||
// | Terrain Type | Magnitude | Base Color Logic | Visual Description |
|
||||
// | :---------------- | :-------- | :---------------------------------------------- | :-------------------- |
|
||||
// | **Shore (Land)** | N/A | Fixed: `rgb(134, 133, 88)` | Dark olive. |
|
||||
// | **Plains** | 0 - 9 | `rgb(140, 170, 88)` - `rgb(140, 152, 88)` | Muted green. |
|
||||
// | **Highland** | 10 - 19 | `rgb(170, 153, 108)` - `rgb(188, 171, 126)` | Dark earth tone. |
|
||||
// | **Mountain** | 20 - 30 | `rgb(190, 190, 190)` - `rgb(195, 195, 195)` | Dark gray. |
|
||||
// | **Water (Shore)** | 0 | Fixed: `rgb(50, 50, 50)` | Dark gray/black. |
|
||||
// | **Water (Deep)** | 1 - 10+ | `rgb(22, 19, 38)` - `rgb(14, 11, 30)` | Very dark blue/black. |
|
||||
|
||||
terrainColor(gm: GameMap, tile: TileRef): Colord {
|
||||
const mag = gm.magnitude(tile);
|
||||
if (gm.isShore(tile)) {
|
||||
return this.darkShore;
|
||||
}
|
||||
switch (gm.terrainType(tile)) {
|
||||
case TerrainType.Ocean:
|
||||
case TerrainType.Lake: {
|
||||
const w = this.darkWater.rgba;
|
||||
if (gm.isShoreline(tile) && gm.isWater(tile)) {
|
||||
return this.darkShorelineWater;
|
||||
}
|
||||
if (gm.magnitude(tile) < 10) {
|
||||
return colord({
|
||||
r: Math.max(w.r + 9 - mag, 0),
|
||||
g: Math.max(w.g + 9 - mag, 0),
|
||||
b: Math.max(w.b + 9 - mag, 0),
|
||||
});
|
||||
}
|
||||
return this.darkWater;
|
||||
}
|
||||
case TerrainType.Plains:
|
||||
return colord({
|
||||
r: 140,
|
||||
g: 170 - 2 * mag,
|
||||
b: 88,
|
||||
});
|
||||
case TerrainType.Highland:
|
||||
return colord({
|
||||
r: 150 + 2 * mag,
|
||||
g: 133 + 2 * mag,
|
||||
b: 88 + 2 * mag,
|
||||
});
|
||||
case TerrainType.Mountain:
|
||||
return colord({
|
||||
r: 180 + mag / 2,
|
||||
g: 180 + mag / 2,
|
||||
b: 180 + mag / 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Colord } from "colord";
|
||||
import { Team } from "../../core/game/Game";
|
||||
import { GameMap, TileRef } from "../../core/game/GameMap";
|
||||
import { PlayerView } from "../../core/game/GameView";
|
||||
|
||||
export interface Theme {
|
||||
teamColor(team: Team): Colord;
|
||||
// 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 };
|
||||
focusedBorderColor(): Colord;
|
||||
terrainColor(gm: GameMap, tile: TileRef): Colord;
|
||||
backgroundColor(): Colord;
|
||||
falloutColor(): Colord;
|
||||
font(): string;
|
||||
textColor(playerInfo: PlayerView): string;
|
||||
spawnHighlightColor(): Colord;
|
||||
spawnHighlightSelfColor(): Colord;
|
||||
spawnHighlightTeamColor(): Colord;
|
||||
spawnHighlightEnemyColor(): Colord;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { UserSettings } from "../../core/game/UserSettings";
|
||||
import { PastelTheme } from "./PastelTheme";
|
||||
import { PastelThemeDark } from "./PastelThemeDark";
|
||||
import { Theme } from "./Theme";
|
||||
|
||||
/**
|
||||
* Client-side source of truth for the active theme. Themes were moved out of
|
||||
* `src/core` (the simulation never reads colors); this singleton replaces the
|
||||
* old `Config.theme()` accessor.
|
||||
*/
|
||||
class ThemeProvider {
|
||||
private readonly userSettings = new UserSettings();
|
||||
private light = new PastelTheme();
|
||||
private dark = new PastelThemeDark();
|
||||
|
||||
/** The active theme, selected from the user's dark-mode preference. */
|
||||
current(): Theme {
|
||||
return this.userSettings.darkMode() ? this.dark : this.light;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreate the themes so their colour allocators start empty. Call once per
|
||||
* game — matches the previous per-`Config` theme lifecycle and prevents
|
||||
* colour-pool depletion across games in a single session.
|
||||
*/
|
||||
reset(): void {
|
||||
this.light = new PastelTheme();
|
||||
this.dark = new PastelThemeDark();
|
||||
}
|
||||
}
|
||||
|
||||
export const themeProvider = new ThemeProvider();
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from "../../core/game/GameUpdates";
|
||||
import { UserSettings } from "../../core/game/UserSettings";
|
||||
import { PlayerState, PlayerStatic, PlayerTypeEnum } from "../render/types";
|
||||
import { themeProvider } from "../theme/ThemeProvider";
|
||||
import { GameView } from "./GameView";
|
||||
import { UnitView } from "./UnitView";
|
||||
|
||||
@@ -133,7 +134,7 @@ export class PlayerView {
|
||||
this.anonymousName = createRandomName(data.name!, data.playerType!);
|
||||
}
|
||||
|
||||
const theme = this.game.config().theme();
|
||||
const theme = themeProvider.current();
|
||||
|
||||
const defaultTerritoryColor = theme.territoryColor(this);
|
||||
const defaultBorderColor = theme.borderColor(defaultTerritoryColor);
|
||||
|
||||
Reference in New Issue
Block a user