presentation/components/home.component.ts

presentation/components/home.component.ts is a file in Bane's Lab Site. 128 lines of code and 39 definitions.

import { PIXEL, RADIX, TOP_PROPERTY } from "#configuration/constants/element.constants";
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 { 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 EASE_POWER = 3;
const FULL = 100;
const RAIL_LEAD = 0.35;
const PERCENT = "%";
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 traveled = Math.min(1, Math.max(0, (main.scrollTop - opens) / span));
        declareStyle(rail, PROGRESS_PROPERTY, String(traveled * 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();
        }
    };
};