Add web-rendered portfolio dossier
This commit is contained in:
parent
4f85257e07
commit
131921f97a
4 changed files with 1616 additions and 0 deletions
1150
GameCult/Portfolio-Dossier.md
Normal file
1150
GameCult/Portfolio-Dossier.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -24,6 +24,8 @@ sidebarGroups:
|
|||
slug: Docs/Bifrost
|
||||
- label: Portfolio Pitch
|
||||
slug: Pitch
|
||||
- label: Portfolio Dossier
|
||||
slug: Portfolio-Dossier
|
||||
- title: Projects
|
||||
links:
|
||||
- label: Aetheria
|
||||
|
|
|
|||
252
scripts/render-portfolio-dossier.mjs
Normal file
252
scripts/render-portfolio-dossier.mjs
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const root = process.cwd();
|
||||
const dossierRoot = path.join(root, "docs", "portfolio-dossier");
|
||||
const outputPath = path.join(root, "GameCult", "Portfolio-Dossier.md");
|
||||
|
||||
const figureMap = {
|
||||
"figures/moonshot-trajectory.pdf": "/static/interactive/portfolio-pitch/figures/moonshot-trajectory.svg",
|
||||
"figures/value-orbit-map.pdf": "/static/interactive/portfolio-pitch/figures/value-orbit-map.svg",
|
||||
"figures/patronage-furnace.pdf": "/static/interactive/portfolio-pitch/figures/patronage-furnace.svg",
|
||||
"figures/surface-web-stack.pdf": "/static/interactive/portfolio-pitch/figures/surface-web-stack.svg",
|
||||
};
|
||||
|
||||
function readTex(relativePath) {
|
||||
return fs.readFileSync(path.join(dossierRoot, `${relativePath}.tex`), "utf8");
|
||||
}
|
||||
|
||||
function expandInputs(tex, currentDir = "") {
|
||||
return tex.replace(/\\input\{([^}]+)\}/g, (_, inputPath) => {
|
||||
if (inputPath === "preamble") return "";
|
||||
if (inputPath === "figures/surface-web-stack") return "\n\\SurfaceWebStack\n";
|
||||
if (inputPath === "figures/value-furnace") return "\n\\ValueFurnace\n";
|
||||
const relative = path.normalize(path.join(currentDir, inputPath)).replace(/\\/g, "/");
|
||||
const file = path.join(dossierRoot, `${relative}.tex`);
|
||||
if (!fs.existsSync(file)) return "";
|
||||
return `\n${expandInputs(fs.readFileSync(file, "utf8"), path.dirname(relative))}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
function readBraceArg(text, start) {
|
||||
while (text[start] && /\s/.test(text[start])) start += 1;
|
||||
if (text[start] !== "{") return null;
|
||||
let depth = 0;
|
||||
for (let index = start; index < text.length; index += 1) {
|
||||
const char = text[index];
|
||||
if (char === "\\" && text[index + 1]) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (char === "{") depth += 1;
|
||||
if (char === "}") depth -= 1;
|
||||
if (depth === 0) {
|
||||
return {
|
||||
value: text.slice(start + 1, index),
|
||||
end: index + 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function replaceCommand(text, command, replacer) {
|
||||
let output = "";
|
||||
let cursor = 0;
|
||||
const token = `\\${command}`;
|
||||
while (cursor < text.length) {
|
||||
const index = text.indexOf(token, cursor);
|
||||
if (index === -1) {
|
||||
output += text.slice(cursor);
|
||||
break;
|
||||
}
|
||||
|
||||
output += text.slice(cursor, index);
|
||||
let next = index + token.length;
|
||||
const args = [];
|
||||
while (true) {
|
||||
const arg = readBraceArg(text, next);
|
||||
if (!arg) break;
|
||||
args.push(arg.value);
|
||||
next = arg.end;
|
||||
}
|
||||
output += replacer(args);
|
||||
cursor = next;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function stripInline(tex) {
|
||||
return tex
|
||||
.replace(/\\textbf\{([^{}]+)\}/g, "**$1**")
|
||||
.replace(/\\texttt\{([^{}]+)\}/g, "`$1`")
|
||||
.replace(/\\color\{[^}]+\}/g, "")
|
||||
.replace(/\\Large/g, "")
|
||||
.replace(/\\\\\[[^\]]*\]/g, "\n")
|
||||
.replace(/\\\\/g, "\n")
|
||||
.replace(/\\_/g, "_")
|
||||
.replace(/\\%/g, "%")
|
||||
.replace(/[ \t]+\n/g, "\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function callout(label, body, kind = "orange") {
|
||||
return `<aside class="portfolio-dossier-callout ${kind}">\n<strong>${label}</strong>\n\n${stripInline(body)}\n</aside>`;
|
||||
}
|
||||
|
||||
function projectionTable(rows) {
|
||||
const body = rows
|
||||
.map(([label, value]) => `<tr><th>${label}</th><td>${stripInline(value)}</td></tr>`)
|
||||
.join("\n");
|
||||
return `<table class="portfolio-dossier-table">\n<tbody>\n${body}\n</tbody>\n</table>`;
|
||||
}
|
||||
|
||||
function figure(src, caption) {
|
||||
const image = figureMap[src] || src;
|
||||
return `<figure class="portfolio-dossier-figure">\n<img src="${image}" alt="${stripInline(caption)}" />\n<figcaption>${stripInline(caption)}</figcaption>\n</figure>`;
|
||||
}
|
||||
|
||||
function convertSpecialBlocks(tex) {
|
||||
return tex
|
||||
.replace(
|
||||
/\\begin\{center\}\s*\\fcolorbox\{voidorange\}\{voidpanel\}\{\\parbox\{[^}]+\}\{\\color\{voidtext\}\\Large\s*([\s\S]*?)\}\}\s*\\end\{center\}/g,
|
||||
(_, body) => callout("Public spell", body, "orange"),
|
||||
)
|
||||
.replace(
|
||||
/\\begin\{center\}\s*\\fcolorbox\{voidorange\}\{voidpanel\}\{\\parbox\{[^}]+\}\{\\color\{voidtext\}\s*([\s\S]*?)\}\}\s*\\end\{center\}/g,
|
||||
(_, body) => callout("Pipeline", body, "orange"),
|
||||
)
|
||||
.replace(
|
||||
/\\begin\{center\}\s*\\fcolorbox\{voidblue\}\{voidpanel\}\{\\parbox\{[^}]+\}\{\\color\{voidtext\}\s*([\s\S]*?)\}\}\s*\\end\{center\}/g,
|
||||
(_, body) => callout("Surface contract", body, "blue"),
|
||||
);
|
||||
}
|
||||
|
||||
function convertFigureTables(tex) {
|
||||
return tex
|
||||
.replace(
|
||||
/\\SurfaceWebStack/g,
|
||||
`<section class="portfolio-dossier-card">\n<h4>Eve Surface-Web Stack</h4>\n${projectionTable([
|
||||
["Provider", "Owns state, permissions, commands, and acceptance."],
|
||||
["CultUI Tree", "Declares semantic controls, bindings, layout intent, and provenance."],
|
||||
["Eve", "Projects one surface contract into web, desktop, mobile, game UI, and overlays."],
|
||||
["Renderer", "Chooses native affordances without stealing authority from the provider."],
|
||||
])}\n</section>`,
|
||||
)
|
||||
.replace(
|
||||
/\\ValueFurnace/g,
|
||||
`<section class="portfolio-dossier-card">\n<h4>Value Furnace</h4>\n${projectionTable([
|
||||
["Input", "Patronage, game spend, verified work, support, and risk."],
|
||||
["Normalization", "Project-local rules turn events into effective points."],
|
||||
["Governance", "Log-power voting keeps power legible without pretending all support is equal."],
|
||||
["Output", "Priority, reward pressure, credit, receipts, and scoped voting weight."],
|
||||
])}\n</section>`,
|
||||
);
|
||||
}
|
||||
|
||||
function convertTexToMarkdown(tex) {
|
||||
let text = tex
|
||||
.replace(/\\documentclass[\s\S]*?\\begin\{document\}/, "")
|
||||
.replace(/\\PortfolioTitlePage/g, "")
|
||||
.replace(/\\tableofcontents/g, "")
|
||||
.replace(/\\clearpage/g, "")
|
||||
.replace(/\\appendix/g, "\n## Appendix\n")
|
||||
.replace(/\\end\{document\}/g, "");
|
||||
|
||||
text = convertFigureTables(convertSpecialBlocks(text));
|
||||
|
||||
text = replaceCommand(text, "ProjectHeader", ([name, domain, thesis, room]) => {
|
||||
return `\n## ${stripInline(name)}\n\n<section class="portfolio-dossier-project-meta">\n${projectionTable([
|
||||
["Domain", domain],
|
||||
["Thesis", thesis],
|
||||
["Homepage Room", room],
|
||||
])}\n</section>\n`;
|
||||
});
|
||||
text = replaceCommand(text, "ProjectionBand", ([base, sharp, mythic, failure]) => {
|
||||
return `\n<section class="portfolio-dossier-card projection-band">\n<h4>Projection Band</h4>\n${projectionTable([
|
||||
["Base Case", base],
|
||||
["Sharp Case", sharp],
|
||||
["Mythic Case", mythic],
|
||||
["Failure Mode", failure],
|
||||
])}\n</section>\n`;
|
||||
});
|
||||
text = replaceCommand(text, "ValueLadder", ([survival, product, platform, worldWeb]) => {
|
||||
return `\n<section class="portfolio-dossier-card value-ladder">\n<h4>Value Projection Ladder</h4>\n${projectionTable([
|
||||
["Survival", survival],
|
||||
["Product", product],
|
||||
["Platform", platform],
|
||||
["World-Web", worldWeb],
|
||||
])}\n</section>\n`;
|
||||
});
|
||||
text = replaceCommand(text, "MoonGraphic", ([, src, caption]) => `\n${figure(src, caption)}\n`);
|
||||
text = replaceCommand(text, "Verdict", ([body]) => `\n${callout("Void says", body, "orange")}\n`);
|
||||
text = replaceCommand(text, "Term", ([body]) => `<span class="portfolio-term">${stripInline(body)}</span>`);
|
||||
text = replaceCommand(text, "Signal", ([body]) => `<span class="portfolio-signal">${stripInline(body)}</span>`);
|
||||
|
||||
text = text
|
||||
.replace(/\\chapter\{([^}]+)\}/g, "\n## $1\n")
|
||||
.replace(/\\section\{([^}]+)\}/g, "\n### $1\n")
|
||||
.replace(/\\subsection\{([^}]+)\}/g, "\n#### $1\n")
|
||||
.replace(/\\begin\{itemize\}/g, "")
|
||||
.replace(/\\end\{itemize\}/g, "")
|
||||
.replace(/^\s*\\item\s+/gm, "- ")
|
||||
.replace(/\\begin\{center\}|\\end\{center\}/g, "")
|
||||
.replace(/\\begin\{tabularx\}[\s\S]*?\\end\{tabularx\}/g, "")
|
||||
.replace(/\\begin\{figure\}[\s\S]*?\\end\{figure\}/g, "")
|
||||
.replace(/\\noindent/g, "")
|
||||
.replace(/\\vspace\{[^}]+\}/g, "")
|
||||
.replace(/\\href\{([^}]+)\}\{([^}]+)\}/g, "[$2]($1)")
|
||||
.replace(/\\\[/g, () => "$$")
|
||||
.replace(/\\\]/g, () => "$$")
|
||||
.replace(/\\rightarrow/g, "\\to")
|
||||
.replace(/\\max/g, "\\max")
|
||||
.replace(/\\_/g, "_")
|
||||
.replace(/\\%/g, "%")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
const expanded = expandInputs(readTex("main"));
|
||||
const body = convertTexToMarkdown(expanded);
|
||||
const generatedAt = new Date().toISOString().slice(0, 10);
|
||||
|
||||
const page = `---
|
||||
title: Portfolio Dossier
|
||||
description: "GameCult's over-the-top TeX portfolio dossier rendered as a dark-mode web page with Montserrat and Ubuntu branding."
|
||||
enableToc: true
|
||||
cssclasses:
|
||||
- portfolio-dossier-page
|
||||
sidebarGroups:
|
||||
- title: Studio
|
||||
links:
|
||||
- label: Visual Novel Home
|
||||
slug: index
|
||||
- label: Portfolio Pitch
|
||||
slug: Pitch
|
||||
- label: Portfolio Dossier
|
||||
slug: Portfolio-Dossier
|
||||
- label: Project Atlas
|
||||
slug: Projects
|
||||
- label: Graph
|
||||
slug: Graph
|
||||
---
|
||||
|
||||
<section class="portfolio-dossier-hero">
|
||||
<p class="portfolio-dossier-kicker">GameCult / Void Dossier</p>
|
||||
<h1>Portfolio Dossier</h1>
|
||||
<p>Eve, Bifrost, CultMesh, Sai, and the surface-web portfolio. Same TeX source, rendered for humans who would rather not receive a white PDF brick to the face.</p>
|
||||
<div class="portfolio-dossier-actions">
|
||||
<a href="/static/interactive/portfolio-pitch/figures/moonshot-trajectory.svg">Moonshot graphic</a>
|
||||
<a href="/Pitch">Play the VN pitch</a>
|
||||
<a href="https://github.com/GameCult">GameCult GitHub</a>
|
||||
</div>
|
||||
<p class="portfolio-dossier-source">Generated from <code>docs/portfolio-dossier/main.tex</code> on ${generatedAt}.</p>
|
||||
</section>
|
||||
|
||||
${body}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(outputPath, page, "utf8");
|
||||
console.log(`Wrote ${path.relative(root, outputPath)}`);
|
||||
|
|
@ -1845,3 +1845,215 @@ body[data-slug="Pitch"] .gamecult-pitch-vn .sai-speaker-stage.has-dom-cards {
|
|||
min-height: 520px;
|
||||
}
|
||||
}
|
||||
|
||||
.portfolio-dossier-page {
|
||||
--dossier-panel: rgba(8, 13, 21, 0.9);
|
||||
--dossier-panel-strong: rgba(11, 17, 26, 0.96);
|
||||
--dossier-border: rgba(89, 183, 255, 0.18);
|
||||
--dossier-orange: #ff8a2a;
|
||||
--dossier-blue: #59b7ff;
|
||||
|
||||
article {
|
||||
font-family: "Ubuntu", var(--bodyFont), sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
article h1,
|
||||
article h2,
|
||||
article h3,
|
||||
article h4 {
|
||||
font-family: "Montserrat", var(--headerFont), sans-serif;
|
||||
font-weight: 100;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
article h2 {
|
||||
margin-top: 3.2rem;
|
||||
padding-bottom: 0.45rem;
|
||||
border-bottom: 1px solid rgba(89, 183, 255, 0.14);
|
||||
color: var(--dossier-blue);
|
||||
}
|
||||
|
||||
article h3 {
|
||||
color: var(--dossier-orange);
|
||||
}
|
||||
|
||||
article h4 {
|
||||
color: var(--dossier-blue);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
article p,
|
||||
article li,
|
||||
article td {
|
||||
line-height: 1.68;
|
||||
}
|
||||
}
|
||||
|
||||
.portfolio-dossier-hero {
|
||||
position: relative;
|
||||
margin: 0 0 2.4rem;
|
||||
padding: clamp(1.4rem, 4vw, 3.6rem);
|
||||
border: 1px solid rgba(89, 183, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(rgba(5, 7, 11, 0.84), rgba(5, 7, 11, 0.92)),
|
||||
url("/static/interactive/portfolio-pitch/figures/moonshot-trajectory.svg") center / cover;
|
||||
box-shadow:
|
||||
0 28px 70px rgba(0, 0, 0, 0.34),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.portfolio-dossier-kicker,
|
||||
.portfolio-dossier-source {
|
||||
margin: 0;
|
||||
color: rgba(183, 199, 217, 0.78);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.portfolio-dossier-hero h1 {
|
||||
margin: 0.25rem 0 0.85rem;
|
||||
color: #edf6ff;
|
||||
font-size: clamp(2.4rem, 8vw, 5.8rem);
|
||||
font-weight: 100 !important;
|
||||
line-height: 0.95;
|
||||
}
|
||||
|
||||
.portfolio-dossier-hero p:not(.portfolio-dossier-kicker, .portfolio-dossier-source) {
|
||||
max-width: 48rem;
|
||||
color: #edf6ff;
|
||||
font-size: clamp(1rem, 1.6vw, 1.28rem);
|
||||
}
|
||||
|
||||
.portfolio-dossier-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin: 1.4rem 0 0.9rem;
|
||||
}
|
||||
|
||||
.portfolio-dossier-actions a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 2.35rem;
|
||||
padding: 0.48rem 0.78rem;
|
||||
border: 1px solid rgba(255, 138, 42, 0.36);
|
||||
border-radius: 6px;
|
||||
background: rgba(5, 7, 11, 0.72);
|
||||
color: #ffd6ad;
|
||||
font-family: "Ubuntu", var(--bodyFont), sans-serif;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.portfolio-dossier-callout,
|
||||
.portfolio-dossier-card,
|
||||
.portfolio-dossier-project-meta,
|
||||
.portfolio-dossier-figure {
|
||||
margin: 1.25rem 0;
|
||||
border: 1px solid var(--dossier-border);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(135deg, var(--dossier-panel-strong), var(--dossier-panel)),
|
||||
radial-gradient(circle at 100% 0%, rgba(255, 138, 42, 0.12), transparent 32%);
|
||||
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.portfolio-dossier-callout {
|
||||
padding: 1rem 1.15rem;
|
||||
}
|
||||
|
||||
.portfolio-dossier-callout strong {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
color: var(--dossier-orange);
|
||||
font-family: "Montserrat", var(--headerFont), sans-serif;
|
||||
font-weight: 300;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.portfolio-dossier-callout.blue strong {
|
||||
color: var(--dossier-blue);
|
||||
}
|
||||
|
||||
.portfolio-dossier-card,
|
||||
.portfolio-dossier-project-meta {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.portfolio-dossier-table {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.portfolio-dossier-table th,
|
||||
.portfolio-dossier-table td {
|
||||
padding: 0.72rem 0.4rem;
|
||||
border-bottom: 1px solid rgba(89, 183, 255, 0.1);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.portfolio-dossier-table th {
|
||||
width: 10.5rem;
|
||||
color: var(--dossier-blue);
|
||||
font-family: "Montserrat", var(--headerFont), sans-serif;
|
||||
font-weight: 300;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.portfolio-dossier-table tr:last-child th,
|
||||
.portfolio-dossier-table tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.portfolio-dossier-figure {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.portfolio-dossier-figure img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
background: #05070b;
|
||||
}
|
||||
|
||||
.portfolio-dossier-figure figcaption {
|
||||
margin-top: 0.75rem;
|
||||
color: #ffd6ad;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.portfolio-term {
|
||||
color: var(--dossier-orange);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.portfolio-signal {
|
||||
color: var(--dossier-blue);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.portfolio-dossier-table,
|
||||
.portfolio-dossier-table tbody,
|
||||
.portfolio-dossier-table tr,
|
||||
.portfolio-dossier-table th,
|
||||
.portfolio-dossier-table td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.portfolio-dossier-table th {
|
||||
padding-bottom: 0.2rem;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.portfolio-dossier-table td {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue