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