neekuro 2.2.0 → 2.3.0

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.
@@ -1,7 +1,7 @@
1
- 'use strict';
2
- const Canvas = require('canvas');
3
- const NekoError = require('../errors/Error.js');
4
- const hex_reg = require('hex-color-regex')({ strict: true })
1
+ 'use strict';
2
+ const Canvas = require('canvas');
3
+ const NekoError = require('../errors/Error.js');
4
+ const hex_reg = require('hex-color-regex')({ strict: true })
5
5
  const path = require('path');
6
6
  const fs = require('fs');
7
7
  const { loadImageSource, parseRemoteURL, validateImageBuffer } = require('../../utils/imageLoader.js');
@@ -11,112 +11,112 @@ const DEFAULT_HEIGHT = 520;
11
11
  const MIN_DIMENSION = 128;
12
12
  const MAX_DIMENSION = 4096;
13
13
  const registeredFonts = new Set();
14
-
15
- /**
16
- * @typedef {import('../../typings').FontNames} FontNames
17
- * @typedef {import('../../typings').WelcomeTextData} WelcomeTextData
18
- * @typedef {import('../../typings').WelcomeAvatarData} WelcomeAvatarData
19
- */
20
-
21
- /**
22
- * Clase para generar imágenes de bienvenida personalizadas para Discord
23
- * @class Welcome
24
- * @example
25
- * const Welcome = require('neekuro').Welcome;
26
- * const welcome = new Welcome()
27
- * .setResolution(1200, 600)
28
- * .setFont('arial')
29
- * .setTitle('¡Bienvenido!', { text_color: '#FF5733' })
30
- * .setDescription('Al servidor de la comunidad', { font_size: 24 });
31
- */
32
- class Welcome {
33
-
34
- /**
35
- * Construye una imagen
36
- */
37
- constructor() {
38
- /**
39
- * @property {Object} data - Configuración actual de la imagen
14
+
15
+ /**
16
+ * @typedef {import('../../typings').FontNames} FontNames
17
+ * @typedef {import('../../typings').WelcomeTextData} WelcomeTextData
18
+ * @typedef {import('../../typings').WelcomeAvatarData} WelcomeAvatarData
19
+ */
20
+
21
+ /**
22
+ * Clase para generar imágenes de bienvenida personalizadas para Discord
23
+ * @class Welcome
24
+ * @example
25
+ * const Welcome = require('neekuro').Welcome;
26
+ * const welcome = new Welcome()
27
+ * .setResolution(1200, 600)
28
+ * .setFont('arial')
29
+ * .setTitle('¡Bienvenido!', { text_color: '#FF5733' })
30
+ * .setDescription('Al servidor de la comunidad', { font_size: 24 });
31
+ */
32
+ class Welcome {
33
+
34
+ /**
35
+ * Construye una imagen
36
+ */
37
+ constructor() {
38
+ /**
39
+ * @property {Object} data - Configuración actual de la imagen
40
40
  * @property {string} data.font - Fuente actualmente seleccionada
41
41
  * @property {'center'|'manual'} data.layout - Distribución de los elementos
42
- * @property {number} data.width - Ancho actual de la imagen
43
- * @property {number} data.height - Alto actual de la imagen
44
- * @property {Object} data.background - Configuración del fondo
45
- * @property {'color'|'image'} data.background.type - Tipo de fondo
46
- * @property {string} data.background.value - Color o ruta de imagen
47
- * @property {Object} data.avatar - Configuración del avatar
48
- * @property {?string} data.avatar.source - Ruta a la imagen del avatar
49
- * @property {number} data.avatar.x - Posición X del avatar
50
- * @property {number} data.avatar.y - Posición Y del avatar
51
- * @property {number} data.avatar.radio - Radio para avatar circular
52
- * @property {string} data.avatar.border - Color del borde del avatar
53
- * @property {Object} data.title - Configuración del título
54
- * @property {?string} data.title.content - Contenido del título
55
- * @property {number} data.title.x - Posición X del título
56
- * @property {number} data.title.y - Posición Y del título
57
- * @property {number} data.title.font_size - Tamaño de fuente del título
58
- * @property {string} data.title.text_color - Color del texto del título
59
- * @property {Object} data.description - Configuración de la descripción
60
- * @property {?string} data.description.content - Contenido de la descripción
61
- * @property {number} data.description.x - Posición X de la descripción
62
- * @property {number} data.description.y - Posición Y de la descripción
63
- * @property {number} data.description.font_size - Tamaño de fuente de la descripción
64
- * @property {string} data.description.text_color - Color del texto de la descripción
65
- */
42
+ * @property {number} data.width - Ancho actual de la imagen
43
+ * @property {number} data.height - Alto actual de la imagen
44
+ * @property {Object} data.background - Configuración del fondo
45
+ * @property {'color'|'image'} data.background.type - Tipo de fondo
46
+ * @property {string} data.background.value - Color o ruta de imagen
47
+ * @property {Object} data.avatar - Configuración del avatar
48
+ * @property {?string} data.avatar.source - Ruta a la imagen del avatar
49
+ * @property {number} data.avatar.x - Posición X del avatar
50
+ * @property {number} data.avatar.y - Posición Y del avatar
51
+ * @property {number} data.avatar.radio - Radio para avatar circular
52
+ * @property {string} data.avatar.border - Color del borde del avatar
53
+ * @property {Object} data.title - Configuración del título
54
+ * @property {?string} data.title.content - Contenido del título
55
+ * @property {number} data.title.x - Posición X del título
56
+ * @property {number} data.title.y - Posición Y del título
57
+ * @property {number} data.title.font_size - Tamaño de fuente del título
58
+ * @property {string} data.title.text_color - Color del texto del título
59
+ * @property {Object} data.description - Configuración de la descripción
60
+ * @property {?string} data.description.content - Contenido de la descripción
61
+ * @property {number} data.description.x - Posición X de la descripción
62
+ * @property {number} data.description.y - Posición Y de la descripción
63
+ * @property {number} data.description.font_size - Tamaño de fuente de la descripción
64
+ * @property {string} data.description.text_color - Color del texto de la descripción
65
+ */
66
66
  this.data = {
67
67
  font: 'arial',
68
68
  layout: 'center',
69
69
  width: DEFAULT_WIDTH,
70
70
  height: DEFAULT_HEIGHT,
71
- background: {
72
- type: 'color',
73
- value: '#23272A'
74
- },
75
- avatar: {
76
- source: null,
77
- x: 448,
78
- y: 80,
79
- radio: 120,
80
- border: "#F7F7F7"
81
- },
82
- title: {
83
- content: "¡Bienvenido!",
84
- x: 565,
85
- y: 400,
86
- font_size: 55,
87
- text_color: '#FFFFFF'
88
- },
89
- description: {
90
- content: 'Al mejor servidor de discord',
91
- x: 590,
92
- y: 450,
93
- font_size: 35,
94
- text_color: "#F7F7F7"
95
- }
71
+ background: {
72
+ type: 'color',
73
+ value: '#23272A'
74
+ },
75
+ avatar: {
76
+ source: null,
77
+ x: 448,
78
+ y: 80,
79
+ radio: 120,
80
+ border: "#F7F7F7"
81
+ },
82
+ title: {
83
+ content: "¡Bienvenido!",
84
+ x: 565,
85
+ y: 400,
86
+ font_size: 55,
87
+ text_color: '#FFFFFF'
88
+ },
89
+ description: {
90
+ content: 'Al mejor servidor de discord',
91
+ x: 590,
92
+ y: 450,
93
+ font_size: 35,
94
+ text_color: "#F7F7F7"
95
+ }
96
96
  }
97
97
  this.setFont('arial');
98
98
  }
99
-
100
- /**
101
- * Establece la resolución de la imagen de bienvenida
102
- * @param {number|'default'} width - Ancho en píxeles o 'default' para 1024px
103
- * @param {number|'default'} height - Alto en píxeles o 'default' para 450px
104
- * @returns {Welcome}
105
- * @throws {NekoError} Si los parámetros son inválidos
106
- * @example
107
- * // Establecer resolución personalizada
108
- * welcome.setResolution(1920, 1080);
109
- *
110
- * // Restablecer a valores por defecto
111
- * welcome.setResolution('default', 'default');
112
- */
99
+
100
+ /**
101
+ * Establece la resolución de la imagen de bienvenida
102
+ * @param {number|'default'} width - Ancho en píxeles o 'default' para 1140px
103
+ * @param {number|'default'} height - Alto en píxeles o 'default' para 520px
104
+ * @returns {Welcome}
105
+ * @throws {NekoError} Si los parámetros son inválidos
106
+ * @example
107
+ * // Establecer resolución personalizada
108
+ * welcome.setResolution(1920, 1080);
109
+ *
110
+ * // Restablecer a valores por defecto
111
+ * welcome.setResolution('default', 'default');
112
+ */
113
113
  setResolution(width, height) {
114
- if (!width) throw new NekoError('ResolutionError', 'el parametro <width> no puede quedar vacio');
115
- if (!height) throw new NekoError('ResolutionError', 'el parametro <height> no puede quedar vacio');
116
-
114
+ if (!width) throw new NekoError('ResolutionError', 'el parametro <width> no puede quedar vacio');
115
+ if (!height) throw new NekoError('ResolutionError', 'el parametro <height> no puede quedar vacio');
116
+
117
117
  this.data.width = width === 'default' ? DEFAULT_WIDTH : validateDimension(width, 'width');
118
118
  this.data.height = height === 'default' ? DEFAULT_HEIGHT : validateDimension(height, 'height');
119
-
119
+
120
120
  return this
121
121
  }
122
122
 
@@ -132,309 +132,309 @@ class Welcome {
132
132
  this.data.layout = layout;
133
133
  return this;
134
134
  }
135
-
136
- /**
137
- * Configura la tipografía para el texto de la imagen
138
- * @param {FontNames} name - Nombre de la fuente predefinida o 'custom' para personalizada
139
- * @param {string} [dir] - Ruta al archivo .ttf (requerido si name='custom')
140
- * @returns {Welcome}
141
- * @throws {NekoError} Si la fuente no existe o los parámetros son inválidos
142
- * @example
143
- * // Usar fuente predefinida
144
- * welcome.setFont('calibri');
145
- *
146
- * // Usar fuente personalizada
147
- * welcome.setFont('custom', './fonts/mi_fuente.ttf');
148
- */
149
- setFont(name, dir) {
150
- let ruta = null;
151
-
152
- if (!name) throw new NekoError('FontError', 'el parametro <name> no puede quedar vacio');
153
- if (typeof name != 'string') throw new NekoError('TypeError', 'el parametro <name> solo acepta tipo \'string\', recibió ' + typeof name);
154
-
155
- switch (name) {
156
- case 'arial':
157
- ruta = path.join(__dirname, '../../assets/arial.ttf');
158
- break;
159
- case 'calibri':
160
- ruta = path.join(__dirname, '../../assets/calibri.ttf');
161
- break;
162
- case 'custom':
163
- if (!dir) throw new NekoError('FontError', 'el parametro <dir> no puede quedar vacio en un tipo de fuente customizada');
164
- if (typeof dir != 'string') throw new NekoError('TypeError', 'el parametro <dir> solo acepta tipo \'string\', recibió ' + typeof dir);
135
+
136
+ /**
137
+ * Configura la tipografía para el texto de la imagen
138
+ * @param {FontNames} name - Nombre de la fuente predefinida o 'custom' para personalizada
139
+ * @param {string} [dir] - Ruta al archivo .ttf (requerido si name='custom')
140
+ * @returns {Welcome}
141
+ * @throws {NekoError} Si la fuente no existe o los parámetros son inválidos
142
+ * @example
143
+ * // Usar fuente predefinida
144
+ * welcome.setFont('calibri');
145
+ *
146
+ * // Usar fuente personalizada
147
+ * welcome.setFont('custom', './fonts/mi_fuente.ttf');
148
+ */
149
+ setFont(name, dir) {
150
+ let ruta = null;
151
+
152
+ if (!name) throw new NekoError('FontError', 'el parametro <name> no puede quedar vacio');
153
+ if (typeof name != 'string') throw new NekoError('TypeError', 'el parametro <name> solo acepta tipo \'string\', recibió ' + typeof name);
154
+
155
+ switch (name) {
156
+ case 'arial':
157
+ ruta = path.join(__dirname, '../../assets/arial.ttf');
158
+ break;
159
+ case 'calibri':
160
+ ruta = path.join(__dirname, '../../assets/calibri.ttf');
161
+ break;
162
+ case 'custom':
163
+ if (!dir) throw new NekoError('FontError', 'el parametro <dir> no puede quedar vacio en un tipo de fuente customizada');
164
+ if (typeof dir != 'string') throw new NekoError('TypeError', 'el parametro <dir> solo acepta tipo \'string\', recibió ' + typeof dir);
165
165
  if (path.extname(dir).toLowerCase() !== '.ttf') throw new NekoError('FontError', 'se necesita un archivo TTF en el parametro <dir>')
166
166
  ruta = path.resolve(dir);
167
- break;
168
- default: throw new NekoError('FontError', 'el parametro <name> no acepta ningun tipo diferente a los preterminados');
169
- }
170
- try {
167
+ break;
168
+ default: throw new NekoError('FontError', 'el parametro <name> no acepta ningun tipo diferente a los preterminados');
169
+ }
170
+ try {
171
171
  if (!fs.existsSync(ruta)) throw new Error('Font not found');
172
172
  const fontKey = `${name}:${ruta}`;
173
173
  if (!registeredFonts.has(fontKey)) {
174
174
  Canvas.registerFont(ruta, { family: name });
175
175
  registeredFonts.add(fontKey);
176
176
  }
177
- } catch (error) {
178
- throw new NekoError('DirError', 'no se puede encontrar el directorio especificado')
179
- }
180
-
181
- this.data.font = name
182
- return this;
183
- }
184
-
185
- /**
186
- * Agrega un título a la imagen de bienvenida
187
- * @param {string} text - Texto del título
188
- * @param {WelcomeTextData} data - Configuración del título
189
- * @throws {NekoError} Arroja un error si parámetros son inválidos
190
- * @returns {Welcome}
191
- */
192
- setTitle(text, data) {
193
- // Validaciones del texto
194
- if (!text) {
195
- throw new NekoError('TitleError', 'El parámetro <text> no puede estar vacío');
196
- }
197
- if (typeof text !== 'string') {
198
- throw new NekoError('TypeError', `El parámetro <text> solo acepta tipo 'string', recibió ${typeof text}`);
199
- }
200
-
201
- this.data.title.content = text;
202
-
203
- // Validaciones para las propiedades adicionales
204
- if (data) {
205
- // Validación para text_color
206
- if (data.text_color) {
207
- if (typeof data.text_color !== 'string') {
208
- throw new NekoError('TypeError', `La propiedad <text_color> solo acepta tipo 'string', recibió ${typeof data.text_color}`);
209
- }
210
- if (!data.text_color.startsWith('#')) {
211
- throw new NekoError('TitleError', 'El valor de <text_color> debe comenzar con \'#\'');
212
- }
213
- if (!hex_reg.test(data.text_color)) {
214
- throw new NekoError('TitleError', 'El valor de <text_color> no es un color válido');
215
- }
216
- this.data.title.text_color = data.text_color;
217
- }
218
-
219
- // validacion del tamaño de texto
220
- if (data.font_size) {
221
- if (typeof data.font_size !== 'number') {
222
- throw new NekoError('TypeError', `La propiedad <font_size> solo acepta tipo 'number', recibió ${typeof data.font_size}`);
223
- }
224
- if (data.font_size <= 0) {
225
- throw new NekoError('TitleError', 'El valor de <font_size> debe ser mayor que 0');
226
- }
227
- this.data.title.font_size = data.font_size;
228
- }
229
-
230
- // validacion de posiciones
177
+ } catch (error) {
178
+ throw new NekoError('DirError', 'no se puede encontrar el directorio especificado')
179
+ }
180
+
181
+ this.data.font = name
182
+ return this;
183
+ }
184
+
185
+ /**
186
+ * Agrega un título a la imagen de bienvenida
187
+ * @param {string} text - Texto del título
188
+ * @param {WelcomeTextData} data - Configuración del título
189
+ * @throws {NekoError} Arroja un error si parámetros son inválidos
190
+ * @returns {Welcome}
191
+ */
192
+ setTitle(text, data) {
193
+ // Validaciones del texto
194
+ if (!text) {
195
+ throw new NekoError('TitleError', 'El parámetro <text> no puede estar vacío');
196
+ }
197
+ if (typeof text !== 'string') {
198
+ throw new NekoError('TypeError', `El parámetro <text> solo acepta tipo 'string', recibió ${typeof text}`);
199
+ }
200
+
201
+ this.data.title.content = text;
202
+
203
+ // Validaciones para las propiedades adicionales
204
+ if (data) {
205
+ // Validación para text_color
206
+ if (data.text_color) {
207
+ if (typeof data.text_color !== 'string') {
208
+ throw new NekoError('TypeError', `La propiedad <text_color> solo acepta tipo 'string', recibió ${typeof data.text_color}`);
209
+ }
210
+ if (!data.text_color.startsWith('#')) {
211
+ throw new NekoError('TitleError', 'El valor de <text_color> debe comenzar con \'#\'');
212
+ }
213
+ if (!hex_reg.test(data.text_color)) {
214
+ throw new NekoError('TitleError', 'El valor de <text_color> no es un color válido');
215
+ }
216
+ this.data.title.text_color = data.text_color;
217
+ }
218
+
219
+ // validacion del tamaño de texto
220
+ if (data.font_size) {
221
+ if (typeof data.font_size !== 'number') {
222
+ throw new NekoError('TypeError', `La propiedad <font_size> solo acepta tipo 'number', recibió ${typeof data.font_size}`);
223
+ }
224
+ if (data.font_size <= 0) {
225
+ throw new NekoError('TitleError', 'El valor de <font_size> debe ser mayor que 0');
226
+ }
227
+ this.data.title.font_size = data.font_size;
228
+ }
229
+
230
+ // validacion de posiciones
231
231
  if (data.x !== undefined) {
232
- if (typeof data.x !== 'number') {
233
- throw new NekoError('TypeError', `La propiedad <x> solo acepta tipo 'number', recibió ${typeof data.x}`);
234
- }
232
+ if (typeof data.x !== 'number') {
233
+ throw new NekoError('TypeError', `La propiedad <x> solo acepta tipo 'number', recibió ${typeof data.x}`);
234
+ }
235
235
  this.data.title.x = data.x;
236
236
  this.data.layout = 'manual';
237
- }
238
-
237
+ }
238
+
239
239
  if (data.y !== undefined) {
240
- if (typeof data.y !== 'number') {
241
- throw new NekoError('TypeError', `La propiedad <y> solo acepta tipo 'number', recibió ${typeof data.y}`);
242
- }
240
+ if (typeof data.y !== 'number') {
241
+ throw new NekoError('TypeError', `La propiedad <y> solo acepta tipo 'number', recibió ${typeof data.y}`);
242
+ }
243
243
  this.data.title.y = data.y;
244
244
  this.data.layout = 'manual';
245
- }
246
- }
247
-
248
- return this
249
- }
250
-
251
- /**
252
- * Agrega una descripción a la imagen de bienvenida
253
- * @param {string} text - Texto de la descripción
254
- * @param {WelcomeTextData} data - Configuración de la descripción
255
- * @throws {NekoError} Arroja un error si parámetros son inválidos
256
- * @returns {Welcome}
257
- */
258
- setDescription(text, data) {
259
- // Validaciones del texto
260
- if (!text) {
261
- throw new NekoError('DescriptionError', 'El parámetro <text> no puede estar vacío');
262
- }
263
- if (typeof text !== 'string') {
264
- throw new NekoError('TypeError', `El parámetro <text> solo acepta tipo 'string', recibió ${typeof text}`);
265
- }
266
-
267
- this.data.description.content = text;
268
-
269
- // Validaciones para las propiedades adicionales
270
- if (data) {
271
- // Validación para text_color
272
- if (data.text_color) {
273
- if (typeof data.text_color !== 'string') {
274
- throw new NekoError('TypeError', `La propiedad <text_color> solo acepta tipo 'string', recibió ${typeof data.text_color}`);
275
- }
276
- if (!data.text_color.startsWith('#')) {
277
- throw new NekoError('DescriptionError', 'El valor de <text_color> debe comenzar con \'#\'');
278
- }
279
- if (!hex_reg.test(data.text_color)) {
280
- throw new NekoError('DescriptionError', 'El valor de <text_color> no es un color válido');
281
- }
282
- this.data.description.text_color = data.text_color;
283
- }
284
-
285
- // validacion del tamaño de texto
286
- if (data.font_size) {
287
- if (typeof data.font_size !== 'number') {
288
- throw new NekoError('TypeError', `La propiedad <font_size> solo acepta tipo 'number', recibió ${typeof data.font_size}`);
289
- }
290
- if (data.font_size <= 0) {
291
- throw new NekoError('DescriptionError', 'El valor de <font_size> debe ser mayor que 0');
292
- }
293
- this.data.description.font_size = data.font_size;
294
- }
295
-
296
- // validacion de posiciones
245
+ }
246
+ }
247
+
248
+ return this
249
+ }
250
+
251
+ /**
252
+ * Agrega una descripción a la imagen de bienvenida
253
+ * @param {string} text - Texto de la descripción
254
+ * @param {WelcomeTextData} data - Configuración de la descripción
255
+ * @throws {NekoError} Arroja un error si parámetros son inválidos
256
+ * @returns {Welcome}
257
+ */
258
+ setDescription(text, data) {
259
+ // Validaciones del texto
260
+ if (!text) {
261
+ throw new NekoError('DescriptionError', 'El parámetro <text> no puede estar vacío');
262
+ }
263
+ if (typeof text !== 'string') {
264
+ throw new NekoError('TypeError', `El parámetro <text> solo acepta tipo 'string', recibió ${typeof text}`);
265
+ }
266
+
267
+ this.data.description.content = text;
268
+
269
+ // Validaciones para las propiedades adicionales
270
+ if (data) {
271
+ // Validación para text_color
272
+ if (data.text_color) {
273
+ if (typeof data.text_color !== 'string') {
274
+ throw new NekoError('TypeError', `La propiedad <text_color> solo acepta tipo 'string', recibió ${typeof data.text_color}`);
275
+ }
276
+ if (!data.text_color.startsWith('#')) {
277
+ throw new NekoError('DescriptionError', 'El valor de <text_color> debe comenzar con \'#\'');
278
+ }
279
+ if (!hex_reg.test(data.text_color)) {
280
+ throw new NekoError('DescriptionError', 'El valor de <text_color> no es un color válido');
281
+ }
282
+ this.data.description.text_color = data.text_color;
283
+ }
284
+
285
+ // validacion del tamaño de texto
286
+ if (data.font_size) {
287
+ if (typeof data.font_size !== 'number') {
288
+ throw new NekoError('TypeError', `La propiedad <font_size> solo acepta tipo 'number', recibió ${typeof data.font_size}`);
289
+ }
290
+ if (data.font_size <= 0) {
291
+ throw new NekoError('DescriptionError', 'El valor de <font_size> debe ser mayor que 0');
292
+ }
293
+ this.data.description.font_size = data.font_size;
294
+ }
295
+
296
+ // validacion de posiciones
297
297
  if (data.x !== undefined) {
298
- if (typeof data.x !== 'number') {
299
- throw new NekoError('TypeError', `La propiedad <x> solo acepta tipo 'number', recibió ${typeof data.x}`);
300
- }
298
+ if (typeof data.x !== 'number') {
299
+ throw new NekoError('TypeError', `La propiedad <x> solo acepta tipo 'number', recibió ${typeof data.x}`);
300
+ }
301
301
  this.data.description.x = data.x;
302
302
  this.data.layout = 'manual';
303
- }
304
-
303
+ }
304
+
305
305
  if (data.y !== undefined) {
306
- if (typeof data.y !== 'number') {
307
- throw new NekoError('TypeError', `La propiedad <y> solo acepta tipo 'number', recibió ${typeof data.y}`);
308
- }
306
+ if (typeof data.y !== 'number') {
307
+ throw new NekoError('TypeError', `La propiedad <y> solo acepta tipo 'number', recibió ${typeof data.y}`);
308
+ }
309
309
  this.data.description.y = data.y;
310
310
  this.data.layout = 'manual';
311
- }
312
- }
313
-
314
- return this
315
- }
316
-
317
- /**
318
- * Establece el fondo de la imagen de bienvenida (color o imagen)
319
- * @param {'color'|'image'} type - Tipo de fondo ('color' o 'image')
320
- * @param {string|Buffer} value - Color hexadecimal (ej. '#FF5733') o imagen (URL/Buffer)
321
- * @returns {Welcome}
322
- * @throws {NekoError} Si los parámetros son inválidos
323
- * @example
324
- * // Fondo con color
325
- * welcome.setBackground('color', '#23272A');
326
- *
327
- * // Fondo con imagen desde URL
328
- * welcome.setBackground('image', 'https://ejemplo.com/fondo.jpg');
329
- *
330
- * // Fondo con imagen desde Buffer
331
- * const imageBuffer = fs.readFileSync('fondo.jpg');
332
- * welcome.setBackground('image', imageBuffer);
333
- */
334
- setBackground(type, value) {
335
- if (!type) throw new NekoError('BackgroundError', 'El parámetro <type> no puede estar vacío');
336
- if (typeof type != 'string') throw new NekoError('TypeError', `El parámetro <type> debe ser string, recibió ${typeof type}`);
337
- if (!['color', 'image'].includes(type)) throw new NekoError('BackgroundError', 'El tipo de fondo debe ser "color" o "image"');
338
-
339
- if (!value) throw new NekoError('BackgroundError', 'El parámetro <value> no puede estar vacío');
340
-
341
- if (type == 'color') {
342
- if (typeof value != 'string') throw new NekoError('TypeError', 'Para fondo de color, <data> debe ser string');
343
- if (!value.startsWith('#')) throw new NekoError('BackgroundError', 'El color debe comenzar con \'#\'');
344
- if (!hex_reg.test(value)) throw new NekoError('BackgroundError', 'El valor no es un color hexadecimal válido');
345
-
346
- this.data.background = { type, value };
347
- return this;
348
- }
349
-
350
- if (type == 'image') {
351
- if (typeof value != 'string' && !Buffer.isBuffer(value)) {
352
- throw new NekoError('TypeError', 'Para fondo de imagen, <value> debe ser URL (string) o Buffer');
353
- }
354
-
355
- if (typeof value == 'string') {
311
+ }
312
+ }
313
+
314
+ return this
315
+ }
316
+
317
+ /**
318
+ * Establece el fondo de la imagen de bienvenida (color o imagen)
319
+ * @param {'color'|'image'} type - Tipo de fondo ('color' o 'image')
320
+ * @param {string|Buffer} value - Color hexadecimal (ej. '#FF5733') o imagen (URL/Buffer)
321
+ * @returns {Welcome}
322
+ * @throws {NekoError} Si los parámetros son inválidos
323
+ * @example
324
+ * // Fondo con color
325
+ * welcome.setBackground('color', '#23272A');
326
+ *
327
+ * // Fondo con imagen desde URL
328
+ * welcome.setBackground('image', 'https://ejemplo.com/fondo.jpg');
329
+ *
330
+ * // Fondo con imagen desde Buffer
331
+ * const imageBuffer = fs.readFileSync('fondo.jpg');
332
+ * welcome.setBackground('image', imageBuffer);
333
+ */
334
+ setBackground(type, value) {
335
+ if (!type) throw new NekoError('BackgroundError', 'El parámetro <type> no puede estar vacío');
336
+ if (typeof type != 'string') throw new NekoError('TypeError', `El parámetro <type> debe ser string, recibió ${typeof type}`);
337
+ if (!['color', 'image'].includes(type)) throw new NekoError('BackgroundError', 'El tipo de fondo debe ser "color" o "image"');
338
+
339
+ if (!value) throw new NekoError('BackgroundError', 'El parámetro <value> no puede estar vacío');
340
+
341
+ if (type == 'color') {
342
+ if (typeof value != 'string') throw new NekoError('TypeError', 'Para fondo de color, <data> debe ser string');
343
+ if (!value.startsWith('#')) throw new NekoError('BackgroundError', 'El color debe comenzar con \'#\'');
344
+ if (!hex_reg.test(value)) throw new NekoError('BackgroundError', 'El valor no es un color hexadecimal válido');
345
+
346
+ this.data.background = { type, value };
347
+ return this;
348
+ }
349
+
350
+ if (type == 'image') {
351
+ if (typeof value != 'string' && !Buffer.isBuffer(value)) {
352
+ throw new NekoError('TypeError', 'Para fondo de imagen, <value> debe ser URL (string) o Buffer');
353
+ }
354
+
355
+ if (typeof value == 'string') {
356
356
  parseRemoteURL(value, 'el fondo');
357
357
  } else {
358
358
  validateImageBuffer(value, 'el fondo');
359
- }
360
-
361
- this.data.background = { type, value };
362
- return this;
363
- }
364
- }
365
-
366
- /**
367
- * Establece el avatar y su configuración en la imagen de bienvenida
368
- * @param {string|Buffer} source - URL del avatar o Buffer de la imagen
369
- * @param {WelcomeAvatarData} [data] - Configuración opcional del avatar
370
- * @returns {Welcome}
371
- * @throws {NekoError} Si los parámetros son inválidos
372
- * @example
373
- * // Configuración básica con URL
374
- * welcome.setAvatar('https://example.com/avatar.jpg');
375
- *
376
- * // Configuración completa con Buffer
377
- * const avatarBuffer = fs.readFileSync('avatar.png');
378
- * welcome.setAvatar(avatarBuffer, {
379
- * x: 300,
380
- * y: 100,
381
- * border: '#FF5733',
382
- * radio: 120
383
- * });
384
- */
385
- setAvatar(source, data) {
386
- // Validación del source
387
- if (!source) {
388
- throw new NekoError('AvatarError', 'El parámetro <source> no puede estar vacío');
389
- }
390
-
391
- if (typeof source != 'string' && !Buffer.isBuffer(source)) {
392
- throw new NekoError('TypeError', 'El parámetro <source> debe ser string (URL) o Buffer');
393
- }
394
-
395
- if (typeof source === 'string') {
359
+ }
360
+
361
+ this.data.background = { type, value };
362
+ return this;
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Establece el avatar y su configuración en la imagen de bienvenida
368
+ * @param {string|Buffer} source - URL del avatar o Buffer de la imagen
369
+ * @param {WelcomeAvatarData} [data] - Configuración opcional del avatar
370
+ * @returns {Welcome}
371
+ * @throws {NekoError} Si los parámetros son inválidos
372
+ * @example
373
+ * // Configuración básica con URL
374
+ * welcome.setAvatar('https://example.com/avatar.jpg');
375
+ *
376
+ * // Configuración completa con Buffer
377
+ * const avatarBuffer = fs.readFileSync('avatar.png');
378
+ * welcome.setAvatar(avatarBuffer, {
379
+ * x: 300,
380
+ * y: 100,
381
+ * border: '#FF5733',
382
+ * radio: 120
383
+ * });
384
+ */
385
+ setAvatar(source, data) {
386
+ // Validación del source
387
+ if (!source) {
388
+ throw new NekoError('AvatarError', 'El parámetro <source> no puede estar vacío');
389
+ }
390
+
391
+ if (typeof source != 'string' && !Buffer.isBuffer(source)) {
392
+ throw new NekoError('TypeError', 'El parámetro <source> debe ser string (URL) o Buffer');
393
+ }
394
+
395
+ if (typeof source === 'string') {
396
396
  parseRemoteURL(source, 'el avatar');
397
- }
398
-
399
- if (Buffer.isBuffer(source)) {
397
+ }
398
+
399
+ if (Buffer.isBuffer(source)) {
400
400
  validateImageBuffer(source, 'el avatar');
401
- }
402
-
403
- this.data.avatar.source = source;
404
-
405
- if (data) {
401
+ }
402
+
403
+ this.data.avatar.source = source;
404
+
405
+ if (data) {
406
406
  if (data.x !== undefined) {
407
- if (typeof data.x != 'number') {
408
- throw new NekoError('TypeError', 'La propiedad <x> debe ser un número');
409
- }
410
- if (data.x < 0) {
411
- throw new NekoError('AvatarError', 'La posición X no puede ser negativa');
412
- }
407
+ if (typeof data.x != 'number') {
408
+ throw new NekoError('TypeError', 'La propiedad <x> debe ser un número');
409
+ }
410
+ if (data.x < 0) {
411
+ throw new NekoError('AvatarError', 'La posición X no puede ser negativa');
412
+ }
413
413
  this.data.avatar.x = data.x;
414
414
  this.data.layout = 'manual';
415
- }
416
-
415
+ }
416
+
417
417
  if (data.y !== undefined) {
418
- if (typeof data.y != 'number') {
419
- throw new NekoError('TypeError', 'La propiedad <y> debe ser un número');
420
- }
418
+ if (typeof data.y != 'number') {
419
+ throw new NekoError('TypeError', 'La propiedad <y> debe ser un número');
420
+ }
421
421
  this.data.avatar.y = data.y;
422
422
  this.data.layout = 'manual';
423
- }
424
-
425
- if (data.border) {
426
- if (typeof data.border != 'string') {
427
- throw new NekoError('TypeError', 'La propiedad <border> debe ser un string');
428
- }
429
- if (!data.border.startsWith('#')) {
430
- throw new NekoError('AvatarError', 'El color del borde debe comenzar con #');
431
- }
432
- if (!hex_reg.test(data.border)) {
433
- throw new NekoError('AvatarError', 'El color del borde no es un hexadecimal válido');
434
- }
435
- this.data.avatar.border = data.border;
436
- }
437
-
423
+ }
424
+
425
+ if (data.border) {
426
+ if (typeof data.border != 'string') {
427
+ throw new NekoError('TypeError', 'La propiedad <border> debe ser un string');
428
+ }
429
+ if (!data.border.startsWith('#')) {
430
+ throw new NekoError('AvatarError', 'El color del borde debe comenzar con #');
431
+ }
432
+ if (!hex_reg.test(data.border)) {
433
+ throw new NekoError('AvatarError', 'El color del borde no es un hexadecimal válido');
434
+ }
435
+ this.data.avatar.border = data.border;
436
+ }
437
+
438
438
  if (data.radio !== undefined) {
439
439
  if (typeof data.radio != 'number') {
440
440
  throw new NekoError('TypeError', 'La propiedad <radio> debe ser un número');
@@ -442,71 +442,71 @@ class Welcome {
442
442
  if (!Number.isFinite(data.radio) || data.radio <= 0 || data.radio > Math.min(this.data.width, this.data.height) / 2) {
443
443
  throw new NekoError('AvatarError', 'El radio debe ser positivo y caber dentro de la imagen');
444
444
  }
445
- this.data.avatar.radio = data.radio;
446
- }
447
- }
448
-
449
- return this;
450
- }
451
-
452
- /**
453
- * Genera la imagen de bienvenida con la configuracion actual
454
- * @returns {Promise<Buffer>} Buffer de la imagen generada
455
- * @throws {NekoError} Si faltan datos
456
- * @throws {Error} Si hay problemas al cargar imágenes
457
- */
458
- async build() {
459
- if (!this.data.avatar?.source) {
460
- throw new NekoError('BuildError', 'No se ha podido cargar un avatar. Usa el metodo .setAvatar()')
461
- }
462
- if (!this.data.title?.content) {
463
- throw new NekoError('BuildError', 'No se puede dar un titulo vacío. Usa el metodo .setTitle()')
464
- }
465
- if (!this.data.description?.content) {
466
- throw new NekoError('BuildError', 'No se puede dar una descripcion vacía. Usa el metodo .setDescription()')
467
- }
468
-
469
- try {
470
- const [bg, avatar] = await Promise.all([
445
+ this.data.avatar.radio = data.radio;
446
+ }
447
+ }
448
+
449
+ return this;
450
+ }
451
+
452
+ /**
453
+ * Genera la imagen de bienvenida con la configuracion actual
454
+ * @returns {Promise<Buffer>} Buffer de la imagen generada
455
+ * @throws {NekoError} Si faltan datos
456
+ * @throws {Error} Si hay problemas al cargar imágenes
457
+ */
458
+ async build() {
459
+ if (!this.data.avatar?.source) {
460
+ throw new NekoError('BuildError', 'No se ha podido cargar un avatar. Usa el metodo .setAvatar()')
461
+ }
462
+ if (!this.data.title?.content) {
463
+ throw new NekoError('BuildError', 'No se puede dar un titulo vacío. Usa el metodo .setTitle()')
464
+ }
465
+ if (!this.data.description?.content) {
466
+ throw new NekoError('BuildError', 'No se puede dar una descripcion vacía. Usa el metodo .setDescription()')
467
+ }
468
+
469
+ try {
470
+ const [bg, avatar] = await Promise.all([
471
471
  this.data.background.type == 'image' ? loadImageSource(this.data.background.value, 'el fondo') : null,
472
472
  loadImageSource(this.data.avatar.source, 'el avatar')
473
- ])
474
-
475
- const canvas = Canvas.createCanvas(this.data.width, this.data.height)
476
- const ctx = canvas.getContext('2d')
477
-
478
- if (bg) {
479
- let ratio = Math.max(canvas.width / bg.width, canvas.height / bg.height);
480
- let width = bg.width * ratio;
481
- let height = bg.height * ratio;
482
- ctx.drawImage(bg, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height);
483
- } else {
484
- ctx.fillStyle = this.data.background.value;
485
- ctx.fillRect(0, 0, canvas.width, canvas.height);
486
- }
487
-
473
+ ])
474
+
475
+ const canvas = Canvas.createCanvas(this.data.width, this.data.height)
476
+ const ctx = canvas.getContext('2d')
477
+
478
+ if (bg) {
479
+ let ratio = Math.max(canvas.width / bg.width, canvas.height / bg.height);
480
+ let width = bg.width * ratio;
481
+ let height = bg.height * ratio;
482
+ ctx.drawImage(bg, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height);
483
+ } else {
484
+ ctx.fillStyle = this.data.background.value;
485
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
486
+ }
487
+
488
488
  const maxTitleWidth = this.data.width * 0.8;
489
489
  let titleFontSize = this.data.layout === 'center'
490
490
  ? Math.min(this.data.title.font_size, this.data.height * 0.14)
491
491
  : this.data.title.font_size;
492
492
  ctx.font = `${titleFontSize}px ${this.data.font}`;
493
-
494
- while (ctx.measureText(this.data.title.content).width > maxTitleWidth && titleFontSize > 10) {
495
- titleFontSize -= 1;
496
- ctx.font = `${titleFontSize}px ${this.data.font}`;
497
- }
498
-
493
+
494
+ while (ctx.measureText(this.data.title.content).width > maxTitleWidth && titleFontSize > 10) {
495
+ titleFontSize -= 1;
496
+ ctx.font = `${titleFontSize}px ${this.data.font}`;
497
+ }
498
+
499
499
  const maxDescWidth = this.data.width * 0.9;
500
500
  let descFontSize = this.data.layout === 'center'
501
501
  ? Math.min(this.data.description.font_size, this.data.height * 0.09)
502
502
  : this.data.description.font_size;
503
503
  ctx.font = `${descFontSize}px ${this.data.font}`;
504
-
505
- while (ctx.measureText(this.data.description.content).width > maxDescWidth && descFontSize > 8) {
506
- descFontSize -= 1;
507
- ctx.font = `${descFontSize}px ${this.data.font}`;
508
- }
509
-
504
+
505
+ while (ctx.measureText(this.data.description.content).width > maxDescWidth && descFontSize > 8) {
506
+ descFontSize -= 1;
507
+ ctx.font = `${descFontSize}px ${this.data.font}`;
508
+ }
509
+
510
510
  const layout = this.data.layout === 'center'
511
511
  ? calculateCenteredLayout(this.data, titleFontSize, descFontSize)
512
512
  : {
@@ -522,20 +522,20 @@ class Welcome {
522
522
  const x = layout.avatarX;
523
523
  const y = layout.avatarY;
524
524
  const radio = layout.radio;
525
-
526
- if (this.data.avatar.border) {
527
- ctx.beginPath();
528
- ctx.fillStyle = this.data.avatar.border;
529
- ctx.arc(x + radio, y + radio, radio + 6, 0, 2 * Math.PI);
530
- ctx.fill();
531
- ctx.closePath();
532
- }
533
-
534
- ctx.save();
535
- ctx.beginPath();
536
- ctx.arc(x + radio, y + radio, radio, 0, Math.PI * 2, true);
537
- ctx.closePath();
538
- ctx.clip();
525
+
526
+ if (this.data.avatar.border) {
527
+ ctx.beginPath();
528
+ ctx.fillStyle = this.data.avatar.border;
529
+ ctx.arc(x + radio, y + radio, radio + 6, 0, 2 * Math.PI);
530
+ ctx.fill();
531
+ ctx.closePath();
532
+ }
533
+
534
+ ctx.save();
535
+ ctx.beginPath();
536
+ ctx.arc(x + radio, y + radio, radio, 0, Math.PI * 2, true);
537
+ ctx.closePath();
538
+ ctx.clip();
539
539
  ctx.drawImage(avatar, x, y, radio * 2, radio * 2);
540
540
  ctx.restore();
541
541
 
@@ -548,15 +548,15 @@ class Welcome {
548
548
  ctx.font = `${descFontSize}px ${this.data.font}`;
549
549
  ctx.fillStyle = this.data.description.text_color;
550
550
  ctx.fillText(this.data.description.content, layout.descriptionX, layout.descriptionY);
551
-
552
- return canvas.toBuffer();
553
- } catch (error) {
554
- if(error instanceof NekoError) {
555
- throw error;
556
- }
557
- throw new NekoError('GenerateError', 'Error al generar la imagen: '+error.message)
558
- }
559
- }
551
+
552
+ return canvas.toBuffer();
553
+ } catch (error) {
554
+ if(error instanceof NekoError) {
555
+ throw error;
556
+ }
557
+ throw new NekoError('GenerateError', 'Error al generar la imagen: '+error.message)
558
+ }
559
+ }
560
560
  }
561
561
 
562
562
  function calculateCenteredLayout(data, titleFontSize, descFontSize) {
@@ -602,5 +602,5 @@ function validateDimension(value, name) {
602
602
  }
603
603
  return value;
604
604
  }
605
-
605
+
606
606
  module.exports = Welcome;