Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: unburn/musicard
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 2.0.3
Choose a base ref
...
head repository: unburn/musicard
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 2.0.4
Choose a head ref
  • 4 commits
  • 31 files changed
  • 1 contributor

Commits on Feb 18, 2024

  1. Update package.json

    kandepatilkunal authored Feb 18, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    2e8ede0 View commit details
  2. Create node.js.yml

    kandepatilkunal authored Feb 18, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    5b3e5bf View commit details

Commits on Mar 20, 2024

  1. 2.0.4 Release

    kandepatilkunal committed Mar 20, 2024
    Copy the full SHA
    e56b960 View commit details
  2. Release 2.0.4

    kandepatilkunal committed Mar 20, 2024
    Copy the full SHA
    1416631 View commit details
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
package-lock.json
dist
package-lock.json
6 changes: 6 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/node_modules
/src
/tests

tsconfig.json
tsup.config.ts
21 changes: 0 additions & 21 deletions examples/index.js

This file was deleted.

Binary file added fonts/PlusJakartaSans-Bold.ttf
Binary file not shown.
File renamed without changes.
Binary file added fonts/PlusJakartaSans-ExtraLight.ttf
Binary file not shown.
Binary file added fonts/PlusJakartaSans-Light.ttf
Binary file not shown.
Binary file added fonts/PlusJakartaSans-Medium.ttf
Binary file not shown.
File renamed without changes.
File renamed without changes.
69 changes: 0 additions & 69 deletions index.d.ts

This file was deleted.

27 changes: 0 additions & 27 deletions index.js

This file was deleted.

2,994 changes: 2,188 additions & 806 deletions package-lock.json

Large diffs are not rendered by default.

97 changes: 58 additions & 39 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,40 +1,59 @@
{
"name": "musicard",
"version": "2.0.3",
"description": "Musicard is one of the best canvas libraries to create a variety of music cards.",
"main": "./index.js",
"types": "./index.d.ts",
"files": [
"fonts",
"themes",
"index.js",
"index.d.ts"
],
"keywords": [
"musicard",
"music-card",
"discord-music-card",
"music-card-discord",
"discord.js",
"spotify",
"youtube",
"image",
"node-canvas",
"canvas-constructor",
"discord",
"discord-canvas"
],
"author": "FlameFace",
"license": "GPL-3.0-only",
"dependencies": {
"@napi-rs/canvas": "^0.1.45",
"cropify": "^1.0.8"
},
"repository": {
"type": "git",
"url": "https://github.com/unburn/musicard.git"
},
"bugs": {
"url": "https://github.com/unburn/musicard/issues"
}
}
"name": "musicard",
"version": "2.0.4",
"description": "Musicard is one of the best canvas libraries to create a variety of music cards.",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"fonts"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"deploy": "npm publish",
"deploy:beta": "npm publish --tag beta",
"test:m": "cd tests && node index.mjs",
"test:c": "cd tests && node --watch index.js"
},
"keywords": [
"musicard",
"music-card",
"discord-music-card",
"music-card-discord",
"discord.js",
"spotify",
"youtube",
"image",
"node-canvas",
"canvas-constructor",
"discord",
"discord-canvas"
],
"author": "flameface",
"license": "GPL-3.0-only",
"devDependencies": {
"@types/node": "^20.11.28",
"tsup": "^8.0.2",
"typescript": "^5.4.2"
},
"repository": {
"type": "git",
"url": "https://github.com/unburn/musicard.git"
},
"bugs": {
"url": "https://github.com/unburn/musicard/issues"
},
"dependencies": {
"@napi-rs/canvas": "^0.1.51",
"cropify": "latest"
}
}
43 changes: 9 additions & 34 deletions readme.md
Original file line number Diff line number Diff line change
@@ -34,15 +34,17 @@ You can use the Musicard package in your Discord bots, websites, etc.

## Using Create File
```js
(async () => {
const { Classic } = require("musicard");
const fs = require("fs")
import { Classic } from "musicard";
import fs from 'fs'

const musicard = await Classic({ });
//OR

// Creates PNG file
fs.writeFileSync("musicard.png", musicard);
})()
const { Classic } = require("musicard");
const fs = require('fs')

Classic({}).then(x => {
fs.writeFileSync("output.png", x)
})
```

