This commit is contained in:
echo 2026-05-22 00:45:17 +02:00
parent 1e85df5193
commit 33c70e776a
4 changed files with 265 additions and 203 deletions

View File

@ -9,12 +9,12 @@
"last_modified_iso": ""
},
"user_mode": 0,
"story_idea": "two balls rolling under the sun",
"story_idea": "3 trees on the wind",
"story_genre": "action",
"target_audience": "general",
"art_style": "manga",
"script": {
"title": "Rolling Duel",
"title": "The Last Stand",
"synopsis": "Generated comic synopsis",
"characters": [
@ -28,7 +28,7 @@
"panel_id": "panel_001_001",
"panel_number": 1,
"shot_type": 2,
"description": "A blazing sun dominates the sky, casting harsh light on a vast, empty desert. Two small dots in the distance kick up dust.",
"description": "Wide shot: Three ancient trees stand on a barren hilltop, their branches intertwined. Storm clouds swirl overhead, lightning in the distance. Wind howls, leaves flying.",
"characters_present": [
],
@ -45,7 +45,7 @@
"panel_id": "panel_001_002",
"panel_number": 2,
"shot_type": 2,
"description": "Close-up on two balls: one red with a fiery pattern, one blue with a water-like swirl. They are rolling fast, side by side. Cracks form in the ground beneath them.",
"description": "Close-up on the middle tree's trunk. Bark cracks open, revealing a glowing, pulsing core of light. The other two trees lean inward, as if protecting it.",
"characters_present": [
],
@ -62,17 +62,12 @@
"panel_id": "panel_001_003",
"panel_number": 3,
"shot_type": 2,
"description": "The red ball veers sharply left, kicking up a spray of sand. The blue ball mirrors the move, sparks flying from its surface.",
"description": "From the left, a massive tornado approaches, dark and funnel-shaped. Debris swirls around it. The trees brace, roots gripping the ground.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "VROOM!",
"bubble_type": 0,
"emotion": ""
}
],
"caption": "",
"sound_effects": [
@ -84,29 +79,7 @@
"panel_id": "panel_001_004",
"panel_number": 4,
"shot_type": 2,
"description": "Red ball takes a ramp-like dune and launches into the air, spinning. Blue ball follows, but slightly lower.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "WHOOSH!",
"bubble_type": 0,
"emotion": ""
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_001_005",
"panel_number": 5,
"shot_type": 2,
"description": "Aerial view: both balls are airborne, shadows on the sand below. Red ball is slightly ahead.",
"description": "The tornado hits the left tree. Its branches snap violently, but it holds firm, roots glowing with energy. Sparks fly where wind meets bark.",
"characters_present": [
],
@ -116,28 +89,6 @@
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_001_006",
"panel_number": 6,
"shot_type": 2,
"description": "They land simultaneously, creating twin craters. Dust clouds obscure them. The sun glints off their surfaces.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "BOOM!",
"bubble_type": 0,
"emotion": ""
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
}
@ -151,7 +102,7 @@
"panel_id": "panel_002_001",
"panel_number": 1,
"shot_type": 2,
"description": "From the dust, the red ball emerges first, rolling faster. The blue ball is close behind, leaving a trail of steam.",
"description": "The middle tree pulses brighter, sending a shockwave that pushes the tornado back. The right tree extends a branch to shield the core. Wind howls.",
"characters_present": [
],
@ -168,17 +119,12 @@
"panel_id": "panel_002_002",
"panel_number": 2,
"shot_type": 2,
"description": "Close-up on the red ball: its surface is glowing hot, with tiny flames licking the edges.",
"description": "The tornado splits into two smaller funnels, attacking from both sides. The left tree's roots snap, it starts to topple. The middle tree's core flickers.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "HISS",
"bubble_type": 0,
"emotion": ""
}
],
"caption": "",
"sound_effects": [
@ -190,17 +136,12 @@
"panel_id": "panel_002_003",
"panel_number": 3,
"shot_type": 2,
"description": "The blue ball rams into the red ball from the side. They lock, spinning together in a whirlwind of sand.",
"description": "The right tree bends forward, its trunk wrapping around the middle tree, absorbing the impact. The left tree falls, but its roots still glow, transferring energy.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "CLANG!",
"bubble_type": 0,
"emotion": ""
}
],
"caption": "",
"sound_effects": [
@ -212,29 +153,7 @@
"panel_id": "panel_002_004",
"panel_number": 4,
"shot_type": 2,
"description": "They separate, skidding to a halt. Both balls are facing each other, a few meters apart. The sun is directly overhead.",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "SCREECH",
"bubble_type": 0,
"emotion": ""
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_002_005",
"panel_number": 5,
"shot_type": 2,
"description": "Silence. A single bead of sweat (or condensation) drips from the blue ball. The red ball's glow intensifies.",
"description": "Final wide shot: The storm passes, clouds break. The two remaining trees stand tall, the middle core glowing steady. A single leaf drifts down, landing on the ground. Peace.",
"characters_present": [
],
@ -244,28 +163,6 @@
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
},
{
"panel_id": "panel_002_006",
"panel_number": 6,
"shot_type": 2,
"description": "Both balls lunge forward at the same time. The panel is a blur of motion lines and dust. The final word:",
"characters_present": [
],
"dialogue": [
{
"speaker_id": "",
"text": "CRASH!!!",
"bubble_type": 0,
"emotion": ""
}
],
"caption": "",
"sound_effects": [
],
"transition_from_previous": 0
}
@ -277,90 +174,7 @@
],
"panel_images": {
"panel_001_001": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_001_panel_001_001.png",
"width": 1024,
"height": 1024,
"seed": 1,
"prompt": "local"
},
"panel_002_006": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_012_panel_002_006.png",
"width": 1024,
"height": 1024,
"seed": 12,
"prompt": "local"
},
"panel_001_006": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_006_panel_001_006.png",
"width": 1024,
"height": 1024,
"seed": 6,
"prompt": "local"
},
"panel_002_001": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_007_panel_002_001.png",
"width": 1024,
"height": 1024,
"seed": 7,
"prompt": "local"
},
"panel_001_002": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_002_panel_001_002.png",
"width": 1024,
"height": 1024,
"seed": 2,
"prompt": "local"
},
"panel_002_005": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_011_panel_002_005.png",
"width": 1024,
"height": 1024,
"seed": 11,
"prompt": "local"
},
"panel_001_004": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_004_panel_001_004.png",
"width": 1024,
"height": 1024,
"seed": 4,
"prompt": "local"
},
"panel_002_003": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_009_panel_002_003.png",
"width": 1024,
"height": 1024,
"seed": 9,
"prompt": "local"
},
"panel_001_003": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_003_panel_001_003.png",
"width": 1024,
"height": 1024,
"seed": 3,
"prompt": "local"
},
"panel_002_004": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_010_panel_002_004.png",
"width": 1024,
"height": 1024,
"seed": 10,
"prompt": "local"
},
"panel_002_002": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_008_panel_002_002.png",
"width": 1024,
"height": 1024,
"seed": 8,
"prompt": "local"
},
"panel_001_005": {
"url": "file:///tmp/comic-gui-local-panels-1597088181/panel_005_panel_001_005.png",
"width": 1024,
"height": 1024,
"seed": 5,
"prompt": "local"
}
},
"panel_errors": {

View File

@ -130,6 +130,55 @@ action_layout_auto :: proc(controller: ^ui.App_Controller) -> string {
return "Auto layout generated"
}
action_regenerate_page_layout :: proc(controller: ^ui.App_Controller, page_num: int) -> string {
if page_num < 0 || page_num >= len(controller.state.page_layouts) {
return "Invalid layout page"
}
layout := &controller.state.page_layouts[page_num]
panel_count := len(layout.panels)
if panel_count == 0 {
return "Page has no panels"
}
// For a simple visible regenerate effect, pick a random pattern that fits
pattern_ids := []string{
"single-splash", "grid-2x2", "grid-3x3", "dynamic-action",
"manga-spread", "dialogue-heavy", "cinematic-widescreen",
}
// Just pick the next valid pattern in the list (or wrap around)
start_idx := 0
for id, i in pattern_ids {
if id == layout.pattern_id {
start_idx = i
break
}
}
new_pattern_id := layout.pattern_id
for i in 1..=len(pattern_ids) {
idx := (start_idx + i) % len(pattern_ids)
candidate := pattern_ids[idx]
if core.pattern_max_panels(candidate) >= panel_count {
new_pattern_id = candidate
break
}
}
pattern := core.get_layout_pattern_by_id(new_pattern_id)
layout.pattern_id = new_pattern_id
for i in 0..<panel_count {
cell_index := i
if cell_index >= len(pattern.cells) {
cell_index = len(pattern.cells) - 1
}
layout.panels[i].layout_cell = pattern.cells[cell_index]
}
return "Layout page regenerated"
}
export_format_name :: proc(f: core.Export_Format) -> string {
switch f {
case .PDF: return "PDF"

View File

@ -331,6 +331,26 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.panel_cursor+1, panel_count))
}
}
if controller.active_screen == .Layout && button_clicked(summary_prev_btn) {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
summary_opts.layout_page_cursor -= 1
if summary_opts.layout_page_cursor < 0 {
summary_opts.layout_page_cursor = layout_page_count - 1
}
push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count))
}
}
if controller.active_screen == .Layout && button_clicked(summary_next_btn) {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
summary_opts.layout_page_cursor += 1
if summary_opts.layout_page_cursor >= layout_page_count {
summary_opts.layout_page_cursor = 0
}
push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count))
}
}
if button_clicked(new_btn) {
if is_dirty && !shift_down {
@ -519,6 +539,26 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
push_status(&status_msg, &action_log, fmt.tprintf("Viewing panel %d/%d", summary_opts.panel_cursor+1, panel_count))
}
}
if controller.active_screen == .Layout && ctrl_down && rl.IsKeyPressed(.LEFT_BRACKET) {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
summary_opts.layout_page_cursor -= 1
if summary_opts.layout_page_cursor < 0 {
summary_opts.layout_page_cursor = layout_page_count - 1
}
push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count))
}
}
if controller.active_screen == .Layout && ctrl_down && rl.IsKeyPressed(.RIGHT_BRACKET) {
layout_page_count := len(controller.state.page_layouts)
if layout_page_count > 0 {
summary_opts.layout_page_cursor += 1
if summary_opts.layout_page_cursor >= layout_page_count {
summary_opts.layout_page_cursor = 0
}
push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count))
}
}
if ctrl_down && rl.IsKeyPressed(.MINUS) {
push_dirty_status(&is_dirty, &status_msg, &action_log, set_autosave_interval_text(&autosave_interval_text, autosave_secs-5))
}
@ -889,12 +929,12 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
} else if controller.active_screen == .Panels {
draw_small_button(summary_prev_btn, "< Pn")
draw_small_button(summary_next_btn, "Pn >")
} else if controller.active_screen == .Layout {
draw_small_button(summary_prev_btn, "< Ly")
draw_small_button(summary_next_btn, "Ly >")
}
if !compact_mode {
hint_label := "Ctrl+[ / Ctrl+]"
if controller.active_screen == .Script || controller.active_screen == .Layout {
hint_label = "Ctrl+H / Ctrl+J"
}
draw_hint_pill(rl.Rectangle{x = f32(282 + status_w_loop - 186), y = f32(lower_y_loop + 46), width = 172, height = 20}, hint_label, false)
}
}
@ -935,6 +975,31 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
}
draw_text_fitted("Ctrl+[ / Ctrl+] panel nav", log_x_loop+18, lower_y_loop+8, 13, int(main_w_loop-status_w_loop-34), 7, TEXT_TERTIARY)
} else if controller.active_screen == .Layout {
regen, new_layout_cursor := draw_layout_detail_panel(controller, log_x_loop, lower_y_loop, main_w_loop-status_w_loop-2, 200, summary_opts.layout_page_cursor)
layout_page_count := len(controller.state.page_layouts)
if summary_opts.layout_page_cursor != new_layout_cursor {
summary_opts.layout_page_cursor = new_layout_cursor
push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count))
}
if regen {
msg := action_regenerate_page_layout(&controller, summary_opts.layout_page_cursor)
push_status(&status_msg, &action_log, msg)
}
wheel := rl.GetMouseWheelMove()
if wheel != 0 && layout_page_count > 0 {
summary_opts.layout_page_cursor -= int(wheel)
if summary_opts.layout_page_cursor < 0 {
summary_opts.layout_page_cursor = 0
}
if summary_opts.layout_page_cursor >= layout_page_count {
summary_opts.layout_page_cursor = layout_page_count - 1
}
push_status(&status_msg, &action_log, fmt.tprintf("Viewing layout page %d/%d", summary_opts.layout_page_cursor+1, layout_page_count))
}
draw_text_fitted("Ctrl+H / Ctrl+J layout nav", log_x_loop+18, lower_y_loop+8, 13, int(main_w_loop-status_w_loop-34), 7, TEXT_TERTIARY)
} else {
draw_card(rl.Rectangle{x = f32(log_x_loop), y = f32(lower_y_loop), width = f32(main_w_loop-status_w_loop-2), height = 200})
draw_section_title(log_x_loop+18, lower_y_loop+6, "Action Log")

View File

@ -441,3 +441,137 @@ draw_panels_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32,
}
return
}
clamp_layout_cursor :: proc(layout_count, cursor: int) -> int {
if layout_count <= 0 {
return 0
}
if cursor < 0 {
return 0
}
if cursor >= layout_count {
return layout_count - 1
}
return cursor
}
draw_layout_detail_panel :: proc(controller: ui.App_Controller, x, y, w, h: i32, cursor: int) -> (regen_clicked: bool, new_cursor: int) {
new_cursor = cursor
regen_clicked = false
draw_card(rl.Rectangle{x = f32(x), y = f32(y), width = f32(w), height = f32(h)})
draw_section_title(x+18, y+6, "Layout Detail")
draw_subtle_strip(rl.Rectangle{x = f32(x+12), y = f32(y), width = f32(w-24), height = 34})
layout_count := len(controller.state.page_layouts)
if layout_count == 0 {
draw_summary_line(x+18, y+46, "No layouts yet. Use Layout Auto after panels.", SUMMARY_HINT)
return
}
idx := clamp_layout_cursor(layout_count, cursor)
layout := controller.state.page_layouts[idx]
// Header line with status
draw_summary_line(x+18, y+46, fmt.tprintf("Page %d/%d • pattern: %s • %d panels", idx+1, layout_count, layout.pattern_id, len(layout.panels)), SUMMARY_ACCENT)
// Regenerate button
btn_rec := rl.Rectangle{x = f32(x + w - 100), y = f32(y+42), width = 80, height = 24}
draw_small_button_state(btn_rec, "Regen", true)
if button_clicked(btn_rec) {
regen_clicked = true
}
// Layout dimensions
draw_summary_subline(x+18, y+68, fmt.tprintf("size: %d x %d", layout.width, layout.height), SUMMARY_SUBLINE)
// Mini wireframe preview
preview_x := x + 18
preview_y := y + 88
preview_max_w: f32 = f32(w) * 0.4
preview_max_h: f32 = f32(h - 100)
if preview_max_h < 40 {
preview_max_h = 40
}
// Scale to fit
lw := f32(layout.width)
lh := f32(layout.height)
if lw < 1 { lw = 1 }
if lh < 1 { lh = 1 }
scale_x := preview_max_w / lw
scale_y := preview_max_h / lh
scale := scale_x
if scale_y < scale {
scale = scale_y
}
pw := lw * scale
ph := lh * scale
// Draw page outline
page_rec := rl.Rectangle{x = f32(preview_x), y = f32(preview_y), width = pw, height = ph}
rl.DrawRectangleRounded(page_rec, 0.02, 4, BG_STRIP)
rl.DrawRectangleRoundedLinesEx(page_rec, 0.02, 4, 1.0, BORDER_CARD)
// Draw each panel cell
for i in 0..<len(layout.panels) {
cell := layout.panels[i].layout_cell
cx := f32(preview_x) + cell.x * pw
cy := f32(preview_y) + cell.y * ph
cw := cell.w * pw
ch := cell.h * ph
// Inset slightly for gutter
inset: f32 = 1.5
cell_rec := rl.Rectangle{x = cx + inset, y = cy + inset, width = cw - inset*2, height = ch - inset*2}
rl.DrawRectangleRounded(cell_rec, 0.06, 4, ACCENT_SURFACE)
rl.DrawRectangleRoundedLinesEx(cell_rec, 0.06, 4, 1.0, ACCENT_MUTED)
// Panel number label
num_label := fmt.tprintf("%d", layout.panels[i].panel_number)
label_sz: i32 = 10
if cw > 30 && ch > 14 {
draw_text_fitted(num_label, i32(cx + inset + 3), i32(cy + inset + 2), label_sz, int(cw - inset*2 - 4), 5, TEXT_SECONDARY)
}
}
// Page list (right side)
list_x := x + i32(preview_max_w) + 36
list_y := y + 88
list_w := w - i32(preview_max_w) - 54
row_h: i32 = 18
rows: i32 = (h - 100) / row_h
if rows < 1 {
rows = 1
}
start := idx - int(rows/2)
if start < 0 {
start = 0
}
end := start + int(rows)
if end > layout_count {
end = layout_count
start = end - int(rows)
if start < 0 {
start = 0
}
}
line: i32 = 0
for i in start..<end {
l := controller.state.page_layouts[i]
mark := " "
row_color := TEXT_TERTIARY
if i == idx {
mark = ">"
row_color = TEXT_PRIMARY
}
row_rec := rl.Rectangle{x = f32(list_x), y = f32(list_y + line*row_h), width = f32(list_w), height = f32(row_h)}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && rl.IsMouseButtonPressed(.LEFT) {
new_cursor = i
}
if rl.CheckCollisionPointRec(rl.GetMousePosition(), row_rec) && row_color == TEXT_TERTIARY {
row_color = TEXT_SECONDARY
}
draw_summary_subline(list_x, list_y+line*row_h+2, fit_text_for_width(fmt.tprintf("%s %02d %s (%d)", mark, l.page_number, l.pattern_id, len(l.panels)), int(list_w), 7), row_color)
line += 1
}
return
}