@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,1214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Módulo de banners para E-commerce
|
|
5
|
+
*
|
|
6
|
+
* Este módulo fornece funções para criar banners de e-commerce
|
|
7
|
+
* com diferentes estilos e opções de personalização.
|
|
8
|
+
*
|
|
9
|
+
* @module e-commerce-banner
|
|
10
|
+
* @author Cognima Team (melhorado)
|
|
11
|
+
* @version 2.0.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const PImage = require('pureimage');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const utils = require('../utils');
|
|
18
|
+
const constants = require('./constants');
|
|
19
|
+
const effects = require('./effects');
|
|
20
|
+
const imageProcessor = require('./image-processor');
|
|
21
|
+
const imageFilters = require('./image-filters');
|
|
22
|
+
const validator = require('./validator');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cria um banner de produto para e-commerce
|
|
26
|
+
*
|
|
27
|
+
* @async
|
|
28
|
+
* @param {Object} options - Opções de configuração
|
|
29
|
+
* @param {string} options.productName - Nome do produto
|
|
30
|
+
* @param {string} options.productImageURL - URL da imagem do produto
|
|
31
|
+
* @param {string} [options.price] - Preço do produto
|
|
32
|
+
* @param {string} [options.currency="R$"] - Símbolo da moeda
|
|
33
|
+
* @param {string} [options.discountBadge] - Texto do badge de desconto (ex: "20% OFF")
|
|
34
|
+
* @param {string} [options.ctaText="COMPRAR AGORA"] - Texto do botão de call-to-action
|
|
35
|
+
* @param {string} [options.backgroundColor="#FFFFFF"] - Cor de fundo
|
|
36
|
+
* @param {string} [options.accentColor="#FF6B6B"] - Cor de destaque
|
|
37
|
+
* @param {string} [options.textColor="#333333"] - Cor do texto
|
|
38
|
+
* @param {string} [options.style="standard"] - Estilo do banner ("standard", "minimal", "bold", "gradient")
|
|
39
|
+
* @param {number} [options.width=1200] - Largura do banner em pixels
|
|
40
|
+
* @param {number} [options.height=628] - Altura do banner em pixels
|
|
41
|
+
* @param {string} [options.font="Poppins"] - Nome da fonte
|
|
42
|
+
* @param {boolean} [options.showRating=false] - Se deve mostrar classificação por estrelas
|
|
43
|
+
* @param {number} [options.rating=5] - Classificação do produto (1-5)
|
|
44
|
+
* @param {boolean} [options.showBorder=true] - Se deve mostrar borda
|
|
45
|
+
* @param {string} [options.borderColor="#EEEEEE"] - Cor da borda
|
|
46
|
+
* @param {number} [options.borderWidth=2] - Largura da borda em pixels
|
|
47
|
+
* @param {number} [options.borderRadius=10] - Raio da borda em pixels
|
|
48
|
+
* @param {boolean} [options.showShadow=true] - Se deve mostrar sombra
|
|
49
|
+
* @param {string} [options.shadowColor="rgba(0,0,0,0.1)"] - Cor da sombra
|
|
50
|
+
* @param {Array<string>} [options.tags=[]] - Tags do produto
|
|
51
|
+
* @param {string} [options.backgroundImageURL] - URL da imagem de fundo
|
|
52
|
+
* @param {number} [options.backgroundOpacity=0.1] - Opacidade da imagem de fundo
|
|
53
|
+
* @param {boolean} [options.applyFilter=false] - Se deve aplicar filtro à imagem do produto
|
|
54
|
+
* @param {string} [options.filterType="none"] - Tipo de filtro ("none", "grayscale", "sepia", "blur", etc.)
|
|
55
|
+
* @returns {Promise<Buffer>} - Buffer da imagem gerada
|
|
56
|
+
*/
|
|
57
|
+
async function createProductBanner(options) {
|
|
58
|
+
// Validação de parâmetros
|
|
59
|
+
validator.validateRequiredParams(options, ['productName', 'productImageURL']);
|
|
60
|
+
|
|
61
|
+
// Valores padrão
|
|
62
|
+
const defaults = {
|
|
63
|
+
price: '',
|
|
64
|
+
currency: 'R$',
|
|
65
|
+
discountBadge: '',
|
|
66
|
+
ctaText: 'COMPRAR AGORA',
|
|
67
|
+
backgroundColor: '#FFFFFF',
|
|
68
|
+
accentColor: '#FF6B6B',
|
|
69
|
+
textColor: '#333333',
|
|
70
|
+
style: 'standard',
|
|
71
|
+
width: 1200,
|
|
72
|
+
height: 628,
|
|
73
|
+
font: 'Poppins',
|
|
74
|
+
showRating: false,
|
|
75
|
+
rating: 5,
|
|
76
|
+
showBorder: true,
|
|
77
|
+
borderColor: '#EEEEEE',
|
|
78
|
+
borderWidth: 2,
|
|
79
|
+
borderRadius: 10,
|
|
80
|
+
showShadow: true,
|
|
81
|
+
shadowColor: 'rgba(0,0,0,0.1)',
|
|
82
|
+
tags: [],
|
|
83
|
+
backgroundImageURL: '',
|
|
84
|
+
backgroundOpacity: 0.1,
|
|
85
|
+
applyFilter: false,
|
|
86
|
+
filterType: 'none'
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Mescla as opções com os valores padrão
|
|
90
|
+
const settings = { ...defaults, ...options };
|
|
91
|
+
|
|
92
|
+
// Cria a imagem
|
|
93
|
+
const img = PImage.make(settings.width, settings.height);
|
|
94
|
+
const ctx = img.getContext('2d');
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Aplica o estilo de acordo com a opção selecionada
|
|
98
|
+
switch (settings.style) {
|
|
99
|
+
case 'minimal':
|
|
100
|
+
await drawMinimalStyle(ctx, settings);
|
|
101
|
+
break;
|
|
102
|
+
case 'bold':
|
|
103
|
+
await drawBoldStyle(ctx, settings);
|
|
104
|
+
break;
|
|
105
|
+
case 'gradient':
|
|
106
|
+
await drawGradientStyle(ctx, settings);
|
|
107
|
+
break;
|
|
108
|
+
case 'standard':
|
|
109
|
+
default:
|
|
110
|
+
await drawStandardStyle(ctx, settings);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Retorna o buffer da imagem
|
|
115
|
+
return await utils.getBufferFromImage(img);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Erro ao criar banner de produto:', error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Desenha o estilo padrão do banner de produto
|
|
124
|
+
*
|
|
125
|
+
* @async
|
|
126
|
+
* @param {CanvasRenderingContext2D} ctx - Contexto de renderização
|
|
127
|
+
* @param {Object} settings - Configurações do banner
|
|
128
|
+
*/
|
|
129
|
+
async function drawStandardStyle(ctx, settings) {
|
|
130
|
+
// Desenha o fundo
|
|
131
|
+
ctx.fillStyle = settings.backgroundColor;
|
|
132
|
+
ctx.fillRect(0, 0, settings.width, settings.height);
|
|
133
|
+
|
|
134
|
+
// Se tiver imagem de fundo, desenha com opacidade
|
|
135
|
+
if (settings.backgroundImageURL) {
|
|
136
|
+
const backgroundImage = await utils.loadImage(settings.backgroundImageURL);
|
|
137
|
+
ctx.globalAlpha = settings.backgroundOpacity;
|
|
138
|
+
ctx.drawImage(backgroundImage, 0, 0, settings.width, settings.height);
|
|
139
|
+
ctx.globalAlpha = 1.0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Carrega a imagem do produto
|
|
143
|
+
let productImage = await utils.loadImage(settings.productImageURL);
|
|
144
|
+
|
|
145
|
+
// Aplica filtro se necessário
|
|
146
|
+
if (settings.applyFilter && settings.filterType !== 'none') {
|
|
147
|
+
productImage = await imageFilters.applyFilter(
|
|
148
|
+
productImage,
|
|
149
|
+
settings.filterType
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Calcula as dimensões e posições
|
|
154
|
+
const productWidth = settings.width * 0.4;
|
|
155
|
+
const productHeight = settings.height * 0.7;
|
|
156
|
+
const productX = settings.width * 0.1;
|
|
157
|
+
const productY = (settings.height - productHeight) / 2;
|
|
158
|
+
|
|
159
|
+
// Desenha a imagem do produto
|
|
160
|
+
utils.drawImageProp(ctx, productImage, productX, productY, productWidth, productHeight);
|
|
161
|
+
|
|
162
|
+
// Desenha o conteúdo de texto
|
|
163
|
+
const textX = productX + productWidth + 50;
|
|
164
|
+
const textWidth = settings.width - textX - 50;
|
|
165
|
+
|
|
166
|
+
// Nome do produto
|
|
167
|
+
ctx.font = `bold 48px ${settings.font}`;
|
|
168
|
+
ctx.fillStyle = settings.textColor;
|
|
169
|
+
utils.wrapText(ctx, settings.productName, textX, settings.height * 0.3, textWidth, 60);
|
|
170
|
+
|
|
171
|
+
// Preço
|
|
172
|
+
if (settings.price) {
|
|
173
|
+
ctx.font = `bold 64px ${settings.font}`;
|
|
174
|
+
ctx.fillStyle = settings.accentColor;
|
|
175
|
+
ctx.fillText(`${settings.currency} ${settings.price}`, textX, settings.height * 0.5);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Badge de desconto
|
|
179
|
+
if (settings.discountBadge) {
|
|
180
|
+
const badgeWidth = 120;
|
|
181
|
+
const badgeHeight = 40;
|
|
182
|
+
const badgeX = textX;
|
|
183
|
+
const badgeY = settings.height * 0.55;
|
|
184
|
+
|
|
185
|
+
// Desenha o fundo do badge
|
|
186
|
+
ctx.fillStyle = settings.accentColor;
|
|
187
|
+
utils.roundRect(ctx, badgeX, badgeY, badgeWidth, badgeHeight, 5, true);
|
|
188
|
+
|
|
189
|
+
// Desenha o texto do badge
|
|
190
|
+
ctx.font = `bold 20px ${settings.font}`;
|
|
191
|
+
ctx.fillStyle = '#FFFFFF';
|
|
192
|
+
ctx.textAlign = 'center';
|
|
193
|
+
ctx.fillText(settings.discountBadge, badgeX + badgeWidth / 2, badgeY + 28);
|
|
194
|
+
ctx.textAlign = 'left';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Classificação por estrelas
|
|
198
|
+
if (settings.showRating) {
|
|
199
|
+
const starSize = 25;
|
|
200
|
+
const starSpacing = 5;
|
|
201
|
+
const startX = textX;
|
|
202
|
+
const startY = settings.height * 0.6;
|
|
203
|
+
|
|
204
|
+
for (let i = 0; i < 5; i++) {
|
|
205
|
+
const starX = startX + i * (starSize + starSpacing);
|
|
206
|
+
const filled = i < settings.rating;
|
|
207
|
+
|
|
208
|
+
// Desenha estrela
|
|
209
|
+
ctx.fillStyle = filled ? '#FFD700' : '#DDDDDD';
|
|
210
|
+
utils.drawStar(ctx, starX, startY, starSize);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Tags
|
|
215
|
+
if (settings.tags.length > 0) {
|
|
216
|
+
const tagHeight = 30;
|
|
217
|
+
const tagSpacing = 10;
|
|
218
|
+
const tagY = settings.height * 0.65;
|
|
219
|
+
let currentX = textX;
|
|
220
|
+
|
|
221
|
+
ctx.font = `16px ${settings.font}`;
|
|
222
|
+
|
|
223
|
+
for (const tag of settings.tags) {
|
|
224
|
+
const tagWidth = ctx.measureText(tag).width + 20;
|
|
225
|
+
|
|
226
|
+
// Desenha o fundo da tag
|
|
227
|
+
ctx.fillStyle = utils.adjustColor(settings.accentColor, 0.2);
|
|
228
|
+
utils.roundRect(ctx, currentX, tagY, tagWidth, tagHeight, 15, true);
|
|
229
|
+
|
|
230
|
+
// Desenha o texto da tag
|
|
231
|
+
ctx.fillStyle = settings.textColor;
|
|
232
|
+
ctx.textAlign = 'center';
|
|
233
|
+
ctx.fillText(tag, currentX + tagWidth / 2, tagY + 20);
|
|
234
|
+
ctx.textAlign = 'left';
|
|
235
|
+
|
|
236
|
+
currentX += tagWidth + tagSpacing;
|
|
237
|
+
|
|
238
|
+
// Quebra para a próxima linha se necessário
|
|
239
|
+
if (currentX + tagWidth > textX + textWidth) {
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Botão CTA
|
|
246
|
+
const ctaWidth = 250;
|
|
247
|
+
const ctaHeight = 60;
|
|
248
|
+
const ctaX = textX;
|
|
249
|
+
const ctaY = settings.height * 0.75;
|
|
250
|
+
|
|
251
|
+
// Desenha o fundo do botão
|
|
252
|
+
ctx.fillStyle = settings.accentColor;
|
|
253
|
+
utils.roundRect(ctx, ctaX, ctaY, ctaWidth, ctaHeight, 8, true);
|
|
254
|
+
|
|
255
|
+
// Desenha o texto do botão
|
|
256
|
+
ctx.font = `bold 24px ${settings.font}`;
|
|
257
|
+
ctx.fillStyle = '#FFFFFF';
|
|
258
|
+
ctx.textAlign = 'center';
|
|
259
|
+
ctx.fillText(settings.ctaText, ctaX + ctaWidth / 2, ctaY + 38);
|
|
260
|
+
ctx.textAlign = 'left';
|
|
261
|
+
|
|
262
|
+
// Desenha borda se necessário
|
|
263
|
+
if (settings.showBorder) {
|
|
264
|
+
ctx.strokeStyle = settings.borderColor;
|
|
265
|
+
ctx.lineWidth = settings.borderWidth;
|
|
266
|
+
utils.roundRect(ctx, settings.borderWidth / 2, settings.borderWidth / 2,
|
|
267
|
+
settings.width - settings.borderWidth, settings.height - settings.borderWidth,
|
|
268
|
+
settings.borderRadius, false, true);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Desenha o estilo minimalista do banner de produto
|
|
274
|
+
*
|
|
275
|
+
* @async
|
|
276
|
+
* @param {CanvasRenderingContext2D} ctx - Contexto de renderização
|
|
277
|
+
* @param {Object} settings - Configurações do banner
|
|
278
|
+
*/
|
|
279
|
+
async function drawMinimalStyle(ctx, settings) {
|
|
280
|
+
// Desenha o fundo
|
|
281
|
+
ctx.fillStyle = settings.backgroundColor;
|
|
282
|
+
ctx.fillRect(0, 0, settings.width, settings.height);
|
|
283
|
+
|
|
284
|
+
// Carrega a imagem do produto
|
|
285
|
+
let productImage = await utils.loadImage(settings.productImageURL);
|
|
286
|
+
|
|
287
|
+
// Aplica filtro se necessário
|
|
288
|
+
if (settings.applyFilter && settings.filterType !== 'none') {
|
|
289
|
+
productImage = await imageFilters.applyFilter(
|
|
290
|
+
productImage,
|
|
291
|
+
settings.filterType
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Calcula as dimensões e posições
|
|
296
|
+
const productWidth = settings.width * 0.35;
|
|
297
|
+
const productHeight = settings.height * 0.65;
|
|
298
|
+
const productX = settings.width * 0.55;
|
|
299
|
+
const productY = (settings.height - productHeight) / 2;
|
|
300
|
+
|
|
301
|
+
// Desenha a imagem do produto
|
|
302
|
+
utils.drawImageProp(ctx, productImage, productX, productY, productWidth, productHeight);
|
|
303
|
+
|
|
304
|
+
// Desenha o conteúdo de texto
|
|
305
|
+
const textX = settings.width * 0.1;
|
|
306
|
+
const textWidth = productX - textX - 50;
|
|
307
|
+
|
|
308
|
+
// Nome do produto
|
|
309
|
+
ctx.font = `300 42px ${settings.font}`;
|
|
310
|
+
ctx.fillStyle = settings.textColor;
|
|
311
|
+
utils.wrapText(ctx, settings.productName, textX, settings.height * 0.35, textWidth, 50);
|
|
312
|
+
|
|
313
|
+
// Preço
|
|
314
|
+
if (settings.price) {
|
|
315
|
+
ctx.font = `bold 54px ${settings.font}`;
|
|
316
|
+
ctx.fillStyle = settings.textColor;
|
|
317
|
+
ctx.fillText(`${settings.currency} ${settings.price}`, textX, settings.height * 0.55);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Badge de desconto
|
|
321
|
+
if (settings.discountBadge) {
|
|
322
|
+
ctx.font = `bold 24px ${settings.font}`;
|
|
323
|
+
ctx.fillStyle = settings.accentColor;
|
|
324
|
+
ctx.fillText(settings.discountBadge, textX, settings.height * 0.65);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Botão CTA
|
|
328
|
+
const ctaWidth = 200;
|
|
329
|
+
const ctaHeight = 50;
|
|
330
|
+
const ctaX = textX;
|
|
331
|
+
const ctaY = settings.height * 0.75;
|
|
332
|
+
|
|
333
|
+
// Desenha a borda do botão
|
|
334
|
+
ctx.strokeStyle = settings.accentColor;
|
|
335
|
+
ctx.lineWidth = 2;
|
|
336
|
+
utils.roundRect(ctx, ctaX, ctaY, ctaWidth, ctaHeight, 0, false, true);
|
|
337
|
+
|
|
338
|
+
// Desenha o texto do botão
|
|
339
|
+
ctx.font = `bold 20px ${settings.font}`;
|
|
340
|
+
ctx.fillStyle = settings.accentColor;
|
|
341
|
+
ctx.textAlign = 'center';
|
|
342
|
+
ctx.fillText(settings.ctaText, ctaX + ctaWidth / 2, ctaY + 33);
|
|
343
|
+
ctx.textAlign = 'left';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Desenha o estilo bold do banner de produto
|
|
348
|
+
*
|
|
349
|
+
* @async
|
|
350
|
+
* @param {CanvasRenderingContext2D} ctx - Contexto de renderização
|
|
351
|
+
* @param {Object} settings - Configurações do banner
|
|
352
|
+
*/
|
|
353
|
+
async function drawBoldStyle(ctx, settings) {
|
|
354
|
+
// Desenha o fundo
|
|
355
|
+
ctx.fillStyle = settings.accentColor;
|
|
356
|
+
ctx.fillRect(0, 0, settings.width, settings.height);
|
|
357
|
+
|
|
358
|
+
// Carrega a imagem do produto
|
|
359
|
+
let productImage = await utils.loadImage(settings.productImageURL);
|
|
360
|
+
|
|
361
|
+
// Aplica filtro se necessário
|
|
362
|
+
if (settings.applyFilter && settings.filterType !== 'none') {
|
|
363
|
+
productImage = await imageFilters.applyFilter(
|
|
364
|
+
productImage,
|
|
365
|
+
settings.filterType
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Calcula as dimensões e posições
|
|
370
|
+
const productWidth = settings.width * 0.45;
|
|
371
|
+
const productHeight = settings.height * 0.8;
|
|
372
|
+
const productX = settings.width * 0.5;
|
|
373
|
+
const productY = (settings.height - productHeight) / 2;
|
|
374
|
+
|
|
375
|
+
// Desenha a imagem do produto
|
|
376
|
+
utils.drawImageProp(ctx, productImage, productX, productY, productWidth, productHeight);
|
|
377
|
+
|
|
378
|
+
// Desenha o conteúdo de texto
|
|
379
|
+
const textX = settings.width * 0.08;
|
|
380
|
+
const textWidth = productX - textX - 30;
|
|
381
|
+
|
|
382
|
+
// Nome do produto
|
|
383
|
+
ctx.font = `bold 52px ${settings.font}`;
|
|
384
|
+
ctx.fillStyle = '#FFFFFF';
|
|
385
|
+
utils.wrapText(ctx, settings.productName.toUpperCase(), textX, settings.height * 0.3, textWidth, 60);
|
|
386
|
+
|
|
387
|
+
// Preço
|
|
388
|
+
if (settings.price) {
|
|
389
|
+
ctx.font = `bold 72px ${settings.font}`;
|
|
390
|
+
ctx.fillStyle = '#FFFFFF';
|
|
391
|
+
ctx.fillText(`${settings.currency} ${settings.price}`, textX, settings.height * 0.5);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Badge de desconto
|
|
395
|
+
if (settings.discountBadge) {
|
|
396
|
+
const badgeWidth = 140;
|
|
397
|
+
const badgeHeight = 50;
|
|
398
|
+
const badgeX = textX;
|
|
399
|
+
const badgeY = settings.height * 0.55;
|
|
400
|
+
|
|
401
|
+
// Desenha o fundo do badge
|
|
402
|
+
ctx.fillStyle = '#FFFFFF';
|
|
403
|
+
utils.roundRect(ctx, badgeX, badgeY, badgeWidth, badgeHeight, 25, true);
|
|
404
|
+
|
|
405
|
+
// Desenha o texto do badge
|
|
406
|
+
ctx.font = `bold 24px ${settings.font}`;
|
|
407
|
+
ctx.fillStyle = settings.accentColor;
|
|
408
|
+
ctx.textAlign = 'center';
|
|
409
|
+
ctx.fillText(settings.discountBadge, badgeX + badgeWidth / 2, badgeY + 35);
|
|
410
|
+
ctx.textAlign = 'left';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Botão CTA
|
|
414
|
+
const ctaWidth = 250;
|
|
415
|
+
const ctaHeight = 70;
|
|
416
|
+
const ctaX = textX;
|
|
417
|
+
const ctaY = settings.height * 0.7;
|
|
418
|
+
|
|
419
|
+
// Desenha o fundo do botão
|
|
420
|
+
ctx.fillStyle = '#FFFFFF';
|
|
421
|
+
utils.roundRect(ctx, ctaX, ctaY, ctaWidth, ctaHeight, 35, true);
|
|
422
|
+
|
|
423
|
+
// Desenha o texto do botão
|
|
424
|
+
ctx.font = `bold 26px ${settings.font}`;
|
|
425
|
+
ctx.fillStyle = settings.accentColor;
|
|
426
|
+
ctx.textAlign = 'center';
|
|
427
|
+
ctx.fillText(settings.ctaText, ctaX + ctaWidth / 2, ctaY + 43);
|
|
428
|
+
ctx.textAlign = 'left';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Desenha o estilo gradient do banner de produto
|
|
433
|
+
*
|
|
434
|
+
* @async
|
|
435
|
+
* @param {CanvasRenderingContext2D} ctx - Contexto de renderização
|
|
436
|
+
* @param {Object} settings - Configurações do banner
|
|
437
|
+
*/
|
|
438
|
+
async function drawGradientStyle(ctx, settings) {
|
|
439
|
+
// Cria um gradiente de fundo
|
|
440
|
+
const gradient = ctx.createLinearGradient(0, 0, settings.width, settings.height);
|
|
441
|
+
gradient.addColorStop(0, settings.accentColor);
|
|
442
|
+
gradient.addColorStop(1, utils.adjustColor(settings.accentColor, -0.3));
|
|
443
|
+
|
|
444
|
+
// Desenha o fundo
|
|
445
|
+
ctx.fillStyle = gradient;
|
|
446
|
+
ctx.fillRect(0, 0, settings.width, settings.height);
|
|
447
|
+
|
|
448
|
+
// Adiciona um padrão de pontos ao fundo
|
|
449
|
+
effects.addDotPattern(ctx, settings.width, settings.height, 20, 'rgba(255,255,255,0.1)');
|
|
450
|
+
|
|
451
|
+
// Carrega a imagem do produto
|
|
452
|
+
let productImage = await utils.loadImage(settings.productImageURL);
|
|
453
|
+
|
|
454
|
+
// Aplica filtro se necessário
|
|
455
|
+
if (settings.applyFilter && settings.filterType !== 'none') {
|
|
456
|
+
productImage = await imageFilters.applyFilter(
|
|
457
|
+
productImage,
|
|
458
|
+
settings.filterType
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Calcula as dimensões e posições
|
|
463
|
+
const productWidth = settings.width * 0.4;
|
|
464
|
+
const productHeight = settings.height * 0.75;
|
|
465
|
+
const productX = settings.width * 0.05;
|
|
466
|
+
const productY = (settings.height - productHeight) / 2;
|
|
467
|
+
|
|
468
|
+
// Adiciona sombra à imagem do produto
|
|
469
|
+
if (settings.showShadow) {
|
|
470
|
+
ctx.shadowColor = settings.shadowColor;
|
|
471
|
+
ctx.shadowBlur = 30;
|
|
472
|
+
ctx.shadowOffsetX = 10;
|
|
473
|
+
ctx.shadowOffsetY = 10;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Desenha a imagem do produto
|
|
477
|
+
utils.drawImageProp(ctx, productImage, productX, productY, productWidth, productHeight);
|
|
478
|
+
|
|
479
|
+
// Remove a sombra
|
|
480
|
+
ctx.shadowColor = 'transparent';
|
|
481
|
+
ctx.shadowBlur = 0;
|
|
482
|
+
ctx.shadowOffsetX = 0;
|
|
483
|
+
ctx.shadowOffsetY = 0;
|
|
484
|
+
|
|
485
|
+
// Desenha o conteúdo de texto
|
|
486
|
+
const textX = productX + productWidth + 60;
|
|
487
|
+
const textWidth = settings.width - textX - 60;
|
|
488
|
+
|
|
489
|
+
// Adiciona um retângulo semi-transparente para o texto
|
|
490
|
+
ctx.fillStyle = 'rgba(255,255,255,0.15)';
|
|
491
|
+
utils.roundRect(ctx, textX - 20, settings.height * 0.2, textWidth + 40, settings.height * 0.6, 20, true);
|
|
492
|
+
|
|
493
|
+
// Nome do produto
|
|
494
|
+
ctx.font = `bold 48px ${settings.font}`;
|
|
495
|
+
ctx.fillStyle = '#FFFFFF';
|
|
496
|
+
utils.wrapText(ctx, settings.productName, textX, settings.height * 0.3, textWidth, 60);
|
|
497
|
+
|
|
498
|
+
// Preço
|
|
499
|
+
if (settings.price) {
|
|
500
|
+
ctx.font = `bold 64px ${settings.font}`;
|
|
501
|
+
ctx.fillStyle = '#FFFFFF';
|
|
502
|
+
|
|
503
|
+
// Adiciona sombra ao texto do preço
|
|
504
|
+
ctx.shadowColor = 'rgba(0,0,0,0.3)';
|
|
505
|
+
ctx.shadowBlur = 5;
|
|
506
|
+
ctx.shadowOffsetX = 2;
|
|
507
|
+
ctx.shadowOffsetY = 2;
|
|
508
|
+
|
|
509
|
+
ctx.fillText(`${settings.currency} ${settings.price}`, textX, settings.height * 0.5);
|
|
510
|
+
|
|
511
|
+
// Remove a sombra
|
|
512
|
+
ctx.shadowColor = 'transparent';
|
|
513
|
+
ctx.shadowBlur = 0;
|
|
514
|
+
ctx.shadowOffsetX = 0;
|
|
515
|
+
ctx.shadowOffsetY = 0;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Badge de desconto
|
|
519
|
+
if (settings.discountBadge) {
|
|
520
|
+
const badgeWidth = 140;
|
|
521
|
+
const badgeHeight = 50;
|
|
522
|
+
const badgeX = textX;
|
|
523
|
+
const badgeY = settings.height * 0.55;
|
|
524
|
+
|
|
525
|
+
// Desenha o fundo do badge
|
|
526
|
+
ctx.fillStyle = '#FFFFFF';
|
|
527
|
+
utils.roundRect(ctx, badgeX, badgeY, badgeWidth, badgeHeight, 25, true);
|
|
528
|
+
|
|
529
|
+
// Desenha o texto do badge
|
|
530
|
+
ctx.font = `bold 24px ${settings.font}`;
|
|
531
|
+
ctx.fillStyle = settings.accentColor;
|
|
532
|
+
ctx.textAlign = 'center';
|
|
533
|
+
ctx.fillText(settings.discountBadge, badgeX + badgeWidth / 2, badgeY + 35);
|
|
534
|
+
ctx.textAlign = 'left';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Botão CTA
|
|
538
|
+
const ctaWidth = 250;
|
|
539
|
+
const ctaHeight = 70;
|
|
540
|
+
const ctaX = textX;
|
|
541
|
+
const ctaY = settings.height * 0.7;
|
|
542
|
+
|
|
543
|
+
// Desenha o fundo do botão com gradiente
|
|
544
|
+
const ctaGradient = ctx.createLinearGradient(ctaX, ctaY, ctaX + ctaWidth, ctaY + ctaHeight);
|
|
545
|
+
ctaGradient.addColorStop(0, '#FFFFFF');
|
|
546
|
+
ctaGradient.addColorStop(1, '#F0F0F0');
|
|
547
|
+
|
|
548
|
+
ctx.fillStyle = ctaGradient;
|
|
549
|
+
utils.roundRect(ctx, ctaX, ctaY, ctaWidth, ctaHeight, 35, true);
|
|
550
|
+
|
|
551
|
+
// Desenha o texto do botão
|
|
552
|
+
ctx.font = `bold 26px ${settings.font}`;
|
|
553
|
+
ctx.fillStyle = settings.accentColor;
|
|
554
|
+
ctx.textAlign = 'center';
|
|
555
|
+
ctx.fillText(settings.ctaText, ctaX + ctaWidth / 2, ctaY + 43);
|
|
556
|
+
ctx.textAlign = 'left';
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Cria um banner de promoção para e-commerce
|
|
561
|
+
*
|
|
562
|
+
* @async
|
|
563
|
+
* @param {Object} options - Opções de configuração
|
|
564
|
+
* @param {string} options.title - Título da promoção
|
|
565
|
+
* @param {string} [options.subtitle] - Subtítulo da promoção
|
|
566
|
+
* @param {string} [options.backgroundImageURL] - URL da imagem de fundo
|
|
567
|
+
* @param {string} [options.discountText] - Texto de desconto (ex: "50% OFF")
|
|
568
|
+
* @param {string} [options.ctaText="COMPRAR AGORA"] - Texto do botão de call-to-action
|
|
569
|
+
* @param {string} [options.validUntil] - Data de validade da promoção
|
|
570
|
+
* @param {string} [options.backgroundColor="#FF6B6B"] - Cor de fundo
|
|
571
|
+
* @param {string} [options.textColor="#FFFFFF"] - Cor do texto
|
|
572
|
+
* @param {string} [options.accentColor="#FFFFFF"] - Cor de destaque
|
|
573
|
+
* @param {string} [options.style="standard"] - Estilo do banner ("standard", "minimal", "bold", "seasonal")
|
|
574
|
+
* @param {number} [options.width=1200] - Largura do banner em pixels
|
|
575
|
+
* @param {number} [options.height=628] - Altura do banner em pixels
|
|
576
|
+
* @param {string} [options.font="Poppins"] - Nome da fonte
|
|
577
|
+
* @param {boolean} [options.showOverlay=true] - Se deve mostrar sobreposição na imagem de fundo
|
|
578
|
+
* @param {number} [options.overlayOpacity=0.5] - Opacidade da sobreposição
|
|
579
|
+
* @param {string} [options.overlayColor="#000000"] - Cor da sobreposição
|
|
580
|
+
* @param {string} [options.season] - Temporada para o estilo seasonal ("christmas", "halloween", "summer", "blackfriday")
|
|
581
|
+
* @returns {Promise<Buffer>} - Buffer da imagem gerada
|
|
582
|
+
*/
|
|
583
|
+
async function createPromotionBanner(options) {
|
|
584
|
+
// Validação de parâmetros
|
|
585
|
+
validator.validateRequiredParams(options, ['title']);
|
|
586
|
+
|
|
587
|
+
// Valores padrão
|
|
588
|
+
const defaults = {
|
|
589
|
+
subtitle: '',
|
|
590
|
+
backgroundImageURL: '',
|
|
591
|
+
discountText: '',
|
|
592
|
+
ctaText: 'COMPRAR AGORA',
|
|
593
|
+
validUntil: '',
|
|
594
|
+
backgroundColor: '#FF6B6B',
|
|
595
|
+
textColor: '#FFFFFF',
|
|
596
|
+
accentColor: '#FFFFFF',
|
|
597
|
+
style: 'standard',
|
|
598
|
+
width: 1200,
|
|
599
|
+
height: 628,
|
|
600
|
+
font: 'Poppins',
|
|
601
|
+
showOverlay: true,
|
|
602
|
+
overlayOpacity: 0.5,
|
|
603
|
+
overlayColor: '#000000',
|
|
604
|
+
season: ''
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// Mescla as opções com os valores padrão
|
|
608
|
+
const settings = { ...defaults, ...options };
|
|
609
|
+
|
|
610
|
+
// Cria a imagem
|
|
611
|
+
const img = PImage.make(settings.width, settings.height);
|
|
612
|
+
const ctx = img.getContext('2d');
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
// Aplica o estilo de acordo com a opção selecionada
|
|
616
|
+
switch (settings.style) {
|
|
617
|
+
case 'minimal':
|
|
618
|
+
await drawMinimalPromotionStyle(ctx, settings);
|
|
619
|
+
break;
|
|
620
|
+
case 'bold':
|
|
621
|
+
await drawBoldPromotionStyle(ctx, settings);
|
|
622
|
+
break;
|
|
623
|
+
case 'seasonal':
|
|
624
|
+
await drawSeasonalPromotionStyle(ctx, settings);
|
|
625
|
+
break;
|
|
626
|
+
case 'standard':
|
|
627
|
+
default:
|
|
628
|
+
await drawStandardPromotionStyle(ctx, settings);
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Retorna o buffer da imagem
|
|
633
|
+
return await utils.getBufferFromImage(img);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
console.error('Erro ao criar banner de promoção:', error);
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Desenha o estilo padrão do banner de promoção
|
|
642
|
+
*
|
|
643
|
+
* @async
|
|
644
|
+
* @param {CanvasRenderingContext2D} ctx - Contexto de renderização
|
|
645
|
+
* @param {Object} settings - Configurações do banner
|
|
646
|
+
*/
|
|
647
|
+
async function drawStandardPromotionStyle(ctx, settings) {
|
|
648
|
+
// Desenha o fundo
|
|
649
|
+
ctx.fillStyle = settings.backgroundColor;
|
|
650
|
+
ctx.fillRect(0, 0, settings.width, settings.height);
|
|
651
|
+
|
|
652
|
+
// Se tiver imagem de fundo, desenha com sobreposição
|
|
653
|
+
if (settings.backgroundImageURL) {
|
|
654
|
+
const backgroundImage = await utils.loadImage(settings.backgroundImageURL);
|
|
655
|
+
ctx.drawImage(backgroundImage, 0, 0, settings.width, settings.height);
|
|
656
|
+
|
|
657
|
+
// Adiciona sobreposição se necessário
|
|
658
|
+
if (settings.showOverlay) {
|
|
659
|
+
ctx.fillStyle = utils.hexToRgba(settings.overlayColor, settings.overlayOpacity);
|
|
660
|
+
ctx.fillRect(0, 0, settings.width, settings.height);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Desenha o conteúdo de texto centralizado
|
|
665
|
+
ctx.textAlign = 'center';
|
|
666
|
+
|
|
667
|
+
// Título
|
|
668
|
+
ctx.font = `bold 72px ${settings.font}`;
|
|
669
|
+
ctx.fillStyle = settings.textColor;
|
|
670
|
+
utils.wrapTextCentered(ctx, settings.title.toUpperCase(), settings.width / 2, settings.height * 0.35, settings.width * 0.8, 80);
|
|
671
|
+
|
|
672
|
+
// Subtítulo
|
|
673
|
+
if (settings.subtitle) {
|
|
674
|
+
ctx.font = `300 36px ${settings.font}`;
|
|
675
|
+
ctx.fillStyle = settings.textColor;
|
|
676
|
+
utils.wrapTextCentered(ctx, settings.subtitle, settings.width / 2, settings.height * 0.5, settings.width * 0.7, 50);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Texto de desconto
|
|
680
|
+
if (settings.discountText) {
|
|
681
|
+
const discountSize = 120;
|
|
682
|
+
const discountX = settings.width / 2;
|
|
683
|
+
const discountY = settings.height * 0.65;
|
|
684
|
+
|
|
685
|
+
// Desenha um círculo para o desconto
|
|
686
|
+
ctx.fillStyle = settings.accentColor;
|
|
687
|
+
ctx.beginPath();
|
|
688
|
+
ctx.arc(discountX, discountY, discountSize, 0, Math.PI * 2);
|
|
689
|
+
ctx.fill();
|
|
690
|
+
|
|
691
|
+
// Desenha o texto de desconto
|
|
692
|
+
ctx.font = `bold 48px ${settings.font}`;
|
|
693
|
+
ctx.fillStyle = settings.backgroundColor;
|
|
694
|
+
ctx.fillText(settings.discountText, discountX, discountY + 15);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Botão CTA
|
|
698
|
+
const ctaWidth = 300;
|
|
699
|
+
const ctaHeight = 70;
|
|
700
|
+
const ctaX = (settings.width - ctaWidth) / 2;
|
|
701
|
+
const ctaY = settings.height * 0.8;
|
|
702
|
+
|
|
703
|
+
// Desenha o fundo do botão
|
|
704
|
+
ctx.fillStyle = settings.accentColor;
|
|
705
|
+
utils.roundRect(ctx, ctaX, ctaY, ctaWidth, ctaHeight, 35, true);
|
|
706
|
+
|
|
707
|
+
// Desenha o texto do botão
|
|
708
|
+
ctx.font = `bold 28px ${settings.font}`;
|
|
709
|
+
ctx.fillStyle = settings.backgroundColor;
|
|
710
|
+
ctx.fillText(settings.ctaText, settings.width / 2, ctaY + 45);
|
|
711
|
+
|
|
712
|
+
// Data de validade
|
|
713
|
+
if (settings.validUntil) {
|
|
714
|
+
ctx.font = `16px ${settings.font}`;
|
|
715
|
+
ctx.fillStyle = settings.textColor;
|
|
716
|
+
ctx.fillText(settings.validUntil, settings.width / 2, settings.height * 0.95);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Restaura o alinhamento de texto
|
|
720
|
+
ctx.textAlign = 'left';
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Desenha o estilo minimalista do banner de promoção
|
|
725
|
+
*
|
|
726
|
+
* @async
|
|
727
|
+
* @param {CanvasRenderingContext2D} ctx - Contexto de renderização
|
|
728
|
+
* @param {Object} settings - Configurações do banner
|
|
729
|
+
*/
|
|
730
|
+
async function drawMinimalPromotionStyle(ctx, settings) {
|
|
731
|
+
// Desenha o fundo
|
|
732
|
+
ctx.fillStyle = '#FFFFFF';
|
|
733
|
+
ctx.fillRect(0, 0, settings.width, settings.height);
|
|
734
|
+
|
|
735
|
+
// Adiciona uma borda fina
|
|
736
|
+
ctx.strokeStyle = settings.backgroundColor;
|
|
737
|
+
ctx.lineWidth = 3;
|
|
738
|
+
ctx.strokeRect(20, 20, settings.width - 40, settings.height - 40);
|
|
739
|
+
|
|
740
|
+
// Desenha o conteúdo de texto centralizado
|
|
741
|
+
ctx.textAlign = 'center';
|
|
742
|
+
|
|
743
|
+
// Título
|
|
744
|
+
ctx.font = `bold 64px ${settings.font}`;
|
|
745
|
+
ctx.fillStyle = settings.backgroundColor;
|
|
746
|
+
utils.wrapTextCentered(ctx, settings.title, settings.width / 2, settings.height * 0.35, settings.width * 0.7, 70);
|
|
747
|
+
|
|
748
|
+
// Subtítulo
|
|
749
|
+
if (settings.subtitle) {
|
|
750
|
+
ctx.font = `300 32px ${settings.font}`;
|
|
751
|
+
ctx.fillStyle = '#333333';
|
|
752
|
+
utils.wrapTextCentered(ctx, settings.subtitle, settings.width / 2, settings.height * 0.5, settings.width * 0.6, 40);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Texto de desconto
|
|
756
|
+
if (settings.discountText) {
|
|
757
|
+
ctx.font = `bold 96px ${settings.font}`;
|
|
758
|
+
ctx.fillStyle = settings.backgroundColor;
|
|
759
|
+
ctx.fillText(settings.discountText, settings.width / 2, settings.height * 0.7);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Botão CTA
|
|
763
|
+
const ctaWidth = 250;
|
|
764
|
+
const ctaHeight = 60;
|
|
765
|
+
const ctaX = (settings.width - ctaWidth) / 2;
|
|
766
|
+
const ctaY = settings.height * 0.8;
|
|
767
|
+
|
|
768
|
+
// Desenha a borda do botão
|
|
769
|
+
ctx.strokeStyle = settings.backgroundColor;
|
|
770
|
+
ctx.lineWidth = 2;
|
|
771
|
+
utils.roundRect(ctx, ctaX, ctaY, ctaWidth, ctaHeight, 0, false, true);
|
|
772
|
+
|
|
773
|
+
// Desenha o texto do botão
|
|
774
|
+
ctx.font = `bold 24px ${settings.font}`;
|
|
775
|
+
ctx.fillStyle = settings.backgroundColor;
|
|
776
|
+
ctx.fillText(settings.ctaText, settings.width / 2, ctaY + 38);
|
|
777
|
+
|
|
778
|
+
// Data de validade
|
|
779
|
+
if (settings.validUntil) {
|
|
780
|
+
ctx.font = `16px ${settings.font}`;
|
|
781
|
+
ctx.fillStyle = '#333333';
|
|
782
|
+
ctx.fillText(settings.validUntil, settings.width / 2, settings.height * 0.95);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Restaura o alinhamento de texto
|
|
786
|
+
ctx.textAlign = 'left';
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Desenha o estilo bold do banner de promoção
|
|
791
|
+
*
|
|
792
|
+
* @async
|
|
793
|
+
* @param {CanvasRenderingContext2D} ctx - Contexto de renderização
|
|
794
|
+
* @param {Object} settings - Configurações do banner
|
|
795
|
+
*/
|
|
796
|
+
async function drawBoldPromotionStyle(ctx, settings) {
|
|
797
|
+
// Cria um gradiente de fundo
|
|
798
|
+
const gradient = ctx.createLinearGradient(0, 0, settings.width, settings.height);
|
|
799
|
+
gradient.addColorStop(0, settings.backgroundColor);
|
|
800
|
+
gradient.addColorStop(1, utils.adjustColor(settings.backgroundColor, -0.3));
|
|
801
|
+
|
|
802
|
+
// Desenha o fundo
|
|
803
|
+
ctx.fillStyle = gradient;
|
|
804
|
+
ctx.fillRect(0, 0, settings.width, settings.height);
|
|
805
|
+
|
|
806
|
+
// Adiciona elementos decorativos
|
|
807
|
+
for (let i = 0; i < 5; i++) {
|
|
808
|
+
const size = Math.random() * 200 + 100;
|
|
809
|
+
const x = Math.random() * settings.width;
|
|
810
|
+
const y = Math.random() * settings.height;
|
|
811
|
+
|
|
812
|
+
ctx.fillStyle = utils.hexToRgba(settings.accentColor, 0.1);
|
|
813
|
+
ctx.beginPath();
|
|
814
|
+
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
815
|
+
ctx.fill();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Desenha o conteúdo de texto centralizado
|
|
819
|
+
ctx.textAlign = 'center';
|
|
820
|
+
|
|
821
|
+
// Texto de desconto
|
|
822
|
+
if (settings.discountText) {
|
|
823
|
+
ctx.font = `bold 120px ${settings.font}`;
|
|
824
|
+
ctx.fillStyle = settings.accentColor;
|
|
825
|
+
|
|
826
|
+
// Adiciona sombra ao texto
|
|
827
|
+
ctx.shadowColor = 'rgba(0,0,0,0.3)';
|
|
828
|
+
ctx.shadowBlur = 10;
|
|
829
|
+
ctx.shadowOffsetX = 5;
|
|
830
|
+
ctx.shadowOffsetY = 5;
|
|
831
|
+
|
|
832
|
+
ctx.fillText(settings.discountText, settings.width / 2, settings.height * 0.4);
|
|
833
|
+
|
|
834
|
+
// Remove a sombra
|
|
835
|
+
ctx.shadowColor = 'transparent';
|
|
836
|
+
ctx.shadowBlur = 0;
|
|
837
|
+
ctx.shadowOffsetX = 0;
|
|
838
|
+
ctx.shadowOffsetY = 0;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Título
|
|
842
|
+
ctx.font = `bold 72px ${settings.font}`;
|
|
843
|
+
ctx.fillStyle = settings.textColor;
|
|
844
|
+
utils.wrapTextCentered(ctx, settings.title.toUpperCase(), settings.width / 2, settings.height * 0.55, settings.width * 0.8, 80);
|
|
845
|
+
|
|
846
|
+
// Subtítulo
|
|
847
|
+
if (settings.subtitle) {
|
|
848
|
+
ctx.font = `300 36px ${settings.font}`;
|
|
849
|
+
ctx.fillStyle = settings.textColor;
|
|
850
|
+
utils.wrapTextCentered(ctx, settings.subtitle, settings.width / 2, settings.height * 0.7, settings.width * 0.7, 50);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Botão CTA
|
|
854
|
+
const ctaWidth = 350;
|
|
855
|
+
const ctaHeight = 80;
|
|
856
|
+
const ctaX = (settings.width - ctaWidth) / 2;
|
|
857
|
+
const ctaY = settings.height * 0.8;
|
|
858
|
+
|
|
859
|
+
// Desenha o fundo do botão
|
|
860
|
+
ctx.fillStyle = settings.accentColor;
|
|
861
|
+
utils.roundRect(ctx, ctaX, ctaY, ctaWidth, ctaHeight, 40, true);
|
|
862
|
+
|
|
863
|
+
// Desenha o texto do botão
|
|
864
|
+
ctx.font = `bold 32px ${settings.font}`;
|
|
865
|
+
ctx.fillStyle = settings.backgroundColor;
|
|
866
|
+
ctx.fillText(settings.ctaText, settings.width / 2, ctaY + 50);
|
|
867
|
+
|
|
868
|
+
// Data de validade
|
|
869
|
+
if (settings.validUntil) {
|
|
870
|
+
ctx.font = `18px ${settings.font}`;
|
|
871
|
+
ctx.fillStyle = settings.textColor;
|
|
872
|
+
ctx.fillText(settings.validUntil, settings.width / 2, settings.height * 0.95);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Restaura o alinhamento de texto
|
|
876
|
+
ctx.textAlign = 'left';
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Desenha o estilo seasonal do banner de promoção
|
|
881
|
+
*
|
|
882
|
+
* @async
|
|
883
|
+
* @param {CanvasRenderingContext2D} ctx - Contexto de renderização
|
|
884
|
+
* @param {Object} settings - Configurações do banner
|
|
885
|
+
*/
|
|
886
|
+
async function drawSeasonalPromotionStyle(ctx, settings) {
|
|
887
|
+
// Configurações específicas para cada temporada
|
|
888
|
+
const seasonalSettings = {
|
|
889
|
+
christmas: {
|
|
890
|
+
backgroundColor: '#D42426',
|
|
891
|
+
accentColor: '#0F8A5F',
|
|
892
|
+
textColor: '#FFFFFF',
|
|
893
|
+
decorations: ['snowflake', 'gift', 'tree']
|
|
894
|
+
},
|
|
895
|
+
halloween: {
|
|
896
|
+
backgroundColor: '#FF6600',
|
|
897
|
+
accentColor: '#000000',
|
|
898
|
+
textColor: '#FFFFFF',
|
|
899
|
+
decorations: ['pumpkin', 'ghost', 'bat']
|
|
900
|
+
},
|
|
901
|
+
summer: {
|
|
902
|
+
backgroundColor: '#00BFFF',
|
|
903
|
+
accentColor: '#FFD700',
|
|
904
|
+
textColor: '#FFFFFF',
|
|
905
|
+
decorations: ['sun', 'palm', 'wave']
|
|
906
|
+
},
|
|
907
|
+
blackfriday: {
|
|
908
|
+
backgroundColor: '#000000',
|
|
909
|
+
accentColor: '#FF0000',
|
|
910
|
+
textColor: '#FFFFFF',
|
|
911
|
+
decorations: ['tag', 'cart', 'discount']
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// Usa as configurações da temporada selecionada ou as configurações padrão
|
|
916
|
+
const season = settings.season.toLowerCase();
|
|
917
|
+
const currentSeason = seasonalSettings[season] || {
|
|
918
|
+
backgroundColor: settings.backgroundColor,
|
|
919
|
+
accentColor: settings.accentColor,
|
|
920
|
+
textColor: settings.textColor,
|
|
921
|
+
decorations: []
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
// Desenha o fundo
|
|
925
|
+
ctx.fillStyle = currentSeason.backgroundColor;
|
|
926
|
+
ctx.fillRect(0, 0, settings.width, settings.height);
|
|
927
|
+
|
|
928
|
+
// Se tiver imagem de fundo, desenha com sobreposição
|
|
929
|
+
if (settings.backgroundImageURL) {
|
|
930
|
+
const backgroundImage = await utils.loadImage(settings.backgroundImageURL);
|
|
931
|
+
ctx.drawImage(backgroundImage, 0, 0, settings.width, settings.height);
|
|
932
|
+
|
|
933
|
+
// Adiciona sobreposição se necessário
|
|
934
|
+
if (settings.showOverlay) {
|
|
935
|
+
ctx.fillStyle = utils.hexToRgba(currentSeason.backgroundColor, settings.overlayOpacity);
|
|
936
|
+
ctx.fillRect(0, 0, settings.width, settings.height);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Adiciona decorações sazonais
|
|
941
|
+
if (currentSeason.decorations.length > 0) {
|
|
942
|
+
await addSeasonalDecorations(ctx, settings, currentSeason.decorations);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Desenha o conteúdo de texto centralizado
|
|
946
|
+
ctx.textAlign = 'center';
|
|
947
|
+
|
|
948
|
+
// Título
|
|
949
|
+
ctx.font = `bold 72px ${settings.font}`;
|
|
950
|
+
ctx.fillStyle = currentSeason.textColor;
|
|
951
|
+
utils.wrapTextCentered(ctx, settings.title.toUpperCase(), settings.width / 2, settings.height * 0.35, settings.width * 0.8, 80);
|
|
952
|
+
|
|
953
|
+
// Subtítulo
|
|
954
|
+
if (settings.subtitle) {
|
|
955
|
+
ctx.font = `300 36px ${settings.font}`;
|
|
956
|
+
ctx.fillStyle = currentSeason.textColor;
|
|
957
|
+
utils.wrapTextCentered(ctx, settings.subtitle, settings.width / 2, settings.height * 0.5, settings.width * 0.7, 50);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Texto de desconto
|
|
961
|
+
if (settings.discountText) {
|
|
962
|
+
const discountSize = 130;
|
|
963
|
+
const discountX = settings.width / 2;
|
|
964
|
+
const discountY = settings.height * 0.65;
|
|
965
|
+
|
|
966
|
+
// Desenha um círculo para o desconto
|
|
967
|
+
ctx.fillStyle = currentSeason.accentColor;
|
|
968
|
+
ctx.beginPath();
|
|
969
|
+
ctx.arc(discountX, discountY, discountSize, 0, Math.PI * 2);
|
|
970
|
+
ctx.fill();
|
|
971
|
+
|
|
972
|
+
// Desenha o texto de desconto
|
|
973
|
+
ctx.font = `bold 54px ${settings.font}`;
|
|
974
|
+
ctx.fillStyle = currentSeason.textColor;
|
|
975
|
+
ctx.fillText(settings.discountText, discountX, discountY + 15);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Botão CTA
|
|
979
|
+
const ctaWidth = 300;
|
|
980
|
+
const ctaHeight = 70;
|
|
981
|
+
const ctaX = (settings.width - ctaWidth) / 2;
|
|
982
|
+
const ctaY = settings.height * 0.8;
|
|
983
|
+
|
|
984
|
+
// Desenha o fundo do botão
|
|
985
|
+
ctx.fillStyle = currentSeason.accentColor;
|
|
986
|
+
utils.roundRect(ctx, ctaX, ctaY, ctaWidth, ctaHeight, 35, true);
|
|
987
|
+
|
|
988
|
+
// Desenha o texto do botão
|
|
989
|
+
ctx.font = `bold 28px ${settings.font}`;
|
|
990
|
+
ctx.fillStyle = currentSeason.textColor;
|
|
991
|
+
ctx.fillText(settings.ctaText, settings.width / 2, ctaY + 45);
|
|
992
|
+
|
|
993
|
+
// Data de validade
|
|
994
|
+
if (settings.validUntil) {
|
|
995
|
+
ctx.font = `16px ${settings.font}`;
|
|
996
|
+
ctx.fillStyle = currentSeason.textColor;
|
|
997
|
+
ctx.fillText(settings.validUntil, settings.width / 2, settings.height * 0.95);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Restaura o alinhamento de texto
|
|
1001
|
+
ctx.textAlign = 'left';
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Adiciona decorações sazonais ao banner
|
|
1006
|
+
*
|
|
1007
|
+
* @async
|
|
1008
|
+
* @param {CanvasRenderingContext2D} ctx - Contexto de renderização
|
|
1009
|
+
* @param {Object} settings - Configurações do banner
|
|
1010
|
+
* @param {Array<string>} decorations - Lista de decorações a serem adicionadas
|
|
1011
|
+
*/
|
|
1012
|
+
async function addSeasonalDecorations(ctx, settings, decorations) {
|
|
1013
|
+
// Implementação simplificada - em uma versão real, carregaria imagens de decorações
|
|
1014
|
+
// ou desenharia formas específicas para cada tipo de decoração
|
|
1015
|
+
|
|
1016
|
+
for (let i = 0; i < 10; i++) {
|
|
1017
|
+
const size = Math.random() * 50 + 20;
|
|
1018
|
+
const x = Math.random() * settings.width;
|
|
1019
|
+
const y = Math.random() * settings.height;
|
|
1020
|
+
|
|
1021
|
+
ctx.fillStyle = utils.hexToRgba('#FFFFFF', 0.2);
|
|
1022
|
+
|
|
1023
|
+
// Escolhe uma decoração aleatória da lista
|
|
1024
|
+
const decoration = decorations[Math.floor(Math.random() * decorations.length)];
|
|
1025
|
+
|
|
1026
|
+
switch (decoration) {
|
|
1027
|
+
case 'snowflake':
|
|
1028
|
+
drawSnowflake(ctx, x, y, size);
|
|
1029
|
+
break;
|
|
1030
|
+
case 'gift':
|
|
1031
|
+
drawGift(ctx, x, y, size);
|
|
1032
|
+
break;
|
|
1033
|
+
case 'tree':
|
|
1034
|
+
drawTree(ctx, x, y, size);
|
|
1035
|
+
break;
|
|
1036
|
+
case 'pumpkin':
|
|
1037
|
+
drawPumpkin(ctx, x, y, size);
|
|
1038
|
+
break;
|
|
1039
|
+
case 'ghost':
|
|
1040
|
+
drawGhost(ctx, x, y, size);
|
|
1041
|
+
break;
|
|
1042
|
+
case 'bat':
|
|
1043
|
+
drawBat(ctx, x, y, size);
|
|
1044
|
+
break;
|
|
1045
|
+
case 'sun':
|
|
1046
|
+
drawSun(ctx, x, y, size);
|
|
1047
|
+
break;
|
|
1048
|
+
case 'palm':
|
|
1049
|
+
drawPalm(ctx, x, y, size);
|
|
1050
|
+
break;
|
|
1051
|
+
case 'wave':
|
|
1052
|
+
drawWave(ctx, x, y, size);
|
|
1053
|
+
break;
|
|
1054
|
+
case 'tag':
|
|
1055
|
+
drawTag(ctx, x, y, size);
|
|
1056
|
+
break;
|
|
1057
|
+
case 'cart':
|
|
1058
|
+
drawCart(ctx, x, y, size);
|
|
1059
|
+
break;
|
|
1060
|
+
case 'discount':
|
|
1061
|
+
drawDiscount(ctx, x, y, size);
|
|
1062
|
+
break;
|
|
1063
|
+
default:
|
|
1064
|
+
// Desenha um círculo como fallback
|
|
1065
|
+
ctx.beginPath();
|
|
1066
|
+
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
|
|
1067
|
+
ctx.fill();
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Funções para desenhar decorações sazonais
|
|
1074
|
+
// Estas são implementações simplificadas - em uma versão real, seriam mais detalhadas
|
|
1075
|
+
|
|
1076
|
+
function drawSnowflake(ctx, x, y, size) {
|
|
1077
|
+
ctx.beginPath();
|
|
1078
|
+
for (let i = 0; i < 6; i++) {
|
|
1079
|
+
const angle = (Math.PI / 3) * i;
|
|
1080
|
+
ctx.moveTo(x, y);
|
|
1081
|
+
ctx.lineTo(x + Math.cos(angle) * size, y + Math.sin(angle) * size);
|
|
1082
|
+
}
|
|
1083
|
+
ctx.stroke();
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function drawGift(ctx, x, y, size) {
|
|
1087
|
+
ctx.fillRect(x - size / 2, y - size / 2, size, size);
|
|
1088
|
+
ctx.fillRect(x - size / 4, y - size / 2 - size / 4, size / 2, size * 1.5);
|
|
1089
|
+
ctx.fillRect(x - size / 2 - size / 4, y - size / 4, size * 1.5, size / 2);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function drawTree(ctx, x, y, size) {
|
|
1093
|
+
ctx.beginPath();
|
|
1094
|
+
ctx.moveTo(x, y - size);
|
|
1095
|
+
ctx.lineTo(x + size / 2, y);
|
|
1096
|
+
ctx.lineTo(x - size / 2, y);
|
|
1097
|
+
ctx.closePath();
|
|
1098
|
+
ctx.fill();
|
|
1099
|
+
ctx.fillRect(x - size / 6, y, size / 3, size / 2);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function drawPumpkin(ctx, x, y, size) {
|
|
1103
|
+
ctx.beginPath();
|
|
1104
|
+
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
|
|
1105
|
+
ctx.fill();
|
|
1106
|
+
ctx.fillRect(x - size / 10, y - size / 2 - size / 5, size / 5, size / 5);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function drawGhost(ctx, x, y, size) {
|
|
1110
|
+
ctx.beginPath();
|
|
1111
|
+
ctx.arc(x, y - size / 3, size / 2, Math.PI, 0, true);
|
|
1112
|
+
ctx.lineTo(x + size / 2, y + size / 2);
|
|
1113
|
+
ctx.lineTo(x + size / 4, y + size / 4);
|
|
1114
|
+
ctx.lineTo(x, y + size / 2);
|
|
1115
|
+
ctx.lineTo(x - size / 4, y + size / 4);
|
|
1116
|
+
ctx.lineTo(x - size / 2, y + size / 2);
|
|
1117
|
+
ctx.closePath();
|
|
1118
|
+
ctx.fill();
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function drawBat(ctx, x, y, size) {
|
|
1122
|
+
ctx.beginPath();
|
|
1123
|
+
ctx.arc(x, y, size / 4, 0, Math.PI * 2);
|
|
1124
|
+
ctx.fill();
|
|
1125
|
+
ctx.beginPath();
|
|
1126
|
+
ctx.arc(x - size / 2, y - size / 4, size / 3, 0, Math.PI * 2);
|
|
1127
|
+
ctx.fill();
|
|
1128
|
+
ctx.beginPath();
|
|
1129
|
+
ctx.arc(x + size / 2, y - size / 4, size / 3, 0, Math.PI * 2);
|
|
1130
|
+
ctx.fill();
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function drawSun(ctx, x, y, size) {
|
|
1134
|
+
ctx.beginPath();
|
|
1135
|
+
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
|
|
1136
|
+
ctx.fill();
|
|
1137
|
+
for (let i = 0; i < 8; i++) {
|
|
1138
|
+
const angle = (Math.PI / 4) * i;
|
|
1139
|
+
ctx.beginPath();
|
|
1140
|
+
ctx.moveTo(x + Math.cos(angle) * size / 2, y + Math.sin(angle) * size / 2);
|
|
1141
|
+
ctx.lineTo(x + Math.cos(angle) * size, y + Math.sin(angle) * size);
|
|
1142
|
+
ctx.stroke();
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function drawPalm(ctx, x, y, size) {
|
|
1147
|
+
ctx.fillRect(x - size / 10, y, size / 5, size);
|
|
1148
|
+
for (let i = 0; i < 5; i++) {
|
|
1149
|
+
const angle = (Math.PI / 6) + (Math.PI / 12) * i;
|
|
1150
|
+
ctx.beginPath();
|
|
1151
|
+
ctx.ellipse(x, y, size / 3, size, angle, 0, Math.PI);
|
|
1152
|
+
ctx.fill();
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function drawWave(ctx, x, y, size) {
|
|
1157
|
+
ctx.beginPath();
|
|
1158
|
+
ctx.moveTo(x - size, y);
|
|
1159
|
+
for (let i = 0; i < 4; i++) {
|
|
1160
|
+
const cp1x = x - size + (i * size / 2);
|
|
1161
|
+
const cp1y = y + ((i % 2 === 0) ? -size / 3 : size / 3);
|
|
1162
|
+
const cp2x = x - size + (i * size / 2) + size / 4;
|
|
1163
|
+
const cp2y = y + ((i % 2 === 0) ? size / 3 : -size / 3);
|
|
1164
|
+
const endX = x - size + ((i + 1) * size / 2);
|
|
1165
|
+
const endY = y;
|
|
1166
|
+
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY);
|
|
1167
|
+
}
|
|
1168
|
+
ctx.stroke();
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function drawTag(ctx, x, y, size) {
|
|
1172
|
+
ctx.beginPath();
|
|
1173
|
+
ctx.moveTo(x, y - size / 2);
|
|
1174
|
+
ctx.lineTo(x + size / 2, y);
|
|
1175
|
+
ctx.lineTo(x, y + size / 2);
|
|
1176
|
+
ctx.lineTo(x - size / 2, y);
|
|
1177
|
+
ctx.closePath();
|
|
1178
|
+
ctx.fill();
|
|
1179
|
+
ctx.beginPath();
|
|
1180
|
+
ctx.arc(x, y, size / 6, 0, Math.PI * 2);
|
|
1181
|
+
ctx.stroke();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function drawCart(ctx, x, y, size) {
|
|
1185
|
+
ctx.beginPath();
|
|
1186
|
+
ctx.moveTo(x - size / 2, y);
|
|
1187
|
+
ctx.lineTo(x + size / 2, y);
|
|
1188
|
+
ctx.lineTo(x + size / 3, y - size / 2);
|
|
1189
|
+
ctx.lineTo(x - size / 3, y - size / 2);
|
|
1190
|
+
ctx.closePath();
|
|
1191
|
+
ctx.fill();
|
|
1192
|
+
ctx.beginPath();
|
|
1193
|
+
ctx.arc(x - size / 4, y + size / 6, size / 6, 0, Math.PI * 2);
|
|
1194
|
+
ctx.fill();
|
|
1195
|
+
ctx.beginPath();
|
|
1196
|
+
ctx.arc(x + size / 4, y + size / 6, size / 6, 0, Math.PI * 2);
|
|
1197
|
+
ctx.fill();
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function drawDiscount(ctx, x, y, size) {
|
|
1201
|
+
ctx.beginPath();
|
|
1202
|
+
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
|
|
1203
|
+
ctx.stroke();
|
|
1204
|
+
ctx.beginPath();
|
|
1205
|
+
ctx.moveTo(x - size / 3, y - size / 3);
|
|
1206
|
+
ctx.lineTo(x + size / 3, y + size / 3);
|
|
1207
|
+
ctx.stroke();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
module.exports = {
|
|
1211
|
+
createProductBanner,
|
|
1212
|
+
createPromotionBanner
|
|
1213
|
+
};
|
|
1214
|
+
|