33
nonebot_plugin_tetris_stats/utils/render/avatar/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from base64 import b64encode
|
||||
from io import BytesIO
|
||||
from random import choice, randint
|
||||
|
||||
from PIL import Image
|
||||
from PIL.Image import Resampling
|
||||
|
||||
from .draw import PIECE_MEMBERS, SkinManager
|
||||
|
||||
|
||||
def get_avatar() -> str:
|
||||
skin = (
|
||||
SkinManager.get_skin()
|
||||
.get_piece(choice(PIECE_MEMBERS)) # noqa: S311
|
||||
.rotate(
|
||||
randint(-360, 360), # noqa: S311
|
||||
expand=True,
|
||||
resample=Resampling.BICUBIC,
|
||||
)
|
||||
)
|
||||
skin = skin.crop(skin.getbbox())
|
||||
background = Image.new('RGBA', (2048, 2048), '#e5e5e5')
|
||||
|
||||
skin_ratio = min(1536 / skin.width, 1536 / skin.height)
|
||||
|
||||
new_size = (int(skin.width * skin_ratio), int(skin.height * skin_ratio))
|
||||
skin = skin.resize(new_size, Resampling.BICUBIC)
|
||||
|
||||
background.paste(skin, ((background.width - skin.width) // 2, (background.height - skin.height) // 2), mask=skin)
|
||||
background = background.resize((512, 512), Resampling.LANCZOS)
|
||||
with BytesIO() as output:
|
||||
background.save(output, format='PNG')
|
||||
return f'data:image/png;base64,{b64encode(output.getvalue()).decode("utf-8")}'
|
||||
169
nonebot_plugin_tetris_stats/utils/render/avatar/draw/__init__.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from random import choice
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from PIL.Image import Image
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
class Piece(Enum):
|
||||
Z = (
|
||||
(True, True, False),
|
||||
(False, True, True),
|
||||
)
|
||||
|
||||
S = (
|
||||
(False, True, True),
|
||||
(True, True, False),
|
||||
)
|
||||
|
||||
J = (
|
||||
(True, False, False),
|
||||
(True, True, True),
|
||||
)
|
||||
|
||||
L = (
|
||||
(False, False, True),
|
||||
(True, True, True),
|
||||
)
|
||||
|
||||
T = (
|
||||
(False, True, False),
|
||||
(True, True, True),
|
||||
)
|
||||
|
||||
I = ( # noqa: E741
|
||||
(True, True, True, True),
|
||||
)
|
||||
|
||||
O = ( # noqa: E741
|
||||
(True, True),
|
||||
(True, True),
|
||||
)
|
||||
|
||||
I5 = (
|
||||
(True, True, True, True, True), # fmt: skip
|
||||
)
|
||||
|
||||
V = (
|
||||
(True, False, False),
|
||||
(True, False, False),
|
||||
(True, True, True),
|
||||
)
|
||||
|
||||
T5 = (
|
||||
(True, True, True),
|
||||
(False, True, False),
|
||||
(False, True, False),
|
||||
)
|
||||
|
||||
U = (
|
||||
(True, False, True),
|
||||
(True, True, True),
|
||||
)
|
||||
|
||||
W = (
|
||||
(True, False, False),
|
||||
(True, True, False),
|
||||
(False, True, True),
|
||||
)
|
||||
|
||||
X = (
|
||||
(False, True, False),
|
||||
(True, True, True),
|
||||
(False, True, False),
|
||||
)
|
||||
|
||||
J5 = (
|
||||
(True, False, False, False),
|
||||
(True, True, True, True),
|
||||
)
|
||||
|
||||
L5 = (
|
||||
(False, False, False, True),
|
||||
(True, True, True, True),
|
||||
)
|
||||
|
||||
H = (
|
||||
(False, False, True, True),
|
||||
(True, True, True, False),
|
||||
)
|
||||
|
||||
N = (
|
||||
(True, True, False, False),
|
||||
(False, True, True, True),
|
||||
)
|
||||
|
||||
Y = (
|
||||
(False, True, False, False),
|
||||
(True, True, True, True),
|
||||
)
|
||||
|
||||
R = (
|
||||
(False, False, True, False),
|
||||
(True, True, True, True),
|
||||
)
|
||||
|
||||
P = (
|
||||
(True, True, False),
|
||||
(True, True, True),
|
||||
)
|
||||
|
||||
Q = (
|
||||
(False, True, True),
|
||||
(True, True, True),
|
||||
)
|
||||
|
||||
F = (
|
||||
(True, False, False),
|
||||
(True, True, True),
|
||||
(False, True, False),
|
||||
)
|
||||
|
||||
E = (
|
||||
(False, False, True),
|
||||
(True, True, True),
|
||||
(False, True, False),
|
||||
)
|
||||
|
||||
S5 = (
|
||||
(False, True, True),
|
||||
(False, True, False),
|
||||
(True, True, False),
|
||||
)
|
||||
|
||||
Z5 = (
|
||||
(True, True, False),
|
||||
(False, True, False),
|
||||
(False, True, True),
|
||||
)
|
||||
|
||||
|
||||
PIECE_MEMBERS = tuple(Piece)
|
||||
|
||||
|
||||
class SkinManager:
|
||||
skin: ClassVar[list['Skin']] = []
|
||||
|
||||
@classmethod
|
||||
def register(cls, skin: 'Skin') -> None:
|
||||
cls.skin.append(skin)
|
||||
|
||||
@classmethod
|
||||
def get_skin(cls) -> 'Skin':
|
||||
return choice(cls.skin) # noqa: S311
|
||||
|
||||
|
||||
class Skin(ABC):
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Self: # noqa: ANN401, ARG003
|
||||
instance = super().__new__(cls)
|
||||
SkinManager.register(instance)
|
||||
return instance
|
||||
|
||||
@abstractmethod
|
||||
def get_piece(self, piece: Piece) -> Image:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
from . import tech # noqa: E402, F401
|
||||
@@ -0,0 +1,94 @@
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from nonebot import get_driver
|
||||
from PIL import Image
|
||||
from PIL.Image import Resampling
|
||||
from typing_extensions import override
|
||||
|
||||
from .. import Piece, Skin
|
||||
|
||||
SINGLE = 30
|
||||
|
||||
driver = get_driver()
|
||||
|
||||
|
||||
class Block(Enum):
|
||||
Z = (0, 0, 30, 30)
|
||||
Y = (30, 0, 60, 30)
|
||||
L = (60, 0, 90, 30)
|
||||
O = (90, 0, 120, 30) # noqa: E741
|
||||
U = (120, 0, 150, 30)
|
||||
Q = (150, 0, 180, 30)
|
||||
S = (180, 0, 210, 30)
|
||||
H = (210, 0, 240, 30)
|
||||
I = (0, 30, 30, 60) # noqa: E741
|
||||
F = (30, 30, 60, 60)
|
||||
J = (60, 30, 90, 60)
|
||||
R = (90, 30, 120, 60)
|
||||
C = (120, 30, 150, 60)
|
||||
T = (150, 30, 180, 60)
|
||||
W = (180, 30, 210, 60)
|
||||
N = (210, 30, 240, 60)
|
||||
|
||||
|
||||
piece_block_mapping = {
|
||||
Piece.Z: Block.Z,
|
||||
Piece.S: Block.S,
|
||||
Piece.J: Block.J,
|
||||
Piece.L: Block.L,
|
||||
Piece.T: Block.T,
|
||||
Piece.I: Block.I,
|
||||
Piece.O: Block.O,
|
||||
Piece.I5: Block.O,
|
||||
Piece.V: Block.I,
|
||||
Piece.T5: Block.C,
|
||||
Piece.U: Block.U,
|
||||
Piece.W: Block.W,
|
||||
Piece.X: Block.O,
|
||||
Piece.J5: Block.J,
|
||||
Piece.L5: Block.L,
|
||||
Piece.H: Block.H,
|
||||
Piece.N: Block.N,
|
||||
Piece.R: Block.R,
|
||||
Piece.Y: Block.Y,
|
||||
Piece.P: Block.Y,
|
||||
Piece.Q: Block.Q,
|
||||
Piece.F: Block.F,
|
||||
Piece.E: Block.Y,
|
||||
Piece.S5: Block.S,
|
||||
Piece.Z5: Block.Z,
|
||||
}
|
||||
|
||||
|
||||
class TechSkin(Skin):
|
||||
def __init__(self, path: Path, name: str | None = None) -> None:
|
||||
self.path = path
|
||||
self.name = name or path.name
|
||||
self.image = Image.open(path)
|
||||
self._block_cache: dict[Block, Image.Image] = {}
|
||||
|
||||
def get_block(self, block: Block) -> Image.Image:
|
||||
return self._block_cache.setdefault(block, self.image.crop(block.value))
|
||||
|
||||
def draw_piece(self, block: Block, piece: Piece, scale: int = 10) -> Image.Image:
|
||||
canvas = Image.new(
|
||||
'RGBA', (len(piece.value[0]) * SINGLE * scale, len(piece.value) * SINGLE * scale), (0, 0, 0, 0)
|
||||
)
|
||||
block_img = self.get_block(block).resize((SINGLE * scale, SINGLE * scale), resample=Resampling.BICUBIC)
|
||||
for i, row in enumerate(piece.value):
|
||||
for j, mino in enumerate(row):
|
||||
if mino:
|
||||
canvas.paste(block_img, (j * SINGLE * scale, i * SINGLE * scale))
|
||||
return canvas
|
||||
|
||||
@override
|
||||
def get_piece(self, piece: Piece) -> Image.Image:
|
||||
return self.draw_piece(piece_block_mapping[piece], piece)
|
||||
|
||||
|
||||
@driver.on_startup
|
||||
def _():
|
||||
path = Path(__file__).parent / 'skins'
|
||||
for i in path.iterdir():
|
||||
TechSkin(i)
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 613 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 837 B |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |