canvacard
Version:
Powerful image manipulation package for beginners.
310 lines (267 loc) • 10.3 kB
JavaScript
const { createCanvas, loadImage } = require("@napi-rs/canvas");
const fs = require("fs");
const APIError = require("./utils/error.utils");
const formatVariable = require("./utils/formatVariable.utils");
const parseSvg = require("./utils/parseSvg.utils");
const formatAndValidateHex = require("./utils/formatAndValidateHex.utils");
/**
* @kind class
* @description Fortnite Shop card creator
* <details open>
* <summary>PREVIEW</summary>
* <br>
* <a>
* <img src="https://raw.githubusercontent.com/SrGobi/canvacard/refs/heads/test/fortnite_shop.png" alt="Fortnite Shop Card Preview">
* </a>
* </details>
*
* To obtain a Fortnite API token, visit [fortnite-api.com](https://fortnite-api.com/)
*
* @example
* ```js
const shop = new canvacard.FortniteShop()
.setToken("f4a26b940ef54a9a4238cef040bd08fa9001cd6c")
.setText("footer", "ESP CUSTOMS X FORTNITE")
const FortniteShopImage = await shop.build("Luckiest Guy");
canvacard.write(FortniteShopImage, "./fortnite_shop.png");
* ```
*/
class FortniteShop {
constructor() {
this.token = ""; // Requiere token válido
this.textHeader = "FORTNITE ITEMS SHOP";
this.textFooter = "Generated with canvascard";
this.options = { lang: "es", dateFormat: "dddd, MMMM Do YYYY" };
this.rows = 8;
}
/**
* @method setToken
* @name setToken
* @description Set the Fortnite API token
* @param {string} value Fortnite API token
* @returns {FortniteShop} The current instance of FortniteShop
* @throws {APIError} If the value is not a string
*/
setToken(value) {
if (!value || typeof value !== "string") throw new APIError("Please provide a valid token for fortnite-api.com!");
this.token = value;
return this;
}
/**
* @method setRows
* @name setRows
* @description Set the number of rows for the Fortnite Shop card
* @param {number} value Number of rows to set for the card
* @returns {FortniteShop} The current instance of FortniteShop
* @throws {APIError} If the value is not a number
*/
setRows(value) {
if (!value || typeof value !== "number") throw new APIError("Please provide a valid number of rows for fortnite-api.com!");
this.rows = value;
return this;
}
/**
* @method setText
* @name setText
* @description Set the text for the Fortnite Shop card
* @param {string} value Text to set for the card
* @returns {FortniteShop} The current instance of FortniteShop
* @throws {APIError} If the value is not a string
*/
setText(variable, value) {
if (typeof value !== "string") throw new APIError("The value must be a text string!");
const formattedVariable = formatVariable("text", variable);
if (this[formattedVariable]) this[formattedVariable] = value;
return this;
}
/**
* @method build
* @name build
* @description Build the Fortnite Shop card
* @param {string} [font="Arial"] Font to use for the card
* @returns {Promise<Buffer>} Card image in buffer format
* @throws {APIError} If the token is not provided
*/
async build(font = "Arial") {
if (!this.token) throw new APIError("Please provide a valid token for fortnite-api.com!");
const shopRequest = await fetch("https://fortnite-api.com/v2/shop?language=es", {
headers: { "x-api-key": this.token }
});
if (!shopRequest.ok) throw new APIError("Error al obtener datos de la tienda: " + shopRequest.statusText);
const shopData = await shopRequest.json();
const entries = shopData.data.entries || [];
// Process items same as before
let items = [];
for (const entry of entries) {
if (entry.bundle) {
items.push({
type: "bundle",
name: entry.bundle.name,
image: entry.bundle.image,
price: entry.finalPrice,
section: entry.section?.name || "Featured",
colors: entry.colors // Colors for gradient
});
} else if (entry.brItems) {
const itemData = entry.brItems.map(item => ({
type: "item",
name: item.name,
description: item.description,
rarity: item.rarity?.backendValue || "Common",
image: item.images?.featured || item.images?.icon || item.images?.smallIcon || "",
price: entry.finalPrice,
section: entry.section?.name || "Daily",
colors: item?.series?.colors // Array of colors for gradient
}));
items = items.concat(itemData);
}
}
// Group items by section
const sections = {};
items.forEach(item => {
if (!sections[item.section]) sections[item.section] = [];
sections[item.section].push(item);
});
// Canvas setup with new dimensions
const itemsPerRow = this.rows;
const itemWidth = 512;
const itemHeight = 512;
const padding = 20;
const headerHeight = 100;
const sectionPadding = 60;
// Calculate total width based on sections
const canvasWidth = itemsPerRow * (itemWidth + padding) + padding;
// Calculate total height based on sections
const rows = Math.ceil(items.filter(i => i.type === "item").length / itemsPerRow) + Math.ceil(items.filter(i => i.type === "bundle").length / itemsPerRow);
const canvasHeight = rows * (itemHeight + padding) + (headerHeight + sectionPadding) + 300;
const canvas = createCanvas(canvasWidth, canvasHeight);
const ctx = canvas.getContext("2d");
// Create gradient background
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#1e3c72');
gradient.addColorStop(1, '#2a5298');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw header
ctx.fillStyle = '#ffffff';
ctx.font = `bold 60px ${font}`;
ctx.textAlign = 'center';
ctx.fillText(this.textHeader.toUpperCase(), canvas.width / 2, 70);
// Draw date
const date = new Date().toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
});
ctx.font = `30px ${font}`;
ctx.fillText(date, canvas.width / 2, 110);
// Draw sections
let currentY = headerHeight + padding;
for (const [sectionName, sectionItems] of Object.entries(sections)) {
// Section header
ctx.font = `bold 40px ${font}`;
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'left';
ctx.fillText(sectionName.toUpperCase(), padding, currentY + 40);
currentY += 60;
// Draw items in grid
let currentX = padding;
let maxRowHeight = 0;
for (let i = 0; i < sectionItems.length; i++) {
const item = sectionItems[i];
if (currentX + itemWidth > canvas.width - padding) {
currentX = padding;
currentY += maxRowHeight + padding;
maxRowHeight = 0;
}
// Draw item card
await this.drawItemCard(ctx, item, currentX, currentY, itemWidth, itemHeight, font);
currentX += itemWidth + padding;
maxRowHeight = Math.max(maxRowHeight, itemHeight);
if (i === sectionItems.length - 1) {
currentY += maxRowHeight + padding;
}
}
currentY += sectionPadding;
}
// Draw footer
ctx.fillStyle = '#ffffff';
ctx.font = `30px ${font}`;
ctx.textAlign = 'center';
ctx.fillText(this.textFooter, canvas.width / 2, canvas.height - 30);
return canvas.toBuffer("image/png");
}
// New method to draw colored rounded rectangles
getItemColors(item) {
if (item.type === "bundle") {
// Validar los colores a hexadecimal
const color1 = formatAndValidateHex(item.colors.color1);
const color2 = formatAndValidateHex(item.colors.color2);
return [color1, color2];
} else if (item.type === "item") {
if (item.colors) {
// Si tiene colores, toma los dos primeros y los valida
const color1 = formatAndValidateHex(item.colors[0]);
const color2 = formatAndValidateHex(item.colors[1]);
return [color1, color2];
} else {
// Si no tiene series.colors, obtiene los colores por rareza
return this.getRarityColors(item.rarity);
}
}
// Si no hay colores definidos, usa un gradiente por defecto
return ["#ffffff", "#000000"];
}
// New method to draw individual item cards
async drawItemCard(ctx, item, x, y, width, height, font) {
const colors = this.getItemColors(item);
// Draw card background with gradient
const cardGradient = ctx.createLinearGradient(x, y, x, y + height);
cardGradient.addColorStop(0, colors[0]);
cardGradient.addColorStop(1, colors[1]);
// Card shadow
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 5;
// Draw rounded rectangle
ctx.beginPath();
ctx.roundRect(x, y, width, height, 15);
ctx.fillStyle = cardGradient;
ctx.fill();
// Reset shadow
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
// Draw item image
try {
const image = await loadImage(item.image);
const imageSize = Math.min(width - 40, height - 100);
const imageX = x + (width - imageSize) / 2;
const imageY = y + 20;
ctx.drawImage(image, imageX, imageY, imageSize, imageSize);
} catch (error) {
console.error(`Error loading image for ${item.name}:`, error);
}
// Draw item name
ctx.fillStyle = '#ffffff';
ctx.font = `bold 20px ${font}`;
ctx.textAlign = 'center';
ctx.fillText(item.name, x + width / 2, y + height - 60, width - 20);
// Draw price with V-Bucks icon
ctx.font = `bold 24px ${font}`;
ctx.fillText(`${item.price} V-Bucks`, x + width / 2, y + height - 20);
}
// Updated rarity colors method
getRarityColors(rarity) {
const rarities = {
"EFortRarity::Legendary": ['#ea8d23', 'rgba(233, 141, 75, 0.9)'],
"EFortRarity::Epic": ['#c359ff', 'rgba(233, 94, 255, 0.9)'],
"EFortRarity::Rare": ['#2cc1ff', 'rgba(55, 209, 255, 0.9)'],
"EFortRarity::Uncommon": ['#69bb1e', 'rgba(135, 227, 57, 0.9)'],
"EFortRarity::Common": ['#bebebe', 'rgba(177, 177, 177, 0.9)'],
};
return rarities[rarity] || rarities["EFortRarity::Uncommon"];
}
}
module.exports = FortniteShop;