animate-text-weight 1.0.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
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface WaveNavOptions {
|
|
2
|
+
/** Toggled on the current link, as well as aria-current. */
|
|
3
|
+
linkClass?: string;
|
|
4
|
+
/** Toggled on the current link's section, which aria-current cannot mark. */
|
|
5
|
+
sectionClass?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Mark the link of the section across the middle of the screen with
|
|
9
|
+
* aria-current="true". Each link's section is the element its hash names;
|
|
10
|
+
* a link without one is left out. Returns a function that stops following.
|
|
11
|
+
*/
|
|
12
|
+
export declare function waveNav(links: Iterable<HTMLAnchorElement>, options?: WaveNavOptions): () => void;
|
package/dist/wave-nav.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
import { UNIT } from "./index.js";
|
|
18
|
+
/** At least a frame per step, so no link goes on and off before it is drawn. */
|
|
19
|
+
const MIN_STEP = 17;
|
|
20
|
+
/**
|
|
21
|
+
* Mark the link of the section across the middle of the screen with
|
|
22
|
+
* aria-current="true". Each link's section is the element its hash names;
|
|
23
|
+
* a link without one is left out. Returns a function that stops following.
|
|
24
|
+
*/
|
|
25
|
+
export function waveNav(links, options = {}) {
|
|
26
|
+
const pairs = [...links].flatMap((link) => {
|
|
27
|
+
const section = document.getElementById(link.hash.slice(1));
|
|
28
|
+
return section ? [{ link, section }] : [];
|
|
29
|
+
});
|
|
30
|
+
function setCurrent(index, on) {
|
|
31
|
+
const pair = pairs[index];
|
|
32
|
+
if (!pair)
|
|
33
|
+
return;
|
|
34
|
+
if (on)
|
|
35
|
+
pair.link.setAttribute("aria-current", "true");
|
|
36
|
+
else
|
|
37
|
+
pair.link.removeAttribute("aria-current");
|
|
38
|
+
if (options.linkClass)
|
|
39
|
+
pair.link.classList.toggle(options.linkClass, on);
|
|
40
|
+
if (options.sectionClass)
|
|
41
|
+
pair.section.classList.toggle(options.sectionClass, on);
|
|
42
|
+
}
|
|
43
|
+
/** The section across the middle, by index: -1 above the first, the length past the last. */
|
|
44
|
+
function getIndex() {
|
|
45
|
+
const middle = innerHeight / 2;
|
|
46
|
+
const below = pairs.findIndex(({ section }) => section.getBoundingClientRect().bottom > middle);
|
|
47
|
+
if (below === -1)
|
|
48
|
+
return pairs.length;
|
|
49
|
+
return pairs[below].section.getBoundingClientRect().top <= middle
|
|
50
|
+
? below
|
|
51
|
+
: below - 1;
|
|
52
|
+
}
|
|
53
|
+
/** How long the wave takes to cross a link, in ms: its delay once per unit. */
|
|
54
|
+
function getCrossing(link) {
|
|
55
|
+
const delay = getComputedStyle(link)
|
|
56
|
+
.getPropertyValue("--animate-weight-delay")
|
|
57
|
+
.trim();
|
|
58
|
+
const milliseconds = parseFloat(delay) * (delay.endsWith("ms") ? 1 : 1000);
|
|
59
|
+
return link.querySelectorAll(`.${UNIT}`).length * milliseconds || 0;
|
|
60
|
+
}
|
|
61
|
+
let current = getIndex();
|
|
62
|
+
let target = current;
|
|
63
|
+
// The next step, while the wave is still crossing the last link.
|
|
64
|
+
let timer = 0;
|
|
65
|
+
setCurrent(current, true);
|
|
66
|
+
/** One section nearer the target, and the next once the wave is across. */
|
|
67
|
+
function update() {
|
|
68
|
+
timer = 0;
|
|
69
|
+
if (current === target)
|
|
70
|
+
return;
|
|
71
|
+
const next = current + Math.sign(target - current);
|
|
72
|
+
setCurrent(current, false);
|
|
73
|
+
setCurrent(next, true);
|
|
74
|
+
current = next;
|
|
75
|
+
const link = pairs[next]?.link;
|
|
76
|
+
timer = setTimeout(update, Math.max(MIN_STEP, link ? getCrossing(link) : 0));
|
|
77
|
+
}
|
|
78
|
+
function handleMove() {
|
|
79
|
+
target = getIndex();
|
|
80
|
+
if (!timer)
|
|
81
|
+
update();
|
|
82
|
+
}
|
|
83
|
+
// Scroll events already come once a frame. A section that changes height
|
|
84
|
+
// moves the ones below it without a scroll, and a resize moves the middle.
|
|
85
|
+
addEventListener("scroll", handleMove, { passive: true });
|
|
86
|
+
addEventListener("resize", handleMove);
|
|
87
|
+
const observer = new ResizeObserver(handleMove);
|
|
88
|
+
for (const { section } of pairs)
|
|
89
|
+
observer.observe(section);
|
|
90
|
+
return () => {
|
|
91
|
+
removeEventListener("scroll", handleMove);
|
|
92
|
+
removeEventListener("resize", handleMove);
|
|
93
|
+
observer.disconnect();
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
};
|
|
96
|
+
}
|
package/dist/waves.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
/** An end of the text, the way the stylesheet counts: 0 the start, 1 the far one. */
|
|
19
|
+
export type End = "0" | "1";
|
|
20
|
+
/** Which way round the pair is running: up from the start, or back down. */
|
|
21
|
+
export type Way = "1" | "-1";
|
|
22
|
+
/** Where each wave is headed, and which way round the two of them are going. */
|
|
23
|
+
export interface Waves {
|
|
24
|
+
in: End;
|
|
25
|
+
out: End;
|
|
26
|
+
way: Way;
|
|
27
|
+
}
|
|
28
|
+
/** The other one of the two. */
|
|
29
|
+
export declare const other: (end: End) => End;
|
|
30
|
+
/**
|
|
31
|
+
* The end both waves are headed for, which is the one they are not resting at.
|
|
32
|
+
* Every visit runs them from the one to the other.
|
|
33
|
+
*/
|
|
34
|
+
export declare const target: (way: Way) => End;
|
|
35
|
+
/**
|
|
36
|
+
* A pointer arriving: leading wave to the far end, trailing wave home.
|
|
37
|
+
*
|
|
38
|
+
* Both, every time, although one of the two is always already there. On a
|
|
39
|
+
* fresh visit the trailing wave is home and only the leading one moves, which
|
|
40
|
+
* opens the band; mid-band the leading one has arrived and only the trailing
|
|
41
|
+
* one moves, which sends the weight back in behind the pointer. Saying only
|
|
42
|
+
* the half that is needed means guessing which half that is, and a guess that
|
|
43
|
+
* misses writes a value already in place, which does nothing at all.
|
|
44
|
+
*/
|
|
45
|
+
export declare const entered: (waves: Waves) => Waves;
|
|
46
|
+
/** A pointer leaving: the trailing wave after the leading one, to the same end. */
|
|
47
|
+
export declare const left: (waves: Waves) => Waves;
|
|
48
|
+
/**
|
|
49
|
+
* The pair turned round, ready for the next visit.
|
|
50
|
+
*
|
|
51
|
+
* Only where the two were sent to the same end, which is what a band that has
|
|
52
|
+
* come off leaves behind and the only state this may read. A pointer sitting
|
|
53
|
+
* on the text holds them at opposite ends, and turning the pair round under
|
|
54
|
+
* that would let go of all of it at once.
|
|
55
|
+
*/
|
|
56
|
+
export declare const settled: (waves: Waves) => Waves;
|
|
57
|
+
/** What the text weighs once both waves have arrived: 1 all of it, 0 none. */
|
|
58
|
+
export declare const weight: (waves: Waves) => number;
|
package/dist/waves.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
/** The other one of the two. */
|
|
19
|
+
export const other = (end) => (end === "1" ? "0" : "1");
|
|
20
|
+
/**
|
|
21
|
+
* The end both waves are headed for, which is the one they are not resting at.
|
|
22
|
+
* Every visit runs them from the one to the other.
|
|
23
|
+
*/
|
|
24
|
+
export const target = (way) => (way === "-1" ? "0" : "1");
|
|
25
|
+
/**
|
|
26
|
+
* A pointer arriving: leading wave to the far end, trailing wave home.
|
|
27
|
+
*
|
|
28
|
+
* Both, every time, although one of the two is always already there. On a
|
|
29
|
+
* fresh visit the trailing wave is home and only the leading one moves, which
|
|
30
|
+
* opens the band; mid-band the leading one has arrived and only the trailing
|
|
31
|
+
* one moves, which sends the weight back in behind the pointer. Saying only
|
|
32
|
+
* the half that is needed means guessing which half that is, and a guess that
|
|
33
|
+
* misses writes a value already in place, which does nothing at all.
|
|
34
|
+
*/
|
|
35
|
+
export const entered = (waves) => ({
|
|
36
|
+
...waves,
|
|
37
|
+
in: target(waves.way),
|
|
38
|
+
out: other(target(waves.way)),
|
|
39
|
+
});
|
|
40
|
+
/** A pointer leaving: the trailing wave after the leading one, to the same end. */
|
|
41
|
+
export const left = (waves) => ({ ...waves, out: waves.in });
|
|
42
|
+
/**
|
|
43
|
+
* The pair turned round, ready for the next visit.
|
|
44
|
+
*
|
|
45
|
+
* Only where the two were sent to the same end, which is what a band that has
|
|
46
|
+
* come off leaves behind and the only state this may read. A pointer sitting
|
|
47
|
+
* on the text holds them at opposite ends, and turning the pair round under
|
|
48
|
+
* that would let go of all of it at once.
|
|
49
|
+
*/
|
|
50
|
+
export const settled = (waves) => waves.in === waves.out ? { ...waves, way: waves.in === "1" ? "-1" : "1" } : waves;
|
|
51
|
+
/** What the text weighs once both waves have arrived: 1 all of it, 0 none. */
|
|
52
|
+
export const weight = (waves) => (Number(waves.in) - Number(waves.out)) * Number(waves.way);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "animate-text-weight",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Animate short text between two weights of a variable font without moving a single line.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tim Schoch",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"default": "./dist/index.js"
|
|
16
16
|
},
|
|
17
|
+
"./wave-nav": {
|
|
18
|
+
"types": "./dist/wave-nav.d.ts",
|
|
19
|
+
"default": "./dist/wave-nav.js"
|
|
20
|
+
},
|
|
17
21
|
"./style.css": "./src/animateTextWeight.css"
|
|
18
22
|
},
|
|
19
23
|
"files": [
|
|
@@ -22,13 +26,17 @@
|
|
|
22
26
|
],
|
|
23
27
|
"scripts": {
|
|
24
28
|
"build": "tsc",
|
|
29
|
+
"test": "tsc && node --test test/band.test.mjs test/waves.test.mjs",
|
|
30
|
+
"test:browser": "tsc && node --test test/*.browser.test.mjs",
|
|
25
31
|
"prepare": "husky && tsc",
|
|
26
|
-
"demo": "tsc && npx -y serve -l 3000 ."
|
|
32
|
+
"demo": "tsc && npx -y serve -l 3000 .",
|
|
33
|
+
"compare": "tsc && node scripts/compare.mjs"
|
|
27
34
|
},
|
|
28
35
|
"devDependencies": {
|
|
29
36
|
"husky": "^9.1.7",
|
|
30
37
|
"lint-staged": "^17.5.0",
|
|
31
38
|
"prettier": "^3.9.6",
|
|
39
|
+
"puppeteer-core": "^25.12.0",
|
|
32
40
|
"typescript": "^6.0.3"
|
|
33
41
|
}
|
|
34
42
|
}
|
|
@@ -7,9 +7,21 @@
|
|
|
7
7
|
* --animate-weight-to: 700; the weight it animates to
|
|
8
8
|
* --animate-weight: true; on any state or ancestor: switch it on
|
|
9
9
|
*
|
|
10
|
-
* And
|
|
10
|
+
* And three more if you want the text to arrive in a wave:
|
|
11
11
|
* --animate-weight-by: character; one unit per character, not per line
|
|
12
12
|
* --animate-weight-delay: 40ms; one unit waits this long after the one before it
|
|
13
|
+
* --animate-weight-direction: end; the wave runs the other way. Or
|
|
14
|
+
* pointer, or scroll, or both: the
|
|
15
|
+
* script points it where the pointer or
|
|
16
|
+
* the page came from, every time it
|
|
17
|
+
* moves, and the last to move has the
|
|
18
|
+
* say.
|
|
19
|
+
*
|
|
20
|
+
* And two if you want the color to travel with the weight:
|
|
21
|
+
* --animate-weight-color-from: #1c1c1c; the color it animates from,
|
|
22
|
+
* else the text color
|
|
23
|
+
* --animate-weight-color-to: #c8102e; the color it animates to,
|
|
24
|
+
* else the text color
|
|
13
25
|
*
|
|
14
26
|
* The script (index.ts) splits the element into one span per rendered
|
|
15
27
|
* line, class .animate-text-weight-line, and in character mode splits each
|
|
@@ -61,12 +73,122 @@
|
|
|
61
73
|
inherits: true;
|
|
62
74
|
initial-value: 0s;
|
|
63
75
|
}
|
|
76
|
+
/*
|
|
77
|
+
* --animate-weight-direction is which end of the line the wave starts at: the
|
|
78
|
+
* one the text starts at, or the one it ends at. Logical, so right-to-left
|
|
79
|
+
* text waves right-to-left; in line mode the ends are the first and the last
|
|
80
|
+
* line instead.
|
|
81
|
+
*
|
|
82
|
+
* pointer and scroll hand the choice to the script. Name both and the script
|
|
83
|
+
* watches both, and whichever moved last has the say.
|
|
84
|
+
*
|
|
85
|
+
* It is deliberately not registered with @property. A registration takes one
|
|
86
|
+
* syntax, and the value is either one of four words or two of them at once;
|
|
87
|
+
* the loose "<custom-ident>+" that admits the pair is not what the style query
|
|
88
|
+
* further down compares against. Left unregistered, the query compares the
|
|
89
|
+
* words themselves, which is all this ever asked for.
|
|
90
|
+
*
|
|
91
|
+
* --atw-flip-in and --atw-flip-out are what the waits actually read: 0 counts
|
|
92
|
+
* from the start of the text, 1 counts from the far end. Numbers rather than
|
|
93
|
+
* words, and inherited rather than queried, because the script writes them
|
|
94
|
+
* while the pointer is already arriving. An inherited value reaches the units
|
|
95
|
+
* in the same pass that turns :hover on; a container query reaches them one
|
|
96
|
+
* pass later, by which time the visit runs the old way round.
|
|
97
|
+
*/
|
|
98
|
+
@property --atw-flip-in {
|
|
99
|
+
syntax: "<number>";
|
|
100
|
+
inherits: true;
|
|
101
|
+
initial-value: 0;
|
|
102
|
+
}
|
|
103
|
+
@property --atw-flip-out {
|
|
104
|
+
syntax: "<number>";
|
|
105
|
+
inherits: true;
|
|
106
|
+
initial-value: 0;
|
|
107
|
+
}
|
|
108
|
+
/*
|
|
109
|
+
* The two waves. Each is 0 before it has started and 1 once it has passed
|
|
110
|
+
* everything, and a unit is at full weight between the one and the other:
|
|
111
|
+
* --atw-wave-in brings the weight up, --atw-wave-out takes it away again.
|
|
112
|
+
*
|
|
113
|
+
* Two, so that the two fronts can be on the text at the same time. One wave
|
|
114
|
+
* has to arrive everywhere before it can turn round, which leaves the text
|
|
115
|
+
* sitting at full weight for a whole crossing while the far end waits its
|
|
116
|
+
* turn. Two waves let the second follow the first at a fixed distance, and
|
|
117
|
+
* what crosses the text is a band of weight rather than a tide that comes in
|
|
118
|
+
* and then goes out.
|
|
119
|
+
*
|
|
120
|
+
* One transition per line carries each, and every unit works its own share out
|
|
121
|
+
* of them. That is the whole reason they exist. A delay per unit would mean a
|
|
122
|
+
* unit that has not started yet has nothing running to turn round — switch a
|
|
123
|
+
* wave off while it is still waiting and the browser drops it where it stands,
|
|
124
|
+
* and that letter never moves at all. Brush past a link, or scroll a heading
|
|
125
|
+
* out of view mid-wave, and the wave breaks apart. Nothing here ever waits:
|
|
126
|
+
* every unit is reading numbers that are already moving, so a wave can be
|
|
127
|
+
* turned round at any moment and comes apart at none.
|
|
128
|
+
*/
|
|
129
|
+
@property --atw-wave-in {
|
|
130
|
+
syntax: "<number>";
|
|
131
|
+
inherits: true;
|
|
132
|
+
initial-value: 0;
|
|
133
|
+
}
|
|
134
|
+
@property --atw-wave-out {
|
|
135
|
+
syntax: "<number>";
|
|
136
|
+
inherits: true;
|
|
137
|
+
initial-value: 0;
|
|
138
|
+
}
|
|
139
|
+
/* A unit's share of each wave, which the weight below is the gap between. */
|
|
140
|
+
@property --atw-rise {
|
|
141
|
+
syntax: "<number>";
|
|
142
|
+
inherits: false;
|
|
143
|
+
initial-value: 0;
|
|
144
|
+
}
|
|
145
|
+
@property --atw-fall {
|
|
146
|
+
syntax: "<number>";
|
|
147
|
+
inherits: false;
|
|
148
|
+
initial-value: 0;
|
|
149
|
+
}
|
|
150
|
+
/*
|
|
151
|
+
* Which way round the two waves are running: 1 up, -1 back down.
|
|
152
|
+
*
|
|
153
|
+
* A visit leaves both waves parked at the far end from the one it started at,
|
|
154
|
+
* and the next one runs them home again. So nothing is ever put back: the
|
|
155
|
+
* weight is the gap between the two waves whichever way they travel, and this
|
|
156
|
+
* is only the sign that keeps the gap positive. Putting them back instead
|
|
157
|
+
* would mean a second crossing of nothing to look at between every two visits.
|
|
158
|
+
*/
|
|
159
|
+
@property --atw-way {
|
|
160
|
+
syntax: "<number>";
|
|
161
|
+
inherits: true;
|
|
162
|
+
initial-value: 1;
|
|
163
|
+
}
|
|
164
|
+
/*
|
|
165
|
+
* The page's switch again, as a number that can be transitioned, so that the
|
|
166
|
+
* script hears it change. A pointer-following element's waves are the script's
|
|
167
|
+
* to send, and a hover is not the only thing that switches it: a class or the
|
|
168
|
+
* page can hold it on while the pointer comes and goes. The transition is a
|
|
169
|
+
* doorbell and nothing reads the value.
|
|
170
|
+
*/
|
|
171
|
+
@property --atw-on {
|
|
172
|
+
syntax: "<number>";
|
|
173
|
+
inherits: false;
|
|
174
|
+
initial-value: 0;
|
|
175
|
+
}
|
|
64
176
|
/* How many delays this unit waits: the count of the one before it, plus one. */
|
|
65
177
|
@property --atw-delay-count {
|
|
66
178
|
syntax: "<number>";
|
|
67
179
|
inherits: false;
|
|
68
180
|
initial-value: 0;
|
|
69
181
|
}
|
|
182
|
+
/*
|
|
183
|
+
* The count of the last unit of the wave, which is how long the wave is. Set
|
|
184
|
+
* on whatever holds the units — the line in character mode, the element in
|
|
185
|
+
* line mode — and read by each of them to count from the far end.
|
|
186
|
+
*/
|
|
187
|
+
@property --atw-delay-total {
|
|
188
|
+
syntax: "<number>";
|
|
189
|
+
inherits: true;
|
|
190
|
+
initial-value: 0;
|
|
191
|
+
}
|
|
70
192
|
/* The width a line is held at. */
|
|
71
193
|
@property --atw-width {
|
|
72
194
|
syntax: "<length>";
|
|
@@ -76,10 +198,47 @@
|
|
|
76
198
|
|
|
77
199
|
:root {
|
|
78
200
|
--animate-weight-duration: 300ms;
|
|
79
|
-
|
|
201
|
+
/*
|
|
202
|
+
* Flat, because this shapes the wave's travel and not one unit's own change.
|
|
203
|
+
* A curve that is slow at both its ends crawls into the top and crawls back
|
|
204
|
+
* out of it, and the two together read as a pause at full weight.
|
|
205
|
+
*/
|
|
206
|
+
--animate-weight-easing: linear;
|
|
207
|
+
/*
|
|
208
|
+
* The least the trailing wave ever follows the leading one by. Without a
|
|
209
|
+
* floor, a link brushed past in a moment gets a band a letter wide and shows
|
|
210
|
+
* nothing; with one, the pointer can be long gone and the band still crosses.
|
|
211
|
+
*/
|
|
212
|
+
--atw-wave-min-delay: 400ms;
|
|
80
213
|
}
|
|
81
214
|
|
|
82
215
|
.animate-text-weight-line {
|
|
216
|
+
/*
|
|
217
|
+
* How long the whole wave takes: the last unit's wait, plus the time that
|
|
218
|
+
* unit then spends moving. Never zero, or the wave below would have nothing
|
|
219
|
+
* to cross — reduced motion takes both of these to 0s.
|
|
220
|
+
*/
|
|
221
|
+
--atw-span: max(
|
|
222
|
+
1ms,
|
|
223
|
+
calc(var(--atw-delay-total) * var(--animate-weight-delay) + var(--animate-weight-duration))
|
|
224
|
+
);
|
|
225
|
+
/*
|
|
226
|
+
* The script's own switches, in their own properties rather than in
|
|
227
|
+
* --animate-weight, because the page writes that one and the two would take
|
|
228
|
+
* it off each other. Unset, the leading wave is the page's to switch and the
|
|
229
|
+
* trailing one stays put; set, they say outright where each wave is headed.
|
|
230
|
+
*/
|
|
231
|
+
--atw-wave-in: var(--atw-in, 0);
|
|
232
|
+
--atw-wave-out: var(--atw-out, 0);
|
|
233
|
+
/*
|
|
234
|
+
* The two moving parts. The trailing wave's delay is how far it follows the
|
|
235
|
+
* leading one by, which the script works out per visit and writes; a page
|
|
236
|
+
* that never leaves it alone never sets it, and the two run together.
|
|
237
|
+
*/
|
|
238
|
+
transition:
|
|
239
|
+
--atw-wave-in var(--atw-span) var(--animate-weight-easing),
|
|
240
|
+
--atw-wave-out var(--atw-span) var(--animate-weight-easing) var(--atw-out-wait, 0s),
|
|
241
|
+
--atw-on 1ms;
|
|
83
242
|
/* A line can get wider or narrower, never reflow. */
|
|
84
243
|
white-space: nowrap;
|
|
85
244
|
/* Fractional glyph advances, so Firefox does not snap letters mid-tween. */
|
|
@@ -166,14 +325,103 @@
|
|
|
166
325
|
/*
|
|
167
326
|
* Clamped, because an easing may overshoot: the spacing below stops at the
|
|
168
327
|
* last stop, so an unclamped weight would keep going and break the width.
|
|
328
|
+
*
|
|
329
|
+
* Rounded to a whole weight, because Chrome (153, macOS, 2x screen) now and
|
|
330
|
+
* then draws a letter whose weight is a fresh fraction every frame with the
|
|
331
|
+
* wrong one for a single frame: mid-tween it flashes bold or light and is
|
|
332
|
+
* back the next. Plain spans tweening font-weight do it too, so it is not
|
|
333
|
+
* the waves. Whole weights are a few hundred values, the same ones every
|
|
334
|
+
* visit, and one weight is far below what the eye or the width can tell.
|
|
335
|
+
*/
|
|
336
|
+
font-weight: round(
|
|
337
|
+
calc(
|
|
338
|
+
var(--atw-from) + (var(--atw-to) - var(--atw-from)) *
|
|
339
|
+
clamp(0, var(--animate-weight-progress), 1)
|
|
340
|
+
),
|
|
341
|
+
1
|
|
342
|
+
);
|
|
343
|
+
/*
|
|
344
|
+
* This unit's own share of each wave, worked out afresh every frame rather
|
|
345
|
+
* than transitioned: how far that wave has come, less what this unit waits
|
|
346
|
+
* for it, over the time this unit then takes.
|
|
347
|
+
*
|
|
348
|
+
* A flip of 0 leaves the count where it is; a flip of 1 turns it into
|
|
349
|
+
* (total - count), which is the same wave counted from the far end. Read
|
|
350
|
+
* here, in the open, so a wave already running turns round when its flip
|
|
351
|
+
* does — nothing is latched at the moment a tween starts, because no tween
|
|
352
|
+
* starts here.
|
|
353
|
+
*/
|
|
354
|
+
--atw-wait-in: calc(
|
|
355
|
+
(
|
|
356
|
+
var(--atw-delay-count) + var(--atw-flip-in) *
|
|
357
|
+
(var(--atw-delay-total) - 2 * var(--atw-delay-count))
|
|
358
|
+
) * var(--animate-weight-delay)
|
|
359
|
+
);
|
|
360
|
+
--atw-wait-out: calc(
|
|
361
|
+
(
|
|
362
|
+
var(--atw-delay-count) + var(--atw-flip-out) *
|
|
363
|
+
(var(--atw-delay-total) - 2 * var(--atw-delay-count))
|
|
364
|
+
) * var(--animate-weight-delay)
|
|
365
|
+
);
|
|
366
|
+
/*
|
|
367
|
+
* tan(atan2(a, b)) is a / b. A time over a time is typed arithmetic, which
|
|
368
|
+
* Firefox does not ship yet: it drops the division silently and nothing
|
|
369
|
+
* moves. atan2 takes two times and tan hands back a plain number.
|
|
169
370
|
*/
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
371
|
+
--atw-rise: clamp(
|
|
372
|
+
0,
|
|
373
|
+
tan(
|
|
374
|
+
atan2(
|
|
375
|
+
var(--atw-wave-in) * var(--atw-span) - var(--atw-wait-in),
|
|
376
|
+
max(1ms, var(--animate-weight-duration))
|
|
377
|
+
)
|
|
378
|
+
),
|
|
379
|
+
1
|
|
380
|
+
);
|
|
381
|
+
--atw-fall: clamp(
|
|
382
|
+
0,
|
|
383
|
+
tan(
|
|
384
|
+
atan2(
|
|
385
|
+
var(--atw-wave-out) * var(--atw-span) - var(--atw-wait-out),
|
|
386
|
+
max(1ms, var(--animate-weight-duration))
|
|
387
|
+
)
|
|
388
|
+
),
|
|
389
|
+
1
|
|
390
|
+
);
|
|
391
|
+
/*
|
|
392
|
+
* Full weight between the one wave and the other, and --atw-way to keep that
|
|
393
|
+
* gap the right way up when the pair is running back down.
|
|
394
|
+
*
|
|
395
|
+
* Clamped low as well as high, because the two waves need not run the same
|
|
396
|
+
* way round, and a unit the trailing wave reaches before the leading one
|
|
397
|
+
* would otherwise go past light and out the other side.
|
|
398
|
+
*/
|
|
399
|
+
--animate-weight-progress: clamp(
|
|
400
|
+
0,
|
|
401
|
+
calc((var(--atw-rise) - var(--atw-fall)) * var(--atw-way)),
|
|
402
|
+
1
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/*
|
|
407
|
+
* The color, if the element asks for one. An end left unset is the text color.
|
|
408
|
+
*
|
|
409
|
+
* It is read off the unit's own progress rather than transitioned itself, so
|
|
410
|
+
* it takes the curve, the duration and the delay the weight already has, and
|
|
411
|
+
* in character mode it arrives as the same wave. oklab because that is the
|
|
412
|
+
* space a plain color transition interpolates in.
|
|
413
|
+
*
|
|
414
|
+
* Not registered with @property, on purpose: a registration needs an
|
|
415
|
+
* initial-value, and that would hand every unit two colors no page asked for.
|
|
416
|
+
* The fallback is currentColor, which inside color is the inherited color, so
|
|
417
|
+
* an element that sets neither mixes its own color with itself and keeps it.
|
|
418
|
+
*/
|
|
419
|
+
.animate-text-weight-unit {
|
|
420
|
+
color: color-mix(
|
|
421
|
+
in oklab,
|
|
422
|
+
var(--animate-weight-color-to, currentColor) calc(clamp(0, var(--animate-weight-progress), 1) * 100%),
|
|
423
|
+
var(--animate-weight-color-from, currentColor)
|
|
173
424
|
);
|
|
174
|
-
transition: --animate-weight-progress var(--animate-weight-duration) var(--animate-weight-easing);
|
|
175
|
-
/* After the shorthand, which would reset it. */
|
|
176
|
-
transition-delay: calc(var(--atw-delay-count) * var(--animate-weight-delay));
|
|
177
425
|
}
|
|
178
426
|
|
|
179
427
|
/*
|
|
@@ -198,10 +446,40 @@
|
|
|
198
446
|
);
|
|
199
447
|
}
|
|
200
448
|
|
|
201
|
-
/*
|
|
449
|
+
/*
|
|
450
|
+
* The page's switch, which comes after the line's own rule so that it wins
|
|
451
|
+
* while the page holds it on. The element is the line's style container.
|
|
452
|
+
*
|
|
453
|
+
* --atw-in still ahead of it, so that a script following the pointer keeps the
|
|
454
|
+
* last word either way: it hears the switch through --atw-on and sends the
|
|
455
|
+
* waves itself, so a band coming off is never cut short by the switch.
|
|
456
|
+
*/
|
|
202
457
|
@container style(--animate-weight: true) {
|
|
458
|
+
.animate-text-weight-line {
|
|
459
|
+
--atw-wave-in: var(--atw-in, 1);
|
|
460
|
+
--atw-on: 1;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/*
|
|
465
|
+
* The wave the other way round: a unit waits for the ones behind it instead of
|
|
466
|
+
* the ones in front of it, so the last one moves first. The counts are not
|
|
467
|
+
* rewritten for this — they are read backwards from the far end, which is what
|
|
468
|
+
* --atw-delay-total holds. So direction can change at any time, on a state or
|
|
469
|
+
* a media query, and nothing has to be measured again.
|
|
470
|
+
*
|
|
471
|
+
* This is the stylesheet's own end, and only that. A separate property,
|
|
472
|
+
* because the script must not overwrite the word the stylesheet asked for —
|
|
473
|
+
* pointer and scroll write the flips on the element instead, where they reach
|
|
474
|
+
* the units by inheritance and this rule never matches.
|
|
475
|
+
*
|
|
476
|
+
* Both waves, because a page that fixes an end wants the whole thing that way
|
|
477
|
+
* round; the two only part company when the pointer sends them.
|
|
478
|
+
*/
|
|
479
|
+
@container style(--animate-weight-direction: end) {
|
|
203
480
|
.animate-text-weight-unit {
|
|
204
|
-
--
|
|
481
|
+
--atw-flip-in: 1;
|
|
482
|
+
--atw-flip-out: 1;
|
|
205
483
|
}
|
|
206
484
|
}
|
|
207
485
|
|