Полезные сниппеты и функции

chromiusj

fullstack eblan
Модератор
5,817
4,119
Описание: Возможно сырой,но процедурный генератор тем. Создает полноценную, гармоничную и настраиваемую дизайн-систему для окна на основе одного базового цвета и продвинутых вычислениях (короче прочитал https://matthewstrom.com/writing/generating-color-palettes/):
Пример использования:
Lua:
-- 1. Создание генератора (вызывается один раз)
-- config: необязательная таблица; если не указана, используется default_theme_config.
local theme_generator = createThemeGenerator(config)

-- 2. Применение темы (вызывается в OnInitialize, не особо сильно влияет на производительность)
-- Применяет сгенерированную тему.
theme_generator:apply()

-- 3. Отрисовка настроек (вызывается в OnFrame, где вам удобно)
-- Рисует все элементы управления: колорпикер, чекбокс и слайдеры настроек.
theme_generator:render_controls()
Конфигурация (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:
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)
Пример темы:

123.png

В планах добавить более удобную адаптацию цветов к неактивным элементам по типу фона окна, чайлдов и т.п