diff --git a/konabot/plugins/roll_dice/__init__.py b/konabot/plugins/roll_dice/__init__.py index d980b5a..bb5c452 100644 --- a/konabot/plugins/roll_dice/__init__.py +++ b/konabot/plugins/roll_dice/__init__.py @@ -1,19 +1,42 @@ +from typing import Optional from nonebot.adapters import Event as BaseEvent from nonebot.adapters.console.event import MessageEvent as ConsoleMessageEvent from nonebot.adapters.discord.event import MessageEvent as DiscordMessageEvent -from nonebot_plugin_alconna import Alconna, UniMessage, on_alconna +from nonebot_plugin_alconna import Alconna, Args, UniMessage, on_alconna -from konabot.plugins.roll_dice.roll_dice import roll_dice +from konabot.plugins.roll_dice.roll_dice import generate_dice_image +from konabot.plugins.roll_dice.roll_number import get_random_number, roll_number + +# evt = on_alconna(Alconna( +# "摇数字" +# ), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True) + +# @evt.handle() +# async def _(event: BaseEvent): +# if isinstance(event, DiscordMessageEvent): +# await evt.send(await UniMessage().text("```\n" + roll_number() + "\n```").export()) +# elif isinstance(event, ConsoleMessageEvent): +# await evt.send(await UniMessage().text(roll_number()).export()) +# else: +# await evt.send(await UniMessage().text(roll_number(wide=True)).export()) evt = on_alconna(Alconna( - "摇骰子" + "摇骰子", + Args["f1?", int]["f2?", int] ), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=True) @evt.handle() -async def _(event: BaseEvent): - if isinstance(event, DiscordMessageEvent): - await evt.send(await UniMessage().text("```\n" + roll_dice() + "\n```").export()) - elif isinstance(event, ConsoleMessageEvent): - await evt.send(await UniMessage().text(roll_dice()).export()) +async def _(event: BaseEvent, f1: Optional[int] = None, f2: Optional[int] = None): + # if isinstance(event, DiscordMessageEvent): + # await evt.send(await UniMessage().text("```\n" + roll_dice() + "\n```").export()) + # elif isinstance(event, ConsoleMessageEvent): + number = 0 + if(f1 is not None and f2 is not None): + number = get_random_number(f1, f2) + elif f1 is not None: + number = get_random_number(1, f1) else: - await evt.send(await UniMessage().text(roll_dice(wide=True)).export()) + number = get_random_number() + await evt.send(await UniMessage().image(raw=await generate_dice_image(number)).export()) + # else: + # await evt.send(await UniMessage().text(roll_dice(wide=True)).export()) diff --git a/konabot/plugins/roll_dice/assets/1.png b/konabot/plugins/roll_dice/assets/1.png new file mode 100644 index 0000000..c5b8584 Binary files /dev/null and b/konabot/plugins/roll_dice/assets/1.png differ diff --git a/konabot/plugins/roll_dice/assets/10.png b/konabot/plugins/roll_dice/assets/10.png new file mode 100644 index 0000000..527e95f Binary files /dev/null and b/konabot/plugins/roll_dice/assets/10.png differ diff --git a/konabot/plugins/roll_dice/assets/11.png b/konabot/plugins/roll_dice/assets/11.png new file mode 100644 index 0000000..a19a6fd Binary files /dev/null and b/konabot/plugins/roll_dice/assets/11.png differ diff --git a/konabot/plugins/roll_dice/assets/12.png b/konabot/plugins/roll_dice/assets/12.png new file mode 100644 index 0000000..2bcc14e Binary files /dev/null and b/konabot/plugins/roll_dice/assets/12.png differ diff --git a/konabot/plugins/roll_dice/assets/2.png b/konabot/plugins/roll_dice/assets/2.png new file mode 100644 index 0000000..d1f9428 Binary files /dev/null and b/konabot/plugins/roll_dice/assets/2.png differ diff --git a/konabot/plugins/roll_dice/assets/3.png b/konabot/plugins/roll_dice/assets/3.png new file mode 100644 index 0000000..a19a6fd Binary files /dev/null and b/konabot/plugins/roll_dice/assets/3.png differ diff --git a/konabot/plugins/roll_dice/assets/4.png b/konabot/plugins/roll_dice/assets/4.png new file mode 100644 index 0000000..2bcc14e Binary files /dev/null and b/konabot/plugins/roll_dice/assets/4.png differ diff --git a/konabot/plugins/roll_dice/assets/5.png b/konabot/plugins/roll_dice/assets/5.png new file mode 100644 index 0000000..7462a01 Binary files /dev/null and b/konabot/plugins/roll_dice/assets/5.png differ diff --git a/konabot/plugins/roll_dice/assets/6.png b/konabot/plugins/roll_dice/assets/6.png new file mode 100644 index 0000000..527e95f Binary files /dev/null and b/konabot/plugins/roll_dice/assets/6.png differ diff --git a/konabot/plugins/roll_dice/assets/7.png b/konabot/plugins/roll_dice/assets/7.png new file mode 100644 index 0000000..a19a6fd Binary files /dev/null and b/konabot/plugins/roll_dice/assets/7.png differ diff --git a/konabot/plugins/roll_dice/assets/8.png b/konabot/plugins/roll_dice/assets/8.png new file mode 100644 index 0000000..2bcc14e Binary files /dev/null and b/konabot/plugins/roll_dice/assets/8.png differ diff --git a/konabot/plugins/roll_dice/assets/9.png b/konabot/plugins/roll_dice/assets/9.png new file mode 100644 index 0000000..7462a01 Binary files /dev/null and b/konabot/plugins/roll_dice/assets/9.png differ diff --git a/konabot/plugins/roll_dice/assets/montserrat.otf b/konabot/plugins/roll_dice/assets/montserrat.otf new file mode 100644 index 0000000..ee37373 Binary files /dev/null and b/konabot/plugins/roll_dice/assets/montserrat.otf differ diff --git a/konabot/plugins/roll_dice/assets/template.png b/konabot/plugins/roll_dice/assets/template.png new file mode 100644 index 0000000..6d0198c Binary files /dev/null and b/konabot/plugins/roll_dice/assets/template.png differ diff --git a/konabot/plugins/roll_dice/base/path.py b/konabot/plugins/roll_dice/base/path.py new file mode 100644 index 0000000..fa3e4cd --- /dev/null +++ b/konabot/plugins/roll_dice/base/path.py @@ -0,0 +1,3 @@ +from pathlib import Path + +ASSETS = Path(__file__).parent.parent / "assets" diff --git a/konabot/plugins/roll_dice/roll_dice.py b/konabot/plugins/roll_dice/roll_dice.py index 53e23bf..ce736ed 100644 --- a/konabot/plugins/roll_dice/roll_dice.py +++ b/konabot/plugins/roll_dice/roll_dice.py @@ -1,54 +1,203 @@ -number_arts = { - 1: ''' _ - / | - | | - | | - |_| +from io import BytesIO +import cv2 +import numpy as np +from PIL import Image, ImageDraw, ImageFont + +from konabot.plugins.roll_dice.base.path import ASSETS + +def text_to_transparent_image(text, font_size=40, padding=0, text_color=(0, 0, 0)): + """ + 将文本转换为带透明背景的图像,图像大小刚好包含文本 + """ + # 创建临时图像来计算文本尺寸 + temp_image = Image.new('RGB', (1, 1), (255, 255, 255)) + temp_draw = ImageDraw.Draw(temp_image) -''', - 2: ''' ____ - |___ \\ - __) | - / __/ - |_____| - ''', - 3: ''' _____ - |___ / - |_ \\ - ___) | - |____/ - ''', - 4: ''' _ _ - | || | - | || |_ - |__ _| - |_| - ''', - 5: ''' ____ - | ___| - |___ \\ - ___) | - |____/ - ''', - 6: ''' __ - / /_ - | '_ \\ - | (_) | - \\___/ - ''' -} + font = ImageFont.truetype(ASSETS / "montserrat.otf", font_size) + # try: + # font = ImageFont.truetype(ASSETS / "montserrat.otf", font_size) + # except: + # try: + # font = ImageFont.truetype("arial.ttf", font_size) + # except: + # # 如果系统字体不可用,使用默认字体 + # font = ImageFont.load_default() + + # 获取文本边界框 + bbox = temp_draw.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + + # 计算图像大小(文本大小 + 内边距) + image_width = text_width + 2 * padding + image_height = text_height + 2 * padding + + # 创建RGBA模式的空白图像(带透明通道) + image = Image.new('RGBA', (image_width, image_height), (0, 0, 0, 0)) + draw = ImageDraw.Draw(image) + + # 绘制文本(考虑内边距) + x = padding - bbox[0] # 调整起始位置 + y = padding - bbox[1] + + # 设置文本颜色(带透明度) + if len(text_color) == 3: + text_color = text_color + (255,) # 添加完全不透明的alpha值 + + draw.text((x, y), text, fill=text_color, font=font) + + # 转换为OpenCV格式(BGRA) + image_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGBA2BGRA) + return image_cv -def get_random_number(min: int = 1, max: int = 6) -> int: - import random - return random.randint(min, max) +def perspective_transform(image, target, corners): + """ + 对图像进行透视变换(保持透明通道) + target: 画布 + corners: 四个角点的坐标,顺序为 [左上, 右上, 右下, 左下] + """ + height, width = image.shape[:2] + + # 源点(原始图像的四个角) + src_points = np.array([ + [0, 0], # 左上 + [width-1, 0], # 右上 + [width-1, height-1], # 右下 + [0, height-1] # 左下 + ], dtype=np.float32) + + # 目标点(变换后的四个角) + dst_points = np.array(corners, dtype=np.float32) + + # 计算透视变换矩阵 + matrix = cv2.getPerspectiveTransform(src_points, dst_points) + + # 获取画布大小 + target_height, target_width = target.shape[:2] -def roll_dice(wide: bool = False) -> str: - raw = number_arts[get_random_number()] - if wide: - raw = (raw - .replace("/", "/") - .replace("\\", "\") - .replace("_", "_") - .replace("|", "|") - .replace(" ", " ")) - return raw + # 应用透视变换(保持所有通道,包括alpha) + transformed = cv2.warpPerspective(image, matrix, (target_width, target_height), flags=cv2.INTER_LINEAR) + + return transformed, matrix + +def blend_with_transparency(background, foreground, position): + """ + 将带透明通道的前景图像合成到背景图像上 + position: 前景图像在背景图像上的位置 (x, y) + """ + bg = background.copy() + + # 如果背景没有alpha通道,添加一个 + if bg.shape[2] == 3: + bg = cv2.cvtColor(bg, cv2.COLOR_BGR2BGRA) + bg[:, :, 3] = 255 # 完全不透明 + + x, y = position + fg_height, fg_width = foreground.shape[:2] + bg_height, bg_width = bg.shape[:2] + + # 确保位置在图像范围内 + x = max(0, min(x, bg_width - fg_width)) + y = max(0, min(y, bg_height - fg_height)) + + # 提取前景的alpha通道并归一化 + alpha_foreground = foreground[:, :, 3] / 255.0 + + # 对于每个颜色通道进行合成 + for c in range(3): + bg_region = bg[y:y+fg_height, x:x+fg_width, c] + fg_region = foreground[:, :, c] + + # alpha混合公式 + bg[y:y+fg_height, x:x+fg_width, c] = ( + alpha_foreground * fg_region + + (1 - alpha_foreground) * bg_region + ) + + # 更新背景的alpha通道(如果需要) + bg_alpha_region = bg[y:y+fg_height, x:x+fg_width, 3] + bg[y:y+fg_height, x:x+fg_width, 3] = np.maximum(bg_alpha_region, foreground[:, :, 3]) + + return bg + +def precise_blend_with_perspective(background, foreground, corners): + """ + 精确合成:根据四个角点将前景图像透视合成到背景上 + """ + # 创建与背景相同大小的空白图像 + bg_height, bg_width = background.shape[:2] + + # 如果背景没有alpha通道,转换为BGRA + if background.shape[2] == 3: + background_bgra = cv2.cvtColor(background, cv2.COLOR_BGR2BGRA) + else: + background_bgra = background.copy() + + # 创建与背景相同大小的前景图层 + foreground_layer = np.zeros((bg_height, bg_width, 4), dtype=np.uint8) + + # 计算前景图像在背景中的边界框 + min_x = int(min(corners[:, 0])) + max_x = int(max(corners[:, 0])) + min_y = int(min(corners[:, 1])) + max_y = int(max(corners[:, 1])) + + # 将变换后的前景图像放置到对应位置 + fg_height, fg_width = foreground.shape[:2] + if min_y + fg_height <= bg_height and min_x + fg_width <= bg_width: + foreground_layer[min_y:min_y+fg_height, min_x:min_x+fg_width] = foreground + + # 创建掩码(只在前景有内容的地方合成) + mask = (foreground_layer[:, :, 3] > 0) + + # 合成图像 + result = background_bgra.copy() + for c in range(3): + result[:, :, c][mask] = foreground_layer[:, :, c][mask] + result[:, :, 3][mask] = foreground_layer[:, :, 3][mask] + + return result + +async def generate_dice_image(number: int) -> BytesIO: + # 将文本转换为带透明背景的图像 + text = str(number) + text_image = text_to_transparent_image( + text, + font_size=60, + text_color=(0, 0, 0) # 黑色文字 + ) + + # 定义3D变换的四个角点(透视效果) + # 顺序: [左上, 右上, 右下, 左下] + corners = np.array([ + [16, 30], # 左上 + [51, 5], # 右上(上移,创建透视) + [88, 33], # 右下 + [49, 62] # 左下(下移) + ], dtype=np.float32) + + # 加载背景图像,保留透明通道 + background = cv2.imread(ASSETS / "template.png", cv2.IMREAD_UNCHANGED) + + + # 对文本图像进行3D变换(保持透明通道) + transformed_text, transform_matrix = perspective_transform(text_image, background, corners) + + min_x = int(min(corners[:, 0])) + min_y = int(min(corners[:, 1])) + final_image_simple = blend_with_transparency(background, transformed_text, (min_x, min_y)) + + pil_final = Image.fromarray(final_image_simple) + # 导入一系列图像 + images = [Image.open(ASSETS / f"{i}.png") for i in range(1, 12)] + images.append(pil_final) + frame_durations = [100] * (len(images) - 1) + [100000] + # 保存为BytesIO对象 + output = BytesIO() + images[0].save(output, + save_all=True, + append_images=images[1:], + duration=frame_durations, + format='GIF', + loop=1) + return output \ No newline at end of file diff --git a/konabot/plugins/roll_dice/roll_number.py b/konabot/plugins/roll_dice/roll_number.py new file mode 100644 index 0000000..6568926 --- /dev/null +++ b/konabot/plugins/roll_dice/roll_number.py @@ -0,0 +1,54 @@ +number_arts = { + 1: ''' _ + / | + | | + | | + |_| + +''', + 2: ''' ____ + |___ \\ + __) | + / __/ + |_____| + ''', + 3: ''' _____ + |___ / + |_ \\ + ___) | + |____/ + ''', + 4: ''' _ _ + | || | + | || |_ + |__ _| + |_| + ''', + 5: ''' ____ + | ___| + |___ \\ + ___) | + |____/ + ''', + 6: ''' __ + / /_ + | '_ \\ + | (_) | + \\___/ + ''' +} + +def get_random_number(min: int = 1, max: int = 6) -> int: + import random + return random.randint(min, max) + +def roll_number(wide: bool = False) -> str: + raw = number_arts[get_random_number()] + if wide: + raw = (raw + .replace("/", "/") + .replace("\\", "\") + .replace("_", "_") + .replace("|", "|") + .replace(" ", " ")) + return raw