UNPKG

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