@konce-pt/motion 0.16.1 → 0.18.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/README.md +25 -10
- package/dist/config.d.ts +1 -1
- package/dist/gallery-dom.d.ts +47 -0
- package/dist/gallery-dom.js +238 -0
- package/dist/gallery.d.ts +44 -0
- package/dist/gallery.js +56 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/zoom-dom.d.ts +30 -0
- package/dist/zoom-dom.js +141 -0
- package/dist/zoom.d.ts +27 -0
- package/dist/zoom.js +20 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://ui.konce.pt/images/koncept_ui_lib.png" alt="Koncept UI
|
|
2
|
+
<img src="https://ui.konce.pt/images/koncept_ui_lib.png" alt="Koncept UI" width="100%" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# @konce-pt/motion
|
|
6
6
|
|
|
7
7
|
Rdzeń ruchu [Koncept UI](https://gitlab.com/konce-pt/koncept-ui): krzywe sprężyn do `linear()`,
|
|
8
8
|
plany lotów FLIP przy zmianie układu, globalna konfiguracja ruchu i mała warstwa DOM, która te loty
|
|
9
|
-
odgrywa
|
|
9
|
+
odgrywa - a także rdzeń `kpt-loader` (sześć wariantów), lot podglądu zdjęcia `kpt-image` i przesuw
|
|
10
|
+
z paskiem miniatur `kpt-galleria`. Czysty TypeScript, **zero zależności runtime**, bez wiązania
|
|
11
|
+
z frameworkiem.
|
|
10
12
|
|
|
11
13
|
Zwykle nie instalujesz go samodzielnie: to zależność `@konce-pt/angular` i `@konce-pt/react`,
|
|
12
14
|
a konfigurację ustawia się przez `provideKptMotion` / `KptMotionProvider` (patrz README portów).
|
|
@@ -34,7 +36,8 @@ const flights = planLayoutMotion(before, after, { motion: 'spring', radius: 8 })
|
|
|
34
36
|
|
|
35
37
|
`KptMotionConfig` ma pięć opcjonalnych kluczy: `enabled` oraz domyślny rodzaj ruchu dla czterech grup
|
|
36
38
|
komponentów - `layout` (`spring | arc | morph | none`), `collapse`, `enter` i `toggle`
|
|
37
|
-
(`spring | fade | none`). Funkcje `resolve*Motion(input, config)`
|
|
39
|
+
(`spring | fade | none`). Funkcje `resolve*Motion(input, config)` (`resolveCollapseMotion` ma trzeci
|
|
40
|
+
argument `animated` - dawny wyłącznik komponentu) rozstrzygają, co dostaje komponent:
|
|
38
41
|
`enabled: false` > input komponentu > wartość z konfiguracji > `spring`. `applyMotionConfig` przy
|
|
39
42
|
`enabled: false` stawia na korzeniu `data-kpt-motion="off"` i zeruje `--kpt-motion-scale` oraz czasy
|
|
40
43
|
`--kpt-motion-duration-*` - tak gaśnie ruch zapisany w samym CSS.
|
|
@@ -42,19 +45,31 @@ komponentów - `layout` (`spring | arc | morph | none`), `collapse`, `enter` i `
|
|
|
42
45
|
## API
|
|
43
46
|
|
|
44
47
|
- **Konfiguracja** - `resolveLayoutMotion`, `resolveCollapseMotion`, `resolveEnterMotion`, `resolveToggleMotion`,
|
|
45
|
-
`motionRootStyle`, stałe `KPT_DEFAULT_*_MOTION`, typy `KptMotionConfig`, `
|
|
46
|
-
`KptCollapseMotion`, `KptEnterMotion`, `KptToggleMotion`
|
|
48
|
+
`motionRootStyle`, stałe `KPT_DEFAULT_*_MOTION`, typy `KptMotionConfig`, `KptMotionRootStyle`,
|
|
49
|
+
`KptLayoutMotion`, `KptCollapseMotion`, `KptEnterMotion`, `KptToggleMotion`
|
|
47
50
|
- **Sprężyny** - `springProgress`, `springEasing`
|
|
48
|
-
- **Loty układu** - `planLayoutMotion`, `layoutMotionDuration`, typy `KptRect`, `KptLayoutFlight
|
|
51
|
+
- **Loty układu** - `planLayoutMotion`, `layoutMotionDuration`, typy `KptRect`, `KptLayoutFlight`,
|
|
52
|
+
`KptLayoutFrame`, `KptLayoutInnerFrame`, `KptLayoutPlanOptions`
|
|
49
53
|
- **Warstwa DOM** - `snapshotLayout`, `playLayoutMotion`, `stopLayoutMotion`, `morphLayout`,
|
|
50
|
-
`applyMotionConfig`, `layoutMotionAllowed`, `layoutMorphSupported`
|
|
54
|
+
`applyMotionConfig`, `layoutMotionAllowed`, `layoutMorphSupported`, typy `KptLayoutSnapshot`,
|
|
55
|
+
`KptPlayLayoutOptions`
|
|
51
56
|
- **Mikroruch** - `playEnterMotion`, `playShakeMotion`, `playPopMotion`, `playInvalidMotion`,
|
|
52
|
-
`playIndicatorMotion`
|
|
57
|
+
`playIndicatorMotion`, typ `KptEnterFrom`
|
|
53
58
|
- **Listy z kluczem** - `snapshotKeyed`, `playKeyedMotion`, `stopKeyedMotion`, `morphKeyed`,
|
|
54
|
-
`isVisibleInScroller`, `revealInScroller`
|
|
59
|
+
`isVisibleInScroller`, `revealInScroller`, typy `KptKeyedNode`, `KptKeyedSnapshot`, `KptKeyedClasses`,
|
|
60
|
+
`KptKeyedMotionOptions`
|
|
55
61
|
- **Zawijany wiersz (tagi, chipy)** - `prepareFlowMotion`, `flowChangeOf`, typ `KptFlowChange`
|
|
56
62
|
- **Kreator i podmiana treści** - `stepperSchedule`, `stepperScheduleOptions`, `playStepperLight`, `preparePanelSwap`, typy
|
|
57
|
-
`KptStepperSchedule`, `KptStepTiming`
|
|
63
|
+
`KptStepperSchedule`, `KptStepperScheduleOptions`, `KptStepTiming`
|
|
64
|
+
- **Podgląd zdjęcia (`kpt-image`)** - `zoomFlight`, `playZoomOpen`, `playZoomClose`, `stopZoomMotion`,
|
|
65
|
+
stałe `KPT_ZOOM_*_MS`, typy `KptZoomFlight`, `KptZoomElements`
|
|
66
|
+
- **Galeria (`kpt-galleria`)** - `gallerySlide`, `revealDelta`, `stripEdges`, `inertiaStep`,
|
|
67
|
+
`playGallerySlide`, `stopGallerySlide`, `revealThumb`, `scrollStrip`, `trackStripEdges`,
|
|
68
|
+
`attachDragScroll`, stałe `KPT_GALLERY_*`, `KPT_INERTIA_STOP`, typy `KptGallerySlide`,
|
|
69
|
+
`KptGallerySlideOptions`
|
|
70
|
+
- **Loader (`kpt-loader`)** - `createLoader`, `loaderProgress`, `loaderPalette`, `loaderColor`,
|
|
71
|
+
`loaderOnColor`, `loaderSubtle`, `cubicBezier`, `swapText`, `setText`, stałe `KPT_LOADER_VARIANTS`,
|
|
72
|
+
`LOADER_*`, typy `KptLoader*`
|
|
58
73
|
|
|
59
74
|
Pełny opis: [`llms.txt`](https://ui.konce.pt/llms/motion/llms.txt) (EN)
|
|
60
75
|
i [`llms-pl.txt`](https://ui.konce.pt/llms/motion/llms-pl.txt) (PL).
|
package/dist/config.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface KptMotionConfig {
|
|
|
32
32
|
layout?: KptLayoutMotion;
|
|
33
33
|
/** Domyślny ruch zwijania sekcji (`kpt-accordion`, `kpt-fieldset`, `kpt-panel`). Domyślnie `spring`. */
|
|
34
34
|
collapse?: KptCollapseMotion;
|
|
35
|
-
/** Domyślny ruch pojawiania się (czat: wiadomości, reakcje, status, dock). Domyślnie `spring`. */
|
|
35
|
+
/** Domyślny ruch pojawiania się (czat: wiadomości, reakcje, status, dock; podgląd `kpt-image`, przesuw `kpt-galleria`). Domyślnie `spring`. */
|
|
36
36
|
enter?: KptEnterMotion;
|
|
37
37
|
/** Domyślny ruch przełączania (checkbox, radio, switch). Domyślnie `spring`. */
|
|
38
38
|
toggle?: KptToggleMotion;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { KptEnterMotion } from './config.ts';
|
|
2
|
+
export interface KptGallerySlideOptions {
|
|
3
|
+
/** Scena - pojemnik z `overflow: hidden`, w którym leży zdjęcie. */
|
|
4
|
+
readonly stage: HTMLElement;
|
|
5
|
+
/** Zdjęcie główne, już z nowym `src`. */
|
|
6
|
+
readonly image: HTMLImageElement;
|
|
7
|
+
/** `src` poprzedniego zdjęcia - z niego powstaje duch, który się cofa. */
|
|
8
|
+
readonly previousSrc: string;
|
|
9
|
+
/** 1 = następne, -1 = poprzednie. */
|
|
10
|
+
readonly direction: 1 | -1;
|
|
11
|
+
readonly motion: KptEnterMotion;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Zmiana zdjęcia w galerii. Wołać po renderze, gdy zdjęcie główne ma już nowy `src`.
|
|
15
|
+
* `spring`: nowe zdjęcie wjeżdża w kierunku ruchu na miękkiej sprężynie, a duch poprzedniego
|
|
16
|
+
* (osobny `<img class="kpt-galleria__ghost">` pod nim) cofa się wolniej i przygasa - paralaksa.
|
|
17
|
+
* `fade` i systemowe "ogranicz ruch": krótkie przenikanie; `none`: nic.
|
|
18
|
+
*
|
|
19
|
+
* Nowe zdjęcie, które jeszcze się nie zdekodowało, czeka niewidoczne - do tego czasu widać ducha,
|
|
20
|
+
* a nie pustą scenę.
|
|
21
|
+
*/
|
|
22
|
+
export declare function playGallerySlide(options: KptGallerySlideOptions): void;
|
|
23
|
+
/** Zatrzymuje przesuw i zdejmuje ducha - przy zniszczeniu galerii albo wymianie listy. */
|
|
24
|
+
export declare function stopGallerySlide(stage: HTMLElement, image?: HTMLImageElement | null): void;
|
|
25
|
+
/**
|
|
26
|
+
* Przewija pasek tak, żeby miniatura była cała widoczna - i tylko wtedy, gdy nie jest.
|
|
27
|
+
* Przewijany jest sam pasek (`scrollBy`), nigdy strona.
|
|
28
|
+
*/
|
|
29
|
+
export declare function revealThumb(strip: HTMLElement, thumb: HTMLElement, smooth?: boolean): void;
|
|
30
|
+
/** Przewija pasek o 80% szerokości; `direction` logiczny: -1 w stronę początku, 1 w stronę końca. */
|
|
31
|
+
export declare function scrollStrip(strip: HTMLElement, direction: 1 | -1): void;
|
|
32
|
+
/**
|
|
33
|
+
* Śledzi, czy pasek da się przewinąć, i wystawia to na `target` jako atrybuty `data-scroll-start`
|
|
34
|
+
* i `data-scroll-end` (logiczne - w RTL "początek" jest po prawej). Styl wygasza po nich krawędzie
|
|
35
|
+
* i pokazuje strzałki paska. `update()` - po zmianie listy miniatur; `dispose()` - przy zniszczeniu.
|
|
36
|
+
*/
|
|
37
|
+
export declare function trackStripEdges(strip: HTMLElement, target: HTMLElement): {
|
|
38
|
+
update: () => void;
|
|
39
|
+
dispose: () => void;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Przeciąganie paska myszą i piórem (dotyk przewija się natywnie). Ruch o mniej niż 5 px to
|
|
43
|
+
* zwykły klik w miniaturę; dłuższy to przeciągnięcie - wtedy klik po puszczeniu jest połykany,
|
|
44
|
+
* a pasek jedzie dalej z bezwładnością (przy "ogranicz ruch" staje od razu). Klasa
|
|
45
|
+
* `is-dragging` na pasku na czas przeciągania. Zwraca funkcję odpinającą.
|
|
46
|
+
*/
|
|
47
|
+
export declare function attachDragScroll(strip: HTMLElement): () => void;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { layoutMotionAllowed } from "./dom.js";
|
|
2
|
+
import { springEasing } from "./spring.js";
|
|
3
|
+
import { KPT_GALLERY_FADE_MS, KPT_GALLERY_SLIDE_MS, KPT_INERTIA_STOP, gallerySlide, inertiaStep, revealDelta, stripEdges, } from "./gallery.js";
|
|
4
|
+
const ANIMATION_ID = 'kpt-gallery-motion';
|
|
5
|
+
const EASE_OUT = 'cubic-bezier(0.2, 0, 0, 1)';
|
|
6
|
+
/** Margines odsłaniania miniatury - szerszy niż wygaszone krawędzie paska, żeby nie stała pod nimi. */
|
|
7
|
+
const REVEAL_PAD = 40;
|
|
8
|
+
/** Duch poprzedniego zdjęcia w scenie - jeden na scenę, nowy przesuw zdejmuje stary od razu. */
|
|
9
|
+
const ghosts = new WeakMap();
|
|
10
|
+
function clearGhost(stage) {
|
|
11
|
+
ghosts.get(stage)?.remove();
|
|
12
|
+
ghosts.delete(stage);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Zmiana zdjęcia w galerii. Wołać po renderze, gdy zdjęcie główne ma już nowy `src`.
|
|
16
|
+
* `spring`: nowe zdjęcie wjeżdża w kierunku ruchu na miękkiej sprężynie, a duch poprzedniego
|
|
17
|
+
* (osobny `<img class="kpt-galleria__ghost">` pod nim) cofa się wolniej i przygasa - paralaksa.
|
|
18
|
+
* `fade` i systemowe "ogranicz ruch": krótkie przenikanie; `none`: nic.
|
|
19
|
+
*
|
|
20
|
+
* Nowe zdjęcie, które jeszcze się nie zdekodowało, czeka niewidoczne - do tego czasu widać ducha,
|
|
21
|
+
* a nie pustą scenę.
|
|
22
|
+
*/
|
|
23
|
+
export function playGallerySlide(options) {
|
|
24
|
+
const { stage, image, previousSrc, direction, motion } = options;
|
|
25
|
+
clearGhost(stage);
|
|
26
|
+
// Bez WAAPI (np. jsdom w testach aplikacji) zdjęcie zmienia się bez ruchu.
|
|
27
|
+
if (!animatable(image))
|
|
28
|
+
return;
|
|
29
|
+
cancelOn(image);
|
|
30
|
+
image.style.removeProperty('opacity');
|
|
31
|
+
if (motion === 'none' || !previousSrc)
|
|
32
|
+
return;
|
|
33
|
+
const ghost = document.createElement('img');
|
|
34
|
+
ghost.className = 'kpt-galleria__ghost';
|
|
35
|
+
ghost.src = previousSrc;
|
|
36
|
+
ghost.alt = '';
|
|
37
|
+
ghost.setAttribute('aria-hidden', 'true');
|
|
38
|
+
stage.insertBefore(ghost, image);
|
|
39
|
+
ghosts.set(stage, ghost);
|
|
40
|
+
const play = () => {
|
|
41
|
+
image.style.removeProperty('opacity');
|
|
42
|
+
if (ghosts.get(stage) !== ghost)
|
|
43
|
+
return;
|
|
44
|
+
let ms = KPT_GALLERY_FADE_MS;
|
|
45
|
+
if (motion === 'fade' || !layoutMotionAllowed()) {
|
|
46
|
+
image.animate({ opacity: [0, 1] }, { id: ANIMATION_ID, duration: ms, easing: EASE_OUT, fill: 'backwards' });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
ms = KPT_GALLERY_SLIDE_MS;
|
|
50
|
+
const slide = gallerySlide(direction, getComputedStyle(stage).direction === 'rtl');
|
|
51
|
+
image.animate({ transform: [...slide.incoming] }, { id: ANIMATION_ID, duration: ms, easing: springEasing(0.18), fill: 'backwards' });
|
|
52
|
+
image.animate({ filter: ['brightness(0.85)', 'brightness(1)'] }, { id: ANIMATION_ID, duration: ms, easing: EASE_OUT, fill: 'backwards' });
|
|
53
|
+
ghost.animate({ transform: [...slide.outgoing], filter: ['brightness(1)', 'brightness(0.45)'] }, { id: ANIMATION_ID, duration: ms, easing: EASE_OUT, fill: 'forwards' });
|
|
54
|
+
}
|
|
55
|
+
// Zegar, nie `finished`: anulowana animacja nigdy by go nie rozwiązała.
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
if (ghosts.get(stage) === ghost)
|
|
58
|
+
clearGhost(stage);
|
|
59
|
+
}, ms);
|
|
60
|
+
};
|
|
61
|
+
if (image.complete && image.naturalWidth > 0) {
|
|
62
|
+
play();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
image.style.opacity = '0';
|
|
66
|
+
// Błąd dekodowania nie przerywa - zdjęcie po prostu się pokaże.
|
|
67
|
+
void image.decode().catch(() => undefined).then(play);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Zatrzymuje przesuw i zdejmuje ducha - przy zniszczeniu galerii albo wymianie listy. */
|
|
71
|
+
export function stopGallerySlide(stage, image) {
|
|
72
|
+
clearGhost(stage);
|
|
73
|
+
if (image && animatable(image))
|
|
74
|
+
cancelOn(image);
|
|
75
|
+
image?.style.removeProperty('opacity');
|
|
76
|
+
}
|
|
77
|
+
function animatable(el) {
|
|
78
|
+
return typeof el.animate === 'function' && typeof el.getAnimations === 'function';
|
|
79
|
+
}
|
|
80
|
+
function cancelOn(el) {
|
|
81
|
+
el.getAnimations().forEach((a) => {
|
|
82
|
+
if (a.id === ANIMATION_ID)
|
|
83
|
+
a.cancel();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Przewija pasek tak, żeby miniatura była cała widoczna - i tylko wtedy, gdy nie jest.
|
|
88
|
+
* Przewijany jest sam pasek (`scrollBy`), nigdy strona.
|
|
89
|
+
*/
|
|
90
|
+
export function revealThumb(strip, thumb, smooth = true) {
|
|
91
|
+
const s = strip.getBoundingClientRect();
|
|
92
|
+
const r = thumb.getBoundingClientRect();
|
|
93
|
+
const pad = Math.min(REVEAL_PAD, Math.max(0, (s.width - r.width) / 2));
|
|
94
|
+
const delta = revealDelta(r.left, r.right, s.left, s.right, pad);
|
|
95
|
+
if (Math.abs(delta) < 1 || typeof strip.scrollBy !== 'function')
|
|
96
|
+
return;
|
|
97
|
+
strip.scrollBy({ left: delta, behavior: smooth && layoutMotionAllowed() ? 'smooth' : 'auto' });
|
|
98
|
+
}
|
|
99
|
+
/** Przewija pasek o 80% szerokości; `direction` logiczny: -1 w stronę początku, 1 w stronę końca. */
|
|
100
|
+
export function scrollStrip(strip, direction) {
|
|
101
|
+
if (typeof strip.scrollBy !== 'function')
|
|
102
|
+
return;
|
|
103
|
+
const physical = getComputedStyle(strip).direction === 'rtl' ? -direction : direction;
|
|
104
|
+
strip.scrollBy({ left: physical * strip.clientWidth * 0.8, behavior: layoutMotionAllowed() ? 'smooth' : 'auto' });
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Śledzi, czy pasek da się przewinąć, i wystawia to na `target` jako atrybuty `data-scroll-start`
|
|
108
|
+
* i `data-scroll-end` (logiczne - w RTL "początek" jest po prawej). Styl wygasza po nich krawędzie
|
|
109
|
+
* i pokazuje strzałki paska. `update()` - po zmianie listy miniatur; `dispose()` - przy zniszczeniu.
|
|
110
|
+
*/
|
|
111
|
+
export function trackStripEdges(strip, target) {
|
|
112
|
+
const update = () => {
|
|
113
|
+
const edges = stripEdges(strip.scrollLeft, strip.clientWidth, strip.scrollWidth);
|
|
114
|
+
target.toggleAttribute('data-scroll-start', edges.start);
|
|
115
|
+
target.toggleAttribute('data-scroll-end', edges.end);
|
|
116
|
+
};
|
|
117
|
+
strip.addEventListener('scroll', update, { passive: true });
|
|
118
|
+
const ro = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(update);
|
|
119
|
+
ro?.observe(strip);
|
|
120
|
+
update();
|
|
121
|
+
return {
|
|
122
|
+
update,
|
|
123
|
+
dispose: () => {
|
|
124
|
+
strip.removeEventListener('scroll', update);
|
|
125
|
+
ro?.disconnect();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Przeciąganie paska myszą i piórem (dotyk przewija się natywnie). Ruch o mniej niż 5 px to
|
|
131
|
+
* zwykły klik w miniaturę; dłuższy to przeciągnięcie - wtedy klik po puszczeniu jest połykany,
|
|
132
|
+
* a pasek jedzie dalej z bezwładnością (przy "ogranicz ruch" staje od razu). Klasa
|
|
133
|
+
* `is-dragging` na pasku na czas przeciągania. Zwraca funkcję odpinającą.
|
|
134
|
+
*/
|
|
135
|
+
export function attachDragScroll(strip) {
|
|
136
|
+
let pointer = -1;
|
|
137
|
+
let startX = 0;
|
|
138
|
+
let startScroll = 0;
|
|
139
|
+
let lastX = 0;
|
|
140
|
+
let lastT = 0;
|
|
141
|
+
let velocity = 0;
|
|
142
|
+
let dragging = false;
|
|
143
|
+
let swallowClick = false;
|
|
144
|
+
let raf = 0;
|
|
145
|
+
const stopGlide = () => {
|
|
146
|
+
if (raf)
|
|
147
|
+
cancelAnimationFrame(raf);
|
|
148
|
+
raf = 0;
|
|
149
|
+
};
|
|
150
|
+
const glide = (initial) => {
|
|
151
|
+
let v = initial;
|
|
152
|
+
let last = performance.now();
|
|
153
|
+
const frame = (now) => {
|
|
154
|
+
const step = inertiaStep(v, Math.min(32, now - last));
|
|
155
|
+
last = now;
|
|
156
|
+
v = step.velocity;
|
|
157
|
+
const before = strip.scrollLeft;
|
|
158
|
+
strip.scrollLeft += step.distance;
|
|
159
|
+
// Koniec bezwładności: prędkość zgasła albo pasek doszedł do krawędzi.
|
|
160
|
+
if (v === 0 || strip.scrollLeft === before) {
|
|
161
|
+
raf = 0;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
raf = requestAnimationFrame(frame);
|
|
165
|
+
};
|
|
166
|
+
raf = requestAnimationFrame(frame);
|
|
167
|
+
};
|
|
168
|
+
const onDown = (e) => {
|
|
169
|
+
if (e.pointerType === 'touch' || e.button !== 0)
|
|
170
|
+
return;
|
|
171
|
+
stopGlide();
|
|
172
|
+
pointer = e.pointerId;
|
|
173
|
+
startX = lastX = e.clientX;
|
|
174
|
+
lastT = e.timeStamp;
|
|
175
|
+
startScroll = strip.scrollLeft;
|
|
176
|
+
velocity = 0;
|
|
177
|
+
dragging = false;
|
|
178
|
+
};
|
|
179
|
+
const onMove = (e) => {
|
|
180
|
+
if (e.pointerId !== pointer)
|
|
181
|
+
return;
|
|
182
|
+
const dx = e.clientX - startX;
|
|
183
|
+
if (!dragging) {
|
|
184
|
+
if (Math.abs(dx) < 5)
|
|
185
|
+
return;
|
|
186
|
+
dragging = true;
|
|
187
|
+
strip.setPointerCapture(pointer);
|
|
188
|
+
strip.classList.add('is-dragging');
|
|
189
|
+
}
|
|
190
|
+
strip.scrollLeft = startScroll - dx;
|
|
191
|
+
const dt = e.timeStamp - lastT;
|
|
192
|
+
if (dt > 0)
|
|
193
|
+
velocity = 0.8 * ((lastX - e.clientX) / dt) + 0.2 * velocity;
|
|
194
|
+
lastX = e.clientX;
|
|
195
|
+
lastT = e.timeStamp;
|
|
196
|
+
};
|
|
197
|
+
const onUp = (e) => {
|
|
198
|
+
if (e.pointerId !== pointer)
|
|
199
|
+
return;
|
|
200
|
+
pointer = -1;
|
|
201
|
+
if (!dragging)
|
|
202
|
+
return;
|
|
203
|
+
dragging = false;
|
|
204
|
+
strip.classList.remove('is-dragging');
|
|
205
|
+
if (strip.hasPointerCapture(e.pointerId))
|
|
206
|
+
strip.releasePointerCapture(e.pointerId);
|
|
207
|
+
swallowClick = true;
|
|
208
|
+
setTimeout(() => (swallowClick = false), 0);
|
|
209
|
+
// Puszczenie po zatrzymaniu ręki nie rzuca paskiem.
|
|
210
|
+
if (layoutMotionAllowed() && Math.abs(velocity) > KPT_INERTIA_STOP && e.timeStamp - lastT < 80)
|
|
211
|
+
glide(velocity);
|
|
212
|
+
};
|
|
213
|
+
const onClick = (e) => {
|
|
214
|
+
if (!swallowClick)
|
|
215
|
+
return;
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
e.stopPropagation();
|
|
218
|
+
swallowClick = false;
|
|
219
|
+
};
|
|
220
|
+
const onDragStart = (e) => e.preventDefault();
|
|
221
|
+
strip.addEventListener('pointerdown', onDown);
|
|
222
|
+
strip.addEventListener('pointermove', onMove);
|
|
223
|
+
strip.addEventListener('pointerup', onUp);
|
|
224
|
+
strip.addEventListener('pointercancel', onUp);
|
|
225
|
+
strip.addEventListener('click', onClick, true);
|
|
226
|
+
strip.addEventListener('dragstart', onDragStart);
|
|
227
|
+
strip.addEventListener('wheel', stopGlide, { passive: true });
|
|
228
|
+
return () => {
|
|
229
|
+
stopGlide();
|
|
230
|
+
strip.removeEventListener('pointerdown', onDown);
|
|
231
|
+
strip.removeEventListener('pointermove', onMove);
|
|
232
|
+
strip.removeEventListener('pointerup', onUp);
|
|
233
|
+
strip.removeEventListener('pointercancel', onUp);
|
|
234
|
+
strip.removeEventListener('click', onClick, true);
|
|
235
|
+
strip.removeEventListener('dragstart', onDragStart);
|
|
236
|
+
strip.removeEventListener('wheel', stopGlide);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruch galerii (`kpt-galleria`): przesuw zdjęć z paralaksą i pasek miniatur bez paska przewijania,
|
|
3
|
+
* prowadzony przeciąganiem. Tu sama arytmetyka - bez DOM, z testami; warstwa DOM to `gallery-dom.ts`.
|
|
4
|
+
*/
|
|
5
|
+
/** Czas przesuwu zdjęcia (miękka sprężyna) i krótkiego przenikania (`fade`, "ogranicz ruch"), ms. */
|
|
6
|
+
export declare const KPT_GALLERY_SLIDE_MS = 620;
|
|
7
|
+
export declare const KPT_GALLERY_FADE_MS = 200;
|
|
8
|
+
/** O ile wolniej od nowego zdjęcia cofa się stare - paralaksa, w procentach szerokości sceny. */
|
|
9
|
+
export declare const KPT_GALLERY_PARALLAX = 28;
|
|
10
|
+
/** Klatki przesuwu: nowe zdjęcie wjeżdża, stare cofa się wolniej. */
|
|
11
|
+
export interface KptGallerySlide {
|
|
12
|
+
readonly incoming: readonly [string, string];
|
|
13
|
+
readonly outgoing: readonly [string, string];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `direction` 1 = następne zdjęcie, -1 = poprzednie. Następne wjeżdża od strony końca wiersza:
|
|
17
|
+
* w LTR z prawej, w RTL z lewej - tak samo jak kolejność miniatur.
|
|
18
|
+
*/
|
|
19
|
+
export declare function gallerySlide(direction: 1 | -1, rtl?: boolean): KptGallerySlide;
|
|
20
|
+
/**
|
|
21
|
+
* O ile przewinąć pasek, żeby miniatura była cała widoczna z marginesem `pad` - i ani piksela
|
|
22
|
+
* więcej: widoczna miniatura daje 0, więc pasek nie skacze przy każdym kliknięciu. Współrzędne
|
|
23
|
+
* fizyczne (prostokąty z ekranu), więc działa tak samo w RTL: dodatnia wartość przesuwa widok w prawo.
|
|
24
|
+
*/
|
|
25
|
+
export declare function revealDelta(itemLeft: number, itemRight: number, viewLeft: number, viewRight: number, pad?: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* Czy pasek da się przewinąć w stronę początku i końca. `scrollLeft` w RTL bywa ujemny (od 0 do
|
|
28
|
+
* `-max`), więc liczy się wartość bezwzględna - wynik jest logiczny, nie fizyczny.
|
|
29
|
+
*/
|
|
30
|
+
export declare function stripEdges(scrollLeft: number, clientWidth: number, scrollWidth: number): {
|
|
31
|
+
start: boolean;
|
|
32
|
+
end: boolean;
|
|
33
|
+
};
|
|
34
|
+
/** Prędkość (px/ms), poniżej której bezwładność się zatrzymuje. */
|
|
35
|
+
export declare const KPT_INERTIA_STOP = 0.02;
|
|
36
|
+
/**
|
|
37
|
+
* Jeden krok bezwładności po puszczeniu przeciągnięcia: prędkość gaśnie wykładniczo (`friction`
|
|
38
|
+
* na każde 16 ms), a droga to średnia prędkość w kroku razy czas. Niezależne od liczby klatek -
|
|
39
|
+
* na 120 Hz pasek zatrzymuje się w tym samym miejscu co na 60 Hz.
|
|
40
|
+
*/
|
|
41
|
+
export declare function inertiaStep(velocity: number, dt: number, friction?: number): {
|
|
42
|
+
velocity: number;
|
|
43
|
+
distance: number;
|
|
44
|
+
};
|
package/dist/gallery.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ruch galerii (`kpt-galleria`): przesuw zdjęć z paralaksą i pasek miniatur bez paska przewijania,
|
|
3
|
+
* prowadzony przeciąganiem. Tu sama arytmetyka - bez DOM, z testami; warstwa DOM to `gallery-dom.ts`.
|
|
4
|
+
*/
|
|
5
|
+
/** Czas przesuwu zdjęcia (miękka sprężyna) i krótkiego przenikania (`fade`, "ogranicz ruch"), ms. */
|
|
6
|
+
export const KPT_GALLERY_SLIDE_MS = 620;
|
|
7
|
+
export const KPT_GALLERY_FADE_MS = 200;
|
|
8
|
+
/** O ile wolniej od nowego zdjęcia cofa się stare - paralaksa, w procentach szerokości sceny. */
|
|
9
|
+
export const KPT_GALLERY_PARALLAX = 28;
|
|
10
|
+
/**
|
|
11
|
+
* `direction` 1 = następne zdjęcie, -1 = poprzednie. Następne wjeżdża od strony końca wiersza:
|
|
12
|
+
* w LTR z prawej, w RTL z lewej - tak samo jak kolejność miniatur.
|
|
13
|
+
*/
|
|
14
|
+
export function gallerySlide(direction, rtl = false) {
|
|
15
|
+
const side = rtl ? -direction : direction;
|
|
16
|
+
return {
|
|
17
|
+
incoming: [`translateX(${side * 100}%)`, 'none'],
|
|
18
|
+
outgoing: ['none', `translateX(${side * -KPT_GALLERY_PARALLAX}%) scale(0.96)`],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* O ile przewinąć pasek, żeby miniatura była cała widoczna z marginesem `pad` - i ani piksela
|
|
23
|
+
* więcej: widoczna miniatura daje 0, więc pasek nie skacze przy każdym kliknięciu. Współrzędne
|
|
24
|
+
* fizyczne (prostokąty z ekranu), więc działa tak samo w RTL: dodatnia wartość przesuwa widok w prawo.
|
|
25
|
+
*/
|
|
26
|
+
export function revealDelta(itemLeft, itemRight, viewLeft, viewRight, pad = 0) {
|
|
27
|
+
if (itemRight - itemLeft > viewRight - viewLeft - 2 * pad) {
|
|
28
|
+
// Miniatura szersza niż widok - wyrównaj do początku, reszty i tak nie zmieścimy.
|
|
29
|
+
return itemLeft - (viewLeft + pad);
|
|
30
|
+
}
|
|
31
|
+
if (itemLeft < viewLeft + pad)
|
|
32
|
+
return itemLeft - (viewLeft + pad);
|
|
33
|
+
if (itemRight > viewRight - pad)
|
|
34
|
+
return itemRight - (viewRight - pad);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Czy pasek da się przewinąć w stronę początku i końca. `scrollLeft` w RTL bywa ujemny (od 0 do
|
|
39
|
+
* `-max`), więc liczy się wartość bezwzględna - wynik jest logiczny, nie fizyczny.
|
|
40
|
+
*/
|
|
41
|
+
export function stripEdges(scrollLeft, clientWidth, scrollWidth) {
|
|
42
|
+
const max = scrollWidth - clientWidth;
|
|
43
|
+
const at = Math.abs(scrollLeft);
|
|
44
|
+
return { start: max > 1 && at > 1, end: max > 1 && at < max - 1 };
|
|
45
|
+
}
|
|
46
|
+
/** Prędkość (px/ms), poniżej której bezwładność się zatrzymuje. */
|
|
47
|
+
export const KPT_INERTIA_STOP = 0.02;
|
|
48
|
+
/**
|
|
49
|
+
* Jeden krok bezwładności po puszczeniu przeciągnięcia: prędkość gaśnie wykładniczo (`friction`
|
|
50
|
+
* na każde 16 ms), a droga to średnia prędkość w kroku razy czas. Niezależne od liczby klatek -
|
|
51
|
+
* na 120 Hz pasek zatrzymuje się w tym samym miejscu co na 60 Hz.
|
|
52
|
+
*/
|
|
53
|
+
export function inertiaStep(velocity, dt, friction = 0.95) {
|
|
54
|
+
const next = velocity * Math.pow(friction, dt / 16);
|
|
55
|
+
return { velocity: Math.abs(next) < KPT_INERTIA_STOP ? 0 : next, distance: ((velocity + next) / 2) * dt };
|
|
56
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export { springProgress, springEasing } from './spring.ts';
|
|
2
|
+
export { zoomFlight, KPT_ZOOM_OPEN_MS, KPT_ZOOM_CLOSE_MS, KPT_ZOOM_FADE_MS } from './zoom.ts';
|
|
3
|
+
export type { KptZoomFlight } from './zoom.ts';
|
|
4
|
+
export { playZoomOpen, playZoomClose, stopZoomMotion } from './zoom-dom.ts';
|
|
5
|
+
export type { KptZoomElements } from './zoom-dom.ts';
|
|
6
|
+
export { gallerySlide, revealDelta, stripEdges, inertiaStep, KPT_GALLERY_SLIDE_MS, KPT_GALLERY_FADE_MS, KPT_GALLERY_PARALLAX, KPT_INERTIA_STOP, } from './gallery.ts';
|
|
7
|
+
export type { KptGallerySlide } from './gallery.ts';
|
|
8
|
+
export { playGallerySlide, stopGallerySlide, revealThumb, scrollStrip, trackStripEdges, attachDragScroll, } from './gallery-dom.ts';
|
|
9
|
+
export type { KptGallerySlideOptions } from './gallery-dom.ts';
|
|
2
10
|
export { planLayoutMotion, layoutMotionDuration } from './layout.ts';
|
|
3
11
|
export type { KptLayoutMotion, KptRect, KptLayoutFrame, KptLayoutInnerFrame, KptLayoutFlight, KptLayoutPlanOptions, } from './layout.ts';
|
|
4
12
|
export { layoutMotionAllowed, layoutMorphSupported, snapshotLayout, stopLayoutMotion, playLayoutMotion, morphLayout, } from './dom.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export { springProgress, springEasing } from "./spring.js";
|
|
2
|
+
export { zoomFlight, KPT_ZOOM_OPEN_MS, KPT_ZOOM_CLOSE_MS, KPT_ZOOM_FADE_MS } from "./zoom.js";
|
|
3
|
+
export { playZoomOpen, playZoomClose, stopZoomMotion } from "./zoom-dom.js";
|
|
4
|
+
export { gallerySlide, revealDelta, stripEdges, inertiaStep, KPT_GALLERY_SLIDE_MS, KPT_GALLERY_FADE_MS, KPT_GALLERY_PARALLAX, KPT_INERTIA_STOP, } from "./gallery.js";
|
|
5
|
+
export { playGallerySlide, stopGallerySlide, revealThumb, scrollStrip, trackStripEdges, attachDragScroll, } from "./gallery-dom.js";
|
|
2
6
|
export { planLayoutMotion, layoutMotionDuration } from "./layout.js";
|
|
3
7
|
export { layoutMotionAllowed, layoutMorphSupported, snapshotLayout, stopLayoutMotion, playLayoutMotion, morphLayout, } from "./dom.js";
|
|
4
8
|
export { applyMotionConfig, playEnterMotion, playShakeMotion, playPopMotion, playInvalidMotion, playIndicatorMotion, } from "./dom.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { KptEnterMotion } from './config.ts';
|
|
2
|
+
/** Elementy podglądu zdjęcia - te same w obu portach `kpt-image`. */
|
|
3
|
+
export interface KptZoomElements {
|
|
4
|
+
/** Miniatura, z której podgląd wylatuje i do której wraca. */
|
|
5
|
+
readonly thumb: HTMLElement;
|
|
6
|
+
/** Duże zdjęcie w podglądzie. */
|
|
7
|
+
readonly image: HTMLImageElement;
|
|
8
|
+
/** Natywne okno - jego `::backdrop` przenika się razem z lotem. */
|
|
9
|
+
readonly dialog: HTMLDialogElement;
|
|
10
|
+
/** Przycisk zamknięcia - pojawia się po starcie lotu, znika pierwszy. */
|
|
11
|
+
readonly close?: HTMLElement | null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Otwarcie podglądu. Wołać zaraz po `showModal()` - przed malowaniem, żeby pierwsza klatka
|
|
15
|
+
* już stała na miniaturze. `spring` (domyślny): zdjęcie wylatuje z miniatury i ląduje na miękkiej
|
|
16
|
+
* sprężynie, a miniatura na ten czas znika - zdjęcie opuściło swoje miejsce; `fade` i systemowe
|
|
17
|
+
* "ogranicz ruch": samo przenikanie; `none`: nic.
|
|
18
|
+
*
|
|
19
|
+
* Zdjęcie bez wymiarów (plik jeszcze się dekoduje) nie ma celu lotu - czekamy na `decode()`,
|
|
20
|
+
* a do tego czasu jest niewidoczne, żeby nie mignęło na środku przed startem.
|
|
21
|
+
*/
|
|
22
|
+
export declare function playZoomOpen(els: KptZoomElements, motion: KptEnterMotion): void;
|
|
23
|
+
/**
|
|
24
|
+
* Zamknięcie podglądu: zdjęcie wraca do miniatury sprężyną z krótkim domknięciem, tło gaśnie.
|
|
25
|
+
* Zwraca czas ruchu w ms - dopiero po nim wołający zamyka okno (`dialog.close()`), a potem
|
|
26
|
+
* `stopZoomMotion`. `0` = zamknij od razu.
|
|
27
|
+
*/
|
|
28
|
+
export declare function playZoomClose(els: KptZoomElements, motion: KptEnterMotion): number;
|
|
29
|
+
/** Sprząta po podglądzie: zatrzymuje ruch, zdejmuje style i oddaje miniaturę na jej miejsce. */
|
|
30
|
+
export declare function stopZoomMotion(els: KptZoomElements): void;
|
package/dist/zoom-dom.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { layoutMotionAllowed } from "./dom.js";
|
|
2
|
+
import { springEasing } from "./spring.js";
|
|
3
|
+
import { KPT_ZOOM_CLOSE_MS, KPT_ZOOM_FADE_MS, KPT_ZOOM_OPEN_MS, zoomFlight } from "./zoom.js";
|
|
4
|
+
const ANIMATION_ID = 'kpt-zoom-motion';
|
|
5
|
+
const EASE_OUT = 'cubic-bezier(0.2, 0, 0, 1)';
|
|
6
|
+
const EASE_IN = 'cubic-bezier(0.4, 0, 1, 1)';
|
|
7
|
+
function rectOf(el) {
|
|
8
|
+
const r = el.getBoundingClientRect();
|
|
9
|
+
return { x: r.left, y: r.top, width: r.width, height: r.height };
|
|
10
|
+
}
|
|
11
|
+
function radiusOf(el) {
|
|
12
|
+
return parseFloat(getComputedStyle(el).borderTopLeftRadius) || 0;
|
|
13
|
+
}
|
|
14
|
+
/** `::backdrop` animowany przez WAAPI; przeglądarka bez `pseudoElement` po prostu go nie animuje. */
|
|
15
|
+
function animateBackdrop(dialog, opacity, duration, easing) {
|
|
16
|
+
try {
|
|
17
|
+
dialog.animate({ opacity }, { id: ANIMATION_ID, pseudoElement: '::backdrop', duration, easing, fill: 'forwards' });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
/* bez wsparcia pseudoElement - tło pojawia się od razu */
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function fadeOpen(els) {
|
|
24
|
+
els.dialog.animate({ opacity: [0, 1] }, { id: ANIMATION_ID, duration: KPT_ZOOM_FADE_MS, easing: EASE_OUT, fill: 'backwards' });
|
|
25
|
+
animateBackdrop(els.dialog, [0, 1], KPT_ZOOM_FADE_MS, EASE_OUT);
|
|
26
|
+
}
|
|
27
|
+
/** Bez WAAPI (np. jsdom w testach aplikacji) ruchu nie ma - podgląd po prostu się otwiera i zamyka. */
|
|
28
|
+
function animatable() {
|
|
29
|
+
return typeof Element !== 'undefined' && typeof Element.prototype.animate === 'function';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pokolenie podglądu dla zdjęcia. Otwarcie czekające na `decode()` sprawdza je przed startem:
|
|
33
|
+
* zamknięcie albo kolejne otwarcie w międzyczasie je podbija, więc spóźniony lot się nie odpala.
|
|
34
|
+
*/
|
|
35
|
+
const generation = new WeakMap();
|
|
36
|
+
function nextGeneration(image) {
|
|
37
|
+
const value = (generation.get(image) ?? 0) + 1;
|
|
38
|
+
generation.set(image, value);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Otwarcie podglądu. Wołać zaraz po `showModal()` - przed malowaniem, żeby pierwsza klatka
|
|
43
|
+
* już stała na miniaturze. `spring` (domyślny): zdjęcie wylatuje z miniatury i ląduje na miękkiej
|
|
44
|
+
* sprężynie, a miniatura na ten czas znika - zdjęcie opuściło swoje miejsce; `fade` i systemowe
|
|
45
|
+
* "ogranicz ruch": samo przenikanie; `none`: nic.
|
|
46
|
+
*
|
|
47
|
+
* Zdjęcie bez wymiarów (plik jeszcze się dekoduje) nie ma celu lotu - czekamy na `decode()`,
|
|
48
|
+
* a do tego czasu jest niewidoczne, żeby nie mignęło na środku przed startem.
|
|
49
|
+
*/
|
|
50
|
+
export function playZoomOpen(els, motion) {
|
|
51
|
+
stopZoomMotion(els);
|
|
52
|
+
if (motion === 'none' || !animatable())
|
|
53
|
+
return;
|
|
54
|
+
if (motion === 'fade' || !layoutMotionAllowed()) {
|
|
55
|
+
fadeOpen(els);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const mine = nextGeneration(els.image);
|
|
59
|
+
const fly = () => {
|
|
60
|
+
if (generation.get(els.image) !== mine)
|
|
61
|
+
return;
|
|
62
|
+
els.image.style.removeProperty('opacity');
|
|
63
|
+
els.close?.style.removeProperty('opacity');
|
|
64
|
+
cancelBackdrop(els.dialog);
|
|
65
|
+
if (!els.dialog.open)
|
|
66
|
+
return;
|
|
67
|
+
const to = rectOf(els.image);
|
|
68
|
+
if (to.width === 0) {
|
|
69
|
+
fadeOpen(els);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const radius = radiusOf(els.image);
|
|
73
|
+
const flight = zoomFlight(rectOf(els.thumb), to, radiusOf(els.thumb) || radius);
|
|
74
|
+
els.thumb.style.visibility = 'hidden';
|
|
75
|
+
els.image.animate({ transform: [flight.transform, 'none'], borderRadius: [`${flight.radiusFrom}px`, `${radius}px`] }, { id: ANIMATION_ID, duration: KPT_ZOOM_OPEN_MS, easing: springEasing(0.22), fill: 'backwards' });
|
|
76
|
+
animateBackdrop(els.dialog, [0, 1], 260, EASE_OUT);
|
|
77
|
+
els.close?.animate({ opacity: [0, 1], transform: ['scale(0.6)', 'none'] }, { id: ANIMATION_ID, duration: 320, delay: 220, easing: springEasing(0.32), fill: 'backwards' });
|
|
78
|
+
};
|
|
79
|
+
if (els.image.complete && els.image.naturalWidth > 0) {
|
|
80
|
+
fly();
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
// Do startu lotu nic nie może się pokazać: ani zdjęcie na środku, ani tło i przycisk x
|
|
84
|
+
// w pełnym kryciu (potem zgasłyby i wróciły z animacją).
|
|
85
|
+
els.image.style.opacity = '0';
|
|
86
|
+
if (els.close)
|
|
87
|
+
els.close.style.opacity = '0';
|
|
88
|
+
animateBackdrop(els.dialog, [0, 0], 60_000, 'linear');
|
|
89
|
+
// Błąd dekodowania nie przerywa - `fly` bez wymiarów zdjęcia schodzi do przenikania.
|
|
90
|
+
void els.image.decode().catch(() => undefined).then(fly);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Zdejmuje animacje `::backdrop` okna - `getAnimations()` okna ich nie obejmuje, więc z dokumentu. */
|
|
94
|
+
function cancelBackdrop(dialog) {
|
|
95
|
+
document.getAnimations().forEach((a) => {
|
|
96
|
+
if (a.id === ANIMATION_ID && a.effect?.target === dialog)
|
|
97
|
+
a.cancel();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Zamknięcie podglądu: zdjęcie wraca do miniatury sprężyną z krótkim domknięciem, tło gaśnie.
|
|
102
|
+
* Zwraca czas ruchu w ms - dopiero po nim wołający zamyka okno (`dialog.close()`), a potem
|
|
103
|
+
* `stopZoomMotion`. `0` = zamknij od razu.
|
|
104
|
+
*/
|
|
105
|
+
export function playZoomClose(els, motion) {
|
|
106
|
+
nextGeneration(els.image);
|
|
107
|
+
if (motion === 'none' || !els.dialog.open || !animatable())
|
|
108
|
+
return 0;
|
|
109
|
+
if (motion === 'fade' || !layoutMotionAllowed()) {
|
|
110
|
+
els.dialog.animate({ opacity: [1, 0] }, { id: ANIMATION_ID, duration: KPT_ZOOM_FADE_MS, easing: EASE_IN, fill: 'forwards' });
|
|
111
|
+
animateBackdrop(els.dialog, [1, 0], KPT_ZOOM_FADE_MS, EASE_IN);
|
|
112
|
+
return KPT_ZOOM_FADE_MS;
|
|
113
|
+
}
|
|
114
|
+
// Zamknięcie w trakcie lotu: prostokąt celu mierzymy bez transformu otwarcia.
|
|
115
|
+
cancelOn(els.image);
|
|
116
|
+
els.image.style.removeProperty('opacity');
|
|
117
|
+
const radius = radiusOf(els.image);
|
|
118
|
+
const flight = zoomFlight(rectOf(els.thumb), rectOf(els.image), radiusOf(els.thumb) || radius);
|
|
119
|
+
els.image.animate({ transform: ['none', flight.transform], borderRadius: [`${radius}px`, `${flight.radiusFrom}px`] }, { id: ANIMATION_ID, duration: KPT_ZOOM_CLOSE_MS, easing: springEasing(0.32), fill: 'forwards' });
|
|
120
|
+
animateBackdrop(els.dialog, [1, 0], 220, EASE_IN);
|
|
121
|
+
els.close?.animate({ opacity: [1, 0] }, { id: ANIMATION_ID, duration: 120, fill: 'forwards' });
|
|
122
|
+
return KPT_ZOOM_CLOSE_MS;
|
|
123
|
+
}
|
|
124
|
+
function cancelOn(el) {
|
|
125
|
+
el?.getAnimations().forEach((a) => {
|
|
126
|
+
if (a.id === ANIMATION_ID)
|
|
127
|
+
a.cancel();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** Sprząta po podglądzie: zatrzymuje ruch, zdejmuje style i oddaje miniaturę na jej miejsce. */
|
|
131
|
+
export function stopZoomMotion(els) {
|
|
132
|
+
nextGeneration(els.image);
|
|
133
|
+
els.image.style.removeProperty('opacity');
|
|
134
|
+
els.close?.style.removeProperty('opacity');
|
|
135
|
+
els.thumb.style.removeProperty('visibility');
|
|
136
|
+
if (typeof Element === 'undefined' || typeof Element.prototype.getAnimations !== 'function')
|
|
137
|
+
return;
|
|
138
|
+
cancelOn(els.image);
|
|
139
|
+
cancelOn(els.close);
|
|
140
|
+
cancelBackdrop(els.dialog);
|
|
141
|
+
}
|
package/dist/zoom.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { KptRect } from './layout.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Lot podglądu zdjęcia (`kpt-image`): duże zdjęcie startuje dokładnie z miejsca miniatury
|
|
4
|
+
* i ląduje w swoim docelowym prostokącie - technika FLIP. Miniatura i podgląd pokazują ten sam
|
|
5
|
+
* plik, więc mają te same proporcje i wystarcza jedna skala.
|
|
6
|
+
*/
|
|
7
|
+
export interface KptZoomFlight {
|
|
8
|
+
/** `transform`, który kładzie podgląd (prostokąt `to`) na miniaturze (prostokąt `from`). */
|
|
9
|
+
readonly transform: string;
|
|
10
|
+
/** Skala tego położenia - do przeliczenia promienia rogów. */
|
|
11
|
+
readonly scale: number;
|
|
12
|
+
/**
|
|
13
|
+
* Promień rogów podglądu na starcie, w px. Skala zmniejsza też rogi, więc żeby w locie miały
|
|
14
|
+
* promień miniatury, podgląd startuje z promieniem podzielonym przez skalę.
|
|
15
|
+
*/
|
|
16
|
+
readonly radiusFrom: number;
|
|
17
|
+
}
|
|
18
|
+
/** Czas otwarcia (miękka sprężyna) i zamknięcia (sprężyna z krótkim domknięciem), ms. */
|
|
19
|
+
export declare const KPT_ZOOM_OPEN_MS = 640;
|
|
20
|
+
export declare const KPT_ZOOM_CLOSE_MS = 460;
|
|
21
|
+
/** Czas przenikania - wariant `fade` i "ogranicz ruch". */
|
|
22
|
+
export declare const KPT_ZOOM_FADE_MS = 180;
|
|
23
|
+
/**
|
|
24
|
+
* Położenie startowe lotu. Przy zerowej szerokości celu (zdjęcie jeszcze bez wymiarów) skala
|
|
25
|
+
* zostaje 1 - lepiej przesunięcie bez skali niż dzielenie przez zero.
|
|
26
|
+
*/
|
|
27
|
+
export declare function zoomFlight(from: KptRect, to: KptRect, thumbRadius: number): KptZoomFlight;
|
package/dist/zoom.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { round } from "./spring.js";
|
|
2
|
+
/** Czas otwarcia (miękka sprężyna) i zamknięcia (sprężyna z krótkim domknięciem), ms. */
|
|
3
|
+
export const KPT_ZOOM_OPEN_MS = 640;
|
|
4
|
+
export const KPT_ZOOM_CLOSE_MS = 460;
|
|
5
|
+
/** Czas przenikania - wariant `fade` i "ogranicz ruch". */
|
|
6
|
+
export const KPT_ZOOM_FADE_MS = 180;
|
|
7
|
+
/**
|
|
8
|
+
* Położenie startowe lotu. Przy zerowej szerokości celu (zdjęcie jeszcze bez wymiarów) skala
|
|
9
|
+
* zostaje 1 - lepiej przesunięcie bez skali niż dzielenie przez zero.
|
|
10
|
+
*/
|
|
11
|
+
export function zoomFlight(from, to, thumbRadius) {
|
|
12
|
+
const scale = to.width > 0 ? from.width / to.width : 1;
|
|
13
|
+
const dx = from.x + from.width / 2 - (to.x + to.width / 2);
|
|
14
|
+
const dy = from.y + from.height / 2 - (to.y + to.height / 2);
|
|
15
|
+
return {
|
|
16
|
+
transform: `translate(${round(dx, 1)}px, ${round(dy, 1)}px) scale(${round(scale, 4)})`,
|
|
17
|
+
scale,
|
|
18
|
+
radiusFrom: scale > 0 ? round(thumbRadius / scale, 2) : thumbRadius,
|
|
19
|
+
};
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@konce-pt/motion",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Motion core for Koncept UI - spring curves sampled to linear()
|
|
3
|
+
"version": "0.18.0",
|
|
4
|
+
"description": "Motion core for Koncept UI - spring curves sampled to linear(), FLIP flight plans for layout changes, loader, image zoom and gallery motion. Framework-free TypeScript, zero runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "konce.pt",
|
|
7
7
|
"homepage": "https://ui.konce.pt/",
|