make_palettes.py now also emits kissaten-full-card.png and kissaten-portrait-card.png — each swatch shown with its palette index and hex code, so a colour can be identified while hand-editing without counting along an anonymous strip. Text is drawn with a hand-rolled 3x5 bitmap font covering digits, A-F and '#'; there is no font library available here. Drops palette_ref.png, superseded by kissaten-full.png. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
5.0 KiB
Python
132 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Emit the Kissaten palette in every format the art tools want.
|
|
|
|
python3 tools/make_palettes.py
|
|
|
|
.gpl GIMP / Inkscape / Krita / Aseprite
|
|
.ase Affinity (Photo, Designer, Publisher), Illustrator
|
|
.aco Affinity, Photoshop
|
|
.act Photoshop indexed-colour tables
|
|
.png 16x1 exact + a swatch strip, and a palette reference for generators
|
|
|
|
Two sets are written: the full 16 (scene work) and the 11-colour portrait
|
|
subset with the sky slots and the transparent index removed.
|
|
"""
|
|
import struct, os, sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from pixtool import PALETTE, write_png
|
|
|
|
NAMES = ["transparent", "outline", "dark wood", "mid wood", "light wood",
|
|
"lamp glow", "cream", "muted wall", "dusk deep", "dusk violet",
|
|
"dusk rose", "horizon amber", "coffee", "coat blue", "skin",
|
|
"warm white"]
|
|
PORTRAIT_IDX = [1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 15]
|
|
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "art")
|
|
|
|
|
|
def gpl(path, cols, names, title):
|
|
L = [f"GIMP Palette", f"Name: {title}", f"Columns: {len(cols)}", "#"]
|
|
for c, n in zip(cols, names):
|
|
L.append(f"{c[0]:3d} {c[1]:3d} {c[2]:3d}\t{n}")
|
|
open(path, "w").write("\n".join(L) + "\n")
|
|
|
|
|
|
def ase(path, cols, names):
|
|
blocks = b""
|
|
for c, n in zip(cols, names):
|
|
nm = n.encode("utf-16-be") + b"\x00\x00"
|
|
body = (struct.pack(">H", len(n) + 1) + nm + b"RGB "
|
|
+ struct.pack(">fff", c[0] / 255, c[1] / 255, c[2] / 255)
|
|
+ struct.pack(">H", 2)) # 2 = normal colour
|
|
blocks += struct.pack(">HI", 0x0001, len(body)) + body
|
|
open(path, "wb").write(b"ASEF" + struct.pack(">HHI", 1, 0, len(cols)) + blocks)
|
|
|
|
|
|
def aco(path, cols, names):
|
|
# v1 then v2 concatenated — v1 for old readers, v2 carries the names
|
|
v1 = struct.pack(">HH", 1, len(cols))
|
|
for c in cols:
|
|
v1 += struct.pack(">HHHHH", 0, c[0] * 257, c[1] * 257, c[2] * 257, 0)
|
|
v2 = struct.pack(">HH", 2, len(cols))
|
|
for c, n in zip(cols, names):
|
|
v2 += struct.pack(">HHHHH", 0, c[0] * 257, c[1] * 257, c[2] * 257, 0)
|
|
v2 += struct.pack(">I", len(n) + 1) + n.encode("utf-16-be") + b"\x00\x00"
|
|
open(path, "wb").write(v1 + v2)
|
|
|
|
|
|
def act(path, cols):
|
|
d = bytearray()
|
|
for c in cols:
|
|
d += bytes(c)
|
|
d += bytes(768 - len(d))
|
|
open(path, "wb").write(bytes(d))
|
|
|
|
|
|
def swatches(path, cols, sw=32, h=64):
|
|
write_png(path, [[c for c in cols for _ in range(sw)]] * h)
|
|
|
|
|
|
# 3x5 glyphs, enough for indices and hex codes. No font library here.
|
|
GLYPHS = {
|
|
"0": "111101101101111", "1": "010110010010111", "2": "111001111100111",
|
|
"3": "111001111001111", "4": "101101111001001", "5": "111100111001111",
|
|
"6": "111100111101111", "7": "111001001001001", "8": "111101111101111",
|
|
"9": "111101111001111", "A": "111101111101101", "B": "110101110101110",
|
|
"C": "111100100100111", "D": "110101101101110", "E": "111100111100111",
|
|
"F": "111100111100100", "#": "101111101111101", " ": "000000000000000",
|
|
"-": "000000111000000",
|
|
}
|
|
|
|
|
|
def _text(px, x, y, s, col, scale=2):
|
|
for ch in s.upper():
|
|
g = GLYPHS.get(ch, GLYPHS[" "])
|
|
for gy in range(5):
|
|
for gx in range(3):
|
|
if g[gy*3 + gx] == "1":
|
|
for sy in range(scale):
|
|
for sx in range(scale):
|
|
yy, xx = y + gy*scale + sy, x + gx*scale + sx
|
|
if 0 <= yy < len(px) and 0 <= xx < len(px[0]):
|
|
px[yy][xx] = col
|
|
x += 4 * scale
|
|
|
|
|
|
def card(path, cols, names, idx, scale=2):
|
|
"""Labelled reference: swatch, palette index, hex code, role name."""
|
|
rowh, sww, pad = 7*scale + 8, 22*scale, 6
|
|
W = sww + pad*2 + 4*scale*22
|
|
H = rowh*len(cols) + pad*2
|
|
bg, fg = (26, 26, 26), (255, 255, 219)
|
|
px = [[bg]*W for _ in range(H)]
|
|
for i, (c, n, ix) in enumerate(zip(cols, names, idx)):
|
|
y = pad + i*rowh
|
|
for yy in range(y, y + 7*scale):
|
|
for xx in range(pad, pad + sww):
|
|
px[yy][xx] = c
|
|
label = f"{ix:02d} #{c[0]:02X}{c[1]:02X}{c[2]:02X}"
|
|
_text(px, pad + sww + pad, y + scale, label, fg, scale)
|
|
write_png(path, px)
|
|
|
|
|
|
def emit(stem, idx, title):
|
|
cols = [PALETTE[i] for i in idx]
|
|
names = [NAMES[i] for i in idx]
|
|
p = lambda ext: os.path.normpath(os.path.join(OUT, f"{stem}.{ext}"))
|
|
gpl(p("gpl"), cols, names, title)
|
|
ase(p("ase"), cols, names)
|
|
aco(p("aco"), cols, names)
|
|
act(p("act"), cols)
|
|
swatches(p("png"), cols)
|
|
card(os.path.normpath(os.path.join(OUT, f"{stem}-card.png")), cols, names, idx)
|
|
print(f"{stem}: {len(cols)} colours -> .gpl .ase .aco .act .png + -card.png")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
emit("kissaten-full", list(range(16)), "Kissaten Yugure (full)")
|
|
emit("kissaten-portrait", PORTRAIT_IDX, "Kissaten Yugure (portrait)")
|
|
# exact 1px-per-entry image, for generators that take a palette image
|
|
write_png(os.path.normpath(os.path.join(OUT, "palette.png")), [list(PALETTE)])
|
|
print("palette.png: 16x1 exact")
|