UNPKG

almtools

Version:

Tools Downloader For WhatsApp Bot

438 lines (352 loc) 13 kB
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(); }