126 lines
5.6 KiB
Plaintext
126 lines
5.6 KiB
Plaintext
---
|
|
/**
|
|
* The method as a closed loop of four stages, drawn as a spec-sheet diagram.
|
|
* Main nodes sit on the corners of the loop with their labels inside;
|
|
* each stage's instruments fan outwards. Wide and tall compositions of the same drawing.
|
|
*/
|
|
interface Stage { title: string; items: string[] }
|
|
interface Props { title: string; desc: string; stages: Stage[] }
|
|
const { title, desc, stages } = Astro.props;
|
|
|
|
const stroke = 1.25;
|
|
const ring = 36;
|
|
|
|
/* Wide composition: 1400 x 560. Nodes on the corners of a loop. */
|
|
const W = { w: 1400, h: 560, x1: 420, x2: 980, y1: 140, y2: 420, sat: 175, dy: 62 };
|
|
const wideNodes = [
|
|
{ x: W.x1, y: W.y1, side: -1, up: -1 }, // top-left: threat analysis
|
|
{ x: W.x2, y: W.y1, side: 1, up: -1 }, // top-right: engineering
|
|
{ x: W.x2, y: W.y2, side: 1, up: 1 }, // bottom-right: verification
|
|
{ x: W.x1, y: W.y2, side: -1, up: 1 }, // bottom-left: monitoring
|
|
];
|
|
|
|
/* Tall composition: 400 x 780. Nodes stacked on a spine, the loop closes on the left.
|
|
The title sits right of the node, its instruments hang below the title. */
|
|
const T = { w: 400, h: 780, x: 60, loopX: 20, y0: 60, step: 200, ring: 28, labelX: 40, satX: 44, satY: 36, dy: 28 };
|
|
const tallNodes = stages.map((_, i) => ({ x: T.x, y: T.y0 + i * T.step }));
|
|
|
|
/* Direction tick: a small chevron at the midpoint of an edge, pointing along it. */
|
|
const tick = (x: number, y: number, dx: number, dy: number, s = 7) => {
|
|
const len = Math.hypot(dx, dy) || 1; const ux = dx / len, uy = dy / len;
|
|
const px = -uy, py = ux;
|
|
return `${x - ux * s + px * s},${y - uy * s + py * s} ${x},${y} ${x - ux * s - px * s},${y - uy * s - py * s}`;
|
|
};
|
|
---
|
|
<svg class="diagram diagram--wide" viewBox={`0 0 ${W.w} ${W.h}`} role="img" aria-labelledby="method-w-title method-w-desc" xmlns="http://www.w3.org/2000/svg">
|
|
<title id="method-w-title">{title}</title>
|
|
<desc id="method-w-desc">{desc}</desc>
|
|
<g fill="none" stroke="currentColor" stroke-width={stroke} stroke-linecap="square" vector-effect="non-scaling-stroke">
|
|
<rect x={W.x1} y={W.y1} width={W.x2 - W.x1} height={W.y2 - W.y1} />
|
|
<polyline points={tick((W.x1 + W.x2) / 2, W.y1, 1, 0)} />
|
|
<polyline points={tick(W.x2, (W.y1 + W.y2) / 2, 0, 1)} />
|
|
<polyline points={tick((W.x1 + W.x2) / 2, W.y2, -1, 0)} />
|
|
<polyline points={tick(W.x1, (W.y1 + W.y2) / 2, 0, -1)} />
|
|
{wideNodes.map((n, i) => (
|
|
<g>
|
|
<circle cx={n.x} cy={n.y} r={ring} stroke-dasharray="2 5" />
|
|
{stages[i].items.map((_, k) => {
|
|
const sx = n.x + n.side * W.sat;
|
|
const sy = n.y + (k - 1) * W.dy;
|
|
return <line x1={n.x} y1={n.y} x2={sx} y2={sy} class="diagram__branch" />;
|
|
})}
|
|
</g>
|
|
))}
|
|
</g>
|
|
<g fill="currentColor">
|
|
{wideNodes.map((n, i) => (
|
|
<g>
|
|
<circle cx={n.x} cy={n.y} r="5" />
|
|
{stages[i].items.map((_, k) => <circle cx={n.x + n.side * W.sat} cy={n.y + (k - 1) * W.dy} r="3" class="diagram__dot" />)}
|
|
</g>
|
|
))}
|
|
</g>
|
|
<g class="diagram__labels" font-size="17" font-weight="500">
|
|
{wideNodes.map((n, i) => (
|
|
<text x={n.x - n.side * (ring + 14)} y={n.y - n.up * 30 + 6} text-anchor={n.side < 0 ? 'start' : 'end'}>{stages[i].title}</text>
|
|
))}
|
|
</g>
|
|
<g class="diagram__meta" font-size="15">
|
|
{wideNodes.map((n, i) => stages[i].items.map((item, k) => (
|
|
<text x={n.x + n.side * (W.sat + 14)} y={n.y + (k - 1) * W.dy + 5} text-anchor={n.side < 0 ? 'end' : 'start'}>{item}</text>
|
|
)))}
|
|
</g>
|
|
</svg>
|
|
|
|
<svg class="diagram diagram--tall" viewBox={`0 0 ${T.w} ${T.h}`} role="img" aria-labelledby="method-t-title method-t-desc" xmlns="http://www.w3.org/2000/svg">
|
|
<title id="method-t-title">{title}</title>
|
|
<desc id="method-t-desc">{desc}</desc>
|
|
<g fill="none" stroke="currentColor" stroke-width={stroke} stroke-linecap="square" vector-effect="non-scaling-stroke">
|
|
<line x1={T.x} y1={tallNodes[0].y} x2={T.x} y2={tallNodes[3].y} />
|
|
<polyline points={`${T.x},${tallNodes[3].y} ${T.loopX},${tallNodes[3].y} ${T.loopX},${tallNodes[0].y} ${T.x},${tallNodes[0].y}`} />
|
|
{tallNodes.slice(0, 3).map((n) => <polyline points={tick(T.x, n.y + T.step / 2 + 20, 0, 1)} />)}
|
|
<polyline points={tick(T.loopX, (tallNodes[0].y + tallNodes[3].y) / 2, 0, -1)} />
|
|
{tallNodes.map((n, i) => (
|
|
<g>
|
|
<circle cx={n.x} cy={n.y} r={T.ring} stroke-dasharray="2 5" />
|
|
{stages[i].items.map((_, k) => <line x1={n.x} y1={n.y} x2={n.x + T.satX} y2={n.y + T.satY + k * T.dy} class="diagram__branch" />)}
|
|
</g>
|
|
))}
|
|
</g>
|
|
<g fill="currentColor">
|
|
{tallNodes.map((n, i) => (
|
|
<g>
|
|
<circle cx={n.x} cy={n.y} r="5" />
|
|
{stages[i].items.map((_, k) => <circle cx={n.x + T.satX} cy={n.y + T.satY + k * T.dy} r="3" class="diagram__dot" />)}
|
|
</g>
|
|
))}
|
|
</g>
|
|
<g class="diagram__labels" font-size="16" font-weight="500">
|
|
{tallNodes.map((n, i) => <text x={n.x + T.labelX} y={n.y + 6}>{stages[i].title}</text>)}
|
|
</g>
|
|
<g class="diagram__meta" font-size="14">
|
|
{tallNodes.map((n, i) => stages[i].items.map((item, k) => (
|
|
<text x={n.x + T.satX + 12} y={n.y + T.satY + k * T.dy + 5}>{item}</text>
|
|
)))}
|
|
</g>
|
|
</svg>
|
|
|
|
<style>
|
|
.diagram {
|
|
width: 100%;
|
|
height: auto;
|
|
color: var(--ink);
|
|
font-family: var(--font-sans);
|
|
}
|
|
.diagram__labels { fill: var(--ink); }
|
|
.diagram__meta { fill: var(--ink-muted); }
|
|
.diagram__branch { stroke: var(--line); }
|
|
.diagram__dot { fill: var(--ink-muted); }
|
|
.diagram--tall { display: none; }
|
|
/* Below 64rem the wide drawing would scale its labels under 11px; the tall one takes over. */
|
|
@media (max-width: 63.99rem) {
|
|
.diagram--wide { display: none; }
|
|
.diagram--tall { display: block; max-width: 26rem; }
|
|
}
|
|
</style>
|