# presentation/components/home.component.ts

> 130 lines of code and 41 definitions.

Tree: Site tree
Language: typescript
Layer: product
Canonical: https://banes-lab.com/anatomy/tree#file-presentation-components-home-component-ts
Source text: https://banes-lab.com/assets/sources/source.08d8202310f45288ae28c1383d42f6232497fb495e39b4ad8462cb3f51565633.generated.txt

## Definitions

- `midpoint` (lexical_declaration, line 55)
- `railMeter` (lexical_declaration, line 65)
- `progress` (lexical_declaration, line 66)
- `easeOut` (lexical_declaration, line 18)
- `bindRail` (lexical_declaration, line 92)
- `observeRail` (lexical_declaration, line 121)
- `step` (lexical_declaration, line 24)
- `attachHome` (lexical_declaration, line 133, exported)
- `COUNT_DURATION` (lexical_declaration, line 9)
- `RADIX` (lexical_declaration, line 10)
- `EASE_POWER` (lexical_declaration, line 11)
- `FULL` (lexical_declaration, line 12)
- `RAIL_LEAD` (lexical_declaration, line 13)
- `PERCENT` (lexical_declaration, line 14)
- `TOP_PROPERTY` (lexical_declaration, line 15)
- `HEIGHT_PROPERTY` (lexical_declaration, line 16)
- `countUp` (lexical_declaration, line 22)
- `opened` (lexical_declaration, line 23)
- `t` (lexical_declaration, line 25)
- `observeCounts` (lexical_declaration, line 34)
- `observer` (lexical_declaration, line 35)
- `RailMeter` (interface_declaration, line 60)
- `box` (lexical_declaration, line 67)
- `view` (lexical_declaration, line 68)
- `railTop` (lexical_declaration, line 72)
- `furthest` (lexical_declaration, line 73)
- `opens` (lexical_declaration, line 74)
- `span` (lexical_declaration, line 75)
- `travelled` (lexical_declaration, line 76)
- `anchor` (lexical_declaration, line 82)
- `queued` (lexical_declaration, line 93)
- `onScroll` (lexical_declaration, line 94)
- `sizes` (lexical_declaration, line 106)
- `fonts` (lexical_declaration, line 108)
- `ready` (lexical_declaration, line 109)
- `rail` (lexical_declaration, line 122)
- `icons` (lexical_declaration, line 123)
- `main` (lexical_declaration, line 124)
- `first` (lexical_declaration, line 125)
- `last` (lexical_declaration, line 126)
- `disposers` (lexical_declaration, line 134, exported)

## Uses

