- 5,817
- 4,119
Описание: Возможно сырой,но процедурный генератор тем. Создает полноценную, гармоничную и настраиваемую дизайн-систему для окна на основе одного базового цвета и продвинутых вычислениях (короче прочитал https://matthewstrom.com/writing/generating-color-palettes/):
Пример использования:
Конфигурация (default_theme_config):
Это главная таблица, управляющая внешним видом. Она состоит из двух частей:
1. colors
Здесь каждому элементу интерфейса назначается цвет из сгенерированных палитр.
Типо так: [imgui.Col.Элемент] = { настройки }
Настройки:
- source: Из какой палитры брать цвет.
- 'primary' - Основная, цветная палитра. Используется для акцентов и интерактивных элементов.
- 'neutral' - Вспомогательная, тонированная серая палитра. Используется для фона и неактивных элементов.
- light_shade: Какой оттенок (от 1 до 10) использовать для СВЕТЛОЙ темы. 1 - самый светлый, 10 - самый темный.
- dark_shade: Какой оттенок (от 1 до 10) использовать для ТЕМНОЙ темы. 1 - самый светлый, 10 - самый темный.
- a: Прозрачность (альфа-канал). Необязательный параметр. 1.0 - полностью непрозрачный.
Пример: [imgui.Col.Button] = { source = 'primary', light_shade = 6, dark_shade = 5 } означает, что в светлой теме кнопка возьмет 6-й (довольно темный) оттенок из цветной палитры, а в темной даст 5-й (более яркий).
2. style
Отвечает за не-цветовые параметры: размеры, скругления, отступы и т.д.
Пример: WindowRounding = 8.0 задает скругление углов окна. WindowPadding = {8, 8} задает внутренние отступы в окне.
Пример работы:
Пример темы:
В планах добавить более удобную адаптацию цветов к неактивным элементам по типу фона окна, чайлдов и т.п
Пример использования:
Lua:
-- 1. Создание генератора (вызывается один раз)
-- config: необязательная таблица; если не указана, используется default_theme_config.
local theme_generator = createThemeGenerator(config)
-- 2. Применение темы (вызывается в OnInitialize, не особо сильно влияет на производительность)
-- Применяет сгенерированную тему.
theme_generator:apply()
-- 3. Отрисовка настроек (вызывается в OnFrame, где вам удобно)
-- Рисует все элементы управления: колорпикер, чекбокс и слайдеры настроек.
theme_generator:render_controls()
Это главная таблица, управляющая внешним видом. Она состоит из двух частей:
1. colors
Здесь каждому элементу интерфейса назначается цвет из сгенерированных палитр.
Типо так: [imgui.Col.Элемент] = { настройки }
Настройки:
- source: Из какой палитры брать цвет.
- 'primary' - Основная, цветная палитра. Используется для акцентов и интерактивных элементов.
- 'neutral' - Вспомогательная, тонированная серая палитра. Используется для фона и неактивных элементов.
- light_shade: Какой оттенок (от 1 до 10) использовать для СВЕТЛОЙ темы. 1 - самый светлый, 10 - самый темный.
- dark_shade: Какой оттенок (от 1 до 10) использовать для ТЕМНОЙ темы. 1 - самый светлый, 10 - самый темный.
- a: Прозрачность (альфа-канал). Необязательный параметр. 1.0 - полностью непрозрачный.
Пример: [imgui.Col.Button] = { source = 'primary', light_shade = 6, dark_shade = 5 } означает, что в светлой теме кнопка возьмет 6-й (довольно темный) оттенок из цветной палитры, а в темной даст 5-й (более яркий).
2. style
Отвечает за не-цветовые параметры: размеры, скругления, отступы и т.д.
Пример: WindowRounding = 8.0 задает скругление углов окна. WindowPadding = {8, 8} задает внутренние отступы в окне.
Пример работы:
Lua:
local imgui = require 'mimgui'
local encoding = require 'encoding'
encoding.default = 'CP1251'
local u8 = encoding.UTF8
local new = imgui.new
local default_theme_config = {
colors = {
[imgui.Col.Text] = { source = 'neutral', light_shade = 10, dark_shade = 2 },
[imgui.Col.TextDisabled] = { source = 'neutral', light_shade = 7, dark_shade = 4 },
[imgui.Col.TextSelectedBg] = { source = 'primary', light_shade = 3, dark_shade = 8, a = 0.35 },
[imgui.Col.WindowBg] = { source = 'neutral', light_shade = 2, dark_shade = 10, a = 0.97 },
[imgui.Col.ChildBg] = { source = 'neutral', light_shade = 1, dark_shade = 9, a = 1.0 },
[imgui.Col.PopupBg] = { source = 'neutral', light_shade = 1, dark_shade = 10, a = 0.96 },
[imgui.Col.Border] = { source = 'neutral', light_shade = 5, dark_shade = 7 },
[imgui.Col.BorderShadow] = { a = 0.0 },
[imgui.Col.Separator] = { source = 'neutral', light_shade = 4, dark_shade = 8 },
[imgui.Col.SeparatorHovered] = { source = 'primary', light_shade = 5, dark_shade = 6 },
[imgui.Col.SeparatorActive] = { source = 'primary', light_shade = 6, dark_shade = 5 },
[imgui.Col.FrameBg] = { source = 'neutral', light_shade = 3, dark_shade = 8 },
[imgui.Col.FrameBgHovered] = { source = 'neutral', light_shade = 4, dark_shade = 7 },
[imgui.Col.FrameBgActive] = { source = 'primary', light_shade = 3, dark_shade = 9 },
[imgui.Col.TitleBg] = { source = 'primary', light_shade = 4, dark_shade = 8 },
[imgui.Col.TitleBgActive] = { source = 'primary', light_shade = 5, dark_shade = 7 },
[imgui.Col.TitleBgCollapsed] = { source = 'neutral', light_shade = 3, dark_shade = 9 },
[imgui.Col.Button] = { source = 'primary', light_shade = 6, dark_shade = 5 },
[imgui.Col.ButtonHovered] = { source = 'primary', light_shade = 5, dark_shade = 6 },
[imgui.Col.ButtonActive] = { source = 'primary', light_shade = 7, dark_shade = 4 },
[imgui.Col.Header] = { source = 'primary', light_shade = 6, dark_shade = 6 },
[imgui.Col.HeaderHovered] = { source = 'primary', light_shade = 5, dark_shade = 7 },
[imgui.Col.HeaderActive] = { source = 'primary', light_shade = 7, dark_shade = 5 },
[imgui.Col.CheckMark] = { source = 'primary', light_shade = 8, dark_shade = 3 },
[imgui.Col.SliderGrab] = { source = 'primary', light_shade = 7, dark_shade = 4 },
[imgui.Col.SliderGrabActive] = { source = 'primary', light_shade = 8, dark_shade = 3 },
[imgui.Col.ResizeGrip] = { source = 'neutral', light_shade = 5, dark_shade = 5, a = 0.25 },
[imgui.Col.ResizeGripHovered] = { source = 'neutral', light_shade = 6, dark_shade = 4, a = 0.67 },
[imgui.Col.ResizeGripActive] = { source = 'neutral', light_shade = 7, dark_shade = 3, a = 0.95 },
[imgui.Col.PlotLines] = { source = 'primary', light_shade = 7, dark_shade = 4 },
[imgui.Col.PlotLinesHovered] = { source = 'primary', light_shade = 8, dark_shade = 3 },
[imgui.Col.PlotHistogram] = { source = 'primary', light_shade = 7, dark_shade = 4 },
[imgui.Col.PlotHistogramHovered] = { source = 'primary', light_shade = 8, dark_shade = 3 },
[imgui.Col.Tab] = { source = 'neutral', light_shade = 4, dark_shade = 8 },
[imgui.Col.TabHovered] = { source = 'primary', light_shade = 5, dark_shade = 6 },
[imgui.Col.TabActive] = { source = 'primary', light_shade = 5, dark_shade = 7 },
[imgui.Col.TabUnfocused] = { source = 'neutral', light_shade = 3, dark_shade = 9 },
[imgui.Col.TabUnfocusedActive] = { source = 'primary', light_shade = 4, dark_shade = 8 },
[imgui.Col.MenuBarBg] = { source = 'neutral', light_shade = 2, dark_shade = 10 },
[imgui.Col.ScrollbarBg] = { source = 'neutral', light_shade = 2, dark_shade = 9 },
[imgui.Col.ScrollbarGrab] = { source = 'neutral', light_shade = 5, dark_shade = 6 },
[imgui.Col.ScrollbarGrabHovered] = { source = 'neutral', light_shade = 6, dark_shade = 5 },
[imgui.Col.ScrollbarGrabActive] = { source = 'neutral', light_shade = 7, dark_shade = 4 },
[imgui.Col.DragDropTarget] = { source = 'primary', light_shade = 8, dark_shade = 2, a = 0.9 },
[imgui.Col.NavHighlight] = { source = 'primary', light_shade = 5, dark_shade = 5, a = 0.8 },
[imgui.Col.NavWindowingHighlight] = { source = 'primary', light_shade = 6, dark_shade = 4, a = 0.7 },
[imgui.Col.NavWindowingDimBg] = { a = 0.2 },
[imgui.Col.ModalWindowDimBg] = { a = 0.35 },
},
style = {
WindowPadding={8,8}, FramePadding={6,4}, ItemSpacing={8,4}, ItemInnerSpacing={4,4}, WindowRounding=8.0, FrameRounding=6.0,
ChildRounding=6.0, PopupRounding=6.0, GrabRounding=6.0, ScrollbarRounding=12.0, TabRounding=6.0, WindowBorderSize=1.0,
FrameBorderSize=0.0, ChildBorderSize=1.0,
}
}
function createThemeGenerator(config)
config = config or default_theme_config
local function rgb_to_hsv(r,g,b) local m,n=math.max(r,g,b),math.min(r,g,b) local h,s,v=0,0,m local d=m-n if m~=0 then s=d/m end if m~=n then if r==m then h=(g-b)/d elseif g==m then h=2+(b-r)/d else h=4+(r-g)/d end h=h/6;if h<0 then h=h+1 end end return h,s,v end
local function hsv_to_rgb(h,s,v) if s==0 then return v,v,v end local i=math.floor(h*6) local f=(h*6)-i local p=v*(1-s) local q=v*(1-s*f) local t=v*(1-s*(1-f)) i=i%6 if i==0 then return v,t,p elseif i==1 then return q,v,p elseif i==2 then return p,v,t elseif i==3 then return p,q,v elseif i==4 then return t,p,v else return v,p,q end end
local function lerp(a,b,t) return a+(b-a)*t end
local function easeInOutSine(x) return -(math.cos(math.pi * x) - 1) / 2 end
local generator={base_color=new.float[4]({0.5,0.1,0.6,0.96}),is_dark_theme=new.bool(true),hue_shift_amount=new.float({-0.03}),saturation=new.float({1.0}),brightness=new.float({0.0}),contrast=new.float({1.0}),generated_palettes={primary={},neutral={}},anim={progress=1.0,start_progress=1.0,start_time=0,duration=0.3}}
local function generate_palette_scale(h_base, s_base, v_base, base_alpha)
local scale = {}
local V_MIN_BASE, V_MAX_BASE = 0.10, 0.98
local v_min = math.max(0, V_MIN_BASE + generator.brightness[0])
local v_max = math.min(1, V_MAX_BASE + generator.brightness[0])
local v_center = (v_min + v_max) / 2
local v_range = (v_max - v_min) * generator.contrast[0]
for i = 1, 10 do
local t = (i - 1) / 9
local v_curve = (1 - t)^2
local final_v = lerp(v_center - v_range/2, v_center + v_range/2, v_curve)
local s_curve = s_base * (-0.002 * t^3 + 0.04 * t^2 + 0.86 * t + 0.1)
local final_s = s_curve * generator.saturation[0]
local final_h = (h_base + generator.hue_shift_amount[0] * t + 1) % 1
final_s, final_v = math.max(0, math.min(1, final_s)), math.max(0, math.min(1, final_v))
local r, g, b = hsv_to_rgb(final_h, final_s, final_v)
scale[i] = imgui.ImVec4(r, g, b, base_alpha)
end
return scale
end
function generator:apply()
local style, colors = imgui.GetStyle(), imgui.GetStyle().Colors
local r, g, b, a = self.base_color[0], self.base_color[1], self.base_color[2], self.base_color[3]
local h, s, v = rgb_to_hsv(r, g, b)
local neutral_s = math.max(0.05, s * 0.1)
self.generated_palettes.primary = generate_palette_scale(h, s, v, a)
self.generated_palettes.neutral = generate_palette_scale(h, neutral_s, v, a)
local target_inversion = self.is_dark_theme[0] and 1.0 or 0.0
if self.anim.progress ~= target_inversion then
local time_since_start = os.clock() - self.anim.start_time
if time_since_start >= self.anim.duration then self.anim.progress = target_inversion
else self.anim.progress = lerp(self.anim.start_progress, target_inversion, easeInOutSine(time_since_start / self.anim.duration)) end
end
local theme_inversion = self.anim.progress
for col_id, cfg in pairs(config.colors) do
if cfg.source then
local source_palette = self.generated_palettes[cfg.source]
local light_shade, dark_shade = cfg.light_shade, cfg.dark_shade
local current_shade = lerp(light_shade, dark_shade, theme_inversion)
local final_idx = math.max(1, math.min(10, math.floor(current_shade + 0.5)))
colors[col_id] = source_palette[final_idx]
end
if cfg.a then colors[col_id].w = cfg.a end
end
for style_name, value in pairs(config.style) do
if type(value) == 'table' then style[style_name] = imgui.ImVec2(value[1], value[2])
else style[style_name] = value end
end
end
function generator:render_controls()
local theme_changed = false
if imgui.ColorPicker4(u8'Базовый цвет', self.base_color, imgui.ColorEditFlags.NoSidePreview + imgui.ColorEditFlags.NoSmallPreview + imgui.ColorEditFlags.NoInputs + imgui.ColorEditFlags.PickerHueWheel + imgui.ColorEditFlags.AlphaBar) then theme_changed = true end
imgui.Separator()
if imgui.Checkbox(u8'Темная тема', self.is_dark_theme) then self.anim.start_time = os.clock(); self.anim.start_progress = self.anim.progress end
imgui.Spacing()
imgui.Text(u8'Тонкая настройка палитры')
if imgui.SliderFloat(u8'Насыщенность', self.saturation, 0.2, 1.8, '%.2f') then theme_changed = true end
if imgui.SliderFloat(u8'Яркость', self.brightness, -0.2, 0.2, '%.2f') then theme_changed = true end
if imgui.SliderFloat(u8'Контраст', self.contrast, 0.5, 1.5, '%.2f') then theme_changed = true end
if imgui.SliderFloat(u8'Сдвиг оттенка', self.hue_shift_amount, -0.2, 0.2, '%.3f') then theme_changed = true end
if theme_changed then self:apply() end
end
return generator
end
local theme_generator = createThemeGenerator(default_theme_config)
local window_state = new.bool(true)
imgui.OnInitialize(function()
theme_generator:apply()
end)
imgui.OnFrame(function() return window_state[0] end, function()
theme_generator:apply()
imgui.SetNextWindowSize(imgui.ImVec2(370, 520), imgui.Cond.FirstUseEver)
imgui.Begin(u8'Генератор тем 666', window_state)
if imgui.BeginTabBar('Tabs') then
if imgui.BeginTabItem(u8'Главное') then
imgui.TextWrapped(u8'Пример элементов')
imgui.Separator()
if imgui.CollapsingHeader(u8'Интерактивные элементы') then
imgui.Button(u8'Кнопка')
imgui.Checkbox(u8"Чекбокс)", new.bool(true))
imgui.SliderFloat(u8'Слайдер', new.float(0.5), 0, 1)
end
imgui.EndTabItem()
end
if imgui.BeginTabItem(u8'Настройки темы') then
theme_generator:render_controls()
imgui.Separator()
imgui.Text(u8'Основная (акцентная) палитра:')
local palette = theme_generator.generated_palettes.primary
for i=1, 10 do
imgui.ColorButton(('c%d'):format(i), palette[i], imgui.ColorEditFlags.NoTooltip, imgui.ImVec2(28, 28))
if i < 10 then imgui.SameLine() end
end
imgui.Text(u8'Нейтральная (основная) палитра:')
local neutral_palette = theme_generator.generated_palettes.neutral
for i=1, 10 do
imgui.ColorButton(('nc%d'):format(i), neutral_palette[i], imgui.ColorEditFlags.NoTooltip, imgui.ImVec2(28, 28))
if i < 10 then imgui.SameLine() end
end
imgui.EndTabItem()
end
imgui.EndTabBar()
end
imgui.End()
end)
В планах добавить более удобную адаптацию цветов к неактивным элементам по типу фона окна, чайлдов и т.п