# presentation/renderers/learning.renderer.ts

> 138 lines of code and 30 definitions.

Tree: Site tree
Language: typescript
Layer: product
Canonical: https://banes-lab.com/anatomy/tree#file-presentation-renderers-learning-renderer-ts
Source text: https://banes-lab.com/assets/sources/source.e2f26d5eb353208e1902dd2d1a427423bb794aee6d34b8dc90e1ba32a969efd6.generated.txt

## Definitions

- `numbers` (lexical_declaration, line 32)
- `stopLink` (lexical_declaration, line 113)
- `blockShell` (lexical_declaration, line 70)
- `markFor` (lexical_declaration, line 52)
- `renderLearning` (lexical_declaration, line 123, exported)
- `toneDefs` (lexical_declaration, line 38)
- `stopNode` (lexical_declaration, line 93)
- `DECIMALS` (lexical_declaration, line 30)
- `TONE_PREFIX` (lexical_declaration, line 36)
- `gradients` (lexical_declaration, line 39)
- `glyph` (lexical_declaration, line 53)
- `shell` (lexical_declaration, line 71)
- `tagWidth` (lexical_declaration, line 78)
- `tagX` (lexical_declaration, line 79)
- `tag` (lexical_declaration, line 80)
- `mark` (lexical_declaration, line 84)
- `caption` (lexical_declaration, line 85)
- `box` (lexical_declaration, line 94)
- `label` (lexical_declaration, line 97)
- `pathData` (lexical_declaration, line 117)
- `parts` (lexical_declaration, line 124, exported)
- `placed` (lexical_declaration, line 125, exported)
- `node` (lexical_declaration, line 131, exported)
- `grid` (lexical_declaration, line 136, exported)
- `data` (lexical_declaration, line 137, exported)
- `rail` (lexical_declaration, line 138, exported)
- `trace` (lexical_declaration, line 139, exported)
- `lines` (lexical_declaration, line 140, exported)
- `accents` (lexical_declaration, line 141, exported)
- `vector` (lexical_declaration, line 142, exported)

## Uses

- [core/factories/element.factory.ts](https://banes-lab.com/source/tree/core/factories/element.factory.ts.md)

## Source

```typescript
import {
    BLOCK_RADIUS,
    ICON_OFFSET,
    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 "#assets/image.assets";
import { ICON_GLYPHS } from "#assets/glyph.generated";
import { createVectorNode } from "#core/factories/element.factory";
import { gridFor } from "#domain/converters/learning.grid.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 glyph = ICON_GLYPHS.get(block.icon);
    if (glyph === undefined) {
        return createVectorNode("image", {
            attributes: {
                ...numbers({ height: MARK_SIZE, width: MARK_SIZE, x, y: block.y + ICON_OFFSET - MARK_SIZE }),
                href: GRAMMAR_SIDEBAR_MARK,
            },
            className: "learning-mark",
        });
    }
    return createVectorNode("text", {
        attributes: numbers({ x, y: block.y + ICON_OFFSET }),
        className: "learning-icon",
        text: glyph,
    });
};

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 data = pathData(orthogonal(grid, tracePoints(grid, 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 };
};
```
