@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.
- package/assets/fonts/Manrope/Manrope-Bold.ttf +0 -0
- package/assets/fonts/Manrope/Manrope-Regular.ttf +0 -0
- package/assets/fonts/Others/AbyssinicaSIL-Regular.ttf +0 -0
- package/assets/fonts/Others/ChirpRegular.ttf +0 -0
- package/assets/fonts/Poppins/Poppins-Bold.ttf +0 -0
- package/assets/fonts/Poppins/Poppins-Medium.ttf +0 -0
- package/assets/fonts/Poppins/Poppins-Regular.ttf +0 -0
- package/assets/placeholders/album_art.png +0 -0
- package/assets/placeholders/avatar.png +0 -0
- package/assets/placeholders/badge.jpg +0 -0
- package/assets/placeholders/badge.png +0 -0
- package/assets/placeholders/badge_2.jpg +0 -0
- package/assets/placeholders/badge_3.jpg +0 -0
- package/assets/placeholders/badge_4.jpg +0 -0
- package/assets/placeholders/badge_5.jpg +0 -0
- package/assets/placeholders/banner.jpeg +0 -0
- package/assets/placeholders/images.jpeg +0 -0
- package/index.js +153 -0
- package/package.json +34 -0
- package/src/animation-effects.js +631 -0
- package/src/cache-manager.js +258 -0
- package/src/community-banner.js +1536 -0
- package/src/constants.js +208 -0
- package/src/discord-profile.js +584 -0
- package/src/e-commerce-banner.js +1214 -0
- package/src/effects.js +355 -0
- package/src/error-handler.js +305 -0
- package/src/event-banner.js +1319 -0
- package/src/facebook-post.js +679 -0
- package/src/gradient-welcome.js +430 -0
- package/src/image-filters.js +1034 -0
- package/src/image-processor.js +1014 -0
- package/src/instagram-post.js +504 -0
- package/src/interactive-elements.js +1208 -0
- package/src/linkedin-post.js +658 -0
- package/src/marketing-banner.js +1089 -0
- package/src/minimalist-banner.js +892 -0
- package/src/modern-profile.js +755 -0
- package/src/performance-optimizer.js +216 -0
- package/src/telegram-header.js +544 -0
- package/src/test-runner.js +645 -0
- package/src/tiktok-post.js +713 -0
- package/src/twitter-header.js +604 -0
- package/src/validator.js +442 -0
- package/src/welcome-leave.js +445 -0
- package/src/whatsapp-status.js +386 -0
- package/src/youtube-thumbnail.js +681 -0
- 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
|
+
|