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
package/15.png
ADDED
|
Binary file
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Fuelcard 🔥
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
> **The Most Advanced Discord Music Card Generator**
|
|
6
|
+
>
|
|
7
|
+
> By **Ramkrishna** & **ZayDocs**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 🚀 Features
|
|
12
|
+
|
|
13
|
+
- 🎨 **4 Premium Themes** - Fuelex, Classic, Neon, Flame
|
|
14
|
+
- 🦊 **Unique Mascot** - Cyber-fox branding with golden flames
|
|
15
|
+
- ⚡ **High Performance** - Native canvas bindings (@napi-rs/canvas)
|
|
16
|
+
- 🎵 **Universal Compatibility** - Works with DisTube, Riffy, Shoukaku, Erela.js
|
|
17
|
+
- 📦 **Simple API** - Async functions with clean options
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install fuelcard
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 🎨 Themes
|
|
30
|
+
|
|
31
|
+
| Theme | Description |
|
|
32
|
+
|-------|-------------|
|
|
33
|
+
| **Fuelex** | Main theme with large mascot on the right |
|
|
34
|
+
| **Classic** | Dark background with orange accents |
|
|
35
|
+
| **Neon** | Glowing cyan/magenta cyberpunk effects |
|
|
36
|
+
| **Flame** | Fire gradient with corner mascot |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 💻 Quick Start
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
import { Fuelex, Classic, Neon, Flame } from 'fuelcard';
|
|
44
|
+
|
|
45
|
+
// Generate a Fuelex card (recommended)
|
|
46
|
+
const card = await Fuelex({
|
|
47
|
+
thumbnail: 'https://...album-art.jpg',
|
|
48
|
+
trackName: 'Blinding Lights',
|
|
49
|
+
artistName: 'The Weeknd',
|
|
50
|
+
progress: 52,
|
|
51
|
+
startTime: '1:45',
|
|
52
|
+
endTime: '3:22'
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Use with Discord.js
|
|
56
|
+
import { AttachmentBuilder } from 'discord.js';
|
|
57
|
+
const attachment = new AttachmentBuilder(card, { name: 'now-playing.png' });
|
|
58
|
+
await channel.send({ files: [attachment] });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 🎵 Integration Examples
|
|
64
|
+
|
|
65
|
+
### DisTube
|
|
66
|
+
```javascript
|
|
67
|
+
client.distube.on('playSong', async (queue, song) => {
|
|
68
|
+
const card = await Fuelex({
|
|
69
|
+
thumbnail: song.thumbnail,
|
|
70
|
+
trackName: song.name,
|
|
71
|
+
artistName: song.uploader?.name || 'Unknown',
|
|
72
|
+
progress: 0,
|
|
73
|
+
startTime: '0:00',
|
|
74
|
+
endTime: song.formattedDuration
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
queue.textChannel.send({
|
|
78
|
+
files: [new AttachmentBuilder(card, { name: 'now-playing.png' })]
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Riffy / Lavalink
|
|
84
|
+
```javascript
|
|
85
|
+
client.riffy.on('trackStart', async (player, track) => {
|
|
86
|
+
const card = await Fuelex({
|
|
87
|
+
thumbnail: track.thumbnail,
|
|
88
|
+
trackName: track.title,
|
|
89
|
+
artistName: track.author,
|
|
90
|
+
progress: 0,
|
|
91
|
+
startTime: '0:00',
|
|
92
|
+
endTime: formatTime(track.length)
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
channel.send({
|
|
96
|
+
files: [new AttachmentBuilder(card, { name: 'now-playing.png' })]
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## ⚙️ Theme Options
|
|
104
|
+
|
|
105
|
+
### Fuelex Options
|
|
106
|
+
```javascript
|
|
107
|
+
await Fuelex({
|
|
108
|
+
thumbnail: string | Buffer, // Album art (required)
|
|
109
|
+
trackName: string, // Song title
|
|
110
|
+
artistName: string, // Artist name
|
|
111
|
+
progress: number, // 0-100
|
|
112
|
+
startTime: string, // e.g., '1:45'
|
|
113
|
+
endTime: string, // e.g., '3:22'
|
|
114
|
+
backgroundColor: string, // Hex color (default: '#1a1a2e')
|
|
115
|
+
accentColor: string // Hex color (default: '#ff6b00')
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 📄 License
|
|
122
|
+
|
|
123
|
+
**Private & Proprietary** - © 2026 Ramkrishna & ZayDocs
|
|
124
|
+
|
|
125
|
+
This package is closed-source. Unauthorized distribution is prohibited.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<svg width="900" height="280" viewBox="0 0 900 280" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<!-- Gold gradient -->
|
|
4
|
+
<linearGradient id="goldGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
5
|
+
<stop offset="0%" style="stop-color:#FFD700"/>
|
|
6
|
+
<stop offset="50%" style="stop-color:#FFA500"/>
|
|
7
|
+
<stop offset="100%" style="stop-color:#FF6B00"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
|
|
10
|
+
<!-- Text glow filter -->
|
|
11
|
+
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
|
12
|
+
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
|
13
|
+
<feMerge>
|
|
14
|
+
<feMergeNode in="coloredBlur"/>
|
|
15
|
+
<feMergeNode in="SourceGraphic"/>
|
|
16
|
+
</feMerge>
|
|
17
|
+
</filter>
|
|
18
|
+
|
|
19
|
+
<!-- Border glow -->
|
|
20
|
+
<filter id="borderGlow" x="-10%" y="-10%" width="120%" height="120%">
|
|
21
|
+
<feGaussianBlur stdDeviation="4" result="blur"/>
|
|
22
|
+
<feMerge>
|
|
23
|
+
<feMergeNode in="blur"/>
|
|
24
|
+
<feMergeNode in="SourceGraphic"/>
|
|
25
|
+
</feMerge>
|
|
26
|
+
</filter>
|
|
27
|
+
</defs>
|
|
28
|
+
|
|
29
|
+
<!-- Background -->
|
|
30
|
+
<rect width="900" height="280" rx="20" fill="#0a0a0a"/>
|
|
31
|
+
|
|
32
|
+
<!-- Subtle gradient overlay -->
|
|
33
|
+
<rect width="900" height="280" rx="20" fill="url(#goldGradient)" opacity="0.05"/>
|
|
34
|
+
|
|
35
|
+
<!-- Gold border -->
|
|
36
|
+
<rect x="3" y="3" width="894" height="274" rx="18" stroke="url(#goldGradient)" stroke-width="3" fill="none" filter="url(#borderGlow)"/>
|
|
37
|
+
|
|
38
|
+
<!-- Inner subtle border -->
|
|
39
|
+
<rect x="8" y="8" width="884" height="264" rx="15" stroke="#FFD700" stroke-width="1" fill="none" opacity="0.3"/>
|
|
40
|
+
|
|
41
|
+
<!-- FUELCARD Text -->
|
|
42
|
+
<text x="50" y="150" font-family="Arial, sans-serif" font-size="90" font-weight="bold" fill="url(#goldGradient)" filter="url(#glow)">
|
|
43
|
+
FUELCARD
|
|
44
|
+
</text>
|
|
45
|
+
|
|
46
|
+
<!-- Subtitle -->
|
|
47
|
+
<text x="55" y="200" font-family="Arial, sans-serif" font-size="24" fill="#888888">
|
|
48
|
+
The Most Advanced Music Card Generator
|
|
49
|
+
</text>
|
|
50
|
+
|
|
51
|
+
<!-- Version badge -->
|
|
52
|
+
<rect x="50" y="220" width="60" height="24" rx="12" fill="url(#goldGradient)" opacity="0.8"/>
|
|
53
|
+
<text x="80" y="237" font-family="Arial, sans-serif" font-size="12" fill="#0a0a0a" text-anchor="middle" font-weight="bold">v1.0.0</text>
|
|
54
|
+
|
|
55
|
+
<!-- Authors -->
|
|
56
|
+
<text x="130" y="237" font-family="Arial, sans-serif" font-size="14" fill="#666666">
|
|
57
|
+
By Ramkrishna & ZayDocs
|
|
58
|
+
</text>
|
|
59
|
+
|
|
60
|
+
<!-- Decorative flame elements -->
|
|
61
|
+
<circle cx="520" cy="140" r="3" fill="#FF6B00" opacity="0.6">
|
|
62
|
+
<animate attributeName="opacity" values="0.6;1;0.6" dur="2s" repeatCount="indefinite"/>
|
|
63
|
+
</circle>
|
|
64
|
+
<circle cx="540" cy="130" r="2" fill="#FFD700" opacity="0.5">
|
|
65
|
+
<animate attributeName="opacity" values="0.5;1;0.5" dur="1.5s" repeatCount="indefinite"/>
|
|
66
|
+
</circle>
|
|
67
|
+
<circle cx="530" cy="155" r="2.5" fill="#FFA500" opacity="0.7">
|
|
68
|
+
<animate attributeName="opacity" values="0.7;1;0.7" dur="1.8s" repeatCount="indefinite"/>
|
|
69
|
+
</circle>
|
|
70
|
+
|
|
71
|
+
<!-- Fire emoji placeholder for animation -->
|
|
72
|
+
<text x="560" y="180" font-size="40" opacity="0.15">🔥</text>
|
|
73
|
+
|
|
74
|
+
<!-- Mascot area indicator (actual mascot will be embedded) -->
|
|
75
|
+
<rect x="620" y="20" width="260" height="240" rx="15" fill="none" stroke="#FFD700" stroke-width="1" opacity="0.2" stroke-dasharray="5,5"/>
|
|
76
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate banner with mascot for README
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createCanvas, loadImage } from '@napi-rs/canvas';
|
|
6
|
+
import { cropImage } from 'cropify';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
|
|
14
|
+
async function generateBanner() {
|
|
15
|
+
console.log('╔═══════════════════════════════════════════════════════════════╗');
|
|
16
|
+
console.log('║ FUELCARD BANNER GENERATOR ║');
|
|
17
|
+
console.log('╚═══════════════════════════════════════════════════════════════╝');
|
|
18
|
+
console.log();
|
|
19
|
+
|
|
20
|
+
const width = 900;
|
|
21
|
+
const height = 280;
|
|
22
|
+
|
|
23
|
+
const canvas = createCanvas(width, height);
|
|
24
|
+
const ctx = canvas.getContext('2d');
|
|
25
|
+
|
|
26
|
+
// Background
|
|
27
|
+
ctx.fillStyle = '#0a0a0a';
|
|
28
|
+
ctx.beginPath();
|
|
29
|
+
ctx.roundRect(0, 0, width, height, 20);
|
|
30
|
+
ctx.fill();
|
|
31
|
+
|
|
32
|
+
// Gold gradient border
|
|
33
|
+
const borderGradient = ctx.createLinearGradient(0, 0, width, height);
|
|
34
|
+
borderGradient.addColorStop(0, '#FFD700');
|
|
35
|
+
borderGradient.addColorStop(0.5, '#FFA500');
|
|
36
|
+
borderGradient.addColorStop(1, '#FF6B00');
|
|
37
|
+
|
|
38
|
+
// Outer glow effect
|
|
39
|
+
ctx.shadowColor = '#FFD700';
|
|
40
|
+
ctx.shadowBlur = 20;
|
|
41
|
+
ctx.strokeStyle = borderGradient;
|
|
42
|
+
ctx.lineWidth = 4;
|
|
43
|
+
ctx.beginPath();
|
|
44
|
+
ctx.roundRect(3, 3, width - 6, height - 6, 18);
|
|
45
|
+
ctx.stroke();
|
|
46
|
+
ctx.shadowBlur = 0;
|
|
47
|
+
|
|
48
|
+
// Inner subtle border
|
|
49
|
+
ctx.strokeStyle = 'rgba(255, 215, 0, 0.3)';
|
|
50
|
+
ctx.lineWidth = 1;
|
|
51
|
+
ctx.beginPath();
|
|
52
|
+
ctx.roundRect(10, 10, width - 20, height - 20, 15);
|
|
53
|
+
ctx.stroke();
|
|
54
|
+
|
|
55
|
+
// FUELCARD text with glow
|
|
56
|
+
ctx.shadowColor = '#FFD700';
|
|
57
|
+
ctx.shadowBlur = 15;
|
|
58
|
+
ctx.font = 'bold 85px Arial';
|
|
59
|
+
|
|
60
|
+
// Create gradient for text
|
|
61
|
+
const textGradient = ctx.createLinearGradient(50, 80, 500, 160);
|
|
62
|
+
textGradient.addColorStop(0, '#FFD700');
|
|
63
|
+
textGradient.addColorStop(0.5, '#FFA500');
|
|
64
|
+
textGradient.addColorStop(1, '#FF6B00');
|
|
65
|
+
|
|
66
|
+
ctx.fillStyle = textGradient;
|
|
67
|
+
ctx.fillText('FUELCARD', 50, 145);
|
|
68
|
+
ctx.shadowBlur = 0;
|
|
69
|
+
|
|
70
|
+
// Subtitle
|
|
71
|
+
ctx.fillStyle = '#888888';
|
|
72
|
+
ctx.font = '22px Arial';
|
|
73
|
+
ctx.fillText('The Most Advanced Music Card Generator', 55, 195);
|
|
74
|
+
|
|
75
|
+
// Version badge
|
|
76
|
+
ctx.fillStyle = '#FF6B00';
|
|
77
|
+
ctx.beginPath();
|
|
78
|
+
ctx.roundRect(50, 215, 65, 26, 13);
|
|
79
|
+
ctx.fill();
|
|
80
|
+
|
|
81
|
+
ctx.fillStyle = '#0a0a0a';
|
|
82
|
+
ctx.font = 'bold 13px Arial';
|
|
83
|
+
ctx.textAlign = 'center';
|
|
84
|
+
ctx.fillText('v1.0.0', 82, 233);
|
|
85
|
+
ctx.textAlign = 'left';
|
|
86
|
+
|
|
87
|
+
// Authors
|
|
88
|
+
ctx.fillStyle = '#666666';
|
|
89
|
+
ctx.font = '15px Arial';
|
|
90
|
+
ctx.fillText('By Ramkrishna & ZayDocs', 130, 233);
|
|
91
|
+
|
|
92
|
+
// Load and draw mascot
|
|
93
|
+
try {
|
|
94
|
+
const mascotPath = path.join(__dirname, 'assets', 'mascot.png');
|
|
95
|
+
const mascotImage = await loadImage(mascotPath);
|
|
96
|
+
|
|
97
|
+
// Draw mascot large on the right
|
|
98
|
+
const mascotSize = 250;
|
|
99
|
+
const mascotX = width - mascotSize - 30;
|
|
100
|
+
const mascotY = (height - mascotSize) / 2 + 10;
|
|
101
|
+
|
|
102
|
+
ctx.drawImage(mascotImage, mascotX, mascotY, mascotSize, mascotSize);
|
|
103
|
+
|
|
104
|
+
console.log('✓ Mascot loaded and drawn');
|
|
105
|
+
} catch (e) {
|
|
106
|
+
console.log('✗ Mascot not found:', e.message);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add some flame particles
|
|
110
|
+
const particles = [
|
|
111
|
+
{ x: 530, y: 130, r: 4, color: '#FF6B00' },
|
|
112
|
+
{ x: 550, y: 120, r: 3, color: '#FFD700' },
|
|
113
|
+
{ x: 540, y: 145, r: 3.5, color: '#FFA500' },
|
|
114
|
+
{ x: 560, y: 135, r: 2.5, color: '#FF6B00' },
|
|
115
|
+
{ x: 545, y: 155, r: 3, color: '#FFD700' },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
particles.forEach(p => {
|
|
119
|
+
ctx.beginPath();
|
|
120
|
+
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
|
121
|
+
ctx.fillStyle = p.color;
|
|
122
|
+
ctx.globalAlpha = 0.7;
|
|
123
|
+
ctx.fill();
|
|
124
|
+
});
|
|
125
|
+
ctx.globalAlpha = 1;
|
|
126
|
+
|
|
127
|
+
// Save banner
|
|
128
|
+
const outputPath = path.join(__dirname, 'assets', 'banner.png');
|
|
129
|
+
|
|
130
|
+
// Crop with rounded corners
|
|
131
|
+
const finalImage = await cropImage({
|
|
132
|
+
imagePath: canvas.toBuffer('image/png'),
|
|
133
|
+
width: width,
|
|
134
|
+
height: height,
|
|
135
|
+
borderRadius: 25
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
fs.writeFileSync(outputPath, finalImage);
|
|
139
|
+
console.log('✓ Banner saved:', outputPath);
|
|
140
|
+
|
|
141
|
+
console.log();
|
|
142
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
generateBanner().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fuelcard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The most advanced Discord music card generator - stunning visual cards for your music bot",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node test.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"discord",
|
|
12
|
+
"music",
|
|
13
|
+
"card",
|
|
14
|
+
"musicard",
|
|
15
|
+
"lavalink",
|
|
16
|
+
"riffy",
|
|
17
|
+
"distube",
|
|
18
|
+
"canvas",
|
|
19
|
+
"image",
|
|
20
|
+
"generator"
|
|
21
|
+
],
|
|
22
|
+
"author": "Ramkrishna & ZayDocs",
|
|
23
|
+
"license": "UNLICENSED",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@napi-rs/canvas": "^0.1.83",
|
|
29
|
+
"cropify": "^2.0.1"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ╔═══════════════════════════════════════════════════════════════╗
|
|
3
|
+
* ║ FUELCARD ║
|
|
4
|
+
* ║ The Most Advanced Discord Music Card Generator ║
|
|
5
|
+
* ║ ║
|
|
6
|
+
* ║ By Ramkrishna & ZayDocs ║
|
|
7
|
+
* ╚═══════════════════════════════════════════════════════════════╝
|
|
8
|
+
*
|
|
9
|
+
* @module fuelcard
|
|
10
|
+
* @private
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Main themes (like reference cards with mascot)
|
|
14
|
+
export { Fuego } from './themes/Fuego.js'; // Orange/Gold
|
|
15
|
+
export { Aqua } from './themes/Aqua.js'; // Blue/Cyan
|
|
16
|
+
export { Rosa } from './themes/Rosa.js'; // Pink/Magenta
|
|
17
|
+
export { Oscuro } from './themes/Oscuro.js'; // Dark with accent
|
|
18
|
+
|
|
19
|
+
// Premium themes
|
|
20
|
+
export { Flame } from './themes/Flame.js'; // Fire gradient
|
|
21
|
+
export { Fuelex } from './themes/Fuelex.js'; // Wide with mascot
|
|
22
|
+
|
|
23
|
+
// Version info
|
|
24
|
+
export const version = '1.0.0';
|
|
25
|
+
export const authors = ['Ramkrishna', 'ZayDocs'];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ╔═══════════════════════════════════════════════════════════════╗
|
|
3
|
+
* ║ FUELCARD - Aqua Theme ║
|
|
4
|
+
* ║ Blue/Cyan 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 an Aqua-styled music card (blue/cyan like reference)
|
|
20
|
+
*/
|
|
21
|
+
export const Aqua = 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
|
+
if (trackName.length > 22) trackName = trackName.substring(0, 19) + '...';
|
|
33
|
+
if (artistName.length > 28) artistName = artistName.substring(0, 25) + '...';
|
|
34
|
+
|
|
35
|
+
const safeProgress = Math.min(Math.max(progress, 0), 100);
|
|
36
|
+
|
|
37
|
+
const canvas = createCanvas(width, height);
|
|
38
|
+
const ctx = canvas.getContext('2d');
|
|
39
|
+
|
|
40
|
+
// Blue/Cyan gradient background
|
|
41
|
+
const bgGradient = ctx.createLinearGradient(0, 0, width, height);
|
|
42
|
+
bgGradient.addColorStop(0, '#3498db');
|
|
43
|
+
bgGradient.addColorStop(1, '#2980b9');
|
|
44
|
+
|
|
45
|
+
ctx.fillStyle = bgGradient;
|
|
46
|
+
ctx.beginPath();
|
|
47
|
+
ctx.roundRect(0, 0, width, height, 15);
|
|
48
|
+
ctx.fill();
|
|
49
|
+
|
|
50
|
+
// Subtle overlay
|
|
51
|
+
const overlay = ctx.createLinearGradient(0, 0, width, 0);
|
|
52
|
+
overlay.addColorStop(0, 'rgba(0,0,0,0.05)');
|
|
53
|
+
overlay.addColorStop(0.6, 'rgba(0,0,0,0)');
|
|
54
|
+
overlay.addColorStop(1, 'rgba(0,0,0,0.15)');
|
|
55
|
+
ctx.fillStyle = overlay;
|
|
56
|
+
ctx.beginPath();
|
|
57
|
+
ctx.roundRect(0, 0, width, height, 15);
|
|
58
|
+
ctx.fill();
|
|
59
|
+
|
|
60
|
+
// Thumbnail
|
|
61
|
+
const thumbSize = 100;
|
|
62
|
+
const thumbX = 20;
|
|
63
|
+
const thumbY = (height - thumbSize) / 2;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const thumbBuffer = await cropImage({
|
|
67
|
+
imagePath: thumbnail,
|
|
68
|
+
width: thumbSize,
|
|
69
|
+
height: thumbSize,
|
|
70
|
+
borderRadius: 12
|
|
71
|
+
});
|
|
72
|
+
const thumbImage = await loadImage(thumbBuffer);
|
|
73
|
+
|
|
74
|
+
ctx.shadowColor = 'rgba(0,0,0,0.3)';
|
|
75
|
+
ctx.shadowBlur = 10;
|
|
76
|
+
ctx.shadowOffsetX = 3;
|
|
77
|
+
ctx.shadowOffsetY = 3;
|
|
78
|
+
ctx.drawImage(thumbImage, thumbX, thumbY, thumbSize, thumbSize);
|
|
79
|
+
ctx.shadowBlur = 0;
|
|
80
|
+
ctx.shadowOffsetX = 0;
|
|
81
|
+
ctx.shadowOffsetY = 0;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
|
84
|
+
ctx.beginPath();
|
|
85
|
+
ctx.roundRect(thumbX, thumbY, thumbSize, thumbSize, 12);
|
|
86
|
+
ctx.fill();
|
|
87
|
+
|
|
88
|
+
ctx.fillStyle = '#ffffff';
|
|
89
|
+
ctx.font = 'bold 40px Arial';
|
|
90
|
+
ctx.textAlign = 'center';
|
|
91
|
+
ctx.textBaseline = 'middle';
|
|
92
|
+
ctx.fillText('♪', thumbX + thumbSize / 2, thumbY + thumbSize / 2);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Text
|
|
96
|
+
const textX = thumbX + thumbSize + 20;
|
|
97
|
+
ctx.textAlign = 'left';
|
|
98
|
+
ctx.textBaseline = 'top';
|
|
99
|
+
|
|
100
|
+
ctx.fillStyle = '#ffffff';
|
|
101
|
+
ctx.font = 'bold 22px Arial';
|
|
102
|
+
ctx.fillText(trackName, textX, 25);
|
|
103
|
+
|
|
104
|
+
ctx.fillStyle = 'rgba(255,255,255,0.7)';
|
|
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(255,255,255,0.6)';
|
|
129
|
+
ctx.font = '12px Arial';
|
|
130
|
+
ctx.textAlign = 'left';
|
|
131
|
+
ctx.fillText(startTime, progressX, progressY + 14);
|
|
132
|
+
ctx.textAlign = 'right';
|
|
133
|
+
ctx.fillText(endTime, progressX + progressWidth, progressY + 14);
|
|
134
|
+
|
|
135
|
+
// Mascot
|
|
136
|
+
try {
|
|
137
|
+
const mascotImage = await loadImage(MASCOT_PATH);
|
|
138
|
+
const mascotHeight = 190;
|
|
139
|
+
const mascotWidth = 190;
|
|
140
|
+
const mascotX = width - mascotWidth + 40;
|
|
141
|
+
const mascotY = height - mascotHeight + 15;
|
|
142
|
+
|
|
143
|
+
ctx.drawImage(mascotImage, mascotX, mascotY, mascotWidth, mascotHeight);
|
|
144
|
+
} catch (e) { }
|
|
145
|
+
|
|
146
|
+
const finalImage = await cropImage({
|
|
147
|
+
imagePath: canvas.toBuffer('image/png'),
|
|
148
|
+
width: width,
|
|
149
|
+
height: height,
|
|
150
|
+
borderRadius: 20
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return finalImage;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export default Aqua;
|