@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
JavaScript
"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.");
}
}
};