Intial Commit
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user