## Using Discord Bot
@@ -174,33 +176,6 @@ Musicard is the #1 canvas library to create music cards with awesome themes.
})()
```

***

## Mini Pro

![minipro](https://ik.imagekit.io/unburn/MiniPro.svg)

```js
(async () => {
const { MiniPro } = require("musicard");
const fs = require("fs")

const musicard = await MiniPro({
thumbnailImage: "https://lh3.googleusercontent.com/yavtBZZnoxaY21GSS_VIKSg0mvzu1b0r6arH8xvWVskoMaZ5ww3iDMgBNujnIWCt7MOkDsrKapSGCfc=w544-h544-l90-rj",
backgroundColor: "#070707",
progress: 10,
progressColor: "#FF7A00",
progressBarColor: "#5F2D00",
name: "Burn",
nameColor: "#FF7A00",
author: "By 2WEI & Edda Hayes",
authorColor: "#696969"
});

fs.writeFileSync("musicard.png", musicard);
})()
```

# Resource
Pull request to add your project here.

5 changes: 5 additions & 0 deletions src/functions/generateSvg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const generateSvg = (svgContent: string) => {
return `data:image/svg+xml;base64,${Buffer.from(svgContent).toString('base64')}`;
}

export { generateSvg }
19 changes: 19 additions & 0 deletions src/functions/registerFont.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { GlobalFonts } from "@napi-rs/canvas";
import path from "path";
import fs from "fs";

function registerFont(fontPath: string, fontName: string) {
const rootFontsPath = path.join(__dirname, "../fonts", fontPath);
if (fs.existsSync(rootFontsPath)) {
GlobalFonts.registerFromPath(rootFontsPath, fontName);
} else {
const srcFontsPath = path.join(__dirname, "../fonts", fontPath);
if (fs.existsSync(srcFontsPath)) {
GlobalFonts.registerFromPath(srcFontsPath, fontName);
} else {
throw new Error(`Font file not found at ${rootFontsPath} or ${srcFontsPath}`);
}
}
}

export { registerFont };
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./typings/types"
export * from "./themes/classic"
export * from "./themes/classicpro"
export * from "./themes/dynamic"
export * from "./themes/mini"
179 changes: 179 additions & 0 deletions src/themes/classic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { generateSvg } from "../functions/generateSvg";
import { registerFont } from "../functions/registerFont";
import { ClassicOption } from "../typings/types";
import { createCanvas, loadImage } from "@napi-rs/canvas";
import { cropImage } from "cropify";

registerFont("PlusJakartaSans-Bold.ttf", "bold")
registerFont("PlusJakartaSans-ExtraBold.ttf", "extrabold")
registerFont("PlusJakartaSans-ExtraLight.ttf", "extralight")
registerFont("PlusJakartaSans-Light.ttf", "light")
registerFont("PlusJakartaSans-Medium.ttf", "medium")
registerFont("PlusJakartaSans-Regular.ttf", "regular")
registerFont("PlusJakartaSans-SemiBold.ttf", "semibold")

const Classic = async (option: ClassicOption) => {
if (!option.progress) option.progress = 10;
if (!option.name) option.name = "Musicard"
if (!option.author) option.author = "By Unburn"
if (!option.startTime) option.startTime = "0:00"
if (!option.endTime) option.endTime = "0:00"

if (!option.progressBarColor) option.progressBarColor = "#5F2D00";
if (!option.progressColor) option.progressColor = "#FF7A00";
if (!option.backgroundColor) option.backgroundColor = "#070707"
if (!option.nameColor) option.nameColor = "#FF7A00"
if (!option.authorColor) option.authorColor = "#FFFFFF"
if (!option.timeColor) option.timeColor = "#FFFFFF"
if (!option.imageDarkness) option.imageDarkness = 10

const noImageSvg = generateSvg(`<svg width="837" height="837" viewBox="0 0 837 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="837" height="837" fill="${option.progressColor}"/>
<path d="M419.324 635.912C406.035 635.912 394.658 631.18 385.195 621.717C375.732 612.254 371 600.878 371 587.589C371 574.3 375.732 562.923 385.195 553.46C394.658 543.997 406.035 539.265 419.324 539.265C432.613 539.265 443.989 543.997 453.452 553.46C462.915 562.923 467.647 574.3 467.647 587.589C467.647 600.878 462.915 612.254 453.452 621.717C443.989 631.18 432.613 635.912 419.324 635.912ZM371 490.941V201H467.647V490.941H371Z" fill="${option.backgroundColor}"/>
</svg>`)

if (!option.thumbnailImage) {
option.thumbnailImage = noImageSvg
};

let thumbnail;

try {
thumbnail = await loadImage(await cropImage({
imagePath: option.thumbnailImage,
borderRadius: 50,
width: 837,
height: 837,
cropCenter: true
}))
} catch {
thumbnail = await loadImage(await cropImage({
imagePath: noImageSvg,
borderRadius: 50,
width: 837,
height: 837,
cropCenter: true
}))
}

if (option.progress < 10) {
option.progress = 10
} else if (option.progress > 100) {
option.progress = 100
}

if (option.imageDarkness < 0) {
option.imageDarkness = 0
} else if (option.imageDarkness > 100) {
option.imageDarkness = 100
}

if (option.name.length > 18) {
option.name = option.name.slice(0, 18) + "...";
}

if (option.author.length > 18) {
option.author = option.author.slice(0, 18) + "...";
}

try {
const canvas = createCanvas(2458, 837);
const ctx = canvas.getContext("2d");

if (!option.backgroundImage) {
const backgroundSvg = generateSvg(`<svg width="2458" height="837" viewBox="0 0 2458 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1568" height="512" rx="50" fill="${option.backgroundColor}"/>
<rect y="565" width="1568" height="272" rx="50" fill="${option.backgroundColor}"/>
</svg>`);


const background = await loadImage(backgroundSvg);

ctx.drawImage(background, 0, 0);
} else {
try {
const darknessSvg = generateSvg(`<svg width="1568" height="837" viewBox="0 0 1568 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1568" height="512" rx="50" fill="#070707" fill-opacity="${option.imageDarkness / 100}"/>
<rect y="565" width="1568" height="272" rx="50" fill="#070707" fill-opacity="${option.imageDarkness / 100}"/>
</svg>`)

const image = await cropImage({
imagePath: option.backgroundImage,
width: 1568,
height: 837,
cropCenter: true
})

await cropImage({
imagePath: image,
x: 0,
y: -170,
width: 1568,
height: 512,
borderRadius: 50
}).then(async (x: any) => {
ctx.drawImage(await loadImage(x), 0, 0)
})

await cropImage({
imagePath: image,
x: 0,
y: -845,
width: 1568,
height: 272,
borderRadius: 50
}).then(async (x: any) => {
ctx.drawImage(await loadImage(x), 0, 565)
})

ctx.drawImage(await loadImage(darknessSvg), 0, 0)
} catch (err) {
const backgroundSvg = generateSvg(`<svg width="2458" height="837" viewBox="0 0 2458 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1568" height="512" rx="50" fill="${option.backgroundColor}"/>
<rect y="565" width="1568" height="272" rx="50" fill="${option.backgroundColor}"/>
</svg>`);


const background = await loadImage(backgroundSvg);

ctx.drawImage(background, 0, 0);
}
}

ctx.drawImage(thumbnail, 1621, 0);

const completed = 1342 * option.progress / 100;

const progressBarSvg = generateSvg(`<svg width="1342" height="76" viewBox="0 0 1342 76" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="13" width="1342" height="47" rx="20" fill="${option.progressBarColor}"/>
<rect y="13" width="${completed}" height="47" rx="20" fill="${option.progressColor}"/>
<rect x="${completed - 40}" y="3" width="69.4422" height="69.4422" rx="34.7211" fill="${option.progressColor}" stroke="${option.backgroundColor}" stroke-width="6"/>
</svg>`);

const progressBar = await loadImage(progressBarSvg);

ctx.drawImage(progressBar, 113, 635);

ctx.fillStyle = `${option.nameColor}`;
ctx.font = "124px extrabold";
ctx.fillText(option.name, 113, 230);

ctx.fillStyle = `${option.authorColor}`;
ctx.font = "87px regular";
ctx.fillText(option.author, 113, 370);

ctx.fillStyle = `${option.timeColor}`;
ctx.font = "50px semibold";
ctx.fillText(option.startTime, 113, 768);

ctx.fillStyle = `${option.timeColor}`;
ctx.font = "50px semibold";
ctx.fillText(option.endTime, 1332, 768);

return canvas.toBuffer("image/png");
} catch (e: any) {
throw new Error(e.message);
}
}

export { Classic }
139 changes: 139 additions & 0 deletions src/themes/classicpro.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { ClassicProOption } from "../typings/types";
import { createCanvas, loadImage } from "@napi-rs/canvas";
import { cropImage } from "cropify";
import { generateSvg } from "../functions/generateSvg";

const ClassicPro = async (option: ClassicProOption) => {
if (!option.progress) option.progress = 10;
if (!option.name) option.name = "Musicard";
if (!option.author) option.author = "By Unburn";
if (!option.startTime) option.startTime = "0:00";
if (!option.endTime) option.endTime = "0:00";

if (!option.progressBarColor) option.progressBarColor = "#5F2D00";
if (!option.progressColor) option.progressColor = "#FF7A00";
if (!option.backgroundColor) option.backgroundColor = "#070707"
if (!option.nameColor) option.nameColor = "#FF7A00"
if (!option.authorColor) option.authorColor = "#FFFFFF"
if (!option.timeColor) option.timeColor = "#FFFFFF"
if (!option.imageDarkness) option.imageDarkness = 10

const noImageSvg = generateSvg(`<svg width="837" height="837" viewBox="0 0 837 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="837" height="837" fill="${option.progressColor}"/>
<path d="M419.324 635.912C406.035 635.912 394.658 631.18 385.195 621.717C375.732 612.254 371 600.878 371 587.589C371 574.3 375.732 562.923 385.195 553.46C394.658 543.997 406.035 539.265 419.324 539.265C432.613 539.265 443.989 543.997 453.452 553.46C462.915 562.923 467.647 574.3 467.647 587.589C467.647 600.878 462.915 612.254 453.452 621.717C443.989 631.18 432.613 635.912 419.324 635.912ZM371 490.941V201H467.647V490.941H371Z" fill="${option.backgroundColor}"/>
</svg>`)

if (!option.thumbnailImage) {
option.thumbnailImage = noImageSvg
};

let thumbnail;

try {
thumbnail = await loadImage(await cropImage({
imagePath: option.thumbnailImage,
borderRadius: 50,
width: 331,
height: 331,
cropCenter: true
}))
} catch {
thumbnail = await loadImage(await cropImage({
imagePath: noImageSvg,
borderRadius: 50,
width: 331,
height: 331,
cropCenter: true
}))
}

if (option.progress < 10) {
option.progress = 10;
} else if (option.progress > 100) {
option.progress = 100;
}

if (option.name.length > 12) {
option.name = option.name.slice(0, 12) + "...";
}

if (option.author.length > 12) {
option.author = option.author.slice(0, 12) + "...";
}

try {
const canvas = createCanvas(1252, 708);
const ctx = canvas.getContext("2d");

if (!option.backgroundImage) {
const backgroundSvg = generateSvg(`<svg width="1252" height="708" viewBox="0 0 1252 708" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1252" height="708" rx="50" fill="${option.backgroundColor}"/>
</svg>`);

const background = await loadImage(backgroundSvg);

ctx.drawImage(background, 0, 0);
} else {
try {
const darknessSvg = generateSvg(`<svg width="1252" height="708" viewBox="0 0 1252 708" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1252" height="708" rx="50" fill="#070707" fill-opacity="${option.imageDarkness / 100}"/>
</svg>`)

const image = await cropImage({
imagePath: option.backgroundImage,
width: 1252,
height: 708,
borderRadius: 50,
cropCenter: true
})

ctx.drawImage(await loadImage(image), 0, 0)
ctx.drawImage(await loadImage(darknessSvg), 0, 0)
} catch (error) {
const backgroundSvg = generateSvg(`<svg width="1252" height="708" viewBox="0 0 1252 708" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1252" height="708" rx="50" fill="${option.backgroundColor}"/>
</svg>`);

const background = await loadImage(backgroundSvg);

ctx.drawImage(background, 0, 0);
}
}

ctx.drawImage(thumbnail, 87, 91);

const completed = 1083 * option.progress / 100;

const progressBarSvg = generateSvg(`<svg width="1083" height="77" viewBox="0 0 1083 77" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="19" width="1083" height="40" rx="20" fill="${option.progressBarColor}"/>
<rect y="19" width="${completed}" height="40" rx="20" fill="${option.progressColor}"/>
<rect x="${completed - 40}" y="5" width="65" height="65" rx="35.5" fill="${option.progressColor}" stroke="${option.backgroundColor}" stroke-width="5"/>
</svg>`);

const progressBar = await loadImage(progressBarSvg);

ctx.drawImage(progressBar, 87, 490);

ctx.fillStyle = `${option.nameColor}`;
ctx.font = "90px extrabold";
ctx.fillText(option.name, 486, 240);

ctx.fillStyle = `${option.authorColor}`;
ctx.font = "60px semibold";
ctx.fillText(option.author, 486, 330);

ctx.fillStyle = `${option.timeColor}`;
ctx.font = "40px semibold";
ctx.fillText(option.startTime, 85, 630);

ctx.fillStyle = `${option.timeColor}`;
ctx.font = "40px semibold";
ctx.fillText(option.endTime, 1070, 630);

return canvas.toBuffer("image/png");
} catch (e: any) {
throw new Error(e.message);
}
}

export { ClassicPro };
139 changes: 139 additions & 0 deletions src/themes/dynamic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { generateSvg } from "../functions/generateSvg";
import { DynamicOption } from "../typings/types";
import { createCanvas, loadImage } from "@napi-rs/canvas";
import { cropImage } from "cropify";

const Dynamic = async (option: DynamicOption) => {
if (!option.progress) option.progress = 10;
if (!option.name) option.name = "Musicard"
if (!option.author) option.author = "By Unburn"

if (!option.progressBarColor) option.progressBarColor = "#5F2D00";
if (!option.progressColor) option.progressColor = "#FF7A00";
if (!option.backgroundColor) option.backgroundColor = "#070707"
if (!option.nameColor) option.nameColor = "#FF7A00"
if (!option.authorColor) option.authorColor = "#FFFFFF"
if (!option.imageDarkness) option.imageDarkness = 10

const noImageSvg = generateSvg(`<svg width="837" height="837" viewBox="0 0 837 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="837" height="837" fill="${option.progressColor}"/>
<path d="M419.324 635.912C406.035 635.912 394.658 631.18 385.195 621.717C375.732 612.254 371 600.878 371 587.589C371 574.3 375.732 562.923 385.195 553.46C394.658 543.997 406.035 539.265 419.324 539.265C432.613 539.265 443.989 543.997 453.452 553.46C462.915 562.923 467.647 574.3 467.647 587.589C467.647 600.878 462.915 612.254 453.452 621.717C443.989 631.18 432.613 635.912 419.324 635.912ZM371 490.941V201H467.647V490.941H371Z" fill="${option.backgroundColor}"/>
</svg>`)

if (!option.thumbnailImage) {
option.thumbnailImage = noImageSvg
};

let thumbnail;

try {
thumbnail = await loadImage(await cropImage({
imagePath: option.thumbnailImage,
borderRadius: 210,
width: 400,
height: 400,
cropCenter: true
}))
} catch {
thumbnail = await loadImage(await cropImage({
imagePath: noImageSvg,
borderRadius: 210,
width: 400,
height: 400,
cropCenter: true
}))
}

if (option.progress < 10) {
option.progress = 10
} else if (option.progress >= 100) {
option.progress = 99.999
}

if (option.name.length > 20) {
option.name = option.name.slice(0, 20) + "...";
}

if (option.author.length > 20) {
option.author = option.author.slice(0, 20) + "...";
}

try {
const canvas = createCanvas(2367, 520);
const ctx = canvas.getContext("2d");

if (!option.backgroundImage) {
const backgroundSvg = generateSvg(`<svg width="2367" height="520" viewBox="0 0 2367 520" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 260C0 116.406 116.406 0 260 0H2107C2250.59 0 2367 116.406 2367 260V260C2367 403.594 2250.59 520 2107 520H260C116.406 520 0 403.594 0 260V260Z" fill="${option.backgroundColor}"/>
</svg>`);

const background = await loadImage(backgroundSvg);

ctx.drawImage(background, 0, 0);
} else {
try {
const darknessSvg = generateSvg(`<svg width="2367" height="520" viewBox="0 0 2367 520" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0H2367V520H0V0Z" fill="#070707" fill-opacity="${option.imageDarkness / 100}"/>
</svg>`)

const image = await cropImage({
imagePath: option.backgroundImage,
width: 2367,
height: 520,
borderRadius: 270,
cropCenter: true
})

const darkImage = await cropImage({
imagePath: darknessSvg,
width: 2367,
height: 520,
borderRadius: 270,
cropCenter: true
})

ctx.drawImage(await loadImage(image), 0, 0)
ctx.drawImage(await loadImage(darkImage), 0, 0)
} catch (error) {
const backgroundSvg = generateSvg(`<svg width="2367" height="520" viewBox="0 0 2367 520" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 260C0 116.406 116.406 0 260 0H2107C2250.59 0 2367 116.406 2367 260V260C2367 403.594 2250.59 520 2107 520H260C116.406 520 0 403.594 0 260V260Z" fill="${option.backgroundColor}"/>
</svg>`);

const background = await loadImage(backgroundSvg);

ctx.drawImage(background, 0, 0);
}
}

ctx.drawImage(thumbnail, 69, 61)

ctx.beginPath();
ctx.arc(2100, 260, 155, 0, Math.PI * 2, true);
ctx.closePath();
ctx.lineWidth = 35;
ctx.strokeStyle = `${option.progressBarColor}`;
ctx.stroke();

const angle = (option.progress / 100) * Math.PI * 2;

ctx.beginPath();
ctx.arc(2100, 260, 155, -Math.PI / 2, -Math.PI / 2 + angle, false);
ctx.lineWidth = 35;
ctx.strokeStyle = option.progressColor;
ctx.stroke();

ctx.fillStyle = `${option.nameColor}`
ctx.font = "100px extrabold"
ctx.fillText(option.name, 550, 240);

ctx.fillStyle = `${option.authorColor}`
ctx.font = "70px semibold"
ctx.fillText(option.author, 550, 350);

return canvas.toBuffer("image/png");
} catch (e: any) {
throw new Error(e.message)
}
}

export { Dynamic };
149 changes: 83 additions & 66 deletions themes/mini.js → src/themes/mini.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1,131 @@
const { createCanvas, loadImage } = require("@napi-rs/canvas");
const { cropImage } = require("cropify");

async function Mini({
thumbnailImage,
backgroundColor,
menuColor,
progress,
progressColor,
progressBarColor,
paused
}) {
if (!progress) progress = 10;

if (!progressBarColor) progressBarColor = "#5F2D00";
if (!progressColor) progressColor = "#FF7A00";
if (!backgroundColor) backgroundColor = "#070707"
if (!menuColor) menuColor = "#FF7A00"

const noImageSvg = `<svg width="837" height="837" viewBox="0 0 837 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="837" height="837" fill="${progressColor}"/>
<path d="M419.324 635.912C406.035 635.912 394.658 631.18 385.195 621.717C375.732 612.254 371 600.878 371 587.589C371 574.3 375.732 562.923 385.195 553.46C394.658 543.997 406.035 539.265 419.324 539.265C432.613 539.265 443.989 543.997 453.452 553.46C462.915 562.923 467.647 574.3 467.647 587.589C467.647 600.878 462.915 612.254 453.452 621.717C443.989 631.18 432.613 635.912 419.324 635.912ZM371 490.941V201H467.647V490.941H371Z" fill="${backgroundColor}"/>
</svg>`

const noimageDataUrl = `data:image/svg+xml;base64,${Buffer.from(noImageSvg).toString('base64')}`;

if (!thumbnailImage) {
thumbnailImage = noimageDataUrl
import { generateSvg } from "../functions/generateSvg";
import { MiniOption } from "../typings/types";
import { createCanvas, loadImage } from "@napi-rs/canvas";
import { cropImage } from "cropify";

const Mini = async (option: MiniOption) => {
if (!option.progress) option.progress = 10;

if (!option.progressBarColor) option.progressBarColor = "#5F2D00";
if (!option.progressColor) option.progressColor = "#FF7A00";
if (!option.backgroundColor) option.backgroundColor = "#070707"
if (!option.menuColor) option.menuColor = "#FF7A00"
if (!option.imageDarkness) option.imageDarkness = 10

const noImageSvg = generateSvg(`<svg width="837" height="837" viewBox="0 0 837 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="837" height="837" fill="${option.progressColor}"/>
<path d="M419.324 635.912C406.035 635.912 394.658 631.18 385.195 621.717C375.732 612.254 371 600.878 371 587.589C371 574.3 375.732 562.923 385.195 553.46C394.658 543.997 406.035 539.265 419.324 539.265C432.613 539.265 443.989 543.997 453.452 553.46C462.915 562.923 467.647 574.3 467.647 587.589C467.647 600.878 462.915 612.254 453.452 621.717C443.989 631.18 432.613 635.912 419.324 635.912ZM371 490.941V201H467.647V490.941H371Z" fill="${option.backgroundColor}"/>
</svg>`)

if (!option.thumbnailImage) {
option.thumbnailImage = noImageSvg
};

let thumbnail;

try {
thumbnail = await loadImage(await cropImage({
imagePath: thumbnailImage,
borderRadius: 80,
imagePath: option.thumbnailImage,
borderRadius: 50,
width: 544,
height: 544,
cropCenter: true
}))
} catch {
thumbnail = await loadImage(await cropImage({
imagePath: noimageDataUrl,
borderRadius: 80,
imagePath: noImageSvg,
borderRadius: 50,
width: 544,
height: 544,
cropCenter: true
}))
}

if (progress < 10) {
progress = 10
} else if (progress > 100) {
progress = 100
if (option.progress < 10) {
option.progress = 10
} else if (option.progress > 100) {
option.progress = 100
}

try {
const canvas = createCanvas(613, 837);
const ctx = canvas.getContext("2d");

// ---------------------------------
const backgroundSvg = `<svg width="613" height="837" viewBox="0 0 613 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="613" height="837" rx="84" fill="${backgroundColor}" />
</svg>`
if (!option.backgroundImage) {
const backgroundSvg = generateSvg(`<svg width="613" height="837" viewBox="0 0 613 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="613" height="837" rx="50" fill="${option.backgroundColor}" />
</svg>`)

const background = await loadImage(backgroundSvg);

const backgroundDataUrl = `data:image/svg+xml;base64,${Buffer.from(backgroundSvg).toString('base64')}`;
ctx.drawImage(background, 0, 0);
} else {
try {
const darknessSvg = generateSvg(`<svg width="618" height="837" viewBox="0 0 618 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="618" height="837" rx="50" fill="#070707" fill-opacity="${option.imageDarkness / 100}"/>
</svg>`)

const image = await cropImage({
imagePath: option.backgroundImage,
width: 613,
height: 837,
borderRadius: 50,
cropCenter: true
})

ctx.drawImage(await loadImage(image), 0, 0)
ctx.drawImage(await loadImage(darknessSvg), 0, 0)
} catch (error) {
const backgroundSvg = generateSvg(`<svg width="613" height="837" viewBox="0 0 613 837" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="613" height="837" rx="50" fill="${option.backgroundColor}" />
</svg>`)

const background = await loadImage(backgroundSvg);

ctx.drawImage(background, 0, 0);
}
}

const background = await loadImage(backgroundDataUrl);

ctx.drawImage(background, 0, 0);
// ---------------------------------
ctx.drawImage(thumbnail, 34, 29);
// ---------------------------------
const completed = 544 * progress / 100

const progressBarSvg = `<svg width="544" height="34" viewBox="0 0 544 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="544" height="34" rx="17" fill="${progressBarColor}" />
<rect width="${completed}" height="34" rx="17" fill="${progressColor}" />
</svg>`
const completed = 544 * option.progress / 100

const progressDataUrl = `data:image/svg+xml;base64,${Buffer.from(progressBarSvg).toString('base64')}`;
const progressBarSvg = generateSvg(`<svg width="544" height="34" viewBox="0 0 544 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="544" height="34" rx="17" fill="${option.progressBarColor}" />
<rect width="${completed}" height="34" rx="17" fill="${option.progressColor}" />
</svg>`)

const progressBar = await loadImage(progressDataUrl);
const progressBar = await loadImage(progressBarSvg);

ctx.drawImage(progressBar, 34, 611);
// ---------------------------------

let middleMenu;

if (paused) {
middleMenu = `<path d="M145 69.6L178.6 48L145 26.4V69.6ZM157 96C150.36 96 144.12 94.74 138.28 92.22C132.44 89.7 127.36 86.28 123.04 81.96C118.72 77.64 115.3 72.56 112.78 66.72C110.26 60.88 109 54.64 109 48C109 41.36 110.26 35.12 112.78 29.28C115.3 23.44 118.72 18.36 123.04 14.04C127.36 9.72 132.44 6.3 138.28 3.78C144.12 1.26 150.36 0 157 0C163.64 0 169.88 1.26 175.72 3.78C181.56 6.3 186.64 9.72 190.96 14.04C195.28 18.36 198.7 23.44 201.22 29.28C203.74 35.12 205 41.36 205 48C205 54.64 203.74 60.88 201.22 66.72C198.7 72.56 195.28 77.64 190.96 81.96C186.64 86.28 181.56 89.7 175.72 92.22C169.88 94.74 163.64 96 157 96Z" fill="${menuColor}" />`
if (option.paused) {
middleMenu = `<path d="M145 69.6L178.6 48L145 26.4V69.6ZM157 96C150.36 96 144.12 94.74 138.28 92.22C132.44 89.7 127.36 86.28 123.04 81.96C118.72 77.64 115.3 72.56 112.78 66.72C110.26 60.88 109 54.64 109 48C109 41.36 110.26 35.12 112.78 29.28C115.3 23.44 118.72 18.36 123.04 14.04C127.36 9.72 132.44 6.3 138.28 3.78C144.12 1.26 150.36 0 157 0C163.64 0 169.88 1.26 175.72 3.78C181.56 6.3 186.64 9.72 190.96 14.04C195.28 18.36 198.7 23.44 201.22 29.28C203.74 35.12 205 41.36 205 48C205 54.64 203.74 60.88 201.22 66.72C198.7 72.56 195.28 77.64 190.96 81.96C186.64 86.28 181.56 89.7 175.72 92.22C169.88 94.74 163.64 96 157 96Z" fill="${option.menuColor}" />`
} else {
middleMenu = `<path d="M142.6 67.2H152.2V28.8H142.6V67.2ZM161.8 67.2H171.4V28.8H161.8V67.2ZM157 96C150.36 96 144.12 94.74 138.28 92.22C132.44 89.7 127.36 86.28 123.04 81.96C118.72 77.64 115.3 72.56 112.78 66.72C110.26 60.88 109 54.64 109 48C109 41.36 110.26 35.12 112.78 29.28C115.3 23.44 118.72 18.36 123.04 14.04C127.36 9.72 132.44 6.3 138.28 3.78C144.12 1.26 150.36 0 157 0C163.64 0 169.88 1.26 175.72 3.78C181.56 6.3 186.64 9.72 190.96 14.04C195.28 18.36 198.7 23.44 201.22 29.28C203.74 35.12 205 41.36 205 48C205 54.64 203.74 60.88 201.22 66.72C198.7 72.56 195.28 77.64 190.96 81.96C186.64 86.28 181.56 89.7 175.72 92.22C169.88 94.74 163.64 96 157 96Z" fill="${menuColor}" />`
middleMenu = `<path d="M142.6 67.2H152.2V28.8H142.6V67.2ZM161.8 67.2H171.4V28.8H161.8V67.2ZM157 96C150.36 96 144.12 94.74 138.28 92.22C132.44 89.7 127.36 86.28 123.04 81.96C118.72 77.64 115.3 72.56 112.78 66.72C110.26 60.88 109 54.64 109 48C109 41.36 110.26 35.12 112.78 29.28C115.3 23.44 118.72 18.36 123.04 14.04C127.36 9.72 132.44 6.3 138.28 3.78C144.12 1.26 150.36 0 157 0C163.64 0 169.88 1.26 175.72 3.78C181.56 6.3 186.64 9.72 190.96 14.04C195.28 18.36 198.7 23.44 201.22 29.28C203.74 35.12 205 41.36 205 48C205 54.64 203.74 60.88 201.22 66.72C198.7 72.56 195.28 77.64 190.96 81.96C186.64 86.28 181.56 89.7 175.72 92.22C169.88 94.74 163.64 96 157 96Z" fill="${option.menuColor}" />`
}
const menuSvg = `<svg width="315" height="96" viewBox="0 0 315 96" fill="none" xmlns="http://www.w3.org/2000/svg">`

const menuSvg = generateSvg(`<svg width="315" height="96" viewBox="0 0 315 96" fill="none" xmlns="http://www.w3.org/2000/svg">`
+
`${middleMenu}`
+
`<path d="M263.2 62.8H270.6V33.2H263.2V62.8ZM278 62.8L300.2 48L278 33.2V62.8ZM278 85C272.882 85 268.072 84.0287 263.57 82.0862C259.068 80.1437 255.153 77.5075 251.822 74.1775C248.492 70.8475 245.856 66.9317 243.914 62.43C241.971 57.9283 241 53.1183 241 48C241 42.8817 241.971 38.0717 243.914 33.57C245.856 29.0683 248.492 25.1525 251.822 21.8225C255.153 18.4925 259.068 15.8563 263.57 13.9138C268.072 11.9712 272.882 11 278 11C283.118 11 287.928 11.9712 292.43 13.9138C296.932 15.8563 300.848 18.4925 304.178 21.8225C307.508 25.1525 310.144 29.0683 312.086 33.57C314.029 38.0717 315 42.8817 315 48C315 53.1183 314.029 57.9283 312.086 62.43C310.144 66.9317 307.508 70.8475 304.178 74.1775C300.848 77.5075 296.932 80.1437 292.43 82.0862C287.928 84.0287 283.118 85 278 85Z" fill="${menuColor}" />`
`<path d="M263.2 62.8H270.6V33.2H263.2V62.8ZM278 62.8L300.2 48L278 33.2V62.8ZM278 85C272.882 85 268.072 84.0287 263.57 82.0862C259.068 80.1437 255.153 77.5075 251.822 74.1775C248.492 70.8475 245.856 66.9317 243.914 62.43C241.971 57.9283 241 53.1183 241 48C241 42.8817 241.971 38.0717 243.914 33.57C245.856 29.0683 248.492 25.1525 251.822 21.8225C255.153 18.4925 259.068 15.8563 263.57 13.9138C268.072 11.9712 272.882 11 278 11C283.118 11 287.928 11.9712 292.43 13.9138C296.932 15.8563 300.848 18.4925 304.178 21.8225C307.508 25.1525 310.144 29.0683 312.086 33.57C314.029 38.0717 315 42.8817 315 48C315 53.1183 314.029 57.9283 312.086 62.43C310.144 66.9317 307.508 70.8475 304.178 74.1775C300.848 77.5075 296.932 80.1437 292.43 82.0862C287.928 84.0287 283.118 85 278 85Z" fill="${option.menuColor}" />`
+
`<path d="M51.8 33.2L44.4 33.2L44.4 62.8H51.8L51.8 33.2ZM37 33.2L14.8 48L37 62.8L37 33.2ZM37 11C42.1183 11 46.9283 11.9713 51.43 13.9138C55.9317 15.8563 59.8475 18.4925 63.1775 21.8225C66.5075 25.1525 69.1437 29.0683 71.0862 33.57C73.0288 38.0717 74 42.8817 74 48C74 53.1183 73.0288 57.9283 71.0862 62.43C69.1437 66.9317 66.5075 70.8475 63.1775 74.1775C59.8475 77.5075 55.9317 80.1437 51.43 82.0862C46.9283 84.0288 42.1183 85 37 85C31.8817 85 27.0717 84.0288 22.57 82.0862C18.0683 80.1437 14.1525 77.5075 10.8225 74.1775C7.4925 70.8475 4.85625 66.9317 2.91375 62.43C0.97125 57.9283 -9.53674e-07 53.1183 -9.53674e-07 48C-9.53674e-07 42.8817 0.97125 38.0717 2.91375 33.57C4.85625 29.0683 7.4925 25.1525 10.8225 21.8225C14.1525 18.4925 18.0683 15.8563 22.57 13.9138C27.0717 11.9713 31.8817 11 37 11Z" fill="${menuColor}" />`
`<path d="M51.8 33.2L44.4 33.2L44.4 62.8H51.8L51.8 33.2ZM37 33.2L14.8 48L37 62.8L37 33.2ZM37 11C42.1183 11 46.9283 11.9713 51.43 13.9138C55.9317 15.8563 59.8475 18.4925 63.1775 21.8225C66.5075 25.1525 69.1437 29.0683 71.0862 33.57C73.0288 38.0717 74 42.8817 74 48C74 53.1183 73.0288 57.9283 71.0862 62.43C69.1437 66.9317 66.5075 70.8475 63.1775 74.1775C59.8475 77.5075 55.9317 80.1437 51.43 82.0862C46.9283 84.0288 42.1183 85 37 85C31.8817 85 27.0717 84.0288 22.57 82.0862C18.0683 80.1437 14.1525 77.5075 10.8225 74.1775C7.4925 70.8475 4.85625 66.9317 2.91375 62.43C0.97125 57.9283 -9.53674e-07 53.1183 -9.53674e-07 48C-9.53674e-07 42.8817 0.97125 38.0717 2.91375 33.57C4.85625 29.0683 7.4925 25.1525 10.8225 21.8225C14.1525 18.4925 18.0683 15.8563 22.57 13.9138C27.0717 11.9713 31.8817 11 37 11Z" fill="${option.menuColor}" />`
+
`</svg>`
`</svg>`)

const menuDataUrl = `data:image/svg+xml;base64,${Buffer.from(menuSvg).toString('base64')}`;

const menu = await loadImage(menuDataUrl);
const menu = await loadImage(menuSvg);

ctx.drawImage(menu, 143, 693)
// ---------------------------------

return canvas.toBuffer("image/png");
} catch (e) {
} catch (e: any) {
throw new Error(e.message)
}
}

module.exports = { Mini };
export { Mini }
59 changes: 59 additions & 0 deletions src/typings/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export type ClassicOption = {
thumbnailImage?: string;
backgroundColor?: string;
backgroundImage?: string;
imageDarkness?: number;
progress?: number;
progressColor?: string;
progressBarColor?: string;
name?: string;
nameColor?: string;
author?: string;
authorColor?: string;
startTime?: string;
endTime?: string;
timeColor?: string
}

export type ClassicProOption = {
thumbnailImage?: string;
backgroundColor?: string;
backgroundImage?: string;
imageDarkness?: number;
progress?: number;
progressColor?: string;
progressBarColor?: string;
name?: string;
nameColor?: string;
author?: string;
authorColor?: string;
startTime?: string;
endTime?: string;
timeColor?: string;
}

export type DynamicOption = {
thumbnailImage?: string;
backgroundColor?: string;
backgroundImage?: string;
imageDarkness?: number;
progress?: number;
progressColor?: string;
progressBarColor?: string;
name?: string;
nameColor?: string;
author?: string;
authorColor?: string;
}

export type MiniOption = {
thumbnailImage: string;
backgroundColor: string;
backgroundImage?: string;
imageDarkness?: number;
menuColor: string;
progress: number;
progressColor: string;
progressBarColor: string;
paused: boolean;
}
6 changes: 6 additions & 0 deletions tests/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { Classic } = require("musicard");
const fs = require('fs')

Classic({}).then(x => {
fs.writeFileSync("output.png", x)
})
Binary file added tests/output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
131 changes: 0 additions & 131 deletions themes/classic.js

This file was deleted.

129 changes: 0 additions & 129 deletions themes/classicpro.js

This file was deleted.

116 changes: 0 additions & 116 deletions themes/dynamic.js

This file was deleted.

130 changes: 0 additions & 130 deletions themes/minipro.js

This file was deleted.

19 changes: 19 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2016",
"module": "CommonJS",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"declaration": true,
"noImplicitAny": true,
"isolatedModules": true,
"noEmit": true,
},
"include": [
"src"
],
"exclude": [
"node_modules"
]
}
10 changes: 10 additions & 0 deletions tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'tsup';

export default defineConfig({
format: ['cjs', 'esm'],
entry: ['./src/index.ts'],
dts: true,
shims: true,
skipNodeModulesBundle: true,
clean: ['./src/index.ts']
});