@justherza/canvafy
Version:
Make configurable canvas easily with Canvafy
328 lines (283 loc) • 11 kB
JavaScript
;
const { createCanvas, loadImage, registerFont } = require('canvas');
const Util = require("../plugins/Util");
const path = require('path');
module.exports = class newSpotify {
constructor(options) {
this.font = { name: options?.font?.name ?? "Manrope", path: options?.font?.path };
this.album = null;
this.artist = null;
this.border = null;
this._bar_width = 1400;
this.end = null;
this.overlay_opacity = null;
this.image = null;
this.blur = 3;
this.title = null;
this.start = null;
this.spotifyLogo = true;
this.randomColors = ["#0c0c0c","#121212","#282828","#1c1c1c","#244c66"];
this.emojiCache = new Map();
}
setAlbum(name) {
if (!name || typeof name !== "string") throw new Error("The argument of the setAlbum method must be a string.");
this.album = name;
return this;
}
setAuthor(name) {
if (!name || typeof name !== "string") throw new Error("The argument of the setAuthor method must be a string.");
this.artist = name;
return this;
}
setBorder(color) {
if (color) {
if (/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/.test(color)) {
this.border = color;
return this;
} else {
throw new Error("Invalid color for the argument in the setBorder method. You must give a hexadecimal color.")
}
} else {
throw new Error("You must give a hexadecimal color as the argument of setBorder method.");
}
}
setOverlayOpacity(opacity = 0) {
if (opacity) {
if (opacity >= 0 && opacity <= 1) {
this.overlay_opacity = opacity;
return this;
} else {
throw new Error("The value of the opacity of setOverlayOpacity method must be between 0 and 1 (0 and 1 included).");
}
}
}
setBlur(blur = 3) {
if (blur) {
if (blur >= 0 && blur <= 15) {
this.blur = blur;
return this;
} else {
throw new Error("The value of the opacity of setBlur method must be between 0 and 15 (0 and 15 included).");
}
}
}
setImage(image) {
if (!image) throw new Error("The argument of the setImage method must be a string or a Buffer or a Canvas.Image.");
this.image = image;
return this;
}
setTitle(title) {
if (!title || typeof title !== "string") throw new Error("The argument of the setTitle method must be a string.");
this.title = title;
return this;
}
setSpotifyLogo(bool){
if(typeof bool !== "boolean") {
throw new Error("You must give a abbreviate number true or false argument.");
}
this.spotifyLogo = bool;
return this;
}
setTimestamp(start, end) {
if (!start || typeof start !== "number") throw new Error("The first argument of the setTimestamp method must be a number.");
if (!end || typeof end !== "number") throw new Error("The first argument of the setTimestamp method must be a number.");
this.start = start;
this.end = end;
return this;
}
getEmojiCodePoint(emoji) {
return Array.from(emoji).map(char =>
char.codePointAt(0).toString(16)
).join('-');
}
async loadEmojiImage(emojiChar) {
if (this.emojiCache.has(emojiChar)) {
return this.emojiCache.get(emojiChar);
}
try {
const codePoint = this.getEmojiCodePoint(emojiChar);
const emojiUrl = `https://twemoji.maxcdn.com/v/latest/72x72/${codePoint}.png`;
const emojiImage = await loadImage(emojiUrl);
this.emojiCache.set(emojiChar, emojiImage);
return emojiImage;
} catch (error) {
return null;
}
}
async drawTextWithEmoji(ctx, text, x, y, fontSize, color, font, align = 'center') {
const emojiRegex = /(\p{Emoji_Presentation}|\p{Emoji}\uFE0F|\p{Extended_Pictographic})/gu;
if (!emojiRegex.test(text)) {
ctx.fillStyle = color;
ctx.font = font;
ctx.textAlign = align;
ctx.fillText(text, x, y);
return;
}
const parts = text.split(emojiRegex).filter(part => part !== '');
let totalWidth = 0;
ctx.font = font;
for (const part of parts) {
if (emojiRegex.test(part)) {
totalWidth += fontSize * 0.9;
} else {
totalWidth += ctx.measureText(part).width;
}
}
let currentX = align === 'center' ? x - totalWidth / 2 : x;
ctx.fillStyle = color;
ctx.textAlign = 'left';
for (const part of parts) {
if (emojiRegex.test(part)) {
const emojiImage = await this.loadEmojiImage(part);
if (emojiImage) {
ctx.drawImage(emojiImage, currentX, y - fontSize * 0.75, fontSize * 0.9, fontSize * 0.9);
currentX += fontSize * 0.9;
} else {
ctx.fillText(part, currentX, y);
currentX += ctx.measureText(part).width;
}
} else if (part.trim()) {
ctx.fillText(part, currentX, y);
currentX += ctx.measureText(part).width;
}
}
}
_calcule_progress(current, total) {
const progress = (current / total) * this._bar_width;
if (isNaN(progress) || current < 0) {
return 0;
} else if (progress > this._bar_width) {
return this._bar_width;
} else {
return progress;
}
}
async build() {
if (!this.title) throw new Error("Missing 'title' parameter.");
if (!this.artist) throw new Error("Missing 'artist' parameter.");
if (!this.start) throw new Error("Missing 'start' parameter.");
if (!this.end) throw new Error("Missing 'end' parameter.");
if (this.font.path) {
registerFont(this.font.path, { family: this.font.name });
}
const start_format = Util.format_time(this.start > this.end ? this.end : this.start);
const end_format = Util.format_time(this.end);
const canvas = createCanvas(2000, 585);
const ctx = canvas.getContext("2d");
if (this.border) {
ctx.beginPath();
ctx.lineWidth = 8;
ctx.strokeStyle = this.border;
ctx.moveTo(55, 15);
ctx.lineTo(canvas.width - 55, 15);
ctx.quadraticCurveTo(canvas.width - 20, 20, canvas.width - 15, 55);
ctx.lineTo(canvas.width - 15, canvas.height - 55);
ctx.quadraticCurveTo(canvas.width - 20, canvas.height - 20, canvas.width - 55, canvas.height - 15);
ctx.lineTo(55, canvas.height - 15);
ctx.quadraticCurveTo(20, canvas.height - 20, 15, canvas.height - 55);
ctx.lineTo(15, 55);
ctx.quadraticCurveTo(20, 20, 55, 15);
ctx.lineTo(56, 15);
ctx.stroke();
ctx.closePath();
}
ctx.beginPath();
ctx.moveTo(65, 25);
ctx.lineTo(canvas.width - 65, 25);
ctx.quadraticCurveTo(canvas.width - 25, 25, canvas.width - 25, 65);
ctx.lineTo(canvas.width - 25, canvas.height - 65);
ctx.quadraticCurveTo(canvas.width - 25, canvas.height - 25, canvas.width - 65, canvas.height - 25);
ctx.lineTo(65, canvas.height - 25);
ctx.quadraticCurveTo(25, canvas.height - 25, 25, canvas.height - 65);
ctx.lineTo(25, 65);
ctx.quadraticCurveTo(25, 25, 65, 25);
ctx.lineTo(66, 25);
ctx.closePath();
ctx.clip();
ctx.globalAlpha = 1;
if (!this.image || this.image == null) {
ctx.beginPath();
ctx.fillStyle = `${this.randomColors[Math.floor(Math.random()*this.randomColors.length)]}`;
ctx.fillRect(10, 10, canvas.width, canvas.height)
} else if (this.image) {
try {
ctx.filter = `blur(${this.blur}px)`;
ctx.drawImage(await loadImage(this.image), 0, -500, canvas.width, 2000);
} catch {
throw new Error("The image given in the parameter of the setImage method is not valid or you are not connected to the internet.");
}
}
ctx.filter = "none";
if(this.overlay_opacity){
ctx.beginPath();
ctx.globalAlpha = this.overlay_opacity;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
}
ctx.globalAlpha = 1;
const progressBar = (ctx, x, y, width, height) => {
ctx.fillStyle = "#6a625e";
roundRect(ctx, x, y, width, height, 8, true, false);
ctx.fillStyle = "#fff";
roundRect(ctx, x, y, this._calcule_progress(this.start, this.end), height, 8, true, false);
ctx.beginPath();
ctx.arc(x + this._calcule_progress(this.start, this.end), y + height / 2, height * 1.25, 0, 360);
ctx.fill();
ctx.closePath();
};
progressBar(ctx, 300, 400, this._bar_width, 8)
if(this.spotifyLogo){
try {
ctx.drawImage(await loadImage(`${__dirname}/../assets/images/spotify-logo.png`), 950, 60, 100, 100);
} catch {
throw new Error("The image given in the parameter of the Spotify method is not valid or you are not connected to the internet.");
}
}
const titleText = this.title.length >= 40 ? this.title.slice(0, 40)+"...": this.title;
await this.drawTextWithEmoji(ctx, titleText, 1000, 285, 50, "#fff", `bold 50px ${this.font.name}`, 'center');
const artistText = this.artist.length >= 40 ? this.artist.slice(0, 40)+"...":this.artist;
await this.drawTextWithEmoji(ctx, artistText, 1000, 215, 28, "#94a3b8", `bold 28px ${this.font.name}`, 'center');
if (this.album && typeof this.album === "string") {
const albumText = this.album.length >= 40 ? this.album.slice(0, 40)+"...":this.album;
await this.drawTextWithEmoji(ctx, albumText, 1000, 350, 30, "#94a3b8", `regular 30px ${this.font.name}`, 'center');
}
ctx.fillStyle = "#cbd5e1";
ctx.font = `"regular 20px ${this.font.name}`
ctx.textAlign = "center";
ctx.fillText(end_format, 1782, 412);
ctx.fillStyle = "#cbd5e1";
ctx.font = `regular 20px ${this.font.name}`;
ctx.textAlign = "center";
ctx.fillText(start_format, 230, 412);
return canvas.toBuffer('image/png');
}
}
function roundRect(ctx, x, y, width, height, radius = 5, fill = true, stroke = false) {
if (typeof radius === "number") {
radius = { tl: radius, tr: radius, br: radius, bl: radius };
}
else {
let defaultRadius = { tl: 0, tr: 0, br: 0, bl: 0 };
for (let side in defaultRadius) {
radius[side] = radius[side] || defaultRadius[side];
}
}
ctx.beginPath();
ctx.moveTo(x + radius.tl, y);
ctx.lineTo(x + width - radius.tr, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
ctx.lineTo(x + width, y + height - radius.br);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
ctx.lineTo(x + radius.bl, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
ctx.lineTo(x, y + radius.tl);
ctx.quadraticCurveTo(x, y, x + radius.tl, y);
ctx.closePath();
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.stroke();
}
};