UNPKG

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