first commit
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
// Generates abstract dark "product shot" SVGs for the portfolio gallery.
|
||||
// Run once: `node scripts/gen-portfolio-art.mjs`. Output -> public/portfolio/<id>/*.svg
|
||||
// These are tasteful placeholders; the admin panel can upload real screenshots later.
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const OUT = join(process.cwd(), 'public', 'portfolio');
|
||||
|
||||
// id -> base hue (matches the accent assigned in the dictionary)
|
||||
const PROJECTS = {
|
||||
'atlas-rag': 199, // electric
|
||||
'sentinel-agents': 245, // violet
|
||||
'vertex-vision': 187, // cyan
|
||||
'mirage-mobile': 292, // magenta
|
||||
'flux-stream': 158, // emerald
|
||||
'oracle-forecast': 205, // electric-2
|
||||
};
|
||||
|
||||
const W = 1600;
|
||||
const H = 1000;
|
||||
|
||||
const defs = (hue, id) => `
|
||||
<defs>
|
||||
<radialGradient id="glow${id}" cx="28%" cy="18%" r="90%">
|
||||
<stop offset="0%" stop-color="hsl(${hue} 90% 62% / 0.55)"/>
|
||||
<stop offset="45%" stop-color="hsl(${(hue + 28) % 360} 85% 55% / 0.18)"/>
|
||||
<stop offset="100%" stop-color="hsl(${hue} 60% 8% / 0)"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="bg${id}" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#04060f"/>
|
||||
<stop offset="100%" stop-color="#020308"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="line${id}" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="hsl(${hue} 90% 65%)"/>
|
||||
<stop offset="100%" stop-color="hsl(${(hue + 40) % 360} 90% 68%)"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" fill="url(#bg${id})"/>
|
||||
<rect width="${W}" height="${H}" fill="url(#glow${id})"/>
|
||||
<g stroke="hsl(${hue} 40% 60% / 0.06)" stroke-width="1">
|
||||
${Array.from({ length: 16 }, (_, i) => `<line x1="${i * 100}" y1="0" x2="${i * 100}" y2="${H}"/>`).join('')}
|
||||
${Array.from({ length: 10 }, (_, i) => `<line x1="0" y1="${i * 100}" x2="${W}" y2="${i * 100}"/>`).join('')}
|
||||
</g>`;
|
||||
|
||||
const chrome = (hue, id) => `
|
||||
<g>
|
||||
<rect x="80" y="70" width="${W - 160}" height="60" rx="14" fill="hsl(${hue} 30% 18% / 0.4)" stroke="hsl(${hue} 60% 60% / 0.25)"/>
|
||||
<circle cx="120" cy="100" r="9" fill="hsl(0 70% 60% / .7)"/>
|
||||
<circle cx="152" cy="100" r="9" fill="hsl(45 80% 60% / .7)"/>
|
||||
<circle cx="184" cy="100" r="9" fill="hsl(140 60% 55% / .7)"/>
|
||||
<rect x="240" y="88" width="420" height="24" rx="12" fill="hsl(${hue} 30% 40% / 0.25)"/>
|
||||
</g>`;
|
||||
|
||||
function dashboard(hue, id) {
|
||||
const bars = Array.from({ length: 9 }, (_, i) => {
|
||||
const bh = 80 + ((i * 53) % 260);
|
||||
return `<rect x="${180 + i * 96}" y="${760 - bh}" width="54" height="${bh}" rx="8" fill="url(#line${id})" opacity="${0.5 + (i % 3) * 0.16}"/>`;
|
||||
}).join('');
|
||||
const path = `M 1050 620 ${Array.from({ length: 9 }, (_, i) => `L ${1050 + i * 54} ${620 - Math.sin(i / 1.4) * 120 - i * 6}`).join(' ')}`;
|
||||
return `${defs(hue, id)}${chrome(hue, id)}
|
||||
<rect x="120" y="200" width="700" height="560" rx="20" fill="hsl(${hue} 30% 12% / 0.5)" stroke="hsl(${hue} 60% 60% / 0.16)"/>
|
||||
<text x="160" y="270" font-family="monospace" font-size="30" fill="hsl(${hue} 70% 75%)">throughput / day</text>
|
||||
${bars}
|
||||
<rect x="880" y="200" width="600" height="560" rx="20" fill="hsl(${hue} 30% 12% / 0.5)" stroke="hsl(${hue} 60% 60% / 0.16)"/>
|
||||
<path d="${path}" fill="none" stroke="url(#line${id})" stroke-width="6" stroke-linecap="round"/>
|
||||
<circle cx="1482" cy="${620 - Math.sin(8 / 1.4) * 120 - 48}" r="12" fill="hsl(${hue} 90% 70%)"/>
|
||||
<text x="920" y="270" font-family="monospace" font-size="30" fill="hsl(${hue} 70% 75%)">latency p95</text>`;
|
||||
}
|
||||
|
||||
function flow(hue, id) {
|
||||
const node = (x, y, label) =>
|
||||
`<g><rect x="${x}" y="${y}" width="230" height="110" rx="18" fill="hsl(${hue} 35% 14% / 0.7)" stroke="hsl(${hue} 70% 62% / 0.5)"/><text x="${x + 115}" y="${y + 62}" text-anchor="middle" font-family="monospace" font-size="26" fill="hsl(${hue} 70% 80%)">${label}</text></g>`;
|
||||
const edge = (x1, y1, x2, y2) =>
|
||||
`<path d="M ${x1} ${y1} C ${(x1 + x2) / 2} ${y1}, ${(x1 + x2) / 2} ${y2}, ${x2} ${y2}" fill="none" stroke="url(#line${id})" stroke-width="4" opacity="0.7"/>`;
|
||||
return `${defs(hue, id)}${chrome(hue, id)}
|
||||
${edge(350, 305, 590, 215)}${edge(350, 305, 590, 405)}${edge(820, 215, 1060, 305)}${edge(820, 405, 1060, 305)}${edge(1290, 305, 1290, 305)}
|
||||
${node(120, 250, 'ingest')}
|
||||
${node(590, 160, 'embed')}
|
||||
${node(590, 350, 'retrieve')}
|
||||
${node(1060, 250, 'rerank')}
|
||||
${node(1300, 250, 'generate')}
|
||||
<g opacity="0.5">${Array.from({ length: 6 }, (_, i) => `<circle cx="${260 + i * 200}" cy="700" r="6" fill="hsl(${hue} 90% 70%)"/>`).join('')}</g>`;
|
||||
}
|
||||
|
||||
function mobile(hue, id) {
|
||||
return `${defs(hue, id)}
|
||||
<g transform="translate(560 120)">
|
||||
<rect width="480" height="780" rx="56" fill="hsl(${hue} 30% 10% / 0.9)" stroke="hsl(${hue} 60% 60% / 0.35)" stroke-width="3"/>
|
||||
<rect x="22" y="22" width="436" height="736" rx="40" fill="#04060f"/>
|
||||
<rect x="180" y="40" width="120" height="22" rx="11" fill="hsl(${hue} 30% 25%)"/>
|
||||
<circle cx="240" cy="230" r="86" fill="none" stroke="url(#line${id})" stroke-width="10"/>
|
||||
<circle cx="240" cy="230" r="56" fill="hsl(${hue} 80% 60% / 0.18)"/>
|
||||
${Array.from({ length: 4 }, (_, i) => `<rect x="70" y="${380 + i * 80}" width="${340 - i * 40}" height="46" rx="14" fill="hsl(${hue} 35% 18% / 0.8)"/>`).join('')}
|
||||
<rect x="70" y="700" width="340" height="56" rx="18" fill="url(#line${id})"/>
|
||||
</g>`;
|
||||
}
|
||||
|
||||
const SHOTS = [dashboard, flow, mobile];
|
||||
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
for (const [id, hue] of Object.entries(PROJECTS)) {
|
||||
const dir = join(OUT, id);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
// cover = dashboard variant; gallery = all three variants
|
||||
const files = {
|
||||
'cover.svg': dashboard(hue, id.replace(/\W/g, '')),
|
||||
'01.svg': dashboard(hue, id.replace(/\W/g, '') + 'a'),
|
||||
'02.svg': flow(hue, id.replace(/\W/g, '') + 'b'),
|
||||
'03.svg': mobile(hue, id.replace(/\W/g, '') + 'c'),
|
||||
};
|
||||
for (const [name, inner] of Object.entries(files)) {
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${inner}</svg>`;
|
||||
writeFileSync(join(dir, name), svg);
|
||||
}
|
||||
console.log('wrote', id);
|
||||
}
|
||||
console.log('done');
|
||||
Reference in New Issue
Block a user