From 4445574b430d4ffe3c88a9bdbe7fdda4f001efa0 Mon Sep 17 00:00:00 2001 From: echo Date: Thu, 28 May 2026 15:43:00 +0200 Subject: [PATCH] Phase 3: Color correction panel with sliders, tint, and preset filters - Color_Filter enum: None, Vintage, Noir, Cool_Tone, Warm_Tone, High_Contrast, Faded, Dramatic - editor_apply_filter_preset: maps each filter to adjustment values - editor_render_adjust_panel: right-side panel with: - Brightness/Contrast/Saturation/Tint sliders via rl.GuiSliderBar - Tint color swatches (6 preset tints) - 8 preset filter buttons - Apply and Reset buttons - editor_update_display: composites base image with tint preview into display_rt for real-time adjustment preview - 'Adjust' toggle button in toolbar (highlights when active) - Canvas width shrinks when adjustments panel is open - Color_Adjustments struct gains filter field - Panel_Editor_State gains show_adjust_panel field Build passes. 155/156 tests pass (1 pre-existing CLI/TUI failure). --- odin/src/gui/panel_editor.odin | 273 +++++++++++++++++++++++++++++++-- 1 file changed, 264 insertions(+), 9 deletions(-) diff --git a/odin/src/gui/panel_editor.odin b/odin/src/gui/panel_editor.odin index 73b401b..a19e93c 100644 --- a/odin/src/gui/panel_editor.odin +++ b/odin/src/gui/panel_editor.odin @@ -28,12 +28,24 @@ Brush_Stroke :: struct { tool: Editor_Tool, } +Color_Filter :: enum { + None, + Vintage, + Noir, + Cool_Tone, + Warm_Tone, + High_Contrast, + Faded, + Dramatic, +} + Color_Adjustments :: struct { brightness: f32, contrast: f32, saturation: f32, tint_color: rl.Color, tint_amount: f32, + filter: Color_Filter, } Panel_Editor_State :: struct { @@ -60,8 +72,56 @@ Panel_Editor_State :: struct { is_panning: bool, pan_start: rl.Vector2, pan_offset_start: rl.Vector2, - show_color_panel: bool, - needs_redraw: bool, + show_color_panel: bool, + show_adjust_panel: bool, + needs_redraw: bool, +} + +editor_apply_filter_preset :: proc(adj: ^Color_Adjustments) { + switch adj.filter { + case .None: + return + case .Vintage: + adj.brightness = -0.05 + adj.contrast = 0.1 + adj.saturation = -0.3 + adj.tint_color = rl.Color{255, 230, 180, 255} + adj.tint_amount = 0.15 + case .Noir: + adj.brightness = -0.1 + adj.contrast = 0.4 + adj.saturation = -1.0 + adj.tint_amount = 0 + case .Cool_Tone: + adj.brightness = 0.0 + adj.contrast = 0.05 + adj.saturation = -0.1 + adj.tint_color = rl.Color{180, 200, 255, 255} + adj.tint_amount = 0.12 + case .Warm_Tone: + adj.brightness = 0.05 + adj.contrast = 0.05 + adj.saturation = -0.1 + adj.tint_color = rl.Color{255, 200, 150, 255} + adj.tint_amount = 0.15 + case .High_Contrast: + adj.brightness = 0.0 + adj.contrast = 0.5 + adj.saturation = 0.2 + adj.tint_amount = 0 + case .Faded: + adj.brightness = 0.1 + adj.contrast = -0.2 + adj.saturation = -0.4 + adj.tint_color = rl.Color{240, 235, 220, 255} + adj.tint_amount = 0.1 + case .Dramatic: + adj.brightness = -0.15 + adj.contrast = 0.6 + adj.saturation = 0.15 + adj.tint_color = rl.Color{200, 180, 255, 255} + adj.tint_amount = 0.08 + } } editor_default_adjustments :: proc() -> Color_Adjustments { @@ -71,6 +131,7 @@ editor_default_adjustments :: proc() -> Color_Adjustments { saturation = 0, tint_color = rl.WHITE, tint_amount = 0, + filter = .None, } } @@ -141,6 +202,7 @@ editor_open :: proc(app: ^GUI_App_State, panel_id: string) -> bool { app.editor.pan_start = rl.Vector2{} app.editor.pan_offset_start = rl.Vector2{} app.editor.show_color_panel = false + app.editor.show_adjust_panel = false app.editor.needs_redraw = true app.editor.current_stroke = Brush_Stroke{points = make([dynamic]rl.Vector2)} @@ -464,6 +526,10 @@ editor_render :: proc(app: ^GUI_App_State) { if ed.needs_redraw { editor_replay_strokes(ed) + editor_update_display(ed) + } + if ed.adjustments_dirty { + editor_update_display(ed) } screen_w := rl.GetScreenWidth() @@ -476,7 +542,9 @@ editor_render :: proc(app: ^GUI_App_State) { canvas_x := sidebar_w_f32 + 4 canvas_y := top_bar_h + toolbar_h + 4 - canvas_w := f32(screen_w) - sidebar_w_f32 - 8 + adj_panel_w: f32 = 0 + if ed.show_adjust_panel { adj_panel_w = 224 } + canvas_w := f32(screen_w) - sidebar_w_f32 - 8 - adj_panel_w canvas_h := f32(screen_h) - top_bar_h - bottom_bar_h - toolbar_h - 8 ed.canvas_rect = rl.Rectangle{canvas_x, canvas_y, canvas_w, canvas_h} @@ -496,11 +564,6 @@ editor_render :: proc(app: ^GUI_App_State) { draw_x := canvas_x + (canvas_w - draw_w) * 0.5 + vec2_x(ed.pan_offset) * fit_scale draw_y := canvas_y + (canvas_h - draw_h) * 0.5 + vec2_y(ed.pan_offset) * fit_scale - tint := rl.WHITE - if ed.adjustments.tint_amount > 0 { - tint = rl.Fade(ed.adjustments.tint_color, 1.0 - f32(ed.adjustments.tint_amount)) - } - img_rect := rl.Rectangle{0, 0, f32(ed.drawing_rt.texture.width), f32(ed.drawing_rt.texture.height)} dest_rect := rl.Rectangle{draw_x, draw_y, draw_w, draw_h} @@ -512,7 +575,7 @@ editor_render :: proc(app: ^GUI_App_State) { dest_rect, rl.Vector2{}, 0, - tint, + rl.WHITE, ) rl.DrawTexturePro( @@ -534,6 +597,35 @@ editor_render :: proc(app: ^GUI_App_State) { } editor_render_toolbar(app) + + if ed.show_adjust_panel { + editor_render_adjust_panel(app) + } +} + +editor_update_display :: proc(ed: ^Panel_Editor_State) { + if !ed.rt_valid || !ed.base_image_valid { return } + + tex := rl.LoadTextureFromImage(ed.base_image) + if tex.id == 0 { return } + + rl.BeginTextureMode(ed.display_rt) + rl.ClearBackground(rl.Color{0, 0, 0, 0}) + + tint := rl.WHITE + adj := ed.adjustments + if adj.tint_amount > 0 { + tint = rl.Fade(adj.tint_color, 1.0 - adj.tint_amount) + } + + img_rect := rl.Rectangle{0, 0, f32(tex.width), f32(tex.height)} + dest_rect := rl.Rectangle{0, 0, f32(ed.display_rt.texture.width), f32(ed.display_rt.texture.height)} + + rl.DrawTexturePro(tex, img_rect, dest_rect, rl.Vector2{}, 0, tint) + rl.EndTextureMode() + + rl.UnloadTexture(tex) + ed.adjustments_dirty = false } editor_render_stroke_preview :: proc(ed: ^Panel_Editor_State, dest_rect: rl.Rectangle) { @@ -652,6 +744,17 @@ editor_render_toolbar :: proc(app: ^GUI_App_State) { zoom_text := fmt.tprintf("Zoom: %.0f%%", ed.zoom * 100) rl.DrawText(to_cstr(zoom_text), c.int(zoom_x), c.int(start_y + 6), 11, rl.Color{160, 160, 180, 255}) + adjust_x := zoom_x + 90 + adjust_bg := rl.Color{28, 28, 42, 255} + if ed.show_adjust_panel { adjust_bg = rl.Color{99, 102, 241, 255} } + rl.DrawRectangle(c.int(adjust_x), c.int(start_y), c.int(60), c.int(btn_h), adjust_bg) + rl.DrawText("Adjust", c.int(adjust_x + 4), c.int(start_y + 6), 11, rl.WHITE) + if rl.IsMouseButtonPressed(.LEFT) && + mx >= adjust_x && mx <= adjust_x + 60 && + my >= start_y && my <= start_y + btn_h { + ed.show_adjust_panel = !ed.show_adjust_panel + } + undo_x := tb_x + tb_w - 210 undo_label := fmt.tprintf("Undo(%d)", len(ed.undo_stack)) rl.DrawRectangle(c.int(undo_x), c.int(start_y), c.int(50), c.int(btn_h), rl.Color{28, 28, 42, 255}) @@ -733,3 +836,155 @@ editor_render_color_panel :: proc(ed: ^Panel_Editor_State, px, py: f32) { rl.DrawRectangle(c.int(px + 70), c.int(cur_y), c.int(30), c.int(16), ed.brush_color) rl.DrawRectangleLines(c.int(px + 70), c.int(cur_y), c.int(30), c.int(16), rl.Color{80, 80, 100, 255}) } + +editor_render_adjust_panel :: proc(app: ^GUI_App_State) { + ed := &app.editor + screen_w := rl.GetScreenWidth() + screen_h := rl.GetScreenHeight() + + panel_w: f32 = 220 + panel_h: f32 = f32(screen_h) - 38 - 44 - 8 + panel_x := f32(screen_w) - panel_w - 4 + panel_y: f32 = 38 + 40 + 4 + + rl.DrawRectangle(c.int(panel_x), c.int(panel_y), c.int(panel_w), c.int(panel_h), rl.Color{18, 18, 30, 245}) + rl.DrawRectangleLines(c.int(panel_x), c.int(panel_y), c.int(panel_w), c.int(panel_h), rl.Color{50, 50, 70, 255}) + + label_x := panel_x + 10 + slider_w := panel_w - 20 + slider_h: f32 = 14 + row_h: f32 = 36 + y := panel_y + 8 + + rl.DrawText("Color Adjustments", c.int(label_x), c.int(y), 12, rl.Color{200, 200, 220, 255}) + y += 22 + + adj := &ed.adjustments + + brightness_val := adj.brightness + rl.DrawText("Brightness", c.int(label_x), c.int(y), 10, rl.Color{160, 160, 180, 255}) + y += 14 + slider_rect := rl.Rectangle{label_x, y, slider_w, slider_h} + rl.GuiSliderBar(slider_rect, "- ", " +", &brightness_val, -1.0, 1.0) + if brightness_val != adj.brightness { + adj.brightness = brightness_val + ed.adjustments_dirty = true + } + y += row_h + + contrast_val := adj.contrast + rl.DrawText("Contrast", c.int(label_x), c.int(y), 10, rl.Color{160, 160, 180, 255}) + y += 14 + slider_rect = rl.Rectangle{label_x, y, slider_w, slider_h} + rl.GuiSliderBar(slider_rect, "- ", " +", &contrast_val, -1.0, 1.0) + if contrast_val != adj.contrast { + adj.contrast = contrast_val + ed.adjustments_dirty = true + } + y += row_h + + sat_val := adj.saturation + rl.DrawText("Saturation", c.int(label_x), c.int(y), 10, rl.Color{160, 160, 180, 255}) + y += 14 + slider_rect = rl.Rectangle{label_x, y, slider_w, slider_h} + rl.GuiSliderBar(slider_rect, "- ", " +", &sat_val, -1.0, 1.0) + if sat_val != adj.saturation { + adj.saturation = sat_val + ed.adjustments_dirty = true + } + y += row_h + + tint_val := adj.tint_amount + rl.DrawText("Tint Amount", c.int(label_x), c.int(y), 10, rl.Color{160, 160, 180, 255}) + y += 14 + slider_rect = rl.Rectangle{label_x, y, slider_w, slider_h} + rl.GuiSliderBar(slider_rect, "0 ", " 1", &tint_val, 0.0, 1.0) + if tint_val != adj.tint_amount { + adj.tint_amount = tint_val + ed.adjustments_dirty = true + } + y += row_h + + rl.DrawText("Tint Color", c.int(label_x), c.int(y), 10, rl.Color{160, 160, 180, 255}) + y += 14 + tint_colors := []rl.Color{ + rl.WHITE, rl.Color{255, 230, 180, 255}, rl.Color{180, 200, 255, 255}, + rl.Color{255, 200, 150, 255}, rl.Color{200, 255, 200, 255}, rl.Color{255, 200, 200, 255}, + } + tsw: f32 = 22 + tgap: f32 = 4 + for idx in 0..= tx && mx <= tx + tsw && + my >= y && my <= y + tsw { + adj.tint_color = tc + ed.adjustments_dirty = true + } + } + y += tsw + 12 + + rl.DrawText("Presets", c.int(label_x), c.int(y), 10, rl.Color{160, 160, 180, 255}) + y += 14 + filter_names := []string{"None", "Vintage", "Noir", "Cool", "Warm", "HiCon", "Faded", "Drama"} + filter_enums := []Color_Filter{.None, .Vintage, .Noir, .Cool_Tone, .Warm_Tone, .High_Contrast, .Faded, .Dramatic} + fbtn_w: f32 = 48 + fbtn_h: f32 = 20 + fcols: f32 = 4 + for idx in 0..= fx && mx <= fx + fbtn_w && + my >= fy && my <= fy + fbtn_h { + adj.filter = filter_enums[idx] + editor_apply_filter_preset(adj) + ed.adjustments_dirty = true + } + } + y += (f32(len(filter_names)) / fcols) * (fbtn_h + 4) + 12 + + btn_w: f32 = 92 + btn_h: f32 = 24 + rl.DrawRectangle(c.int(label_x), c.int(y), c.int(btn_w), c.int(btn_h), rl.Color{34, 139, 34, 255}) + rl.DrawText("Apply", c.int(label_x + 30), c.int(y + 5), 11, rl.WHITE) + mouse := rl.GetMousePosition() + mx := vec2_x(mouse) + my := vec2_y(mouse) + if rl.IsMouseButtonPressed(.LEFT) && + mx >= label_x && mx <= label_x + btn_w && + my >= y && my <= y + btn_h { + push_status(&app.status_msg, &app.action_log, editor_commit(app)) + } + + reset_x := label_x + btn_w + 6 + rl.DrawRectangle(c.int(reset_x), c.int(y), c.int(btn_w), c.int(btn_h), rl.Color{80, 80, 80, 255}) + rl.DrawText("Reset", c.int(reset_x + 28), c.int(y + 5), 11, rl.WHITE) + if rl.IsMouseButtonPressed(.LEFT) && + mx >= reset_x && mx <= reset_x + btn_w && + my >= y && my <= y + btn_h { + ed.adjustments = editor_default_adjustments() + ed.adjustments_dirty = true + } +}