@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.
Files changed (48) hide show
  1. package/assets/fonts/Manrope/Manrope-Bold.ttf +0 -0
  2. package/assets/fonts/Manrope/Manrope-Regular.ttf +0 -0
  3. package/assets/fonts/Others/AbyssinicaSIL-Regular.ttf +0 -0
  4. package/assets/fonts/Others/ChirpRegular.ttf +0 -0
  5. package/assets/fonts/Poppins/Poppins-Bold.ttf +0 -0
  6. package/assets/fonts/Poppins/Poppins-Medium.ttf +0 -0
  7. package/assets/fonts/Poppins/Poppins-Regular.ttf +0 -0
  8. package/assets/placeholders/album_art.png +0 -0
  9. package/assets/placeholders/avatar.png +0 -0
  10. package/assets/placeholders/badge.jpg +0 -0
  11. package/assets/placeholders/badge.png +0 -0
  12. package/assets/placeholders/badge_2.jpg +0 -0
  13. package/assets/placeholders/badge_3.jpg +0 -0
  14. package/assets/placeholders/badge_4.jpg +0 -0
  15. package/assets/placeholders/badge_5.jpg +0 -0
  16. package/assets/placeholders/banner.jpeg +0 -0
  17. package/assets/placeholders/images.jpeg +0 -0
  18. package/index.js +153 -0
  19. package/package.json +34 -0
  20. package/src/animation-effects.js +631 -0
  21. package/src/cache-manager.js +258 -0
  22. package/src/community-banner.js +1536 -0
  23. package/src/constants.js +208 -0
  24. package/src/discord-profile.js +584 -0
  25. package/src/e-commerce-banner.js +1214 -0
  26. package/src/effects.js +355 -0
  27. package/src/error-handler.js +305 -0
  28. package/src/event-banner.js +1319 -0
  29. package/src/facebook-post.js +679 -0
  30. package/src/gradient-welcome.js +430 -0
  31. package/src/image-filters.js +1034 -0
  32. package/src/image-processor.js +1014 -0
  33. package/src/instagram-post.js +504 -0
  34. package/src/interactive-elements.js +1208 -0
  35. package/src/linkedin-post.js +658 -0
  36. package/src/marketing-banner.js +1089 -0
  37. package/src/minimalist-banner.js +892 -0
  38. package/src/modern-profile.js +755 -0
  39. package/src/performance-optimizer.js +216 -0
  40. package/src/telegram-header.js +544 -0
  41. package/src/test-runner.js +645 -0
  42. package/src/tiktok-post.js +713 -0
  43. package/src/twitter-header.js +604 -0
  44. package/src/validator.js +442 -0
  45. package/src/welcome-leave.js +445 -0
  46. package/src/whatsapp-status.js +386 -0
  47. package/src/youtube-thumbnail.js +681 -0
  48. 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
+