65 lines
3.6 KiB
Python
65 lines
3.6 KiB
Python
"""Create circular board and static preview from the real JS geometry and PNG art.
|
|
Preview is a composited illustration, NOT a Vela runtime screenshot.
|
|
Existing sprite art is nearest-neighbor enlarged to native 32px, not blurred.
|
|
"""
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
a = json.loads(subprocess.check_output(['node', '-p', "JSON.stringify(require('./src/common/snake.js').ARENA)"], cwd=ROOT))
|
|
tex = ROOT / 'src/common/tex'
|
|
for p in tex.glob('*.png'):
|
|
if p.name != 'board.png':
|
|
img = Image.open(p).convert('RGBA')
|
|
if img.size != (a['cell'], a['cell']):
|
|
img.resize((a['cell'], a['cell']), Image.Resampling.NEAREST).save(p)
|
|
S=4
|
|
size=a['radius']*2
|
|
board=Image.new('RGBA',(size*S,size*S),(0,0,0,0))
|
|
d=ImageDraw.Draw(board)
|
|
d.ellipse((0,0,size*S-1,size*S-1),fill='#14251e',outline='#36513c',width=2*S)
|
|
local=a['offset']-(233-a['radius'])
|
|
for c in a['cells']:
|
|
x=(local+c['x']*a['cell'])*S;y=(local+c['y']*a['cell'])*S
|
|
d.rectangle((x,y,x+a['cell']*S-1,y+a['cell']*S-1),fill=('#1d3227' if (c['x']+c['y'])%2 else '#20362a'))
|
|
board=board.resize((size,size),Image.Resampling.LANCZOS)
|
|
board.save(tex/'board.png')
|
|
# Read actual CSS text placement/colors instead of inventing a separate layout.
|
|
import re
|
|
ux=(ROOT/'src/pages/index/index.ux').read_text()
|
|
def rule(name):
|
|
body=re.search(r'\.'+name+r'\s*\{([^}]+)\}',ux)[1]
|
|
return dict(re.findall(r'([\w-]+):\s*([^;]+)',body))
|
|
def px(r,key): return int(r[key].removesuffix('px'))
|
|
font=subprocess.check_output(['fc-match','-f','%{file}','sans'],text=True)
|
|
canvas=Image.new('RGB',(980,526),'#10131a')
|
|
for panel,mask in enumerate([False,True]):
|
|
watch=Image.new('RGBA',(466,466),'black');watch.alpha_composite(board,(233-a['radius'],233-a['radius']))
|
|
wd=ImageDraw.Draw(watch)
|
|
if mask:
|
|
for c in a['cells']:
|
|
x=a['offset']+c['x']*a['cell'];y=a['offset']+c['y']*a['cell']
|
|
wd.rectangle((x,y,x+a['cell']-1,y+a['cell']-1),outline='#71967a')
|
|
cells=[{'x':7,'y':4},{'x':6,'y':4},{'x':5,'y':4},{'x':5,'y':5},{'x':4,'y':5},{'x':3,'y':5}]
|
|
names=json.loads(subprocess.check_output(['node','-p',f"JSON.stringify(require('./src/common/snake.js').renderPieces({json.dumps(cells)},{{x:1,y:0}}))"],cwd=ROOT))
|
|
for c,name in zip(cells,names):
|
|
watch.alpha_composite(Image.open(tex/(name+'.png')),(a['offset']+c['x']*a['cell'],a['offset']+c['y']*a['cell']))
|
|
watch.alpha_composite(Image.open(tex/'apple.png'),(a['offset']+8*a['cell'],a['offset']+6*a['cell']))
|
|
for parent,entries in [('hud',[('score','SCORE 3'),('best','BEST 253')]),('footer',[('modeLabel','CLASSIC'),('hint','swipe / tap pause')])]:
|
|
pr=rule(parent); y=px(pr,'top')
|
|
for cls,text in entries:
|
|
r=rule(cls); f=ImageFont.truetype(font,px(r,'font-size'))
|
|
wd.text((233,y+px(r,'height')/2),text,font=f,fill=r['color'],anchor='mm')
|
|
y+=px(r,'height')
|
|
circle=Image.new('L',(466,466));ImageDraw.Draw(circle).ellipse((0,0,465,465),fill=255)
|
|
canvas.paste(watch.convert('RGB'),(12+panel*490,12),circle)
|
|
cd=ImageDraw.Draw(canvas)
|
|
cd.text((20,490),'STATIC ART / GEOMETRY COMPOSITE — NOT WATCH RUNTIME',fill='white',font=ImageFont.truetype(font,16))
|
|
cd.text((510,490),'Legal-cell overlay: 89 cells, 32px tiles',fill='white',font=ImageFont.truetype(font,16))
|
|
canvas.save(ROOT/'tests/preview-0.8-static.png')
|
|
print(ROOT/'tests/preview-0.8-static.png')
|
|
# Alpha corners prove the replacement board is circular, not a square frame.
|
|
assert all(board.getpixel(p)[3]==0 for p in [(0,0),(size-1,0),(0,size-1),(size-1,size-1)])
|
|
print('Circular PNG alpha corners verified; native 32px sprites generated.')
|