Files
game-cards-poker-design/backend/apps/projects/management/commands/seed_library.py

429 lines
17 KiB
Python
Raw Normal View History

"""
初始化预设素材库 Pillow 直接画 PNG200×300存到 media/library/并写入 LibraryAsset
主题4 × 4 = 16
classical 古典宫廷王子/皇后/国王/宫廷小丑
modern 现代人物小孩/女青年/男青年/小丑鱼
astronomy 天文 星星/月亮/太阳/黑洞
minimal 简笔符号圆点///
风格深色描边 4px + 半透明浅色填充便于在任意牌面背景上叠加
"""
import os
import math
from django.core.management.base import BaseCommand
from django.conf import settings
from apps.projects.models import LibraryAsset
from PIL import Image, ImageDraw
W, H = 200, 300
STROKE = (26, 26, 26, 255)
STROKE_W = 4
# 关键:素材主体只画在 y=20~200上半身y=200~300 留空。
# 因为后端会把整图 fill 到 body 区域,再切上半 + 翻转下半拼成上下对称,
# 所以素材本身只需要"上半身",下半由系统自动翻转生成。
BODY_TOP = 20
BODY_BOTTOM = 200
def new_canvas():
img = Image.new('RGBA', (W, H), (255, 255, 255, 0))
return img, ImageDraw.Draw(img)
def shift_up(img, dy=-30):
"""把图整体上移 dy 像素,下方留空。
因为素材只需要画上半身下半由系统对称生成"""
out = Image.new('RGBA', (W, H), (0, 0, 0, 0))
out.paste(img, (0, dy), img)
return out
def crop_to_upper(img):
"""把图裁到 y=0~150 区域(只保留上半身),下面 150px 留空。
后端 fill 整图后切上下半 = 上半=原图上半(0~150)下半=原图下半(150~300 留空)
翻转下半也是空白 整张牌上半身 + 空白不会有奇怪的下半"""
out = Image.new('RGBA', (W, H), (0, 0, 0, 0))
upper = img.crop((0, 0, W, 150))
out.paste(upper, (0, 0), upper)
return out
# ----------------- 古典宫廷 -----------------
def classical_prince():
img, d = new_canvas()
# 头发
d.polygon([(50, 110), (55, 130), (145, 130), (150, 110), (145, 100), (55, 100)],
fill=(180, 140, 80, 100), outline=STROKE, width=STROKE_W)
# 脸
d.ellipse((60, 100, 140, 180), fill=(255, 224, 189, 150), outline=STROKE, width=STROKE_W)
# 眼睛
d.ellipse((85, 134, 91, 140), fill=STROKE)
d.ellipse((109, 134, 115, 140), fill=STROKE)
# 嘴
d.arc((90, 148, 110, 162), start=0, end=180, fill=STROKE, width=3)
# 小皇冠
d.polygon([(70, 78), (78, 60), (86, 75), (100, 55), (114, 75), (122, 60), (130, 78)],
fill=(255, 220, 80, 130), outline=STROKE, width=STROKE_W)
d.ellipse((96, 52, 104, 60), fill=(255, 220, 80, 200), outline=STROKE, width=2)
# 衣领
d.line([(60, 200), (100, 220), (140, 200)], fill=STROKE, width=STROKE_W)
return img
def classical_queen():
img, d = new_canvas()
# 长发(披肩)
d.polygon([(45, 150), (50, 220), (150, 220), (155, 150),
(160, 100), (140, 80), (60, 80), (40, 100)],
fill=(200, 80, 80, 100), outline=STROKE, width=STROKE_W)
# 脸
d.ellipse((62, 102, 138, 178), fill=(255, 224, 189, 150), outline=STROKE, width=STROKE_W)
# 眼睛
d.ellipse((85, 134, 91, 140), fill=STROKE)
d.ellipse((109, 134, 115, 140), fill=STROKE)
# 嘴
d.arc((90, 150, 110, 164), start=0, end=180, fill=(200, 60, 60, 255), width=3)
# 高尖皇冠
d.polygon([(75, 75), (100, 30), (125, 75)],
fill=(255, 220, 80, 130), outline=STROKE, width=STROKE_W)
d.ellipse((96, 30, 104, 40), fill=(255, 220, 80, 200), outline=STROKE, width=2)
# 皇冠装饰线
d.arc((80, 65, 120, 85), start=0, end=180, fill=STROKE, width=2)
return img
def classical_king():
img, d = new_canvas()
# 头发
d.polygon([(55, 115), (58, 135), (142, 135), (145, 115), (140, 100), (60, 100)],
fill=(120, 90, 60, 100), outline=STROKE, width=STROKE_W)
# 脸
d.ellipse((60, 100, 140, 180), fill=(255, 224, 189, 150), outline=STROKE, width=STROKE_W)
# 眼睛
d.ellipse((85, 130, 91, 136), fill=STROKE)
d.ellipse((109, 130, 115, 136), fill=STROKE)
# 嘴
d.line([(88, 152), (112, 152)], fill=STROKE, width=3)
# 大胡子
d.polygon([(75, 158), (80, 178), (100, 185), (120, 178), (125, 158),
(115, 170), (100, 175), (85, 170)],
fill=(120, 90, 60, 130), outline=STROKE, width=STROKE_W)
# 厚重皇冠
d.polygon([(65, 88), (70, 65), (85, 80), (100, 50), (115, 80), (130, 65), (135, 88)],
fill=(255, 220, 80, 130), outline=STROKE, width=STROKE_W)
# 皇冠红宝石
d.rectangle((95, 70, 105, 76), fill=(200, 40, 40, 200), outline=STROKE, width=1)
# 衣领
d.line([(55, 205), (100, 225), (145, 205)], fill=STROKE, width=STROKE_W)
return img
def classical_joker():
img, d = new_canvas()
# 双角帽
d.polygon([(60, 95), (70, 35), (80, 85)], fill=(220, 40, 40, 130), outline=STROKE, width=STROKE_W)
d.polygon([(140, 95), (130, 35), (120, 85)], fill=(40, 80, 200, 130), outline=STROKE, width=STROKE_W)
d.ellipse((65, 30, 75, 40), fill=(220, 40, 40, 200), outline=STROKE, width=2)
d.ellipse((125, 30, 135, 40), fill=(40, 80, 200, 200), outline=STROKE, width=2)
# 脸
d.ellipse((58, 93, 142, 177), fill=(255, 235, 200, 180), outline=STROKE, width=STROKE_W)
# 菱形眼
d.polygon([(80, 130), (86, 125), (92, 130), (86, 135)], fill=STROKE)
d.polygon([(108, 130), (114, 125), (120, 130), (114, 135)], fill=STROKE)
# 大嘴
d.arc((78, 148, 122, 175), start=0, end=180, fill=(200, 40, 40, 80), width=3)
d.arc((78, 148, 122, 175), start=0, end=180, fill=STROKE, width=3)
# 领结
d.polygon([(90, 200), (100, 195), (110, 200), (100, 215)], fill=(220, 40, 40, 200), outline=STROKE, width=2)
return img
# ----------------- 现代人物 -----------------
def modern_child():
img, d = new_canvas()
# 短发
d.chord((55, 80, 145, 115), start=180, end=360, fill=(60, 60, 60, 130), outline=STROKE, width=STROKE_W)
# 脸
d.ellipse((62, 97, 138, 173), fill=(255, 225, 190, 180), outline=STROKE, width=STROKE_W)
# 大眼睛
d.ellipse((83, 128, 93, 138), fill=STROKE)
d.ellipse((107, 128, 117, 138), fill=STROKE)
# 腮红
d.ellipse((73, 145, 83, 155), fill=(255, 150, 150, 100))
d.ellipse((117, 145, 127, 155), fill=(255, 150, 150, 100))
# 嘴
d.arc((93, 150, 107, 160), start=0, end=180, fill=STROKE, width=3)
return img
def modern_young_woman():
img, d = new_canvas()
# 长发
d.polygon([(45, 150), (50, 240), (150, 240), (155, 150),
(160, 90), (140, 80), (60, 80), (40, 90)],
fill=(80, 40, 30, 130), outline=STROKE, width=STROKE_W)
# 脸(椭圆)
d.ellipse((65, 98, 135, 178), fill=(255, 225, 190, 180), outline=STROKE, width=STROKE_W)
# 眉
d.arc((80, 116, 96, 128), start=200, end=340, fill=STROKE, width=3)
d.arc((104, 116, 120, 128), start=200, end=340, fill=STROKE, width=3)
# 眼睛
d.ellipse((85, 132, 91, 138), fill=STROKE)
d.ellipse((109, 132, 115, 138), fill=STROKE)
# 嘴
d.arc((90, 152, 110, 166), start=0, end=180, fill=STROKE, width=3)
return img
def modern_young_man():
img, d = new_canvas()
# 短发
d.chord((55, 80, 145, 115), start=180, end=360, fill=(40, 30, 20, 130), outline=STROKE, width=STROKE_W)
# 脸(方下颌:圆角矩形近似)
d.rounded_rectangle((65, 100, 135, 180), radius=18, fill=(255, 225, 190, 180), outline=STROKE, width=STROKE_W)
# 眉
d.line([(78, 122), (98, 122)], fill=STROKE, width=3)
d.line([(102, 122), (122, 122)], fill=STROKE, width=3)
# 眼睛
d.ellipse((85, 133, 91, 139), fill=STROKE)
d.ellipse((109, 133, 115, 139), fill=STROKE)
# 嘴
d.line([(90, 158), (110, 158)], fill=STROKE, width=3)
# 胡茬点
d.ellipse((88, 168, 92, 172), fill=STROKE)
d.ellipse((98, 170, 102, 174), fill=STROKE)
d.ellipse((108, 168, 112, 172), fill=STROKE)
return img
def modern_clownfish():
img, d = new_canvas()
# 鱼身
d.ellipse((45, 112, 155, 188), fill=(255, 140, 40, 130), outline=STROKE, width=STROKE_W)
# 嘴部
d.polygon([(55, 150), (40, 145), (40, 155)], fill=(255, 140, 40, 150), outline=STROKE, width=STROKE_W)
# 眼睛(白底+黑瞳)
d.ellipse((58, 125, 74, 141), fill=(255, 255, 255, 230), outline=STROKE, width=2)
d.ellipse((62, 130, 70, 138), fill=STROKE)
# 白条纹
d.arc((75, 110, 90, 200), start=270, end=90, fill=(255, 255, 255, 200), width=8)
d.arc((100, 105, 115, 200), start=270, end=90, fill=(255, 255, 255, 200), width=6)
# 背鳍
d.polygon([(85, 112), (100, 88), (115, 112)], fill=(255, 140, 40, 130), outline=STROKE, width=STROKE_W)
# 腹鳍
d.polygon([(90, 188), (95, 208), (105, 188)], fill=(255, 140, 40, 130), outline=STROKE, width=STROKE_W)
# 尾鳍
d.polygon([(152, 150), (180, 120), (180, 180)], fill=(255, 140, 40, 130), outline=STROKE, width=STROKE_W)
return img
# ----------------- 天文 -----------------
def astronomy_star():
img, d = new_canvas()
cx, cy = 100, 140
r_out, r_in = 60, 25
pts = []
for i in range(10):
a = -math.pi / 2 + i * math.pi / 5
r = r_out if i % 2 == 0 else r_in
pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
d.polygon(pts, fill=(255, 220, 80, 130), outline=STROKE, width=STROKE_W)
# 中心高光
d.ellipse((94, 134, 106, 146), fill=(255, 255, 200, 200), outline=STROKE, width=1)
return img
def astronomy_moon():
img, d = new_canvas()
# 大圆
d.ellipse((45, 80, 155, 220), fill=(240, 240, 210, 180), outline=STROKE, width=STROKE_W)
# 遮挡圆(右侧偏移实现弯月)
d.ellipse((75, 80, 165, 220), fill=(0, 0, 0, 0)) # 透明
# 用图层减法实现:在 PIL 中不好做,直接画月牙
img2 = Image.new('RGBA', (W, H), (255, 255, 255, 0))
d2 = ImageDraw.Draw(img2)
d2.ellipse((45, 80, 155, 220), fill=(240, 240, 210, 180), outline=STROKE, width=STROKE_W)
# 透明圆覆盖(用镂空 mask
mask = Image.new('L', (W, H), 0)
dm = ImageDraw.Draw(mask)
dm.ellipse((80, 80, 165, 220), fill=255)
out = Image.new('RGBA', (W, H), (0, 0, 0, 0))
out.paste(img2, (0, 0), mask)
d = ImageDraw.Draw(out)
d.ellipse((45, 80, 155, 220), outline=STROKE, width=STROKE_W)
# 表面纹理
d.ellipse((85, 125, 95, 135), fill=(180, 180, 160, 130))
d.ellipse((95, 165, 102, 172), fill=(180, 180, 160, 130))
d.ellipse((75, 155, 82, 162), fill=(180, 180, 160, 130))
return out
def astronomy_sun():
img, d = new_canvas()
# 8 道光芒
rays = [
(100, 55, 100, 80),
(100, 220, 100, 245),
(30, 150, 55, 150),
(145, 150, 170, 150),
(50, 100, 68, 118),
(132, 118, 150, 100),
(50, 200, 68, 182),
(132, 182, 150, 200),
]
for x1, y1, x2, y2 in rays:
d.line([(x1, y1), (x2, y2)], fill=STROKE, width=5)
# 太阳主体
d.ellipse((55, 105, 145, 195), fill=(255, 180, 40, 150), outline=STROKE, width=STROKE_W)
# 笑眼
d.arc((73, 128, 88, 143), start=200, end=340, fill=STROKE, width=3)
d.arc((112, 128, 127, 143), start=200, end=340, fill=STROKE, width=3)
# 嘴
d.arc((85, 155, 115, 175), start=0, end=180, fill=STROKE, width=3)
return img
def astronomy_blackhole():
img, d = new_canvas()
# 吸积盘(两层椭圆)
d.ellipse((30, 128, 170, 172), fill=(120, 60, 200, 100), outline=STROKE, width=STROKE_W)
d.ellipse((42, 134, 158, 166), fill=(200, 100, 50, 130), outline=STROKE, width=STROKE_W)
# 中心黑洞
d.ellipse((78, 128, 122, 172), fill=(0, 0, 0, 240), outline=STROKE, width=STROKE_W)
# 高光
d.ellipse((85, 134, 115, 142), fill=(255, 180, 80, 200))
return img
# ----------------- 简笔 -----------------
def minimal_dot():
img, d = new_canvas()
d.ellipse((45, 95, 155, 205), fill=(80, 180, 200, 150), outline=STROKE, width=STROKE_W)
# 内圈虚线
d.ellipse((65, 115, 135, 185), outline=STROKE, width=2)
# 笑脸
d.ellipse((82, 138, 88, 144), fill=STROKE)
d.ellipse((112, 138, 118, 144), fill=STROKE)
d.arc((85, 155, 115, 170), start=0, end=180, fill=STROKE, width=3)
return img
def minimal_venus():
img, d = new_canvas()
d.ellipse((60, 80, 140, 160), fill=(220, 80, 140, 150), outline=STROKE, width=STROKE_W)
d.line([(100, 160), (100, 230)], fill=STROKE, width=8)
d.line([(80, 200), (120, 200)], fill=STROKE, width=8)
return img
def minimal_mars():
img, d = new_canvas()
d.ellipse((40, 120, 120, 200), fill=(60, 120, 220, 150), outline=STROKE, width=STROKE_W)
d.line([(115, 120), (155, 80)], fill=STROKE, width=6)
d.line([(135, 75), (158, 75)], fill=STROKE, width=6)
d.line([(158, 75), (158, 98)], fill=STROKE, width=6)
return img
def minimal_cross():
img, d = new_canvas()
# 双色叉:先画浅红底,再深色描边
d.line([(55, 95), (155, 225)], fill=(200, 40, 40, 130), width=14)
d.line([(155, 95), (55, 225)], fill=(200, 40, 40, 130), width=14)
d.line([(55, 95), (155, 225)], fill=STROKE, width=4)
d.line([(155, 95), (55, 225)], fill=STROKE, width=4)
return img
# 元数据
THEMES = [
{
'theme_id': 'classical', 'theme_name': '古典宫廷',
'description': '王子/皇后/国王/宫廷小丑,传统扑克牌',
'items': [
('J', 'prince', classical_prince, '王子'),
('Q', 'queen', classical_queen, '皇后'),
('K', 'king', classical_king, '国王'),
('joker', 'jester', classical_joker, '小丑'),
],
},
{
'theme_id': 'modern', 'theme_name': '现代人物',
'description': '小孩/女青年/男青年/小丑鱼,日常生活',
'items': [
('J', 'child', modern_child, '小孩'),
('Q', 'young_woman', modern_young_woman, '女青年'),
('K', 'young_man', modern_young_man, '男青年'),
('joker', 'clownfish', modern_clownfish, '小丑鱼'),
],
},
{
'theme_id': 'astronomy', 'theme_name': '天文',
'description': '星星/月亮/太阳/黑洞',
'items': [
('J', 'star', astronomy_star, '星星'),
('Q', 'moon', astronomy_moon, '月亮'),
('K', 'sun', astronomy_sun, '太阳'),
('joker', 'blackhole', astronomy_blackhole, '黑洞'),
],
},
{
'theme_id': 'minimal', 'theme_name': '简笔符号',
'description': '圆点/♀/♂/×,极简风格',
'items': [
('J', 'dot', minimal_dot, '圆点'),
('Q', 'venus', minimal_venus, ''),
('K', 'mars', minimal_mars, ''),
('joker', 'cross', minimal_cross, '×'),
],
},
]
class Command(BaseCommand):
help = '用 Pillow 画 16 张预设素材 PNG存到 media/library/,并写入 LibraryAsset 表'
def handle(self, *args, **options):
lib_dir = os.path.join(settings.MEDIA_ROOT, 'library')
os.makedirs(lib_dir, exist_ok=True)
written = 0
for theme in THEMES:
theme_dir = os.path.join(lib_dir, theme['theme_id'])
os.makedirs(theme_dir, exist_ok=True)
for role, asset_id, gen_fn, role_name in theme['items']:
img = gen_fn()
# 上移 30 让主体更靠上,再裁掉 y=150 之后(只留上半身)
img = shift_up(img, dy=-30)
img = crop_to_upper(img)
file_name = f'{asset_id}.png'
file_path = os.path.join(theme_dir, file_name)
img.save(file_path)
rel_path = f'library/{theme["theme_id"]}/{file_name}'
label = f'{theme["theme_name"]}·{role_name}'
obj, created = LibraryAsset.objects.update_or_create(
theme_id=theme['theme_id'],
asset_id=asset_id,
defaults=dict(
theme_name=theme['theme_name'],
role=role,
role_name=role_name,
label=label,
description=theme['description'],
file_path=rel_path,
file_name=file_name,
),
)
verb = 'created' if created else 'updated'
self.stdout.write(f' [{verb}] {theme["theme_id"]}/{file_name} -> {label}')
written += 1
self.stdout.write(self.style.SUCCESS(
f'\n已生成 {written} 张 PNG 素材({len(THEMES)} 套主题)\n'
f'文件目录: media/library/\n'
f'数据库表: apps_projects_libraryasset'
))