@konce-pt/motion 0.0.0-stage → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -2
- package/dist/config.d.ts +71 -0
- package/dist/config.js +54 -0
- package/dist/dom.d.ts +86 -0
- package/dist/dom.js +210 -0
- package/dist/flow-diff.d.ts +7 -0
- package/dist/flow-diff.js +17 -0
- package/dist/flow.d.ts +22 -0
- package/dist/flow.js +84 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +13 -0
- package/dist/keyed.d.ts +64 -0
- package/dist/keyed.js +221 -0
- package/dist/layout.d.ts +65 -0
- package/dist/layout.js +115 -0
- package/dist/loader-dom.d.ts +48 -0
- package/dist/loader-dom.js +821 -0
- package/dist/loader.d.ts +177 -0
- package/dist/loader.js +400 -0
- package/dist/spring.d.ts +16 -0
- package/dist/spring.js +41 -0
- package/dist/stepper-dom.d.ts +9 -0
- package/dist/stepper-dom.js +44 -0
- package/dist/stepper.d.ts +51 -0
- package/dist/stepper.js +60 -0
- package/dist/swap.d.ts +13 -0
- package/dist/swap.js +61 -0
- package/package.json +44 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { springProgress, springEasing } from './spring.ts';
|
|
2
|
+
export { planLayoutMotion, layoutMotionDuration } from './layout.ts';
|
|
3
|
+
export type { KptLayoutMotion, KptRect, KptLayoutFrame, KptLayoutInnerFrame, KptLayoutFlight, KptLayoutPlanOptions, } from './layout.ts';
|
|
4
|
+
export { layoutMotionAllowed, layoutMorphSupported, snapshotLayout, stopLayoutMotion, playLayoutMotion, morphLayout, } from './dom.ts';
|
|
5
|
+
export type { KptLayoutSnapshot, KptPlayLayoutOptions } from './dom.ts';
|
|
6
|
+
export { applyMotionConfig, playEnterMotion, playShakeMotion, playPopMotion, playInvalidMotion, playIndicatorMotion, } from './dom.ts';
|
|
7
|
+
export type { KptEnterFrom } from './dom.ts';
|
|
8
|
+
export { resolveLayoutMotion, resolveCollapseMotion, resolveEnterMotion, resolveToggleMotion, KPT_DEFAULT_TOGGLE_MOTION, KPT_DEFAULT_ENTER_MOTION, motionRootStyle, KPT_DEFAULT_LAYOUT_MOTION, KPT_DEFAULT_COLLAPSE_MOTION, } from './config.ts';
|
|
9
|
+
export type { KptMotionConfig, KptMotionRootStyle, KptCollapseMotion, KptEnterMotion, KptToggleMotion } from './config.ts';
|
|
10
|
+
export { snapshotKeyed, playKeyedMotion, stopKeyedMotion, morphKeyed, isVisibleInScroller, revealInScroller, } from './keyed.ts';
|
|
11
|
+
export type { KptKeyedNode, KptKeyedSnapshot, KptKeyedClasses, KptKeyedMotionOptions } from './keyed.ts';
|
|
12
|
+
export { prepareFlowMotion } from './flow.ts';
|
|
13
|
+
export type { KptFlowChange } from './flow.ts';
|
|
14
|
+
export { flowChangeOf } from './flow-diff.ts';
|
|
15
|
+
export { stepperSchedule, stepperScheduleOptions } from './stepper.ts';
|
|
16
|
+
export type { KptStepperSchedule, KptStepperScheduleOptions, KptStepTiming } from './stepper.ts';
|
|
17
|
+
export { preparePanelSwap } from './swap.ts';
|
|
18
|
+
export { playStepperLight } from './stepper-dom.ts';
|
|
19
|
+
export { KPT_LOADER_VARIANTS, LOADER_PALETTE, LOADER_VIEWBOX, LOADER_RESULT_MS, LOADER_QUIET_MS, cubicBezier, loaderProgress, loaderColor, loaderOnColor, loaderSubtle, loaderPalette, } from './loader.ts';
|
|
20
|
+
export type { KptLoaderVariant, KptLoaderStatus, KptLoaderResult, KptLoaderRole, KptLoaderColor } from './loader.ts';
|
|
21
|
+
export { createLoader, swapText, setText } from './loader-dom.ts';
|
|
22
|
+
export type { KptLoaderOptions, KptLoaderController } from './loader-dom.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { springProgress, springEasing } from "./spring.js";
|
|
2
|
+
export { planLayoutMotion, layoutMotionDuration } from "./layout.js";
|
|
3
|
+
export { layoutMotionAllowed, layoutMorphSupported, snapshotLayout, stopLayoutMotion, playLayoutMotion, morphLayout, } from "./dom.js";
|
|
4
|
+
export { applyMotionConfig, playEnterMotion, playShakeMotion, playPopMotion, playInvalidMotion, playIndicatorMotion, } from "./dom.js";
|
|
5
|
+
export { resolveLayoutMotion, resolveCollapseMotion, resolveEnterMotion, resolveToggleMotion, KPT_DEFAULT_TOGGLE_MOTION, KPT_DEFAULT_ENTER_MOTION, motionRootStyle, KPT_DEFAULT_LAYOUT_MOTION, KPT_DEFAULT_COLLAPSE_MOTION, } from "./config.js";
|
|
6
|
+
export { snapshotKeyed, playKeyedMotion, stopKeyedMotion, morphKeyed, isVisibleInScroller, revealInScroller, } from "./keyed.js";
|
|
7
|
+
export { prepareFlowMotion } from "./flow.js";
|
|
8
|
+
export { flowChangeOf } from "./flow-diff.js";
|
|
9
|
+
export { stepperSchedule, stepperScheduleOptions } from "./stepper.js";
|
|
10
|
+
export { preparePanelSwap } from "./swap.js";
|
|
11
|
+
export { playStepperLight } from "./stepper-dom.js";
|
|
12
|
+
export { KPT_LOADER_VARIANTS, LOADER_PALETTE, LOADER_VIEWBOX, LOADER_RESULT_MS, LOADER_QUIET_MS, cubicBezier, loaderProgress, loaderColor, loaderOnColor, loaderSubtle, loaderPalette, } from "./loader.js";
|
|
13
|
+
export { createLoader, swapText, setText } from "./loader-dom.js";
|
package/dist/keyed.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type KptRect } from './layout.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Ruch list z kluczem - elementy rozpoznawane po danych, nie po pozycji w DOM. Komponenty śledzą
|
|
4
|
+
* wiersze po indeksie, więc po przeniesieniu ten sam `<li>` pokazuje już inny element; dlatego
|
|
5
|
+
* stan przed zmianą zapamiętujemy pod kluczem (referencją elementu danych), a po renderze
|
|
6
|
+
* szukamy węzła, który ten klucz pokazuje.
|
|
7
|
+
*
|
|
8
|
+
* Przeskok między dwoma przewijanymi pojemnikami (np. dwa panele `kpt-pick-list`) leci jako
|
|
9
|
+
* duch - klon w warstwie nad całym komponentem - bo prawdziwy wiersz przyciąłby `overflow`
|
|
10
|
+
* panelu. Przesunięcia wewnątrz jednego pojemnika to zwykły FLIP prawdziwych węzłów.
|
|
11
|
+
*/
|
|
12
|
+
/** Węzeł listy i klucz elementu danych, który pokazuje. */
|
|
13
|
+
export interface KptKeyedNode<K> {
|
|
14
|
+
key: K;
|
|
15
|
+
element: HTMLElement;
|
|
16
|
+
}
|
|
17
|
+
interface KeyedEntry {
|
|
18
|
+
rect: KptRect;
|
|
19
|
+
visible: boolean;
|
|
20
|
+
scroller: HTMLElement | null;
|
|
21
|
+
}
|
|
22
|
+
/** Stan list przed zmianą: prostokąt (względem korzenia), widoczność i pojemnik pod kluczem. */
|
|
23
|
+
export type KptKeyedSnapshot<K> = Map<K, KeyedEntry>;
|
|
24
|
+
export interface KptKeyedClasses {
|
|
25
|
+
/** Klon lecący między pojemnikami. */
|
|
26
|
+
ghostClass: string;
|
|
27
|
+
/** Prawdziwy węzeł czekający, aż duch wyląduje. */
|
|
28
|
+
hiddenClass: string;
|
|
29
|
+
/** Węzeł przesuwany wewnątrz pojemnika (np. `position: relative; z-index`). */
|
|
30
|
+
movingClass: string;
|
|
31
|
+
}
|
|
32
|
+
export interface KptKeyedMotionOptions extends KptKeyedClasses {
|
|
33
|
+
/** Ruch duchów między pojemnikami; przesunięcia wewnątrz pojemnika są zawsze sprężyną. */
|
|
34
|
+
motion: 'spring' | 'arc';
|
|
35
|
+
/** Przewijany pojemnik węzła - widoczność i przeskok liczymy względem niego. */
|
|
36
|
+
scrollerOf: (element: HTMLElement) => HTMLElement | null;
|
|
37
|
+
/**
|
|
38
|
+
* Klucze elementów przestawianych przez użytkownika (główna akcja) w obrębie pojemnika - lecą
|
|
39
|
+
* w materiale `motion` i nad sąsiadami; reszta robi im miejsce spokojną sprężyną.
|
|
40
|
+
*/
|
|
41
|
+
lead?: ReadonlySet<unknown>;
|
|
42
|
+
}
|
|
43
|
+
/** Czy węzeł jest widoczny w swoim przewijanym pojemniku (choćby częściowo). */
|
|
44
|
+
export declare function isVisibleInScroller(element: HTMLElement, scroller: HTMLElement | null): boolean;
|
|
45
|
+
/** Przewija pojemnik tak, żeby węzeł był w nim widoczny (bez przewijania strony). */
|
|
46
|
+
export declare function revealInScroller(element: HTMLElement, scroller: HTMLElement | null): void;
|
|
47
|
+
/** Mierzy węzły przed zmianą. Węzeł czekający na ducha bierze pozycję ducha z ekranu. */
|
|
48
|
+
export declare function snapshotKeyed<K>(root: HTMLElement, nodes: readonly KptKeyedNode<K>[], scrollerOf: (element: HTMLElement) => HTMLElement | null): KptKeyedSnapshot<K>;
|
|
49
|
+
/** Przerywa trwający ruch: animacje, duchy i klasy stanu. Wołać PO pomiarze. */
|
|
50
|
+
export declare function stopKeyedMotion(root: HTMLElement, classes: KptKeyedClasses): void;
|
|
51
|
+
/**
|
|
52
|
+
* Gra ruch od `first` do bieżącego (wyrenderowanego) stanu list. Węzeł, który zmienił pojemnik,
|
|
53
|
+
* leci jako duch (gdy był widoczny przed i jest widoczny po); węzeł w tym samym pojemniku
|
|
54
|
+
* przesuwa się sam. Klucze nieznane w `first` (nowe dane) się nie ruszają. Zwraca czas w ms.
|
|
55
|
+
*/
|
|
56
|
+
export declare function playKeyedMotion<K>(root: HTMLElement, first: KptKeyedSnapshot<K>, nodes: readonly KptKeyedNode<K>[], options: KptKeyedMotionOptions, originEl?: Element | null): number;
|
|
57
|
+
/**
|
|
58
|
+
* Natywny morf (View Transitions) list z kluczem. Nazwy idą za danymi: przed zmianą dostają je
|
|
59
|
+
* węzły pokazujące dany klucz, w środku przejścia - po `update()` - węzły, które pokazują go
|
|
60
|
+
* teraz. Przejście rysuje się nad stroną, więc `overflow` paneli go nie przycina. `include`
|
|
61
|
+
* zawęża nazwy do widocznych węzłów (przewinięte poza panel nie mają skąd i dokąd lecieć).
|
|
62
|
+
*/
|
|
63
|
+
export declare function morphKeyed<K>(before: readonly KptKeyedNode<K>[], after: () => readonly KptKeyedNode<K>[], namePrefix: string, className: string, update: () => void, include?: (element: HTMLElement) => boolean): void;
|
|
64
|
+
export {};
|
package/dist/keyed.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { layoutMorphSupported } from "./dom.js";
|
|
2
|
+
import { layoutMotionDuration, planLayoutMotion } from "./layout.js";
|
|
3
|
+
const ANIMATION_ID = 'kpt-keyed-motion';
|
|
4
|
+
/** Duch w locie dla prawdziwego węzła - przerwany lot startuje od miejsca ducha na ekranie. */
|
|
5
|
+
const ghostOf = new WeakMap();
|
|
6
|
+
/** Czy węzeł jest widoczny w swoim przewijanym pojemniku (choćby częściowo). */
|
|
7
|
+
export function isVisibleInScroller(element, scroller) {
|
|
8
|
+
return intersects(element.getBoundingClientRect(), scroller);
|
|
9
|
+
}
|
|
10
|
+
/** Przewija pojemnik tak, żeby węzeł był w nim widoczny (bez przewijania strony). */
|
|
11
|
+
export function revealInScroller(element, scroller) {
|
|
12
|
+
if (!scroller)
|
|
13
|
+
return;
|
|
14
|
+
const s = scroller.getBoundingClientRect();
|
|
15
|
+
const r = element.getBoundingClientRect();
|
|
16
|
+
if (r.bottom > s.bottom)
|
|
17
|
+
scroller.scrollTop += r.bottom - s.bottom;
|
|
18
|
+
else if (r.top < s.top)
|
|
19
|
+
scroller.scrollTop -= s.top - r.top;
|
|
20
|
+
}
|
|
21
|
+
/** Mierzy węzły przed zmianą. Węzeł czekający na ducha bierze pozycję ducha z ekranu. */
|
|
22
|
+
export function snapshotKeyed(root, nodes, scrollerOf) {
|
|
23
|
+
const box = root.getBoundingClientRect();
|
|
24
|
+
const map = new Map();
|
|
25
|
+
for (const { key, element } of nodes) {
|
|
26
|
+
if (map.has(key))
|
|
27
|
+
continue;
|
|
28
|
+
const ghost = ghostOf.get(element);
|
|
29
|
+
const live = ghost?.isConnected ? ghost : element;
|
|
30
|
+
const scroller = scrollerOf(element);
|
|
31
|
+
const r = live.getBoundingClientRect();
|
|
32
|
+
map.set(key, { rect: relative(r, box), visible: live !== element || intersects(r, scroller), scroller });
|
|
33
|
+
}
|
|
34
|
+
return map;
|
|
35
|
+
}
|
|
36
|
+
/** Przerywa trwający ruch: animacje, duchy i klasy stanu. Wołać PO pomiarze. */
|
|
37
|
+
export function stopKeyedMotion(root, classes) {
|
|
38
|
+
for (const a of root.getAnimations({ subtree: true }))
|
|
39
|
+
if (a.id === ANIMATION_ID)
|
|
40
|
+
a.cancel();
|
|
41
|
+
for (const ghost of root.querySelectorAll(`.${classes.ghostClass}`))
|
|
42
|
+
ghost.remove();
|
|
43
|
+
for (const el of root.querySelectorAll(`.${classes.hiddenClass}, .${classes.movingClass}`)) {
|
|
44
|
+
el.classList.remove(classes.hiddenClass, classes.movingClass);
|
|
45
|
+
el.style.zIndex = '';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Gra ruch od `first` do bieżącego (wyrenderowanego) stanu list. Węzeł, który zmienił pojemnik,
|
|
50
|
+
* leci jako duch (gdy był widoczny przed i jest widoczny po); węzeł w tym samym pojemniku
|
|
51
|
+
* przesuwa się sam. Klucze nieznane w `first` (nowe dane) się nie ruszają. Zwraca czas w ms.
|
|
52
|
+
*/
|
|
53
|
+
export function playKeyedMotion(root, first, nodes, options, originEl) {
|
|
54
|
+
const box = root.getBoundingClientRect();
|
|
55
|
+
const direct = [];
|
|
56
|
+
const leads = [];
|
|
57
|
+
const ghosts = [];
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
for (const { key, element } of nodes) {
|
|
60
|
+
const before = first.get(key);
|
|
61
|
+
if (!before || seen.has(key))
|
|
62
|
+
continue;
|
|
63
|
+
seen.add(key);
|
|
64
|
+
const scroller = options.scrollerOf(element);
|
|
65
|
+
const r = element.getBoundingClientRect();
|
|
66
|
+
const move = { element, from: before.rect, to: relative(r, box) };
|
|
67
|
+
if (before.scroller !== scroller) {
|
|
68
|
+
if (before.visible && intersects(r, scroller))
|
|
69
|
+
ghosts.push(move);
|
|
70
|
+
}
|
|
71
|
+
else if (before.visible || intersects(r, scroller)) {
|
|
72
|
+
(options.lead?.has(key) ? leads : direct).push(move);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const sample = nodes[0]?.element;
|
|
76
|
+
const radius = sample ? Number.parseFloat(getComputedStyle(sample).borderTopLeftRadius) || 0 : 0;
|
|
77
|
+
const scrim = getComputedStyle(root).getPropertyValue('--kpt-color-overlay-scrim').trim();
|
|
78
|
+
let origin;
|
|
79
|
+
if (originEl) {
|
|
80
|
+
const o = originEl.getBoundingClientRect();
|
|
81
|
+
origin = { x: o.left - box.left + o.width / 2, y: o.top - box.top + o.height / 2 };
|
|
82
|
+
}
|
|
83
|
+
// Wtórny ruch: sąsiedzi domykają lukę i robią miejsce - zawsze spokojna sprężyna.
|
|
84
|
+
const directFlights = planLayoutMotion(direct.map((m) => m.from), direct.map((m) => m.to), { motion: 'spring', resize: false, radius, stagger: 12 });
|
|
85
|
+
for (const flight of directFlights) {
|
|
86
|
+
const move = direct[flight.index];
|
|
87
|
+
if (!move)
|
|
88
|
+
continue;
|
|
89
|
+
move.element.classList.add(options.movingClass);
|
|
90
|
+
const anim = move.element.animate(frames(flight), timing(flight));
|
|
91
|
+
anim.finished.then(() => {
|
|
92
|
+
if (!move.element.getAnimations().some((a) => a.id === ANIMATION_ID && a.playState === 'running')) {
|
|
93
|
+
move.element.classList.remove(options.movingClass);
|
|
94
|
+
}
|
|
95
|
+
}, () => undefined);
|
|
96
|
+
}
|
|
97
|
+
// Główna akcja w pojemniku: przestawiany element leci w wybranym materiale, nad sąsiadami.
|
|
98
|
+
const shadowColor = scrim ? { shadowColor: `color-mix(in srgb, ${scrim} 55%, transparent)` } : {};
|
|
99
|
+
const leadFlights = planLayoutMotion(leads.map((m) => m.from), leads.map((m) => m.to), { motion: options.motion, resize: false, radius, ...(origin ? { origin } : {}), ...shadowColor });
|
|
100
|
+
for (const flight of leadFlights) {
|
|
101
|
+
const move = leads[flight.index];
|
|
102
|
+
if (!move)
|
|
103
|
+
continue;
|
|
104
|
+
move.element.classList.add(options.movingClass);
|
|
105
|
+
move.element.style.zIndex = '2';
|
|
106
|
+
const land = () => {
|
|
107
|
+
if (move.element.getAnimations().some((a) => a.id === ANIMATION_ID && a.playState === 'running'))
|
|
108
|
+
return;
|
|
109
|
+
move.element.classList.remove(options.movingClass);
|
|
110
|
+
move.element.style.zIndex = '';
|
|
111
|
+
};
|
|
112
|
+
move.element.animate(frames(flight), timing(flight)).finished.then(land, land);
|
|
113
|
+
}
|
|
114
|
+
// Główna akcja między pojemnikami: duchy lecą w wybranym materiale.
|
|
115
|
+
const ghostFlights = planLayoutMotion(ghosts.map((m) => m.from), ghosts.map((m) => m.to), {
|
|
116
|
+
motion: options.motion,
|
|
117
|
+
resize: false,
|
|
118
|
+
radius,
|
|
119
|
+
stagger: 40,
|
|
120
|
+
...(origin ? { origin } : {}),
|
|
121
|
+
...shadowColor,
|
|
122
|
+
});
|
|
123
|
+
for (const flight of ghostFlights) {
|
|
124
|
+
const move = ghosts[flight.index];
|
|
125
|
+
if (!move)
|
|
126
|
+
continue;
|
|
127
|
+
const ghost = makeGhost(move.element, move.to, options);
|
|
128
|
+
root.appendChild(ghost);
|
|
129
|
+
ghostOf.set(move.element, ghost);
|
|
130
|
+
move.element.classList.add(options.hiddenClass);
|
|
131
|
+
const land = () => {
|
|
132
|
+
ghost.remove();
|
|
133
|
+
if (ghostOf.get(move.element) === ghost) {
|
|
134
|
+
ghostOf.delete(move.element);
|
|
135
|
+
move.element.classList.remove(options.hiddenClass);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
ghost.animate(frames(flight), timing(flight)).finished.then(land, land);
|
|
139
|
+
}
|
|
140
|
+
return Math.max(layoutMotionDuration(directFlights), layoutMotionDuration(leadFlights), layoutMotionDuration(ghostFlights));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Natywny morf (View Transitions) list z kluczem. Nazwy idą za danymi: przed zmianą dostają je
|
|
144
|
+
* węzły pokazujące dany klucz, w środku przejścia - po `update()` - węzły, które pokazują go
|
|
145
|
+
* teraz. Przejście rysuje się nad stroną, więc `overflow` paneli go nie przycina. `include`
|
|
146
|
+
* zawęża nazwy do widocznych węzłów (przewinięte poza panel nie mają skąd i dokąd lecieć).
|
|
147
|
+
*/
|
|
148
|
+
export function morphKeyed(before, after, namePrefix, className, update, include = () => true) {
|
|
149
|
+
if (!layoutMorphSupported()) {
|
|
150
|
+
update();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const ids = new Map();
|
|
154
|
+
const named = [];
|
|
155
|
+
const name = (element, id) => {
|
|
156
|
+
element.style.viewTransitionName = id;
|
|
157
|
+
element.style.viewTransitionClass = className;
|
|
158
|
+
named.push(element);
|
|
159
|
+
};
|
|
160
|
+
const clear = () => {
|
|
161
|
+
for (const el of named.splice(0)) {
|
|
162
|
+
el.style.viewTransitionName = '';
|
|
163
|
+
el.style.viewTransitionClass = '';
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
before.forEach(({ key, element }, i) => {
|
|
167
|
+
if (ids.has(key) || !include(element))
|
|
168
|
+
return;
|
|
169
|
+
const id = `${namePrefix}-${i}`;
|
|
170
|
+
ids.set(key, id);
|
|
171
|
+
name(element, id);
|
|
172
|
+
});
|
|
173
|
+
document
|
|
174
|
+
.startViewTransition(() => {
|
|
175
|
+
clear();
|
|
176
|
+
update();
|
|
177
|
+
const used = new Set();
|
|
178
|
+
for (const { key, element } of after()) {
|
|
179
|
+
const id = ids.get(key);
|
|
180
|
+
if (!id || used.has(id) || !include(element))
|
|
181
|
+
continue;
|
|
182
|
+
used.add(id);
|
|
183
|
+
name(element, id);
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
.finished.then(clear, clear);
|
|
187
|
+
}
|
|
188
|
+
function makeGhost(source, at, options) {
|
|
189
|
+
const ghost = source.cloneNode(true);
|
|
190
|
+
for (const attr of ['id', 'role', 'tabindex', 'aria-selected', 'aria-label', 'draggable'])
|
|
191
|
+
ghost.removeAttribute(attr);
|
|
192
|
+
ghost.setAttribute('aria-hidden', 'true');
|
|
193
|
+
ghost.classList.remove(options.hiddenClass, options.movingClass);
|
|
194
|
+
ghost.classList.add(options.ghostClass);
|
|
195
|
+
Object.assign(ghost.style, {
|
|
196
|
+
position: 'absolute',
|
|
197
|
+
left: `${at.x}px`,
|
|
198
|
+
top: `${at.y}px`,
|
|
199
|
+
width: `${at.width}px`,
|
|
200
|
+
height: `${at.height}px`,
|
|
201
|
+
margin: '0',
|
|
202
|
+
boxSizing: 'border-box',
|
|
203
|
+
pointerEvents: 'none',
|
|
204
|
+
});
|
|
205
|
+
return ghost;
|
|
206
|
+
}
|
|
207
|
+
function frames(flight) {
|
|
208
|
+
return flight.item.map((f) => ({ ...f }));
|
|
209
|
+
}
|
|
210
|
+
function timing(flight) {
|
|
211
|
+
return { id: ANIMATION_ID, duration: flight.duration, delay: flight.delay, easing: 'linear', fill: 'backwards' };
|
|
212
|
+
}
|
|
213
|
+
function relative(r, box) {
|
|
214
|
+
return { x: r.left - box.left, y: r.top - box.top, width: r.width, height: r.height };
|
|
215
|
+
}
|
|
216
|
+
function intersects(r, scroller) {
|
|
217
|
+
if (!scroller)
|
|
218
|
+
return true;
|
|
219
|
+
const s = scroller.getBoundingClientRect();
|
|
220
|
+
return r.bottom > s.top + 1 && r.top < s.bottom - 1 && r.right > s.left && r.left < s.right;
|
|
221
|
+
}
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styl ruchu przy zmianie układu: bez ruchu, sprężyna, lot po łuku z głębią albo natywny morf
|
|
3
|
+
* przeglądarki (View Transitions). Klatki liczy ten moduł tylko dla `spring` i `arc` - morf
|
|
4
|
+
* robi przeglądarka ze zrzutów starego i nowego stanu.
|
|
5
|
+
*/
|
|
6
|
+
export type KptLayoutMotion = 'none' | 'spring' | 'arc' | 'morph';
|
|
7
|
+
/** Prostokąt elementu względem wspólnego kontenera (lewy górny róg + rozmiar). */
|
|
8
|
+
export interface KptRect {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
/** Klatka elementu: przesunięcie i skala względem pozycji końcowej oraz wyrównany promień rogu. */
|
|
15
|
+
export interface KptLayoutFrame {
|
|
16
|
+
transform: string;
|
|
17
|
+
borderRadius: string;
|
|
18
|
+
boxShadow?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Klatka wnętrza: odwrotność skali elementu, żeby treść się nie rozciągała. */
|
|
21
|
+
export interface KptLayoutInnerFrame {
|
|
22
|
+
transform: string;
|
|
23
|
+
}
|
|
24
|
+
/** Lot jednego elementu - gotowe tablice klatek do `element.animate(frames, { easing: 'linear' })`. */
|
|
25
|
+
export interface KptLayoutFlight {
|
|
26
|
+
/** Indeks elementu w tablicach `first` / `last`. */
|
|
27
|
+
index: number;
|
|
28
|
+
delay: number;
|
|
29
|
+
duration: number;
|
|
30
|
+
item: KptLayoutFrame[];
|
|
31
|
+
inner: KptLayoutInnerFrame[];
|
|
32
|
+
}
|
|
33
|
+
export interface KptLayoutPlanOptions {
|
|
34
|
+
motion: 'spring' | 'arc';
|
|
35
|
+
/** Promień rogu elementu w px - wyrównywany co klatkę, żeby skala go nie spłaszczyła. */
|
|
36
|
+
radius?: number;
|
|
37
|
+
/** Skąd rusza fala w `arc` (zwykle środek klikniętego przycisku); domyślnie prawy górny róg. */
|
|
38
|
+
origin?: {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
};
|
|
42
|
+
/** Kolor cienia unoszonej karty w `arc`; bez niego cienia nie ma. */
|
|
43
|
+
shadowColor?: string;
|
|
44
|
+
/** Liczba klatek na lot (domyślnie 36 - przy `linear` między nimi to wystarcza na 60 kl./s). */
|
|
45
|
+
frames?: number;
|
|
46
|
+
/**
|
|
47
|
+
* `false` - lot tylko przesuwa, bez zmiany rozmiaru (wiersze listy, które mają tę samą
|
|
48
|
+
* szerokość po obu stronach; bez wrappera do kontr-skali tekst rozciągałby się o piksel).
|
|
49
|
+
*/
|
|
50
|
+
resize?: boolean;
|
|
51
|
+
/** Kaskada `spring` w ms na element (domyślnie 22); całość przycięta do 300 ms. */
|
|
52
|
+
stagger?: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Plan FLIP dla zmiany układu: z prostokątów przed zmianą (`first`, najlepiej zmierzonych
|
|
56
|
+
* z ekranu, razem z trwającym transformem - wtedy przerwany ruch zaczyna od miejsca, w którym
|
|
57
|
+
* element jest widoczny) i po zmianie (`last`, bez transformów) liczy klatki elementu i jego
|
|
58
|
+
* wnętrza. Element gra od starej pozycji do `transform: none`; wnętrze dostaje odwrotną skalę.
|
|
59
|
+
*
|
|
60
|
+
* `transform-origin` obu musi być `0 0`: skala względem środka pokazywałaby w pierwszych
|
|
61
|
+
* klatkach środek treści - przy przejściu siatka -> lista to pusty pas między nazwą a ceną.
|
|
62
|
+
*/
|
|
63
|
+
export declare function planLayoutMotion(first: readonly KptRect[], last: readonly KptRect[], options: KptLayoutPlanOptions): KptLayoutFlight[];
|
|
64
|
+
/** Ile trwa cały plan - do zdjęcia stanu "w ruchu" bez czekania na każdą obietnicę. */
|
|
65
|
+
export declare function layoutMotionDuration(flights: readonly KptLayoutFlight[]): number;
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { round, springProgress } from "./spring.js";
|
|
2
|
+
/** Element, który się nie ruszył (poniżej pół piksela), nie dostaje lotu. */
|
|
3
|
+
const STILL = 0.5;
|
|
4
|
+
const SPRING = springProgress(0.32);
|
|
5
|
+
const SOFT = springProgress(0.18);
|
|
6
|
+
/**
|
|
7
|
+
* Plan FLIP dla zmiany układu: z prostokątów przed zmianą (`first`, najlepiej zmierzonych
|
|
8
|
+
* z ekranu, razem z trwającym transformem - wtedy przerwany ruch zaczyna od miejsca, w którym
|
|
9
|
+
* element jest widoczny) i po zmianie (`last`, bez transformów) liczy klatki elementu i jego
|
|
10
|
+
* wnętrza. Element gra od starej pozycji do `transform: none`; wnętrze dostaje odwrotną skalę.
|
|
11
|
+
*
|
|
12
|
+
* `transform-origin` obu musi być `0 0`: skala względem środka pokazywałaby w pierwszych
|
|
13
|
+
* klatkach środek treści - przy przejściu siatka -> lista to pusty pas między nazwą a ceną.
|
|
14
|
+
*/
|
|
15
|
+
export function planLayoutMotion(first, last, options) {
|
|
16
|
+
const count = Math.min(first.length, last.length);
|
|
17
|
+
const frames = Math.max(2, Math.round(options.frames ?? 36));
|
|
18
|
+
const origin = options.origin ?? { x: Math.max(0, ...last.map((r) => r.x + r.width)), y: 0 };
|
|
19
|
+
const flights = [];
|
|
20
|
+
for (let i = 0; i < count; i++) {
|
|
21
|
+
const f = first[i];
|
|
22
|
+
const l = last[i];
|
|
23
|
+
if (!f || !l || l.width <= 0 || l.height <= 0 || isStill(f, l))
|
|
24
|
+
continue;
|
|
25
|
+
const from = options.resize === false ? { ...f, width: l.width, height: l.height } : f;
|
|
26
|
+
const course = options.motion === 'spring' ? springCourse(i, from, l, options.stagger ?? 22) : arcCourse(from, l, origin);
|
|
27
|
+
flights.push({ index: i, ...sample(course, frames, options), delay: Math.round(course.delay), duration: course.duration });
|
|
28
|
+
}
|
|
29
|
+
return flights;
|
|
30
|
+
}
|
|
31
|
+
/** Sprężyna po prostej z lekką kaskadą od pierwszego elementu. */
|
|
32
|
+
function springCourse(i, f, l, stagger) {
|
|
33
|
+
const dx = f.x - l.x;
|
|
34
|
+
const dy = f.y - l.y;
|
|
35
|
+
const fsx = f.width / l.width;
|
|
36
|
+
const fsy = f.height / l.height;
|
|
37
|
+
return {
|
|
38
|
+
delay: Math.min(300, i * stagger),
|
|
39
|
+
duration: 640,
|
|
40
|
+
state: (q) => {
|
|
41
|
+
const p = SPRING(q);
|
|
42
|
+
return { x: dx * (1 - p), y: dy * (1 - p), sx: lerp(fsx, 1, p), sy: lerp(fsy, 1, p), rotate: 0, lift: 0 };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Lot po łuku: karta unosi się (lekkie powiększenie, cień), leci łukiem z przechyłem w stronę
|
|
48
|
+
* ruchu i ląduje na miękkiej sprężynie. Fala startuje od `origin` - bliższe karty ruszają pierwsze.
|
|
49
|
+
*/
|
|
50
|
+
function arcCourse(f, l, origin) {
|
|
51
|
+
const dx = f.x - l.x;
|
|
52
|
+
const dy = f.y - l.y;
|
|
53
|
+
const fsx = f.width / l.width;
|
|
54
|
+
const fsy = f.height / l.height;
|
|
55
|
+
const dist = Math.hypot(f.x + f.width / 2 - origin.x, f.y + f.height / 2 - origin.y);
|
|
56
|
+
const arc = Math.min(46, 12 + Math.hypot(dx, dy) * 0.12);
|
|
57
|
+
const tilt = clamp(-dx / 55, -7, 7);
|
|
58
|
+
return {
|
|
59
|
+
delay: Math.min(300, dist * 0.55),
|
|
60
|
+
duration: 780,
|
|
61
|
+
state: (q) => {
|
|
62
|
+
const p = SOFT(q);
|
|
63
|
+
const lift = Math.sin(Math.PI * Math.min(1, q * 1.25));
|
|
64
|
+
const m = 1 + 0.05 * lift;
|
|
65
|
+
return { x: dx * (1 - p), y: dy * (1 - p) - arc * lift, sx: lerp(fsx, 1, p) * m, sy: lerp(fsy, 1, p) * m, rotate: tilt * lift, lift };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Próbkuje stan lotu do klatek elementu i wnętrza; ostatnia klatka trafia dokładnie w cel. */
|
|
70
|
+
function sample(course, frames, options) {
|
|
71
|
+
const radius = options.radius ?? 0;
|
|
72
|
+
const shadowColor = options.motion === 'arc' ? options.shadowColor : undefined;
|
|
73
|
+
const item = [];
|
|
74
|
+
const inner = [];
|
|
75
|
+
for (let k = 0; k < frames; k++) {
|
|
76
|
+
const s = course.state(k / frames);
|
|
77
|
+
const sx = Math.max(0.02, s.sx);
|
|
78
|
+
const sy = Math.max(0.02, s.sy);
|
|
79
|
+
const frame = {
|
|
80
|
+
transform: `translate(${round(s.x, 2)}px, ${round(s.y, 2)}px) rotate(${round(s.rotate, 2)}deg) scale(${round(sx, 4)}, ${round(sy, 4)})`,
|
|
81
|
+
borderRadius: `${round(radius / sx, 2)}px / ${round(radius / sy, 2)}px`,
|
|
82
|
+
};
|
|
83
|
+
if (shadowColor)
|
|
84
|
+
frame.boxShadow = shadow(s.lift, shadowColor);
|
|
85
|
+
item.push(frame);
|
|
86
|
+
inner.push({ transform: `scale(${round(1 / sx, 4)}, ${round(1 / sy, 4)})` });
|
|
87
|
+
}
|
|
88
|
+
// Ostatnia klatka dokładnie w celu - inaczej po animacji zostałby ślad zaokrągleń.
|
|
89
|
+
const end = { transform: 'none', borderRadius: `${radius}px` };
|
|
90
|
+
if (shadowColor)
|
|
91
|
+
end.boxShadow = 'none';
|
|
92
|
+
item.push(end);
|
|
93
|
+
inner.push({ transform: 'none' });
|
|
94
|
+
return { item, inner };
|
|
95
|
+
}
|
|
96
|
+
/** Ile trwa cały plan - do zdjęcia stanu "w ruchu" bez czekania na każdą obietnicę. */
|
|
97
|
+
export function layoutMotionDuration(flights) {
|
|
98
|
+
return flights.reduce((max, f) => Math.max(max, f.delay + f.duration), 0);
|
|
99
|
+
}
|
|
100
|
+
function isStill(a, b) {
|
|
101
|
+
return (Math.abs(a.x - b.x) < STILL &&
|
|
102
|
+
Math.abs(a.y - b.y) < STILL &&
|
|
103
|
+
Math.abs(a.width - b.width) < STILL &&
|
|
104
|
+
Math.abs(a.height - b.height) < STILL);
|
|
105
|
+
}
|
|
106
|
+
function shadow(lift, color) {
|
|
107
|
+
const k = Math.max(0, Math.min(1, lift));
|
|
108
|
+
return `0 ${round(4 + 22 * k, 1)}px ${round(10 + 34 * k, 1)}px -10px ${color}`;
|
|
109
|
+
}
|
|
110
|
+
function lerp(a, b, p) {
|
|
111
|
+
return a + (b - a) * p;
|
|
112
|
+
}
|
|
113
|
+
function clamp(v, lo, hi) {
|
|
114
|
+
return Math.max(lo, Math.min(hi, v));
|
|
115
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type KptLoaderColor, type KptLoaderResult, type KptLoaderStatus, type KptLoaderVariant } from './loader.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Warstwa DOM `kpt-loader` - wspólna dla obu portów, bo budowa rysunku, zegar i choreografia
|
|
4
|
+
* wyniku są w Angularze i Reakcie identyczne. Komponent renderuje pusty element sceny, a ten
|
|
5
|
+
* moduł wkłada do niego SVG i gra go w jednej pętli `requestAnimationFrame`.
|
|
6
|
+
*
|
|
7
|
+
* Wszystko, co się rusza, chodzi na własnym zegarze loadera (timery i przejścia też), nie na
|
|
8
|
+
* WAAPI: `speed` przyspiesza pętlę w całości, loader poza ekranem stoi razem ze swoim czasem,
|
|
9
|
+
* a wynik da się przewinąć do końca w jednej chwili (loader, który od razu ma `success`).
|
|
10
|
+
* Bez testów jednostkowych - potrzebuje przeglądarki; liczby siedzą w `loader.ts`.
|
|
11
|
+
*/
|
|
12
|
+
export interface KptLoaderOptions {
|
|
13
|
+
variant: KptLoaderVariant;
|
|
14
|
+
status: KptLoaderStatus;
|
|
15
|
+
/** Postęp 0..1 albo `null` (nieokreślony). Rysują go `ring` i `wave`. */
|
|
16
|
+
progress: number | null;
|
|
17
|
+
/** Barwy pętli (rola motywu albo kolor CSS); `null` = paleta wariantu. */
|
|
18
|
+
colors: readonly KptLoaderColor[] | null;
|
|
19
|
+
successColor: KptLoaderColor;
|
|
20
|
+
errorColor: KptLoaderColor;
|
|
21
|
+
/** Napis w kapsule wyniku (`bridge`); zmiana w trakcie przepisuje go na miejscu. */
|
|
22
|
+
successText: string;
|
|
23
|
+
errorText: string;
|
|
24
|
+
/** Mnożnik tempa pętli (wynik zawsze w swoim czasie). */
|
|
25
|
+
speed: number;
|
|
26
|
+
/** Ruch wyłączony konfiguracją - pętla zwalnia, wynik bez choreografii. */
|
|
27
|
+
reduced: boolean;
|
|
28
|
+
/** Koniec choreografii wyniku. Nie przychodzi, gdy loader od razu ma wynik. */
|
|
29
|
+
onSettled?: (result: KptLoaderResult) => void;
|
|
30
|
+
}
|
|
31
|
+
export interface KptLoaderController {
|
|
32
|
+
update(next: Partial<KptLoaderOptions>): void;
|
|
33
|
+
destroy(): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Wkłada loader do `stage` i zaczyna grać. `content` to element z treścią komponentu (ikona
|
|
37
|
+
* w środku pierścienia) - moduł tylko go skaluje, nigdy nie zmienia jego dzieci.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createLoader(stage: HTMLElement, content: HTMLElement | null, options: KptLoaderOptions): KptLoaderController;
|
|
40
|
+
/**
|
|
41
|
+
* Podmienia napis w `box` z przenikaniem: stary odpływa w górę i rozmywa się, nowy wchodzi z dołu.
|
|
42
|
+
* Pudełko musi trzymać dzieci w jednej komórce siatki (np. `.kpt-update__swap`), żeby na chwilę
|
|
43
|
+
* leżały na sobie. Stary napis od razu dostaje `aria-hidden`. Bez ruchu (`motion: false` albo
|
|
44
|
+
* systemowe "ogranicz ruch") napis zmienia się od razu.
|
|
45
|
+
*/
|
|
46
|
+
export declare function swapText(box: HTMLElement, text: string, motion?: boolean): void;
|
|
47
|
+
/** Zmienia bieżący napis `box` bez przenikania - licznik bajtów w trakcie pobierania. */
|
|
48
|
+
export declare function setText(box: HTMLElement, text: string): void;
|