clone

fifteen puzzle games for the terminal, with daily puzzles, weekly ladders and xp, built on bubble tea

feat/elo-difficulty
puzzletea / theme / palette.go
palette.go 8.3K
1package theme23import (4 "image/color"56 "charm.land/lipgloss/v2"7)89// Palette holds the semantic color tokens derived from a terminal color scheme.10// Every style in the application reads from the active palette via [Current].11//12// All ANSI-sourced tokens are foreground/text-role colors. Background-role13// colors (AccentBG, SuccessBG, ErrorBG, SelectionBG, HighlightBG) are derived14// by blending a FG-role color toward BG or Surface, so they always match the15// theme's luminance range.16type Palette struct {17 // Surfaces18 BG color.Color // main background (theme Background)19 FG color.Color // primary text (theme Foreground)20 Surface color.Color // subtle elevation (crosshair BG tint)2122 // Brand / accent (FG-role from ANSI)23 Accent color.Color // primary accent (ANSI 3 — yellow)24 AccentSoft color.Color // muted accent (ANSI 11 — brightYellow)25 AccentText color.Color // text on AccentBG (ANSI 7 — white)2627 // Text28 TextDim color.Color // secondary / muted text (derived)2930 // Chrome31 Border color.Color // borders, separators (ANSI 0 — black)3233 // Game FG-role tokens (all from ANSI slots, enforced against BG)34 Success color.Color // success/solved FG text (ANSI 2 — green)35 SuccessBorder color.Color // solved border FG (ANSI 10 — brightGreen)36 SolvedFG color.Color // high-contrast text on state BGs (ANSI 15 — brightWhite)37 Error color.Color // conflict/error FG text (ANSI 1 — red)38 Info color.Color // hints, informational text (ANSI 4 — blue)39 Given color.Color // immutable/provided cell FG (ANSI 5 — purple)40 Linked color.Color // connected/related element FG (ANSI 6 — cyan)41 Highlight color.Color // selection/highlight FG (ANSI 14 — brightCyan)42 Secondary color.Color // second value type (ANSI 12 — brightBlue)43 Tertiary color.Color // third differentiator (ANSI 13 — brightPurple)4445 // Derived background-role tokens (blended, not from ANSI slots)46 AccentBG color.Color // cursor / active element BG47 SuccessBG color.Color // solved cell BG48 ErrorBG color.Color // conflict cell BG49 SelectionBG color.Color // drag / selection BG50 HighlightBG color.Color // adjacent / neighbor highlight BG5152 // Full 16-color ANSI palette for puzzle-specific use (e.g. shikaku rects).53 // Order: black, red, green, yellow, blue, purple, cyan, white,54 // brightBlack..brightWhite.55 ANSI [16]color.Color56}5758// ThemeColors returns a curated slice of distinct, colorful palette colors59// suitable for differentiating themed UI elements (cards, hover bars, puzzle60// regions, etc). The colors are drawn from chromatic ANSI slots (skipping61// black/white/gray), giving up to 10 visually distinct options per theme.62func (p Palette) ThemeColors() []color.Color {63 return []color.Color{64 p.ANSI[3], // yellow65 p.ANSI[4], // blue66 p.ANSI[2], // green67 p.ANSI[5], // purple68 p.ANSI[6], // cyan69 p.ANSI[1], // red70 p.ANSI[11], // brightYellow71 p.ANSI[12], // brightBlue72 p.ANSI[14], // brightCyan73 p.ANSI[13], // brightPurple74 }75}7677// Blend ratios for derived background colors. Each ratio controls how much78// of the source FG color is mixed into the base (BG or Surface) to produce79// the derived BG. Higher values produce more vivid tinted backgrounds.80const (81 blendAccentBG = 0.45 // cursor / active element82 blendSuccessBG = 0.30 // solved cells83 blendErrorBG = 0.25 // conflict cells84 blendSelectionBG = 0.35 // drag selection85 blendHighlightBG = 0.30 // adjacent / neighbor highlight86)8788// derivePalette maps a Theme's 16 terminal colors onto semantic tokens and89// enforces minimum WCAG contrast ratios for every critical FG/BG pairing.90//91// All ANSI slots are mapped to FG-role tokens. Background-role tokens are92// derived by blending a FG color toward BG, respecting the theme's luminance.93func derivePalette(t Theme) Palette {94 bg := lipgloss.Color(t.Background)95 fg := lipgloss.Color(t.Foreground)9697 ansi := [16]color.Color{98 lipgloss.Color(t.Black),99 lipgloss.Color(t.Red),100 lipgloss.Color(t.Green),101 lipgloss.Color(t.Yellow),102 lipgloss.Color(t.Blue),103 lipgloss.Color(t.Purple),104 lipgloss.Color(t.Cyan),105 lipgloss.Color(t.White),106 lipgloss.Color(t.BrightBlack),107 lipgloss.Color(t.BrightRed),108 lipgloss.Color(t.BrightGreen),109 lipgloss.Color(t.BrightYellow),110 lipgloss.Color(t.BrightBlue),111 lipgloss.Color(t.BrightPurple),112 lipgloss.Color(t.BrightCyan),113 lipgloss.Color(t.BrightWhite),114 }115116 // --- Non-chromatic luminance ramp ---117 surface := ansi[8] // brightBlack — subtle elevation118 textDim := MidTone(bg, fg) // halfway between BG and FG119 border := ansi[0] // black — grid chrome120121 // If border is too close to BG, swap to brightBlack.122 if contrastRatio(border, bg) < 1.5 {123 border = surface124 }125126 // --- FG-role chromatic tokens (all from ANSI slots) ---127 accent := ansi[3] // yellow — primary accent128 success := ansi[2] // green — success/solved FG129 errorFG := ansi[1] // red — conflict/error FG130 linked := ansi[6] // cyan — connected elements131 highlight := ansi[14] // bCyan — selection/highlight FG132 secondary := ansi[12] // bBlue — second value type133 highlightSrc := linked // source color for HighlightBG blend134 _ = highlightSrc135136 // --- Derived BG-role tokens (blended toward BG) ---137 accentBG := Blend(bg, accent, blendAccentBG)138 successBG := Blend(bg, success, blendSuccessBG)139 errorBG := Blend(bg, errorFG, blendErrorBG)140 selectionBG := Blend(surface, highlight, blendSelectionBG)141 highlightBG := Blend(surface, linked, blendHighlightBG)142143 p := Palette{144 BG: bg,145 FG: fg,146 Surface: surface,147148 Accent: accent,149 AccentSoft: ansi[11], // brightYellow150 AccentText: ansi[7], // white — text on AccentBG151152 TextDim: textDim,153 Border: border,154155 Success: success,156 SuccessBorder: ansi[10], // brightGreen157 SolvedFG: ansi[15], // brightWhite — text on state BGs158 Error: errorFG,159 Info: ansi[12], // brightBlue160 Given: ansi[13], // brightPurple161 Linked: linked,162 Highlight: highlight,163 Secondary: secondary,164 Tertiary: ansi[5], // purple165166 AccentBG: accentBG,167 SuccessBG: successBG,168 ErrorBG: errorBG,169 SelectionBG: selectionBG,170 HighlightBG: highlightBG,171172 ANSI: ansi,173 }174175 return enforcePaletteContrast(p)176}177178// enforcePaletteContrast ensures every critical FG-on-BG pairing in p meets179// WCAG AA minimums. It nudges foreground colors toward the palette's brightest180// or darkest ANSI slot when the original contrast is insufficient.181//182// FG-role tokens are enforced against BG (the main canvas). Tokens used on183// derived BGs (AccentBG, SuccessBG, ErrorBG) are enforced against those184// derived backgrounds after the FG tokens are finalized.185func enforcePaletteContrast(p Palette) Palette {186 // --- Phase 1: FG-role tokens against the main BG (normal text → 4.5:1) ---187188 p.Accent = ensurePairContrast(p.Accent, p.BG, p, minContrastNormal)189 p.AccentSoft = ensurePairContrast(p.AccentSoft, p.BG, p, minContrastNormal)190 p.FG = ensurePairContrast(p.FG, p.BG, p, minContrastNormal)191 p.TextDim = ensurePairContrast(p.TextDim, p.BG, p, minContrastNormal)192 p.Info = ensurePairContrast(p.Info, p.BG, p, minContrastNormal)193 p.Error = ensurePairContrast(p.Error, p.BG, p, minContrastNormal)194 p.Success = ensurePairContrast(p.Success, p.BG, p, minContrastNormal)195 p.Given = ensurePairContrast(p.Given, p.BG, p, minContrastNormal)196 p.Linked = ensurePairContrast(p.Linked, p.BG, p, minContrastNormal)197 p.Secondary = ensurePairContrast(p.Secondary, p.BG, p, minContrastNormal)198 p.Tertiary = ensurePairContrast(p.Tertiary, p.BG, p, minContrastNormal)199 p.Highlight = ensurePairContrast(p.Highlight, p.BG, p, minContrastNormal)200201 // --- Phase 2: re-derive BGs after FG colors are finalized ---202 // (FG nudging may have shifted the source colors used for blending.)203204 p.AccentBG = Blend(p.BG, p.Accent, blendAccentBG)205 p.SuccessBG = Blend(p.BG, p.Success, blendSuccessBG)206 p.ErrorBG = Blend(p.BG, p.Error, blendErrorBG)207 p.SelectionBG = Blend(p.Surface, p.Highlight, blendSelectionBG)208 p.HighlightBG = Blend(p.Surface, p.Linked, blendHighlightBG)209210 // --- Phase 3: FG tokens on derived BGs (bold text → 3:1) ---211212 p.AccentText = ensurePairContrast(p.AccentText, p.AccentBG, p, minContrastLarge)213 p.SolvedFG = ensurePairContrast(p.SolvedFG, p.SuccessBG, p, minContrastNormal)214 p.SuccessBorder = ensurePairContrast(p.SuccessBorder, p.SuccessBG, p, minContrastLarge)215216 return p217}