@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,755 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Módulo de Banner de Perfil Moderno
5
+ *
6
+ * Este módulo gera banners de perfil com design moderno, utilizando elementos
7
+ * visuais contemporâneos como glassmorphism, gradientes e efeitos 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
+ USER_STATUS
36
+ } = require("./constants");
37
+
38
+ const {
39
+ applyGlassmorphism,
40
+ applyNeomorphism,
41
+ applyMultiColorGradient,
42
+ applyGlow
43
+ } = require("./effects");
44
+
45
+ /**
46
+ * @class ModernProfile
47
+ * @classdesc Gera um banner de perfil com design moderno e elementos visuais contemporâneos.
48
+ * @example const profile = new ModernProfile()
49
+ * .setName("Nome Completo")
50
+ * .setTitle("Desenvolvedor Frontend")
51
+ * .setBio("Especialista em UI/UX e desenvolvimento web moderno")
52
+ * .setAvatar("avatar.png")
53
+ * .setBackground("image", "background.jpg")
54
+ * .addStat("Projetos", "125")
55
+ * .addStat("Seguidores", "3.2K")
56
+ * .addStat("Avaliação", "4.9")
57
+ * .setTheme("glassmorphism")
58
+ * .build();
59
+ */
60
+ module.exports = class ModernProfile {
61
+ constructor(options) {
62
+ // Dados Principais
63
+ this.name = "Nome Completo";
64
+ this.title = null;
65
+ this.bio = null;
66
+ this.avatar = null;
67
+ this.background = { type: "color", value: DEFAULT_COLORS.gradient.purple.start };
68
+ this.stats = [];
69
+ this.badges = [];
70
+ this.links = [];
71
+ this.status = "online";
72
+
73
+ // Personalização Visual
74
+ this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path };
75
+ this.theme = "glassmorphism"; // glassmorphism, neomorphism, gradient, minimal
76
+ this.primaryColor = "#FFFFFF";
77
+ this.secondaryColor = "rgba(255, 255, 255, 0.7)";
78
+ this.accentColor = DEFAULT_COLORS.accent.purple;
79
+ this.useTextShadow = true;
80
+ this.useGradientBackground = true;
81
+ this.gradientColors = [DEFAULT_COLORS.gradient.purple.start, DEFAULT_COLORS.gradient.purple.end];
82
+ this.gradientDirection = "diagonal";
83
+
84
+ // Configurações de Layout
85
+ this.cardWidth = DEFAULT_DIMENSIONS.profile.width;
86
+ this.cardHeight = 400;
87
+ this.cornerRadius = LAYOUT.cornerRadius.large;
88
+ this.avatarSize = 120;
89
+ this.avatarBorderWidth = 4;
90
+ }
91
+
92
+ // --- Setters para Dados Principais ---
93
+ /**
94
+ * Define o nome completo
95
+ * @param {string} text - Nome completo
96
+ * @returns {ModernProfile} - Instância atual para encadeamento
97
+ */
98
+ setName(text) {
99
+ if (!text || typeof text !== "string") throw new Error("O nome completo deve ser uma string não vazia.");
100
+ this.name = text;
101
+ return this;
102
+ }
103
+
104
+ /**
105
+ * Define o título/cargo
106
+ * @param {string} text - Título ou cargo
107
+ * @returns {ModernProfile} - Instância atual para encadeamento
108
+ */
109
+ setTitle(text) {
110
+ if (!text || typeof text !== "string") throw new Error("O título deve ser uma string não vazia.");
111
+ this.title = text;
112
+ return this;
113
+ }
114
+
115
+ /**
116
+ * Define a bio
117
+ * @param {string} text - Texto da bio
118
+ * @returns {ModernProfile} - Instância atual para encadeamento
119
+ */
120
+ setBio(text) {
121
+ if (!text || typeof text !== "string") throw new Error("A bio deve ser uma string não vazia.");
122
+ this.bio = text;
123
+ return this;
124
+ }
125
+
126
+ /**
127
+ * Define o avatar
128
+ * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do avatar
129
+ * @returns {ModernProfile} - Instância atual para encadeamento
130
+ */
131
+ setAvatar(image) {
132
+ if (!image) throw new Error("A fonte da imagem do avatar não pode estar vazia.");
133
+ this.avatar = image;
134
+ return this;
135
+ }
136
+
137
+ /**
138
+ * Define o plano de fundo
139
+ * @param {string} type - Tipo de plano de fundo ('color', 'image' ou 'gradient')
140
+ * @param {string|Array} value - Valor do plano de fundo (cor hexadecimal, URL/caminho da imagem ou array de cores para gradiente)
141
+ * @param {string} direction - Direção do gradiente (apenas para tipo 'gradient')
142
+ * @returns {ModernProfile} - Instância atual para encadeamento
143
+ */
144
+ setBackground(type, value, direction = "diagonal") {
145
+ const types = ["color", "image", "gradient"];
146
+ if (!type || !types.includes(type.toLowerCase())) {
147
+ throw new Error("O tipo de plano de fundo deve ser 'color', 'image' ou 'gradient'.");
148
+ }
149
+
150
+ if (!value) throw new Error("O valor do plano de fundo não pode estar vazio.");
151
+
152
+ if (type.toLowerCase() === "color" && !isValidHexColor(value)) {
153
+ throw new Error("Cor de plano de fundo inválida. Use o formato hexadecimal.");
154
+ }
155
+
156
+ if (type.toLowerCase() === "gradient") {
157
+ if (!Array.isArray(value) || value.length < 2) {
158
+ throw new Error("Para gradiente, forneça um array com pelo menos duas cores hexadecimais.");
159
+ }
160
+
161
+ for (const color of value) {
162
+ if (!isValidHexColor(color)) {
163
+ throw new Error("Todas as cores do gradiente devem estar no formato hexadecimal.");
164
+ }
165
+ }
166
+
167
+ this.useGradientBackground = true;
168
+ this.gradientColors = value;
169
+
170
+ const validDirections = ["horizontal", "vertical", "diagonal", "radial"];
171
+ if (direction && validDirections.includes(direction.toLowerCase())) {
172
+ this.gradientDirection = direction.toLowerCase();
173
+ }
174
+
175
+ this.background = { type: "gradient", value };
176
+ } else {
177
+ this.useGradientBackground = false;
178
+ this.background = { type: type.toLowerCase(), value };
179
+ }
180
+
181
+ return this;
182
+ }
183
+
184
+ /**
185
+ * Adiciona uma estatística ao perfil
186
+ * @param {string} label - Rótulo da estatística
187
+ * @param {string|number} value - Valor da estatística
188
+ * @returns {ModernProfile} - Instância atual para encadeamento
189
+ */
190
+ addStat(label, value) {
191
+ if (!label || typeof label !== "string") throw new Error("O rótulo da estatística deve ser uma string não vazia.");
192
+ if (value === undefined || value === null) throw new Error("O valor da estatística não pode estar vazio.");
193
+
194
+ this.stats.push({ label, value: value.toString() });
195
+ return this;
196
+ }
197
+
198
+ /**
199
+ * Adiciona um distintivo ao perfil
200
+ * @param {Object} badge - Objeto do distintivo com url e descrição
201
+ * @returns {ModernProfile} - Instância atual para encadeamento
202
+ */
203
+ addBadge(badge) {
204
+ if (!badge || typeof badge !== "object" || !badge.url) {
205
+ throw new Error("O distintivo deve ser um objeto com pelo menos uma propriedade \"url\".");
206
+ }
207
+
208
+ this.badges.push({ url: badge.url, description: badge.description || "Distintivo" });
209
+ return this;
210
+ }
211
+
212
+ /**
213
+ * Adiciona um link ao perfil
214
+ * @param {string} label - Rótulo do link
215
+ * @param {string} url - URL do link
216
+ * @param {string} icon - Ícone do link (opcional)
217
+ * @returns {ModernProfile} - Instância atual para encadeamento
218
+ */
219
+ addLink(label, url, icon = null) {
220
+ if (!label || typeof label !== "string") throw new Error("O rótulo do link deve ser uma string não vazia.");
221
+ if (!url || typeof url !== "string") throw new Error("A URL do link deve ser uma string não vazia.");
222
+
223
+ this.links.push({ label, url, icon });
224
+ return this;
225
+ }
226
+
227
+ /**
228
+ * Define o status do usuário
229
+ * @param {string} status - Status do usuário ('online', 'idle', 'dnd', 'streaming', 'offline')
230
+ * @returns {ModernProfile} - Instância atual para encadeamento
231
+ */
232
+ setStatus(status) {
233
+ if (!status || !USER_STATUS[status.toLowerCase()]) {
234
+ throw new Error(`Status inválido. Use um dos seguintes: ${Object.keys(USER_STATUS).join(", ")}`);
235
+ }
236
+
237
+ this.status = status.toLowerCase();
238
+ return this;
239
+ }
240
+
241
+ // --- Setters para Personalização Visual ---
242
+ /**
243
+ * Define o tema
244
+ * @param {string} theme - Tema ('glassmorphism', 'neomorphism', 'gradient', 'minimal')
245
+ * @returns {ModernProfile} - Instância atual para encadeamento
246
+ */
247
+ setTheme(theme) {
248
+ const validThemes = ["glassmorphism", "neomorphism", "gradient", "minimal"];
249
+ if (!theme || !validThemes.includes(theme.toLowerCase())) {
250
+ throw new Error(`Tema inválido. Use um dos seguintes: ${validThemes.join(", ")}`);
251
+ }
252
+
253
+ this.theme = theme.toLowerCase();
254
+ return this;
255
+ }
256
+
257
+ /**
258
+ * Define a cor primária
259
+ * @param {string} color - Cor hexadecimal
260
+ * @returns {ModernProfile} - Instância atual para encadeamento
261
+ */
262
+ setPrimaryColor(color) {
263
+ if (!color || !isValidHexColor(color)) throw new Error("Cor primária inválida. Use o formato hexadecimal.");
264
+ this.primaryColor = color;
265
+ return this;
266
+ }
267
+
268
+ /**
269
+ * Define a cor secundária
270
+ * @param {string} color - Cor hexadecimal ou rgba
271
+ * @returns {ModernProfile} - Instância atual para encadeamento
272
+ */
273
+ setSecondaryColor(color) {
274
+ this.secondaryColor = color;
275
+ return this;
276
+ }
277
+
278
+ /**
279
+ * Define a cor de destaque
280
+ * @param {string} color - Cor hexadecimal
281
+ * @returns {ModernProfile} - Instância atual para encadeamento
282
+ */
283
+ setAccentColor(color) {
284
+ if (!color || !isValidHexColor(color)) throw new Error("Cor de destaque inválida. Use o formato hexadecimal.");
285
+ this.accentColor = color;
286
+ return this;
287
+ }
288
+
289
+ /**
290
+ * Ativa ou desativa a sombra de texto
291
+ * @param {boolean} enabled - Se a sombra de texto deve ser ativada
292
+ * @returns {ModernProfile} - Instância atual para encadeamento
293
+ */
294
+ enableTextShadow(enabled = true) {
295
+ this.useTextShadow = enabled;
296
+ return this;
297
+ }
298
+
299
+ /**
300
+ * Define as dimensões do card
301
+ * @param {number} width - Largura do card em pixels
302
+ * @param {number} height - Altura do card em pixels
303
+ * @returns {ModernProfile} - Instância atual para encadeamento
304
+ */
305
+ setCardDimensions(width, height) {
306
+ if (typeof width !== "number" || width < 400 || width > 1200) {
307
+ throw new Error("A largura do card deve estar entre 400 e 1200 pixels.");
308
+ }
309
+
310
+ if (typeof height !== "number" || height < 300 || height > 800) {
311
+ throw new Error("A altura do card deve estar entre 300 e 800 pixels.");
312
+ }
313
+
314
+ this.cardWidth = width;
315
+ this.cardHeight = height;
316
+
317
+ return this;
318
+ }
319
+
320
+ /**
321
+ * Define o raio dos cantos arredondados
322
+ * @param {number} radius - Raio dos cantos em pixels
323
+ * @returns {ModernProfile} - Instância atual para encadeamento
324
+ */
325
+ setCornerRadius(radius) {
326
+ if (typeof radius !== "number" || radius < 0) throw new Error("O raio dos cantos deve ser um número não negativo.");
327
+ this.cornerRadius = radius;
328
+ return this;
329
+ }
330
+
331
+ /**
332
+ * Define o tamanho do avatar
333
+ * @param {number} size - Tamanho do avatar em pixels
334
+ * @returns {ModernProfile} - Instância atual para encadeamento
335
+ */
336
+ setAvatarSize(size) {
337
+ if (typeof size !== "number" || size < 80 || size > 200) {
338
+ throw new Error("O tamanho do avatar deve estar entre 80 e 200 pixels.");
339
+ }
340
+
341
+ this.avatarSize = size;
342
+ return this;
343
+ }
344
+
345
+ /**
346
+ * Define a largura da borda do avatar
347
+ * @param {number} width - Largura da borda em pixels
348
+ * @returns {ModernProfile} - Instância atual para encadeamento
349
+ */
350
+ setAvatarBorderWidth(width) {
351
+ if (typeof width !== "number" || width < 0) throw new Error("A largura da borda do avatar deve ser um número não negativo.");
352
+ this.avatarBorderWidth = width;
353
+ return this;
354
+ }
355
+
356
+ // --- Método de Construção ---
357
+ /**
358
+ * Constrói o banner e retorna um buffer de imagem
359
+ * @returns {Promise<Buffer>} - Buffer contendo a imagem do banner
360
+ */
361
+ async build() {
362
+ // --- Registro de Fonte ---
363
+ const registeredFontName = await registerFontIfNeeded(this.font);
364
+
365
+ // --- Configuração do Canvas ---
366
+ const cardWidth = this.cardWidth;
367
+ const cardHeight = this.cardHeight;
368
+ const cornerRadius = this.cornerRadius;
369
+ const avatarSize = this.avatarSize;
370
+ const padding = 25;
371
+
372
+ const canvas = pureimage.make(cardWidth, cardHeight);
373
+ const ctx = canvas.getContext("2d");
374
+
375
+ // --- Desenha Plano de Fundo ---
376
+ if (this.background.type === "gradient" || this.useGradientBackground) {
377
+ // Plano de fundo com gradiente
378
+ const gradient = applyMultiColorGradient(
379
+ ctx,
380
+ 0,
381
+ 0,
382
+ cardWidth,
383
+ cardHeight,
384
+ this.gradientColors,
385
+ this.gradientDirection
386
+ );
387
+
388
+ ctx.fillStyle = gradient;
389
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
390
+ } else if (this.background.type === "color") {
391
+ // Plano de fundo de cor sólida
392
+ ctx.fillStyle = this.background.value;
393
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
394
+ } else {
395
+ // Plano de fundo de imagem
396
+ try {
397
+ ctx.save();
398
+
399
+ if (cornerRadius > 0) {
400
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, false, false);
401
+ ctx.clip();
402
+ }
403
+
404
+ const img = await loadImageWithAxios(this.background.value);
405
+ const aspect = img.width / img.height;
406
+ let drawWidth = cardWidth;
407
+ let drawHeight = cardWidth / aspect;
408
+
409
+ // Ajusta as dimensões para cobrir todo o card
410
+ if (drawHeight < cardHeight) {
411
+ drawHeight = cardHeight;
412
+ drawWidth = cardHeight * aspect;
413
+ }
414
+
415
+ const offsetX = (cardWidth - drawWidth) / 2;
416
+ const offsetY = (cardHeight - drawHeight) / 2;
417
+
418
+ ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
419
+
420
+ // Aplica sobreposição para melhorar legibilidade
421
+ ctx.fillStyle = "rgba(0, 0, 0, 0.3)";
422
+ ctx.fillRect(0, 0, cardWidth, cardHeight);
423
+
424
+ ctx.restore();
425
+ } catch (e) {
426
+ console.error("Falha ao desenhar imagem de plano de fundo:", e.message);
427
+
428
+ // Fallback para gradiente
429
+ const gradient = createLinearGradient(
430
+ ctx,
431
+ 0,
432
+ 0,
433
+ cardWidth,
434
+ cardHeight,
435
+ DEFAULT_COLORS.gradient.purple.start,
436
+ DEFAULT_COLORS.gradient.purple.end,
437
+ "diagonal"
438
+ );
439
+
440
+ ctx.fillStyle = gradient;
441
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
442
+ }
443
+ }
444
+
445
+ // --- Aplica Efeitos com base no Tema ---
446
+ switch (this.theme) {
447
+ case "glassmorphism":
448
+ // Não aplica efeito no fundo, apenas nos elementos
449
+ break;
450
+ case "neomorphism":
451
+ // Redefine o fundo para uma cor sólida clara
452
+ ctx.fillStyle = "#E0E0E0";
453
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
454
+
455
+ // Aplica efeito de neomorfismo no card
456
+ applyNeomorphism(
457
+ ctx,
458
+ padding / 2,
459
+ padding / 2,
460
+ cardWidth - padding,
461
+ cardHeight - padding,
462
+ cornerRadius - padding / 4,
463
+ "#E0E0E0",
464
+ false
465
+ );
466
+ break;
467
+ case "minimal":
468
+ // Fundo branco simples
469
+ ctx.fillStyle = "#FFFFFF";
470
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
471
+
472
+ // Borda sutil
473
+ ctx.strokeStyle = "#EEEEEE";
474
+ ctx.lineWidth = 1;
475
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, false, true);
476
+ break;
477
+ }
478
+
479
+ // --- Desenha Avatar ---
480
+ const avatarX = padding;
481
+ const avatarY = padding;
482
+
483
+ // Efeito de brilho no avatar (apenas para temas específicos)
484
+ if (this.theme === "gradient" || this.theme === "glassmorphism") {
485
+ applyGlow(
486
+ ctx,
487
+ avatarX - 5,
488
+ avatarY - 5,
489
+ avatarSize + 10,
490
+ avatarSize + 10,
491
+ avatarSize / 2 + 5,
492
+ this.accentColor,
493
+ 15
494
+ );
495
+ }
496
+
497
+ ctx.save();
498
+ ctx.beginPath();
499
+ ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
500
+ ctx.closePath();
501
+ ctx.clip();
502
+
503
+ try {
504
+ const avatarImg = await loadImageWithAxios(this.avatar || path.join(__dirname, "../assets/placeholders/avatar.png"));
505
+ ctx.drawImage(avatarImg, avatarX, avatarY, avatarSize, avatarSize);
506
+ } catch (e) {
507
+ console.error("Falha ao desenhar avatar:", e.message);
508
+
509
+ // Avatar de fallback
510
+ ctx.fillStyle = this.accentColor;
511
+ ctx.fillRect(avatarX, avatarY, avatarSize, avatarSize);
512
+
513
+ ctx.fillStyle = "#FFFFFF";
514
+ ctx.font = `bold ${avatarSize / 3}px ${registeredFontName}-Bold`;
515
+ ctx.textAlign = "center";
516
+ ctx.textBaseline = "middle";
517
+ ctx.fillText(this.name.charAt(0).toUpperCase(), avatarX + avatarSize / 2, avatarY + avatarSize / 2);
518
+ }
519
+
520
+ ctx.restore();
521
+
522
+ // Borda do avatar
523
+ ctx.strokeStyle = this.theme === "neomorphism" ? "#E0E0E0" : this.primaryColor;
524
+ ctx.lineWidth = this.avatarBorderWidth;
525
+ ctx.beginPath();
526
+ ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 + ctx.lineWidth / 2, 0, Math.PI * 2);
527
+ ctx.stroke();
528
+ ctx.closePath();
529
+
530
+ // --- Desenha Indicador de Status ---
531
+ const statusColor = USER_STATUS[this.status]?.color || USER_STATUS.offline.color;
532
+ const statusSize = 24;
533
+ const statusX = avatarX + avatarSize - statusSize * 0.7;
534
+ const statusY = avatarY + avatarSize - statusSize * 0.7;
535
+
536
+ ctx.fillStyle = statusColor;
537
+ ctx.beginPath();
538
+ ctx.arc(statusX, statusY, statusSize / 2, 0, Math.PI * 2);
539
+ ctx.fill();
540
+ ctx.closePath();
541
+
542
+ ctx.strokeStyle = this.theme === "neomorphism" ? "#E0E0E0" : this.primaryColor;
543
+ ctx.lineWidth = 2;
544
+ ctx.beginPath();
545
+ ctx.arc(statusX, statusY, statusSize / 2, 0, Math.PI * 2);
546
+ ctx.stroke();
547
+ ctx.closePath();
548
+
549
+ // --- Desenha Informações Principais ---
550
+ const infoX = avatarX + avatarSize + padding;
551
+ let infoY = avatarY + 10;
552
+ const infoWidth = cardWidth - infoX - padding;
553
+
554
+ // Aplica efeito de glassmorphism para a área de informações (apenas para tema glassmorphism)
555
+ if (this.theme === "glassmorphism") {
556
+ applyGlassmorphism(
557
+ ctx,
558
+ infoX - padding / 2,
559
+ infoY - padding / 2,
560
+ infoWidth + padding,
561
+ avatarSize + padding,
562
+ 10,
563
+ 0.2,
564
+ "#FFFFFF"
565
+ );
566
+ }
567
+
568
+ // Nome
569
+ ctx.fillStyle = this.theme === "neomorphism" || this.theme === "minimal" ? "#333333" : this.primaryColor;
570
+ ctx.font = `bold 24px ${registeredFontName}-Bold`;
571
+ ctx.textAlign = "left";
572
+ ctx.textBaseline = "top";
573
+
574
+ // Aplica sombra de texto se ativada
575
+ if (this.useTextShadow && this.theme !== "neomorphism" && this.theme !== "minimal") {
576
+ applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 3, 1, 1);
577
+ }
578
+
579
+ ctx.fillText(this.name, infoX, infoY);
580
+ infoY += 30;
581
+
582
+ // Remove sombra para o próximo texto
583
+ if (this.useTextShadow && this.theme !== "neomorphism" && this.theme !== "minimal") {
584
+ clearShadow(ctx);
585
+ }
586
+
587
+ // Título
588
+ if (this.title) {
589
+ ctx.fillStyle = this.theme === "neomorphism" || this.theme === "minimal" ? "#666666" : this.secondaryColor;
590
+ ctx.font = `medium 16px ${registeredFontName}-Medium`;
591
+ ctx.textAlign = "left";
592
+
593
+ ctx.fillText(this.title, infoX, infoY);
594
+ infoY += 25;
595
+ }
596
+
597
+ // --- Desenha Bio ---
598
+ const bioY = avatarY + avatarSize + padding;
599
+
600
+ if (this.bio) {
601
+ // Aplica efeito de glassmorphism para a área da bio (apenas para tema glassmorphism)
602
+ if (this.theme === "glassmorphism") {
603
+ applyGlassmorphism(
604
+ ctx,
605
+ padding,
606
+ bioY - padding / 2,
607
+ cardWidth - padding * 2,
608
+ cardHeight / 3,
609
+ 10,
610
+ 0.2,
611
+ "#FFFFFF"
612
+ );
613
+ }
614
+
615
+ ctx.fillStyle = this.theme === "neomorphism" || this.theme === "minimal" ? "#333333" : this.primaryColor;
616
+ ctx.font = `regular 16px ${registeredFontName}-Regular`;
617
+ ctx.textAlign = "left";
618
+ ctx.textBaseline = "top";
619
+
620
+ // Aplica sombra de texto se ativada
621
+ if (this.useTextShadow && this.theme !== "neomorphism" && this.theme !== "minimal") {
622
+ applyTextShadow(ctx, "rgba(0, 0, 0, 0.3)", 2, 1, 1);
623
+ }
624
+
625
+ wrapText(ctx, this.bio, padding, bioY, cardWidth - padding * 2, 20, registeredFontName);
626
+
627
+ // Remove sombra
628
+ if (this.useTextShadow && this.theme !== "neomorphism" && this.theme !== "minimal") {
629
+ clearShadow(ctx);
630
+ }
631
+ }
632
+
633
+ // --- Desenha Estatísticas ---
634
+ if (this.stats.length > 0) {
635
+ const statsY = bioY + (this.bio ? cardHeight / 3 + padding : padding);
636
+ const statWidth = (cardWidth - padding * 2) / Math.min(this.stats.length, 3);
637
+
638
+ // Aplica efeito de glassmorphism para a área de estatísticas (apenas para tema glassmorphism)
639
+ if (this.theme === "glassmorphism") {
640
+ applyGlassmorphism(
641
+ ctx,
642
+ padding,
643
+ statsY - padding / 2,
644
+ cardWidth - padding * 2,
645
+ 80,
646
+ 10,
647
+ 0.2,
648
+ "#FFFFFF"
649
+ );
650
+ }
651
+
652
+ this.stats.slice(0, 3).forEach((stat, index) => {
653
+ const statX = padding + statWidth * index + statWidth / 2;
654
+
655
+ // Valor da estatística
656
+ ctx.fillStyle = this.theme === "neomorphism" || this.theme === "minimal" ? "#333333" : this.primaryColor;
657
+ ctx.font = `bold 24px ${registeredFontName}-Bold`;
658
+ ctx.textAlign = "center";
659
+ ctx.textBaseline = "top";
660
+
661
+ // Aplica sombra de texto se ativada
662
+ if (this.useTextShadow && this.theme !== "neomorphism" && this.theme !== "minimal") {
663
+ applyTextShadow(ctx, "rgba(0, 0, 0, 0.3)", 2, 1, 1);
664
+ }
665
+
666
+ ctx.fillText(stat.value, statX, statsY);
667
+
668
+ // Remove sombra para o próximo texto
669
+ if (this.useTextShadow && this.theme !== "neomorphism" && this.theme !== "minimal") {
670
+ clearShadow(ctx);
671
+ }
672
+
673
+ // Rótulo da estatística
674
+ ctx.fillStyle = this.theme === "neomorphism" || this.theme === "minimal" ? "#666666" : this.secondaryColor;
675
+ ctx.font = `regular 14px ${registeredFontName}-Regular`;
676
+
677
+ ctx.fillText(stat.label, statX, statsY + 30);
678
+ });
679
+ }
680
+
681
+ // --- Desenha Distintivos ---
682
+ if (this.badges.length > 0) {
683
+ const badgesY = cardHeight - 60;
684
+ const badgeSize = 40;
685
+ const badgeSpacing = 10;
686
+ let currentBadgeX = padding;
687
+
688
+ // Aplica efeito de glassmorphism para a área de distintivos (apenas para tema glassmorphism)
689
+ if (this.theme === "glassmorphism") {
690
+ applyGlassmorphism(
691
+ ctx,
692
+ padding,
693
+ badgesY - padding / 2,
694
+ cardWidth - padding * 2,
695
+ 60,
696
+ 10,
697
+ 0.2,
698
+ "#FFFFFF"
699
+ );
700
+ }
701
+
702
+ for (const badge of this.badges.slice(0, 5)) {
703
+ try {
704
+ const badgeImg = await loadImageWithAxios(badge.url);
705
+ ctx.drawImage(badgeImg, currentBadgeX, badgesY, badgeSize, badgeSize);
706
+ currentBadgeX += badgeSize + badgeSpacing;
707
+ } catch (e) {
708
+ console.warn(`Falha ao carregar imagem do distintivo: ${badge.url}`, e.message);
709
+
710
+ // Distintivo de fallback
711
+ ctx.fillStyle = this.accentColor;
712
+ ctx.beginPath();
713
+ ctx.arc(currentBadgeX + badgeSize / 2, badgesY + badgeSize / 2, badgeSize / 2, 0, Math.PI * 2);
714
+ ctx.fill();
715
+
716
+ ctx.fillStyle = "#FFFFFF";
717
+ ctx.font = `bold 16px ${registeredFontName}-Bold`;
718
+ ctx.textAlign = "center";
719
+ ctx.textBaseline = "middle";
720
+ ctx.fillText("?", currentBadgeX + badgeSize / 2, badgesY + badgeSize / 2);
721
+
722
+ currentBadgeX += badgeSize + badgeSpacing;
723
+ }
724
+ }
725
+ }
726
+
727
+ // --- Desenha Links ---
728
+ if (this.links.length > 0) {
729
+ const linksY = cardHeight - 30;
730
+ let currentLinkX = padding;
731
+
732
+ ctx.fillStyle = this.theme === "neomorphism" || this.theme === "minimal" ? this.accentColor : this.primaryColor;
733
+ ctx.font = `medium 14px ${registeredFontName}-Medium`;
734
+ ctx.textAlign = "left";
735
+ ctx.textBaseline = "bottom";
736
+
737
+ for (const link of this.links.slice(0, 3)) {
738
+ const linkText = link.icon ? `${link.icon} ${link.label}` : link.label;
739
+ ctx.fillText(linkText, currentLinkX, linksY);
740
+
741
+ const linkWidth = ctx.measureText(linkText).width;
742
+ currentLinkX += linkWidth + 20;
743
+ }
744
+ }
745
+
746
+ // --- Codifica e Retorna Buffer ---
747
+ try {
748
+ return await encodeToBuffer(canvas);
749
+ } catch (err) {
750
+ console.error("Falha ao codificar o Perfil Moderno:", err);
751
+ throw new Error("Não foi possível gerar o buffer de imagem do Perfil Moderno.");
752
+ }
753
+ }
754
+ };
755
+