#!/usr/bin/env python3 """Erzeugt die Ausrichtungsvorlage fuer OBS. Ein 1920x1080-Bild, das Abdu in OBS als oberste Bildquelle einfuegt. Danach zieht er Facecam, Chat und Overlays so, dass sie in die markierten Zonen passen, und loescht die Quelle wieder. Aufruf: python scripts/make-layout-template.py Braucht: Pillow (nur zum Erzeugen der Vorlage, nicht zur Laufzeit) Die Zahlen hier und in config/layout.yaml muessen uebereinstimmen - siehe docs/obs-layout.md. """ from PIL import Image, ImageDraw, ImageFont from pathlib import Path W, H = 1920, 1080 CAM = (0, 0, 480, 270) # x, y, w, h GAME = (515, 0, 890, 1080) # der Bereich, der ins Hochformat wandert CHAT_X = 1405 # ab hier darf Chat und Overlay stehen OUT = Path(__file__).resolve().parent.parent / "docs" / "assets" / "obs-layout-vorlage.png" def font(size, bold=True): for name in ("arialbd.ttf" if bold else "arial.ttf", "segoeuib.ttf", "DejaVuSans-Bold.ttf"): try: return ImageFont.truetype(name, size) except OSError: continue return ImageFont.load_default() def box(d, rect, color, width=6): x, y, w, h = rect d.rectangle([x, y, x + w - 1, y + h - 1], outline=color, width=width) def label(d, xy, text, color, size=30, bg=(0, 0, 0)): f = font(size) x, y = xy l, t, r, b = d.textbbox((x, y), text, font=f) d.rectangle([l - 10, t - 6, r + 10, b + 6], fill=bg) d.text((x, y), text, fill=color, font=f) img = Image.new("RGB", (W, H), (18, 18, 22)) d = ImageDraw.Draw(img) # Flaechen, die nicht ins Hochformat wandern, abdunkeln d.rectangle([0, 0, GAME[0] - 1, H], fill=(30, 26, 26)) d.rectangle([GAME[0] + GAME[2], 0, W, H], fill=(30, 26, 26)) # Raster alle 100 px for x in range(0, W, 100): d.line([(x, 0), (x, H)], fill=(52, 52, 60), width=1) for y in range(0, H, 100): d.line([(0, y), (W, y)], fill=(52, 52, 60), width=1) # Gameplay-Band: was hier steht, landet im Short box(d, GAME, (0, 200, 255), 8) d.rectangle([GAME[0], 0, GAME[0] + GAME[2] - 1, H - 1], outline=None, fill=None) label(d, (GAME[0] + 24, 108), "GAMEPLAY", (0, 200, 255), 40) label(d, (GAME[0] + 24, 162), "dieser Streifen wird zum Short", (0, 200, 255), 30) label(d, (GAME[0] + 24, 208), f"x {GAME[0]} bis {GAME[0] + GAME[2]} ({GAME[2]} px breit)", (150, 220, 255), 26) label(d, (GAME[0] + 24, H - 80), "Hier NICHTS drueberlegen: kein Chat, keine Alerts, keine Kamera", (255, 220, 120), 26) # Facecam box(d, CAM, (60, 230, 130), 8) label(d, (16, 300), "FACECAM", (60, 230, 130), 40) label(d, (16, 352), f"genau {CAM[2]} x {CAM[3]} px", (170, 240, 200), 28) label(d, (16, 396), "oben links, buendig an der Ecke", (170, 240, 200), 24) label(d, (16, 434), "Seitenverhaeltnis 16:9 —", (170, 240, 200), 24) label(d, (16, 468), "die Kamera NICHT verzerren", (170, 240, 200), 24) # Chatspalte d.line([(CHAT_X, 0), (CHAT_X, H)], fill=(255, 150, 60), width=6) label(d, (CHAT_X + 20, 108), "CHAT + ALERTS", (255, 150, 60), 34) label(d, (CHAT_X + 20, 160), f"nur rechts von x = {CHAT_X}", (255, 200, 150), 26) label(d, (CHAT_X + 20, 204), f"{W - CHAT_X} px Platz", (255, 200, 150), 26) # Linke Restspalte label(d, (16, 620), "Frei fuer Minimap,", (200, 170, 170), 26) label(d, (16, 656), "HUD, Kacheln —", (200, 170, 170), 26) label(d, (16, 692), "kommt nicht ins Short", (200, 170, 170), 26) # Kopfzeile label(d, (16, 16), "OBS-Ausrichtungsvorlage", (235, 235, 235), 30) label(d, (16, 60), "docs/obs-layout.md", (170, 170, 170), 22) OUT.parent.mkdir(parents=True, exist_ok=True) img.save(OUT) print(f"geschrieben: {OUT} ({img.size[0]}x{img.size[1]})")