@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,1319 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Módulo de Banner de Evento
5
+ *
6
+ * Este módulo gera banners para eventos com informações como título,
7
+ * data, local, descrição e outros elementos visuais personalizáveis.
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
+ applyMultiColorGradient,
40
+ applyGlow,
41
+ applyDotPattern,
42
+ applyLinePattern
43
+ } = require("./effects");
44
+
45
+ /**
46
+ * @class EventBanner
47
+ * @classdesc Gera um banner para eventos com informações detalhadas e design personalizável.
48
+ * @example const banner = new EventBanner()
49
+ * .setTitle("Nome do Evento")
50
+ * .setDate("25 de Dezembro, 2025")
51
+ * .setTime("19:00 - 23:00")
52
+ * .setLocation("Local do Evento, Cidade")
53
+ * .setDescription("Descrição detalhada do evento com informações importantes.")
54
+ * .setBackground("image", "background.jpg")
55
+ * .setStyle("modern")
56
+ * .build();
57
+ */
58
+ module.exports = class EventBanner {
59
+ constructor(options) {
60
+ // Dados Principais
61
+ this.title = "Nome do Evento";
62
+ this.date = null;
63
+ this.time = null;
64
+ this.location = null;
65
+ this.description = null;
66
+ this.organizer = null;
67
+ this.logo = null;
68
+ this.qrCode = null;
69
+ this.ticketPrice = null;
70
+ this.contactInfo = null;
71
+ this.speakers = [];
72
+ this.sponsors = [];
73
+ this.categories = [];
74
+ this.registrationUrl = null;
75
+
76
+ // Personalização Visual
77
+ this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path };
78
+ this.background = { type: "color", value: DEFAULT_COLORS.gradient.blue.start };
79
+ this.style = "modern"; // modern, classic, minimal, bold, festive
80
+ this.colorScheme = "blue"; // blue, green, purple, red, orange, custom
81
+ this.customColors = {
82
+ primary: null,
83
+ secondary: null,
84
+ accent: null,
85
+ text: "#FFFFFF",
86
+ background: null
87
+ };
88
+ this.useGradient = true;
89
+ this.useTextShadow = true;
90
+ this.usePatterns = false;
91
+ this.patternType = "dots"; // dots, lines
92
+ this.useGlassmorphism = false;
93
+
94
+ // Configurações de Layout
95
+ this.cardWidth = DEFAULT_DIMENSIONS.banner.width;
96
+ this.cardHeight = DEFAULT_DIMENSIONS.banner.height;
97
+ this.cornerRadius = LAYOUT.cornerRadius.medium;
98
+ this.orientation = "landscape"; // landscape, portrait, square
99
+ }
100
+
101
+ // --- Setters para Dados Principais ---
102
+ /**
103
+ * Define o título do evento
104
+ * @param {string} text - Título do evento
105
+ * @returns {EventBanner} - Instância atual para encadeamento
106
+ */
107
+ setTitle(text) {
108
+ if (!text || typeof text !== "string") throw new Error("O título do evento deve ser uma string não vazia.");
109
+ this.title = text;
110
+ return this;
111
+ }
112
+
113
+ /**
114
+ * Define a data do evento
115
+ * @param {string} text - Data do evento
116
+ * @returns {EventBanner} - Instância atual para encadeamento
117
+ */
118
+ setDate(text) {
119
+ if (!text || typeof text !== "string") throw new Error("A data do evento deve ser uma string não vazia.");
120
+ this.date = text;
121
+ return this;
122
+ }
123
+
124
+ /**
125
+ * Define o horário do evento
126
+ * @param {string} text - Horário do evento
127
+ * @returns {EventBanner} - Instância atual para encadeamento
128
+ */
129
+ setTime(text) {
130
+ if (!text || typeof text !== "string") throw new Error("O horário do evento deve ser uma string não vazia.");
131
+ this.time = text;
132
+ return this;
133
+ }
134
+
135
+ /**
136
+ * Define o local do evento
137
+ * @param {string} text - Local do evento
138
+ * @returns {EventBanner} - Instância atual para encadeamento
139
+ */
140
+ setLocation(text) {
141
+ if (!text || typeof text !== "string") throw new Error("O local do evento deve ser uma string não vazia.");
142
+ this.location = text;
143
+ return this;
144
+ }
145
+
146
+ /**
147
+ * Define a descrição do evento
148
+ * @param {string} text - Descrição do evento
149
+ * @returns {EventBanner} - Instância atual para encadeamento
150
+ */
151
+ setDescription(text) {
152
+ if (!text || typeof text !== "string") throw new Error("A descrição do evento deve ser uma string não vazia.");
153
+ this.description = text;
154
+ return this;
155
+ }
156
+
157
+ /**
158
+ * Define o organizador do evento
159
+ * @param {string} text - Nome do organizador
160
+ * @returns {EventBanner} - Instância atual para encadeamento
161
+ */
162
+ setOrganizer(text) {
163
+ if (!text || typeof text !== "string") throw new Error("O organizador do evento deve ser uma string não vazia.");
164
+ this.organizer = text;
165
+ return this;
166
+ }
167
+
168
+ /**
169
+ * Define o logo do evento
170
+ * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do logo
171
+ * @returns {EventBanner} - Instância atual para encadeamento
172
+ */
173
+ setLogo(image) {
174
+ if (!image) throw new Error("A fonte da imagem do logo não pode estar vazia.");
175
+ this.logo = image;
176
+ return this;
177
+ }
178
+
179
+ /**
180
+ * Define o código QR para o evento
181
+ * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do código QR
182
+ * @returns {EventBanner} - Instância atual para encadeamento
183
+ */
184
+ setQRCode(image) {
185
+ if (!image) throw new Error("A fonte da imagem do código QR não pode estar vazia.");
186
+ this.qrCode = image;
187
+ return this;
188
+ }
189
+
190
+ /**
191
+ * Define o preço do ingresso
192
+ * @param {string} text - Preço do ingresso
193
+ * @returns {EventBanner} - Instância atual para encadeamento
194
+ */
195
+ setTicketPrice(text) {
196
+ if (!text || typeof text !== "string") throw new Error("O preço do ingresso deve ser uma string não vazia.");
197
+ this.ticketPrice = text;
198
+ return this;
199
+ }
200
+
201
+ /**
202
+ * Define as informações de contato
203
+ * @param {string} text - Informações de contato
204
+ * @returns {EventBanner} - Instância atual para encadeamento
205
+ */
206
+ setContactInfo(text) {
207
+ if (!text || typeof text !== "string") throw new Error("As informações de contato devem ser uma string não vazia.");
208
+ this.contactInfo = text;
209
+ return this;
210
+ }
211
+
212
+ /**
213
+ * Adiciona um palestrante ao evento
214
+ * @param {string} name - Nome do palestrante
215
+ * @param {string} role - Cargo/função do palestrante
216
+ * @param {string|Buffer|Object} avatar - URL, Buffer ou caminho da imagem do avatar (opcional)
217
+ * @returns {EventBanner} - Instância atual para encadeamento
218
+ */
219
+ addSpeaker(name, role, avatar = null) {
220
+ if (!name || typeof name !== "string") throw new Error("O nome do palestrante deve ser uma string não vazia.");
221
+ if (!role || typeof role !== "string") throw new Error("O cargo/função do palestrante deve ser uma string não vazia.");
222
+
223
+ this.speakers.push({ name, role, avatar });
224
+ return this;
225
+ }
226
+
227
+ /**
228
+ * Adiciona um patrocinador ao evento
229
+ * @param {string} name - Nome do patrocinador
230
+ * @param {string|Buffer|Object} logo - URL, Buffer ou caminho da imagem do logo (opcional)
231
+ * @returns {EventBanner} - Instância atual para encadeamento
232
+ */
233
+ addSponsor(name, logo = null) {
234
+ if (!name || typeof name !== "string") throw new Error("O nome do patrocinador deve ser uma string não vazia.");
235
+
236
+ this.sponsors.push({ name, logo });
237
+ return this;
238
+ }
239
+
240
+ /**
241
+ * Define as categorias do evento
242
+ * @param {Array<string>} categories - Array de categorias
243
+ * @returns {EventBanner} - Instância atual para encadeamento
244
+ */
245
+ setCategories(categories) {
246
+ if (!Array.isArray(categories)) throw new Error("As categorias devem ser um array de strings.");
247
+
248
+ this.categories = categories;
249
+ return this;
250
+ }
251
+
252
+ /**
253
+ * Define a URL de registro/inscrição
254
+ * @param {string} url - URL de registro
255
+ * @returns {EventBanner} - Instância atual para encadeamento
256
+ */
257
+ setRegistrationUrl(url) {
258
+ if (!url || typeof url !== "string") throw new Error("A URL de registro deve ser uma string não vazia.");
259
+ this.registrationUrl = url;
260
+ return this;
261
+ }
262
+
263
+ // --- Setters para Personalização Visual ---
264
+ /**
265
+ * Define o plano de fundo
266
+ * @param {string} type - Tipo de plano de fundo ('color', 'image' ou 'gradient')
267
+ * @param {string|Array} value - Valor do plano de fundo (cor hexadecimal, URL/caminho da imagem ou array de cores para gradiente)
268
+ * @returns {EventBanner} - Instância atual para encadeamento
269
+ */
270
+ setBackground(type, value) {
271
+ const types = ["color", "image", "gradient"];
272
+ if (!type || !types.includes(type.toLowerCase())) {
273
+ throw new Error("O tipo de plano de fundo deve ser 'color', 'image' ou 'gradient'.");
274
+ }
275
+
276
+ if (!value) throw new Error("O valor do plano de fundo não pode estar vazio.");
277
+
278
+ if (type.toLowerCase() === "color" && !isValidHexColor(value)) {
279
+ throw new Error("Cor de plano de fundo inválida. Use o formato hexadecimal.");
280
+ }
281
+
282
+ if (type.toLowerCase() === "gradient") {
283
+ if (!Array.isArray(value) || value.length < 2) {
284
+ throw new Error("Para gradiente, forneça um array com pelo menos duas cores hexadecimais.");
285
+ }
286
+
287
+ for (const color of value) {
288
+ if (!isValidHexColor(color)) {
289
+ throw new Error("Todas as cores do gradiente devem estar no formato hexadecimal.");
290
+ }
291
+ }
292
+
293
+ this.useGradient = true;
294
+ }
295
+
296
+ this.background = { type: type.toLowerCase(), value };
297
+ return this;
298
+ }
299
+
300
+ /**
301
+ * Define o estilo do banner
302
+ * @param {string} style - Estilo ('modern', 'classic', 'minimal', 'bold', 'festive')
303
+ * @returns {EventBanner} - Instância atual para encadeamento
304
+ */
305
+ setStyle(style) {
306
+ const validStyles = ["modern", "classic", "minimal", "bold", "festive"];
307
+ if (!style || !validStyles.includes(style.toLowerCase())) {
308
+ throw new Error(`Estilo inválido. Use um dos seguintes: ${validStyles.join(", ")}`);
309
+ }
310
+
311
+ this.style = style.toLowerCase();
312
+
313
+ // Ajusta configurações com base no estilo
314
+ switch (this.style) {
315
+ case "modern":
316
+ this.useGradient = true;
317
+ this.useTextShadow = true;
318
+ this.usePatterns = false;
319
+ this.useGlassmorphism = true;
320
+ this.cornerRadius = LAYOUT.cornerRadius.medium;
321
+ break;
322
+ case "classic":
323
+ this.useGradient = false;
324
+ this.useTextShadow = false;
325
+ this.usePatterns = false;
326
+ this.useGlassmorphism = false;
327
+ this.cornerRadius = LAYOUT.cornerRadius.small;
328
+ break;
329
+ case "minimal":
330
+ this.useGradient = false;
331
+ this.useTextShadow = false;
332
+ this.usePatterns = false;
333
+ this.useGlassmorphism = false;
334
+ this.cornerRadius = 0;
335
+ break;
336
+ case "bold":
337
+ this.useGradient = true;
338
+ this.useTextShadow = true;
339
+ this.usePatterns = true;
340
+ this.useGlassmorphism = false;
341
+ this.cornerRadius = LAYOUT.cornerRadius.large;
342
+ break;
343
+ case "festive":
344
+ this.useGradient = true;
345
+ this.useTextShadow = true;
346
+ this.usePatterns = true;
347
+ this.useGlassmorphism = false;
348
+ this.cornerRadius = LAYOUT.cornerRadius.medium;
349
+ break;
350
+ }
351
+
352
+ return this;
353
+ }
354
+
355
+ /**
356
+ * Define o esquema de cores
357
+ * @param {string} scheme - Esquema de cores ('blue', 'green', 'purple', 'red', 'orange', 'custom')
358
+ * @param {Object} customColors - Cores personalizadas (apenas para esquema 'custom')
359
+ * @returns {EventBanner} - Instância atual para encadeamento
360
+ */
361
+ setColorScheme(scheme, customColors = null) {
362
+ const validSchemes = ["blue", "green", "purple", "red", "orange", "custom"];
363
+ if (!scheme || !validSchemes.includes(scheme.toLowerCase())) {
364
+ throw new Error(`Esquema de cores inválido. Use um dos seguintes: ${validSchemes.join(", ")}`);
365
+ }
366
+
367
+ this.colorScheme = scheme.toLowerCase();
368
+
369
+ if (this.colorScheme === "custom") {
370
+ if (!customColors || typeof customColors !== "object") {
371
+ throw new Error("Para o esquema de cores personalizado, forneça um objeto com as cores.");
372
+ }
373
+
374
+ this.customColors = {
375
+ primary: customColors.primary || this.customColors.primary,
376
+ secondary: customColors.secondary || this.customColors.secondary,
377
+ accent: customColors.accent || this.customColors.accent,
378
+ text: customColors.text || this.customColors.text,
379
+ background: customColors.background || this.customColors.background
380
+ };
381
+ }
382
+
383
+ return this;
384
+ }
385
+
386
+ /**
387
+ * Ativa ou desativa o uso de gradiente
388
+ * @param {boolean} enabled - Se o gradiente deve ser ativado
389
+ * @returns {EventBanner} - Instância atual para encadeamento
390
+ */
391
+ enableGradient(enabled = true) {
392
+ this.useGradient = enabled;
393
+ return this;
394
+ }
395
+
396
+ /**
397
+ * Ativa ou desativa a sombra de texto
398
+ * @param {boolean} enabled - Se a sombra de texto deve ser ativada
399
+ * @returns {EventBanner} - Instância atual para encadeamento
400
+ */
401
+ enableTextShadow(enabled = true) {
402
+ this.useTextShadow = enabled;
403
+ return this;
404
+ }
405
+
406
+ /**
407
+ * Ativa ou desativa o uso de padrões decorativos
408
+ * @param {boolean} enabled - Se os padrões devem ser ativados
409
+ * @param {string} type - Tipo de padrão ('dots', 'lines')
410
+ * @returns {EventBanner} - Instância atual para encadeamento
411
+ */
412
+ enablePatterns(enabled = true, type = "dots") {
413
+ this.usePatterns = enabled;
414
+
415
+ const validTypes = ["dots", "lines"];
416
+ if (validTypes.includes(type.toLowerCase())) {
417
+ this.patternType = type.toLowerCase();
418
+ }
419
+
420
+ return this;
421
+ }
422
+
423
+ /**
424
+ * Ativa ou desativa o efeito de glassmorphism
425
+ * @param {boolean} enabled - Se o efeito deve ser ativado
426
+ * @returns {EventBanner} - Instância atual para encadeamento
427
+ */
428
+ enableGlassmorphism(enabled = true) {
429
+ this.useGlassmorphism = enabled;
430
+ return this;
431
+ }
432
+
433
+ /**
434
+ * Define o raio dos cantos arredondados
435
+ * @param {number} radius - Raio dos cantos em pixels
436
+ * @returns {EventBanner} - Instância atual para encadeamento
437
+ */
438
+ setCornerRadius(radius) {
439
+ if (typeof radius !== "number" || radius < 0) throw new Error("O raio dos cantos deve ser um número não negativo.");
440
+ this.cornerRadius = radius;
441
+ return this;
442
+ }
443
+
444
+ /**
445
+ * Define a orientação do banner
446
+ * @param {string} orientation - Orientação ('landscape', 'portrait', 'square')
447
+ * @returns {EventBanner} - Instância atual para encadeamento
448
+ */
449
+ setOrientation(orientation) {
450
+ const validOrientations = ["landscape", "portrait", "square"];
451
+ if (!orientation || !validOrientations.includes(orientation.toLowerCase())) {
452
+ throw new Error(`Orientação inválida. Use uma das seguintes: ${validOrientations.join(", ")}`);
453
+ }
454
+
455
+ this.orientation = orientation.toLowerCase();
456
+
457
+ // Ajusta as dimensões com base na orientação
458
+ switch (this.orientation) {
459
+ case "landscape":
460
+ this.cardWidth = DEFAULT_DIMENSIONS.banner.width;
461
+ this.cardHeight = DEFAULT_DIMENSIONS.banner.height;
462
+ break;
463
+ case "portrait":
464
+ this.cardWidth = DEFAULT_DIMENSIONS.banner.height;
465
+ this.cardHeight = DEFAULT_DIMENSIONS.banner.width;
466
+ break;
467
+ case "square":
468
+ this.cardWidth = Math.min(DEFAULT_DIMENSIONS.banner.width, DEFAULT_DIMENSIONS.banner.height);
469
+ this.cardHeight = this.cardWidth;
470
+ break;
471
+ }
472
+
473
+ return this;
474
+ }
475
+
476
+ /**
477
+ * Define as dimensões do card
478
+ * @param {number} width - Largura do card em pixels
479
+ * @param {number} height - Altura do card em pixels
480
+ * @returns {EventBanner} - Instância atual para encadeamento
481
+ */
482
+ setCardDimensions(width, height) {
483
+ if (typeof width !== "number" || width < 400 || width > 1920) {
484
+ throw new Error("A largura do card deve estar entre 400 e 1920 pixels.");
485
+ }
486
+
487
+ if (typeof height !== "number" || height < 400 || height > 1920) {
488
+ throw new Error("A altura do card deve estar entre 400 e 1920 pixels.");
489
+ }
490
+
491
+ this.cardWidth = width;
492
+ this.cardHeight = height;
493
+
494
+ return this;
495
+ }
496
+
497
+ // --- Método de Construção ---
498
+ /**
499
+ * Constrói o banner e retorna um buffer de imagem
500
+ * @returns {Promise<Buffer>} - Buffer contendo a imagem do banner
501
+ */
502
+ async build() {
503
+ // --- Registro de Fonte ---
504
+ const registeredFontName = await registerFontIfNeeded(this.font);
505
+
506
+ // --- Configuração do Canvas ---
507
+ const cardWidth = this.cardWidth;
508
+ const cardHeight = this.cardHeight;
509
+ const cornerRadius = this.cornerRadius;
510
+ const padding = 30;
511
+
512
+ const canvas = pureimage.make(cardWidth, cardHeight);
513
+ const ctx = canvas.getContext("2d");
514
+
515
+ // --- Configuração de Cores com base no Esquema ---
516
+ const colors = this._getColorScheme();
517
+
518
+ // --- Desenha Plano de Fundo ---
519
+ if (this.background.type === "image") {
520
+ try {
521
+ ctx.save();
522
+
523
+ if (cornerRadius > 0) {
524
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, false, false);
525
+ ctx.clip();
526
+ }
527
+
528
+ const img = await loadImageWithAxios(this.background.value);
529
+ const aspect = img.width / img.height;
530
+ let drawWidth = cardWidth;
531
+ let drawHeight = cardWidth / aspect;
532
+
533
+ // Ajusta as dimensões para cobrir todo o card
534
+ if (drawHeight < cardHeight) {
535
+ drawHeight = cardHeight;
536
+ drawWidth = cardHeight * aspect;
537
+ }
538
+
539
+ const offsetX = (cardWidth - drawWidth) / 2;
540
+ const offsetY = (cardHeight - drawHeight) / 2;
541
+
542
+ ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
543
+
544
+ // Aplica sobreposição para melhorar legibilidade
545
+ ctx.fillStyle = hexToRgba(colors.background || "#000000", 0.5);
546
+ ctx.fillRect(0, 0, cardWidth, cardHeight);
547
+
548
+ ctx.restore();
549
+ } catch (e) {
550
+ console.error("Falha ao desenhar imagem de plano de fundo:", e.message);
551
+
552
+ // Plano de fundo de fallback
553
+ if (this.useGradient) {
554
+ const gradient = createLinearGradient(
555
+ ctx,
556
+ 0,
557
+ 0,
558
+ cardWidth,
559
+ cardHeight,
560
+ colors.primary,
561
+ colors.secondary,
562
+ "diagonal"
563
+ );
564
+
565
+ ctx.fillStyle = gradient;
566
+ } else {
567
+ ctx.fillStyle = colors.primary;
568
+ }
569
+
570
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
571
+ }
572
+ } else if (this.background.type === "gradient" || this.useGradient) {
573
+ // Plano de fundo com gradiente
574
+ let gradientColors;
575
+
576
+ if (this.background.type === "gradient" && Array.isArray(this.background.value)) {
577
+ gradientColors = this.background.value;
578
+ } else {
579
+ gradientColors = [colors.primary, colors.secondary];
580
+ }
581
+
582
+ const gradient = applyMultiColorGradient(
583
+ ctx,
584
+ 0,
585
+ 0,
586
+ cardWidth,
587
+ cardHeight,
588
+ gradientColors,
589
+ "diagonal"
590
+ );
591
+
592
+ ctx.fillStyle = gradient;
593
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
594
+ } else {
595
+ // Plano de fundo de cor sólida
596
+ ctx.fillStyle = this.background.type === "color" ? this.background.value : colors.primary;
597
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
598
+ }
599
+
600
+ // --- Aplica Padrões Decorativos (se ativados) ---
601
+ if (this.usePatterns) {
602
+ if (this.patternType === "dots") {
603
+ applyDotPattern(
604
+ ctx,
605
+ 0,
606
+ 0,
607
+ cardWidth,
608
+ cardHeight,
609
+ 40,
610
+ 3,
611
+ "#FFFFFF",
612
+ 0.1
613
+ );
614
+ } else if (this.patternType === "lines") {
615
+ applyLinePattern(
616
+ ctx,
617
+ 0,
618
+ 0,
619
+ cardWidth,
620
+ cardHeight,
621
+ 40,
622
+ 1,
623
+ "#FFFFFF",
624
+ 0.1,
625
+ "diagonal"
626
+ );
627
+ }
628
+ }
629
+
630
+ // --- Desenha Conteúdo com base no Estilo ---
631
+ switch (this.style) {
632
+ case "modern":
633
+ await this._drawModernStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
634
+ break;
635
+ case "classic":
636
+ await this._drawClassicStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
637
+ break;
638
+ case "minimal":
639
+ await this._drawMinimalStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
640
+ break;
641
+ case "bold":
642
+ await this._drawBoldStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
643
+ break;
644
+ case "festive":
645
+ await this._drawFestiveStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
646
+ break;
647
+ default:
648
+ await this._drawModernStyle(ctx, registeredFontName, colors, cardWidth, cardHeight, padding);
649
+ }
650
+
651
+ // --- Codifica e Retorna Buffer ---
652
+ try {
653
+ return await encodeToBuffer(canvas);
654
+ } catch (err) {
655
+ console.error("Falha ao codificar o Banner de Evento:", err);
656
+ throw new Error("Não foi possível gerar o buffer de imagem do Banner de Evento.");
657
+ }
658
+ }
659
+
660
+ // --- Métodos Auxiliares Privados ---
661
+ /**
662
+ * Obtém as cores com base no esquema selecionado
663
+ * @private
664
+ */
665
+ _getColorScheme() {
666
+ if (this.colorScheme === "custom" && this.customColors.primary) {
667
+ return this.customColors;
668
+ }
669
+
670
+ switch (this.colorScheme) {
671
+ case "green":
672
+ return {
673
+ primary: "#2ECC71",
674
+ secondary: "#27AE60",
675
+ accent: "#F1C40F",
676
+ text: "#FFFFFF",
677
+ background: "#1E8449"
678
+ };
679
+ case "purple":
680
+ return {
681
+ primary: "#9B59B6",
682
+ secondary: "#8E44AD",
683
+ accent: "#F1C40F",
684
+ text: "#FFFFFF",
685
+ background: "#6C3483"
686
+ };
687
+ case "red":
688
+ return {
689
+ primary: "#E74C3C",
690
+ secondary: "#C0392B",
691
+ accent: "#F1C40F",
692
+ text: "#FFFFFF",
693
+ background: "#922B21"
694
+ };
695
+ case "orange":
696
+ return {
697
+ primary: "#F39C12",
698
+ secondary: "#D35400",
699
+ accent: "#3498DB",
700
+ text: "#FFFFFF",
701
+ background: "#A04000"
702
+ };
703
+ case "blue":
704
+ default:
705
+ return {
706
+ primary: "#3498DB",
707
+ secondary: "#2980B9",
708
+ accent: "#F1C40F",
709
+ text: "#FFFFFF",
710
+ background: "#1F618D"
711
+ };
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Desenha o estilo moderno
717
+ * @private
718
+ */
719
+ async _drawModernStyle(ctx, fontName, colors, width, height, padding) {
720
+ // --- Desenha Área de Conteúdo Principal ---
721
+ const contentWidth = width - padding * 2;
722
+ const contentHeight = height - padding * 2;
723
+ const contentX = padding;
724
+ const contentY = padding;
725
+
726
+ // Aplica efeito de glassmorphism se ativado
727
+ if (this.useGlassmorphism) {
728
+ applyGlassmorphism(
729
+ ctx,
730
+ contentX,
731
+ contentY,
732
+ contentWidth,
733
+ contentHeight,
734
+ this.cornerRadius,
735
+ 0.2,
736
+ "#FFFFFF"
737
+ );
738
+ }
739
+
740
+ // --- Desenha Logo (se fornecido) ---
741
+ let currentY = contentY + padding;
742
+
743
+ if (this.logo) {
744
+ try {
745
+ const logoSize = Math.min(contentWidth, contentHeight) * 0.15;
746
+ const logoX = contentX + padding;
747
+
748
+ const logoImg = await loadImageWithAxios(this.logo);
749
+ const aspect = logoImg.width / logoImg.height;
750
+ const logoHeight = logoSize;
751
+ const logoWidth = logoHeight * aspect;
752
+
753
+ ctx.drawImage(logoImg, logoX, currentY, logoWidth, logoHeight);
754
+ currentY += logoHeight + padding;
755
+ } catch (e) {
756
+ console.error("Falha ao desenhar logo:", e.message);
757
+ }
758
+ }
759
+
760
+ // --- Desenha Título ---
761
+ const titleFontSize = Math.min(contentWidth, contentHeight) * 0.08;
762
+ ctx.fillStyle = colors.text;
763
+ ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
764
+ ctx.textAlign = "center";
765
+ ctx.textBaseline = "top";
766
+
767
+ // Aplica sombra de texto se ativada
768
+ if (this.useTextShadow) {
769
+ applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 3, 1, 1);
770
+ }
771
+
772
+ const titleText = this.title;
773
+ ctx.fillText(titleText, contentX + contentWidth / 2, currentY);
774
+ currentY += titleFontSize * 1.2;
775
+
776
+ // Remove sombra para o próximo texto
777
+ if (this.useTextShadow) {
778
+ clearShadow(ctx);
779
+ }
780
+
781
+ // --- Desenha Data e Hora ---
782
+ if (this.date || this.time) {
783
+ const dateTimeFontSize = titleFontSize * 0.5;
784
+ ctx.fillStyle = colors.text;
785
+ ctx.font = `medium ${dateTimeFontSize}px ${fontName}-Medium`;
786
+ ctx.textAlign = "center";
787
+
788
+ let dateTimeText = "";
789
+ if (this.date && this.time) {
790
+ dateTimeText = `${this.date} • ${this.time}`;
791
+ } else if (this.date) {
792
+ dateTimeText = this.date;
793
+ } else if (this.time) {
794
+ dateTimeText = this.time;
795
+ }
796
+
797
+ ctx.fillText(dateTimeText, contentX + contentWidth / 2, currentY);
798
+ currentY += dateTimeFontSize * 1.5;
799
+ }
800
+
801
+ // --- Desenha Local ---
802
+ if (this.location) {
803
+ const locationFontSize = titleFontSize * 0.4;
804
+ ctx.fillStyle = colors.text;
805
+ ctx.font = `regular ${locationFontSize}px ${fontName}-Regular`;
806
+ ctx.textAlign = "center";
807
+
808
+ ctx.fillText(`📍 ${this.location}`, contentX + contentWidth / 2, currentY);
809
+ currentY += locationFontSize * 1.5;
810
+ }
811
+
812
+ // --- Desenha Descrição ---
813
+ if (this.description) {
814
+ const descriptionFontSize = titleFontSize * 0.35;
815
+ ctx.fillStyle = colors.text;
816
+ ctx.font = `regular ${descriptionFontSize}px ${fontName}-Regular`;
817
+ ctx.textAlign = "center";
818
+
819
+ currentY = wrapText(
820
+ ctx,
821
+ this.description,
822
+ contentX + contentWidth / 2,
823
+ currentY + padding,
824
+ contentWidth - padding * 2,
825
+ descriptionFontSize * 1.5,
826
+ fontName,
827
+ true
828
+ );
829
+ }
830
+
831
+ // --- Desenha Categorias ---
832
+ if (this.categories.length > 0) {
833
+ const categoryFontSize = titleFontSize * 0.3;
834
+ const categoryHeight = categoryFontSize * 1.5;
835
+ const categoryPadding = 10;
836
+ const categorySpacing = 10;
837
+ let categoryX = contentX + padding;
838
+ const categoryY = height - padding * 3 - categoryHeight;
839
+
840
+ ctx.font = `regular ${categoryFontSize}px ${fontName}-Regular`;
841
+ ctx.textAlign = "left";
842
+ ctx.textBaseline = "middle";
843
+
844
+ for (const category of this.categories) {
845
+ const categoryWidth = ctx.measureText(category).width + categoryPadding * 2;
846
+
847
+ if (categoryX + categoryWidth > contentX + contentWidth - padding) {
848
+ break; // Não há mais espaço para categorias
849
+ }
850
+
851
+ // Fundo da categoria
852
+ ctx.fillStyle = hexToRgba(colors.accent, 0.8);
853
+ roundRect(ctx, categoryX, categoryY, categoryWidth, categoryHeight, categoryHeight / 2, true, false);
854
+
855
+ // Texto da categoria
856
+ ctx.fillStyle = colors.text;
857
+ ctx.fillText(category, categoryX + categoryPadding, categoryY + categoryHeight / 2);
858
+
859
+ categoryX += categoryWidth + categorySpacing;
860
+ }
861
+ }
862
+
863
+ // --- Desenha Informações de Registro ---
864
+ if (this.registrationUrl) {
865
+ const registrationFontSize = titleFontSize * 0.35;
866
+ ctx.fillStyle = colors.text;
867
+ ctx.font = `bold ${registrationFontSize}px ${fontName}-Bold`;
868
+ ctx.textAlign = "center";
869
+ ctx.textBaseline = "bottom";
870
+
871
+ ctx.fillText("Registre-se: " + this.registrationUrl, contentX + contentWidth / 2, height - padding * 1.5);
872
+ }
873
+
874
+ // --- Desenha QR Code (se fornecido) ---
875
+ if (this.qrCode) {
876
+ try {
877
+ const qrSize = Math.min(contentWidth, contentHeight) * 0.15;
878
+ const qrX = width - qrSize - padding * 2;
879
+ const qrY = height - qrSize - padding * 2;
880
+
881
+ const qrImg = await loadImageWithAxios(this.qrCode);
882
+ ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize);
883
+ } catch (e) {
884
+ console.error("Falha ao desenhar QR code:", e.message);
885
+ }
886
+ }
887
+ }
888
+
889
+ /**
890
+ * Desenha o estilo clássico
891
+ * @private
892
+ */
893
+ async _drawClassicStyle(ctx, fontName, colors, width, height, padding) {
894
+ // --- Desenha Borda Decorativa ---
895
+ const borderWidth = 5;
896
+ ctx.strokeStyle = colors.accent;
897
+ ctx.lineWidth = borderWidth;
898
+ roundRect(ctx, padding / 2, padding / 2, width - padding, height - padding, this.cornerRadius, false, true);
899
+
900
+ // --- Desenha Título ---
901
+ const titleFontSize = Math.min(width, height) * 0.07;
902
+ const titleY = padding * 2;
903
+
904
+ ctx.fillStyle = colors.text;
905
+ ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
906
+ ctx.textAlign = "center";
907
+ ctx.textBaseline = "top";
908
+
909
+ const titleText = this.title;
910
+ ctx.fillText(titleText, width / 2, titleY);
911
+
912
+ // --- Desenha Linha Separadora ---
913
+ const lineY = titleY + titleFontSize * 1.5;
914
+ const lineWidth = width * 0.6;
915
+ const lineHeight = 2;
916
+
917
+ ctx.fillStyle = colors.accent;
918
+ ctx.fillRect((width - lineWidth) / 2, lineY, lineWidth, lineHeight);
919
+
920
+ // --- Desenha Data e Hora ---
921
+ const dateTimeFontSize = titleFontSize * 0.5;
922
+ const dateTimeY = lineY + lineHeight + padding;
923
+
924
+ ctx.fillStyle = colors.text;
925
+ ctx.font = `medium ${dateTimeFontSize}px ${fontName}-Medium`;
926
+ ctx.textAlign = "center";
927
+
928
+ if (this.date) {
929
+ ctx.fillText(this.date, width / 2, dateTimeY);
930
+ }
931
+
932
+ if (this.time) {
933
+ ctx.fillText(this.time, width / 2, dateTimeY + dateTimeFontSize * 1.5);
934
+ }
935
+
936
+ // --- Desenha Local ---
937
+ const locationFontSize = titleFontSize * 0.4;
938
+ const locationY = dateTimeY + (this.time ? dateTimeFontSize * 3 : dateTimeFontSize * 1.5);
939
+
940
+ if (this.location) {
941
+ ctx.fillStyle = colors.text;
942
+ ctx.font = `regular ${locationFontSize}px ${fontName}-Regular`;
943
+ ctx.textAlign = "center";
944
+
945
+ ctx.fillText(this.location, width / 2, locationY);
946
+ }
947
+
948
+ // --- Desenha Descrição ---
949
+ const descriptionFontSize = titleFontSize * 0.35;
950
+ const descriptionY = locationY + locationFontSize * 2;
951
+
952
+ if (this.description) {
953
+ ctx.fillStyle = colors.text;
954
+ ctx.font = `regular ${descriptionFontSize}px ${fontName}-Regular`;
955
+ ctx.textAlign = "center";
956
+
957
+ wrapText(
958
+ ctx,
959
+ this.description,
960
+ width / 2,
961
+ descriptionY,
962
+ width - padding * 4,
963
+ descriptionFontSize * 1.5,
964
+ fontName,
965
+ true
966
+ );
967
+ }
968
+
969
+ // --- Desenha Organizador ---
970
+ const organizerY = height - padding * 3;
971
+
972
+ if (this.organizer) {
973
+ ctx.fillStyle = colors.text;
974
+ ctx.font = `italic ${descriptionFontSize}px ${fontName}-Regular`;
975
+ ctx.textAlign = "center";
976
+ ctx.textBaseline = "bottom";
977
+
978
+ ctx.fillText(`Organizado por: ${this.organizer}`, width / 2, organizerY);
979
+ }
980
+
981
+ // --- Desenha Informações de Contato ---
982
+ const contactY = height - padding * 1.5;
983
+
984
+ if (this.contactInfo) {
985
+ ctx.fillStyle = colors.text;
986
+ ctx.font = `regular ${descriptionFontSize}px ${fontName}-Regular`;
987
+ ctx.textAlign = "center";
988
+ ctx.textBaseline = "bottom";
989
+
990
+ ctx.fillText(this.contactInfo, width / 2, contactY);
991
+ }
992
+ }
993
+
994
+ /**
995
+ * Desenha o estilo minimalista
996
+ * @private
997
+ */
998
+ async _drawMinimalStyle(ctx, fontName, colors, width, height, padding) {
999
+ // --- Desenha Título ---
1000
+ const titleFontSize = Math.min(width, height) * 0.08;
1001
+ const titleY = height * 0.3;
1002
+
1003
+ ctx.fillStyle = colors.text;
1004
+ ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
1005
+ ctx.textAlign = "center";
1006
+ ctx.textBaseline = "middle";
1007
+
1008
+ const titleText = this.title;
1009
+ ctx.fillText(titleText, width / 2, titleY);
1010
+
1011
+ // --- Desenha Data e Hora ---
1012
+ const dateTimeFontSize = titleFontSize * 0.5;
1013
+ const dateTimeY = titleY + titleFontSize;
1014
+
1015
+ ctx.fillStyle = colors.text;
1016
+ ctx.font = `regular ${dateTimeFontSize}px ${fontName}-Regular`;
1017
+ ctx.textAlign = "center";
1018
+
1019
+ let dateTimeText = "";
1020
+ if (this.date && this.time) {
1021
+ dateTimeText = `${this.date} • ${this.time}`;
1022
+ } else if (this.date) {
1023
+ dateTimeText = this.date;
1024
+ } else if (this.time) {
1025
+ dateTimeText = this.time;
1026
+ }
1027
+
1028
+ if (dateTimeText) {
1029
+ ctx.fillText(dateTimeText, width / 2, dateTimeY);
1030
+ }
1031
+
1032
+ // --- Desenha Local ---
1033
+ const locationFontSize = titleFontSize * 0.4;
1034
+ const locationY = dateTimeY + dateTimeFontSize * 1.5;
1035
+
1036
+ if (this.location) {
1037
+ ctx.fillStyle = colors.text;
1038
+ ctx.font = `regular ${locationFontSize}px ${fontName}-Regular`;
1039
+ ctx.textAlign = "center";
1040
+
1041
+ ctx.fillText(this.location, width / 2, locationY);
1042
+ }
1043
+ }
1044
+
1045
+ /**
1046
+ * Desenha o estilo bold
1047
+ * @private
1048
+ */
1049
+ async _drawBoldStyle(ctx, fontName, colors, width, height, padding) {
1050
+ // --- Desenha Título em Destaque ---
1051
+ const titleFontSize = Math.min(width, height) * 0.12;
1052
+ const titleY = padding * 2;
1053
+
1054
+ ctx.fillStyle = colors.text;
1055
+ ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
1056
+ ctx.textAlign = "center";
1057
+ ctx.textBaseline = "top";
1058
+
1059
+ // Aplica sombra de texto se ativada
1060
+ if (this.useTextShadow) {
1061
+ applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 5, 2, 2);
1062
+ }
1063
+
1064
+ const titleText = this.title;
1065
+ ctx.fillText(titleText, width / 2, titleY);
1066
+
1067
+ // Remove sombra para o próximo texto
1068
+ if (this.useTextShadow) {
1069
+ clearShadow(ctx);
1070
+ }
1071
+
1072
+ // --- Desenha Destaque para Data ---
1073
+ const dateY = titleY + titleFontSize * 1.5;
1074
+ const dateFontSize = titleFontSize * 0.6;
1075
+
1076
+ if (this.date) {
1077
+ // Fundo para a data
1078
+ const dateText = this.date;
1079
+ const dateWidth = ctx.measureText(dateText).width + padding * 2;
1080
+ const dateHeight = dateFontSize * 1.5;
1081
+ const dateX = (width - dateWidth) / 2;
1082
+
1083
+ ctx.fillStyle = colors.accent;
1084
+ roundRect(ctx, dateX, dateY, dateWidth, dateHeight, dateHeight / 2, true, false);
1085
+
1086
+ // Texto da data
1087
+ ctx.fillStyle = "#000000"; // Texto escuro para contraste
1088
+ ctx.font = `bold ${dateFontSize}px ${fontName}-Bold`;
1089
+ ctx.textAlign = "center";
1090
+ ctx.textBaseline = "middle";
1091
+
1092
+ ctx.fillText(dateText, width / 2, dateY + dateHeight / 2);
1093
+ }
1094
+
1095
+ // --- Desenha Hora ---
1096
+ const timeFontSize = dateFontSize * 0.8;
1097
+ const timeY = dateY + dateFontSize * 2;
1098
+
1099
+ if (this.time) {
1100
+ ctx.fillStyle = colors.text;
1101
+ ctx.font = `medium ${timeFontSize}px ${fontName}-Medium`;
1102
+ ctx.textAlign = "center";
1103
+ ctx.textBaseline = "top";
1104
+
1105
+ ctx.fillText(this.time, width / 2, timeY);
1106
+ }
1107
+
1108
+ // --- Desenha Local ---
1109
+ const locationFontSize = timeFontSize;
1110
+ const locationY = timeY + timeFontSize * 1.5;
1111
+
1112
+ if (this.location) {
1113
+ ctx.fillStyle = colors.text;
1114
+ ctx.font = `medium ${locationFontSize}px ${fontName}-Medium`;
1115
+ ctx.textAlign = "center";
1116
+
1117
+ ctx.fillText(`📍 ${this.location}`, width / 2, locationY);
1118
+ }
1119
+
1120
+ // --- Desenha Preço do Ingresso ---
1121
+ const priceY = height - padding * 4;
1122
+ const priceFontSize = titleFontSize * 0.5;
1123
+
1124
+ if (this.ticketPrice) {
1125
+ // Fundo para o preço
1126
+ const priceText = this.ticketPrice;
1127
+ const priceWidth = ctx.measureText(priceText).width + padding * 2;
1128
+ const priceHeight = priceFontSize * 1.5;
1129
+ const priceX = (width - priceWidth) / 2;
1130
+
1131
+ ctx.fillStyle = colors.accent;
1132
+ roundRect(ctx, priceX, priceY, priceWidth, priceHeight, priceHeight / 2, true, false);
1133
+
1134
+ // Texto do preço
1135
+ ctx.fillStyle = "#000000"; // Texto escuro para contraste
1136
+ ctx.font = `bold ${priceFontSize}px ${fontName}-Bold`;
1137
+ ctx.textAlign = "center";
1138
+ ctx.textBaseline = "middle";
1139
+
1140
+ ctx.fillText(priceText, width / 2, priceY + priceHeight / 2);
1141
+ }
1142
+
1143
+ // --- Desenha URL de Registro ---
1144
+ const registrationY = height - padding * 2;
1145
+ const registrationFontSize = priceFontSize * 0.8;
1146
+
1147
+ if (this.registrationUrl) {
1148
+ ctx.fillStyle = colors.text;
1149
+ ctx.font = `regular ${registrationFontSize}px ${fontName}-Regular`;
1150
+ ctx.textAlign = "center";
1151
+ ctx.textBaseline = "bottom";
1152
+
1153
+ ctx.fillText(this.registrationUrl, width / 2, registrationY);
1154
+ }
1155
+ }
1156
+
1157
+ /**
1158
+ * Desenha o estilo festivo
1159
+ * @private
1160
+ */
1161
+ async _drawFestiveStyle(ctx, fontName, colors, width, height, padding) {
1162
+ // --- Desenha Elementos Decorativos ---
1163
+ // Círculos decorativos
1164
+ const circleCount = 20;
1165
+ const maxRadius = Math.min(width, height) * 0.1;
1166
+
1167
+ for (let i = 0; i < circleCount; i++) {
1168
+ const radius = Math.random() * maxRadius + 5;
1169
+ const x = Math.random() * width;
1170
+ const y = Math.random() * height;
1171
+
1172
+ ctx.fillStyle = hexToRgba(colors.accent, Math.random() * 0.3 + 0.1);
1173
+ ctx.beginPath();
1174
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
1175
+ ctx.fill();
1176
+ }
1177
+
1178
+ // --- Desenha Área de Conteúdo Principal ---
1179
+ const contentWidth = width * 0.8;
1180
+ const contentHeight = height * 0.8;
1181
+ const contentX = (width - contentWidth) / 2;
1182
+ const contentY = (height - contentHeight) / 2;
1183
+
1184
+ // Aplica efeito de glassmorphism
1185
+ applyGlassmorphism(
1186
+ ctx,
1187
+ contentX,
1188
+ contentY,
1189
+ contentWidth,
1190
+ contentHeight,
1191
+ this.cornerRadius,
1192
+ 0.3,
1193
+ "#FFFFFF"
1194
+ );
1195
+
1196
+ // --- Desenha Título ---
1197
+ const titleFontSize = Math.min(contentWidth, contentHeight) * 0.1;
1198
+ ctx.fillStyle = colors.text;
1199
+ ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
1200
+ ctx.textAlign = "center";
1201
+ ctx.textBaseline = "top";
1202
+
1203
+ // Aplica sombra de texto
1204
+ applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 3, 1, 1);
1205
+
1206
+ const titleText = this.title;
1207
+ ctx.fillText(titleText, width / 2, contentY + padding);
1208
+
1209
+ // Remove sombra para o próximo texto
1210
+ clearShadow(ctx);
1211
+
1212
+ // --- Desenha Data e Hora ---
1213
+ const dateTimeFontSize = titleFontSize * 0.5;
1214
+ const dateTimeY = contentY + padding + titleFontSize * 1.2;
1215
+
1216
+ ctx.fillStyle = colors.text;
1217
+ ctx.font = `medium ${dateTimeFontSize}px ${fontName}-Medium`;
1218
+ ctx.textAlign = "center";
1219
+
1220
+ if (this.date) {
1221
+ ctx.fillText(this.date, width / 2, dateTimeY);
1222
+ }
1223
+
1224
+ if (this.time) {
1225
+ ctx.fillText(this.time, width / 2, dateTimeY + dateTimeFontSize * 1.5);
1226
+ }
1227
+
1228
+ // --- Desenha Local ---
1229
+ const locationFontSize = dateTimeFontSize * 0.8;
1230
+ const locationY = dateTimeY + (this.time ? dateTimeFontSize * 3 : dateTimeFontSize * 1.5);
1231
+
1232
+ if (this.location) {
1233
+ ctx.fillStyle = colors.text;
1234
+ ctx.font = `regular ${locationFontSize}px ${fontName}-Regular`;
1235
+ ctx.textAlign = "center";
1236
+
1237
+ ctx.fillText(`📍 ${this.location}`, width / 2, locationY);
1238
+ }
1239
+
1240
+ // --- Desenha Descrição ---
1241
+ const descriptionFontSize = locationFontSize * 0.9;
1242
+ const descriptionY = locationY + locationFontSize * 2;
1243
+
1244
+ if (this.description) {
1245
+ ctx.fillStyle = colors.text;
1246
+ ctx.font = `regular ${descriptionFontSize}px ${fontName}-Regular`;
1247
+ ctx.textAlign = "center";
1248
+
1249
+ wrapText(
1250
+ ctx,
1251
+ this.description,
1252
+ width / 2,
1253
+ descriptionY,
1254
+ contentWidth - padding * 2,
1255
+ descriptionFontSize * 1.5,
1256
+ fontName,
1257
+ true
1258
+ );
1259
+ }
1260
+
1261
+ // --- Desenha Informações de Registro ---
1262
+ const registrationY = contentY + contentHeight - padding * 2;
1263
+
1264
+ if (this.registrationUrl) {
1265
+ ctx.fillStyle = colors.text;
1266
+ ctx.font = `bold ${descriptionFontSize}px ${fontName}-Bold`;
1267
+ ctx.textAlign = "center";
1268
+ ctx.textBaseline = "bottom";
1269
+
1270
+ ctx.fillText("Registre-se: " + this.registrationUrl, width / 2, registrationY);
1271
+ }
1272
+
1273
+ // --- Desenha Efeito de Brilho nos Cantos ---
1274
+ applyGlow(
1275
+ ctx,
1276
+ contentX - 10,
1277
+ contentY - 10,
1278
+ 30,
1279
+ 30,
1280
+ 15,
1281
+ colors.accent,
1282
+ 10
1283
+ );
1284
+
1285
+ applyGlow(
1286
+ ctx,
1287
+ contentX + contentWidth - 20,
1288
+ contentY - 10,
1289
+ 30,
1290
+ 30,
1291
+ 15,
1292
+ colors.accent,
1293
+ 10
1294
+ );
1295
+
1296
+ applyGlow(
1297
+ ctx,
1298
+ contentX - 10,
1299
+ contentY + contentHeight - 20,
1300
+ 30,
1301
+ 30,
1302
+ 15,
1303
+ colors.accent,
1304
+ 10
1305
+ );
1306
+
1307
+ applyGlow(
1308
+ ctx,
1309
+ contentX + contentWidth - 20,
1310
+ contentY + contentHeight - 20,
1311
+ 30,
1312
+ 30,
1313
+ 15,
1314
+ colors.accent,
1315
+ 10
1316
+ );
1317
+ }
1318
+ };
1319
+