almtools
Version:
Tools Downloader For WhatsApp Bot
438 lines (352 loc) • 13 kB
JavaScript
const { createCanvas, loadImage } = require('canvas');
const axios = require('axios');
async function generateQC({ name, text, background, profilePicture, quoted, mode = "bright" }) {
mode = mode == "dark" ? "dark" : "bright";
const measureCanvas = createCanvas(1, 1);
const measureCtx = measureCanvas.getContext('2d');
const colorScheme = mode === "bright" ? {
defaultBg: '#F5F5F5',
overlayBg: 'rgba(255, 255, 255, 0.05)',
bubbleGradientStart: '#FFFFFF',
bubbleGradientEnd: '#F0F0F0',
bubbleBorder: 'rgba(0, 0, 0, 0.1)',
nameColor: getRandomColor(),
mainTextColor: '#000000',
quotedBg: '#E8E8E8',
quotedBorder: 'rgba(245, 244, 242, 1)',
quotedNameColor: getRandomColor(),
quotedTextColor: '#2E2E2E',
timeColor: 'rgba(46, 46, 46, 1)',
shadowColor: 'rgba(0, 0, 0, 0.15)'
} : {
defaultBg: '#0F0F23',
overlayBg: 'rgba(0, 0, 0, 0.05)',
bubbleGradientStart: '#3A3A3D',
bubbleGradientEnd: '#2D2D30',
bubbleBorder: 'rgba(255, 255, 255, 0.1)',
nameColor: getRandomColor(),
mainTextColor: '#FFFFFF',
quotedBg: '#1E1E1E',
quotedBorder: 'rgba(255, 255, 255, 0.1)',
quotedNameColor: getRandomColor(),
quotedTextColor: '#CCCCCC',
timeColor: 'rgba(255, 255, 255, 0.6)',
shadowColor: 'rgba(0, 0, 0, 0.3)'
};
const paddingX = 25;
const paddingY = 16;
const quotedPaddingX = 25;
const quotedPaddingY = 14;
const quotedBorderWidth = 3;
const maxBubbleWidth = 392;
const minBubbleWidth = 200;
const textHeight = 28;
const lineSpacing = 10;
measureCtx.font = 'bold 28px Roboto';
const nameWidth = measureCtx.measureText(name).width;
const nameHeight = 46;
measureCtx.font = '26px Roboto';
const maxMainTextWidth = maxBubbleWidth - (paddingX * 2);
const textLines = wrapText(measureCtx, text, maxMainTextWidth);
const totalTextHeight = (textLines.length * textHeight) + ((textLines.length - 1) * lineSpacing);
let quotedHeight = 0;
let quotedLines = [];
let quotedTextWidth = 0;
let quotedNameWidth = 0;
if (quoted) {
measureCtx.font = 'bold 20px Roboto';
quotedNameWidth = measureCtx.measureText(quoted.name).width;
const maxQuotedContentWidth = maxBubbleWidth - (paddingX * 2) - (quotedPaddingX * 2) - quotedBorderWidth - 20;
if (quoted.message.text) {
measureCtx.font = '18px Roboto';
quotedLines = wrapText(measureCtx, quoted.message.text, maxQuotedContentWidth);
quotedTextWidth = Math.max(...quotedLines.map(line => measureCtx.measureText(line).width));
}
const singleQuotedLineHeight = 26;
const quotedTextTotalHeight = quotedLines.length * singleQuotedLineHeight;
quotedHeight = quotedPaddingY * 2 + 34 + quotedTextTotalHeight + (quotedLines.length > 1 ? (quotedLines.length - 1) * 6 : 0);
}
const contentWidths = [
nameWidth,
...textLines.map(line => {
measureCtx.font = '26px Roboto';
return measureCtx.measureText(line).width;
})
];
if (quoted) {
const quotedContentWidth = Math.max(quotedNameWidth, quotedTextWidth) + quotedPaddingX * 2 + quotedBorderWidth + 10;
contentWidths.push(quotedContentWidth);
}
const maxContentWidth = Math.max(...contentWidths);
const bubbleWidth = Math.max(minBubbleWidth, Math.min(maxContentWidth + paddingX * 2, maxBubbleWidth));
const bubbleHeight = nameHeight + totalTextHeight + paddingY * 2 + 40 + quotedHeight + (quoted ? 16 : 0);
const finalBubbleHeight = Math.max(bubbleHeight, 120);
const pfpSize = 80;
const pfpBorderSize = 4;
const contentWidth = bubbleWidth + pfpSize + 18;
const contentHeight = Math.max(finalBubbleHeight, pfpSize + 20);
const paddingAround = 28;
const canvasContentWidth = contentWidth + paddingAround * 2;
const canvasContentHeight = contentHeight + paddingAround * 2;
const squareCanvasSize = Math.max(canvasContentWidth, canvasContentHeight);
const canvas = createCanvas(squareCanvasSize, squareCanvasSize);
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
const offsetX = (squareCanvasSize - canvasContentWidth) / 2;
const offsetY = (squareCanvasSize - canvasContentHeight) / 2;
if (background?.startsWith("#")) {
ctx.fillStyle = background;
ctx.fillRect(0, 0, squareCanvasSize, squareCanvasSize);
} else {
const bgImg = await loadImageSafe(mode == 'bright' ? "https://files.catbox.moe/srjg8h.jpg" : "https://files.catbox.moe/nq9pfq.jpg");
if (bgImg) {
ctx.drawImage(bgImg, 0, 0, squareCanvasSize, squareCanvasSize);
ctx.fillStyle = colorScheme.overlayBg;
ctx.fillRect(0, 0, squareCanvasSize, squareCanvasSize);
} else {
ctx.fillStyle = mode === 'bright' ? '#F5F5F5' : '#0F0F23';
ctx.fillRect(0, 0, squareCanvasSize, squareCanvasSize);
}
}
const pfpX = paddingAround;
const pfpY = 44
const bubbleX = paddingAround + pfpSize + 18;
const bubbleY = paddingAround;
const pfpImg = await loadImageSafe(profilePicture);
if (pfpImg) {
ctx.save();
ctx.shadowColor = colorScheme.shadowColor;
ctx.shadowBlur = 10;
ctx.shadowOffsetY = 3;
ctx.fillStyle = '#FFFFFF';
ctx.beginPath();
ctx.arc(
offsetX + pfpX + pfpSize / 2,
offsetY + pfpY + pfpSize / 2,
(pfpSize / 2) + pfpBorderSize,
0,
Math.PI * 2
);
ctx.fill();
ctx.beginPath();
ctx.arc(
offsetX + pfpX + pfpSize / 2,
offsetY + pfpY + pfpSize / 2,
pfpSize / 2,
0,
Math.PI * 2
);
ctx.closePath();
ctx.clip();
ctx.drawImage(pfpImg, offsetX + pfpX, offsetY + pfpY, pfpSize, pfpSize);
ctx.restore();
}
ctx.save();
ctx.shadowColor = colorScheme.shadowColor;
ctx.shadowBlur = 18;
ctx.shadowOffsetY = 4;
const gradient = ctx.createLinearGradient(
offsetX + bubbleX,
offsetY + bubbleY,
offsetX + bubbleX,
offsetY + bubbleY + finalBubbleHeight
);
gradient.addColorStop(0, colorScheme.bubbleGradientStart);
gradient.addColorStop(1, colorScheme.bubbleGradientEnd);
ctx.fillStyle = gradient;
drawRoundedRect(
ctx,
offsetX + bubbleX,
offsetY + bubbleY,
bubbleWidth,
finalBubbleHeight,
20
);
ctx.fill();
const tailSize = 20;
const tailX = offsetX + bubbleX;
const tailY = offsetY + bubbleY + 34;
drawTopLeftBubbleTail(ctx, tailX, tailY, tailSize, mode);
ctx.restore();
ctx.strokeStyle = colorScheme.bubbleBorder;
ctx.lineWidth = 1;
drawRoundedRect(
ctx,
offsetX + bubbleX,
offsetY + bubbleY,
bubbleWidth,
finalBubbleHeight,
20
);
ctx.stroke();
let currentContentY = offsetY + bubbleY + 32;
ctx.font = 'bold 28px Roboto';
ctx.fillStyle = colorScheme.nameColor;
ctx.fillText(name, offsetX + bubbleX + paddingX, currentContentY);
currentContentY += 50;
if (quoted) {
const quotedY = currentContentY;
const quotedX = offsetX + bubbleX + paddingX;
const maxQuotedBgWidth = bubbleWidth - (paddingX * 2);
const quotedBgWidth = Math.min(
Math.max(quotedNameWidth, quotedTextWidth) + quotedPaddingX * 2 + quotedBorderWidth + 10,
maxQuotedBgWidth
);
ctx.fillStyle = colorScheme.quotedBg;
drawRoundedRect(ctx, quotedX, quotedY - 10, quotedBgWidth, quotedHeight, 10);
ctx.fill();
ctx.strokeStyle = colorScheme.quotedBorder;
ctx.lineWidth = 1;
drawRoundedRect(ctx, quotedX, quotedY - 10, quotedBgWidth, quotedHeight, 10);
ctx.stroke();
ctx.fillStyle = colorScheme.quotedNameColor;
ctx.fillRect(quotedX + 10, quotedY - 6, 4, quotedHeight - 8);
ctx.font = 'bold 20px Roboto';
ctx.fillStyle = colorScheme.quotedNameColor;
ctx.fillText(quoted.name, quotedX + quotedPaddingX, quotedY + quotedPaddingY + 10);
if (quoted.message.text) {
ctx.font = '18px Roboto';
ctx.fillStyle = colorScheme.quotedTextColor;
ctx.save();
drawRoundedRect(ctx, quotedX + quotedBorderWidth + 6, quotedY - 10, quotedBgWidth - quotedBorderWidth - 12, quotedHeight, 10);
ctx.clip();
await Promise.all(quotedLines.map(async (line, index) => {
await drawTextWithEmoji(
ctx,
line,
quotedX + quotedPaddingX,
quotedY + quotedPaddingY + 36 + (index * 26),
'18px Roboto',
colorScheme.quotedTextColor
);
}));
ctx.restore();
}
currentContentY += quotedHeight + 16;
}
ctx.font = '26px Roboto';
ctx.fillStyle = colorScheme.mainTextColor;
textLines.forEach(async(line, index) => {
await drawTextWithEmoji(
ctx,
line,
offsetX + bubbleX + paddingX,
currentContentY + 5 + (index * (textHeight + lineSpacing)),
'26px Roboto',
colorScheme.mainTextColor
);
});
const now = new Date();
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const time = `${hours}.${minutes}`;
ctx.font = '22px Roboto';
ctx.fillStyle = colorScheme.timeColor;
const timeWidth = ctx.measureText(time).width;
ctx.fillText(
time,
offsetX + bubbleX + bubbleWidth - timeWidth - paddingX,
offsetY + bubbleY + finalBubbleHeight - 18
);
return canvas.toBuffer('image/png');
}
module.exports = generateQC;
function getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
async function drawTextWithEmoji(ctx, text, x, y, font, color) {
ctx.font = font;
ctx.fillStyle = color;
const regex = emojiRegex;
let match;
let currentX = x;
for (let i = 0; i < text.length;) {
const emojiMatch = regex.exec(text.slice(i));
if (emojiMatch && emojiMatch.index === 0) {
const emoji = emojiMatch[0];
const codepoint = twemoji.convert.toCodePoint(emoji);
const url = `https://twemoji.maxcdn.com/v/latest/72x72/${codepoint}.png`;
try {
const img = await loadImage(url);
const fontSize = parseInt(font.match(/\d+/)[0]);
const size = fontSize + 6; // emoji size a bit larger
ctx.drawImage(img, currentX, y - fontSize + 4, size, size);
currentX += size - 6;
} catch (e) {
// fallback ke teks jika gagal load
ctx.fillText(emoji, currentX, y);
currentX += ctx.measureText(emoji).width;
}
i += emoji.length;
} else {
const normalChar = text[i];
ctx.fillText(normalChar, currentX, y);
currentX += ctx.measureText(normalChar).width;
i++;
}
}
}
async function loadImageSafe(source) {
if (!source) return null;
try {
if (source.startsWith("http")) {
const response = await axios.get(source, { responseType: 'arraybuffer' });
return await loadImage(response.data);
} else {
return await loadImage(source);
}
} catch (e) {
console.warn("Failed to load image:", source);
return null;
}
}
function wrapText(ctx, text, maxWidth) {
const words = text.split(' ');
const lines = [];
let currentLine = words[0];
for (let i = 1; i < words.length; i++) {
const word = words[i];
const width = ctx.measureText(currentLine + " " + word).width;
if (width < maxWidth) {
currentLine += " " + word;
} else {
lines.push(currentLine);
currentLine = word;
}
}
lines.push(currentLine);
return lines;
}
function drawRoundedRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
function drawTopLeftBubbleTail(ctx, x, y, size, mode) {
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x - size, y - size / 2);
ctx.lineTo(x, y - size);
ctx.closePath();
const gradient = ctx.createLinearGradient(x, y - size, x, y);
gradient.addColorStop(0, mode === "bright" ? '#FFFFFF' : '#3A3A3D');
gradient.addColorStop(1, mode === "bright" ? '#F0F0F0' : '#2D2D30');
ctx.fillStyle = gradient;
ctx.fill();
ctx.strokeStyle = mode === "bright" ? 'rgba(0, 0, 0, 0.1)' : 'rgba(255, 255, 255, 0.1)';
ctx.stroke();
}