Add shape-aware sprite families
Browse files
app.py
CHANGED
|
@@ -592,6 +592,100 @@ def draw_sprite(spec: AssetSpec, rng: random.Random) -> bytes:
|
|
| 592 |
accent = colors[1] + (255,)
|
| 593 |
trim = colors[3] + (255,)
|
| 594 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
if role in ("bullet", "projectile", "laser", "shot") or any(word in role_prompt for word in ("bullet", "projectile", "laser", "energy")):
|
| 596 |
glow = colors[1] + (85,)
|
| 597 |
core_width = rng.randint(7, 13) * scale
|
|
@@ -944,23 +1038,29 @@ def build_prompt_preview(specs: list[AssetSpec]) -> str:
|
|
| 944 |
return "\n\n".join(f"{spec.role}:\n{spec.prompt}" for spec in specs)
|
| 945 |
|
| 946 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 947 |
def generate_images_and_game(html_code: str, roles: str, style_hint: str):
|
| 948 |
if not html_code.strip():
|
| 949 |
-
return "", "Paste HTML game code first.", [], "", ""
|
| 950 |
|
| 951 |
specs = parse_assets(roles, style_hint or "pixel art style")
|
| 952 |
if not specs:
|
| 953 |
-
return html_code, "Add at least one asset role, like `player: brave knight`.", [], "", build_preview(html_code)
|
| 954 |
|
| 955 |
assets: dict[str, str] = {}
|
| 956 |
gallery = []
|
| 957 |
errors = []
|
|
|
|
| 958 |
run_id = time.time_ns()
|
| 959 |
|
| 960 |
for index, spec in enumerate(specs):
|
| 961 |
data_uri, gallery_path, error = generate_asset(spec, index, run_id)
|
| 962 |
assets[spec.role] = data_uri
|
| 963 |
gallery.append((gallery_path, f"{spec.role} -> {spec.filename}"))
|
|
|
|
| 964 |
if error:
|
| 965 |
errors.append(f"{spec.role}: fallback used ({error})")
|
| 966 |
|
|
@@ -970,7 +1070,7 @@ def generate_images_and_game(html_code: str, roles: str, style_hint: str):
|
|
| 970 |
status = f"Generated and embedded {len(specs)} fresh asset(s) using {source}. Run {str(run_id)[-6:]}."
|
| 971 |
if errors:
|
| 972 |
status += "\n\n" + "\n".join(f"{item}: no HF image returned, used local style fallback" for item in [e.split(':', 1)[0] for e in errors])
|
| 973 |
-
return rewritten, status, gallery, build_prompt_preview(specs), build_preview(rewritten)
|
| 974 |
|
| 975 |
|
| 976 |
with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
|
@@ -1014,6 +1114,11 @@ with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
|
| 1014 |
lines=8,
|
| 1015 |
interactive=False,
|
| 1016 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1017 |
|
| 1018 |
gr.Markdown("## Game preview")
|
| 1019 |
preview = gr.HTML(build_preview(STARTER_HTML))
|
|
@@ -1021,7 +1126,7 @@ with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
|
| 1021 |
generate_btn.click(
|
| 1022 |
fn=generate_images_and_game,
|
| 1023 |
inputs=[html_input, roles, style],
|
| 1024 |
-
outputs=[output_code, status, gallery, prompt_preview, preview],
|
| 1025 |
)
|
| 1026 |
|
| 1027 |
|
|
|
|
| 592 |
accent = colors[1] + (255,)
|
| 593 |
trim = colors[3] + (255,)
|
| 594 |
|
| 595 |
+
if any(word in role_prompt for word in ("car", "racing", "racer", "buggy", "vehicle", "truck")):
|
| 596 |
+
shadow = (int(cx - 43 * scale), int(cy + 28 * scale), int(cx + 43 * scale), int(cy + 42 * scale))
|
| 597 |
+
draw.ellipse(shadow, fill=(0, 0, 0, 58))
|
| 598 |
+
draw.rounded_rectangle(
|
| 599 |
+
(cx - 34 * scale, cy - 42 * scale, cx + 34 * scale, cy + 42 * scale),
|
| 600 |
+
radius=int(18 * scale),
|
| 601 |
+
fill=outline,
|
| 602 |
+
)
|
| 603 |
+
draw.rounded_rectangle(
|
| 604 |
+
(cx - 26 * scale, cy - 34 * scale, cx + 26 * scale, cy + 34 * scale),
|
| 605 |
+
radius=int(13 * scale),
|
| 606 |
+
fill=body,
|
| 607 |
+
)
|
| 608 |
+
draw.rectangle((cx - 15 * scale, cy - 24 * scale, cx + 15 * scale, cy - 5 * scale), fill=accent)
|
| 609 |
+
draw.rectangle((cx - 18 * scale, cy + 8 * scale, cx + 18 * scale, cy + 27 * scale), fill=trim)
|
| 610 |
+
for side in (-1, 1):
|
| 611 |
+
draw.rounded_rectangle(
|
| 612 |
+
(cx + side * 27 * scale - 9 * scale, cy - 30 * scale, cx + side * 27 * scale + 9 * scale, cy - 13 * scale),
|
| 613 |
+
radius=int(5 * scale),
|
| 614 |
+
fill=(20, 22, 27, 255),
|
| 615 |
+
)
|
| 616 |
+
draw.rounded_rectangle(
|
| 617 |
+
(cx + side * 27 * scale - 9 * scale, cy + 15 * scale, cx + side * 27 * scale + 9 * scale, cy + 32 * scale),
|
| 618 |
+
radius=int(5 * scale),
|
| 619 |
+
fill=(20, 22, 27, 255),
|
| 620 |
+
)
|
| 621 |
+
out = io.BytesIO()
|
| 622 |
+
image.save(out, format="PNG")
|
| 623 |
+
return out.getvalue()
|
| 624 |
+
|
| 625 |
+
if any(word in role_prompt for word in ("turret", "tower", "cannon", "launcher")):
|
| 626 |
+
draw.ellipse((cx - 38 * scale, cy + 22 * scale, cx + 38 * scale, cy + 45 * scale), fill=(0, 0, 0, 60))
|
| 627 |
+
draw.ellipse((cx - 36 * scale, cy - 12 * scale, cx + 36 * scale, cy + 42 * scale), fill=outline)
|
| 628 |
+
draw.ellipse((cx - 28 * scale, cy - 5 * scale, cx + 28 * scale, cy + 34 * scale), fill=body)
|
| 629 |
+
barrel_angle = rng.choice((-0.45, -0.2, 0, 0.2, 0.45))
|
| 630 |
+
length = 52 * scale
|
| 631 |
+
bx = cx + math.sin(barrel_angle) * length
|
| 632 |
+
by = cy - 38 * scale
|
| 633 |
+
draw.line((cx, cy - 5 * scale, bx, by), fill=outline, width=max(10, int(17 * scale)))
|
| 634 |
+
draw.line((cx, cy - 5 * scale, bx, by), fill=trim, width=max(5, int(9 * scale)))
|
| 635 |
+
draw.rectangle((cx - 19 * scale, cy + 15 * scale, cx + 19 * scale, cy + 39 * scale), fill=accent)
|
| 636 |
+
out = io.BytesIO()
|
| 637 |
+
image.save(out, format="PNG")
|
| 638 |
+
return out.getvalue()
|
| 639 |
+
|
| 640 |
+
if role in ("coin", "gem", "key", "orb", "pickup", "collectible") or any(
|
| 641 |
+
word in role_prompt for word in ("coin", "gem", "key", "pickup", "collectible", "powerup", "power-up", "treasure")
|
| 642 |
+
):
|
| 643 |
+
draw.ellipse((cx - 33 * scale, cy - 33 * scale, cx + 33 * scale, cy + 33 * scale), fill=outline)
|
| 644 |
+
if any(word in role_prompt for word in ("gem", "crystal", "diamond")):
|
| 645 |
+
points = [
|
| 646 |
+
(cx, cy - 43 * scale),
|
| 647 |
+
(cx + 36 * scale, cy - 8 * scale),
|
| 648 |
+
(cx + 20 * scale, cy + 40 * scale),
|
| 649 |
+
(cx - 20 * scale, cy + 40 * scale),
|
| 650 |
+
(cx - 36 * scale, cy - 8 * scale),
|
| 651 |
+
]
|
| 652 |
+
draw.polygon(points, fill=body)
|
| 653 |
+
draw.polygon([(cx, cy - 34 * scale), (cx + 18 * scale, cy - 4 * scale), (cx, cy + 28 * scale), (cx - 18 * scale, cy - 4 * scale)], fill=accent)
|
| 654 |
+
elif "key" in role_prompt:
|
| 655 |
+
draw.ellipse((cx - 30 * scale, cy - 20 * scale, cx + 8 * scale, cy + 18 * scale), fill=body)
|
| 656 |
+
draw.ellipse((cx - 17 * scale, cy - 8 * scale, cx - 3 * scale, cy + 6 * scale), fill=(0, 0, 0, 0))
|
| 657 |
+
draw.rounded_rectangle((cx + 2 * scale, cy - 5 * scale, cx + 43 * scale, cy + 7 * scale), radius=int(5 * scale), fill=accent)
|
| 658 |
+
draw.rectangle((cx + 30 * scale, cy + 4 * scale, cx + 43 * scale, cy + 17 * scale), fill=trim)
|
| 659 |
+
else:
|
| 660 |
+
draw.ellipse((cx - 27 * scale, cy - 27 * scale, cx + 27 * scale, cy + 27 * scale), fill=body)
|
| 661 |
+
draw.ellipse((cx - 17 * scale, cy - 17 * scale, cx + 17 * scale, cy + 17 * scale), outline=trim, width=max(3, int(7 * scale)))
|
| 662 |
+
draw.arc((cx - 12 * scale, cy - 20 * scale, cx + 20 * scale, cy + 13 * scale), 195, 300, fill=(255, 255, 255, 180), width=max(2, int(4 * scale)))
|
| 663 |
+
out = io.BytesIO()
|
| 664 |
+
image.save(out, format="PNG")
|
| 665 |
+
return out.getvalue()
|
| 666 |
+
|
| 667 |
+
if any(word in role_prompt for word in ("slime", "blob", "gel", "jelly")):
|
| 668 |
+
draw.ellipse((cx - 40 * scale, cy + 24 * scale, cx + 40 * scale, cy + 42 * scale), fill=(0, 0, 0, 55))
|
| 669 |
+
draw.pieslice((cx - 40 * scale, cy - 28 * scale, cx + 40 * scale, cy + 52 * scale), 180, 360, fill=outline)
|
| 670 |
+
draw.pieslice((cx - 32 * scale, cy - 20 * scale, cx + 32 * scale, cy + 42 * scale), 180, 360, fill=body)
|
| 671 |
+
for side in (-1, 1):
|
| 672 |
+
draw.ellipse((cx + side * 11 * scale - 5 * scale, cy + 3 * scale, cx + side * 11 * scale + 5 * scale, cy + 13 * scale), fill=(255, 255, 255, 230))
|
| 673 |
+
draw.arc((cx - 12 * scale, cy + 12 * scale, cx + 12 * scale, cy + 27 * scale), 10, 170, fill=trim, width=max(2, int(4 * scale)))
|
| 674 |
+
out = io.BytesIO()
|
| 675 |
+
image.save(out, format="PNG")
|
| 676 |
+
return out.getvalue()
|
| 677 |
+
|
| 678 |
+
if any(word in role_prompt for word in ("beetle", "bug", "spider", "insect", "crawler", "creep")):
|
| 679 |
+
draw.ellipse((cx - 28 * scale, cy - 34 * scale, cx + 28 * scale, cy + 34 * scale), fill=outline)
|
| 680 |
+
draw.ellipse((cx - 21 * scale, cy - 27 * scale, cx + 21 * scale, cy + 29 * scale), fill=body)
|
| 681 |
+
draw.line((cx, cy - 25 * scale, cx, cy + 30 * scale), fill=trim, width=max(2, int(4 * scale)))
|
| 682 |
+
for y in (-18, 0, 18):
|
| 683 |
+
for side in (-1, 1):
|
| 684 |
+
draw.line((cx + side * 18 * scale, cy + y * scale, cx + side * 45 * scale, cy + (y + rng.choice((-10, 10))) * scale), fill=outline, width=max(3, int(5 * scale)))
|
| 685 |
+
out = io.BytesIO()
|
| 686 |
+
image.save(out, format="PNG")
|
| 687 |
+
return out.getvalue()
|
| 688 |
+
|
| 689 |
if role in ("bullet", "projectile", "laser", "shot") or any(word in role_prompt for word in ("bullet", "projectile", "laser", "energy")):
|
| 690 |
glow = colors[1] + (85,)
|
| 691 |
core_width = rng.randint(7, 13) * scale
|
|
|
|
| 1038 |
return "\n\n".join(f"{spec.role}:\n{spec.prompt}" for spec in specs)
|
| 1039 |
|
| 1040 |
|
| 1041 |
+
def build_model_report(rows: list[tuple[str, str]]) -> str:
|
| 1042 |
+
return "\n".join(f"{role}: {source}" for role, source in rows)
|
| 1043 |
+
|
| 1044 |
+
|
| 1045 |
def generate_images_and_game(html_code: str, roles: str, style_hint: str):
|
| 1046 |
if not html_code.strip():
|
| 1047 |
+
return "", "Paste HTML game code first.", [], "", "", ""
|
| 1048 |
|
| 1049 |
specs = parse_assets(roles, style_hint or "pixel art style")
|
| 1050 |
if not specs:
|
| 1051 |
+
return html_code, "Add at least one asset role, like `player: brave knight`.", [], "", "", build_preview(html_code)
|
| 1052 |
|
| 1053 |
assets: dict[str, str] = {}
|
| 1054 |
gallery = []
|
| 1055 |
errors = []
|
| 1056 |
+
model_rows = []
|
| 1057 |
run_id = time.time_ns()
|
| 1058 |
|
| 1059 |
for index, spec in enumerate(specs):
|
| 1060 |
data_uri, gallery_path, error = generate_asset(spec, index, run_id)
|
| 1061 |
assets[spec.role] = data_uri
|
| 1062 |
gallery.append((gallery_path, f"{spec.role} -> {spec.filename}"))
|
| 1063 |
+
model_rows.append((spec.role, HF_IMAGE_MODEL if error is None and HF_TOKEN else "local style fallback"))
|
| 1064 |
if error:
|
| 1065 |
errors.append(f"{spec.role}: fallback used ({error})")
|
| 1066 |
|
|
|
|
| 1070 |
status = f"Generated and embedded {len(specs)} fresh asset(s) using {source}. Run {str(run_id)[-6:]}."
|
| 1071 |
if errors:
|
| 1072 |
status += "\n\n" + "\n".join(f"{item}: no HF image returned, used local style fallback" for item in [e.split(':', 1)[0] for e in errors])
|
| 1073 |
+
return rewritten, status, gallery, build_prompt_preview(specs), build_model_report(model_rows), build_preview(rewritten)
|
| 1074 |
|
| 1075 |
|
| 1076 |
with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
|
|
|
| 1114 |
lines=8,
|
| 1115 |
interactive=False,
|
| 1116 |
)
|
| 1117 |
+
model_report = gr.Textbox(
|
| 1118 |
+
label="Model/source used by role",
|
| 1119 |
+
lines=5,
|
| 1120 |
+
interactive=False,
|
| 1121 |
+
)
|
| 1122 |
|
| 1123 |
gr.Markdown("## Game preview")
|
| 1124 |
preview = gr.HTML(build_preview(STARTER_HTML))
|
|
|
|
| 1126 |
generate_btn.click(
|
| 1127 |
fn=generate_images_and_game,
|
| 1128 |
inputs=[html_input, roles, style],
|
| 1129 |
+
outputs=[output_code, status, gallery, prompt_preview, model_report, preview],
|
| 1130 |
)
|
| 1131 |
|
| 1132 |
|