add to gitignore
@ -23,6 +23,21 @@ cd odin
|
||||
./bin/comic_odin status # Quick status check
|
||||
```
|
||||
|
||||
### First-time setup
|
||||
|
||||
This project uses git submodules for vendored dependencies:
|
||||
|
||||
- `vendor/clay`
|
||||
- `vendor/osdialog`
|
||||
|
||||
If you cloned without submodules, initialize them once from the repository root:
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
`./build.sh` will also try to initialize missing submodules automatically.
|
||||
|
||||
## Building
|
||||
|
||||
Requires [Odin](https://odin-lang.org/) and [Raylib](https://www.raylib.com/).
|
||||
|
||||
BIN
odin/assets/1Mb0-VrUFf4OcLyfN_uSs.jpg
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
odin/assets/1o9DDzvxdOWyWrapdz9O3.jpg
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
odin/assets/CTG3-vZla0nhBdJiGjLqX.jpg
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
odin/assets/M6pgwRDegI9ljFNcpSfXT.jpg
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
odin/assets/Pg45i32bk1FZQ1s0eNfSL.jpg
Normal file
|
After Width: | Height: | Size: 319 KiB |
BIN
odin/assets/_kY-5nug4C8BuB0ky8hMy.jpg
Normal file
|
After Width: | Height: | Size: 471 KiB |
BIN
odin/assets/c3nx--nACNKoh93CiqFaH.jpg
Normal file
|
After Width: | Height: | Size: 496 KiB |
BIN
odin/assets/eLE_UREYZMOqyNQY8lI6O.jpg
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
odin/assets/edited_panel_001.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
odin/assets/edited_panel_003.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
odin/assets/vkfWvSFBC7oPpBFcoh4KB.jpg
Normal file
|
After Width: | Height: | Size: 269 KiB |
BIN
odin/assets/w5dBNU8TZztqbwp3tYQP3.jpg
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
odin/assets/w8FjnlmAMgXN1MzeYeH3D.jpg
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
odin/assets/z2xfTsYSzt6_dM4EHLTy3.jpg
Normal file
|
After Width: | Height: | Size: 259 KiB |
@ -2,9 +2,30 @@
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
CLAY_DIR="$SCRIPT_DIR/vendor/clay/bindings/odin/clay-odin"
|
||||
BUILD_DIR="$SCRIPT_DIR/build"
|
||||
|
||||
ensure_clay_submodule() {
|
||||
if [ -d "$CLAY_DIR" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Clay bindings not found at: $CLAY_DIR"
|
||||
echo "Attempting to initialize git submodules..."
|
||||
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
git -C "$REPO_ROOT" submodule update --init --recursive odin/vendor/clay odin/vendor/osdialog || true
|
||||
fi
|
||||
|
||||
if [ ! -d "$CLAY_DIR" ]; then
|
||||
echo "error: clay submodule is missing. Run: git submodule update --init --recursive" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_clay_submodule
|
||||
|
||||
# Build C dependencies
|
||||
"$SCRIPT_DIR/build_osdialog.sh"
|
||||
|
||||
|
||||
@ -1,11 +1,33 @@
|
||||
#!/bin/bash
|
||||
# Build osdialog static library for Linux (zenity backend)
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
OSDIALOG_DIR="$(dirname "$0")/vendor/osdialog"
|
||||
OUTPUT_DIR="$(dirname "$0")/build"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
OSDIALOG_DIR="$SCRIPT_DIR/vendor/osdialog"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/build"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
ensure_osdialog_submodule() {
|
||||
if [ -f "$OSDIALOG_DIR/osdialog.c" ] && [ -f "$OSDIALOG_DIR/osdialog_zenity.c" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "osdialog sources not found at: $OSDIALOG_DIR"
|
||||
echo "Attempting to initialize git submodules..."
|
||||
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
git -C "$REPO_ROOT" submodule update --init --recursive odin/vendor/osdialog || true
|
||||
fi
|
||||
|
||||
if [ ! -f "$OSDIALOG_DIR/osdialog.c" ] || [ ! -f "$OSDIALOG_DIR/osdialog_zenity.c" ]; then
|
||||
echo "error: osdialog submodule is missing. Run: git submodule update --init --recursive" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_osdialog_submodule
|
||||
|
||||
LIB_OUT="$OUTPUT_DIR/libosdialog.a"
|
||||
|
||||
# Only rebuild if sources changed
|
||||
@ -30,6 +52,11 @@ for src in "${SOURCES[@]}"; do
|
||||
gcc -c -O2 -std=c99 -I"$OSDIALOG_DIR" "$src" -o "$obj"
|
||||
done
|
||||
|
||||
ar rcs "$LIB_OUT" "$OUTPUT_DIR"/osdialog.o "$OUTPUT_DIR"/osdialog_zenity.o
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
libtool -static -o "$LIB_OUT" "$OUTPUT_DIR"/osdialog.o "$OUTPUT_DIR"/osdialog_zenity.o
|
||||
else
|
||||
ar rcs "$LIB_OUT" "$OUTPUT_DIR"/osdialog.o "$OUTPUT_DIR"/osdialog_zenity.o
|
||||
fi
|
||||
|
||||
echo " AR $LIB_OUT"
|
||||
echo "osdialog build complete."
|
||||
|
||||
BIN
odin/comic.pdf
@ -9,19 +9,19 @@
|
||||
"last_modified_iso": ""
|
||||
},
|
||||
"user_mode": 0,
|
||||
"story_idea": "car race in downton newyork",
|
||||
"story_idea": "2 ball roling down the street",
|
||||
"story_genre": "scifi",
|
||||
"target_audience": "general",
|
||||
"art_style": "noir",
|
||||
"target_audience": "mature",
|
||||
"art_style": "watercolor",
|
||||
"script": {
|
||||
"title": "Neon Chase",
|
||||
"synopsis": "In a rain-soaked, neon-lit downtown New York of the future, two rival street racers, Kael and Mira, engage in a high-stakes duel through the canyons of skyscrapers. The race is a blur of glowing tire tracks and clashing headlights, ending in a dramatic crash that leaves only one victor standing beneath the flickering signs.",
|
||||
"title": "Orbital Descent",
|
||||
"synopsis": "Two mysterious metallic spheres roll down a rain-slicked city street, leaving trails of glowing energy. As they move, the environment warps and distorts around them, revealing hidden dimensions and cryptic symbols. The spheres seem to be searching for something, or someone, as they navigate the neon-lit urban landscape.",
|
||||
"characters": [
|
||||
{
|
||||
"id": "char_001",
|
||||
"name": "Kael",
|
||||
"name": "Sphere Alpha",
|
||||
"role": 0,
|
||||
"description": "Mid-20s male, sharp angular face, short black hair slicked back, pale skin, piercing blue eyes, lean athletic build. Wears a worn leather jacket with metallic accents, black jeans, and combat boots. Has a cybernetic implant glowing faintly on his temple. First appearance in panel_001.",
|
||||
"description": "A perfectly smooth, chrome-colored metallic sphere about the size of a bowling ball. It has a subtle, pulsing blue glow emanating from within, and its surface reflects the surrounding city lights like a mirror. No visible seams or markings, but faint geometric patterns appear on its surface when it emits energy.",
|
||||
"prompt_template": {
|
||||
"age": "",
|
||||
"gender": "",
|
||||
@ -38,7 +38,7 @@
|
||||
"character_sheet_urls": [
|
||||
|
||||
],
|
||||
"seed": 2330781,
|
||||
"seed": 1158036661,
|
||||
"color_palette": {
|
||||
"hair": "",
|
||||
"eyes": "",
|
||||
@ -50,9 +50,9 @@
|
||||
},
|
||||
{
|
||||
"id": "char_002",
|
||||
"name": "Mira",
|
||||
"role": 1,
|
||||
"description": "Late 20s female, long silver hair tied in a high ponytail, dark skin, intense green eyes, tall and muscular. Wears a sleek black racing suit with red neon piping and a visor that obscures her eyes. Has a scar running from her left cheek to jaw. First appearance in panel_002.",
|
||||
"name": "Sphere Beta",
|
||||
"role": 0,
|
||||
"description": "Identical in size and shape to Sphere Alpha, but with a warm golden glow instead of blue. Its surface is slightly textured with fine, almost invisible lines that spiral around the sphere. It moves in a synchronized but slightly offset path relative to Alpha.",
|
||||
"prompt_template": {
|
||||
"age": "",
|
||||
"gender": "",
|
||||
@ -69,7 +69,7 @@
|
||||
"character_sheet_urls": [
|
||||
|
||||
],
|
||||
"seed": 2398443,
|
||||
"seed": 1699900829,
|
||||
"color_palette": {
|
||||
"hair": "",
|
||||
"eyes": "",
|
||||
@ -77,29 +77,29 @@
|
||||
"outfit": ""
|
||||
},
|
||||
"appearance_count": 0,
|
||||
"first_appearance_panel": "panel_002"
|
||||
"first_appearance_panel": "panel_001"
|
||||
}
|
||||
],
|
||||
"pages": [
|
||||
{
|
||||
"page_number": 1,
|
||||
"layout_type": 2,
|
||||
"layout_type": 1,
|
||||
"panels": [
|
||||
{
|
||||
"panel_id": "panel_001",
|
||||
"panel_number": 1,
|
||||
"shot_type": 0,
|
||||
"description": "Noir-style rain-slicked downtown New York at night, towering skyscrapers with flickering neon signs in cyan and magenta. A lone figure, Kael, stands beside his sleek black hovercar, its engine glowing blue. The scene is drenched in shadows and reflective puddles, with steam rising from grates. Art style: high contrast, chiaroscuro, grain texture.",
|
||||
"description": "A wide, watercolor-style establishing shot of a rain-soaked futuristic city street at night. Neon signs in electric blue and pink reflect off wet asphalt. The street is empty except for two metallic spheres rolling in the distance, leaving trails of glowing light. Soft, diffused lighting with a moody, mysterious atmosphere. Watercolor washes create a dreamlike quality with blurred edges.",
|
||||
"characters_present": [
|
||||
"char_001"
|
||||
"char_001",
|
||||
"char_002"
|
||||
],
|
||||
"dialogue": [
|
||||
|
||||
],
|
||||
"caption": "Downtown New York, 2147. The city never sleeps, and neither do its demons.",
|
||||
"caption": "In the neon-drenched streets of Neo-Tokyo, two orbs begin their silent journey.",
|
||||
"sound_effects": [
|
||||
"drip...",
|
||||
"hummm"
|
||||
"Pitter-patter"
|
||||
],
|
||||
"transition_from_previous": 0
|
||||
},
|
||||
@ -107,71 +107,60 @@
|
||||
"panel_id": "panel_002",
|
||||
"panel_number": 2,
|
||||
"shot_type": 2,
|
||||
"description": "Mira inside her red hovercar, hands on the wheel, visor glowing. Rain streaks down the windshield. Her eyes are intense behind the visor. Low angle, harsh shadows from dashboard lights. Art style: film noir, dramatic sidelighting, sharp focus.",
|
||||
"description": "A medium shot of the two spheres rolling side by side, close-up. Watercolor style with vivid colors. Sphere Alpha emits a blue pulse that distorts the air around it, creating a ripple effect. Sphere Beta leaves a golden trail that seems to hang in the air. The background is a blur of rain and neon lights, with watercolor splashes suggesting movement.",
|
||||
"characters_present": [
|
||||
"char_001",
|
||||
"char_002"
|
||||
],
|
||||
"dialogue": [
|
||||
{
|
||||
"speaker_id": "char_002",
|
||||
"text": "Ready to lose, Kael?",
|
||||
"bubble_type": 0,
|
||||
"emotion": 5
|
||||
}
|
||||
|
||||
],
|
||||
"caption": "",
|
||||
"sound_effects": [
|
||||
|
||||
],
|
||||
"transition_from_previous": 3
|
||||
"transition_from_previous": 4
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_003",
|
||||
"panel_number": 3,
|
||||
"shot_type": 3,
|
||||
"description": "Kael's face, half-lit by neon, a faint smirk. Raindrops cling to his skin. Cybernetic implant glows. Extreme close-up on his eyes, reflecting the city lights. Art style: high contrast, shallow depth of field, noir grain.",
|
||||
"shot_type": 5,
|
||||
"description": "Over-shoulder shot from behind Sphere Alpha, looking down the street. The perspective emphasizes the spheres' small size against the towering skyscrapers. Watercolor technique with soft edges and muted tones, but the spheres' glow stands out in stark contrast. Shadows are deep and inky, suggesting a gritty, mature atmosphere.",
|
||||
"characters_present": [
|
||||
"char_001"
|
||||
"char_001",
|
||||
"char_002"
|
||||
],
|
||||
"dialogue": [
|
||||
{
|
||||
"speaker_id": "char_001",
|
||||
"text": "I was born ready.",
|
||||
"bubble_type": 0,
|
||||
"emotion": 4
|
||||
}
|
||||
|
||||
],
|
||||
"caption": "",
|
||||
"sound_effects": [
|
||||
|
||||
],
|
||||
"transition_from_previous": 4
|
||||
"transition_from_previous": 2
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_004",
|
||||
"panel_number": 4,
|
||||
"shot_type": 1,
|
||||
"description": "Two hovercars side by side at a starting line, rain pouring, neon lights reflecting on their glossy surfaces. Kael's black car and Mira's red car. In the background, a holographic countdown flickers. Art style: noir, desaturated colors with neon pops, motion blur.",
|
||||
"shot_type": 3,
|
||||
"description": "Close-up of Sphere Alpha's surface, revealing intricate geometric patterns that appear like glowing circuit boards. Watercolor style with detailed brushwork, showing the reflection of a neon sign in the chrome surface. The pattern seems to pulse with energy, and faint symbols are visible beneath the surface.",
|
||||
"characters_present": [
|
||||
"char_001",
|
||||
"char_002"
|
||||
"char_001"
|
||||
],
|
||||
"dialogue": [
|
||||
|
||||
],
|
||||
"caption": "",
|
||||
"sound_effects": [
|
||||
"3...",
|
||||
"2...",
|
||||
"1..."
|
||||
"Hum..."
|
||||
],
|
||||
"transition_from_previous": 1
|
||||
"transition_from_previous": 3
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_005",
|
||||
"panel_number": 5,
|
||||
"shot_type": 6,
|
||||
"description": "Top-down view of the two cars launching from the start, leaving trails of light. The city street stretches ahead, lined with glowing billboards. Raindrops blur the lens. Art style: high contrast, neon trails, noir atmosphere.",
|
||||
"shot_type": 1,
|
||||
"description": "Wide shot of the spheres rolling past a graffiti-covered wall. The graffiti seems to react to their presence, glowing and shifting into alien symbols. Watercolor with heavy, dark washes for the wall, contrasting with the bright trails of the spheres. Rain streaks down the wall, creating a sense of motion.",
|
||||
"characters_present": [
|
||||
"char_001",
|
||||
"char_002"
|
||||
@ -179,12 +168,28 @@
|
||||
"dialogue": [
|
||||
|
||||
],
|
||||
"caption": "",
|
||||
"caption": "The city responds to their passage.",
|
||||
"sound_effects": [
|
||||
"VROOM!",
|
||||
"SCREECH!"
|
||||
|
||||
],
|
||||
"transition_from_previous": 4
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_006",
|
||||
"panel_number": 6,
|
||||
"shot_type": 4,
|
||||
"description": "Extreme close-up of Sphere Beta, focusing on the spiral lines on its surface. The lines seem to move, creating a hypnotic effect. Watercolor style with fine, delicate lines and a golden glow that bleeds into the surrounding area. The background is a soft blur of rain and light.",
|
||||
"characters_present": [
|
||||
"char_002"
|
||||
],
|
||||
"dialogue": [
|
||||
|
||||
],
|
||||
"caption": "",
|
||||
"sound_effects": [
|
||||
|
||||
],
|
||||
"transition_from_previous": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -192,34 +197,11 @@
|
||||
"page_number": 2,
|
||||
"layout_type": 3,
|
||||
"panels": [
|
||||
{
|
||||
"panel_id": "panel_006",
|
||||
"panel_number": 1,
|
||||
"shot_type": 5,
|
||||
"description": "Over Kael's shoulder, view through his windshield: Mira's car ahead, sparks flying as it scrapes a barrier. Rain and neon blur. Art style: motion blur, high contrast, noir lighting.",
|
||||
"characters_present": [
|
||||
"char_001",
|
||||
"char_002"
|
||||
],
|
||||
"dialogue": [
|
||||
{
|
||||
"speaker_id": "char_001",
|
||||
"text": "She's pushing too hard.",
|
||||
"bubble_type": 1,
|
||||
"emotion": 4
|
||||
}
|
||||
],
|
||||
"caption": "",
|
||||
"sound_effects": [
|
||||
"CRUNCH!"
|
||||
],
|
||||
"transition_from_previous": 4
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_007",
|
||||
"panel_number": 2,
|
||||
"shot_type": 1,
|
||||
"description": "Cars racing through a narrow alley, walls on both sides, neon signs flickering. Water splashes. Kael's car tries to overtake on the inside. Art style: dramatic shadows, dynamic angle, noir grain.",
|
||||
"panel_number": 1,
|
||||
"shot_type": 6,
|
||||
"description": "Aerial view of the street, looking down at the two spheres as they approach a crosswalk. Their trails form intricate patterns that resemble a circuit diagram. Watercolor style with a bird's-eye perspective, showing the city layout below. The rain creates a soft, blurred effect on the scene.",
|
||||
"characters_present": [
|
||||
"char_001",
|
||||
"char_002"
|
||||
@ -229,38 +211,33 @@
|
||||
],
|
||||
"caption": "",
|
||||
"sound_effects": [
|
||||
"ROAR!",
|
||||
"SPLASH!"
|
||||
|
||||
],
|
||||
"transition_from_previous": 4
|
||||
"transition_from_previous": 0
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_008",
|
||||
"panel_number": 3,
|
||||
"shot_type": 3,
|
||||
"description": "Mira's hand gripping the gear shift, knuckles white, visor reflecting the chaos. Sweat on her brow. Art style: extreme close-up, high contrast, noir shadows.",
|
||||
"panel_number": 2,
|
||||
"shot_type": 2,
|
||||
"description": "Medium shot of the spheres stopping at the crosswalk. The traffic light above them flickers and changes from red to blue. Watercolor with sharp contrasts: the red light casts a warm glow on the wet street, while the spheres' blue and gold lights add cool tones. The atmosphere is tense and expectant.",
|
||||
"characters_present": [
|
||||
"char_001",
|
||||
"char_002"
|
||||
],
|
||||
"dialogue": [
|
||||
{
|
||||
"speaker_id": "char_002",
|
||||
"text": "Not today!",
|
||||
"bubble_type": 2,
|
||||
"emotion": 2
|
||||
}
|
||||
],
|
||||
"caption": "",
|
||||
"sound_effects": [
|
||||
|
||||
],
|
||||
"caption": "They pause. As if waiting.",
|
||||
"sound_effects": [
|
||||
"Click..."
|
||||
],
|
||||
"transition_from_previous": 4
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_009",
|
||||
"panel_number": 4,
|
||||
"shot_type": 2,
|
||||
"description": "Kael's car sideswipes Mira's, sparks flying, metal screeching. Both cars spin. Rain illuminated by headlights. Art style: action lines, high contrast, noir grit.",
|
||||
"panel_number": 3,
|
||||
"shot_type": 1,
|
||||
"description": "Wide shot as the spheres roll across the crosswalk. The lines of the crosswalk begin to glow and shift, forming a portal of light beneath them. Watercolor style with vibrant, surreal colors: the portal is a swirl of blue and gold, with the city around it fading into dark washes. The spheres are silhouetted against the bright portal.",
|
||||
"characters_present": [
|
||||
"char_001",
|
||||
"char_002"
|
||||
@ -268,68 +245,60 @@
|
||||
"dialogue": [
|
||||
|
||||
],
|
||||
"caption": "",
|
||||
"caption": "The fabric of reality tears.",
|
||||
"sound_effects": [
|
||||
"CRASH!",
|
||||
"SCREECH!"
|
||||
"WHOOOSH!"
|
||||
],
|
||||
"transition_from_previous": 4
|
||||
"transition_from_previous": 2
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_010",
|
||||
"panel_number": 5,
|
||||
"shot_type": 4,
|
||||
"description": "Mira's visor cracks, her eye visible, wide with shock. A single tear mixes with blood. Art style: extreme close-up, noir chiaroscuro, shallow depth of field.",
|
||||
"panel_number": 4,
|
||||
"shot_type": 3,
|
||||
"description": "Close-up of Sphere Alpha as it enters the portal. Half of it is already through, and the surface is distorting, showing glimpses of another dimension: a starry void with floating geometric shapes. Watercolor with a dreamlike quality, blending the sphere's metallic surface with the cosmic background. The edges are soft, suggesting transition.",
|
||||
"characters_present": [
|
||||
"char_002"
|
||||
"char_001"
|
||||
],
|
||||
"dialogue": [
|
||||
|
||||
],
|
||||
"caption": "",
|
||||
"sound_effects": [
|
||||
"CRACK!"
|
||||
"ZAP!"
|
||||
],
|
||||
"transition_from_previous": 4
|
||||
"transition_from_previous": 3
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_011",
|
||||
"panel_number": 6,
|
||||
"shot_type": 1,
|
||||
"description": "Mira's car crashes into a neon sign, exploding in a shower of sparks and glass. Kael's car skids to a halt nearby. The scene is lit by the fire. Art style: dramatic lighting, noir silhouette, high contrast.",
|
||||
"panel_number": 5,
|
||||
"shot_type": 2,
|
||||
"description": "Medium shot of Sphere Beta following Alpha into the portal. The golden glow intensifies, and the portal begins to close. Watercolor with dramatic lighting: the portal's light casts long shadows, and the rain appears as streaks of gold and blue. The city behind them is fading into darkness.",
|
||||
"characters_present": [
|
||||
"char_001",
|
||||
"char_002"
|
||||
],
|
||||
"dialogue": [
|
||||
|
||||
],
|
||||
"caption": "The race ends in flames, but the city remembers.",
|
||||
"caption": "And then, as suddenly as they appeared, they are gone.",
|
||||
"sound_effects": [
|
||||
"BOOM!",
|
||||
"FZZZZ"
|
||||
"FWOOM!"
|
||||
],
|
||||
"transition_from_previous": 4
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_012",
|
||||
"panel_number": 7,
|
||||
"shot_type": 3,
|
||||
"description": "Kael steps out of his car, rain pouring, his face unreadable. The fire reflects in his eyes. He looks down. Art style: noir, high contrast, rain streaks, focus on expression.",
|
||||
"panel_number": 6,
|
||||
"shot_type": 0,
|
||||
"description": "Final establishing shot of the empty street after the spheres have vanished. The rain continues to fall, but the neon lights seem dimmer. The only evidence of their passage is a faint residual glow on the asphalt and a few lingering symbols on the graffiti wall. Watercolor with melancholy tones: deep blues and grays, with a sense of loss. The mood is quiet and contemplative.",
|
||||
"characters_present": [
|
||||
"char_001"
|
||||
|
||||
],
|
||||
"dialogue": [
|
||||
{
|
||||
"speaker_id": "char_001",
|
||||
"text": "Another ghost for the city.",
|
||||
"bubble_type": 0,
|
||||
"emotion": 4
|
||||
}
|
||||
],
|
||||
"caption": "",
|
||||
"sound_effects": [
|
||||
|
||||
],
|
||||
"caption": "The city returns to its slumber. But something has changed.",
|
||||
"sound_effects": [
|
||||
"Pitter-patter"
|
||||
],
|
||||
"transition_from_previous": 1
|
||||
}
|
||||
@ -340,30 +309,30 @@
|
||||
"characters": [
|
||||
{
|
||||
"id": "char_001",
|
||||
"name": "Kael",
|
||||
"name": "Sphere Alpha",
|
||||
"role": 0,
|
||||
"description": "Mid-20s male, sharp angular face, short black hair slicked back, pale skin, piercing blue eyes, lean athletic build. Wears a worn leather jacket with metallic accents, black jeans, and combat boots. Has a cybernetic implant glowing faintly on his temple. First appearance in panel_001.",
|
||||
"description": "A perfectly smooth, chrome-colored metallic sphere about the size of a bowling ball. It has a subtle, pulsing blue glow emanating from within, and its surface reflects the surrounding city lights like a mirror. No visible seams or markings, but faint geometric patterns appear on its surface when it emits energy.",
|
||||
"prompt_template": {
|
||||
"age": "",
|
||||
"gender": "female",
|
||||
"hair_color": "black",
|
||||
"hair_style": "short",
|
||||
"skin_tone": "pale",
|
||||
"hair_color": "red",
|
||||
"hair_style": "",
|
||||
"skin_tone": "light",
|
||||
"eye_color": "blue",
|
||||
"body_type": "athletic",
|
||||
"body_type": "thin",
|
||||
"outfit": "",
|
||||
"accessories": "",
|
||||
"distinguishing_features": "piercing"
|
||||
"accessories": "bow",
|
||||
"distinguishing_features": ""
|
||||
},
|
||||
"reference_image_url": "https://v3b.fal.media/files/b/0a9c87e0/XDP-j987_G5EihEbcXAnd.jpg",
|
||||
"reference_image_url": "https://v3b.fal.media/files/b/0a9cfb84/mG2jkWTuIPlwpzbJ9R7ie.jpg",
|
||||
"character_sheet_urls": [
|
||||
|
||||
],
|
||||
"seed": 2330781,
|
||||
"seed": 1158036661,
|
||||
"color_palette": {
|
||||
"hair": "black",
|
||||
"hair": "red",
|
||||
"eyes": "blue",
|
||||
"skin": "pale",
|
||||
"skin": "light",
|
||||
"outfit": ""
|
||||
},
|
||||
"appearance_count": 0,
|
||||
@ -371,141 +340,276 @@
|
||||
},
|
||||
{
|
||||
"id": "char_002",
|
||||
"name": "Mira",
|
||||
"role": 1,
|
||||
"description": "Late 20s female, long silver hair tied in a high ponytail, dark skin, intense green eyes, tall and muscular. Wears a sleek black racing suit with red neon piping and a visor that obscures her eyes. Has a scar running from her left cheek to jaw. First appearance in panel_002.",
|
||||
"name": "Sphere Beta",
|
||||
"role": 0,
|
||||
"description": "Identical in size and shape to Sphere Alpha, but with a warm golden glow instead of blue. Its surface is slightly textured with fine, almost invisible lines that spiral around the sphere. It moves in a synchronized but slightly offset path relative to Alpha.",
|
||||
"prompt_template": {
|
||||
"age": "",
|
||||
"age": "old",
|
||||
"gender": "female",
|
||||
"hair_color": "black",
|
||||
"hair_style": "long",
|
||||
"skin_tone": "dark",
|
||||
"eye_color": "green",
|
||||
"body_type": "muscular",
|
||||
"hair_color": "red",
|
||||
"hair_style": "",
|
||||
"skin_tone": "light",
|
||||
"eye_color": "blue",
|
||||
"body_type": "",
|
||||
"outfit": "",
|
||||
"accessories": "hat, tie",
|
||||
"distinguishing_features": "scar"
|
||||
"accessories": "hat",
|
||||
"distinguishing_features": ""
|
||||
},
|
||||
"reference_image_url": "https://v3b.fal.media/files/b/0a9c87e0/DOuHDHGLrUt5SU55BSxr3.jpg",
|
||||
"reference_image_url": "https://v3b.fal.media/files/b/0a9cfb89/Xn1bnhoadG4S0iIIB3vTz.jpg",
|
||||
"character_sheet_urls": [
|
||||
|
||||
],
|
||||
"seed": 2398443,
|
||||
"seed": 1699900829,
|
||||
"color_palette": {
|
||||
"hair": "black",
|
||||
"eyes": "green",
|
||||
"skin": "dark",
|
||||
"hair": "red",
|
||||
"eyes": "blue",
|
||||
"skin": "light",
|
||||
"outfit": ""
|
||||
},
|
||||
"appearance_count": 0,
|
||||
"first_appearance_panel": "panel_002"
|
||||
"first_appearance_panel": "panel_001"
|
||||
}
|
||||
],
|
||||
"panel_images": {
|
||||
"panel_005": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e4/GNmUI2C5p0R_ccYiDMqmZ.jpg",
|
||||
"width": 1344,
|
||||
"height": 768,
|
||||
"seed": 441640662,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Top-down view of the two cars launching from the start, leaving trails of light. The city street stretches ahead, lined with glowing billboards. Raindrops blur the lens. Art style: high contrast, neon trails, noir atmosphere.. Characters: female with black short hair, blue eyes, pale skin, athletic build, wearing female with black long hair, green eyes, dark skin, muscular build, wearing . Aerial shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_012": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e7/AYl-gjMRO5819nzjlUk0T.jpg",
|
||||
"width": 896,
|
||||
"height": 1152,
|
||||
"seed": 259423944,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Kael steps out of his car, rain pouring, his face unreadable. The fire reflects in his eyes. He looks down. Art style: noir, high contrast, rain streaks, focus on expression.. Characters: female with black short hair, blue eyes, pale skin, athletic build, wearing . Close_Up shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_003": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e3/MBudts4OSVnzvQbdOjebW.jpg",
|
||||
"width": 896,
|
||||
"height": 1152,
|
||||
"seed": 1142705242,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Kael's face, half-lit by neon, a faint smirk. Raindrops cling to his skin. Cybernetic implant glows. Extreme close-up on his eyes, reflecting the city lights. Art style: high contrast, shallow depth of field, noir grain.. Characters: female with black short hair, blue eyes, pale skin, athletic build, wearing . Close_Up shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_004": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e3/R8XTRdbBauVsOoHJ-YQc1.jpg",
|
||||
"width": 1344,
|
||||
"height": 768,
|
||||
"seed": 1355310696,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Two hovercars side by side at a starting line, rain pouring, neon lights reflecting on their glossy surfaces. Kael's black car and Mira's red car. In the background, a holographic countdown flickers. Art style: noir, desaturated colors with neon pops, motion blur.. Characters: female with black short hair, blue eyes, pale skin, athletic build, wearing female with black long hair, green eyes, dark skin, muscular build, wearing . Wide shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_009": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e6/fvMS92PXsUhEB9kZCQoEs.jpg",
|
||||
"width": 1152,
|
||||
"height": 896,
|
||||
"seed": 1355310701,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Kael's car sideswipes Mira's, sparks flying, metal screeching. Both cars spin. Rain illuminated by headlights. Art style: action lines, high contrast, noir grit.. Characters: female with black short hair, blue eyes, pale skin, athletic build, wearing female with black long hair, green eyes, dark skin, muscular build, wearing . Medium shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_002": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e2/Arf3z-7Bb8xsuQmMtsnJn.jpg",
|
||||
"width": 1152,
|
||||
"height": 896,
|
||||
"seed": 654246116,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Mira inside her red hovercar, hands on the wheel, visor glowing. Rain streaks down the windshield. Her eyes are intense behind the visor. Low angle, harsh shadows from dashboard lights. Art style: film noir, dramatic sidelighting, sharp focus.. Characters: female with black long hair, green eyes, dark skin, muscular build, wearing . Medium shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_007": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e5/ghngnHy3pfsS5-EoKe_7-.jpg",
|
||||
"width": 1344,
|
||||
"height": 768,
|
||||
"seed": 654246121,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Cars racing through a narrow alley, walls on both sides, neon signs flickering. Water splashes. Kael's car tries to overtake on the inside. Art style: dramatic shadows, dynamic angle, noir grain.. Characters: female with black short hair, blue eyes, pale skin, athletic build, wearing female with black long hair, green eyes, dark skin, muscular build, wearing . Wide shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_010": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e6/gHdzzCxIMw3o3h0Of0bHl.jpg",
|
||||
"panel_006": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9cfb8e/c3nx--nACNKoh93CiqFaH.jpg",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"seed": 441640636,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Mira's visor cracks, her eye visible, wide with shock. A single tear mixes with blood. Art style: extreme close-up, noir chiaroscuro, shallow depth of field.. Characters: female with black long hair, green eyes, dark skin, muscular build, wearing . Extreme_Close_Up shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
"seed": 2056375276,
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. Extreme close-up of Sphere Beta, focusing on the spiral lines on its surface. The lines seem to move, creating a hypnotic effect. Watercolor style with fine, delicate lines and a golden glow that bleeds into the surrounding area. The background is a soft blur of rain and light.. Characters: old female with red hair, blue eyes, light skin, build, wearing . Extreme_Close_Up shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_008": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e5/ZILfPOXF0iqES-uVU9hxT.jpg",
|
||||
"width": 896,
|
||||
"height": 1152,
|
||||
"seed": 1142705237,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Mira's hand gripping the gear shift, knuckles white, visor reflecting the chaos. Sweat on her brow. Art style: extreme close-up, high contrast, noir shadows.. Characters: female with black long hair, green eyes, dark skin, muscular build, wearing . Close_Up shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_006": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e4/FEm2NuaJTbIvsXv1lw9NJ.jpg",
|
||||
"width": 1152,
|
||||
"height": 896,
|
||||
"seed": 1843769817,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Over Kael's shoulder, view through his windshield: Mira's car ahead, sparks flying as it scrapes a barrier. Rain and neon blur. Art style: motion blur, high contrast, noir lighting.. Characters: female with black short hair, blue eyes, pale skin, athletic build, wearing female with black long hair, green eyes, dark skin, muscular build, wearing . Over_Shoulder shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_011": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e6/Gdee2-1s-GInm5y8wxbw2.jpg",
|
||||
"panel_012": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9cfb92/1Mb0-VrUFf4OcLyfN_uSs.jpg",
|
||||
"width": 1344,
|
||||
"height": 768,
|
||||
"seed": 2056375302,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Mira's car crashes into a neon sign, exploding in a shower of sparks and glass. Kael's car skids to a halt nearby. The scene is lit by the fire. Art style: dramatic lighting, noir silhouette, high contrast.. Characters: female with black short hair, blue eyes, pale skin, athletic build, wearing female with black long hair, green eyes, dark skin, muscular build, wearing . Wide shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
"seed": 2056375303,
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. Final establishing shot of the empty street after the spheres have vanished. The rain continues to fall, but the neon lights seem dimmer. The only evidence of their passage is a faint residual glow on the asphalt and a few lingering symbols on the graffiti wall. Watercolor with melancholy tones: deep blues and grays, with a sense of loss. The mood is quiet and contemplative.. Characters: . Establishing shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_001": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9c87e2/HTTktLBTq-PJrdpPFjt9e.jpg",
|
||||
"url": "file://./assets/edited_panel_001.png",
|
||||
"width": 1344,
|
||||
"height": 768,
|
||||
"seed": 1843769822,
|
||||
"prompt": "film noir style, high contrast black and white, dramatic chiaroscuro lighting, gritty, shadow-heavy, 1940s aesthetic comic panel. Noir-style rain-slicked downtown New York at night, towering skyscrapers with flickering neon signs in cyan and magenta. A lone figure, Kael, stands beside his sleek black hovercar, its engine glowing blue. The scene is drenched in shadows and reflective puddles, with steam rising from grates. Art style: high contrast, chiaroscuro, grain texture.. Characters: female with black short hair, blue eyes, pale skin, athletic build, wearing . Establishing shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. A wide, watercolor-style establishing shot of a rain-soaked futuristic city street at night. Neon signs in electric blue and pink reflect off wet asphalt. The street is empty except for two metallic spheres rolling in the distance, leaving trails of glowing light. Soft, diffused lighting with a moody, mysterious atmosphere. Watercolor washes create a dreamlike quality with blurred edges.. Characters: female with red hair, blue eyes, light skin, thin build, wearing old female with red hair, blue eyes, light skin, build, wearing . Establishing shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_007": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9cfb8f/M6pgwRDegI9ljFNcpSfXT.jpg",
|
||||
"width": 1344,
|
||||
"height": 768,
|
||||
"seed": 1843769816,
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. Aerial view of the street, looking down at the two spheres as they approach a crosswalk. Their trails form intricate patterns that resemble a circuit diagram. Watercolor style with a bird's-eye perspective, showing the city layout below. The rain creates a soft, blurred effect on the scene.. Characters: female with red hair, blue eyes, light skin, thin build, wearing old female with red hair, blue eyes, light skin, build, wearing . Aerial shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_002": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9cfb87/vkfWvSFBC7oPpBFcoh4KB.jpg",
|
||||
"width": 1152,
|
||||
"height": 896,
|
||||
"seed": 654246116,
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. A medium shot of the two spheres rolling side by side, close-up. Watercolor style with vivid colors. Sphere Alpha emits a blue pulse that distorts the air around it, creating a ripple effect. Sphere Beta leaves a golden trail that seems to hang in the air. The background is a blur of rain and neon lights, with watercolor splashes suggesting movement.. Characters: female with red hair, blue eyes, light skin, thin build, wearing old female with red hair, blue eyes, light skin, build, wearing . Medium shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_011": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9cfb92/Pg45i32bk1FZQ1s0eNfSL.jpg",
|
||||
"width": 1152,
|
||||
"height": 896,
|
||||
"seed": 441640635,
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. Medium shot of Sphere Beta following Alpha into the portal. The golden glow intensifies, and the portal begins to close. Watercolor with dramatic lighting: the portal's light casts long shadows, and the rain appears as streaks of gold and blue. The city behind them is fading into darkness.. Characters: old female with red hair, blue eyes, light skin, build, wearing . Medium shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_008": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9cfb8c/1o9DDzvxdOWyWrapdz9O3.jpg",
|
||||
"width": 1152,
|
||||
"height": 896,
|
||||
"seed": 654246122,
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. Medium shot of the spheres stopping at the crosswalk. The traffic light above them flickers and changes from red to blue. Watercolor with sharp contrasts: the red light casts a warm glow on the wet street, while the spheres' blue and gold lights add cool tones. The atmosphere is tense and expectant.. Characters: female with red hair, blue eyes, light skin, thin build, wearing old female with red hair, blue eyes, light skin, build, wearing . Medium shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_003": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9cfb88/eLE_UREYZMOqyNQY8lI6O.jpg",
|
||||
"width": 1152,
|
||||
"height": 896,
|
||||
"seed": 1142705242,
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. Over-shoulder shot from behind Sphere Alpha, looking down the street. The perspective emphasizes the spheres' small size against the towering skyscrapers. Watercolor technique with soft edges and muted tones, but the spheres' glow stands out in stark contrast. Shadows are deep and inky, suggesting a gritty, mature atmosphere.. Characters: female with red hair, blue eyes, light skin, thin build, wearing old female with red hair, blue eyes, light skin, build, wearing . Over_Shoulder shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_010": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9cfb91/w8FjnlmAMgXN1MzeYeH3D.jpg",
|
||||
"width": 896,
|
||||
"height": 1152,
|
||||
"seed": 1355310723,
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. Close-up of Sphere Alpha as it enters the portal. Half of it is already through, and the surface is distorting, showing glimpses of another dimension: a starry void with floating geometric shapes. Watercolor with a dreamlike quality, blending the sphere's metallic surface with the cosmic background. The edges are soft, suggesting transition.. Characters: female with red hair, blue eyes, light skin, thin build, wearing . Close_Up shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_009": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9cfb90/z2xfTsYSzt6_dM4EHLTy3.jpg",
|
||||
"width": 1344,
|
||||
"height": 768,
|
||||
"seed": 1142705236,
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. Wide shot as the spheres roll across the crosswalk. The lines of the crosswalk begin to glow and shift, forming a portal of light beneath them. Watercolor style with vibrant, surreal colors: the portal is a swirl of blue and gold, with the city around it fading into dark washes. The spheres are silhouetted against the bright portal.. Characters: female with red hair, blue eyes, light skin, thin build, wearing old female with red hair, blue eyes, light skin, build, wearing . Wide shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_004": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9cfb89/_kY-5nug4C8BuB0ky8hMy.jpg",
|
||||
"width": 896,
|
||||
"height": 1152,
|
||||
"seed": 1355310696,
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. Close-up of Sphere Alpha's surface, revealing intricate geometric patterns that appear like glowing circuit boards. Watercolor style with detailed brushwork, showing the reflection of a neon sign in the chrome surface. The pattern seems to pulse with energy, and faint symbols are visible beneath the surface.. Characters: female with red hair, blue eyes, light skin, thin build, wearing . Close_Up shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
},
|
||||
"panel_005": {
|
||||
"url": "https://v3b.fal.media/files/b/0a9cfb89/CTG3-vZla0nhBdJiGjLqX.jpg",
|
||||
"width": 1344,
|
||||
"height": 768,
|
||||
"seed": 441640662,
|
||||
"prompt": "watercolor painting style, soft edges, paper texture, translucent washes, delicate linework, artistic comic panel. Wide shot of the spheres rolling past a graffiti-covered wall. The graffiti seems to react to their presence, glowing and shifting into alien symbols. Watercolor with heavy, dark washes for the wall, contrasting with the bright trails of the spheres. Rain streaks down the wall, creating a sense of motion.. Characters: female with red hair, blue eyes, light skin, thin build, wearing old female with red hair, blue eyes, light skin, build, wearing . Wide shot. . . high quality, detailed, clean lines, vibrant colors, professional illustration, best quality, masterpiece"
|
||||
}
|
||||
},
|
||||
"panel_errors": {
|
||||
|
||||
},
|
||||
"page_layouts": [
|
||||
|
||||
{
|
||||
"page_number": 1,
|
||||
"pattern_id": "grid-2x2",
|
||||
"panels": [
|
||||
{
|
||||
"panel_id": "panel_001",
|
||||
"panel_number": 1,
|
||||
"layout_cell": {
|
||||
"x": 0.02000000,
|
||||
"y": 0.02000000,
|
||||
"w": 0.47000000,
|
||||
"h": 0.47000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_002",
|
||||
"panel_number": 2,
|
||||
"layout_cell": {
|
||||
"x": 0.50999999,
|
||||
"y": 0.02000000,
|
||||
"w": 0.47000000,
|
||||
"h": 0.47000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_003",
|
||||
"panel_number": 3,
|
||||
"layout_cell": {
|
||||
"x": 0.02000000,
|
||||
"y": 0.50999999,
|
||||
"w": 0.47000000,
|
||||
"h": 0.47000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_004",
|
||||
"panel_number": 4,
|
||||
"layout_cell": {
|
||||
"x": 0.50999999,
|
||||
"y": 0.50999999,
|
||||
"w": 0.47000000,
|
||||
"h": 0.47000000
|
||||
}
|
||||
}
|
||||
],
|
||||
"width": 2550,
|
||||
"height": 3300
|
||||
},
|
||||
{
|
||||
"page_number": 2,
|
||||
"pattern_id": "dialogue-heavy",
|
||||
"panels": [
|
||||
{
|
||||
"panel_id": "panel_005",
|
||||
"panel_number": 5,
|
||||
"layout_cell": {
|
||||
"x": 0.02000000,
|
||||
"y": 0.02000000,
|
||||
"w": 0.47000000,
|
||||
"h": 0.22000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_006",
|
||||
"panel_number": 6,
|
||||
"layout_cell": {
|
||||
"x": 0.50999999,
|
||||
"y": 0.02000000,
|
||||
"w": 0.47000000,
|
||||
"h": 0.22000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_007",
|
||||
"panel_number": 1,
|
||||
"layout_cell": {
|
||||
"x": 0.02000000,
|
||||
"y": 0.25999999,
|
||||
"w": 0.47000000,
|
||||
"h": 0.22000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_008",
|
||||
"panel_number": 2,
|
||||
"layout_cell": {
|
||||
"x": 0.50999999,
|
||||
"y": 0.25999999,
|
||||
"w": 0.47000000,
|
||||
"h": 0.22000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_009",
|
||||
"panel_number": 3,
|
||||
"layout_cell": {
|
||||
"x": 0.02000000,
|
||||
"y": 0.50000000,
|
||||
"w": 0.47000000,
|
||||
"h": 0.22000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_010",
|
||||
"panel_number": 4,
|
||||
"layout_cell": {
|
||||
"x": 0.50999999,
|
||||
"y": 0.50000000,
|
||||
"w": 0.47000000,
|
||||
"h": 0.22000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_011",
|
||||
"panel_number": 5,
|
||||
"layout_cell": {
|
||||
"x": 0.02000000,
|
||||
"y": 0.74000001,
|
||||
"w": 0.47000000,
|
||||
"h": 0.22000000
|
||||
}
|
||||
},
|
||||
{
|
||||
"panel_id": "panel_012",
|
||||
"panel_number": 6,
|
||||
"layout_cell": {
|
||||
"x": 0.50999999,
|
||||
"y": 0.74000001,
|
||||
"w": 0.47000000,
|
||||
"h": 0.22000000
|
||||
}
|
||||
}
|
||||
],
|
||||
"width": 2550,
|
||||
"height": 3300
|
||||
}
|
||||
],
|
||||
"speech_bubbles": {
|
||||
|
||||
},
|
||||
"export_format": 0,
|
||||
"page_size": 0,
|
||||
"page_size": 1,
|
||||
"color_profile": 0,
|
||||
"workflow": {
|
||||
"current_step": 2,
|
||||
"current_step": 7,
|
||||
"completed_steps": [
|
||||
|
||||
],
|
||||
"is_generating": false,
|
||||
"generation_progress": 0.00000000,
|
||||
"generation_progress": 100.00000000,
|
||||
"error_message": ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,7 +258,7 @@ declare_workspace :: proc(app: ^GUI_App_State, can_gen_panels, can_layout, can_e
|
||||
layout = {sizing = {width = clay.SizingGrow({}), height = clay.SizingGrow({})}, layoutDirection = .TopToBottom, padding = {top = CLAY_SPACE_12, right = CLAY_SPACE_20, bottom = CLAY_SPACE_12, left = CLAY_SPACE_20}, childGap = CLAY_SPACE_12},
|
||||
}) {
|
||||
if app.editor.active {
|
||||
clay_body_text(fmt.tprintf("Editing panel: %s | Right-click drag to pan | Scroll to zoom | Shift+Scroll for brush size | Esc to cancel", app.editor.panel_id), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
clay_body_text(fmt.tprintf("Editing panel: %s | Space/Right-drag pan | Scroll zoom / Shift+Scroll size | F fit D 100%% G debug P before | Ctrl+Z/Y undo/redo Ctrl+A select all Ctrl+D duplicate selection Arrows nudge Shift+Arrows 10px Del clear Esc clear selection/cancel | Shift constrains line/rect/crop | U fill K crop M move/resize selection Q/W/X rotate H/V flip O outline | Enter commit", app.editor.panel_id), color = CLAY_TEXT_SECONDARY, size = CLAY_FONT_SIZE_SM)
|
||||
} else {
|
||||
switch screen {
|
||||
case .Story:
|
||||
|
||||
1041
odin/src/gui/panel_editor_core.odin
Normal file
206
odin/src/gui/panel_editor_input.odin
Normal file
@ -0,0 +1,206 @@
|
||||
package gui
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
editor_update :: proc(app: ^GUI_App_State) {
|
||||
ed := &app.editor
|
||||
if !ed.active { return }
|
||||
|
||||
pen := &app.pen
|
||||
|
||||
mouse := rl.GetMousePosition()
|
||||
cr := ed.canvas_rect
|
||||
mx := vec2_x(mouse)
|
||||
my := vec2_y(mouse)
|
||||
in_canvas := mx >= cr.x && mx <= cr.x + cr.width && my >= cr.y && my <= cr.y + cr.height
|
||||
space_down := rl.IsKeyDown(.SPACE)
|
||||
pan_pressed := (rl.IsMouseButtonPressed(.RIGHT) || (space_down && rl.IsMouseButtonPressed(.LEFT))) && in_canvas
|
||||
pan_down := rl.IsMouseButtonDown(.RIGHT) || (space_down && rl.IsMouseButtonDown(.LEFT))
|
||||
|
||||
if pan_pressed {
|
||||
ed.is_panning = true
|
||||
ed.pan_start = mouse
|
||||
ed.pan_offset_start = ed.pan_offset
|
||||
}
|
||||
if ed.is_panning {
|
||||
if pan_down {
|
||||
_, fit_scale := editor_image_dest_rect(ed)
|
||||
if fit_scale <= 0 { fit_scale = 1.0 }
|
||||
dx := vec2_x(mouse) - vec2_x(ed.pan_start)
|
||||
dy := vec2_y(mouse) - vec2_y(ed.pan_start)
|
||||
ed.pan_offset = rl.Vector2{vec2_x(ed.pan_offset_start) + dx / fit_scale, vec2_y(ed.pan_offset_start) + dy / fit_scale}
|
||||
ed.needs_redraw = true
|
||||
} else {
|
||||
ed.is_panning = false
|
||||
}
|
||||
}
|
||||
|
||||
wheel := rl.GetMouseWheelMove()
|
||||
shift_down := rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT)
|
||||
if wheel != 0 && in_canvas {
|
||||
if shift_down {
|
||||
ed.brush_size += wheel * 2.0
|
||||
if ed.brush_size < 1.0 { ed.brush_size = 1.0 }
|
||||
if ed.brush_size > 80.0 { ed.brush_size = 80.0 }
|
||||
} else {
|
||||
editor_zoom_at_mouse(ed, mouse, wheel * 0.1)
|
||||
}
|
||||
}
|
||||
|
||||
image_rect, _ := editor_image_dest_rect(ed)
|
||||
in_image := mx >= image_rect.x && mx <= image_rect.x + image_rect.width && my >= image_rect.y && my <= image_rect.y + image_rect.height
|
||||
|
||||
if ed.current_tool == .Move && !ed.is_panning && !space_down {
|
||||
canvas_pos := editor_clamp_canvas_pos(ed, editor_screen_to_canvas(ed, mouse))
|
||||
if rl.IsMouseButtonPressed(.LEFT) && in_image {
|
||||
handle := editor_selection_handle_at_screen_pos(ed, mouse)
|
||||
if ed.selection_active && handle != .None {
|
||||
ed.is_resizing_selection = true
|
||||
ed.active_selection_handle = handle
|
||||
ed.selection_drag_origin = ed.selection_rect
|
||||
ed.selection_resize_anchor = editor_selection_opposite_corner(ed.selection_rect, handle)
|
||||
} else if ed.selection_active && editor_rect_contains_point(ed.selection_rect, canvas_pos) {
|
||||
ed.is_moving_selection = true
|
||||
ed.selection_drag_start = canvas_pos
|
||||
ed.selection_drag_origin = ed.selection_rect
|
||||
} else {
|
||||
ed.selection_active = false
|
||||
ed.is_moving_selection = false
|
||||
ed.is_resizing_selection = false
|
||||
ed.active_selection_handle = .None
|
||||
ed.is_drawing = true
|
||||
ed.draw_start = canvas_pos
|
||||
clear(&ed.current_stroke.points)
|
||||
append(&ed.current_stroke.points, canvas_pos)
|
||||
ed.current_stroke.tool = .Move
|
||||
}
|
||||
}
|
||||
if ed.is_resizing_selection && rl.IsMouseButtonDown(.LEFT) {
|
||||
end_pos := editor_constrain_shape_point(.Crop, ed.selection_resize_anchor, canvas_pos, shift_down)
|
||||
ed.selection_rect = editor_clamp_rect_to_image(ed, editor_make_rect_from_points(ed.selection_resize_anchor, end_pos))
|
||||
}
|
||||
if ed.is_moving_selection && rl.IsMouseButtonDown(.LEFT) {
|
||||
dx := vec2_x(canvas_pos) - vec2_x(ed.selection_drag_start)
|
||||
dy := vec2_y(canvas_pos) - vec2_y(ed.selection_drag_start)
|
||||
ed.selection_rect = editor_move_rect_within_image(ed, rl.Rectangle{ed.selection_drag_origin.x + dx, ed.selection_drag_origin.y + dy, ed.selection_drag_origin.width, ed.selection_drag_origin.height})
|
||||
}
|
||||
if ed.is_drawing && rl.IsMouseButtonDown(.LEFT) {
|
||||
end_pos := editor_constrain_shape_point(.Crop, ed.draw_start, canvas_pos, shift_down)
|
||||
if len(ed.current_stroke.points) > 1 {
|
||||
ed.current_stroke.points[len(ed.current_stroke.points)-1] = end_pos
|
||||
} else {
|
||||
append(&ed.current_stroke.points, end_pos)
|
||||
}
|
||||
}
|
||||
if ed.is_resizing_selection && rl.IsMouseButtonReleased(.LEFT) {
|
||||
ed.is_resizing_selection = false
|
||||
ed.active_selection_handle = .None
|
||||
return
|
||||
}
|
||||
if ed.is_moving_selection && rl.IsMouseButtonReleased(.LEFT) {
|
||||
ed.is_moving_selection = false
|
||||
if ed.selection_rect.x != ed.selection_drag_origin.x || ed.selection_rect.y != ed.selection_drag_origin.y {
|
||||
push_status(&app.status_msg, &app.action_log, editor_apply_move_selection(ed, ed.selection_drag_origin, ed.selection_rect))
|
||||
}
|
||||
}
|
||||
if ed.is_drawing && rl.IsMouseButtonReleased(.LEFT) {
|
||||
ed.is_drawing = false
|
||||
if len(ed.current_stroke.points) >= 2 {
|
||||
ed.selection_rect = editor_clamp_rect_to_image(ed, editor_make_rect_from_points(ed.current_stroke.points[0], ed.current_stroke.points[len(ed.current_stroke.points)-1]))
|
||||
ed.selection_active = ed.selection_rect.width >= 2 && ed.selection_rect.height >= 2
|
||||
}
|
||||
clear(&ed.current_stroke.points)
|
||||
return
|
||||
}
|
||||
}
|
||||
if ed.current_tool == .Move {
|
||||
return
|
||||
}
|
||||
|
||||
if !ed.is_panning && !space_down && (in_image || ed.is_drawing) {
|
||||
if rl.IsMouseButtonPressed(.LEFT) {
|
||||
canvas_pos := editor_clamp_canvas_pos(ed, editor_screen_to_canvas(ed, mouse))
|
||||
|
||||
if ed.current_tool == .Color_Pick {
|
||||
picked, ok := editor_sample_composited_color(ed, canvas_pos)
|
||||
if ok {
|
||||
ed.brush_color = picked
|
||||
editor_push_recent_color(ed, picked)
|
||||
}
|
||||
} else if ed.current_tool == .Fill {
|
||||
push_status(&app.status_msg, &app.action_log, editor_apply_fill(ed, canvas_pos))
|
||||
return
|
||||
} else {
|
||||
ed.is_drawing = true
|
||||
ed.draw_start = canvas_pos
|
||||
clear(&ed.current_stroke.points)
|
||||
append(&ed.current_stroke.points, canvas_pos)
|
||||
ed.current_stroke.color = ed.brush_color
|
||||
effective_size := ed.brush_size * pen_tablet_pressure_for_stroke(pen)
|
||||
ed.current_stroke.thickness = effective_size
|
||||
ed.current_stroke.tool = ed.current_tool
|
||||
ed.current_stroke.filled = !ed.shape_outline_only
|
||||
if ed.current_tool == .Eraser || pen.eraser {
|
||||
ed.current_stroke.color = rl.Color{0, 0, 0, 0}
|
||||
ed.current_stroke.thickness = effective_size * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ed.is_drawing && rl.IsMouseButtonDown(.LEFT) {
|
||||
canvas_pos := editor_clamp_canvas_pos(ed, editor_screen_to_canvas(ed, mouse))
|
||||
canvas_pos = editor_constrain_shape_point(ed.current_tool, ed.draw_start, canvas_pos, shift_down)
|
||||
if ed.current_tool == .Pen || ed.current_tool == .Eraser {
|
||||
append(&ed.current_stroke.points, canvas_pos)
|
||||
} else {
|
||||
if len(ed.current_stroke.points) > 1 {
|
||||
ed.current_stroke.points[len(ed.current_stroke.points)-1] = canvas_pos
|
||||
} else {
|
||||
append(&ed.current_stroke.points, canvas_pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ed.is_drawing && rl.IsMouseButtonReleased(.LEFT) {
|
||||
ed.is_drawing = false
|
||||
|
||||
if ed.current_tool == .Crop {
|
||||
if len(ed.current_stroke.points) >= 2 {
|
||||
p0 := ed.current_stroke.points[0]
|
||||
p1 := ed.current_stroke.points[len(ed.current_stroke.points)-1]
|
||||
crop_rect := rl.Rectangle{min(vec2_x(p0), vec2_x(p1)), min(vec2_y(p0), vec2_y(p1)), abs(vec2_x(p1) - vec2_x(p0)), abs(vec2_y(p1) - vec2_y(p0))}
|
||||
push_status(&app.status_msg, &app.action_log, editor_apply_crop(ed, crop_rect))
|
||||
}
|
||||
ed.current_stroke = Brush_Stroke{}
|
||||
ed.current_stroke.points = make([dynamic]rl.Vector2)
|
||||
ed.current_stroke.color = ed.brush_color
|
||||
ed.current_stroke.thickness = ed.brush_size
|
||||
ed.current_stroke.tool = ed.current_tool
|
||||
ed.current_stroke.filled = !ed.shape_outline_only
|
||||
return
|
||||
}
|
||||
|
||||
editor_push_undo(ed)
|
||||
|
||||
rl.BeginTextureMode(ed.drawing_rt)
|
||||
if ed.current_tool == .Eraser {
|
||||
rl.BeginBlendMode(.ALPHA)
|
||||
}
|
||||
editor_render_stroke(ed.current_stroke)
|
||||
if ed.current_tool == .Eraser {
|
||||
rl.EndBlendMode()
|
||||
}
|
||||
rl.EndTextureMode()
|
||||
|
||||
append(&ed.committed_strokes, ed.current_stroke)
|
||||
ed.current_stroke = Brush_Stroke{}
|
||||
ed.current_stroke.points = make([dynamic]rl.Vector2)
|
||||
ed.current_stroke.color = ed.brush_color
|
||||
ed.current_stroke.thickness = ed.brush_size
|
||||
ed.current_stroke.tool = ed.current_tool
|
||||
ed.current_stroke.filled = !ed.shape_outline_only
|
||||
|
||||
ed.needs_redraw = true
|
||||
}
|
||||
}
|
||||
795
odin/src/gui/panel_editor_render.odin
Normal file
@ -0,0 +1,795 @@
|
||||
package gui
|
||||
|
||||
import c "core:c"
|
||||
import "core:fmt"
|
||||
import "core:math"
|
||||
import rl "vendor:raylib"
|
||||
import "../shared"
|
||||
|
||||
editor_render_before_preview :: proc(ed: ^Panel_Editor_State, dest_rect: rl.Rectangle) {
|
||||
if !ed.base_image_valid { return }
|
||||
tex := rl.LoadTextureFromImage(ed.base_image)
|
||||
if tex.id == 0 { return }
|
||||
defer rl.UnloadTexture(tex)
|
||||
img_rect := rl.Rectangle{0, 0, f32(tex.width), f32(tex.height)}
|
||||
rl.DrawTexturePro(tex, img_rect, dest_rect, rl.Vector2{}, 0, rl.WHITE)
|
||||
}
|
||||
|
||||
editor_render :: proc(app: ^GUI_App_State) {
|
||||
ed := &app.editor
|
||||
if !ed.active { return }
|
||||
|
||||
if ed.needs_redraw {
|
||||
editor_replay_strokes(ed)
|
||||
editor_update_display(ed)
|
||||
}
|
||||
if ed.adjustments_dirty {
|
||||
editor_update_display(ed)
|
||||
}
|
||||
|
||||
screen_w := rl.GetScreenWidth()
|
||||
screen_h := rl.GetScreenHeight()
|
||||
|
||||
sidebar_w_f32 := f32(sidebar_width(shared.breakpoint(screen_w, screen_h), app.sidebar_collapsed, app.sidebar_anim))
|
||||
top_bar_h: f32 = 38
|
||||
bottom_bar_h: f32 = 44
|
||||
toolbar_h: f32 = 40
|
||||
|
||||
canvas_x := sidebar_w_f32 + 4
|
||||
canvas_y := top_bar_h + toolbar_h + 4
|
||||
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}
|
||||
|
||||
rl.DrawRectangleRec(ed.canvas_rect, rl.Color{6, 6, 10, 255})
|
||||
|
||||
if ed.rt_valid && ed.base_image_valid {
|
||||
display_src_rect := editor_texture_src_rect(ed.display_rt.texture)
|
||||
drawing_src_rect := editor_texture_src_rect(ed.drawing_rt.texture)
|
||||
dest_rect, _ := editor_image_dest_rect(ed)
|
||||
|
||||
rl.DrawRectangleLinesEx(dest_rect, 1, rl.Color{60, 60, 80, 255})
|
||||
rl.BeginScissorMode(c.int(ed.canvas_rect.x), c.int(ed.canvas_rect.y), c.int(ed.canvas_rect.width), c.int(ed.canvas_rect.height))
|
||||
|
||||
if ed.show_before_preview {
|
||||
editor_render_before_preview(ed, dest_rect)
|
||||
} else {
|
||||
rl.DrawTexturePro(
|
||||
ed.display_rt.texture,
|
||||
display_src_rect,
|
||||
dest_rect,
|
||||
rl.Vector2{},
|
||||
0,
|
||||
rl.WHITE,
|
||||
)
|
||||
|
||||
rl.DrawTexturePro(
|
||||
ed.drawing_rt.texture,
|
||||
drawing_src_rect,
|
||||
dest_rect,
|
||||
rl.Vector2{},
|
||||
0,
|
||||
rl.WHITE,
|
||||
)
|
||||
|
||||
if ed.is_drawing && len(ed.current_stroke.points) > 0 {
|
||||
rl.BeginBlendMode(.ALPHA)
|
||||
editor_render_stroke_preview(ed, dest_rect)
|
||||
rl.EndBlendMode()
|
||||
}
|
||||
}
|
||||
editor_render_selection_overlay(ed, dest_rect)
|
||||
editor_render_cursor_preview(app, dest_rect)
|
||||
editor_render_debug_overlay(app, dest_rect)
|
||||
|
||||
rl.EndScissorMode()
|
||||
}
|
||||
|
||||
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) {
|
||||
scale_x := dest_rect.width / f32(ed.base_image.width)
|
||||
scale_y := dest_rect.height / f32(ed.base_image.height)
|
||||
|
||||
scaled: [dynamic]rl.Vector2
|
||||
defer delete(scaled)
|
||||
for p in ed.current_stroke.points {
|
||||
sx := dest_rect.x + vec2_x(p) * scale_x
|
||||
sy := dest_rect.y + vec2_y(p) * scale_y
|
||||
append(&scaled, rl.Vector2{sx, sy})
|
||||
}
|
||||
|
||||
if len(scaled) == 0 { return }
|
||||
|
||||
thickness := ed.current_stroke.thickness * min(scale_x, scale_y)
|
||||
|
||||
switch ed.current_tool {
|
||||
case .Pen:
|
||||
if len(scaled) >= 4 {
|
||||
rl.DrawSplineCatmullRom(&scaled[0], c.int(len(scaled)), thickness, ed.current_stroke.color)
|
||||
} else if len(scaled) == 3 {
|
||||
rl.DrawSplineBezierQuadratic(&scaled[0], c.int(len(scaled)), thickness, ed.current_stroke.color)
|
||||
} else if len(scaled) == 2 {
|
||||
rl.DrawLineEx(scaled[0], scaled[1], thickness, ed.current_stroke.color)
|
||||
} else {
|
||||
rl.DrawCircleV(scaled[0], thickness * 0.5, ed.current_stroke.color)
|
||||
}
|
||||
case .Eraser:
|
||||
if len(scaled) >= 2 {
|
||||
rl.DrawLineEx(scaled[0], scaled[len(scaled)-1], thickness * 2, rl.Color{255, 255, 255, 80})
|
||||
}
|
||||
case .Line:
|
||||
if len(scaled) >= 2 {
|
||||
rl.DrawLineEx(scaled[0], scaled[len(scaled)-1], thickness, ed.current_stroke.color)
|
||||
}
|
||||
case .Rectangle:
|
||||
if len(scaled) >= 2 {
|
||||
p0 := scaled[0]
|
||||
p1 := scaled[len(scaled)-1]
|
||||
rect := rl.Rectangle{min(vec2_x(p0), vec2_x(p1)), min(vec2_y(p0), vec2_y(p1)), abs(vec2_x(p1) - vec2_x(p0)), abs(vec2_y(p1) - vec2_y(p0))}
|
||||
if ed.current_stroke.filled {
|
||||
rl.DrawRectangleRec(rect, ed.current_stroke.color)
|
||||
} else {
|
||||
rl.DrawRectangleLinesEx(rect, max(thickness, 1.0), ed.current_stroke.color)
|
||||
}
|
||||
}
|
||||
case .Circle:
|
||||
if len(scaled) >= 2 {
|
||||
p0 := scaled[0]
|
||||
p1 := scaled[len(scaled)-1]
|
||||
dx := vec2_x(p1) - vec2_x(p0)
|
||||
dy := vec2_y(p1) - vec2_y(p0)
|
||||
radius := math.sqrt_f32(dx*dx + dy*dy)
|
||||
if ed.current_stroke.filled {
|
||||
rl.DrawCircleV(p0, radius, ed.current_stroke.color)
|
||||
} else {
|
||||
rl.DrawRingLines(p0, max(radius - max(thickness, 1.0), 0), radius, 0, 360, 48, ed.current_stroke.color)
|
||||
}
|
||||
}
|
||||
case .Color_Pick:
|
||||
rl.DrawCircleV(scaled[0], 6, ed.current_stroke.color)
|
||||
case .Fill:
|
||||
// fill has no drag preview
|
||||
case .Crop, .Move:
|
||||
if len(scaled) >= 2 {
|
||||
p0 := scaled[0]
|
||||
p1 := scaled[len(scaled)-1]
|
||||
rect := rl.Rectangle{min(vec2_x(p0), vec2_x(p1)), min(vec2_y(p0), vec2_y(p1)), abs(vec2_x(p1) - vec2_x(p0)), abs(vec2_y(p1) - vec2_y(p0))}
|
||||
preview_color := rl.Color{99, 102, 241, 255}
|
||||
if ed.current_tool == .Move {
|
||||
preview_color = rl.Color{16, 185, 129, 255}
|
||||
}
|
||||
rl.DrawRectangleRec(rect, rl.Color{preview_color[0], preview_color[1], preview_color[2], 40})
|
||||
rl.DrawRectangleLinesEx(rect, 2, preview_color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor_render_selection_overlay :: proc(ed: ^Panel_Editor_State, dest_rect: rl.Rectangle) {
|
||||
if !ed.selection_active || !ed.base_image_valid { return }
|
||||
scale_x := dest_rect.width / f32(ed.base_image.width)
|
||||
scale_y := dest_rect.height / f32(ed.base_image.height)
|
||||
rect := rl.Rectangle{
|
||||
dest_rect.x + ed.selection_rect.x * scale_x,
|
||||
dest_rect.y + ed.selection_rect.y * scale_y,
|
||||
ed.selection_rect.width * scale_x,
|
||||
ed.selection_rect.height * scale_y,
|
||||
}
|
||||
rl.DrawRectangleRec(rect, rl.Color{16, 185, 129, 32})
|
||||
rl.DrawRectangleLinesEx(rect, 2, rl.Color{16, 185, 129, 255})
|
||||
handle_r: f32 = 3
|
||||
handles := []rl.Vector2{
|
||||
{rect.x, rect.y},
|
||||
{rect.x + rect.width, rect.y},
|
||||
{rect.x, rect.y + rect.height},
|
||||
{rect.x + rect.width, rect.y + rect.height},
|
||||
}
|
||||
for h in handles {
|
||||
rl.DrawCircleV(h, handle_r, rl.Color{16, 185, 129, 255})
|
||||
}
|
||||
}
|
||||
|
||||
editor_render_cursor_preview :: proc(app: ^GUI_App_State, dest_rect: rl.Rectangle) {
|
||||
ed := &app.editor
|
||||
if !ed.base_image_valid { return }
|
||||
mouse := rl.GetMousePosition()
|
||||
mx := vec2_x(mouse)
|
||||
my := vec2_y(mouse)
|
||||
if mx < dest_rect.x || mx > dest_rect.x + dest_rect.width || my < dest_rect.y || my > dest_rect.y + dest_rect.height {
|
||||
return
|
||||
}
|
||||
|
||||
scale := min(dest_rect.width / f32(ed.base_image.width), dest_rect.height / f32(ed.base_image.height))
|
||||
if scale <= 0 { return }
|
||||
canvas_pos := editor_clamp_canvas_pos(ed, editor_screen_to_canvas(ed, mouse))
|
||||
cx := dest_rect.x + vec2_x(canvas_pos) * scale
|
||||
cy := dest_rect.y + vec2_y(canvas_pos) * scale
|
||||
thickness := max(ed.brush_size * scale, 1.0)
|
||||
|
||||
if ed.current_tool == .Color_Pick || ed.current_tool == .Fill || ed.current_tool == .Crop || ed.current_tool == .Move {
|
||||
rl.DrawLine(c.int(cx - 8), c.int(cy), c.int(cx + 8), c.int(cy), rl.WHITE)
|
||||
rl.DrawLine(c.int(cx), c.int(cy - 8), c.int(cx), c.int(cy + 8), rl.WHITE)
|
||||
if ed.current_tool == .Fill {
|
||||
rl.DrawCircleLinesV(rl.Vector2{cx, cy}, 6, ed.brush_color)
|
||||
}
|
||||
if ed.current_tool == .Crop {
|
||||
rl.DrawCircleLinesV(rl.Vector2{cx, cy}, 6, rl.Color{99, 102, 241, 255})
|
||||
}
|
||||
if ed.current_tool == .Move {
|
||||
rl.DrawCircleLinesV(rl.Vector2{cx, cy}, 6, rl.Color{16, 185, 129, 255})
|
||||
}
|
||||
} else {
|
||||
radius := thickness * 0.5
|
||||
if ed.current_tool == .Eraser || app.pen.eraser {
|
||||
radius = thickness
|
||||
rl.DrawCircleLinesV(rl.Vector2{cx, cy}, radius, rl.WHITE)
|
||||
rl.DrawCircleLinesV(rl.Vector2{cx, cy}, max(radius - 1, 0), rl.Color{255, 80, 80, 255})
|
||||
} else {
|
||||
rl.DrawCircleLinesV(rl.Vector2{cx, cy}, radius, rl.WHITE)
|
||||
rl.DrawCircleLinesV(rl.Vector2{cx, cy}, max(radius - 1, 0), ed.brush_color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor_render_debug_overlay :: proc(app: ^GUI_App_State, dest_rect: rl.Rectangle) {
|
||||
ed := &app.editor
|
||||
if !ed.show_debug_overlay { return }
|
||||
mouse := rl.GetMousePosition()
|
||||
canvas_pos := editor_clamp_canvas_pos(ed, editor_screen_to_canvas(ed, mouse))
|
||||
lines := []string{
|
||||
fmt.tprintf("Mouse: %.1f, %.1f", vec2_x(mouse), vec2_y(mouse)),
|
||||
fmt.tprintf("Canvas: %.1f, %.1f", vec2_x(canvas_pos), vec2_y(canvas_pos)),
|
||||
fmt.tprintf("ImageRect: %.1f, %.1f %.1fx%.1f", dest_rect.x, dest_rect.y, dest_rect.width, dest_rect.height),
|
||||
fmt.tprintf("Zoom: %.2f Pan: %.1f, %.1f", ed.zoom, vec2_x(ed.pan_offset), vec2_y(ed.pan_offset)),
|
||||
fmt.tprintf("Tool: %v Size: %.1f", ed.current_tool, ed.brush_size),
|
||||
}
|
||||
box_x := ed.canvas_rect.x + 8
|
||||
box_y := ed.canvas_rect.y + 8
|
||||
box_w: f32 = 230
|
||||
box_h: f32 = 18 + f32(len(lines)) * 14
|
||||
rl.DrawRectangle(c.int(box_x), c.int(box_y), c.int(box_w), c.int(box_h), rl.Color{10, 10, 18, 220})
|
||||
rl.DrawRectangleLines(c.int(box_x), c.int(box_y), c.int(box_w), c.int(box_h), rl.Color{70, 70, 100, 255})
|
||||
for line, i in lines {
|
||||
rl.DrawText(to_cstr(line), c.int(box_x + 8), c.int(box_y + 6 + f32(i) * 14), 10, rl.Color{210, 210, 230, 255})
|
||||
}
|
||||
}
|
||||
|
||||
editor_render_toolbar :: proc(app: ^GUI_App_State) {
|
||||
ed := &app.editor
|
||||
screen_w := rl.GetScreenWidth()
|
||||
screen_h := rl.GetScreenHeight()
|
||||
sidebar_w_f32 := f32(sidebar_width(shared.breakpoint(screen_w, screen_h), app.sidebar_collapsed, app.sidebar_anim))
|
||||
|
||||
tb_x := sidebar_w_f32 + 4
|
||||
tb_y: f32 = 38
|
||||
tb_w := f32(screen_w) - sidebar_w_f32 - 8
|
||||
tb_h: f32 = 40
|
||||
|
||||
rl.DrawRectangle(c.int(tb_x), c.int(tb_y), c.int(tb_w), c.int(tb_h), rl.Color{18, 18, 30, 240})
|
||||
|
||||
tool_names := []string{"Pen", "Eraser", "Line", "Rect", "Circle", "Pick", "Fill", "Crop", "Move"}
|
||||
tool_enums := []Editor_Tool{.Pen, .Eraser, .Line, .Rectangle, .Circle, .Color_Pick, .Fill, .Crop, .Move}
|
||||
tool_labels := []string{"Tool: Pen", "Tool: Eraser", "Tool: Line", "Tool: Rect", "Tool: Circle", "Tool: Pick", "Tool: Fill", "Tool: Crop", "Tool: Move"}
|
||||
|
||||
btn_w: f32 = 48
|
||||
btn_h: f32 = 28
|
||||
btn_gap: f32 = 4
|
||||
start_x := tb_x + 8
|
||||
start_y := tb_y + (tb_h - btn_h) * 0.5
|
||||
|
||||
mouse := rl.GetMousePosition()
|
||||
mx := vec2_x(mouse)
|
||||
my := vec2_y(mouse)
|
||||
|
||||
active_tool_idx := 0
|
||||
for i in 0..<len(tool_names) {
|
||||
bx := start_x + f32(i) * (btn_w + btn_gap)
|
||||
is_active := ed.current_tool == tool_enums[i]
|
||||
bg := rl.Color{28, 28, 42, 255}
|
||||
if is_active { bg = rl.Color{99, 102, 241, 255} }
|
||||
|
||||
rl.DrawRectangle(c.int(bx), c.int(start_y), c.int(btn_w), c.int(btn_h), bg)
|
||||
rl.DrawText(to_cstr(tool_names[i]), c.int(bx + 4), c.int(start_y + 6), 11, rl.WHITE)
|
||||
|
||||
if is_active { active_tool_idx = i }
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= bx && mx <= bx + btn_w &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
ed.current_tool = tool_enums[i]
|
||||
active_tool_idx = i
|
||||
}
|
||||
}
|
||||
|
||||
sep_tools_x := start_x + f32(len(tool_names)) * (btn_w + btn_gap) + 2
|
||||
rl.DrawLine(c.int(sep_tools_x), c.int(tb_y + 6), c.int(sep_tools_x), c.int(tb_y + tb_h - 6), rl.Color{44, 44, 64, 255})
|
||||
color_x := start_x + f32(len(tool_names)) * (btn_w + btn_gap) + 8
|
||||
rl.DrawRectangle(c.int(color_x), c.int(start_y), c.int(btn_h), c.int(btn_h), ed.brush_color)
|
||||
rl.DrawRectangleLines(c.int(color_x), c.int(start_y), c.int(btn_h), c.int(btn_h), rl.Color{60, 60, 80, 255})
|
||||
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= color_x && mx <= color_x + f32(btn_h) &&
|
||||
my >= start_y && my <= start_y + f32(btn_h) {
|
||||
ed.show_color_panel = !ed.show_color_panel
|
||||
}
|
||||
|
||||
size_x := color_x + f32(btn_h) + 12
|
||||
size_dec_x := size_x
|
||||
rl.DrawRectangle(c.int(size_dec_x), c.int(start_y), c.int(22), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText("-", c.int(size_dec_x + 8), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= size_dec_x && mx <= size_dec_x + 22 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
ed.brush_size = max(ed.brush_size - 1, 1)
|
||||
}
|
||||
|
||||
size_text_x := size_dec_x + 28
|
||||
size_text := fmt.tprintf("Size: %.0f", ed.brush_size)
|
||||
rl.DrawText(to_cstr(size_text), c.int(size_text_x), c.int(start_y + 6), 11, rl.Color{160, 160, 180, 255})
|
||||
|
||||
size_inc_x := size_text_x + 58
|
||||
rl.DrawRectangle(c.int(size_inc_x), c.int(start_y), c.int(22), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText("+", c.int(size_inc_x + 8), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= size_inc_x && mx <= size_inc_x + 22 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
ed.brush_size = min(ed.brush_size + 1, 80)
|
||||
}
|
||||
|
||||
sep_brush_x := size_inc_x + 28
|
||||
rl.DrawLine(c.int(sep_brush_x), c.int(tb_y + 6), c.int(sep_brush_x), c.int(tb_y + tb_h - 6), rl.Color{44, 44, 64, 255})
|
||||
zoom_x := size_inc_x + 34
|
||||
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})
|
||||
|
||||
fit_x := zoom_x + 76
|
||||
rl.DrawRectangle(c.int(fit_x), c.int(start_y), c.int(32), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText("Fit", c.int(fit_x + 5), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= fit_x && mx <= fit_x + 32 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
editor_fit_to_view(ed)
|
||||
}
|
||||
|
||||
actual_x := fit_x + 36
|
||||
rl.DrawRectangle(c.int(actual_x), c.int(start_y), c.int(34), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText("100", c.int(actual_x + 5), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= actual_x && mx <= actual_x + 34 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
editor_zoom_actual_size(ed)
|
||||
}
|
||||
|
||||
reset_x := actual_x + 38
|
||||
rl.DrawRectangle(c.int(reset_x), c.int(start_y), c.int(42), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText("Reset", c.int(reset_x + 4), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= reset_x && mx <= reset_x + 42 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
editor_zoom_reset_pan(ed)
|
||||
}
|
||||
|
||||
sep_view_x := reset_x + 42
|
||||
rl.DrawLine(c.int(sep_view_x), c.int(tb_y + 6), c.int(sep_view_x), c.int(tb_y + tb_h - 6), rl.Color{44, 44, 64, 255})
|
||||
pen_x := reset_x + 48
|
||||
if app.pen.stylus_down || app.pen.available {
|
||||
pen_text := fmt.tprintf("P: %d%%", c.int(app.pen.pressure * 100))
|
||||
rl.DrawText(to_cstr(pen_text), c.int(pen_x), c.int(start_y + 6), 11, rl.Color{130, 200, 130, 255})
|
||||
if app.pen.eraser {
|
||||
rl.DrawText("ERASER", c.int(pen_x + 55), c.int(start_y + 6), 11, rl.Color{255, 130, 130, 255})
|
||||
}
|
||||
}
|
||||
|
||||
rotate_l_x := pen_x + 60
|
||||
rl.DrawRectangle(c.int(rotate_l_x), c.int(start_y), c.int(40), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText("RotL", c.int(rotate_l_x + 4), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= rotate_l_x && mx <= rotate_l_x + 40 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
push_status(&app.status_msg, &app.action_log, editor_transform_canvas(ed, .Rotate_CCW))
|
||||
}
|
||||
|
||||
rotate_r_x := rotate_l_x + 44
|
||||
rl.DrawRectangle(c.int(rotate_r_x), c.int(start_y), c.int(40), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText("RotR", c.int(rotate_r_x + 4), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= rotate_r_x && mx <= rotate_r_x + 40 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
push_status(&app.status_msg, &app.action_log, editor_transform_canvas(ed, .Rotate_CW))
|
||||
}
|
||||
|
||||
rotate_180_x := rotate_r_x + 44
|
||||
rl.DrawRectangle(c.int(rotate_180_x), c.int(start_y), c.int(34), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText("180", c.int(rotate_180_x + 5), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= rotate_180_x && mx <= rotate_180_x + 34 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
push_status(&app.status_msg, &app.action_log, editor_transform_canvas(ed, .Rotate_180))
|
||||
}
|
||||
|
||||
flip_h_x := rotate_180_x + 38
|
||||
rl.DrawRectangle(c.int(flip_h_x), c.int(start_y), c.int(32), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText("FH", c.int(flip_h_x + 7), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= flip_h_x && mx <= flip_h_x + 32 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
push_status(&app.status_msg, &app.action_log, editor_transform_canvas(ed, .Flip_Horizontal))
|
||||
}
|
||||
|
||||
flip_v_x := flip_h_x + 36
|
||||
rl.DrawRectangle(c.int(flip_v_x), c.int(start_y), c.int(32), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText("FV", c.int(flip_v_x + 7), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= flip_v_x && mx <= flip_v_x + 32 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
push_status(&app.status_msg, &app.action_log, editor_transform_canvas(ed, .Flip_Vertical))
|
||||
}
|
||||
|
||||
sep_transform_x := flip_v_x + 32
|
||||
rl.DrawLine(c.int(sep_transform_x), c.int(tb_y + 6), c.int(sep_transform_x), c.int(tb_y + tb_h - 6), rl.Color{44, 44, 64, 255})
|
||||
outline_x := flip_v_x + 40
|
||||
outline_bg := rl.Color{28, 28, 42, 255}
|
||||
if ed.shape_outline_only { outline_bg = rl.Color{99, 102, 241, 255} }
|
||||
rl.DrawRectangle(c.int(outline_x), c.int(start_y), c.int(52), c.int(btn_h), outline_bg)
|
||||
rl.DrawText("Outline", c.int(outline_x + 4), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= outline_x && mx <= outline_x + 52 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
ed.shape_outline_only = !ed.shape_outline_only
|
||||
ed.current_stroke.filled = !ed.shape_outline_only
|
||||
}
|
||||
|
||||
adjust_x := outline_x + 60
|
||||
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
|
||||
}
|
||||
|
||||
debug_x := adjust_x + 64
|
||||
debug_bg := rl.Color{28, 28, 42, 255}
|
||||
if ed.show_debug_overlay { debug_bg = rl.Color{99, 102, 241, 255} }
|
||||
rl.DrawRectangle(c.int(debug_x), c.int(start_y), c.int(42), c.int(btn_h), debug_bg)
|
||||
rl.DrawText("Dbg", c.int(debug_x + 8), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= debug_x && mx <= debug_x + 42 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
ed.show_debug_overlay = !ed.show_debug_overlay
|
||||
}
|
||||
|
||||
before_x := debug_x + 46
|
||||
before_bg := rl.Color{28, 28, 42, 255}
|
||||
if ed.show_before_preview { before_bg = rl.Color{99, 102, 241, 255} }
|
||||
rl.DrawRectangle(c.int(before_x), c.int(start_y), c.int(56), c.int(btn_h), before_bg)
|
||||
rl.DrawText("Before", c.int(before_x + 4), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= before_x && mx <= before_x + 56 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
ed.show_before_preview = !ed.show_before_preview
|
||||
}
|
||||
|
||||
tool_label := tool_labels[active_tool_idx]
|
||||
rl.DrawText(to_cstr(tool_label), c.int(before_x + 64), c.int(start_y + 6), 11, rl.Color{160, 160, 180, 255})
|
||||
|
||||
if ed.selection_active {
|
||||
dup_x := before_x + 140
|
||||
rl.DrawRectangle(c.int(dup_x), c.int(start_y), c.int(46), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText("Dupe", c.int(dup_x + 5), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= dup_x && mx <= dup_x + 46 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
push_status(&app.status_msg, &app.action_log, editor_apply_duplicate_selection(ed))
|
||||
}
|
||||
|
||||
clear_x := dup_x + 50
|
||||
rl.DrawRectangle(c.int(clear_x), c.int(start_y), c.int(46), c.int(btn_h), rl.Color{80, 40, 40, 255})
|
||||
rl.DrawText("Clear", c.int(clear_x + 4), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= clear_x && mx <= clear_x + 46 &&
|
||||
my >= start_y && my <= start_y + btn_h {
|
||||
push_status(&app.status_msg, &app.action_log, editor_apply_clear_selection(ed))
|
||||
}
|
||||
}
|
||||
|
||||
undo_x := tb_x + tb_w - 266
|
||||
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})
|
||||
rl.DrawText(to_cstr(undo_label), c.int(undo_x + 2), c.int(start_y + 6), 11, rl.Color{180, 180, 200, 255})
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= undo_x && mx <= undo_x + 50 &&
|
||||
my >= start_y && my <= start_y + f32(btn_h) {
|
||||
editor_undo(ed)
|
||||
}
|
||||
|
||||
redo_x := undo_x + 54
|
||||
redo_label := fmt.tprintf("Redo(%d)", len(ed.redo_stack))
|
||||
rl.DrawRectangle(c.int(redo_x), c.int(start_y), c.int(54), c.int(btn_h), rl.Color{28, 28, 42, 255})
|
||||
rl.DrawText(to_cstr(redo_label), c.int(redo_x + 2), c.int(start_y + 6), 11, rl.Color{180, 180, 200, 255})
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= redo_x && mx <= redo_x + 54 &&
|
||||
my >= start_y && my <= start_y + f32(btn_h) {
|
||||
editor_redo(ed)
|
||||
}
|
||||
|
||||
commit_x := tb_x + tb_w - 150
|
||||
rl.DrawRectangle(c.int(commit_x), c.int(start_y), c.int(70), c.int(btn_h), rl.Color{34, 139, 34, 255})
|
||||
rl.DrawText("Commit", c.int(commit_x + 6), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= commit_x && mx <= commit_x + 70 &&
|
||||
my >= start_y && my <= start_y + f32(btn_h) {
|
||||
push_status(&app.status_msg, &app.action_log, editor_commit(app))
|
||||
}
|
||||
|
||||
cancel_x := tb_x + tb_w - 72
|
||||
rl.DrawRectangle(c.int(cancel_x), c.int(start_y), c.int(66), c.int(btn_h), rl.Color{139, 34, 34, 255})
|
||||
rl.DrawText("Cancel", c.int(cancel_x + 4), c.int(start_y + 6), 11, rl.WHITE)
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= cancel_x && mx <= cancel_x + 66 &&
|
||||
my >= start_y && my <= start_y + f32(btn_h) {
|
||||
editor_close(app)
|
||||
push_status(&app.status_msg, &app.action_log, "Edit cancelled")
|
||||
}
|
||||
|
||||
if ed.show_color_panel {
|
||||
editor_render_color_panel(ed, color_x, start_y + btn_h + 4)
|
||||
}
|
||||
}
|
||||
|
||||
editor_render_color_panel :: proc(ed: ^Panel_Editor_State, px, py: f32) {
|
||||
panel_w: f32 = 200
|
||||
panel_h: f32 = 150
|
||||
|
||||
rl.DrawRectangle(c.int(px), c.int(py), c.int(panel_w), c.int(panel_h), rl.Color{22, 22, 34, 250})
|
||||
rl.DrawRectangleLines(c.int(px), c.int(py), c.int(panel_w), c.int(panel_h), rl.Color{50, 50, 70, 255})
|
||||
|
||||
preset_colors := []rl.Color{
|
||||
rl.BLACK, rl.WHITE, rl.RED, rl.GREEN, rl.BLUE,
|
||||
rl.YELLOW, rl.MAGENTA, rl.ORANGE, rl.Color{128, 0, 0, 255},
|
||||
rl.Color{0, 128, 0, 255}, rl.Color{0, 0, 128, 255},
|
||||
rl.Color{128, 128, 128, 255}, rl.Color{64, 64, 64, 255},
|
||||
rl.Color{192, 192, 192, 255}, rl.Color{255, 165, 0, 255},
|
||||
rl.Color{255, 192, 203, 255}, rl.Color{0, 255, 255, 255},
|
||||
rl.Color{255, 0, 255, 255}, rl.Color{128, 0, 128, 255},
|
||||
rl.Color{210, 105, 30, 255}, rl.Color{255, 215, 0, 255},
|
||||
}
|
||||
|
||||
swatch_size: f32 = 18
|
||||
swatch_gap: f32 = 4
|
||||
cols := c.int(panel_w / (swatch_size + swatch_gap))
|
||||
|
||||
mouse := rl.GetMousePosition()
|
||||
mx := vec2_x(mouse)
|
||||
my := vec2_y(mouse)
|
||||
|
||||
rl.DrawText("Recent", c.int(px + 6), c.int(py + 6), 10, rl.Color{160, 160, 180, 255})
|
||||
for idx in 0..<ed.recent_color_count {
|
||||
clr := ed.recent_colors[idx]
|
||||
sx := px + 6 + f32(idx) * (swatch_size + swatch_gap)
|
||||
sy := py + 20
|
||||
rl.DrawRectangle(c.int(sx), c.int(sy), c.int(swatch_size), c.int(swatch_size), clr)
|
||||
if editor_colors_equal(clr, ed.brush_color) {
|
||||
rl.DrawRectangleLines(c.int(sx - 1), c.int(sy - 1), c.int(swatch_size + 2), c.int(swatch_size + 2), rl.Color{99, 102, 241, 255})
|
||||
}
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= sx && mx <= sx + swatch_size &&
|
||||
my >= sy && my <= sy + swatch_size {
|
||||
ed.brush_color = clr
|
||||
editor_push_recent_color(ed, clr)
|
||||
ed.show_color_panel = false
|
||||
}
|
||||
}
|
||||
|
||||
rl.DrawText("Palette", c.int(px + 6), c.int(py + 46), 10, rl.Color{160, 160, 180, 255})
|
||||
for idx in 0..<len(preset_colors) {
|
||||
clr := preset_colors[idx]
|
||||
col := idx % int(cols)
|
||||
row := idx / int(cols)
|
||||
sx := px + 6 + f32(col) * (swatch_size + swatch_gap)
|
||||
sy := py + 60 + f32(row) * (swatch_size + swatch_gap)
|
||||
rl.DrawRectangle(c.int(sx), c.int(sy), c.int(swatch_size), c.int(swatch_size), clr)
|
||||
if editor_colors_equal(clr, ed.brush_color) {
|
||||
rl.DrawRectangleLines(c.int(sx - 1), c.int(sy - 1), c.int(swatch_size + 2), c.int(swatch_size + 2), rl.Color{99, 102, 241, 255})
|
||||
}
|
||||
|
||||
if rl.IsMouseButtonPressed(.LEFT) &&
|
||||
mx >= sx && mx <= sx + swatch_size &&
|
||||
my >= sy && my <= sy + swatch_size {
|
||||
ed.brush_color = clr
|
||||
editor_push_recent_color(ed, clr)
|
||||
ed.show_color_panel = false
|
||||
}
|
||||
}
|
||||
|
||||
cur_y := py + panel_h - 22
|
||||
rl.DrawText(to_cstr(fmt.tprintf("Current:")), c.int(px + 6), c.int(cur_y + 2), 11, rl.Color{160, 160, 180, 255})
|
||||
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
|
||||
}
|
||||
}
|
||||
213
odin/src/gui/panel_editor_types.odin
Normal file
@ -0,0 +1,213 @@
|
||||
package gui
|
||||
|
||||
import c "core:c"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
EDITOR_UNDO_MAX :: 30
|
||||
EDITOR_DEFAULT_BRUSH_SIZE :: 4.0
|
||||
EDITOR_RECENT_COLOR_MAX :: 8
|
||||
|
||||
Editor_Tool :: enum {
|
||||
Pen,
|
||||
Eraser,
|
||||
Line,
|
||||
Rectangle,
|
||||
Circle,
|
||||
Color_Pick,
|
||||
Fill,
|
||||
Crop,
|
||||
Move,
|
||||
}
|
||||
|
||||
Brush_Stroke :: struct {
|
||||
points: [dynamic]rl.Vector2,
|
||||
color: rl.Color,
|
||||
thickness: f32,
|
||||
tool: Editor_Tool,
|
||||
filled: bool,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
Editor_Document_Snapshot :: struct {
|
||||
base_image: rl.Image,
|
||||
base_image_valid: bool,
|
||||
strokes: [dynamic]Brush_Stroke,
|
||||
adjustments: Color_Adjustments,
|
||||
}
|
||||
|
||||
Fill_Node :: struct {
|
||||
x: c.int,
|
||||
y: c.int,
|
||||
}
|
||||
|
||||
Selection_Handle :: enum {
|
||||
None,
|
||||
Top_Left,
|
||||
Top_Right,
|
||||
Bottom_Left,
|
||||
Bottom_Right,
|
||||
}
|
||||
|
||||
Panel_Editor_State :: struct {
|
||||
active: bool,
|
||||
panel_id: string,
|
||||
base_image: rl.Image,
|
||||
base_image_valid: bool,
|
||||
drawing_rt: rl.RenderTexture2D,
|
||||
display_rt: rl.RenderTexture2D,
|
||||
rt_valid: bool,
|
||||
current_tool: Editor_Tool,
|
||||
brush_color: rl.Color,
|
||||
brush_size: f32,
|
||||
shape_outline_only: bool,
|
||||
is_drawing: bool,
|
||||
draw_start: rl.Vector2,
|
||||
current_stroke: Brush_Stroke,
|
||||
committed_strokes: [dynamic]Brush_Stroke,
|
||||
undo_stack: [dynamic]Editor_Document_Snapshot,
|
||||
redo_stack: [dynamic]Editor_Document_Snapshot,
|
||||
adjustments: Color_Adjustments,
|
||||
adjustments_dirty: bool,
|
||||
canvas_rect: rl.Rectangle,
|
||||
zoom: f32,
|
||||
pan_offset: rl.Vector2,
|
||||
is_panning: bool,
|
||||
pan_start: rl.Vector2,
|
||||
pan_offset_start: rl.Vector2,
|
||||
show_color_panel: bool,
|
||||
show_adjust_panel: bool,
|
||||
show_debug_overlay: bool,
|
||||
show_before_preview: bool,
|
||||
selection_active: bool,
|
||||
selection_rect: rl.Rectangle,
|
||||
selection_drag_start: rl.Vector2,
|
||||
selection_drag_origin: rl.Rectangle,
|
||||
selection_resize_anchor: rl.Vector2,
|
||||
is_moving_selection: bool,
|
||||
is_resizing_selection: bool,
|
||||
active_selection_handle: Selection_Handle,
|
||||
recent_colors: [EDITOR_RECENT_COLOR_MAX]rl.Color,
|
||||
recent_color_count: int,
|
||||
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,
|
||||
contrast = 0,
|
||||
saturation = 0,
|
||||
tint_color = rl.WHITE,
|
||||
tint_amount = 0,
|
||||
filter = .None,
|
||||
}
|
||||
}
|
||||
|
||||
editor_colors_equal :: proc(a, b: rl.Color) -> bool {
|
||||
return a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3]
|
||||
}
|
||||
|
||||
editor_push_recent_color :: proc(ed: ^Panel_Editor_State, color: rl.Color) {
|
||||
if color[3] == 0 { return }
|
||||
if ed.recent_color_count > 0 && editor_colors_equal(ed.recent_colors[0], color) {
|
||||
return
|
||||
}
|
||||
|
||||
found := -1
|
||||
for i in 0..<ed.recent_color_count {
|
||||
if editor_colors_equal(ed.recent_colors[i], color) {
|
||||
found = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found >= 0 {
|
||||
for i := found; i > 0; i -= 1 {
|
||||
ed.recent_colors[i] = ed.recent_colors[i-1]
|
||||
}
|
||||
ed.recent_colors[0] = color
|
||||
return
|
||||
}
|
||||
|
||||
limit := min(ed.recent_color_count, EDITOR_RECENT_COLOR_MAX - 1)
|
||||
for i := limit; i > 0; i -= 1 {
|
||||
ed.recent_colors[i] = ed.recent_colors[i-1]
|
||||
}
|
||||
ed.recent_colors[0] = color
|
||||
if ed.recent_color_count < EDITOR_RECENT_COLOR_MAX {
|
||||
ed.recent_color_count += 1
|
||||
}
|
||||
}
|
||||
|
||||
to_cstr :: proc(s: string) -> cstring {
|
||||
if len(s) == 0 { return "" }
|
||||
buf := make([]u8, len(s)+1)
|
||||
for i in 0..<len(s) { buf[i] = s[i] }
|
||||
buf[len(s)] = 0
|
||||
return cstring(rawptr(&buf[0]))
|
||||
}
|
||||
@ -259,8 +259,20 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
if rl.IsKeyPressed(.SLASH) { toggle_help_overlay(&app.show_help_overlay) }
|
||||
if rl.IsKeyPressed(.ESCAPE) {
|
||||
if app.editor.active {
|
||||
editor_close(&app)
|
||||
push_status(&app.status_msg, &app.action_log, "Edit cancelled")
|
||||
if app.editor.selection_active {
|
||||
app.editor.selection_active = false
|
||||
app.editor.is_moving_selection = false
|
||||
app.editor.is_resizing_selection = false
|
||||
app.editor.active_selection_handle = .None
|
||||
push_status(&app.status_msg, &app.action_log, "Selection cleared")
|
||||
} else if app.editor.show_color_panel {
|
||||
app.editor.show_color_panel = false
|
||||
} else if app.editor.show_adjust_panel {
|
||||
app.editor.show_adjust_panel = false
|
||||
} else {
|
||||
editor_close(&app)
|
||||
push_status(&app.status_msg, &app.action_log, "Edit cancelled")
|
||||
}
|
||||
} else {
|
||||
close_help_overlay_if_open(&app.show_help_overlay)
|
||||
}
|
||||
@ -273,6 +285,21 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
if rl.IsKeyPressed(.E) {
|
||||
app.editor.current_tool = .Eraser
|
||||
}
|
||||
if rl.IsKeyPressed(.Q) {
|
||||
push_status(&app.status_msg, &app.action_log, editor_transform_canvas(&app.editor, .Rotate_CCW))
|
||||
}
|
||||
if rl.IsKeyPressed(.W) {
|
||||
push_status(&app.status_msg, &app.action_log, editor_transform_canvas(&app.editor, .Rotate_CW))
|
||||
}
|
||||
if rl.IsKeyPressed(.X) {
|
||||
push_status(&app.status_msg, &app.action_log, editor_transform_canvas(&app.editor, .Rotate_180))
|
||||
}
|
||||
if rl.IsKeyPressed(.H) {
|
||||
push_status(&app.status_msg, &app.action_log, editor_transform_canvas(&app.editor, .Flip_Horizontal))
|
||||
}
|
||||
if rl.IsKeyPressed(.V) {
|
||||
push_status(&app.status_msg, &app.action_log, editor_transform_canvas(&app.editor, .Flip_Vertical))
|
||||
}
|
||||
if rl.IsKeyPressed(.L) {
|
||||
app.editor.current_tool = .Line
|
||||
}
|
||||
@ -285,6 +312,36 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
if rl.IsKeyPressed(.I) {
|
||||
app.editor.current_tool = .Color_Pick
|
||||
}
|
||||
if rl.IsKeyPressed(.U) {
|
||||
app.editor.current_tool = .Fill
|
||||
}
|
||||
if rl.IsKeyPressed(.K) {
|
||||
app.editor.current_tool = .Crop
|
||||
}
|
||||
if rl.IsKeyPressed(.M) {
|
||||
app.editor.current_tool = .Move
|
||||
}
|
||||
if rl.IsKeyPressed(.DELETE) || rl.IsKeyPressed(.BACKSPACE) {
|
||||
if app.editor.selection_active {
|
||||
push_status(&app.status_msg, &app.action_log, editor_apply_clear_selection(&app.editor))
|
||||
}
|
||||
}
|
||||
nudge_step: f32 = 1
|
||||
if rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) {
|
||||
nudge_step = 10
|
||||
}
|
||||
if rl.IsKeyPressed(.LEFT) && app.editor.selection_active {
|
||||
push_status(&app.status_msg, &app.action_log, editor_nudge_selection(&app.editor, -nudge_step, 0))
|
||||
}
|
||||
if rl.IsKeyPressed(.RIGHT) && app.editor.selection_active {
|
||||
push_status(&app.status_msg, &app.action_log, editor_nudge_selection(&app.editor, nudge_step, 0))
|
||||
}
|
||||
if rl.IsKeyPressed(.UP) && app.editor.selection_active {
|
||||
push_status(&app.status_msg, &app.action_log, editor_nudge_selection(&app.editor, 0, -nudge_step))
|
||||
}
|
||||
if rl.IsKeyPressed(.DOWN) && app.editor.selection_active {
|
||||
push_status(&app.status_msg, &app.action_log, editor_nudge_selection(&app.editor, 0, nudge_step))
|
||||
}
|
||||
if rl.IsKeyPressed(.LEFT_BRACKET) {
|
||||
app.editor.brush_size = max(app.editor.brush_size - 2, 1)
|
||||
}
|
||||
@ -292,9 +349,19 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
app.editor.brush_size = min(app.editor.brush_size + 2, 80)
|
||||
}
|
||||
if rl.IsKeyPressed(.ZERO) {
|
||||
app.editor.zoom = 1.0
|
||||
app.editor.pan_offset = rl.Vector2{}
|
||||
app.editor.needs_redraw = true
|
||||
editor_fit_to_view(&app.editor)
|
||||
}
|
||||
if rl.IsKeyPressed(.F) {
|
||||
editor_fit_to_view(&app.editor)
|
||||
}
|
||||
if rl.IsKeyPressed(.D) {
|
||||
editor_zoom_actual_size(&app.editor)
|
||||
}
|
||||
if rl.IsKeyPressed(.G) {
|
||||
app.editor.show_debug_overlay = !app.editor.show_debug_overlay
|
||||
}
|
||||
if rl.IsKeyPressed(.P) {
|
||||
app.editor.show_before_preview = !app.editor.show_before_preview
|
||||
}
|
||||
if rl.IsKeyPressed(.EQUAL) || rl.IsKeyPressed(.KP_ADD) {
|
||||
app.editor.zoom = min(app.editor.zoom + 0.25, 8.0)
|
||||
@ -305,16 +372,39 @@ run_gui_app :: proc(state: ^core.Comic_State) -> shared.App_Error {
|
||||
app.editor.needs_redraw = true
|
||||
}
|
||||
if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) {
|
||||
shift_down := rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT)
|
||||
if rl.IsKeyPressed(.Z) {
|
||||
editor_undo(&app.editor)
|
||||
if shift_down {
|
||||
editor_redo(&app.editor)
|
||||
} else {
|
||||
editor_undo(&app.editor)
|
||||
}
|
||||
}
|
||||
if rl.IsKeyPressed(.Y) {
|
||||
editor_redo(&app.editor)
|
||||
}
|
||||
if rl.IsKeyPressed(.D) {
|
||||
if app.editor.selection_active {
|
||||
push_status(&app.status_msg, &app.action_log, editor_apply_duplicate_selection(&app.editor))
|
||||
}
|
||||
}
|
||||
if rl.IsKeyPressed(.A) {
|
||||
push_status(&app.status_msg, &app.action_log, editor_select_all(&app.editor))
|
||||
}
|
||||
if rl.IsKeyPressed(.S) {
|
||||
push_status(&app.status_msg, &app.action_log, editor_commit(&app))
|
||||
}
|
||||
}
|
||||
if rl.IsKeyPressed(.ENTER) || rl.IsKeyPressed(.KP_ENTER) {
|
||||
push_status(&app.status_msg, &app.action_log, editor_commit(&app))
|
||||
}
|
||||
if rl.IsKeyPressed(.A) && !rl.IsKeyDown(.LEFT_CONTROL) && !rl.IsKeyDown(.RIGHT_CONTROL) {
|
||||
app.editor.show_adjust_panel = !app.editor.show_adjust_panel
|
||||
}
|
||||
if rl.IsKeyPressed(.O) && !rl.IsKeyDown(.LEFT_CONTROL) && !rl.IsKeyDown(.RIGHT_CONTROL) {
|
||||
app.editor.shape_outline_only = !app.editor.shape_outline_only
|
||||
app.editor.current_stroke.filled = !app.editor.shape_outline_only
|
||||
}
|
||||
}
|
||||
|
||||
// Compact mode forces the sidebar collapsed
|
||||
|
||||