@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,713 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Módulo de Banner de Post do TikTok
5
+ *
6
+ * Este módulo gera banners no estilo de posts do TikTok com
7
+ * elementos visuais característicos da plataforma.
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
+ formatNumber
30
+ } = require("../utils");
31
+
32
+ const {
33
+ DEFAULT_COLORS,
34
+ LAYOUT,
35
+ DEFAULT_DIMENSIONS
36
+ } = require("./constants");
37
+
38
+ const {
39
+ applyGlassmorphism,
40
+ applyMultiColorGradient,
41
+ applyGlow
42
+ } = require("./effects");
43
+
44
+ /**
45
+ * @class TikTokPost
46
+ * @classdesc Gera um banner no estilo de post do TikTok.
47
+ * @example const post = new TikTokPost()
48
+ * .setUsername("usuario_tiktok")
49
+ * .setCaption("Texto da legenda com #hashtags")
50
+ * .setImage("imagem.jpg")
51
+ * .setLikes(15000)
52
+ * .setComments(500)
53
+ * .setShares(200)
54
+ * .setMusicInfo("Nome da música - Artista")
55
+ * .build();
56
+ */
57
+ module.exports = class TikTokPost {
58
+ constructor(options) {
59
+ // Dados Principais
60
+ this.username = "usuario_tiktok";
61
+ this.userAvatar = null;
62
+ this.verified = false;
63
+ this.caption = "Texto da legenda com #hashtags";
64
+ this.image = null;
65
+ this.likes = 0;
66
+ this.comments = 0;
67
+ this.shares = 0;
68
+ this.bookmarks = 0;
69
+ this.musicInfo = null;
70
+ this.duration = "00:30";
71
+ this.hashtags = [];
72
+ this.effect = null;
73
+
74
+ // Personalização Visual
75
+ this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path };
76
+ this.theme = "dark"; // dark, light
77
+ this.style = "standard"; // standard, duet, stitch
78
+ this.useGlassmorphism = false;
79
+ this.useTextShadow = true;
80
+ this.useGradientOverlay = true;
81
+
82
+ // Configurações de Layout
83
+ this.cardWidth = DEFAULT_DIMENSIONS.story.width;
84
+ this.cardHeight = DEFAULT_DIMENSIONS.story.height;
85
+ this.cornerRadius = 0;
86
+ }
87
+
88
+ // --- Setters para Dados Principais ---
89
+ /**
90
+ * Define o nome de usuário
91
+ * @param {string} text - Nome de usuário (sem @)
92
+ * @returns {TikTokPost} - Instância atual para encadeamento
93
+ */
94
+ setUsername(text) {
95
+ if (!text || typeof text !== "string") throw new Error("O nome de usuário deve ser uma string não vazia.");
96
+ this.username = text.replace(/^@/, ''); // Remove @ se presente
97
+ return this;
98
+ }
99
+
100
+ /**
101
+ * Define o avatar do usuário
102
+ * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do avatar
103
+ * @returns {TikTokPost} - Instância atual para encadeamento
104
+ */
105
+ setUserAvatar(image) {
106
+ if (!image) throw new Error("A fonte da imagem do avatar não pode estar vazia.");
107
+ this.userAvatar = image;
108
+ return this;
109
+ }
110
+
111
+ /**
112
+ * Define se o usuário é verificado
113
+ * @param {boolean} isVerified - Se o usuário é verificado
114
+ * @returns {TikTokPost} - Instância atual para encadeamento
115
+ */
116
+ setVerified(isVerified = true) {
117
+ this.verified = !!isVerified;
118
+ return this;
119
+ }
120
+
121
+ /**
122
+ * Define a legenda do post
123
+ * @param {string} text - Texto da legenda
124
+ * @returns {TikTokPost} - Instância atual para encadeamento
125
+ */
126
+ setCaption(text) {
127
+ if (!text || typeof text !== "string") throw new Error("A legenda deve ser uma string não vazia.");
128
+ this.caption = text;
129
+
130
+ // Extrai hashtags automaticamente
131
+ const hashtagRegex = /#(\w+)/g;
132
+ const matches = text.match(hashtagRegex);
133
+
134
+ if (matches) {
135
+ this.hashtags = matches.map(tag => tag.substring(1)); // Remove o # do início
136
+ }
137
+
138
+ return this;
139
+ }
140
+
141
+ /**
142
+ * Define a imagem principal do post
143
+ * @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem
144
+ * @returns {TikTokPost} - Instância atual para encadeamento
145
+ */
146
+ setImage(image) {
147
+ if (!image) throw new Error("A fonte da imagem não pode estar vazia.");
148
+ this.image = image;
149
+ return this;
150
+ }
151
+
152
+ /**
153
+ * Define o número de curtidas
154
+ * @param {number} count - Número de curtidas
155
+ * @returns {TikTokPost} - Instância atual para encadeamento
156
+ */
157
+ setLikes(count) {
158
+ if (typeof count !== "number" || count < 0) throw new Error("O número de curtidas deve ser um número não negativo.");
159
+ this.likes = count;
160
+ return this;
161
+ }
162
+
163
+ /**
164
+ * Define o número de comentários
165
+ * @param {number} count - Número de comentários
166
+ * @returns {TikTokPost} - Instância atual para encadeamento
167
+ */
168
+ setComments(count) {
169
+ if (typeof count !== "number" || count < 0) throw new Error("O número de comentários deve ser um número não negativo.");
170
+ this.comments = count;
171
+ return this;
172
+ }
173
+
174
+ /**
175
+ * Define o número de compartilhamentos
176
+ * @param {number} count - Número de compartilhamentos
177
+ * @returns {TikTokPost} - Instância atual para encadeamento
178
+ */
179
+ setShares(count) {
180
+ if (typeof count !== "number" || count < 0) throw new Error("O número de compartilhamentos deve ser um número não negativo.");
181
+ this.shares = count;
182
+ return this;
183
+ }
184
+
185
+ /**
186
+ * Define o número de salvamentos
187
+ * @param {number} count - Número de salvamentos
188
+ * @returns {TikTokPost} - Instância atual para encadeamento
189
+ */
190
+ setBookmarks(count) {
191
+ if (typeof count !== "number" || count < 0) throw new Error("O número de salvamentos deve ser um número não negativo.");
192
+ this.bookmarks = count;
193
+ return this;
194
+ }
195
+
196
+ /**
197
+ * Define as informações da música
198
+ * @param {string} text - Informações da música (ex: "Nome da música - Artista")
199
+ * @returns {TikTokPost} - Instância atual para encadeamento
200
+ */
201
+ setMusicInfo(text) {
202
+ if (!text || typeof text !== "string") throw new Error("As informações da música devem ser uma string não vazia.");
203
+ this.musicInfo = text;
204
+ return this;
205
+ }
206
+
207
+ /**
208
+ * Define a duração do vídeo
209
+ * @param {string} text - Duração do vídeo (ex: "00:30")
210
+ * @returns {TikTokPost} - Instância atual para encadeamento
211
+ */
212
+ setDuration(text) {
213
+ if (!text || typeof text !== "string") throw new Error("A duração deve ser uma string não vazia.");
214
+ this.duration = text;
215
+ return this;
216
+ }
217
+
218
+ /**
219
+ * Define as hashtags do post
220
+ * @param {Array<string>} tags - Array de hashtags (sem o #)
221
+ * @returns {TikTokPost} - Instância atual para encadeamento
222
+ */
223
+ setHashtags(tags) {
224
+ if (!Array.isArray(tags)) throw new Error("As hashtags devem ser um array de strings.");
225
+ this.hashtags = tags;
226
+ return this;
227
+ }
228
+
229
+ /**
230
+ * Define o efeito usado no post
231
+ * @param {string} text - Nome do efeito
232
+ * @returns {TikTokPost} - Instância atual para encadeamento
233
+ */
234
+ setEffect(text) {
235
+ if (!text || typeof text !== "string") throw new Error("O efeito deve ser uma string não vazia.");
236
+ this.effect = text;
237
+ return this;
238
+ }
239
+
240
+ // --- Setters para Personalização Visual ---
241
+ /**
242
+ * Define o tema
243
+ * @param {string} theme - Tema ('dark', 'light')
244
+ * @returns {TikTokPost} - Instância atual para encadeamento
245
+ */
246
+ setTheme(theme) {
247
+ const validThemes = ["dark", "light"];
248
+ if (!theme || !validThemes.includes(theme.toLowerCase())) {
249
+ throw new Error(`Tema inválido. Use um dos seguintes: ${validThemes.join(", ")}`);
250
+ }
251
+
252
+ this.theme = theme.toLowerCase();
253
+ return this;
254
+ }
255
+
256
+ /**
257
+ * Define o estilo
258
+ * @param {string} style - Estilo ('standard', 'duet', 'stitch')
259
+ * @returns {TikTokPost} - Instância atual para encadeamento
260
+ */
261
+ setStyle(style) {
262
+ const validStyles = ["standard", "duet", "stitch"];
263
+ if (!style || !validStyles.includes(style.toLowerCase())) {
264
+ throw new Error(`Estilo inválido. Use um dos seguintes: ${validStyles.join(", ")}`);
265
+ }
266
+
267
+ this.style = style.toLowerCase();
268
+ return this;
269
+ }
270
+
271
+ /**
272
+ * Ativa ou desativa o efeito de glassmorphism
273
+ * @param {boolean} enabled - Se o efeito deve ser ativado
274
+ * @returns {TikTokPost} - Instância atual para encadeamento
275
+ */
276
+ enableGlassmorphism(enabled = true) {
277
+ this.useGlassmorphism = enabled;
278
+ return this;
279
+ }
280
+
281
+ /**
282
+ * Ativa ou desativa a sombra de texto
283
+ * @param {boolean} enabled - Se a sombra de texto deve ser ativada
284
+ * @returns {TikTokPost} - Instância atual para encadeamento
285
+ */
286
+ enableTextShadow(enabled = true) {
287
+ this.useTextShadow = enabled;
288
+ return this;
289
+ }
290
+
291
+ /**
292
+ * Ativa ou desativa o overlay de gradiente
293
+ * @param {boolean} enabled - Se o overlay de gradiente deve ser ativado
294
+ * @returns {TikTokPost} - Instância atual para encadeamento
295
+ */
296
+ enableGradientOverlay(enabled = true) {
297
+ this.useGradientOverlay = enabled;
298
+ return this;
299
+ }
300
+
301
+ /**
302
+ * Define as dimensões do card
303
+ * @param {number} width - Largura do card em pixels
304
+ * @param {number} height - Altura do card em pixels
305
+ * @returns {TikTokPost} - Instância atual para encadeamento
306
+ */
307
+ setCardDimensions(width, height) {
308
+ if (typeof width !== "number" || width < 400 || width > 1080) {
309
+ throw new Error("A largura do card deve estar entre 400 e 1080 pixels.");
310
+ }
311
+
312
+ if (typeof height !== "number" || height < 600 || height > 1920) {
313
+ throw new Error("A altura do card deve estar entre 600 e 1920 pixels.");
314
+ }
315
+
316
+ this.cardWidth = width;
317
+ this.cardHeight = height;
318
+
319
+ return this;
320
+ }
321
+
322
+ /**
323
+ * Define o raio dos cantos arredondados
324
+ * @param {number} radius - Raio dos cantos em pixels
325
+ * @returns {TikTokPost} - Instância atual para encadeamento
326
+ */
327
+ setCornerRadius(radius) {
328
+ if (typeof radius !== "number" || radius < 0) throw new Error("O raio dos cantos deve ser um número não negativo.");
329
+ this.cornerRadius = radius;
330
+ return this;
331
+ }
332
+
333
+ // --- Método de Construção ---
334
+ /**
335
+ * Constrói o banner e retorna um buffer de imagem
336
+ * @returns {Promise<Buffer>} - Buffer contendo a imagem do banner
337
+ */
338
+ async build() {
339
+ // --- Registro de Fonte ---
340
+ const registeredFontName = await registerFontIfNeeded(this.font);
341
+
342
+ // --- Configuração do Canvas ---
343
+ const cardWidth = this.cardWidth;
344
+ const cardHeight = this.cardHeight;
345
+ const cornerRadius = this.cornerRadius;
346
+ const padding = 20;
347
+
348
+ const canvas = pureimage.make(cardWidth, cardHeight);
349
+ const ctx = canvas.getContext("2d");
350
+
351
+ // --- Configuração de Cores com base no Tema ---
352
+ const colors = this._getThemeColors();
353
+
354
+ // --- Desenha Plano de Fundo ---
355
+ ctx.fillStyle = colors.background;
356
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
357
+
358
+ // --- Desenha Imagem Principal ---
359
+ try {
360
+ ctx.save();
361
+
362
+ if (cornerRadius > 0) {
363
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, false, false);
364
+ ctx.clip();
365
+ }
366
+
367
+ const img = await loadImageWithAxios(this.image || path.join(__dirname, "../assets/placeholders/banner.png"));
368
+ const aspect = img.width / img.height;
369
+ let drawWidth, drawHeight;
370
+
371
+ // Ajusta as dimensões com base no estilo
372
+ if (this.style === "duet") {
373
+ // Modo dueto (imagem ocupa metade da largura)
374
+ drawWidth = cardWidth / 2;
375
+ drawHeight = cardHeight;
376
+
377
+ if (drawWidth / drawHeight > aspect) {
378
+ drawWidth = drawHeight * aspect;
379
+ } else {
380
+ drawHeight = drawWidth / aspect;
381
+ }
382
+
383
+ const offsetX = 0;
384
+ const offsetY = (cardHeight - drawHeight) / 2;
385
+
386
+ ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
387
+
388
+ // Desenha área para o segundo vídeo
389
+ ctx.fillStyle = colors.secondaryBackground;
390
+ ctx.fillRect(cardWidth / 2, 0, cardWidth / 2, cardHeight);
391
+ } else if (this.style === "stitch") {
392
+ // Modo stitch (imagem ocupa parte superior)
393
+ drawWidth = cardWidth;
394
+ drawHeight = cardHeight * 0.4;
395
+
396
+ if (drawWidth / drawHeight > aspect) {
397
+ drawWidth = drawHeight * aspect;
398
+ } else {
399
+ drawHeight = drawWidth / aspect;
400
+ }
401
+
402
+ const offsetX = (cardWidth - drawWidth) / 2;
403
+ const offsetY = 0;
404
+
405
+ ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
406
+
407
+ // Desenha área para o segundo vídeo
408
+ ctx.fillStyle = colors.secondaryBackground;
409
+ ctx.fillRect(0, cardHeight * 0.4, cardWidth, cardHeight * 0.6);
410
+ } else {
411
+ // Modo padrão (imagem ocupa tela inteira)
412
+ drawWidth = cardWidth;
413
+ drawHeight = cardHeight;
414
+
415
+ if (drawWidth / drawHeight > aspect) {
416
+ drawWidth = drawHeight * aspect;
417
+ } else {
418
+ drawHeight = drawWidth / aspect;
419
+ }
420
+
421
+ const offsetX = (cardWidth - drawWidth) / 2;
422
+ const offsetY = (cardHeight - drawHeight) / 2;
423
+
424
+ ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
425
+ }
426
+
427
+ ctx.restore();
428
+ } catch (e) {
429
+ console.error("Falha ao desenhar imagem principal:", e.message);
430
+
431
+ // Plano de fundo de fallback
432
+ ctx.fillStyle = colors.background;
433
+ roundRect(ctx, 0, 0, cardWidth, cardHeight, cornerRadius, true, false);
434
+ }
435
+
436
+ // --- Aplica Overlay de Gradiente ---
437
+ if (this.useGradientOverlay) {
438
+ const gradient = createLinearGradient(
439
+ ctx,
440
+ 0,
441
+ cardHeight * 0.7,
442
+ 0,
443
+ cardHeight,
444
+ "rgba(0, 0, 0, 0)",
445
+ "rgba(0, 0, 0, 0.7)",
446
+ "vertical"
447
+ );
448
+
449
+ ctx.fillStyle = gradient;
450
+ ctx.fillRect(0, cardHeight * 0.7, cardWidth, cardHeight * 0.3);
451
+ }
452
+
453
+ // --- Desenha Interface do TikTok ---
454
+
455
+ // --- Barra Superior ---
456
+ const topBarHeight = 50;
457
+
458
+ // Aplica efeito de glassmorphism se ativado
459
+ if (this.useGlassmorphism) {
460
+ applyGlassmorphism(
461
+ ctx,
462
+ 0,
463
+ 0,
464
+ cardWidth,
465
+ topBarHeight,
466
+ 0,
467
+ 0.3,
468
+ "#000000"
469
+ );
470
+ }
471
+
472
+ // Ícones da barra superior
473
+ ctx.fillStyle = colors.text;
474
+ ctx.font = `bold 16px ${registeredFontName}-Bold`;
475
+ ctx.textAlign = "left";
476
+ ctx.textBaseline = "middle";
477
+
478
+ // Ícone de transmissão ao vivo (se aplicável)
479
+ if (this.style === "standard") {
480
+ ctx.fillText("LIVE", padding, topBarHeight / 2);
481
+ }
482
+
483
+ // Duração do vídeo
484
+ ctx.textAlign = "right";
485
+ ctx.fillText(this.duration, cardWidth - padding, topBarHeight / 2);
486
+
487
+ // --- Barra Lateral (Ações) ---
488
+ const sideBarWidth = 80;
489
+ const sideBarX = cardWidth - sideBarWidth;
490
+ const sideBarY = cardHeight * 0.3;
491
+
492
+ // Avatar do usuário
493
+ const avatarSize = 50;
494
+ const avatarX = sideBarX + (sideBarWidth - avatarSize) / 2;
495
+ const avatarY = sideBarY;
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.userAvatar || 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 = "#FF0050";
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.username.charAt(0).toUpperCase(), avatarX + avatarSize / 2, avatarY + avatarSize / 2);
518
+ }
519
+
520
+ ctx.restore();
521
+
522
+ // Botão de seguir
523
+ const followButtonSize = 20;
524
+ const followButtonX = avatarX + (avatarSize - followButtonSize) / 2;
525
+ const followButtonY = avatarY + avatarSize + 5;
526
+
527
+ ctx.fillStyle = "#FF0050";
528
+ ctx.beginPath();
529
+ ctx.arc(followButtonX + followButtonSize / 2, followButtonY + followButtonSize / 2, followButtonSize / 2, 0, Math.PI * 2);
530
+ ctx.fill();
531
+
532
+ ctx.fillStyle = "#FFFFFF";
533
+ ctx.font = `bold ${followButtonSize * 0.7}px ${registeredFontName}-Bold`;
534
+ ctx.textAlign = "center";
535
+ ctx.textBaseline = "middle";
536
+ ctx.fillText("+", followButtonX + followButtonSize / 2, followButtonY + followButtonSize / 2);
537
+
538
+ // Ícones de interação
539
+ const iconSpacing = 70;
540
+ let currentIconY = followButtonY + followButtonSize + 30;
541
+
542
+ // Ícone de curtida
543
+ ctx.fillStyle = colors.text;
544
+ ctx.font = `bold 14px ${registeredFontName}-Bold`;
545
+ ctx.textAlign = "center";
546
+ ctx.textBaseline = "top";
547
+
548
+ ctx.fillText("♥", avatarX + avatarSize / 2, currentIconY);
549
+ ctx.fillText(formatNumber(this.likes), avatarX + avatarSize / 2, currentIconY + 25);
550
+ currentIconY += iconSpacing;
551
+
552
+ // Ícone de comentário
553
+ ctx.fillText("💬", avatarX + avatarSize / 2, currentIconY);
554
+ ctx.fillText(formatNumber(this.comments), avatarX + avatarSize / 2, currentIconY + 25);
555
+ currentIconY += iconSpacing;
556
+
557
+ // Ícone de compartilhamento
558
+ ctx.fillText("↗", avatarX + avatarSize / 2, currentIconY);
559
+ ctx.fillText(formatNumber(this.shares), avatarX + avatarSize / 2, currentIconY + 25);
560
+ currentIconY += iconSpacing;
561
+
562
+ // Ícone de salvamento
563
+ ctx.fillText("🔖", avatarX + avatarSize / 2, currentIconY);
564
+ ctx.fillText(formatNumber(this.bookmarks), avatarX + avatarSize / 2, currentIconY + 25);
565
+
566
+ // --- Barra Inferior (Informações) ---
567
+ const bottomBarHeight = 150;
568
+ const bottomBarY = cardHeight - bottomBarHeight;
569
+
570
+ // Aplica efeito de glassmorphism se ativado
571
+ if (this.useGlassmorphism) {
572
+ applyGlassmorphism(
573
+ ctx,
574
+ 0,
575
+ bottomBarY,
576
+ cardWidth - sideBarWidth,
577
+ bottomBarHeight,
578
+ 0,
579
+ 0.3,
580
+ "#000000"
581
+ );
582
+ }
583
+
584
+ // Nome de usuário
585
+ ctx.fillStyle = colors.text;
586
+ ctx.font = `bold 18px ${registeredFontName}-Bold`;
587
+ ctx.textAlign = "left";
588
+ ctx.textBaseline = "top";
589
+
590
+ // Aplica sombra de texto se ativada
591
+ if (this.useTextShadow) {
592
+ applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 2, 1, 1);
593
+ }
594
+
595
+ const usernameText = `@${this.username}`;
596
+ const usernameWidth = ctx.measureText(usernameText).width;
597
+ ctx.fillText(usernameText, padding, bottomBarY + padding);
598
+
599
+ // Desenha ícone de verificado (se aplicável)
600
+ if (this.verified) {
601
+ const verifiedSize = 16;
602
+ const verifiedX = padding + usernameWidth + 5;
603
+
604
+ ctx.fillStyle = "#20D5EC";
605
+ ctx.beginPath();
606
+ ctx.arc(verifiedX + verifiedSize / 2, bottomBarY + padding + 9, verifiedSize / 2, 0, Math.PI * 2);
607
+ ctx.fill();
608
+
609
+ ctx.fillStyle = "#FFFFFF";
610
+ ctx.font = `bold 12px ${registeredFontName}-Bold`;
611
+ ctx.textAlign = "center";
612
+ ctx.fillText("✓", verifiedX + verifiedSize / 2, bottomBarY + padding + 9);
613
+ }
614
+
615
+ // Remove sombra para o próximo texto
616
+ if (this.useTextShadow) {
617
+ clearShadow(ctx);
618
+ }
619
+
620
+ // Legenda
621
+ ctx.fillStyle = colors.text;
622
+ ctx.font = `regular 16px ${registeredFontName}-Regular`;
623
+ ctx.textAlign = "left";
624
+
625
+ // Aplica sombra de texto se ativada
626
+ if (this.useTextShadow) {
627
+ applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 2, 1, 1);
628
+ }
629
+
630
+ wrapText(ctx, this.caption, padding, bottomBarY + padding + 30, cardWidth - sideBarWidth - padding * 2, 20, registeredFontName);
631
+
632
+ // Remove sombra para o próximo texto
633
+ if (this.useTextShadow) {
634
+ clearShadow(ctx);
635
+ }
636
+
637
+ // Informações da música
638
+ if (this.musicInfo) {
639
+ ctx.fillStyle = colors.text;
640
+ ctx.font = `regular 14px ${registeredFontName}-Regular`;
641
+ ctx.textAlign = "left";
642
+ ctx.textBaseline = "bottom";
643
+
644
+ // Aplica sombra de texto se ativada
645
+ if (this.useTextShadow) {
646
+ applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 2, 1, 1);
647
+ }
648
+
649
+ ctx.fillText(`🎵 ${this.musicInfo}`, padding, cardHeight - padding);
650
+
651
+ // Remove sombra para o próximo texto
652
+ if (this.useTextShadow) {
653
+ clearShadow(ctx);
654
+ }
655
+ }
656
+
657
+ // Efeito (se fornecido)
658
+ if (this.effect) {
659
+ ctx.fillStyle = colors.text;
660
+ ctx.font = `regular 14px ${registeredFontName}-Regular`;
661
+ ctx.textAlign = "right";
662
+ ctx.textBaseline = "bottom";
663
+
664
+ // Aplica sombra de texto se ativada
665
+ if (this.useTextShadow) {
666
+ applyTextShadow(ctx, "rgba(0, 0, 0, 0.5)", 2, 1, 1);
667
+ }
668
+
669
+ ctx.fillText(`✨ ${this.effect}`, cardWidth - sideBarWidth - padding, cardHeight - padding);
670
+
671
+ // Remove sombra para o próximo texto
672
+ if (this.useTextShadow) {
673
+ clearShadow(ctx);
674
+ }
675
+ }
676
+
677
+ // --- Codifica e Retorna Buffer ---
678
+ try {
679
+ return await encodeToBuffer(canvas);
680
+ } catch (err) {
681
+ console.error("Falha ao codificar o Post do TikTok:", err);
682
+ throw new Error("Não foi possível gerar o buffer de imagem do Post do TikTok.");
683
+ }
684
+ }
685
+
686
+ // --- Métodos Auxiliares Privados ---
687
+ /**
688
+ * Obtém as cores com base no tema selecionado
689
+ * @private
690
+ */
691
+ _getThemeColors() {
692
+ switch (this.theme) {
693
+ case "light":
694
+ return {
695
+ background: "#FFFFFF",
696
+ secondaryBackground: "#F8F8F8",
697
+ text: "#000000",
698
+ textSecondary: "#888888",
699
+ accent: "#FF0050"
700
+ };
701
+ case "dark":
702
+ default:
703
+ return {
704
+ background: "#000000",
705
+ secondaryBackground: "#121212",
706
+ text: "#FFFFFF",
707
+ textSecondary: "#AAAAAA",
708
+ accent: "#FF0050"
709
+ };
710
+ }
711
+ }
712
+ };
713
+