Initial commit

This commit is contained in:
2026-10-09 02:48:24 +02:00
commit e05d973a8d
133 changed files with 101507 additions and 0 deletions

127
tools/make_assets.py Normal file
View File

@@ -0,0 +1,127 @@
"""Import the user's Arkham City artwork and build the app icon from it.
python tools/make_assets.py ["C:/Users/<you>/Downloads/Arkham City"]
tools/build_app.py runs this by itself when app/assets is missing or older than this script (from the default
folder or --art FOLDER).
Input folder: bac_logo_full.png (logo with transparent background) and wallpaper.jpg.
Output (app/assets/, git-ignored because it is Warner Bros. artwork):
wallpaper.jpg background of the filter panel
app_icon.png 256 px icon: the logo bat in black over a red audio waveform, on a white rounded tile
app_icon.ico Windows icon (16-256 px)
"""
import math
import os
import shutil
import struct
import sys
from PySide6.QtCore import QBuffer, QByteArray, QIODevice, QPointF, QRect, QRectF, Qt
from PySide6.QtGui import QColor, QGuiApplication, QImage, QLinearGradient, QPainter
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "app", "assets")
DEFAULT_SRC = os.path.join(os.path.expanduser("~"), "Downloads", "Arkham City")
RED, RED_HI = "#b0242c", "#d8434b" # the app accent
def crop_bat(logo):
"""The bat is the opaque block above the first fully transparent band (the text sits below it)."""
logo = logo.convertToFormat(QImage.Format.Format_ARGB32)
w, h = logo.width(), logo.height()
opaque = [any(logo.pixelColor(x, y).alpha() > 40 for x in range(0, w, 2)) for y in range(h)]
top = opaque.index(True)
bottom = top
while bottom < h and (opaque[bottom] or any(opaque[bottom:bottom + 12])):
bottom += 1
cols = [x for x in range(w) if any(logo.pixelColor(x, y).alpha() > 40 for y in range(top, bottom, 2))]
return logo.copy(QRect(cols[0], top, cols[-1] - cols[0] + 1, bottom - top))
def silhouette(bat, w, color):
"""The bat's outline filled with one flat colour (drops the stone texture and the lettering)."""
h = round(w * bat.height() / bat.width())
img = bat.scaled(w, h, Qt.AspectRatioMode.IgnoreAspectRatio, Qt.TransformationMode.SmoothTransformation)
p = QPainter(img)
p.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
p.fillRect(img.rect(), QColor(color))
p.end()
return img
def draw_icon(bat, s):
"""White rounded tile, a red audio waveform (rounded bars) across it, the logo bat in black on top."""
img = QImage(s, s, QImage.Format.Format_ARGB32)
img.fill(Qt.GlobalColor.transparent)
p = QPainter(img)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
p.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
small = s <= 24
m = s * 0.04
bg = QLinearGradient(0, 0, 0, s)
bg.setColorAt(0, QColor("#ffffff"))
bg.setColorAt(1, QColor("#ececec"))
p.setPen(Qt.PenStyle.NoPen)
p.setBrush(bg)
p.drawRoundedRect(QRectF(m, m, s - 2 * m, s - 2 * m), s * 0.22, s * 0.22)
# waveform: symmetric bars, tallest in the middle so they rise above and below the bat
cy = s / 2
n = 5 if small else 9
x0, x1 = s * 0.12, s * 0.88
step = (x1 - x0) / n
bar = step * 0.68
for i in range(n):
j = min(i, n - 1 - i) # mirror
t = (i + 0.5) / n
env = math.exp(-((t - 0.5) / 0.34) ** 2)
h = s * (0.10 + 0.32 * env * (0.65 + 0.35 * abs(math.sin(j * 2.3 + 0.7))))
g = QLinearGradient(0, cy - h, 0, cy + h)
g.setColorAt(0, QColor(RED_HI))
g.setColorAt(0.5, QColor(RED))
g.setColorAt(1, QColor(RED_HI))
p.setBrush(g)
p.drawRoundedRect(QRectF(x0 + i * step + (step - bar) / 2, cy - h, bar, 2 * h), bar / 2, bar / 2)
# the logo bat, flat black
sil = silhouette(bat, max(1, round(s * (0.84 if small else 0.78))), "#0d0d0d")
p.drawImage(QPointF((s - sil.width()) / 2, cy - sil.height() / 2), sil)
p.end()
return img
def write_ico(images, path):
"""Multi-size .ico with PNG-compressed entries (Windows Vista+)."""
blobs = []
for im in images:
ba = QByteArray()
buf = QBuffer(ba)
buf.open(QIODevice.OpenModeFlag.WriteOnly)
im.save(buf, "PNG")
blobs.append((im.width(), bytes(ba)))
out = struct.pack("<HHH", 0, 1, len(blobs))
off = 6 + 16 * len(blobs)
for w, b in blobs:
out += struct.pack("<BBBBHHII", w % 256, w % 256, 0, 0, 1, 32, len(b), off)
off += len(b)
out += b"".join(b for _, b in blobs)
open(path, "wb").write(out)
def main(src=None):
src = src or (sys.argv[1] if len(sys.argv) > 1 else DEFAULT_SRC)
global _APP
_APP = QGuiApplication.instance() or QGuiApplication(sys.argv[:1]) # needed for QPainter
os.makedirs(OUT, exist_ok=True)
logo = QImage(os.path.join(src, "bac_logo_full.png"))
if logo.isNull():
raise SystemExit("bac_logo_full.png not found in %s" % src)
bat = crop_bat(logo)
shutil.copy(os.path.join(src, "wallpaper.jpg"), os.path.join(OUT, "wallpaper.jpg"))
icon = draw_icon(bat, 256)
icon.save(os.path.join(OUT, "app_icon.png"))
write_ico([draw_icon(bat, n) for n in (16, 24, 32, 48, 64, 128, 256)], os.path.join(OUT, "app_icon.ico"))
print("assets written to", OUT)
if __name__ == "__main__":
main()