Compare commits

..

4 Commits

Author SHA1 Message Date
21e996a3b9 真正的骰子
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-09-29 21:30:44 +08:00
a68c8bee98 真正的骰子 2025-09-29 21:30:14 +08:00
6362ed4a88 补全 Emoji 显示和另一个显示
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-09-29 18:09:28 +08:00
7e3611afcd alias by asynkio 2025-09-29 17:41:57 +08:00
26 changed files with 346 additions and 65 deletions

View File

@ -1,14 +1,17 @@
from io import BytesIO
from nonebot_plugin_alconna import Alconna, Args, Field, MultiVar, UniMessage, on_alconna
from nonebot_plugin_alconna import (Alconna, Args, Field, MultiVar, UniMessage,
on_alconna)
from konabot.plugins.memepack.drawing.geimao import draw_geimao
from konabot.plugins.memepack.drawing.pt import draw_pt
geimao = on_alconna(Alconna(
"给猫说",
Args["saying", MultiVar(str, '+'), Field(
missing_tips=lambda: "你没有写给猫说了什么"
)]
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=False)
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=False, aliases={"给猫哈"})
@geimao.handle()
async def _(saying: list[str]):
@ -17,3 +20,19 @@ async def _(saying: list[str]):
img.save(img_bytes, format="PNG")
await geimao.send(await UniMessage().image(raw=img_bytes).export())
pt = on_alconna(Alconna(
"pt说",
Args["saying", MultiVar(str, '+'), Field(
missing_tips=lambda: "你没有写小帕说了什么"
)]
), use_cmd_start=True, use_cmd_sep=False, skip_for_unmatch=False, aliases={"小帕说"})
@pt.handle()
async def _(saying: list[str]):
img = await draw_pt("\n".join(saying))
img_bytes = BytesIO()
img.save(img_bytes, format="PNG")
await pt.send(await UniMessage().image(raw=img_bytes).export())

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

View File

@ -1,6 +1,12 @@
from imagetext_py import FontDB
from imagetext_py import EmojiOptions, FontDB
from .path import assets
from .path import ASSETS
FontDB.LoadFromDir(str(ASSETS))
FontDB.SetDefaultEmojiOptions(EmojiOptions(
parse_shortcodes=False,
))
FontDB.LoadFromDir(str(assets))
HARMONYOS_SANS_SC_BLACK = FontDB.Query("HarmonyOS_Sans_SC_Black")
HARMONYOS_SANS_SC_REGULAR = FontDB.Query("HarmonyOS_Sans_SC_Regular")

View File

@ -1,3 +1,3 @@
from pathlib import Path
assets = Path(__file__).parent.parent.parent / "assets"
ASSETS = Path(__file__).parent.parent.parent / "assets"

View File

@ -5,9 +5,9 @@ import imagetext_py
import PIL.Image
from .base.fonts import HARMONYOS_SANS_SC_BLACK
from .base.path import assets
from .base.path import ASSETS
geimao_image = PIL.Image.open(assets / "geimao.jpg").convert("RGBA")
geimao_image = PIL.Image.open(ASSETS / "geimao.jpg").convert("RGBA")
def _draw_geimao(saying: str):
@ -20,6 +20,7 @@ def _draw_geimao(saying: str):
imagetext_py.TextAlign.Center,
cast(Any, 30.0),
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("FFFFFFFF")),
draw_emojis=True,
)
return img

View File

@ -0,0 +1,26 @@
import asyncio
import imagetext_py
import PIL.Image
from .base.fonts import HARMONYOS_SANS_SC_REGULAR
from .base.path import ASSETS
pt_image = PIL.Image.open(ASSETS / "ptsay.png").convert("RGBA")
def _draw_pt(saying: str):
img = pt_image.copy()
with imagetext_py.Writer(img) as iw:
iw.draw_text_wrapped(
saying, 259, 278, 0.5, 0.5, 360, 48, HARMONYOS_SANS_SC_REGULAR,
imagetext_py.Paint.Color(imagetext_py.Color.from_hex("000000FF")),
1.0,
imagetext_py.TextAlign.Center,
draw_emojis=True,
)
return img
async def draw_pt(saying: str):
return await asyncio.to_thread(_draw_pt, saying)

View File

@ -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_dice() + "\n```").export())
await evt.send(await UniMessage().text("```\n" + roll_number() + "\n```").export())
elif isinstance(event, ConsoleMessageEvent):
await evt.send(await UniMessage().text(roll_dice()).export())
await evt.send(await UniMessage().text(roll_number()).export())
else:
await evt.send(await UniMessage().text(roll_dice(wide=True)).export())
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, 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:
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())

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1,3 @@
from pathlib import Path
ASSETS = Path(__file__).parent.parent / "assets"

View File

@ -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

View File

@ -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