UNPKG

@justherza/canvafy

Version:

Make configurable canvas easily with Canvafy

328 lines (283 loc) 11 kB
"use strict"; 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(); } };