check point

This commit is contained in:
dimitar committed 2026-05-21 06:10:32 +02:00
1 parent ae1cae967b
commit 1e85df5193
72 files changed
+11452

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
package core
import "core:fmt"
DEFAULT_BUBBLE_STYLE := Speech_Bubble_Style{
background_color = "#ffffff",
border_color = "#000000",
border_width = 2,
border_radius = 16,
font_family = "Comic Sans MS, cursive, sans-serif",
font_size = 14,
text_color = "#000000",
padding = 12,
}
clampf :: proc(v, minv, maxv: f32) -> f32 {
if v < minv { return minv }
if v > maxv { return maxv }
return v
}
calculate_bubble_size :: proc(text: string, style: Speech_Bubble_Style) -> Size {
char_width := style.font_size * 0.6
char_height := style.font_size * 1.4
chars_per_line: f32 = 25
lines := f32(len(text)) / chars_per_line
if lines < 1 {
lines = 1
}
width := f32(len(text))*char_width/lines + style.padding*2
width = clampf(width, 100, 300)
height := lines*char_height + style.padding*2 + 20
return Size{width = width, height = height}
}
auto_place_bubble :: proc(bubble: Speech_Bubble, panel_w, panel_h: f32, speaker_pos: Position) -> Speech_Bubble {
updated := bubble
size := calculate_bubble_size(bubble.text, bubble.style)
updated.size = size
speaker := speaker_pos
if speaker.x == 0 && speaker.y == 0 {
speaker = Position{x = panel_w * 0.5, y = panel_h * 0.7}
}
switch bubble.type {
case .Thought:
updated.position = Position{
x = clampf(speaker.x-size.width/2, 20, panel_w-size.width-20),
y = clampf(speaker.y-size.height-40, 20, panel_h-size.height-10),
}
updated.tail_target = Position{x = speaker.x, y = speaker.y - 20}
updated.tail_direction = "bottom"
case .Shout:
updated.size = Size{width = size.width * 1.1, height = size.height * 1.1}
updated.position = Position{
x = clampf(speaker.x-updated.size.width/2, 10, panel_w-updated.size.width-10),
y = clampf(speaker.y-updated.size.height-30, 10, panel_h-updated.size.height-10),
}
updated.tail_target = Position{x = speaker.x, y = speaker.y - 10}
updated.tail_direction = "bottom"
case .Whisper:
updated.position = Position{
x = clampf(speaker.x-size.width/2, 20, panel_w-size.width-20),
y = clampf(speaker.y-size.height-30, 20, panel_h-size.height-10),
}
updated.tail_target = Position{x = speaker.x, y = speaker.y - 20}
updated.tail_direction = "bottom"
case .Narration:
updated.position = Position{x = panel_w * 0.1, y = 20}
updated.size = Size{width = panel_w * 0.8, height = size.height}
updated.tail_direction = "bottom"
case .Sound_Effect:
updated.position = Position{
x = clampf(speaker.x+20, 10, panel_w-size.width-10),
y = clampf(speaker.y-size.height-50, 10, panel_h-size.height-10),
}
updated.tail_target = Position{x = speaker.x + 30, y = speaker.y - 30}
updated.tail_direction = "bottom-left"
case .Normal:
updated.position = Position{
x = clampf(speaker.x-size.width/2, 20, panel_w-size.width-20),
y = clampf(speaker.y-size.height-30, 20, panel_h-size.height-10),
}
updated.tail_target = Position{x = speaker.x, y = speaker.y - 20}
updated.tail_direction = "bottom"
}
return updated
}
auto_place_panel_bubbles :: proc(panel: Panel, panel_w, panel_h: f32) -> []Speech_Bubble {
if len(panel.dialogue) == 0 && len(panel.caption) == 0 {
return nil
}
bubbles: [dynamic]Speech_Bubble
for dialogue, idx in panel.dialogue {
speaker_count := len(panel.characters_present)
speaker_idx := 0
for char_id, j in panel.characters_present {
if char_id == dialogue.speaker_id {
speaker_idx = j
break
}
}
speaker_x := panel_w * 0.5
if speaker_count > 1 {
speaker_x = (panel_w / f32(speaker_count + 1)) * f32(speaker_idx + 1)
}
speaker_y := panel_h * 0.75
bubble := Speech_Bubble{
id = fmt.aprintf("bubble_%s_%d", panel.panel_id, idx),
panel_id = panel.panel_id,
type = dialogue.bubble_type,
text = dialogue.text,
position = Position{},
size = Size{width = 100, height = 50},
tail_direction = "bottom",
tail_target = Position{x = speaker_x, y = speaker_y},
style = DEFAULT_BUBBLE_STYLE,
speaker_id = dialogue.speaker_id,
}
placed := auto_place_bubble(bubble, panel_w, panel_h, Position{x = speaker_x, y = speaker_y})
placed.position.y = clampf(placed.position.y-f32(idx*10), 10, panel_h-placed.size.height-10)
placed.position.x = clampf(placed.position.x, 10, panel_w-placed.size.width-10)
append(&bubbles, placed)
}
if len(panel.caption) > 0 {
caption_bubble := Speech_Bubble{
id = fmt.aprintf("caption_%s", panel.panel_id),
panel_id = panel.panel_id,
type = .Narration,
text = panel.caption,
position = Position{x = panel_w * 0.1, y = 20},
size = Size{width = panel_w * 0.8, height = 40},
tail_direction = "bottom",
tail_target = Position{x = panel_w * 0.5, y = 60},
style = Speech_Bubble_Style{
background_color = "#f5f5f5",
border_color = "#999999",
border_width = DEFAULT_BUBBLE_STYLE.border_width,
border_radius = DEFAULT_BUBBLE_STYLE.border_radius,
font_family = DEFAULT_BUBBLE_STYLE.font_family,
font_size = DEFAULT_BUBBLE_STYLE.font_size,
text_color = DEFAULT_BUBBLE_STYLE.text_color,
padding = DEFAULT_BUBBLE_STYLE.padding,
},
speaker_id = "",
}
append(&bubbles, caption_bubble)
}
return bubbles[:]
}
+108
View File
@@ -0,0 +1,108 @@
package core
import "core:fmt"
DEFAULT_PROMPT_AGE :: "young adult"
DEFAULT_PROMPT_GENDER :: "person"
DEFAULT_PROMPT_BODY :: "average"
DEFAULT_PROMPT_OUTFIT :: "casual clothing"
DEFAULT_PROMPT_ACCESSORIES :: "no accessories"
build_character_prompt :: proc(c: Character, action, setting, lighting, art_style: string) -> string {
t := c.prompt_template
age := t.age
if len(age) == 0 { age = DEFAULT_PROMPT_AGE }
gender := t.gender
if len(gender) == 0 { gender = DEFAULT_PROMPT_GENDER }
body := t.body_type
if len(body) == 0 { body = DEFAULT_PROMPT_BODY }
outfit := t.outfit
if len(outfit) == 0 { outfit = DEFAULT_PROMPT_OUTFIT }
accessories := t.accessories
if len(accessories) == 0 { accessories = DEFAULT_PROMPT_ACCESSORIES }
return fmt.aprintf(
"%s style. %s-year-old %s, %s %s hair, %s eyes, %s skin, %s build, wearing %s, %s, %s, %s, %s, %s",
art_style,
age,
gender,
t.hair_color,
t.hair_style,
t.eye_color,
t.skin_tone,
body,
outfit,
accessories,
t.distinguishing_features,
action,
setting,
lighting,
)
}
derive_seed_from_string :: proc(s: string) -> i64 {
hash: i32 = 0
for r in s {
hash = ((hash << 5) - hash) + i32(r)
}
res := i64(hash)
if res < 0 {
res = -res
}
return res % 2147483647
}
hash_char :: proc(hash: ^i32, c: u8) {
hash^ = ((hash^ << 5) - hash^) + i32(c)
}
hash_string :: proc(hash: ^i32, s: string) {
for r in s {
hash^ = ((hash^ << 5) - hash^) + i32(r)
}
}
hash_decimal_int :: proc(hash: ^i32, n: int) {
if n == 0 {
hash_char(hash, '0')
return
}
value := n
if value < 0 {
hash_char(hash, '-')
value = -value
}
buf: [20]u8
i := len(buf)
for value > 0 {
i -= 1
d := value % 10
buf[i] = u8('0' + d)
value /= 10
}
for ; i < len(buf); i += 1 {
hash_char(hash, buf[i])
}
}
generate_panel_seed :: proc(project_id: string, page_number, panel_number: int, panel_id: string) -> i64 {
hash: i32 = 0
hash_string(&hash, project_id)
hash_char(&hash, '_')
hash_decimal_int(&hash, page_number)
hash_char(&hash, '_')
hash_decimal_int(&hash, panel_number)
hash_char(&hash, '_')
hash_string(&hash, panel_id)
res := i64(hash)
if res < 0 {
res = -res
}
return res % 2147483647
}
+140
View File
@@ -0,0 +1,140 @@
package core
dispose_character :: proc(c: Character) {
delete(c.character_sheet_urls)
}
dispose_character_owned :: proc(c: Character) {
delete(c.id)
delete(c.name)
delete(c.description)
delete(c.reference_image_url)
delete(c.first_appearance_panel)
delete(c.character_sheet_urls)
}
dispose_script :: proc(script: ^Comic_Script) {
for c in script.characters {
dispose_character(c)
}
for p in script.pages {
for pan in p.panels {
delete(pan.characters_present)
delete(pan.dialogue)
delete(pan.sound_effects)
}
delete(p.panels)
}
delete(script.characters)
delete(script.pages)
}
dispose_script_owned :: proc(script: ^Comic_Script) {
delete(script.title)
delete(script.synopsis)
for c in script.characters {
dispose_character_owned(c)
}
for p in script.pages {
for pan in p.panels {
delete(pan.panel_id)
delete(pan.description)
delete(pan.caption)
for id in pan.characters_present { delete(id) }
delete(pan.characters_present)
for d in pan.dialogue {
delete(d.speaker_id)
delete(d.text)
delete(d.emotion)
}
delete(pan.dialogue)
for s in pan.sound_effects { delete(s) }
delete(pan.sound_effects)
}
delete(p.panels)
}
delete(script.characters)
delete(script.pages)
}
dispose_page_layouts :: proc(layouts: ^[]Page_Layout) {
for l in layouts^ {
delete(l.panels)
}
delete(layouts^)
}
dispose_speech_bubbles :: proc(bubbles: ^map[string][]Speech_Bubble) {
for _, s in bubbles^ {
delete(s)
}
delete(bubbles^)
}
dispose_state :: proc(state: ^Comic_State) {
dispose_script(&state.script)
for c in state.characters {
dispose_character(c)
}
if raw_data(state.characters) != raw_data(state.script.characters) {
delete(state.characters)
}
delete(state.panel_images)
delete(state.panel_errors)
dispose_page_layouts(&state.page_layouts)
dispose_speech_bubbles(&state.speech_bubbles)
delete(state.workflow.completed_steps)
state.panel_images = nil
state.panel_errors = nil
state.speech_bubbles = nil
state.page_layouts = nil
state.characters = nil
state.workflow.completed_steps = nil
}
dispose_state_owned :: proc(state: ^Comic_State) {
delete(state.project.project_id)
delete(state.project.project_name)
delete(state.project.created_at_iso)
delete(state.project.last_modified_iso)
delete(state.story_idea)
delete(state.story_genre)
delete(state.target_audience)
delete(state.art_style)
delete(state.workflow.error_message)
dispose_script_owned(&state.script)
for c in state.characters {
dispose_character_owned(c)
}
if raw_data(state.characters) != raw_data(state.script.characters) {
delete(state.characters)
}
for _, img in state.panel_images {
delete(img.url)
delete(img.prompt)
}
delete(state.panel_images)
for _, msg in state.panel_errors {
delete(msg)
}
delete(state.panel_errors)
dispose_page_layouts(&state.page_layouts)
dispose_speech_bubbles(&state.speech_bubbles)
delete(state.workflow.completed_steps)
state.panel_images = nil
state.panel_errors = nil
state.speech_bubbles = nil
state.page_layouts = nil
state.characters = nil
state.workflow.completed_steps = nil
}
+264
View File
@@ -0,0 +1,264 @@
package core
Page_Size :: struct {
name: string,
width: int,
height: int,
dpi: int,
}
get_page_size :: proc(name: Page_Size_Name) -> Page_Size {
switch name {
case .A4:
return Page_Size{"A4", 2480, 3508, 300}
case .Letter:
return Page_Size{"US Letter", 2550, 3300, 300}
case .Manga:
return Page_Size{"B5 Manga", 2158, 3035, 300}
case .Webtoon:
return Page_Size{"Webtoon", 800, 1280, 72}
case .Square:
return Page_Size{"Square", 1080, 1080, 72}
}
return Page_Size{"A4", 2480, 3508, 300}
}
GRID_2X2_CELLS : [4]Layout_Cell = [4]Layout_Cell{
{x = 0.02, y = 0.02, w = 0.47, h = 0.47},
{x = 0.51, y = 0.02, w = 0.47, h = 0.47},
{x = 0.02, y = 0.51, w = 0.47, h = 0.47},
{x = 0.51, y = 0.51, w = 0.47, h = 0.47},
}
MANGA_3_TIER_CELLS : [6]Layout_Cell = [6]Layout_Cell{
{x = 0.02, y = 0.02, w = 0.3, h = 0.3},
{x = 0.35, y = 0.02, w = 0.3, h = 0.3},
{x = 0.68, y = 0.02, w = 0.3, h = 0.3},
{x = 0.02, y = 0.35, w = 0.47, h = 0.3},
{x = 0.51, y = 0.35, w = 0.47, h = 0.3},
{x = 0.02, y = 0.68, w = 0.96, h = 0.3},
}
ACTION_DYNAMIC_CELLS : [5]Layout_Cell = [5]Layout_Cell{
{x = 0.02, y = 0.02, w = 0.6, h = 0.35},
{x = 0.64, y = 0.02, w = 0.34, h = 0.35},
{x = 0.02, y = 0.4, w = 0.34, h = 0.28},
{x = 0.38, y = 0.4, w = 0.6, h = 0.28},
{x = 0.02, y = 0.71, w = 0.96, h = 0.27},
}
SPLASH_CELLS : [1]Layout_Cell = [1]Layout_Cell{
{x = 0.02, y = 0.02, w = 0.96, h = 0.96},
}
WEBTOON_CELLS : [3]Layout_Cell = [3]Layout_Cell{
{x = 0.02, y = 0.01, w = 0.96, h = 0.32},
{x = 0.02, y = 0.34, w = 0.96, h = 0.32},
{x = 0.02, y = 0.67, w = 0.96, h = 0.32},
}
WESTERN_3X3_CELLS : [9]Layout_Cell = [9]Layout_Cell{
{x = 0.02, y = 0.02, w = 0.3, h = 0.29},
{x = 0.35, y = 0.02, w = 0.3, h = 0.29},
{x = 0.68, y = 0.02, w = 0.3, h = 0.29},
{x = 0.02, y = 0.35, w = 0.3, h = 0.29},
{x = 0.35, y = 0.35, w = 0.3, h = 0.29},
{x = 0.68, y = 0.35, w = 0.3, h = 0.29},
{x = 0.02, y = 0.68, w = 0.3, h = 0.29},
{x = 0.35, y = 0.68, w = 0.3, h = 0.29},
{x = 0.68, y = 0.68, w = 0.3, h = 0.29},
}
DIALOGUE_HEAVY_CELLS : [8]Layout_Cell = [8]Layout_Cell{
{x = 0.02, y = 0.02, w = 0.47, h = 0.22},
{x = 0.51, y = 0.02, w = 0.47, h = 0.22},
{x = 0.02, y = 0.26, w = 0.47, h = 0.22},
{x = 0.51, y = 0.26, w = 0.47, h = 0.22},
{x = 0.02, y = 0.5, w = 0.47, h = 0.22},
{x = 0.51, y = 0.5, w = 0.47, h = 0.22},
{x = 0.02, y = 0.74, w = 0.47, h = 0.22},
{x = 0.51, y = 0.74, w = 0.47, h = 0.22},
}
CINEMATIC_CELLS : [4]Layout_Cell = [4]Layout_Cell{
{x = 0.02, y = 0.02, w = 0.96, h = 0.22},
{x = 0.02, y = 0.26, w = 0.96, h = 0.22},
{x = 0.02, y = 0.5, w = 0.96, h = 0.22},
{x = 0.02, y = 0.74, w = 0.96, h = 0.22},
}
pattern_max_panels :: proc(id: string) -> int {
switch id {
case "grid-2x2":
return 4
case "manga-3-tier":
return 6
case "action-dynamic":
return 5
case "splash-page":
return 1
case "webtoon-scroll":
return 3
case "western-3x3":
return 9
case "dialogue-heavy":
return 8
case "cinematic-widescreen":
return 4
}
return 4
}
pattern_matches_genre :: proc(id: string, genre: string) -> bool {
if len(genre) == 0 {
return true
}
switch id {
case "grid-2x2":
return true
case "manga-3-tier":
return genre == "manga" || genre == "action"
case "action-dynamic":
return genre == "action" || genre == "superhero"
case "splash-page":
return true
case "webtoon-scroll":
return genre == "webtoon" || genre == "slice-of-life"
case "western-3x3":
return genre == "western-comic"
case "dialogue-heavy":
return genre == "drama" || genre == "slice-of-life" || genre == "romance"
case "cinematic-widescreen":
return genre == "action" || genre == "scifi" || genre == "noir"
}
return true
}
get_layout_pattern_by_id :: proc(id: string) -> Layout_Pattern {
switch id {
case "grid-2x2":
return Layout_Pattern{id = id, name = "Classic Grid", description = "2x2 equal panels", genres = nil, max_panels = 4, cells = GRID_2X2_CELLS[:]}
case "manga-3-tier":
return Layout_Pattern{id = id, name = "Manga 3-Tier", description = "Three horizontal tiers", genres = nil, max_panels = 6, cells = MANGA_3_TIER_CELLS[:]}
case "action-dynamic":
return Layout_Pattern{id = id, name = "Action Dynamic", description = "Varied action panels", genres = nil, max_panels = 5, cells = ACTION_DYNAMIC_CELLS[:]}
case "splash-page":
return Layout_Pattern{id = id, name = "Splash Page", description = "Single full-page panel", genres = nil, max_panels = 1, cells = SPLASH_CELLS[:]}
case "webtoon-scroll":
return Layout_Pattern{id = id, name = "Webtoon Scroll", description = "Vertical mobile panels", genres = nil, max_panels = 3, cells = WEBTOON_CELLS[:]}
case "western-3x3":
return Layout_Pattern{id = id, name = "Western 3x3", description = "Classic 3x3", genres = nil, max_panels = 9, cells = WESTERN_3X3_CELLS[:]}
case "dialogue-heavy":
return Layout_Pattern{id = id, name = "Dialogue Heavy", description = "Conversation-focused", genres = nil, max_panels = 8, cells = DIALOGUE_HEAVY_CELLS[:]}
case "cinematic-widescreen":
return Layout_Pattern{id = id, name = "Cinematic Widescreen", description = "Wide cinematic panels", genres = nil, max_panels = 4, cells = CINEMATIC_CELLS[:]}
}
return Layout_Pattern{id = "grid-2x2", name = "Classic Grid", description = "2x2 equal panels", genres = nil, max_panels = 4, cells = GRID_2X2_CELLS[:]}
}
select_best_pattern :: proc(panel_count: int, genre: string, preference: string) -> Layout_Pattern {
pattern_ids := [8]string{
"grid-2x2",
"manga-3-tier",
"action-dynamic",
"splash-page",
"webtoon-scroll",
"western-3x3",
"dialogue-heavy",
"cinematic-widescreen",
}
if len(preference) > 0 {
if pattern_max_panels(preference) >= panel_count {
return get_layout_pattern_by_id(preference)
}
}
best_genre_id := ""
best_genre_max := 1 << 30
for id in pattern_ids {
max_panels := pattern_max_panels(id)
if max_panels < panel_count {
continue
}
if !pattern_matches_genre(id, genre) {
continue
}
if max_panels < best_genre_max {
best_genre_max = max_panels
best_genre_id = id
}
}
if len(best_genre_id) > 0 {
return get_layout_pattern_by_id(best_genre_id)
}
best_id := ""
best_max := 1 << 30
for id in pattern_ids {
max_panels := pattern_max_panels(id)
if max_panels < panel_count {
continue
}
if max_panels < best_max {
best_max = max_panels
best_id = id
}
}
if len(best_id) > 0 {
return get_layout_pattern_by_id(best_id)
}
return get_layout_pattern_by_id("grid-2x2")
}
auto_layout_pages :: proc(panels: []Panel, page_size: Page_Size_Name, genre: string, pattern_preference: string) -> []Page_Layout {
if len(panels) == 0 {
return nil
}
size := get_page_size(page_size)
pages: [dynamic]Page_Layout
panel_index := 0
page_number := 1
for panel_index < len(panels) {
remaining := len(panels) - panel_index
pattern := select_best_pattern(remaining, genre, pattern_preference)
take := pattern.max_panels
if take > remaining {
take = remaining
}
layout_panels: [dynamic]Page_Layout_Panel
for i in 0..<take {
p := panels[panel_index+i]
cell_index := i
if cell_index >= len(pattern.cells) {
cell_index = len(pattern.cells) - 1
}
append(&layout_panels, Page_Layout_Panel{
panel_id = p.panel_id,
panel_number = p.panel_number,
layout_cell = pattern.cells[cell_index],
})
}
append(&pages, Page_Layout{
page_number = page_number,
pattern_id = pattern.id,
panels = layout_panels[:],
width = size.width,
height = size.height,
})
panel_index += take
page_number += 1
}
return pages[:]
}
+56
View File
@@ -0,0 +1,56 @@
package core
import "core:fmt"
script_is_valid_minimal :: proc(script: Comic_Script) -> bool {
if len(script.title) == 0 {
return false
}
if len(script.synopsis) == 0 {
return false
}
if len(script.pages) == 0 {
return false
}
return true
}
normalize_script :: proc(script: Comic_Script) -> Comic_Script {
normalized := script
if len(normalized.title) == 0 {
normalized.title = fmt.aprintf("Untitled Comic")
}
if len(normalized.synopsis) == 0 {
normalized.synopsis = fmt.aprintf("Generated comic synopsis")
}
for idx in 0..<len(normalized.characters) {
if len(normalized.characters[idx].id) == 0 {
normalized.characters[idx].id = fmt.aprintf("char_%03d", idx+1)
}
if len(normalized.characters[idx].name) == 0 {
normalized.characters[idx].name = fmt.aprintf("Character %d", idx+1)
}
if normalized.characters[idx].seed == 0 {
normalized.characters[idx].seed = derive_seed_from_string(normalized.characters[idx].name)
}
}
for page_idx in 0..<len(normalized.pages) {
if normalized.pages[page_idx].page_number <= 0 {
normalized.pages[page_idx].page_number = page_idx + 1
}
for panel_idx in 0..<len(normalized.pages[page_idx].panels) {
if len(normalized.pages[page_idx].panels[panel_idx].panel_id) == 0 {
normalized.pages[page_idx].panels[panel_idx].panel_id = fmt.aprintf("panel_%03d_%03d", page_idx+1, panel_idx+1)
}
if normalized.pages[page_idx].panels[panel_idx].panel_number <= 0 {
normalized.pages[page_idx].panels[panel_idx].panel_number = panel_idx + 1
}
}
}
return normalized
}
+51
View File
@@ -0,0 +1,51 @@
package core
new_initial_state :: proc() -> Comic_State {
iso := ""
return Comic_State{
project = Project_Metadata{
project_id = "proj_todo",
project_name = "Untitled Comic",
created_at_iso = iso,
last_modified_iso = iso,
},
user_mode = .Casual,
story_idea = "",
story_genre = "action",
target_audience = "general",
art_style = "manga",
export_format = .PDF,
page_size = .A4,
color_profile = .RGB,
workflow = Workflow_State{
current_step = .Story_Input,
completed_steps = nil,
is_generating = false,
generation_progress = 0,
error_message = "",
},
panel_images = nil,
panel_errors = nil,
speech_bubbles = nil,
}
}
set_workflow_step :: proc(state: ^Comic_State, step: Workflow_Step) {
state.workflow.current_step = step
for s in state.workflow.completed_steps {
if s == step {
return
}
}
old_steps := state.workflow.completed_steps
steps: [dynamic]Workflow_Step
for s in old_steps {
append(&steps, s)
}
append(&steps, step)
state.workflow.completed_steps = steps[:]
delete(old_steps)
}
+251
View File
@@ -0,0 +1,251 @@
package core
User_Mode :: enum {
Casual,
Professional,
}
Workflow_Step :: enum {
Story_Input,
Generating_Script,
Script_Review,
Character_Setup,
Generating_Panels,
Layout,
Speech_Bubbles,
Complete,
}
Export_Format :: enum {
PDF,
CBZ,
PNG,
}
Page_Size_Name :: enum {
A4,
Letter,
Manga,
Webtoon,
Square,
}
Color_Profile :: enum {
RGB,
CMYK,
}
Character_Role :: enum {
Protagonist,
Antagonist,
Supporting,
Extra,
}
Shot_Type :: enum {
Establishing,
Wide,
Medium,
Close_Up,
Extreme_Close_Up,
Over_Shoulder,
Aerial,
}
Layout_Type :: enum {
Grid,
Manga,
Western,
Action,
Dialogue,
Splash,
}
Bubble_Type :: enum {
Normal,
Thought,
Shout,
Whisper,
Narration,
Sound_Effect,
}
Transition_Type :: enum {
None,
Fade,
Wipe,
Dissolve,
Action_Lines,
}
Position :: struct {
x: f32,
y: f32,
}
Size :: struct {
width: f32,
height: f32,
}
Character_Prompt_Template :: struct {
age: string,
gender: string,
hair_color: string,
hair_style: string,
skin_tone: string,
eye_color: string,
body_type: string,
outfit: string,
accessories: string,
distinguishing_features: string,
}
Color_Palette :: struct {
hair: string,
eyes: string,
skin: string,
outfit: string,
}
Character :: struct {
id: string,
name: string,
role: Character_Role,
description: string,
prompt_template: Character_Prompt_Template,
reference_image_url: string,
character_sheet_urls: []string,
seed: i64,
color_palette: Color_Palette,
appearance_count: int,
first_appearance_panel: string,
}
Dialogue :: struct {
speaker_id: string,
text: string,
bubble_type: Bubble_Type,
emotion: string,
}
Panel :: struct {
panel_id: string,
panel_number: int,
shot_type: Shot_Type,
description: string,
characters_present: []string,
dialogue: []Dialogue,
caption: string,
sound_effects: []string,
transition_from_previous: Transition_Type,
}
Page :: struct {
page_number: int,
layout_type: Layout_Type,
panels: []Panel,
}
Comic_Script :: struct {
title: string,
synopsis: string,
characters: []Character,
pages: []Page,
}
Panel_Image :: struct {
url: string,
width: int,
height: int,
seed: i64,
prompt: string,
}
Layout_Cell :: struct {
x: f32,
y: f32,
w: f32,
h: f32,
}
Layout_Pattern :: struct {
id: string,
name: string,
description: string,
genres: []string,
max_panels: int,
cells: []Layout_Cell,
}
Page_Layout_Panel :: struct {
panel_id: string,
panel_number: int,
layout_cell: Layout_Cell,
}
Page_Layout :: struct {
page_number: int,
pattern_id: string,
panels: []Page_Layout_Panel,
width: int,
height: int,
}
Speech_Bubble_Style :: struct {
background_color: string,
border_color: string,
border_width: f32,
border_radius: f32,
font_family: string,
font_size: f32,
text_color: string,
padding: f32,
}
Speech_Bubble :: struct {
id: string,
panel_id: string,
type: Bubble_Type,
text: string,
position: Position,
size: Size,
tail_direction: string,
tail_target: Position,
style: Speech_Bubble_Style,
speaker_id: string,
}
Project_Metadata :: struct {
project_id: string,
project_name: string,
created_at_iso: string,
last_modified_iso: string,
}
Workflow_State :: struct {
current_step: Workflow_Step,
completed_steps: []Workflow_Step,
is_generating: bool,
generation_progress: f32,
error_message: string,
}
Comic_State :: struct {
project: Project_Metadata,
user_mode: User_Mode,
story_idea: string,
story_genre: string,
target_audience: string,
art_style: string,
script: Comic_Script,
characters: []Character,
panel_images: map[string]Panel_Image,
panel_errors: map[string]string,
page_layouts: []Page_Layout,
speech_bubbles: map[string][]Speech_Bubble,
export_format: Export_Format,
page_size: Page_Size_Name,
color_profile: Color_Profile,
workflow: Workflow_State,
}
+24
View File
@@ -0,0 +1,24 @@
package core
can_transition :: proc(from, to: Workflow_Step) -> bool {
switch from {
case .Story_Input:
return to == .Generating_Script
case .Generating_Script:
return to == .Script_Review || to == .Story_Input
case .Script_Review:
return to == .Character_Setup
case .Character_Setup:
return to == .Generating_Panels
case .Generating_Panels:
return to == .Layout
case .Layout:
return to == .Speech_Bubbles
case .Speech_Bubbles:
return to == .Complete
case .Complete:
return to == .Story_Input
}
return false
}