- [core/registries/style.registry.ts](https://banes-lab.com/source/tree/core/registries/style.registry.ts.md)
- [presentation/widgets/home.widget.ts](https://banes-lab.com/source/tree/presentation/widgets/home.widget.ts.md)

## Source

```typescript
import { PROGRESS_PROPERTY, RAIL_ID, ROW_ICON_SELECTOR } from "#core/ids/home.ids";
import type { Disposer } from "#types/base.types";
import { MAIN_ID } from "#core/ids/site.ids";
import { PIXEL } from "#configuration/constants/element.constants";
import { SETTLED_TEXT_ATTRIBUTE } from "#configuration/constants/document.constants";
import { VISIBLE_FRACTION } from "#configuration/constants/learning.constants";
import { declareStyle } from "#core/registries/style.registry";

const COUNT_DURATION = 1400;
const RADIX = 10;
const EASE_POWER = 3;
const FULL = 100;
const RAIL_LEAD = 0.35;
const PERCENT = "%";
const TOP_PROPERTY = "top";
const HEIGHT_PROPERTY = "height";

const easeOut = function easeOut(t: number): number {
    return 1 - (1 - t) ** EASE_POWER;
};

const countUp = function countUp(element: HTMLElement, target: number): void {
    const opened = performance.now();
    const step = function step(now: number): void {
        const t = Math.min(1, (now - opened) / COUNT_DURATION);
        element.textContent = String(Math.round(target * easeOut(t)));
        if (t < 1) {
            requestAnimationFrame(step);
        }
    };
    requestAnimationFrame(step);
};

const observeCounts = function observeCounts(root: Element): Disposer {
    const observer = new IntersectionObserver(
        (entries, held) => {
            for (const entry of entries) {
                if (!entry.isIntersecting || !(entry.target instanceof HTMLElement)) {
                    continue;
                }
                countUp(entry.target, Number.parseInt(entry.target.getAttribute(SETTLED_TEXT_ATTRIBUTE) ?? "", RADIX));
                held.unobserve(entry.target);
            }
        },
        { threshold: VISIBLE_FRACTION },
    );
    for (const element of root.querySelectorAll(`[${SETTLED_TEXT_ATTRIBUTE}]`)) {
        observer.observe(element);
    }
    return () => {
        observer.disconnect();
    };
};

const midpoint = function midpoint(element: Element): number {
    const box = element.getBoundingClientRect();
    return box.top + box.height / 2;
};

interface RailMeter {
    readonly progress: () => void;
    readonly refresh: () => void;
}

const railMeter = function railMeter(rail: HTMLElement, main: HTMLElement, first: Element, last: Element): RailMeter {
    const progress = function progress(): void {
        const box = rail.getBoundingClientRect();
        const view = main.getBoundingClientRect();
        if (box.height === 0) {
            return;
        }
        const railTop = box.top - view.top + main.scrollTop;
        const furthest = Math.max(0, main.scrollHeight - main.clientHeight);
        const opens = Math.max(0, railTop - main.clientHeight * (1 - RAIL_LEAD));
        const span = Math.max(1, furthest - opens);
        const travelled = Math.min(1, Math.max(0, (main.scrollTop - opens) / span));
        declareStyle(rail, PROGRESS_PROPERTY, String(travelled * FULL) + PERCENT);
    };
    return {
        progress,
        refresh: () => {
            const anchor = rail.parentElement;
            if (anchor !== null) {
                declareStyle(rail, TOP_PROPERTY, String(midpoint(first) - anchor.getBoundingClientRect().top) + PIXEL);
            }
            declareStyle(rail, HEIGHT_PROPERTY, String(midpoint(last) - midpoint(first)) + PIXEL);
            progress();
        },
    };
};

const bindRail = function bindRail(meter: RailMeter, main: HTMLElement, root: Element): Disposer {
    let queued = false;
    const onScroll = function onScroll(): void {
        if (queued) {
            return;
        }
        queued = true;
        requestAnimationFrame(() => {
            queued = false;
            meter.progress();
        });
    };
    main.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", meter.refresh);
    const sizes = new ResizeObserver(meter.refresh);
    sizes.observe(root);
    const fonts: unknown = Reflect.get(document, "fonts");
    const ready: unknown = typeof fonts === "object" && fonts !== null ? Reflect.get(fonts, "ready") : null;
    if (ready instanceof Promise) {
        void ready.then(meter.refresh);
    }
    meter.refresh();
    return () => {
        main.removeEventListener("scroll", onScroll);
        window.removeEventListener("resize", meter.refresh);
        sizes.disconnect();
    };
};

const observeRail = function observeRail(root: Element): Disposer {
    const rail = root.querySelector(`#${RAIL_ID}`);
    const icons = [...root.querySelectorAll(ROW_ICON_SELECTOR)];
    const main = document.getElementById(MAIN_ID);
    const first = icons.at(0);
    const last = icons.at(-1);
    if (!(rail instanceof HTMLElement) || main === null || first === undefined || last === undefined) {
        return () => {};
    }
    return bindRail(railMeter(rail, main, first, last), main, root);
};

export const attachHome = function attachHome(root: Element): Disposer {
    const disposers = [observeCounts(root), observeRail(root)];
    return () => {
        for (const dispose of disposers) {
            dispose();
        }
    };
};
```
