fuelcard 1.0.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.
- package/15.png +0 -0
- package/README.md +125 -0
- package/assets/banner-base.svg +76 -0
- package/assets/banner.png +0 -0
- package/assets/mascot.png +0 -0
- package/file_0000000041d871f8a662e051a98493c7.png +0 -0
- package/generate-banner.js +145 -0
- package/package.json +31 -0
- package/src/index.js +25 -0
- package/src/themes/Aqua.js +156 -0
- package/src/themes/Flame.js +224 -0
- package/src/themes/Fuego.js +155 -0
- package/src/themes/Fuelex.js +212 -0
- package/src/themes/Oscuro.js +157 -0
- package/src/themes/Rosa.js +156 -0
- package/test.js +129 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ╔═══════════════════════════════════════════════════════════════╗
|
|
3
|
+
* ║ FUELCARD - Flame Theme ║
|
|
4
|
+
* ║ Premium theme with Fuelcard mascot ║
|
|
5
|
+
* ╚═══════════════════════════════════════════════════════════════╝
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createCanvas, loadImage } from '@napi-rs/canvas';
|
|
9
|
+
import { cropImage } from 'cropify';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Path to mascot image
|
|
17
|
+
const MASCOT_PATH = path.join(__dirname, '../../assets/mascot.png');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} FlameOptions
|
|
21
|
+
* @property {string|Buffer} thumbnail - Album art URL or buffer
|
|
22
|
+
* @property {string} [trackName='Unknown Track'] - Song title
|
|
23
|
+
* @property {string} [artistName='Unknown Artist'] - Artist name
|
|
24
|
+
* @property {string} [requester] - Who requested the track
|
|
25
|
+
* @property {number} [progress=0] - Progress percentage (0-100)
|
|
26
|
+
* @property {string} [startTime='0:00'] - Current timestamp
|
|
27
|
+
* @property {string} [endTime='0:00'] - Total duration
|
|
28
|
+
* @property {boolean} [showMascot=true] - Show the Fuelcard mascot
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a flame-styled music card with optional mascot
|
|
33
|
+
* @param {FlameOptions} options
|
|
34
|
+
* @returns {Promise<Buffer>}
|
|
35
|
+
*/
|
|
36
|
+
export const Flame = async ({
|
|
37
|
+
thumbnail,
|
|
38
|
+
trackName = 'Unknown Track',
|
|
39
|
+
artistName = 'Unknown Artist',
|
|
40
|
+
requester = '',
|
|
41
|
+
progress = 0,
|
|
42
|
+
startTime = '0:00',
|
|
43
|
+
endTime = '0:00',
|
|
44
|
+
showMascot = true
|
|
45
|
+
}) => {
|
|
46
|
+
const width = 900;
|
|
47
|
+
const height = 280;
|
|
48
|
+
|
|
49
|
+
// Flame theme colors
|
|
50
|
+
const primaryColor = '#ff6b00'; // Orange
|
|
51
|
+
const secondaryColor = '#ffd700'; // Gold
|
|
52
|
+
const bgColor = '#0a0a0a';
|
|
53
|
+
|
|
54
|
+
// Truncate text
|
|
55
|
+
if (trackName.length > 32) trackName = trackName.substring(0, 29) + '...';
|
|
56
|
+
if (artistName.length > 35) artistName = artistName.substring(0, 32) + '...';
|
|
57
|
+
if (requester && requester.length > 20) requester = requester.substring(0, 17) + '...';
|
|
58
|
+
|
|
59
|
+
const safeProgress = Math.min(Math.max(progress, 0), 100);
|
|
60
|
+
const progressWidth = (safeProgress / 100) * 450;
|
|
61
|
+
|
|
62
|
+
// Create canvas
|
|
63
|
+
const canvas = createCanvas(width, height);
|
|
64
|
+
const ctx = canvas.getContext('2d');
|
|
65
|
+
|
|
66
|
+
// Dark background with subtle gradient
|
|
67
|
+
const gradient = ctx.createLinearGradient(0, 0, width, height);
|
|
68
|
+
gradient.addColorStop(0, bgColor);
|
|
69
|
+
gradient.addColorStop(0.5, '#111111');
|
|
70
|
+
gradient.addColorStop(1, '#0a0a0a');
|
|
71
|
+
|
|
72
|
+
ctx.fillStyle = gradient;
|
|
73
|
+
ctx.beginPath();
|
|
74
|
+
ctx.roundRect(0, 0, width, height, 20);
|
|
75
|
+
ctx.fill();
|
|
76
|
+
|
|
77
|
+
// Flame gradient border
|
|
78
|
+
const borderGradient = ctx.createLinearGradient(0, 0, width, 0);
|
|
79
|
+
borderGradient.addColorStop(0, primaryColor);
|
|
80
|
+
borderGradient.addColorStop(0.5, secondaryColor);
|
|
81
|
+
borderGradient.addColorStop(1, primaryColor);
|
|
82
|
+
|
|
83
|
+
ctx.strokeStyle = borderGradient;
|
|
84
|
+
ctx.lineWidth = 3;
|
|
85
|
+
ctx.beginPath();
|
|
86
|
+
ctx.roundRect(2, 2, width - 4, height - 4, 19);
|
|
87
|
+
ctx.stroke();
|
|
88
|
+
|
|
89
|
+
// Load and draw thumbnail
|
|
90
|
+
try {
|
|
91
|
+
const thumbBuffer = await cropImage({
|
|
92
|
+
imagePath: thumbnail,
|
|
93
|
+
width: 200,
|
|
94
|
+
height: 200,
|
|
95
|
+
borderRadius: 20
|
|
96
|
+
});
|
|
97
|
+
const thumbImage = await loadImage(thumbBuffer);
|
|
98
|
+
|
|
99
|
+
// Flame glow effect
|
|
100
|
+
ctx.shadowColor = primaryColor;
|
|
101
|
+
ctx.shadowBlur = 20;
|
|
102
|
+
ctx.fillStyle = primaryColor;
|
|
103
|
+
ctx.beginPath();
|
|
104
|
+
ctx.roundRect(30, 40, 200, 200, 20);
|
|
105
|
+
ctx.fill();
|
|
106
|
+
ctx.shadowBlur = 0;
|
|
107
|
+
|
|
108
|
+
// Draw thumbnail
|
|
109
|
+
ctx.drawImage(thumbImage, 30, 40, 200, 200);
|
|
110
|
+
|
|
111
|
+
// Gold border
|
|
112
|
+
ctx.strokeStyle = secondaryColor;
|
|
113
|
+
ctx.lineWidth = 3;
|
|
114
|
+
ctx.beginPath();
|
|
115
|
+
ctx.roundRect(30, 40, 200, 200, 20);
|
|
116
|
+
ctx.stroke();
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// Placeholder
|
|
119
|
+
ctx.fillStyle = '#1a1a1a';
|
|
120
|
+
ctx.beginPath();
|
|
121
|
+
ctx.roundRect(30, 40, 200, 200, 20);
|
|
122
|
+
ctx.fill();
|
|
123
|
+
|
|
124
|
+
ctx.fillStyle = primaryColor;
|
|
125
|
+
ctx.font = 'bold 60px Arial';
|
|
126
|
+
ctx.textAlign = 'center';
|
|
127
|
+
ctx.textBaseline = 'middle';
|
|
128
|
+
ctx.fillText('🔥', 130, 140);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Draw mascot if enabled
|
|
132
|
+
if (showMascot) {
|
|
133
|
+
try {
|
|
134
|
+
const mascotImage = await loadImage(MASCOT_PATH);
|
|
135
|
+
// Draw mascot in bottom right corner
|
|
136
|
+
ctx.drawImage(mascotImage, width - 140, height - 140, 130, 130);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
// Mascot not found, continue without it
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Text content
|
|
143
|
+
ctx.textAlign = 'left';
|
|
144
|
+
ctx.textBaseline = 'top';
|
|
145
|
+
|
|
146
|
+
// Track name with flame glow
|
|
147
|
+
ctx.shadowColor = primaryColor;
|
|
148
|
+
ctx.shadowBlur = 10;
|
|
149
|
+
ctx.fillStyle = '#ffffff';
|
|
150
|
+
ctx.font = 'bold 30px Arial';
|
|
151
|
+
ctx.fillText(trackName, 280, 45);
|
|
152
|
+
ctx.shadowBlur = 0;
|
|
153
|
+
|
|
154
|
+
// Artist name
|
|
155
|
+
ctx.fillStyle = '#cccccc';
|
|
156
|
+
ctx.font = '20px Arial';
|
|
157
|
+
ctx.fillText(artistName, 280, 85);
|
|
158
|
+
|
|
159
|
+
// Requester with gold accent
|
|
160
|
+
if (requester) {
|
|
161
|
+
ctx.fillStyle = secondaryColor;
|
|
162
|
+
ctx.font = '16px Arial';
|
|
163
|
+
ctx.fillText(`Requested by ${requester}`, 280, 120);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Progress bar
|
|
167
|
+
const progressY = 175;
|
|
168
|
+
const progressX = 280;
|
|
169
|
+
const barWidth = showMascot ? 450 : 520;
|
|
170
|
+
const actualProgressWidth = (safeProgress / 100) * barWidth;
|
|
171
|
+
|
|
172
|
+
// Background
|
|
173
|
+
ctx.fillStyle = 'rgba(255,255,255,0.12)';
|
|
174
|
+
ctx.beginPath();
|
|
175
|
+
ctx.roundRect(progressX, progressY, barWidth, 14, 7);
|
|
176
|
+
ctx.fill();
|
|
177
|
+
|
|
178
|
+
// Flame gradient fill
|
|
179
|
+
if (safeProgress > 0) {
|
|
180
|
+
const fillGradient = ctx.createLinearGradient(progressX, 0, progressX + actualProgressWidth, 0);
|
|
181
|
+
fillGradient.addColorStop(0, primaryColor);
|
|
182
|
+
fillGradient.addColorStop(1, secondaryColor);
|
|
183
|
+
|
|
184
|
+
ctx.fillStyle = fillGradient;
|
|
185
|
+
ctx.beginPath();
|
|
186
|
+
ctx.roundRect(progressX, progressY, actualProgressWidth, 14, 7);
|
|
187
|
+
ctx.fill();
|
|
188
|
+
|
|
189
|
+
// Glowing knob
|
|
190
|
+
ctx.shadowColor = secondaryColor;
|
|
191
|
+
ctx.shadowBlur = 10;
|
|
192
|
+
ctx.fillStyle = '#ffffff';
|
|
193
|
+
ctx.beginPath();
|
|
194
|
+
ctx.arc(progressX + actualProgressWidth, progressY + 7, 9, 0, Math.PI * 2);
|
|
195
|
+
ctx.fill();
|
|
196
|
+
ctx.shadowBlur = 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Timestamps
|
|
200
|
+
ctx.fillStyle = '#888888';
|
|
201
|
+
ctx.font = '14px Arial';
|
|
202
|
+
ctx.textAlign = 'left';
|
|
203
|
+
ctx.fillText(startTime, progressX, progressY + 22);
|
|
204
|
+
ctx.textAlign = 'right';
|
|
205
|
+
ctx.fillText(endTime, progressX + barWidth, progressY + 22);
|
|
206
|
+
|
|
207
|
+
// Branding
|
|
208
|
+
ctx.fillStyle = '#444444';
|
|
209
|
+
ctx.font = '12px Arial';
|
|
210
|
+
ctx.textAlign = 'right';
|
|
211
|
+
ctx.fillText('Powered by Fuelcard', width - 20, height - 15);
|
|
212
|
+
|
|
213
|
+
// Crop final image
|
|
214
|
+
const finalImage = await cropImage({
|
|
215
|
+
imagePath: canvas.toBuffer('image/png'),
|
|
216
|
+
width: width,
|
|
217
|
+
height: height,
|
|
218
|
+
borderRadius: 30
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return finalImage;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export default Flame;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ╔═══════════════════════════════════════════════════════════════╗
|
|
3
|
+
* ║ FUELCARD - Fuego Theme ║
|
|
4
|
+
* ║ Orange/Gold theme with mascot (like reference cards) ║
|
|
5
|
+
* ╚═══════════════════════════════════════════════════════════════╝
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createCanvas, loadImage } from '@napi-rs/canvas';
|
|
9
|
+
import { cropImage } from 'cropify';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const MASCOT_PATH = path.join(__dirname, '../../assets/mascot.png');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate a Fuego-styled music card (orange/gold like reference)
|
|
20
|
+
*/
|
|
21
|
+
export const Fuego = async ({
|
|
22
|
+
thumbnail,
|
|
23
|
+
trackName = 'Unknown Track',
|
|
24
|
+
artistName = 'Unknown Artist',
|
|
25
|
+
progress = 0,
|
|
26
|
+
startTime = '0:00',
|
|
27
|
+
endTime = '0:00'
|
|
28
|
+
}) => {
|
|
29
|
+
const width = 600;
|
|
30
|
+
const height = 170;
|
|
31
|
+
|
|
32
|
+
// Truncate text
|
|
33
|
+
if (trackName.length > 22) trackName = trackName.substring(0, 19) + '...';
|
|
34
|
+
if (artistName.length > 28) artistName = artistName.substring(0, 25) + '...';
|
|
35
|
+
|
|
36
|
+
const safeProgress = Math.min(Math.max(progress, 0), 100);
|
|
37
|
+
|
|
38
|
+
const canvas = createCanvas(width, height);
|
|
39
|
+
const ctx = canvas.getContext('2d');
|
|
40
|
+
|
|
41
|
+
// Solid orange/gold background
|
|
42
|
+
ctx.fillStyle = '#e67e22';
|
|
43
|
+
ctx.beginPath();
|
|
44
|
+
ctx.roundRect(0, 0, width, height, 15);
|
|
45
|
+
ctx.fill();
|
|
46
|
+
|
|
47
|
+
// Subtle gradient overlay
|
|
48
|
+
const gradient = ctx.createLinearGradient(0, 0, width, 0);
|
|
49
|
+
gradient.addColorStop(0, 'rgba(0,0,0,0.1)');
|
|
50
|
+
gradient.addColorStop(0.6, 'rgba(0,0,0,0)');
|
|
51
|
+
gradient.addColorStop(1, 'rgba(0,0,0,0.2)');
|
|
52
|
+
ctx.fillStyle = gradient;
|
|
53
|
+
ctx.beginPath();
|
|
54
|
+
ctx.roundRect(0, 0, width, height, 15);
|
|
55
|
+
ctx.fill();
|
|
56
|
+
|
|
57
|
+
// Load thumbnail
|
|
58
|
+
const thumbSize = 100;
|
|
59
|
+
const thumbX = 20;
|
|
60
|
+
const thumbY = (height - thumbSize) / 2;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const thumbBuffer = await cropImage({
|
|
64
|
+
imagePath: thumbnail,
|
|
65
|
+
width: thumbSize,
|
|
66
|
+
height: thumbSize,
|
|
67
|
+
borderRadius: 12
|
|
68
|
+
});
|
|
69
|
+
const thumbImage = await loadImage(thumbBuffer);
|
|
70
|
+
|
|
71
|
+
// Shadow behind thumbnail
|
|
72
|
+
ctx.shadowColor = 'rgba(0,0,0,0.3)';
|
|
73
|
+
ctx.shadowBlur = 10;
|
|
74
|
+
ctx.shadowOffsetX = 3;
|
|
75
|
+
ctx.shadowOffsetY = 3;
|
|
76
|
+
ctx.drawImage(thumbImage, thumbX, thumbY, thumbSize, thumbSize);
|
|
77
|
+
ctx.shadowBlur = 0;
|
|
78
|
+
ctx.shadowOffsetX = 0;
|
|
79
|
+
ctx.shadowOffsetY = 0;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
|
82
|
+
ctx.beginPath();
|
|
83
|
+
ctx.roundRect(thumbX, thumbY, thumbSize, thumbSize, 12);
|
|
84
|
+
ctx.fill();
|
|
85
|
+
|
|
86
|
+
ctx.fillStyle = '#ffffff';
|
|
87
|
+
ctx.font = 'bold 40px Arial';
|
|
88
|
+
ctx.textAlign = 'center';
|
|
89
|
+
ctx.textBaseline = 'middle';
|
|
90
|
+
ctx.fillText('♪', thumbX + thumbSize / 2, thumbY + thumbSize / 2);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Text content
|
|
94
|
+
const textX = thumbX + thumbSize + 20;
|
|
95
|
+
ctx.textAlign = 'left';
|
|
96
|
+
ctx.textBaseline = 'top';
|
|
97
|
+
|
|
98
|
+
// Track name (white, bold)
|
|
99
|
+
ctx.fillStyle = '#ffffff';
|
|
100
|
+
ctx.font = 'bold 22px Arial';
|
|
101
|
+
ctx.fillText(trackName, textX, 25);
|
|
102
|
+
|
|
103
|
+
// Artist name (darker)
|
|
104
|
+
ctx.fillStyle = 'rgba(0,0,0,0.6)';
|
|
105
|
+
ctx.font = '16px Arial';
|
|
106
|
+
ctx.fillText(artistName, textX, 55);
|
|
107
|
+
|
|
108
|
+
// Progress bar
|
|
109
|
+
const progressX = textX;
|
|
110
|
+
const progressY = 95;
|
|
111
|
+
const progressWidth = 200;
|
|
112
|
+
const progressHeight = 8;
|
|
113
|
+
const actualProgress = (safeProgress / 100) * progressWidth;
|
|
114
|
+
|
|
115
|
+
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
|
116
|
+
ctx.beginPath();
|
|
117
|
+
ctx.roundRect(progressX, progressY, progressWidth, progressHeight, 4);
|
|
118
|
+
ctx.fill();
|
|
119
|
+
|
|
120
|
+
if (safeProgress > 0) {
|
|
121
|
+
ctx.fillStyle = '#ffffff';
|
|
122
|
+
ctx.beginPath();
|
|
123
|
+
ctx.roundRect(progressX, progressY, actualProgress, progressHeight, 4);
|
|
124
|
+
ctx.fill();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Timestamps
|
|
128
|
+
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
|
129
|
+
ctx.font = '12px Arial';
|
|
130
|
+
ctx.fillText(startTime, progressX, progressY + 14);
|
|
131
|
+
ctx.textAlign = 'right';
|
|
132
|
+
ctx.fillText(endTime, progressX + progressWidth, progressY + 14);
|
|
133
|
+
|
|
134
|
+
// Draw mascot on the right (extending beyond)
|
|
135
|
+
try {
|
|
136
|
+
const mascotImage = await loadImage(MASCOT_PATH);
|
|
137
|
+
const mascotHeight = 190;
|
|
138
|
+
const mascotWidth = 190;
|
|
139
|
+
const mascotX = width - mascotWidth + 40;
|
|
140
|
+
const mascotY = height - mascotHeight + 15;
|
|
141
|
+
|
|
142
|
+
ctx.drawImage(mascotImage, mascotX, mascotY, mascotWidth, mascotHeight);
|
|
143
|
+
} catch (e) { }
|
|
144
|
+
|
|
145
|
+
const finalImage = await cropImage({
|
|
146
|
+
imagePath: canvas.toBuffer('image/png'),
|
|
147
|
+
width: width,
|
|
148
|
+
height: height,
|
|
149
|
+
borderRadius: 20
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return finalImage;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default Fuego;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ╔═══════════════════════════════════════════════════════════════╗
|
|
3
|
+
* ║ FUELCARD - Fuelex Theme ║
|
|
4
|
+
* ║ Premium theme with mascot character featured prominently ║
|
|
5
|
+
* ╚═══════════════════════════════════════════════════════════════╝
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createCanvas, loadImage } from '@napi-rs/canvas';
|
|
9
|
+
import { cropImage } from 'cropify';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Path to mascot image
|
|
17
|
+
const MASCOT_PATH = path.join(__dirname, '../../assets/mascot.png');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} FuelexOptions
|
|
21
|
+
* @property {string|Buffer} thumbnail - Album art URL or buffer
|
|
22
|
+
* @property {string} [trackName='Unknown Track'] - Song title
|
|
23
|
+
* @property {string} [artistName='Unknown Artist'] - Artist name
|
|
24
|
+
* @property {number} [progress=0] - Progress percentage (0-100)
|
|
25
|
+
* @property {string} [startTime='0:00'] - Current timestamp
|
|
26
|
+
* @property {string} [endTime='0:00'] - Total duration
|
|
27
|
+
* @property {string} [backgroundColor='#1a1a2e'] - Card background color
|
|
28
|
+
* @property {string} [accentColor='#ff6b00'] - Accent color
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a Fuelex-styled music card with the mascot character
|
|
33
|
+
* @param {FuelexOptions} options
|
|
34
|
+
* @returns {Promise<Buffer>}
|
|
35
|
+
*/
|
|
36
|
+
export const Fuelex = async ({
|
|
37
|
+
thumbnail,
|
|
38
|
+
trackName = 'Unknown Track',
|
|
39
|
+
artistName = 'Unknown Artist',
|
|
40
|
+
progress = 0,
|
|
41
|
+
startTime = '0:00',
|
|
42
|
+
endTime = '0:00',
|
|
43
|
+
backgroundColor = '#1a1a2e',
|
|
44
|
+
accentColor = '#ff6b00'
|
|
45
|
+
}) => {
|
|
46
|
+
// Card dimensions (similar to reference)
|
|
47
|
+
const width = 900;
|
|
48
|
+
const height = 200;
|
|
49
|
+
|
|
50
|
+
// Truncate text
|
|
51
|
+
if (trackName.length > 28) trackName = trackName.substring(0, 25) + '...';
|
|
52
|
+
if (artistName.length > 35) artistName = artistName.substring(0, 32) + '...';
|
|
53
|
+
|
|
54
|
+
const safeProgress = Math.min(Math.max(progress, 0), 100);
|
|
55
|
+
|
|
56
|
+
// Create canvas
|
|
57
|
+
const canvas = createCanvas(width, height);
|
|
58
|
+
const ctx = canvas.getContext('2d');
|
|
59
|
+
|
|
60
|
+
// Background gradient (dark with subtle color)
|
|
61
|
+
const bgGradient = ctx.createLinearGradient(0, 0, width, 0);
|
|
62
|
+
bgGradient.addColorStop(0, backgroundColor);
|
|
63
|
+
bgGradient.addColorStop(0.7, '#0d0d1a');
|
|
64
|
+
bgGradient.addColorStop(1, '#0a0a12');
|
|
65
|
+
|
|
66
|
+
ctx.fillStyle = bgGradient;
|
|
67
|
+
ctx.beginPath();
|
|
68
|
+
ctx.roundRect(0, 0, width, height, 15);
|
|
69
|
+
ctx.fill();
|
|
70
|
+
|
|
71
|
+
// Accent border
|
|
72
|
+
const borderGradient = ctx.createLinearGradient(0, 0, width, 0);
|
|
73
|
+
borderGradient.addColorStop(0, accentColor);
|
|
74
|
+
borderGradient.addColorStop(0.5, '#ffd700');
|
|
75
|
+
borderGradient.addColorStop(1, accentColor);
|
|
76
|
+
|
|
77
|
+
ctx.strokeStyle = borderGradient;
|
|
78
|
+
ctx.lineWidth = 3;
|
|
79
|
+
ctx.beginPath();
|
|
80
|
+
ctx.roundRect(2, 2, width - 4, height - 4, 14);
|
|
81
|
+
ctx.stroke();
|
|
82
|
+
|
|
83
|
+
// Load and draw thumbnail on left side
|
|
84
|
+
const thumbSize = 140;
|
|
85
|
+
const thumbX = 25;
|
|
86
|
+
const thumbY = (height - thumbSize) / 2;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const thumbBuffer = await cropImage({
|
|
90
|
+
imagePath: thumbnail,
|
|
91
|
+
width: thumbSize,
|
|
92
|
+
height: thumbSize,
|
|
93
|
+
borderRadius: 15
|
|
94
|
+
});
|
|
95
|
+
const thumbImage = await loadImage(thumbBuffer);
|
|
96
|
+
|
|
97
|
+
// Glow effect behind thumbnail
|
|
98
|
+
ctx.shadowColor = accentColor;
|
|
99
|
+
ctx.shadowBlur = 15;
|
|
100
|
+
ctx.drawImage(thumbImage, thumbX, thumbY, thumbSize, thumbSize);
|
|
101
|
+
ctx.shadowBlur = 0;
|
|
102
|
+
|
|
103
|
+
// Border on thumbnail
|
|
104
|
+
ctx.strokeStyle = accentColor;
|
|
105
|
+
ctx.lineWidth = 2;
|
|
106
|
+
ctx.beginPath();
|
|
107
|
+
ctx.roundRect(thumbX, thumbY, thumbSize, thumbSize, 15);
|
|
108
|
+
ctx.stroke();
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// Placeholder
|
|
111
|
+
ctx.fillStyle = '#2a2a4e';
|
|
112
|
+
ctx.beginPath();
|
|
113
|
+
ctx.roundRect(thumbX, thumbY, thumbSize, thumbSize, 15);
|
|
114
|
+
ctx.fill();
|
|
115
|
+
|
|
116
|
+
ctx.fillStyle = accentColor;
|
|
117
|
+
ctx.font = 'bold 50px Arial';
|
|
118
|
+
ctx.textAlign = 'center';
|
|
119
|
+
ctx.textBaseline = 'middle';
|
|
120
|
+
ctx.fillText('♪', thumbX + thumbSize / 2, thumbY + thumbSize / 2);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Draw mascot on the right side (large and prominent)
|
|
124
|
+
try {
|
|
125
|
+
const mascotImage = await loadImage(MASCOT_PATH);
|
|
126
|
+
const mascotWidth = 220;
|
|
127
|
+
const mascotHeight = 220;
|
|
128
|
+
const mascotX = width - mascotWidth + 15;
|
|
129
|
+
const mascotY = height - mascotHeight + 20;
|
|
130
|
+
|
|
131
|
+
ctx.drawImage(mascotImage, mascotX, mascotY, mascotWidth, mascotHeight);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
// Mascot not available
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Text content (middle area)
|
|
137
|
+
const textX = thumbX + thumbSize + 25;
|
|
138
|
+
const textMaxWidth = 380;
|
|
139
|
+
|
|
140
|
+
ctx.textAlign = 'left';
|
|
141
|
+
ctx.textBaseline = 'top';
|
|
142
|
+
|
|
143
|
+
// Track name with glow
|
|
144
|
+
ctx.shadowColor = accentColor;
|
|
145
|
+
ctx.shadowBlur = 8;
|
|
146
|
+
ctx.fillStyle = '#ffffff';
|
|
147
|
+
ctx.font = 'bold 28px Arial';
|
|
148
|
+
ctx.fillText(trackName, textX, 30, textMaxWidth);
|
|
149
|
+
ctx.shadowBlur = 0;
|
|
150
|
+
|
|
151
|
+
// Artist name
|
|
152
|
+
ctx.fillStyle = '#cccccc';
|
|
153
|
+
ctx.font = '18px Arial';
|
|
154
|
+
ctx.fillText(artistName, textX, 68, textMaxWidth);
|
|
155
|
+
|
|
156
|
+
// Progress bar
|
|
157
|
+
const progressX = textX;
|
|
158
|
+
const progressY = 115;
|
|
159
|
+
const progressWidth = 360;
|
|
160
|
+
const progressHeight = 10;
|
|
161
|
+
const actualProgressWidth = (safeProgress / 100) * progressWidth;
|
|
162
|
+
|
|
163
|
+
// Background bar
|
|
164
|
+
ctx.fillStyle = 'rgba(255,255,255,0.15)';
|
|
165
|
+
ctx.beginPath();
|
|
166
|
+
ctx.roundRect(progressX, progressY, progressWidth, progressHeight, 5);
|
|
167
|
+
ctx.fill();
|
|
168
|
+
|
|
169
|
+
// Progress fill with gradient
|
|
170
|
+
if (safeProgress > 0) {
|
|
171
|
+
const progressGradient = ctx.createLinearGradient(progressX, 0, progressX + actualProgressWidth, 0);
|
|
172
|
+
progressGradient.addColorStop(0, accentColor);
|
|
173
|
+
progressGradient.addColorStop(1, '#ffd700');
|
|
174
|
+
|
|
175
|
+
ctx.fillStyle = progressGradient;
|
|
176
|
+
ctx.beginPath();
|
|
177
|
+
ctx.roundRect(progressX, progressY, actualProgressWidth, progressHeight, 5);
|
|
178
|
+
ctx.fill();
|
|
179
|
+
|
|
180
|
+
// Knob
|
|
181
|
+
ctx.fillStyle = '#ffffff';
|
|
182
|
+
ctx.beginPath();
|
|
183
|
+
ctx.arc(progressX + actualProgressWidth, progressY + progressHeight / 2, 7, 0, Math.PI * 2);
|
|
184
|
+
ctx.fill();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Timestamps
|
|
188
|
+
ctx.fillStyle = '#888888';
|
|
189
|
+
ctx.font = '14px Arial';
|
|
190
|
+
ctx.textAlign = 'left';
|
|
191
|
+
ctx.fillText(startTime, progressX, progressY + 18);
|
|
192
|
+
ctx.textAlign = 'right';
|
|
193
|
+
ctx.fillText(endTime, progressX + progressWidth, progressY + 18);
|
|
194
|
+
|
|
195
|
+
// Fuelex branding
|
|
196
|
+
ctx.fillStyle = '#555555';
|
|
197
|
+
ctx.font = 'bold 11px Arial';
|
|
198
|
+
ctx.textAlign = 'left';
|
|
199
|
+
ctx.fillText('FUELCARD', textX, height - 22);
|
|
200
|
+
|
|
201
|
+
// Crop with rounded corners
|
|
202
|
+
const finalImage = await cropImage({
|
|
203
|
+
imagePath: canvas.toBuffer('image/png'),
|
|
204
|
+
width: width,
|
|
205
|
+
height: height,
|
|
206
|
+
borderRadius: 20
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return finalImage;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export default Fuelex;
|