UNPKG

canvacard

Version:

Powerful image manipulation package for beginners.

317 lines (267 loc) 10.9 kB
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"); /** * @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 = "f4a26b940ef54a9a4238cef040bd08fa9001cd6c"; this.textHeader = "FORTNITE ITEMS SHOP"; this.textFooter = "Generated with canvascard"; this.options = { lang: "es", dateFormat: "dddd, MMMM Do YYYY" }; } /** * @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 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/br/combined?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.featured.entries; // Create a new arrangement for the items let items = []; const existingItemsSet = new Set(); // Use Set to avoid duplicates for (const entry of entries) { if (entry.bundle) { // If it's a bundle, get the first item in the bundle for the rarity const bundle = entry.bundle; const firstItem = entry.items[0]; // Get the first item // Add the batch to the array, using the rarity of the first item items.push({ name: bundle.name, type: bundle.info, images: bundle.image, regularPrice: entry.regularPrice, finalPrice: entry.finalPrice, rarity: firstItem?.rarity || { backendValue: 'EFortRarity::Common' } // Add first item rarity or default value }); } else { // If it is a loose item, add only the item entry.items.forEach(item => { if (!existingItemsSet.has(item.name)) { existingItemsSet.add(item.name); items.push({ name: item.name, type: "Article", rarity: item.rarity, images: item.images?.icon || item.images?.smallIcon || item.images?.featured, finalPrice: entry.finalPrice // Add price to item object }); } }); } } // Sort items by finalPrice (lowest to highest) items.sort((a, b) => b.finalPrice - a.finalPrice); const itemsPerRow = 8; // Maximum 8 items per row const maxRows = 10; // Maximum 10 rows const itemWidth = 384; // Width of each item const itemHeight = 384; // Height of each item const padding = 15; // Space between items const canvasWidth = itemsPerRow * itemWidth + (itemsPerRow - 1) * padding; const canvasHeight = maxRows * itemHeight + (maxRows - 1) * padding + 300; // Extra adjustment for header const canvas = createCanvas(canvasWidth, canvasHeight); const ctx = canvas.getContext("2d"); // Create a blue circular radial gradient const gradient = ctx.createRadialGradient( canvasWidth / 2, // X center of gradient canvasHeight / 2, // Y center of gradient 0, // Start radius (0 to start at a point) canvasWidth / 2, // X center of gradient canvasHeight / 2, // Y center of gradient canvasWidth // Final radius set to canvas width ); gradient.addColorStop(0, '#0064BD'); // Light Blue (SkyBlue) gradient.addColorStop(1, '#001E8E'); // Dark blue // Apply gradient as background ctx.fillStyle = gradient; ctx.fillRect(0, 0, canvasWidth, canvasHeight); // Draw header ctx.fillStyle = "#ffffff"; ctx.font = `70px ${font}`; ctx.textAlign = "center"; ctx.fillText(this.textHeader, canvas.width / 2, 100); // Initialize position variables let currentRow = 0; let currentColumn = 0; // Iterate over the items and draw them on the canvas for (const item of items) { try { const { name, type, rarity, images, finalPrice } = item; // Calculate item position based on current column and row const x = currentColumn * (itemWidth + padding); const y = 200 + currentRow * (itemHeight + padding); // Get colors by rarity const rarityColors = this.getRarityColors(rarity.backendValue || 'EFortRarity::Common'); // Draw the item background ctx.fillStyle = rarityColors.colorCenter; ctx.fillRect(x, y, itemWidth, itemHeight); // Background for the article ctx.beginPath(); // Upload and draw the image of the item or lot const itemIcon = await loadImage(images); // If it is a loose item ctx.drawImage(itemIcon, x, y, itemWidth, itemHeight); ctx.globalAlpha = 1; ctx.closePath(); ctx.save(); ctx.beginPath(); const overlay = await loadImage(`${__dirname}/../assets/images/fortnite/shop/SmallOverlay.png`); ctx.drawImage(overlay, x, y, itemWidth, itemHeight); ctx.closePath(); ctx.save(); ctx.globalAlpha = 1; // Adjust the item name (lot or article) this.drawItemName(ctx, name, x + 196, y + 310, 375, font); // Adjust the price within the card area await this.drawItemPrice(ctx, finalPrice, x + 192, y + 340, 200, font); currentColumn++; // If we reach the maximum number of columns, move to the next row if (currentColumn >= itemsPerRow) { currentColumn = 0; currentRow++; // If we reach the maximum number of rows, we are done. if (currentRow >= maxRows) break; } } catch (error) { console.error("Error al procesar el ítem:", item, error); // Skip the item that has an error and continue with the next one continue; } } // Footer ctx.font = `50px ${font}`; ctx.fillText(this.textFooter, canvas.width / 2, canvas.height - 50); // Save Image const outputPath = `${__dirname}/../assets/images/fortnite/shop/output.png`; const buffer = canvas.toBuffer("image/png"); fs.writeFileSync(outputPath, buffer); return canvas.toBuffer("image/png"); } // Function to obtain a numerical value according to the rarity of the item getRarityValue(rarity) { const rarityValues = { "EFortRarity::Legendary": 5, "EFortRarity::Epic": 4, "EFortRarity::Rare": 3, "EFortRarity::Uncommon": 2, "EFortRarity::Common": 1 }; return rarityValues[rarity] || 0; // Use 0 by default if not found } // Function to get colors based on item rarity getRarityColors(rarity) { const rarities = { "EFortRarity::Legendary": { colorBorder: "#e98d4b", colorCenter: "#ea8d23" }, "EFortRarity::Epic": { colorBorder: "#e95eff", colorCenter: "#c359ff" }, "EFortRarity::Rare": { colorBorder: "#37d1ff", colorCenter: "#2cc1ff" }, "EFortRarity::Uncommon": { colorBorder: "#87e339", colorCenter: "#69bb1e" }, "EFortRarity::Common": { colorBorder: "#b1b1b1", colorCenter: "#bebebe" } }; return rarities[rarity] || rarities.common; // Use common by default } // Function to split the name into two lines if necessary drawItemName(ctx, text, x, y, maxWidth, font) { ctx.font = `30px ${font}`; ctx.fillStyle = "#ffffff"; const words = text.split(' '); let line = ''; let lineHeight = 30; let lineCount = 0; const maxLines = 2; // Maximum 2 lines for (let i = 0; i < words.length; i++) { const testLine = line + words[i] + ' '; const metrics = ctx.measureText(testLine); const testWidth = metrics.width; if (testWidth > maxWidth && lineCount < maxLines - 1) { ctx.fillText(line, x, y); line = words[i] + ' '; y += lineHeight; lineCount++; } else { line = testLine; } } // Draw the last line ctx.fillText(line, x, y); } // Function to adjust the price within the card async drawItemPrice(ctx, price, x, y, maxWidth, font) { const vbuckIcon = await loadImage(parseSvg(`${__dirname}/../assets/images/fortnite/shop/vBucks.svg`)); const iconSize = 20; // Draw the V-Bucks icon ctx.drawImage(vbuckIcon, x - 40, y + 10, iconSize, iconSize); // Draw the price adjusted to the available width ctx.font = `30px ${font}`; ctx.fillStyle = "#ffffff"; let priceText = `${price}`; const priceWidth = ctx.measureText(priceText).width; // If the price exceeds the available space, adjust the font size if (priceWidth > maxWidth - iconSize - 10) { let fontSize = 30; do { fontSize -= 1; ctx.font = `${fontSize}px ${font}`; } while (ctx.measureText(priceText).width > maxWidth - iconSize - 10); } // Draw the price text ctx.fillText(priceText, x + iconSize, y + 30); } } module.exports = FortniteShop;