@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,681 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Módulo de Thumbnail do YouTube
|
|
5
|
+
*
|
|
6
|
+
* Este módulo gera thumbnails no estilo do YouTube com título, avatar do canal,
|
|
7
|
+
* duração do vídeo e outros elementos visuais.
|
|
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
|
+
formatTime
|
|
30
|
+
} = require("../utils");
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @class YouTubeThumbnail
|
|
34
|
+
* @classdesc Gera uma thumbnail no estilo do YouTube.
|
|
35
|
+
* @example const thumbnail = new YouTubeThumbnail()
|
|
36
|
+
* .setTitle("Como criar thumbnails atraentes para o YouTube")
|
|
37
|
+
* .setBackground("image", "background.jpg")
|
|
38
|
+
* .setChannelAvatar("avatar.png")
|
|
39
|
+
* .setChannelName("Canal Criativo")
|
|
40
|
+
* .setDuration(1250)
|
|
41
|
+
* .build();
|
|
42
|
+
*/
|
|
43
|
+
module.exports = class YouTubeThumbnail {
|
|
44
|
+
constructor(options) {
|
|
45
|
+
// Dados Principais
|
|
46
|
+
this.title = "Título do Vídeo";
|
|
47
|
+
this.background = null;
|
|
48
|
+
this.channelName = null;
|
|
49
|
+
this.channelAvatar = null;
|
|
50
|
+
this.duration = 0; // em segundos
|
|
51
|
+
this.views = null;
|
|
52
|
+
this.publishedAt = null;
|
|
53
|
+
|
|
54
|
+
// Elementos Visuais
|
|
55
|
+
this.overlayText = null;
|
|
56
|
+
this.overlayTextColor = "#FFFFFF";
|
|
57
|
+
this.overlayTextBackground = "#FF0000";
|
|
58
|
+
this.showPlayButton = true;
|
|
59
|
+
|
|
60
|
+
// Personalização Visual
|
|
61
|
+
this.font = { name: options?.font?.name ?? DEFAULT_FONT_FAMILY, path: options?.font?.path };
|
|
62
|
+
this.titleColor = "#FFFFFF";
|
|
63
|
+
this.titleBackground = "rgba(0, 0, 0, 0.6)";
|
|
64
|
+
this.overlayOpacity = 0.3;
|
|
65
|
+
this.useTextShadow = true;
|
|
66
|
+
this.style = "standard"; // standard, shorts, premium
|
|
67
|
+
|
|
68
|
+
// Configurações de Layout
|
|
69
|
+
this.cardWidth = 1280;
|
|
70
|
+
this.cardHeight = 720;
|
|
71
|
+
this.cornerRadius = 12;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Setters para Dados Principais ---
|
|
75
|
+
/**
|
|
76
|
+
* Define o título do vídeo
|
|
77
|
+
* @param {string} text - Título do vídeo
|
|
78
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
79
|
+
*/
|
|
80
|
+
setTitle(text) {
|
|
81
|
+
if (!text || typeof text !== "string") throw new Error("O título do vídeo deve ser uma string não vazia.");
|
|
82
|
+
this.title = text;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Define o plano de fundo da thumbnail
|
|
88
|
+
* @param {string} type - Tipo de plano de fundo ('color' ou 'image')
|
|
89
|
+
* @param {string} value - Valor do plano de fundo (cor hexadecimal ou URL/caminho da imagem)
|
|
90
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
91
|
+
*/
|
|
92
|
+
setBackground(type, value) {
|
|
93
|
+
const types = ["color", "image"];
|
|
94
|
+
if (!type || !types.includes(type.toLowerCase())) throw new Error("O tipo de plano de fundo deve ser 'color' ou 'image'.");
|
|
95
|
+
if (!value) throw new Error("O valor do plano de fundo não pode estar vazio.");
|
|
96
|
+
if (type.toLowerCase() === "color" && !isValidHexColor(value)) throw new Error("Cor de plano de fundo inválida. Use o formato hexadecimal.");
|
|
97
|
+
this.background = { type: type.toLowerCase(), value };
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Define o nome do canal
|
|
103
|
+
* @param {string} name - Nome do canal
|
|
104
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
105
|
+
*/
|
|
106
|
+
setChannelName(name) {
|
|
107
|
+
if (!name || typeof name !== "string") throw new Error("O nome do canal deve ser uma string não vazia.");
|
|
108
|
+
this.channelName = name;
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Define o avatar do canal
|
|
114
|
+
* @param {string|Buffer|Object} image - URL, Buffer ou caminho da imagem do avatar
|
|
115
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
116
|
+
*/
|
|
117
|
+
setChannelAvatar(image) {
|
|
118
|
+
if (!image) throw new Error("A fonte da imagem do avatar não pode estar vazia.");
|
|
119
|
+
this.channelAvatar = image;
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Define a duração do vídeo
|
|
125
|
+
* @param {number} seconds - Duração em segundos
|
|
126
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
127
|
+
*/
|
|
128
|
+
setDuration(seconds) {
|
|
129
|
+
if (typeof seconds !== "number" || seconds < 0) throw new Error("A duração do vídeo deve ser um número não negativo.");
|
|
130
|
+
this.duration = seconds;
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Define o número de visualizações
|
|
136
|
+
* @param {number} count - Número de visualizações
|
|
137
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
138
|
+
*/
|
|
139
|
+
setViews(count) {
|
|
140
|
+
if (typeof count !== "number" || count < 0) throw new Error("O número de visualizações deve ser um número não negativo.");
|
|
141
|
+
this.views = count;
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Define a data de publicação
|
|
147
|
+
* @param {string} date - Data de publicação (ex: "há 2 dias")
|
|
148
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
149
|
+
*/
|
|
150
|
+
setPublishedAt(date) {
|
|
151
|
+
if (!date || typeof date !== "string") throw new Error("A data de publicação deve ser uma string não vazia.");
|
|
152
|
+
this.publishedAt = date;
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- Setters para Elementos Visuais ---
|
|
157
|
+
/**
|
|
158
|
+
* Define o texto de sobreposição (ex: "NOVO", "AO VIVO", etc.)
|
|
159
|
+
* @param {string} text - Texto de sobreposição
|
|
160
|
+
* @param {string} textColor - Cor do texto (hexadecimal)
|
|
161
|
+
* @param {string} backgroundColor - Cor de fundo (hexadecimal)
|
|
162
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
163
|
+
*/
|
|
164
|
+
setOverlayText(text, textColor = "#FFFFFF", backgroundColor = "#FF0000") {
|
|
165
|
+
if (!text || typeof text !== "string") throw new Error("O texto de sobreposição deve ser uma string não vazia.");
|
|
166
|
+
if (textColor && !isValidHexColor(textColor)) throw new Error("Cor de texto inválida. Use o formato hexadecimal.");
|
|
167
|
+
if (backgroundColor && !isValidHexColor(backgroundColor)) throw new Error("Cor de fundo inválida. Use o formato hexadecimal.");
|
|
168
|
+
|
|
169
|
+
this.overlayText = text;
|
|
170
|
+
this.overlayTextColor = textColor;
|
|
171
|
+
this.overlayTextBackground = backgroundColor;
|
|
172
|
+
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Ativa ou desativa o botão de play
|
|
178
|
+
* @param {boolean} show - Se o botão de play deve ser exibido
|
|
179
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
180
|
+
*/
|
|
181
|
+
setShowPlayButton(show = true) {
|
|
182
|
+
this.showPlayButton = show;
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- Setters para Personalização Visual ---
|
|
187
|
+
/**
|
|
188
|
+
* Define a cor do título
|
|
189
|
+
* @param {string} color - Cor hexadecimal
|
|
190
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
191
|
+
*/
|
|
192
|
+
setTitleColor(color) {
|
|
193
|
+
if (!color || !isValidHexColor(color)) throw new Error("Cor de título inválida. Use o formato hexadecimal.");
|
|
194
|
+
this.titleColor = color;
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Define o fundo do título
|
|
200
|
+
* @param {string} color - Cor hexadecimal ou rgba
|
|
201
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
202
|
+
*/
|
|
203
|
+
setTitleBackground(color) {
|
|
204
|
+
this.titleBackground = color;
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Define a opacidade da sobreposição
|
|
210
|
+
* @param {number} opacity - Valor de opacidade (0-1)
|
|
211
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
212
|
+
*/
|
|
213
|
+
setOverlayOpacity(opacity) {
|
|
214
|
+
if (typeof opacity !== "number" || opacity < 0 || opacity > 1) throw new Error("A opacidade da sobreposição deve estar entre 0 e 1.");
|
|
215
|
+
this.overlayOpacity = opacity;
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Ativa ou desativa a sombra de texto
|
|
221
|
+
* @param {boolean} enabled - Se a sombra de texto deve ser ativada
|
|
222
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
223
|
+
*/
|
|
224
|
+
enableTextShadow(enabled = true) {
|
|
225
|
+
this.useTextShadow = enabled;
|
|
226
|
+
return this;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Define o estilo da thumbnail
|
|
231
|
+
* @param {string} style - Estilo da thumbnail ('standard', 'shorts', 'premium')
|
|
232
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
233
|
+
*/
|
|
234
|
+
setStyle(style) {
|
|
235
|
+
const validStyles = ["standard", "shorts", "premium"];
|
|
236
|
+
if (!style || !validStyles.includes(style.toLowerCase())) {
|
|
237
|
+
throw new Error(`Estilo de thumbnail inválido. Use um dos seguintes: ${validStyles.join(", ")}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.style = style.toLowerCase();
|
|
241
|
+
|
|
242
|
+
// Ajusta as dimensões com base no estilo
|
|
243
|
+
if (this.style === "shorts") {
|
|
244
|
+
this.cardWidth = 1080;
|
|
245
|
+
this.cardHeight = 1920;
|
|
246
|
+
} else {
|
|
247
|
+
this.cardWidth = 1280;
|
|
248
|
+
this.cardHeight = 720;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Define as dimensões do card
|
|
256
|
+
* @param {number} width - Largura do card em pixels
|
|
257
|
+
* @param {number} height - Altura do card em pixels
|
|
258
|
+
* @returns {YouTubeThumbnail} - Instância atual para encadeamento
|
|
259
|
+
*/
|
|
260
|
+
setCardDimensions(width, height) {
|
|
261
|
+
if (typeof width !== "number" || width < 640 || width > 1920) {
|
|
262
|
+
throw new Error("A largura do card deve estar entre 640 e 1920 pixels.");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (typeof height !== "number" || height < 360 || height > 1920) {
|
|
266
|
+
throw new Error("A altura do card deve estar entre 360 e 1920 pixels.");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.cardWidth = width;
|
|
270
|
+
this.cardHeight = height;
|
|
271
|
+
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// --- Método de Construção ---
|
|
276
|
+
/**
|
|
277
|
+
* Constrói a thumbnail e retorna um buffer de imagem
|
|
278
|
+
* @returns {Promise<Buffer>} - Buffer contendo a imagem da thumbnail
|
|
279
|
+
*/
|
|
280
|
+
async build() {
|
|
281
|
+
if (!this.background) throw new Error("O plano de fundo da thumbnail deve ser definido usando setBackground().");
|
|
282
|
+
|
|
283
|
+
// --- Registro de Fonte ---
|
|
284
|
+
const registeredFontName = await registerFontIfNeeded(this.font);
|
|
285
|
+
|
|
286
|
+
// --- Configuração do Canvas ---
|
|
287
|
+
const cardWidth = this.cardWidth;
|
|
288
|
+
const cardHeight = this.cardHeight;
|
|
289
|
+
const padding = Math.floor(cardWidth * 0.03); // Padding proporcional
|
|
290
|
+
const cornerRadius = this.cornerRadius;
|
|
291
|
+
|
|
292
|
+
const canvas = pureimage.make(cardWidth, cardHeight);
|
|
293
|
+
const ctx = canvas.getContext("2d");
|
|
294
|
+
|
|
295
|
+
// --- Desenha Plano de Fundo ---
|
|
296
|
+
if (this.background.type === "color") {
|
|
297
|
+
// Plano de fundo de cor sólida
|
|
298
|
+
ctx.fillStyle = this.background.value;
|
|
299
|
+
ctx.fillRect(0, 0, cardWidth, cardHeight);
|
|
300
|
+
} else {
|
|
301
|
+
// Plano de fundo de imagem
|
|
302
|
+
try {
|
|
303
|
+
const img = await loadImageWithAxios(this.background.value);
|
|
304
|
+
const aspect = img.width / img.height;
|
|
305
|
+
let drawWidth = cardWidth;
|
|
306
|
+
let drawHeight = cardWidth / aspect;
|
|
307
|
+
|
|
308
|
+
// Ajusta as dimensões para cobrir todo o card
|
|
309
|
+
if (drawHeight < cardHeight) {
|
|
310
|
+
drawHeight = cardHeight;
|
|
311
|
+
drawWidth = cardHeight * aspect;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const offsetX = (cardWidth - drawWidth) / 2;
|
|
315
|
+
const offsetY = (cardHeight - drawHeight) / 2;
|
|
316
|
+
|
|
317
|
+
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
|
|
318
|
+
|
|
319
|
+
// Aplica sobreposição para melhorar legibilidade do texto
|
|
320
|
+
ctx.globalAlpha = this.overlayOpacity;
|
|
321
|
+
ctx.fillStyle = "#000000";
|
|
322
|
+
ctx.fillRect(0, 0, cardWidth, cardHeight);
|
|
323
|
+
ctx.globalAlpha = 1;
|
|
324
|
+
} catch (e) {
|
|
325
|
+
console.error("Falha ao desenhar imagem de plano de fundo:", e.message);
|
|
326
|
+
ctx.fillStyle = "#000000";
|
|
327
|
+
ctx.fillRect(0, 0, cardWidth, cardHeight);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// --- Desenha Elementos com base no Estilo ---
|
|
332
|
+
if (this.style === "shorts") {
|
|
333
|
+
// Estilo Shorts (vertical)
|
|
334
|
+
await this._drawShortsStyle(ctx, registeredFontName, cardWidth, cardHeight, padding);
|
|
335
|
+
} else if (this.style === "premium") {
|
|
336
|
+
// Estilo Premium
|
|
337
|
+
await this._drawPremiumStyle(ctx, registeredFontName, cardWidth, cardHeight, padding);
|
|
338
|
+
} else {
|
|
339
|
+
// Estilo Padrão
|
|
340
|
+
await this._drawStandardStyle(ctx, registeredFontName, cardWidth, cardHeight, padding);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// --- Codifica e Retorna Buffer ---
|
|
344
|
+
try {
|
|
345
|
+
return await encodeToBuffer(canvas);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
console.error("Falha ao codificar a Thumbnail do YouTube:", err);
|
|
348
|
+
throw new Error("Não foi possível gerar o buffer de imagem da Thumbnail do YouTube.");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// --- Métodos Auxiliares Privados ---
|
|
353
|
+
/**
|
|
354
|
+
* Desenha elementos no estilo padrão do YouTube
|
|
355
|
+
* @private
|
|
356
|
+
*/
|
|
357
|
+
async _drawStandardStyle(ctx, fontName, width, height, padding) {
|
|
358
|
+
// --- Desenha Duração do Vídeo ---
|
|
359
|
+
if (this.duration > 0) {
|
|
360
|
+
const durationText = this._formatDuration(this.duration);
|
|
361
|
+
const durationWidth = 70;
|
|
362
|
+
const durationHeight = 25;
|
|
363
|
+
const durationX = width - durationWidth - padding;
|
|
364
|
+
const durationY = height - durationHeight - padding;
|
|
365
|
+
|
|
366
|
+
// Fundo da duração
|
|
367
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.8)";
|
|
368
|
+
roundRect(ctx, durationX, durationY, durationWidth, durationHeight, 4, true, false);
|
|
369
|
+
|
|
370
|
+
// Texto da duração
|
|
371
|
+
ctx.fillStyle = "#FFFFFF";
|
|
372
|
+
ctx.font = `bold 14px ${fontName}-Bold`;
|
|
373
|
+
ctx.textAlign = "center";
|
|
374
|
+
ctx.textBaseline = "middle";
|
|
375
|
+
ctx.fillText(durationText, durationX + durationWidth / 2, durationY + durationHeight / 2);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// --- Desenha Botão de Play (se ativado) ---
|
|
379
|
+
if (this.showPlayButton) {
|
|
380
|
+
const playSize = Math.min(width, height) * 0.15;
|
|
381
|
+
const playX = width / 2 - playSize / 2;
|
|
382
|
+
const playY = height / 2 - playSize / 2;
|
|
383
|
+
|
|
384
|
+
// Círculo de fundo
|
|
385
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
|
|
386
|
+
ctx.beginPath();
|
|
387
|
+
ctx.arc(playX + playSize / 2, playY + playSize / 2, playSize / 2, 0, Math.PI * 2);
|
|
388
|
+
ctx.fill();
|
|
389
|
+
|
|
390
|
+
// Triângulo de play
|
|
391
|
+
ctx.fillStyle = "#FFFFFF";
|
|
392
|
+
ctx.beginPath();
|
|
393
|
+
ctx.moveTo(playX + playSize * 0.35, playY + playSize * 0.25);
|
|
394
|
+
ctx.lineTo(playX + playSize * 0.35, playY + playSize * 0.75);
|
|
395
|
+
ctx.lineTo(playX + playSize * 0.75, playY + playSize * 0.5);
|
|
396
|
+
ctx.closePath();
|
|
397
|
+
ctx.fill();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// --- Desenha Texto de Sobreposição (se fornecido) ---
|
|
401
|
+
if (this.overlayText) {
|
|
402
|
+
const overlayPadding = 8;
|
|
403
|
+
const overlayHeight = 30;
|
|
404
|
+
|
|
405
|
+
ctx.font = `bold 16px ${fontName}-Bold`;
|
|
406
|
+
const overlayWidth = ctx.measureText(this.overlayText).width + overlayPadding * 2;
|
|
407
|
+
|
|
408
|
+
const overlayX = padding;
|
|
409
|
+
const overlayY = padding;
|
|
410
|
+
|
|
411
|
+
// Fundo do texto de sobreposição
|
|
412
|
+
ctx.fillStyle = this.overlayTextBackground;
|
|
413
|
+
roundRect(ctx, overlayX, overlayY, overlayWidth, overlayHeight, 4, true, false);
|
|
414
|
+
|
|
415
|
+
// Texto de sobreposição
|
|
416
|
+
ctx.fillStyle = this.overlayTextColor;
|
|
417
|
+
ctx.textAlign = "center";
|
|
418
|
+
ctx.textBaseline = "middle";
|
|
419
|
+
ctx.fillText(this.overlayText, overlayX + overlayWidth / 2, overlayY + overlayHeight / 2);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// --- Desenha Título ---
|
|
423
|
+
const titleFontSize = Math.floor(width * 0.04); // Tamanho proporcional
|
|
424
|
+
const titleMaxWidth = width - padding * 2;
|
|
425
|
+
const titleY = height - padding - titleFontSize * 3;
|
|
426
|
+
|
|
427
|
+
// Fundo do título (opcional)
|
|
428
|
+
if (this.titleBackground) {
|
|
429
|
+
ctx.fillStyle = this.titleBackground;
|
|
430
|
+
roundRect(ctx, 0, titleY - padding, width, padding * 2 + titleFontSize * 3, 0, true, false);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Texto do título
|
|
434
|
+
ctx.fillStyle = this.titleColor;
|
|
435
|
+
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
|
|
436
|
+
ctx.textAlign = "left";
|
|
437
|
+
ctx.textBaseline = "top";
|
|
438
|
+
|
|
439
|
+
// Aplica sombra de texto se ativada
|
|
440
|
+
if (this.useTextShadow) {
|
|
441
|
+
applyTextShadow(ctx, "rgba(0, 0, 0, 0.7)", 3, 2, 2);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
wrapText(ctx, this.title, padding, titleY, titleMaxWidth, titleFontSize * 1.2, fontName);
|
|
445
|
+
|
|
446
|
+
// Remove sombra
|
|
447
|
+
if (this.useTextShadow) {
|
|
448
|
+
clearShadow(ctx);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Desenha elementos no estilo Shorts do YouTube
|
|
454
|
+
* @private
|
|
455
|
+
*/
|
|
456
|
+
async _drawShortsStyle(ctx, fontName, width, height, padding) {
|
|
457
|
+
// --- Desenha Ícone de Shorts ---
|
|
458
|
+
const shortsIconSize = 40;
|
|
459
|
+
const shortsIconX = width - shortsIconSize - padding;
|
|
460
|
+
const shortsIconY = padding;
|
|
461
|
+
|
|
462
|
+
// Fundo do ícone
|
|
463
|
+
ctx.fillStyle = "#FF0000";
|
|
464
|
+
roundRect(ctx, shortsIconX, shortsIconY, shortsIconSize, shortsIconSize, 8, true, false);
|
|
465
|
+
|
|
466
|
+
// Letra "S" estilizada
|
|
467
|
+
ctx.fillStyle = "#FFFFFF";
|
|
468
|
+
ctx.font = `bold 28px ${fontName}-Bold`;
|
|
469
|
+
ctx.textAlign = "center";
|
|
470
|
+
ctx.textBaseline = "middle";
|
|
471
|
+
ctx.fillText("S", shortsIconX + shortsIconSize / 2, shortsIconY + shortsIconSize / 2);
|
|
472
|
+
|
|
473
|
+
// --- Desenha Título (na parte inferior) ---
|
|
474
|
+
const titleFontSize = Math.floor(width * 0.05); // Tamanho proporcional
|
|
475
|
+
const titleMaxWidth = width - padding * 2;
|
|
476
|
+
const titleY = height - padding * 6 - titleFontSize * 3;
|
|
477
|
+
|
|
478
|
+
// Fundo do título
|
|
479
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
|
|
480
|
+
roundRect(ctx, 0, titleY - padding, width, padding * 2 + titleFontSize * 3, 0, true, false);
|
|
481
|
+
|
|
482
|
+
// Texto do título
|
|
483
|
+
ctx.fillStyle = "#FFFFFF";
|
|
484
|
+
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
|
|
485
|
+
ctx.textAlign = "left";
|
|
486
|
+
ctx.textBaseline = "top";
|
|
487
|
+
|
|
488
|
+
// Aplica sombra de texto se ativada
|
|
489
|
+
if (this.useTextShadow) {
|
|
490
|
+
applyTextShadow(ctx, "rgba(0, 0, 0, 0.7)", 3, 2, 2);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
wrapText(ctx, this.title, padding, titleY, titleMaxWidth, titleFontSize * 1.2, fontName);
|
|
494
|
+
|
|
495
|
+
// Remove sombra
|
|
496
|
+
if (this.useTextShadow) {
|
|
497
|
+
clearShadow(ctx);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// --- Desenha Informações do Canal ---
|
|
501
|
+
if (this.channelName) {
|
|
502
|
+
const channelY = height - padding * 3;
|
|
503
|
+
const avatarSize = 40;
|
|
504
|
+
|
|
505
|
+
// Avatar do canal (se fornecido)
|
|
506
|
+
if (this.channelAvatar) {
|
|
507
|
+
try {
|
|
508
|
+
const avatarImg = await loadImageWithAxios(this.channelAvatar);
|
|
509
|
+
|
|
510
|
+
// Desenha círculo de recorte
|
|
511
|
+
ctx.save();
|
|
512
|
+
ctx.beginPath();
|
|
513
|
+
ctx.arc(padding + avatarSize / 2, channelY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
|
|
514
|
+
ctx.clip();
|
|
515
|
+
|
|
516
|
+
// Desenha avatar
|
|
517
|
+
ctx.drawImage(avatarImg, padding, channelY, avatarSize, avatarSize);
|
|
518
|
+
ctx.restore();
|
|
519
|
+
} catch (e) {
|
|
520
|
+
console.error("Falha ao desenhar avatar do canal:", e.message);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Nome do canal
|
|
525
|
+
ctx.fillStyle = "#FFFFFF";
|
|
526
|
+
ctx.font = `16px ${fontName}`;
|
|
527
|
+
ctx.textAlign = "left";
|
|
528
|
+
ctx.textBaseline = "middle";
|
|
529
|
+
ctx.fillText(this.channelName, padding + avatarSize + 10, channelY + avatarSize / 2);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Desenha elementos no estilo Premium do YouTube
|
|
535
|
+
* @private
|
|
536
|
+
*/
|
|
537
|
+
async _drawPremiumStyle(ctx, fontName, width, height, padding) {
|
|
538
|
+
// --- Desenha Gradiente Premium ---
|
|
539
|
+
const gradient = createLinearGradient(ctx, 0, 0, width, height, ["#FF0000", "#8E2DE2"]);
|
|
540
|
+
ctx.fillStyle = gradient;
|
|
541
|
+
ctx.globalAlpha = 0.2;
|
|
542
|
+
ctx.fillRect(0, 0, width, height);
|
|
543
|
+
ctx.globalAlpha = 1;
|
|
544
|
+
|
|
545
|
+
// --- Desenha Ícone Premium ---
|
|
546
|
+
const premiumIconSize = 40;
|
|
547
|
+
const premiumIconX = width - premiumIconSize - padding;
|
|
548
|
+
const premiumIconY = padding;
|
|
549
|
+
|
|
550
|
+
// Fundo do ícone
|
|
551
|
+
ctx.fillStyle = "#FF0000";
|
|
552
|
+
roundRect(ctx, premiumIconX, premiumIconY, premiumIconSize, premiumIconSize, 20, true, false);
|
|
553
|
+
|
|
554
|
+
// Símbolo Premium (triângulo)
|
|
555
|
+
ctx.fillStyle = "#FFFFFF";
|
|
556
|
+
ctx.beginPath();
|
|
557
|
+
ctx.moveTo(premiumIconX + premiumIconSize / 2, premiumIconY + premiumIconSize * 0.25);
|
|
558
|
+
ctx.lineTo(premiumIconX + premiumIconSize * 0.25, premiumIconY + premiumIconSize * 0.75);
|
|
559
|
+
ctx.lineTo(premiumIconX + premiumIconSize * 0.75, premiumIconY + premiumIconSize * 0.75);
|
|
560
|
+
ctx.closePath();
|
|
561
|
+
ctx.fill();
|
|
562
|
+
|
|
563
|
+
// --- Desenha Duração do Vídeo ---
|
|
564
|
+
if (this.duration > 0) {
|
|
565
|
+
const durationText = this._formatDuration(this.duration);
|
|
566
|
+
const durationWidth = 70;
|
|
567
|
+
const durationHeight = 25;
|
|
568
|
+
const durationX = width - durationWidth - padding;
|
|
569
|
+
const durationY = height - durationHeight - padding;
|
|
570
|
+
|
|
571
|
+
// Fundo da duração
|
|
572
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.8)";
|
|
573
|
+
roundRect(ctx, durationX, durationY, durationWidth, durationHeight, 12, true, false);
|
|
574
|
+
|
|
575
|
+
// Texto da duração
|
|
576
|
+
ctx.fillStyle = "#FFFFFF";
|
|
577
|
+
ctx.font = `bold 14px ${fontName}-Bold`;
|
|
578
|
+
ctx.textAlign = "center";
|
|
579
|
+
ctx.textBaseline = "middle";
|
|
580
|
+
ctx.fillText(durationText, durationX + durationWidth / 2, durationY + durationHeight / 2);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// --- Desenha Título com Estilo Premium ---
|
|
584
|
+
const titleFontSize = Math.floor(width * 0.05); // Tamanho proporcional
|
|
585
|
+
const titleMaxWidth = width - padding * 2;
|
|
586
|
+
const titleY = height - padding * 8;
|
|
587
|
+
|
|
588
|
+
// Fundo do título com gradiente
|
|
589
|
+
const titleGradient = createLinearGradient(ctx, 0, titleY - padding, width, titleY + titleFontSize * 3 + padding, ["rgba(0,0,0,0.9)", "rgba(0,0,0,0.7)"]);
|
|
590
|
+
ctx.fillStyle = titleGradient;
|
|
591
|
+
roundRect(ctx, 0, titleY - padding, width, padding * 2 + titleFontSize * 3, 0, true, false);
|
|
592
|
+
|
|
593
|
+
// Texto do título
|
|
594
|
+
ctx.fillStyle = "#FFFFFF";
|
|
595
|
+
ctx.font = `bold ${titleFontSize}px ${fontName}-Bold`;
|
|
596
|
+
ctx.textAlign = "left";
|
|
597
|
+
ctx.textBaseline = "top";
|
|
598
|
+
|
|
599
|
+
// Aplica sombra de texto
|
|
600
|
+
applyTextShadow(ctx, "rgba(0, 0, 0, 0.7)", 3, 2, 2);
|
|
601
|
+
|
|
602
|
+
wrapText(ctx, this.title, padding, titleY, titleMaxWidth, titleFontSize * 1.2, fontName);
|
|
603
|
+
|
|
604
|
+
// Remove sombra
|
|
605
|
+
clearShadow(ctx);
|
|
606
|
+
|
|
607
|
+
// --- Desenha Informações do Canal ---
|
|
608
|
+
if (this.channelName) {
|
|
609
|
+
const channelY = height - padding * 3;
|
|
610
|
+
const avatarSize = 40;
|
|
611
|
+
|
|
612
|
+
// Avatar do canal (se fornecido)
|
|
613
|
+
if (this.channelAvatar) {
|
|
614
|
+
try {
|
|
615
|
+
const avatarImg = await loadImageWithAxios(this.channelAvatar);
|
|
616
|
+
|
|
617
|
+
// Desenha círculo de recorte
|
|
618
|
+
ctx.save();
|
|
619
|
+
ctx.beginPath();
|
|
620
|
+
ctx.arc(padding + avatarSize / 2, channelY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
|
|
621
|
+
ctx.clip();
|
|
622
|
+
|
|
623
|
+
// Desenha avatar
|
|
624
|
+
ctx.drawImage(avatarImg, padding, channelY, avatarSize, avatarSize);
|
|
625
|
+
ctx.restore();
|
|
626
|
+
|
|
627
|
+
// Borda premium
|
|
628
|
+
ctx.strokeStyle = "#FF0000";
|
|
629
|
+
ctx.lineWidth = 2;
|
|
630
|
+
ctx.beginPath();
|
|
631
|
+
ctx.arc(padding + avatarSize / 2, channelY + avatarSize / 2, avatarSize / 2 + 2, 0, Math.PI * 2);
|
|
632
|
+
ctx.stroke();
|
|
633
|
+
} catch (e) {
|
|
634
|
+
console.error("Falha ao desenhar avatar do canal:", e.message);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Nome do canal
|
|
639
|
+
ctx.fillStyle = "#FFFFFF";
|
|
640
|
+
ctx.font = `16px ${fontName}`;
|
|
641
|
+
ctx.textAlign = "left";
|
|
642
|
+
ctx.textBaseline = "middle";
|
|
643
|
+
ctx.fillText(this.channelName, padding + avatarSize + 10, channelY + avatarSize / 2);
|
|
644
|
+
|
|
645
|
+
// Ícone de verificado
|
|
646
|
+
const verifiedSize = 16;
|
|
647
|
+
const verifiedX = padding + avatarSize + 10 + ctx.measureText(this.channelName).width + 5;
|
|
648
|
+
const verifiedY = channelY + avatarSize / 2 - verifiedSize / 2;
|
|
649
|
+
|
|
650
|
+
ctx.fillStyle = "#3EA6FF";
|
|
651
|
+
ctx.beginPath();
|
|
652
|
+
ctx.arc(verifiedX + verifiedSize / 2, verifiedY + verifiedSize / 2, verifiedSize / 2, 0, Math.PI * 2);
|
|
653
|
+
ctx.fill();
|
|
654
|
+
|
|
655
|
+
ctx.fillStyle = "#FFFFFF";
|
|
656
|
+
ctx.font = `bold 10px ${fontName}-Bold`;
|
|
657
|
+
ctx.textAlign = "center";
|
|
658
|
+
ctx.textBaseline = "middle";
|
|
659
|
+
ctx.fillText("✓", verifiedX + verifiedSize / 2, verifiedY + verifiedSize / 2);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Formata a duração em segundos para o formato MM:SS ou HH:MM:SS
|
|
665
|
+
* @param {number} seconds - Duração em segundos
|
|
666
|
+
* @returns {string} - Duração formatada
|
|
667
|
+
* @private
|
|
668
|
+
*/
|
|
669
|
+
_formatDuration(seconds) {
|
|
670
|
+
const hours = Math.floor(seconds / 3600);
|
|
671
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
672
|
+
const secs = Math.floor(seconds % 60);
|
|
673
|
+
|
|
674
|
+
if (hours > 0) {
|
|
675
|
+
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
676
|
+
} else {
|
|
677
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|