Skip to content

Commit 1e2de80

Browse files
committed
feat: setup automated showcase graphic generator
1 parent 6c22f01 commit 1e2de80

4 files changed

Lines changed: 220 additions & 0 deletions

File tree

.github/images/inv_slot.png

181 Bytes
Loading
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import os
2+
import json
3+
import urllib.request
4+
from io import BytesIO
5+
from PIL import Image, ImageDraw, ImageFont, ImageFilter
6+
7+
CONFIG_PATH = "../showcase_config.json"
8+
INV_SLOT_PATH = "../images/inv_slot.png"
9+
OUTPUT_PATH = "../images/showcase_output.png"
10+
CUSTOM_ASSETS_DIR = "../images/showcase_assets"
11+
12+
MC_ASSETS_BASE = "https://raw.githubusercontent.com/Owen1212055/mc-assets/master/assets/minecraft/textures/"
13+
14+
GRID_COLS = 9
15+
GRID_ROWS = 3
16+
SLOT_SIZE = 162
17+
ITEM_SIZE = 144
18+
GAP = 54
19+
SIDEBAR_WIDTH = 250
20+
21+
def download_image(url):
22+
try:
23+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
24+
with urllib.request.urlopen(req) as response:
25+
return Image.open(BytesIO(response.read())).convert("RGBA")
26+
except Exception as e:
27+
print(f"Failed to download {url}: {e}")
28+
# Return a transparent dummy image
29+
return Image.new("RGBA", (256, 256), (0, 0, 0, 0))
30+
31+
def get_texture(path, is_block):
32+
# Try local first
33+
local_path = os.path.join(CUSTOM_ASSETS_DIR, f"{path}.png")
34+
if os.path.exists(local_path):
35+
img = Image.open(local_path).convert("RGBA")
36+
else:
37+
url = f"{MC_ASSETS_BASE}{path}.png"
38+
img = download_image(url)
39+
40+
# Scale from 256x256 to 144x144
41+
if is_block:
42+
# Blocks use Lanczos for smooth 3D edges
43+
img = img.resize((ITEM_SIZE, ITEM_SIZE), Image.Resampling.LANCZOS)
44+
else:
45+
# Items are downscaled to 16x16 nearest neighbor, then upscaled to 144x144 nearest neighbor
46+
img = img.resize((16, 16), Image.Resampling.NEAREST)
47+
img = img.resize((ITEM_SIZE, ITEM_SIZE), Image.Resampling.NEAREST)
48+
49+
return img
50+
51+
def create_background(width, height, override_path):
52+
if override_path and os.path.exists(override_path):
53+
bg = Image.open(override_path).convert("RGBA")
54+
# Resize to fill, center crop
55+
ratio = max(width / bg.width, height / bg.height)
56+
new_size = (int(bg.width * ratio), int(bg.height * ratio))
57+
bg = bg.resize(new_size, Image.Resampling.LANCZOS)
58+
left = (bg.width - width) / 2
59+
top = (bg.height - height) / 2
60+
bg = bg.crop((left, top, left + width, top + height))
61+
bg = bg.filter(ImageFilter.GaussianBlur(radius=5))
62+
return bg
63+
else:
64+
# Default gradient (solid color as a fallback if no dynamic gen is wanted)
65+
bg = Image.new("RGBA", (width, height), (211, 140, 83, 255))
66+
return bg
67+
68+
def main():
69+
# Resolve paths relative to script
70+
script_dir = os.path.dirname(os.path.abspath(__file__))
71+
os.chdir(script_dir)
72+
73+
if not os.path.exists(CONFIG_PATH):
74+
print(f"Config not found at {CONFIG_PATH}")
75+
return
76+
77+
with open(CONFIG_PATH, "r") as f:
78+
config = json.load(f)
79+
80+
grid_width = GRID_COLS * SLOT_SIZE
81+
grid_height = GRID_ROWS * SLOT_SIZE
82+
83+
canvas_width = SIDEBAR_WIDTH + grid_width + 50
84+
canvas_height = 50 + grid_height + GAP + grid_height + 50
85+
86+
bg = create_background(canvas_width, canvas_height, config.get("background_override"))
87+
88+
try:
89+
inv_slot = Image.open(INV_SLOT_PATH).convert("RGBA")
90+
except Exception:
91+
# fallback if inv_slot doesn't exist yet
92+
inv_slot = Image.new("RGBA", (SLOT_SIZE, SLOT_SIZE), (255, 0, 0, 100))
93+
94+
off_grid = Image.new("RGBA", (grid_width, grid_height), (0,0,0,0))
95+
on_grid = Image.new("RGBA", (grid_width, grid_height), (0,0,0,0))
96+
97+
# Draw empty slots
98+
for r in range(GRID_ROWS):
99+
for c in range(GRID_COLS):
100+
x = c * SLOT_SIZE
101+
y = r * SLOT_SIZE
102+
off_grid.paste(inv_slot, (x, y), inv_slot)
103+
on_grid.paste(inv_slot, (x, y), inv_slot)
104+
105+
# Draw items
106+
for item in config.get("items", []):
107+
slot = item.get("slot", 0)
108+
r = slot // GRID_COLS
109+
c = slot % GRID_COLS
110+
if r >= GRID_ROWS:
111+
continue
112+
113+
x = c * SLOT_SIZE + (SLOT_SIZE - ITEM_SIZE) // 2
114+
y = r * SLOT_SIZE + (SLOT_SIZE - ITEM_SIZE) // 2
115+
116+
is_block = (item.get("type") == "block")
117+
118+
if "off_texture" in item:
119+
off_img = get_texture(item["off_texture"], is_block)
120+
off_grid.paste(off_img, (x, y), off_img)
121+
122+
if "on_texture" in item:
123+
on_img = get_texture(item["on_texture"], is_block)
124+
on_grid.paste(on_img, (x, y), on_img)
125+
126+
# Paste grids to bg
127+
grid_x = SIDEBAR_WIDTH
128+
top_y = 50
129+
bottom_y = top_y + grid_height + GAP
130+
131+
bg.paste(off_grid, (grid_x, top_y), off_grid)
132+
bg.paste(on_grid, (grid_x, bottom_y), on_grid)
133+
134+
# Draw sidebar
135+
draw = ImageDraw.Draw(bg)
136+
try:
137+
# Try to get a system font or download one
138+
url = "https://github.com/googlefonts/opensans/raw/main/fonts/ttf/OpenSans-Bold.ttf"
139+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
140+
font_bytes = BytesIO(urllib.request.urlopen(req).read())
141+
font = ImageFont.truetype(font_bytes, 100)
142+
except Exception:
143+
font = ImageFont.load_default()
144+
145+
# Draw rotated text
146+
def draw_rotated_text(text, x, y):
147+
txt_img = Image.new('RGBA', (300, 120), (0, 0, 0, 0))
148+
txt_draw = ImageDraw.Draw(txt_img)
149+
txt_draw.text((0, 0), text, font=font, fill=(255, 255, 255, 255))
150+
txt_img = txt_img.rotate(90, expand=1)
151+
bg.paste(txt_img, (x, y), txt_img)
152+
153+
draw_rotated_text("OFF", 80, top_y + 100)
154+
draw_rotated_text("ON", 80, bottom_y + 150)
155+
156+
# Draw arrows down
157+
arrow_y = top_y + grid_height + (GAP // 2) - 30
158+
draw.polygon([(100, arrow_y), (150, arrow_y), (125, arrow_y + 40)], fill=(255, 255, 255, 255))
159+
draw.polygon([(100, arrow_y+20), (150, arrow_y+20), (125, arrow_y + 60)], fill=(255, 255, 255, 255))
160+
161+
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
162+
bg.save(OUTPUT_PATH)
163+
print(f"Saved to {os.path.abspath(OUTPUT_PATH)}")
164+
165+
if __name__ == "__main__":
166+
main()

.github/showcase_config.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"title": "ViaBackwards-Plus Showcase",
3+
"background_override": "",
4+
"items": [
5+
{
6+
"slot": 0,
7+
"type": "block",
8+
"off_texture": "block/oak_log",
9+
"on_texture": "block/acacia_log"
10+
},
11+
{
12+
"slot": 1,
13+
"type": "item",
14+
"off_texture": "item/diamond_sword",
15+
"on_texture": "item/netherite_sword"
16+
}
17+
]
18+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Generate Showcase Graphic
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
paths:
7+
- '.github/showcase_config.json'
8+
9+
jobs:
10+
build-showcase:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: write
14+
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: '3.11'
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install Pillow requests
28+
29+
- name: Run Generator Script
30+
run: python .github/scripts/generate_showcase.py
31+
32+
- name: Commit and push changes
33+
uses: stefanzweifel/git-auto-commit-action@v5
34+
with:
35+
commit_message: "chore: auto-generate showcase graphic"
36+
file_pattern: '.github/images/showcase_output.png'

0 commit comments

Comments
 (0)