UNPKG

@cognima/banners

Version:

Biblioteca avançada para geração de banners dinâmicos para diversas plataformas e aplicações

893 lines (760 loc) 28.9 kB
"use strict"; /** * Módulo de Banner Minimalista * * Este módulo gera banners com design minimalista, utilizando espaços em branco, * tipografia elegante e elementos visuais sutis. * * @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, applyLinePattern } = require("./effects"); /** * @class MinimalistBanner * @classdesc Gera um banner com design minimalista e elegante. * @example const banner = new MinimalistBanner() * .setTitle("Título Elegante") * .setSubtitle("Subtítulo complementar") * .setText("Texto adicional com informações relevantes") * .setBackground("color", "#FFFFFF") * .setAccentColor("#000000") * .build(); */ module.exports = class MinimalistBanner { constructor(options) { // Dados Principais this.title = "Título"; this.subtitle = null; this.text = null; this.logo = null; this.image = null; this.qrCode = null; this.ctaText = null; this.ctaUrl = null; // Personalização Visual this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path }; this.background = { type: "color", value: "#FFFFFF" }; this.textColor = "#000000"; this.accentColor = "#000000"; this.secondaryColor = "#888888"; this.layout = "centered"; // centered, left, right, split this.style = "clean"; // clean, bordered, lined, dotted // Configurações de Layout this.cardWidth = DEFAULT_DIMENSIONS.banner.width; this.cardHeight = 400; this.cornerRadius = 0; this.padding = LAYOUT.padding.large; } // --- Setters para Dados Principais --- /** * Define o título principal * @param {string} text - Texto do título * @returns {MinimalistBanner} - Instância atual para encadeamento */ setTitle(text) { if (!text || typeof text !== "string") throw new Error("O título deve ser uma string não vazia."); this.title = text; return this; } /** * Define o subtítulo * @param {string} text - Texto do subtítulo * @returns {MinimalistBanner} - Instância atual para encadeamento */ setSubtitle(text) { if (!text || typeof text !== "string") throw new Error("O subtítulo deve ser uma string não vazia."); this.subtitle = text; return this; } /** * Define o texto adicional * @param {string} text - Texto adicional * @returns {MinimalistBanner} - Instância atual para encadeamento */ setText(text) { if (!text || typeof text !== "string") throw new Error("O texto deve ser uma string não vazia."); this.text = text; return this; } /** * Define o logo * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do logo * @returns {MinimalistBanner} - 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 a imagem complementar * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem * @returns {MinimalistBanner} - Instância atual para encadeamento */ setImage(image) { if (!image) throw new Error("A fonte da imagem não pode estar vazia."); this.image = image; return this; } /** * Define o código QR * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do código QR * @returns {MinimalistBanner} - 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 texto de chamada para ação (CTA) * @param {string} text - Texto do CTA * @param {string} url - URL do CTA * @returns {MinimalistBanner} - Instância atual para encadeamento */ setCTA(text, url) { if (!text || typeof text !== "string") throw new Error("O texto do CTA deve ser uma string não vazia."); if (url && typeof url !== "string") throw new Error("A URL do CTA deve ser uma string."); this.ctaText = text; this.ctaUrl = url; return this; } // --- Setters para Personalização Visual --- /** * Define o plano de fundo * @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 {MinimalistBanner} - Instância atual para encadeamento */ setBackground(type, value) { const types = ["color", "image"]; if (!type || !types.includes(type.toLowerCase())) throw new Error("O tipo de plano de fundo deve ser 'color' ou 'image'."); if (!value) throw new Error("O valor do plano de fundo não pode estar vazio."); if (type.toLowerCase() === "color" && !isValidHexColor(value)) throw new Error("Cor de plano de fundo inválida. Use o formato hexadecimal."); this.background = { type: type.toLowerCase(), value }; return this; } /** * Define a cor do texto * @param {string} color - Cor hexadecimal * @returns {MinimalistBanner} - Instância atual para encadeamento */ setTextColor(color) { if (!color || !isValidHexColor(color)) throw new Error("Cor de texto inválida. Use o formato hexadecimal."); this.textColor = color; return this; } /** * Define a cor de destaque * @param {string} color - Cor hexadecimal * @returns {MinimalistBanner} - Instância atual para encadeamento */ setAccentColor(color) { if (!color || !isValidHexColor(color)) throw new Error("Cor de destaque inválida. Use o formato hexadecimal."); this.accentColor = color; return this; } /** * Define a cor secundária * @param {string} color - Cor hexadecimal * @returns {MinimalistBanner} - Instância atual para encadeamento */ setSecondaryColor(color) { if (!color || !isValidHexColor(color)) throw new Error("Cor secundária inválida. Use o formato hexadecimal."); this.secondaryColor = color; return this; } /** * Define o layout * @param {string} layout - Tipo de layout ('centered', 'left', 'right', 'split') * @returns {MinimalistBanner} - Instância atual para encadeamento */ setLayout(layout) { const validLayouts = ["centered", "left", "right", "split"]; if (!layout || !validLayouts.includes(layout.toLowerCase())) { throw new Error(`Layout inválido. Use um dos seguintes: ${validLayouts.join(", ")}`); } this.layout = layout.toLowerCase(); return this; } /** * Define o estilo * @param {string} style - Tipo de estilo ('clean', 'bordered', 'lined', 'dotted') * @returns {MinimalistBanner} - Instância atual para encadeamento */ setStyle(style) { const validStyles = ["clean", "bordered", "lined", "dotted"]; if (!style || !validStyles.includes(style.toLowerCase())) { throw new Error(`Estilo inválido. Use um dos seguintes: ${validStyles.join(", ")}`); } this.style = style.toLowerCase(); 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 {MinimalistBanner} - 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 < 200 || height > 1080) { throw new Error("A altura do card deve estar entre 200 e 1080 pixels."); } this.cardWidth = width; this.cardHeight = height; return this; } /** * Define o raio dos cantos arredondados * @param {number} radius - Raio dos cantos em pixels * @returns {MinimalistBanner} - 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 o padding * @param {number} padding - Padding em pixels * @returns {MinimalistBanner} - Instância atual para encadeamento */ setPadding(padding) { if (typeof padding !== "number" || padding < 0) throw new Error("O padding deve ser um número não negativo."); this.padding = padding; return this; } // --- Método de Construção --- /** * Constrói o banner e retorna um buffer de imagem * @returns {Promise<Buffer>} - Buffer contendo a imagem do banner */ async build() { // --- Registro de Fonte --- const registeredFontName = await registerFontIfNeeded(this.font); // --- Configuração do Canvas --- const cardWidth = this.cardWidth; const cardHeight = this.cardHeight; const padding = this.padding; const cornerRadius = this.cornerRadius; const canvas = pureimage.make(cardWidth, cardHeight); const ctx = canvas.getContext("2d"); // --- Desenha Plano de Fundo --- if (this.background.type === "color") { // Plano de fundo de cor sólida ctx.fillStyle = this.background.value; roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false); } else { // Plano de fundo de imagem 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); ctx.restore(); } catch (e) { console.error("Falha ao desenhar imagem de plano de fundo:", e.message); ctx.fillStyle = "#FFFFFF"; roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false); } } // --- Aplica Estilo --- switch (this.style) { case "bordered": // Borda simples ctx.strokeStyle = this.accentColor; ctx.lineWidth = 2; roundRect(ctx, padding / 2, padding / 2, cardWidth - padding, cardHeight - padding, cornerRadius > 0 ? cornerRadius - padding / 4 : 0, false, true); break; case "lined": // Padrão de linhas applyLinePattern( ctx, 0, 0, cardWidth, cardHeight, 20, 1, this.accentColor, 0.1, "horizontal" ); break; case "dotted": // Padrão de pontos (implementado como linhas pontilhadas) ctx.strokeStyle = hexToRgba(this.accentColor, 0.2); ctx.lineWidth = 1; ctx.setLineDash([2, 4]); roundRect(ctx, padding / 2, padding / 2, cardWidth - padding, cardHeight - padding, cornerRadius > 0 ? cornerRadius - padding / 4 : 0, false, true); ctx.setLineDash([]); break; } // --- Desenha Conteúdo com base no Layout --- switch (this.layout) { case "left": await this._drawLeftLayout(ctx, registeredFontName, cardWidth, cardHeight, padding); break; case "right": await this._drawRightLayout(ctx, registeredFontName, cardWidth, cardHeight, padding); break; case "split": await this._drawSplitLayout(ctx, registeredFontName, cardWidth, cardHeight, padding); break; case "centered": default: await this._drawCenteredLayout(ctx, registeredFontName, cardWidth, cardHeight, padding); break; } // --- Codifica e Retorna Buffer --- try { return await encodeToBuffer(canvas); } catch (err) { console.error("Falha ao codificar o Banner Minimalista:", err); throw new Error("Não foi possível gerar o buffer de imagem do Banner Minimalista."); } } // --- Métodos Auxiliares Privados --- /** * Desenha layout centralizado * @private */ async _drawCenteredLayout(ctx, fontName, width, height, padding) { // --- Desenha Logo (se fornecido) --- let currentY = padding * 1.5; if (this.logo) { try { const logoSize = Math.min(width, height) * 0.15; const logoX = (width - logoSize) / 2; 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(width, height) * 0.08; ctx.fillStyle = this.textColor; ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`; ctx.textAlign = "center"; ctx.textBaseline = "top"; const titleText = this.title; ctx.fillText(titleText, width / 2, currentY); currentY += titleFontSize * 1.2; // --- Desenha Linha Decorativa --- const lineWidth = width * 0.1; const lineHeight = 2; const lineX = (width - lineWidth) / 2; const lineY = currentY + padding / 2; ctx.fillStyle = this.accentColor; ctx.fillRect(lineX, lineY, lineWidth, lineHeight); currentY = lineY + lineHeight + padding; // --- Desenha Subtítulo (se fornecido) --- if (this.subtitle) { const subtitleFontSize = titleFontSize * 0.6; ctx.fillStyle = this.secondaryColor; ctx.font = `medium ${subtitleFontSize}px ${fontName}-Medium`; ctx.textAlign = "center"; ctx.fillText(this.subtitle, width / 2, currentY); currentY += subtitleFontSize * 1.5; } // --- Desenha Texto (se fornecido) --- if (this.text) { const textFontSize = titleFontSize * 0.4; ctx.fillStyle = this.textColor; ctx.font = `regular ${textFontSize}px ${fontName}-Regular`; ctx.textAlign = "center"; currentY = wrapText(ctx, this.text, width / 2, currentY + padding, width - padding * 4, textFontSize * 1.5, fontName); } // --- Desenha Imagem (se fornecida) --- if (this.image) { try { const remainingHeight = height - currentY - padding * 2; const imageHeight = Math.min(remainingHeight, height * 0.3); const imageWidth = width * 0.6; const imageX = (width - imageWidth) / 2; const imageY = currentY + padding; const img = await loadImageWithAxios(this.image); ctx.drawImage(img, imageX, imageY, imageWidth, imageHeight); currentY = imageY + imageHeight; } catch (e) { console.error("Falha ao desenhar imagem:", e.message); } } // --- Desenha CTA (se fornecido) --- if (this.ctaText) { const ctaFontSize = titleFontSize * 0.5; const ctaY = height - padding * 2; ctx.fillStyle = this.accentColor; ctx.font = `bold ${ctaFontSize}px ${fontName}-Bold`; ctx.textAlign = "center"; ctx.textBaseline = "bottom"; ctx.fillText(this.ctaText, width / 2, ctaY); // Desenha URL (se fornecida) if (this.ctaUrl) { const urlFontSize = ctaFontSize * 0.7; ctx.fillStyle = this.secondaryColor; ctx.font = `regular ${urlFontSize}px ${fontName}-Regular`; ctx.fillText(this.ctaUrl, width / 2, ctaY + urlFontSize * 1.2); } } // --- Desenha QR Code (se fornecido) --- if (this.qrCode) { try { const qrSize = Math.min(width, height) * 0.15; const qrX = width - qrSize - padding; const qrY = height - qrSize - padding; 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 layout alinhado à esquerda * @private */ async _drawLeftLayout(ctx, fontName, width, height, padding) { const contentX = padding * 2; let currentY = padding * 2; const contentWidth = width * 0.6; // --- Desenha Logo (se fornecido) --- if (this.logo) { try { const logoSize = Math.min(width, height) * 0.1; const logoImg = await loadImageWithAxios(this.logo); const aspect = logoImg.width / logoImg.height; const logoHeight = logoSize; const logoWidth = logoHeight * aspect; ctx.drawImage(logoImg, contentX, currentY, logoWidth, logoHeight); currentY += logoHeight + padding; } catch (e) { console.error("Falha ao desenhar logo:", e.message); } } // --- Desenha Título --- const titleFontSize = Math.min(width, height) * 0.07; ctx.fillStyle = this.textColor; ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`; ctx.textAlign = "left"; ctx.textBaseline = "top"; const titleText = this.title; ctx.fillText(titleText, contentX, currentY); currentY += titleFontSize * 1.2; // --- Desenha Linha Decorativa --- const lineWidth = contentWidth * 0.2; const lineHeight = 2; const lineY = currentY + padding / 2; ctx.fillStyle = this.accentColor; ctx.fillRect(contentX, lineY, lineWidth, lineHeight); currentY = lineY + lineHeight + padding; // --- Desenha Subtítulo (se fornecido) --- if (this.subtitle) { const subtitleFontSize = titleFontSize * 0.6; ctx.fillStyle = this.secondaryColor; ctx.font = `medium ${subtitleFontSize}px ${fontName}-Medium`; ctx.textAlign = "left"; ctx.fillText(this.subtitle, contentX, currentY); currentY += subtitleFontSize * 1.5; } // --- Desenha Texto (se fornecido) --- if (this.text) { const textFontSize = titleFontSize * 0.4; ctx.fillStyle = this.textColor; ctx.font = `regular ${textFontSize}px ${fontName}-Regular`; ctx.textAlign = "left"; currentY = wrapText(ctx, this.text, contentX, currentY + padding, contentWidth - padding, textFontSize * 1.5, fontName); } // --- Desenha CTA (se fornecido) --- if (this.ctaText) { const ctaFontSize = titleFontSize * 0.5; const ctaY = height - padding * 2; ctx.fillStyle = this.accentColor; ctx.font = `bold ${ctaFontSize}px ${fontName}-Bold`; ctx.textAlign = "left"; ctx.textBaseline = "bottom"; ctx.fillText(this.ctaText, contentX, ctaY); // Desenha URL (se fornecida) if (this.ctaUrl) { const urlFontSize = ctaFontSize * 0.7; ctx.fillStyle = this.secondaryColor; ctx.font = `regular ${urlFontSize}px ${fontName}-Regular`; ctx.fillText(this.ctaUrl, contentX, ctaY + urlFontSize * 1.2); } } // --- Desenha Imagem (se fornecida) --- if (this.image) { try { const imageWidth = width * 0.35; const imageHeight = height * 0.6; const imageX = width - imageWidth - padding * 2; const imageY = (height - imageHeight) / 2; const img = await loadImageWithAxios(this.image); ctx.drawImage(img, imageX, imageY, imageWidth, imageHeight); } catch (e) { console.error("Falha ao desenhar imagem:", e.message); } } // --- Desenha QR Code (se fornecido) --- if (this.qrCode) { try { const qrSize = Math.min(width, height) * 0.15; const qrX = width - qrSize - padding; const qrY = height - qrSize - padding; 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 layout alinhado à direita * @private */ async _drawRightLayout(ctx, fontName, width, height, padding) { const contentWidth = width * 0.6; const contentX = width - contentWidth - padding * 2; let currentY = padding * 2; // --- Desenha Logo (se fornecido) --- if (this.logo) { try { const logoSize = Math.min(width, height) * 0.1; const logoImg = await loadImageWithAxios(this.logo); const aspect = logoImg.width / logoImg.height; const logoHeight = logoSize; const logoWidth = logoHeight * aspect; ctx.drawImage(logoImg, contentX, currentY, logoWidth, logoHeight); currentY += logoHeight + padding; } catch (e) { console.error("Falha ao desenhar logo:", e.message); } } // --- Desenha Título --- const titleFontSize = Math.min(width, height) * 0.07; ctx.fillStyle = this.textColor; ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`; ctx.textAlign = "left"; ctx.textBaseline = "top"; const titleText = this.title; ctx.fillText(titleText, contentX, currentY); currentY += titleFontSize * 1.2; // --- Desenha Linha Decorativa --- const lineWidth = contentWidth * 0.2; const lineHeight = 2; const lineY = currentY + padding / 2; ctx.fillStyle = this.accentColor; ctx.fillRect(contentX, lineY, lineWidth, lineHeight); currentY = lineY + lineHeight + padding; // --- Desenha Subtítulo (se fornecido) --- if (this.subtitle) { const subtitleFontSize = titleFontSize * 0.6; ctx.fillStyle = this.secondaryColor; ctx.font = `medium ${subtitleFontSize}px ${fontName}-Medium`; ctx.textAlign = "left"; ctx.fillText(this.subtitle, contentX, currentY); currentY += subtitleFontSize * 1.5; } // --- Desenha Texto (se fornecido) --- if (this.text) { const textFontSize = titleFontSize * 0.4; ctx.fillStyle = this.textColor; ctx.font = `regular ${textFontSize}px ${fontName}-Regular`; ctx.textAlign = "left"; currentY = wrapText(ctx, this.text, contentX, currentY + padding, contentWidth - padding, textFontSize * 1.5, fontName); } // --- Desenha CTA (se fornecido) --- if (this.ctaText) { const ctaFontSize = titleFontSize * 0.5; const ctaY = height - padding * 2; ctx.fillStyle = this.accentColor; ctx.font = `bold ${ctaFontSize}px ${fontName}-Bold`; ctx.textAlign = "left"; ctx.textBaseline = "bottom"; ctx.fillText(this.ctaText, contentX, ctaY); // Desenha URL (se fornecida) if (this.ctaUrl) { const urlFontSize = ctaFontSize * 0.7; ctx.fillStyle = this.secondaryColor; ctx.font = `regular ${urlFontSize}px ${fontName}-Regular`; ctx.fillText(this.ctaUrl, contentX, ctaY + urlFontSize * 1.2); } } // --- Desenha Imagem (se fornecida) --- if (this.image) { try { const imageWidth = width * 0.35; const imageHeight = height * 0.6; const imageX = padding * 2; const imageY = (height - imageHeight) / 2; const img = await loadImageWithAxios(this.image); ctx.drawImage(img, imageX, imageY, imageWidth, imageHeight); } catch (e) { console.error("Falha ao desenhar imagem:", e.message); } } // --- Desenha QR Code (se fornecido) --- if (this.qrCode) { try { const qrSize = Math.min(width, height) * 0.15; const qrX = padding; const qrY = height - qrSize - padding; 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 layout dividido * @private */ async _drawSplitLayout(ctx, fontName, width, height, padding) { // Divide o canvas em duas partes const halfWidth = width / 2; // --- Desenha Divisão Visual --- if (this.style !== "clean") { ctx.fillStyle = this.accentColor; ctx.fillRect(halfWidth - 1, 0, 2, height); } // --- Lado Esquerdo (Texto) --- const leftContentX = padding * 2; let leftCurrentY = padding * 2; const leftContentWidth = halfWidth - padding * 3; // Logo (se fornecido) if (this.logo) { try { const logoSize = Math.min(halfWidth, height) * 0.15; const logoImg = await loadImageWithAxios(this.logo); const aspect = logoImg.width / logoImg.height; const logoHeight = logoSize; const logoWidth = logoHeight * aspect; ctx.drawImage(logoImg, leftContentX, leftCurrentY, logoWidth, logoHeight); leftCurrentY += logoHeight + padding; } catch (e) { console.error("Falha ao desenhar logo:", e.message); } } // Título const titleFontSize = Math.min(halfWidth, height) * 0.08; ctx.fillStyle = this.textColor; ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`; ctx.textAlign = "left"; ctx.textBaseline = "top"; const titleText = this.title; ctx.fillText(titleText, leftContentX, leftCurrentY); leftCurrentY += titleFontSize * 1.2; // Linha Decorativa const lineWidth = leftContentWidth * 0.3; const lineHeight = 2; const lineY = leftCurrentY + padding / 2; ctx.fillStyle = this.accentColor; ctx.fillRect(leftContentX, lineY, lineWidth, lineHeight); leftCurrentY = lineY + lineHeight + padding; // Subtítulo (se fornecido) if (this.subtitle) { const subtitleFontSize = titleFontSize * 0.6; ctx.fillStyle = this.secondaryColor; ctx.font = `medium ${subtitleFontSize}px ${fontName}-Medium`; ctx.textAlign = "left"; ctx.fillText(this.subtitle, leftContentX, leftCurrentY); leftCurrentY += subtitleFontSize * 1.5; } // Texto (se fornecido) if (this.text) { const textFontSize = titleFontSize * 0.4; ctx.fillStyle = this.textColor; ctx.font = `regular ${textFontSize}px ${fontName}-Regular`; ctx.textAlign = "left"; leftCurrentY = wrapText(ctx, this.text, leftContentX, leftCurrentY + padding, leftContentWidth, textFontSize * 1.5, fontName); } // CTA (se fornecido) if (this.ctaText) { const ctaFontSize = titleFontSize * 0.5; const ctaY = height - padding * 2; ctx.fillStyle = this.accentColor; ctx.font = `bold ${ctaFontSize}px ${fontName}-Bold`; ctx.textAlign = "left"; ctx.textBaseline = "bottom"; ctx.fillText(this.ctaText, leftContentX, ctaY); // URL (se fornecida) if (this.ctaUrl) { const urlFontSize = ctaFontSize * 0.7; ctx.fillStyle = this.secondaryColor; ctx.font = `regular ${urlFontSize}px ${fontName}-Regular`; ctx.fillText(this.ctaUrl, leftContentX, ctaY + urlFontSize * 1.2); } } // --- Lado Direito (Imagem) --- if (this.image) { try { const rightContentX = halfWidth + padding; const imageWidth = halfWidth - padding * 2; const imageHeight = height - padding * 2; const imageY = padding; const img = await loadImageWithAxios(this.image); ctx.drawImage(img, rightContentX, imageY, imageWidth, imageHeight); } catch (e) { console.error("Falha ao desenhar imagem:", e.message); } } // QR Code (se fornecido) if (this.qrCode) { try { const qrSize = Math.min(halfWidth, height) * 0.2; const qrX = width - qrSize - padding; const qrY = height - qrSize - padding; const qrImg = await loadImageWithAxios(this.qrCode); ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize); } catch (e) { console.error("Falha ao desenhar QR code:", e.message); } } } };