check point
This commit is contained in:
1 parent
ae1cae967b
commit
1e85df5193
72 files changed
+11452
No files matched your search
@@ -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[:]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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[:]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user