kuinetic 0.1.2 → 0.1.3
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-5DON3UFQ.mjs} +1195 -214
- package/dist/esm/{chunk-LWS4OSLX.mjs → chunk-JT4PZL3A.mjs} +50 -7
- package/dist/esm/{chunk-5EVG7CIY.mjs → chunk-QUVFODSQ.mjs} +27 -13
- 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 +1269 -233
- package/dist/kuinetic.css +1392 -52
- package/dist/kuinetic.js +1268 -232
- 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 +93 -1
- 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 +0 -1
- package/dist/types/effects/catalog/subtree-capture.d.ts +23 -0
- package/dist/types/effects/catalog/text-shared.d.ts +5 -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/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,7 +12,7 @@ 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
|
|
17
18
|
var COMMON = {
|
|
@@ -51,6 +52,13 @@ var float = {
|
|
|
51
52
|
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
|
|
52
53
|
distance: { type: "length", default: "14px", cssProperty: "--kui-distance" }
|
|
53
54
|
};
|
|
55
|
+
var orbit = {
|
|
56
|
+
duration: { type: "time", default: "3.5s", cssProperty: "--kui-duration" },
|
|
57
|
+
// `linear` by default, unlike every other ambient primitive: a continuous rotation that eases
|
|
58
|
+
// visibly stutters once per revolution, because the ease restarts at each iteration boundary.
|
|
59
|
+
ease: { type: "easing", default: "linear", cssProperty: "--kui-ease" },
|
|
60
|
+
angle: { type: "angle", default: "360deg", cssProperty: "--kui-to-angle" }
|
|
61
|
+
};
|
|
54
62
|
var pulse = {
|
|
55
63
|
duration: { type: "time", default: "2.2s", cssProperty: "--kui-duration" },
|
|
56
64
|
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
|
|
@@ -69,6 +77,12 @@ var AMBIENT_PRIMITIVES = [
|
|
|
69
77
|
reducedMotion: "disable",
|
|
70
78
|
perfClass: "continuous"
|
|
71
79
|
}),
|
|
80
|
+
cssPrimitive("ambient-orbit", [CHANNEL.rotate], {
|
|
81
|
+
parameters: orbit,
|
|
82
|
+
defaultActivation: "load",
|
|
83
|
+
reducedMotion: "disable",
|
|
84
|
+
perfClass: "continuous"
|
|
85
|
+
}),
|
|
72
86
|
cssPrimitive("ambient-pulse", [CHANNEL.scale, CHANNEL.opacity], {
|
|
73
87
|
parameters: pulse,
|
|
74
88
|
defaultActivation: "load",
|
|
@@ -140,6 +154,7 @@ var AMBIENT_PRESETS = [
|
|
|
140
154
|
keyframes: "kui-floating-shapes",
|
|
141
155
|
params: { duration: "6s", distance: "10px" }
|
|
142
156
|
},
|
|
157
|
+
{ name: "orbit", primitive: "ambient-orbit", keyframes: "kui-orbit" },
|
|
143
158
|
{ name: "glow-pulse", primitive: "ambient-pulse", keyframes: "kui-glow-pulse" }
|
|
144
159
|
];
|
|
145
160
|
function registerAmbient(registry) {
|
|
@@ -456,6 +471,9 @@ var hoverTiming = {
|
|
|
456
471
|
var liftParams = {
|
|
457
472
|
distance: { type: "length", default: "6px", cssProperty: "--kui-lift-distance" }
|
|
458
473
|
};
|
|
474
|
+
var popParams = {
|
|
475
|
+
scale: { type: "number", default: "1.06", cssProperty: "--kui-pop-scale", finite: true, minimum: 0 }
|
|
476
|
+
};
|
|
459
477
|
function hoverPrimitive(id, channels, extraParams = {}) {
|
|
460
478
|
return {
|
|
461
479
|
id,
|
|
@@ -477,6 +495,7 @@ function hoverPrimitive(id, channels, extraParams = {}) {
|
|
|
477
495
|
}
|
|
478
496
|
var HOVER_PRIMITIVES = [
|
|
479
497
|
hoverPrimitive("lift", ["translate"], liftParams),
|
|
498
|
+
hoverPrimitive("pop", ["scale"], popParams),
|
|
480
499
|
hoverPrimitive("lift-shadow", ["translate", "shadow"], liftParams),
|
|
481
500
|
hoverPrimitive("shine-sweep", ["sweep"]),
|
|
482
501
|
hoverPrimitive("split-flap", ["rotate"]),
|
|
@@ -787,12 +806,173 @@ function registerInteraction(registry) {
|
|
|
787
806
|
return registry.registerPrimitives(INTERACTION_PRIMITIVES).registerPresets(INTERACTION_PRESETS);
|
|
788
807
|
}
|
|
789
808
|
|
|
809
|
+
// src/effects/catalog/media-shared.ts
|
|
810
|
+
var AXIS_DEGREES = { vertical: 0, horizontal: 90 };
|
|
811
|
+
var UNIT_DEGREES = { deg: 1, grad: 0.9, rad: 180 / Math.PI, turn: 360 };
|
|
812
|
+
var BAND_OVERLAP_PX = 1;
|
|
813
|
+
var STAGE_CLASS = "kui-slat-stage";
|
|
814
|
+
var SLAT_CLASS = "kui-slat-item";
|
|
815
|
+
var GOLDEN_RATIO_CONJUGATE = 0.6180339887498949;
|
|
816
|
+
function slatOrder(index, count, from) {
|
|
817
|
+
if (count <= 1) return 0;
|
|
818
|
+
switch (from) {
|
|
819
|
+
case "start":
|
|
820
|
+
return index;
|
|
821
|
+
case "end":
|
|
822
|
+
return count - 1 - index;
|
|
823
|
+
case "edges":
|
|
824
|
+
return Math.min(index, count - 1 - index);
|
|
825
|
+
case "random-ish":
|
|
826
|
+
return Math.floor(index * GOLDEN_RATIO_CONJUGATE % 1 * count);
|
|
827
|
+
case "alternate":
|
|
828
|
+
default:
|
|
829
|
+
return zigzagRank(index, count);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function zigzagRank(index, count) {
|
|
833
|
+
const fromStart = index;
|
|
834
|
+
const fromEnd = count - 1 - index;
|
|
835
|
+
const pair = Math.min(fromStart, fromEnd);
|
|
836
|
+
return fromStart <= fromEnd ? pair * 2 : pair * 2 + 1;
|
|
837
|
+
}
|
|
838
|
+
function slatAngleDegrees(authored, axis) {
|
|
839
|
+
const trimmed = authored.trim();
|
|
840
|
+
if (!trimmed) return AXIS_DEGREES[axis];
|
|
841
|
+
const match = /^(-?(?:\d+(?:\.\d+)?|\.\d+))(deg|rad|grad|turn)?$/.exec(trimmed);
|
|
842
|
+
if (!match) return AXIS_DEGREES[axis];
|
|
843
|
+
const value = Number(match[1]);
|
|
844
|
+
if (!Number.isFinite(value)) return AXIS_DEGREES[axis];
|
|
845
|
+
const unit = match[2] ?? "deg";
|
|
846
|
+
const degrees = value * UNIT_DEGREES[unit];
|
|
847
|
+
return (degrees % 180 + 180) % 180;
|
|
848
|
+
}
|
|
849
|
+
function slatTravelVector(angleDegrees) {
|
|
850
|
+
const theta = angleDegrees * Math.PI / 180;
|
|
851
|
+
return { x: -Math.sin(theta), y: Math.cos(theta) };
|
|
852
|
+
}
|
|
853
|
+
function slatBandClip(index, count, angleDegrees, box) {
|
|
854
|
+
const { width, height } = box;
|
|
855
|
+
const theta = angleDegrees * Math.PI / 180;
|
|
856
|
+
const nx = Math.cos(theta);
|
|
857
|
+
const ny = Math.sin(theta);
|
|
858
|
+
const { x: dx, y: dy } = slatTravelVector(angleDegrees);
|
|
859
|
+
const span = Math.abs(nx) * width + Math.abs(ny) * height;
|
|
860
|
+
const centreX = width / 2;
|
|
861
|
+
const centreY = height / 2;
|
|
862
|
+
const step = span / count;
|
|
863
|
+
const near = index * step - span / 2 - BAND_OVERLAP_PX;
|
|
864
|
+
const far = (index + 1) * step - span / 2 + BAND_OVERLAP_PX;
|
|
865
|
+
const reach = width + height;
|
|
866
|
+
const corner = (along, across) => `${(centreX + along * nx + across * dx).toFixed(2)}px ${(centreY + along * ny + across * dy).toFixed(2)}px`;
|
|
867
|
+
return `polygon(${corner(near, reach)}, ${corner(near, -reach)}, ${corner(far, -reach)}, ${corner(far, reach)})`;
|
|
868
|
+
}
|
|
869
|
+
function axisLabel(angleDegrees) {
|
|
870
|
+
if (angleDegrees === 0) return "vertical";
|
|
871
|
+
if (angleDegrees === 90) return "horizontal";
|
|
872
|
+
return "diagonal";
|
|
873
|
+
}
|
|
874
|
+
function syncStageToImage(stage, img, bands) {
|
|
875
|
+
const { slats, angleDegrees } = bands;
|
|
876
|
+
const width = img.offsetWidth;
|
|
877
|
+
const height = img.offsetHeight;
|
|
878
|
+
stage.style.top = `${img.offsetTop}px`;
|
|
879
|
+
stage.style.left = `${img.offsetLeft}px`;
|
|
880
|
+
stage.style.width = `${width}px`;
|
|
881
|
+
stage.style.height = `${height}px`;
|
|
882
|
+
slats.forEach((slat, index) => {
|
|
883
|
+
slat.style.clipPath = slatBandClip(index, slats.length, angleDegrees, { width, height });
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
function watchImageBox(stage, img, win, bands) {
|
|
887
|
+
const handler = () => syncStageToImage(stage, img, bands);
|
|
888
|
+
win.addEventListener("resize", handler, { passive: true });
|
|
889
|
+
const stopWindow = () => win.removeEventListener("resize", handler);
|
|
890
|
+
const ResizeObserverCtor = win.ResizeObserver;
|
|
891
|
+
if (!ResizeObserverCtor) return stopWindow;
|
|
892
|
+
const observer = new ResizeObserverCtor(handler);
|
|
893
|
+
observer.observe(img);
|
|
894
|
+
return () => {
|
|
895
|
+
stopWindow();
|
|
896
|
+
observer.disconnect();
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
function imagePaintStyle(img, win) {
|
|
900
|
+
const computed = win.getComputedStyle(img);
|
|
901
|
+
const position = computed.objectPosition || "50% 50%";
|
|
902
|
+
const fit = computed.objectFit;
|
|
903
|
+
if (fit === "cover") return { size: "cover", position };
|
|
904
|
+
if (fit === "contain" || fit === "scale-down") return { size: "contain", position };
|
|
905
|
+
if (fit === "none") return { size: "auto", position };
|
|
906
|
+
return { size: "100% 100%", position };
|
|
907
|
+
}
|
|
908
|
+
function installSlatStage(el, doc, win, options) {
|
|
909
|
+
const img = el.querySelector("img");
|
|
910
|
+
if (!img) return null;
|
|
911
|
+
const url = img.currentSrc || img.getAttribute("src") || "";
|
|
912
|
+
if (!url) return null;
|
|
913
|
+
const { count, angleDegrees, from, fold } = options;
|
|
914
|
+
const travel = slatTravelVector(angleDegrees);
|
|
915
|
+
const stage = doc.createElement("div");
|
|
916
|
+
stage.className = STAGE_CLASS;
|
|
917
|
+
stage.setAttribute("aria-hidden", "true");
|
|
918
|
+
stage.dataset.kuiSlatAxis = axisLabel(angleDegrees);
|
|
919
|
+
stage.dataset.kuiSlatAngle = String(angleDegrees);
|
|
920
|
+
stage.dataset.kuiSlatFold = String(fold);
|
|
921
|
+
stage.style.setProperty("--kui-slat-count", String(count));
|
|
922
|
+
stage.style.setProperty("--kui-slat-dx", travel.x.toFixed(4));
|
|
923
|
+
stage.style.setProperty("--kui-slat-dy", travel.y.toFixed(4));
|
|
924
|
+
const paint = imagePaintStyle(img, win);
|
|
925
|
+
const slats = [];
|
|
926
|
+
for (let index = 0; index < count; index++) {
|
|
927
|
+
const slat = doc.createElement("div");
|
|
928
|
+
slat.className = SLAT_CLASS;
|
|
929
|
+
slat.style.setProperty("--kui-slat-index", String(index));
|
|
930
|
+
slat.style.setProperty("--kui-i", String(slatOrder(index, count, from)));
|
|
931
|
+
slat.style.backgroundImage = `url("${url}")`;
|
|
932
|
+
slat.style.backgroundSize = paint.size;
|
|
933
|
+
slat.style.backgroundPosition = paint.position;
|
|
934
|
+
stage.append(slat);
|
|
935
|
+
slats.push(slat);
|
|
936
|
+
}
|
|
937
|
+
el.append(stage);
|
|
938
|
+
const bands = { slats, angleDegrees };
|
|
939
|
+
syncStageToImage(stage, img, bands);
|
|
940
|
+
const stopWatching = watchImageBox(stage, img, win, bands);
|
|
941
|
+
const imageStyles = createStyleLedger(img);
|
|
942
|
+
imageStyles.set("visibility", "hidden");
|
|
943
|
+
return {
|
|
944
|
+
stage,
|
|
945
|
+
slats,
|
|
946
|
+
restore: () => {
|
|
947
|
+
stopWatching();
|
|
948
|
+
imageStyles.restore();
|
|
949
|
+
stage.remove();
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function applySlatTimingVars(stage, params) {
|
|
954
|
+
const { durationMs, delayMs, easing } = params.timing;
|
|
955
|
+
const duration = durationMs === void 0 ? params.text("duration", "500ms") : `${durationMs}ms`;
|
|
956
|
+
const delay = delayMs === void 0 ? params.text("delay", "0ms") : `${delayMs}ms`;
|
|
957
|
+
stage.style.setProperty("--kui-duration", duration);
|
|
958
|
+
stage.style.setProperty("--kui-delay", delay);
|
|
959
|
+
stage.style.setProperty("--kui-ease", easing ?? params.text("ease", "ease-out"));
|
|
960
|
+
stage.style.setProperty("--kui-stagger", params.text("stagger", "60ms"));
|
|
961
|
+
}
|
|
962
|
+
function slatAssembleFinishMs(params, count) {
|
|
963
|
+
if (count === 0) return 0;
|
|
964
|
+
const durationMs = params.timing.durationMs ?? params.ms("duration", 500);
|
|
965
|
+
const delayMs = params.timing.delayMs ?? params.ms("delay", 0);
|
|
966
|
+
const staggerMs = params.ms("stagger", 60);
|
|
967
|
+
return delayMs + (count - 1) * staggerMs + durationMs;
|
|
968
|
+
}
|
|
969
|
+
|
|
790
970
|
// src/effects/catalog/media.ts
|
|
791
971
|
var geometry = {
|
|
792
972
|
distance: { type: "length", default: "24px", cssProperty: "--kui-distance" },
|
|
793
973
|
scale: { type: "number", default: "1.12", cssProperty: "--kui-to-scale" }
|
|
794
974
|
};
|
|
795
|
-
var
|
|
975
|
+
var MEDIA_CSS_PRIMITIVES = [
|
|
796
976
|
cssPrimitive("media-wipe", [CHANNEL.clip]),
|
|
797
977
|
cssPrimitive("media-mask", ["mask"], { perfClass: "paint" }),
|
|
798
978
|
// Not `reducedMotion: 'disable'` — that policy means "no finite duration would make sense,
|
|
@@ -813,33 +993,66 @@ var MEDIA_PRIMITIVES = [
|
|
|
813
993
|
defaultActivation: "hover",
|
|
814
994
|
perfClass: "paint"
|
|
815
995
|
}),
|
|
996
|
+
// `geometry.distance` only, not `...geometry`: `kui-blur-up` (media.css) reads `--kui-distance`
|
|
997
|
+
// and `--kui-blur` but never `--kui-to-scale` — this primitive doesn't even declare
|
|
998
|
+
// `CHANNEL.scale`. Spreading the whole shared object used to expose `scale:` as an
|
|
999
|
+
// apparently-valid, silently-inert parameter, the same shape `flip-3d`'s dead `perspective`
|
|
1000
|
+
// parameter was (`entrance.css`'s comment on `kui-flip-in-x`).
|
|
816
1001
|
cssPrimitive("media-blur-up", [CHANNEL.translate, CHANNEL.filter], {
|
|
817
|
-
parameters: {
|
|
1002
|
+
parameters: {
|
|
1003
|
+
distance: geometry.distance,
|
|
1004
|
+
blur: { type: "length", default: "16px", cssProperty: "--kui-blur" }
|
|
1005
|
+
},
|
|
818
1006
|
perfClass: "paint"
|
|
819
1007
|
}),
|
|
1008
|
+
// No `defaultActivation` — same convention `core.ts`'s `parallax`/`parallax-scale`/
|
|
1009
|
+
// `parallax-rotate`/`scroll-fade`/`desaturate`/`skew`/`progress`/`progress-stroke` already use
|
|
1010
|
+
// for every other `timelines: ['view', 'scroll', ...]` primitive. `resolveActivation`
|
|
1011
|
+
// (`animator.ts`) only consults `defaultActivation` when the author named no activation, and
|
|
1012
|
+
// falls through to `element-config.ts`'s hardcoded `'enter'` when a primitive declares none.
|
|
1013
|
+
// Setting it to `'manual'` here (matching `activations: ['manual']`) looked like the obviously
|
|
1014
|
+
// correct pairing, but it is what actually broke the effect: `effectiveActivation`
|
|
1015
|
+
// (`style-plan.ts`) only converts a stuck `'manual'` into `'enter'` when `config.timeline !==
|
|
1016
|
+
// 'time'` — i.e. only once the author has actually written `timeline:view`/`timeline:scroll`.
|
|
1017
|
+
// Authored bare (no `timeline:`, the sweep's own probe and the likely first thing anyone
|
|
1018
|
+
// tries), `config.timeline` stays the default `'time'`, that conversion never fires, and the
|
|
1019
|
+
// element sits at `data-kui-state="ready"` forever — which is also the state
|
|
1020
|
+
// `entrance.css`'s `[data-kui-state='ready'] { --kui-distance: 0px !important; }` targets, so
|
|
1021
|
+
// every sample read the same permanently-zeroed `--kui-distance`: not a paused animation, a
|
|
1022
|
+
// zeroed one. `timeline:view`/`timeline:scroll` usage is unaffected either way, since a native
|
|
1023
|
+
// timeline resolves to the `'native-timeline'` gate before activation is even consulted.
|
|
1024
|
+
// `geometry.distance` only: `kui-image-parallax-frame` never reads `--kui-to-scale` and this
|
|
1025
|
+
// primitive doesn't declare `CHANNEL.scale` — same dead-parameter shape as `media-blur-up` above.
|
|
820
1026
|
cssPrimitive("media-parallax-frame", [CHANNEL.translate], {
|
|
821
|
-
parameters: geometry,
|
|
1027
|
+
parameters: { distance: geometry.distance },
|
|
822
1028
|
timelines: ["view", "scroll"],
|
|
823
1029
|
activations: ["manual"],
|
|
824
|
-
defaultActivation: "manual",
|
|
825
1030
|
reducedMotion: "disable"
|
|
826
1031
|
}),
|
|
1032
|
+
// Its own `scale` parameter, not `geometry`: `kui-lightbox-open` opens from a fixed 0.92, never
|
|
1033
|
+
// reading `--kui-to-scale` (or any `distance`, since this primitive doesn't animate position at
|
|
1034
|
+
// all) — the whole shared object was dead weight here. Unlike `media-blur-up`/
|
|
1035
|
+
// `media-parallax-frame`, this primitive *does* declare `CHANNEL.scale`, so the fix is to wire
|
|
1036
|
+
// the keyframe up to a real parameter rather than remove the promise: `--kui-from-scale`, the
|
|
1037
|
+
// same name and "starts at this scale, animates to 1" meaning `scale`/`scale-move`
|
|
1038
|
+
// (`catalog/core.ts`) already use, so `lightbox-open scale:0.8` now does what it looks like it
|
|
1039
|
+
// should. See `kui-lightbox-open` in `media.css`.
|
|
827
1040
|
cssPrimitive("media-lightbox", [CHANNEL.opacity, CHANNEL.scale], {
|
|
828
|
-
parameters:
|
|
1041
|
+
parameters: { scale: { type: "number", default: "0.92", cssProperty: "--kui-from-scale" } }
|
|
829
1042
|
})
|
|
830
1043
|
];
|
|
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" },
|
|
1044
|
+
var MEDIA_CSS_PRESETS = [
|
|
1045
|
+
{ name: "wipe-up", primitive: "media-wipe", keyframes: "kui-wipe-up", cloak: true },
|
|
1046
|
+
{ name: "wipe-down", primitive: "media-wipe", keyframes: "kui-wipe-down", cloak: true },
|
|
1047
|
+
{ name: "wipe-left", primitive: "media-wipe", keyframes: "kui-wipe-left", cloak: true },
|
|
1048
|
+
{ name: "wipe-right", primitive: "media-wipe", keyframes: "kui-wipe-right", cloak: true },
|
|
1049
|
+
{ name: "wipe-circle", primitive: "media-wipe", keyframes: "kui-wipe-circle", cloak: true },
|
|
1050
|
+
{ name: "wipe-diagonal", primitive: "media-wipe", keyframes: "kui-wipe-diagonal", cloak: true },
|
|
1051
|
+
{ name: "mask-reveal", primitive: "media-mask", keyframes: "kui-mask-reveal", cloak: true },
|
|
1052
|
+
{ name: "curtain-reveal", primitive: "media-wipe", keyframes: "kui-curtain-reveal", cloak: true },
|
|
840
1053
|
{ name: "ken-burns", primitive: "media-ken-burns", keyframes: "kui-ken-burns" },
|
|
841
1054
|
{ 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" },
|
|
1055
|
+
{ name: "blur-up", primitive: "media-blur-up", keyframes: "kui-blur-up", cloak: true },
|
|
843
1056
|
{ name: "duotone-hover", primitive: "media-filter", keyframes: "kui-duotone-hover" },
|
|
844
1057
|
{ name: "grayscale-hover", primitive: "media-filter", keyframes: "kui-grayscale-hover" },
|
|
845
1058
|
{ name: "saturate-hover", primitive: "media-filter", keyframes: "kui-saturate-hover" },
|
|
@@ -851,14 +1064,131 @@ var MEDIA_PRESETS = [
|
|
|
851
1064
|
{ name: "before-after-wipe", primitive: "media-wipe", keyframes: "kui-before-after-wipe" },
|
|
852
1065
|
{ name: "lightbox-open", primitive: "media-lightbox", keyframes: "kui-lightbox-open" }
|
|
853
1066
|
];
|
|
1067
|
+
var slatParams = {
|
|
1068
|
+
slats: {
|
|
1069
|
+
type: "number",
|
|
1070
|
+
default: "8",
|
|
1071
|
+
cssProperty: "--kui-slats",
|
|
1072
|
+
minimum: 2,
|
|
1073
|
+
maximum: 24,
|
|
1074
|
+
integer: true
|
|
1075
|
+
},
|
|
1076
|
+
axis: {
|
|
1077
|
+
type: "keyword",
|
|
1078
|
+
default: "vertical",
|
|
1079
|
+
cssProperty: "--kui-axis",
|
|
1080
|
+
values: ["vertical", "horizontal"]
|
|
1081
|
+
},
|
|
1082
|
+
/*
|
|
1083
|
+
* The general form of `axis:`, in degrees: `0deg` is `axis:vertical`, `90deg` is
|
|
1084
|
+
* `axis:horizontal`, and anything between cuts the picture into diagonal bands. An authored
|
|
1085
|
+
* angle wins; leaving it off reads `axis:`, so every existing attribute keeps its meaning.
|
|
1086
|
+
*
|
|
1087
|
+
* `text`, not `angle`, for one reason: `readParams` pre-fills every declared parameter with its
|
|
1088
|
+
* schema default, so a typed default is indistinguishable from an authored value and there would
|
|
1089
|
+
* be no way to tell `angle:0deg` from "no angle, use the axis". An empty default is only possible
|
|
1090
|
+
* on a type that is never written to a stylesheet, which is exactly what `text` is for — and this
|
|
1091
|
+
* value never reaches CSS anyway. `slatAngleDegrees` does the parsing and the range clamp.
|
|
1092
|
+
*/
|
|
1093
|
+
angle: { type: "text", default: "", cssProperty: "--kui-slat-angle" },
|
|
1094
|
+
from: {
|
|
1095
|
+
type: "keyword",
|
|
1096
|
+
default: "alternate",
|
|
1097
|
+
cssProperty: "--kui-from",
|
|
1098
|
+
values: ["alternate", "start", "end", "edges", "random-ish"]
|
|
1099
|
+
},
|
|
1100
|
+
fold: { type: "keyword", default: "false", cssProperty: "--kui-fold", values: ["true", "false"] },
|
|
1101
|
+
duration: { type: "time", default: "500ms", cssProperty: "--kui-duration" },
|
|
1102
|
+
delay: { type: "time", default: "0ms", cssProperty: "--kui-delay" },
|
|
1103
|
+
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" },
|
|
1104
|
+
stagger: { type: "time", default: "60ms", cssProperty: "--kui-stagger" }
|
|
1105
|
+
};
|
|
1106
|
+
function prepareSlatAssemble(el, params, ctx) {
|
|
1107
|
+
const doc = el.ownerDocument;
|
|
1108
|
+
const count = Math.min(24, Math.max(2, Math.round(params.num("slats", 8))));
|
|
1109
|
+
const axis = params.text("axis", "vertical");
|
|
1110
|
+
const angleDegrees = slatAngleDegrees(params.text("angle", ""), axis);
|
|
1111
|
+
const from = params.text("from", "alternate");
|
|
1112
|
+
const fold = params.is("fold");
|
|
1113
|
+
const node = el;
|
|
1114
|
+
if (ctx.win.getComputedStyle(node).position === "static") ctx.style.set("position", "relative");
|
|
1115
|
+
const built = installSlatStage(el, doc, ctx.win, { count, angleDegrees, from, fold });
|
|
1116
|
+
if (!built) return () => {
|
|
1117
|
+
};
|
|
1118
|
+
const { stage } = built;
|
|
1119
|
+
applySlatTimingVars(stage, params);
|
|
1120
|
+
stage.classList.add("kui-slat-animating");
|
|
1121
|
+
let settle2;
|
|
1122
|
+
const finished = new Promise((resolve) => {
|
|
1123
|
+
settle2 = resolve;
|
|
1124
|
+
});
|
|
1125
|
+
let landed = false;
|
|
1126
|
+
const land = () => {
|
|
1127
|
+
if (landed) return;
|
|
1128
|
+
landed = true;
|
|
1129
|
+
stage.classList.remove("kui-slat-animating");
|
|
1130
|
+
built.restore();
|
|
1131
|
+
settle2();
|
|
1132
|
+
};
|
|
1133
|
+
const timer = ctx.win.setTimeout(land, slatAssembleFinishMs(params, count));
|
|
1134
|
+
return {
|
|
1135
|
+
cleanup: () => {
|
|
1136
|
+
ctx.win.clearTimeout(timer);
|
|
1137
|
+
if (landed) return;
|
|
1138
|
+
landed = true;
|
|
1139
|
+
built.restore();
|
|
1140
|
+
},
|
|
1141
|
+
finished,
|
|
1142
|
+
finish: () => {
|
|
1143
|
+
ctx.win.clearTimeout(timer);
|
|
1144
|
+
land();
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
var MEDIA_JS_PRIMITIVES = [
|
|
1149
|
+
{
|
|
1150
|
+
id: "slat-assemble",
|
|
1151
|
+
renderer: "javascript",
|
|
1152
|
+
channels: [CHANNEL.opacity, CHANNEL.translate, CHANNEL.rotate],
|
|
1153
|
+
parameters: slatParams,
|
|
1154
|
+
supportedTimelines: ["time"],
|
|
1155
|
+
supportedActivations: ["load", "enter", "hover", "focus", "click", "manual"],
|
|
1156
|
+
defaultActivation: "enter",
|
|
1157
|
+
perfClass: "dom-transform",
|
|
1158
|
+
// Same reasoning as every JS-rendered primitive in `text.ts`: nothing here declares a CSS
|
|
1159
|
+
// `animation-duration` the reduced-motion policy layer could shorten, and `disable` is what
|
|
1160
|
+
// stops `installSlatStage`'s DOM surgery from ever running at all under reduced motion — the
|
|
1161
|
+
// animator never calls `activate()`, so the wrapped `<img>` is simply left exactly as authored.
|
|
1162
|
+
reducedMotion: "disable",
|
|
1163
|
+
// Land()ing hands the picture back to the real `<img>` and tears down every slat — see
|
|
1164
|
+
// `prepareSlatAssemble`'s `land()`. Declared, not assumed: see `restoresOnFinish`'s own comment
|
|
1165
|
+
// in `core/types.ts` for why the catalog's default is the opposite of this.
|
|
1166
|
+
restoresOnFinish: true,
|
|
1167
|
+
prepare: deferPrepare(prepareSlatAssemble)
|
|
1168
|
+
}
|
|
1169
|
+
];
|
|
1170
|
+
var MEDIA_JS_PRESETS = [
|
|
1171
|
+
{ name: "slat-assemble", primitive: "slat-assemble", cloak: true }
|
|
1172
|
+
];
|
|
1173
|
+
var MEDIA_PRIMITIVES = [...MEDIA_CSS_PRIMITIVES, ...MEDIA_JS_PRIMITIVES];
|
|
1174
|
+
var MEDIA_PRESETS = [...MEDIA_CSS_PRESETS, ...MEDIA_JS_PRESETS];
|
|
854
1175
|
function registerMedia(registry) {
|
|
855
1176
|
return registry.registerPrimitives(MEDIA_PRIMITIVES).registerPresets(MEDIA_PRESETS);
|
|
856
1177
|
}
|
|
857
1178
|
|
|
1179
|
+
// src/effects/catalog/subtree-capture.ts
|
|
1180
|
+
function captureChildren(el) {
|
|
1181
|
+
const authored = Array.from(el.childNodes);
|
|
1182
|
+
return () => {
|
|
1183
|
+
el.replaceChildren(...authored);
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
858
1187
|
// src/effects/catalog/numbers-shared.ts
|
|
859
1188
|
var SR_ONLY_CLASS = "kui-sr-only";
|
|
860
1189
|
var DECORATIVE_CLASS = "kui-count-decorative";
|
|
861
1190
|
function installCountLayers(el, doc) {
|
|
1191
|
+
const restoreChildren = captureChildren(el);
|
|
862
1192
|
const decorative = doc.createElement("span");
|
|
863
1193
|
decorative.setAttribute("aria-hidden", "true");
|
|
864
1194
|
decorative.className = DECORATIVE_CLASS;
|
|
@@ -870,9 +1200,7 @@ function installCountLayers(el, doc) {
|
|
|
870
1200
|
return {
|
|
871
1201
|
decorative,
|
|
872
1202
|
srOnly,
|
|
873
|
-
restore:
|
|
874
|
-
el.textContent = srOnly.textContent;
|
|
875
|
-
}
|
|
1203
|
+
restore: restoreChildren
|
|
876
1204
|
};
|
|
877
1205
|
}
|
|
878
1206
|
function easeOutCubic(t) {
|
|
@@ -1182,7 +1510,12 @@ var COUNT_PRESETS = [
|
|
|
1182
1510
|
];
|
|
1183
1511
|
var METER_PRIMITIVES = [
|
|
1184
1512
|
cssPrimitive("stroke-sweep", [CHANNEL.stroke]),
|
|
1185
|
-
|
|
1513
|
+
// `from` gives `progress-bar` a real knob on its start scale — it had none before. `--kui-bar-from`
|
|
1514
|
+
// is also what the `[data-kui-fx~='progress-bar'][data-kui-state='ready']` gate in numbers.css
|
|
1515
|
+
// neutralizes; see that rule's comment for the on:enter fix this parameter doubles as.
|
|
1516
|
+
cssPrimitive("meter-bar", [CHANNEL.scale], {
|
|
1517
|
+
parameters: { from: { type: "number", default: "0", cssProperty: "--kui-bar-from" } }
|
|
1518
|
+
}),
|
|
1186
1519
|
cssPrimitive("meter-segments", [CHANNEL.opacity]),
|
|
1187
1520
|
cssPrimitive("meter-stars", [CHANNEL.clip])
|
|
1188
1521
|
];
|
|
@@ -1191,7 +1524,13 @@ var METER_PRESETS = [
|
|
|
1191
1524
|
{ name: "gauge-sweep", primitive: "stroke-sweep", keyframes: "kui-gauge-sweep" },
|
|
1192
1525
|
{ name: "donut-sweep", primitive: "stroke-sweep", keyframes: "kui-donut-sweep" },
|
|
1193
1526
|
{ name: "sparkline-draw", primitive: "stroke-sweep", keyframes: "kui-sparkline-draw" },
|
|
1194
|
-
|
|
1527
|
+
// `cloak: true`: `kui-progress-bar`'s `from { scale: 0 1 }` (numbers.css) is a zero-width box
|
|
1528
|
+
// while paused, not just an invisible one — so while it waits it occupies no space in layout at
|
|
1529
|
+
// all. Not, despite the tidier story, because an observer refuses to fire on it: Chromium was
|
|
1530
|
+
// measured resolving a zero-area target's `intersectionRatio` to `1`. See numbers.css's
|
|
1531
|
+
// `[data-kui-fx~='progress-bar'][data-kui-state='ready']` rule for the geometry half of the fix;
|
|
1532
|
+
// `cloak` keeps the pre-JS and post-JS "waiting" look the same (invisible) either side of it.
|
|
1533
|
+
{ name: "progress-bar", primitive: "meter-bar", keyframes: "kui-progress-bar", cloak: true },
|
|
1195
1534
|
{ name: "progress-segments", primitive: "meter-segments", keyframes: "kui-progress-segments" },
|
|
1196
1535
|
{ name: "star-rating-fill", primitive: "meter-stars", keyframes: "kui-star-rating-fill" }
|
|
1197
1536
|
];
|
|
@@ -1206,6 +1545,7 @@ var SR_ONLY_CLASS2 = "kui-sr-only";
|
|
|
1206
1545
|
var DECORATIVE_CLASS2 = "kui-split-decorative";
|
|
1207
1546
|
function installSplitLayers(el, doc) {
|
|
1208
1547
|
const originalText = el.textContent.trim();
|
|
1548
|
+
const restoreChildren = captureChildren(el);
|
|
1209
1549
|
const decorative = doc.createElement("span");
|
|
1210
1550
|
decorative.setAttribute("aria-hidden", "true");
|
|
1211
1551
|
decorative.className = DECORATIVE_CLASS2;
|
|
@@ -1217,9 +1557,7 @@ function installSplitLayers(el, doc) {
|
|
|
1217
1557
|
return {
|
|
1218
1558
|
decorative,
|
|
1219
1559
|
originalText,
|
|
1220
|
-
restore:
|
|
1221
|
-
el.textContent = originalText;
|
|
1222
|
-
}
|
|
1560
|
+
restore: restoreChildren
|
|
1223
1561
|
};
|
|
1224
1562
|
}
|
|
1225
1563
|
function segmentGraphemes(text) {
|
|
@@ -1387,14 +1725,43 @@ var extrudeParams = {
|
|
|
1387
1725
|
distance: { type: "length", default: "32px", cssProperty: "--kui-distance" }
|
|
1388
1726
|
};
|
|
1389
1727
|
var TEXT_CSS_PRIMITIVES = [
|
|
1390
|
-
|
|
1728
|
+
// `color`, alongside `background`: both presets' unconditional rule sets `-webkit-text-fill-color:
|
|
1729
|
+
// transparent` so the `background-image` gradient shows through the glyphs (the standard
|
|
1730
|
+
// gradient-text technique). That is a real claim on the glyph fill, the same physical property
|
|
1731
|
+
// `text-outline-fill` animates on its own `color` channel below — without this, the two looked
|
|
1732
|
+
// disjoint to the compiler (`background` vs `stroke`+`color`) and composing them would let
|
|
1733
|
+
// whichever applied last silently win the glyph fill instead of being flagged as a conflict.
|
|
1734
|
+
cssPrimitive("text-shimmer", [CHANNEL.background, CHANNEL.color], { reducedMotion: "disable" }),
|
|
1735
|
+
// `gradient-sweep` is the only one of the three `text-sweep`-family presets whose keyframe
|
|
1736
|
+
// touches `-webkit-text-fill-color` (see text.css) — `highlight-sweep` and `underline-draw` only
|
|
1737
|
+
// paint a `background-image`. Claiming `color` for all three made the compiler reject compositions
|
|
1738
|
+
// like `underline-draw, text-outline-fill` as a glyph-fill conflict even though they touch disjoint
|
|
1739
|
+
// properties. Split so only the preset that actually claims the fill declares the channel.
|
|
1740
|
+
cssPrimitive("text-gradient-sweep", [CHANNEL.background, CHANNEL.color], { parameters: textSweepParams }),
|
|
1391
1741
|
cssPrimitive("text-sweep", [CHANNEL.background], { parameters: textSweepParams }),
|
|
1392
1742
|
cssPrimitive("text-outline-fill", [CHANNEL.stroke, CHANNEL.color]),
|
|
1393
1743
|
cssPrimitive("var-weight", ["font"], { parameters: fontWeightParams }),
|
|
1394
1744
|
cssPrimitive("var-width", ["font"], { parameters: fontWidthParams }),
|
|
1395
1745
|
cssPrimitive("var-slant", ["font"], { parameters: fontSlantParams }),
|
|
1746
|
+
/*
|
|
1747
|
+
* `defaultActivation: 'load'` for the same reason every ambient primitive declares it, and it
|
|
1748
|
+
* was missing here: a marquee is continuous motion, so `reducedMotion: 'disable'` is only half
|
|
1749
|
+
* the rule `ambient.ts` spells out — the other half is starting on `load` rather than waiting on
|
|
1750
|
+
* a scroll-triggered `enter`.
|
|
1751
|
+
*
|
|
1752
|
+
* Without it, a bare `data-kui="marquee 42s"` resolved to `enter`, which `style-plan.ts`'s
|
|
1753
|
+
* `resolveGate` sends down the `deferred` path and stamps `animation-play-state: paused`. It
|
|
1754
|
+
* compiled correctly, reported `data-kui-state="ready"`, and never ran — measured on a marquee
|
|
1755
|
+
* fully in view, so this was not an observer that had simply not fired yet. Every page carrying
|
|
1756
|
+
* one had to know to write `on:load`, which is exactly the kind of thing an author cannot be
|
|
1757
|
+
* expected to guess.
|
|
1758
|
+
*
|
|
1759
|
+
* `marquee-scroll-linked` is unaffected: its position comes from `animation-timeline: scroll()`,
|
|
1760
|
+
* not from an activation.
|
|
1761
|
+
*/
|
|
1396
1762
|
cssPrimitive("text-marquee", [CHANNEL.translate], {
|
|
1397
1763
|
timelines: ["time", "scroll"],
|
|
1764
|
+
defaultActivation: "load",
|
|
1398
1765
|
reducedMotion: "disable"
|
|
1399
1766
|
}),
|
|
1400
1767
|
cssPrimitive("redaction-reveal", [CHANNEL.clip]),
|
|
@@ -1402,7 +1769,7 @@ var TEXT_CSS_PRIMITIVES = [
|
|
|
1402
1769
|
];
|
|
1403
1770
|
var TEXT_CSS_PRESETS = [
|
|
1404
1771
|
{ name: "gradient-shimmer", primitive: "text-shimmer", keyframes: "kui-gradient-shimmer" },
|
|
1405
|
-
{ name: "gradient-sweep", primitive: "text-sweep", keyframes: "kui-gradient-sweep" },
|
|
1772
|
+
{ name: "gradient-sweep", primitive: "text-gradient-sweep", keyframes: "kui-gradient-sweep" },
|
|
1406
1773
|
{
|
|
1407
1774
|
name: "highlight-sweep",
|
|
1408
1775
|
primitive: "text-sweep",
|
|
@@ -1565,9 +1932,10 @@ function prepareScramble(el, params, ctx) {
|
|
|
1565
1932
|
layers.decorative.textContent = scrambledFrame(graphemes, resolved, charset, Math.random);
|
|
1566
1933
|
};
|
|
1567
1934
|
render();
|
|
1935
|
+
const totalTicks = Math.max(1, graphemes.length * revealEvery);
|
|
1568
1936
|
const run = createStepRunner(ctx.win, {
|
|
1569
1937
|
delayMs: params.timing.delayMs ?? 0,
|
|
1570
|
-
stepMs: stepMsFor(params,
|
|
1938
|
+
stepMs: stepMsFor(params, totalTicks, Math.max(1, 700 / totalTicks)),
|
|
1571
1939
|
tick: () => {
|
|
1572
1940
|
ticks++;
|
|
1573
1941
|
if (ticks % revealEvery === 0) resolved++;
|
|
@@ -1592,7 +1960,7 @@ function prepareWordCycler(el, params, ctx) {
|
|
|
1592
1960
|
const words = params.text("words", "").split("|").map((word) => word.trim()).filter(Boolean);
|
|
1593
1961
|
if (words.length === 0) return () => {
|
|
1594
1962
|
};
|
|
1595
|
-
const
|
|
1963
|
+
const restoreChildren = captureChildren(el);
|
|
1596
1964
|
const swapMs = 150;
|
|
1597
1965
|
let index = 0;
|
|
1598
1966
|
el.textContent = words[0];
|
|
@@ -1613,7 +1981,7 @@ function prepareWordCycler(el, params, ctx) {
|
|
|
1613
1981
|
cleanup: () => {
|
|
1614
1982
|
run.stop();
|
|
1615
1983
|
el.classList.remove("kui-word-cycler-swap");
|
|
1616
|
-
|
|
1984
|
+
restoreChildren();
|
|
1617
1985
|
},
|
|
1618
1986
|
finished: run.finished,
|
|
1619
1987
|
finish: () => run.stop()
|
|
@@ -1649,9 +2017,9 @@ var TEXT_JS_PRESETS = [
|
|
|
1649
2017
|
{ name: "split-chars", primitive: "split-text", params: { unit: "chars", direction: "fade" } },
|
|
1650
2018
|
{ name: "split-words", primitive: "split-text", params: { unit: "words", direction: "fade" } },
|
|
1651
2019
|
{ 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" } },
|
|
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 },
|
|
1655
2023
|
{ name: "text-wave", primitive: "split-text-motion", params: { motion: "wave" } },
|
|
1656
2024
|
{ name: "text-jitter", primitive: "split-text-motion", params: { motion: "jitter" } },
|
|
1657
2025
|
{ name: "typewriter", primitive: "typewriter", params: { loop: "false" } },
|
|
@@ -1683,66 +2051,130 @@ var distance = {
|
|
|
1683
2051
|
distance: { type: "length", default: "24px", cssProperty: "--kui-distance" },
|
|
1684
2052
|
opacity: { type: "number", default: "0", cssProperty: "--kui-from-opacity" }
|
|
1685
2053
|
};
|
|
2054
|
+
var ENTRANCE_TIMELINES = ["time", "view", "scroll", "pin"];
|
|
1686
2055
|
var PRIMITIVES = [
|
|
1687
2056
|
// --- entrance / exit -------------------------------------------------------------------
|
|
1688
|
-
cssPrimitive("reveal", [CHANNEL.opacity, CHANNEL.translate], { parameters: distance }),
|
|
2057
|
+
cssPrimitive("reveal", [CHANNEL.opacity, CHANNEL.translate], { timelines: ENTRANCE_TIMELINES, parameters: distance }),
|
|
1689
2058
|
cssPrimitive("scale", [CHANNEL.scale], {
|
|
2059
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1690
2060
|
parameters: { scale: { type: "number", default: "0.92", cssProperty: "--kui-from-scale" } }
|
|
1691
2061
|
}),
|
|
1692
2062
|
// Separate from `scale` because it claims translate as well and so composes differently.
|
|
2063
|
+
// `distance.distance` only, not `...distance`: `kui-zoom-in-up`/`-down` (entrance.css) read
|
|
2064
|
+
// `--kui-distance` and `--kui-from-scale` but never `--kui-from-opacity` — this primitive
|
|
2065
|
+
// doesn't declare `CHANNEL.opacity`. Spreading the whole shared `distance` object used to expose
|
|
2066
|
+
// `opacity:` as an apparently-valid, silently-inert parameter (same dead-parameter shape as
|
|
2067
|
+
// `flip-3d`'s old `perspective`, fixed above).
|
|
1693
2068
|
cssPrimitive("scale-move", [CHANNEL.scale, CHANNEL.translate], {
|
|
2069
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1694
2070
|
parameters: {
|
|
1695
|
-
|
|
2071
|
+
distance: distance.distance,
|
|
1696
2072
|
scale: { type: "number", default: "0.92", cssProperty: "--kui-from-scale" }
|
|
1697
2073
|
}
|
|
1698
2074
|
}),
|
|
1699
2075
|
cssPrimitive("rotate", [CHANNEL.rotate], {
|
|
2076
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1700
2077
|
parameters: { angle: { type: "angle", default: "-8deg", cssProperty: "--kui-from-angle" } }
|
|
1701
2078
|
}),
|
|
2079
|
+
// `distance.distance` only, not `...distance`: `kui-roll-in`/`-out` (entrance.css) write
|
|
2080
|
+
// `rotate`/`translate`, never `opacity` — this primitive doesn't declare `CHANNEL.opacity`. Same
|
|
2081
|
+
// dead-parameter shape as `scale-move` above.
|
|
1702
2082
|
cssPrimitive("roll", [CHANNEL.rotate, CHANNEL.translate], {
|
|
2083
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1703
2084
|
parameters: {
|
|
1704
|
-
|
|
2085
|
+
distance: distance.distance,
|
|
1705
2086
|
angle: { type: "angle", default: "-120deg", cssProperty: "--kui-from-angle" }
|
|
1706
2087
|
}
|
|
1707
2088
|
}),
|
|
1708
|
-
|
|
2089
|
+
// `skew`, not `rotate`: entrance.css's keyframes write `transform: perspective(...)
|
|
2090
|
+
// rotateX/Y(...)`, not the individual `rotate:` property — the `perspective` parameter below
|
|
2091
|
+
// used to compile cleanly and do nothing, because nothing read it. See entrance.css's own
|
|
2092
|
+
// comment on `kui-flip-in-x` for the fix; `CHANNEL.skew` is this catalog's name for "claims the
|
|
2093
|
+
// whole `transform` shorthand" (`core/types.ts`), shared with `scroll-skew` and `flip-face`.
|
|
2094
|
+
cssPrimitive("flip-3d", [CHANNEL.skew], {
|
|
2095
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1709
2096
|
parameters: {
|
|
1710
2097
|
angle: { type: "angle", default: "90deg", cssProperty: "--kui-from-angle" },
|
|
1711
2098
|
perspective: { type: "length", default: "1200px", cssProperty: "--kui-perspective" }
|
|
1712
2099
|
}
|
|
1713
2100
|
}),
|
|
1714
2101
|
cssPrimitive("blur", [CHANNEL.filter], {
|
|
2102
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1715
2103
|
parameters: { blur: { type: "length", default: "12px", cssProperty: "--kui-blur" } }
|
|
1716
2104
|
}),
|
|
1717
2105
|
// Purpose-built combination: one keyframe, so opacity is written once instead of twice.
|
|
1718
2106
|
cssPrimitive("reveal-blur", [CHANNEL.opacity, CHANNEL.translate, CHANNEL.filter], {
|
|
2107
|
+
timelines: ENTRANCE_TIMELINES,
|
|
1719
2108
|
parameters: { ...distance, blur: { type: "length", default: "12px", cssProperty: "--kui-blur" } }
|
|
1720
2109
|
}),
|
|
1721
2110
|
// --- scroll-linked ---------------------------------------------------------------------
|
|
1722
2111
|
// These are progress-linked, not time-triggered: they reverse as the user scrolls back.
|
|
1723
2112
|
// That is by design and is why `timeline:` is a different axis from `on:`.
|
|
2113
|
+
// `distance.distance` only, not the whole `distance` object: `kui-parallax-y`/`-x` (scroll.css)
|
|
2114
|
+
// write only `translate` — this primitive doesn't declare `CHANNEL.opacity`. Same dead-parameter
|
|
2115
|
+
// shape as `scale-move`/`roll` above; `parallax-y`/`parallax-x`/`depth-layer` never read
|
|
2116
|
+
// `--kui-from-opacity`.
|
|
1724
2117
|
cssPrimitive("parallax", [CHANNEL.translate], {
|
|
1725
|
-
parameters: distance,
|
|
1726
|
-
timelines: ["view", "scroll"],
|
|
2118
|
+
parameters: { distance: distance.distance },
|
|
2119
|
+
timelines: ["view", "scroll", "pin"],
|
|
1727
2120
|
activations: ["manual"],
|
|
1728
2121
|
reducedMotion: "disable",
|
|
1729
2122
|
perfClass: "compositor"
|
|
1730
2123
|
}),
|
|
2124
|
+
// `from` exists because the resting end was hardcoded (scale 1 / rotate 0deg), which fixed
|
|
2125
|
+
// these to "grow slightly" and "tilt slightly" — a scroll-driven element that should sweep in
|
|
2126
|
+
// from a quarter-size or from half a turn away had no way to say so. Same `--kui-from-*`
|
|
2127
|
+
// properties the entrance primitives already use, so the two stay spellable the same way.
|
|
1731
2128
|
cssPrimitive("parallax-scale", [CHANNEL.scale], {
|
|
1732
|
-
parameters: {
|
|
1733
|
-
|
|
2129
|
+
parameters: {
|
|
2130
|
+
scale: { type: "number", default: "1.2", cssProperty: "--kui-to-scale" },
|
|
2131
|
+
from: { type: "number", default: "1", cssProperty: "--kui-from-scale" }
|
|
2132
|
+
},
|
|
2133
|
+
timelines: ["view", "scroll", "pin"],
|
|
1734
2134
|
activations: ["manual"],
|
|
1735
2135
|
reducedMotion: "disable"
|
|
1736
2136
|
}),
|
|
1737
2137
|
cssPrimitive("parallax-rotate", [CHANNEL.rotate], {
|
|
1738
|
-
parameters: {
|
|
1739
|
-
|
|
2138
|
+
parameters: {
|
|
2139
|
+
angle: { type: "angle", default: "12deg", cssProperty: "--kui-to-angle" },
|
|
2140
|
+
from: { type: "angle", default: "0deg", cssProperty: "--kui-from-angle" }
|
|
2141
|
+
},
|
|
2142
|
+
timelines: ["view", "scroll", "pin"],
|
|
1740
2143
|
activations: ["manual"],
|
|
1741
2144
|
reducedMotion: "disable"
|
|
1742
2145
|
}),
|
|
1743
2146
|
cssPrimitive("scroll-fade", [CHANNEL.opacity], {
|
|
1744
2147
|
parameters: { opacity: { type: "number", default: "0", cssProperty: "--kui-from-opacity" } },
|
|
1745
|
-
timelines: ["view", "scroll"],
|
|
2148
|
+
timelines: ["view", "scroll", "pin"],
|
|
2149
|
+
activations: ["manual"],
|
|
2150
|
+
reducedMotion: "disable"
|
|
2151
|
+
}),
|
|
2152
|
+
// `filter`, not `opacity` — it collides with `blur`, and declaring the real channel is what
|
|
2153
|
+
// makes `channels.ts` say so instead of letting the two silently overwrite each other's
|
|
2154
|
+
// `filter` declaration.
|
|
2155
|
+
cssPrimitive("desaturate", [CHANNEL.filter], {
|
|
2156
|
+
parameters: {
|
|
2157
|
+
from: { type: "percentage", default: "100%", cssProperty: "--kui-from-grayscale" },
|
|
2158
|
+
to: { type: "percentage", default: "0%", cssProperty: "--kui-to-grayscale" }
|
|
2159
|
+
},
|
|
2160
|
+
timelines: ["view", "scroll", "pin"],
|
|
2161
|
+
activations: ["manual"],
|
|
2162
|
+
reducedMotion: "disable",
|
|
2163
|
+
perfClass: "paint"
|
|
2164
|
+
}),
|
|
2165
|
+
/*
|
|
2166
|
+
* `transform`, not one of the independent transform properties, because CSS never shipped a
|
|
2167
|
+
* standalone `skew:`. That is also why `skew` is its own channel rather than folded in with
|
|
2168
|
+
* `rotate`: writing `transform` replaces the entire shorthand, so a skew composed with anything
|
|
2169
|
+
* else that wrote `transform` would silently win. Nothing else in the catalog does — every other
|
|
2170
|
+
* transform in the library goes through `translate`/`rotate`/`scale` — so the shorthand is free.
|
|
2171
|
+
*/
|
|
2172
|
+
cssPrimitive("skew", [CHANNEL.skew], {
|
|
2173
|
+
parameters: {
|
|
2174
|
+
from: { type: "angle", default: "8deg", cssProperty: "--kui-from-skew" },
|
|
2175
|
+
to: { type: "angle", default: "0deg", cssProperty: "--kui-to-skew" }
|
|
2176
|
+
},
|
|
2177
|
+
timelines: ["view", "scroll", "pin"],
|
|
1746
2178
|
activations: ["manual"],
|
|
1747
2179
|
reducedMotion: "disable"
|
|
1748
2180
|
}),
|
|
@@ -1762,13 +2194,14 @@ var PRIMITIVES = [
|
|
|
1762
2194
|
})
|
|
1763
2195
|
];
|
|
1764
2196
|
var p = (name, primitive, keyframes, params) => ({ name, primitive, keyframes, ...params ? { params } : {} });
|
|
2197
|
+
var pIn = (name, primitive, keyframes, params) => ({ ...p(name, primitive, keyframes, params), cloak: true });
|
|
1765
2198
|
var FADE = [
|
|
1766
|
-
|
|
2199
|
+
pIn("fade-in", "reveal", "kui-in"),
|
|
1767
2200
|
p("fade-out", "reveal", "kui-out"),
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
2201
|
+
pIn("fade-up", "reveal", "kui-in-up"),
|
|
2202
|
+
pIn("fade-down", "reveal", "kui-in-down"),
|
|
2203
|
+
pIn("fade-left", "reveal", "kui-in-left"),
|
|
2204
|
+
pIn("fade-right", "reveal", "kui-in-right"),
|
|
1772
2205
|
p("fade-out-up", "reveal", "kui-out-up"),
|
|
1773
2206
|
p("fade-out-down", "reveal", "kui-out-down"),
|
|
1774
2207
|
p("fade-out-left", "reveal", "kui-out-left"),
|
|
@@ -1776,56 +2209,56 @@ var FADE = [
|
|
|
1776
2209
|
];
|
|
1777
2210
|
var SLIDE_PARAMS = { distance: "100px", opacity: "1" };
|
|
1778
2211
|
var SLIDE = [
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
2212
|
+
pIn("slide-up", "reveal", "kui-in-up", SLIDE_PARAMS),
|
|
2213
|
+
pIn("slide-down", "reveal", "kui-in-down", SLIDE_PARAMS),
|
|
2214
|
+
pIn("slide-left", "reveal", "kui-in-left", SLIDE_PARAMS),
|
|
2215
|
+
pIn("slide-right", "reveal", "kui-in-right", SLIDE_PARAMS),
|
|
1783
2216
|
p("slide-out-up", "reveal", "kui-out-up", SLIDE_PARAMS),
|
|
1784
2217
|
p("slide-out-down", "reveal", "kui-out-down", SLIDE_PARAMS),
|
|
1785
2218
|
p("slide-out-left", "reveal", "kui-out-left", SLIDE_PARAMS),
|
|
1786
2219
|
p("slide-out-right", "reveal", "kui-out-right", SLIDE_PARAMS)
|
|
1787
2220
|
];
|
|
1788
2221
|
var LOGICAL = [
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
2222
|
+
pIn("slide-inline-start", "reveal", "kui-in-inline-start", SLIDE_PARAMS),
|
|
2223
|
+
pIn("slide-inline-end", "reveal", "kui-in-inline-end", SLIDE_PARAMS),
|
|
2224
|
+
pIn("slide-block-start", "reveal", "kui-in-up", SLIDE_PARAMS),
|
|
2225
|
+
pIn("slide-block-end", "reveal", "kui-in-down", SLIDE_PARAMS)
|
|
1793
2226
|
];
|
|
1794
2227
|
var ZOOM = [
|
|
1795
|
-
|
|
2228
|
+
pIn("zoom-in", "scale", "kui-zoom-in"),
|
|
1796
2229
|
p("zoom-out", "scale", "kui-zoom-out"),
|
|
1797
|
-
|
|
2230
|
+
pIn("pop-in", "scale", "kui-zoom-in", { scale: "0.6", ease: "back-out" }),
|
|
1798
2231
|
p("pop-out", "scale", "kui-zoom-out", { scale: "0.6", ease: "back-in" }),
|
|
1799
|
-
|
|
1800
|
-
|
|
2232
|
+
pIn("zoom-in-up", "scale-move", "kui-zoom-in-up"),
|
|
2233
|
+
pIn("zoom-in-down", "scale-move", "kui-zoom-in-down")
|
|
1801
2234
|
];
|
|
1802
2235
|
var FLIP = [
|
|
1803
|
-
|
|
1804
|
-
|
|
2236
|
+
pIn("flip-in-x", "flip-3d", "kui-flip-in-x"),
|
|
2237
|
+
pIn("flip-in-y", "flip-3d", "kui-flip-in-y"),
|
|
1805
2238
|
p("flip-out-x", "flip-3d", "kui-flip-out-x"),
|
|
1806
2239
|
p("flip-out-y", "flip-3d", "kui-flip-out-y")
|
|
1807
2240
|
];
|
|
1808
2241
|
var ROTATE = [
|
|
1809
|
-
|
|
2242
|
+
pIn("rotate-in", "rotate", "kui-rotate-in"),
|
|
1810
2243
|
p("rotate-out", "rotate", "kui-rotate-out"),
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2244
|
+
pIn("rotate-in-left", "rotate", "kui-rotate-in", { angle: "-45deg" }),
|
|
2245
|
+
pIn("rotate-in-right", "rotate", "kui-rotate-in", { angle: "45deg" }),
|
|
2246
|
+
pIn("roll-in", "roll", "kui-roll-in"),
|
|
1814
2247
|
p("roll-out", "roll", "kui-roll-out"),
|
|
1815
|
-
|
|
2248
|
+
pIn("swing-in", "rotate", "kui-swing-in", { angle: "-15deg", ease: "back-out" })
|
|
1816
2249
|
];
|
|
1817
2250
|
var BLUR = [
|
|
1818
|
-
|
|
2251
|
+
pIn("blur-in", "blur", "kui-blur-in"),
|
|
1819
2252
|
p("blur-out", "blur", "kui-blur-out"),
|
|
1820
|
-
|
|
1821
|
-
|
|
2253
|
+
pIn("fade-blur-up", "reveal-blur", "kui-fade-blur-up"),
|
|
2254
|
+
pIn("fade-blur-in", "reveal-blur", "kui-fade-blur-in")
|
|
1822
2255
|
];
|
|
1823
2256
|
var CHARACTER = [
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2257
|
+
pIn("bounce-in", "scale", "kui-zoom-in", { scale: "0.3", ease: "bounce" }),
|
|
2258
|
+
pIn("bounce-in-up", "reveal", "kui-in-up", { distance: "60px", ease: "back-out" }),
|
|
2259
|
+
pIn("bounce-in-down", "reveal", "kui-in-down", { distance: "60px", ease: "back-out" }),
|
|
2260
|
+
pIn("back-in-up", "reveal", "kui-in-up", { distance: "120px", ease: "expo-out" }),
|
|
2261
|
+
pIn("back-in-down", "reveal", "kui-in-down", { distance: "120px", ease: "expo-out" })
|
|
1829
2262
|
];
|
|
1830
2263
|
var SCROLL = [
|
|
1831
2264
|
p("parallax-y", "parallax", "kui-parallax-y"),
|
|
@@ -1834,11 +2267,14 @@ var SCROLL = [
|
|
|
1834
2267
|
p("parallax-rotate", "parallax-rotate", "kui-parallax-rotate"),
|
|
1835
2268
|
p("depth-layer", "parallax", "kui-parallax-y", { distance: "200px" }),
|
|
1836
2269
|
p("scroll-fade", "scroll-fade", "kui-scroll-fade"),
|
|
2270
|
+
p("scroll-desaturate", "desaturate", "kui-desaturate"),
|
|
2271
|
+
p("scroll-skew", "skew", "kui-scroll-skew"),
|
|
1837
2272
|
p("scroll-progress-bar", "progress", "kui-progress-x"),
|
|
2273
|
+
p("scroll-progress-bar-y", "progress", "kui-progress-y"),
|
|
1838
2274
|
p("scroll-progress-ring", "progress-stroke", "kui-progress-ring"),
|
|
1839
2275
|
// `reveal-repeat` was removed: it was byte-identical to `reveal-once`, and the activation
|
|
1840
2276
|
// binder unobserves after first entry, so a repeating reveal is not implementable yet.
|
|
1841
|
-
|
|
2277
|
+
pIn("reveal-once", "reveal", "kui-in-up")
|
|
1842
2278
|
];
|
|
1843
2279
|
var PRESETS = [
|
|
1844
2280
|
...FADE,
|
|
@@ -1861,6 +2297,59 @@ function registerCore(registry) {
|
|
|
1861
2297
|
return registry;
|
|
1862
2298
|
}
|
|
1863
2299
|
|
|
2300
|
+
// src/effects/step-marking.ts
|
|
2301
|
+
var STEP_STATE_ATTR = "data-kui-step-state";
|
|
2302
|
+
function selectorBreadth(selector, doc) {
|
|
2303
|
+
try {
|
|
2304
|
+
if (doc.documentElement.matches(selector)) return "document-wide";
|
|
2305
|
+
if (doc.body?.matches(selector)) return "document-wide";
|
|
2306
|
+
return "ok";
|
|
2307
|
+
} catch {
|
|
2308
|
+
return "invalid";
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
function resolveTarget(selector, ctx, effect) {
|
|
2312
|
+
if (!selector) return selector;
|
|
2313
|
+
const breadth = selectorBreadth(selector, ctx.doc);
|
|
2314
|
+
if (breadth === "invalid") {
|
|
2315
|
+
ctx.warn(`${effect} target "${selector}" is not a valid selector and will be ignored`);
|
|
2316
|
+
return "";
|
|
2317
|
+
}
|
|
2318
|
+
if (breadth === "document-wide") {
|
|
2319
|
+
ctx.warn(`${effect} target "${selector}" matches the whole document and will be ignored`);
|
|
2320
|
+
return "";
|
|
2321
|
+
}
|
|
2322
|
+
return selector;
|
|
2323
|
+
}
|
|
2324
|
+
function createStepMarker(resolve) {
|
|
2325
|
+
const ledgers = /* @__PURE__ */ new Map();
|
|
2326
|
+
return {
|
|
2327
|
+
mark(index) {
|
|
2328
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2329
|
+
for (const node of resolve()) {
|
|
2330
|
+
const parent = node.parentElement;
|
|
2331
|
+
const position = seen.get(parent) ?? 0;
|
|
2332
|
+
seen.set(parent, position + 1);
|
|
2333
|
+
let ledger = ledgers.get(node);
|
|
2334
|
+
if (!ledger) {
|
|
2335
|
+
ledger = createAttributeLedger(node);
|
|
2336
|
+
ledgers.set(node, ledger);
|
|
2337
|
+
}
|
|
2338
|
+
ledger.set(STEP_STATE_ATTR, stepStateFor(position, index));
|
|
2339
|
+
}
|
|
2340
|
+
},
|
|
2341
|
+
restore() {
|
|
2342
|
+
for (const ledger of ledgers.values()) ledger.restore();
|
|
2343
|
+
ledgers.clear();
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
function stepStateFor(position, index) {
|
|
2348
|
+
if (position < index) return "before";
|
|
2349
|
+
if (position === index) return "active";
|
|
2350
|
+
return "after";
|
|
2351
|
+
}
|
|
2352
|
+
|
|
1864
2353
|
// src/effects/forms/primitives.ts
|
|
1865
2354
|
var timing = {
|
|
1866
2355
|
duration: { type: "time", default: "400ms", cssProperty: "--kui-duration" },
|
|
@@ -1979,10 +2468,18 @@ var RANGE_FILL_PRIMITIVE = jsInputPrimitive(
|
|
|
1979
2468
|
function nextStep(step, total) {
|
|
1980
2469
|
return total > 0 ? (step + 1) % total : 0;
|
|
1981
2470
|
}
|
|
1982
|
-
function prepareStepProgress(el, params) {
|
|
2471
|
+
function prepareStepProgress(el, params, ctx) {
|
|
1983
2472
|
const total = Math.max(1, Math.round(params.num("steps", 4)));
|
|
2473
|
+
const selector = resolveTarget(params.text("target"), ctx, "step-progress");
|
|
2474
|
+
const marker = createStepMarker(
|
|
2475
|
+
() => selector ? ctx.doc.querySelectorAll(selector) : el.children
|
|
2476
|
+
);
|
|
2477
|
+
const self = createAttributeLedger(el);
|
|
1984
2478
|
let step = 0;
|
|
1985
|
-
const render = () =>
|
|
2479
|
+
const render = () => {
|
|
2480
|
+
self.set("data-kui-step", String(step));
|
|
2481
|
+
marker.mark(step);
|
|
2482
|
+
};
|
|
1986
2483
|
const advance = () => {
|
|
1987
2484
|
step = nextStep(step, total);
|
|
1988
2485
|
render();
|
|
@@ -1991,13 +2488,17 @@ function prepareStepProgress(el, params) {
|
|
|
1991
2488
|
render();
|
|
1992
2489
|
return () => {
|
|
1993
2490
|
el.removeEventListener("click", advance);
|
|
1994
|
-
|
|
2491
|
+
self.restore();
|
|
2492
|
+
marker.restore();
|
|
1995
2493
|
};
|
|
1996
2494
|
}
|
|
1997
2495
|
var STEP_PROGRESS_PRIMITIVE = jsInputPrimitive(
|
|
1998
2496
|
"step-progress",
|
|
1999
2497
|
["state"],
|
|
2000
|
-
{
|
|
2498
|
+
{
|
|
2499
|
+
steps: { type: "number", default: "4", cssProperty: "--kui-steps", minimum: 1, maximum: 20, integer: true },
|
|
2500
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" }
|
|
2501
|
+
},
|
|
2001
2502
|
deferPrepare(prepareStepProgress)
|
|
2002
2503
|
);
|
|
2003
2504
|
function nextSubmitStage(stage) {
|
|
@@ -2254,6 +2755,7 @@ function prepareDraggable(el, params, ctx) {
|
|
|
2254
2755
|
const resistance = params.num("resistance", 0.55);
|
|
2255
2756
|
const momentum = params.num("momentum", 0.2);
|
|
2256
2757
|
const offset = { x: 0, y: 0 };
|
|
2758
|
+
const pickup = { x: 0, y: 0 };
|
|
2257
2759
|
const deps = springDeps(ctx);
|
|
2258
2760
|
const runners = {
|
|
2259
2761
|
x: createSpringRunner(config, (value) => write({ ...offset, x: value }), deps),
|
|
@@ -2270,10 +2772,15 @@ function prepareDraggable(el, params, ctx) {
|
|
|
2270
2772
|
onStart() {
|
|
2271
2773
|
runners.x.stop();
|
|
2272
2774
|
runners.y.stop();
|
|
2775
|
+
pickup.x = offset.x;
|
|
2776
|
+
pickup.y = offset.y;
|
|
2273
2777
|
el.setAttribute("data-kui-dragging", "true");
|
|
2274
2778
|
},
|
|
2275
2779
|
onMove(vector) {
|
|
2276
|
-
write({
|
|
2780
|
+
write({
|
|
2781
|
+
x: resist(pickup.x + vector.dx, bounds, resistance),
|
|
2782
|
+
y: resist(pickup.y + vector.dy, bounds, resistance)
|
|
2783
|
+
});
|
|
2277
2784
|
},
|
|
2278
2785
|
onEnd(vector) {
|
|
2279
2786
|
el.setAttribute("data-kui-dragging", "false");
|
|
@@ -2301,8 +2808,9 @@ function settle(runners, offset, vector, mode) {
|
|
|
2301
2808
|
runners.y.to(0);
|
|
2302
2809
|
return;
|
|
2303
2810
|
}
|
|
2304
|
-
|
|
2305
|
-
runners.
|
|
2811
|
+
const carry = mode.inertia ? mode.momentum : 0;
|
|
2812
|
+
runners.x.to(offset.x + vector.vx * carry);
|
|
2813
|
+
runners.y.to(offset.y + vector.vy * carry);
|
|
2306
2814
|
}
|
|
2307
2815
|
function prepareSwipeable(el, params) {
|
|
2308
2816
|
const stop = recognise(
|
|
@@ -2586,9 +3094,12 @@ function prepareAutoHeight(el, params, ctx) {
|
|
|
2586
3094
|
ctx.style.claim("height");
|
|
2587
3095
|
const duration = effectDurationMs(params, 400);
|
|
2588
3096
|
let animation = null;
|
|
3097
|
+
let previous = node.getBoundingClientRect().height;
|
|
2589
3098
|
const observer = watchAttribute(node, params.text("attribute"), () => {
|
|
2590
3099
|
animation?.cancel();
|
|
2591
|
-
|
|
3100
|
+
const endpoints = heightEndpoints(node, previous);
|
|
3101
|
+
previous = endpoints.to;
|
|
3102
|
+
animation = animateHeight(node, endpoints, duration, params.text("ease"));
|
|
2592
3103
|
});
|
|
2593
3104
|
ctx.invalidate();
|
|
2594
3105
|
return () => {
|
|
@@ -2596,13 +3107,19 @@ function prepareAutoHeight(el, params, ctx) {
|
|
|
2596
3107
|
animation?.cancel();
|
|
2597
3108
|
};
|
|
2598
3109
|
}
|
|
2599
|
-
function
|
|
2600
|
-
const from = `${node.getBoundingClientRect().height}px`;
|
|
3110
|
+
function heightEndpoints(node, previous) {
|
|
2601
3111
|
node.style.removeProperty("height");
|
|
2602
|
-
const to =
|
|
3112
|
+
const to = node.getBoundingClientRect().height;
|
|
3113
|
+
return { from: previous, to };
|
|
3114
|
+
}
|
|
3115
|
+
function animateHeight(node, endpoints, duration, easing) {
|
|
2603
3116
|
const animate = node.animate;
|
|
2604
3117
|
if (typeof animate !== "function") return null;
|
|
2605
|
-
return animate.call(
|
|
3118
|
+
return animate.call(
|
|
3119
|
+
node,
|
|
3120
|
+
[{ height: `${endpoints.from}px` }, { height: `${endpoints.to}px` }],
|
|
3121
|
+
{ duration, easing, fill: "none" }
|
|
3122
|
+
);
|
|
2606
3123
|
}
|
|
2607
3124
|
function prepareIndicator(el, params, ctx) {
|
|
2608
3125
|
const node = el;
|
|
@@ -2732,27 +3249,42 @@ function subscribeScrollTop(el, ctx, onScrollTop) {
|
|
|
2732
3249
|
}
|
|
2733
3250
|
function prepareHeaderShrink(el, params, ctx) {
|
|
2734
3251
|
const offset = params.num("offset", 120);
|
|
2735
|
-
|
|
3252
|
+
const state = createAttributeLedger(el);
|
|
3253
|
+
const unsubscribe = subscribeScrollTop(el, ctx, (top) => {
|
|
2736
3254
|
const progress = offset > 0 ? Math.min(1, Math.max(0, top / offset)) : 1;
|
|
2737
3255
|
ctx.style.set("--kui-shrink", progress.toFixed(4));
|
|
2738
|
-
|
|
3256
|
+
state.set("data-kui-shrunk", String(progress >= 1));
|
|
2739
3257
|
});
|
|
3258
|
+
return () => {
|
|
3259
|
+
unsubscribe();
|
|
3260
|
+
state.restore();
|
|
3261
|
+
};
|
|
2740
3262
|
}
|
|
2741
3263
|
function prepareHeaderHide(el, params, ctx) {
|
|
2742
3264
|
const minDelta = params.num("offset", 8);
|
|
2743
3265
|
let last = 0;
|
|
2744
|
-
|
|
3266
|
+
const state = createAttributeLedger(el);
|
|
3267
|
+
const unsubscribe = subscribeScrollTop(el, ctx, (top) => {
|
|
2745
3268
|
const delta = top - last;
|
|
2746
3269
|
if (Math.abs(delta) < minDelta) return;
|
|
2747
|
-
|
|
3270
|
+
state.set("data-kui-hidden", String(delta > 0 && top > minDelta));
|
|
2748
3271
|
last = top;
|
|
2749
3272
|
});
|
|
3273
|
+
return () => {
|
|
3274
|
+
unsubscribe();
|
|
3275
|
+
state.restore();
|
|
3276
|
+
};
|
|
2750
3277
|
}
|
|
2751
3278
|
function prepareBackToTop(el, params, ctx) {
|
|
2752
3279
|
const offset = params.num("offset", 400);
|
|
2753
|
-
|
|
2754
|
-
|
|
3280
|
+
const state = createAttributeLedger(el);
|
|
3281
|
+
const unsubscribe = subscribeScrollTop(el, ctx, (top) => {
|
|
3282
|
+
state.set("data-kui-visible", String(top > offset));
|
|
2755
3283
|
});
|
|
3284
|
+
return () => {
|
|
3285
|
+
unsubscribe();
|
|
3286
|
+
state.restore();
|
|
3287
|
+
};
|
|
2756
3288
|
}
|
|
2757
3289
|
var NAV_JS_PRIMITIVES = [
|
|
2758
3290
|
navPrimitive(
|
|
@@ -2790,13 +3322,38 @@ var domGeometry = (el) => {
|
|
|
2790
3322
|
const rect = el.getBoundingClientRect();
|
|
2791
3323
|
return { top: rect.top, height: rect.height };
|
|
2792
3324
|
};
|
|
3325
|
+
var domPosition = (el) => {
|
|
3326
|
+
const view = el.ownerDocument?.defaultView;
|
|
3327
|
+
return view ? view.getComputedStyle(el).position : "static";
|
|
3328
|
+
};
|
|
3329
|
+
var domOffsetTop = (el) => {
|
|
3330
|
+
const view = el.ownerDocument?.defaultView;
|
|
3331
|
+
if (!view) return 0;
|
|
3332
|
+
const parsed = Number.parseFloat(view.getComputedStyle(el).top);
|
|
3333
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
3334
|
+
};
|
|
3335
|
+
function geometrySource(el, positionOf) {
|
|
3336
|
+
let outermostSticky = null;
|
|
3337
|
+
for (let node = el; node; node = node.parentElement) {
|
|
3338
|
+
if (positionOf(node) === "sticky") outermostSticky = node;
|
|
3339
|
+
}
|
|
3340
|
+
return outermostSticky?.parentElement ?? el;
|
|
3341
|
+
}
|
|
3342
|
+
function sourceTop(el, box, measure, positionOf) {
|
|
3343
|
+
const source = geometrySource(el, positionOf);
|
|
3344
|
+
return source === el ? box.top : measure(source).top;
|
|
3345
|
+
}
|
|
2793
3346
|
function trackProgress(el, ctx, options, onProgress) {
|
|
2794
3347
|
const measure = options.measure ?? domGeometry;
|
|
3348
|
+
const positionOf = options.positionOf ?? domPosition;
|
|
3349
|
+
const offsetOf = options.offsetOf ?? domOffsetTop;
|
|
2795
3350
|
let scrollTop = 0;
|
|
2796
3351
|
let scrollportTop = 0;
|
|
2797
3352
|
const geometry2 = createMeasureCache(() => {
|
|
2798
3353
|
const box = measure(el);
|
|
2799
|
-
|
|
3354
|
+
const top = options.contentAnchor ? measure(options.contentAnchor).top - box.height : sourceTop(el, box, measure, positionOf);
|
|
3355
|
+
const stickyOffset = options.stickyEl ? offsetOf(options.stickyEl) : 0;
|
|
3356
|
+
return { contentTop: top - stickyOffset - scrollportTop + scrollTop, height: box.height };
|
|
2800
3357
|
});
|
|
2801
3358
|
return ctx.scheduler.subscribe(ctx.rootFor(el), (frame) => {
|
|
2802
3359
|
scrollTop = frame.metrics.scrollTop;
|
|
@@ -2822,11 +3379,167 @@ function progressFrom(top, span) {
|
|
|
2822
3379
|
return clamp01(-top / span);
|
|
2823
3380
|
}
|
|
2824
3381
|
|
|
3382
|
+
// src/effects/scroll-mechanics/scroll-spy.ts
|
|
3383
|
+
function prepareScrollSpy(el, params, ctx) {
|
|
3384
|
+
const sectionsAuthored = params.text("sections");
|
|
3385
|
+
if (sectionsAuthored) {
|
|
3386
|
+
const sectionsSelector = resolveTarget(sectionsAuthored, ctx, "scroll-spy sections");
|
|
3387
|
+
return prepareScrollSpyContainer(el, params, ctx, sectionsSelector);
|
|
3388
|
+
}
|
|
3389
|
+
return prepareScrollSpySingle(el, params, ctx);
|
|
3390
|
+
}
|
|
3391
|
+
function prepareScrollSpySingle(el, params, ctx) {
|
|
3392
|
+
if (params.text("offset-top", "0px") !== "0px") {
|
|
3393
|
+
ctx.warn('scroll-spy "offset-top" has no effect without sections: and is ignored here');
|
|
3394
|
+
}
|
|
3395
|
+
const selector = resolveTarget(params.text("target"), ctx, "scroll-spy");
|
|
3396
|
+
const links = /* @__PURE__ */ new Map();
|
|
3397
|
+
const self = createAttributeLedger(el);
|
|
3398
|
+
let last;
|
|
3399
|
+
const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
|
|
3400
|
+
const active = progress > 0 && progress < 1;
|
|
3401
|
+
if (active === last) return;
|
|
3402
|
+
last = active;
|
|
3403
|
+
self.set("data-kui-active", String(active));
|
|
3404
|
+
if (selector) markLinks(ctx.doc, selector, active, links);
|
|
3405
|
+
});
|
|
3406
|
+
return continuousSetup(() => {
|
|
3407
|
+
untrack();
|
|
3408
|
+
self.restore();
|
|
3409
|
+
for (const ledger of links.values()) ledger.restore();
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
function markLinks(doc, selector, active, links) {
|
|
3413
|
+
for (const link of doc.querySelectorAll(selector)) {
|
|
3414
|
+
let ledger = links.get(link);
|
|
3415
|
+
if (!ledger) {
|
|
3416
|
+
ledger = createAttributeLedger(link);
|
|
3417
|
+
links.set(link, ledger);
|
|
3418
|
+
}
|
|
3419
|
+
ledger.set("data-kui-active", String(active));
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
function hrefHash(link) {
|
|
3423
|
+
const href = link.getAttribute("href") ?? "";
|
|
3424
|
+
const at = href.indexOf("#");
|
|
3425
|
+
return at === -1 ? "" : href.slice(at);
|
|
3426
|
+
}
|
|
3427
|
+
function pairSectionsWithLinks(sections, links, ctx, sectionsSelector) {
|
|
3428
|
+
const linksByHash = /* @__PURE__ */ new Map();
|
|
3429
|
+
for (const link of links) {
|
|
3430
|
+
const hash = hrefHash(link);
|
|
3431
|
+
if (hash) linksByHash.set(hash, link);
|
|
3432
|
+
}
|
|
3433
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
3434
|
+
const pairs = sections.map((section) => {
|
|
3435
|
+
if (!section.id) {
|
|
3436
|
+
ctx.warn(
|
|
3437
|
+
`scroll-spy: a section matched by sections:"${sectionsSelector}" has no id and cannot be paired with a link`
|
|
3438
|
+
);
|
|
3439
|
+
return { section, link: null };
|
|
3440
|
+
}
|
|
3441
|
+
const hash = `#${section.id}`;
|
|
3442
|
+
const link = linksByHash.get(hash) ?? null;
|
|
3443
|
+
if (link) claimed.add(hash);
|
|
3444
|
+
return { section, link };
|
|
3445
|
+
});
|
|
3446
|
+
for (const [hash, link] of linksByHash) {
|
|
3447
|
+
if (!claimed.has(hash)) {
|
|
3448
|
+
ctx.warn(`scroll-spy: link "${link.getAttribute("href")}" matches no section id in sections:"${sectionsSelector}"`);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
return pairs;
|
|
3452
|
+
}
|
|
3453
|
+
function offsetTopPixels(authored, frame) {
|
|
3454
|
+
return toPixels(
|
|
3455
|
+
authored,
|
|
3456
|
+
{
|
|
3457
|
+
viewportWidth: frame.metrics.viewportWidth,
|
|
3458
|
+
viewportHeight: frame.metrics.viewportHeight,
|
|
3459
|
+
percentBasis: 0,
|
|
3460
|
+
fontSize: 16,
|
|
3461
|
+
rootFontSize: 16
|
|
3462
|
+
},
|
|
3463
|
+
0
|
|
3464
|
+
);
|
|
3465
|
+
}
|
|
3466
|
+
function highestReachedIndex(tops, scrollTop, line) {
|
|
3467
|
+
let index = -1;
|
|
3468
|
+
for (let i = 0; i < tops.length; i++) {
|
|
3469
|
+
if (tops[i] - scrollTop - line <= 0) index = i;
|
|
3470
|
+
}
|
|
3471
|
+
return index;
|
|
3472
|
+
}
|
|
3473
|
+
function prepareScrollSpyContainer(el, params, ctx, sectionsSelector) {
|
|
3474
|
+
if (params.text("distance", "100vh") !== "100vh") {
|
|
3475
|
+
ctx.warn('scroll-spy "distance" has no effect with sections: \u2014 each section measures its own height');
|
|
3476
|
+
}
|
|
3477
|
+
const linksSelector = resolveTarget(params.text("target"), ctx, "scroll-spy target");
|
|
3478
|
+
const sections = sectionsSelector ? [...el.querySelectorAll(sectionsSelector)] : [];
|
|
3479
|
+
if (sectionsSelector && sections.length === 0) {
|
|
3480
|
+
ctx.warn(`scroll-spy sections:"${sectionsSelector}" matched nothing inside this element`);
|
|
3481
|
+
}
|
|
3482
|
+
const links = linksSelector ? [...el.querySelectorAll(linksSelector)] : [];
|
|
3483
|
+
const pairs = pairSectionsWithLinks(sections, links, ctx, sectionsSelector);
|
|
3484
|
+
const sectionLedgers = pairs.map((pair) => createAttributeLedger(pair.section));
|
|
3485
|
+
const linkLedgers = /* @__PURE__ */ new Map();
|
|
3486
|
+
for (const { link } of pairs) {
|
|
3487
|
+
if (link && !linkLedgers.has(link)) linkLedgers.set(link, createAttributeLedger(link));
|
|
3488
|
+
}
|
|
3489
|
+
const offsetAuthored = params.text("offset-top", "0px");
|
|
3490
|
+
let scrollTop = 0;
|
|
3491
|
+
let scrollportTop = 0;
|
|
3492
|
+
const contentTops = createMeasureCache(
|
|
3493
|
+
() => pairs.map(({ section }) => domGeometry(section).top - scrollportTop + scrollTop)
|
|
3494
|
+
);
|
|
3495
|
+
let active = -1;
|
|
3496
|
+
function setActive(index, value) {
|
|
3497
|
+
const pair = pairs[index];
|
|
3498
|
+
sectionLedgers[index].set("data-kui-active", String(value));
|
|
3499
|
+
if (pair.link) linkLedgers.get(pair.link).set("data-kui-active", String(value));
|
|
3500
|
+
}
|
|
3501
|
+
const untrack = ctx.scheduler.subscribe(ctx.rootFor(el), (frame) => {
|
|
3502
|
+
scrollTop = frame.metrics.scrollTop;
|
|
3503
|
+
scrollportTop = frame.metrics.viewportTop;
|
|
3504
|
+
const tops = contentTops.read(frame.epoch);
|
|
3505
|
+
const line = offsetTopPixels(offsetAuthored, frame);
|
|
3506
|
+
const next = highestReachedIndex(tops, scrollTop, line);
|
|
3507
|
+
if (next === active) return;
|
|
3508
|
+
if (active !== -1) setActive(active, false);
|
|
3509
|
+
if (next !== -1) setActive(next, true);
|
|
3510
|
+
active = next;
|
|
3511
|
+
});
|
|
3512
|
+
return continuousSetup(() => {
|
|
3513
|
+
untrack();
|
|
3514
|
+
for (const ledger of sectionLedgers) ledger.restore();
|
|
3515
|
+
for (const ledger of linkLedgers.values()) ledger.restore();
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
3518
|
+
|
|
2825
3519
|
// src/effects/scroll-mechanics/primitives.ts
|
|
2826
3520
|
var PROGRESS_VAR = "--kui-progress";
|
|
2827
3521
|
var distanceParam = {
|
|
2828
3522
|
distance: { type: "length", default: "100vh", cssProperty: "--kui-distance" }
|
|
2829
3523
|
};
|
|
3524
|
+
var stickyParams = {
|
|
3525
|
+
"offset-top": {
|
|
3526
|
+
type: "length",
|
|
3527
|
+
default: "var(--kui-pin-offset, 0px)",
|
|
3528
|
+
cssProperty: "--kui-offset-top"
|
|
3529
|
+
},
|
|
3530
|
+
spacer: {
|
|
3531
|
+
type: "keyword",
|
|
3532
|
+
default: "false",
|
|
3533
|
+
cssProperty: "--kui-spacer",
|
|
3534
|
+
values: ["true", "false"]
|
|
3535
|
+
}
|
|
3536
|
+
};
|
|
3537
|
+
function installSticky(node, params, ctx) {
|
|
3538
|
+
ctx.style.set("position", "sticky");
|
|
3539
|
+
ctx.style.set("top", params.text("offset-top", "var(--kui-pin-offset, 0px)"));
|
|
3540
|
+
const inserted = params.is("spacer") ? insertSpacer(node, params.text("distance"), ctx) : null;
|
|
3541
|
+
return { spacer: inserted?.spacer ?? null, dispose: () => inserted?.remove() };
|
|
3542
|
+
}
|
|
2830
3543
|
function scrollPrimitive(spec) {
|
|
2831
3544
|
const { id, channels, parameters, prepare, perfClass = "compositor" } = spec;
|
|
2832
3545
|
return {
|
|
@@ -2834,7 +3547,12 @@ function scrollPrimitive(spec) {
|
|
|
2834
3547
|
renderer: "javascript",
|
|
2835
3548
|
channels,
|
|
2836
3549
|
parameters,
|
|
2837
|
-
|
|
3550
|
+
// `'pin'` is accepted but ignored: these primitives read scroll position themselves and are
|
|
3551
|
+
// never driven by an `animation-timeline`. It is listed so that composing the driver with the
|
|
3552
|
+
// effects it drives — `data-kui="pin-section distance:200vh, parallax-rotate ... "` plus
|
|
3553
|
+
// `timeline:pin` — survives `compile.ts`'s `intersect`. Without it the intersection empties,
|
|
3554
|
+
// `style-plan.ts` refuses the timeline, and the scrub silently degrades to a one-shot.
|
|
3555
|
+
supportedTimelines: ["time", "view", "scroll", "pin"],
|
|
2838
3556
|
supportedActivations: ["manual", "load", "enter"],
|
|
2839
3557
|
defaultActivation: "load",
|
|
2840
3558
|
perfClass,
|
|
@@ -2847,19 +3565,17 @@ function writeProgress(ctx, progress) {
|
|
|
2847
3565
|
}
|
|
2848
3566
|
function preparePin(el, params, ctx) {
|
|
2849
3567
|
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;
|
|
3568
|
+
const { dispose: unstick } = installSticky(node, params, ctx);
|
|
2853
3569
|
const tracked = node.parentElement ?? el;
|
|
2854
|
-
const untrack = trackProgress(tracked, ctx, { distance: params.text("distance") }, (progress) => {
|
|
3570
|
+
const untrack = trackProgress(tracked, ctx, { distance: params.text("distance"), stickyEl: node }, (progress) => {
|
|
2855
3571
|
writeProgress(ctx, progress);
|
|
2856
3572
|
el.setAttribute("data-kui-pinned", progress > 0 && progress < 1 ? "true" : "false");
|
|
2857
3573
|
});
|
|
2858
|
-
return () => {
|
|
3574
|
+
return continuousSetup(() => {
|
|
2859
3575
|
untrack();
|
|
2860
|
-
|
|
3576
|
+
unstick();
|
|
2861
3577
|
el.removeAttribute("data-kui-pinned");
|
|
2862
|
-
};
|
|
3578
|
+
});
|
|
2863
3579
|
}
|
|
2864
3580
|
function insertSpacer(node, distance2, ctx) {
|
|
2865
3581
|
const spacer = ctx.doc.createElement("div");
|
|
@@ -2869,34 +3585,81 @@ function insertSpacer(node, distance2, ctx) {
|
|
|
2869
3585
|
spacer.style.pointerEvents = "none";
|
|
2870
3586
|
node.after(spacer);
|
|
2871
3587
|
ctx.invalidate();
|
|
2872
|
-
return
|
|
2873
|
-
spacer
|
|
2874
|
-
|
|
3588
|
+
return {
|
|
3589
|
+
spacer,
|
|
3590
|
+
remove: () => {
|
|
3591
|
+
spacer.remove();
|
|
3592
|
+
ctx.invalidate();
|
|
3593
|
+
}
|
|
2875
3594
|
};
|
|
2876
3595
|
}
|
|
2877
3596
|
function prepareProgress(el, params, ctx) {
|
|
2878
3597
|
const steps = Math.max(0, Math.round(params.num("steps", 0)));
|
|
3598
|
+
const selector = resolveTarget(params.text("target"), ctx, "scrollytelling-step");
|
|
3599
|
+
const marker = createStepMarker(() => ctx.doc.querySelectorAll(selector));
|
|
3600
|
+
const self = createAttributeLedger(el);
|
|
3601
|
+
let lastIndex;
|
|
2879
3602
|
const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
|
|
2880
3603
|
writeProgress(ctx, progress);
|
|
2881
|
-
if (steps
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
3604
|
+
if (steps === 0) return;
|
|
3605
|
+
const index = Math.min(steps - 1, Math.floor(progress * steps));
|
|
3606
|
+
if (index === lastIndex) return;
|
|
3607
|
+
lastIndex = index;
|
|
3608
|
+
self.set("data-kui-step", String(index));
|
|
3609
|
+
ctx.style.set("--kui-step", String(index));
|
|
3610
|
+
if (selector) marker.mark(index);
|
|
2885
3611
|
});
|
|
2886
|
-
return () => {
|
|
3612
|
+
return continuousSetup(() => {
|
|
2887
3613
|
untrack();
|
|
2888
|
-
|
|
2889
|
-
|
|
3614
|
+
self.restore();
|
|
3615
|
+
marker.restore();
|
|
3616
|
+
});
|
|
2890
3617
|
}
|
|
2891
3618
|
function prepareHorizontal(el, params, ctx) {
|
|
2892
|
-
const
|
|
3619
|
+
const selector = resolveTarget(params.text("target"), ctx, "horizontal-scroll");
|
|
3620
|
+
if (!selector) return prepareBareTrack(el, params, ctx);
|
|
3621
|
+
const track = el.querySelector(selector);
|
|
3622
|
+
if (!track) {
|
|
3623
|
+
ctx.warn(`horizontal-scroll target "${selector}" matched nothing inside this element`);
|
|
3624
|
+
return () => {
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
return prepareManagedTrack(el, track, params, ctx);
|
|
3628
|
+
}
|
|
3629
|
+
function prepareBareTrack(node, params, ctx) {
|
|
2893
3630
|
const authored = params.text("travel", "auto");
|
|
2894
3631
|
const travel = createMeasureCache(() => trackTravel(node, authored, node.ownerDocument));
|
|
2895
|
-
|
|
2896
|
-
ctx.
|
|
3632
|
+
return continuousSetup(
|
|
3633
|
+
trackProgress(node, ctx, { distance: params.text("distance") }, (progress, frame) => {
|
|
3634
|
+
ctx.style.set("translate", `${-progress * travel.read(frame.epoch)}px 0`);
|
|
3635
|
+
writeProgress(ctx, progress);
|
|
3636
|
+
})
|
|
3637
|
+
);
|
|
3638
|
+
}
|
|
3639
|
+
function prepareManagedTrack(host, track, params, ctx) {
|
|
3640
|
+
const offsetTop = params.text("offset-top", "var(--kui-pin-offset, 0px)");
|
|
3641
|
+
ctx.style.set("position", "sticky");
|
|
3642
|
+
ctx.style.set("top", offsetTop);
|
|
3643
|
+
ctx.style.set("height", `calc(100vh - ${offsetTop})`);
|
|
3644
|
+
ctx.style.set("overflow", "hidden");
|
|
3645
|
+
ctx.style.set("display", "grid");
|
|
3646
|
+
ctx.style.set("align-content", "center");
|
|
3647
|
+
const { remove: removeSpacer } = insertSpacer(host, params.text("distance"), ctx);
|
|
3648
|
+
const rail = createStyleLedger(track);
|
|
3649
|
+
rail.set("display", "flex");
|
|
3650
|
+
rail.set("width", "max-content");
|
|
3651
|
+
const authored = params.text("travel", "auto");
|
|
3652
|
+
const travel = createMeasureCache(() => trackTravel(track, authored, track.ownerDocument));
|
|
3653
|
+
const tracked = host.parentElement ?? host;
|
|
3654
|
+
const untrack = trackProgress(tracked, ctx, { distance: params.text("distance"), stickyEl: host }, (progress, frame) => {
|
|
3655
|
+
rail.set("translate", `${-progress * travel.read(frame.epoch)}px 0`);
|
|
2897
3656
|
writeProgress(ctx, progress);
|
|
2898
3657
|
});
|
|
2899
|
-
return
|
|
3658
|
+
return continuousSetup(() => {
|
|
3659
|
+
untrack();
|
|
3660
|
+
rail.restore();
|
|
3661
|
+
removeSpacer();
|
|
3662
|
+
});
|
|
2900
3663
|
}
|
|
2901
3664
|
function trackTravel(node, authored, doc) {
|
|
2902
3665
|
if (authored && authored !== "auto") return toPixels(authored, ABSOLUTE_BASIS, 0);
|
|
@@ -2906,11 +3669,41 @@ function trackTravel(node, authored, doc) {
|
|
|
2906
3669
|
return Math.max(0, node.scrollWidth - viewportWidth);
|
|
2907
3670
|
}
|
|
2908
3671
|
function prepareMediaScrub(el, params, ctx) {
|
|
3672
|
+
const managed = params.is("spacer") ? installSticky(el, params, ctx) : null;
|
|
3673
|
+
const contentAnchor = managed?.spacer ?? void 0;
|
|
3674
|
+
const stickyEl = managed ? el : void 0;
|
|
3675
|
+
const selector = resolveTarget(params.text("target"), ctx, "media-scrub");
|
|
3676
|
+
const scrub = selector ? prepareTargetScrub(el, params, ctx, { selector, contentAnchor, stickyEl }) : prepareSrcScrub(el, params, ctx, { contentAnchor, stickyEl });
|
|
3677
|
+
return continuousSetup(() => {
|
|
3678
|
+
scrub();
|
|
3679
|
+
managed?.dispose();
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
function prepareTargetScrub(el, params, ctx, authored) {
|
|
3683
|
+
const { selector, contentAnchor, stickyEl } = authored;
|
|
3684
|
+
const marker = createStepMarker(() => ctx.doc.querySelectorAll(selector));
|
|
3685
|
+
const frames = Math.max(1, ctx.doc.querySelectorAll(selector).length);
|
|
3686
|
+
let lastIndex;
|
|
3687
|
+
const untrack = trackProgress(el, ctx, { distance: params.text("distance"), contentAnchor, stickyEl }, (progress) => {
|
|
3688
|
+
writeProgress(ctx, progress);
|
|
3689
|
+
const index = Math.min(frames - 1, Math.floor(progress * frames));
|
|
3690
|
+
if (index === lastIndex) return;
|
|
3691
|
+
lastIndex = index;
|
|
3692
|
+
marker.mark(index);
|
|
3693
|
+
});
|
|
3694
|
+
marker.mark(0);
|
|
3695
|
+
return () => {
|
|
3696
|
+
untrack();
|
|
3697
|
+
marker.restore();
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
function prepareSrcScrub(el, params, ctx, anchors) {
|
|
3701
|
+
const { contentAnchor, stickyEl } = anchors;
|
|
2909
3702
|
const frames = Math.max(1, Math.round(params.num("frames", 1)));
|
|
2910
3703
|
const pattern = mediaSrcPattern(params.text("src"), ctx);
|
|
2911
3704
|
const media = el;
|
|
2912
3705
|
let lastIndex = -1;
|
|
2913
|
-
const untrack = trackProgress(el, ctx, { distance: params.text("distance") }, (progress) => {
|
|
3706
|
+
const untrack = trackProgress(el, ctx, { distance: params.text("distance"), contentAnchor, stickyEl }, (progress) => {
|
|
2914
3707
|
writeProgress(ctx, progress);
|
|
2915
3708
|
const index = Math.min(frames - 1, Math.floor(progress * frames));
|
|
2916
3709
|
if (index === lastIndex) return;
|
|
@@ -2934,80 +3727,37 @@ function applyFrame(media, write) {
|
|
|
2934
3727
|
if (media.tagName !== "IMG") return;
|
|
2935
3728
|
if (pattern) media.src = pattern.replace("{i}", String(index).padStart(String(frames).length, "0"));
|
|
2936
3729
|
}
|
|
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
|
-
});
|
|
3730
|
+
function prepareSmoothScroll(el, params, ctx) {
|
|
3731
|
+
ctx.style.set("scroll-behavior", params.text("behavior", "smooth"));
|
|
2949
3732
|
return () => {
|
|
2950
|
-
untrack();
|
|
2951
|
-
self.restore();
|
|
2952
|
-
for (const ledger of links.values()) ledger.restore();
|
|
2953
3733
|
};
|
|
2954
3734
|
}
|
|
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
3735
|
function prepareSnap(el, params, ctx) {
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
);
|
|
2992
|
-
|
|
3736
|
+
const axis = params.is("axis", "x") ? "x" : "y";
|
|
3737
|
+
ctx.style.set("scroll-snap-type", `${axis} ${params.text("strictness", "mandatory")}`);
|
|
3738
|
+
const selector = resolveTarget(params.text("target"), ctx, "scroll-snap");
|
|
3739
|
+
const items = selector ? [...el.querySelectorAll(selector)] : [...el.children];
|
|
3740
|
+
if (selector && items.length === 0) {
|
|
3741
|
+
ctx.warn(`scroll-snap target "${selector}" matched nothing inside this element`);
|
|
3742
|
+
}
|
|
3743
|
+
if (selector) installSnapContainer(axis, ctx);
|
|
3744
|
+
const childLedgers = items.map((child) => createStyleLedger(child));
|
|
2993
3745
|
for (const ledger of childLedgers) ledger.set("scroll-snap-align", params.text("align", "start"));
|
|
2994
3746
|
return () => {
|
|
2995
3747
|
for (const ledger of childLedgers) ledger.restore();
|
|
2996
3748
|
};
|
|
2997
3749
|
}
|
|
3750
|
+
function installSnapContainer(axis, ctx) {
|
|
3751
|
+
ctx.style.set(axis === "x" ? "overflow-x" : "overflow-y", "auto");
|
|
3752
|
+
if (axis === "x") ctx.style.set("display", "flex");
|
|
3753
|
+
}
|
|
2998
3754
|
var SCROLL_PRIMITIVES = [
|
|
2999
3755
|
scrollPrimitive({
|
|
3000
3756
|
id: "pin",
|
|
3001
3757
|
channels: ["layout", "progress"],
|
|
3002
3758
|
parameters: {
|
|
3003
3759
|
...distanceParam,
|
|
3004
|
-
|
|
3005
|
-
spacer: {
|
|
3006
|
-
type: "keyword",
|
|
3007
|
-
default: "false",
|
|
3008
|
-
cssProperty: "--kui-spacer",
|
|
3009
|
-
values: ["true", "false"]
|
|
3010
|
-
}
|
|
3760
|
+
...stickyParams
|
|
3011
3761
|
},
|
|
3012
3762
|
prepare: deferPrepare(preparePin),
|
|
3013
3763
|
perfClass: "layout"
|
|
@@ -3017,7 +3767,10 @@ var SCROLL_PRIMITIVES = [
|
|
|
3017
3767
|
channels: ["progress"],
|
|
3018
3768
|
parameters: {
|
|
3019
3769
|
...distanceParam,
|
|
3020
|
-
steps: { type: "number", default: "0", cssProperty: "--kui-steps" }
|
|
3770
|
+
steps: { type: "number", default: "0", cssProperty: "--kui-steps" },
|
|
3771
|
+
// Same name, same shape and the same validation as scroll-spy's: one `target:` convention
|
|
3772
|
+
// across the library rather than a second word for "the elements this effect marks".
|
|
3773
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" }
|
|
3021
3774
|
},
|
|
3022
3775
|
prepare: deferPrepare(prepareProgress)
|
|
3023
3776
|
}),
|
|
@@ -3026,7 +3779,16 @@ var SCROLL_PRIMITIVES = [
|
|
|
3026
3779
|
channels: ["translate", "progress"],
|
|
3027
3780
|
parameters: {
|
|
3028
3781
|
...distanceParam,
|
|
3029
|
-
travel: { type: "text", default: "auto", cssProperty: "--kui-travel" }
|
|
3782
|
+
travel: { type: "text", default: "auto", cssProperty: "--kui-travel" },
|
|
3783
|
+
// Same name, same shape and the same validation as scroll-spy's and media-scrub's: one
|
|
3784
|
+
// `target:` convention across the library. Naming the row that moves is also what opts this
|
|
3785
|
+
// primitive into owning the stage, the sticky window and the row's layout itself.
|
|
3786
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" },
|
|
3787
|
+
"offset-top": {
|
|
3788
|
+
type: "length",
|
|
3789
|
+
default: "var(--kui-pin-offset, 0px)",
|
|
3790
|
+
cssProperty: "--kui-offset-top"
|
|
3791
|
+
}
|
|
3030
3792
|
},
|
|
3031
3793
|
prepare: deferPrepare(prepareHorizontal)
|
|
3032
3794
|
}),
|
|
@@ -3035,8 +3797,13 @@ var SCROLL_PRIMITIVES = [
|
|
|
3035
3797
|
channels: ["media", "progress"],
|
|
3036
3798
|
parameters: {
|
|
3037
3799
|
...distanceParam,
|
|
3800
|
+
// A scrub is a hold, so it needs the same two knobs a pin does. Declaring them here is what
|
|
3801
|
+
// lets `sequence-scrub` become one attribute with no wrapper at all.
|
|
3802
|
+
...stickyParams,
|
|
3038
3803
|
frames: { type: "number", default: "1", cssProperty: "--kui-frames" },
|
|
3039
|
-
src: { type: "text", default: "", cssProperty: "--kui-src" }
|
|
3804
|
+
src: { type: "text", default: "", cssProperty: "--kui-src" },
|
|
3805
|
+
// The preferred form. `frames:`/`src:` remain for sequences too long to author as tags.
|
|
3806
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" }
|
|
3040
3807
|
},
|
|
3041
3808
|
prepare: deferPrepare(prepareMediaScrub),
|
|
3042
3809
|
perfClass: "paint"
|
|
@@ -3045,11 +3812,30 @@ var SCROLL_PRIMITIVES = [
|
|
|
3045
3812
|
id: "scroll-spy",
|
|
3046
3813
|
channels: ["state"],
|
|
3047
3814
|
parameters: {
|
|
3815
|
+
// `distance`: the per-section form only. `offset-top`: the container form only. Each is a
|
|
3816
|
+
// no-op — warned, not silent — in the other; see `prepareScrollSpySingle` and
|
|
3817
|
+
// `prepareScrollSpyContainer`.
|
|
3048
3818
|
...distanceParam,
|
|
3049
|
-
|
|
3819
|
+
// Same name and meaning in both forms: the link(s) this instance marks. Per-section, the
|
|
3820
|
+
// one link this section names; with `sections:`, every link `target:` matches, each paired
|
|
3821
|
+
// to its own section by `href`. See `prepareScrollSpyContainer`.
|
|
3822
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" },
|
|
3823
|
+
// Presence, not value, selects the container form: authoring this at all switches
|
|
3824
|
+
// `prepareScrollSpy` from one-section-per-instance to one-instance-on-the-shared-ancestor.
|
|
3825
|
+
sections: { type: "text", default: "", cssProperty: "--kui-sections" },
|
|
3826
|
+
"offset-top": { type: "length", default: "0px", cssProperty: "--kui-offset-top" }
|
|
3050
3827
|
},
|
|
3051
3828
|
prepare: deferPrepare(prepareScrollSpy)
|
|
3052
3829
|
}),
|
|
3830
|
+
scrollPrimitive({
|
|
3831
|
+
id: "smooth-scroll",
|
|
3832
|
+
channels: ["layout"],
|
|
3833
|
+
parameters: {
|
|
3834
|
+
behavior: { type: "keyword", default: "smooth", cssProperty: "--kui-scroll-behavior", values: ["smooth", "auto"] }
|
|
3835
|
+
},
|
|
3836
|
+
prepare: deferPrepare(prepareSmoothScroll),
|
|
3837
|
+
perfClass: "layout"
|
|
3838
|
+
}),
|
|
3053
3839
|
scrollPrimitive({
|
|
3054
3840
|
id: "scroll-snap",
|
|
3055
3841
|
channels: ["layout"],
|
|
@@ -3066,7 +3852,11 @@ var SCROLL_PRIMITIVES = [
|
|
|
3066
3852
|
default: "start",
|
|
3067
3853
|
cssProperty: "--kui-snap-align",
|
|
3068
3854
|
values: ["start", "center", "end"]
|
|
3069
|
-
}
|
|
3855
|
+
},
|
|
3856
|
+
// Names the snap items. Also what opts this primitive into owning the scroll container
|
|
3857
|
+
// itself — see `installSnapContainer`. Without it, the direct children are the items and
|
|
3858
|
+
// the page keeps its own `overflow`, exactly as before.
|
|
3859
|
+
target: { type: "text", default: "", cssProperty: "--kui-target" }
|
|
3070
3860
|
},
|
|
3071
3861
|
prepare: deferPrepare(prepareSnap),
|
|
3072
3862
|
perfClass: "layout"
|
|
@@ -3089,11 +3879,28 @@ var SCROLL_PRESETS = [
|
|
|
3089
3879
|
// --- travel ------------------------------------------------------------------------------
|
|
3090
3880
|
{ name: "horizontal-scroll", primitive: "horizontal-track" },
|
|
3091
3881
|
// --- media -------------------------------------------------------------------------------
|
|
3092
|
-
|
|
3882
|
+
/*
|
|
3883
|
+
* `spacer:true` is what deletes the `.scrub-stage` wrapper a page used to hand-write.
|
|
3884
|
+
*
|
|
3885
|
+
* It could not be switched on until the tracker stopped measuring against the parent. A scrub
|
|
3886
|
+
* makes itself sticky, and `geometrySource` escapes a sticky subtree by taking its parent — so
|
|
3887
|
+
* with the wrapper gone the scrub was measured against whatever section contained it. Measured
|
|
3888
|
+
* on `demo/scroll.html`: the parent started 926px above the scrub against a 1817px distance, so
|
|
3889
|
+
* progress reached 51% before the element had even stuck and half the sequence played off
|
|
3890
|
+
* screen. The wrapper was not ceremony; it was the tight box that made the parent honest.
|
|
3891
|
+
*
|
|
3892
|
+
* `trackProgress`'s `contentAnchor` is the fix. Progress is read from the spacer, which the
|
|
3893
|
+
* library inserts, is exactly `distance` tall, is never sticky, and moves with the content — so
|
|
3894
|
+
* there is no wrapper to write and nothing to disagree with.
|
|
3895
|
+
*
|
|
3896
|
+
* `video-scrub` stays off: a video positioned by the page is not asking the library for a box.
|
|
3897
|
+
*/
|
|
3898
|
+
{ name: "sequence-scrub", primitive: "media-scrub", params: { spacer: "true" } },
|
|
3093
3899
|
{ name: "video-scrub", primitive: "media-scrub" },
|
|
3094
3900
|
// --- navigation --------------------------------------------------------------------------
|
|
3095
3901
|
{ name: "scroll-spy", primitive: "scroll-spy" },
|
|
3096
3902
|
// --- native CSS passthroughs ---------------------------------------------------------------
|
|
3903
|
+
{ name: "smooth-scroll-to", primitive: "smooth-scroll" },
|
|
3097
3904
|
{ name: "scroll-snap-x", primitive: "scroll-snap", params: { axis: "x" } },
|
|
3098
3905
|
{ name: "scroll-snap-y", primitive: "scroll-snap", params: { axis: "y" } }
|
|
3099
3906
|
];
|
|
@@ -3108,11 +3915,16 @@ var COMMAND = /([mlhvcz])|(-?(?:\d+(?:\.\d+)?|\.\d+))/gi;
|
|
|
3108
3915
|
var UNSUPPORTED = /[AaSsQqTt]/;
|
|
3109
3916
|
function parsePath(d) {
|
|
3110
3917
|
if (UNSUPPORTED.test(d)) {
|
|
3111
|
-
return {
|
|
3918
|
+
return {
|
|
3919
|
+
segments: [],
|
|
3920
|
+
subpaths: [],
|
|
3921
|
+
reason: "arc and shorthand commands (A S Q T) are not supported"
|
|
3922
|
+
};
|
|
3112
3923
|
}
|
|
3113
3924
|
const tokens = [...d.matchAll(COMMAND)].map((m) => m[1] ?? m[2]);
|
|
3114
3925
|
const state = {
|
|
3115
|
-
|
|
3926
|
+
subpaths: [],
|
|
3927
|
+
open: void 0,
|
|
3116
3928
|
current: { x: 0, y: 0 },
|
|
3117
3929
|
start: { x: 0, y: 0 },
|
|
3118
3930
|
command: ""
|
|
@@ -3126,18 +3938,33 @@ function parsePath(d) {
|
|
|
3126
3938
|
index++;
|
|
3127
3939
|
continue;
|
|
3128
3940
|
}
|
|
3129
|
-
if (!state.command)
|
|
3941
|
+
if (!state.command) {
|
|
3942
|
+
return { segments: [], subpaths: [], reason: "path must start with a command letter" };
|
|
3943
|
+
}
|
|
3130
3944
|
index = consume(tokens, index, state);
|
|
3131
3945
|
}
|
|
3132
|
-
if (state.
|
|
3133
|
-
|
|
3946
|
+
if (state.subpaths.length === 0) {
|
|
3947
|
+
return { segments: [], subpaths: [], reason: "no drawable segments" };
|
|
3948
|
+
}
|
|
3949
|
+
return { segments: state.subpaths.flatMap((sub) => sub.segments), subpaths: state.subpaths };
|
|
3950
|
+
}
|
|
3951
|
+
function pushSegment(state, segment) {
|
|
3952
|
+
if (!state.open) {
|
|
3953
|
+
state.open = { segments: [], closed: false };
|
|
3954
|
+
state.subpaths.push(state.open);
|
|
3955
|
+
}
|
|
3956
|
+
state.open.segments.push(segment);
|
|
3134
3957
|
}
|
|
3135
3958
|
var ARITY = { m: 2, l: 2, h: 1, v: 1, c: 6, z: 0 };
|
|
3136
3959
|
function closeSubpath(state) {
|
|
3137
|
-
const { current, start } = state;
|
|
3138
|
-
if (
|
|
3139
|
-
|
|
3960
|
+
const { current, start, open } = state;
|
|
3961
|
+
if (!open) return;
|
|
3962
|
+
if (Math.abs(current.x - start.x) >= 1e-6 || Math.abs(current.y - start.y) >= 1e-6) {
|
|
3963
|
+
open.segments.push(lineToCubic(current, start));
|
|
3964
|
+
}
|
|
3965
|
+
open.closed = true;
|
|
3140
3966
|
state.current = { ...start };
|
|
3967
|
+
state.open = void 0;
|
|
3141
3968
|
}
|
|
3142
3969
|
function consume(tokens, index, state) {
|
|
3143
3970
|
const key = state.command.toLowerCase();
|
|
@@ -3149,10 +3976,11 @@ function consume(tokens, index, state) {
|
|
|
3149
3976
|
if (key === "m") {
|
|
3150
3977
|
state.current = next;
|
|
3151
3978
|
state.start = next;
|
|
3979
|
+
state.open = void 0;
|
|
3152
3980
|
state.command = relative ? "l" : "L";
|
|
3153
3981
|
return index + arity;
|
|
3154
3982
|
}
|
|
3155
|
-
state
|
|
3983
|
+
pushSegment(state, straightOrCubic({ key, args, from: state.current, relative }, next));
|
|
3156
3984
|
state.current = next;
|
|
3157
3985
|
return index + arity;
|
|
3158
3986
|
}
|
|
@@ -3215,34 +4043,78 @@ function normaliseCount(segments, target) {
|
|
|
3215
4043
|
function chordLength(segment) {
|
|
3216
4044
|
return Math.hypot(segment.to.x - segment.from.x, segment.to.y - segment.from.y);
|
|
3217
4045
|
}
|
|
3218
|
-
function
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
);
|
|
4046
|
+
function round(value) {
|
|
4047
|
+
return Math.round(value * 100) / 100;
|
|
4048
|
+
}
|
|
4049
|
+
function subpathsToPathData(subpaths) {
|
|
4050
|
+
const parts = [];
|
|
4051
|
+
for (const sub of subpaths) {
|
|
4052
|
+
const head = sub.segments[0];
|
|
4053
|
+
parts.push(`M${round(head.from.x)},${round(head.from.y)}`);
|
|
4054
|
+
for (const s of sub.segments) {
|
|
4055
|
+
parts.push(
|
|
4056
|
+
`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)}`
|
|
4057
|
+
);
|
|
4058
|
+
}
|
|
4059
|
+
if (sub.closed) parts.push("Z");
|
|
3226
4060
|
}
|
|
3227
4061
|
return parts.join(" ");
|
|
3228
4062
|
}
|
|
3229
|
-
function
|
|
3230
|
-
|
|
4063
|
+
function centroidOf(subpath) {
|
|
4064
|
+
let x = 0;
|
|
4065
|
+
let y = 0;
|
|
4066
|
+
for (const s of subpath.segments) {
|
|
4067
|
+
x += s.from.x;
|
|
4068
|
+
y += s.from.y;
|
|
4069
|
+
}
|
|
4070
|
+
return { x: x / subpath.segments.length, y: y / subpath.segments.length };
|
|
4071
|
+
}
|
|
4072
|
+
function collapsedLike(partner) {
|
|
4073
|
+
const at = centroidOf(partner);
|
|
4074
|
+
return {
|
|
4075
|
+
segments: partner.segments.map(() => ({
|
|
4076
|
+
from: { ...at },
|
|
4077
|
+
c1: { ...at },
|
|
4078
|
+
c2: { ...at },
|
|
4079
|
+
to: { ...at }
|
|
4080
|
+
})),
|
|
4081
|
+
closed: partner.closed
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
function normaliseSubpaths(a, b) {
|
|
4085
|
+
const count = Math.max(a.length, b.length);
|
|
4086
|
+
const from = [];
|
|
4087
|
+
const to = [];
|
|
4088
|
+
for (let i = 0; i < count; i++) {
|
|
4089
|
+
const left = a[i] ?? collapsedLike(b[i]);
|
|
4090
|
+
const right = b[i] ?? collapsedLike(a[i]);
|
|
4091
|
+
const target = Math.max(left.segments.length, right.segments.length);
|
|
4092
|
+
const closed = left.closed && right.closed;
|
|
4093
|
+
from.push({ segments: normaliseCount(left.segments, target), closed });
|
|
4094
|
+
to.push({ segments: normaliseCount(right.segments, target), closed });
|
|
4095
|
+
}
|
|
4096
|
+
return { from, to };
|
|
3231
4097
|
}
|
|
3232
4098
|
function createMorph(fromPath, toPath) {
|
|
3233
4099
|
const a = parsePath(fromPath);
|
|
3234
4100
|
const b = parsePath(toPath);
|
|
3235
4101
|
if (a.reason) return { reason: `start path: ${a.reason}` };
|
|
3236
4102
|
if (b.reason) return { reason: `end path: ${b.reason}` };
|
|
3237
|
-
const
|
|
3238
|
-
const
|
|
3239
|
-
const to = normaliseCount(b.segments, count);
|
|
4103
|
+
const { from, to } = normaliseSubpaths(a.subpaths, b.subpaths);
|
|
4104
|
+
const count = from.reduce((total, sub) => total + sub.segments.length, 0);
|
|
3240
4105
|
return {
|
|
3241
4106
|
morph: {
|
|
3242
4107
|
segmentCount: count,
|
|
3243
4108
|
at(t) {
|
|
3244
4109
|
const clamped = Math.min(1, Math.max(0, t));
|
|
3245
|
-
return
|
|
4110
|
+
return subpathsToPathData(
|
|
4111
|
+
from.map((sub, i) => ({
|
|
4112
|
+
segments: sub.segments.map(
|
|
4113
|
+
(segment, j) => lerpCubic(segment, to[i].segments[j], clamped)
|
|
4114
|
+
),
|
|
4115
|
+
closed: sub.closed
|
|
4116
|
+
}))
|
|
4117
|
+
);
|
|
3246
4118
|
}
|
|
3247
4119
|
}
|
|
3248
4120
|
};
|
|
@@ -3299,6 +4171,35 @@ function prepareMorph(el, params, ctx) {
|
|
|
3299
4171
|
path.setAttribute("d", startPath);
|
|
3300
4172
|
};
|
|
3301
4173
|
}
|
|
4174
|
+
var PATH_DRAW_PRIMITIVE = cssPrimitive("path-draw", [CHANNEL.stroke], {
|
|
4175
|
+
parameters: {
|
|
4176
|
+
length: { type: "number", default: "100", cssProperty: "--kui-path-length", finite: true }
|
|
4177
|
+
}
|
|
4178
|
+
});
|
|
4179
|
+
var SHAPE_FILL_PRIMITIVE = cssPrimitive("shape-fill", [CHANNEL.clip]);
|
|
4180
|
+
var BAR_GROW_PRIMITIVE = cssPrimitive("bar-grow", [CHANNEL.scale], {
|
|
4181
|
+
parameters: { from: { type: "number", default: "0", cssProperty: "--kui-bar-from" } }
|
|
4182
|
+
});
|
|
4183
|
+
var LOGO_BUILD_PRIMITIVE = cssPrimitive("logo-assemble", [
|
|
4184
|
+
CHANNEL.opacity,
|
|
4185
|
+
CHANNEL.scale,
|
|
4186
|
+
CHANNEL.rotate
|
|
4187
|
+
]);
|
|
4188
|
+
var ICON_TOGGLE_PRIMITIVE = {
|
|
4189
|
+
id: "icon-toggle",
|
|
4190
|
+
renderer: "javascript",
|
|
4191
|
+
channels: [CHANNEL.translate, CHANNEL.rotate, CHANNEL.scale, CHANNEL.opacity, CHANNEL.clip],
|
|
4192
|
+
parameters: {
|
|
4193
|
+
duration: { type: "time", default: "260ms", cssProperty: "--kui-duration" },
|
|
4194
|
+
ease: { type: "easing", default: "ease-out", cssProperty: "--kui-ease" }
|
|
4195
|
+
},
|
|
4196
|
+
supportedTimelines: ["time"],
|
|
4197
|
+
supportedActivations: ["load"],
|
|
4198
|
+
defaultActivation: "load",
|
|
4199
|
+
perfClass: "compositor",
|
|
4200
|
+
reducedMotion: "disable",
|
|
4201
|
+
prepare: () => inertInstance()
|
|
4202
|
+
};
|
|
3302
4203
|
var SVG_PRIMITIVES = [
|
|
3303
4204
|
{
|
|
3304
4205
|
id: "path-morph",
|
|
@@ -3318,19 +4219,77 @@ var SVG_PRIMITIVES = [
|
|
|
3318
4219
|
perfClass: "paint",
|
|
3319
4220
|
reducedMotion: "disable",
|
|
3320
4221
|
prepare: deferPrepare(prepareMorph)
|
|
3321
|
-
}
|
|
4222
|
+
},
|
|
4223
|
+
PATH_DRAW_PRIMITIVE,
|
|
4224
|
+
SHAPE_FILL_PRIMITIVE,
|
|
4225
|
+
BAR_GROW_PRIMITIVE,
|
|
4226
|
+
LOGO_BUILD_PRIMITIVE,
|
|
4227
|
+
ICON_TOGGLE_PRIMITIVE
|
|
3322
4228
|
];
|
|
3323
4229
|
var SVG_PRESETS = [
|
|
3324
4230
|
{ name: "icon-morph", primitive: "path-morph" },
|
|
3325
|
-
{ name: "blob-morph", primitive: "path-morph", params: { duration: "800ms" } }
|
|
4231
|
+
{ name: "blob-morph", primitive: "path-morph", params: { duration: "800ms" } },
|
|
4232
|
+
// Stroke draws. One keyframe block each rather than one shared block, matching the
|
|
4233
|
+
// progress-ring/gauge-sweep/donut-sweep/sparkline-draw group in numbers.ts: identical bodies
|
|
4234
|
+
// today, but each name is free to diverge and a consumer can restyle one without the others.
|
|
4235
|
+
{ name: "draw-stroke", primitive: "path-draw", keyframes: "kui-draw-stroke", params: { duration: "800ms", ease: "ease-in-out" } },
|
|
4236
|
+
{ name: "draw-signature", primitive: "path-draw", keyframes: "kui-draw-signature", params: { duration: "1600ms", ease: "ease-in-out" } },
|
|
4237
|
+
{ name: "draw-underline", primitive: "path-draw", keyframes: "kui-draw-underline", params: { duration: "420ms" } },
|
|
4238
|
+
{ name: "checkmark-draw", primitive: "path-draw", keyframes: "kui-checkmark-draw", params: { duration: "320ms" } },
|
|
4239
|
+
{ name: "cross-draw", primitive: "path-draw", keyframes: "kui-cross-draw", params: { duration: "260ms" } },
|
|
4240
|
+
{ name: "chart-line-draw", primitive: "path-draw", keyframes: "kui-chart-line-draw", params: { duration: "1200ms", ease: "ease-in-out" } },
|
|
4241
|
+
{ name: "gradient-stroke", primitive: "path-draw", keyframes: "kui-gradient-stroke", params: { duration: "2400ms", ease: "ease-in-out" } },
|
|
4242
|
+
// Fills.
|
|
4243
|
+
{ name: "heart-fill", primitive: "shape-fill", keyframes: "kui-heart-fill", params: { duration: "420ms" } },
|
|
4244
|
+
{ name: "bookmark-fill", primitive: "shape-fill", keyframes: "kui-bookmark-fill", params: { duration: "360ms" } },
|
|
4245
|
+
{ name: "chart-area-fill", primitive: "shape-fill", keyframes: "kui-chart-area-fill", params: { duration: "900ms" } },
|
|
4246
|
+
// `cloak: true`: `kui-chart-bar-grow`'s `from { scale: 1 0 }` (svg.css) is a zero-height box
|
|
4247
|
+
// while paused, not just an invisible one — so it occupies no space in layout for the whole
|
|
4248
|
+
// wait. Same defect `fold-panel` has, and the same fix shape: see svg.css's
|
|
4249
|
+
// `[data-kui-fx~='chart-bar-grow'][data-kui-state='ready']` rule.
|
|
4250
|
+
{
|
|
4251
|
+
name: "chart-bar-grow",
|
|
4252
|
+
primitive: "bar-grow",
|
|
4253
|
+
keyframes: "kui-chart-bar-grow",
|
|
4254
|
+
params: { duration: "700ms", ease: "back-out" },
|
|
4255
|
+
cloak: true
|
|
4256
|
+
},
|
|
4257
|
+
{ name: "logo-build", primitive: "logo-assemble", keyframes: "kui-logo-build", params: { duration: "520ms", ease: "back-out" } },
|
|
4258
|
+
// Icon toggles — no `keyframes`, because their motion is a CSS transition in svg.css keyed off
|
|
4259
|
+
// aria state, not a compiled animation. Same shape as forms.ts's native-state presets.
|
|
4260
|
+
{ name: "hamburger-to-x", primitive: "icon-toggle" },
|
|
4261
|
+
{ name: "play-to-pause", primitive: "icon-toggle" },
|
|
4262
|
+
{ name: "plus-to-minus", primitive: "icon-toggle" }
|
|
3326
4263
|
];
|
|
3327
4264
|
function registerSvg(registry) {
|
|
3328
4265
|
return registry.registerPrimitives(SVG_PRIMITIVES).registerPresets(SVG_PRESETS);
|
|
3329
4266
|
}
|
|
3330
4267
|
|
|
3331
4268
|
// src/effects/three-d/index.ts
|
|
4269
|
+
var CARD_TOGGLE_PRIMITIVE = {
|
|
4270
|
+
id: "card-toggle",
|
|
4271
|
+
renderer: "javascript",
|
|
4272
|
+
channels: [CHANNEL.rotate],
|
|
4273
|
+
parameters: {
|
|
4274
|
+
duration: { type: "time", default: "700ms", cssProperty: "--kui-duration" },
|
|
4275
|
+
ease: { type: "easing", default: "ease-in-out", cssProperty: "--kui-ease" },
|
|
4276
|
+
perspective: { type: "length", default: "1600px", cssProperty: "--kui-perspective" }
|
|
4277
|
+
},
|
|
4278
|
+
supportedTimelines: ["time"],
|
|
4279
|
+
supportedActivations: ["load"],
|
|
4280
|
+
defaultActivation: "load",
|
|
4281
|
+
perfClass: "compositor",
|
|
4282
|
+
reducedMotion: "disable",
|
|
4283
|
+
prepare: () => inertInstance()
|
|
4284
|
+
};
|
|
3332
4285
|
var THREE_D_PRIMITIVES = [
|
|
3333
|
-
|
|
4286
|
+
// `skew`, not `rotate`: the keyframes in three-d.css write `transform: perspective(...)
|
|
4287
|
+
// rotateX/Y(...)`, not the individual `rotate:` property — `perspective` only creates depth for
|
|
4288
|
+
// an element's *children*, so giving one of these effects its own depth means reaching for the
|
|
4289
|
+
// `perspective()` transform *function*, which only exists inside the `transform` shorthand.
|
|
4290
|
+
// `CHANNEL.skew` is this catalog's name for "claims the whole `transform` shorthand"; see the
|
|
4291
|
+
// comment on it in `core/types.ts`.
|
|
4292
|
+
cssPrimitive("flip-face", [CHANNEL.skew], {
|
|
3334
4293
|
parameters: {
|
|
3335
4294
|
angle: { type: "angle", default: "180deg", cssProperty: "--kui-from-angle" },
|
|
3336
4295
|
perspective: { type: "length", default: "1200px", cssProperty: "--kui-perspective" }
|
|
@@ -3340,12 +4299,19 @@ var THREE_D_PRIMITIVES = [
|
|
|
3340
4299
|
parameters: { distance: { type: "length", default: "40px", cssProperty: "--kui-distance" } }
|
|
3341
4300
|
}),
|
|
3342
4301
|
cssPrimitive("wipe", [CHANNEL.clip]),
|
|
3343
|
-
|
|
4302
|
+
// `from` gives `loading-bar` a real knob on its start scale — it had none before, unlike
|
|
4303
|
+
// `flip-face`'s `angle:` two rows up. `--kui-bar-from` is also what the fold-panel-style
|
|
4304
|
+
// `[data-kui-fx~='loading-bar'][data-kui-state='ready']` gate neutralizes in three-d.css; see
|
|
4305
|
+
// that rule's comment for the on:enter fix this parameter doubles as.
|
|
4306
|
+
cssPrimitive("bar", [CHANNEL.scale], {
|
|
4307
|
+
parameters: { from: { type: "number", default: "0", cssProperty: "--kui-bar-from" } }
|
|
4308
|
+
}),
|
|
4309
|
+
CARD_TOGGLE_PRIMITIVE
|
|
3344
4310
|
];
|
|
3345
4311
|
var THREE_D_PRESETS = [
|
|
3346
4312
|
// --- 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" },
|
|
4313
|
+
{ name: "card-flip-y", primitive: "flip-face", keyframes: "kui-card-flip-y", cloak: true },
|
|
4314
|
+
{ name: "card-flip-x", primitive: "flip-face", keyframes: "kui-card-flip-x", cloak: true },
|
|
3349
4315
|
{ name: "cube-rotate", primitive: "flip-face", keyframes: "kui-cube-rotate", params: { angle: "90deg" } },
|
|
3350
4316
|
{
|
|
3351
4317
|
name: "book-page-turn",
|
|
@@ -3353,17 +4319,32 @@ var THREE_D_PRESETS = [
|
|
|
3353
4319
|
keyframes: "kui-book-page-turn",
|
|
3354
4320
|
params: { angle: "-160deg", duration: "900ms" }
|
|
3355
4321
|
},
|
|
4322
|
+
// `cloak: true`, unlike its `flip-face` siblings above: those are `to`-only keyframes, so their
|
|
4323
|
+
// paused/waiting box is the ordinary, untransformed rest state. `fold-panel` is `from`-only —
|
|
4324
|
+
// its `rotateX(-90deg)` (three-d.css) *is* the paused box, edge-on and zero-height, so it holds
|
|
4325
|
+
// no space in layout for the whole wait; see the
|
|
4326
|
+
// `[data-kui-fx~='fold-panel'][data-kui-state='ready']` rule in three-d.css for the other half
|
|
4327
|
+
// of that fix. `cloak` only ever hid the pre-JS flash, not this, but adding it here keeps the
|
|
4328
|
+
// pre-JS and post-JS "ready" appearances the same (invisible) instead of trading one flash for
|
|
4329
|
+
// the other.
|
|
3356
4330
|
{
|
|
3357
4331
|
name: "fold-panel",
|
|
3358
4332
|
primitive: "flip-face",
|
|
3359
4333
|
keyframes: "kui-fold-panel",
|
|
3360
|
-
params: { angle: "-90deg" }
|
|
4334
|
+
params: { angle: "-90deg" },
|
|
4335
|
+
cloak: true
|
|
3361
4336
|
},
|
|
3362
4337
|
// --- page transitions ---
|
|
3363
4338
|
{ name: "page-fade", primitive: "page-reveal", keyframes: "kui-page-fade" },
|
|
3364
4339
|
{ name: "page-slide", primitive: "page-reveal", keyframes: "kui-page-slide" },
|
|
3365
4340
|
{ name: "curtain-wipe", primitive: "wipe", keyframes: "kui-curtain-wipe", params: { duration: "800ms" } },
|
|
3366
|
-
|
|
4341
|
+
// `cloak: true` for the same reason as `fold-panel`: `kui-loading-bar`'s `from { scale: 0 1 }`
|
|
4342
|
+
// (three-d.css) is a zero-width box, not just an invisible one — see that file's
|
|
4343
|
+
// `[data-kui-fx~='loading-bar'][data-kui-state='ready']` rule.
|
|
4344
|
+
{ name: "loading-bar", primitive: "bar", keyframes: "kui-loading-bar", cloak: true },
|
|
4345
|
+
// No `keyframes`: its motion is a CSS transition in three-d.css keyed off the control's
|
|
4346
|
+
// aria-pressed, not a compiled animation. Same shape as the icon toggles in svg.ts.
|
|
4347
|
+
{ name: "flip-card", primitive: "card-toggle" }
|
|
3367
4348
|
];
|
|
3368
4349
|
function registerThreeD(registry) {
|
|
3369
4350
|
return registry.registerPrimitives(THREE_D_PRIMITIVES).registerPresets(THREE_D_PRESETS);
|