kuinetic 0.1.3 → 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 +2 -2
- package/dist/esm/{chunk-5DON3UFQ.mjs → chunk-7WMNPIOZ.mjs} +543 -40
- package/dist/esm/{chunk-QUVFODSQ.mjs → chunk-TJDDIQRG.mjs} +15 -3
- package/dist/esm/core/index.mjs +1 -1
- package/dist/esm/effects/index.mjs +1 -1
- package/dist/esm/index.mjs +2 -2
- package/dist/kuinetic.all.js +559 -44
- package/dist/kuinetic.css +120 -63
- package/dist/kuinetic.js +558 -43
- package/dist/types/core/types.d.ts +6 -0
- package/dist/types/effects/catalog/background-media.d.ts +152 -0
- package/dist/types/effects/catalog/shared.d.ts +2 -2
- package/dist/types/effects/catalog/text-shared.d.ts +6 -0
- package/dist/types/effects/shared.d.ts +40 -0
- package/package.json +1 -1
|
@@ -15,9 +15,13 @@ import {
|
|
|
15
15
|
} from "./chunk-JT4PZL3A.mjs";
|
|
16
16
|
|
|
17
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"];
|
|
18
22
|
var COMMON = {
|
|
19
23
|
duration: { type: "time", default: "600ms", cssProperty: "--kui-duration" },
|
|
20
|
-
|
|
24
|
+
...TRIGGER_DELAY_PARAM,
|
|
21
25
|
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
|
|
22
26
|
stagger: { type: "time", default: "0ms", cssProperty: "--kui-stagger" }
|
|
23
27
|
};
|
|
@@ -45,7 +49,14 @@ function cssPrimitive(id, channels, options = {}) {
|
|
|
45
49
|
// src/effects/catalog/ambient.ts
|
|
46
50
|
var drift = {
|
|
47
51
|
duration: { type: "time", default: "10s", cssProperty: "--kui-duration" },
|
|
48
|
-
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" }
|
|
49
60
|
};
|
|
50
61
|
var float = {
|
|
51
62
|
duration: { type: "time", default: "4s", cssProperty: "--kui-duration" },
|
|
@@ -71,6 +82,34 @@ var AMBIENT_PRIMITIVES = [
|
|
|
71
82
|
reducedMotion: "disable",
|
|
72
83
|
perfClass: "continuous"
|
|
73
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
|
+
}),
|
|
74
113
|
cssPrimitive("ambient-float", [CHANNEL.translate], {
|
|
75
114
|
parameters: float,
|
|
76
115
|
defaultActivation: "load",
|
|
@@ -95,49 +134,58 @@ var AMBIENT_PRESETS = [
|
|
|
95
134
|
{ name: "aurora", primitive: "ambient-gradient", keyframes: "kui-aurora" },
|
|
96
135
|
{
|
|
97
136
|
name: "gradient-rotate-border",
|
|
98
|
-
primitive: "ambient-gradient",
|
|
137
|
+
primitive: "ambient-gradient-ring",
|
|
99
138
|
keyframes: "kui-gradient-rotate-border",
|
|
100
139
|
params: { duration: "6s", ease: "linear" }
|
|
101
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
|
+
},
|
|
102
150
|
{
|
|
103
151
|
name: "noise-overlay",
|
|
104
|
-
primitive: "ambient-
|
|
152
|
+
primitive: "ambient-tint",
|
|
105
153
|
keyframes: "kui-noise-overlay",
|
|
106
154
|
params: { duration: "650ms", ease: "steps(6)" }
|
|
107
155
|
},
|
|
108
156
|
{
|
|
109
157
|
name: "scanline",
|
|
110
|
-
primitive: "ambient-
|
|
158
|
+
primitive: "ambient-tint",
|
|
111
159
|
keyframes: "kui-scanline",
|
|
112
160
|
params: { duration: "3.5s", ease: "linear" }
|
|
113
161
|
},
|
|
114
162
|
{
|
|
115
163
|
name: "dot-grid-drift",
|
|
116
|
-
primitive: "ambient-
|
|
164
|
+
primitive: "ambient-tint",
|
|
117
165
|
keyframes: "kui-dot-grid-drift",
|
|
118
166
|
params: { duration: "16s", ease: "linear" }
|
|
119
167
|
},
|
|
120
168
|
{
|
|
121
169
|
name: "line-grid-drift",
|
|
122
|
-
primitive: "ambient-
|
|
170
|
+
primitive: "ambient-tint",
|
|
123
171
|
keyframes: "kui-line-grid-drift",
|
|
124
172
|
params: { duration: "16s", ease: "linear" }
|
|
125
173
|
},
|
|
126
174
|
{
|
|
127
175
|
name: "starfield",
|
|
128
|
-
primitive: "ambient-
|
|
176
|
+
primitive: "ambient-tint",
|
|
129
177
|
keyframes: "kui-starfield",
|
|
130
178
|
params: { duration: "40s", ease: "linear" }
|
|
131
179
|
},
|
|
132
180
|
{
|
|
133
181
|
name: "spotlight-follow",
|
|
134
|
-
primitive: "ambient-
|
|
182
|
+
primitive: "ambient-tint",
|
|
135
183
|
keyframes: "kui-spotlight-follow",
|
|
136
184
|
params: { duration: "9s" }
|
|
137
185
|
},
|
|
138
186
|
{
|
|
139
187
|
name: "wave-blob",
|
|
140
|
-
primitive: "ambient-
|
|
188
|
+
primitive: "ambient-tint",
|
|
141
189
|
keyframes: "kui-wave-blob",
|
|
142
190
|
params: { duration: "12s" }
|
|
143
191
|
},
|
|
@@ -474,6 +522,10 @@ var liftParams = {
|
|
|
474
522
|
var popParams = {
|
|
475
523
|
scale: { type: "number", default: "1.06", cssProperty: "--kui-pop-scale", finite: true, minimum: 0 }
|
|
476
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
|
+
};
|
|
477
529
|
function hoverPrimitive(id, channels, extraParams = {}) {
|
|
478
530
|
return {
|
|
479
531
|
id,
|
|
@@ -501,7 +553,7 @@ var HOVER_PRIMITIVES = [
|
|
|
501
553
|
hoverPrimitive("split-flap", ["rotate"]),
|
|
502
554
|
hoverPrimitive("border-draw", ["border"]),
|
|
503
555
|
hoverPrimitive("border-glow", ["shadow"]),
|
|
504
|
-
hoverPrimitive("beam-border", ["border"]),
|
|
556
|
+
hoverPrimitive("beam-border", ["border"], beamParams),
|
|
505
557
|
hoverPrimitive("underline-slide", ["scale"]),
|
|
506
558
|
hoverPrimitive("underline-center", ["scale"]),
|
|
507
559
|
hoverPrimitive("icon-wiggle", ["rotate"]),
|
|
@@ -517,7 +569,7 @@ var CONTINUOUS_BORDER_PRIMITIVES = [
|
|
|
517
569
|
id: "beam-border-auto",
|
|
518
570
|
renderer: "javascript",
|
|
519
571
|
channels: ["border"],
|
|
520
|
-
parameters: hoverTiming,
|
|
572
|
+
parameters: { ...hoverTiming, ...beamParams },
|
|
521
573
|
supportedTimelines: ["time"],
|
|
522
574
|
supportedActivations: ["load"],
|
|
523
575
|
defaultActivation: "load",
|
|
@@ -806,6 +858,177 @@ function registerInteraction(registry) {
|
|
|
806
858
|
return registry.registerPrimitives(INTERACTION_PRIMITIVES).registerPresets(INTERACTION_PRESETS);
|
|
807
859
|
}
|
|
808
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
|
+
|
|
809
1032
|
// src/effects/catalog/media-shared.ts
|
|
810
1033
|
var AXIS_DEGREES = { vertical: 0, horizontal: 90 };
|
|
811
1034
|
var UNIT_DEGREES = { deg: 1, grad: 0.9, rad: 180 / Math.PI, turn: 360 };
|
|
@@ -1099,7 +1322,7 @@ var slatParams = {
|
|
|
1099
1322
|
},
|
|
1100
1323
|
fold: { type: "keyword", default: "false", cssProperty: "--kui-fold", values: ["true", "false"] },
|
|
1101
1324
|
duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" },
|
|
1102
|
-
|
|
1325
|
+
...TRIGGER_DELAY_PARAM,
|
|
1103
1326
|
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
|
|
1104
1327
|
stagger: { type: "time", default: "60ms", cssProperty: "--kui-stagger" }
|
|
1105
1328
|
};
|
|
@@ -1145,6 +1368,133 @@ function prepareSlatAssemble(el, params, ctx) {
|
|
|
1145
1368
|
}
|
|
1146
1369
|
};
|
|
1147
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
|
+
}
|
|
1148
1498
|
var MEDIA_JS_PRIMITIVES = [
|
|
1149
1499
|
{
|
|
1150
1500
|
id: "slat-assemble",
|
|
@@ -1165,10 +1515,68 @@ var MEDIA_JS_PRIMITIVES = [
|
|
|
1165
1515
|
// in `core/types.ts` for why the catalog's default is the opposite of this.
|
|
1166
1516
|
restoresOnFinish: true,
|
|
1167
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)
|
|
1168
1562
|
}
|
|
1169
1563
|
];
|
|
1170
1564
|
var MEDIA_JS_PRESETS = [
|
|
1171
|
-
{ name: "slat-assemble", primitive: "slat-assemble", cloak: true }
|
|
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" }
|
|
1172
1580
|
];
|
|
1173
1581
|
var MEDIA_PRIMITIVES = [...MEDIA_CSS_PRIMITIVES, ...MEDIA_JS_PRIMITIVES];
|
|
1174
1582
|
var MEDIA_PRESETS = [...MEDIA_CSS_PRESETS, ...MEDIA_JS_PRESETS];
|
|
@@ -1311,6 +1719,7 @@ var COUNT_STEP_MS = 16;
|
|
|
1311
1719
|
var REDUCED_MOTION_DURATION_MS = 1;
|
|
1312
1720
|
var countParams = {
|
|
1313
1721
|
duration: { type: "time", default: "1600ms", cssProperty: "--kui-duration" },
|
|
1722
|
+
...TRIGGER_DELAY_PARAM,
|
|
1314
1723
|
from: { type: "number", default: "0", cssProperty: "--kui-from" },
|
|
1315
1724
|
to: { type: "number", default: "100", cssProperty: "--kui-to" },
|
|
1316
1725
|
decimals: {
|
|
@@ -1331,6 +1740,7 @@ var countParams = {
|
|
|
1331
1740
|
};
|
|
1332
1741
|
var odometerParams = {
|
|
1333
1742
|
duration: { type: "time", default: "1600ms", cssProperty: "--kui-duration" },
|
|
1743
|
+
...TRIGGER_DELAY_PARAM,
|
|
1334
1744
|
from: { type: "number", default: "0", cssProperty: "--kui-from" },
|
|
1335
1745
|
to: { type: "number", default: "100", cssProperty: "--kui-to" }
|
|
1336
1746
|
};
|
|
@@ -1357,7 +1767,9 @@ function tweenDurationMs(params, ctx) {
|
|
|
1357
1767
|
function tweenTimingFor(params, ctx) {
|
|
1358
1768
|
return {
|
|
1359
1769
|
durationMs: tweenDurationMs(params, ctx),
|
|
1360
|
-
|
|
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)),
|
|
1361
1773
|
easing: resolveEasing(params.timing.easing, ctx.warn)
|
|
1362
1774
|
};
|
|
1363
1775
|
}
|
|
@@ -1617,15 +2029,22 @@ function createStepRunner(win, options) {
|
|
|
1617
2029
|
function appendCharSpans(container, doc, text) {
|
|
1618
2030
|
const spans = [];
|
|
1619
2031
|
let index = 0;
|
|
2032
|
+
let wordWrapper = null;
|
|
1620
2033
|
for (const grapheme of segmentGraphemes(text)) {
|
|
1621
2034
|
if (grapheme.trim() === "") {
|
|
1622
2035
|
container.append(doc.createTextNode(grapheme));
|
|
2036
|
+
wordWrapper = null;
|
|
1623
2037
|
continue;
|
|
1624
2038
|
}
|
|
2039
|
+
if (!wordWrapper) {
|
|
2040
|
+
wordWrapper = doc.createElement("span");
|
|
2041
|
+
wordWrapper.className = "kui-split-word";
|
|
2042
|
+
container.append(wordWrapper);
|
|
2043
|
+
}
|
|
1625
2044
|
const span = doc.createElement("span");
|
|
1626
2045
|
markItem(span, index);
|
|
1627
2046
|
span.textContent = grapheme;
|
|
1628
|
-
|
|
2047
|
+
wordWrapper.append(span);
|
|
1629
2048
|
spans.push(span);
|
|
1630
2049
|
index++;
|
|
1631
2050
|
}
|
|
@@ -1672,7 +2091,13 @@ function appendLineSpans(container, doc, text) {
|
|
|
1672
2091
|
return buckets.map((nodes, index) => {
|
|
1673
2092
|
const line = doc.createElement("span");
|
|
1674
2093
|
markItem(line, index, "kui-split-line");
|
|
1675
|
-
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
|
+
}
|
|
1676
2101
|
container.append(line);
|
|
1677
2102
|
return line;
|
|
1678
2103
|
});
|
|
@@ -1806,7 +2231,7 @@ function jsTextPrimitive(id, channels, options) {
|
|
|
1806
2231
|
}
|
|
1807
2232
|
var splitTiming = {
|
|
1808
2233
|
duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" },
|
|
1809
|
-
|
|
2234
|
+
...TRIGGER_DELAY_PARAM,
|
|
1810
2235
|
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
|
|
1811
2236
|
stagger: { type: "time", default: "30ms", cssProperty: "--kui-stagger" },
|
|
1812
2237
|
unit: {
|
|
@@ -1824,6 +2249,11 @@ var splitTiming = {
|
|
|
1824
2249
|
};
|
|
1825
2250
|
var motionParams = {
|
|
1826
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,
|
|
1827
2257
|
motion: {
|
|
1828
2258
|
type: "keyword",
|
|
1829
2259
|
default: "wave",
|
|
@@ -1833,10 +2263,15 @@ var motionParams = {
|
|
|
1833
2263
|
};
|
|
1834
2264
|
var typewriterParams = {
|
|
1835
2265
|
step: { type: "time", default: "55ms", cssProperty: "--kui-step" },
|
|
1836
|
-
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
|
|
1837
2268
|
};
|
|
1838
2269
|
var scrambleParams = {
|
|
1839
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,
|
|
1840
2275
|
revealEvery: {
|
|
1841
2276
|
type: "number",
|
|
1842
2277
|
default: "2",
|
|
@@ -1853,7 +2288,11 @@ var scrambleParams = {
|
|
|
1853
2288
|
};
|
|
1854
2289
|
var wordCyclerParams = {
|
|
1855
2290
|
words: { type: "text", default: "", cssProperty: "--kui-words" },
|
|
1856
|
-
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
|
|
1857
2296
|
};
|
|
1858
2297
|
function prepareSplitText(el, params, ctx) {
|
|
1859
2298
|
const doc = el.ownerDocument;
|
|
@@ -1899,7 +2338,7 @@ function prepareTypewriter(el, params, ctx) {
|
|
|
1899
2338
|
layers.decorative.textContent = graphemes.slice(0, count).join("");
|
|
1900
2339
|
};
|
|
1901
2340
|
const run = createStepRunner(ctx.win, {
|
|
1902
|
-
delayMs: params.timing.delayMs ?? 0,
|
|
2341
|
+
delayMs: params.timing.delayMs ?? params.ms("delay", 0),
|
|
1903
2342
|
stepMs: stepMsFor(params, graphemes.length, 55),
|
|
1904
2343
|
tick: () => {
|
|
1905
2344
|
const step = nextTypeState(state, graphemes.length, loop2);
|
|
@@ -1923,6 +2362,16 @@ function prepareTypewriter(el, params, ctx) {
|
|
|
1923
2362
|
function prepareScramble(el, params, ctx) {
|
|
1924
2363
|
const charset = SCRAMBLE_CHARSETS[params.text("charset", "upper")];
|
|
1925
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
|
+
};
|
|
1926
2375
|
const layers = installSplitLayers(el, el.ownerDocument);
|
|
1927
2376
|
layers.decorative.classList.add("kui-scramble");
|
|
1928
2377
|
const graphemes = segmentGraphemes(layers.originalText);
|
|
@@ -1934,25 +2383,29 @@ function prepareScramble(el, params, ctx) {
|
|
|
1934
2383
|
render();
|
|
1935
2384
|
const totalTicks = Math.max(1, graphemes.length * revealEvery);
|
|
1936
2385
|
const run = createStepRunner(ctx.win, {
|
|
1937
|
-
delayMs: params.timing.delayMs ?? 0,
|
|
2386
|
+
delayMs: params.timing.delayMs ?? params.ms("delay", 0),
|
|
1938
2387
|
stepMs: stepMsFor(params, totalTicks, Math.max(1, 700 / totalTicks)),
|
|
1939
2388
|
tick: () => {
|
|
1940
2389
|
ticks++;
|
|
1941
2390
|
if (ticks % revealEvery === 0) resolved++;
|
|
1942
2391
|
render();
|
|
1943
|
-
|
|
2392
|
+
const done = resolved >= graphemes.length;
|
|
2393
|
+
if (done) releaseSizeLock();
|
|
2394
|
+
return done;
|
|
1944
2395
|
}
|
|
1945
2396
|
});
|
|
1946
2397
|
return {
|
|
1947
2398
|
cleanup: () => {
|
|
1948
2399
|
run.stop();
|
|
1949
2400
|
layers.restore();
|
|
2401
|
+
releaseSizeLock();
|
|
1950
2402
|
},
|
|
1951
2403
|
finished: run.finished,
|
|
1952
2404
|
finish: () => {
|
|
1953
2405
|
run.stop();
|
|
1954
2406
|
resolved = graphemes.length;
|
|
1955
2407
|
render();
|
|
2408
|
+
releaseSizeLock();
|
|
1956
2409
|
}
|
|
1957
2410
|
};
|
|
1958
2411
|
}
|
|
@@ -1965,7 +2418,7 @@ function prepareWordCycler(el, params, ctx) {
|
|
|
1965
2418
|
let index = 0;
|
|
1966
2419
|
el.textContent = words[0];
|
|
1967
2420
|
const run = createStepRunner(ctx.win, {
|
|
1968
|
-
delayMs: params.timing.delayMs ?? 0,
|
|
2421
|
+
delayMs: params.timing.delayMs ?? params.ms("delay", 0),
|
|
1969
2422
|
stepMs: params.ms("interval", 2200),
|
|
1970
2423
|
tick: () => {
|
|
1971
2424
|
el.classList.add("kui-word-cycler-swap");
|
|
@@ -2013,13 +2466,16 @@ var TEXT_JS_PRIMITIVES = [
|
|
|
2013
2466
|
perfClass: "continuous"
|
|
2014
2467
|
})
|
|
2015
2468
|
];
|
|
2469
|
+
var CHARS_STAGGER = "30ms";
|
|
2470
|
+
var WORDS_STAGGER = "90ms";
|
|
2471
|
+
var LINES_STAGGER = "160ms";
|
|
2016
2472
|
var TEXT_JS_PRESETS = [
|
|
2017
|
-
{ name: "split-chars", primitive: "split-text", params: { unit: "chars", direction: "fade" } },
|
|
2018
|
-
{ name: "split-words", primitive: "split-text", params: { unit: "words", direction: "fade" } },
|
|
2019
|
-
{ name: "split-lines", primitive: "split-text", params: { unit: "lines", direction: "fade" } },
|
|
2020
|
-
{ name: "text-reveal-up", primitive: "split-text", params: { unit: "words", direction: "up" }, cloak: true },
|
|
2021
|
-
{ name: "text-reveal-down", primitive: "split-text", params: { unit: "words", direction: "down" }, cloak: true },
|
|
2022
|
-
{ name: "text-reveal-mask", primitive: "split-text", params: { unit: "lines", direction: "mask" }, cloak: true },
|
|
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 },
|
|
2023
2479
|
{ name: "text-wave", primitive: "split-text-motion", params: { motion: "wave" } },
|
|
2024
2480
|
{ name: "text-jitter", primitive: "split-text-motion", params: { motion: "jitter" } },
|
|
2025
2481
|
{ name: "typewriter", primitive: "typewriter", params: { loop: "false" } },
|
|
@@ -2353,7 +2809,7 @@ function stepStateFor(position, index) {
|
|
|
2353
2809
|
// src/effects/forms/primitives.ts
|
|
2354
2810
|
var timing = {
|
|
2355
2811
|
duration: { type: "time", default: "400ms", cssProperty: "--kui-duration" },
|
|
2356
|
-
|
|
2812
|
+
...TRIGGER_DELAY_PARAM,
|
|
2357
2813
|
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
|
|
2358
2814
|
};
|
|
2359
2815
|
var NATIVE_STATE_PRIMITIVE = {
|
|
@@ -3547,12 +4003,13 @@ function scrollPrimitive(spec) {
|
|
|
3547
4003
|
renderer: "javascript",
|
|
3548
4004
|
channels,
|
|
3549
4005
|
parameters,
|
|
3550
|
-
//
|
|
3551
|
-
//
|
|
3552
|
-
//
|
|
3553
|
-
//
|
|
3554
|
-
//
|
|
3555
|
-
|
|
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,
|
|
3556
4013
|
supportedActivations: ["manual", "load", "enter"],
|
|
3557
4014
|
defaultActivation: "load",
|
|
3558
4015
|
perfClass,
|
|
@@ -3829,7 +4286,21 @@ var SCROLL_PRIMITIVES = [
|
|
|
3829
4286
|
}),
|
|
3830
4287
|
scrollPrimitive({
|
|
3831
4288
|
id: "smooth-scroll",
|
|
3832
|
-
|
|
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"],
|
|
3833
4304
|
parameters: {
|
|
3834
4305
|
behavior: { type: "keyword", default: "smooth", cssProperty: "--kui-scroll-behavior", values: ["smooth", "auto"] }
|
|
3835
4306
|
},
|
|
@@ -4266,6 +4737,32 @@ function registerSvg(registry) {
|
|
|
4266
4737
|
}
|
|
4267
4738
|
|
|
4268
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
|
+
}
|
|
4269
4766
|
var CARD_TOGGLE_PRIMITIVE = {
|
|
4270
4767
|
id: "card-toggle",
|
|
4271
4768
|
renderer: "javascript",
|
|
@@ -4273,14 +4770,20 @@ var CARD_TOGGLE_PRIMITIVE = {
|
|
|
4273
4770
|
parameters: {
|
|
4274
4771
|
duration: { type: "time", default: "700ms", cssProperty: "--kui-duration" },
|
|
4275
4772
|
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
|
|
4276
|
-
perspective: { type: "length", default: "1600px", cssProperty: "--kui-perspective" }
|
|
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
|
+
}
|
|
4277
4780
|
},
|
|
4278
4781
|
supportedTimelines: ["time"],
|
|
4279
4782
|
supportedActivations: ["load"],
|
|
4280
4783
|
defaultActivation: "load",
|
|
4281
4784
|
perfClass: "compositor",
|
|
4282
4785
|
reducedMotion: "disable",
|
|
4283
|
-
prepare: ()
|
|
4786
|
+
prepare: deferPrepare(prepareCardToggle)
|
|
4284
4787
|
};
|
|
4285
4788
|
var THREE_D_PRIMITIVES = [
|
|
4286
4789
|
// `skew`, not `rotate`: the keyframes in three-d.css write `transform: perspective(...)
|