Intial Commit

This commit is contained in:
2026-10-27 17:27:44 -04:00
parent 1fa6e14bde
commit 5743c34a21
49 changed files with 1454 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
"""Compose the release art from native Blender renders and original typography."""
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageChops, ImageOps
import numpy as np, math, json, argparse
work=Path(__file__).parent
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output-dir',type=Path,default=work.parent/'outputs/FunctionalTurrets-release-art')
out=parser.parse_args().output_dir
W=2048;coral=(241,100,101);cream=(232,226,209);rng=np.random.default_rng(271026)
impact=Path(r'C:\Windows\Fonts\impact.ttf');mono=Path(r'C:\Windows\Fonts\courbd.ttf')
font=lambda n:ImageFont.truetype(str(impact),n)
small=lambda n:ImageFont.truetype(str(mono),n)
def fit(text,maxwidth,maxsize,fontpath=impact):
for n in range(maxsize,10,-1):
f=ImageFont.truetype(str(fontpath),n)
if f.getlength(text)<=maxwidth:return f
def cutout(path):
im=Image.open(path).convert('RGBA');return im.crop(im.getchannel('A').getbbox())
def arrows(layer,box,color,width):
d=ImageDraw.Draw(layer);cx=(box[0]+box[2])/2;cy=(box[1]+box[3])/2;rx=(box[2]-box[0])/2;ry=(box[3]-box[1])/2
for start,end in [(12,155),(195,338)]:
d.arc(box,start,end,fill=color,width=width)
a=math.radians(end);x=cx+rx*math.cos(a);y=cy+ry*math.sin(a)
tangent=np.array([-rx*math.sin(a),ry*math.cos(a)]);tangent=tangent/np.linalg.norm(tangent)
side=np.array([-tangent[1],tangent[0]])
tip=np.array([x,y])+tangent*width*1.6;base=np.array([x,y])-tangent*width*1.9
d.polygon([tuple(tip),tuple(base+side*width*1.55),tuple(base-side*width*1.55)],fill=color)
def distressed_text(canvas,position,text,typeface,color):
mask=Image.new('L',canvas.size);d=ImageDraw.Draw(mask);d.text(position,text,font=typeface,fill=255,anchor='lt')
a=np.asarray(mask).copy();noise=rng.random(a.shape)
a[(noise<.025)&(a>0)]=30
# Thin uneven scratches cut through the lettering without sacrificing shape.
scratches=Image.new('L',canvas.size);sd=ImageDraw.Draw(scratches)
for _ in range(40):
x=int(rng.integers(70,W-70));y=int(rng.integers(170,650));sd.line((x,y,x+int(rng.integers(5,100)),y+int(rng.integers(-3,4))),fill=150,width=int(rng.integers(1,3)))
mask=ImageChops.subtract(Image.fromarray(a),scratches)
canvas.alpha_composite(Image.composite(Image.new('RGBA',canvas.size,(*color,255)),Image.new('RGBA',canvas.size),mask))
# Weathered dark olive paper, with a restrained warm vignette.
yy,xx=np.mgrid[0:W,0:W];rad=((xx/W-.5)**2+(yy/W-.5)**2)
grain=rng.normal(0,2.3,(W,W));coarse=Image.fromarray(rng.integers(0,255,(64,64),dtype=np.uint8)).resize((W,W),Image.Resampling.BICUBIC)
tone=(np.asarray(coarse).astype(float)-127)/35
background=np.stack([22+grain+tone-10*rad,27+grain+tone-12*rad,24+grain+tone-11*rad],axis=2)
poster=Image.fromarray(np.clip(background,0,255).astype('uint8')).convert('RGBA')
texture=Image.new('RGBA',poster.size);td=ImageDraw.Draw(texture)
for x in range(82,W,128):td.line((x,725,x,1770),fill=(190,190,160,9),width=1)
for y in range(770,1790,128):td.line((85,y,1960,y),fill=(190,190,160,9),width=1)
for _ in range(180):
x,y=rng.integers(15,W-15,2);td.line((int(x),int(y),int(x+rng.integers(-2,3)),int(y+rng.integers(2,55))),fill=(150,145,120,int(rng.integers(5,20))),width=1)
td.rectangle((36,36,2011,2011),outline=(213,209,185,40),width=2)
poster.alpha_composite(texture)
distressed_text(poster,(88,175),'FUNCTIONAL',fit('FUNCTIONAL',1872,262),cream)
distressed_text(poster,(82,384),'TURRETS',fit('TURRETS',1884,377),cream)
ferret=cutout(out/'ferret-turret-render.png');lav=cutout(out/'lav-turret-render.png')
ferret=ImageOps.contain(ferret,(840,890),Image.Resampling.LANCZOS)
lav=ImageOps.contain(lav,(1260,1020),Image.Resampling.LANCZOS)
def place(im,x,y):
shadow=Image.new('RGBA',poster.size);a=im.getchannel('A').filter(ImageFilter.GaussianBlur(9))
shade=Image.new('RGBA',im.size,(0,0,0,0));shade.putalpha(a.point(lambda p:int(p*.65)))
shadow.alpha_composite(shade,(x+14,y+24));poster.alpha_composite(shadow);poster.alpha_composite(im,(x,y))
place(ferret,82,822)
place(lav,1945-lav.width,775)
poster.convert('RGB').save(out/'preview-master.png')
poster.convert('RGB').resize((1024,1024),Image.Resampling.LANCZOS).save(out/'preview.png')
# Shared icon: transparent model silhouette and rotation arrows, no tiny text.
icon=Image.new('RGBA',(1024,1024));ring=Image.new('RGBA',icon.size)
arrows(ring,(90,637,930,960),(*coral,240),24);icon.alpha_composite(ring)
model=ImageOps.contain(cutout(out/'lav-turret-render.png'),(880,865),Image.Resampling.LANCZOS)
a=model.getchannel('A');outline=ImageChops.subtract(a.filter(ImageFilter.MaxFilter(9)),a)
outline_layer=Image.new('RGBA',model.size,(*cream,255));outline_layer.putalpha(outline.point(lambda p:int(p*.78)))
x=(1024-model.width)//2;y=32
icon.alpha_composite(outline_layer,(x,y));icon.alpha_composite(model,(x,y))
icon.save(out/'icon-master.png');icon.resize((512,512),Image.Resampling.LANCZOS).save(out/'icon.png')
icon.resize((128,128),Image.Resampling.LANCZOS).save(out/'icon-128.png')
poster.convert('RGB').resize((256,256),Image.Resampling.LANCZOS).save(out/'preview-256.png')
(out/'artwork.json').write_text(json.dumps({'style':'Original distressed survival-poster treatment; actual game-model renders','pink':'#F16465','preview_master':[2048,2048],'preview':[1024,1024],'icon_master':[1024,1024],'icon':[512,512],'icon_transparent':True,'no_image_generation':True},indent=2))
print('Composed release preview and transparent icon:',out)