@quitscope/discord-welcomecard 0.2.0 → 0.2.1
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/README.md +112 -102
- package/dist/index.cjs +100 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +100 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,102 +1,112 @@
|
|
|
1
|
-
# @quitscope/discord-welcomecard
|
|
2
|
-
|
|
3
|
-
Render Discord welcome cards as static **PNG** or animated **GIF** from one builder.
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
- **One API, two outputs** — the same builder renders `.toPNG()` and `.toGIF()`.
|
|
8
|
-
- **Animated GIFs** — background sheen, text fade-in,
|
|
9
|
-
- **No compile pain** — built on `@napi-rs/canvas` (prebuilt binaries, no node-gyp).
|
|
10
|
-
- **Bundled font** — cards look the same on every server.
|
|
11
|
-
- **TypeScript** — full types, ESM + CJS.
|
|
12
|
-
|
|
13
|
-
## Install
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install @quitscope/discord-welcomecard
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import { WelcomeCard } from '@quitscope/discord-welcomecard';
|
|
23
|
-
import { toAttachment } from '@quitscope/discord-welcomecard/discord';
|
|
24
|
-
|
|
25
|
-
const card = new WelcomeCard()
|
|
26
|
-
.setPreset('centered')
|
|
27
|
-
.setUsername('Quit')
|
|
28
|
-
.setAvatar('https://cdn.discordapp.com/avatars/.../avatar.png')
|
|
29
|
-
.setSubtitle('Welcome to the server!')
|
|
30
|
-
.setMemberCount(1234)
|
|
31
|
-
.setTheme('dark')
|
|
32
|
-
.setAnimations(['background', 'text', 'avatar']);
|
|
33
|
-
|
|
34
|
-
const png = await card.toPNG();
|
|
35
|
-
const gif = await card.toGIF();
|
|
36
|
-
|
|
37
|
-
// discord.js
|
|
38
|
-
channel.send({ files: [toAttachment(gif, 'welcome.gif')] });
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### With discord.js events
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
client.on('guildMemberAdd', async (member) => {
|
|
45
|
-
const gif = await new WelcomeCard()
|
|
46
|
-
.setUsername(member.user.displayName)
|
|
47
|
-
.setAvatar(member.user.displayAvatarURL({ extension: 'png', size: 256 }))
|
|
48
|
-
.setSubtitle(`Welcome to ${member.guild.name}!`)
|
|
49
|
-
.setMemberCount(member.guild.memberCount)
|
|
50
|
-
.setAnimations(['text', 'avatar'])
|
|
51
|
-
.toGIF();
|
|
52
|
-
|
|
53
|
-
const channel = member.guild.systemChannel;
|
|
54
|
-
channel?.send({ files: [toAttachment(gif, 'welcome.gif')] });
|
|
55
|
-
});
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Presets
|
|
59
|
-
|
|
60
|
-
| `centered` (default) | `neon` |
|
|
61
|
-
| --- | --- |
|
|
62
|
-
|  |  |
|
|
63
|
-
|
|
64
|
-
| `minimal` | `hero` |
|
|
65
|
-
| --- | --- |
|
|
66
|
-
|  |  |
|
|
67
|
-
|
|
68
|
-
## Animated
|
|
69
|
-
|
|
70
|
-
`setAnimations([...])` + `toGIF()` —
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
1
|
+
# @quitscope/discord-welcomecard
|
|
2
|
+
|
|
3
|
+
Render Discord welcome cards as static **PNG** or animated **GIF** from one builder.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
- **One API, two outputs** — the same builder renders `.toPNG()` and `.toGIF()`.
|
|
8
|
+
- **Animated GIFs** — background sheen, text fade-in, avatar glow/bounce, rainbow ring, slide-in. Autoplays in Discord.
|
|
9
|
+
- **No compile pain** — built on `@napi-rs/canvas` (prebuilt binaries, no node-gyp).
|
|
10
|
+
- **Bundled font** — cards look the same on every server.
|
|
11
|
+
- **TypeScript** — full types, ESM + CJS.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @quitscope/discord-welcomecard
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { WelcomeCard } from '@quitscope/discord-welcomecard';
|
|
23
|
+
import { toAttachment } from '@quitscope/discord-welcomecard/discord';
|
|
24
|
+
|
|
25
|
+
const card = new WelcomeCard()
|
|
26
|
+
.setPreset('centered')
|
|
27
|
+
.setUsername('Quit')
|
|
28
|
+
.setAvatar('https://cdn.discordapp.com/avatars/.../avatar.png')
|
|
29
|
+
.setSubtitle('Welcome to the server!')
|
|
30
|
+
.setMemberCount(1234)
|
|
31
|
+
.setTheme('dark')
|
|
32
|
+
.setAnimations(['background', 'text', 'avatar']);
|
|
33
|
+
|
|
34
|
+
const png = await card.toPNG();
|
|
35
|
+
const gif = await card.toGIF();
|
|
36
|
+
|
|
37
|
+
// discord.js
|
|
38
|
+
channel.send({ files: [toAttachment(gif, 'welcome.gif')] });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### With discord.js events
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
client.on('guildMemberAdd', async (member) => {
|
|
45
|
+
const gif = await new WelcomeCard()
|
|
46
|
+
.setUsername(member.user.displayName)
|
|
47
|
+
.setAvatar(member.user.displayAvatarURL({ extension: 'png', size: 256 }))
|
|
48
|
+
.setSubtitle(`Welcome to ${member.guild.name}!`)
|
|
49
|
+
.setMemberCount(member.guild.memberCount)
|
|
50
|
+
.setAnimations(['text', 'avatar'])
|
|
51
|
+
.toGIF();
|
|
52
|
+
|
|
53
|
+
const channel = member.guild.systemChannel;
|
|
54
|
+
channel?.send({ files: [toAttachment(gif, 'welcome.gif')] });
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Presets
|
|
59
|
+
|
|
60
|
+
| `centered` (default) | `neon` |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
|  |  |
|
|
63
|
+
|
|
64
|
+
| `minimal` | `hero` |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
|  |  |
|
|
67
|
+
|
|
68
|
+
## Animated
|
|
69
|
+
|
|
70
|
+
`setAnimations([...])` + `toGIF()` — combine any of the 6 animation types:
|
|
71
|
+
|
|
72
|
+
| Type | Effect |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `'background'` | Diagonal sheen sweeps across the card |
|
|
75
|
+
| `'text'` | Text fades in (ease-out reveal) |
|
|
76
|
+
| `'avatar'` | Avatar ring pulses with a glow |
|
|
77
|
+
| `'ring'` | Ring color cycles through the full color wheel |
|
|
78
|
+
| `'slide'` | Text slides up into position from below |
|
|
79
|
+
| `'bounce'` | Avatar bounces up and down |
|
|
80
|
+
|
|
81
|
+

|
|
82
|
+
|
|
83
|
+
## API
|
|
84
|
+
|
|
85
|
+
| Method | Description |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `setPreset(name)` | `'centered'` (default), `'neon'`, `'minimal'`, `'hero'` |
|
|
88
|
+
| `setUsername(name)` | Main text (required) |
|
|
89
|
+
| `setAvatar(urlOrBuffer)` | Avatar image; falls back to a colored circle if it fails to load |
|
|
90
|
+
| `setSubtitle(text)` | Secondary line, e.g. "Welcome to the server!" |
|
|
91
|
+
| `setMemberCount(n)` | Renders "MEMBER #n" — optional, omit to hide |
|
|
92
|
+
| `setMemberCountPosition(pos)` | 3×3 grid: `'top-left'`, `'top-center'`, `'top-right'`, `'center-left'`, `'center'`, `'center-right'`, `'bottom-left'`, `'bottom-center'`, `'bottom-right'`. `'corner'` is an alias for `'bottom-right'`. Defaults: `'bottom-center'` in the centered presets, `'bottom-right'` in `hero`. |
|
|
93
|
+
| `setBackground(value)` | Hex color (`#1e1e2e`), image URL, or `Buffer` |
|
|
94
|
+
| `setRingColor(hex)` | Override the avatar ring / glow color |
|
|
95
|
+
| `setTheme(theme)` | `'dark'` (default) or `'light'` |
|
|
96
|
+
| `setFont({ family, color, usernameColor, size, subtitleSize })` | Override font settings; `usernameColor` applies only to the username line |
|
|
97
|
+
| `setAnimations(list)` | Any of `'background'`, `'text'`, `'avatar'`, `'ring'`, `'slide'`, `'bounce'` — used by `toGIF()` |
|
|
98
|
+
| `toPNG()` | `Promise<Buffer>` — static card |
|
|
99
|
+
| `toGIF()` | `Promise<Buffer>` — animated card |
|
|
100
|
+
|
|
101
|
+
The `toAttachment(buffer, name?)` helper lives in `@quitscope/discord-welcomecard/discord` and requires
|
|
102
|
+
`discord.js` (optional peer dependency). The core package works without it.
|
|
103
|
+
|
|
104
|
+
Only `setUsername()` is required — everything else has sensible defaults or fallbacks.
|
|
105
|
+
|
|
106
|
+
## Requirements
|
|
107
|
+
|
|
108
|
+
- Node.js ≥ 18
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT. Font: [Poppins](https://fonts.google.com/specimen/Poppins) (SIL Open Font License, bundled).
|
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,7 @@ var CARD_WIDTH = 840;
|
|
|
34
34
|
var CARD_HEIGHT = 420;
|
|
35
35
|
var GIF_FRAMES = 30;
|
|
36
36
|
var GIF_DELAY_MS = 50;
|
|
37
|
-
var GIF_SCALE =
|
|
37
|
+
var GIF_SCALE = 1;
|
|
38
38
|
var DEFAULT_OPTIONS = {
|
|
39
39
|
preset: "centered",
|
|
40
40
|
theme: "dark",
|
|
@@ -106,6 +106,7 @@ function centeredLayout(opts) {
|
|
|
106
106
|
opts.theme === "dark" ? "#1e1e2e" : "#eff1f5"
|
|
107
107
|
);
|
|
108
108
|
const textColor = opts.font.color ?? (opts.theme === "dark" ? "#ffffff" : "#11111b");
|
|
109
|
+
const usernameColor = opts.font.usernameColor ?? textColor;
|
|
109
110
|
const family = opts.font.family ?? DEFAULT_FONT_FAMILY;
|
|
110
111
|
const avatarSize = 170;
|
|
111
112
|
const avatarY = 56;
|
|
@@ -118,7 +119,7 @@ function centeredLayout(opts) {
|
|
|
118
119
|
x: width / 2 - avatarSize / 2,
|
|
119
120
|
y: avatarY,
|
|
120
121
|
size: avatarSize,
|
|
121
|
-
ringColor: accent,
|
|
122
|
+
ringColor: opts.ringColor ?? accent,
|
|
122
123
|
source: opts.avatar
|
|
123
124
|
},
|
|
124
125
|
username: {
|
|
@@ -126,7 +127,7 @@ function centeredLayout(opts) {
|
|
|
126
127
|
x: width / 2,
|
|
127
128
|
y: avatarY + avatarSize + 66,
|
|
128
129
|
size: opts.font.size ?? 56,
|
|
129
|
-
color:
|
|
130
|
+
color: usernameColor,
|
|
130
131
|
family,
|
|
131
132
|
align: "center"
|
|
132
133
|
},
|
|
@@ -263,16 +264,75 @@ async function loadImageOrFallback(source, fallbackColor, w = 256, h = 256) {
|
|
|
263
264
|
}
|
|
264
265
|
}
|
|
265
266
|
|
|
267
|
+
// src/assets/colorUtils.ts
|
|
268
|
+
function hexToHsl(hex) {
|
|
269
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
270
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
271
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
272
|
+
const max = Math.max(r, g, b);
|
|
273
|
+
const min = Math.min(r, g, b);
|
|
274
|
+
const l = (max + min) / 2;
|
|
275
|
+
if (max === min) return [0, 0, l];
|
|
276
|
+
const d = max - min;
|
|
277
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
278
|
+
let h;
|
|
279
|
+
switch (max) {
|
|
280
|
+
case r:
|
|
281
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
282
|
+
break;
|
|
283
|
+
case g:
|
|
284
|
+
h = ((b - r) / d + 2) / 6;
|
|
285
|
+
break;
|
|
286
|
+
default:
|
|
287
|
+
h = ((r - g) / d + 4) / 6;
|
|
288
|
+
}
|
|
289
|
+
return [h, s, l];
|
|
290
|
+
}
|
|
291
|
+
function hslToHex(h, s, l) {
|
|
292
|
+
const hue2rgb = (p, q2, t) => {
|
|
293
|
+
if (t < 0) t += 1;
|
|
294
|
+
if (t > 1) t -= 1;
|
|
295
|
+
if (t < 1 / 6) return p + (q2 - p) * 6 * t;
|
|
296
|
+
if (t < 1 / 2) return q2;
|
|
297
|
+
if (t < 2 / 3) return p + (q2 - p) * (2 / 3 - t) * 6;
|
|
298
|
+
return p;
|
|
299
|
+
};
|
|
300
|
+
let r, g, b;
|
|
301
|
+
if (s === 0) {
|
|
302
|
+
r = g = b = l;
|
|
303
|
+
} else {
|
|
304
|
+
const q2 = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
305
|
+
const p = 2 * l - q2;
|
|
306
|
+
r = hue2rgb(p, q2, h + 1 / 3);
|
|
307
|
+
g = hue2rgb(p, q2, h);
|
|
308
|
+
b = hue2rgb(p, q2, h - 1 / 3);
|
|
309
|
+
}
|
|
310
|
+
const hex = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
|
|
311
|
+
return `#${hex(r)}${hex(g)}${hex(b)}`;
|
|
312
|
+
}
|
|
313
|
+
function rotateHue(hexColor, amount) {
|
|
314
|
+
const [h, s, l] = hexToHsl(hexColor);
|
|
315
|
+
return hslToHex((h + amount + 1) % 1, s, l);
|
|
316
|
+
}
|
|
317
|
+
|
|
266
318
|
// src/render/drawFrame.ts
|
|
267
|
-
var STATIC_FRAME = {
|
|
268
|
-
|
|
319
|
+
var STATIC_FRAME = {
|
|
320
|
+
textAlpha: 1,
|
|
321
|
+
avatarGlow: 1,
|
|
322
|
+
backgroundShift: 0,
|
|
323
|
+
ringShift: 0,
|
|
324
|
+
textSlide: 1,
|
|
325
|
+
avatarBounce: 0
|
|
326
|
+
};
|
|
327
|
+
var BOUNCE_AMPLITUDE = 10;
|
|
328
|
+
function drawText(ctx, box, alpha, slideOffset) {
|
|
269
329
|
ctx.save();
|
|
270
330
|
ctx.globalAlpha = alpha;
|
|
271
331
|
ctx.fillStyle = box.color;
|
|
272
332
|
ctx.textAlign = box.align;
|
|
273
333
|
ctx.textBaseline = "alphabetic";
|
|
274
334
|
ctx.font = `${box.size}px ${box.family}`;
|
|
275
|
-
ctx.fillText(box.text, box.x, box.y);
|
|
335
|
+
ctx.fillText(box.text, box.x, box.y + slideOffset);
|
|
276
336
|
ctx.restore();
|
|
277
337
|
}
|
|
278
338
|
async function drawFrame(ctx, l, state) {
|
|
@@ -292,13 +352,15 @@ async function drawFrame(ctx, l, state) {
|
|
|
292
352
|
ctx.fillRect(0, 0, l.width, l.height);
|
|
293
353
|
}
|
|
294
354
|
const { x, y, size, ringColor } = l.avatar;
|
|
355
|
+
const animatedRingColor = state.ringShift > 0 ? rotateHue(ringColor, state.ringShift) : ringColor;
|
|
356
|
+
const bounceOffset = state.avatarBounce * BOUNCE_AMPLITUDE;
|
|
295
357
|
const cx = x + size / 2;
|
|
296
|
-
const cy = y + size / 2;
|
|
358
|
+
const cy = y + size / 2 + bounceOffset;
|
|
297
359
|
ctx.save();
|
|
298
|
-
ctx.shadowColor =
|
|
360
|
+
ctx.shadowColor = animatedRingColor;
|
|
299
361
|
ctx.shadowBlur = 10 + state.avatarGlow * 25;
|
|
300
362
|
ctx.lineWidth = 6;
|
|
301
|
-
ctx.strokeStyle =
|
|
363
|
+
ctx.strokeStyle = animatedRingColor;
|
|
302
364
|
ctx.beginPath();
|
|
303
365
|
ctx.arc(cx, cy, size / 2 + 3, 0, Math.PI * 2);
|
|
304
366
|
ctx.stroke();
|
|
@@ -308,11 +370,12 @@ async function drawFrame(ctx, l, state) {
|
|
|
308
370
|
ctx.beginPath();
|
|
309
371
|
ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);
|
|
310
372
|
ctx.clip();
|
|
311
|
-
ctx.drawImage(avatarImg, x, y, size, size);
|
|
373
|
+
ctx.drawImage(avatarImg, x, y + bounceOffset, size, size);
|
|
312
374
|
ctx.restore();
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (l.
|
|
375
|
+
const slideOffset = (1 - state.textSlide) * l.height;
|
|
376
|
+
drawText(ctx, l.username, state.textAlpha, slideOffset);
|
|
377
|
+
if (l.subtitle) drawText(ctx, l.subtitle, state.textAlpha, slideOffset);
|
|
378
|
+
if (l.memberCount) drawText(ctx, l.memberCount, state.textAlpha, slideOffset);
|
|
316
379
|
}
|
|
317
380
|
|
|
318
381
|
// src/render/toPNG.ts
|
|
@@ -691,12 +754,31 @@ function backgroundShift(progress) {
|
|
|
691
754
|
return progress;
|
|
692
755
|
}
|
|
693
756
|
|
|
757
|
+
// src/animate/ring.ts
|
|
758
|
+
function ringShift(progress) {
|
|
759
|
+
return progress;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/animate/slide.ts
|
|
763
|
+
function textSlide(progress) {
|
|
764
|
+
const p = Math.min(progress / 0.8, 1);
|
|
765
|
+
return p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/animate/bounce.ts
|
|
769
|
+
function avatarBounce(progress) {
|
|
770
|
+
return Math.sin(progress * Math.PI * 2);
|
|
771
|
+
}
|
|
772
|
+
|
|
694
773
|
// src/animate/index.ts
|
|
695
774
|
function frameStateFor(animations, progress) {
|
|
696
775
|
return {
|
|
697
776
|
textAlpha: animations.includes("text") ? textAlpha(progress) : 1,
|
|
698
777
|
avatarGlow: animations.includes("avatar") ? avatarGlow(progress) : 1,
|
|
699
|
-
backgroundShift: animations.includes("background") ? backgroundShift(progress) : 0
|
|
778
|
+
backgroundShift: animations.includes("background") ? backgroundShift(progress) : 0,
|
|
779
|
+
ringShift: animations.includes("ring") ? ringShift(progress) : 0,
|
|
780
|
+
textSlide: animations.includes("slide") ? textSlide(progress) : 1,
|
|
781
|
+
avatarBounce: animations.includes("bounce") ? avatarBounce(progress) : 0
|
|
700
782
|
};
|
|
701
783
|
}
|
|
702
784
|
|
|
@@ -769,6 +851,10 @@ var WelcomeCard = class {
|
|
|
769
851
|
this.opts.background = background;
|
|
770
852
|
return this;
|
|
771
853
|
}
|
|
854
|
+
setRingColor(color) {
|
|
855
|
+
this.opts.ringColor = color;
|
|
856
|
+
return this;
|
|
857
|
+
}
|
|
772
858
|
setTheme(theme) {
|
|
773
859
|
this.opts.theme = theme;
|
|
774
860
|
return this;
|