@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
package/utils.js
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Módulo de Utilitários para @cognima/banners
|
|
5
|
+
*
|
|
6
|
+
* Este módulo contém funções auxiliares para manipulação de imagens, fontes,
|
|
7
|
+
* desenho em canvas e formatação de dados utilizados pelos geradores de banners.
|
|
8
|
+
*
|
|
9
|
+
* @author Cognima Team (melhorado)
|
|
10
|
+
* @version 2.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const pureimage = require("pureimage");
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const axios = require("axios");
|
|
17
|
+
const { Readable } = require("stream");
|
|
18
|
+
|
|
19
|
+
// --- Constantes para Padronização ---
|
|
20
|
+
const DEFAULT_FONT_FAMILY = "Poppins";
|
|
21
|
+
const DEFAULT_FONT_DIR = path.join(__dirname, "/assets/fonts/Poppins");
|
|
22
|
+
const FONT_WEIGHTS = {
|
|
23
|
+
bold: "Poppins-Bold.ttf",
|
|
24
|
+
medium: "Poppins-Medium.ttf",
|
|
25
|
+
regular: "Poppins-Regular.ttf",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// --- Validação de Cores ---
|
|
29
|
+
/**
|
|
30
|
+
* Verifica se uma string é uma cor hexadecimal válida
|
|
31
|
+
* @param {string} color - Cor em formato hexadecimal (#RRGGBB ou #RGB)
|
|
32
|
+
* @returns {boolean} - Verdadeiro se for uma cor hexadecimal válida
|
|
33
|
+
*/
|
|
34
|
+
function isValidHexColor(color) {
|
|
35
|
+
return /^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/.test(color);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Registro de Fontes ---
|
|
39
|
+
let fontsRegistered = false;
|
|
40
|
+
const registeredFontNames = new Set(); // Controla nomes de fontes registradas com sucesso
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Registra as fontes padrão do sistema
|
|
44
|
+
* @returns {Promise<string>} - Nome da família de fonte registrada
|
|
45
|
+
*/
|
|
46
|
+
async function registerDefaultFonts() {
|
|
47
|
+
if (fontsRegistered) {
|
|
48
|
+
return DEFAULT_FONT_FAMILY;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const fontPromises = [];
|
|
53
|
+
|
|
54
|
+
// Registra cada peso de fonte
|
|
55
|
+
for (const weight in FONT_WEIGHTS) {
|
|
56
|
+
const fontPath = path.join(DEFAULT_FONT_DIR, FONT_WEIGHTS[weight]);
|
|
57
|
+
|
|
58
|
+
if (fs.existsSync(fontPath)) {
|
|
59
|
+
const fontName = `${DEFAULT_FONT_FAMILY}-${weight.charAt(0).toUpperCase() + weight.slice(1)}`;
|
|
60
|
+
const font = pureimage.registerFont(fontPath, fontName);
|
|
61
|
+
|
|
62
|
+
fontPromises.push(
|
|
63
|
+
font.load().then(() => {
|
|
64
|
+
registeredFontNames.add(fontName);
|
|
65
|
+
}).catch(err => {
|
|
66
|
+
console.error(`Falha ao carregar fonte ${fontName}:`, err);
|
|
67
|
+
throw err;
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
} else {
|
|
71
|
+
console.warn(`Arquivo de fonte padrão não encontrado: ${fontPath}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (fontPromises.length > 0) {
|
|
76
|
+
await Promise.all(fontPromises);
|
|
77
|
+
|
|
78
|
+
if (registeredFontNames.size === Object.keys(FONT_WEIGHTS).length) {
|
|
79
|
+
fontsRegistered = true;
|
|
80
|
+
return DEFAULT_FONT_FAMILY;
|
|
81
|
+
} else {
|
|
82
|
+
throw new Error(`Apenas ${registeredFontNames.size}/${Object.keys(FONT_WEIGHTS).length} fontes padrão foram registradas com sucesso.`);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error("Nenhum arquivo de fonte padrão encontrado no diretório esperado.");
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error("Erro ao registrar fontes padrão:", err);
|
|
89
|
+
throw new Error("Falha ao carregar fontes padrão. Certifique-se de que os arquivos da fonte Poppins (Bold, Medium, Regular) estão em assets/fonts/Poppins.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Registra uma fonte personalizada se necessário
|
|
95
|
+
* @param {Object} fontConfig - Configuração da fonte (nome e caminho)
|
|
96
|
+
* @returns {Promise<string>} - Nome da família de fonte registrada
|
|
97
|
+
*/
|
|
98
|
+
async function registerFontIfNeeded(fontConfig) {
|
|
99
|
+
if (!fontConfig || !fontConfig.path || !fontConfig.name) {
|
|
100
|
+
// Usa fontes padrão se nenhuma fonte personalizada for fornecida ou a configuração estiver incompleta
|
|
101
|
+
return await registerDefaultFonts();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
if (fs.existsSync(fontConfig.path)) {
|
|
106
|
+
const font = pureimage.registerFont(fontConfig.path, fontConfig.name);
|
|
107
|
+
await font.load();
|
|
108
|
+
registeredFontNames.add(fontConfig.name);
|
|
109
|
+
return fontConfig.name; // Retorna o nome da fonte personalizada registrada
|
|
110
|
+
} else {
|
|
111
|
+
console.warn(`Arquivo de fonte personalizada não encontrado: ${fontConfig.path}. Usando fontes padrão.`);
|
|
112
|
+
return await registerDefaultFonts();
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error(`Erro ao registrar fonte personalizada ${fontConfig.name}:`, err);
|
|
116
|
+
console.warn("Usando fontes padrão como alternativa.");
|
|
117
|
+
return await registerDefaultFonts();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Carregamento de Imagens ---
|
|
122
|
+
/**
|
|
123
|
+
* Carrega uma imagem a partir de várias fontes possíveis (URL, Buffer, caminho local)
|
|
124
|
+
* @param {string|Buffer|Object} source - Fonte da imagem (URL, Buffer ou caminho local)
|
|
125
|
+
* @returns {Promise<Object>} - Objeto bitmap da imagem carregada
|
|
126
|
+
*/
|
|
127
|
+
async function loadImageWithAxios(source) {
|
|
128
|
+
if (!source) throw new Error("A fonte da imagem não pode ser nula ou vazia.");
|
|
129
|
+
|
|
130
|
+
// Carrega imagem a partir de URL
|
|
131
|
+
if (typeof source === "string" && source.startsWith("http")) {
|
|
132
|
+
try {
|
|
133
|
+
const response = await axios.get(source, { responseType: "arraybuffer" });
|
|
134
|
+
const buffer = Buffer.from(response.data, "binary");
|
|
135
|
+
const contentType = response.headers["content-type"];
|
|
136
|
+
|
|
137
|
+
// Tenta decodificar com base no tipo de conteúdo
|
|
138
|
+
if (contentType && contentType.includes("png")) {
|
|
139
|
+
return await pureimage.decodePNGFromStream(Readable.from(buffer));
|
|
140
|
+
} else if (contentType && (contentType.includes("jpeg") || contentType.includes("jpg"))) {
|
|
141
|
+
return await pureimage.decodeJPEGFromStream(Readable.from(buffer));
|
|
142
|
+
} else {
|
|
143
|
+
// Tenta decodificar, priorizando PNG e depois JPEG
|
|
144
|
+
try {
|
|
145
|
+
return await pureimage.decodePNGFromStream(Readable.from(buffer));
|
|
146
|
+
} catch (e) { /* ignora */ }
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
return await pureimage.decodeJPEGFromStream(Readable.from(buffer));
|
|
150
|
+
} catch (e) { /* ignora */ }
|
|
151
|
+
|
|
152
|
+
throw new Error(`Tipo de imagem não suportado '${contentType}' da URL: ${source}`);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`Erro ao carregar imagem da URL ${source}:`, error.message);
|
|
156
|
+
throw new Error(`Falha ao carregar imagem da URL: ${source}. Verifique se a URL está correta e o formato da imagem é PNG ou JPEG.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Carrega imagem a partir de Buffer
|
|
160
|
+
else if (Buffer.isBuffer(source)) {
|
|
161
|
+
try {
|
|
162
|
+
return await pureimage.decodePNGFromStream(Readable.from(source));
|
|
163
|
+
} catch (e) { /* ignora */ }
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
return await pureimage.decodeJPEGFromStream(Readable.from(source));
|
|
167
|
+
} catch (e) { /* ignora */ }
|
|
168
|
+
|
|
169
|
+
throw new Error("Falha ao decodificar imagem do buffer. Certifique-se de que é um buffer PNG ou JPEG válido.");
|
|
170
|
+
}
|
|
171
|
+
// Carrega imagem a partir de caminho local
|
|
172
|
+
else if (typeof source === "string") {
|
|
173
|
+
try {
|
|
174
|
+
const stream = fs.createReadStream(source);
|
|
175
|
+
|
|
176
|
+
if (source.toLowerCase().endsWith(".png")) {
|
|
177
|
+
return await pureimage.decodePNGFromStream(stream);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (source.toLowerCase().endsWith(".jpg") || source.toLowerCase().endsWith(".jpeg")) {
|
|
181
|
+
return await pureimage.decodeJPEGFromStream(stream);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw new Error("Tipo de arquivo de imagem local não suportado. Apenas PNG e JPEG são suportados.");
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error(`Erro ao carregar imagem do caminho ${source}:`, error.message);
|
|
187
|
+
throw new Error(`Falha ao carregar imagem do caminho: ${source}. Verifique se o caminho está correto e o arquivo é PNG ou JPEG.`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Já é um bitmap
|
|
191
|
+
else if (source && source.width && source.height && source.data) {
|
|
192
|
+
return source;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
throw new Error("Fonte de imagem inválida fornecida. Deve ser uma URL (http/https), Buffer, caminho local ou um bitmap pureimage.");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// --- Codificação de Canvas ---
|
|
199
|
+
/**
|
|
200
|
+
* Codifica um canvas para um buffer PNG
|
|
201
|
+
* @param {Object} canvas - Objeto canvas do pureimage
|
|
202
|
+
* @returns {Promise<Buffer>} - Buffer contendo a imagem PNG
|
|
203
|
+
*/
|
|
204
|
+
async function encodeToBuffer(canvas) {
|
|
205
|
+
const writableStream = new (require("stream").Writable)({
|
|
206
|
+
write(chunk, encoding, callback) {
|
|
207
|
+
if (!this.data) this.data = [];
|
|
208
|
+
this.data.push(chunk);
|
|
209
|
+
callback();
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await pureimage.encodePNGToStream(canvas, writableStream);
|
|
214
|
+
|
|
215
|
+
if (!writableStream.data) throw new Error("Falha ao codificar imagem para buffer.");
|
|
216
|
+
|
|
217
|
+
return Buffer.concat(writableStream.data);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- Auxiliares de Desenho ---
|
|
221
|
+
/**
|
|
222
|
+
* Desenha um retângulo com cantos arredondados
|
|
223
|
+
* @param {Object} ctx - Contexto de renderização 2D
|
|
224
|
+
* @param {number} x - Posição X do retângulo
|
|
225
|
+
* @param {number} y - Posição Y do retângulo
|
|
226
|
+
* @param {number} width - Largura do retângulo
|
|
227
|
+
* @param {number} height - Altura do retângulo
|
|
228
|
+
* @param {number|Object} radius - Raio dos cantos (número ou objeto com raios específicos)
|
|
229
|
+
* @param {boolean} fill - Se deve preencher o retângulo
|
|
230
|
+
* @param {boolean} stroke - Se deve desenhar o contorno do retângulo
|
|
231
|
+
*/
|
|
232
|
+
function roundRect(ctx, x, y, width, height, radius, fill, stroke) {
|
|
233
|
+
// Normaliza o raio para um objeto com todos os cantos
|
|
234
|
+
if (typeof radius === "number") {
|
|
235
|
+
radius = { tl: radius, tr: radius, br: radius, bl: radius };
|
|
236
|
+
} else {
|
|
237
|
+
radius = { ...{ tl: 0, tr: 0, br: 0, bl: 0 }, ...radius };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Desenha o caminho do retângulo arredondado
|
|
241
|
+
ctx.beginPath();
|
|
242
|
+
ctx.moveTo(x + radius.tl, y);
|
|
243
|
+
ctx.lineTo(x + width - radius.tr, y);
|
|
244
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
|
|
245
|
+
ctx.lineTo(x + width, y + height - radius.br);
|
|
246
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
|
|
247
|
+
ctx.lineTo(x + radius.bl, y + height);
|
|
248
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
|
|
249
|
+
ctx.lineTo(x, y + radius.tl);
|
|
250
|
+
ctx.quadraticCurveTo(x, y, x + radius.tl, y);
|
|
251
|
+
ctx.closePath();
|
|
252
|
+
|
|
253
|
+
// Aplica preenchimento e/ou contorno conforme solicitado
|
|
254
|
+
if (fill) {
|
|
255
|
+
ctx.fill();
|
|
256
|
+
}
|
|
257
|
+
if (stroke) {
|
|
258
|
+
ctx.stroke();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Quebra texto em múltiplas linhas para caber em uma largura máxima
|
|
264
|
+
* @param {Object} context - Contexto de renderização 2D
|
|
265
|
+
* @param {string} text - Texto a ser quebrado
|
|
266
|
+
* @param {number} x - Posição X inicial do texto
|
|
267
|
+
* @param {number} y - Posição Y inicial do texto
|
|
268
|
+
* @param {number} maxWidth - Largura máxima para o texto
|
|
269
|
+
* @param {number} lineHeight - Altura da linha
|
|
270
|
+
* @param {string} baseFontName - Nome base da fonte
|
|
271
|
+
* @returns {number} - Posição Y final após desenhar o texto
|
|
272
|
+
*/
|
|
273
|
+
function wrapText(context, text, x, y, maxWidth, lineHeight, baseFontName) {
|
|
274
|
+
const words = text.split(" ");
|
|
275
|
+
let line = "";
|
|
276
|
+
let currentY = y;
|
|
277
|
+
|
|
278
|
+
// Extrai informações da fonte atual
|
|
279
|
+
const initialFont = context.font || `16px ${baseFontName}-Regular`;
|
|
280
|
+
const fontMatch = initialFont.match(/\d+px/);
|
|
281
|
+
const fontSize = fontMatch ? fontMatch[0] : "16px";
|
|
282
|
+
const fontWeight = initialFont.includes("Bold") ? "Bold" : initialFont.includes("Medium") ? "Medium" : "Regular";
|
|
283
|
+
const fontName = `${baseFontName}-${fontWeight}`;
|
|
284
|
+
|
|
285
|
+
// Verifica se a fonte está registrada
|
|
286
|
+
if (!registeredFontNames.has(fontName)) {
|
|
287
|
+
context.font = `${fontSize} ${baseFontName}-Regular`;
|
|
288
|
+
} else {
|
|
289
|
+
context.font = `${fontSize} ${fontName}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Processa cada palavra e quebra em linhas
|
|
293
|
+
for (let n = 0; n < words.length; n++) {
|
|
294
|
+
const testLine = line + words[n] + " ";
|
|
295
|
+
context.font = `${fontSize} ${fontName}`;
|
|
296
|
+
const metrics = context.measureText(testLine);
|
|
297
|
+
const testWidth = metrics.width;
|
|
298
|
+
|
|
299
|
+
if (testWidth > maxWidth && n > 0) {
|
|
300
|
+
context.fillText(line.trim(), x, currentY);
|
|
301
|
+
line = words[n] + " ";
|
|
302
|
+
currentY += lineHeight;
|
|
303
|
+
} else {
|
|
304
|
+
line = testLine;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Desenha a última linha
|
|
309
|
+
context.font = `${fontSize} ${fontName}`;
|
|
310
|
+
context.fillText(line.trim(), x, currentY);
|
|
311
|
+
|
|
312
|
+
return currentY + lineHeight; // Retorna a posição Y para a próxima linha
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// --- Auxiliares de Formatação ---
|
|
316
|
+
/**
|
|
317
|
+
* Formata um tempo em milissegundos para o formato MM:SS
|
|
318
|
+
* @param {number} ms - Tempo em milissegundos
|
|
319
|
+
* @returns {string} - Tempo formatado (MM:SS)
|
|
320
|
+
*/
|
|
321
|
+
function formatTime(ms) {
|
|
322
|
+
if (typeof ms !== "number" || isNaN(ms) || ms < 0) return "0:00";
|
|
323
|
+
|
|
324
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
325
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
326
|
+
const seconds = totalSeconds % 60;
|
|
327
|
+
|
|
328
|
+
return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Formata um número para exibição amigável (com K, M para milhares e milhões)
|
|
333
|
+
* @param {number} num - Número a ser formatado
|
|
334
|
+
* @returns {string} - Número formatado
|
|
335
|
+
*/
|
|
336
|
+
function formatNumber(num) {
|
|
337
|
+
if (typeof num !== "number" || isNaN(num)) return "0";
|
|
338
|
+
|
|
339
|
+
// Formatação simples K/M para números grandes
|
|
340
|
+
if (num >= 1000000) return (num / 1000000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
341
|
+
if (num >= 1000) return (num / 1000).toFixed(1).replace(/\.0$/, "") + "K";
|
|
342
|
+
|
|
343
|
+
return num.toString();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Aplica um efeito de sombra de texto padronizado
|
|
348
|
+
* @param {Object} ctx - Contexto de renderização 2D
|
|
349
|
+
* @param {string} color - Cor da sombra (hexadecimal)
|
|
350
|
+
* @param {number} blur - Desfoque da sombra
|
|
351
|
+
* @param {number} offsetX - Deslocamento X da sombra
|
|
352
|
+
* @param {number} offsetY - Deslocamento Y da sombra
|
|
353
|
+
*/
|
|
354
|
+
function applyTextShadow(ctx, color = "#000000", blur = 3, offsetX = 1, offsetY = 1) {
|
|
355
|
+
ctx.shadowColor = color;
|
|
356
|
+
ctx.shadowBlur = blur;
|
|
357
|
+
ctx.shadowOffsetX = offsetX;
|
|
358
|
+
ctx.shadowOffsetY = offsetY;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Remove efeitos de sombra do contexto
|
|
363
|
+
* @param {Object} ctx - Contexto de renderização 2D
|
|
364
|
+
*/
|
|
365
|
+
function clearShadow(ctx) {
|
|
366
|
+
ctx.shadowColor = "transparent";
|
|
367
|
+
ctx.shadowBlur = 0;
|
|
368
|
+
ctx.shadowOffsetX = 0;
|
|
369
|
+
ctx.shadowOffsetY = 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Aplica um efeito de gradiente linear ao contexto
|
|
374
|
+
* @param {Object} ctx - Contexto de renderização 2D
|
|
375
|
+
* @param {number} x - Posição X inicial do gradiente
|
|
376
|
+
* @param {number} y - Posição Y inicial do gradiente
|
|
377
|
+
* @param {number} width - Largura do gradiente
|
|
378
|
+
* @param {number} height - Altura do gradiente
|
|
379
|
+
* @param {string} startColor - Cor inicial do gradiente (hexadecimal)
|
|
380
|
+
* @param {string} endColor - Cor final do gradiente (hexadecimal)
|
|
381
|
+
* @param {string} direction - Direção do gradiente ('horizontal', 'vertical', 'diagonal')
|
|
382
|
+
* @returns {Object} - Objeto de gradiente criado
|
|
383
|
+
*/
|
|
384
|
+
function createLinearGradient(ctx, x, y, width, height, startColor, endColor, direction = 'vertical') {
|
|
385
|
+
let gradient;
|
|
386
|
+
|
|
387
|
+
switch (direction.toLowerCase()) {
|
|
388
|
+
case 'horizontal':
|
|
389
|
+
gradient = ctx.createLinearGradient(x, y, x + width, y);
|
|
390
|
+
break;
|
|
391
|
+
case 'diagonal':
|
|
392
|
+
gradient = ctx.createLinearGradient(x, y, x + width, y + height);
|
|
393
|
+
break;
|
|
394
|
+
case 'vertical':
|
|
395
|
+
default:
|
|
396
|
+
gradient = ctx.createLinearGradient(x, y, x, y + height);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
gradient.addColorStop(0, startColor);
|
|
401
|
+
gradient.addColorStop(1, endColor);
|
|
402
|
+
|
|
403
|
+
return gradient;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Converte uma cor hexadecimal para RGBA
|
|
408
|
+
* @param {string} hex - Cor em formato hexadecimal
|
|
409
|
+
* @param {number} alpha - Valor de transparência (0-1)
|
|
410
|
+
* @returns {string} - Cor em formato RGBA
|
|
411
|
+
*/
|
|
412
|
+
function hexToRgba(hex, alpha = 1) {
|
|
413
|
+
if (!isValidHexColor(hex)) {
|
|
414
|
+
return `rgba(0, 0, 0, ${alpha})`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Expande formato abreviado (#RGB para #RRGGBB)
|
|
418
|
+
let r, g, b;
|
|
419
|
+
if (hex.length === 4) {
|
|
420
|
+
r = parseInt(hex[1] + hex[1], 16);
|
|
421
|
+
g = parseInt(hex[2] + hex[2], 16);
|
|
422
|
+
b = parseInt(hex[3] + hex[3], 16);
|
|
423
|
+
} else {
|
|
424
|
+
r = parseInt(hex.slice(1, 3), 16);
|
|
425
|
+
g = parseInt(hex.slice(3, 5), 16);
|
|
426
|
+
b = parseInt(hex.slice(5, 7), 16);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// --- Funções Adicionais de Utilidade ---
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Alias para loadImageWithAxios para compatibilidade
|
|
436
|
+
* @param {string|Buffer|Object} source - Fonte da imagem
|
|
437
|
+
* @returns {Promise<Object>} - Objeto bitmap da imagem carregada
|
|
438
|
+
*/
|
|
439
|
+
async function loadImage(source) {
|
|
440
|
+
return loadImageWithAxios(source);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Alias para encodeToBuffer para compatibilidade
|
|
445
|
+
* @param {Object} canvas - Objeto canvas do pureimage
|
|
446
|
+
* @returns {Promise<Buffer>} - Buffer contendo a imagem PNG
|
|
447
|
+
*/
|
|
448
|
+
async function getBufferFromImage(canvas) {
|
|
449
|
+
return encodeToBuffer(canvas);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Desenha uma imagem mantendo proporção e preenchendo a área especificada
|
|
454
|
+
* @param {Object} ctx - Contexto de renderização 2D
|
|
455
|
+
* @param {Object} img - Imagem a ser desenhada
|
|
456
|
+
* @param {number} x - Posição X
|
|
457
|
+
* @param {number} y - Posição Y
|
|
458
|
+
* @param {number} w - Largura da área
|
|
459
|
+
* @param {number} h - Altura da área
|
|
460
|
+
* @param {number} offsetX - Deslocamento X (0-1, padrão 0.5 = centro)
|
|
461
|
+
* @param {number} offsetY - Deslocamento Y (0-1, padrão 0.5 = centro)
|
|
462
|
+
*/
|
|
463
|
+
function drawImageProp(ctx, img, x, y, w, h, offsetX = 0.5, offsetY = 0.5) {
|
|
464
|
+
if (!img || !img.width || !img.height) return;
|
|
465
|
+
|
|
466
|
+
const iw = img.width;
|
|
467
|
+
const ih = img.height;
|
|
468
|
+
const r = Math.min(w / iw, h / ih);
|
|
469
|
+
let nw = iw * r;
|
|
470
|
+
let nh = ih * r;
|
|
471
|
+
let cx, cy, cw, ch, ar = 1;
|
|
472
|
+
|
|
473
|
+
// Decide qual dimensão ajustar
|
|
474
|
+
if (nw < w) ar = w / nw;
|
|
475
|
+
if (Math.abs(ar - 1) < 1e-14 && nh < h) ar = h / nh;
|
|
476
|
+
nw *= ar;
|
|
477
|
+
nh *= ar;
|
|
478
|
+
|
|
479
|
+
// Calcula a posição de recorte
|
|
480
|
+
cw = iw / (nw / w);
|
|
481
|
+
ch = ih / (nh / h);
|
|
482
|
+
cx = (iw - cw) * offsetX;
|
|
483
|
+
cy = (ih - ch) * offsetY;
|
|
484
|
+
|
|
485
|
+
// Garante que os valores não sejam negativos
|
|
486
|
+
if (cx < 0) cx = 0;
|
|
487
|
+
if (cy < 0) cy = 0;
|
|
488
|
+
if (cw > iw) cw = iw;
|
|
489
|
+
if (ch > ih) ch = ih;
|
|
490
|
+
|
|
491
|
+
ctx.drawImage(img, cx, cy, cw, ch, x, y, w, h);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Quebra texto em múltiplas linhas centralizadas
|
|
496
|
+
* @param {Object} ctx - Contexto de renderização 2D
|
|
497
|
+
* @param {string} text - Texto a ser quebrado
|
|
498
|
+
* @param {number} x - Posição X central
|
|
499
|
+
* @param {number} y - Posição Y inicial
|
|
500
|
+
* @param {number} maxWidth - Largura máxima
|
|
501
|
+
* @param {number} lineHeight - Altura da linha
|
|
502
|
+
* @returns {number} - Posição Y final
|
|
503
|
+
*/
|
|
504
|
+
function wrapTextCentered(ctx, text, x, y, maxWidth, lineHeight) {
|
|
505
|
+
const words = text.split(" ");
|
|
506
|
+
let line = "";
|
|
507
|
+
let currentY = y;
|
|
508
|
+
const lines = [];
|
|
509
|
+
|
|
510
|
+
// Primeiro, calcula todas as linhas
|
|
511
|
+
for (let n = 0; n < words.length; n++) {
|
|
512
|
+
const testLine = line + words[n] + " ";
|
|
513
|
+
const metrics = ctx.measureText(testLine);
|
|
514
|
+
const testWidth = metrics.width;
|
|
515
|
+
|
|
516
|
+
if (testWidth > maxWidth && n > 0) {
|
|
517
|
+
lines.push(line.trim());
|
|
518
|
+
line = words[n] + " ";
|
|
519
|
+
} else {
|
|
520
|
+
line = testLine;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
lines.push(line.trim());
|
|
524
|
+
|
|
525
|
+
// Ajusta Y para centralizar verticalmente
|
|
526
|
+
const totalHeight = lines.length * lineHeight;
|
|
527
|
+
currentY = y - totalHeight / 2 + lineHeight / 2;
|
|
528
|
+
|
|
529
|
+
// Desenha cada linha centralizada
|
|
530
|
+
const originalAlign = ctx.textAlign;
|
|
531
|
+
ctx.textAlign = "center";
|
|
532
|
+
|
|
533
|
+
for (const l of lines) {
|
|
534
|
+
ctx.fillText(l, x, currentY);
|
|
535
|
+
currentY += lineHeight;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
ctx.textAlign = originalAlign;
|
|
539
|
+
return currentY;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Ajusta o brilho de uma cor
|
|
544
|
+
* @param {string} color - Cor em formato hexadecimal ou rgba
|
|
545
|
+
* @param {number} amount - Quantidade de ajuste (-1 a 1)
|
|
546
|
+
* @returns {string} - Cor ajustada
|
|
547
|
+
*/
|
|
548
|
+
function adjustColor(color, amount) {
|
|
549
|
+
// Se for rgba, extrai os valores
|
|
550
|
+
if (color.startsWith("rgba")) {
|
|
551
|
+
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
|
552
|
+
if (match) {
|
|
553
|
+
let r = parseInt(match[1]);
|
|
554
|
+
let g = parseInt(match[2]);
|
|
555
|
+
let b = parseInt(match[3]);
|
|
556
|
+
const a = match[4] ? parseFloat(match[4]) : 1;
|
|
557
|
+
|
|
558
|
+
r = Math.max(0, Math.min(255, r + Math.round(255 * amount)));
|
|
559
|
+
g = Math.max(0, Math.min(255, g + Math.round(255 * amount)));
|
|
560
|
+
b = Math.max(0, Math.min(255, b + Math.round(255 * amount)));
|
|
561
|
+
|
|
562
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Se for hexadecimal
|
|
567
|
+
let hex = color.replace("#", "");
|
|
568
|
+
|
|
569
|
+
// Expande formato abreviado
|
|
570
|
+
if (hex.length === 3) {
|
|
571
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
let r = parseInt(hex.substring(0, 2), 16);
|
|
575
|
+
let g = parseInt(hex.substring(2, 4), 16);
|
|
576
|
+
let b = parseInt(hex.substring(4, 6), 16);
|
|
577
|
+
|
|
578
|
+
r = Math.max(0, Math.min(255, r + Math.round(255 * amount)));
|
|
579
|
+
g = Math.max(0, Math.min(255, g + Math.round(255 * amount)));
|
|
580
|
+
b = Math.max(0, Math.min(255, b + Math.round(255 * amount)));
|
|
581
|
+
|
|
582
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Gera uma cor aleatória
|
|
587
|
+
* @param {number} minBrightness - Brilho mínimo (0-255)
|
|
588
|
+
* @param {number} maxBrightness - Brilho máximo (0-255)
|
|
589
|
+
* @returns {string} - Cor hexadecimal aleatória
|
|
590
|
+
*/
|
|
591
|
+
function getRandomColor(minBrightness = 0, maxBrightness = 255) {
|
|
592
|
+
const range = maxBrightness - minBrightness;
|
|
593
|
+
const r = Math.floor(Math.random() * range + minBrightness);
|
|
594
|
+
const g = Math.floor(Math.random() * range + minBrightness);
|
|
595
|
+
const b = Math.floor(Math.random() * range + minBrightness);
|
|
596
|
+
|
|
597
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Desenha uma estrela
|
|
602
|
+
* @param {Object} ctx - Contexto de renderização 2D
|
|
603
|
+
* @param {number} cx - Centro X
|
|
604
|
+
* @param {number} cy - Centro Y
|
|
605
|
+
* @param {number} spikes - Número de pontas
|
|
606
|
+
* @param {number} outerRadius - Raio externo
|
|
607
|
+
* @param {number} innerRadius - Raio interno
|
|
608
|
+
* @param {string} fillColor - Cor de preenchimento
|
|
609
|
+
* @param {string} strokeColor - Cor do contorno (opcional)
|
|
610
|
+
* @param {number} lineWidth - Largura do contorno (opcional)
|
|
611
|
+
*/
|
|
612
|
+
function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius, fillColor, strokeColor = null, lineWidth = 1) {
|
|
613
|
+
let rot = Math.PI / 2 * 3;
|
|
614
|
+
let x = cx;
|
|
615
|
+
let y = cy;
|
|
616
|
+
const step = Math.PI / spikes;
|
|
617
|
+
|
|
618
|
+
ctx.beginPath();
|
|
619
|
+
ctx.moveTo(cx, cy - outerRadius);
|
|
620
|
+
|
|
621
|
+
for (let i = 0; i < spikes; i++) {
|
|
622
|
+
x = cx + Math.cos(rot) * outerRadius;
|
|
623
|
+
y = cy + Math.sin(rot) * outerRadius;
|
|
624
|
+
ctx.lineTo(x, y);
|
|
625
|
+
rot += step;
|
|
626
|
+
|
|
627
|
+
x = cx + Math.cos(rot) * innerRadius;
|
|
628
|
+
y = cy + Math.sin(rot) * innerRadius;
|
|
629
|
+
ctx.lineTo(x, y);
|
|
630
|
+
rot += step;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
ctx.lineTo(cx, cy - outerRadius);
|
|
634
|
+
ctx.closePath();
|
|
635
|
+
|
|
636
|
+
if (fillColor) {
|
|
637
|
+
ctx.fillStyle = fillColor;
|
|
638
|
+
ctx.fill();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (strokeColor) {
|
|
642
|
+
ctx.strokeStyle = strokeColor;
|
|
643
|
+
ctx.lineWidth = lineWidth;
|
|
644
|
+
ctx.stroke();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Trunca texto para caber em uma largura máxima
|
|
650
|
+
* @param {Object} ctx - Contexto de renderização 2D
|
|
651
|
+
* @param {string} text - Texto a ser truncado
|
|
652
|
+
* @param {number} maxWidth - Largura máxima
|
|
653
|
+
* @param {string} ellipsis - Caracteres de truncamento (padrão: "...")
|
|
654
|
+
* @returns {string} - Texto truncado
|
|
655
|
+
*/
|
|
656
|
+
function truncateText(ctx, text, maxWidth, ellipsis = "...") {
|
|
657
|
+
if (!text) return "";
|
|
658
|
+
|
|
659
|
+
const textWidth = ctx.measureText(text).width;
|
|
660
|
+
if (textWidth <= maxWidth) return text;
|
|
661
|
+
|
|
662
|
+
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
|
663
|
+
let truncated = text;
|
|
664
|
+
|
|
665
|
+
while (ctx.measureText(truncated).width + ellipsisWidth > maxWidth && truncated.length > 0) {
|
|
666
|
+
truncated = truncated.slice(0, -1);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return truncated + ellipsis;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// --- Exportações ---
|
|
673
|
+
module.exports = {
|
|
674
|
+
// Funções de validação
|
|
675
|
+
isValidHexColor,
|
|
676
|
+
|
|
677
|
+
// Funções de gerenciamento de fontes
|
|
678
|
+
registerDefaultFonts,
|
|
679
|
+
registerFontIfNeeded,
|
|
680
|
+
|
|
681
|
+
// Funções de manipulação de imagens
|
|
682
|
+
loadImageWithAxios,
|
|
683
|
+
loadImage,
|
|
684
|
+
encodeToBuffer,
|
|
685
|
+
getBufferFromImage,
|
|
686
|
+
drawImageProp,
|
|
687
|
+
|
|
688
|
+
// Funções de desenho
|
|
689
|
+
roundRect,
|
|
690
|
+
wrapText,
|
|
691
|
+
wrapTextCentered,
|
|
692
|
+
applyTextShadow,
|
|
693
|
+
clearShadow,
|
|
694
|
+
createLinearGradient,
|
|
695
|
+
drawStar,
|
|
696
|
+
|
|
697
|
+
// Funções de cor
|
|
698
|
+
hexToRgba,
|
|
699
|
+
adjustColor,
|
|
700
|
+
getRandomColor,
|
|
701
|
+
|
|
702
|
+
// Funções de formatação
|
|
703
|
+
formatTime,
|
|
704
|
+
formatNumber,
|
|
705
|
+
truncateText,
|
|
706
|
+
|
|
707
|
+
// Constantes
|
|
708
|
+
DEFAULT_FONT_FAMILY
|
|
709
|
+
};
|
|
710
|
+
|