@cognima/banners
Version:
Biblioteca avançada para geração de banners dinâmicos para diversas plataformas e aplicações
446 lines (388 loc) • 14.9 kB
JavaScript
"use strict";
/**
* Módulo de Banner de Boas-vindas/Saída
*
* Este módulo gera banners de boas-vindas ou despedida com avatar centralizado
* e mensagem personalizada.
*
* @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 WelcomeLeave
* @classdesc Gera um banner de boas-vindas ou despedida visualmente refinado com avatar central.
* @example const welcomeCard = new WelcomeLeave()
* .setAvatar("avatar.png")
* .setTitle("Bem-vindo!")
* .setUsername("NovoUsuário")
* .setMessage("Ficamos felizes em ter você aqui!")
* .build();
*/
module.exports = class WelcomeLeave {
constructor(options) {
// Dados Principais
this.avatar = null;
this.title = "Bem-vindo!";
this.username = "Usuário";
this.message = "Texto da mensagem aqui.";
// Personalização Visual
this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path };
this.background = { type: "color", value: "#2C2F33" };
this.titleColor = "#FFFFFF";
this.usernameColor = "#FFFFFF";
this.messageColor = "#B9BBBE";
this.avatarBorderColor = null;
this.cardBorderColor = null;
this.overlayOpacity = 0.3;
// Novas opções de personalização
this.useTextShadow = false;
this.useGradient = false;
this.gradientColors = { start: "#3498db", end: "#8e44ad" };
this.gradientDirection = "vertical";
this.cornerRadius = 0; // Raio dos cantos arredondados
this.avatarBorderWidth = 4; // Largura da borda do avatar
}
// --- Setters para Dados Principais ---
/**
* Define a imagem do avatar
* @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do avatar
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
setAvatar(image) {
if (!image) throw new Error("A fonte da imagem do avatar não pode estar vazia.");
this.avatar = image;
return this;
}
/**
* Define o título do banner
* @param {string} text - Texto do título
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
setTitle(text) {
if (!text || typeof text !== "string") throw new Error("O texto do título deve ser uma string não vazia.");
this.title = text;
return this;
}
/**
* Define o nome de usuário
* @param {string} name - Nome do usuário
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
setUsername(name) {
if (!name || typeof name !== "string") throw new Error("O nome de usuário deve ser uma string não vazia.");
this.username = name;
return this;
}
/**
* Define o texto da mensagem
* @param {string} text - Texto da mensagem
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
setMessage(text) {
if (!text || typeof text !== "string") throw new Error("O texto da mensagem deve ser uma string não vazia.");
this.message = text;
return this;
}
// --- Setters para Personalização Visual ---
/**
* Define o plano de fundo do banner
* @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 {WelcomeLeave} - 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 título
* @param {string} color - Cor hexadecimal
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
setTitleColor(color) {
if (!color || !isValidHexColor(color)) throw new Error("Cor de título inválida. Use o formato hexadecimal.");
this.titleColor = color;
return this;
}
/**
* Define a cor do nome de usuário
* @param {string} color - Cor hexadecimal
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
setUsernameColor(color) {
if (!color || !isValidHexColor(color)) throw new Error("Cor de nome de usuário inválida. Use o formato hexadecimal.");
this.usernameColor = color;
return this;
}
/**
* Define a cor da mensagem
* @param {string} color - Cor hexadecimal
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
setMessageColor(color) {
if (!color || !isValidHexColor(color)) throw new Error("Cor de mensagem inválida. Use o formato hexadecimal.");
this.messageColor = color;
return this;
}
/**
* Define a cor da borda do avatar
* @param {string} color - Cor hexadecimal
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
setAvatarBorderColor(color) {
if (color && !isValidHexColor(color)) throw new Error("Cor de borda do avatar inválida. Use o formato hexadecimal.");
this.avatarBorderColor = color;
return this;
}
/**
* Define a cor da borda do card
* @param {string} color - Cor hexadecimal
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
setCardBorderColor(color) {
if (color && !isValidHexColor(color)) throw new Error("Cor de borda do card inválida. Use o formato hexadecimal.");
this.cardBorderColor = color;
return this;
}
/**
* Define a opacidade da sobreposição
* @param {number} opacity - Valor de opacidade (0-1)
* @returns {WelcomeLeave} - 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 {WelcomeLeave} - Instância atual para encadeamento
*/
enableTextShadow(enabled = true) {
this.useTextShadow = enabled;
return this;
}
/**
* Ativa ou desativa o gradiente de fundo
* @param {boolean} enabled - Se o gradiente deve ser ativado
* @param {string} startColor - Cor inicial do gradiente (hexadecimal)
* @param {string} endColor - Cor final do gradiente (hexadecimal)
* @param {string} direction - Direção do gradiente ('vertical', 'horizontal', 'diagonal')
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
enableGradient(enabled = true, startColor = "#3498db", endColor = "#8e44ad", direction = "vertical") {
this.useGradient = enabled;
if (startColor && isValidHexColor(startColor)) {
this.gradientColors.start = startColor;
}
if (endColor && isValidHexColor(endColor)) {
this.gradientColors.end = endColor;
}
const validDirections = ["vertical", "horizontal", "diagonal"];
if (direction && validDirections.includes(direction.toLowerCase())) {
this.gradientDirection = direction.toLowerCase();
}
return this;
}
/**
* Define o raio dos cantos arredondados
* @param {number} radius - Raio dos cantos em pixels
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
setCornerRadius(radius) {
if (typeof radius !== "number" || radius < 0) throw new Error("O raio dos cantos deve ser um número não negativo.");
this.cornerRadius = radius;
return this;
}
/**
* Define a largura da borda do avatar
* @param {number} width - Largura da borda em pixels
* @returns {WelcomeLeave} - Instância atual para encadeamento
*/
setAvatarBorderWidth(width) {
if (typeof width !== "number" || width < 0) throw new Error("A largura da borda do avatar deve ser um número não negativo.");
this.avatarBorderWidth = width;
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() {
if (!this.avatar) throw new Error("A imagem do avatar deve ser definida usando setAvatar().");
// --- Registro de Fonte ---
const registeredFontName = await registerFontIfNeeded(this.font);
// --- Configuração do Canvas ---
const cardWidth = 700;
const cardHeight = 350;
const avatarSize = 128;
const borderRadius = this.cornerRadius;
const padding = 30;
const canvas = pureimage.make(cardWidth, cardHeight);
const ctx = canvas.getContext("2d");
// --- Desenha Plano de Fundo e Sobreposição ---
ctx.save();
roundRect(ctx, 0, 0, cardWidth, cardHeight, borderRadius, false, false);
ctx.clip();
ctx.globalAlpha = 1;
if (this.useGradient && this.background.type === "color") {
// Aplica gradiente como plano de fundo
const gradient = createLinearGradient(
ctx,
0,
0,
cardWidth,
cardHeight,
this.gradientColors.start,
this.gradientColors.end,
this.gradientDirection
);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, cardWidth, cardHeight);
} else 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);
} catch (e) {
console.error("Falha ao desenhar imagem de plano de fundo:", e.message);
ctx.fillStyle = "#2C2F33";
ctx.fillRect(0, 0, cardWidth, cardHeight);
}
}
// Aplica sobreposição para melhorar legibilidade do texto
if (this.overlayOpacity > 0 && this.background.type === "image") {
ctx.globalAlpha = this.overlayOpacity;
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, cardWidth, cardHeight);
}
ctx.restore();
ctx.globalAlpha = 1;
// --- Desenha Borda do Card (Opcional) ---
if (this.cardBorderColor) {
ctx.strokeStyle = this.cardBorderColor;
ctx.lineWidth = 5;
roundRect(ctx, ctx.lineWidth / 2, ctx.lineWidth / 2, cardWidth - ctx.lineWidth, cardHeight - ctx.lineWidth, borderRadius - ctx.lineWidth / 2, false, true);
}
// --- Desenha Avatar ---
const avatarX = cardWidth / 2 - avatarSize / 2;
const avatarY = padding + 20;
ctx.save();
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
try {
const avatarImg = await loadImageWithAxios(this.avatar);
ctx.drawImage(avatarImg, avatarX, avatarY, avatarSize, avatarSize);
} catch (e) {
console.error("Falha ao desenhar imagem do avatar:", e.message);
ctx.fillStyle = "#555";
ctx.fillRect(avatarX, avatarY, avatarSize, avatarSize);
ctx.fillStyle = "#FFF";
ctx.font = `bold 30px ${registeredFontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("?", avatarX + avatarSize / 2, avatarY + avatarSize / 2);
}
ctx.restore();
// --- Desenha Borda do Avatar (Opcional) ---
if (this.avatarBorderColor) {
ctx.strokeStyle = this.avatarBorderColor;
ctx.lineWidth = this.avatarBorderWidth;
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 + ctx.lineWidth / 2, 0, Math.PI * 2);
ctx.stroke();
ctx.closePath();
}
// --- Desenha Texto --- (Centralizado abaixo do avatar)
const textYStart = avatarY + avatarSize + 30;
const textMaxWidth = cardWidth - 2 * padding;
// Título
ctx.fillStyle = this.titleColor;
ctx.font = `bold 32px ${registeredFontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
// Aplica sombra de texto se ativada
if (this.useTextShadow) {
applyTextShadow(ctx);
}
const titleText = this.title.length > 30 ? this.title.slice(0, 27) + "..." : this.title;
wrapText(ctx, titleText, cardWidth / 2, textYStart, textMaxWidth - 40, 28, registeredFontName)
// Remove sombra para o próximo texto
if (this.useTextShadow) {
clearShadow(ctx);
}
// Nome de Usuário (Abaixo do Título)
ctx.fillStyle = this.usernameColor;
ctx.font = `medium 28px ${registeredFontName}-Medium`;
ctx.textAlign = "center";
const usernameY = textYStart + 40;
// Aplica sombra de texto se ativada
if (this.useTextShadow) {
applyTextShadow(ctx);
}
const usernameText = this.username.length > 35 ? this.username.slice(0, 32) + "..." : this.username;
// Remove sombra para o próximo texto
if (this.useTextShadow) {
clearShadow(ctx);
}
// Mensagem (Abaixo do Nome de Usuário)
ctx.fillStyle = this.messageColor;
ctx.font = `regular 22px ${registeredFontName}-Regular`;
ctx.textAlign = "center";
const messageY = usernameY + 35;
// Aplica sombra de texto se ativada
if (this.useTextShadow) {
applyTextShadow(ctx, "#000000", 2, 1, 1);
}
wrapText(ctx, this.message, cardWidth / 2, messageY, textMaxWidth - 40, 28, registeredFontName);
// Remove sombra
if (this.useTextShadow) {
clearShadow(ctx);
}
// --- Codifica e Retorna Buffer ---
try {
return await encodeToBuffer(canvas);
} catch (err) {
console.error("Falha ao codificar o card de Boas-vindas/Saída:", err);
throw new Error("Não foi possível gerar o buffer de imagem do card de Boas-vindas/Saída.");
}
}
};