# presentation/components/learning.component.ts

> 140 lines of code and 35 definitions.

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

## Definitions

- `numberAt` (lexical_declaration, line 30)
- `pause` (lexical_declaration, line 121, exported)
- `attachLearningMap` (lexical_declaration, line 35, exported)
- `glideBack` (lexical_declaration, line 57, exported)
- `light` (lexical_declaration, line 86, exported)
- `play` (lexical_declaration, line 113, exported)
- `frame` (lexical_declaration, line 96, exported)
- `settle` (lexical_declaration, line 74, exported)
- `onVisibility` (lexical_declaration, line 124, exported)
- `Step` (interface_declaration, line 18)
- `HALF` (lexical_declaration, line 24)
- `ease` (lexical_declaration, line 26)
- `value` (lexical_declaration, line 31)
- `framed` (lexical_declaration, line 36, exported)
- `{ box, geometry, style }` (lexical_declaration, line 40, exported)
- `total` (lexical_declaration, line 41, exported)
- `marks` (lexical_declaration, line 44, exported)
- `spans` (lexical_declaration, line 45, exported)
- `aspect` (lexical_declaration, line 46, exported)
- `viewWidth` (lexical_declaration, line 47, exported)
- `viewHeight` (lexical_declaration, line 48, exported)
- `opening` (lexical_declaration, line 49, exported)
- `closing` (lexical_declaration, line 50, exported)
- `elapsed` (lexical_declaration, line 51, exported)
- `held` (lexical_declaration, line 52, exported)
- `back` (lexical_declaration, line 53, exported)
- `last` (lexical_declaration, line 54, exported)
- `running` (lexical_declaration, line 55, exported)
- `t` (lexical_declaration, line 59, exported)
- `glide` (lexical_declaration, line 66, exported)
- `lit` (lexical_declaration, line 88, exported)
- `tick` (lexical_declaration, line 100, exported)
- `step` (lexical_declaration, line 102, exported)
- `head` (lexical_declaration, line 103, exported)
- `observer` (lexical_declaration, line 132, exported)

## Uses

- [presentation/components/learning.fragment.component.ts](https://banes-lab.com/source/tree/presentation/components/learning.fragment.component.ts.md)

## Used by

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

## Source

```typescript
import {
    FRAME_CAP,
    GLIDE_POWER,
    HOLD,
    LIFT,
    LIT_CLASS,
    MILLISECONDS,
    RETURN,
    SLOW_PACE,
    SPEED,
    VISIBLE_FRACTION,
    ZOOM,
} from "#configuration/constants/learning.constants";
import type { LearningPoint, LearningView } from "#types/learning.types";
import { framedOf, marksOf, paceAt, spansOf, widestOf } from "#presentation/components/learning.fragment.component";
import type { Disposer } from "#types/base.types";

interface Step {
    readonly focus: LearningPoint | null;
    readonly lift: number;
    readonly travelled: number;
}

const HALF = 2;

const ease = function ease(t: number): number {
    return t * t * (3 - 2 * t);
};

const numberAt = function numberAt(record: Record<string, unknown>, key: string): number {
    const value = record[key];
    return typeof value === "number" ? value : 0;
};

export const attachLearningMap = function attachLearningMap(shell: Element, view: LearningView): Disposer {
    const framed = framedOf(view);
    if (framed === null) {
        return () => {};
    }
    const { box, geometry, style } = framed;
    const total = geometry.getTotalLength();
    style["strokeDasharray"] = String(total);
    style["strokeDashoffset"] = String(total);
    const marks = marksOf(geometry, view.stops, total);
    const spans = spansOf(marks);
    const aspect = shell.clientHeight / shell.clientWidth;
    const viewWidth = widestOf(view.stops) * ZOOM;
    const viewHeight = viewWidth * aspect;
    const opening = geometry.getPointAtLength(0);
    const closing = geometry.getPointAtLength(total);
    let elapsed = 0;
    let held = 0;
    let back = 0;
    let last = 0;
    let running = false;

    const glideBack = function glideBack(tick: number): Step {
        back = Math.min(RETURN, back + tick);
        const t = back / RETURN;
        if (back >= RETURN) {
            elapsed = 0;
            held = 0;
            back = 0;
            return { focus: null, lift: SLOW_PACE, travelled: 0 };
        }
        const glide = ease(t);
        return {
            focus: { x: closing.x + (opening.x - closing.x) * glide, y: closing.y + (opening.y - closing.y) * glide },
            lift: SLOW_PACE + (LIFT - SLOW_PACE) * Math.sin(Math.PI * t ** GLIDE_POWER),
            travelled: total * (1 - t) * (1 - t),
        };
    };

    const settle = function settle(tick: number): Step {
        if (elapsed < total) {
            elapsed = Math.min(total, elapsed + tick * SPEED * paceAt(spans, elapsed));
            return { focus: null, lift: SLOW_PACE, travelled: elapsed };
        }
        if (held < HOLD) {
            held += tick;
            return { focus: null, lift: SLOW_PACE, travelled: total };
        }
        return glideBack(tick);
    };

    const light = function light(travelled: number): void {
        for (const mark of marks) {
            const lit = travelled >= mark.at && travelled > 0;
            if (mark.lit !== lit) {
                mark.lit = lit;
                mark.stop.node.classList.toggle(LIT_CLASS, lit);
            }
        }
    };

    const frame = function frame(now: number): void {
        if (!running) {
            return;
        }
        const tick = Math.min(FRAME_CAP, (now - last) / MILLISECONDS);
        last = now;
        const step = settle(tick);
        const head = step.focus ?? geometry.getPointAtLength(step.travelled);
        style["strokeDashoffset"] = String(total - step.travelled);
        box["width"] = viewWidth * step.lift;
        box["height"] = viewHeight * step.lift;
        box["x"] = head.x - numberAt(box, "width") / HALF;
        box["y"] = head.y - numberAt(box, "height") / HALF;
        light(step.travelled);
        requestAnimationFrame(frame);
    };

    const play = function play(): void {
        if (running || document.hidden) {
            return;
        }
        running = true;
        last = performance.now();
        requestAnimationFrame(frame);
    };
    const pause = function pause(): void {
        running = false;
    };
    const onVisibility = function onVisibility(): void {
        if (document.hidden) {
            pause();
            return;
        }
        play();
    };
    document.addEventListener("visibilitychange", onVisibility);
    const observer = new IntersectionObserver(
        (entries) => {
            for (const entry of entries) {
                if (entry.isIntersecting) {
                    play();
                } else {
                    pause();
                }
            }
        },
        { threshold: VISIBLE_FRACTION },
    );
    observer.observe(shell);
    return () => {
        pause();
        observer.disconnect();
        document.removeEventListener("visibilitychange", onVisibility);
    };
};
```
