animate-text-weight 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +216 -39
- package/dist/index.d.ts +7 -4
- package/dist/index.js +484 -72
- package/dist/wave-nav.d.ts +12 -0
- package/dist/wave-nav.js +96 -0
- package/dist/waves.d.ts +58 -0
- package/dist/waves.js +52 -0
- package/package.json +10 -2
- package/src/animateTextWeight.css +288 -10
- package/src/index.ts +506 -68
- package/src/wave-nav.ts +113 -0
- package/src/waves.ts +73 -0
package/src/wave-nav.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* waveNav: a nav whose links follow the page, one section at a time.
|
|
3
|
+
*
|
|
4
|
+
* The current section is the one across the middle of the screen, worked out
|
|
5
|
+
* afresh from where things are on every scroll, never from what changed: a
|
|
6
|
+
* jump by Home or End passes every section in a few frames, and a switch that
|
|
7
|
+
* trusts enter and exit events is left on by the one that goes missing.
|
|
8
|
+
*
|
|
9
|
+
* A jump does not skip what it passes. The switch walks there one section at
|
|
10
|
+
* a time, each link on and then off again, and takes the next step once the
|
|
11
|
+
* wave has crossed the link it just switched on: its delay once per unit. So
|
|
12
|
+
* the links pick the wave up one from the other, and a jump sends one wave
|
|
13
|
+
* through the whole nav rather than a small one through each link.
|
|
14
|
+
*
|
|
15
|
+
* Optional, and apart from the library: nothing in index.ts imports it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { UNIT } from "./index.js";
|
|
19
|
+
|
|
20
|
+
export interface WaveNavOptions {
|
|
21
|
+
/** Toggled on the current link, as well as aria-current. */
|
|
22
|
+
linkClass?: string;
|
|
23
|
+
/** Toggled on the current link's section, which aria-current cannot mark. */
|
|
24
|
+
sectionClass?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** At least a frame per step, so no link goes on and off before it is drawn. */
|
|
28
|
+
const MIN_STEP = 17;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Mark the link of the section across the middle of the screen with
|
|
32
|
+
* aria-current="true". Each link's section is the element its hash names;
|
|
33
|
+
* a link without one is left out. Returns a function that stops following.
|
|
34
|
+
*/
|
|
35
|
+
export function waveNav(
|
|
36
|
+
links: Iterable<HTMLAnchorElement>,
|
|
37
|
+
options: WaveNavOptions = {}
|
|
38
|
+
): () => void {
|
|
39
|
+
const pairs = [...links].flatMap((link) => {
|
|
40
|
+
const section = document.getElementById(link.hash.slice(1));
|
|
41
|
+
return section ? [{ link, section }] : [];
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function setCurrent(index: number, on: boolean) {
|
|
45
|
+
const pair = pairs[index];
|
|
46
|
+
if (!pair) return;
|
|
47
|
+
if (on) pair.link.setAttribute("aria-current", "true");
|
|
48
|
+
else pair.link.removeAttribute("aria-current");
|
|
49
|
+
if (options.linkClass) pair.link.classList.toggle(options.linkClass, on);
|
|
50
|
+
if (options.sectionClass) pair.section.classList.toggle(options.sectionClass, on);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The section across the middle, by index: -1 above the first, the length past the last. */
|
|
54
|
+
function getIndex() {
|
|
55
|
+
const middle = innerHeight / 2;
|
|
56
|
+
const below = pairs.findIndex(
|
|
57
|
+
({ section }) => section.getBoundingClientRect().bottom > middle
|
|
58
|
+
);
|
|
59
|
+
if (below === -1) return pairs.length;
|
|
60
|
+
return pairs[below]!.section.getBoundingClientRect().top <= middle
|
|
61
|
+
? below
|
|
62
|
+
: below - 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** How long the wave takes to cross a link, in ms: its delay once per unit. */
|
|
66
|
+
function getCrossing(link: HTMLElement) {
|
|
67
|
+
const delay = getComputedStyle(link)
|
|
68
|
+
.getPropertyValue("--animate-weight-delay")
|
|
69
|
+
.trim();
|
|
70
|
+
const milliseconds = parseFloat(delay) * (delay.endsWith("ms") ? 1 : 1000);
|
|
71
|
+
return link.querySelectorAll(`.${UNIT}`).length * milliseconds || 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let current = getIndex();
|
|
75
|
+
let target = current;
|
|
76
|
+
// The next step, while the wave is still crossing the last link.
|
|
77
|
+
let timer = 0;
|
|
78
|
+
setCurrent(current, true);
|
|
79
|
+
|
|
80
|
+
/** One section nearer the target, and the next once the wave is across. */
|
|
81
|
+
function update() {
|
|
82
|
+
timer = 0;
|
|
83
|
+
if (current === target) return;
|
|
84
|
+
const next = current + Math.sign(target - current);
|
|
85
|
+
setCurrent(current, false);
|
|
86
|
+
setCurrent(next, true);
|
|
87
|
+
current = next;
|
|
88
|
+
const link = pairs[next]?.link;
|
|
89
|
+
timer = setTimeout(
|
|
90
|
+
update,
|
|
91
|
+
Math.max(MIN_STEP, link ? getCrossing(link) : 0)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function handleMove() {
|
|
96
|
+
target = getIndex();
|
|
97
|
+
if (!timer) update();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Scroll events already come once a frame. A section that changes height
|
|
101
|
+
// moves the ones below it without a scroll, and a resize moves the middle.
|
|
102
|
+
addEventListener("scroll", handleMove, { passive: true });
|
|
103
|
+
addEventListener("resize", handleMove);
|
|
104
|
+
const observer = new ResizeObserver(handleMove);
|
|
105
|
+
for (const { section } of pairs) observer.observe(section);
|
|
106
|
+
|
|
107
|
+
return () => {
|
|
108
|
+
removeEventListener("scroll", handleMove);
|
|
109
|
+
removeEventListener("resize", handleMove);
|
|
110
|
+
observer.disconnect();
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
};
|
|
113
|
+
}
|
package/src/waves.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a pointer element's two waves stand, and where a crossing sends them.
|
|
3
|
+
*
|
|
4
|
+
* The leading wave brings the weight up and the trailing one takes it away, so
|
|
5
|
+
* the text is at full weight between the two and a band of weight crosses it.
|
|
6
|
+
* Which way round the pair is travelling is the whole of the state: a visit
|
|
7
|
+
* runs them both to one end, the next visit runs them home again, and nothing
|
|
8
|
+
* is ever put back.
|
|
9
|
+
*
|
|
10
|
+
* Kept apart from the element it is written on, and out of the package's
|
|
11
|
+
* exports, because what it has to get right is arithmetic and not animation.
|
|
12
|
+
* A crossing that lands mid-band cannot be ordered against the frame the waves
|
|
13
|
+
* are on, so the rest it comes to must be right whatever they were doing: an
|
|
14
|
+
* entry always leaves the pair a whole crossing apart, which reads as full
|
|
15
|
+
* weight, and a leaving always brings them together, which reads as none. The
|
|
16
|
+
* waves decide how the text gets there and never whether.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** An end of the text, the way the stylesheet counts: 0 the start, 1 the far one. */
|
|
20
|
+
export type End = "0" | "1";
|
|
21
|
+
|
|
22
|
+
/** Which way round the pair is running: up from the start, or back down. */
|
|
23
|
+
export type Way = "1" | "-1";
|
|
24
|
+
|
|
25
|
+
/** Where each wave is headed, and which way round the two of them are going. */
|
|
26
|
+
export interface Waves {
|
|
27
|
+
in: End;
|
|
28
|
+
out: End;
|
|
29
|
+
way: Way;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The other one of the two. */
|
|
33
|
+
export const other = (end: End): End => (end === "1" ? "0" : "1");
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The end both waves are headed for, which is the one they are not resting at.
|
|
37
|
+
* Every visit runs them from the one to the other.
|
|
38
|
+
*/
|
|
39
|
+
export const target = (way: Way): End => (way === "-1" ? "0" : "1");
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A pointer arriving: leading wave to the far end, trailing wave home.
|
|
43
|
+
*
|
|
44
|
+
* Both, every time, although one of the two is always already there. On a
|
|
45
|
+
* fresh visit the trailing wave is home and only the leading one moves, which
|
|
46
|
+
* opens the band; mid-band the leading one has arrived and only the trailing
|
|
47
|
+
* one moves, which sends the weight back in behind the pointer. Saying only
|
|
48
|
+
* the half that is needed means guessing which half that is, and a guess that
|
|
49
|
+
* misses writes a value already in place, which does nothing at all.
|
|
50
|
+
*/
|
|
51
|
+
export const entered = (waves: Waves): Waves => ({
|
|
52
|
+
...waves,
|
|
53
|
+
in: target(waves.way),
|
|
54
|
+
out: other(target(waves.way)),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** A pointer leaving: the trailing wave after the leading one, to the same end. */
|
|
58
|
+
export const left = (waves: Waves): Waves => ({ ...waves, out: waves.in });
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The pair turned round, ready for the next visit.
|
|
62
|
+
*
|
|
63
|
+
* Only where the two were sent to the same end, which is what a band that has
|
|
64
|
+
* come off leaves behind and the only state this may read. A pointer sitting
|
|
65
|
+
* on the text holds them at opposite ends, and turning the pair round under
|
|
66
|
+
* that would let go of all of it at once.
|
|
67
|
+
*/
|
|
68
|
+
export const settled = (waves: Waves): Waves =>
|
|
69
|
+
waves.in === waves.out ? { ...waves, way: waves.in === "1" ? "-1" : "1" } : waves;
|
|
70
|
+
|
|
71
|
+
/** What the text weighs once both waves have arrived: 1 all of it, 0 none. */
|
|
72
|
+
export const weight = (waves: Waves): number =>
|
|
73
|
+
(Number(waves.in) - Number(waves.out)) * Number(waves.way);
|