sakana-card
v1.0.0
Published
π High-performance WhatsApp bot event card image generator by Sakanaa. Supports Welcome, Leave, Level Up, Rank, Promote, Demote, and Banned cards with dynamic avatars, smart typography, auto-color picking, and blazing fast rendering powered by @napi-rs/c
Maintainers
Readme
β¨ Features at a Glance
| Feature | Description |
|---------|-------------|
| π¨ 8 Event Templates | Welcome, Leave, Level Up, Rank, Promote, Demote, Banned, Premium Tier |
| πΌοΈ Dynamic Avatar | Circle, Rounded Rectangle, Hexagon clipping with gradient borders |
| π Smart Auto-Shrink | Long usernames automatically resize β never overflow or clip |
| π― Auto-Color Picker | Extracts dominant color from avatar for accent theming |
| π«οΈ Background Filters | Dark overlay + blur effects for readability |
| π Overlay/Ornaments | Add PNG overlay layers for seasonal themes |
| β‘ In-Memory LRU Cache | Cached images & fonts β no redundant downloads |
| π Ultra Fast | ~50ms per card on average (native Skia engine) |
| π¦ Zero Config | Works out of the box with sensible defaults |
| π Edge-Case Safe | Dead URLs, empty names, emojis, huge numbers β all handled |
| π Premium Tiers | Free β Standard β Ultra β Super Enterprise (Sultan mode!) |
| π Dual Render Modes | avatar mode (with photo) or typography mode (text-only) |
π¦ Installation
npm install sakana-cardRequirement: Node.js >= 16. The
@napi-rs/canvasdependency ships prebuilt native binaries for Linux, macOS, and Windows β no build tools needed.
π Quick Start
Welcome Card
const { generateWelcome } = require('sakana-card');
const { writeFileSync } = require('fs');
async function main() {
const buffer = await generateWelcome({
username: 'Sakanaa',
groupName: 'Developers Hub',
message: 'Welcome to the team!',
memberCount: 142,
avatarShape: 'circle',
accentColor: ['#43e97b', '#38f9d7'],
// avatar: 'https://example.com/avatar.png', // URL, file path, or Buffer
});
writeFileSync('welcome.png', buffer);
console.log('β
Welcome card generated!');
}
main();Leave Card
const { generateLeave } = require('sakana-card');
const buffer = await generateLeave({
username: 'GoodbyeFriend',
groupName: 'Coding Masters',
message: 'Left the group. We will miss you!',
memberCount: 89,
accentColor: ['#e74c3c', '#c0392b'],
});Level Up Card
const { generateLevelUp } = require('sakana-card');
const buffer = await generateLevelUp({
username: 'ProGamer',
level: 50,
previousLevel: 49, // Shows "Level 49 β Level 50"
xp: 24500,
requiredXp: 25000,
accentColor: ['#ffd700', '#ff8c00'],
});Premium Tier Card (NEW!)
const { generatePremium } = require('sakana-card');
// Super Enterprise β Sultan Mode (Avatar)
const seCard = await generatePremium({
tier: 'super_enterprise',
mode: 'avatar',
username: 'Sakanaa',
title: 'SUPER ENTERPRISE',
description: 'Exclusive premium tier for elite members',
avatar: 'https://example.com/avatar.png',
avatarShape: 'circle',
});
// Ultra β Typography Mode (Text-Only, no avatar)
const ultraCard = await generatePremium({
tier: 'ultra',
mode: 'typography',
username: 'UltraUser',
title: 'ULTRA',
description: 'Next level visual experience',
});π Full API Reference
Template Generators
All generators are async functions that return Promise<Buffer> β a PNG image buffer ready to send via bot or save to file.
generateWelcome(options)
Generates a modern welcome card for new members.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| username | string | 'User' | Display name (auto-shrinks if long) |
| groupName | string | '' | Group name text |
| avatar | string \| Buffer | placeholder | Avatar URL, file path, or Buffer |
| background | string \| Buffer | gradient | Custom background image |
| message | string | 'Welcome to the group!' | Custom welcome message |
| memberCount | number | β | Member count badge (e.g. #142) |
| avatarShape | 'circle' \| 'rounded' \| 'hexagon' | 'circle' | Avatar clipping shape |
| accentColor | string \| string[] | auto-detected | Accent color or gradient array |
| autoColor | boolean | true | Auto-detect accent from avatar |
| overlay | string \| Buffer | β | PNG overlay for seasonal themes |
| overlayOpacity | number | 0.3 | Overlay opacity (0-1) |
| backgroundOpts | object | {} | { blur?: boolean, overlayOpacity?: number, blurRadius?: number } |
generateLeave(options)
Generates a farewell/goodbye card. Same options as generateWelcome.
| Key Differences | |
|---|---|
| Default message | 'has left the group. Goodbye!' |
| Default accent | Red tones |
| Visual | Dashed ring around avatar, muted palette |
generateLevelUp(options)
Generates a level-up celebration card with progress bar and XP display.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| username | string | 'User' | Display name |
| level | number | 1 | Current level |
| previousLevel | number | β | Previous level (shows 49 β 50 transition) |
| xp | number | 0 | Current XP amount |
| requiredXp | number | 100 | XP needed for next level |
| avatar | string \| Buffer | placeholder | Avatar image |
| background | string \| Buffer | gradient | Background image |
| avatarShape | AvatarShape | 'circle' | Avatar shape |
| accentColor | string \| string[] | ['#ffd700', '#ff8c00'] | Gold accent colors |
| autoColor | boolean | true | Auto-detect from avatar |
| overlay | string \| Buffer | β | Overlay PNG |
| backgroundOpts | object | {} | Background filter options |
generateRank(options)
Generates a compact rank/profile card showing level, XP, and ranking position.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| username | string | 'User' | Display name |
| discriminator | string | '' | Tag like '#0001' |
| level | number | 1 | Current level |
| rank | number | 1 | Global rank position |
| xp | number | 0 | Current XP |
| requiredXp | number | 100 | XP for next level |
| status | 'online' \| 'idle' \| 'dnd' \| 'offline' | β | Status indicator dot |
| avatar | string \| Buffer | placeholder | Avatar image |
| background | string \| Buffer | gradient | Background image |
| accentColor | string \| string[] | auto | Accent colors |
generatePromote(options)
Generates a promotion notice card.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| username | string | 'User' | Display name |
| groupName | string | '' | Group name |
| newRole | string | 'Admin' | New role title |
| avatarShape | AvatarShape | 'hexagon' | Default hexagon for promote |
Plus all common options: avatar, background, accentColor, autoColor, overlay, overlayOpacity, backgroundOpts.
generateDemote(options)
Generates a demotion notice card.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| username | string | 'User' | Display name |
| groupName | string | '' | Group name |
| previousRole | string | 'Admin' | Old role |
| newRole | string | 'Member' | New role |
Plus all common options.
generateBanned(options)
Generates a ban notice card with warning stripes and a slash across the avatar.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| username | string | 'User' | Display name |
| groupName | string | '' | Group name |
| reason | string | '' | Ban reason (auto-shrinks) |
| bannedBy | string | '' | Who issued the ban |
Plus all common options.
generatePremium(options) π
Generates a premium-tier user card with luxury visual effects.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| tier | 'free' \| 'standard' \| 'ultra' \| 'super_enterprise' | 'super_enterprise' | Premium tier level |
| mode | 'avatar' \| 'typography' | 'avatar' | Render with photo or text-only |
| title | string | Auto from tier | Title text (e.g. 'SUPER ENTERPRISE') |
| username | string | 'User' | User display name |
| description | string | '' | Small faded description text |
| avatar | string \| Buffer | placeholder | Avatar image (ignored in typography mode) |
| background | string \| Buffer | gradient | Custom background image |
| avatarShape | AvatarShape | 'circle' | Avatar clipping shape |
| accentColor | string \| string[] | tier default | Override accent color(s) |
| autoColor | boolean | true | Auto-detect accent from avatar |
| overlay | string \| Buffer | β | Overlay PNG |
| overlayOpacity | number | 0.3 | Overlay opacity |
| backgroundOpts | object | {} | Background filter options |
Premium Tier Hierarchy
| Tier | Visual Style | Background |
|------|-------------|------------|
| free | Clean, minimal with accent bars | Simple gradient |
| standard | Enhanced with richer gradients | Purple gradient |
| ultra | Neon glow, gradient text, particles | Nebula spots + vignette |
| super_enterprise | Sultan mode β Extreme neon, 3D emboss, multi-color particles, metallic text | Deep purple/pink with dramatic light effects |
Mode Comparison
| Mode | What's Rendered | Best For |
|------|----------------|----------|
| avatar | Avatar + Title + Username + Description | Profile cards, membership badges |
| typography | Title + Username + Description (no avatar) | Announcements, tier displays |
Super Enterprise is the Sultan/Hardcore version of Ultra β same purple/pink/neon-blue palette but with dramatically thicker strokes, heavier glow, denser particles, and 3D emboss text effects.
π¨ Visual Customization Guide
Avatar Shapes
// Circle (default) β clean, universal
await generateWelcome({ username: 'User', avatarShape: 'circle' });
// Rounded Rectangle β modern, sleek
await generateWelcome({ username: 'User', avatarShape: 'rounded' });
// Hexagon β great for gaming bots
await generateWelcome({ username: 'User', avatarShape: 'hexagon' });Color Theming
// Single solid accent color
await generateWelcome({ username: 'User', accentColor: '#e74c3c' });
// Gradient accent (applied to borders, labels, bars)
await generateWelcome({ username: 'User', accentColor: ['#667eea', '#764ba2'] });
// Auto-detect from avatar (enabled by default)
await generateWelcome({
username: 'User',
avatar: 'https://example.com/avatar.png',
autoColor: true,
});
// Disable auto-color, use default palette
await generateWelcome({ username: 'User', autoColor: false });Background Effects
await generateWelcome({
username: 'User',
background: 'https://example.com/bg.jpg',
backgroundOpts: {
blur: true, // Enable background blur
blurRadius: 12, // Blur intensity
overlayOpacity: 0.7, // Darker overlay for better readability
},
});Overlay / Ornament Layer
await generateWelcome({
username: 'User',
overlay: './assets/cyberpunk-frame.png',
overlayOpacity: 0.4,
});Auto-Shrink Text
No configuration needed! If a username like "This_Is_An_Extremely_Long_Username_That_Should_Auto_Shrink_Properly" is provided, the text automatically shrinks to fit within the card boundaries. This works for all text elements across all templates.
π§ Cache Management
const { clearCache, getCacheStats } = require('sakana-card');
// Check cache performance
console.log(getCacheStats());
// Output:
// {
// image: { size: 5, hits: 12, misses: 3, hitRate: '80.0%' },
// font: { size: 0, hits: 0, misses: 0, hitRate: '0%' }
// }
// Clear all caches (useful for long-running bots to free memory)
clearCache();π οΈ Advanced: Build Custom Cards
Use the exposed core engine to create completely custom card layouts:
const { engine, loadImageSafe, extractDominantColor } = require('sakana-card');
async function myCustomCard() {
const { canvas, ctx } = engine.createCard(800, 400);
// Gradient background
engine.fillBackground(ctx, 800, 400, ['#1a1a2e', '#16213e'], 'diagonal');
// Load and draw avatar
const avatar = await loadImageSafe('https://example.com/avatar.png');
engine.drawAvatar(ctx, avatar, 100, 200, 120, {
shape: 'hexagon',
borderWidth: 4,
borderColor: ['#43e97b', '#38f9d7'],
});
// Styled text with gradient, shadow, and stroke
engine.drawStyledText(ctx, 'Custom Card!', 400, 50, {
font: 'bold 40px "DejaVu Sans", sans-serif',
color: ['#43e97b', '#38f9d7'],
align: 'center',
shadow: { color: 'rgba(0,0,0,0.5)', blur: 8, offsetX: 2, offsetY: 2 },
strokes: [{ color: '#000', width: 3 }],
});
// Progress bar
engine.drawProgressBar(ctx, 200, 300, 400, 30, 0.75, {
fillColor: ['#667eea', '#764ba2'],
});
return canvas.toBuffer('image/png');
}Engine API
| Method | Description |
|--------|-------------|
| engine.createCard(w, h) | Create a canvas context |
| engine.fillBackground(ctx, w, h, color, direction) | Fill with solid/gradient |
| engine.drawBackgroundImage(ctx, w, h, img, opts) | Draw cover-fit background |
| engine.drawAvatar(ctx, img, x, y, size, opts) | Draw clipped avatar |
| engine.autoShrinkFont(ctx, text, max, min, maxWidth) | Auto-fit text |
| engine.drawStyledText(ctx, text, x, y, opts) | Styled text rendering |
| engine.drawProgressBar(ctx, x, y, w, h, progress, opts) | Gradient progress bar |
| engine.drawStatusDot(ctx, x, y, r, color) | Status indicator |
| engine.roundedRect(ctx, x, y, w, h, r) | Rounded rectangle path |
| engine.drawOverlay(ctx, img, w, h, opts) | Overlay image layer |
| engine.stripEmoji(text) | Remove emoji from text |
β‘ Performance
Benchmarked on a standard VPS (2 vCPU, 4GB RAM):
| Metric | Value | |--------|-------| | Average per card | ~50ms | | Batch of 10 (parallel) | ~560ms | | 7 card types (sequential) | ~340ms | | Memory per 20 cards | < 1 MB heap increase | | Cache hit rate | Up to 80%+ |
π§ͺ Testing
# Run full test suite (86+ tests)
npm test
# Check generated output images
ls output/Output images are saved to the output/ directory for visual inspection:
output/
βββ welcome-default.png
βββ welcome-full.png
βββ welcome-long-name.png
βββ welcome-emoji.png
βββ welcome-hexagon.png
βββ leave-default.png
βββ levelup-default.png
βββ rank-default.png
βββ rank-status-online.png
βββ promote-default.png
βββ demote-default.png
βββ banned-default.png
βββ banned-full.png
βββ premium-se-avatar.png
βββ premium-se-typography.png
βββ premium-ultra-avatar.png
βββ premium-ultra-typography.png
βββ premium-free-avatar.png
βββ premium-standard-typography.png
βββ ... (and many more)π Project Structure
sakana-card/
βββ src/
β βββ index.js # Main entry point & exports
β βββ index.d.ts # TypeScript type definitions
β βββ core/
β β βββ engine.js # Core canvas rendering engine
β βββ templates/
β β βββ welcome.js # Welcome card
β β βββ leave.js # Leave/goodbye card
β β βββ levelup.js # Level up card (with progress bar)
β β βββ rank.js # Rank/profile card
β β βββ promote.js # Promotion card
β β βββ demote.js # Demotion card
β β βββ banned.js # Ban notice card
β β βββ premium.js # Premium tier card (4 tiers Γ 2 modes)
β βββ utils/
β βββ cache.js # LRU in-memory cache
β βββ loader.js # Image/resource loader with timeout
β βββ color.js # Color utilities & auto-picker
βββ test/
β βββ run-all-tests.js # Comprehensive test suite
βββ output/ # Generated test images
βββ .github/
β βββ workflows/
β βββ npm-publish.yml # Auto-publish to npm on release
βββ package.json
βββ .gitignore
βββ .npmignore
βββ LICENSE
βββ README.mdπ WhatsApp Bot Integration Example
const { generateWelcome, generateLeave, generateLevelUp } = require('sakana-card');
// With Baileys (WhatsApp Web API)
async function onMemberJoin(sock, groupJid, participant) {
const card = await generateWelcome({
username: participant.name || participant.id.split('@')[0],
groupName: groupMetadata.subject,
avatar: await getProfilePicUrl(participant.id),
memberCount: groupMetadata.participants.length,
});
await sock.sendMessage(groupJid, {
image: card,
caption: `Welcome @${participant.id.split('@')[0]}!`,
mentions: [participant.id],
});
}
async function onMemberLeave(sock, groupJid, participant) {
const card = await generateLeave({
username: participant.name || participant.id.split('@')[0],
groupName: groupMetadata.subject,
avatar: await getProfilePicUrl(participant.id),
memberCount: groupMetadata.participants.length,
});
await sock.sendMessage(groupJid, { image: card });
}
async function onLevelUp(sock, chatJid, user, newLevel, prevLevel) {
const card = await generateLevelUp({
username: user.name,
level: newLevel,
previousLevel: prevLevel,
xp: user.xp,
requiredXp: user.requiredXp,
avatar: user.avatarUrl,
});
await sock.sendMessage(chatJid, {
image: card,
caption: `π ${user.name} leveled up to Level ${newLevel}!`,
});
}π Discord Bot Integration Example
const { generateRank, generatePremium } = require('sakana-card');
const { AttachmentBuilder } = require('discord.js');
// Rank command
client.on('interactionCreate', async (interaction) => {
if (interaction.commandName === 'rank') {
const user = interaction.user;
const userData = await getUserData(user.id);
const card = await generateRank({
username: user.displayName,
discriminator: `#${user.discriminator}`,
level: userData.level,
rank: userData.rank,
xp: userData.xp,
requiredXp: userData.requiredXp,
avatar: user.displayAvatarURL({ format: 'png', size: 256 }),
status: interaction.member?.presence?.status || 'offline',
});
const attachment = new AttachmentBuilder(card, { name: 'rank.png' });
await interaction.reply({ files: [attachment] });
}
});π License
MIT License β Created with π by Sakanaa
