Repository navigation
Expand file tree
/
Copy pathTheme.lua
More file actions
394 lines (360 loc) · 17.5 KB
/
Copy pathTheme.lua
File metadata and controls
394 lines (360 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
-- WickCore
-- Theme.lua — palette switching for the Wick chrome.
--
-- A theme never changes shape; the look does that (Chrome.Styles in
-- Chrome.lua). What a theme changes is the five colors the look is drawn
-- in. Fel is the brand, the default on TBC, and the warlock theme; Forever
-- defaults to the player's class theme. Looks other than Wick Modern and
-- Wick OG bring palettes of their own, kept here as themes marked `look`. The other eight themes take their accent from the client's own
-- class color table (RAID_CLASS_COLORS, C_ClassColor on retail) so they
-- match what the game paints in raid frames and chat, and sit on deep
-- companion darks the way Fel's green sits on the brand's purple.
--
-- Chrome.Colors is mutated in place so every reference a product holds
-- follows the switch, and every region Chrome created with a palette token
-- is re-tinted live through the registry Chrome keeps.
local ADDON = ...
local Core = LibStub("WickCore-1.0", true)
if not Core or Core._sourceAddon ~= ADDON then return end
local Chrome = Core.Chrome
local C = Chrome.Colors
local function rgb(hex, a)
return { tonumber(hex:sub(1, 2), 16) / 255, tonumber(hex:sub(3, 4), 16) / 255, tonumber(hex:sub(5, 6), 16) / 255, a or 1 }
end
local function hex(c)
return ("%02X%02X%02X"):format(math.floor(c[1] * 255 + 0.5), math.floor(c[2] * 255 + 0.5), math.floor(c[3] * 255 + 0.5))
end
local function mix(a, b, t)
return { a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t, a[3] + (b[3] - a[3]) * t, 1 }
end
-- The brand palette, verbatim. Never derived.
local FEL = { fel = "4FC778", void = "0D0A14", shadow = "171124", border = "383058", text = "D4C8A1" }
-- Blizzard's class colors, as a fallback when the client table is absent.
local CLASS_HEX = {
WARRIOR = "C69B6D", PALADIN = "F48CBA", HUNTER = "AAD372", ROGUE = "FFF468", PRIEST = "FFFFFF",
SHAMAN = "0070DD", MAGE = "3FC7EB", WARLOCK = "8788EE", DRUID = "FF7C0A",
}
-- The Classic-era set, the one TBC and its class-colored UIs still show.
local CLASSIC_HEX = {
WARRIOR = "C79C6E", PALADIN = "F58CBA", HUNTER = "ABD473", ROGUE = "FFF569", PRIEST = "FFFFFF",
SHAMAN = "0070DE", MAGE = "69CCF0", WARLOCK = "9482C9", DRUID = "FF7D0A",
}
-- The looks' palettes: accent, panel, second panel, border, text.
local LOOK_PALETTES = {
{ id = "hologram", name = "Hologram", fel = "3FE0FF", void = "04111A", shadow = "0B2230", border = "1D6E86", text = "CDEFF7" },
{ id = "rebel", name = "Rebel", fel = "E5091A", void = "0B0B0B", shadow = "1D1D1D", border = "5E5E5E", text = "E6E6E6" },
{ id = "gilded", name = "Gilded", fel = "D4B66A", void = "0B0A08", shadow = "17140F", border = "6E5F3E", text = "E3D9C0" },
{ id = "arena", name = "Arena", fel = "FF5263", void = "0C1620", shadow = "213040", border = "3E5163", text = "ECE8E1" },
{ id = "foundry", name = "Foundry", fel = "FF8A1F", void = "18181B", shadow = "242428", border = "5A5A62", text = "D8D2C4" },
{ id = "frost", name = "Frost", fel = "8FD3FF", void = "0B1117", shadow = "131C24", border = "3C5566", text = "E6F1F7" },
-- Neutral darks under the player's own class colour: the accent is
-- the class, the rest stays grey, so it reads the same on every class.
{ id = "crisp", name = "Crisp", fel = "class", void = "121212", shadow = "0A0A0A", border = "000000", text = "F0F0F0" },
-- The game's own colours: its gold for headings and the accent, white
-- text, and its tooltips' near-black for the panels (their middle is a
-- dark texture under a navy tint, far darker than the tint alone).
{ id = "classic", name = "Classic", fel = "FFD100", void = "0B0B13", shadow = "15151F", border = "5A5A66", text = "FFFFFF" },
}
local CLASS_ORDER = { "SHAMAN", "DRUID", "HUNTER", "MAGE", "PRIEST", "PALADIN", "ROGUE", "WARRIOR" }
-- "client" reads the game's table; "classic" uses the Classic-era codes.
Chrome.classColorSet = "client"
local function classAccent(token)
if Chrome.classColorSet == "classic" then return rgb(CLASSIC_HEX[token] or CLASS_HEX[token]) end
if C_ClassColor and C_ClassColor.GetClassColor then
local ok, col = pcall(C_ClassColor.GetClassColor, token)
if ok and col and col.r then return { col.r, col.g, col.b, 1 } end
end
local t = rawget(_G, "RAID_CLASS_COLORS")
if t and t[token] and t[token].r then return { t[token].r, t[token].g, t[token].b, 1 } end
return rgb(CLASS_HEX[token] or FEL.fel)
end
-- The class colour in the set the player chose, for anything in the
-- suite that paints by class. Comforts colours health bars with this so
-- "classic" means classic everywhere, not only in the chrome.
function Chrome:ClassColor(token)
local c = classAccent(token)
return c[1], c[2], c[3]
end
-- The brand's darks are not the accent's hue: Fel's green sits on a deep
-- saturated purple. Each class theme gets the same treatment, a companion
-- hue for void, shadow and border at the brand's own saturation and
-- lightness, with the client's class color left untouched as the accent.
-- Hue and saturation per class; lightness is fixed to match Fel's darks.
local DARK_HUE = {
SHAMAN = { h = 215, s = 0.42 }, -- deep sea
DRUID = { h = 24, s = 0.42 }, -- umber
HUNTER = { h = 112, s = 0.30 }, -- forest
MAGE = { h = 236, s = 0.40 }, -- midnight
PRIEST = { h = 42, s = 0.24 }, -- candle-lit oak
PALADIN = { h = 346, s = 0.36 }, -- crimson
ROGUE = { h = 222, s = 0.12 }, -- charcoal
WARRIOR = { h = 8, s = 0.40 }, -- blood and rust
}
local DARK_L = { void = 0.06, shadow = 0.10, border = 0.27 }
local function hsl(h, s, l)
local c = (1 - math.abs(2 * l - 1)) * s
local hp = (h % 360) / 60
local x = c * (1 - math.abs(hp % 2 - 1))
local r, g, b = 0, 0, 0
if hp < 1 then r, g = c, x elseif hp < 2 then r, g = x, c elseif hp < 3 then g, b = c, x
elseif hp < 4 then g, b = x, c elseif hp < 5 then r, b = x, c else r, b = c, x end
local m = l - c / 2
return { r + m, g + m, b + m, 1 }
end
local function rgbToHsl(c)
local r, g, b = c[1], c[2], c[3]
local mx, mn = math.max(r, g, b), math.min(r, g, b)
local l = (mx + mn) / 2
if mx == mn then return 0, 0, l end
local d = mx - mn
local s = l > 0.5 and d / (2 - mx - mn) or d / (mx + mn)
local h
if mx == r then h = (g - b) / d + (g < b and 6 or 0)
elseif mx == g then h = (b - r) / d + 2
else h = (r - g) / d + 4 end
return h * 60, s, l
end
-- The custom theme: the player picks a main color, whose hue and
-- saturation drive the dark family, and an accent, used untouched.
Chrome.CUSTOM_DEFAULT = { main = "383058", accent = FEL.fel }
Chrome.customColors = { main = Chrome.CUSTOM_DEFAULT.main, accent = Chrome.CUSTOM_DEFAULT.accent }
local TEXT = rgb(FEL.text)
local function derive(accent, token, dark)
local d = dark or DARK_HUE[token] or { h = 258, s = 0.33 }
return {
fel = { accent[1], accent[2], accent[3], 1 },
void = hsl(d.h, d.s, DARK_L.void),
shadow = hsl(d.h, d.s, DARK_L.shadow),
border = hsl(d.h, d.s * 0.9, DARK_L.border),
text = { TEXT[1], TEXT[2], TEXT[3], 1 },
}
end
local function finish(t)
local v, x = t.colors.void, t.colors.text
t.colors.muted = { x[1] * 0.67 + v[1] * 0.33, x[2] * 0.67 + v[2] * 0.33, x[3] * 0.67 + v[3] * 0.33, 1 }
t.hex = {}
for k, c in pairs(t.colors) do if k ~= "muted" then t.hex[k] = hex(c) end end
return t
end
Chrome.Themes = {}
Chrome.ThemeByID = {}
Chrome.ThemeByClass = {}
local function add(t)
finish(t)
Chrome.Themes[#Chrome.Themes + 1] = t
Chrome.ThemeByID[t.id] = t
if t.class then Chrome.ThemeByClass[t.class] = t end
end
local function buildThemes()
Chrome.Themes, Chrome.ThemeByID, Chrome.ThemeByClass = {}, {}, {}
add({ id = "fel", name = "Fel", class = "WARLOCK", brand = true,
colors = { fel = rgb(FEL.fel), void = rgb(FEL.void), shadow = rgb(FEL.shadow), border = rgb(FEL.border), text = rgb(FEL.text) } })
for _, token in ipairs(CLASS_ORDER) do
local name = token:sub(1, 1) .. token:sub(2):lower()
add({ id = token:lower(), name = name, class = token, colors = derive(classAccent(token), token) })
end
-- The looks' own palettes, chosen with the look (Chrome.Styles).
for _, look in ipairs(LOOK_PALETTES) do
local accent
if look.fel == "class" then
-- The class colour in the set the player chose. Before the
-- client knows the class it is Fel, and login builds it again.
local _, token = UnitClass("player")
accent = token and classAccent(token) or rgb(FEL.fel)
if not token then Chrome.lookNeedsClass = true end
else
accent = rgb(look.fel)
end
add({ id = look.id, name = look.name, look = true,
colors = { fel = accent, void = rgb(look.void), shadow = rgb(look.shadow),
border = rgb(look.border), text = rgb(look.text) } })
end
local cc = Chrome.customColors
local h, s = rgbToHsl(rgb(cc.main))
-- The accent is drawn as text (headings, titles) as well as rings and
-- lines, so a custom one is never allowed darker than reads on the
-- panel: its hue and saturation are kept, its lightness lifted to at
-- least 0.45. Black, which a colour picker can hand back by accident,
-- becomes a mid grey rather than invisible text.
local ah, as, al = rgbToHsl(rgb(cc.accent))
local accent = al < 0.45 and hsl(ah, as, 0.45) or rgb(cc.accent)
add({ id = "custom", name = "Custom", custom = true,
colors = derive(accent, nil, { h = h, s = math.max(0.08, math.min(0.6, s)) }) })
end
buildThemes()
local function cleanHex(v)
v = tostring(v or ""):gsub("^#", ""):upper()
if #v == 3 then v = v:gsub("(.)", "%1%1") end
return v:match("^%x%x%x%x%x%x$")
end
-- Set the custom theme's two colors (hex strings or {r,g,b} tables).
-- Rebuilds the theme and repaints if it is the active one.
function Chrome:SetCustomColors(main, accent)
if type(main) == "table" then main = hex(main) end
if type(accent) == "table" then accent = hex(accent) end
main = cleanHex(main) or self.customColors.main
accent = cleanHex(accent) or self.customColors.accent
self.customColors.main, self.customColors.accent = main, accent
self:SaveTheme()
self:RebuildThemes()
return self.ThemeByID.custom
end
-- Classic class-color addons rewrite RAID_CLASS_COLORS at login; rebuild
-- then so the themes follow, and reapply the active one.
function Chrome:RebuildThemes()
local active = self.activeTheme
buildThemes()
if active and self.ThemeByID[active] then self:ApplyTheme(active) end
end
-- Fel is the theme anything unknown falls back to. The setting a new
-- install starts on is the player's class on Forever, and fel on TBC
-- Anniversary, where the suite has always been fel green and an update
-- should look like the addons the player already had.
Chrome.DEFAULT_THEME = "fel"
Chrome.DEFAULT_SETTING = (Core.Client and Core.Client.isTBC) and "fel" or "auto"
Chrome.activeTheme = Chrome.DEFAULT_THEME
-- The player's choice lives here first. The saved variable is written on
-- every change and again at logout, so a change made before the profile
-- was bound still survives the session.
Chrome.themeSetting = Chrome.DEFAULT_SETTING
-- Always go to the live saved variable first. The client assigns it just
-- before ADDON_LOADED, and if our profile was ever bound to a table the
-- client then replaced, that copy is an orphan: reads see defaults and
-- writes are never serialized, while the real values sit untouched on
-- disk. Reading the global directly is immune to that.
local function themeStore()
local sv = rawget(_G, "WickCoreDB")
if type(sv) == "table" then
sv.global = sv.global or {}
return sv.global
end
return Core.self and Core.self.db and Core.self.db.global
end
-- True once a saved theme has been read back, or the player has chosen
-- one. Until then the value on disk is better than anything in memory
-- and must not be overwritten.
Chrome.themeKnown = false
function Chrome:SaveTheme()
local g = themeStore()
if not g then return false end
self.themeKnown = true
-- The theme is this character's; the class colour set and the custom
-- colours are preferences, kept for every character.
local ch = Chrome.CharStore and Chrome:CharStore()
if ch then ch.theme = self.themeSetting end
g.classColors = self.classColorSet
g.custom = { main = self.customColors.main, accent = self.customColors.accent }
-- The saved variable is written and never read back by this client,
-- so the choice only survives if the macro store writes it too, and
-- the store writes when something says it is dirty. Nothing did:
-- the options page marks its own check boxes and a theme is picked
-- from a list somewhere else entirely.
if Core.Store then Core.Store:Dirty() end
return true
end
-- Two moments matter. At logout the choice is written once more, right
-- before the client serializes saved variables. At login it is read and
-- applied again: the init-time pass runs inside a protected call that
-- swallows its own errors, and anything loading after it could have
-- repainted, so login is the authoritative application.
local flush = CreateFrame("Frame")
flush:RegisterEvent("PLAYER_LOGOUT")
flush:RegisterEvent("PLAYER_LOGIN")
flush:SetScript("OnEvent", function(_, event)
if event == "PLAYER_LOGOUT" then
-- Never write a default we only fell back to. A session that
-- failed to read the setting must leave the good value alone.
if Chrome.themeKnown then Chrome:SaveTheme() end
return
end
-- A look taking the class colour, built before the class was known.
if Chrome.lookNeedsClass then Chrome.lookNeedsClass = nil; buildThemes() end
Chrome:ApplySavedTheme()
end)
local listeners = {}
function Chrome:OnThemeChanged(fn) listeners[#listeners + 1] = fn end
-- "auto" resolves to the player's class theme; unknown ids fall back to Fel.
function Chrome:ResolveTheme(setting)
if setting == "auto" or setting == "class" then
local _, token = UnitClass("player")
local t = token and self.ThemeByClass[token]
return t and t.id or self.DEFAULT_THEME
end
if setting and self.ThemeByID[setting] then return setting end
return self.DEFAULT_THEME
end
local function copyInto(dst, src, alpha)
dst[1], dst[2], dst[3], dst[4] = src[1], src[2], src[3], alpha or src[4] or 1
end
-- Repaint the palette. Every table in Chrome.Colors keeps its identity.
function Chrome:ApplyTheme(id)
local t = self.ThemeByID[id] or self.ThemeByID[self.DEFAULT_THEME]
copyInto(C.fel, t.colors.fel)
copyInto(C.void, t.colors.void)
copyInto(C.voidBG, t.colors.void, 0.97)
copyInto(C.shadow, t.colors.shadow)
copyInto(C.border, t.colors.border)
copyInto(C.text, t.colors.text)
copyInto(C.muted, t.colors.muted)
for k, v in pairs(t.hex) do self.Hex[k] = v end
Core.COLOR_ACCENT = "|cff" .. t.hex.fel
Core.COLOR_TEXT = "|cff" .. t.hex.text
self.activeTheme = t.id
self:Retint()
for _, fn in ipairs(listeners) do Core.safe(fn, t) end
return t
end
-- Choose a theme and remember it. setting may be a theme id or "auto".
function Chrome:SetTheme(setting)
local id = self:ResolveTheme(setting)
self.themeSetting = (setting == "auto" or setting == "class") and "auto" or id
self:SaveTheme()
return self:ApplyTheme(id)
end
-- Switch the class color source and rebuild the class themes on the spot.
function Chrome:SetClassColorSet(which)
which = which == "classic" and "classic" or "client"
self.classColorSet = which
self:SaveTheme()
self:RebuildThemes()
return which
end
function Chrome:ThemeSetting()
return self.themeSetting or self.DEFAULT_SETTING
end
-- Called by WickCore's own OnInitialize, before any product builds a frame.
function Chrome:ApplySavedTheme()
local global = themeStore()
local ch = Chrome.CharStore and Chrome:CharStore()
-- Did we actually read a choice, or are we falling back? The two must
-- not be treated alike, because this function saves at the end.
local stored = ch and ch.theme
local setting = stored or self.DEFAULT_SETTING
-- Old saved ids from the first cut map onto the class ids.
local legacy = { storm = "shaman", wild = "druid", quiver = "hunter", arcane = "mage",
holy = "priest", light = "paladin", shadow = "rogue", iron = "warrior" }
if legacy[setting] then setting = legacy[setting] end
self.themeSetting = setting
local rebuild = false
if global and global.classColors == "classic" then
self.classColorSet = "classic"
rebuild = true
end
if global and type(global.custom) == "table" then
self.customColors.main = cleanHex(global.custom.main) or self.customColors.main
self.customColors.accent = cleanHex(global.custom.accent) or self.customColors.accent
rebuild = true
end
if rebuild then buildThemes() end
-- Only write back a choice we actually read.
--
-- This used to save unconditionally, so a login that read nothing fell
-- back to Fel and then wrote Fel over the real setting: one read that
-- came too early turned into the choice being gone for good. On this
-- client the settings arrive from the macro store, which can land after
-- login, and Store re-applies the theme when it does. The
-- logout path has always guarded this with themeKnown; this is the same
-- guard at the other end.
if stored then self:SaveTheme() end
self:ApplyTheme(self:ResolveTheme(setting))
end