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();
}
};
};