@cognima/banners
Version:
Biblioteca avançada para geração de banners dinâmicos para diversas plataformas e aplicações
1,320 lines (1,129 loc) • 40.7 kB
JavaScript
"use strict";
/**
* Módulo de Banner de Evento
*
* Este módulo gera banners para eventos com informações como título,
* data, local, descrição e outros elementos visuais personalizáveis.
*
* @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");
const {
DEFAULT_COLORS,
LAYOUT,
DEFAULT_DIMENSIONS
} = require("./constants");
const {
applyGlassmorphism,
applyMultiColorGradient,
applyGlow,
applyDotPattern,
applyLinePattern
} = require("./effects");
/**
* @class EventBanner
* @classdesc Gera um banner para eventos com informações detalhadas e design personalizável.
* @example const banner = new EventBanner()
* .setTitle("Nome do Evento")
* .setDate("25 de Dezembro, 2025")
* .setTime("19:00 - 23:00")
* .setLocation("Local do Evento, Cidade")
* .setDescription("Descrição detalhada do evento com informações importantes.")
* .setBackground("image", "background.jpg")
* .setStyle("modern")
* .build();
*/
module.exports = class EventBanner {
constructor(options) {
// Dados Principais
this.title = "Nome do Evento";
this.date = null;
this.time = null;
this.location = null;
this.description = null;
this.organizer = null;
this.logo = null;
this.qrCode = null;
this.ticketPrice = null;
this.contactInfo = null;
this.speakers = [];
this.sponsors = [];
this.categories = [];
this.registrationUrl = null;
// Personalização Visual
this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path };
this.background = { type: "color", value: DEFAULT_COLORS.gradient.blue.start };
this.style = "modern"; // modern, classic, minimal, bold, festive
this.colorScheme = "blue"; // blue, green, purple, red, orange, custom
this.customColors = {
primary: null,
secondary: null,
accent: null,
text: "#FFFFFF",
background: null
};
this.useGradient = true;
this.useTextShadow = true;
this.usePatterns = false;
this.patternType = "dots"; // dots, lines
this.useGlassmorphism = false;
// Configurações de Layout
this.cardWidth = DEFAULT_DIMENSIONS.banner.width;
this.cardHeight = DEFAULT_DIMENSIONS.banner.height;
this.cornerRadius = LAYOUT.cornerRadius.medium;
this.orientation = "landscape"; // landscape, portrait, square
}
// --- Setters para Dados Principais ---
/**
* Define o título do evento
* @param {string} text - Título do evento
* @returns {EventBanner} - Instância atual para encadeamento
*/
setTitle(text) {
if (!text || typeof text !== "string") throw new Error("O título do evento deve ser uma string não vazia.");
this.title = text;
return this;
}
/**
* Define a data do evento
* @param {string} text - Data do evento
* @returns {EventBanner} - Instância atual para encadeamento
*/
setDate(text) {
if (!text || typeof text !== "string") throw new Error("A data do evento deve ser uma string não vazia.");
this.date = text;
return this;
}
/**
* Define o horário do evento
* @param {string} text - Horário do evento
* @returns {EventBanner} - Instância atual para encadeamento
*/
setTime(text) {
if (!text || typeof text !== "string") throw new Error("O horário do evento deve ser uma string não vazia.");
this.time = text;
return this;
}
/**
* Define o local do evento
* @param {string} text - Local do evento
* @returns {EventBanner} - Instância atual para encadeamento
*/
setLocation(text) {
if (!text || typeof text !== "string") throw new Error("O local do evento deve ser uma string não vazia.");
this.location = text;
return this;
}
/**
* Define a descrição do evento
* @param {string} text - Descrição do evento
* @returns {EventBanner} - Instância atual para encadeamento
*/
setDescription(text) {
if (!text || typeof text !== "string") throw new Error("A descrição do evento deve ser uma string não vazia.");
this.description = text;
return this;
}
/**
* Define o organizador do evento
* @param {string} text - Nome do organizador
* @returns {EventBanner} - Instância atual para encadeamento
*/
setOrganizer(text) {
if (!text || typeof text !== "string") throw new Error("O organizador do evento deve ser uma string não vazia.");
this.organizer = text;
return this;
}
/**
* Define o logo do evento
* @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do logo
* @returns {EventBanner} - Instância atual para encadeamento
*/
setLogo(image) {
if (!image) throw new Error("A fonte da imagem do logo não pode estar vazia.");
this.logo = image;
return this;
}
/**
* Define o código QR para o evento
* @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do código QR
* @returns {EventBanner} - Instância atual para encadeamento
*/
setQRCode(image) {
if (!image) throw new Error("A fonte da imagem do código QR não pode estar vazia.");
this.qrCode = image;
return this;
}
/**
* Define o preço do ingresso
* @param {string} text - Preço do ingresso
* @returns {EventBanner} - Instância atual para encadeamento
*/
setTicketPrice(text) {
if (!text || typeof text !== "string") throw new Error("O preço do ingresso deve ser uma string não vazia.");
this.ticketPrice = text;
return this;
}
/**
* Define as informações de contato
* @param {string} text - Informações de contato
* @returns {EventBanner} - Instância atual para encadeamento
*/
setContactInfo(text) {
if (!text || typeof text !== "string") throw new Error("As informações de contato devem ser uma string não vazia.");
this.contactInfo = text;
return this;
}
/**
* Adiciona um palestrante ao evento
* @param {string} name - Nome do palestrante
* @param {string} role - Cargo/função do palestrante
* @param {string|Buffer|Object} avatar - URL, Buffer ou caminho da imagem do avatar (opcional)
* @returns {EventBanner} - Instância atual para encadeamento
*/
addSpeaker(name, role, avatar = null) {
if (!name || typeof name !== "string") throw new Error("O nome do palestrante deve ser uma string não vazia.");
if (!role || typeof role !== "string") throw new Error("O cargo/função do palestrante deve ser uma string não vazia.");
this.speakers.push({ name, role, avatar });
return this;
}
/**
* Adiciona um patrocinador ao evento
* @param {string} name - Nome do patrocinador
* @param {string|Buffer|Object} logo - URL, Buffer ou caminho da imagem do logo (opcional)
* @returns {EventBanner} - Instância atual para encadeamento
*/
addSponsor(name, logo = null) {
if (!name || typeof name !== "string") throw new Error("O nome do patrocinador deve ser uma string não vazia.");
this.sponsors.push({ name, logo });
return this;
}
/**
* Define as categorias do evento
* @param {Array<string>} categories - Array de categorias
* @returns {EventBanner} - Instância atual para encadeamento
*/
setCategories(categories) {
if (!Array.isArray(categories)) throw new Error("As categorias devem ser um array de strings.");
this.categories = categories;
return this;
}
/**
* Define a URL de registro/inscrição
* @param {string} url - URL de registro
* @returns {EventBanner} - Instância atual para encadeamento
*/
setRegistrationUrl(url) {
if (!url || typeof url !== "string") throw new Error("A URL de registro deve ser uma string não vazia.");
this.registrationUrl = url;
return this;
}
// --- Setters para Personalização Visual ---
/**
* Define o plano de fundo
* @param {string} type - Tipo de plano de fundo ('color', 'image' ou 'gradient')
* @param {string|Array} value - Valor do plano de fundo (cor hexadecimal, URL/caminho da imagem ou array de cores para gradiente)
* @returns {EventBanner} - Instância atual para encadeamento
*/
setBackground(type, value) {
const types = ["color", "image", "gradient"];
if (!type || !types.includes(type.toLowerCase())) {
throw new Error("O tipo de plano de fundo deve ser 'color', 'image' ou 'gradient'.");
}
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.");
}
if (type.toLowerCase() === "gradient") {
if (!Array.isArray(value) || value.length < 2) {
throw new Error("Para gradiente, forneça um array com pelo menos duas cores hexadecimais.");
}
for (const color of value) {
if (!isValidHexColor(color)) {
throw new Error("Todas as cores do gradiente devem estar no formato hexadecimal.");
}
}
this.useGradient = true;
}
this.background = { type: type.toLowerCase(), value };
return this;
}
/**
* Define o estilo do banner
* @param {string} style - Estilo ('modern', 'classic', 'minimal', 'bold', 'festive')
* @returns {EventBanner} - Instância atual para encadeamento
*/
setStyle(style) {
const validStyles = ["modern", "classic", "minimal", "bold", "festive"];
if (!style || !validStyles.includes(style.toLowerCase())) {
throw new Error(`Estilo inválido. Use um dos seguintes: ${validStyles.join(", ")}`);
}
this.style = style.toLowerCase();
// Ajusta configurações com base no estilo
switch (this.style) {
case "modern":
this.useGradient = true;
this.useTextShadow = true;
this.usePatterns = false;
this.useGlassmorphism = true;
this.cornerRadius = LAYOUT.cornerRadius.medium;
break;
case "classic":
this.useGradient = false;
this.useTextShadow = false;
this.usePatterns = false;
this.useGlassmorphism = false;
this.cornerRadius = LAYOUT.cornerRadius.small;
break;
case "minimal":
this.useGradient = false;
this.useTextShadow = false;
this.usePatterns = false;
this.useGlassmorphism = false;
this.cornerRadius = 0;
break;
case "bold":
this.useGradient = true;
this.useTextShadow = true;
this.usePatterns = true;
this.useGlassmorphism = false;
this.cornerRadius = LAYOUT.cornerRadius.large;
break;
case "festive":
this.useGradient = true;
this.useTextShadow = true;
this.usePatterns = true;
this.useGlassmorphism = false;
this.cornerRadius = LAYOUT.cornerRadius.medium;
break;
}
return this;
}
/**
* Define o esquema de cores
* @param {string} scheme - Esquema de cores ('blue', 'green', 'purple', 'red', 'orange', 'custom')
* @param {Object} customColors - Cores personalizadas (apenas para esquema 'custom')
* @returns {EventBanner} - Instância atual para encadeamento
*/
setColorScheme(scheme, customColors = null) {
const validSchemes = ["blue", "green", "purple", "red", "orange", "custom"];
if (!scheme || !validSchemes.includes(scheme.toLowerCase())) {
throw new Error(`Esquema de cores inválido. Use um dos seguintes: ${validSchemes.join(", ")}`);
}
this.colorScheme = scheme.toLowerCase();
if (this.colorScheme === "custom") {
if (!customColors || typeof customColors !== "object") {
throw new Error("Para o esquema de cores personalizado, forneça um objeto com as cores.");
}
this.customColors = {
primary: customColors.primary || this.customColors.primary,
secondary: customColors.secondary || this.customColors.secondary,
accent: customColors.accent || this.customColors.accent,
text: customColors.text || this.customColors.text,
background: customColors.background || this.customColors.background
};
}
return this;
}
/**
* Ativa ou desativa o uso de gradiente
* @param {boolean} enabled - Se o gradiente deve ser ativado
* @returns {EventBanner} - Instância atual para encadeamento
*/
enableGradient(enabled = true) {
this.useGradient = enabled;
return this;
}
/**
* Ativa ou desativa a sombra de texto
* @param {boolean} enabled - Se a sombra de texto deve ser ativada
* @returns {EventBanner} - Instância atual para encadeamento
*/
enableTextShadow(enabled = true) {
this.useTextShadow = enabled;
return this;
}
/**
* Ativa ou desativa o uso de padrões decorativos
* @param {boolean} enabled - Se os padrões devem ser ativados
* @param {string} type - Tipo de padrão ('dots', 'lines')
* @returns {EventBanner} - Instância atual para encadeamento
*/
enablePatterns(enabled = true, type = "dots") {
this.usePatterns = enabled;
const validTypes = ["dots", "lines"];
if (validTypes.includes(type.toLowerCase())) {
this.patternType = type.toLowerCase();
}
return this;
}
/**
* Ativa ou desativa o efeito de glassmorphism
* @param {boolean} enabled - Se o efeito deve ser ativado
* @returns {EventBanner} - Instância atual para encadeamento
*/
enableGlassmorphism(enabled = true) {
this.useGlassmorphism = enabled;
return this;
}
/**
* Define o raio dos cantos arredondados
* @param {number} radius - Raio dos cantos em pixels
* @returns {EventBanner} - 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 orientação do banner
* @param {string} orientation - Orientação ('landscape', 'portrait', 'square')
* @returns {EventBanner} - Instância atual para encadeamento
*/
setOrientation(orientation) {
const validOrientations = ["landscape", "portrait", "square"];
if (!orientation || !validOrientations.includes(orientation.toLowerCase())) {
throw new Error(`Orientação inválida. Use uma das seguintes: ${validOrientations.join(", ")}`);
}
this.orientation = orientation.toLowerCase();
// Ajusta as dimensões com base na orientação
switch (this.orientation) {
case "landscape":
this.cardWidth = DEFAULT_DIMENSIONS.banner.width;
this.cardHeight = DEFAULT_DIMENSIONS.banner.height;
break;
case "portrait":
this.cardWidth = DEFAULT_DIMENSIONS.banner.height;
this.cardHeight = DEFAULT_DIMENSIONS.banner.width;
break;
case "square":
this.cardWidth = Math.min(DEFAULT_DIMENSIONS.banner.width, DEFAULT_DIMENSIONS.banner.height);
this.cardHeight = this.cardWidth;
break;
}
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 {EventBanner} - Instância atual para encadeamento
*/
setCardDimensions(width, height) {
if (typeof width !== "number" || width < 400 || width > 1920) {
throw new Error("A largura do card deve estar entre 400 e 1920 pixels.");
}
if (typeof height !== "number" || height < 400 || height > 1920) {
throw new Error("A altura do card deve estar entre 400 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 cornerRadius = this.cornerRadius;
const padding = 30;
const canvas = pureimage.make(cardWidth, cardHeight);
const ctx = canvas.getContext("2d");
// --- Configuração de Cores com base no Esquema ---
const colors = this._getColorScheme();
// --- Desenha Plano de Fundo ---
if (this.background.type === "image") {
try {
ctx.save();
if (cornerRadius > 0) {
roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, false, false);
ctx.clip();
}
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
ctx.fillStyle = hexToRgba(colors.background || "#000000", 0.5);
ctx.fillRect(0, 0, cardWidth, cardHeight);
ctx.restore();
} catch (e) {
console.error("Falha ao desenhar imagem de plano de fundo:", e.message);
// Plano de fundo de fallback
if (this.useGradient) {
const gradient = createLinearGradient(
ctx,
0,
0,
cardWidth,
cardHeight,
colors.primary,
colors.secondary,
"diagonal"
);
ctx.fillStyle = gradient;
} else {
ctx.fillStyle = colors.primary;
}
roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
}
} else if (this.background.type === "gradient" || this.useGradient) {
// Plano de fundo com gradiente
let gradientColors;
if (this.background.type === "gradient" && Array.isArray(this.background.value)) {
gradientColors = this.background.value;
} else {
gradientColors = [colors.primary, colors.secondary];
}
const gradient = applyMultiColorGradient(
ctx,
0,
0,
cardWidth,
cardHeight,
gradientColors,
"diagonal"
);
ctx.fillStyle = gradient;
roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
} else {
// Plano de fundo de cor sólida
ctx.fillStyle = this.background.type === "color" ? this.background.value : colors.primary;
roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
}
// --- Aplica Padrões Decorativos (se ativados) ---
if (this.usePatterns) {
if (this.patternType === "dots") {
applyDotPattern(
ctx,
0,
0,
cardWidth,
cardHeight,
40,
3,
"#FFFFFF",
0.1
);
} else if (this.patternType === "lines") {
applyLinePattern(
ctx,
0,
0,
cardWidth,
cardHeight,
40,
1,
"#FFFFFF",
0.1,
"diagonal"
);
}
}
// --- Desenha Conteúdo com base no Estilo ---
switch (this.style) {
case "modern":
await this._drawModernStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
break;
case "classic":
await this._drawClassicStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
break;
case "minimal":
await this._drawMinimalStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
break;
case "bold":
await this._drawBoldStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
break;
case "festive":
await this._drawFestiveStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
break;
default:
await this._drawModernStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
}
// --- Codifica e Retorna Buffer ---
try {
return await encodeToBuffer(canvas);
} catch (err) {
console.error("Falha ao codificar o Banner de Evento:", err);
throw new Error("Não foi possível gerar o buffer de imagem do Banner de Evento.");
}
}
// --- Métodos Auxiliares Privados ---
/**
* Obtém as cores com base no esquema selecionado
* @private
*/
_getColorScheme() {
if (this.colorScheme === "custom" && this.customColors.primary) {
return this.customColors;
}
switch (this.colorScheme) {
case "green":
return {
primary: "#2ECC71",
secondary: "#27AE60",
accent: "#F1C40F",
text: "#FFFFFF",
background: "#1E8449"
};
case "purple":
return {
primary: "#9B59B6",
secondary: "#8E44AD",
accent: "#F1C40F",
text: "#FFFFFF",
background: "#6C3483"
};
case "red":
return {
primary: "#E74C3C",
secondary: "#C0392B",
accent: "#F1C40F",
text: "#FFFFFF",
background: "#922B21"
};
case "orange":
return {
primary: "#F39C12",
secondary: "#D35400",
accent: "#3498DB",
text: "#FFFFFF",
background: "#A04000"
};
case "blue":
default:
return {
primary: "#3498DB",
secondary: "#2980B9",
accent: "#F1C40F",
text: "#FFFFFF",
background: "#1F618D"
};
}
}
/**
* Desenha o estilo moderno
* @private
*/
async _drawModernStyle(ctx, fontName, colors, width, height, padding) {
// --- Desenha Área de Conteúdo Principal ---
const contentWidth = width - padding * 2;
const contentHeight = height - padding * 2;
const contentX = padding;
const contentY = padding;
// Aplica efeito de glassmorphism se ativado
if (this.useGlassmorphism) {
applyGlassmorphism(
ctx,
contentX,
contentY,
contentWidth,
contentHeight,
this.cornerRadius,
0.2,
"#FFFFFF"
);
}
// --- Desenha Logo (se fornecido) ---
let currentY = contentY + padding;
if (this.logo) {
try {
const logoSize = Math.min(contentWidth, contentHeight) * 0.15;
const logoX = contentX + padding;
const logoImg = await loadImageWithAxios(this.logo);
const aspect = logoImg.width / logoImg.height;
const logoHeight = logoSize;
const logoWidth = logoHeight * aspect;
ctx.drawImage(logoImg, logoX, currentY, logoWidth, logoHeight);
currentY += logoHeight + padding;
} catch (e) {
console.error("Falha ao desenhar logo:", e.message);
}
}
// --- Desenha Título ---
const titleFontSize = Math.min(contentWidth, contentHeight) * 0.08;
ctx.fillStyle = colors.text;
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
// Aplica sombra de texto se ativada
if (this.useTextShadow) {
applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 3, 1, 1);
}
const titleText = this.title;
ctx.fillText(titleText, contentX + contentWidth / 2, currentY);
currentY += titleFontSize * 1.2;
// Remove sombra para o próximo texto
if (this.useTextShadow) {
clearShadow(ctx);
}
// --- Desenha Data e Hora ---
if (this.date || this.time) {
const dateTimeFontSize = titleFontSize * 0.5;
ctx.fillStyle = colors.text;
ctx.font = `medium ${dateTimeFontSize}px ${fontName}-Medium`;
ctx.textAlign = "center";
let dateTimeText = "";
if (this.date && this.time) {
dateTimeText = `${this.date} • ${this.time}`;
} else if (this.date) {
dateTimeText = this.date;
} else if (this.time) {
dateTimeText = this.time;
}
ctx.fillText(dateTimeText, contentX + contentWidth / 2, currentY);
currentY += dateTimeFontSize * 1.5;
}
// --- Desenha Local ---
if (this.location) {
const locationFontSize = titleFontSize * 0.4;
ctx.fillStyle = colors.text;
ctx.font = `regular ${locationFontSize}px ${fontName}-Regular`;
ctx.textAlign = "center";
ctx.fillText(`📍 ${this.location}`, contentX + contentWidth / 2, currentY);
currentY += locationFontSize * 1.5;
}
// --- Desenha Descrição ---
if (this.description) {
const descriptionFontSize = titleFontSize * 0.35;
ctx.fillStyle = colors.text;
ctx.font = `regular ${descriptionFontSize}px ${fontName}-Regular`;
ctx.textAlign = "center";
currentY = wrapText(
ctx,
this.description,
contentX + contentWidth / 2,
currentY + padding,
contentWidth - padding * 2,
descriptionFontSize * 1.5,
fontName,
true
);
}
// --- Desenha Categorias ---
if (this.categories.length > 0) {
const categoryFontSize = titleFontSize * 0.3;
const categoryHeight = categoryFontSize * 1.5;
const categoryPadding = 10;
const categorySpacing = 10;
let categoryX = contentX + padding;
const categoryY = height - padding * 3 - categoryHeight;
ctx.font = `regular ${categoryFontSize}px ${fontName}-Regular`;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
for (const category of this.categories) {
const categoryWidth = ctx.measureText(category).width + categoryPadding * 2;
if (categoryX + categoryWidth > contentX + contentWidth - padding) {
break; // Não há mais espaço para categorias
}
// Fundo da categoria
ctx.fillStyle = hexToRgba(colors.accent, 0.8);
roundRect(ctx, categoryX, categoryY, categoryWidth, categoryHeight, categoryHeight / 2, true, false);
// Texto da categoria
ctx.fillStyle = colors.text;
ctx.fillText(category, categoryX + categoryPadding, categoryY + categoryHeight / 2);
categoryX += categoryWidth + categorySpacing;
}
}
// --- Desenha Informações de Registro ---
if (this.registrationUrl) {
const registrationFontSize = titleFontSize * 0.35;
ctx.fillStyle = colors.text;
ctx.font = `bold ${registrationFontSize}px ${fontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText("Registre-se: " + this.registrationUrl, contentX + contentWidth / 2, height - padding * 1.5);
}
// --- Desenha QR Code (se fornecido) ---
if (this.qrCode) {
try {
const qrSize = Math.min(contentWidth, contentHeight) * 0.15;
const qrX = width - qrSize - padding * 2;
const qrY = height - qrSize - padding * 2;
const qrImg = await loadImageWithAxios(this.qrCode);
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
} catch (e) {
console.error("Falha ao desenhar QR code:", e.message);
}
}
}
/**
* Desenha o estilo clássico
* @private
*/
async _drawClassicStyle(ctx, fontName, colors, width, height, padding) {
// --- Desenha Borda Decorativa ---
const borderWidth = 5;
ctx.strokeStyle = colors.accent;
ctx.lineWidth = borderWidth;
roundRect(ctx, padding / 2, padding / 2, width - padding, height - padding, this.cornerRadius, false, true);
// --- Desenha Título ---
const titleFontSize = Math.min(width, height) * 0.07;
const titleY = padding * 2;
ctx.fillStyle = colors.text;
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
const titleText = this.title;
ctx.fillText(titleText, width / 2, titleY);
// --- Desenha Linha Separadora ---
const lineY = titleY + titleFontSize * 1.5;
const lineWidth = width * 0.6;
const lineHeight = 2;
ctx.fillStyle = colors.accent;
ctx.fillRect((width - lineWidth) / 2, lineY, lineWidth, lineHeight);
// --- Desenha Data e Hora ---
const dateTimeFontSize = titleFontSize * 0.5;
const dateTimeY = lineY + lineHeight + padding;
ctx.fillStyle = colors.text;
ctx.font = `medium ${dateTimeFontSize}px ${fontName}-Medium`;
ctx.textAlign = "center";
if (this.date) {
ctx.fillText(this.date, width / 2, dateTimeY);
}
if (this.time) {
ctx.fillText(this.time, width / 2, dateTimeY + dateTimeFontSize * 1.5);
}
// --- Desenha Local ---
const locationFontSize = titleFontSize * 0.4;
const locationY = dateTimeY + (this.time ? dateTimeFontSize * 3 : dateTimeFontSize * 1.5);
if (this.location) {
ctx.fillStyle = colors.text;
ctx.font = `regular ${locationFontSize}px ${fontName}-Regular`;
ctx.textAlign = "center";
ctx.fillText(this.location, width / 2, locationY);
}
// --- Desenha Descrição ---
const descriptionFontSize = titleFontSize * 0.35;
const descriptionY = locationY + locationFontSize * 2;
if (this.description) {
ctx.fillStyle = colors.text;
ctx.font = `regular ${descriptionFontSize}px ${fontName}-Regular`;
ctx.textAlign = "center";
wrapText(
ctx,
this.description,
width / 2,
descriptionY,
width - padding * 4,
descriptionFontSize * 1.5,
fontName,
true
);
}
// --- Desenha Organizador ---
const organizerY = height - padding * 3;
if (this.organizer) {
ctx.fillStyle = colors.text;
ctx.font = `italic ${descriptionFontSize}px ${fontName}-Regular`;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText(`Organizado por: ${this.organizer}`, width / 2, organizerY);
}
// --- Desenha Informações de Contato ---
const contactY = height - padding * 1.5;
if (this.contactInfo) {
ctx.fillStyle = colors.text;
ctx.font = `regular ${descriptionFontSize}px ${fontName}-Regular`;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText(this.contactInfo, width / 2, contactY);
}
}
/**
* Desenha o estilo minimalista
* @private
*/
async _drawMinimalStyle(ctx, fontName, colors, width, height, padding) {
// --- Desenha Título ---
const titleFontSize = Math.min(width, height) * 0.08;
const titleY = height * 0.3;
ctx.fillStyle = colors.text;
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const titleText = this.title;
ctx.fillText(titleText, width / 2, titleY);
// --- Desenha Data e Hora ---
const dateTimeFontSize = titleFontSize * 0.5;
const dateTimeY = titleY + titleFontSize;
ctx.fillStyle = colors.text;
ctx.font = `regular ${dateTimeFontSize}px ${fontName}-Regular`;
ctx.textAlign = "center";
let dateTimeText = "";
if (this.date && this.time) {
dateTimeText = `${this.date} • ${this.time}`;
} else if (this.date) {
dateTimeText = this.date;
} else if (this.time) {
dateTimeText = this.time;
}
if (dateTimeText) {
ctx.fillText(dateTimeText, width / 2, dateTimeY);
}
// --- Desenha Local ---
const locationFontSize = titleFontSize * 0.4;
const locationY = dateTimeY + dateTimeFontSize * 1.5;
if (this.location) {
ctx.fillStyle = colors.text;
ctx.font = `regular ${locationFontSize}px ${fontName}-Regular`;
ctx.textAlign = "center";
ctx.fillText(this.location, width / 2, locationY);
}
}
/**
* Desenha o estilo bold
* @private
*/
async _drawBoldStyle(ctx, fontName, colors, width, height, padding) {
// --- Desenha Título em Destaque ---
const titleFontSize = Math.min(width, height) * 0.12;
const titleY = padding * 2;
ctx.fillStyle = colors.text;
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
// Aplica sombra de texto se ativada
if (this.useTextShadow) {
applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 5, 2, 2);
}
const titleText = this.title;
ctx.fillText(titleText, width / 2, titleY);
// Remove sombra para o próximo texto
if (this.useTextShadow) {
clearShadow(ctx);
}
// --- Desenha Destaque para Data ---
const dateY = titleY + titleFontSize * 1.5;
const dateFontSize = titleFontSize * 0.6;
if (this.date) {
// Fundo para a data
const dateText = this.date;
const dateWidth = ctx.measureText(dateText).width + padding * 2;
const dateHeight = dateFontSize * 1.5;
const dateX = (width - dateWidth) / 2;
ctx.fillStyle = colors.accent;
roundRect(ctx, dateX, dateY, dateWidth, dateHeight, dateHeight / 2, true, false);
// Texto da data
ctx.fillStyle = "#000000"; // Texto escuro para contraste
ctx.font = `bold ${dateFontSize}px ${fontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(dateText, width / 2, dateY + dateHeight / 2);
}
// --- Desenha Hora ---
const timeFontSize = dateFontSize * 0.8;
const timeY = dateY + dateFontSize * 2;
if (this.time) {
ctx.fillStyle = colors.text;
ctx.font = `medium ${timeFontSize}px ${fontName}-Medium`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillText(this.time, width / 2, timeY);
}
// --- Desenha Local ---
const locationFontSize = timeFontSize;
const locationY = timeY + timeFontSize * 1.5;
if (this.location) {
ctx.fillStyle = colors.text;
ctx.font = `medium ${locationFontSize}px ${fontName}-Medium`;
ctx.textAlign = "center";
ctx.fillText(`📍 ${this.location}`, width / 2, locationY);
}
// --- Desenha Preço do Ingresso ---
const priceY = height - padding * 4;
const priceFontSize = titleFontSize * 0.5;
if (this.ticketPrice) {
// Fundo para o preço
const priceText = this.ticketPrice;
const priceWidth = ctx.measureText(priceText).width + padding * 2;
const priceHeight = priceFontSize * 1.5;
const priceX = (width - priceWidth) / 2;
ctx.fillStyle = colors.accent;
roundRect(ctx, priceX, priceY, priceWidth, priceHeight, priceHeight / 2, true, false);
// Texto do preço
ctx.fillStyle = "#000000"; // Texto escuro para contraste
ctx.font = `bold ${priceFontSize}px ${fontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(priceText, width / 2, priceY + priceHeight / 2);
}
// --- Desenha URL de Registro ---
const registrationY = height - padding * 2;
const registrationFontSize = priceFontSize * 0.8;
if (this.registrationUrl) {
ctx.fillStyle = colors.text;
ctx.font = `regular ${registrationFontSize}px ${fontName}-Regular`;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText(this.registrationUrl, width / 2, registrationY);
}
}
/**
* Desenha o estilo festivo
* @private
*/
async _drawFestiveStyle(ctx, fontName, colors, width, height, padding) {
// --- Desenha Elementos Decorativos ---
// Círculos decorativos
const circleCount = 20;
const maxRadius = Math.min(width, height) * 0.1;
for (let i = 0; i < circleCount; i++) {
const radius = Math.random() * maxRadius + 5;
const x = Math.random() * width;
const y = Math.random() * height;
ctx.fillStyle = hexToRgba(colors.accent, Math.random() * 0.3 + 0.1);
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
// --- Desenha Área de Conteúdo Principal ---
const contentWidth = width * 0.8;
const contentHeight = height * 0.8;
const contentX = (width - contentWidth) / 2;
const contentY = (height - contentHeight) / 2;
// Aplica efeito de glassmorphism
applyGlassmorphism(
ctx,
contentX,
contentY,
contentWidth,
contentHeight,
this.cornerRadius,
0.3,
"#FFFFFF"
);
// --- Desenha Título ---
const titleFontSize = Math.min(contentWidth, contentHeight) * 0.1;
ctx.fillStyle = colors.text;
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
// Aplica sombra de texto
applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 3, 1, 1);
const titleText = this.title;
ctx.fillText(titleText, width / 2, contentY + padding);
// Remove sombra para o próximo texto
clearShadow(ctx);
// --- Desenha Data e Hora ---
const dateTimeFontSize = titleFontSize * 0.5;
const dateTimeY = contentY + padding + titleFontSize * 1.2;
ctx.fillStyle = colors.text;
ctx.font = `medium ${dateTimeFontSize}px ${fontName}-Medium`;
ctx.textAlign = "center";
if (this.date) {
ctx.fillText(this.date, width / 2, dateTimeY);
}
if (this.time) {
ctx.fillText(this.time, width / 2, dateTimeY + dateTimeFontSize * 1.5);
}
// --- Desenha Local ---
const locationFontSize = dateTimeFontSize * 0.8;
const locationY = dateTimeY + (this.time ? dateTimeFontSize * 3 : dateTimeFontSize * 1.5);
if (this.location) {
ctx.fillStyle = colors.text;
ctx.font = `regular ${locationFontSize}px ${fontName}-Regular`;
ctx.textAlign = "center";
ctx.fillText(`📍 ${this.location}`, width / 2, locationY);
}
// --- Desenha Descrição ---
const descriptionFontSize = locationFontSize * 0.9;
const descriptionY = locationY + locationFontSize * 2;
if (this.description) {
ctx.fillStyle = colors.text;
ctx.font = `regular ${descriptionFontSize}px ${fontName}-Regular`;
ctx.textAlign = "center";
wrapText(
ctx,
this.description,
width / 2,
descriptionY,
contentWidth - padding * 2,
descriptionFontSize * 1.5,
fontName,
true
);
}
// --- Desenha Informações de Registro ---
const registrationY = contentY + contentHeight - padding * 2;
if (this.registrationUrl) {
ctx.fillStyle = colors.text;
ctx.font = `bold ${descriptionFontSize}px ${fontName}-Bold`;
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText("Registre-se: " + this.registrationUrl, width / 2, registrationY);
}
// --- Desenha Efeito de Brilho nos Cantos ---
applyGlow(
ctx,
contentX - 10,
contentY - 10,
30,
30,
15,
colors.accent,
10
);
applyGlow(
ctx,
contentX + contentWidth - 20,
contentY - 10,
30,
30,
15,
colors.accent,
10
);
applyGlow(
ctx,
contentX - 10,
contentY + contentHeight - 20,
30,
30,
15,
colors.accent,
10
);
applyGlow(
ctx,
contentX + contentWidth - 20,
contentY + contentHeight - 20,
30,
30,
15,
colors.accent,
10
);
}
};