First commit

This commit is contained in:
2026-04-04 22:16:26 +09:00
commit 1ed3e7415c
11 changed files with 1137 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
import { createCanvas, loadImage, registerFont } from "canvas";
import sharp from "sharp";
import { readFileSync } from "node:fs";
registerFont(`${import.meta.dirname}/../assets/fonts/MPLUS.ttf`, {
family: "M PLUS Rounded 1c",
weight: "400",
});
function autoLineBreak(
text: string,
maxWidth: number,
maxHeight: number,
font: string,
) {
const ctx = createCanvas(maxWidth, 100).getContext("2d")!;
ctx.font = font;
const lines: string[] = [];
let currentLine = "";
const lineHeight = parseInt(font);
const maxLines = Math.floor(maxHeight / lineHeight);
for (let i = 0; i < text.length; i++) {
const char = text[i];
const testLine = currentLine + char;
const width = ctx.measureText(testLine).width;
if (width > maxWidth) {
if (lines.length + 1 === maxLines) {
let trimmed = currentLine;
while (ctx.measureText(trimmed + "...").width > maxWidth && trimmed.length > 0) {
trimmed = trimmed.slice(0, -1);
}
lines.push(trimmed + "...");
return lines.join("\n");
} else {
lines.push(currentLine);
currentLine = char;
}
while (ctx.measureText(currentLine).width > maxWidth && currentLine.length > 1) {
const cutPoint = currentLine.length - 1;
lines.push(currentLine.slice(0, cutPoint));
currentLine = currentLine.slice(cutPoint);
}
} else {
currentLine = testLine;
}
}
if (currentLine) {
if (lines.length < maxLines) {
lines.push(currentLine);
} else if (lines.length === maxLines) {
let trimmed = currentLine;
while (ctx.measureText(trimmed + "...").width > maxWidth && trimmed.length > 0) {
trimmed = trimmed.slice(0, -1);
}
lines[maxLines - 1] = trimmed + "...";
}
}
return lines.join("\n");
}
function textReplace(text: string, maxWidth: number, font: string, maxHeight: number) {
text = text.replaceAll("\n", "")
text = text.replaceAll("\r", "");
return autoLineBreak(text, maxWidth, maxHeight, font)
}
async function iconReplace(color: boolean, iconURL: string) {
let result = "";
const bufferReq = await fetch(iconURL, { method: "GET", cache: "no-store" });
if (bufferReq.status < 200 || bufferReq.status > 299) {
return readFileSync(`${import.meta.dirname}/../assets/PersonIcon.txt`, "utf-8");
}
const buffer = await bufferReq.arrayBuffer();
if (color) {
const img = await sharp(Buffer.from(buffer)).resize(512, 512, { fit: "inside" }).png().toBuffer();
result = `data:image/png;base64,${img.toString("base64")}`;
} else {
const img = await sharp(Buffer.from(buffer)).resize(512, 512, { fit: "inside" }).png().grayscale().toBuffer();
result = `data:image/png;base64,${img.toString("base64")}`;
}
return result;
}
export default async function MiQ(data: {
type: "Buffer" | "Base64Data" | "Base64URL";
color: boolean;
text: string;
iconURL: string;
username: string;
userid: string;
}) {
const canvas = createCanvas(1200, 630);
const ctx = canvas.getContext("2d");
// 背景
ctx.fillStyle = "black";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// アイコン
const iconImg = await loadImage(await iconReplace(data.color, data.iconURL));
const iconSize = canvas.height;
ctx.drawImage(iconImg, 0, 0, iconSize, iconSize);
// ユーザー名
ctx.font = '38px "M PLUS Rounded 1c"';
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const x = 910;
const y = 520;
ctx.fillText(`- ${data.username}`, x, y, canvas.width - iconSize);
// ユーザーID
ctx.font = '28px "M PLUS Rounded 1c"';
ctx.fillStyle = "#b4b4b4";
ctx.fillText(`@${data.userid}`, x, y + 50, canvas.width - iconSize);
// 本文
const maxWidth = canvas.width - iconSize;
const maxHeight = 480 - 19;
const lineHeight = 40;
ctx.font = `${lineHeight}px "M PLUS Rounded 1c"`;
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.textBaseline = "top";
const wrappedText = textReplace(data.text, maxWidth, ctx.font, maxHeight);
const lines = wrappedText.split("\n");
lines.forEach((line, index) => {
ctx.fillText(line, x, 40 + index * lineHeight);
});
// フェード
const fadeColor = "black";
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, "rgba(0, 0, 0, 0)");
gradient.addColorStop(0.5, fadeColor);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, iconSize, canvas.height);
switch (data.type) {
case "Buffer":
return canvas.toBuffer();
case "Base64Data":
return canvas.toDataURL().replace("data:image/png;base64,", "");
case "Base64URL":
return canvas.toDataURL();
default:
return new Error("The type property is invalid.");
}
}