cipher-md-api / visuals /index.js
sh4lu-z's picture
docs: update API documentation and initialize new service modules
493b302
Raw
History Blame Contribute Delete
23.8 kB
const express = require('express');
const { Canvas, loadImage } = require('skia-canvas');
const axios = require('axios'); // For Translation
const app = express();
const multer = require('multer'); // เท†เถบเท’เถฝเทŠ เถœเถฑเทŠเถฑ
const { Sticker, StickerTypes } = require('wa-sticker-formatter');
const upload = multer({ storage: multer.memoryStorage() });
app.use(express.json({ limit: '50mb' }));
app.get('/', (req, res) => res.send("๐Ÿš€ Cipher-MD ULTRA-SHARP ENGINE"));
// ============================================================
// ๐ŸŽจ 1. WELCOME IMAGE GENERATOR (Exact Custom Design)
// ============================================================
app.post('/welcome', upload.single('image'), async (req, res) => {
try {
// เท†เทœเถงเท เถ‘เถšเถšเทŠ เถฑเทเถญเทŠเถฑเถธเทŠ เถ‘เถปเถปเทŠ เถ‘เถšเถšเทŠ
if (!req.file) return res.status(400).send("No Profile Pic Uploaded");
// เถถเทœเถงเทŠเถœเท™เถฑเทŠ เถ‘เถฑ เถฉเทšเถงเท (gName = title เท€เท’เถฏเท’เท„เถง เถœเถฑเทŠเถฑเท€เท)
const { title, count, userName } = req.body;
const gName = title || "Group Name"; // เถฑเถธ เท„เท’เทƒเทŠ เถฑเถธเทŠ default เถ‘เถšเถšเทŠ
const size = 1024;
const canvas = new Canvas(size, size);
const ctx = canvas.getContext('2d');
const watermark = "SYNTIOX VISUAL ENGINE";
// Style 1 เท„เท 3 เท€เท’เถญเถปเถบเท’ เถดเทเท€เท’เถ เทŠเถ เท’ เถšเถปเถฑเทŠเถฑเทš
const styleId = Math.random() > 0.5 ? 1 : 3;
// 1. Image Load (Buffer เถ‘เถšเท™เถฑเทŠ เถšเท™เถฝเท’เถฑเทŠเถธ เถœเถฑเทŠเถฑเท€เท)
const img = await loadImage(req.file.buffer);
// 2. Background Blur
ctx.filter = 'blur(50px) brightness(0.5)';
ctx.drawImage(img, -50, -50, size + 100, size + 100);
ctx.filter = 'none';
// 3. Styles Logic (1 & 3)
if (styleId === 1) {
ctx.strokeStyle = 'rgba(0, 242, 255, 0.1)';
for (let i = 0; i < size; i += 60) {
ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, size); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(size, i); ctx.stroke();
}
} else {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
ctx.lineWidth = 1;
for (let i = 0; i < 20; i++) {
ctx.beginPath(); ctx.moveTo(Math.random() * size, Math.random() * size);
ctx.lineTo(Math.random() * size, Math.random() * size); ctx.stroke();
}
}
// 4. Highlight Beam (เถ‹เถนเทš เถšเทเถฉเทŠ เถ‘เถšเทš เถญเท’เถถเทŠเถถ เถ…เถฝเท”เถญเทŠ เถšเท‘เถฝเทŠเถฝ)
ctx.save();
const beamGrad = ctx.createLinearGradient(0, 0, size, size);
beamGrad.addColorStop(0, 'rgba(0, 242, 255, 0)');
beamGrad.addColorStop(0.5, 'rgba(0, 242, 255, 0.15)');
beamGrad.addColorStop(1, 'rgba(0, 242, 255, 0)');
ctx.fillStyle = beamGrad; ctx.rotate(-15 * Math.PI / 180);
ctx.fillRect(-size, size / 3, size * 2, 150); ctx.restore();
// 5. โœ… AVATAR CENTERED (เถดเทœเถงเท เถ‘เถš เท„เถปเท’ เถธเทเถฏเถง)
const centerX = size / 2;
const centerY = size / 2;
const radius = 250;
ctx.save();
ctx.strokeStyle = '#00f2ff'; ctx.lineWidth = 12; ctx.shadowBlur = 40; ctx.shadowColor = '#00f2ff';
ctx.beginPath(); ctx.arc(centerX, centerY, radius + 15, 0, Math.PI * 2); ctx.stroke(); ctx.restore();
ctx.save();
ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.clip();
ctx.drawImage(img, centerX - radius, centerY - radius, radius * 2, radius * 2); ctx.restore();
// 6. โœ… TEXT LAYOUT
ctx.textAlign = 'center';
// "W E L C O M E" Label
ctx.font = 'bold 60px sans-serif';
ctx.strokeStyle = 'rgba(0, 242, 255, 0.8)';
ctx.lineWidth = 3;
ctx.strokeText("W E L C O M E", centerX, centerY + radius + 120);
// Group Name & Count
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.font = '35px sans-serif';
// Emoji Safe Regex เถ‘เถšเถญเทŠ เถฏเทเถฝเท เถญเท’เถบเท™เถฑเทŠเถฑเทš เถ†เถปเถšเทŠเท‚เทเท€เถง
let safeTitle = gName.replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, '');
safeTitle = await translateSinhalaToEnglish(safeTitle);
ctx.fillText(`${safeTitle} โ€ข MEMBER #${count}`, centerX, centerY + radius + 190);
// User Name (if provided)
if (userName) {
let safeUserName = userName.replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, '');
safeUserName = await translateSinhalaToEnglish(safeUserName);
ctx.fillStyle = '#00f2ff';
ctx.font = 'bold 45px sans-serif';
ctx.fillText(safeUserName, centerX, centerY + radius + 250);
}
// Watermark
ctx.globalAlpha = 0.3;
ctx.font = 'bold 25px sans-serif';
ctx.fillStyle = '#ffffff';
ctx.fillText(watermark, centerX, size - 30);
// เถบเทเท€เท“เถธ
const buffer = await canvas.toBuffer('png');
res.set('Content-Type', 'image/png');
res.send(buffer);
} catch (e) {
console.error(e);
res.status(500).send("Welcome Gen Error: " + e.message);
}
});
// ============================================================
// ๐Ÿ–ผ๏ธ 2. WATERMARK (Only for .img gen)
// ============================================================
app.post('/watermark', upload.single('image'), async (req, res) => {
try {
if (!req.file) return res.status(400).send("No image");
const { watermarkText } = req.body;
const finalWatermark = watermarkText || 'CIPHER-MD GEN ENGINE';
const img = await loadImage(req.file.buffer);
const canvas = new Canvas(img.width, img.height);
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// Add Watermark
const fontSize = Math.floor(img.width * 0.02);
ctx.font = `bold ${fontSize}px sans-serif`;
ctx.textAlign = 'right';
ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
ctx.shadowBlur = 5; ctx.shadowOffsetX = 3; ctx.shadowOffsetY = 3;
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.fillText(finalWatermark, img.width - 20, img.height - 20);
res.set('Content-Type', 'image/png');
res.send(await canvas.toBuffer('png'));
} catch (e) { res.status(500).send("WM Error"); }
});
// ============================================================
// ๐Ÿ› ๏ธ HELPER: SINHALA TO ENGLISH TRANSLATION & TRANSLITERATION
// ============================================================
async function translateSinhalaToEnglish(text) {
if (!text) return text;
// Check if it has Sinhala characters
if (!/[\u0D80-\u0DFF]/.test(text)) return text;
try {
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=si&tl=en&dt=t&q=${encodeURIComponent(text)}`;
const res = await axios.get(url);
let translated = res.data[0][0][0];
// Capitalize words
return translated.replace(/\b\w/g, l => l.toUpperCase());
} catch (e) {
console.error("Translation API error:", e.message);
return transliterateSinhala(text);
}
}
const transliterateSinhala = (text) => {
if (!text) return text;
text = text.replace(/\u200D/g, ''); // Remove ZWJ
const vowels = { 'เถ…': 'a', 'เถ†': 'aa', 'เถ‡': 'ae', 'เถˆ': 'aae', 'เถ‰': 'i', 'เถŠ': 'ii', 'เถ‹': 'u', 'เถŒ': 'uu', 'เถ‘': 'e', 'เถ’': 'ee', 'เถ“': 'ai', 'เถ”': 'o', 'เถ•': 'oo', 'เถ–': 'au' };
const consonants = { 'เถš': 'k', 'เถ›': 'kh', 'เถœ': 'g', 'เถ': 'gh', 'เถž': 'ng', 'เถŸ': 'ng', 'เถ ': 'ch', 'เถก': 'chh', 'เถข': 'j', 'เถฃ': 'jh', 'เถค': 'ny', 'เถฅ': 'gn', 'เถฆ': 'ndj', 'เถง': 't', 'เถจ': 'th', 'เถฉ': 'd', 'เถช': 'dh', 'เถซ': 'n', 'เถฌ': 'nd', 'เถญ': 't', 'เถฎ': 'th', 'เถฏ': 'd', 'เถฐ': 'dh', 'เถฑ': 'n', 'เถณ': 'nd', 'เถด': 'p', 'เถต': 'ph', 'เถถ': 'b', 'เถท': 'bh', 'เถธ': 'm', 'เถน': 'mb', 'เถบ': 'y', 'เถป': 'r', 'เถฝ': 'l', 'เท€': 'v', 'เท': 'sh', 'เท‚': 'sh', 'เทƒ': 's', 'เท„': 'h', 'เท…': 'l', 'เท†': 'f' };
const modifiers = { 'เท': 'a', 'เท': 'ae', 'เท‘': 'aae', 'เท’': 'i', 'เท“': 'ii', 'เท”': 'u', 'เท–': 'uu', 'เท˜': 'ru', 'เท™': 'e', 'เทš': 'ee', 'เท›': 'ai', 'เทœ': 'o', 'เท': 'oo', 'เทž': 'au', 'เทŠ': '' };
const specials = { 'เถ‚': 'n', 'เถƒ': 'h', 'เทฒ': 'ruu' };
let res = "";
for (let i = 0; i < text.length; i++) {
let c = text[i];
if (vowels[c]) {
res += vowels[c];
} else if (specials[c]) {
res += specials[c];
} else if (consonants[c]) {
res += consonants[c];
if (i + 1 < text.length && modifiers[text[i + 1]] !== undefined) {
res += modifiers[text[i + 1]];
i++;
} else {
res += "a";
}
} else {
res += c;
}
}
// Capitalize first letter of words to make it look nicer
return res.replace(/\b\w/g, l => l.toUpperCase());
};
// ============================================================
// ๐Ÿ› ๏ธ HELPER: ROUNDED RECTANGLE
// ============================================================
function roundedRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
// ============================================================
// ๐Ÿ“ˆ ENGINE 1: ULTRA-SHARP LINE CHART (Gold, P2P, CSE)
// ============================================================
async function drawLineChart(title, subTitle, history, colorTheme) {
const width = 2400;
const height = 1500;
const canvas = new Canvas(width, height);
const ctx = canvas.getContext('2d');
ctx.scale(2, 2);
// 1. Background (Solid Dark)
ctx.fillStyle = '#131722';
ctx.fillRect(0, 0, width, height);
// ๐Ÿ“ Precise Layout Calculation
// เถบเถง เถ‰เถฉ (Bottom Padding) เท€เทเถฉเท’ เถšเท…เท เถ…เถšเท”เถปเท” เถœเท‘เท€เท™เถฑเทŠเถฑเท™ เถฑเทเถญเท’ เท€เท™เถฑเทŠเถฑ
const chartX = 20;
const chartY = 120; // Title area height
const chartW = 1050;
const chartH = 530; // Chart height reduced slightly to keep bottom clear
// Data Min/Max
const values = history.map(h => (h.price || h.rate || h.value));
const minVal = Math.min(...values) * 0.9995;
const maxVal = Math.max(...values) * 1.0005;
const range = maxVal - minVal;
const getY = (val) => chartY + chartH - ((val - minVal) / range) * chartH;
const getX = (i) => chartX + (i / (values.length - 1)) * chartW;
// 2. GRID LINES (Very Subtle)
ctx.lineWidth = 1;
ctx.strokeStyle = '#2a2e39';
// Horizontal Grids
const rows = 6;
for (let i = 0; i <= rows; i++) {
const y = chartY + (chartH / rows) * i;
ctx.beginPath(); ctx.moveTo(chartX, y); ctx.lineTo(chartX + chartW, y); ctx.stroke();
// Price Label (Sharp Font)
const price = maxVal - (range / rows) * i;
ctx.fillStyle = '#787b86';
ctx.font = '13px Consolas, monospace'; // Monospace for alignment
ctx.textAlign = 'left';
ctx.fillText(price.toLocaleString(undefined, { minimumFractionDigits: 2 }), chartX + chartW + 12, y + 8);
}
// Vertical Grids
const cols = 8;
for (let i = 0; i <= cols; i++) {
const x = chartX + (chartW / cols) * i;
ctx.beginPath(); ctx.moveTo(x, chartY); ctx.lineTo(x, chartY + chartH + 60); ctx.stroke();
}
// 3. THE CHART AREA (Clean Gradient)
ctx.save();
ctx.beginPath();
ctx.moveTo(getX(0), getY(values[0]));
for (let i = 0; i < values.length; i++) ctx.lineTo(getX(i), getY(values[i]));
// Close path to bottom of chart area
ctx.lineTo(getX(values.length - 1), chartY + chartH + 60);
ctx.lineTo(getX(0), chartY + chartH + 60);
ctx.closePath();
// Sharp Gradient (No blurring at bottom)
const gradient = ctx.createLinearGradient(0, chartY, 0, chartY + chartH + 60);
gradient.addColorStop(0, `${colorTheme}55`); // Top: Semi-transparent
gradient.addColorStop(1, `${colorTheme}00`); // Bottom: Completely transparent
ctx.fillStyle = gradient;
ctx.fill();
ctx.restore();
// 4. THE MAIN LINE (Thin & Sharp)
ctx.beginPath();
ctx.lineWidth = 3; // Thinner line for "Pro" look (was 5)
ctx.strokeStyle = colorTheme;
ctx.lineJoin = 'round';
// Removed shadowBlur here to avoid "smudge" look
for (let i = 0; i < values.length; i++) {
if (i === 0) ctx.moveTo(getX(i), getY(values[i]));
else ctx.lineTo(getX(i), getY(values[i]));
}
ctx.stroke();
// 5. CURRENT PRICE BADGE (Right Axis)
const lastVal = values[values.length - 1];
const lastY = getY(lastVal);
// Dotted line to axis
ctx.setLineDash([2, 2]);
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(getX(values.length - 1), lastY); ctx.lineTo(chartX + chartW, lastY); ctx.stroke();
ctx.setLineDash([]);
// Badge
ctx.fillStyle = colorTheme;
roundedRect(ctx, chartX + chartW + 5, lastY - 11, 115, 22, 3);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 13px Consolas';
ctx.textAlign = 'left';
ctx.fillText(lastVal.toLocaleString(undefined, { minimumFractionDigits: 2 }), chartX + chartW + 10, lastY + 5);
// Pulsing Dot (Sharp)
const lastX = getX(values.length - 1);
ctx.beginPath(); ctx.arc(lastX, lastY, 5, 0, Math.PI * 2); ctx.fillStyle = '#ffffff'; ctx.fill();
ctx.beginPath(); ctx.arc(lastX, lastY, 12, 0, Math.PI * 2); ctx.strokeStyle = `${colorTheme}66`; ctx.lineWidth = 2; ctx.stroke();
// 6. HEADER (Aligned)
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 42px sans-serif'; ctx.textAlign = 'left';
ctx.fillText(title, 30, 60);
ctx.fillStyle = colorTheme; ctx.font = 'bold 32px sans-serif';
ctx.fillText(subTitle, 30, 100);
// 7. WATERMARK (Safe Distance)
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; ctx.font = '18px sans-serif'; ctx.textAlign = 'right';
ctx.fillText('โšก Cipher-MD Analytics (4K)', (width / 2) - 20, (height / 2) - 20);
return canvas.toBuffer('png');
}
// ============================================================
// ๐Ÿ•ฏ๏ธ ENGINE 2: CANDLESTICK CHART (Fixed Sharpness)
// ============================================================
app.post('/chart/candle', async (req, res) => {
try {
const { candles, symbol, change, currentPrice } = req.body;
const width = 2400;
const height = 1500;
const canvas = new Canvas(width, height);
const ctx = canvas.getContext('2d');
ctx.scale(2, 2);
ctx.fillStyle = '#131722'; ctx.fillRect(0, 0, width, height);
const chartX = 20;
const chartY = 60;
const chartW = 1050;
const chartH = 530;
const minPrice = Math.min(...candles.map(c => c.low));
const maxPrice = Math.max(...candles.map(c => c.high));
const range = maxPrice - minPrice;
const maxVol = Math.max(...candles.map(c => c.volume));
const getY = (p) => chartY + chartH - ((p - minPrice) / range) * chartH;
// Grid
ctx.lineWidth = 1; ctx.strokeStyle = '#2a2e39';
const rows = 8;
for (let i = 0; i <= rows; i++) {
const y = chartY + (chartH / rows) * i;
ctx.beginPath(); ctx.moveTo(chartX, y); ctx.lineTo(chartX + chartW, y); ctx.stroke();
const p = maxPrice - (range / rows) * i;
ctx.fillStyle = '#787b86'; ctx.textAlign = 'left'; ctx.font = '13px Consolas';
ctx.fillText(p.toFixed(2), chartX + chartW + 10, y + 4);
}
const candleW = (chartW / candles.length) * 0.7;
const spacing = chartW / candles.length;
candles.forEach((c, i) => {
const x = chartX + (i * spacing) + (spacing / 2);
const isUp = c.close >= c.open;
const color = isUp ? '#089981' : '#f23645';
// Volume (Sharp, low opacity)
const volH = (c.volume / maxVol) * 120;
ctx.fillStyle = isUp ? 'rgba(8, 153, 129, 0.2)' : 'rgba(242, 54, 69, 0.2)';
ctx.fillRect(x - candleW / 2, chartY + chartH - volH + 150, candleW, volH);
// Wick (Thin line)
ctx.strokeStyle = color; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(x, getY(c.high)); ctx.lineTo(x, getY(c.low)); ctx.stroke();
// Body
ctx.fillStyle = color;
const yOpen = getY(c.open);
const yClose = getY(c.close);
ctx.fillRect(x - candleW / 2, Math.min(yOpen, yClose), candleW, Math.max(Math.abs(yOpen - yClose), 1));
});
// Price Badge
const lastC = candles[candles.length - 1];
const lastY = getY(lastC.close);
const lastColor = lastC.close >= lastC.open ? '#089981' : '#f23645';
ctx.strokeStyle = '#ffffff'; ctx.setLineDash([2, 2]); ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, lastY); ctx.lineTo(chartX + chartW, lastY); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = lastColor;
roundedRect(ctx, chartX + chartW + 5, lastY - 12, 115, 24, 3); ctx.fill();
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 13px Consolas';
ctx.fillText(parseFloat(currentPrice).toFixed(2), chartX + chartW + 10, lastY + 5);
// Header
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 42px sans-serif'; ctx.textAlign = 'left';
ctx.fillText(`${symbol} / USDT`, 30, 60);
ctx.fillStyle = parseFloat(change) >= 0 ? '#089981' : '#f23645'; ctx.font = 'bold 32px sans-serif';
ctx.fillText(`${change}%`, 30, 100);
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; ctx.font = '18px sans-serif'; ctx.textAlign = 'right';
ctx.fillText('โšก Cipher-MD Analytics', (width / 2) - 20, (height / 2) - 20);
res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png'));
} catch (e) { res.status(500).send(e.message); }
});
// ============================================================
// ๐Ÿ’Ž ENGINE 3: NFT & ARB (Retained Fixed Version)
// ============================================================
app.post('/chart/nft', async (req, res) => {
try {
const { data } = req.body;
const canvas = new Canvas(800, 400); const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0a0b0d'; roundedRect(ctx, 0, 0, 800, 400, 25); ctx.fill();
if (data.image_url) {
try {
const img = await loadImage(data.image_url);
ctx.save(); ctx.beginPath(); roundedRect(ctx, 40, 40, 200, 200, 20); ctx.clip();
ctx.drawImage(img, 40, 40, 200, 200); ctx.restore();
} catch (e) { }
}
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 36px sans-serif';
ctx.fillText(data.name.substring(0, 18), 270, 70);
ctx.fillStyle = '#8a939b'; ctx.font = '24px sans-serif';
ctx.fillText(`Chain: ${data.chain.toUpperCase()}`, 270, 110);
ctx.fillStyle = '#2081e2'; ctx.font = 'bold 55px sans-serif';
ctx.fillText(`${data.floor_price} ${data.floor_price_symbol}`, 270, 190);
ctx.fillStyle = '#ffffff'; ctx.font = '18px sans-serif';
ctx.fillText('FLOOR PRICE', 270, 220);
ctx.fillStyle = '#161b22'; roundedRect(ctx, 270, 260, 480, 80, 15); ctx.fill();
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 20px sans-serif';
ctx.textAlign = 'left'; ctx.fillText(`Vol: ${Math.round(data.total_volume)}`, 290, 308);
ctx.textAlign = 'right'; ctx.fillText(`Owners: ${data.owner_count}`, 730, 308);
ctx.textAlign = 'right'; ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.fillText('Cipher-MD NFT', 750, 380);
res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png'));
} catch (e) { res.status(500).send(e.message); }
});
app.post('/chart/arb', async (req, res) => {
try {
const { coin, exData } = req.body;
const canvas = new Canvas(900, 600); const ctx = canvas.getContext('2d');
ctx.fillStyle = '#131722'; ctx.fillRect(0, 0, 900, 600);
ctx.fillStyle = '#ffffff'; ctx.font = 'bold 40px sans-serif';
ctx.fillText(`${coin} Arbitrage Monitor`, 40, 60);
let y = 120;
const avg = exData.reduce((a, b) => a + b.price, 0) / exData.length;
exData.forEach(ex => {
const diff = (((ex.price - avg) / avg) * 100);
const color = diff >= 0 ? '#089981' : '#f23645';
ctx.fillStyle = '#1e222d'; roundedRect(ctx, 40, y, 820, 70, 10); ctx.fill();
ctx.fillStyle = '#b2b5be'; ctx.textAlign = 'left'; ctx.font = 'bold 24px sans-serif';
ctx.fillText(ex.name, 70, y + 45);
ctx.fillStyle = '#ffffff'; ctx.textAlign = 'right';
ctx.fillText(`$${ex.price.toLocaleString()}`, 600, y + 45);
ctx.fillStyle = color;
ctx.fillText(`${diff > 0 ? '+' : ''}${diff.toFixed(2)}%`, 830, y + 45);
y += 85;
});
res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png'));
} catch (e) { res.status(500).send(e.message); }
});
// ============================================================
// โšก ROUTERS
// ============================================================
app.post('/chart/p2p', async (req, res) => {
try {
const { history, currentRate } = req.body;
const buf = await drawLineChart("USDT / LKR (P2P)", `Rs. ${currentRate}`, history, '#2962ff');
res.set('Content-Type', 'image/png'); res.send(buf);
} catch (e) { res.status(500).send(e.message); }
});
app.post('/chart/forex', async (req, res) => {
try {
const { history, currentRate } = req.body;
const buf = await drawLineChart("USD / LKR (Official)", `Rs. ${currentRate}`, history, '#089981');
res.set('Content-Type', 'image/png'); res.send(buf);
} catch (e) { res.status(500).send(e.message); }
});
app.post('/chart/gold', async (req, res) => {
try {
const { history, currentRate } = req.body;
const buf = await drawLineChart("Gold (24K)", `Rs. ${currentRate}`, history, '#f2a900');
res.set('Content-Type', 'image/png'); res.send(buf);
} catch (e) { res.status(500).send(e.message); }
});
app.post('/chart/cse', async (req, res) => {
try {
const { history, currentRate } = req.body;
const buf = await drawLineChart("CSE ASPI", `${currentRate}`, history, '#a300cc');
res.set('Content-Type', 'image/png'); res.send(buf);
} catch (e) { res.status(500).send(e.message); }
});
app.post('/chart/fear', async (req, res) => {
try {
const { history } = req.body;
const last = history[history.length - 1].value;
const color = last > 50 ? '#089981' : '#f23645';
const buf = await drawLineChart("Fear & Greed", `${last}/100`, history, color);
res.set('Content-Type', 'image/png'); res.send(buf);
} catch (e) { res.status(500).send(e.message); }
});
const port = 7861;
app.listen(port, () => console.log(`๐Ÿš€ ULTRA-SHARP ENGINE running on port ${port}`));