@cognima/banners 0.0.1-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/fonts/Manrope/Manrope-Bold.ttf +0 -0
- package/assets/fonts/Manrope/Manrope-Regular.ttf +0 -0
- package/assets/fonts/Others/AbyssinicaSIL-Regular.ttf +0 -0
- package/assets/fonts/Others/ChirpRegular.ttf +0 -0
- package/assets/fonts/Poppins/Poppins-Bold.ttf +0 -0
- package/assets/fonts/Poppins/Poppins-Medium.ttf +0 -0
- package/assets/fonts/Poppins/Poppins-Regular.ttf +0 -0
- package/assets/placeholders/album_art.png +0 -0
- package/assets/placeholders/avatar.png +0 -0
- package/assets/placeholders/badge.jpg +0 -0
- package/assets/placeholders/badge.png +0 -0
- package/assets/placeholders/badge_2.jpg +0 -0
- package/assets/placeholders/badge_3.jpg +0 -0
- package/assets/placeholders/badge_4.jpg +0 -0
- package/assets/placeholders/badge_5.jpg +0 -0
- package/assets/placeholders/banner.jpeg +0 -0
- package/assets/placeholders/images.jpeg +0 -0
- package/index.js +153 -0
- package/package.json +34 -0
- package/src/animation-effects.js +631 -0
- package/src/cache-manager.js +258 -0
- package/src/community-banner.js +1536 -0
- package/src/constants.js +208 -0
- package/src/discord-profile.js +584 -0
- package/src/e-commerce-banner.js +1214 -0
- package/src/effects.js +355 -0
- package/src/error-handler.js +305 -0
- package/src/event-banner.js +1319 -0
- package/src/facebook-post.js +679 -0
- package/src/gradient-welcome.js +430 -0
- package/src/image-filters.js +1034 -0
- package/src/image-processor.js +1014 -0
- package/src/instagram-post.js +504 -0
- package/src/interactive-elements.js +1208 -0
- package/src/linkedin-post.js +658 -0
- package/src/marketing-banner.js +1089 -0
- package/src/minimalist-banner.js +892 -0
- package/src/modern-profile.js +755 -0
- package/src/performance-optimizer.js +216 -0
- package/src/telegram-header.js +544 -0
- package/src/test-runner.js +645 -0
- package/src/tiktok-post.js +713 -0
- package/src/twitter-header.js +604 -0
- package/src/validator.js +442 -0
- package/src/welcome-leave.js +445 -0
- package/src/whatsapp-status.js +386 -0
- package/src/youtube-thumbnail.js +681 -0
- package/utils.js +710 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Módulo de Banner Minimalista
|
|
5
|
+
*
|
|
6
|
+
* Este módulo gera banners com design minimalista, utilizando espaços em branco,
|
|
7
|
+
* tipografia elegante e elementos visuais sutis.
|
|
8
|
+
*
|
|
9
|
+
* @author Cognima Team (melhorado)
|
|
10
|
+
* @version 2.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
|
|
15
|
+
const pureimage = require("pureimage");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const {
|
|
18
|
+
loadImageWithAxios,
|
|
19
|
+
encodeToBuffer,
|
|
20
|
+
roundRect,
|
|
21
|
+
wrapText,
|
|
22
|
+
registerFontIfNeeded,
|
|
23
|
+
isValidHexColor,
|
|
24
|
+
DEFAULT_FONT_FAMILY,
|
|
25
|
+
applyTextShadow,
|
|
26
|
+
clearShadow,
|
|
27
|
+
createLinearGradient,
|
|
28
|
+
hexToRgba
|
|
29
|
+
} = require("../utils");
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
DEFAULT_COLORS,
|
|
33
|
+
LAYOUT,
|
|
34
|
+
DEFAULT_DIMENSIONS
|
|
35
|
+
} = require("./constants");
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
applyGlassmorphism,
|
|
39
|
+
applyLinePattern
|
|
40
|
+
} = require("./effects");
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @class MinimalistBanner
|
|
44
|
+
* @classdesc Gera um banner com design minimalista e elegante.
|
|
45
|
+
* @example const banner = new MinimalistBanner()
|
|
46
|
+
* .setTitle("Título Elegante")
|
|
47
|
+
* .setSubtitle("Subtítulo complementar")
|
|
48
|
+
* .setText("Texto adicional com informações relevantes")
|
|
49
|
+
* .setBackground("color", "#FFFFFF")
|
|
50
|
+
* .setAccentColor("#000000")
|
|
51
|
+
* .build();
|
|
52
|
+
*/
|
|
53
|
+
module.exports = class MinimalistBanner {
|
|
54
|
+
constructor(options) {
|
|
55
|
+
// Dados Principais
|
|
56
|
+
this.title = "Título";
|
|
57
|
+
this.subtitle = null;
|
|
58
|
+
this.text = null;
|
|
59
|
+
this.logo = null;
|
|
60
|
+
this.image = null;
|
|
61
|
+
this.qrCode = null;
|
|
62
|
+
this.ctaText = null;
|
|
63
|
+
this.ctaUrl = null;
|
|
64
|
+
|
|
65
|
+
// Personalização Visual
|
|
66
|
+
this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path };
|
|
67
|
+
this.background = { type: "color", value: "#FFFFFF" };
|
|
68
|
+
this.textColor = "#000000";
|
|
69
|
+
this.accentColor = "#000000";
|
|
70
|
+
this.secondaryColor = "#888888";
|
|
71
|
+
this.layout = "centered"; // centered, left, right, split
|
|
72
|
+
this.style = "clean"; // clean, bordered, lined, dotted
|
|
73
|
+
|
|
74
|
+
// Configurações de Layout
|
|
75
|
+
this.cardWidth = DEFAULT_DIMENSIONS.banner.width;
|
|
76
|
+
this.cardHeight = 400;
|
|
77
|
+
this.cornerRadius = 0;
|
|
78
|
+
this.padding = LAYOUT.padding.large;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- Setters para Dados Principais ---
|
|
82
|
+
/**
|
|
83
|
+
* Define o título principal
|
|
84
|
+
* @param {string} text - Texto do título
|
|
85
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
86
|
+
*/
|
|
87
|
+
setTitle(text) {
|
|
88
|
+
if (!text || typeof text !== "string") throw new Error("O título deve ser uma string não vazia.");
|
|
89
|
+
this.title = text;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Define o subtítulo
|
|
95
|
+
* @param {string} text - Texto do subtítulo
|
|
96
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
97
|
+
*/
|
|
98
|
+
setSubtitle(text) {
|
|
99
|
+
if (!text || typeof text !== "string") throw new Error("O subtítulo deve ser uma string não vazia.");
|
|
100
|
+
this.subtitle = text;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Define o texto adicional
|
|
106
|
+
* @param {string} text - Texto adicional
|
|
107
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
108
|
+
*/
|
|
109
|
+
setText(text) {
|
|
110
|
+
if (!text || typeof text !== "string") throw new Error("O texto deve ser uma string não vazia.");
|
|
111
|
+
this.text = text;
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Define o logo
|
|
117
|
+
* @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do logo
|
|
118
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
119
|
+
*/
|
|
120
|
+
setLogo(image) {
|
|
121
|
+
if (!image) throw new Error("A fonte da imagem do logo não pode estar vazia.");
|
|
122
|
+
this.logo = image;
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Define a imagem complementar
|
|
128
|
+
* @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem
|
|
129
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
130
|
+
*/
|
|
131
|
+
setImage(image) {
|
|
132
|
+
if (!image) throw new Error("A fonte da imagem não pode estar vazia.");
|
|
133
|
+
this.image = image;
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Define o código QR
|
|
139
|
+
* @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do código QR
|
|
140
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
141
|
+
*/
|
|
142
|
+
setQRCode(image) {
|
|
143
|
+
if (!image) throw new Error("A fonte da imagem do código QR não pode estar vazia.");
|
|
144
|
+
this.qrCode = image;
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Define o texto de chamada para ação (CTA)
|
|
150
|
+
* @param {string} text - Texto do CTA
|
|
151
|
+
* @param {string} url - URL do CTA
|
|
152
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
153
|
+
*/
|
|
154
|
+
setCTA(text, url) {
|
|
155
|
+
if (!text || typeof text !== "string") throw new Error("O texto do CTA deve ser uma string não vazia.");
|
|
156
|
+
if (url && typeof url !== "string") throw new Error("A URL do CTA deve ser uma string.");
|
|
157
|
+
|
|
158
|
+
this.ctaText = text;
|
|
159
|
+
this.ctaUrl = url;
|
|
160
|
+
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- Setters para Personalização Visual ---
|
|
165
|
+
/**
|
|
166
|
+
* Define o plano de fundo
|
|
167
|
+
* @param {string} type - Tipo de plano de fundo ('color' ou 'image')
|
|
168
|
+
* @param {string} value - Valor do plano de fundo (cor hexadecimal ou URL/caminho da imagem)
|
|
169
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
170
|
+
*/
|
|
171
|
+
setBackground(type, value) {
|
|
172
|
+
const types = ["color", "image"];
|
|
173
|
+
if (!type || !types.includes(type.toLowerCase())) throw new Error("O tipo de plano de fundo deve ser 'color' ou 'image'.");
|
|
174
|
+
if (!value) throw new Error("O valor do plano de fundo não pode estar vazio.");
|
|
175
|
+
if (type.toLowerCase() === "color" && !isValidHexColor(value)) throw new Error("Cor de plano de fundo inválida. Use o formato hexadecimal.");
|
|
176
|
+
|
|
177
|
+
this.background = { type: type.toLowerCase(), value };
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Define a cor do texto
|
|
183
|
+
* @param {string} color - Cor hexadecimal
|
|
184
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
185
|
+
*/
|
|
186
|
+
setTextColor(color) {
|
|
187
|
+
if (!color || !isValidHexColor(color)) throw new Error("Cor de texto inválida. Use o formato hexadecimal.");
|
|
188
|
+
this.textColor = color;
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Define a cor de destaque
|
|
194
|
+
* @param {string} color - Cor hexadecimal
|
|
195
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
196
|
+
*/
|
|
197
|
+
setAccentColor(color) {
|
|
198
|
+
if (!color || !isValidHexColor(color)) throw new Error("Cor de destaque inválida. Use o formato hexadecimal.");
|
|
199
|
+
this.accentColor = color;
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Define a cor secundária
|
|
205
|
+
* @param {string} color - Cor hexadecimal
|
|
206
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
207
|
+
*/
|
|
208
|
+
setSecondaryColor(color) {
|
|
209
|
+
if (!color || !isValidHexColor(color)) throw new Error("Cor secundária inválida. Use o formato hexadecimal.");
|
|
210
|
+
this.secondaryColor = color;
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Define o layout
|
|
216
|
+
* @param {string} layout - Tipo de layout ('centered', 'left', 'right', 'split')
|
|
217
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
218
|
+
*/
|
|
219
|
+
setLayout(layout) {
|
|
220
|
+
const validLayouts = ["centered", "left", "right", "split"];
|
|
221
|
+
if (!layout || !validLayouts.includes(layout.toLowerCase())) {
|
|
222
|
+
throw new Error(`Layout inválido. Use um dos seguintes: ${validLayouts.join(", ")}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.layout = layout.toLowerCase();
|
|
226
|
+
return this;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Define o estilo
|
|
231
|
+
* @param {string} style - Tipo de estilo ('clean', 'bordered', 'lined', 'dotted')
|
|
232
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
233
|
+
*/
|
|
234
|
+
setStyle(style) {
|
|
235
|
+
const validStyles = ["clean", "bordered", "lined", "dotted"];
|
|
236
|
+
if (!style || !validStyles.includes(style.toLowerCase())) {
|
|
237
|
+
throw new Error(`Estilo inválido. Use um dos seguintes: ${validStyles.join(", ")}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.style = style.toLowerCase();
|
|
241
|
+
return this;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Define as dimensões do card
|
|
246
|
+
* @param {number} width - Largura do card em pixels
|
|
247
|
+
* @param {number} height - Altura do card em pixels
|
|
248
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
249
|
+
*/
|
|
250
|
+
setCardDimensions(width, height) {
|
|
251
|
+
if (typeof width !== "number" || width < 400 || width > 1920) {
|
|
252
|
+
throw new Error("A largura do card deve estar entre 400 e 1920 pixels.");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (typeof height !== "number" || height < 200 || height > 1080) {
|
|
256
|
+
throw new Error("A altura do card deve estar entre 200 e 1080 pixels.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.cardWidth = width;
|
|
260
|
+
this.cardHeight = height;
|
|
261
|
+
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Define o raio dos cantos arredondados
|
|
267
|
+
* @param {number} radius - Raio dos cantos em pixels
|
|
268
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
269
|
+
*/
|
|
270
|
+
setCornerRadius(radius) {
|
|
271
|
+
if (typeof radius !== "number" || radius < 0) throw new Error("O raio dos cantos deve ser um número não negativo.");
|
|
272
|
+
this.cornerRadius = radius;
|
|
273
|
+
return this;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Define o padding
|
|
278
|
+
* @param {number} padding - Padding em pixels
|
|
279
|
+
* @returns {MinimalistBanner} - Instância atual para encadeamento
|
|
280
|
+
*/
|
|
281
|
+
setPadding(padding) {
|
|
282
|
+
if (typeof padding !== "number" || padding < 0) throw new Error("O padding deve ser um número não negativo.");
|
|
283
|
+
this.padding = padding;
|
|
284
|
+
return this;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- Método de Construção ---
|
|
288
|
+
/**
|
|
289
|
+
* Constrói o banner e retorna um buffer de imagem
|
|
290
|
+
* @returns {Promise<Buffer>} - Buffer contendo a imagem do banner
|
|
291
|
+
*/
|
|
292
|
+
async build() {
|
|
293
|
+
// --- Registro de Fonte ---
|
|
294
|
+
const registeredFontName = await registerFontIfNeeded(this.font);
|
|
295
|
+
|
|
296
|
+
// --- Configuração do Canvas ---
|
|
297
|
+
const cardWidth = this.cardWidth;
|
|
298
|
+
const cardHeight = this.cardHeight;
|
|
299
|
+
const padding = this.padding;
|
|
300
|
+
const cornerRadius = this.cornerRadius;
|
|
301
|
+
|
|
302
|
+
const canvas = pureimage.make(cardWidth, cardHeight);
|
|
303
|
+
const ctx = canvas.getContext("2d");
|
|
304
|
+
|
|
305
|
+
// --- Desenha Plano de Fundo ---
|
|
306
|
+
if (this.background.type === "color") {
|
|
307
|
+
// Plano de fundo de cor sólida
|
|
308
|
+
ctx.fillStyle = this.background.value;
|
|
309
|
+
roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
|
|
310
|
+
} else {
|
|
311
|
+
// Plano de fundo de imagem
|
|
312
|
+
try {
|
|
313
|
+
ctx.save();
|
|
314
|
+
|
|
315
|
+
if (cornerRadius > 0) {
|
|
316
|
+
roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, false, false);
|
|
317
|
+
ctx.clip();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const img = await loadImageWithAxios(this.background.value);
|
|
321
|
+
const aspect = img.width / img.height;
|
|
322
|
+
let drawWidth = cardWidth;
|
|
323
|
+
let drawHeight = cardWidth / aspect;
|
|
324
|
+
|
|
325
|
+
// Ajusta as dimensões para cobrir todo o card
|
|
326
|
+
if (drawHeight < cardHeight) {
|
|
327
|
+
drawHeight = cardHeight;
|
|
328
|
+
drawWidth = cardHeight * aspect;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const offsetX = (cardWidth - drawWidth) / 2;
|
|
332
|
+
const offsetY = (cardHeight - drawHeight) / 2;
|
|
333
|
+
|
|
334
|
+
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
|
|
335
|
+
|
|
336
|
+
ctx.restore();
|
|
337
|
+
} catch (e) {
|
|
338
|
+
console.error("Falha ao desenhar imagem de plano de fundo:", e.message);
|
|
339
|
+
ctx.fillStyle = "#FFFFFF";
|
|
340
|
+
roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// --- Aplica Estilo ---
|
|
345
|
+
switch (this.style) {
|
|
346
|
+
case "bordered":
|
|
347
|
+
// Borda simples
|
|
348
|
+
ctx.strokeStyle = this.accentColor;
|
|
349
|
+
ctx.lineWidth = 2;
|
|
350
|
+
roundRect(ctx, padding / 2, padding / 2, cardWidth - padding, cardHeight - padding, cornerRadius > 0 ? cornerRadius - padding / 4 : 0, false, true);
|
|
351
|
+
break;
|
|
352
|
+
case "lined":
|
|
353
|
+
// Padrão de linhas
|
|
354
|
+
applyLinePattern(
|
|
355
|
+
ctx,
|
|
356
|
+
0,
|
|
357
|
+
0,
|
|
358
|
+
cardWidth,
|
|
359
|
+
cardHeight,
|
|
360
|
+
20,
|
|
361
|
+
1,
|
|
362
|
+
this.accentColor,
|
|
363
|
+
0.1,
|
|
364
|
+
"horizontal"
|
|
365
|
+
);
|
|
366
|
+
break;
|
|
367
|
+
case "dotted":
|
|
368
|
+
// Padrão de pontos (implementado como linhas pontilhadas)
|
|
369
|
+
ctx.strokeStyle = hexToRgba(this.accentColor, 0.2);
|
|
370
|
+
ctx.lineWidth = 1;
|
|
371
|
+
ctx.setLineDash([2, 4]);
|
|
372
|
+
roundRect(ctx, padding / 2, padding / 2, cardWidth - padding, cardHeight - padding, cornerRadius > 0 ? cornerRadius - padding / 4 : 0, false, true);
|
|
373
|
+
ctx.setLineDash([]);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// --- Desenha Conteúdo com base no Layout ---
|
|
378
|
+
switch (this.layout) {
|
|
379
|
+
case "left":
|
|
380
|
+
await this._drawLeftLayout(ctx, registeredFontName, cardWidth, cardHeight, padding);
|
|
381
|
+
break;
|
|
382
|
+
case "right":
|
|
383
|
+
await this._drawRightLayout(ctx, registeredFontName, cardWidth, cardHeight, padding);
|
|
384
|
+
break;
|
|
385
|
+
case "split":
|
|
386
|
+
await this._drawSplitLayout(ctx, registeredFontName, cardWidth, cardHeight, padding);
|
|
387
|
+
break;
|
|
388
|
+
case "centered":
|
|
389
|
+
default:
|
|
390
|
+
await this._drawCenteredLayout(ctx, registeredFontName, cardWidth, cardHeight, padding);
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// --- Codifica e Retorna Buffer ---
|
|
395
|
+
try {
|
|
396
|
+
return await encodeToBuffer(canvas);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
console.error("Falha ao codificar o Banner Minimalista:", err);
|
|
399
|
+
throw new Error("Não foi possível gerar o buffer de imagem do Banner Minimalista.");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// --- Métodos Auxiliares Privados ---
|
|
404
|
+
/**
|
|
405
|
+
* Desenha layout centralizado
|
|
406
|
+
* @private
|
|
407
|
+
*/
|
|
408
|
+
async _drawCenteredLayout(ctx, fontName, width, height, padding) {
|
|
409
|
+
// --- Desenha Logo (se fornecido) ---
|
|
410
|
+
let currentY = padding * 1.5;
|
|
411
|
+
|
|
412
|
+
if (this.logo) {
|
|
413
|
+
try {
|
|
414
|
+
const logoSize = Math.min(width, height) * 0.15;
|
|
415
|
+
const logoX = (width - logoSize) / 2;
|
|
416
|
+
|
|
417
|
+
const logoImg = await loadImageWithAxios(this.logo);
|
|
418
|
+
const aspect = logoImg.width / logoImg.height;
|
|
419
|
+
const logoHeight = logoSize;
|
|
420
|
+
const logoWidth = logoHeight * aspect;
|
|
421
|
+
|
|
422
|
+
ctx.drawImage(logoImg, logoX, currentY, logoWidth, logoHeight);
|
|
423
|
+
currentY += logoHeight + padding;
|
|
424
|
+
} catch (e) {
|
|
425
|
+
console.error("Falha ao desenhar logo:", e.message);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// --- Desenha Título ---
|
|
430
|
+
const titleFontSize = Math.min(width, height) * 0.08;
|
|
431
|
+
ctx.fillStyle = this.textColor;
|
|
432
|
+
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
|
|
433
|
+
ctx.textAlign = "center";
|
|
434
|
+
ctx.textBaseline = "top";
|
|
435
|
+
|
|
436
|
+
const titleText = this.title;
|
|
437
|
+
ctx.fillText(titleText, width / 2, currentY);
|
|
438
|
+
currentY += titleFontSize * 1.2;
|
|
439
|
+
|
|
440
|
+
// --- Desenha Linha Decorativa ---
|
|
441
|
+
const lineWidth = width * 0.1;
|
|
442
|
+
const lineHeight = 2;
|
|
443
|
+
const lineX = (width - lineWidth) / 2;
|
|
444
|
+
const lineY = currentY + padding / 2;
|
|
445
|
+
|
|
446
|
+
ctx.fillStyle = this.accentColor;
|
|
447
|
+
ctx.fillRect(lineX, lineY, lineWidth, lineHeight);
|
|
448
|
+
currentY = lineY + lineHeight + padding;
|
|
449
|
+
|
|
450
|
+
// --- Desenha Subtítulo (se fornecido) ---
|
|
451
|
+
if (this.subtitle) {
|
|
452
|
+
const subtitleFontSize = titleFontSize * 0.6;
|
|
453
|
+
ctx.fillStyle = this.secondaryColor;
|
|
454
|
+
ctx.font = `medium ${subtitleFontSize}px ${fontName}-Medium`;
|
|
455
|
+
ctx.textAlign = "center";
|
|
456
|
+
|
|
457
|
+
ctx.fillText(this.subtitle, width / 2, currentY);
|
|
458
|
+
currentY += subtitleFontSize * 1.5;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// --- Desenha Texto (se fornecido) ---
|
|
462
|
+
if (this.text) {
|
|
463
|
+
const textFontSize = titleFontSize * 0.4;
|
|
464
|
+
ctx.fillStyle = this.textColor;
|
|
465
|
+
ctx.font = `regular ${textFontSize}px ${fontName}-Regular`;
|
|
466
|
+
ctx.textAlign = "center";
|
|
467
|
+
|
|
468
|
+
currentY = wrapText(ctx, this.text, width / 2, currentY + padding, width - padding * 4, textFontSize * 1.5, fontName);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// --- Desenha Imagem (se fornecida) ---
|
|
472
|
+
if (this.image) {
|
|
473
|
+
try {
|
|
474
|
+
const remainingHeight = height - currentY - padding * 2;
|
|
475
|
+
const imageHeight = Math.min(remainingHeight, height * 0.3);
|
|
476
|
+
const imageWidth = width * 0.6;
|
|
477
|
+
const imageX = (width - imageWidth) / 2;
|
|
478
|
+
const imageY = currentY + padding;
|
|
479
|
+
|
|
480
|
+
const img = await loadImageWithAxios(this.image);
|
|
481
|
+
ctx.drawImage(img, imageX, imageY, imageWidth, imageHeight);
|
|
482
|
+
currentY = imageY + imageHeight;
|
|
483
|
+
} catch (e) {
|
|
484
|
+
console.error("Falha ao desenhar imagem:", e.message);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// --- Desenha CTA (se fornecido) ---
|
|
489
|
+
if (this.ctaText) {
|
|
490
|
+
const ctaFontSize = titleFontSize * 0.5;
|
|
491
|
+
const ctaY = height - padding * 2;
|
|
492
|
+
|
|
493
|
+
ctx.fillStyle = this.accentColor;
|
|
494
|
+
ctx.font = `bold ${ctaFontSize}px ${fontName}-Bold`;
|
|
495
|
+
ctx.textAlign = "center";
|
|
496
|
+
ctx.textBaseline = "bottom";
|
|
497
|
+
|
|
498
|
+
ctx.fillText(this.ctaText, width / 2, ctaY);
|
|
499
|
+
|
|
500
|
+
// Desenha URL (se fornecida)
|
|
501
|
+
if (this.ctaUrl) {
|
|
502
|
+
const urlFontSize = ctaFontSize * 0.7;
|
|
503
|
+
ctx.fillStyle = this.secondaryColor;
|
|
504
|
+
ctx.font = `regular ${urlFontSize}px ${fontName}-Regular`;
|
|
505
|
+
|
|
506
|
+
ctx.fillText(this.ctaUrl, width / 2, ctaY + urlFontSize * 1.2);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// --- Desenha QR Code (se fornecido) ---
|
|
511
|
+
if (this.qrCode) {
|
|
512
|
+
try {
|
|
513
|
+
const qrSize = Math.min(width, height) * 0.15;
|
|
514
|
+
const qrX = width - qrSize - padding;
|
|
515
|
+
const qrY = height - qrSize - padding;
|
|
516
|
+
|
|
517
|
+
const qrImg = await loadImageWithAxios(this.qrCode);
|
|
518
|
+
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
|
|
519
|
+
} catch (e) {
|
|
520
|
+
console.error("Falha ao desenhar QR code:", e.message);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Desenha layout alinhado à esquerda
|
|
527
|
+
* @private
|
|
528
|
+
*/
|
|
529
|
+
async _drawLeftLayout(ctx, fontName, width, height, padding) {
|
|
530
|
+
const contentX = padding * 2;
|
|
531
|
+
let currentY = padding * 2;
|
|
532
|
+
const contentWidth = width * 0.6;
|
|
533
|
+
|
|
534
|
+
// --- Desenha Logo (se fornecido) ---
|
|
535
|
+
if (this.logo) {
|
|
536
|
+
try {
|
|
537
|
+
const logoSize = Math.min(width, height) * 0.1;
|
|
538
|
+
|
|
539
|
+
const logoImg = await loadImageWithAxios(this.logo);
|
|
540
|
+
const aspect = logoImg.width / logoImg.height;
|
|
541
|
+
const logoHeight = logoSize;
|
|
542
|
+
const logoWidth = logoHeight * aspect;
|
|
543
|
+
|
|
544
|
+
ctx.drawImage(logoImg, contentX, currentY, logoWidth, logoHeight);
|
|
545
|
+
currentY += logoHeight + padding;
|
|
546
|
+
} catch (e) {
|
|
547
|
+
console.error("Falha ao desenhar logo:", e.message);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// --- Desenha Título ---
|
|
552
|
+
const titleFontSize = Math.min(width, height) * 0.07;
|
|
553
|
+
ctx.fillStyle = this.textColor;
|
|
554
|
+
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
|
|
555
|
+
ctx.textAlign = "left";
|
|
556
|
+
ctx.textBaseline = "top";
|
|
557
|
+
|
|
558
|
+
const titleText = this.title;
|
|
559
|
+
ctx.fillText(titleText, contentX, currentY);
|
|
560
|
+
currentY += titleFontSize * 1.2;
|
|
561
|
+
|
|
562
|
+
// --- Desenha Linha Decorativa ---
|
|
563
|
+
const lineWidth = contentWidth * 0.2;
|
|
564
|
+
const lineHeight = 2;
|
|
565
|
+
const lineY = currentY + padding / 2;
|
|
566
|
+
|
|
567
|
+
ctx.fillStyle = this.accentColor;
|
|
568
|
+
ctx.fillRect(contentX, lineY, lineWidth, lineHeight);
|
|
569
|
+
currentY = lineY + lineHeight + padding;
|
|
570
|
+
|
|
571
|
+
// --- Desenha Subtítulo (se fornecido) ---
|
|
572
|
+
if (this.subtitle) {
|
|
573
|
+
const subtitleFontSize = titleFontSize * 0.6;
|
|
574
|
+
ctx.fillStyle = this.secondaryColor;
|
|
575
|
+
ctx.font = `medium ${subtitleFontSize}px ${fontName}-Medium`;
|
|
576
|
+
ctx.textAlign = "left";
|
|
577
|
+
|
|
578
|
+
ctx.fillText(this.subtitle, contentX, currentY);
|
|
579
|
+
currentY += subtitleFontSize * 1.5;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// --- Desenha Texto (se fornecido) ---
|
|
583
|
+
if (this.text) {
|
|
584
|
+
const textFontSize = titleFontSize * 0.4;
|
|
585
|
+
ctx.fillStyle = this.textColor;
|
|
586
|
+
ctx.font = `regular ${textFontSize}px ${fontName}-Regular`;
|
|
587
|
+
ctx.textAlign = "left";
|
|
588
|
+
|
|
589
|
+
currentY = wrapText(ctx, this.text, contentX, currentY + padding, contentWidth - padding, textFontSize * 1.5, fontName);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// --- Desenha CTA (se fornecido) ---
|
|
593
|
+
if (this.ctaText) {
|
|
594
|
+
const ctaFontSize = titleFontSize * 0.5;
|
|
595
|
+
const ctaY = height - padding * 2;
|
|
596
|
+
|
|
597
|
+
ctx.fillStyle = this.accentColor;
|
|
598
|
+
ctx.font = `bold ${ctaFontSize}px ${fontName}-Bold`;
|
|
599
|
+
ctx.textAlign = "left";
|
|
600
|
+
ctx.textBaseline = "bottom";
|
|
601
|
+
|
|
602
|
+
ctx.fillText(this.ctaText, contentX, ctaY);
|
|
603
|
+
|
|
604
|
+
// Desenha URL (se fornecida)
|
|
605
|
+
if (this.ctaUrl) {
|
|
606
|
+
const urlFontSize = ctaFontSize * 0.7;
|
|
607
|
+
ctx.fillStyle = this.secondaryColor;
|
|
608
|
+
ctx.font = `regular ${urlFontSize}px ${fontName}-Regular`;
|
|
609
|
+
|
|
610
|
+
ctx.fillText(this.ctaUrl, contentX, ctaY + urlFontSize * 1.2);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// --- Desenha Imagem (se fornecida) ---
|
|
615
|
+
if (this.image) {
|
|
616
|
+
try {
|
|
617
|
+
const imageWidth = width * 0.35;
|
|
618
|
+
const imageHeight = height * 0.6;
|
|
619
|
+
const imageX = width - imageWidth - padding * 2;
|
|
620
|
+
const imageY = (height - imageHeight) / 2;
|
|
621
|
+
|
|
622
|
+
const img = await loadImageWithAxios(this.image);
|
|
623
|
+
ctx.drawImage(img, imageX, imageY, imageWidth, imageHeight);
|
|
624
|
+
} catch (e) {
|
|
625
|
+
console.error("Falha ao desenhar imagem:", e.message);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// --- Desenha QR Code (se fornecido) ---
|
|
630
|
+
if (this.qrCode) {
|
|
631
|
+
try {
|
|
632
|
+
const qrSize = Math.min(width, height) * 0.15;
|
|
633
|
+
const qrX = width - qrSize - padding;
|
|
634
|
+
const qrY = height - qrSize - padding;
|
|
635
|
+
|
|
636
|
+
const qrImg = await loadImageWithAxios(this.qrCode);
|
|
637
|
+
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
|
|
638
|
+
} catch (e) {
|
|
639
|
+
console.error("Falha ao desenhar QR code:", e.message);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Desenha layout alinhado à direita
|
|
646
|
+
* @private
|
|
647
|
+
*/
|
|
648
|
+
async _drawRightLayout(ctx, fontName, width, height, padding) {
|
|
649
|
+
const contentWidth = width * 0.6;
|
|
650
|
+
const contentX = width - contentWidth - padding * 2;
|
|
651
|
+
let currentY = padding * 2;
|
|
652
|
+
|
|
653
|
+
// --- Desenha Logo (se fornecido) ---
|
|
654
|
+
if (this.logo) {
|
|
655
|
+
try {
|
|
656
|
+
const logoSize = Math.min(width, height) * 0.1;
|
|
657
|
+
|
|
658
|
+
const logoImg = await loadImageWithAxios(this.logo);
|
|
659
|
+
const aspect = logoImg.width / logoImg.height;
|
|
660
|
+
const logoHeight = logoSize;
|
|
661
|
+
const logoWidth = logoHeight * aspect;
|
|
662
|
+
|
|
663
|
+
ctx.drawImage(logoImg, contentX, currentY, logoWidth, logoHeight);
|
|
664
|
+
currentY += logoHeight + padding;
|
|
665
|
+
} catch (e) {
|
|
666
|
+
console.error("Falha ao desenhar logo:", e.message);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// --- Desenha Título ---
|
|
671
|
+
const titleFontSize = Math.min(width, height) * 0.07;
|
|
672
|
+
ctx.fillStyle = this.textColor;
|
|
673
|
+
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
|
|
674
|
+
ctx.textAlign = "left";
|
|
675
|
+
ctx.textBaseline = "top";
|
|
676
|
+
|
|
677
|
+
const titleText = this.title;
|
|
678
|
+
ctx.fillText(titleText, contentX, currentY);
|
|
679
|
+
currentY += titleFontSize * 1.2;
|
|
680
|
+
|
|
681
|
+
// --- Desenha Linha Decorativa ---
|
|
682
|
+
const lineWidth = contentWidth * 0.2;
|
|
683
|
+
const lineHeight = 2;
|
|
684
|
+
const lineY = currentY + padding / 2;
|
|
685
|
+
|
|
686
|
+
ctx.fillStyle = this.accentColor;
|
|
687
|
+
ctx.fillRect(contentX, lineY, lineWidth, lineHeight);
|
|
688
|
+
currentY = lineY + lineHeight + padding;
|
|
689
|
+
|
|
690
|
+
// --- Desenha Subtítulo (se fornecido) ---
|
|
691
|
+
if (this.subtitle) {
|
|
692
|
+
const subtitleFontSize = titleFontSize * 0.6;
|
|
693
|
+
ctx.fillStyle = this.secondaryColor;
|
|
694
|
+
ctx.font = `medium ${subtitleFontSize}px ${fontName}-Medium`;
|
|
695
|
+
ctx.textAlign = "left";
|
|
696
|
+
|
|
697
|
+
ctx.fillText(this.subtitle, contentX, currentY);
|
|
698
|
+
currentY += subtitleFontSize * 1.5;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// --- Desenha Texto (se fornecido) ---
|
|
702
|
+
if (this.text) {
|
|
703
|
+
const textFontSize = titleFontSize * 0.4;
|
|
704
|
+
ctx.fillStyle = this.textColor;
|
|
705
|
+
ctx.font = `regular ${textFontSize}px ${fontName}-Regular`;
|
|
706
|
+
ctx.textAlign = "left";
|
|
707
|
+
|
|
708
|
+
currentY = wrapText(ctx, this.text, contentX, currentY + padding, contentWidth - padding, textFontSize * 1.5, fontName);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// --- Desenha CTA (se fornecido) ---
|
|
712
|
+
if (this.ctaText) {
|
|
713
|
+
const ctaFontSize = titleFontSize * 0.5;
|
|
714
|
+
const ctaY = height - padding * 2;
|
|
715
|
+
|
|
716
|
+
ctx.fillStyle = this.accentColor;
|
|
717
|
+
ctx.font = `bold ${ctaFontSize}px ${fontName}-Bold`;
|
|
718
|
+
ctx.textAlign = "left";
|
|
719
|
+
ctx.textBaseline = "bottom";
|
|
720
|
+
|
|
721
|
+
ctx.fillText(this.ctaText, contentX, ctaY);
|
|
722
|
+
|
|
723
|
+
// Desenha URL (se fornecida)
|
|
724
|
+
if (this.ctaUrl) {
|
|
725
|
+
const urlFontSize = ctaFontSize * 0.7;
|
|
726
|
+
ctx.fillStyle = this.secondaryColor;
|
|
727
|
+
ctx.font = `regular ${urlFontSize}px ${fontName}-Regular`;
|
|
728
|
+
|
|
729
|
+
ctx.fillText(this.ctaUrl, contentX, ctaY + urlFontSize * 1.2);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// --- Desenha Imagem (se fornecida) ---
|
|
734
|
+
if (this.image) {
|
|
735
|
+
try {
|
|
736
|
+
const imageWidth = width * 0.35;
|
|
737
|
+
const imageHeight = height * 0.6;
|
|
738
|
+
const imageX = padding * 2;
|
|
739
|
+
const imageY = (height - imageHeight) / 2;
|
|
740
|
+
|
|
741
|
+
const img = await loadImageWithAxios(this.image);
|
|
742
|
+
ctx.drawImage(img, imageX, imageY, imageWidth, imageHeight);
|
|
743
|
+
} catch (e) {
|
|
744
|
+
console.error("Falha ao desenhar imagem:", e.message);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// --- Desenha QR Code (se fornecido) ---
|
|
749
|
+
if (this.qrCode) {
|
|
750
|
+
try {
|
|
751
|
+
const qrSize = Math.min(width, height) * 0.15;
|
|
752
|
+
const qrX = padding;
|
|
753
|
+
const qrY = height - qrSize - padding;
|
|
754
|
+
|
|
755
|
+
const qrImg = await loadImageWithAxios(this.qrCode);
|
|
756
|
+
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
|
|
757
|
+
} catch (e) {
|
|
758
|
+
console.error("Falha ao desenhar QR code:", e.message);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Desenha layout dividido
|
|
765
|
+
* @private
|
|
766
|
+
*/
|
|
767
|
+
async _drawSplitLayout(ctx, fontName, width, height, padding) {
|
|
768
|
+
// Divide o canvas em duas partes
|
|
769
|
+
const halfWidth = width / 2;
|
|
770
|
+
|
|
771
|
+
// --- Desenha Divisão Visual ---
|
|
772
|
+
if (this.style !== "clean") {
|
|
773
|
+
ctx.fillStyle = this.accentColor;
|
|
774
|
+
ctx.fillRect(halfWidth - 1, 0, 2, height);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// --- Lado Esquerdo (Texto) ---
|
|
778
|
+
const leftContentX = padding * 2;
|
|
779
|
+
let leftCurrentY = padding * 2;
|
|
780
|
+
const leftContentWidth = halfWidth - padding * 3;
|
|
781
|
+
|
|
782
|
+
// Logo (se fornecido)
|
|
783
|
+
if (this.logo) {
|
|
784
|
+
try {
|
|
785
|
+
const logoSize = Math.min(halfWidth, height) * 0.15;
|
|
786
|
+
|
|
787
|
+
const logoImg = await loadImageWithAxios(this.logo);
|
|
788
|
+
const aspect = logoImg.width / logoImg.height;
|
|
789
|
+
const logoHeight = logoSize;
|
|
790
|
+
const logoWidth = logoHeight * aspect;
|
|
791
|
+
|
|
792
|
+
ctx.drawImage(logoImg, leftContentX, leftCurrentY, logoWidth, logoHeight);
|
|
793
|
+
leftCurrentY += logoHeight + padding;
|
|
794
|
+
} catch (e) {
|
|
795
|
+
console.error("Falha ao desenhar logo:", e.message);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Título
|
|
800
|
+
const titleFontSize = Math.min(halfWidth, height) * 0.08;
|
|
801
|
+
ctx.fillStyle = this.textColor;
|
|
802
|
+
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
|
|
803
|
+
ctx.textAlign = "left";
|
|
804
|
+
ctx.textBaseline = "top";
|
|
805
|
+
|
|
806
|
+
const titleText = this.title;
|
|
807
|
+
ctx.fillText(titleText, leftContentX, leftCurrentY);
|
|
808
|
+
leftCurrentY += titleFontSize * 1.2;
|
|
809
|
+
|
|
810
|
+
// Linha Decorativa
|
|
811
|
+
const lineWidth = leftContentWidth * 0.3;
|
|
812
|
+
const lineHeight = 2;
|
|
813
|
+
const lineY = leftCurrentY + padding / 2;
|
|
814
|
+
|
|
815
|
+
ctx.fillStyle = this.accentColor;
|
|
816
|
+
ctx.fillRect(leftContentX, lineY, lineWidth, lineHeight);
|
|
817
|
+
leftCurrentY = lineY + lineHeight + padding;
|
|
818
|
+
|
|
819
|
+
// Subtítulo (se fornecido)
|
|
820
|
+
if (this.subtitle) {
|
|
821
|
+
const subtitleFontSize = titleFontSize * 0.6;
|
|
822
|
+
ctx.fillStyle = this.secondaryColor;
|
|
823
|
+
ctx.font = `medium ${subtitleFontSize}px ${fontName}-Medium`;
|
|
824
|
+
ctx.textAlign = "left";
|
|
825
|
+
|
|
826
|
+
ctx.fillText(this.subtitle, leftContentX, leftCurrentY);
|
|
827
|
+
leftCurrentY += subtitleFontSize * 1.5;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Texto (se fornecido)
|
|
831
|
+
if (this.text) {
|
|
832
|
+
const textFontSize = titleFontSize * 0.4;
|
|
833
|
+
ctx.fillStyle = this.textColor;
|
|
834
|
+
ctx.font = `regular ${textFontSize}px ${fontName}-Regular`;
|
|
835
|
+
ctx.textAlign = "left";
|
|
836
|
+
|
|
837
|
+
leftCurrentY = wrapText(ctx, this.text, leftContentX, leftCurrentY + padding, leftContentWidth, textFontSize * 1.5, fontName);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// CTA (se fornecido)
|
|
841
|
+
if (this.ctaText) {
|
|
842
|
+
const ctaFontSize = titleFontSize * 0.5;
|
|
843
|
+
const ctaY = height - padding * 2;
|
|
844
|
+
|
|
845
|
+
ctx.fillStyle = this.accentColor;
|
|
846
|
+
ctx.font = `bold ${ctaFontSize}px ${fontName}-Bold`;
|
|
847
|
+
ctx.textAlign = "left";
|
|
848
|
+
ctx.textBaseline = "bottom";
|
|
849
|
+
|
|
850
|
+
ctx.fillText(this.ctaText, leftContentX, ctaY);
|
|
851
|
+
|
|
852
|
+
// URL (se fornecida)
|
|
853
|
+
if (this.ctaUrl) {
|
|
854
|
+
const urlFontSize = ctaFontSize * 0.7;
|
|
855
|
+
ctx.fillStyle = this.secondaryColor;
|
|
856
|
+
ctx.font = `regular ${urlFontSize}px ${fontName}-Regular`;
|
|
857
|
+
|
|
858
|
+
ctx.fillText(this.ctaUrl, leftContentX, ctaY + urlFontSize * 1.2);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// --- Lado Direito (Imagem) ---
|
|
863
|
+
if (this.image) {
|
|
864
|
+
try {
|
|
865
|
+
const rightContentX = halfWidth + padding;
|
|
866
|
+
const imageWidth = halfWidth - padding * 2;
|
|
867
|
+
const imageHeight = height - padding * 2;
|
|
868
|
+
const imageY = padding;
|
|
869
|
+
|
|
870
|
+
const img = await loadImageWithAxios(this.image);
|
|
871
|
+
ctx.drawImage(img, rightContentX, imageY, imageWidth, imageHeight);
|
|
872
|
+
} catch (e) {
|
|
873
|
+
console.error("Falha ao desenhar imagem:", e.message);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// QR Code (se fornecido)
|
|
878
|
+
if (this.qrCode) {
|
|
879
|
+
try {
|
|
880
|
+
const qrSize = Math.min(halfWidth, height) * 0.2;
|
|
881
|
+
const qrX = width - qrSize - padding;
|
|
882
|
+
const qrY = height - qrSize - padding;
|
|
883
|
+
|
|
884
|
+
const qrImg = await loadImageWithAxios(this.qrCode);
|
|
885
|
+
ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
|
|
886
|
+
} catch (e) {
|
|
887
|
+
console.error("Falha ao desenhar QR code:", e.message);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
|