UNPKG

@yamabuki/koishi-plugin-pjsk-stamp-maker

Version:

koishi-plugin-pjsk-stamp-maker改版,使用skia-canvas替代koishi-plugin-canvas

430 lines (429 loc) 18.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.drawBaseImageList = drawBaseImageList; exports.draw = draw; exports.drawTextWithoutCurve = drawTextWithoutCurve; exports.drawTextWithCurve = drawTextWithCurve; const skia_canvas_1 = require("skia-canvas"); const path = __importStar(require("path")); const config_1 = require("./config"); async function drawBaseImageList(koishiContext, baseImages) { async function drawBaseImageInList(BaseImageId) { const canvasWidth = 100; const canvasHeight = 150; const canvas = new skia_canvas_1.Canvas(canvasWidth, canvasHeight); const ctx = canvas.getContext('2d'); //获取底图 const stampBuffer = await draw(koishiContext, baseImages, '测试用文本\n测试用', BaseImageId); const stampImage = (await (0, skia_canvas_1.loadImage)(stampBuffer)); //宽度固定为100,高度自适应 const stampImageWidth = 100; const stampImageHeight = stampImage['height'] / stampImage['width'] * stampImageWidth; //画在距离顶部30px的位置 ctx.drawImage(stampImage, 0, 30, stampImageWidth, stampImageHeight); //写底图ID再下方,50px字体 const baseImageIdText = `ID: ${BaseImageId}`; ctx.font = `20px 'Kingnam Bobo', 'Microsoft YaHei'`; ctx.textAlign = 'center'; ctx.fillStyle = '#505050'; ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 4; ctx.textBaseline = 'top'; ctx.save(); ctx.strokeText(baseImageIdText, 50, 5); ctx.fillText(baseImageIdText, 50, 5); //加一个宽度1像素的描边 ctx.lineWidth = 1; ctx.strokeStyle = '#505050'; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, 150); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(100, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(100, 150); ctx.lineTo(0, 150); ctx.stroke(); ctx.beginPath(); ctx.moveTo(100, 150); ctx.lineTo(100, 0); ctx.stroke(); return canvas.toBuffer('png'); } //每一种fileDir为一行,计算出每一行的宽度,然后根据宽度和高度绘制 let maxWidth = 0; let tempWidth = 0; let fileDirCount = 1; for (let i = 0; i < baseImages.length; i++) { if (baseImages[i].fileDir != baseImages[i - 1]?.fileDir && i != 0) { fileDirCount++; maxWidth = Math.max(maxWidth, tempWidth); tempWidth = 0; } else { tempWidth += 100; } } //绘制 const canvasWidth = maxWidth; const canvasHeight = fileDirCount * 150; const canvas = new skia_canvas_1.Canvas(canvasWidth, canvasHeight); const ctx = canvas.getContext('2d'); let currentXCount = 0; let currentYCount = 0; for (let i = 0; i < baseImages.length; i++) { const baseImageData = baseImages[i]; const stampBuffer = await drawBaseImageInList(i); const stampImage = (await (0, skia_canvas_1.loadImage)(stampBuffer)); //宽度固定为100,高度自适应 const stampImageWidth = 100; const stampImageHeight = stampImage['height'] / stampImage['width'] * stampImageWidth; if (baseImages[i].fileDir != baseImages[i - 1]?.fileDir && i != 0) { currentXCount = 0; currentYCount += 1; } const x = currentXCount * 100; const y = currentYCount * 150; ctx.drawImage(stampImage, x, y); currentXCount += 1; } return canvas.toBuffer('png'); } let baseImageCache = {}; async function loadBaseImageById(koishiContext, baseImages, baseImageId) { if (!baseImageCache[baseImageId]) { const baseImageData = baseImages[baseImageId]; const img = (await (0, skia_canvas_1.loadImage)(path.join(koishiContext.baseDir, 'data', 'pjsk', 'img', baseImageData.fileDir, baseImageData.fileName))); baseImageCache[baseImageId] = img; } return baseImageCache[baseImageId]; } async function draw(context, baseImages, inputText, baseImageId) { const pluginDataDir = path.join(context.baseDir, 'data', 'pjsk'); // 随机选择表情包ID const baseImageData = baseImages[baseImageId]; const img = await loadBaseImageById(context, baseImages, baseImageId); const canvasWidth = img.width * 1.5; const canvasHeight = img.height * 1.25; // 创建画布 const canvas = new skia_canvas_1.Canvas(canvasWidth, canvasHeight); const ctx = canvas.getContext('2d'); //居中绘制底图 ctx.drawImage(img, (canvasWidth - img.width) / 2, (canvasHeight - img.height) / 2, img.width, img.height); const style = config_1.defaultStyle[baseImageData.defaultStyleId.toString()]; let drawTextOptions = { position: style.position, rotate: style.rotate, curve: style.curve, textAlign: style.textAlign, textOrientation: style.textOrientation, textScreenShare: style.textScreenShare, color: baseImageData.color, }; let textBuffer; if (drawTextOptions.curve && drawTextOptions.textOrientation === 'horizontal') { textBuffer = await drawTextWithCurve(context, inputText, drawTextOptions); } else { textBuffer = await drawTextWithoutCurve(context, inputText, drawTextOptions); } const textCanvas = (await (0, skia_canvas_1.loadImage)(textBuffer)); const textCanvasWidth = textCanvas['width']; const textCanvasHeight = textCanvas['height']; //根据position,rotate,将textCanvas绘制到canvas上,比如上方,最多占用textScreenShare比例,左右居中,不超出边界的情况下自适应缩放 if (drawTextOptions.position === 'top' || drawTextOptions.position === 'bottom') { //确定缩放比例 const scale = Math.min(canvasWidth / textCanvasWidth, canvasHeight / textCanvasHeight * drawTextOptions.textScreenShare); const screenShareHeight = canvasHeight * drawTextOptions.textScreenShare; //占用的高度 //确定绘制的宽高 const drawWidth = textCanvasWidth * scale; const drawHeight = textCanvasHeight * scale; if (drawTextOptions.rotate === 0) { //确定绘制的起点 const drawX = (canvasWidth - drawWidth) / 2; const drawY = drawTextOptions.position === 'top' ? 0 : (canvasHeight - drawHeight); //绘制 ctx.drawImage(textCanvas, drawX, drawY, drawWidth, drawHeight); } else { const drawY = drawTextOptions.position === 'top' ? 0 : (canvasHeight - drawHeight); // 旋转角度转换为弧度 const radians = drawTextOptions.rotate * Math.PI / 180; if (drawTextOptions.rotate > 0) { ctx.translate(0, drawY); ctx.rotate(radians); ctx.drawImage(textCanvas, 0, 0, drawWidth, drawHeight); } else { ctx.translate(canvasWidth, drawY); ctx.rotate(radians); ctx.translate(-drawWidth, 0); ctx.drawImage(textCanvas, 0, 0, drawWidth, drawHeight); } } } else if (drawTextOptions.position === 'left' || drawTextOptions.position === 'right') { //确定缩放比例 const scale = Math.min(canvasWidth / textCanvasWidth * drawTextOptions.textScreenShare, canvasHeight / textCanvasHeight); //确定绘制的宽高 const drawWidth = textCanvasWidth * scale; const drawHeight = textCanvasHeight * scale; if (drawTextOptions.rotate === 0) { //确定绘制的起点 const drawX = drawTextOptions.position === 'left' ? 0 : (canvasWidth - drawWidth); const drawY = (canvasHeight - drawHeight) / 2; //绘制 ctx.drawImage(textCanvas, drawX, drawY, drawWidth, drawHeight); } else { const drawX = drawTextOptions.position === 'left' ? 0 : (canvasWidth - drawWidth); // 旋转角度转换为弧度 const radians = drawTextOptions.rotate * Math.PI / 180; if (drawTextOptions.position === 'left') { if (drawTextOptions.rotate > 0) { ctx.translate(drawX, canvasHeight); ctx.rotate(radians); ctx.translate(0, -drawHeight); ctx.drawImage(textCanvas, 0, 0, drawWidth, drawHeight); } else { ctx.translate(drawX, 0); ctx.rotate(radians); ctx.drawImage(textCanvas, 0, 0, drawWidth, drawHeight); } } else { if (drawTextOptions.rotate > 0) { ctx.translate(drawX, 0); ctx.rotate(radians); ctx.drawImage(textCanvas, 0, 0, drawWidth, drawHeight); } else { ctx.translate(drawX, canvasHeight); ctx.rotate(radians); ctx.translate(0, -drawHeight); ctx.drawImage(textCanvas, 0, 0, drawWidth, drawHeight); } } } } const buffer = await canvas.toBuffer('png'); return buffer; } async function drawTextWithoutCurve(koishiContext, text, { color = '#1b1b1b', textAlign = 'center', textOrientation = 'horizontal' }) { const fontSize = 100; const lineHeight = 0.95; //行高 const lineWidth = 20; //描边宽度 // tempCanvas 用于获取文本的宽度 const tempCanvas = new skia_canvas_1.Canvas(1, 1); const tempCtx = tempCanvas.getContext('2d'); tempCtx.font = `${fontSize}px 'Kingnam Bobo', 'Microsoft YaHei'`; // 按行分割文本 const lines = text.split('\n'); // 计算最终画布的宽高 let maxWidth = 0; let maxHeight = 0; if (textOrientation === 'horizontal') { for (let line of lines) { const { width } = tempCtx.measureText(line); maxWidth = Math.max(maxWidth, width); maxHeight += fontSize * lineHeight; } //加上描边的宽度,防止文字被裁剪 maxWidth += lineWidth * 2; maxHeight += lineWidth * 2; } else { for (let line of lines) { const height = fontSize * line.length; maxHeight = Math.max(maxHeight, height); } maxWidth = fontSize * lines.length * lineHeight; //加上描边的宽度,防止文字被裁剪 maxWidth += lineWidth * 2; maxHeight += lineWidth * 2; } // 创建画布 const canvas = new skia_canvas_1.Canvas(maxWidth, maxHeight); const ctx = canvas.getContext('2d'); ctx.font = `${fontSize}px 'Kingnam Bobo', 'Microsoft YaHei'`; ctx.textAlign = textAlign; ctx.fillStyle = color; ctx.strokeStyle = '#ffffff'; ctx.lineWidth = lineWidth; ctx.textBaseline = 'top'; ctx.save(); if (textOrientation === 'horizontal') { //从下向上 if (textAlign === 'center') { ctx.translate(maxWidth / 2, 0); } else if (textAlign === 'left') { ctx.translate(0 + lineWidth, 0); } else if (textAlign === 'right') { ctx.translate(maxWidth - lineWidth, 0); } let currentY = 0; for (let line of lines) { ctx.strokeText(line, 0, currentY); ctx.fillText(line, 0, currentY); currentY += fontSize * lineHeight; } } else if (textOrientation === 'vertical') { //从右向左 let currentY = 0; let currentX = maxWidth; ctx.textAlign = 'right'; ctx.textBaseline = 'top'; for (let line of lines) { if (textAlign === 'center') { //根据文字的长度,计算出文字的起点 currentY = (maxHeight - lineWidth - line.length * fontSize) / 2; } if (textAlign === 'left') { currentY = 0; } if (textAlign === 'right') { //根据文字的长度,计算出文字的起点 currentY = maxHeight - lineWidth - line.length * fontSize; } for (let i = 0; i < line.length; i++) { const char = line[i]; // 处理中文标点符号的特殊情况 if (',。;?!、'.includes(char)) { ctx.strokeText(char, currentX - fontSize / 2, currentY); ctx.fillText(char, currentX - fontSize / 2, currentY); currentY += fontSize; // 下一个字符的y坐标 } else { ctx.strokeText(char, currentX, currentY); ctx.fillText(char, currentX, currentY); currentY += fontSize; // 下一个字符的y坐标 } } currentX -= fontSize * lineHeight; } } return canvas.toBuffer('png'); } async function drawTextWithCurve(koishiContext, text, { color = '#1b1b1b', textAlign = 'center', }) { const fontSize = 100; const lineHeight = 0.95; //行高 const lineWidth = 20; //描边宽度 const curveAngle = Math.PI / 4; //弧度 // tempCanvas 用于获取文本的宽度 const tempCanvas = new skia_canvas_1.Canvas(1, 1); const tempCtx = tempCanvas.getContext('2d'); tempCtx.font = `${fontSize}px 'Kingnam Bobo', 'Microsoft YaHei'`; // 按行分割文本 const lines = text.split('\n'); //确定文本的最大宽度 let textMaxWidth = 0; for (let line of lines) { const { width } = tempCtx.measureText(line); textMaxWidth = Math.max(textMaxWidth, width); } //弧长等于文本的最大宽度 const arcLength = textMaxWidth; //,根据弧长和弧度计算出半径 const radius = arcLength / curveAngle; //计算弦长 const chordLength = radius * Math.sin(curveAngle / 2) * 2; //计算弧到弦的最远点垂直距离 const perpendicularHeight = radius * (1 - Math.cos(curveAngle / 2)); async function drawTextLine(line) { // 计算每一行画布的宽高 const canvasWidth = chordLength + fontSize; //加上一个字的宽度,防止文字被裁剪 const canvasHeight = perpendicularHeight + fontSize + fontSize; const canvas = new skia_canvas_1.Canvas(canvasWidth, canvasHeight); const ctx = canvas.getContext('2d'); ctx.font = `${fontSize}px 'Kingnam Bobo', 'Microsoft YaHei'`; ctx.fillStyle = color; ctx.strokeStyle = '#ffffff'; ctx.lineWidth = lineWidth; ctx.textBaseline = 'bottom'; ctx.textAlign = 'center'; ctx.save(); //计算当前行的最大宽度(弧长) const currentTextMaxWidth = ctx.measureText(line).width; //前进弧度函数 function forwardAngle(angle) { ctx.rotate(angle); ctx.translate(radius * Math.sin(angle / 2) * 2, 0); } //确定起始角度和位置 let startAngle = -((Math.PI / 2) - ((Math.PI - curveAngle) / 2)); ctx.translate(fontSize / 2, canvasHeight - fontSize); ctx.rotate(startAngle); //根据textAlign,确定起始角度和位置 const angleDiff = (arcLength - currentTextMaxWidth) / radius; if (textAlign === 'center') { forwardAngle(angleDiff / 2); } else if (textAlign === 'right') { forwardAngle(angleDiff); } //依次绘制每个字 for (let char of line) { const charWidth = ctx.measureText(char).width; const charAngle = charWidth / arcLength * curveAngle; //前进半个字的宽度 forwardAngle(charAngle / 2); //绘制 ctx.strokeText(char, 0, 0); ctx.fillText(char, 0, 0); //前进半个字的宽度 forwardAngle(charAngle / 2); } return canvas.toBuffer('png'); } // 创建画布 const canvasWidth = chordLength + fontSize; //加上一个字的宽度,防止文字被裁剪 const canvasHeight = perpendicularHeight + fontSize * lineHeight * lines.length + 20; const canvas = new skia_canvas_1.Canvas(canvasWidth, canvasHeight); const ctx = canvas.getContext('2d'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const textCanvas = await drawTextLine(line); ctx.drawImage(await (0, skia_canvas_1.loadImage)(textCanvas), 0, i * (fontSize * lineHeight)); } return canvas.toBuffer('png'); }