{
const w = 640, h = 320;
const svg = d3.create("svg")
.attr("viewBox", `0 0 ${w} ${h}`)
.attr("width", "100%")
.style("font-family", "system-ui, sans-serif");
const defs = svg.append("defs");
defs.append("marker")
.attr("id", "arr")
.attr("viewBox", "0 0 10 10")
.attr("refX", 8).attr("refY", 5)
.attr("markerWidth", 6).attr("markerHeight", 6)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", "M2 1L8 5L2 9")
.attr("fill", "none")
.attr("stroke", "#999")
.attr("stroke-width", 1.5)
.attr("stroke-linecap", "round");
const teal = { fill: "#e1f5ee", stroke: "#0f6e56", text: "#085041" };
const gray = { fill: "#f1efe8", stroke: "#888780", text: "#444441" };
function box(g, x, y, bw, bh, color, title, sub) {
g.append("rect")
.attr("x", x).attr("y", y).attr("width", bw).attr("height", bh)
.attr("rx", 8).attr("fill", color.fill)
.attr("stroke", color.stroke).attr("stroke-width", 0.5);
if (sub) {
g.append("text").attr("x", x+bw/2).attr("y", y+bh/2-9)
.attr("text-anchor","middle").attr("dominant-baseline","central")
.attr("fill", color.text).attr("font-size", 13).attr("font-weight", 500)
.text(title);
g.append("text").attr("x", x+bw/2).attr("y", y+bh/2+10)
.attr("text-anchor","middle").attr("dominant-baseline","central")
.attr("fill", color.stroke).attr("font-size", 11).text(sub);
} else {
g.append("text").attr("x", x+bw/2).attr("y", y+bh/2)
.attr("text-anchor","middle").attr("dominant-baseline","central")
.attr("fill", color.text).attr("font-size", 13).attr("font-weight", 500)
.text(title);
}
}
function arrow(g, x1, y1, x2, y2) {
g.append("line")
.attr("x1",x1).attr("y1",y1).attr("x2",x2).attr("y2",y2)
.attr("stroke","#bbb").attr("stroke-width",1.2)
.attr("marker-end","url(#arr)");
}
const root = svg.append("g");
box(root, 240, 20, 160, 44, gray, "Game loop", null);
arrow(root, 320, 64, 320, 94);
root.append("text").attr("x",335).attr("y",83)
.attr("font-size",11).attr("fill","#bbb").text("dt = 1/60s");
const steps = [
{ y:94, title:"Integrate forces", sub:"Semi-implicit Euler, gravity", ms:"~0.2ms" },
{ y:178, title:"Broadphase", sub:"AABB overlap, O(n²) for now", ms:"~0.5ms" },
{ y:262, title:"Narrowphase + resolve", sub:"SAT, sequential impulse solver", ms:"~2-8ms" },
];
steps.forEach((s, i) => {
box(root, 200, s.y, 240, 54, teal, s.title, s.sub);
if (i < steps.length - 1) arrow(root, 320, s.y+54, 320, s.y+84);
root.append("text").attr("x",60).attr("y",s.y+27)
.attr("text-anchor","middle").attr("font-size",11).attr("fill","#bbb").text(s.ms);
});
root.append("text").attr("x",60).attr("y",42)
.attr("text-anchor","middle").attr("font-size",11).attr("fill","#bbb")
.text("budget: 16.6ms");
root.append("path")
.attr("d","M440 289 L540 289 L540 42 L400 42")
.attr("fill","none").attr("stroke","#ccc")
.attr("stroke-width",1).attr("stroke-dasharray","5 4")
.attr("marker-end","url(#arr)");
root.append("text").attr("x",554).attr("y",170)
.attr("text-anchor","middle").attr("font-size",11).attr("fill","#bbb")
.attr("transform","rotate(-90 554 170)").text("next frame");
return svg.node();
}