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 |