import { createCanvas, loadImage, registerFont } from "canvas"; import sharp from "sharp"; import { MiQOptions } from "./miq"; import { readFileSync } from "fs"; // フォント読み込み registerFont("miq/fonts/MPLUS.ttf", { family: "M PLUS Rounded 1c", weight: "400", }); function maxLengthCut( text: string, maxLength: number, ) { if (text.length > maxLength) { text = text.substring(0, maxLength) + "..."; } return text; } function autoLineBreak( text: string, maxWidth: number, font: string = '48px "M PLUS Rounded 1c"' ): string { const ctx = createCanvas(maxWidth, 100).getContext("2d"); ctx.font = font; const lines: string[] = []; let currentLine = ""; 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) { 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) lines.push(currentLine); return lines.join("\n"); } function textReplace( text: string, maxWidth: number, font?: string ) { // 改行削除 text = text.replaceAll("\n", ""); text = text.replaceAll("\r", ""); // 自動改行 text = autoLineBreak(text, maxWidth, font); // 100文字以上を省略 text = maxLengthCut(text, 100); return text; } 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("miq/PersonIcon.txt", "utf-8"); } const buffer = await bufferReq.arrayBuffer(); if (color) { const img = await sharp(Buffer.from(buffer)) .png() .toBuffer(); result = `data:image/png;base64,${img.toString("base64")}`; } else { const img = await sharp(Buffer.from(buffer)) .png() .grayscale() .toBuffer(); result = `data:image/png;base64,${img.toString("base64")}`; } return result; } /** * A function to generate * Make it a quote on Node.js. */ export default async function MiQ({ type, color, text, iconURL, userName, userID, }: MiQOptions) { // 初期化 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( color, 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"; let x = 910; let y = 480; ctx.fillText(`- ${userName}`, x, y, canvas.width-iconSize); // ユーザーID描画 ctx.font = '28px "M PLUS Rounded 1c"'; ctx.fillStyle = "#b4b4b4"; ctx.fillText(`@${userID}`, x, y+50, canvas.width-iconSize); // 本文描画 const maxWidth = canvas.width - iconSize; ctx.font = '48px "M PLUS Rounded 1c"'; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillStyle = "white"; text = textReplace(text, maxWidth, ctx.font); ctx.fillText(text, x, 80); // フェード描画 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 (type) { case "Buffer": return canvas.toBuffer(); case "Base64Data": return canvas.toDataURL() .replace("data:image/png;base64,", ""); case "Base64URL": return canvas.toDataURL(); default: return "Error: The type property is invalid."; } }