UNPKG

@cognima/banners

Version:

Biblioteca avançada para geração de banners dinâmicos para diversas plataformas e aplicações

387 lines (335 loc) 13 kB
"use strict"; /** * Módulo de Banner de Status do WhatsApp * * Este módulo gera banners no estilo de status do WhatsApp com texto personalizado, * gradientes e efeitos visuais. * * @author Cognima Team (melhorado) * @version 2.0.0 */ Object.defineProperty(exports, "__esModule", { value: true }); const pureimage = require("pureimage"); const path = require("path"); const { loadImageWithAxios, encodeToBuffer, roundRect, wrapText, registerFontIfNeeded, isValidHexColor, DEFAULT_FONT_FAMILY, applyTextShadow, clearShadow, createLinearGradient, hexToRgba } = require("../utils"); /** * @class WhatsAppStatus * @classdesc Gera um banner no estilo de status do WhatsApp. * @example const statusCard = new WhatsAppStatus() * .setText("Bom dia a todos!") * .setBackground("image", "background.jpg") * .setTextColor("#FFFFFF") * .build(); */ module.exports = class WhatsAppStatus { constructor(options) { // Dados Principais this.text = "Digite seu status aqui"; this.author = null; this.authorAvatar = null; this.timestamp = new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }); // Personalização Visual this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path }; this.background = { type: "color", value: "#128C7E" }; // Cor padrão do WhatsApp this.textColor = "#FFFFFF"; this.textAlign = "center"; this.textSize = 32; this.overlayOpacity = 0.3; this.useTextShadow = true; this.useEmoji = false; this.emoji = "❤️"; // Configurações de Layout this.cardWidth = 720; this.cardHeight = 1280; this.cornerRadius = 0; // Status do WhatsApp não tem cantos arredondados this.padding = 40; } // --- Setters para Dados Principais --- /** * Define o texto do status * @param {string} text - Texto do status * @returns {WhatsAppStatus} - Instância atual para encadeamento */ setText(text) { if (!text || typeof text !== "string") throw new Error("O texto do status deve ser uma string não vazia."); this.text = text; return this; } /** * Define o autor do status * @param {string} name - Nome do autor * @returns {WhatsAppStatus} - Instância atual para encadeamento */ setAuthor(name) { if (!name || typeof name !== "string") throw new Error("O nome do autor deve ser uma string não vazia."); this.author = name; return this; } /** * Define o avatar do autor * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do avatar * @returns {WhatsAppStatus} - Instância atual para encadeamento */ setAuthorAvatar(image) { if (!image) throw new Error("A fonte da imagem do avatar não pode estar vazia."); this.authorAvatar = image; return this; } /** * Define o timestamp do status * @param {string} time - Hora do status (ex: "14:30") * @returns {WhatsAppStatus} - Instância atual para encadeamento */ setTimestamp(time) { if (!time || typeof time !== "string") throw new Error("O timestamp deve ser uma string não vazia."); this.timestamp = time; return this; } // --- Setters para Personalização Visual --- /** * Define o plano de fundo do status * @param {string} type - Tipo de plano de fundo ('color' ou 'image') * @param {string} value - Valor do plano de fundo (cor hexadecimal ou URL/caminho da imagem) * @returns {WhatsAppStatus} - Instância atual para encadeamento */ setBackground(type, value) { const types = ["color", "image"]; if (!type || !types.includes(type.toLowerCase())) throw new Error("O tipo de plano de fundo deve ser 'color' ou 'image'."); if (!value) throw new Error("O valor do plano de fundo não pode estar vazio."); if (type.toLowerCase() === "color" && !isValidHexColor(value)) throw new Error("Cor de plano de fundo inválida. Use o formato hexadecimal."); this.background = { type: type.toLowerCase(), value }; return this; } /** * Define a cor do texto * @param {string} color - Cor hexadecimal * @returns {WhatsAppStatus} - Instância atual para encadeamento */ setTextColor(color) { if (!color || !isValidHexColor(color)) throw new Error("Cor de texto inválida. Use o formato hexadecimal."); this.textColor = color; return this; } /** * Define o alinhamento do texto * @param {string} align - Alinhamento do texto ('left', 'center', 'right') * @returns {WhatsAppStatus} - Instância atual para encadeamento */ setTextAlign(align) { const validAlignments = ["left", "center", "right"]; if (!align || !validAlignments.includes(align.toLowerCase())) { throw new Error(`Alinhamento de texto inválido. Use um dos seguintes: ${validAlignments.join(", ")}`); } this.textAlign = align.toLowerCase(); return this; } /** * Define o tamanho do texto * @param {number} size - Tamanho do texto em pixels * @returns {WhatsAppStatus} - Instância atual para encadeamento */ setTextSize(size) { if (typeof size !== "number" || size < 16 || size > 72) { throw new Error("O tamanho do texto deve estar entre 16 e 72 pixels."); } this.textSize = size; return this; } /** * Define a opacidade da sobreposição * @param {number} opacity - Valor de opacidade (0-1) * @returns {WhatsAppStatus} - Instância atual para encadeamento */ setOverlayOpacity(opacity) { if (typeof opacity !== "number" || opacity < 0 || opacity > 1) throw new Error("A opacidade da sobreposição deve estar entre 0 e 1."); this.overlayOpacity = opacity; return this; } /** * Ativa ou desativa a sombra de texto * @param {boolean} enabled - Se a sombra de texto deve ser ativada * @returns {WhatsAppStatus} - Instância atual para encadeamento */ enableTextShadow(enabled = true) { this.useTextShadow = enabled; return this; } /** * Ativa ou desativa o emoji flutuante * @param {boolean} enabled - Se o emoji deve ser ativado * @param {string} emoji - Emoji a ser usado * @returns {WhatsAppStatus} - Instância atual para encadeamento */ enableEmoji(enabled = true, emoji = "❤️") { this.useEmoji = enabled; if (emoji && typeof emoji === "string") { this.emoji = emoji; } return this; } /** * Define as dimensões do card * @param {number} width - Largura do card em pixels * @param {number} height - Altura do card em pixels * @returns {WhatsAppStatus} - Instância atual para encadeamento */ setCardDimensions(width, height) { if (typeof width !== "number" || width < 400 || width > 1080) { throw new Error("A largura do card deve estar entre 400 e 1080 pixels."); } if (typeof height !== "number" || height < 600 || height > 1920) { throw new Error("A altura do card deve estar entre 600 e 1920 pixels."); } this.cardWidth = width; this.cardHeight = height; return this; } // --- Método de Construção --- /** * Constrói o banner e retorna um buffer de imagem * @returns {Promise<Buffer>} - Buffer contendo a imagem do banner */ async build() { // --- Registro de Fonte --- const registeredFontName = await registerFontIfNeeded(this.font); // --- Configuração do Canvas --- const cardWidth = this.cardWidth; const cardHeight = this.cardHeight; const padding = this.padding; const canvas = pureimage.make(cardWidth, cardHeight); const ctx = canvas.getContext("2d"); // --- Desenha Plano de Fundo --- if (this.background.type === "color") { // Plano de fundo de cor sólida ctx.fillStyle = this.background.value; ctx.fillRect(0, 0, cardWidth, cardHeight); } else { // Plano de fundo de imagem try { const img = await loadImageWithAxios(this.background.value); const aspect = img.width / img.height; let drawWidth = cardWidth; let drawHeight = cardWidth / aspect; // Ajusta as dimensões para cobrir todo o card if (drawHeight < cardHeight) { drawHeight = cardHeight; drawWidth = cardHeight * aspect; } const offsetX = (cardWidth - drawWidth) / 2; const offsetY = (cardHeight - drawHeight) / 2; ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight); // Aplica sobreposição para melhorar legibilidade do texto ctx.globalAlpha = this.overlayOpacity; ctx.fillStyle = "#000000"; ctx.fillRect(0, 0, cardWidth, cardHeight); ctx.globalAlpha = 1; } catch (e) { console.error("Falha ao desenhar imagem de plano de fundo:", e.message); ctx.fillStyle = "#128C7E"; // Cor padrão do WhatsApp ctx.fillRect(0, 0, cardWidth, cardHeight); } } // --- Desenha Emoji Flutuante (se ativado) --- if (this.useEmoji) { const emojiSize = Math.min(cardWidth, cardHeight) * 0.4; ctx.globalAlpha = 0.2; ctx.font = `${emojiSize}px Arial`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(this.emoji, cardWidth / 2, cardHeight / 2); wrapText(ctx, this.emoji, cardWidth / 2, cardHeight / 2, cardWidth - (padding * 2), this.textSize * 1.2, registeredFontName); ctx.globalAlpha = 1; } // --- Desenha Texto Principal --- ctx.fillStyle = this.textColor; ctx.font = `bold ${this.textSize}px ${registeredFontName}-Bold`; ctx.textAlign = this.textAlign; ctx.textBaseline = "middle"; // Aplica sombra de texto se ativada if (this.useTextShadow) { applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 3, 2, 2); } // Calcula a posição Y do texto com base no alinhamento const textY = cardHeight / 2; // Calcula a posição X do texto com base no alinhamento let textX; switch (this.textAlign) { case "left": textX = padding; break; case "right": textX = cardWidth - padding; break; case "center": default: textX = cardWidth / 2; break; } // Desenha o texto com quebra de linha wrapText(ctx, this.text, textX, textY, cardWidth - (padding * 2), this.textSize * 1.2, registeredFontName); // Remove sombra if (this.useTextShadow) { clearShadow(ctx); } // --- Desenha Informações do Autor (se fornecidas) --- if (this.author || this.authorAvatar) { const authorAreaHeight = 60; const authorAreaY = cardHeight - authorAreaHeight - padding; const avatarSize = 40; // Desenha o fundo semi-transparente para a área do autor ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; roundRect(ctx, padding, authorAreaY, cardWidth - (padding * 2), authorAreaHeight, 10, true, false); let authorX = padding + 10; // Desenha o avatar do autor (se fornecido) if (this.authorAvatar) { try { ctx.save(); ctx.beginPath(); ctx.arc(authorX + avatarSize / 2, authorAreaY + authorAreaHeight / 2, avatarSize / 2, 0, Math.PI * 2); ctx.closePath(); ctx.clip(); const avatarImg = await loadImageWithAxios(this.authorAvatar); ctx.drawImage(avatarImg, authorX, authorAreaY + (authorAreaHeight - avatarSize) / 2, avatarSize, avatarSize); ctx.restore(); authorX += avatarSize + 10; } catch (e) { console.error("Falha ao desenhar avatar do autor:", e.message); authorX = padding + 10; } } // Desenha o nome do autor (se fornecido) if (this.author) { ctx.fillStyle = "#FFFFFF"; ctx.font = `medium 18px ${registeredFontName}-Medium`; ctx.textAlign = "left"; ctx.textBaseline = "middle"; ctx.fillText(this.author, authorX, authorAreaY + authorAreaHeight / 2 - 5); wrapText(ctx, this.author, authorX, authorAreaY + authorAreaHeight / 2 - 5, cardWidth - (padding * 2), this.textSize * 1.2, registeredFontName); // Desenha o timestamp ctx.fillStyle = "rgba(255, 255, 255, 0.7)"; ctx.font = `regular 14px ${registeredFontName}-Regular`; ctx.fillText(this.timestamp, authorX, authorAreaY + authorAreaHeight / 2 + 15); wrapText(ctx, this.timestamp, authorX, authorAreaY + authorAreaHeight / 2 + 15, cardWidth - (padding * 2), this.textSize * 1.2, registeredFontName); } } // --- Codifica e Retorna Buffer --- try { return await encodeToBuffer(canvas); } catch (err) { console.error("Falha ao codificar o card de Status do WhatsApp:", err); throw new Error("Não foi possível gerar o buffer de imagem do card de Status do WhatsApp."); } } };