Initial commit
This commit is contained in:
127
tools/make_assets.py
Normal file
127
tools/make_assets.py
Normal 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()
|
||||
Reference in New Issue
Block a user