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).
This commit is contained in:
echo 2026-05-28 15:43:00 +02:00
parent 49b383db2e
commit 4445574b43

View File

@ -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 {
@ -61,9 +73,57 @@ Panel_Editor_State :: struct {
pan_start: rl.Vector2,
pan_offset_start: rl.Vector2,
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 {
return Color_Adjustments{
brightness = 0,
@ -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..<len(tint_colors) {
tc := tint_colors[idx]
tx := label_x + f32(idx) * (tsw + tgap)
rl.DrawRectangle(c.int(tx), c.int(y), c.int(tsw), c.int(tsw), tc)
is_sel := adj.tint_color == tc
if is_sel {
rl.DrawRectangleLines(c.int(tx), c.int(y), c.int(tsw), c.int(tsw), rl.Color{99, 102, 241, 255})
} else {
rl.DrawRectangleLines(c.int(tx), c.int(y), c.int(tsw), c.int(tsw), rl.Color{60, 60, 80, 255})
}
mouse := rl.GetMousePosition()
mx := vec2_x(mouse)
my := vec2_y(mouse)
if rl.IsMouseButtonPressed(.LEFT) &&
mx >= 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..<len(filter_names) {
col := f32(idx % int(fcols))
row := f32(idx / int(fcols))
fx := label_x + col * (fbtn_w + 4)
fy := y + row * (fbtn_h + 4)
is_sel := adj.filter == filter_enums[idx]
bg := rl.Color{28, 28, 42, 255}
if is_sel { bg = rl.Color{99, 102, 241, 255} }
rl.DrawRectangle(c.int(fx), c.int(fy), c.int(fbtn_w), c.int(fbtn_h), bg)
rl.DrawText(to_cstr(filter_names[idx]), c.int(fx + 3), c.int(fy + 4), 9, rl.WHITE)
mouse := rl.GetMousePosition()
mx := vec2_x(mouse)
my := vec2_y(mouse)
if rl.IsMouseButtonPressed(.LEFT) &&
mx >= 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
}
}