presentation/renderers/learning.renderer.ts
presentation/renderers/learning.renderer.ts is a file in Bane's Lab Site. 145 lines of code and 31 definitions.
import {
BLOCK_RADIUS,
EVEN_ODD,
FILL_ATTRIBUTE,
ICON_OFFSET,
ICON_VIEWBOX,
LABEL_OFFSET,
LINE_HEIGHT,
MARK_SIZE,
PAD_X,
PAD_Y,
TAG_HEIGHT,
TAG_LIFT,
TAG_PAD,
TAG_RADIUS,
TEXT_BASELINE,
} from "#configuration/constants/learning.constants";
import type {
LaidBlock,
LaidStop,
LearningModel,
LearningPoint,
LearningView,
PlacedStop,
} from "#types/learning.types";
import { orthogonal, tracePoints } from "#domain/converters/learning.fragment.converter";
import { GRAMMAR_SIDEBAR_MARK } from "#core/assets/image.assets";
import { ICON_PATHS } from "#core/generated/glyph.generated";
import { createVectorNode } from "#core/factories/element.factory";
import { gridFor } from "#domain/converters/learning.grid.converter";
import { unfold } from "#domain/converters/learning.route.converter";
import { widthOf } from "#domain/converters/learning.converter";
const DECIMALS = 1;
const numbers = function numbers(record: Readonly<Record<string, number | string>>): Record<string, string> {
return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, String(value)]));
};
const TONE_PREFIX = "tone-";
const toneDefs = function toneDefs(accents: readonly string[]): Element {
const gradients = accents.map((accent) =>
createVectorNode("linearGradient", {
attributes: { id: TONE_PREFIX + accent, x1: "0", x2: "0", y1: "0", y2: "1" },
children: [
createVectorNode("stop", { attributes: { offset: "0%" }, className: "learning-tone-head" }),
createVectorNode("stop", { attributes: { offset: "100%" }, className: "learning-tone-tail" }),
],
className: accent,
}),
);
return createVectorNode("defs", { children: gradients });
};
const markFor = function markFor(block: LaidBlock, x: number): Element {
const paths = ICON_PATHS.get(block.icon);
const box = numbers({ height: MARK_SIZE, width: MARK_SIZE, x, y: block.y + ICON_OFFSET - MARK_SIZE });
if (paths === undefined) {
return createVectorNode("image", {
attributes: { ...box, href: GRAMMAR_SIDEBAR_MARK },
className: "learning-mark",
});
}
return createVectorNode("svg", {
attributes: { ...box, viewBox: ICON_VIEWBOX },
children: paths.map((path) =>
createVectorNode("path", {
attributes: path.evenOdd ? { d: path.d, [FILL_ATTRIBUTE]: EVEN_ODD } : { d: path.d },
}),
),
className: "learning-icon",
});
};
const blockShell = function blockShell(block: LaidBlock): readonly Element[] {
const shell = createVectorNode("rect", {
attributes: {
...numbers({ height: block.height, rx: BLOCK_RADIUS, width: block.width, x: block.x, y: block.y }),
fill: `url(#${TONE_PREFIX}${block.accent})`,
},
className: `learning-block ${block.accent}`,
});
const tagWidth = widthOf([block.label]) + TAG_PAD;
const tagX = block.x + (block.width - tagWidth) / 2;
const tag = createVectorNode("rect", {
attributes: numbers({ height: TAG_HEIGHT, rx: TAG_RADIUS, width: tagWidth, x: tagX, y: block.y + 1 }),
className: `learning-tag ${block.accent}`,
});
const mark = markFor(block, tagX + TAG_LIFT);
const caption = createVectorNode("text", {
attributes: numbers({ x: tagX + TAG_LIFT + LABEL_OFFSET, y: block.y + ICON_OFFSET }),
className: "learning-label",
text: block.label,
});
return [shell, tag, mark, caption];
};
const stopNode = function stopNode(stop: LaidStop, accent: string): Element {
const box = createVectorNode("rect", {
attributes: numbers({ height: stop.height, rx: TAG_RADIUS, width: stop.width, x: stop.x, y: stop.y }),
});
const label = createVectorNode("text", {
attributes: numbers({ x: stop.x + PAD_X, y: stop.y + PAD_Y + TEXT_BASELINE }),
children: stop.rows.map((text, row) =>
createVectorNode("tspan", {
attributes: numbers({ x: stop.x + PAD_X, y: stop.y + PAD_Y + TEXT_BASELINE + row * LINE_HEIGHT }),
text,
}),
),
});
return createVectorNode("g", {
attributes: { id: stop.id },
children: [box, label],
className: `learning-stop ${accent}`,
});
};
const stopLink = function stopLink(stop: LaidStop, group: Element): Element {
return createVectorNode("a", { attributes: { href: stop.path }, children: [group], className: "learning-jump" });
};
const pathData = function pathData(points: readonly LearningPoint[]): string {
return points
.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x.toFixed(DECIMALS)} ${point.y.toFixed(DECIMALS)}`)
.join(" ");
};
export const renderLearning = function renderLearning(model: LearningModel): LearningView {
const parts = createVectorNode("g");
const placed: PlacedStop[] = [];
for (const block of model.blocks) {
for (const element of blockShell(block)) {
parts.append(element);
}
for (const stop of block.stops) {
const node = stopNode(stop, block.accent);
parts.append(stopLink(stop, node));
placed.push({ ...stop, block, node });
}
}
const grid = gridFor(model);
const straight = orthogonal(grid, tracePoints(grid, placed));
const data = pathData(unfold(straight, placed));
const rail = createVectorNode("path", { attributes: { d: data }, className: "learning-rail" });
const trace = createVectorNode("path", { attributes: { d: data }, className: "learning-trace" });
const lines = createVectorNode("g", { children: [rail, trace] });
const accents = [...new Set(model.blocks.map((block) => block.accent))];
const vector = createVectorNode("svg", {
attributes: { viewBox: `0 0 ${String(model.width)} ${String(model.height)}` },
children: [toneDefs(accents), lines, parts],
className: "learning-vector",
});
return { stops: placed, trace, vector };
};