@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,584 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Módulo de Banner de Perfil do Discord
5
+ *
6
+ * Este módulo gera banners de perfil no estilo do Discord com suporte a banner de fundo,
7
+ * avatar, status, distintivos e informações personalizadas.
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
+ /**
32
+ * @class DiscordProfile
33
+ * @classdesc Gera um banner de perfil no estilo do Discord com suporte completo a banner de fundo.
34
+ * @example const profileCard = new DiscordProfile()
35
+ * .setUsername("UsuárioDiscord")
36
+ * .setDiscriminator("1234")
37
+ * .setAvatar("avatar.png")
38
+ * .setBanner("banner.png")
39
+ * .setAboutMe("Desenvolvedor | Gamer | Entusiasta de IA")
40
+ * .addBadge({ url: "badge1.png", description: "Nitro" })
41
+ * .setStatus("online")
42
+ * .build();
43
+ */
44
+ module.exports = class DiscordProfile {
45
+ constructor(options) {
46
+ // Dados Principais
47
+ this.username = "Usuário";
48
+ this.discriminator = "0000";
49
+ this.avatar = null;
50
+ this.banner = null;
51
+ this.aboutMe = null;
52
+ this.badges = [];
53
+ this.customFields = {};
54
+ this.memberSince = null;
55
+ this.serverMemberSince = null;
56
+
57
+ // Personalização Visual
58
+ this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path };
59
+ this.backgroundColor = "#36393f";
60
+ this.secondaryColor = "#2f3136";
61
+ this.accentColor = "#5865f2";
62
+ this.textColor = "#ffffff";
63
+ this.secondaryTextColor = "#b9bbbe";
64
+ this.badgeBackgroundColor = "#2f3136";
65
+ this.avatarBorderColor = null;
66
+ this.overlayOpacity = 0.4;
67
+ this.status = { type: "offline", color: "#747F8D" };
68
+
69
+ // Configurações de Layout
70
+ this.cornerRadius = 0;
71
+ this.avatarSize = 128;
72
+ this.avatarBorderWidth = 5;
73
+ this.bannerHeight = 360;
74
+ this.cardWidth = 1200;
75
+ }
76
+
77
+ // --- Setters para Dados Principais ---
78
+ /**
79
+ * Define o nome de usuário
80
+ * @param {string} name - Nome do usuário
81
+ * @returns {DiscordProfile} - Instância atual para encadeamento
82
+ */
83
+ setUsername(name) {
84
+ if (!name || typeof name !== "string") throw new Error("O nome de usuário deve ser uma string não vazia.");
85
+ this.username = name;
86
+ return this;
87
+ }
88
+
89
+ /**
90
+ * Define o discriminador (tag) do usuário
91
+ * @param {string} discrim - Discriminador (ex: "1234")
92
+ * @returns {DiscordProfile} - Instância atual para encadeamento
93
+ */
94
+ setDiscriminator(discrim) {
95
+ if (!discrim || typeof discrim !== "string") throw new Error("O discriminador deve ser uma string não vazia.");
96
+ this.discriminator = discrim;
97
+ return this;
98
+ }
99
+
100
+ /**
101
+ * Define a imagem do avatar
102
+ * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do avatar
103
+ * @returns {DiscordProfile} - Instância atual para encadeamento
104
+ */
105
+ setAvatar(image) {
106
+ if (!image) throw new Error("A fonte da imagem do avatar não pode estar vazia.");
107
+ this.avatar = image;
108
+ return this;
109
+ }
110
+
111
+ /**
112
+ * Define a imagem do banner de fundo
113
+ * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do banner
114
+ * @returns {DiscordProfile} - Instância atual para encadeamento
115
+ */
116
+ setBanner(image) {
117
+ if (!image) throw new Error("A fonte da imagem do banner não pode estar vazia.");
118
+ this.banner = image;
119
+ return this;
120
+ }
121
+
122
+ /**
123
+ * Define o texto "Sobre mim"
124
+ * @param {string} text - Texto de descrição
125
+ * @returns {DiscordProfile} - Instância atual para encadeamento
126
+ */
127
+ setAboutMe(text) {
128
+ if (text && typeof text !== "string") throw new Error("O texto 'Sobre mim' deve ser uma string se fornecido.");
129
+ this.aboutMe = text;
130
+ return this;
131
+ }
132
+
133
+ /**
134
+ * Adiciona um distintivo ao perfil
135
+ * @param {Object} badge - Objeto do distintivo com url e descrição
136
+ * @returns {DiscordProfile} - Instância atual para encadeamento
137
+ */
138
+ addBadge(badge) {
139
+ if (!badge || typeof badge !== "object" || !badge.url) throw new Error("O distintivo deve ser um objeto com pelo menos uma propriedade \"url\".");
140
+ this.badges.push({ url: badge.url, description: badge.description || "Distintivo" });
141
+ return this;
142
+ }
143
+
144
+ /**
145
+ * Define um campo personalizado
146
+ * @param {string} title - Título do campo
147
+ * @param {string} value - Valor do campo
148
+ * @returns {DiscordProfile} - Instância atual para encadeamento
149
+ */
150
+ setCustomField(title, value) {
151
+ if (!title || typeof title !== "string") throw new Error("O título do campo personalizado deve ser uma string não vazia.");
152
+ if (!value || typeof value !== "string") throw new Error("O valor do campo personalizado deve ser uma string não vazia.");
153
+ this.customFields[title] = value;
154
+ return this;
155
+ }
156
+
157
+ /**
158
+ * Define a data de entrada no Discord
159
+ * @param {string} date - Data de entrada (ex: "25 Mai 2020")
160
+ * @returns {DiscordProfile} - Instância atual para encadeamento
161
+ */
162
+ setMemberSince(date) {
163
+ if (!date || typeof date !== "string") throw new Error("A data de entrada deve ser uma string não vazia.");
164
+ this.memberSince = date;
165
+ return this;
166
+ }
167
+
168
+ /**
169
+ * Define a data de entrada no servidor
170
+ * @param {string} date - Data de entrada no servidor (ex: "10 Jun 2021")
171
+ * @returns {DiscordProfile} - Instância atual para encadeamento
172
+ */
173
+ setServerMemberSince(date) {
174
+ if (!date || typeof date !== "string") throw new Error("A data de entrada no servidor deve ser uma string não vazia.");
175
+ this.serverMemberSince = date;
176
+ return this;
177
+ }
178
+
179
+ // --- Setters para Personalização Visual ---
180
+ /**
181
+ * Define a cor de fundo principal
182
+ * @param {string} color - Cor hexadecimal
183
+ * @returns {DiscordProfile} - Instância atual para encadeamento
184
+ */
185
+ setBackgroundColor(color) {
186
+ if (!color || !isValidHexColor(color)) throw new Error("Cor de fundo inválida. Use o formato hexadecimal.");
187
+ this.backgroundColor = color;
188
+ return this;
189
+ }
190
+
191
+ /**
192
+ * Define a cor secundária
193
+ * @param {string} color - Cor hexadecimal
194
+ * @returns {DiscordProfile} - Instância atual para encadeamento
195
+ */
196
+ setSecondaryColor(color) {
197
+ if (!color || !isValidHexColor(color)) throw new Error("Cor secundária inválida. Use o formato hexadecimal.");
198
+ this.secondaryColor = color;
199
+ return this;
200
+ }
201
+
202
+ /**
203
+ * Define a cor de destaque
204
+ * @param {string} color - Cor hexadecimal
205
+ * @returns {DiscordProfile} - Instância atual para encadeamento
206
+ */
207
+ setAccentColor(color) {
208
+ if (!color || !isValidHexColor(color)) throw new Error("Cor de destaque inválida. Use o formato hexadecimal.");
209
+ this.accentColor = color;
210
+ return this;
211
+ }
212
+
213
+ /**
214
+ * Define a cor do texto principal
215
+ * @param {string} color - Cor hexadecimal
216
+ * @returns {DiscordProfile} - Instância atual para encadeamento
217
+ */
218
+ setTextColor(color) {
219
+ if (!color || !isValidHexColor(color)) throw new Error("Cor de texto inválida. Use o formato hexadecimal.");
220
+ this.textColor = color;
221
+ return this;
222
+ }
223
+
224
+ /**
225
+ * Define a cor do texto secundário
226
+ * @param {string} color - Cor hexadecimal
227
+ * @returns {DiscordProfile} - Instância atual para encadeamento
228
+ */
229
+ setSecondaryTextColor(color) {
230
+ if (!color || !isValidHexColor(color)) throw new Error("Cor de texto secundário inválida. Use o formato hexadecimal.");
231
+ this.secondaryTextColor = color;
232
+ return this;
233
+ }
234
+
235
+ /**
236
+ * Define a cor da borda do avatar
237
+ * @param {string} color - Cor hexadecimal
238
+ * @returns {DiscordProfile} - Instância atual para encadeamento
239
+ */
240
+ setAvatarBorderColor(color) {
241
+ if (color && !isValidHexColor(color)) throw new Error("Cor de borda do avatar inválida. Use o formato hexadecimal.");
242
+ this.avatarBorderColor = color;
243
+ return this;
244
+ }
245
+
246
+ /**
247
+ * Define a opacidade da sobreposição
248
+ * @param {number} opacity - Valor de opacidade (0-1)
249
+ * @returns {DiscordProfile} - Instância atual para encadeamento
250
+ */
251
+ setOverlayOpacity(opacity) {
252
+ if (typeof opacity !== "number" || opacity < 0 || opacity > 1) throw new Error("A opacidade da sobreposição deve estar entre 0 e 1.");
253
+ this.overlayOpacity = opacity;
254
+ return this;
255
+ }
256
+
257
+ /**
258
+ * Define o status do usuário
259
+ * @param {string} type - Tipo de status ('online', 'idle', 'dnd', 'streaming', 'offline')
260
+ * @returns {DiscordProfile} - Instância atual para encadeamento
261
+ */
262
+ setStatus(type) {
263
+ const validTypes = { online: "#43B581", idle: "#FAA61A", dnd: "#F04747", streaming: "#593695", offline: "#747F8D" };
264
+ if (!type || !validTypes[type.toLowerCase()]) throw new Error(`Tipo de status inválido. Use um dos seguintes: ${Object.keys(validTypes).join(", ")}`);
265
+ this.status = { type: type.toLowerCase(), color: validTypes[type.toLowerCase()] };
266
+ return this;
267
+ }
268
+
269
+ /**
270
+ * Define o raio dos cantos arredondados
271
+ * @param {number} radius - Raio dos cantos em pixels
272
+ * @returns {DiscordProfile} - Instância atual para encadeamento
273
+ */
274
+ setCornerRadius(radius) {
275
+ if (typeof radius !== "number" || radius < 0) throw new Error("O raio dos cantos deve ser um número não negativo.");
276
+ this.cornerRadius = radius;
277
+ return this;
278
+ }
279
+
280
+ /**
281
+ * Define o tamanho do avatar
282
+ * @param {number} size - Tamanho do avatar em pixels
283
+ * @returns {DiscordProfile} - Instância atual para encadeamento
284
+ */
285
+ setAvatarSize(size) {
286
+ if (typeof size !== "number" || size < 64 || size > 256) throw new Error("O tamanho do avatar deve estar entre 64 e 256 pixels.");
287
+ this.avatarSize = size;
288
+ return this;
289
+ }
290
+
291
+ /**
292
+ * Define a largura da borda do avatar
293
+ * @param {number} width - Largura da borda em pixels
294
+ * @returns {DiscordProfile} - Instância atual para encadeamento
295
+ */
296
+ setAvatarBorderWidth(width) {
297
+ if (typeof width !== "number" || width < 0) throw new Error("A largura da borda do avatar deve ser um número não negativo.");
298
+ this.avatarBorderWidth = width;
299
+ return this;
300
+ }
301
+
302
+ /**
303
+ * Define a altura do banner
304
+ * @param {number} height - Altura do banner em pixels
305
+ * @returns {DiscordProfile} - Instância atual para encadeamento
306
+ */
307
+ setBannerHeight(height) {
308
+ if (typeof height !== "number" || height < 100 || height > 300) throw new Error("A altura do banner deve estar entre 100 e 300 pixels.");
309
+ this.bannerHeight = height;
310
+ return this;
311
+ }
312
+
313
+ /**
314
+ * Define a largura do card
315
+ * @param {number} width - Largura do card em pixels
316
+ * @returns {DiscordProfile} - Instância atual para encadeamento
317
+ */
318
+ setCardWidth(width) {
319
+ if (typeof width !== "number" || width < 400 || width > 1000) throw new Error("A largura do card deve estar entre 400 e 1000 pixels.");
320
+ this.cardWidth = width;
321
+ return this;
322
+ }
323
+
324
+ // --- Método de Construção ---
325
+ /**
326
+ * Constrói o banner de perfil e retorna um buffer de imagem
327
+ * @returns {Promise<Buffer>} - Buffer contendo a imagem do banner
328
+ */
329
+ async build() {
330
+ if (!this.avatar) throw new Error("A imagem do avatar deve ser definida usando setAvatar().");
331
+
332
+ // --- Registro de Fonte ---
333
+ const registeredFontName = await registerFontIfNeeded(this.font);
334
+
335
+ // --- Configuração do Canvas ---
336
+ const cardWidth = this.cardWidth;
337
+ const headerHeight = this.bannerHeight;
338
+ const avatarSize = this.avatarSize;
339
+ const avatarOverlap = 40;
340
+ const bodyPadding = 25;
341
+ const contentStartY = headerHeight - avatarOverlap + avatarSize / 2 + bodyPadding;
342
+ const badgeAreaHeight = this.badges.length > 0 ? 80 : 0;
343
+
344
+ // Estima a altura necessária para o conteúdo
345
+ let estimatedContentHeight = 0;
346
+ if (this.aboutMe) estimatedContentHeight += 80;
347
+ estimatedContentHeight += Object.keys(this.customFields).length * 45;
348
+ if (this.memberSince) estimatedContentHeight += 30;
349
+ if (this.serverMemberSince) estimatedContentHeight += 30;
350
+
351
+ let cardHeight = contentStartY + Math.max(100, estimatedContentHeight) + badgeAreaHeight + bodyPadding;
352
+
353
+ const borderRadius = this.cornerRadius;
354
+ const statusIndicatorSize = 32;
355
+
356
+ const canvas = pureimage.make(cardWidth, cardHeight);
357
+ const ctx = canvas.getContext("2d");
358
+
359
+ // --- Desenha Plano de Fundo Principal ---
360
+ ctx.fillStyle = this.backgroundColor;
361
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, borderRadius, true, false);
362
+
363
+ // --- Desenha Banner de Fundo ---
364
+ ctx.save();
365
+ // Define o caminho de recorte para o cabeçalho (apenas cantos superiores arredondados)
366
+ ctx.beginPath();
367
+ ctx.moveTo(0, headerHeight); // Inicia no canto inferior esquerdo
368
+ ctx.lineTo(0, borderRadius); // Borda esquerda até o raio
369
+ ctx.quadraticCurveTo(0, 0, borderRadius, 0); // Canto superior esquerdo
370
+ ctx.lineTo(cardWidth - borderRadius, 0); // Borda superior
371
+ ctx.quadraticCurveTo(cardWidth, 0, cardWidth, borderRadius); // Canto superior direito
372
+ ctx.lineTo(cardWidth, headerHeight); // Borda direita para baixo
373
+ ctx.closePath(); // Fecha o caminho de volta para o canto inferior direito (implicitamente)
374
+ ctx.clip();
375
+
376
+ ctx.globalAlpha = 1;
377
+
378
+ if (this.banner) {
379
+ try {
380
+ const img = await loadImageWithAxios(this.banner);
381
+ const aspect = img.width / img.height;
382
+ let drawWidth = cardWidth;
383
+ let drawHeight = cardWidth / aspect;
384
+
385
+ if (drawHeight < headerHeight) {
386
+ drawHeight = headerHeight;
387
+ drawWidth = headerHeight * aspect;
388
+ }
389
+
390
+ const offsetX = (cardWidth - drawWidth) / 2;
391
+ const offsetY = 0;
392
+
393
+ ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
394
+
395
+ } catch (e) {
396
+ console.error("Falha ao desenhar imagem de banner:", e.message);
397
+ ctx.fillStyle = this.accentColor;
398
+ ctx.fillRect(0, 0, cardWidth, headerHeight);
399
+ }
400
+ } else {
401
+ // Banner de cor sólida se nenhuma imagem for fornecida
402
+ ctx.fillStyle = this.accentColor;
403
+ ctx.fillRect(0, 0, cardWidth, headerHeight);
404
+ }
405
+
406
+ ctx.restore();
407
+ ctx.globalAlpha = 1;
408
+
409
+ // --- Desenha Avatar ---
410
+ const avatarX = bodyPadding;
411
+ const avatarY = headerHeight - avatarOverlap - avatarSize / 2;
412
+
413
+ ctx.save();
414
+ ctx.beginPath();
415
+ ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
416
+ ctx.closePath();
417
+ ctx.clip();
418
+
419
+ try {
420
+ const avatarImg = await loadImageWithAxios(this.avatar);
421
+ ctx.drawImage(avatarImg, avatarX, avatarY, avatarSize, avatarSize);
422
+ } catch (e) {
423
+ console.error("Falha ao desenhar imagem do avatar:", e.message);
424
+ ctx.fillStyle = "#555";
425
+ ctx.fillRect(avatarX, avatarY, avatarSize, avatarSize);
426
+ ctx.fillStyle = "#FFF";
427
+ ctx.font = `bold 30px ${registeredFontName}-Bold`;
428
+ ctx.textAlign = "center";
429
+ ctx.textBaseline = "middle";
430
+ ctx.fillText("?", avatarX + avatarSize / 2, avatarY + avatarSize / 2);
431
+ }
432
+
433
+ ctx.restore();
434
+
435
+ // --- Desenha Borda do Avatar ---
436
+ if (this.avatarBorderColor) {
437
+ ctx.strokeStyle = this.avatarBorderColor;
438
+ } else {
439
+ ctx.strokeStyle = this.backgroundColor; // Usa a cor de fundo como padrão
440
+ }
441
+
442
+ ctx.lineWidth = this.avatarBorderWidth;
443
+ ctx.beginPath();
444
+ ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 + ctx.lineWidth / 2, 0, Math.PI * 2);
445
+ ctx.stroke();
446
+ ctx.closePath();
447
+
448
+ // --- Desenha Indicador de Status ---
449
+ ctx.fillStyle = this.status.color;
450
+ ctx.beginPath();
451
+ const statusX = avatarX + avatarSize - statusIndicatorSize * 0.7;
452
+ const statusY = avatarY + avatarSize - statusIndicatorSize * 0.7;
453
+ ctx.arc(statusX, statusY, statusIndicatorSize / 2, 0, Math.PI * 2);
454
+ ctx.fill();
455
+ ctx.closePath();
456
+ ctx.strokeStyle = this.backgroundColor;
457
+ ctx.lineWidth = 3;
458
+ ctx.stroke();
459
+
460
+ // --- Desenha Nome de Usuário e Discriminador ---
461
+ ctx.fillStyle = this.textColor;
462
+ const usernameFont = `28px ${registeredFontName}-Bold`;
463
+ ctx.font = usernameFont;
464
+ ctx.textAlign = "start";
465
+ ctx.textBaseline = "top";
466
+ const usernameX = avatarX + avatarSize + bodyPadding;
467
+ const usernameY = headerHeight + 5;
468
+
469
+ // Aplica sombra de texto para melhor legibilidade
470
+ applyTextShadow(ctx);
471
+
472
+ const usernameText = this.username.length > 20 ? this.username.slice(0, 17) + "..." : this.username;
473
+ ctx.fillText(usernameText, usernameX, usernameY);
474
+
475
+ // Desenha o discriminador
476
+ ctx.fillStyle = this.secondaryTextColor;
477
+ const discrimX = usernameX + ctx.measureText(usernameText).width + 5;
478
+ ctx.font = `20px ${registeredFontName}-Regular`;
479
+ ctx.fillText(`#${this.discriminator}`, discrimX, usernameY + 5);
480
+
481
+ clearShadow(ctx);
482
+
483
+ // --- Desenha Conteúdo Abaixo do Avatar ---
484
+ let currentY = contentStartY;
485
+ const contentX = bodyPadding;
486
+ const contentWidth = cardWidth - 2 * bodyPadding;
487
+
488
+ // Seção "Sobre mim"
489
+ if (this.aboutMe) {
490
+ // Título da seção
491
+ ctx.fillStyle = this.textColor;
492
+ ctx.font = `18px ${registeredFontName}-Bold`;
493
+ ctx.textAlign = "start";
494
+ ctx.textBaseline = "top";
495
+ ctx.fillText("SOBRE MIM", contentX, currentY);
496
+ currentY += 25;
497
+
498
+ // Conteúdo da seção
499
+ ctx.fillStyle = this.secondaryTextColor;
500
+ ctx.font = `16px ${registeredFontName}-Regular`;
501
+ currentY = wrapText(ctx, this.aboutMe, contentX, currentY, contentWidth, 22, registeredFontName) + 15;
502
+ }
503
+
504
+ // Campos Personalizados
505
+ if (Object.keys(this.customFields).length > 0) {
506
+ currentY += 10;
507
+ ctx.textBaseline = "top";
508
+
509
+ for (const title in this.customFields) {
510
+ // Título do campo
511
+ ctx.fillStyle = this.textColor;
512
+ ctx.font = `16px ${registeredFontName}-Bold`;
513
+ ctx.fillText(title.toUpperCase(), contentX, currentY);
514
+ currentY += 22;
515
+
516
+ // Valor do campo
517
+ ctx.fillStyle = this.secondaryTextColor;
518
+ ctx.font = `16px ${registeredFontName}-Regular`;
519
+ const valueText = this.customFields[title];
520
+ currentY = wrapText(ctx, valueText, contentX, currentY, contentWidth, 20, registeredFontName) + 15;
521
+ }
522
+ }
523
+
524
+ // Informações de Membro
525
+ if (this.memberSince || this.serverMemberSince) {
526
+ currentY += 10;
527
+ ctx.fillStyle = this.textColor;
528
+ ctx.font = `16px ${registeredFontName}-Bold`;
529
+ ctx.fillText("MEMBRO DESDE", contentX, currentY);
530
+ currentY += 22;
531
+
532
+ if (this.memberSince) {
533
+ ctx.fillStyle = this.secondaryTextColor;
534
+ ctx.font = `16px ${registeredFontName}-Regular`;
535
+ ctx.fillText(`Discord: ${this.memberSince}`, contentX, currentY);
536
+ currentY += 22;
537
+ }
538
+
539
+ if (this.serverMemberSince) {
540
+ ctx.fillStyle = this.secondaryTextColor;
541
+ ctx.font = `16px ${registeredFontName}-Regular`;
542
+ ctx.fillText(`Servidor: ${this.serverMemberSince}`, contentX, currentY);
543
+ currentY += 22;
544
+ }
545
+ }
546
+
547
+ // --- Desenha Área de Distintivos ---
548
+ if (this.badges.length > 0) {
549
+ const badgeAreaY = cardHeight - badgeAreaHeight;
550
+ ctx.fillStyle = this.secondaryColor;
551
+ roundRect(ctx, 0, badgeAreaY, cardWidth, badgeAreaHeight,
552
+ { tl: 0, tr: 0, br: borderRadius, bl: borderRadius }, true, false);
553
+
554
+ const badgeSize = 40;
555
+ const badgePadding = 15;
556
+ let currentBadgeX = bodyPadding;
557
+ const badgeY = badgeAreaY + (badgeAreaHeight - badgeSize) / 2;
558
+
559
+ // Desenha os distintivos
560
+ currentBadgeX = bodyPadding;
561
+ for (const badge of this.badges.slice(0, 10)) {
562
+ try {
563
+ const badgeImg = await loadImageWithAxios(badge.url);
564
+ ctx.drawImage(badgeImg, currentBadgeX, badgeY, badgeSize, badgeSize);
565
+ currentBadgeX += badgeSize + badgePadding;
566
+ } catch (e) {
567
+ console.warn(`Falha ao carregar imagem do distintivo: ${badge.url}`, e.message);
568
+ ctx.fillStyle = "#555";
569
+ ctx.fillRect(currentBadgeX, badgeY, badgeSize, badgeSize);
570
+ currentBadgeX += badgeSize + badgePadding;
571
+ }
572
+ }
573
+ }
574
+
575
+ // --- Codifica e Retorna Buffer ---
576
+ try {
577
+ return await encodeToBuffer(canvas);
578
+ } catch (err) {
579
+ console.error("Falha ao codificar o card de Perfil do Discord:", err);
580
+ throw new Error("Não foi possível gerar o buffer de imagem do card de Perfil do Discord.");
581
+ }
582
+ }
583
+ };
584
+