kuinetic 0.1.2 → 0.1.4
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/README.md +3 -3
- package/dist/esm/{chunk-WPU33HDL.mjs → chunk-7WMNPIOZ.mjs} +1724 -240
- package/dist/esm/{chunk-LWS4OSLX.mjs → chunk-JT4PZL3A.mjs} +50 -7
- package/dist/esm/{chunk-5EVG7CIY.mjs → chunk-TJDDIQRG.mjs} +42 -16
- package/dist/esm/core/index.mjs +2 -2
- package/dist/esm/effects/index.mjs +2 -2
- package/dist/esm/index.mjs +3 -3
- package/dist/kuinetic.all.js +1813 -262
- package/dist/kuinetic.css +1491 -94
- package/dist/kuinetic.js +1812 -261
- package/dist/types/core/instances.d.ts +34 -3
- package/dist/types/core/path-morph.d.ts +19 -0
- package/dist/types/core/style-plan.d.ts +6 -1
- package/dist/types/core/types.d.ts +99 -1
- package/dist/types/effects/catalog/background-media.d.ts +152 -0
- package/dist/types/effects/catalog/index.d.ts +0 -6
- package/dist/types/effects/catalog/media-shared.d.ts +124 -0
- package/dist/types/effects/catalog/media.d.ts +4 -0
- package/dist/types/effects/catalog/numbers-shared.d.ts +5 -1
- package/dist/types/effects/catalog/shared.d.ts +2 -3
- package/dist/types/effects/catalog/subtree-capture.d.ts +23 -0
- package/dist/types/effects/catalog/text-shared.d.ts +11 -2
- package/dist/types/effects/gestures/index.d.ts +0 -1
- package/dist/types/effects/layout/index.d.ts +0 -2
- package/dist/types/effects/scroll-mechanics/index.d.ts +0 -4
- package/dist/types/effects/scroll-mechanics/scroll-spy.d.ts +26 -0
- package/dist/types/effects/scroll-mechanics/tracker.d.ts +61 -3
- package/dist/types/effects/shared.d.ts +40 -0
- package/dist/types/effects/step-marking.d.ts +99 -0
- package/dist/types/effects/three-d/index.d.ts +0 -7
- package/package.json +9 -2
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
CHANNEL,
|
|
4
4
|
Registry,
|
|
5
5
|
clamp01,
|
|
6
|
+
continuousSetup,
|
|
6
7
|
createAttributeLedger,
|
|
7
8
|
createMeasureCache,
|
|
8
9
|
createStyleLedger,
|
|
@@ -11,12 +12,16 @@ import {
|
|
|
11
12
|
inertInstance,
|
|
12
13
|
isSameOriginPath,
|
|
13
14
|
toPixels
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-JT4PZL3A.mjs";
|
|
15
16
|
|
|
16
17
|
// src/effects/shared.ts
|
|
18
|
+
var TRIGGER_DELAY_PARAM = {
|
|
19
|
+
delay: { type: "time", default: "0ms", cssProperty: "--kui-delay" }
|
|
20
|
+
};
|
|
21
|
+
var TIMELINE_AGNOSTIC = ["time", "view", "scroll", "pin"];
|
|
17
22
|
var COMMON = {
|
|
18
23
|
duration: { type: "time", default: "600ms", cssProperty: "--kui-duration" },
|
|
19
|
-
|
|
24
|
+
...TRIGGER_DELAY_PARAM,
|
|
20
25
|
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
|
|
21
26
|
stagger: { type: "time", default: "0ms", cssProperty: "--kui-stagger" }
|
|
22
27
|
};
|
|
@@ -44,13 +49,27 @@ function cssPrimitive(id, channels, options = {}) {
|
|
|
44
49
|
// src/effects/catalog/ambient.ts
|
|
45
50
|
var drift = {
|
|
46
51
|
duration: { type: "time", default: "10s", cssProperty: "--kui-duration" },
|
|
47
|
-
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" }
|
|
52
|
+
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
|
|
53
|
+
from: { type: "color", default: "", cssProperty: "--kui-ambient-c1" },
|
|
54
|
+
to: { type: "color", default: "", cssProperty: "--kui-ambient-c2" }
|
|
55
|
+
};
|
|
56
|
+
var tint = {
|
|
57
|
+
duration: { type: "time", default: "10s", cssProperty: "--kui-duration" },
|
|
58
|
+
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
|
|
59
|
+
from: { type: "color", default: "", cssProperty: "--kui-ambient-c1" }
|
|
48
60
|
};
|
|
49
61
|
var float = {
|
|
50
62
|
duration: { type: "time", default: "4s", cssProperty: "--kui-duration" },
|
|
51
63
|
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
|
|
52
64
|
distance: { type: "length", default: "14px", cssProperty: "--kui-distance" }
|
|
53
65
|
};
|
|
66
|
+
var orbit = {
|
|
67
|
+
duration: { type: "time", default: "3.5s", cssProperty: "--kui-duration" },
|
|
68
|
+
// `linear` by default, unlike every other ambient primitive: a continuous rotation that eases
|
|
69
|
+
// visibly stutters once per revolution, because the ease restarts at each iteration boundary.
|
|
70
|
+
ease: { type: "easing", default: "linear", cssProperty: "--kui-ease" },
|
|
71
|
+
angle: { type: "angle", default: "360deg", cssProperty: "--kui-to-angle" }
|
|
72
|
+
};
|
|
54
73
|
var pulse = {
|
|
55
74
|
duration: { type: "time", default: "2.2s", cssProperty: "--kui-duration" },
|
|
56
75
|
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
|
|
@@ -63,12 +82,46 @@ var AMBIENT_PRIMITIVES = [
|
|
|
63
82
|
reducedMotion: "disable",
|
|
64
83
|
perfClass: "continuous"
|
|
65
84
|
}),
|
|
85
|
+
/*
|
|
86
|
+
* `gradient-rotate-border` and `gradient-border` are not background fills, and a second
|
|
87
|
+
* primitive is how this catalog says so — the same reason `ambient-tint` exists beside
|
|
88
|
+
* `ambient-gradient` above. Channels are per-primitive, so a different channel set means a
|
|
89
|
+
* different primitive; folding these into `ambient-gradient` would make `gradient-mesh` and
|
|
90
|
+
* `aurora` claim a mask and a box they never touch, and `aurora, pin` would start reporting a
|
|
91
|
+
* conflict that isn't there.
|
|
92
|
+
*
|
|
93
|
+
* What the ring rules actually write (`ambient.css`) beyond the gradient: `mask` +
|
|
94
|
+
* `mask-composite`, which subtract the element's own content box to leave a ring — the same
|
|
95
|
+
* physical property `media-mask` claims under the `'mask'` channel — and `position: relative`
|
|
96
|
+
* plus a `padding` that *is* the ring's thickness, which is a claim on the host's box in the
|
|
97
|
+
* sense `pin` and `background-media` already use `'layout'` for. Declared only as
|
|
98
|
+
* `background`, a `gradient-border, pin-section` pair composed silently while both decided the
|
|
99
|
+
* host's `position`, and `gradient-border, mask-reveal` while both wrote `mask`.
|
|
100
|
+
*/
|
|
101
|
+
cssPrimitive("ambient-gradient-ring", [CHANNEL.background, "mask", "layout"], {
|
|
102
|
+
parameters: drift,
|
|
103
|
+
defaultActivation: "load",
|
|
104
|
+
reducedMotion: "disable",
|
|
105
|
+
perfClass: "continuous"
|
|
106
|
+
}),
|
|
107
|
+
cssPrimitive("ambient-tint", [CHANNEL.background], {
|
|
108
|
+
parameters: tint,
|
|
109
|
+
defaultActivation: "load",
|
|
110
|
+
reducedMotion: "disable",
|
|
111
|
+
perfClass: "continuous"
|
|
112
|
+
}),
|
|
66
113
|
cssPrimitive("ambient-float", [CHANNEL.translate], {
|
|
67
114
|
parameters: float,
|
|
68
115
|
defaultActivation: "load",
|
|
69
116
|
reducedMotion: "disable",
|
|
70
117
|
perfClass: "continuous"
|
|
71
118
|
}),
|
|
119
|
+
cssPrimitive("ambient-orbit", [CHANNEL.rotate], {
|
|
120
|
+
parameters: orbit,
|
|
121
|
+
defaultActivation: "load",
|
|
122
|
+
reducedMotion: "disable",
|
|
123
|
+
perfClass: "continuous"
|
|
124
|
+
}),
|
|
72
125
|
cssPrimitive("ambient-pulse", [CHANNEL.scale, CHANNEL.opacity], {
|
|
73
126
|
parameters: pulse,
|
|
74
127
|
defaultActivation: "load",
|
|
@@ -81,49 +134,58 @@ var AMBIENT_PRESETS = [
|
|
|
81
134
|
{ name: "aurora", primitive: "ambient-gradient", keyframes: "kui-aurora" },
|
|
82
135
|
{
|
|
83
136
|
name: "gradient-rotate-border",
|
|
84
|
-
primitive: "ambient-gradient",
|
|
137
|
+
primitive: "ambient-gradient-ring",
|
|
85
138
|
keyframes: "kui-gradient-rotate-border",
|
|
86
139
|
params: { duration: "6s", ease: "linear" }
|
|
87
140
|
},
|
|
141
|
+
{
|
|
142
|
+
// Not `gradient`: this rule masks its own content box away (`mask-composite: exclude`) to leave
|
|
143
|
+
// a ring, so putting the name on real content deletes the content. `-border` says that out loud,
|
|
144
|
+
// matching `gradient-rotate-border` and `beam-border`.
|
|
145
|
+
name: "gradient-border",
|
|
146
|
+
primitive: "ambient-gradient-ring",
|
|
147
|
+
keyframes: "kui-gradient-border",
|
|
148
|
+
params: { duration: "6s", ease: "linear" }
|
|
149
|
+
},
|
|
88
150
|
{
|
|
89
151
|
name: "noise-overlay",
|
|
90
|
-
primitive: "ambient-
|
|
152
|
+
primitive: "ambient-tint",
|
|
91
153
|
keyframes: "kui-noise-overlay",
|
|
92
154
|
params: { duration: "650ms", ease: "steps(6)" }
|
|
93
155
|
},
|
|
94
156
|
{
|
|
95
157
|
name: "scanline",
|
|
96
|
-
primitive: "ambient-
|
|
158
|
+
primitive: "ambient-tint",
|
|
97
159
|
keyframes: "kui-scanline",
|
|
98
160
|
params: { duration: "3.5s", ease: "linear" }
|
|
99
161
|
},
|
|
100
162
|
{
|
|
101
163
|
name: "dot-grid-drift",
|
|
102
|
-
primitive: "ambient-
|
|
164
|
+
primitive: "ambient-tint",
|
|
103
165
|
keyframes: "kui-dot-grid-drift",
|
|
104
166
|
params: { duration: "16s", ease: "linear" }
|
|
105
167
|
},
|
|
106
168
|
{
|
|
107
169
|
name: "line-grid-drift",
|
|
108
|
-
primitive: "ambient-
|
|
170
|
+
primitive: "ambient-tint",
|
|
109
171
|
keyframes: "kui-line-grid-drift",
|
|
110
172
|
params: { duration: "16s", ease: "linear" }
|
|
111
173
|
},
|
|
112
174
|
{
|
|
113
175
|
name: "starfield",
|
|
114
|
-
primitive: "ambient-
|
|
176
|
+
primitive: "ambient-tint",
|
|
115
177
|
keyframes: "kui-starfield",
|
|
116
178
|
params: { duration: "40s", ease: "linear" }
|
|
117
179
|
},
|
|
118
180
|
{
|
|
119
181
|
name: "spotlight-follow",
|
|
120
|
-
primitive: "ambient-
|
|
182
|
+
primitive: "ambient-tint",
|
|
121
183
|
keyframes: "kui-spotlight-follow",
|
|
122
184
|
params: { duration: "9s" }
|
|
123
185
|
},
|
|
124
186
|
{
|
|
125
187
|
name: "wave-blob",
|
|
126
|
-
primitive: "ambient-
|
|
188
|
+
primitive: "ambient-tint",
|
|
127
189
|
keyframes: "kui-wave-blob",
|
|
128
190
|
params: { duration: "12s" }
|
|
129
191
|
},
|
|
@@ -140,6 +202,7 @@ var AMBIENT_PRESETS = [
|
|
|
140
202
|
keyframes: "kui-floating-shapes",
|
|
141
203
|
params: { duration: "6s", distance: "10px" }
|
|
142
204
|
},
|
|
205
|
+
{ name: "orbit", primitive: "ambient-orbit", keyframes: "kui-orbit" },
|
|
143
206
|
{ name: "glow-pulse", primitive: "ambient-pulse", keyframes: "kui-glow-pulse" }
|
|
144
207
|
];
|
|
145
208
|
function registerAmbient(registry) {
|
|
@@ -456,6 +519,13 @@ var hoverTiming = {
|
|
|
456
519
|
var liftParams = {
|
|
457
520
|
distance: { type: "length", default: "6px", cssProperty: "--kui-lift-distance" }
|
|
458
521
|
};
|
|
522
|
+
var popParams = {
|
|
523
|
+
scale: { type: "number", default: "1.06", cssProperty: "--kui-pop-scale", finite: true, minimum: 0 }
|
|
524
|
+
};
|
|
525
|
+
var beamParams = {
|
|
526
|
+
color: { type: "color", default: "", cssProperty: "--kui-beam-border-c1" },
|
|
527
|
+
outset: { type: "length", default: "", cssProperty: "--kui-beam-border-outset" }
|
|
528
|
+
};
|
|
459
529
|
function hoverPrimitive(id, channels, extraParams = {}) {
|
|
460
530
|
return {
|
|
461
531
|
id,
|
|
@@ -477,12 +547,13 @@ function hoverPrimitive(id, channels, extraParams = {}) {
|
|
|
477
547
|
}
|
|
478
548
|
var HOVER_PRIMITIVES = [
|
|
479
549
|
hoverPrimitive("lift", ["translate"], liftParams),
|
|
550
|
+
hoverPrimitive("pop", ["scale"], popParams),
|
|
480
551
|
hoverPrimitive("lift-shadow", ["translate", "shadow"], liftParams),
|
|
481
552
|
hoverPrimitive("shine-sweep", ["sweep"]),
|
|
482
553
|
hoverPrimitive("split-flap", ["rotate"]),
|
|
483
554
|
hoverPrimitive("border-draw", ["border"]),
|
|
484
555
|
hoverPrimitive("border-glow", ["shadow"]),
|
|
485
|
-
hoverPrimitive("beam-border", ["border"]),
|
|
556
|
+
hoverPrimitive("beam-border", ["border"], beamParams),
|
|
486
557
|
hoverPrimitive("underline-slide", ["scale"]),
|
|
487
558
|
hoverPrimitive("underline-center", ["scale"]),
|
|
488
559
|
hoverPrimitive("icon-wiggle", ["rotate"]),
|
|
@@ -498,7 +569,7 @@ var CONTINUOUS_BORDER_PRIMITIVES = [
|
|
|
498
569
|
id: "beam-border-auto",
|
|
499
570
|
renderer: "javascript",
|
|
500
571
|
channels: ["border"],
|
|
501
|
-
parameters: hoverTiming,
|
|
572
|
+
parameters: { ...hoverTiming, ...beamParams },
|
|
502
573
|
supportedTimelines: ["time"],
|
|
503
574
|
supportedActivations: ["load"],
|
|
504
575
|
defaultActivation: "load",
|
|
@@ -787,12 +858,344 @@ function registerInteraction(registry) {
|
|
|
787
858
|
return registry.registerPrimitives(INTERACTION_PRIMITIVES).registerPresets(INTERACTION_PRESETS);
|
|
788
859
|
}
|
|
789
860
|
|
|
861
|
+
// src/effects/catalog/background-media.ts
|
|
862
|
+
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set(["mp4", "webm", "mov", "m4v", "ogv"]);
|
|
863
|
+
var VIDEO_PAGE_HOSTS = /* @__PURE__ */ new Set([
|
|
864
|
+
"youtube.com",
|
|
865
|
+
"www.youtube.com",
|
|
866
|
+
"m.youtube.com",
|
|
867
|
+
"youtube-nocookie.com",
|
|
868
|
+
"www.youtube-nocookie.com",
|
|
869
|
+
"youtu.be",
|
|
870
|
+
"www.youtu.be"
|
|
871
|
+
]);
|
|
872
|
+
function normalizeUrl(value) {
|
|
873
|
+
const stripped = value.replace(/[\t\n\r]/g, "");
|
|
874
|
+
let start = 0;
|
|
875
|
+
while (start < stripped.length && stripped.charCodeAt(start) <= 32) start += 1;
|
|
876
|
+
return stripped.slice(start);
|
|
877
|
+
}
|
|
878
|
+
function hostOf(value) {
|
|
879
|
+
const withoutScheme = normalizeUrl(value).replace(/^[a-z][a-z0-9+.-]*:/i, "").replace(/^\/\//, "");
|
|
880
|
+
const end = withoutScheme.search(/[/?#]/);
|
|
881
|
+
const authority = end === -1 ? withoutScheme : withoutScheme.slice(0, end);
|
|
882
|
+
return authority.slice(authority.lastIndexOf("@") + 1).toLowerCase();
|
|
883
|
+
}
|
|
884
|
+
function isVideoPageUrl(value) {
|
|
885
|
+
return VIDEO_PAGE_HOSTS.has(hostOf(value));
|
|
886
|
+
}
|
|
887
|
+
var FOCAL_POINTS = {
|
|
888
|
+
center: "50% 50%",
|
|
889
|
+
top: "50% 0%",
|
|
890
|
+
bottom: "50% 100%",
|
|
891
|
+
left: "0% 50%",
|
|
892
|
+
right: "100% 50%",
|
|
893
|
+
"top-left": "0% 0%",
|
|
894
|
+
"top-right": "100% 0%",
|
|
895
|
+
"bottom-left": "0% 100%",
|
|
896
|
+
"bottom-right": "100% 100%"
|
|
897
|
+
};
|
|
898
|
+
var FOCAL_POINT_NAMES = Object.keys(FOCAL_POINTS);
|
|
899
|
+
function focalPosition(focus) {
|
|
900
|
+
return FOCAL_POINTS[focus] ?? FOCAL_POINTS.center;
|
|
901
|
+
}
|
|
902
|
+
function paintsOverlay(options) {
|
|
903
|
+
return options.overlay !== "transparent" && options.overlayOpacity > 0;
|
|
904
|
+
}
|
|
905
|
+
function isVideoSource(src) {
|
|
906
|
+
const path = src.split("?")[0].split("#")[0];
|
|
907
|
+
const dot = path.lastIndexOf(".");
|
|
908
|
+
if (dot <= path.lastIndexOf("/")) return false;
|
|
909
|
+
return VIDEO_EXTENSIONS.has(path.slice(dot + 1).toLowerCase());
|
|
910
|
+
}
|
|
911
|
+
var ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http", "https"]);
|
|
912
|
+
function schemeOf(value) {
|
|
913
|
+
const match = /^([a-z][a-z0-9+.-]*):/i.exec(normalizeUrl(value));
|
|
914
|
+
return match ? match[1].toLowerCase() : "";
|
|
915
|
+
}
|
|
916
|
+
function mediaSource(authored, name, ctx) {
|
|
917
|
+
if (!authored) return authored;
|
|
918
|
+
if (isVideoPageUrl(authored)) {
|
|
919
|
+
ctx.warn(
|
|
920
|
+
`background-media "${name}": "${authored}" is a video *page*, not a media file \u2014 a YouTube URL cannot be played by <video> and needs an iframe embed instead. Point "${name}" at an .mp4/.webm file, or use a YouTube facade alongside this effect.`
|
|
921
|
+
);
|
|
922
|
+
return "";
|
|
923
|
+
}
|
|
924
|
+
const scheme = schemeOf(authored);
|
|
925
|
+
if (!scheme || ALLOWED_SCHEMES.has(scheme)) return authored;
|
|
926
|
+
ctx.warn(
|
|
927
|
+
`background-media "${name}": "${scheme}:" URLs are not allowed \u2014 use https:, http:, or a path such as "/media/hero.mp4".`
|
|
928
|
+
);
|
|
929
|
+
return "";
|
|
930
|
+
}
|
|
931
|
+
function styleLayer(node) {
|
|
932
|
+
const { style } = node;
|
|
933
|
+
style.setProperty("position", "absolute");
|
|
934
|
+
style.setProperty("top", "0");
|
|
935
|
+
style.setProperty("left", "0");
|
|
936
|
+
style.setProperty("width", "100%");
|
|
937
|
+
style.setProperty("height", "100%");
|
|
938
|
+
style.setProperty("border-radius", "inherit");
|
|
939
|
+
style.setProperty("pointer-events", "none");
|
|
940
|
+
style.setProperty("z-index", "-1");
|
|
941
|
+
}
|
|
942
|
+
function createOverlay(doc, options) {
|
|
943
|
+
const scrim = doc.createElement("div");
|
|
944
|
+
styleLayer(scrim);
|
|
945
|
+
scrim.style.setProperty("background", options.overlay);
|
|
946
|
+
scrim.style.setProperty("opacity", String(options.overlayOpacity));
|
|
947
|
+
scrim.setAttribute("aria-hidden", "true");
|
|
948
|
+
return scrim;
|
|
949
|
+
}
|
|
950
|
+
function createVideo(doc, options) {
|
|
951
|
+
const video = doc.createElement("video");
|
|
952
|
+
video.muted = true;
|
|
953
|
+
video.setAttribute("muted", "");
|
|
954
|
+
video.playsInline = true;
|
|
955
|
+
video.setAttribute("playsinline", "");
|
|
956
|
+
video.loop = options.loop;
|
|
957
|
+
video.defaultPlaybackRate = options.rate;
|
|
958
|
+
video.playbackRate = options.rate;
|
|
959
|
+
video.preload = "metadata";
|
|
960
|
+
if (options.poster) video.poster = options.poster;
|
|
961
|
+
video.src = options.src;
|
|
962
|
+
return video;
|
|
963
|
+
}
|
|
964
|
+
function play(video) {
|
|
965
|
+
const started = video.play();
|
|
966
|
+
if (started) void started.catch(() => {
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
function startPlayback(video, win, options) {
|
|
970
|
+
if (options.reducedMotion || options.autoplay === "never") return () => {
|
|
971
|
+
};
|
|
972
|
+
if (options.autoplay === "always") {
|
|
973
|
+
play(video);
|
|
974
|
+
return () => {
|
|
975
|
+
if (!video.paused) video.pause();
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
return autoplayInView(video, win);
|
|
979
|
+
}
|
|
980
|
+
function createImage(doc, options) {
|
|
981
|
+
const image = doc.createElement("img");
|
|
982
|
+
image.alt = "";
|
|
983
|
+
image.decoding = "async";
|
|
984
|
+
image.src = options.src;
|
|
985
|
+
return image;
|
|
986
|
+
}
|
|
987
|
+
function autoplayInView(video, win) {
|
|
988
|
+
const Observer = win.IntersectionObserver;
|
|
989
|
+
if (!Observer) return () => {
|
|
990
|
+
};
|
|
991
|
+
const observer = new Observer(
|
|
992
|
+
(entries) => {
|
|
993
|
+
for (const entry of entries) {
|
|
994
|
+
if (entry.isIntersecting) play(video);
|
|
995
|
+
else if (!video.paused) video.pause();
|
|
996
|
+
}
|
|
997
|
+
},
|
|
998
|
+
{ threshold: 0 }
|
|
999
|
+
);
|
|
1000
|
+
observer.observe(video);
|
|
1001
|
+
return () => {
|
|
1002
|
+
observer.disconnect();
|
|
1003
|
+
if (!video.paused) video.pause();
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function installBackgroundMedia(el, ctx, options) {
|
|
1007
|
+
const doc = el.ownerDocument;
|
|
1008
|
+
const video = isVideoSource(options.src) ? createVideo(doc, options) : null;
|
|
1009
|
+
const node = video ?? createImage(doc, options);
|
|
1010
|
+
styleLayer(node);
|
|
1011
|
+
node.style.setProperty("object-fit", options.fit);
|
|
1012
|
+
node.style.setProperty("object-position", options.position);
|
|
1013
|
+
node.setAttribute("aria-hidden", "true");
|
|
1014
|
+
node.setAttribute("data-kui-background", "");
|
|
1015
|
+
el.append(node);
|
|
1016
|
+
const overlay = paintsOverlay(options) ? createOverlay(doc, options) : null;
|
|
1017
|
+
if (overlay) {
|
|
1018
|
+
overlay.setAttribute("data-kui-background-overlay", "");
|
|
1019
|
+
el.append(overlay);
|
|
1020
|
+
}
|
|
1021
|
+
const stopPlayback = video ? startPlayback(video, ctx.win, options) : () => {
|
|
1022
|
+
};
|
|
1023
|
+
return {
|
|
1024
|
+
remove: () => {
|
|
1025
|
+
stopPlayback();
|
|
1026
|
+
node.remove();
|
|
1027
|
+
overlay?.remove();
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/effects/catalog/media-shared.ts
|
|
1033
|
+
var AXIS_DEGREES = { vertical: 0, horizontal: 90 };
|
|
1034
|
+
var UNIT_DEGREES = { deg: 1, grad: 0.9, rad: 180 / Math.PI, turn: 360 };
|
|
1035
|
+
var BAND_OVERLAP_PX = 1;
|
|
1036
|
+
var STAGE_CLASS = "kui-slat-stage";
|
|
1037
|
+
var SLAT_CLASS = "kui-slat-item";
|
|
1038
|
+
var GOLDEN_RATIO_CONJUGATE = 0.6180339887498949;
|
|
1039
|
+
function slatOrder(index, count, from) {
|
|
1040
|
+
if (count <= 1) return 0;
|
|
1041
|
+
switch (from) {
|
|
1042
|
+
case "start":
|
|
1043
|
+
return index;
|
|
1044
|
+
case "end":
|
|
1045
|
+
return count - 1 - index;
|
|
1046
|
+
case "edges":
|
|
1047
|
+
return Math.min(index, count - 1 - index);
|
|
1048
|
+
case "random-ish":
|
|
1049
|
+
return Math.floor(index * GOLDEN_RATIO_CONJUGATE % 1 * count);
|
|
1050
|
+
case "alternate":
|
|
1051
|
+
default:
|
|
1052
|
+
return zigzagRank(index, count);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function zigzagRank(index, count) {
|
|
1056
|
+
const fromStart = index;
|
|
1057
|
+
const fromEnd = count - 1 - index;
|
|
1058
|
+
const pair = Math.min(fromStart, fromEnd);
|
|
1059
|
+
return fromStart <= fromEnd ? pair * 2 : pair * 2 + 1;
|
|
1060
|
+
}
|
|
1061
|
+
function slatAngleDegrees(authored, axis) {
|
|
1062
|
+
const trimmed = authored.trim();
|
|
1063
|
+
if (!trimmed) return AXIS_DEGREES[axis];
|
|
1064
|
+
const match = /^(-?(?:\d+(?:\.\d+)?|\.\d+))(deg|rad|grad|turn)?$/.exec(trimmed);
|
|
1065
|
+
if (!match) return AXIS_DEGREES[axis];
|
|
1066
|
+
const value = Number(match[1]);
|
|
1067
|
+
if (!Number.isFinite(value)) return AXIS_DEGREES[axis];
|
|
1068
|
+
const unit = match[2] ?? "deg";
|
|
1069
|
+
const degrees = value * UNIT_DEGREES[unit];
|
|
1070
|
+
return (degrees % 180 + 180) % 180;
|
|
1071
|
+
}
|
|
1072
|
+
function slatTravelVector(angleDegrees) {
|
|
1073
|
+
const theta = angleDegrees * Math.PI / 180;
|
|
1074
|
+
return { x: -Math.sin(theta), y: Math.cos(theta) };
|
|
1075
|
+
}
|
|
1076
|
+
function slatBandClip(index, count, angleDegrees, box) {
|
|
1077
|
+
const { width, height } = box;
|
|
1078
|
+
const theta = angleDegrees * Math.PI / 180;
|
|
1079
|
+
const nx = Math.cos(theta);
|
|
1080
|
+
const ny = Math.sin(theta);
|
|
1081
|
+
const { x: dx, y: dy } = slatTravelVector(angleDegrees);
|
|
1082
|
+
const span = Math.abs(nx) * width + Math.abs(ny) * height;
|
|
1083
|
+
const centreX = width / 2;
|
|
1084
|
+
const centreY = height / 2;
|
|
1085
|
+
const step = span / count;
|
|
1086
|
+
const near = index * step - span / 2 - BAND_OVERLAP_PX;
|
|
1087
|
+
const far = (index + 1) * step - span / 2 + BAND_OVERLAP_PX;
|
|
1088
|
+
const reach = width + height;
|
|
1089
|
+
const corner = (along, across) => `${(centreX + along * nx + across * dx).toFixed(2)}px ${(centreY + along * ny + across * dy).toFixed(2)}px`;
|
|
1090
|
+
return `polygon(${corner(near, reach)}, ${corner(near, -reach)}, ${corner(far, -reach)}, ${corner(far, reach)})`;
|
|
1091
|
+
}
|
|
1092
|
+
function axisLabel(angleDegrees) {
|
|
1093
|
+
if (angleDegrees === 0) return "vertical";
|
|
1094
|
+
if (angleDegrees === 90) return "horizontal";
|
|
1095
|
+
return "diagonal";
|
|
1096
|
+
}
|
|
1097
|
+
function syncStageToImage(stage, img, bands) {
|
|
1098
|
+
const { slats, angleDegrees } = bands;
|
|
1099
|
+
const width = img.offsetWidth;
|
|
1100
|
+
const height = img.offsetHeight;
|
|
1101
|
+
stage.style.top = `${img.offsetTop}px`;
|
|
1102
|
+
stage.style.left = `${img.offsetLeft}px`;
|
|
1103
|
+
stage.style.width = `${width}px`;
|
|
1104
|
+
stage.style.height = `${height}px`;
|
|
1105
|
+
slats.forEach((slat, index) => {
|
|
1106
|
+
slat.style.clipPath = slatBandClip(index, slats.length, angleDegrees, { width, height });
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
function watchImageBox(stage, img, win, bands) {
|
|
1110
|
+
const handler = () => syncStageToImage(stage, img, bands);
|
|
1111
|
+
win.addEventListener("resize", handler, { passive: true });
|
|
1112
|
+
const stopWindow = () => win.removeEventListener("resize", handler);
|
|
1113
|
+
const ResizeObserverCtor = win.ResizeObserver;
|
|
1114
|
+
if (!ResizeObserverCtor) return stopWindow;
|
|
1115
|
+
const observer = new ResizeObserverCtor(handler);
|
|
1116
|
+
observer.observe(img);
|
|
1117
|
+
return () => {
|
|
1118
|
+
stopWindow();
|
|
1119
|
+
observer.disconnect();
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function imagePaintStyle(img, win) {
|
|
1123
|
+
const computed = win.getComputedStyle(img);
|
|
1124
|
+
const position = computed.objectPosition || "50% 50%";
|
|
1125
|
+
const fit = computed.objectFit;
|
|
1126
|
+
if (fit === "cover") return { size: "cover", position };
|
|
1127
|
+
if (fit === "contain" || fit === "scale-down") return { size: "contain", position };
|
|
1128
|
+
if (fit === "none") return { size: "auto", position };
|
|
1129
|
+
return { size: "100% 100%", position };
|
|
1130
|
+
}
|
|
1131
|
+
function installSlatStage(el, doc, win, options) {
|
|
1132
|
+
const img = el.querySelector("img");
|
|
1133
|
+
if (!img) return null;
|
|
1134
|
+
const url = img.currentSrc || img.getAttribute("src") || "";
|
|
1135
|
+
if (!url) return null;
|
|
1136
|
+
const { count, angleDegrees, from, fold } = options;
|
|
1137
|
+
const travel = slatTravelVector(angleDegrees);
|
|
1138
|
+
const stage = doc.createElement("div");
|
|
1139
|
+
stage.className = STAGE_CLASS;
|
|
1140
|
+
stage.setAttribute("aria-hidden", "true");
|
|
1141
|
+
stage.dataset.kuiSlatAxis = axisLabel(angleDegrees);
|
|
1142
|
+
stage.dataset.kuiSlatAngle = String(angleDegrees);
|
|
1143
|
+
stage.dataset.kuiSlatFold = String(fold);
|
|
1144
|
+
stage.style.setProperty("--kui-slat-count", String(count));
|
|
1145
|
+
stage.style.setProperty("--kui-slat-dx", travel.x.toFixed(4));
|
|
1146
|
+
stage.style.setProperty("--kui-slat-dy", travel.y.toFixed(4));
|
|
1147
|
+
const paint = imagePaintStyle(img, win);
|
|
1148
|
+
const slats = [];
|
|
1149
|
+
for (let index = 0; index < count; index++) {
|
|
1150
|
+
const slat = doc.createElement("div");
|
|
1151
|
+
slat.className = SLAT_CLASS;
|
|
1152
|
+
slat.style.setProperty("--kui-slat-index", String(index));
|
|
1153
|
+
slat.style.setProperty("--kui-i", String(slatOrder(index, count, from)));
|
|
1154
|
+
slat.style.backgroundImage = `url("${url}")`;
|
|
1155
|
+
slat.style.backgroundSize = paint.size;
|
|
1156
|
+
slat.style.backgroundPosition = paint.position;
|
|
1157
|
+
stage.append(slat);
|
|
1158
|
+
slats.push(slat);
|
|
1159
|
+
}
|
|
1160
|
+
el.append(stage);
|
|
1161
|
+
const bands = { slats, angleDegrees };
|
|
1162
|
+
syncStageToImage(stage, img, bands);
|
|
1163
|
+
const stopWatching = watchImageBox(stage, img, win, bands);
|
|
1164
|
+
const imageStyles = createStyleLedger(img);
|
|
1165
|
+
imageStyles.set("visibility", "hidden");
|
|
1166
|
+
return {
|
|
1167
|
+
stage,
|
|
1168
|
+
slats,
|
|
1169
|
+
restore: () => {
|
|
1170
|
+
stopWatching();
|
|
1171
|
+
imageStyles.restore();
|
|
1172
|
+
stage.remove();
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
function applySlatTimingVars(stage, params) {
|
|
1177
|
+
const { durationMs, delayMs, easing } = params.timing;
|
|
1178
|
+
const duration = durationMs === void 0 ? params.text("duration", "500ms") : `${durationMs}ms`;
|
|
1179
|
+
const delay = delayMs === void 0 ? params.text("delay", "0ms") : `${delayMs}ms`;
|
|
1180
|
+
stage.style.setProperty("--kui-duration", duration);
|
|
1181
|
+
stage.style.setProperty("--kui-delay", delay);
|
|
1182
|
+
stage.style.setProperty("--kui-ease", easing ?? params.text("ease", "ease-out"));
|
|
1183
|
+
stage.style.setProperty("--kui-stagger", params.text("stagger", "60ms"));
|
|
1184
|
+
}
|
|
1185
|
+
function slatAssembleFinishMs(params, count) {
|
|
1186
|
+
if (count === 0) return 0;
|
|
1187
|
+
const durationMs = params.timing.durationMs ?? params.ms("duration", 500);
|
|
1188
|
+
const delayMs = params.timing.delayMs ?? params.ms("delay", 0);
|
|
1189
|
+
const staggerMs = params.ms("stagger", 60);
|
|
1190
|
+
return delayMs + (count - 1) * staggerMs + durationMs;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
790
1193
|
// src/effects/catalog/media.ts
|
|
791
1194
|
var geometry = {
|
|
792
1195
|
distance: { type: "length", default: "24px", cssProperty: "--kui-distance" },
|
|
793
1196
|
scale: { type: "number", default: "1.12", cssProperty: "--kui-to-scale" }
|
|
794
1197
|
};
|
|
795
|
-
var
|
|
1198
|
+
var MEDIA_CSS_PRIMITIVES = [
|
|
796
1199
|
cssPrimitive("media-wipe", [CHANNEL.clip]),
|
|
797
1200
|
cssPrimitive("media-mask", ["mask"], { perfClass: "paint" }),
|
|
798
1201
|
// Not `reducedMotion: 'disable'` — that policy means "no finite duration would make sense,
|
|
@@ -813,33 +1216,66 @@ var MEDIA_PRIMITIVES = [
|
|
|
813
1216
|
defaultActivation: "hover",
|
|
814
1217
|
perfClass: "paint"
|
|
815
1218
|
}),
|
|
1219
|
+
// `geometry.distance` only, not `...geometry`: `kui-blur-up` (media.css) reads `--kui-distance`
|
|
1220
|
+
// and `--kui-blur` but never `--kui-to-scale` — this primitive doesn't even declare
|
|
1221
|
+
// `CHANNEL.scale`. Spreading the whole shared object used to expose `scale:` as an
|
|
1222
|
+
// apparently-valid, silently-inert parameter, the same shape `flip-3d`'s dead `perspective`
|
|
1223
|
+
// parameter was (`entrance.css`'s comment on `kui-flip-in-x`).
|
|
816
1224
|
cssPrimitive("media-blur-up", [CHANNEL.translate, CHANNEL.filter], {
|
|
817
|
-
parameters: {
|
|
1225
|
+
parameters: {
|
|
1226
|
+
distance: geometry.distance,
|
|
1227
|
+
blur: { type: "length", default: "16px", cssProperty: "--kui-blur" }
|
|
1228
|
+
},
|
|
818
1229
|
perfClass: "paint"
|
|
819
1230
|
}),
|
|
1231
|
+
// No `defaultActivation` — same convention `core.ts`'s `parallax`/`parallax-scale`/
|
|
1232
|
+
// `parallax-rotate`/`scroll-fade`/`desaturate`/`skew`/`progress`/`progress-stroke` already use
|
|
1233
|
+
// for every other `timelines: ['view', 'scroll', ...]` primitive. `resolveActivation`
|
|
1234
|
+
// (`animator.ts`) only consults `defaultActivation` when the author named no activation, and
|
|
1235
|
+
// falls through to `element-config.ts`'s hardcoded `'enter'` when a primitive declares none.
|
|
1236
|
+
// Setting it to `'manual'` here (matching `activations: ['manual']`) looked like the obviously
|
|
1237
|
+
// correct pairing, but it is what actually broke the effect: `effectiveActivation`
|
|
1238
|
+
// (`style-plan.ts`) only converts a stuck `'manual'` into `'enter'` when `config.timeline !==
|
|
1239
|
+
// 'time'` — i.e. only once the author has actually written `timeline:view`/`timeline:scroll`.
|
|
1240
|
+
// Authored bare (no `timeline:`, the sweep's own probe and the likely first thing anyone
|
|
1241
|
+
// tries), `config.timeline` stays the default `'time'`, that conversion never fires, and the
|
|
1242
|
+
// element sits at `data-kui-state="ready"` forever — which is also the state
|
|
1243
|
+
// `entrance.css`'s `[data-kui-state='ready'] { --kui-distance: 0px !important; }` targets, so
|
|
1244
|
+
// every sample read the same permanently-zeroed `--kui-distance`: not a paused animation, a
|
|
1245
|
+
// zeroed one. `timeline:view`/`timeline:scroll` usage is unaffected either way, since a native
|
|
1246
|
+
// timeline resolves to the `'native-timeline'` gate before activation is even consulted.
|
|
1247
|
+
// `geometry.distance` only: `kui-image-parallax-frame` never reads `--kui-to-scale` and this
|
|
1248
|
+
// primitive doesn't declare `CHANNEL.scale` — same dead-parameter shape as `media-blur-up` above.
|
|
820
1249
|
cssPrimitive("media-parallax-frame", [CHANNEL.translate], {
|
|
821
|
-
parameters: geometry,
|
|
1250
|
+
parameters: { distance: geometry.distance },
|
|
822
1251
|
timelines: ["view", "scroll"],
|
|
823
1252
|
activations: ["manual"],
|
|
824
|
-
defaultActivation: "manual",
|
|
825
1253
|
reducedMotion: "disable"
|
|
826
1254
|
}),
|
|
1255
|
+
// Its own `scale` parameter, not `geometry`: `kui-lightbox-open` opens from a fixed 0.92, never
|
|
1256
|
+
// reading `--kui-to-scale` (or any `distance`, since this primitive doesn't animate position at
|
|
1257
|
+
// all) — the whole shared object was dead weight here. Unlike `media-blur-up`/
|
|
1258
|
+
// `media-parallax-frame`, this primitive *does* declare `CHANNEL.scale`, so the fix is to wire
|
|
1259
|
+
// the keyframe up to a real parameter rather than remove the promise: `--kui-from-scale`, the
|
|
1260
|
+
// same name and "starts at this scale, animates to 1" meaning `scale`/`scale-move`
|
|
1261
|
+
// (`catalog/core.ts`) already use, so `lightbox-open scale:0.8` now does what it looks like it
|
|
1262
|
+
// should. See `kui-lightbox-open` in `media.css`.
|
|
827
1263
|
cssPrimitive("media-lightbox", [CHANNEL.opacity, CHANNEL.scale], {
|
|
828
|
-
parameters:
|
|
1264
|
+
parameters: { scale: { type: "number", default: "0.92", cssProperty: "--kui-from-scale" } }
|
|
829
1265
|
})
|
|
830
1266
|
];
|
|
831
|
-
var
|
|
832
|
-
{ name: "wipe-up", primitive: "media-wipe", keyframes: "kui-wipe-up" },
|
|
833
|
-
{ name: "wipe-down", primitive: "media-wipe", keyframes: "kui-wipe-down" },
|
|
834
|
-
{ name: "wipe-left", primitive: "media-wipe", keyframes: "kui-wipe-left" },
|
|
835
|
-
{ name: "wipe-right", primitive: "media-wipe", keyframes: "kui-wipe-right" },
|
|
836
|
-
{ name: "wipe-circle", primitive: "media-wipe", keyframes: "kui-wipe-circle" },
|
|
837
|
-
{ name: "wipe-diagonal", primitive: "media-wipe", keyframes: "kui-wipe-diagonal" },
|
|
838
|
-
{ name: "mask-reveal", primitive: "media-mask", keyframes: "kui-mask-reveal" },
|
|
839
|
-
{ name: "curtain-reveal", primitive: "media-wipe", keyframes: "kui-curtain-reveal" },
|
|
1267
|
+
var MEDIA_CSS_PRESETS = [
|
|
1268
|
+
{ name: "wipe-up", primitive: "media-wipe", keyframes: "kui-wipe-up", cloak: true },
|
|
1269
|
+
{ name: "wipe-down", primitive: "media-wipe", keyframes: "kui-wipe-down", cloak: true },
|
|
1270
|
+
{ name: "wipe-left", primitive: "media-wipe", keyframes: "kui-wipe-left", cloak: true },
|
|
1271
|
+
{ name: "wipe-right", primitive: "media-wipe", keyframes: "kui-wipe-right", cloak: true },
|
|
1272
|
+
{ name: "wipe-circle", primitive: "media-wipe", keyframes: "kui-wipe-circle", cloak: true },
|
|
1273
|
+
{ name: "wipe-diagonal", primitive: "media-wipe", keyframes: "kui-wipe-diagonal", cloak: true },
|
|
1274
|
+
{ name: "mask-reveal", primitive: "media-mask", keyframes: "kui-mask-reveal", cloak: true },
|
|
1275
|
+
{ name: "curtain-reveal", primitive: "media-wipe", keyframes: "kui-curtain-reveal", cloak: true },
|
|
840
1276
|
{ name: "ken-burns", primitive: "media-ken-burns", keyframes: "kui-ken-burns" },
|
|
841
1277
|
{ name: "ken-burns-out", primitive: "media-ken-burns", keyframes: "kui-ken-burns-out" },
|
|
842
|
-
{ name: "blur-up", primitive: "media-blur-up", keyframes: "kui-blur-up" },
|
|
1278
|
+
{ name: "blur-up", primitive: "media-blur-up", keyframes: "kui-blur-up", cloak: true },
|
|
843
1279
|
{ name: "duotone-hover", primitive: "media-filter", keyframes: "kui-duotone-hover" },
|
|
844
1280
|
{ name: "grayscale-hover", primitive: "media-filter", keyframes: "kui-grayscale-hover" },
|
|
845
1281
|
{ name: "saturate-hover", primitive: "media-filter", keyframes: "kui-saturate-hover" },
|
|
@@ -851,14 +1287,316 @@ var MEDIA_PRESETS = [
|
|
|
851
1287
|
{ name: "before-after-wipe", primitive: "media-wipe", keyframes: "kui-before-after-wipe" },
|
|
852
1288
|
{ name: "lightbox-open", primitive: "media-lightbox", keyframes: "kui-lightbox-open" }
|
|
853
1289
|
];
|
|
1290
|
+
var slatParams = {
|
|
1291
|
+
slats: {
|
|
1292
|
+
type: "number",
|
|
1293
|
+
default: "8",
|
|
1294
|
+
cssProperty: "--kui-slats",
|
|
1295
|
+
minimum: 2,
|
|
1296
|
+
maximum: 24,
|
|
1297
|
+
integer: true
|
|
1298
|
+
},
|
|
1299
|
+
axis: {
|
|
1300
|
+
type: "keyword",
|
|
1301
|
+
default: "vertical",
|
|
1302
|
+
cssProperty: "--kui-axis",
|
|
1303
|
+
values: ["vertical", "horizontal"]
|
|
1304
|
+
},
|
|
1305
|
+
/*
|
|
1306
|
+
* The general form of `axis:`, in degrees: `0deg` is `axis:vertical`, `90deg` is
|
|
1307
|
+
* `axis:horizontal`, and anything between cuts the picture into diagonal bands. An authored
|
|
1308
|
+
* angle wins; leaving it off reads `axis:`, so every existing attribute keeps its meaning.
|
|
1309
|
+
*
|
|
1310
|
+
* `text`, not `angle`, for one reason: `readParams` pre-fills every declared parameter with its
|
|
1311
|
+
* schema default, so a typed default is indistinguishable from an authored value and there would
|
|
1312
|
+
* be no way to tell `angle:0deg` from "no angle, use the axis". An empty default is only possible
|
|
1313
|
+
* on a type that is never written to a stylesheet, which is exactly what `text` is for — and this
|
|
1314
|
+
* value never reaches CSS anyway. `slatAngleDegrees` does the parsing and the range clamp.
|
|
1315
|
+
*/
|
|
1316
|
+
angle: { type: "text", default: "", cssProperty: "--kui-slat-angle" },
|
|
1317
|
+
from: {
|
|
1318
|
+
type: "keyword",
|
|
1319
|
+
default: "alternate",
|
|
1320
|
+
cssProperty: "--kui-from",
|
|
1321
|
+
values: ["alternate", "start", "end", "edges", "random-ish"]
|
|
1322
|
+
},
|
|
1323
|
+
fold: { type: "keyword", default: "false", cssProperty: "--kui-fold", values: ["true", "false"] },
|
|
1324
|
+
duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" },
|
|
1325
|
+
...TRIGGER_DELAY_PARAM,
|
|
1326
|
+
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
|
|
1327
|
+
stagger: { type: "time", default: "60ms", cssProperty: "--kui-stagger" }
|
|
1328
|
+
};
|
|
1329
|
+
function prepareSlatAssemble(el, params, ctx) {
|
|
1330
|
+
const doc = el.ownerDocument;
|
|
1331
|
+
const count = Math.min(24, Math.max(2, Math.round(params.num("slats", 8))));
|
|
1332
|
+
const axis = params.text("axis", "vertical");
|
|
1333
|
+
const angleDegrees = slatAngleDegrees(params.text("angle", ""), axis);
|
|
1334
|
+
const from = params.text("from", "alternate");
|
|
1335
|
+
const fold = params.is("fold");
|
|
1336
|
+
const node = el;
|
|
1337
|
+
if (ctx.win.getComputedStyle(node).position === "static") ctx.style.set("position", "relative");
|
|
1338
|
+
const built = installSlatStage(el, doc, ctx.win, { count, angleDegrees, from, fold });
|
|
1339
|
+
if (!built) return () => {
|
|
1340
|
+
};
|
|
1341
|
+
const { stage } = built;
|
|
1342
|
+
applySlatTimingVars(stage, params);
|
|
1343
|
+
stage.classList.add("kui-slat-animating");
|
|
1344
|
+
let settle2;
|
|
1345
|
+
const finished = new Promise((resolve) => {
|
|
1346
|
+
settle2 = resolve;
|
|
1347
|
+
});
|
|
1348
|
+
let landed = false;
|
|
1349
|
+
const land = () => {
|
|
1350
|
+
if (landed) return;
|
|
1351
|
+
landed = true;
|
|
1352
|
+
stage.classList.remove("kui-slat-animating");
|
|
1353
|
+
built.restore();
|
|
1354
|
+
settle2();
|
|
1355
|
+
};
|
|
1356
|
+
const timer = ctx.win.setTimeout(land, slatAssembleFinishMs(params, count));
|
|
1357
|
+
return {
|
|
1358
|
+
cleanup: () => {
|
|
1359
|
+
ctx.win.clearTimeout(timer);
|
|
1360
|
+
if (landed) return;
|
|
1361
|
+
landed = true;
|
|
1362
|
+
built.restore();
|
|
1363
|
+
},
|
|
1364
|
+
finished,
|
|
1365
|
+
finish: () => {
|
|
1366
|
+
ctx.win.clearTimeout(timer);
|
|
1367
|
+
land();
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
var backgroundMediaParams = {
|
|
1372
|
+
/*
|
|
1373
|
+
* `text`, and same-origin-checked at the point of use rather than by the type — identical
|
|
1374
|
+
* shape and identical reasoning to `media-scrub`'s own `src` (`scroll-mechanics/primitives.ts`).
|
|
1375
|
+
* A URL has no lexical shape to validate against, and `type: 'text'` is the one type that never
|
|
1376
|
+
* reaches a stylesheet, which is what makes accepting arbitrary path characters safe.
|
|
1377
|
+
*/
|
|
1378
|
+
src: { type: "text", default: "", cssProperty: "--kui-src" },
|
|
1379
|
+
/*
|
|
1380
|
+
* The still a `<video>` shows before its first frame decodes. Not optional polish: without it a
|
|
1381
|
+
* background clip paints as an empty box for as long as the network takes, and that box is the
|
|
1382
|
+
* backdrop to the author's text — the one place on the page where a flash of nothing is most
|
|
1383
|
+
* visible. Every background video in this repo's own demo pages is authored with one.
|
|
1384
|
+
*/
|
|
1385
|
+
poster: { type: "text", default: "", cssProperty: "--kui-poster" },
|
|
1386
|
+
/*
|
|
1387
|
+
* `fill`, `none` and `scale-down` are deliberately absent. `fill` is the only `object-fit` value
|
|
1388
|
+
* that distorts — it stretches the picture to the box rather than cropping it — and the standing
|
|
1389
|
+
* rule for imagery in this project is to crop, never stretch. The other two leave the media at
|
|
1390
|
+
* its intrinsic size inside a box sized to something else, which for a *backdrop* is a gap, not
|
|
1391
|
+
* a layout. Adding them would be offering three ways to get a broken background.
|
|
1392
|
+
*/
|
|
1393
|
+
fit: {
|
|
1394
|
+
type: "keyword",
|
|
1395
|
+
default: "cover",
|
|
1396
|
+
cssProperty: "--kui-fit",
|
|
1397
|
+
values: ["cover", "contain"]
|
|
1398
|
+
},
|
|
1399
|
+
/*
|
|
1400
|
+
* Which part of the picture a `cover` crop keeps. Nine named points rather than a free
|
|
1401
|
+
* `object-position` string, because a free string would have to be `type: 'text'` — the one type
|
|
1402
|
+
* that is explicitly never written to a stylesheet (see `core/params.ts`) — and this value is
|
|
1403
|
+
* written to one. A keyword list is validated against its own `values`, so the author gets real
|
|
1404
|
+
* focal control and the CSS surface stays closed.
|
|
1405
|
+
*/
|
|
1406
|
+
focus: {
|
|
1407
|
+
type: "keyword",
|
|
1408
|
+
default: "center",
|
|
1409
|
+
cssProperty: "--kui-focus",
|
|
1410
|
+
values: FOCAL_POINT_NAMES
|
|
1411
|
+
},
|
|
1412
|
+
/*
|
|
1413
|
+
* The scrim. This is the parameter that makes the whole effect usable, because the point of a
|
|
1414
|
+
* backdrop here is animated text on top of it, and text over unmodified footage is illegible
|
|
1415
|
+
* about half the time — a light frame arrives and the headline vanishes for those seconds.
|
|
1416
|
+
*
|
|
1417
|
+
* `type: 'color'` so it goes through the same validator every other colour does. `transparent`
|
|
1418
|
+
* as the default rather than an empty string for the same reason: `''` is not a colour, and a
|
|
1419
|
+
* default that its own type would reject is a lie the schema cannot catch. It is also the honest
|
|
1420
|
+
* spelling of "no scrim", and no scrim node is created for it.
|
|
1421
|
+
*/
|
|
1422
|
+
overlay: { type: "color", default: "transparent", cssProperty: "--kui-overlay" },
|
|
1423
|
+
/*
|
|
1424
|
+
* Separate from the colour rather than folded into it. `overlay:rgb(0 0 0 / 45%)` does parse —
|
|
1425
|
+
* the tokenizer is paren-aware — but `overlay:black overlay-opacity:45%` is the spelling someone
|
|
1426
|
+
* reaches for while tuning legibility, and tuning is exactly what this value is for.
|
|
1427
|
+
*/
|
|
1428
|
+
"overlay-opacity": { type: "percentage", default: "100%", cssProperty: "--kui-overlay-opacity" },
|
|
1429
|
+
/*
|
|
1430
|
+
* The opt-out for the play-while-visible behaviour. `in-view` pairs the clip with the viewport
|
|
1431
|
+
* and is right for a long section. `always` is for a short hero clip that must never be caught
|
|
1432
|
+
* mid-stall by a visibility heuristic. `never` installs the clip and leaves it on its poster,
|
|
1433
|
+
* which is also where any mode lands under a reduced-motion preference.
|
|
1434
|
+
*/
|
|
1435
|
+
autoplay: {
|
|
1436
|
+
type: "keyword",
|
|
1437
|
+
default: "in-view",
|
|
1438
|
+
cssProperty: "--kui-autoplay",
|
|
1439
|
+
values: ["in-view", "always", "never"]
|
|
1440
|
+
},
|
|
1441
|
+
/*
|
|
1442
|
+
* Bounded at both ends: `0` is a clip that is loaded, decoding, and permanently frozen — worse
|
|
1443
|
+
* than `autoplay:never`, which at least says so — and browsers stop honouring rates past roughly
|
|
1444
|
+
* 4 anyway, so a larger number is a silent no-op rather than a faster clip.
|
|
1445
|
+
*/
|
|
1446
|
+
rate: {
|
|
1447
|
+
type: "number",
|
|
1448
|
+
default: "1",
|
|
1449
|
+
cssProperty: "--kui-rate",
|
|
1450
|
+
finite: true,
|
|
1451
|
+
minimum: 0.25,
|
|
1452
|
+
maximum: 4
|
|
1453
|
+
},
|
|
1454
|
+
loop: { type: "keyword", default: "true", cssProperty: "--kui-loop", values: ["true", "false"] }
|
|
1455
|
+
/*
|
|
1456
|
+
* There is deliberately no `controls:`. The layer this primitive builds paints at `z-index: -1`
|
|
1457
|
+
* behind the author's own children, so a native control bar there is focusable by keyboard and
|
|
1458
|
+
* occluded by whatever the page happens to put over it — a player you can tab into and cannot
|
|
1459
|
+
* see. A clip meant to be controlled is a content `<video controls>` the author writes, not a
|
|
1460
|
+
* background one.
|
|
1461
|
+
*/
|
|
1462
|
+
};
|
|
1463
|
+
function prepareBackgroundMedia(el, params, ctx) {
|
|
1464
|
+
const authored = params.text("src");
|
|
1465
|
+
if (!authored) {
|
|
1466
|
+
ctx.warn('background-media needs a "src:" \u2014 nothing installed');
|
|
1467
|
+
return () => {
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
const src = mediaSource(authored, "src", ctx);
|
|
1471
|
+
if (!src) return () => {
|
|
1472
|
+
};
|
|
1473
|
+
const node = el;
|
|
1474
|
+
if (ctx.win.getComputedStyle(node).position === "static") ctx.style.set("position", "relative");
|
|
1475
|
+
ctx.style.set("isolation", "isolate");
|
|
1476
|
+
const layer = installBackgroundMedia(el, ctx, {
|
|
1477
|
+
src,
|
|
1478
|
+
poster: mediaSource(params.text("poster"), "poster", ctx),
|
|
1479
|
+
fit: params.is("fit", "contain") ? "contain" : "cover",
|
|
1480
|
+
position: focalPosition(params.text("focus", "center")),
|
|
1481
|
+
overlay: params.text("overlay", "transparent"),
|
|
1482
|
+
// `num` returns a percentage as a 0–1 ratio, which is exactly what `opacity` takes.
|
|
1483
|
+
overlayOpacity: Math.min(1, Math.max(0, params.num("overlay-opacity", 1))),
|
|
1484
|
+
autoplay: params.text("autoplay", "in-view"),
|
|
1485
|
+
rate: params.num("rate", 1),
|
|
1486
|
+
// `!is('loop', 'false')`, not `is('loop')`. Every other read here names its own fallback, and
|
|
1487
|
+
// this one has to as well: `is()` takes no fallback argument, so a bare `is('loop')` is only
|
|
1488
|
+
// true when something already filled the schema default in. That holds on the animator's path
|
|
1489
|
+
// (`readEffectParams` pre-fills every declared parameter) and not on `createParams`, so the
|
|
1490
|
+
// positive spelling silently defaulted a true-by-default parameter to false for any caller
|
|
1491
|
+
// handing over raw values. Reading it as "loop unless explicitly told not to" states the
|
|
1492
|
+
// default at the point of use, where it cannot drift.
|
|
1493
|
+
loop: !params.is("loop", "false"),
|
|
1494
|
+
reducedMotion: ctx.reducedMotion
|
|
1495
|
+
});
|
|
1496
|
+
return continuousSetup(layer.remove);
|
|
1497
|
+
}
|
|
1498
|
+
var MEDIA_JS_PRIMITIVES = [
|
|
1499
|
+
{
|
|
1500
|
+
id: "slat-assemble",
|
|
1501
|
+
renderer: "javascript",
|
|
1502
|
+
channels: [CHANNEL.opacity, CHANNEL.translate, CHANNEL.rotate],
|
|
1503
|
+
parameters: slatParams,
|
|
1504
|
+
supportedTimelines: ["time"],
|
|
1505
|
+
supportedActivations: ["load", "enter", "hover", "focus", "click", "manual"],
|
|
1506
|
+
defaultActivation: "enter",
|
|
1507
|
+
perfClass: "dom-transform",
|
|
1508
|
+
// Same reasoning as every JS-rendered primitive in `text.ts`: nothing here declares a CSS
|
|
1509
|
+
// `animation-duration` the reduced-motion policy layer could shorten, and `disable` is what
|
|
1510
|
+
// stops `installSlatStage`'s DOM surgery from ever running at all under reduced motion — the
|
|
1511
|
+
// animator never calls `activate()`, so the wrapped `<img>` is simply left exactly as authored.
|
|
1512
|
+
reducedMotion: "disable",
|
|
1513
|
+
// Land()ing hands the picture back to the real `<img>` and tears down every slat — see
|
|
1514
|
+
// `prepareSlatAssemble`'s `land()`. Declared, not assumed: see `restoresOnFinish`'s own comment
|
|
1515
|
+
// in `core/types.ts` for why the catalog's default is the opposite of this.
|
|
1516
|
+
restoresOnFinish: true,
|
|
1517
|
+
prepare: deferPrepare(prepareSlatAssemble)
|
|
1518
|
+
},
|
|
1519
|
+
{
|
|
1520
|
+
id: "background-media",
|
|
1521
|
+
/*
|
|
1522
|
+
* `media` is the same word `media-scrub` uses for "this effect owns what the element shows",
|
|
1523
|
+
* and it is what makes `background-media, video-scrub` on one element a reported conflict
|
|
1524
|
+
* rather than two effects silently fighting over the same picture.
|
|
1525
|
+
*
|
|
1526
|
+
* `layout` is claimed for the same reason `pin` claims it: preparation writes `position` and
|
|
1527
|
+
* `isolation` on the *host*, which is a stacking-context claim on someone else's element. Left
|
|
1528
|
+
* undeclared, `background-media, pin-section` composed silently while both decided what
|
|
1529
|
+
* `position` the host has — the conflict detector cannot report a claim it was never told about.
|
|
1530
|
+
*/
|
|
1531
|
+
channels: ["media", "layout"],
|
|
1532
|
+
renderer: "javascript",
|
|
1533
|
+
parameters: backgroundMediaParams,
|
|
1534
|
+
// Not a claim to support four timelines — an abstention. A backdrop is not driven by progress
|
|
1535
|
+
// of any kind and this primitive never reads `Timeline`; the list exists only so that
|
|
1536
|
+
// `data-kui="background-media src:/hero.mp4, parallax"` plus a `timeline:view` survives
|
|
1537
|
+
// `compile.ts`'s `intersect`. See `TIMELINE_AGNOSTIC` (`effects/shared.ts`), shared with the
|
|
1538
|
+
// scroll-mechanics drivers, which abstain for the same reason.
|
|
1539
|
+
supportedTimelines: TIMELINE_AGNOSTIC,
|
|
1540
|
+
supportedActivations: ["load", "enter", "manual"],
|
|
1541
|
+
/*
|
|
1542
|
+
* `'load'`, not the catalog's usual `'enter'`, and this is the difference between working and
|
|
1543
|
+
* not. A backdrop is the element's appearance, so gating it on an IntersectionObserver means a
|
|
1544
|
+
* section that is already on screen at page load, one in a background tab (no IO callbacks
|
|
1545
|
+
* fire at all until the tab is foregrounded), or one whose own box is still zero-area waits an
|
|
1546
|
+
* unbounded time to have any background — and unlike a missed reveal, that is a visibly broken
|
|
1547
|
+
* page. An author who *wants* a heavy clip deferred can still write `on:enter`.
|
|
1548
|
+
*/
|
|
1549
|
+
defaultActivation: "load",
|
|
1550
|
+
perfClass: "paint",
|
|
1551
|
+
/*
|
|
1552
|
+
* `'shorten'`, unlike every other JS-rendered primitive in this file and in `text.ts`, and
|
|
1553
|
+
* deliberately so. `'disable'` means the animator never calls `activate()` under a reduced
|
|
1554
|
+
* motion preference, which for an animation is exactly right and for this is not: it would
|
|
1555
|
+
* leave the element with no backdrop at all rather than a calmer one. There is no CSS duration
|
|
1556
|
+
* here for `'shorten'` to shorten, so the policy is inert and the effect installs normally;
|
|
1557
|
+
* `ctx.reducedMotion` is then read inside, where it suppresses the one genuinely motion-y part
|
|
1558
|
+
* — a clip's autoplay — and leaves the poster frame standing. See `autoplayInView`.
|
|
1559
|
+
*/
|
|
1560
|
+
reducedMotion: "shorten",
|
|
1561
|
+
prepare: deferPrepare(prepareBackgroundMedia)
|
|
1562
|
+
}
|
|
1563
|
+
];
|
|
1564
|
+
var MEDIA_JS_PRESETS = [
|
|
1565
|
+
{ name: "slat-assemble", primitive: "slat-assemble", cloak: true },
|
|
1566
|
+
/*
|
|
1567
|
+
* Two names, one primitive — the alias shape the catalog already uses everywhere (`pin-until`,
|
|
1568
|
+
* `pin-spacer` and `stacking-cards` are three names over the one `pin` primitive; six `wipe-*`
|
|
1569
|
+
* names share `media-wipe`). A preset row is the alias mechanism, so a second spelling costs a
|
|
1570
|
+
* table entry and nothing else: no duplicated implementation to keep in sync, and both names
|
|
1571
|
+
* resolve to the same `prepare`.
|
|
1572
|
+
*
|
|
1573
|
+
* No `cloak` on either: the pre-JS cloak rule hides an element until the runtime installs the
|
|
1574
|
+
* effect's from-state, and this element is the author's own content. Cloaking it would blank
|
|
1575
|
+
* their text for as long as the bundle takes to arrive, to hide a backdrop that has no
|
|
1576
|
+
* from-state at all.
|
|
1577
|
+
*/
|
|
1578
|
+
{ name: "bg", primitive: "background-media" },
|
|
1579
|
+
{ name: "background", primitive: "background-media" }
|
|
1580
|
+
];
|
|
1581
|
+
var MEDIA_PRIMITIVES = [...MEDIA_CSS_PRIMITIVES, ...MEDIA_JS_PRIMITIVES];
|
|
1582
|
+
var MEDIA_PRESETS = [...MEDIA_CSS_PRESETS, ...MEDIA_JS_PRESETS];
|
|
854
1583
|
function registerMedia(registry) {
|
|
855
1584
|
return registry.registerPrimitives(MEDIA_PRIMITIVES).registerPresets(MEDIA_PRESETS);
|
|
856
1585
|
}
|
|
857
1586
|
|
|
1587
|
+
// src/effects/catalog/subtree-capture.ts
|
|
1588
|
+
function captureChildren(el) {
|
|
1589
|
+
const authored = Array.from(el.childNodes);
|
|
1590
|
+
return () => {
|
|
1591
|
+
el.replaceChildren(...authored);
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
858
1595
|
// src/effects/catalog/numbers-shared.ts
|
|
859
1596
|
var SR_ONLY_CLASS = "kui-sr-only";
|
|
860
1597
|
var DECORATIVE_CLASS = "kui-count-decorative";
|
|
861
1598
|
function installCountLayers(el, doc) {
|
|
1599
|
+
const restoreChildren = captureChildren(el);
|
|
862
1600
|
const decorative = doc.createElement("span");
|
|
863
1601
|
decorative.setAttribute("aria-hidden", "true");
|
|
864
1602
|
decorative.className = DECORATIVE_CLASS;
|
|
@@ -870,9 +1608,7 @@ function installCountLayers(el, doc) {
|
|
|
870
1608
|
return {
|
|
871
1609
|
decorative,
|
|
872
1610
|
srOnly,
|
|
873
|
-
restore:
|
|
874
|
-
el.textContent = srOnly.textContent;
|
|
875
|
-
}
|
|
1611
|
+
restore: restoreChildren
|
|
876
1612
|
};
|
|
877
1613
|
}
|
|
878
1614
|
function easeOutCubic(t) {
|
|
@@ -983,6 +1719,7 @@ var COUNT_STEP_MS = 16;
|
|
|
983
1719
|
var REDUCED_MOTION_DURATION_MS = 1;
|
|
984
1720
|
var countParams = {
|
|
985
1721
|
duration: { type: "time", default: "1600ms", cssProperty: "--kui-duration" },
|
|
1722
|
+
...TRIGGER_DELAY_PARAM,
|
|
986
1723
|
from: { type: "number", default: "0", cssProperty: "--kui-from" },
|
|
987
1724
|
to: { type: "number", default: "100", cssProperty: "--kui-to" },
|
|
988
1725
|
decimals: {
|
|
@@ -1003,6 +1740,7 @@ var countParams = {
|
|
|
1003
1740
|
};
|
|
1004
1741
|
var odometerParams = {
|
|
1005
1742
|
duration: { type: "time", default: "1600ms", cssProperty: "--kui-duration" },
|
|
1743
|
+
...TRIGGER_DELAY_PARAM,
|
|
1006
1744
|
from: { type: "number", default: "0", cssProperty: "--kui-from" },
|
|
1007
1745
|
to: { type: "number", default: "100", cssProperty: "--kui-to" }
|
|
1008
1746
|
};
|
|
@@ -1029,7 +1767,9 @@ function tweenDurationMs(params, ctx) {
|
|
|
1029
1767
|
function tweenTimingFor(params, ctx) {
|
|
1030
1768
|
return {
|
|
1031
1769
|
durationMs: tweenDurationMs(params, ctx),
|
|
1032
|
-
|
|
1770
|
+
// Positional first, then the same-named parameter — the two-spellings rule `effectDurationMs`
|
|
1771
|
+
// applies to `duration` just above, now applied to `delay` as well.
|
|
1772
|
+
delayMs: Math.max(0, params.timing.delayMs ?? params.ms("delay", 0)),
|
|
1033
1773
|
easing: resolveEasing(params.timing.easing, ctx.warn)
|
|
1034
1774
|
};
|
|
1035
1775
|
}
|
|
@@ -1182,7 +1922,12 @@ var COUNT_PRESETS = [
|
|
|
1182
1922
|
];
|
|
1183
1923
|
var METER_PRIMITIVES = [
|
|
1184
1924
|
cssPrimitive("stroke-sweep", [CHANNEL.stroke]),
|
|
1185
|
-
|
|
1925
|
+
// `from` gives `progress-bar` a real knob on its start scale — it had none before. `--kui-bar-from`
|
|
1926
|
+
// is also what the `[data-kui-fx~='progress-bar'][data-kui-state='ready']` gate in numbers.css
|
|
1927
|
+
// neutralizes; see that rule's comment for the on:enter fix this parameter doubles as.
|
|
1928
|
+
cssPrimitive("meter-bar", [CHANNEL.scale], {
|
|
1929
|
+
parameters: { from: { type: "number", default: "0", cssProperty: "--kui-bar-from" } }
|
|
1930
|
+
}),
|
|
1186
1931
|
cssPrimitive("meter-segments", [CHANNEL.opacity]),
|
|
1187
1932
|
cssPrimitive("meter-stars", [CHANNEL.clip])
|
|
1188
1933
|
];
|
|
@@ -1191,7 +1936,13 @@ var METER_PRESETS = [
|
|
|
1191
1936
|
{ name: "gauge-sweep", primitive: "stroke-sweep", keyframes: "kui-gauge-sweep" },
|
|
1192
1937
|
{ name: "donut-sweep", primitive: "stroke-sweep", keyframes: "kui-donut-sweep" },
|
|
1193
1938
|
{ name: "sparkline-draw", primitive: "stroke-sweep", keyframes: "kui-sparkline-draw" },
|
|
1194
|
-
|
|
1939
|
+
// `cloak: true`: `kui-progress-bar`'s `from { scale: 0 1 }` (numbers.css) is a zero-width box
|
|
1940
|
+
// while paused, not just an invisible one — so while it waits it occupies no space in layout at
|
|
1941
|
+
// all. Not, despite the tidier story, because an observer refuses to fire on it: Chromium was
|
|
1942
|
+
// measured resolving a zero-area target's `intersectionRatio` to `1`. See numbers.css's
|
|
1943
|
+
// `[data-kui-fx~='progress-bar'][data-kui-state='ready']` rule for the geometry half of the fix;
|
|
1944
|
+
// `cloak` keeps the pre-JS and post-JS "waiting" look the same (invisible) either side of it.
|
|
1945
|
+
{ name: "progress-bar", primitive: "meter-bar", keyframes: "kui-progress-bar", cloak: true },
|
|
1195
1946
|
{ name: "progress-segments", primitive: "meter-segments", keyframes: "kui-progress-segments" },
|
|
1196
1947
|
{ name: "star-rating-fill", primitive: "meter-stars", keyframes: "kui-star-rating-fill" }
|
|
1197
1948
|
];
|
|
@@ -1206,6 +1957,7 @@ var SR_ONLY_CLASS2 = "kui-sr-only";
|
|
|
1206
1957
|
var DECORATIVE_CLASS2 = "kui-split-decorative";
|
|
1207
1958
|
function installSplitLayers(el, doc) {
|
|
1208
1959
|
const originalText = el.textContent.trim();
|
|
1960
|
+
const restoreChildren = captureChildren(el);
|
|
1209
1961
|
const decorative = doc.createElement("span");
|
|
1210
1962
|
decorative.setAttribute("aria-hidden", "true");
|
|
1211
1963
|
decorative.className = DECORATIVE_CLASS2;
|
|
@@ -1217,9 +1969,7 @@ function installSplitLayers(el, doc) {
|
|
|
1217
1969
|
return {
|
|
1218
1970
|
decorative,
|
|
1219
1971
|
originalText,
|
|
1220
|
-
restore:
|
|
1221
|
-
el.textContent = originalText;
|
|
1222
|
-
}
|
|
1972
|
+
restore: restoreChildren
|
|
1223
1973
|
};
|
|
1224
1974
|
}
|
|
1225
1975
|
function segmentGraphemes(text) {
|
|
@@ -1279,15 +2029,22 @@ function createStepRunner(win, options) {
|
|
|
1279
2029
|
function appendCharSpans(container, doc, text) {
|
|
1280
2030
|
const spans = [];
|
|
1281
2031
|
let index = 0;
|
|
2032
|
+
let wordWrapper = null;
|
|
1282
2033
|
for (const grapheme of segmentGraphemes(text)) {
|
|
1283
2034
|
if (grapheme.trim() === "") {
|
|
1284
2035
|
container.append(doc.createTextNode(grapheme));
|
|
2036
|
+
wordWrapper = null;
|
|
1285
2037
|
continue;
|
|
1286
2038
|
}
|
|
2039
|
+
if (!wordWrapper) {
|
|
2040
|
+
wordWrapper = doc.createElement("span");
|
|
2041
|
+
wordWrapper.className = "kui-split-word";
|
|
2042
|
+
container.append(wordWrapper);
|
|
2043
|
+
}
|
|
1287
2044
|
const span = doc.createElement("span");
|
|
1288
2045
|
markItem(span, index);
|
|
1289
2046
|
span.textContent = grapheme;
|
|
1290
|
-
|
|
2047
|
+
wordWrapper.append(span);
|
|
1291
2048
|
spans.push(span);
|
|
1292
2049
|
index++;
|
|
1293
2050
|
}
|
|
@@ -1334,7 +2091,13 @@ function appendLineSpans(container, doc, text) {
|
|
|
1334
2091
|
return buckets.map((nodes, index) => {
|
|
1335
2092
|
const line = doc.createElement("span");
|
|
1336
2093
|
markItem(line, index, "kui-split-line");
|
|
1337
|
-
for (const node of nodes)
|
|
2094
|
+
for (const node of nodes) {
|
|
2095
|
+
if (node instanceof HTMLElement) {
|
|
2096
|
+
node.removeAttribute("class");
|
|
2097
|
+
node.style.removeProperty("--kui-i");
|
|
2098
|
+
}
|
|
2099
|
+
line.append(node);
|
|
2100
|
+
}
|
|
1338
2101
|
container.append(line);
|
|
1339
2102
|
return line;
|
|
1340
2103
|
});
|
|
@@ -1387,14 +2150,43 @@ var extrudeParams = {
|
|
|
1387
2150
|
distance: { type: "length", default: "32px", cssProperty: "--kui-distance" }
|
|
1388
2151
|
};
|
|
1389
2152
|
var TEXT_CSS_PRIMITIVES = [
|
|
1390
|
-
|
|
2153
|
+
// `color`, alongside `background`: both presets' unconditional rule sets `-webkit-text-fill-color:
|
|
2154
|
+
// transparent` so the `background-image` gradient shows through the glyphs (the standard
|
|
2155
|
+
// gradient-text technique). That is a real claim on the glyph fill, the same physical property
|
|
2156
|
+
// `text-outline-fill` animates on its own `color` channel below — without this, the two looked
|
|
2157
|
+
// disjoint to the compiler (`background` vs `stroke`+`color`) and composing them would let
|
|
2158
|
+
// whichever applied last silently win the glyph fill instead of being flagged as a conflict.
|
|
2159
|
+
cssPrimitive("text-shimmer", [CHANNEL.background, CHANNEL.color], { reducedMotion: "disable" }),
|
|
2160
|
+
// `gradient-sweep` is the only one of the three `text-sweep`-family presets whose keyframe
|
|
2161
|
+
// touches `-webkit-text-fill-color` (see text.css) — `highlight-sweep` and `underline-draw` only
|
|
2162
|
+
// paint a `background-image`. Claiming `color` for all three made the compiler reject compositions
|
|
2163
|
+
// like `underline-draw, text-outline-fill` as a glyph-fill conflict even though they touch disjoint
|
|
2164
|
+
// properties. Split so only the preset that actually claims the fill declares the channel.
|
|
2165
|
+
cssPrimitive("text-gradient-sweep", [CHANNEL.background, CHANNEL.color], { parameters: textSweepParams }),
|
|
1391
2166
|
cssPrimitive("text-sweep", [CHANNEL.background], { parameters: textSweepParams }),
|
|
1392
2167
|
cssPrimitive("text-outline-fill", [CHANNEL.stroke, CHANNEL.color]),
|
|
1393
2168
|
cssPrimitive("var-weight", ["font"], { parameters: fontWeightParams }),
|
|
1394
2169
|
cssPrimitive("var-width", ["font"], { parameters: fontWidthParams }),
|
|
1395
2170
|
cssPrimitive("var-slant", ["font"], { parameters: fontSlantParams }),
|
|
2171
|
+
/*
|
|
2172
|
+
* `defaultActivation: 'load'` for the same reason every ambient primitive declares it, and it
|
|
2173
|
+
* was missing here: a marquee is continuous motion, so `reducedMotion: 'disable'` is only half
|
|
2174
|
+
* the rule `ambient.ts` spells out — the other half is starting on `load` rather than waiting on
|
|
2175
|
+
* a scroll-triggered `enter`.
|
|
2176
|
+
*
|
|
2177
|
+
* Without it, a bare `data-kui="marquee 42s"` resolved to `enter`, which `style-plan.ts`'s
|
|
2178
|
+
* `resolveGate` sends down the `deferred` path and stamps `animation-play-state: paused`. It
|
|
2179
|
+
* compiled correctly, reported `data-kui-state="ready"`, and never ran — measured on a marquee
|
|
2180
|
+
* fully in view, so this was not an observer that had simply not fired yet. Every page carrying
|
|
2181
|
+
* one had to know to write `on:load`, which is exactly the kind of thing an author cannot be
|
|
2182
|
+
* expected to guess.
|
|
2183
|
+
*
|
|
2184
|
+
* `marquee-scroll-linked` is unaffected: its position comes from `animation-timeline: scroll()`,
|
|
2185
|
+
* not from an activation.
|
|
2186
|
+
*/
|
|
1396
2187
|
cssPrimitive("text-marquee", [CHANNEL.translate], {
|
|
1397
2188
|
timelines: ["time", "scroll"],
|
|
2189
|
+
defaultActivation: "load",
|
|
1398
2190
|
reducedMotion: "disable"
|
|
1399
2191
|
}),
|
|
1400
2192
|
cssPrimitive("redaction-reveal", [CHANNEL.clip]),
|
|
@@ -1402,7 +2194,7 @@ var TEXT_CSS_PRIMITIVES = [
|
|
|
1402
2194
|
];
|
|
1403
2195
|
var TEXT_CSS_PRESETS = [
|
|
1404
2196
|
{ name: "gradient-shimmer", primitive: "text-shimmer", keyframes: "kui-gradient-shimmer" },
|
|
1405
|
-
{ name: "gradient-sweep", primitive: "text-sweep", keyframes: "kui-gradient-sweep" },
|
|
2197
|
+
{ name: "gradient-sweep", primitive: "text-gradient-sweep", keyframes: "kui-gradient-sweep" },
|
|
1406
2198
|
{
|
|
1407
2199
|
name: "highlight-sweep",
|
|
1408
2200
|
primitive: "text-sweep",
|
|
@@ -1439,7 +2231,7 @@ function jsTextPrimitive(id, channels, options) {
|
|
|
1439
2231
|
}
|
|
1440
2232
|
var splitTiming = {
|
|
1441
2233
|
duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" },
|
|
1442
|
-
|
|
2234
|
+
...TRIGGER_DELAY_PARAM,
|
|
1443
2235
|
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
|
|
1444
2236
|
stagger: { type: "time", default: "30ms", cssProperty: "--kui-stagger" },
|
|
1445
2237
|
unit: {
|
|
@@ -1457,6 +2249,11 @@ var splitTiming = {
|
|
|
1457
2249
|
};
|
|
1458
2250
|
var motionParams = {
|
|
1459
2251
|
stagger: { type: "time", default: "40ms", cssProperty: "--kui-stagger" },
|
|
2252
|
+
// `duration`/`ease` are deliberately *not* declared beside the delay: text.css pins both for
|
|
2253
|
+
// wave and jitter on a higher-specificity `[data-kui-split-fx='wave'] .kui-split-item` rule, so
|
|
2254
|
+
// declaring them would advertise two knobs that the stylesheet then overrides. `animation-delay`
|
|
2255
|
+
// is the one the phase-start rule leaves alone, which is what lets `applyStaggerVars` honour it.
|
|
2256
|
+
...TRIGGER_DELAY_PARAM,
|
|
1460
2257
|
motion: {
|
|
1461
2258
|
type: "keyword",
|
|
1462
2259
|
default: "wave",
|
|
@@ -1466,10 +2263,15 @@ var motionParams = {
|
|
|
1466
2263
|
};
|
|
1467
2264
|
var typewriterParams = {
|
|
1468
2265
|
step: { type: "time", default: "55ms", cssProperty: "--kui-step" },
|
|
1469
|
-
loop: { type: "keyword", default: "false", cssProperty: "--kui-loop", values: ["true", "false"] }
|
|
2266
|
+
loop: { type: "keyword", default: "false", cssProperty: "--kui-loop", values: ["true", "false"] },
|
|
2267
|
+
...TRIGGER_DELAY_PARAM
|
|
1470
2268
|
};
|
|
1471
2269
|
var scrambleParams = {
|
|
1472
2270
|
step: { type: "time", default: "40ms", cssProperty: "--kui-step" },
|
|
2271
|
+
// `duration` gets no such shared declaration: `stepMsFor` reads its authored-or-not distinction
|
|
2272
|
+
// to decide between a whole-effect time and a per-tick `step:`, and a schema default would erase
|
|
2273
|
+
// that distinction. A `0ms` delay default has no equivalent problem.
|
|
2274
|
+
...TRIGGER_DELAY_PARAM,
|
|
1473
2275
|
revealEvery: {
|
|
1474
2276
|
type: "number",
|
|
1475
2277
|
default: "2",
|
|
@@ -1486,7 +2288,11 @@ var scrambleParams = {
|
|
|
1486
2288
|
};
|
|
1487
2289
|
var wordCyclerParams = {
|
|
1488
2290
|
words: { type: "text", default: "", cssProperty: "--kui-words" },
|
|
1489
|
-
interval: { type: "time", default: "2200ms", cssProperty: "--kui-interval" }
|
|
2291
|
+
interval: { type: "time", default: "2200ms", cssProperty: "--kui-interval" },
|
|
2292
|
+
// Load-bearing here, not just for symmetry: a cycler has no authored `duration` — `interval:`
|
|
2293
|
+
// paces it — so the positional "duration then delay" slot only reached a delay when the author
|
|
2294
|
+
// wrote a throwaway first value, `word-cycler 0ms 300ms`.
|
|
2295
|
+
...TRIGGER_DELAY_PARAM
|
|
1490
2296
|
};
|
|
1491
2297
|
function prepareSplitText(el, params, ctx) {
|
|
1492
2298
|
const doc = el.ownerDocument;
|
|
@@ -1532,7 +2338,7 @@ function prepareTypewriter(el, params, ctx) {
|
|
|
1532
2338
|
layers.decorative.textContent = graphemes.slice(0, count).join("");
|
|
1533
2339
|
};
|
|
1534
2340
|
const run = createStepRunner(ctx.win, {
|
|
1535
|
-
delayMs: params.timing.delayMs ?? 0,
|
|
2341
|
+
delayMs: params.timing.delayMs ?? params.ms("delay", 0),
|
|
1536
2342
|
stepMs: stepMsFor(params, graphemes.length, 55),
|
|
1537
2343
|
tick: () => {
|
|
1538
2344
|
const step = nextTypeState(state, graphemes.length, loop2);
|
|
@@ -1556,6 +2362,16 @@ function prepareTypewriter(el, params, ctx) {
|
|
|
1556
2362
|
function prepareScramble(el, params, ctx) {
|
|
1557
2363
|
const charset = SCRAMBLE_CHARSETS[params.text("charset", "upper")];
|
|
1558
2364
|
const revealEvery = Math.max(1, Math.round(params.num("revealEvery", 2)));
|
|
2365
|
+
const node = el;
|
|
2366
|
+
const authoredMinWidth = node.style.getPropertyValue("min-width");
|
|
2367
|
+
const authoredMinHeight = node.style.getPropertyValue("min-height");
|
|
2368
|
+
const restRect = el.getBoundingClientRect();
|
|
2369
|
+
ctx.style.set("min-width", `${restRect.width}px`);
|
|
2370
|
+
ctx.style.set("min-height", `${restRect.height}px`);
|
|
2371
|
+
const releaseSizeLock = () => {
|
|
2372
|
+
ctx.style.set("min-width", authoredMinWidth);
|
|
2373
|
+
ctx.style.set("min-height", authoredMinHeight);
|
|
2374
|
+
};
|
|
1559
2375
|
const layers = installSplitLayers(el, el.ownerDocument);
|
|
1560
2376
|
layers.decorative.classList.add("kui-scramble");
|
|
1561
2377
|
const graphemes = segmentGraphemes(layers.originalText);
|
|
@@ -1565,26 +2381,31 @@ function prepareScramble(el, params, ctx) {
|
|
|
1565
2381
|
layers.decorative.textContent = scrambledFrame(graphemes, resolved, charset, Math.random);
|
|
1566
2382
|
};
|
|
1567
2383
|
render();
|
|
2384
|
+
const totalTicks = Math.max(1, graphemes.length * revealEvery);
|
|
1568
2385
|
const run = createStepRunner(ctx.win, {
|
|
1569
|
-
delayMs: params.timing.delayMs ?? 0,
|
|
1570
|
-
stepMs: stepMsFor(params,
|
|
2386
|
+
delayMs: params.timing.delayMs ?? params.ms("delay", 0),
|
|
2387
|
+
stepMs: stepMsFor(params, totalTicks, Math.max(1, 700 / totalTicks)),
|
|
1571
2388
|
tick: () => {
|
|
1572
2389
|
ticks++;
|
|
1573
2390
|
if (ticks % revealEvery === 0) resolved++;
|
|
1574
2391
|
render();
|
|
1575
|
-
|
|
2392
|
+
const done = resolved >= graphemes.length;
|
|
2393
|
+
if (done) releaseSizeLock();
|
|
2394
|
+
return done;
|
|
1576
2395
|
}
|
|
1577
2396
|
});
|
|
1578
2397
|
return {
|
|
1579
2398
|
cleanup: () => {
|
|
1580
2399
|
run.stop();
|
|
1581
2400
|
layers.restore();
|
|
2401
|
+
releaseSizeLock();
|
|
1582
2402
|
},
|
|
1583
2403
|
finished: run.finished,
|
|
1584
2404
|
finish: () => {
|
|
1585
2405
|
run.stop();
|
|
1586
2406
|
resolved = graphemes.length;
|
|
1587
2407
|
render();
|
|
2408
|
+
releaseSizeLock();
|
|
1588
2409
|
}
|
|
1589
2410
|
};
|
|
1590
2411
|
}
|
|
@@ -1592,12 +2413,12 @@ function prepareWordCycler(el, params, ctx) {
|
|
|
1592
2413
|
const words = params.text("words", "").split("|").map((word) => word.trim()).filter(Boolean);
|
|
1593
2414
|
if (words.length === 0) return () => {
|
|
1594
2415
|
};
|
|
1595
|
-
const
|
|
2416
|
+
const restoreChildren = captureChildren(el);
|
|
1596
2417
|
const swapMs = 150;
|
|
1597
2418
|
let index = 0;
|
|
1598
2419
|
el.textContent = words[0];
|
|
1599
2420
|
const run = createStepRunner(ctx.win, {
|
|
1600
|
-
delayMs: params.timing.delayMs ?? 0,
|
|
2421
|
+
delayMs: params.timing.delayMs ?? params.ms("delay", 0),
|
|
1601
2422
|
stepMs: params.ms("interval", 2200),
|
|
1602
2423
|
tick: () => {
|
|
1603
2424
|
el.classList.add("kui-word-cycler-swap");
|
|
@@ -1613,7 +2434,7 @@ function prepareWordCycler(el, params, ctx) {
|
|
|
1613
2434
|
cleanup: () => {
|
|
1614
2435
|
run.stop();
|
|
1615
2436
|
el.classList.remove("kui-word-cycler-swap");
|
|
1616
|
-
|
|
2437
|
+
restoreChildren();
|
|
1617
2438
|
},
|
|
1618
2439
|
finished: run.finished,
|
|
1619
2440
|
finish: () => run.stop()
|
|
@@ -1645,13 +2466,16 @@ var TEXT_JS_PRIMITIVES = [
|
|
|
1645
2466
|
perfClass: "continuous"
|
|
1646
2467
|
})
|
|
1647
2468
|
];
|
|
2469
|
+
var CHARS_STAGGER = "30ms";
|
|
2470
|
+
var WORDS_STAGGER = "90ms";
|
|
2471
|
+
var LINES_STAGGER = "160ms";
|
|
1648
2472
|
var TEXT_JS_PRESETS = [
|
|
1649
|
-
{ name: "split-chars", primitive: "split-text", params: { unit: "chars", direction: "fade" } },
|
|
1650
|
-
{ name: "split-words", primitive: "split-text", params: { unit: "words", direction: "fade" } },
|
|
1651
|
-
{ name: "split-lines", primitive: "split-text", params: { unit: "lines", direction: "fade" } },
|
|
1652
|
-
{ name: "text-reveal-up", primitive: "split-text", params: { unit: "words", direction: "up" } },
|
|
1653
|
-
{ name: "text-reveal-down", primitive: "split-text", params: { unit: "words", direction: "down" } },
|
|
1654
|
-
{ name: "text-reveal-mask", primitive: "split-text", params: { unit: "lines", direction: "mask" } },
|
|
2473
|
+
{ name: "split-chars", primitive: "split-text", params: { unit: "chars", direction: "fade", stagger: CHARS_STAGGER } },
|
|
2474
|
+
{ name: "split-words", primitive: "split-text", params: { unit: "words", direction: "fade", stagger: WORDS_STAGGER } },
|
|
2475
|
+
{ name: "split-lines", primitive: "split-text", params: { unit: "lines", direction: "fade", stagger: LINES_STAGGER } },
|
|
2476
|
+
{ name: "text-reveal-up", primitive: "split-text", params: { unit: "words", direction: "up", stagger: WORDS_STAGGER }, cloak: true },
|
|
2477
|
+
{ name: "text-reveal-down", primitive: "split-text", params: { unit: "words", direction: "down", stagger: WORDS_STAGGER }, cloak: true },
|
|
2478
|
+
{ name: "text-reveal-mask", primitive: "split-text", params: { unit: "lines", direction: "mask", stagger: LINES_STAGGER }, cloak: true },
|
|
1655
2479
|
{ name: "text-wave", primitive: "split-text-motion", params: { motion: "wave" } },
|
|
1656
2480
|
{ name: "text-jitter", primitive: "split-text-motion", params: { motion: "jitter" } },
|
|
1657
2481
|
{ name: "typewriter", primitive: "typewriter", params: { loop: "false" } },
|
|
@@ -1683,66 +2507,130 @@ var distance = {
|
|
|
1683
2507
|
distance: { type: "length", default: "24px", cssProperty: "--kui-distance" },
|
|
1684
2508
|
opacity: { type: "number", default: "0", cssProperty: "--kui-from-opacity" }
|
|
1685
2509
|
};
|
|
2510
|
+
var ENTRANCE_TIMELINES = ["time", "view", "scroll", "pin"];
|
|
1686
2511
|
var PRIMITIVES = [
|
|
1687
2512
|
// --- entrance / exit -------------------------------------------------------------------
|
|
1688
|
-
cssPrimitive("reveal", [CHANNEL.opacity, CHANNEL.translate], { parameters: distance }),
|
|
2513
|
+
cssPrimitive("reveal", [CHANNEL.opacity, CHANNEL.translate], { timelines: ENTRANCE_TIMELINES, parameters: distance }),
|
|
1689
2514
|
cssPrimitive("scale", [CHANNEL.scale], {
|
|
2515
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1690
2516
|
parameters: { scale: { type: "number", default: "0.92", cssProperty: "--kui-from-scale" } }
|
|
1691
2517
|
}),
|
|
1692
2518
|
// Separate from `scale` because it claims translate as well and so composes differently.
|
|
2519
|
+
// `distance.distance` only, not `...distance`: `kui-zoom-in-up`/`-down` (entrance.css) read
|
|
2520
|
+
// `--kui-distance` and `--kui-from-scale` but never `--kui-from-opacity` — this primitive
|
|
2521
|
+
// doesn't declare `CHANNEL.opacity`. Spreading the whole shared `distance` object used to expose
|
|
2522
|
+
// `opacity:` as an apparently-valid, silently-inert parameter (same dead-parameter shape as
|
|
2523
|
+
// `flip-3d`'s old `perspective`, fixed above).
|
|
1693
2524
|
cssPrimitive("scale-move", [CHANNEL.scale, CHANNEL.translate], {
|
|
2525
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1694
2526
|
parameters: {
|
|
1695
|
-
|
|
2527
|
+
distance: distance.distance,
|
|
1696
2528
|
scale: { type: "number", default: "0.92", cssProperty: "--kui-from-scale" }
|
|
1697
2529
|
}
|
|
1698
2530
|
}),
|
|
1699
2531
|
cssPrimitive("rotate", [CHANNEL.rotate], {
|
|
2532
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1700
2533
|
parameters: { angle: { type: "angle", default: "-8deg", cssProperty: "--kui-from-angle" } }
|
|
1701
2534
|
}),
|
|
2535
|
+
// `distance.distance` only, not `...distance`: `kui-roll-in`/`-out` (entrance.css) write
|
|
2536
|
+
// `rotate`/`translate`, never `opacity` — this primitive doesn't declare `CHANNEL.opacity`. Same
|
|
2537
|
+
// dead-parameter shape as `scale-move` above.
|
|
1702
2538
|
cssPrimitive("roll", [CHANNEL.rotate, CHANNEL.translate], {
|
|
2539
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1703
2540
|
parameters: {
|
|
1704
|
-
|
|
2541
|
+
distance: distance.distance,
|
|
1705
2542
|
angle: { type: "angle", default: "-120deg", cssProperty: "--kui-from-angle" }
|
|
1706
2543
|
}
|
|
1707
2544
|
}),
|
|
1708
|
-
|
|
2545
|
+
// `skew`, not `rotate`: entrance.css's keyframes write `transform: perspective(...)
|
|
2546
|
+
// rotateX/Y(...)`, not the individual `rotate:` property — the `perspective` parameter below
|
|
2547
|
+
// used to compile cleanly and do nothing, because nothing read it. See entrance.css's own
|
|
2548
|
+
// comment on `kui-flip-in-x` for the fix; `CHANNEL.skew` is this catalog's name for "claims the
|
|
2549
|
+
// whole `transform` shorthand" (`core/types.ts`), shared with `scroll-skew` and `flip-face`.
|
|
2550
|
+
cssPrimitive("flip-3d", [CHANNEL.skew], {
|
|
2551
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1709
2552
|
parameters: {
|
|
1710
2553
|
angle: { type: "angle", default: "90deg", cssProperty: "--kui-from-angle" },
|
|
1711
2554
|
perspective: { type: "length", default: "1200px", cssProperty: "--kui-perspective" }
|
|
1712
2555
|
}
|
|
1713
2556
|
}),
|
|
1714
2557
|
cssPrimitive("blur", [CHANNEL.filter], {
|
|
2558
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1715
2559
|
parameters: { blur: { type: "length", default: "12px", cssProperty: "--kui-blur" } }
|
|
1716
2560
|
}),
|
|
1717
2561
|
// Purpose-built combination: one keyframe, so opacity is written once instead of twice.
|
|
1718
2562
|
cssPrimitive("reveal-blur", [CHANNEL.opacity, CHANNEL.translate, CHANNEL.filter], {
|
|
2563
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1719
2564
|
parameters: { ...distance, blur: { type: "length", default: "12px", cssProperty: "--kui-blur" } }
|
|
1720
2565
|
}),
|
|
1721
2566
|
// --- scroll-linked ---------------------------------------------------------------------
|
|
1722
2567
|
// These are progress-linked, not time-triggered: they reverse as the user scrolls back.
|
|
1723
2568
|
// That is by design and is why `timeline:` is a different axis from `on:`.
|
|
2569
|
+
// `distance.distance` only, not the whole `distance` object: `kui-parallax-y`/`-x` (scroll.css)
|
|
2570
|
+
// write only `translate` — this primitive doesn't declare `CHANNEL.opacity`. Same dead-parameter
|
|
2571
|
+
// shape as `scale-move`/`roll` above; `parallax-y`/`parallax-x`/`depth-layer` never read
|
|
2572
|
+
// `--kui-from-opacity`.
|
|
1724
2573
|
cssPrimitive("parallax", [CHANNEL.translate], {
|
|
1725
|
-
parameters: distance,
|
|
1726
|
-
timelines: ["view", "scroll"],
|
|
2574
|
+
parameters: { distance: distance.distance },
|
|
2575
|
+
timelines: ["view", "scroll", "pin"],
|
|
1727
2576
|
activations: ["manual"],
|
|
1728
2577
|
reducedMotion: "disable",
|
|
1729
2578
|
perfClass: "compositor"
|
|
1730
2579
|
}),
|
|
2580
|
+
// `from` exists because the resting end was hardcoded (scale 1 / rotate 0deg), which fixed
|
|
2581
|
+
// these to "grow slightly" and "tilt slightly" — a scroll-driven element that should sweep in
|
|
2582
|
+
// from a quarter-size or from half a turn away had no way to say so. Same `--kui-from-*`
|
|
2583
|
+
// properties the entrance primitives already use, so the two stay spellable the same way.
|
|
1731
2584
|
cssPrimitive("parallax-scale", [CHANNEL.scale], {
|
|
1732
|
-
parameters: {
|
|
1733
|
-
|
|
2585
|
+
parameters: {
|
|
2586
|
+
scale: { type: "number", default: "1.2", cssProperty: "--kui-to-scale" },
|
|
2587
|
+
from: { type: "number", default: "1", cssProperty: "--kui-from-scale" }
|
|
2588
|
+
},
|
|
2589
|
+
timelines: ["view", "scroll", "pin"],
|
|
1734
2590
|
activations: ["manual"],
|
|
1735
2591
|
reducedMotion: "disable"
|
|
1736
2592
|
}),
|
|
1737
2593
|
cssPrimitive("parallax-rotate", [CHANNEL.rotate], {
|
|
1738
|
-
parameters: {
|
|
1739
|
-
|
|
2594
|
+
parameters: {
|
|
2595
|
+
angle: { type: "angle", default: "12deg", cssProperty: "--kui-to-angle" },
|
|
2596
|
+
from: { type: "angle", default: "0deg", cssProperty: "--kui-from-angle" }
|
|
2597
|
+
},
|
|
2598
|
+
timelines: ["view", "scroll", "pin"],
|
|
1740
2599
|
activations: ["manual"],
|
|
1741
2600
|
reducedMotion: "disable"
|
|
1742
2601
|
}),
|
|
1743
2602
|
cssPrimitive("scroll-fade", [CHANNEL.opacity], {
|
|
1744
2603
|
parameters: { opacity: { type: "number", default: "0", cssProperty: "--kui-from-opacity" } },
|
|
1745
|
-
timelines: ["view", "scroll"],
|
|
2604
|
+
timelines: ["view", "scroll", "pin"],
|
|
2605
|
+
activations: ["manual"],
|
|
2606
|
+
reducedMotion: "disable"
|
|
2607
|
+
}),
|
|
2608
|
+
// `filter`, not `opacity` — it collides with `blur`, and declaring the real channel is what
|
|
2609
|
+
// makes `channels.ts` say so instead of letting the two silently overwrite each other's
|
|
2610
|
+
// `filter` declaration.
|
|
2611
|
+
cssPrimitive("desaturate", [CHANNEL.filter], {
|
|
2612
|
+
parameters: {
|
|
2613
|
+
from: { type: "percentage", default: "100%", cssProperty: "--kui-from-grayscale" },
|
|
2614
|
+
to: { type: "percentage", default: "0%", cssProperty: "--kui-to-grayscale" }
|
|
2615
|
+
},
|
|
2616
|
+
timelines: ["view", "scroll", "pin"],
|
|
2617
|
+
activations: ["manual"],
|
|
2618
|
+
reducedMotion: "disable",
|
|
2619
|
+
perfClass: "paint"
|
|
2620
|
+
}),
|
|
2621
|
+
/*
|
|
2622
|
+
* `transform`, not one of the independent transform properties, because CSS never shipped a
|
|
2623
|
+
* standalone `skew:`. That is also why `skew` is its own channel rather than folded in with
|
|
2624
|
+
* `rotate`: writing `transform` replaces the entire shorthand, so a skew composed with anything
|
|
2625
|
+
* else that wrote `transform` would silently win. Nothing else in the catalog does — every other
|
|
2626
|
+
* transform in the library goes through `translate`/`rotate`/`scale` — so the shorthand is free.
|
|
2627
|
+
*/
|
|
2628
|
+
cssPrimitive("skew", [CHANNEL.skew], {
|
|
2629
|
+
parameters: {
|
|
2630
|
+
from: { type: "angle", default: "8deg", cssProperty: "--kui-from-skew" },
|
|
2631
|
+
to: { type: "angle", default: "0deg", cssProperty: "--kui-to-skew" }
|
|
2632
|
+
},
|
|
2633
|
+
timelines: ["view", "scroll", "pin"],
|
|
1746
2634
|
activations: ["manual"],
|
|
1747
2635
|
reducedMotion: "disable"
|
|
1748
2636
|
}),
|
|
@@ -1762,13 +2650,14 @@ var PRIMITIVES = [
|
|
|
1762
2650
|
})
|
|
1763
2651
|
];
|
|
1764
2652
|
var p = (name, primitive, keyframes, params) => ({ name, primitive, keyframes, ...params ? { params } : {} });
|
|
2653
|
+
var pIn = (name, primitive, keyframes, params) => ({ ...p(name, primitive, keyframes, params), cloak: true });
|
|
1765
2654
|
var FADE = [
|
|
1766
|
-
|
|
2655
|
+
pIn("fade-in", "reveal", "kui-in"),
|
|
1767
2656
|
p("fade-out", "reveal", "kui-out"),
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
2657
|
+
pIn("fade-up", "reveal", "kui-in-up"),
|
|
2658
|
+
pIn("fade-down", "reveal", "kui-in-down"),
|
|
2659
|
+
pIn("fade-left", "reveal", "kui-in-left"),
|
|
2660
|
+
pIn("fade-right", "reveal", "kui-in-right"),
|
|
1772
2661
|
p("fade-out-up", "reveal", "kui-out-up"),
|
|
1773
2662
|
p("fade-out-down", "reveal", "kui-out-down"),
|
|
1774
2663
|
p("fade-out-left", "reveal", "kui-out-left"),
|
|
@@ -1776,56 +2665,56 @@ var FADE = [
|
|
|
1776
2665
|
];
|
|
1777
2666
|
var SLIDE_PARAMS = { distance: "100px", opacity: "1" };
|
|
1778
2667
|
var SLIDE = [
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
2668
|
+
pIn("slide-up", "reveal", "kui-in-up", SLIDE_PARAMS),
|
|
2669
|
+
pIn("slide-down", "reveal", "kui-in-down", SLIDE_PARAMS),
|
|
2670
|
+
pIn("slide-left", "reveal", "kui-in-left", SLIDE_PARAMS),
|
|
2671
|
+
pIn("slide-right", "reveal", "kui-in-right", SLIDE_PARAMS),
|
|
1783
2672
|
p("slide-out-up", "reveal", "kui-out-up", SLIDE_PARAMS),
|
|
1784
2673
|
p("slide-out-down", "reveal", "kui-out-down", SLIDE_PARAMS),
|
|
1785
2674
|
p("slide-out-left", "reveal", "kui-out-left", SLIDE_PARAMS),
|
|
1786
2675
|
p("slide-out-right", "reveal", "kui-out-right", SLIDE_PARAMS)
|
|
1787
2676
|
];
|
|
1788
2677
|
var LOGICAL = [
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
2678
|
+
pIn("slide-inline-start", "reveal", "kui-in-inline-start", SLIDE_PARAMS),
|
|
2679
|
+
pIn("slide-inline-end", "reveal", "kui-in-inline-end", SLIDE_PARAMS),
|
|
2680
|
+
pIn("slide-block-start", "reveal", "kui-in-up", SLIDE_PARAMS),
|
|
2681
|
+
pIn("slide-block-end", "reveal", "kui-in-down", SLIDE_PARAMS)
|
|
1793
2682
|
];
|
|
1794
2683
|
var ZOOM = [
|
|
1795
|
-
|
|
2684
|
+
pIn("zoom-in", "scale", "kui-zoom-in"),
|
|
1796
2685
|
p("zoom-out", "scale", "kui-zoom-out"),
|
|
1797
|
-
|
|
2686
|
+
pIn("pop-in", "scale", "kui-zoom-in", { scale: "0.6", ease: "back-out" }),
|
|
1798
2687
|
p("pop-out", "scale", "kui-zoom-out", { scale: "0.6", ease: "back-in" }),
|
|
1799
|
-
|
|
1800
|
-
|
|
2688
|
+
pIn("zoom-in-up", "scale-move", "kui-zoom-in-up"),
|
|
2689
|
+
pIn("zoom-in-down", "scale-move", "kui-zoom-in-down")
|
|
1801
2690
|
];
|
|
1802
2691
|
var FLIP = [
|
|
1803
|
-
|
|
1804
|
-
|
|
2692
|
+
pIn("flip-in-x", "flip-3d", "kui-flip-in-x"),
|
|
2693
|
+
pIn("flip-in-y", "flip-3d", "kui-flip-in-y"),
|
|
1805
2694
|
p("flip-out-x", "flip-3d", "kui-flip-out-x"),
|
|
1806
2695
|
p("flip-out-y", "flip-3d", "kui-flip-out-y")
|
|
1807
2696
|
];
|
|
1808
2697
|
var ROTATE = [
|
|
1809
|
-
|
|
2698
|
+
pIn("rotate-in", "rotate", "kui-rotate-in"),
|
|
1810
2699
|
p("rotate-out", "rotate", "kui-rotate-out"),
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2700
|
+
pIn("rotate-in-left", "rotate", "kui-rotate-in", { angle: "-45deg" }),
|
|
2701
|
+
pIn("rotate-in-right", "rotate", "kui-rotate-in", { angle: "45deg" }),
|
|
2702
|
+
pIn("roll-in", "roll", "kui-roll-in"),
|
|
1814
2703
|
p("roll-out", "roll", "kui-roll-out"),
|
|
1815
|
-
|
|
2704
|
+
pIn("swing-in", "rotate", "kui-swing-in", { angle: "-15deg", ease: "back-out" })
|
|
1816
2705
|
];
|
|
1817
2706
|
var BLUR = [
|
|
1818
|
-
|
|
2707
|
+
pIn("blur-in", "blur", "kui-blur-in"),
|
|
1819
2708
|
p("blur-out", "blur", "kui-blur-out"),
|
|
1820
|
-
|
|
1821
|
-
|
|
2709
|
+
pIn("fade-blur-up", "reveal-blur", "kui-fade-blur-up"),
|
|
2710
|
+
pIn("fade-blur-in", "reveal-blur", "kui-fade-blur-in")
|
|
1822
2711
|
];
|
|
1823
2712
|
var CHARACTER = [
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2713
|
+
pIn("bounce-in", "scale", "kui-zoom-in", { scale: "0.3", ease: "bounce" }),
|
|
2714
|
+
pIn("bounce-in-up", "reveal", "kui-in-up", { distance: "60px", ease: "back-out" }),
|
|
2715
|
+
pIn("bounce-in-down", "reveal", "kui-in-down", { distance: "60px", ease: "back-out" }),
|
|
2716
|
+
pIn("back-in-up", "reveal", "kui-in-up", { distance: "120px", ease: "expo-out" }),
|
|
2717
|
+
pIn("back-in-down", "reveal", "kui-in-down", { distance: "120px", ease: "expo-out" })
|
|
1829
2718
|
];
|
|
1830
2719
|
var SCROLL = [
|
|
1831
2720
|
p("parallax-y", "parallax", "kui-parallax-y"),
|
|
@@ -1834,11 +2723,14 @@ var SCROLL = [
|
|
|
1834
2723
|
p("parallax-rotate", "parallax-rotate", "kui-parallax-rotate"),
|
|
1835
2724
|
p("depth-layer", "parallax", "kui-parallax-y", { distance: "200px" }),
|
|
1836
2725
|
p("scroll-fade", "scroll-fade", "kui-scroll-fade"),
|
|
2726
|
+
p("scroll-desaturate", "desaturate", "kui-desaturate"),
|
|
2727
|
+
p("scroll-skew", "skew", "kui-scroll-skew"),
|
|
1837
2728
|
p("scroll-progress-bar", "progress", "kui-progress-x"),
|
|
2729
|
+
p("scroll-progress-bar-y", "progress", "kui-progress-y"),
|
|
1838
2730
|
p("scroll-progress-ring", "progress-stroke", "kui-progress-ring"),
|
|
1839
2731
|
// `reveal-repeat` was removed: it was byte-identical to `reveal-once`, and the activation
|
|
1840
2732
|
// binder unobserves after first entry, so a repeating reveal is not implementable yet.
|
|
1841
|
-
|
|
2733
|
+
pIn("reveal-once", "reveal", "kui-in-up")
|
|
1842
2734
|
];
|
|
1843
2735
|
var PRESETS = [
|
|
1844
2736
|
...FADE,
|
|
@@ -1861,10 +2753,63 @@ function registerCore(registry) {
|
|
|
1861
2753
|
return registry;
|
|
1862
2754
|
}
|
|
1863
2755
|
|
|
2756
|
+
// src/effects/step-marking.ts
|
|
2757
|
+
var STEP_STATE_ATTR = "data-kui-step-state";
|
|
2758
|
+
function selectorBreadth(selector, doc) {
|
|
2759
|
+
try {
|
|
2760
|
+
if (doc.documentElement.matches(selector)) return "document-wide";
|
|
2761
|
+
if (doc.body?.matches(selector)) return "document-wide";
|
|
2762
|
+
return "ok";
|
|
2763
|
+
} catch {
|
|
2764
|
+
return "invalid";
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
function resolveTarget(selector, ctx, effect) {
|
|
2768
|
+
if (!selector) return selector;
|
|
2769
|
+
const breadth = selectorBreadth(selector, ctx.doc);
|
|
2770
|
+
if (breadth === "invalid") {
|
|
2771
|
+
ctx.warn(`${effect} target "${selector}" is not a valid selector and will be ignored`);
|
|
2772
|
+
return "";
|
|
2773
|
+
}
|
|
2774
|
+
if (breadth === "document-wide") {
|
|
2775
|
+
ctx.warn(`${effect} target "${selector}" matches the whole document and will be ignored`);
|
|
2776
|
+
return "";
|
|
2777
|
+
}
|
|
2778
|
+
return selector;
|
|
2779
|
+
}
|
|
2780
|
+
function createStepMarker(resolve) {
|
|
2781
|
+
const ledgers = /* @__PURE__ */ new Map();
|
|
2782
|
+
return {
|
|
2783
|
+
mark(index) {
|
|
2784
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2785
|
+
for (const node of resolve()) {
|
|
2786
|
+
const parent = node.parentElement;
|
|
2787
|
+
const position = seen.get(parent) ?? 0;
|
|
2788
|
+
seen.set(parent, position + 1);
|
|
2789
|
+
let ledger = ledgers.get(node);
|
|
2790
|
+
if (!ledger) {
|
|
2791
|
+
ledger = createAttributeLedger(node);
|
|
2792
|
+
ledgers.set(node, ledger);
|
|
2793
|
+
}
|
|
2794
|
+
ledger.set(STEP_STATE_ATTR, stepStateFor(position, index));
|
|
2795
|
+
}
|
|
2796
|
+
},
|
|
2797
|
+
restore() {
|
|
2798
|
+
for (const ledger of ledgers.values()) ledger.restore();
|
|
2799
|
+
ledgers.clear();
|
|
2800
|
+
}
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
function stepStateFor(position, index) {
|
|
2804
|
+
if (position < index) return "before";
|
|
2805
|
+
if (position === index) return "active";
|
|
2806
|
+
return "after";
|
|
2807
|
+
}
|
|
2808
|
+
|
|
1864
2809
|
// src/effects/forms/primitives.ts
|
|
1865
2810
|
var timing = {
|
|
1866
2811
|
duration: { type: "time", default: "400ms", cssProperty: "--kui-duration" },
|
|
1867
|
-
|
|
2812
|
+
...TRIGGER_DELAY_PARAM,
|
|
1868
2813
|
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
|
|
1869
2814
|
};
|
|
1870
2815
|
var NATIVE_STATE_PRIMITIVE = {
|
|
@@ -1979,10 +2924,18 @@ var RANGE_FILL_PRIMITIVE = jsInputPrimitive(
|
|
|
1979
2924
|
function nextStep(step, total) {
|
|
1980
2925
|
return total > 0 ? (step + 1) % total : 0;
|
|
1981
2926
|
}
|
|
1982
|
-
function prepareStepProgress(el, params) {
|
|
2927
|
+
function prepareStepProgress(el, params, ctx) {
|
|
1983
2928
|
const total = Math.max(1, Math.round(params.num("steps", 4)));
|
|
2929
|
+
const selector = resolveTarget(params.text("target"), ctx, "step-progress");
|
|
2930
|
+
const marker = createStepMarker(
|
|
2931
|
+
() => selector ? ctx.doc.querySelectorAll(selector) : el.children
|
|
2932
|
+
);
|
|
2933
|
+
const self = createAttributeLedger(el);
|
|
1984
2934
|
let step = 0;
|
|
1985
|
-
const render = () =>
|
|
2935
|
+
const render = () => {
|
|
2936
|
+
self.set("data-kui-step", String(step));
|
|
2937
|
+
marker.mark(step);
|
|
2938
|
+
};
|
|
1986
2939
|
const advance = () => {
|
|
1987
2940
|
step = nextStep(step, total);
|
|
1988
2941
|
render();
|
|
@@ -1991,13 +2944,17 @@ function prepareStepProgress(el, params) {
|
|
|
1991
2944
|
render();
|
|
1992
2945
|
return () => {
|
|
1993
2946
|
el.removeEventListener("click", advance);
|
|
1994
|
-
|
|
2947
|
+
self.restore();
|
|
2948
|
+
marker.restore();
|
|
1995
2949
|
};
|
|
1996
2950
|
}
|
|
1997
2951
|
var STEP_PROGRESS_PRIMITIVE = jsInputPrimitive(
|
|
1998
2952
|
"step-progress",
|
|
1999
2953
|
["state"],
|
|
2000
|
-
{
|
|
2954
|
+
{
|
|
2955
|
+
steps: { type: "number", default: "4", cssProperty: "--kui-steps", minimum: 1, maximum: 20, integer: true },
|
|
2956
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" }
|
|
2957
|
+
},
|
|
2001
2958
|
deferPrepare(prepareStepProgress)
|
|
2002
2959
|
);
|
|
2003
2960
|
function nextSubmitStage(stage) {
|
|
@@ -2254,6 +3211,7 @@ function prepareDraggable(el, params, ctx) {
|
|
|
2254
3211
|
const resistance = params.num("resistance", 0.55);
|
|
2255
3212
|
const momentum = params.num("momentum", 0.2);
|
|
2256
3213
|
const offset = { x: 0, y: 0 };
|
|
3214
|
+
const pickup = { x: 0, y: 0 };
|
|
2257
3215
|
const deps = springDeps(ctx);
|
|
2258
3216
|
const runners = {
|
|
2259
3217
|
x: createSpringRunner(config, (value) => write({ ...offset, x: value }), deps),
|
|
@@ -2270,10 +3228,15 @@ function prepareDraggable(el, params, ctx) {
|
|
|
2270
3228
|
onStart() {
|
|
2271
3229
|
runners.x.stop();
|
|
2272
3230
|
runners.y.stop();
|
|
3231
|
+
pickup.x = offset.x;
|
|
3232
|
+
pickup.y = offset.y;
|
|
2273
3233
|
el.setAttribute("data-kui-dragging", "true");
|
|
2274
3234
|
},
|
|
2275
3235
|
onMove(vector) {
|
|
2276
|
-
write({
|
|
3236
|
+
write({
|
|
3237
|
+
x: resist(pickup.x + vector.dx, bounds, resistance),
|
|
3238
|
+
y: resist(pickup.y + vector.dy, bounds, resistance)
|
|
3239
|
+
});
|
|
2277
3240
|
},
|
|
2278
3241
|
onEnd(vector) {
|
|
2279
3242
|
el.setAttribute("data-kui-dragging", "false");
|
|
@@ -2301,8 +3264,9 @@ function settle(runners, offset, vector, mode) {
|
|
|
2301
3264
|
runners.y.to(0);
|
|
2302
3265
|
return;
|
|
2303
3266
|
}
|
|
2304
|
-
|
|
2305
|
-
runners.
|
|
3267
|
+
const carry = mode.inertia ? mode.momentum : 0;
|
|
3268
|
+
runners.x.to(offset.x + vector.vx * carry);
|
|
3269
|
+
runners.y.to(offset.y + vector.vy * carry);
|
|
2306
3270
|
}
|
|
2307
3271
|
function prepareSwipeable(el, params) {
|
|
2308
3272
|
const stop = recognise(
|
|
@@ -2586,9 +3550,12 @@ function prepareAutoHeight(el, params, ctx) {
|
|
|
2586
3550
|
ctx.style.claim("height");
|
|
2587
3551
|
const duration = effectDurationMs(params, 400);
|
|
2588
3552
|
let animation = null;
|
|
3553
|
+
let previous = node.getBoundingClientRect().height;
|
|
2589
3554
|
const observer = watchAttribute(node, params.text("attribute"), () => {
|
|
2590
3555
|
animation?.cancel();
|
|
2591
|
-
|
|
3556
|
+
const endpoints = heightEndpoints(node, previous);
|
|
3557
|
+
previous = endpoints.to;
|
|
3558
|
+
animation = animateHeight(node, endpoints, duration, params.text("ease"));
|
|
2592
3559
|
});
|
|
2593
3560
|
ctx.invalidate();
|
|
2594
3561
|
return () => {
|
|
@@ -2596,13 +3563,19 @@ function prepareAutoHeight(el, params, ctx) {
|
|
|
2596
3563
|
animation?.cancel();
|
|
2597
3564
|
};
|
|
2598
3565
|
}
|
|
2599
|
-
function
|
|
2600
|
-
const from = `${node.getBoundingClientRect().height}px`;
|
|
3566
|
+
function heightEndpoints(node, previous) {
|
|
2601
3567
|
node.style.removeProperty("height");
|
|
2602
|
-
const to =
|
|
3568
|
+
const to = node.getBoundingClientRect().height;
|
|
3569
|
+
return { from: previous, to };
|
|
3570
|
+
}
|
|
3571
|
+
function animateHeight(node, endpoints, duration, easing) {
|
|
2603
3572
|
const animate = node.animate;
|
|
2604
3573
|
if (typeof animate !== "function") return null;
|
|
2605
|
-
return animate.call(
|
|
3574
|
+
return animate.call(
|
|
3575
|
+
node,
|
|
3576
|
+
[{ height: `${endpoints.from}px` }, { height: `${endpoints.to}px` }],
|
|
3577
|
+
{ duration, easing, fill: "none" }
|
|
3578
|
+
);
|
|
2606
3579
|
}
|
|
2607
3580
|
function prepareIndicator(el, params, ctx) {
|
|
2608
3581
|
const node = el;
|
|
@@ -2732,27 +3705,42 @@ function subscribeScrollTop(el, ctx, onScrollTop) {
|
|
|
2732
3705
|
}
|
|
2733
3706
|
function prepareHeaderShrink(el, params, ctx) {
|
|
2734
3707
|
const offset = params.num("offset", 120);
|
|
2735
|
-
|
|
3708
|
+
const state = createAttributeLedger(el);
|
|
3709
|
+
const unsubscribe = subscribeScrollTop(el, ctx, (top) => {
|
|
2736
3710
|
const progress = offset > 0 ? Math.min(1, Math.max(0, top / offset)) : 1;
|
|
2737
3711
|
ctx.style.set("--kui-shrink", progress.toFixed(4));
|
|
2738
|
-
|
|
3712
|
+
state.set("data-kui-shrunk", String(progress >= 1));
|
|
2739
3713
|
});
|
|
3714
|
+
return () => {
|
|
3715
|
+
unsubscribe();
|
|
3716
|
+
state.restore();
|
|
3717
|
+
};
|
|
2740
3718
|
}
|
|
2741
3719
|
function prepareHeaderHide(el, params, ctx) {
|
|
2742
3720
|
const minDelta = params.num("offset", 8);
|
|
2743
3721
|
let last = 0;
|
|
2744
|
-
|
|
3722
|
+
const state = createAttributeLedger(el);
|
|
3723
|
+
const unsubscribe = subscribeScrollTop(el, ctx, (top) => {
|
|
2745
3724
|
const delta = top - last;
|
|
2746
3725
|
if (Math.abs(delta) < minDelta) return;
|
|
2747
|
-
|
|
3726
|
+
state.set("data-kui-hidden", String(delta > 0 && top > minDelta));
|
|
2748
3727
|
last = top;
|
|
2749
3728
|
});
|
|
3729
|
+
return () => {
|
|
3730
|
+
unsubscribe();
|
|
3731
|
+
state.restore();
|
|
3732
|
+
};
|
|
2750
3733
|
}
|
|
2751
3734
|
function prepareBackToTop(el, params, ctx) {
|
|
2752
3735
|
const offset = params.num("offset", 400);
|
|
2753
|
-
|
|
2754
|
-
|
|
3736
|
+
const state = createAttributeLedger(el);
|
|
3737
|
+
const unsubscribe = subscribeScrollTop(el, ctx, (top) => {
|
|
3738
|
+
state.set("data-kui-visible", String(top > offset));
|
|
2755
3739
|
});
|
|
3740
|
+
return () => {
|
|
3741
|
+
unsubscribe();
|
|
3742
|
+
state.restore();
|
|
3743
|
+
};
|
|
2756
3744
|
}
|
|
2757
3745
|
var NAV_JS_PRIMITIVES = [
|
|
2758
3746
|
navPrimitive(
|
|
@@ -2790,13 +3778,38 @@ var domGeometry = (el) => {
|
|
|
2790
3778
|
const rect = el.getBoundingClientRect();
|
|
2791
3779
|
return { top: rect.top, height: rect.height };
|
|
2792
3780
|
};
|
|
3781
|
+
var domPosition = (el) => {
|
|
3782
|
+
const view = el.ownerDocument?.defaultView;
|
|
3783
|
+
return view ? view.getComputedStyle(el).position : "static";
|
|
3784
|
+
};
|
|
3785
|
+
var domOffsetTop = (el) => {
|
|
3786
|
+
const view = el.ownerDocument?.defaultView;
|
|
3787
|
+
if (!view) return 0;
|
|
3788
|
+
const parsed = Number.parseFloat(view.getComputedStyle(el).top);
|
|
3789
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
3790
|
+
};
|
|
3791
|
+
function geometrySource(el, positionOf) {
|
|
3792
|
+
let outermostSticky = null;
|
|
3793
|
+
for (let node = el; node; node = node.parentElement) {
|
|
3794
|
+
if (positionOf(node) === "sticky") outermostSticky = node;
|
|
3795
|
+
}
|
|
3796
|
+
return outermostSticky?.parentElement ?? el;
|
|
3797
|
+
}
|
|
3798
|
+
function sourceTop(el, box, measure, positionOf) {
|
|
3799
|
+
const source = geometrySource(el, positionOf);
|
|
3800
|
+
return source === el ? box.top : measure(source).top;
|
|
3801
|
+
}
|
|
2793
3802
|
function trackProgress(el, ctx, options, onProgress) {
|
|
2794
3803
|
const measure = options.measure ?? domGeometry;
|
|
3804
|
+
const positionOf = options.positionOf ?? domPosition;
|
|
3805
|
+
const offsetOf = options.offsetOf ?? domOffsetTop;
|
|
2795
3806
|
let scrollTop = 0;
|
|
2796
3807
|
let scrollportTop = 0;
|
|
2797
3808
|
const geometry2 = createMeasureCache(() => {
|
|
2798
3809
|
const box = measure(el);
|
|
2799
|
-
|
|
3810
|
+
const top = options.contentAnchor ? measure(options.contentAnchor).top - box.height : sourceTop(el, box, measure, positionOf);
|
|
3811
|
+
const stickyOffset = options.stickyEl ? offsetOf(options.stickyEl) : 0;
|
|
3812
|
+
return { contentTop: top - stickyOffset - scrollportTop + scrollTop, height: box.height };
|
|
2800
3813
|
});
|
|
2801
3814
|
return ctx.scheduler.subscribe(ctx.rootFor(el), (frame) => {
|
|
2802
3815
|
scrollTop = frame.metrics.scrollTop;
|
|
@@ -2822,11 +3835,167 @@ function progressFrom(top, span) {
|
|
|
2822
3835
|
return clamp01(-top / span);
|
|
2823
3836
|
}
|
|
2824
3837
|
|
|
3838
|
+
// src/effects/scroll-mechanics/scroll-spy.ts
|
|
3839
|
+
function prepareScrollSpy(el, params, ctx) {
|
|
3840
|
+
const sectionsAuthored = params.text("sections");
|
|
3841
|
+
if (sectionsAuthored) {
|
|
3842
|
+
const sectionsSelector = resolveTarget(sectionsAuthored, ctx, "scroll-spy sections");
|
|
3843
|
+
return prepareScrollSpyContainer(el, params, ctx, sectionsSelector);
|
|
3844
|
+
}
|
|
3845
|
+
return prepareScrollSpySingle(el, params, ctx);
|
|
3846
|
+
}
|
|
3847
|
+
function prepareScrollSpySingle(el, params, ctx) {
|
|
3848
|
+
if (params.text("offset-top", "0px") !== "0px") {
|
|
3849
|
+
ctx.warn('scroll-spy "offset-top" has no effect without sections: and is ignored here');
|
|
3850
|
+
}
|
|
3851
|
+
const selector = resolveTarget(params.text("target"), ctx, "scroll-spy");
|
|
3852
|
+
const links = /* @__PURE__ */ new Map();
|
|
3853
|
+
const self = createAttributeLedger(el);
|
|
3854
|
+
let last;
|
|
3855
|
+
const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
|
|
3856
|
+
const active = progress > 0 && progress < 1;
|
|
3857
|
+
if (active === last) return;
|
|
3858
|
+
last = active;
|
|
3859
|
+
self.set("data-kui-active", String(active));
|
|
3860
|
+
if (selector) markLinks(ctx.doc, selector, active, links);
|
|
3861
|
+
});
|
|
3862
|
+
return continuousSetup(() => {
|
|
3863
|
+
untrack();
|
|
3864
|
+
self.restore();
|
|
3865
|
+
for (const ledger of links.values()) ledger.restore();
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
function markLinks(doc, selector, active, links) {
|
|
3869
|
+
for (const link of doc.querySelectorAll(selector)) {
|
|
3870
|
+
let ledger = links.get(link);
|
|
3871
|
+
if (!ledger) {
|
|
3872
|
+
ledger = createAttributeLedger(link);
|
|
3873
|
+
links.set(link, ledger);
|
|
3874
|
+
}
|
|
3875
|
+
ledger.set("data-kui-active", String(active));
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
function hrefHash(link) {
|
|
3879
|
+
const href = link.getAttribute("href") ?? "";
|
|
3880
|
+
const at = href.indexOf("#");
|
|
3881
|
+
return at === -1 ? "" : href.slice(at);
|
|
3882
|
+
}
|
|
3883
|
+
function pairSectionsWithLinks(sections, links, ctx, sectionsSelector) {
|
|
3884
|
+
const linksByHash = /* @__PURE__ */ new Map();
|
|
3885
|
+
for (const link of links) {
|
|
3886
|
+
const hash = hrefHash(link);
|
|
3887
|
+
if (hash) linksByHash.set(hash, link);
|
|
3888
|
+
}
|
|
3889
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
3890
|
+
const pairs = sections.map((section) => {
|
|
3891
|
+
if (!section.id) {
|
|
3892
|
+
ctx.warn(
|
|
3893
|
+
`scroll-spy: a section matched by sections:"${sectionsSelector}" has no id and cannot be paired with a link`
|
|
3894
|
+
);
|
|
3895
|
+
return { section, link: null };
|
|
3896
|
+
}
|
|
3897
|
+
const hash = `#${section.id}`;
|
|
3898
|
+
const link = linksByHash.get(hash) ?? null;
|
|
3899
|
+
if (link) claimed.add(hash);
|
|
3900
|
+
return { section, link };
|
|
3901
|
+
});
|
|
3902
|
+
for (const [hash, link] of linksByHash) {
|
|
3903
|
+
if (!claimed.has(hash)) {
|
|
3904
|
+
ctx.warn(`scroll-spy: link "${link.getAttribute("href")}" matches no section id in sections:"${sectionsSelector}"`);
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
return pairs;
|
|
3908
|
+
}
|
|
3909
|
+
function offsetTopPixels(authored, frame) {
|
|
3910
|
+
return toPixels(
|
|
3911
|
+
authored,
|
|
3912
|
+
{
|
|
3913
|
+
viewportWidth: frame.metrics.viewportWidth,
|
|
3914
|
+
viewportHeight: frame.metrics.viewportHeight,
|
|
3915
|
+
percentBasis: 0,
|
|
3916
|
+
fontSize: 16,
|
|
3917
|
+
rootFontSize: 16
|
|
3918
|
+
},
|
|
3919
|
+
0
|
|
3920
|
+
);
|
|
3921
|
+
}
|
|
3922
|
+
function highestReachedIndex(tops, scrollTop, line) {
|
|
3923
|
+
let index = -1;
|
|
3924
|
+
for (let i = 0; i < tops.length; i++) {
|
|
3925
|
+
if (tops[i] - scrollTop - line <= 0) index = i;
|
|
3926
|
+
}
|
|
3927
|
+
return index;
|
|
3928
|
+
}
|
|
3929
|
+
function prepareScrollSpyContainer(el, params, ctx, sectionsSelector) {
|
|
3930
|
+
if (params.text("distance", "100vh") !== "100vh") {
|
|
3931
|
+
ctx.warn('scroll-spy "distance" has no effect with sections: \u2014 each section measures its own height');
|
|
3932
|
+
}
|
|
3933
|
+
const linksSelector = resolveTarget(params.text("target"), ctx, "scroll-spy target");
|
|
3934
|
+
const sections = sectionsSelector ? [...el.querySelectorAll(sectionsSelector)] : [];
|
|
3935
|
+
if (sectionsSelector && sections.length === 0) {
|
|
3936
|
+
ctx.warn(`scroll-spy sections:"${sectionsSelector}" matched nothing inside this element`);
|
|
3937
|
+
}
|
|
3938
|
+
const links = linksSelector ? [...el.querySelectorAll(linksSelector)] : [];
|
|
3939
|
+
const pairs = pairSectionsWithLinks(sections, links, ctx, sectionsSelector);
|
|
3940
|
+
const sectionLedgers = pairs.map((pair) => createAttributeLedger(pair.section));
|
|
3941
|
+
const linkLedgers = /* @__PURE__ */ new Map();
|
|
3942
|
+
for (const { link } of pairs) {
|
|
3943
|
+
if (link && !linkLedgers.has(link)) linkLedgers.set(link, createAttributeLedger(link));
|
|
3944
|
+
}
|
|
3945
|
+
const offsetAuthored = params.text("offset-top", "0px");
|
|
3946
|
+
let scrollTop = 0;
|
|
3947
|
+
let scrollportTop = 0;
|
|
3948
|
+
const contentTops = createMeasureCache(
|
|
3949
|
+
() => pairs.map(({ section }) => domGeometry(section).top - scrollportTop + scrollTop)
|
|
3950
|
+
);
|
|
3951
|
+
let active = -1;
|
|
3952
|
+
function setActive(index, value) {
|
|
3953
|
+
const pair = pairs[index];
|
|
3954
|
+
sectionLedgers[index].set("data-kui-active", String(value));
|
|
3955
|
+
if (pair.link) linkLedgers.get(pair.link).set("data-kui-active", String(value));
|
|
3956
|
+
}
|
|
3957
|
+
const untrack = ctx.scheduler.subscribe(ctx.rootFor(el), (frame) => {
|
|
3958
|
+
scrollTop = frame.metrics.scrollTop;
|
|
3959
|
+
scrollportTop = frame.metrics.viewportTop;
|
|
3960
|
+
const tops = contentTops.read(frame.epoch);
|
|
3961
|
+
const line = offsetTopPixels(offsetAuthored, frame);
|
|
3962
|
+
const next = highestReachedIndex(tops, scrollTop, line);
|
|
3963
|
+
if (next === active) return;
|
|
3964
|
+
if (active !== -1) setActive(active, false);
|
|
3965
|
+
if (next !== -1) setActive(next, true);
|
|
3966
|
+
active = next;
|
|
3967
|
+
});
|
|
3968
|
+
return continuousSetup(() => {
|
|
3969
|
+
untrack();
|
|
3970
|
+
for (const ledger of sectionLedgers) ledger.restore();
|
|
3971
|
+
for (const ledger of linkLedgers.values()) ledger.restore();
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
|
|
2825
3975
|
// src/effects/scroll-mechanics/primitives.ts
|
|
2826
3976
|
var PROGRESS_VAR = "--kui-progress";
|
|
2827
3977
|
var distanceParam = {
|
|
2828
3978
|
distance: { type: "length", default: "100vh", cssProperty: "--kui-distance" }
|
|
2829
3979
|
};
|
|
3980
|
+
var stickyParams = {
|
|
3981
|
+
"offset-top": {
|
|
3982
|
+
type: "length",
|
|
3983
|
+
default: "var(--kui-pin-offset, 0px)",
|
|
3984
|
+
cssProperty: "--kui-offset-top"
|
|
3985
|
+
},
|
|
3986
|
+
spacer: {
|
|
3987
|
+
type: "keyword",
|
|
3988
|
+
default: "false",
|
|
3989
|
+
cssProperty: "--kui-spacer",
|
|
3990
|
+
values: ["true", "false"]
|
|
3991
|
+
}
|
|
3992
|
+
};
|
|
3993
|
+
function installSticky(node, params, ctx) {
|
|
3994
|
+
ctx.style.set("position", "sticky");
|
|
3995
|
+
ctx.style.set("top", params.text("offset-top", "var(--kui-pin-offset, 0px)"));
|
|
3996
|
+
const inserted = params.is("spacer") ? insertSpacer(node, params.text("distance"), ctx) : null;
|
|
3997
|
+
return { spacer: inserted?.spacer ?? null, dispose: () => inserted?.remove() };
|
|
3998
|
+
}
|
|
2830
3999
|
function scrollPrimitive(spec) {
|
|
2831
4000
|
const { id, channels, parameters, prepare, perfClass = "compositor" } = spec;
|
|
2832
4001
|
return {
|
|
@@ -2834,7 +4003,13 @@ function scrollPrimitive(spec) {
|
|
|
2834
4003
|
renderer: "javascript",
|
|
2835
4004
|
channels,
|
|
2836
4005
|
parameters,
|
|
2837
|
-
|
|
4006
|
+
// Accepted, never read: these primitives read scroll position themselves and are never driven
|
|
4007
|
+
// by an `animation-timeline`. The list exists so that composing the driver with the effects it
|
|
4008
|
+
// drives — `data-kui="pin-section distance:200vh, parallax-rotate ... "` plus `timeline:pin` —
|
|
4009
|
+
// survives `compile.ts`'s `intersect`. Without it the intersection empties, `style-plan.ts`
|
|
4010
|
+
// refuses the timeline, and the scrub silently degrades to a one-shot. See `TIMELINE_AGNOSTIC`
|
|
4011
|
+
// (`effects/shared.ts`) for why the name says abstention rather than support.
|
|
4012
|
+
supportedTimelines: TIMELINE_AGNOSTIC,
|
|
2838
4013
|
supportedActivations: ["manual", "load", "enter"],
|
|
2839
4014
|
defaultActivation: "load",
|
|
2840
4015
|
perfClass,
|
|
@@ -2847,19 +4022,17 @@ function writeProgress(ctx, progress) {
|
|
|
2847
4022
|
}
|
|
2848
4023
|
function preparePin(el, params, ctx) {
|
|
2849
4024
|
const node = el;
|
|
2850
|
-
|
|
2851
|
-
ctx.style.set("top", params.text("offset", "0px"));
|
|
2852
|
-
const removeSpacer = params.is("spacer") ? insertSpacer(node, params.text("distance"), ctx) : null;
|
|
4025
|
+
const { dispose: unstick } = installSticky(node, params, ctx);
|
|
2853
4026
|
const tracked = node.parentElement ?? el;
|
|
2854
|
-
const untrack = trackProgress(tracked, ctx, { distance: params.text("distance") }, (progress) => {
|
|
4027
|
+
const untrack = trackProgress(tracked, ctx, { distance: params.text("distance"), stickyEl: node }, (progress) => {
|
|
2855
4028
|
writeProgress(ctx, progress);
|
|
2856
4029
|
el.setAttribute("data-kui-pinned", progress > 0 && progress < 1 ? "true" : "false");
|
|
2857
4030
|
});
|
|
2858
|
-
return () => {
|
|
4031
|
+
return continuousSetup(() => {
|
|
2859
4032
|
untrack();
|
|
2860
|
-
|
|
4033
|
+
unstick();
|
|
2861
4034
|
el.removeAttribute("data-kui-pinned");
|
|
2862
|
-
};
|
|
4035
|
+
});
|
|
2863
4036
|
}
|
|
2864
4037
|
function insertSpacer(node, distance2, ctx) {
|
|
2865
4038
|
const spacer = ctx.doc.createElement("div");
|
|
@@ -2869,34 +4042,81 @@ function insertSpacer(node, distance2, ctx) {
|
|
|
2869
4042
|
spacer.style.pointerEvents = "none";
|
|
2870
4043
|
node.after(spacer);
|
|
2871
4044
|
ctx.invalidate();
|
|
2872
|
-
return
|
|
2873
|
-
spacer
|
|
2874
|
-
|
|
4045
|
+
return {
|
|
4046
|
+
spacer,
|
|
4047
|
+
remove: () => {
|
|
4048
|
+
spacer.remove();
|
|
4049
|
+
ctx.invalidate();
|
|
4050
|
+
}
|
|
2875
4051
|
};
|
|
2876
4052
|
}
|
|
2877
4053
|
function prepareProgress(el, params, ctx) {
|
|
2878
4054
|
const steps = Math.max(0, Math.round(params.num("steps", 0)));
|
|
4055
|
+
const selector = resolveTarget(params.text("target"), ctx, "scrollytelling-step");
|
|
4056
|
+
const marker = createStepMarker(() => ctx.doc.querySelectorAll(selector));
|
|
4057
|
+
const self = createAttributeLedger(el);
|
|
4058
|
+
let lastIndex;
|
|
2879
4059
|
const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
|
|
2880
4060
|
writeProgress(ctx, progress);
|
|
2881
|
-
if (steps
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
4061
|
+
if (steps === 0) return;
|
|
4062
|
+
const index = Math.min(steps - 1, Math.floor(progress * steps));
|
|
4063
|
+
if (index === lastIndex) return;
|
|
4064
|
+
lastIndex = index;
|
|
4065
|
+
self.set("data-kui-step", String(index));
|
|
4066
|
+
ctx.style.set("--kui-step", String(index));
|
|
4067
|
+
if (selector) marker.mark(index);
|
|
2885
4068
|
});
|
|
2886
|
-
return () => {
|
|
4069
|
+
return continuousSetup(() => {
|
|
2887
4070
|
untrack();
|
|
2888
|
-
|
|
2889
|
-
|
|
4071
|
+
self.restore();
|
|
4072
|
+
marker.restore();
|
|
4073
|
+
});
|
|
2890
4074
|
}
|
|
2891
4075
|
function prepareHorizontal(el, params, ctx) {
|
|
2892
|
-
const
|
|
4076
|
+
const selector = resolveTarget(params.text("target"), ctx, "horizontal-scroll");
|
|
4077
|
+
if (!selector) return prepareBareTrack(el, params, ctx);
|
|
4078
|
+
const track = el.querySelector(selector);
|
|
4079
|
+
if (!track) {
|
|
4080
|
+
ctx.warn(`horizontal-scroll target "${selector}" matched nothing inside this element`);
|
|
4081
|
+
return () => {
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
return prepareManagedTrack(el, track, params, ctx);
|
|
4085
|
+
}
|
|
4086
|
+
function prepareBareTrack(node, params, ctx) {
|
|
2893
4087
|
const authored = params.text("travel", "auto");
|
|
2894
4088
|
const travel = createMeasureCache(() => trackTravel(node, authored, node.ownerDocument));
|
|
2895
|
-
|
|
2896
|
-
ctx.
|
|
4089
|
+
return continuousSetup(
|
|
4090
|
+
trackProgress(node, ctx, { distance: params.text("distance") }, (progress, frame) => {
|
|
4091
|
+
ctx.style.set("translate", `${-progress * travel.read(frame.epoch)}px 0`);
|
|
4092
|
+
writeProgress(ctx, progress);
|
|
4093
|
+
})
|
|
4094
|
+
);
|
|
4095
|
+
}
|
|
4096
|
+
function prepareManagedTrack(host, track, params, ctx) {
|
|
4097
|
+
const offsetTop = params.text("offset-top", "var(--kui-pin-offset, 0px)");
|
|
4098
|
+
ctx.style.set("position", "sticky");
|
|
4099
|
+
ctx.style.set("top", offsetTop);
|
|
4100
|
+
ctx.style.set("height", `calc(100vh - ${offsetTop})`);
|
|
4101
|
+
ctx.style.set("overflow", "hidden");
|
|
4102
|
+
ctx.style.set("display", "grid");
|
|
4103
|
+
ctx.style.set("align-content", "center");
|
|
4104
|
+
const { remove: removeSpacer } = insertSpacer(host, params.text("distance"), ctx);
|
|
4105
|
+
const rail = createStyleLedger(track);
|
|
4106
|
+
rail.set("display", "flex");
|
|
4107
|
+
rail.set("width", "max-content");
|
|
4108
|
+
const authored = params.text("travel", "auto");
|
|
4109
|
+
const travel = createMeasureCache(() => trackTravel(track, authored, track.ownerDocument));
|
|
4110
|
+
const tracked = host.parentElement ?? host;
|
|
4111
|
+
const untrack = trackProgress(tracked, ctx, { distance: params.text("distance"), stickyEl: host }, (progress, frame) => {
|
|
4112
|
+
rail.set("translate", `${-progress * travel.read(frame.epoch)}px 0`);
|
|
2897
4113
|
writeProgress(ctx, progress);
|
|
2898
4114
|
});
|
|
2899
|
-
return
|
|
4115
|
+
return continuousSetup(() => {
|
|
4116
|
+
untrack();
|
|
4117
|
+
rail.restore();
|
|
4118
|
+
removeSpacer();
|
|
4119
|
+
});
|
|
2900
4120
|
}
|
|
2901
4121
|
function trackTravel(node, authored, doc) {
|
|
2902
4122
|
if (authored && authored !== "auto") return toPixels(authored, ABSOLUTE_BASIS, 0);
|
|
@@ -2906,11 +4126,41 @@ function trackTravel(node, authored, doc) {
|
|
|
2906
4126
|
return Math.max(0, node.scrollWidth - viewportWidth);
|
|
2907
4127
|
}
|
|
2908
4128
|
function prepareMediaScrub(el, params, ctx) {
|
|
4129
|
+
const managed = params.is("spacer") ? installSticky(el, params, ctx) : null;
|
|
4130
|
+
const contentAnchor = managed?.spacer ?? void 0;
|
|
4131
|
+
const stickyEl = managed ? el : void 0;
|
|
4132
|
+
const selector = resolveTarget(params.text("target"), ctx, "media-scrub");
|
|
4133
|
+
const scrub = selector ? prepareTargetScrub(el, params, ctx, { selector, contentAnchor, stickyEl }) : prepareSrcScrub(el, params, ctx, { contentAnchor, stickyEl });
|
|
4134
|
+
return continuousSetup(() => {
|
|
4135
|
+
scrub();
|
|
4136
|
+
managed?.dispose();
|
|
4137
|
+
});
|
|
4138
|
+
}
|
|
4139
|
+
function prepareTargetScrub(el, params, ctx, authored) {
|
|
4140
|
+
const { selector, contentAnchor, stickyEl } = authored;
|
|
4141
|
+
const marker = createStepMarker(() => ctx.doc.querySelectorAll(selector));
|
|
4142
|
+
const frames = Math.max(1, ctx.doc.querySelectorAll(selector).length);
|
|
4143
|
+
let lastIndex;
|
|
4144
|
+
const untrack = trackProgress(el, ctx, { distance: params.text("distance"), contentAnchor, stickyEl }, (progress) => {
|
|
4145
|
+
writeProgress(ctx, progress);
|
|
4146
|
+
const index = Math.min(frames - 1, Math.floor(progress * frames));
|
|
4147
|
+
if (index === lastIndex) return;
|
|
4148
|
+
lastIndex = index;
|
|
4149
|
+
marker.mark(index);
|
|
4150
|
+
});
|
|
4151
|
+
marker.mark(0);
|
|
4152
|
+
return () => {
|
|
4153
|
+
untrack();
|
|
4154
|
+
marker.restore();
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
4157
|
+
function prepareSrcScrub(el, params, ctx, anchors) {
|
|
4158
|
+
const { contentAnchor, stickyEl } = anchors;
|
|
2909
4159
|
const frames = Math.max(1, Math.round(params.num("frames", 1)));
|
|
2910
4160
|
const pattern = mediaSrcPattern(params.text("src"), ctx);
|
|
2911
4161
|
const media = el;
|
|
2912
4162
|
let lastIndex = -1;
|
|
2913
|
-
const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
|
|
4163
|
+
const untrack = trackProgress(el, ctx, { distance: params.text("distance"), contentAnchor, stickyEl }, (progress) => {
|
|
2914
4164
|
writeProgress(ctx, progress);
|
|
2915
4165
|
const index = Math.min(frames - 1, Math.floor(progress * frames));
|
|
2916
4166
|
if (index === lastIndex) return;
|
|
@@ -2934,80 +4184,37 @@ function applyFrame(media, write) {
|
|
|
2934
4184
|
if (media.tagName !== "IMG") return;
|
|
2935
4185
|
if (pattern) media.src = pattern.replace("{i}", String(index).padStart(String(frames).length, "0"));
|
|
2936
4186
|
}
|
|
2937
|
-
function
|
|
2938
|
-
|
|
2939
|
-
const links = /* @__PURE__ */ new Map();
|
|
2940
|
-
const self = createAttributeLedger(el);
|
|
2941
|
-
let last;
|
|
2942
|
-
const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
|
|
2943
|
-
const active = progress > 0 && progress < 1;
|
|
2944
|
-
if (active === last) return;
|
|
2945
|
-
last = active;
|
|
2946
|
-
self.set("data-kui-active", String(active));
|
|
2947
|
-
if (selector) markLinks(ctx.doc, selector, active, links);
|
|
2948
|
-
});
|
|
4187
|
+
function prepareSmoothScroll(el, params, ctx) {
|
|
4188
|
+
ctx.style.set("scroll-behavior", params.text("behavior", "smooth"));
|
|
2949
4189
|
return () => {
|
|
2950
|
-
untrack();
|
|
2951
|
-
self.restore();
|
|
2952
|
-
for (const ledger of links.values()) ledger.restore();
|
|
2953
4190
|
};
|
|
2954
4191
|
}
|
|
2955
|
-
function spyTarget(selector, ctx) {
|
|
2956
|
-
if (!selector) return selector;
|
|
2957
|
-
const breadth = selectorBreadth(selector, ctx.doc);
|
|
2958
|
-
if (breadth === "invalid") {
|
|
2959
|
-
ctx.warn(`scroll-spy target "${selector}" is not a valid selector and will be ignored`);
|
|
2960
|
-
return "";
|
|
2961
|
-
}
|
|
2962
|
-
if (breadth === "document-wide") {
|
|
2963
|
-
ctx.warn(`scroll-spy target "${selector}" matches the whole document and will be ignored`);
|
|
2964
|
-
return "";
|
|
2965
|
-
}
|
|
2966
|
-
return selector;
|
|
2967
|
-
}
|
|
2968
|
-
function selectorBreadth(selector, doc) {
|
|
2969
|
-
try {
|
|
2970
|
-
if (doc.documentElement.matches(selector)) return "document-wide";
|
|
2971
|
-
if (doc.body?.matches(selector)) return "document-wide";
|
|
2972
|
-
return "ok";
|
|
2973
|
-
} catch {
|
|
2974
|
-
return "invalid";
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
function markLinks(doc, selector, active, links) {
|
|
2978
|
-
for (const link of doc.querySelectorAll(selector)) {
|
|
2979
|
-
let ledger = links.get(link);
|
|
2980
|
-
if (!ledger) {
|
|
2981
|
-
ledger = createAttributeLedger(link);
|
|
2982
|
-
links.set(link, ledger);
|
|
2983
|
-
}
|
|
2984
|
-
ledger.set("data-kui-active", String(active));
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
4192
|
function prepareSnap(el, params, ctx) {
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
);
|
|
2992
|
-
|
|
4193
|
+
const axis = params.is("axis", "x") ? "x" : "y";
|
|
4194
|
+
ctx.style.set("scroll-snap-type", `${axis} ${params.text("strictness", "mandatory")}`);
|
|
4195
|
+
const selector = resolveTarget(params.text("target"), ctx, "scroll-snap");
|
|
4196
|
+
const items = selector ? [...el.querySelectorAll(selector)] : [...el.children];
|
|
4197
|
+
if (selector && items.length === 0) {
|
|
4198
|
+
ctx.warn(`scroll-snap target "${selector}" matched nothing inside this element`);
|
|
4199
|
+
}
|
|
4200
|
+
if (selector) installSnapContainer(axis, ctx);
|
|
4201
|
+
const childLedgers = items.map((child) => createStyleLedger(child));
|
|
2993
4202
|
for (const ledger of childLedgers) ledger.set("scroll-snap-align", params.text("align", "start"));
|
|
2994
4203
|
return () => {
|
|
2995
4204
|
for (const ledger of childLedgers) ledger.restore();
|
|
2996
4205
|
};
|
|
2997
4206
|
}
|
|
4207
|
+
function installSnapContainer(axis, ctx) {
|
|
4208
|
+
ctx.style.set(axis === "x" ? "overflow-x" : "overflow-y", "auto");
|
|
4209
|
+
if (axis === "x") ctx.style.set("display", "flex");
|
|
4210
|
+
}
|
|
2998
4211
|
var SCROLL_PRIMITIVES = [
|
|
2999
4212
|
scrollPrimitive({
|
|
3000
4213
|
id: "pin",
|
|
3001
4214
|
channels: ["layout", "progress"],
|
|
3002
4215
|
parameters: {
|
|
3003
4216
|
...distanceParam,
|
|
3004
|
-
|
|
3005
|
-
spacer: {
|
|
3006
|
-
type: "keyword",
|
|
3007
|
-
default: "false",
|
|
3008
|
-
cssProperty: "--kui-spacer",
|
|
3009
|
-
values: ["true", "false"]
|
|
3010
|
-
}
|
|
4217
|
+
...stickyParams
|
|
3011
4218
|
},
|
|
3012
4219
|
prepare: deferPrepare(preparePin),
|
|
3013
4220
|
perfClass: "layout"
|
|
@@ -3017,7 +4224,10 @@ var SCROLL_PRIMITIVES = [
|
|
|
3017
4224
|
channels: ["progress"],
|
|
3018
4225
|
parameters: {
|
|
3019
4226
|
...distanceParam,
|
|
3020
|
-
steps: { type: "number", default: "0", cssProperty: "--kui-steps" }
|
|
4227
|
+
steps: { type: "number", default: "0", cssProperty: "--kui-steps" },
|
|
4228
|
+
// Same name, same shape and the same validation as scroll-spy's: one `target:` convention
|
|
4229
|
+
// across the library rather than a second word for "the elements this effect marks".
|
|
4230
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" }
|
|
3021
4231
|
},
|
|
3022
4232
|
prepare: deferPrepare(prepareProgress)
|
|
3023
4233
|
}),
|
|
@@ -3026,7 +4236,16 @@ var SCROLL_PRIMITIVES = [
|
|
|
3026
4236
|
channels: ["translate", "progress"],
|
|
3027
4237
|
parameters: {
|
|
3028
4238
|
...distanceParam,
|
|
3029
|
-
travel: { type: "text", default: "auto", cssProperty: "--kui-travel" }
|
|
4239
|
+
travel: { type: "text", default: "auto", cssProperty: "--kui-travel" },
|
|
4240
|
+
// Same name, same shape and the same validation as scroll-spy's and media-scrub's: one
|
|
4241
|
+
// `target:` convention across the library. Naming the row that moves is also what opts this
|
|
4242
|
+
// primitive into owning the stage, the sticky window and the row's layout itself.
|
|
4243
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" },
|
|
4244
|
+
"offset-top": {
|
|
4245
|
+
type: "length",
|
|
4246
|
+
default: "var(--kui-pin-offset, 0px)",
|
|
4247
|
+
cssProperty: "--kui-offset-top"
|
|
4248
|
+
}
|
|
3030
4249
|
},
|
|
3031
4250
|
prepare: deferPrepare(prepareHorizontal)
|
|
3032
4251
|
}),
|
|
@@ -3035,8 +4254,13 @@ var SCROLL_PRIMITIVES = [
|
|
|
3035
4254
|
channels: ["media", "progress"],
|
|
3036
4255
|
parameters: {
|
|
3037
4256
|
...distanceParam,
|
|
4257
|
+
// A scrub is a hold, so it needs the same two knobs a pin does. Declaring them here is what
|
|
4258
|
+
// lets `sequence-scrub` become one attribute with no wrapper at all.
|
|
4259
|
+
...stickyParams,
|
|
3038
4260
|
frames: { type: "number", default: "1", cssProperty: "--kui-frames" },
|
|
3039
|
-
src: { type: "text", default: "", cssProperty: "--kui-src" }
|
|
4261
|
+
src: { type: "text", default: "", cssProperty: "--kui-src" },
|
|
4262
|
+
// The preferred form. `frames:`/`src:` remain for sequences too long to author as tags.
|
|
4263
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" }
|
|
3040
4264
|
},
|
|
3041
4265
|
prepare: deferPrepare(prepareMediaScrub),
|
|
3042
4266
|
perfClass: "paint"
|
|
@@ -3045,11 +4269,44 @@ var SCROLL_PRIMITIVES = [
|
|
|
3045
4269
|
id: "scroll-spy",
|
|
3046
4270
|
channels: ["state"],
|
|
3047
4271
|
parameters: {
|
|
4272
|
+
// `distance`: the per-section form only. `offset-top`: the container form only. Each is a
|
|
4273
|
+
// no-op — warned, not silent — in the other; see `prepareScrollSpySingle` and
|
|
4274
|
+
// `prepareScrollSpyContainer`.
|
|
3048
4275
|
...distanceParam,
|
|
3049
|
-
|
|
4276
|
+
// Same name and meaning in both forms: the link(s) this instance marks. Per-section, the
|
|
4277
|
+
// one link this section names; with `sections:`, every link `target:` matches, each paired
|
|
4278
|
+
// to its own section by `href`. See `prepareScrollSpyContainer`.
|
|
4279
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" },
|
|
4280
|
+
// Presence, not value, selects the container form: authoring this at all switches
|
|
4281
|
+
// `prepareScrollSpy` from one-section-per-instance to one-instance-on-the-shared-ancestor.
|
|
4282
|
+
sections: { type: "text", default: "", cssProperty: "--kui-sections" },
|
|
4283
|
+
"offset-top": { type: "length", default: "0px", cssProperty: "--kui-offset-top" }
|
|
3050
4284
|
},
|
|
3051
4285
|
prepare: deferPrepare(prepareScrollSpy)
|
|
3052
4286
|
}),
|
|
4287
|
+
scrollPrimitive({
|
|
4288
|
+
id: "smooth-scroll",
|
|
4289
|
+
/*
|
|
4290
|
+
* Its own channel, not the `'layout'` it used to share with `pin`, `stacking-cards` and
|
|
4291
|
+
* `scroll-snap`. The channel model exists to stop two effects fighting over the same CSS
|
|
4292
|
+
* property, and this one writes exactly `scroll-behavior` — a property that describes how a
|
|
4293
|
+
* *user-or-script-initiated* scroll is performed, and that no other primitive touches.
|
|
4294
|
+
*
|
|
4295
|
+
* On `'layout'` it made a legitimate pairing impossible. Both `smooth-scroll` and
|
|
4296
|
+
* `scroll-snap` have to sit on the document element to have any effect at all — neither
|
|
4297
|
+
* `scroll-behavior` nor `scroll-snap-type` is propagated to the viewport from `<body>` — so
|
|
4298
|
+
* "apply them to nested elements", the advice the conflict message gives, has no valid
|
|
4299
|
+
* nesting to offer here. `data-kui="smooth-scroll-to, scroll-snap-y"` on `<html>` is the
|
|
4300
|
+
* ordinary way to ask for smooth anchor jumps on a page that also snaps, and it was refused
|
|
4301
|
+
* for a collision that cannot happen: the two write disjoint properties.
|
|
4302
|
+
*/
|
|
4303
|
+
channels: ["scroll-behavior"],
|
|
4304
|
+
parameters: {
|
|
4305
|
+
behavior: { type: "keyword", default: "smooth", cssProperty: "--kui-scroll-behavior", values: ["smooth", "auto"] }
|
|
4306
|
+
},
|
|
4307
|
+
prepare: deferPrepare(prepareSmoothScroll),
|
|
4308
|
+
perfClass: "layout"
|
|
4309
|
+
}),
|
|
3053
4310
|
scrollPrimitive({
|
|
3054
4311
|
id: "scroll-snap",
|
|
3055
4312
|
channels: ["layout"],
|
|
@@ -3066,7 +4323,11 @@ var SCROLL_PRIMITIVES = [
|
|
|
3066
4323
|
default: "start",
|
|
3067
4324
|
cssProperty: "--kui-snap-align",
|
|
3068
4325
|
values: ["start", "center", "end"]
|
|
3069
|
-
}
|
|
4326
|
+
},
|
|
4327
|
+
// Names the snap items. Also what opts this primitive into owning the scroll container
|
|
4328
|
+
// itself — see `installSnapContainer`. Without it, the direct children are the items and
|
|
4329
|
+
// the page keeps its own `overflow`, exactly as before.
|
|
4330
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" }
|
|
3070
4331
|
},
|
|
3071
4332
|
prepare: deferPrepare(prepareSnap),
|
|
3072
4333
|
perfClass: "layout"
|
|
@@ -3089,11 +4350,28 @@ var SCROLL_PRESETS = [
|
|
|
3089
4350
|
// --- travel ------------------------------------------------------------------------------
|
|
3090
4351
|
{ name: "horizontal-scroll", primitive: "horizontal-track" },
|
|
3091
4352
|
// --- media -------------------------------------------------------------------------------
|
|
3092
|
-
|
|
4353
|
+
/*
|
|
4354
|
+
* `spacer:true` is what deletes the `.scrub-stage` wrapper a page used to hand-write.
|
|
4355
|
+
*
|
|
4356
|
+
* It could not be switched on until the tracker stopped measuring against the parent. A scrub
|
|
4357
|
+
* makes itself sticky, and `geometrySource` escapes a sticky subtree by taking its parent — so
|
|
4358
|
+
* with the wrapper gone the scrub was measured against whatever section contained it. Measured
|
|
4359
|
+
* on `demo/scroll.html`: the parent started 926px above the scrub against a 1817px distance, so
|
|
4360
|
+
* progress reached 51% before the element had even stuck and half the sequence played off
|
|
4361
|
+
* screen. The wrapper was not ceremony; it was the tight box that made the parent honest.
|
|
4362
|
+
*
|
|
4363
|
+
* `trackProgress`'s `contentAnchor` is the fix. Progress is read from the spacer, which the
|
|
4364
|
+
* library inserts, is exactly `distance` tall, is never sticky, and moves with the content — so
|
|
4365
|
+
* there is no wrapper to write and nothing to disagree with.
|
|
4366
|
+
*
|
|
4367
|
+
* `video-scrub` stays off: a video positioned by the page is not asking the library for a box.
|
|
4368
|
+
*/
|
|
4369
|
+
{ name: "sequence-scrub", primitive: "media-scrub", params: { spacer: "true" } },
|
|
3093
4370
|
{ name: "video-scrub", primitive: "media-scrub" },
|
|
3094
4371
|
// --- navigation --------------------------------------------------------------------------
|
|
3095
4372
|
{ name: "scroll-spy", primitive: "scroll-spy" },
|
|
3096
4373
|
// --- native CSS passthroughs ---------------------------------------------------------------
|
|
4374
|
+
{ name: "smooth-scroll-to", primitive: "smooth-scroll" },
|
|
3097
4375
|
{ name: "scroll-snap-x", primitive: "scroll-snap", params: { axis: "x" } },
|
|
3098
4376
|
{ name: "scroll-snap-y", primitive: "scroll-snap", params: { axis: "y" } }
|
|
3099
4377
|
];
|
|
@@ -3108,11 +4386,16 @@ var COMMAND = /([mlhvcz])|(-?(?:\d+(?:\.\d+)?|\.\d+))/gi;
|
|
|
3108
4386
|
var UNSUPPORTED = /[AaSsQqTt]/;
|
|
3109
4387
|
function parsePath(d) {
|
|
3110
4388
|
if (UNSUPPORTED.test(d)) {
|
|
3111
|
-
return {
|
|
4389
|
+
return {
|
|
4390
|
+
segments: [],
|
|
4391
|
+
subpaths: [],
|
|
4392
|
+
reason: "arc and shorthand commands (A S Q T) are not supported"
|
|
4393
|
+
};
|
|
3112
4394
|
}
|
|
3113
4395
|
const tokens = [...d.matchAll(COMMAND)].map((m) => m[1] ?? m[2]);
|
|
3114
4396
|
const state = {
|
|
3115
|
-
|
|
4397
|
+
subpaths: [],
|
|
4398
|
+
open: void 0,
|
|
3116
4399
|
current: { x: 0, y: 0 },
|
|
3117
4400
|
start: { x: 0, y: 0 },
|
|
3118
4401
|
command: ""
|
|
@@ -3126,18 +4409,33 @@ function parsePath(d) {
|
|
|
3126
4409
|
index++;
|
|
3127
4410
|
continue;
|
|
3128
4411
|
}
|
|
3129
|
-
if (!state.command)
|
|
4412
|
+
if (!state.command) {
|
|
4413
|
+
return { segments: [], subpaths: [], reason: "path must start with a command letter" };
|
|
4414
|
+
}
|
|
3130
4415
|
index = consume(tokens, index, state);
|
|
3131
4416
|
}
|
|
3132
|
-
if (state.
|
|
3133
|
-
|
|
4417
|
+
if (state.subpaths.length === 0) {
|
|
4418
|
+
return { segments: [], subpaths: [], reason: "no drawable segments" };
|
|
4419
|
+
}
|
|
4420
|
+
return { segments: state.subpaths.flatMap((sub) => sub.segments), subpaths: state.subpaths };
|
|
4421
|
+
}
|
|
4422
|
+
function pushSegment(state, segment) {
|
|
4423
|
+
if (!state.open) {
|
|
4424
|
+
state.open = { segments: [], closed: false };
|
|
4425
|
+
state.subpaths.push(state.open);
|
|
4426
|
+
}
|
|
4427
|
+
state.open.segments.push(segment);
|
|
3134
4428
|
}
|
|
3135
4429
|
var ARITY = { m: 2, l: 2, h: 1, v: 1, c: 6, z: 0 };
|
|
3136
4430
|
function closeSubpath(state) {
|
|
3137
|
-
const { current, start } = state;
|
|
3138
|
-
if (
|
|
3139
|
-
|
|
4431
|
+
const { current, start, open } = state;
|
|
4432
|
+
if (!open) return;
|
|
4433
|
+
if (Math.abs(current.x - start.x) >= 1e-6 || Math.abs(current.y - start.y) >= 1e-6) {
|
|
4434
|
+
open.segments.push(lineToCubic(current, start));
|
|
4435
|
+
}
|
|
4436
|
+
open.closed = true;
|
|
3140
4437
|
state.current = { ...start };
|
|
4438
|
+
state.open = void 0;
|
|
3141
4439
|
}
|
|
3142
4440
|
function consume(tokens, index, state) {
|
|
3143
4441
|
const key = state.command.toLowerCase();
|
|
@@ -3149,10 +4447,11 @@ function consume(tokens, index, state) {
|
|
|
3149
4447
|
if (key === "m") {
|
|
3150
4448
|
state.current = next;
|
|
3151
4449
|
state.start = next;
|
|
4450
|
+
state.open = void 0;
|
|
3152
4451
|
state.command = relative ? "l" : "L";
|
|
3153
4452
|
return index + arity;
|
|
3154
4453
|
}
|
|
3155
|
-
state
|
|
4454
|
+
pushSegment(state, straightOrCubic({ key, args, from: state.current, relative }, next));
|
|
3156
4455
|
state.current = next;
|
|
3157
4456
|
return index + arity;
|
|
3158
4457
|
}
|
|
@@ -3215,34 +4514,78 @@ function normaliseCount(segments, target) {
|
|
|
3215
4514
|
function chordLength(segment) {
|
|
3216
4515
|
return Math.hypot(segment.to.x - segment.from.x, segment.to.y - segment.from.y);
|
|
3217
4516
|
}
|
|
3218
|
-
function
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
);
|
|
4517
|
+
function round(value) {
|
|
4518
|
+
return Math.round(value * 100) / 100;
|
|
4519
|
+
}
|
|
4520
|
+
function subpathsToPathData(subpaths) {
|
|
4521
|
+
const parts = [];
|
|
4522
|
+
for (const sub of subpaths) {
|
|
4523
|
+
const head = sub.segments[0];
|
|
4524
|
+
parts.push(`M${round(head.from.x)},${round(head.from.y)}`);
|
|
4525
|
+
for (const s of sub.segments) {
|
|
4526
|
+
parts.push(
|
|
4527
|
+
`C${round(s.c1.x)},${round(s.c1.y)} ${round(s.c2.x)},${round(s.c2.y)} ${round(s.to.x)},${round(s.to.y)}`
|
|
4528
|
+
);
|
|
4529
|
+
}
|
|
4530
|
+
if (sub.closed) parts.push("Z");
|
|
3226
4531
|
}
|
|
3227
4532
|
return parts.join(" ");
|
|
3228
4533
|
}
|
|
3229
|
-
function
|
|
3230
|
-
|
|
4534
|
+
function centroidOf(subpath) {
|
|
4535
|
+
let x = 0;
|
|
4536
|
+
let y = 0;
|
|
4537
|
+
for (const s of subpath.segments) {
|
|
4538
|
+
x += s.from.x;
|
|
4539
|
+
y += s.from.y;
|
|
4540
|
+
}
|
|
4541
|
+
return { x: x / subpath.segments.length, y: y / subpath.segments.length };
|
|
4542
|
+
}
|
|
4543
|
+
function collapsedLike(partner) {
|
|
4544
|
+
const at = centroidOf(partner);
|
|
4545
|
+
return {
|
|
4546
|
+
segments: partner.segments.map(() => ({
|
|
4547
|
+
from: { ...at },
|
|
4548
|
+
c1: { ...at },
|
|
4549
|
+
c2: { ...at },
|
|
4550
|
+
to: { ...at }
|
|
4551
|
+
})),
|
|
4552
|
+
closed: partner.closed
|
|
4553
|
+
};
|
|
4554
|
+
}
|
|
4555
|
+
function normaliseSubpaths(a, b) {
|
|
4556
|
+
const count = Math.max(a.length, b.length);
|
|
4557
|
+
const from = [];
|
|
4558
|
+
const to = [];
|
|
4559
|
+
for (let i = 0; i < count; i++) {
|
|
4560
|
+
const left = a[i] ?? collapsedLike(b[i]);
|
|
4561
|
+
const right = b[i] ?? collapsedLike(a[i]);
|
|
4562
|
+
const target = Math.max(left.segments.length, right.segments.length);
|
|
4563
|
+
const closed = left.closed && right.closed;
|
|
4564
|
+
from.push({ segments: normaliseCount(left.segments, target), closed });
|
|
4565
|
+
to.push({ segments: normaliseCount(right.segments, target), closed });
|
|
4566
|
+
}
|
|
4567
|
+
return { from, to };
|
|
3231
4568
|
}
|
|
3232
4569
|
function createMorph(fromPath, toPath) {
|
|
3233
4570
|
const a = parsePath(fromPath);
|
|
3234
4571
|
const b = parsePath(toPath);
|
|
3235
4572
|
if (a.reason) return { reason: `start path: ${a.reason}` };
|
|
3236
4573
|
if (b.reason) return { reason: `end path: ${b.reason}` };
|
|
3237
|
-
const
|
|
3238
|
-
const
|
|
3239
|
-
const to = normaliseCount(b.segments, count);
|
|
4574
|
+
const { from, to } = normaliseSubpaths(a.subpaths, b.subpaths);
|
|
4575
|
+
const count = from.reduce((total, sub) => total + sub.segments.length, 0);
|
|
3240
4576
|
return {
|
|
3241
4577
|
morph: {
|
|
3242
4578
|
segmentCount: count,
|
|
3243
4579
|
at(t) {
|
|
3244
4580
|
const clamped = Math.min(1, Math.max(0, t));
|
|
3245
|
-
return
|
|
4581
|
+
return subpathsToPathData(
|
|
4582
|
+
from.map((sub, i) => ({
|
|
4583
|
+
segments: sub.segments.map(
|
|
4584
|
+
(segment, j) => lerpCubic(segment, to[i].segments[j], clamped)
|
|
4585
|
+
),
|
|
4586
|
+
closed: sub.closed
|
|
4587
|
+
}))
|
|
4588
|
+
);
|
|
3246
4589
|
}
|
|
3247
4590
|
}
|
|
3248
4591
|
};
|
|
@@ -3299,6 +4642,35 @@ function prepareMorph(el, params, ctx) {
|
|
|
3299
4642
|
path.setAttribute("d", startPath);
|
|
3300
4643
|
};
|
|
3301
4644
|
}
|
|
4645
|
+
var PATH_DRAW_PRIMITIVE = cssPrimitive("path-draw", [CHANNEL.stroke], {
|
|
4646
|
+
parameters: {
|
|
4647
|
+
length: { type: "number", default: "100", cssProperty: "--kui-path-length", finite: true }
|
|
4648
|
+
}
|
|
4649
|
+
});
|
|
4650
|
+
var SHAPE_FILL_PRIMITIVE = cssPrimitive("shape-fill", [CHANNEL.clip]);
|
|
4651
|
+
var BAR_GROW_PRIMITIVE = cssPrimitive("bar-grow", [CHANNEL.scale], {
|
|
4652
|
+
parameters: { from: { type: "number", default: "0", cssProperty: "--kui-bar-from" } }
|
|
4653
|
+
});
|
|
4654
|
+
var LOGO_BUILD_PRIMITIVE = cssPrimitive("logo-assemble", [
|
|
4655
|
+
CHANNEL.opacity,
|
|
4656
|
+
CHANNEL.scale,
|
|
4657
|
+
CHANNEL.rotate
|
|
4658
|
+
]);
|
|
4659
|
+
var ICON_TOGGLE_PRIMITIVE = {
|
|
4660
|
+
id: "icon-toggle",
|
|
4661
|
+
renderer: "javascript",
|
|
4662
|
+
channels: [CHANNEL.translate, CHANNEL.rotate, CHANNEL.scale, CHANNEL.opacity, CHANNEL.clip],
|
|
4663
|
+
parameters: {
|
|
4664
|
+
duration: { type: "time", default: "260ms", cssProperty: "--kui-duration" },
|
|
4665
|
+
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
|
|
4666
|
+
},
|
|
4667
|
+
supportedTimelines: ["time"],
|
|
4668
|
+
supportedActivations: ["load"],
|
|
4669
|
+
defaultActivation: "load",
|
|
4670
|
+
perfClass: "compositor",
|
|
4671
|
+
reducedMotion: "disable",
|
|
4672
|
+
prepare: () => inertInstance()
|
|
4673
|
+
};
|
|
3302
4674
|
var SVG_PRIMITIVES = [
|
|
3303
4675
|
{
|
|
3304
4676
|
id: "path-morph",
|
|
@@ -3318,19 +4690,109 @@ var SVG_PRIMITIVES = [
|
|
|
3318
4690
|
perfClass: "paint",
|
|
3319
4691
|
reducedMotion: "disable",
|
|
3320
4692
|
prepare: deferPrepare(prepareMorph)
|
|
3321
|
-
}
|
|
4693
|
+
},
|
|
4694
|
+
PATH_DRAW_PRIMITIVE,
|
|
4695
|
+
SHAPE_FILL_PRIMITIVE,
|
|
4696
|
+
BAR_GROW_PRIMITIVE,
|
|
4697
|
+
LOGO_BUILD_PRIMITIVE,
|
|
4698
|
+
ICON_TOGGLE_PRIMITIVE
|
|
3322
4699
|
];
|
|
3323
4700
|
var SVG_PRESETS = [
|
|
3324
4701
|
{ name: "icon-morph", primitive: "path-morph" },
|
|
3325
|
-
{ name: "blob-morph", primitive: "path-morph", params: { duration: "800ms" } }
|
|
4702
|
+
{ name: "blob-morph", primitive: "path-morph", params: { duration: "800ms" } },
|
|
4703
|
+
// Stroke draws. One keyframe block each rather than one shared block, matching the
|
|
4704
|
+
// progress-ring/gauge-sweep/donut-sweep/sparkline-draw group in numbers.ts: identical bodies
|
|
4705
|
+
// today, but each name is free to diverge and a consumer can restyle one without the others.
|
|
4706
|
+
{ name: "draw-stroke", primitive: "path-draw", keyframes: "kui-draw-stroke", params: { duration: "800ms", ease: "ease-in-out" } },
|
|
4707
|
+
{ name: "draw-signature", primitive: "path-draw", keyframes: "kui-draw-signature", params: { duration: "1600ms", ease: "ease-in-out" } },
|
|
4708
|
+
{ name: "draw-underline", primitive: "path-draw", keyframes: "kui-draw-underline", params: { duration: "420ms" } },
|
|
4709
|
+
{ name: "checkmark-draw", primitive: "path-draw", keyframes: "kui-checkmark-draw", params: { duration: "320ms" } },
|
|
4710
|
+
{ name: "cross-draw", primitive: "path-draw", keyframes: "kui-cross-draw", params: { duration: "260ms" } },
|
|
4711
|
+
{ name: "chart-line-draw", primitive: "path-draw", keyframes: "kui-chart-line-draw", params: { duration: "1200ms", ease: "ease-in-out" } },
|
|
4712
|
+
{ name: "gradient-stroke", primitive: "path-draw", keyframes: "kui-gradient-stroke", params: { duration: "2400ms", ease: "ease-in-out" } },
|
|
4713
|
+
// Fills.
|
|
4714
|
+
{ name: "heart-fill", primitive: "shape-fill", keyframes: "kui-heart-fill", params: { duration: "420ms" } },
|
|
4715
|
+
{ name: "bookmark-fill", primitive: "shape-fill", keyframes: "kui-bookmark-fill", params: { duration: "360ms" } },
|
|
4716
|
+
{ name: "chart-area-fill", primitive: "shape-fill", keyframes: "kui-chart-area-fill", params: { duration: "900ms" } },
|
|
4717
|
+
// `cloak: true`: `kui-chart-bar-grow`'s `from { scale: 1 0 }` (svg.css) is a zero-height box
|
|
4718
|
+
// while paused, not just an invisible one — so it occupies no space in layout for the whole
|
|
4719
|
+
// wait. Same defect `fold-panel` has, and the same fix shape: see svg.css's
|
|
4720
|
+
// `[data-kui-fx~='chart-bar-grow'][data-kui-state='ready']` rule.
|
|
4721
|
+
{
|
|
4722
|
+
name: "chart-bar-grow",
|
|
4723
|
+
primitive: "bar-grow",
|
|
4724
|
+
keyframes: "kui-chart-bar-grow",
|
|
4725
|
+
params: { duration: "700ms", ease: "back-out" },
|
|
4726
|
+
cloak: true
|
|
4727
|
+
},
|
|
4728
|
+
{ name: "logo-build", primitive: "logo-assemble", keyframes: "kui-logo-build", params: { duration: "520ms", ease: "back-out" } },
|
|
4729
|
+
// Icon toggles — no `keyframes`, because their motion is a CSS transition in svg.css keyed off
|
|
4730
|
+
// aria state, not a compiled animation. Same shape as forms.ts's native-state presets.
|
|
4731
|
+
{ name: "hamburger-to-x", primitive: "icon-toggle" },
|
|
4732
|
+
{ name: "play-to-pause", primitive: "icon-toggle" },
|
|
4733
|
+
{ name: "plus-to-minus", primitive: "icon-toggle" }
|
|
3326
4734
|
];
|
|
3327
4735
|
function registerSvg(registry) {
|
|
3328
4736
|
return registry.registerPrimitives(SVG_PRIMITIVES).registerPresets(SVG_PRESETS);
|
|
3329
4737
|
}
|
|
3330
4738
|
|
|
3331
4739
|
// src/effects/three-d/index.ts
|
|
4740
|
+
var FLIP_CONTROL_SELECTOR = ":scope > .kui-flip-control";
|
|
4741
|
+
function prepareCardToggle(el, params, ctx) {
|
|
4742
|
+
const trigger = params.text("trigger", "click");
|
|
4743
|
+
if (trigger === "click") return () => {
|
|
4744
|
+
};
|
|
4745
|
+
if (!supportsFineHover(ctx.win)) return () => {
|
|
4746
|
+
};
|
|
4747
|
+
const control = el.querySelector(FLIP_CONTROL_SELECTOR);
|
|
4748
|
+
if (!control) {
|
|
4749
|
+
ctx.warn(`flip-card trigger:${trigger} found no direct-child .kui-flip-control \u2014 the card will not flip`);
|
|
4750
|
+
return () => {
|
|
4751
|
+
};
|
|
4752
|
+
}
|
|
4753
|
+
const set = (flipped) => control.setAttribute("aria-pressed", String(flipped));
|
|
4754
|
+
const isFlipped = () => control.getAttribute("aria-pressed") === "true";
|
|
4755
|
+
const onEnter = () => {
|
|
4756
|
+
set(trigger === "hover-toggle" ? !isFlipped() : true);
|
|
4757
|
+
};
|
|
4758
|
+
const onLeave = () => set(false);
|
|
4759
|
+
el.addEventListener("pointerenter", onEnter, { passive: true });
|
|
4760
|
+
if (trigger === "hover") el.addEventListener("pointerleave", onLeave, { passive: true });
|
|
4761
|
+
return () => {
|
|
4762
|
+
el.removeEventListener("pointerenter", onEnter);
|
|
4763
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
4764
|
+
};
|
|
4765
|
+
}
|
|
4766
|
+
var CARD_TOGGLE_PRIMITIVE = {
|
|
4767
|
+
id: "card-toggle",
|
|
4768
|
+
renderer: "javascript",
|
|
4769
|
+
channels: [CHANNEL.rotate],
|
|
4770
|
+
parameters: {
|
|
4771
|
+
duration: { type: "time", default: "700ms", cssProperty: "--kui-duration" },
|
|
4772
|
+
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
|
|
4773
|
+
perspective: { type: "length", default: "1600px", cssProperty: "--kui-perspective" },
|
|
4774
|
+
trigger: {
|
|
4775
|
+
type: "keyword",
|
|
4776
|
+
default: "click",
|
|
4777
|
+
cssProperty: "--kui-flip-trigger",
|
|
4778
|
+
values: ["click", "hover", "hover-latch", "hover-toggle"]
|
|
4779
|
+
}
|
|
4780
|
+
},
|
|
4781
|
+
supportedTimelines: ["time"],
|
|
4782
|
+
supportedActivations: ["load"],
|
|
4783
|
+
defaultActivation: "load",
|
|
4784
|
+
perfClass: "compositor",
|
|
4785
|
+
reducedMotion: "disable",
|
|
4786
|
+
prepare: deferPrepare(prepareCardToggle)
|
|
4787
|
+
};
|
|
3332
4788
|
var THREE_D_PRIMITIVES = [
|
|
3333
|
-
|
|
4789
|
+
// `skew`, not `rotate`: the keyframes in three-d.css write `transform: perspective(...)
|
|
4790
|
+
// rotateX/Y(...)`, not the individual `rotate:` property — `perspective` only creates depth for
|
|
4791
|
+
// an element's *children*, so giving one of these effects its own depth means reaching for the
|
|
4792
|
+
// `perspective()` transform *function*, which only exists inside the `transform` shorthand.
|
|
4793
|
+
// `CHANNEL.skew` is this catalog's name for "claims the whole `transform` shorthand"; see the
|
|
4794
|
+
// comment on it in `core/types.ts`.
|
|
4795
|
+
cssPrimitive("flip-face", [CHANNEL.skew], {
|
|
3334
4796
|
parameters: {
|
|
3335
4797
|
angle: { type: "angle", default: "180deg", cssProperty: "--kui-from-angle" },
|
|
3336
4798
|
perspective: { type: "length", default: "1200px", cssProperty: "--kui-perspective" }
|
|
@@ -3340,12 +4802,19 @@ var THREE_D_PRIMITIVES = [
|
|
|
3340
4802
|
parameters: { distance: { type: "length", default: "40px", cssProperty: "--kui-distance" } }
|
|
3341
4803
|
}),
|
|
3342
4804
|
cssPrimitive("wipe", [CHANNEL.clip]),
|
|
3343
|
-
|
|
4805
|
+
// `from` gives `loading-bar` a real knob on its start scale — it had none before, unlike
|
|
4806
|
+
// `flip-face`'s `angle:` two rows up. `--kui-bar-from` is also what the fold-panel-style
|
|
4807
|
+
// `[data-kui-fx~='loading-bar'][data-kui-state='ready']` gate neutralizes in three-d.css; see
|
|
4808
|
+
// that rule's comment for the on:enter fix this parameter doubles as.
|
|
4809
|
+
cssPrimitive("bar", [CHANNEL.scale], {
|
|
4810
|
+
parameters: { from: { type: "number", default: "0", cssProperty: "--kui-bar-from" } }
|
|
4811
|
+
}),
|
|
4812
|
+
CARD_TOGGLE_PRIMITIVE
|
|
3344
4813
|
];
|
|
3345
4814
|
var THREE_D_PRESETS = [
|
|
3346
4815
|
// --- 3D & perspective ---
|
|
3347
|
-
{ name: "card-flip-y", primitive: "flip-face", keyframes: "kui-card-flip-y" },
|
|
3348
|
-
{ name: "card-flip-x", primitive: "flip-face", keyframes: "kui-card-flip-x" },
|
|
4816
|
+
{ name: "card-flip-y", primitive: "flip-face", keyframes: "kui-card-flip-y", cloak: true },
|
|
4817
|
+
{ name: "card-flip-x", primitive: "flip-face", keyframes: "kui-card-flip-x", cloak: true },
|
|
3349
4818
|
{ name: "cube-rotate", primitive: "flip-face", keyframes: "kui-cube-rotate", params: { angle: "90deg" } },
|
|
3350
4819
|
{
|
|
3351
4820
|
name: "book-page-turn",
|
|
@@ -3353,17 +4822,32 @@ var THREE_D_PRESETS = [
|
|
|
3353
4822
|
keyframes: "kui-book-page-turn",
|
|
3354
4823
|
params: { angle: "-160deg", duration: "900ms" }
|
|
3355
4824
|
},
|
|
4825
|
+
// `cloak: true`, unlike its `flip-face` siblings above: those are `to`-only keyframes, so their
|
|
4826
|
+
// paused/waiting box is the ordinary, untransformed rest state. `fold-panel` is `from`-only —
|
|
4827
|
+
// its `rotateX(-90deg)` (three-d.css) *is* the paused box, edge-on and zero-height, so it holds
|
|
4828
|
+
// no space in layout for the whole wait; see the
|
|
4829
|
+
// `[data-kui-fx~='fold-panel'][data-kui-state='ready']` rule in three-d.css for the other half
|
|
4830
|
+
// of that fix. `cloak` only ever hid the pre-JS flash, not this, but adding it here keeps the
|
|
4831
|
+
// pre-JS and post-JS "ready" appearances the same (invisible) instead of trading one flash for
|
|
4832
|
+
// the other.
|
|
3356
4833
|
{
|
|
3357
4834
|
name: "fold-panel",
|
|
3358
4835
|
primitive: "flip-face",
|
|
3359
4836
|
keyframes: "kui-fold-panel",
|
|
3360
|
-
params: { angle: "-90deg" }
|
|
4837
|
+
params: { angle: "-90deg" },
|
|
4838
|
+
cloak: true
|
|
3361
4839
|
},
|
|
3362
4840
|
// --- page transitions ---
|
|
3363
4841
|
{ name: "page-fade", primitive: "page-reveal", keyframes: "kui-page-fade" },
|
|
3364
4842
|
{ name: "page-slide", primitive: "page-reveal", keyframes: "kui-page-slide" },
|
|
3365
4843
|
{ name: "curtain-wipe", primitive: "wipe", keyframes: "kui-curtain-wipe", params: { duration: "800ms" } },
|
|
3366
|
-
|
|
4844
|
+
// `cloak: true` for the same reason as `fold-panel`: `kui-loading-bar`'s `from { scale: 0 1 }`
|
|
4845
|
+
// (three-d.css) is a zero-width box, not just an invisible one — see that file's
|
|
4846
|
+
// `[data-kui-fx~='loading-bar'][data-kui-state='ready']` rule.
|
|
4847
|
+
{ name: "loading-bar", primitive: "bar", keyframes: "kui-loading-bar", cloak: true },
|
|
4848
|
+
// No `keyframes`: its motion is a CSS transition in three-d.css keyed off the control's
|
|
4849
|
+
// aria-pressed, not a compiled animation. Same shape as the icon toggles in svg.ts.
|
|
4850
|
+
{ name: "flip-card", primitive: "card-toggle" }
|
|
3367
4851
|
];
|
|
3368
4852
|
function registerThreeD(registry) {
|
|
3369
4853
|
return registry.registerPrimitives(THREE_D_PRIMITIVES).registerPresets(THREE_D_PRESETS);
|