zkit-ui 2.0.5 → 2.0.7
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 +1 -1
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryLayoutSupport.java → NativeImagePreviewLayoutSupport.java} +12 -12
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryMediaItem.java → NativeImagePreviewMediaItem.java} +2 -2
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewModule.kt +43 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryPagerAdapter.java → NativeImagePreviewPagerAdapter.java} +18 -18
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GallerySharedElementGeometry.java → NativeImagePreviewSharedElementGeometry.java} +2 -2
- package/android/src/main/java/cn/fontree/zkit/ui/preview/NativeImagePreviewSharedElementNames.java +39 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GallerySharedElementState.java → NativeImagePreviewSharedElementState.java} +4 -4
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GallerySourceViewRegistry.java → NativeImagePreviewSourceViewRegistry.java} +2 -2
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryTransitionSnapshotView.java → NativeImagePreviewTransitionSnapshotView.java} +3 -3
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryVideoPlayerView.java → NativeImagePreviewVideoPlayerView.java} +7 -7
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GaleriaView.kt → NativeImagePreviewView.kt} +18 -18
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryViewerOverlayView.java → NativeImagePreviewViewerOverlayView.java} +49 -49
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryViewerPageView.java → NativeImagePreviewViewerPageView.java} +27 -27
- package/android/src/main/java/cn/fontree/zkit/ui/preview/{GalleryViewerTransitionController.java → NativeImagePreviewViewerTransitionController.java} +22 -22
- package/dist/ui/Button/index.d.ts +1 -1
- package/dist/ui/Button/index.d.ts.map +1 -1
- package/dist/ui/Button/index.js +18 -3
- package/dist/ui/NativeImagePreview/NativeImagePreview.js +6 -6
- package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts +1 -0
- package/dist/ui/NativeImagePreview/NativeImagePreview.web.d.ts.map +1 -1
- package/dist/ui/NativeImagePreview/NativeImagePreview.web.js +105 -11
- package/dist/ui/NativeImagePreview/context.d.ts +1 -1
- package/dist/ui/NativeImagePreview/context.d.ts.map +1 -1
- package/dist/ui/NativeImagePreview/types.d.ts +3 -1
- package/dist/ui/NativeImagePreview/types.d.ts.map +1 -1
- package/dist/ui/NativeImagePreview/utils.d.ts.map +1 -1
- package/dist/ui/NativeImagePreview/utils.js +1 -0
- package/dist/ui/NativeImagePreview/web/geometry.d.ts +20 -0
- package/dist/ui/NativeImagePreview/web/geometry.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/geometry.js +121 -0
- package/dist/ui/NativeImagePreview/web/media.d.ts +6 -0
- package/dist/ui/NativeImagePreview/web/media.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/media.js +90 -0
- package/dist/ui/NativeImagePreview/web/styles.css +137 -0
- package/dist/ui/NativeImagePreview/web/transition.d.ts +15 -0
- package/dist/ui/NativeImagePreview/web/transition.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/transition.js +77 -0
- package/dist/ui/NativeImagePreview/web/types.d.ts +74 -0
- package/dist/ui/NativeImagePreview/web/types.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/types.js +3 -0
- package/dist/ui/NativeImagePreview/web/viewer.d.ts +104 -0
- package/dist/ui/NativeImagePreview/web/viewer.d.ts.map +1 -0
- package/dist/ui/NativeImagePreview/web/viewer.js +894 -0
- package/expo-module.config.json +2 -2
- package/ios/{GalleryViewer/GaleriaImageLoader.swift → NativeImagePreview/NativeImagePreviewImageLoader.swift} +7 -7
- package/ios/NativeImagePreview/NativeImagePreviewImageViewInteraction.swift +245 -0
- package/ios/{GalleryViewer/GaleriaLayoutSupport.swift → NativeImagePreview/NativeImagePreviewLayoutSupport.swift} +19 -19
- package/ios/NativeImagePreview/NativeImagePreviewMedia.swift +28 -0
- package/ios/NativeImagePreview/NativeImagePreviewSourceViewRegistry.swift +80 -0
- package/ios/{GalleryViewer/GaleriaVideoPlayerView.swift → NativeImagePreview/NativeImagePreviewVideoPlayerView.swift} +1 -1
- package/ios/{GalleryViewer/GaleriaViewerConfiguration.swift → NativeImagePreview/NativeImagePreviewViewerConfiguration.swift} +7 -7
- package/ios/{GalleryViewer/GaleriaViewerController.swift → NativeImagePreview/NativeImagePreviewViewerController.swift} +36 -36
- package/ios/{GalleryViewer/GaleriaViewerPageView.swift → NativeImagePreview/NativeImagePreviewViewerPageView.swift} +33 -33
- package/ios/{GalleryViewer/GaleriaViewerTransitionCoordinator.swift → NativeImagePreview/NativeImagePreviewViewerTransitionCoordinator.swift} +16 -16
- package/ios/{GaleriaModule.swift → NativeImagePreviewModule.swift} +4 -4
- package/ios/{GaleriaView.swift → NativeImagePreviewView.swift} +26 -27
- package/package.json +18 -12
- package/src/services/ImagePreviewService/README.md +1 -1
- package/src/types.d.ts +1 -0
- package/src/ui/Button/README.md +1 -1
- package/src/ui/Button/index.tsx +27 -7
- package/src/ui/NativeImagePreview/NativeImagePreview.tsx +7 -7
- package/src/ui/NativeImagePreview/NativeImagePreview.web.tsx +175 -11
- package/src/ui/NativeImagePreview/README.md +3 -3
- package/src/ui/NativeImagePreview/context.tsx +1 -1
- package/src/ui/NativeImagePreview/types.ts +3 -1
- package/src/ui/NativeImagePreview/utils.ts +1 -0
- package/src/ui/NativeImagePreview/web/geometry.ts +134 -0
- package/src/ui/NativeImagePreview/web/media.ts +91 -0
- package/src/ui/NativeImagePreview/web/styles.css +137 -0
- package/src/ui/NativeImagePreview/web/transition.ts +111 -0
- package/src/ui/NativeImagePreview/web/types.ts +88 -0
- package/src/ui/NativeImagePreview/web/viewer.ts +1024 -0
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GaleriaModule.kt +0 -43
- package/android/src/main/java/cn/fontree/zkit/ui/preview/GallerySharedElementNames.java +0 -39
- package/ios/GalleryViewer/GaleriaImageViewInteraction.swift +0 -245
- package/ios/GalleryViewer/GaleriaMedia.swift +0 -28
- package/ios/GalleryViewer/GaleriaSourceViewRegistry.swift +0 -80
- /package/android/src/main/res/layout/{galeria_player_view.xml → native_image_preview_player_view.xml} +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
import type { NativeImagePreviewWebImageSize, NativeImagePreviewWebObjectFit, NativeImagePreviewWebRect } from './types';
|
|
4
|
+
|
|
5
|
+
export type SharedElementGeometry = {
|
|
6
|
+
visibleFrame: NativeImagePreviewWebRect;
|
|
7
|
+
contentFrame: NativeImagePreviewWebRect;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function rectFromDOMRect(rect: DOMRectReadOnly | NativeImagePreviewWebRect): NativeImagePreviewWebRect {
|
|
11
|
+
return normalizeRect({
|
|
12
|
+
left: rect.left,
|
|
13
|
+
top: rect.top,
|
|
14
|
+
width: rect.width,
|
|
15
|
+
height: rect.height,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function rectFromElement(element: Element): NativeImagePreviewWebRect {
|
|
20
|
+
return rectFromDOMRect(element.getBoundingClientRect());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function viewportRect(): NativeImagePreviewWebRect {
|
|
24
|
+
return normalizeRect({
|
|
25
|
+
left: 0,
|
|
26
|
+
top: 0,
|
|
27
|
+
width: window.innerWidth,
|
|
28
|
+
height: window.innerHeight,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeRect(input: Pick<NativeImagePreviewWebRect, 'left' | 'top' | 'width' | 'height'>): NativeImagePreviewWebRect {
|
|
33
|
+
return {
|
|
34
|
+
left: input.left,
|
|
35
|
+
top: input.top,
|
|
36
|
+
width: input.width,
|
|
37
|
+
height: input.height,
|
|
38
|
+
right: input.left + input.width,
|
|
39
|
+
bottom: input.top + input.height,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isUsableRect(rect: NativeImagePreviewWebRect | null | undefined): rect is NativeImagePreviewWebRect {
|
|
44
|
+
return !!rect && rect.width > 1 && rect.height > 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
48
|
+
return Math.max(min, Math.min(max, value));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function fitRect(
|
|
52
|
+
mediaSize: NativeImagePreviewWebImageSize,
|
|
53
|
+
container: NativeImagePreviewWebRect,
|
|
54
|
+
objectFit: NativeImagePreviewWebObjectFit
|
|
55
|
+
): NativeImagePreviewWebRect {
|
|
56
|
+
if (objectFit === 'fill') {
|
|
57
|
+
return normalizeRect(container);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const widthScale = container.width / Math.max(1, mediaSize.width);
|
|
61
|
+
const heightScale = container.height / Math.max(1, mediaSize.height);
|
|
62
|
+
const scale = objectFit === 'cover'
|
|
63
|
+
? Math.max(widthScale, heightScale)
|
|
64
|
+
: Math.min(widthScale, heightScale);
|
|
65
|
+
const width = mediaSize.width * scale;
|
|
66
|
+
const height = mediaSize.height * scale;
|
|
67
|
+
return normalizeRect({
|
|
68
|
+
left: container.left + (container.width - width) / 2,
|
|
69
|
+
top: container.top + (container.height - height) / 2,
|
|
70
|
+
width,
|
|
71
|
+
height,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function sharedGeometryFor(
|
|
76
|
+
container: NativeImagePreviewWebRect,
|
|
77
|
+
mediaSize: NativeImagePreviewWebImageSize,
|
|
78
|
+
objectFit: NativeImagePreviewWebObjectFit
|
|
79
|
+
): SharedElementGeometry {
|
|
80
|
+
const contentFrameInWindow = fitRect(mediaSize, container, objectFit);
|
|
81
|
+
const visibleFrame = intersectRects(container, contentFrameInWindow) ?? normalizeRect(container);
|
|
82
|
+
const contentFrame = normalizeRect({
|
|
83
|
+
left: contentFrameInWindow.left - visibleFrame.left,
|
|
84
|
+
top: contentFrameInWindow.top - visibleFrame.top,
|
|
85
|
+
width: contentFrameInWindow.width,
|
|
86
|
+
height: contentFrameInWindow.height,
|
|
87
|
+
});
|
|
88
|
+
return { visibleFrame, contentFrame };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function targetGeometryFor(mediaSize: NativeImagePreviewWebImageSize, container: NativeImagePreviewWebRect): SharedElementGeometry {
|
|
92
|
+
const visibleFrame = fitRect(mediaSize, container, 'contain');
|
|
93
|
+
return {
|
|
94
|
+
visibleFrame,
|
|
95
|
+
contentFrame: normalizeRect({
|
|
96
|
+
left: 0,
|
|
97
|
+
top: 0,
|
|
98
|
+
width: visibleFrame.width,
|
|
99
|
+
height: visibleFrame.height,
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function intersectRects(first: NativeImagePreviewWebRect, second: NativeImagePreviewWebRect): NativeImagePreviewWebRect | null {
|
|
105
|
+
const left = Math.max(first.left, second.left);
|
|
106
|
+
const top = Math.max(first.top, second.top);
|
|
107
|
+
const right = Math.min(first.right, second.right);
|
|
108
|
+
const bottom = Math.min(first.bottom, second.bottom);
|
|
109
|
+
if (right <= left || bottom <= top) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return normalizeRect({ left, top, width: right - left, height: bottom - top });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function lerp(start: number, end: number, progress: number): number {
|
|
116
|
+
return start + (end - start) * progress;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function lerpRect(start: NativeImagePreviewWebRect, end: NativeImagePreviewWebRect, progress: number): NativeImagePreviewWebRect {
|
|
120
|
+
return normalizeRect({
|
|
121
|
+
left: lerp(start.left, end.left, progress),
|
|
122
|
+
top: lerp(start.top, end.top, progress),
|
|
123
|
+
width: lerp(start.width, end.width, progress),
|
|
124
|
+
height: lerp(start.height, end.height, progress),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function easeOutCubic(t: number): number {
|
|
129
|
+
return 1 - Math.pow(1 - t, 3);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function easeOutQuart(t: number): number {
|
|
133
|
+
return 1 - Math.pow(1 - t, 4);
|
|
134
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
import type { NativeImagePreviewWebImageSize, NativeImagePreviewWebMediaItem, NativeImagePreviewWebMediaType, NativeImagePreviewWebResolvedItem } from './types';
|
|
4
|
+
|
|
5
|
+
export function normalizeItems(items: readonly NativeImagePreviewWebMediaItem[]): NativeImagePreviewWebResolvedItem[] {
|
|
6
|
+
return items
|
|
7
|
+
.map((item, index): NativeImagePreviewWebResolvedItem | null => {
|
|
8
|
+
const src = item.src ?? item.url ?? '';
|
|
9
|
+
if (!src.trim()) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const type = resolveType(item);
|
|
13
|
+
return {
|
|
14
|
+
id: item.id ?? String(index),
|
|
15
|
+
type,
|
|
16
|
+
src,
|
|
17
|
+
poster: item.poster,
|
|
18
|
+
width: item.width,
|
|
19
|
+
height: item.height,
|
|
20
|
+
alt: item.alt ?? `Image ${index + 1}`,
|
|
21
|
+
raw: item,
|
|
22
|
+
};
|
|
23
|
+
})
|
|
24
|
+
.filter((item): item is NativeImagePreviewWebResolvedItem => item !== null);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function previewSourceFor(item: NativeImagePreviewWebResolvedItem): string {
|
|
28
|
+
return item.type === 'video' ? item.poster ?? item.src : item.src;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function resolveMediaSize(
|
|
32
|
+
item: NativeImagePreviewWebResolvedItem,
|
|
33
|
+
fallback?: NativeImagePreviewWebImageSize | null
|
|
34
|
+
): Promise<NativeImagePreviewWebImageSize> {
|
|
35
|
+
if (fallback && fallback.width > 0 && fallback.height > 0) {
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
38
|
+
if (item.width && item.height) {
|
|
39
|
+
return { width: item.width, height: item.height };
|
|
40
|
+
}
|
|
41
|
+
const previewSource = previewSourceFor(item);
|
|
42
|
+
if (!previewSource) {
|
|
43
|
+
return { width: 1, height: 1 };
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return await loadImageSize(previewSource);
|
|
47
|
+
} catch {
|
|
48
|
+
return { width: 1, height: 1 };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function waitForElementImage(element: Element | null | undefined): Promise<void> {
|
|
53
|
+
if (!(element instanceof HTMLImageElement)) {
|
|
54
|
+
return Promise.resolve();
|
|
55
|
+
}
|
|
56
|
+
if (element.complete && element.naturalWidth > 0) {
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
}
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const done = () => {
|
|
61
|
+
element.removeEventListener('load', done);
|
|
62
|
+
element.removeEventListener('error', done);
|
|
63
|
+
resolve();
|
|
64
|
+
};
|
|
65
|
+
element.addEventListener('load', done, { once: true });
|
|
66
|
+
element.addEventListener('error', done, { once: true });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveType(item: NativeImagePreviewWebMediaItem): NativeImagePreviewWebMediaType {
|
|
71
|
+
if (item.type === 'video') {
|
|
72
|
+
return 'video';
|
|
73
|
+
}
|
|
74
|
+
const src = item.src ?? item.url ?? '';
|
|
75
|
+
return /\.(mp4|webm|ogg)(\?.*)?$/i.test(src) ? 'video' : 'image';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function loadImageSize(src: string): Promise<NativeImagePreviewWebImageSize> {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const image = new Image();
|
|
81
|
+
image.decoding = 'async';
|
|
82
|
+
image.onload = () => {
|
|
83
|
+
resolve({
|
|
84
|
+
width: image.naturalWidth || image.width || 1,
|
|
85
|
+
height: image.naturalHeight || image.height || 1,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
image.onerror = () => reject(new Error(`Unable to load image: ${src}`));
|
|
89
|
+
image.src = src;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
.native-image-preview-root {
|
|
2
|
+
position: fixed;
|
|
3
|
+
top: 0;
|
|
4
|
+
left: 0;
|
|
5
|
+
z-index: 2147483000;
|
|
6
|
+
width: var(--native-image-preview-vw, 100vw);
|
|
7
|
+
height: var(--native-image-preview-vh, 100vh);
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
color: #fff;
|
|
10
|
+
contain: layout size paint;
|
|
11
|
+
overscroll-behavior: none;
|
|
12
|
+
touch-action: none;
|
|
13
|
+
user-select: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.native-image-preview-backdrop {
|
|
17
|
+
position: absolute;
|
|
18
|
+
inset: 0;
|
|
19
|
+
background: #000;
|
|
20
|
+
will-change: opacity;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.native-image-preview-theme-light .native-image-preview-backdrop {
|
|
24
|
+
background: #fff;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.native-image-preview-viewport {
|
|
28
|
+
position: absolute;
|
|
29
|
+
inset: 0;
|
|
30
|
+
z-index: 1;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.native-image-preview-track {
|
|
35
|
+
position: absolute;
|
|
36
|
+
top: 0;
|
|
37
|
+
left: 0;
|
|
38
|
+
display: flex;
|
|
39
|
+
width: var(--native-image-preview-vw, 100vw);
|
|
40
|
+
height: var(--native-image-preview-vh, 100vh);
|
|
41
|
+
will-change: transform, opacity;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.native-image-preview-slide {
|
|
45
|
+
position: relative;
|
|
46
|
+
display: grid;
|
|
47
|
+
flex: 0 0 var(--native-image-preview-vw, 100vw);
|
|
48
|
+
width: var(--native-image-preview-vw, 100vw);
|
|
49
|
+
height: var(--native-image-preview-vh, 100vh);
|
|
50
|
+
place-items: center;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.native-image-preview-media {
|
|
55
|
+
display: block;
|
|
56
|
+
max-width: var(--native-image-preview-vw, 100vw);
|
|
57
|
+
max-height: var(--native-image-preview-vh, 100vh);
|
|
58
|
+
object-fit: contain;
|
|
59
|
+
transform-origin: center center;
|
|
60
|
+
will-change: transform;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.native-image-preview-image {
|
|
64
|
+
width: auto;
|
|
65
|
+
height: auto;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.native-image-preview-video {
|
|
69
|
+
width: auto;
|
|
70
|
+
height: auto;
|
|
71
|
+
background: #000;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.native-image-preview-close {
|
|
75
|
+
position: fixed;
|
|
76
|
+
top: max(12px, env(safe-area-inset-top));
|
|
77
|
+
left: max(12px, env(safe-area-inset-left));
|
|
78
|
+
z-index: 3;
|
|
79
|
+
display: grid;
|
|
80
|
+
width: 42px;
|
|
81
|
+
height: 42px;
|
|
82
|
+
padding: 0;
|
|
83
|
+
border: 0;
|
|
84
|
+
border-radius: 999px;
|
|
85
|
+
place-items: center;
|
|
86
|
+
background: rgba(255, 255, 255, 0.12);
|
|
87
|
+
color: #fff;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
font-size: 30px;
|
|
90
|
+
line-height: 1;
|
|
91
|
+
backdrop-filter: blur(18px);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.native-image-preview-counter {
|
|
95
|
+
position: fixed;
|
|
96
|
+
right: max(14px, env(safe-area-inset-right));
|
|
97
|
+
bottom: max(18px, env(safe-area-inset-bottom));
|
|
98
|
+
z-index: 3;
|
|
99
|
+
min-width: 58px;
|
|
100
|
+
padding: 8px 11px;
|
|
101
|
+
border-radius: 999px;
|
|
102
|
+
background: rgba(255, 255, 255, 0.12);
|
|
103
|
+
color: #fff;
|
|
104
|
+
font-size: 13px;
|
|
105
|
+
font-weight: 720;
|
|
106
|
+
text-align: center;
|
|
107
|
+
backdrop-filter: blur(18px);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.native-image-preview-fly-shell {
|
|
111
|
+
position: fixed;
|
|
112
|
+
top: 0;
|
|
113
|
+
left: 0;
|
|
114
|
+
z-index: 2;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
border-radius: 0;
|
|
117
|
+
pointer-events: none;
|
|
118
|
+
transform-origin: top left;
|
|
119
|
+
will-change: transform, width, height;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.native-image-preview-fly-media {
|
|
123
|
+
position: absolute;
|
|
124
|
+
top: 0;
|
|
125
|
+
left: 0;
|
|
126
|
+
display: block;
|
|
127
|
+
max-width: none;
|
|
128
|
+
max-height: none;
|
|
129
|
+
object-fit: fill;
|
|
130
|
+
transform-origin: top left;
|
|
131
|
+
will-change: transform, width, height;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.native-image-preview-fading-out {
|
|
135
|
+
opacity: 0;
|
|
136
|
+
transition: opacity 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
137
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
import type { NativeImagePreviewWebResolvedItem } from './types';
|
|
4
|
+
import { easeOutCubic, lerpRect, type SharedElementGeometry } from './geometry';
|
|
5
|
+
import { previewSourceFor } from './media';
|
|
6
|
+
|
|
7
|
+
type SharedTransitionOptions = {
|
|
8
|
+
item: NativeImagePreviewWebResolvedItem;
|
|
9
|
+
from: SharedElementGeometry;
|
|
10
|
+
to: SharedElementGeometry;
|
|
11
|
+
duration?: number;
|
|
12
|
+
parent?: HTMLElement;
|
|
13
|
+
beforeStart?: () => void | Promise<void>;
|
|
14
|
+
onProgress?: (progress: number) => void;
|
|
15
|
+
beforeRemove?: () => void | Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function animateSharedElement({
|
|
19
|
+
item,
|
|
20
|
+
from,
|
|
21
|
+
to,
|
|
22
|
+
duration = 320,
|
|
23
|
+
parent = document.body,
|
|
24
|
+
beforeStart,
|
|
25
|
+
onProgress,
|
|
26
|
+
beforeRemove,
|
|
27
|
+
}: SharedTransitionOptions): Promise<void> {
|
|
28
|
+
const shell = document.createElement('div');
|
|
29
|
+
shell.className = 'native-image-preview-fly-shell';
|
|
30
|
+
shell.style.opacity = '0';
|
|
31
|
+
|
|
32
|
+
const media = document.createElement('img');
|
|
33
|
+
media.className = 'native-image-preview-fly-media';
|
|
34
|
+
media.alt = item.alt;
|
|
35
|
+
media.draggable = false;
|
|
36
|
+
media.src = previewSourceFor(item);
|
|
37
|
+
|
|
38
|
+
shell.append(media);
|
|
39
|
+
applyGeometry(shell, media, from);
|
|
40
|
+
parent.append(shell);
|
|
41
|
+
await waitForFlyMedia(media);
|
|
42
|
+
shell.style.opacity = '1';
|
|
43
|
+
await waitFrames(1);
|
|
44
|
+
await beforeStart?.();
|
|
45
|
+
|
|
46
|
+
const startedAt = performance.now();
|
|
47
|
+
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const render = async (now: number) => {
|
|
50
|
+
const raw = Math.min(1, (now - startedAt) / duration);
|
|
51
|
+
const progress = easeOutCubic(raw);
|
|
52
|
+
applyGeometry(shell, media, {
|
|
53
|
+
visibleFrame: lerpRect(from.visibleFrame, to.visibleFrame, progress),
|
|
54
|
+
contentFrame: lerpRect(from.contentFrame, to.contentFrame, progress),
|
|
55
|
+
});
|
|
56
|
+
onProgress?.(progress);
|
|
57
|
+
|
|
58
|
+
if (raw < 1) {
|
|
59
|
+
requestAnimationFrame(render);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await beforeRemove?.();
|
|
63
|
+
await waitFrames(1);
|
|
64
|
+
requestAnimationFrame(() => {
|
|
65
|
+
shell.remove();
|
|
66
|
+
resolve();
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
requestAnimationFrame(render);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function applyGeometry(
|
|
75
|
+
shell: HTMLElement,
|
|
76
|
+
media: HTMLImageElement,
|
|
77
|
+
geometry: SharedElementGeometry
|
|
78
|
+
): void {
|
|
79
|
+
const shellRect = geometry.visibleFrame;
|
|
80
|
+
const mediaRect = geometry.contentFrame;
|
|
81
|
+
shell.style.transform = `translate3d(${shellRect.left}px, ${shellRect.top}px, 0)`;
|
|
82
|
+
shell.style.width = `${shellRect.width}px`;
|
|
83
|
+
shell.style.height = `${shellRect.height}px`;
|
|
84
|
+
media.style.transform = `translate3d(${mediaRect.left}px, ${mediaRect.top}px, 0)`;
|
|
85
|
+
media.style.width = `${mediaRect.width}px`;
|
|
86
|
+
media.style.height = `${mediaRect.height}px`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function waitFrames(count: number): Promise<void> {
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
const step = (remaining: number) => {
|
|
92
|
+
if (remaining <= 0) {
|
|
93
|
+
resolve();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
requestAnimationFrame(() => step(remaining - 1));
|
|
97
|
+
};
|
|
98
|
+
step(count);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function waitForFlyMedia(image: HTMLImageElement): Promise<void> {
|
|
103
|
+
if (image.complete && image.naturalWidth > 0) {
|
|
104
|
+
return Promise.resolve();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return Promise.race([
|
|
108
|
+
image.decode().catch(() => undefined),
|
|
109
|
+
new Promise<void>((resolve) => window.setTimeout(resolve, 180)),
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
|
|
3
|
+
export type NativeImagePreviewWebTheme = 'dark' | 'light';
|
|
4
|
+
|
|
5
|
+
export type NativeImagePreviewWebMediaType = 'image' | 'video';
|
|
6
|
+
|
|
7
|
+
export type NativeImagePreviewWebObjectFit = 'cover' | 'contain' | 'fill';
|
|
8
|
+
|
|
9
|
+
export type NativeImagePreviewWebSourceVisibility = 'hidden' | 'visible';
|
|
10
|
+
|
|
11
|
+
export type NativeImagePreviewWebCloseReason = 'api' | 'backdrop' | 'button' | 'drag' | 'escape' | 'replace';
|
|
12
|
+
|
|
13
|
+
export type NativeImagePreviewWebChangeReason = 'api' | 'swipe';
|
|
14
|
+
|
|
15
|
+
export type NativeImagePreviewWebRect = {
|
|
16
|
+
left: number;
|
|
17
|
+
top: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
right: number;
|
|
21
|
+
bottom: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type NativeImagePreviewWebImageSize = {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type NativeImagePreviewWebMediaItem = {
|
|
30
|
+
id?: string;
|
|
31
|
+
type?: NativeImagePreviewWebMediaType;
|
|
32
|
+
url?: string;
|
|
33
|
+
src?: string;
|
|
34
|
+
poster?: string;
|
|
35
|
+
width?: number;
|
|
36
|
+
height?: number;
|
|
37
|
+
alt?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type NativeImagePreviewWebResolvedItem = {
|
|
41
|
+
id: string;
|
|
42
|
+
type: NativeImagePreviewWebMediaType;
|
|
43
|
+
src: string;
|
|
44
|
+
poster?: string;
|
|
45
|
+
width?: number;
|
|
46
|
+
height?: number;
|
|
47
|
+
alt: string;
|
|
48
|
+
raw: NativeImagePreviewWebMediaItem;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type NativeImagePreviewWebChangeMeta = {
|
|
52
|
+
reason: NativeImagePreviewWebChangeReason;
|
|
53
|
+
previousIndex: number;
|
|
54
|
+
index: number;
|
|
55
|
+
item: NativeImagePreviewWebResolvedItem;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type NativeImagePreviewWebCloseMeta = {
|
|
59
|
+
reason: NativeImagePreviewWebCloseReason;
|
|
60
|
+
index: number;
|
|
61
|
+
item: NativeImagePreviewWebResolvedItem | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type NativeImagePreviewWebOpenOptions = {
|
|
65
|
+
items: readonly NativeImagePreviewWebMediaItem[];
|
|
66
|
+
index?: number;
|
|
67
|
+
sourceElement?: HTMLElement | null;
|
|
68
|
+
sourceElements?: readonly (HTMLElement | null | undefined)[];
|
|
69
|
+
getSourceElement?: (index: number, item: NativeImagePreviewWebResolvedItem) => HTMLElement | null | undefined;
|
|
70
|
+
sourceRect?: NativeImagePreviewWebRect | DOMRectReadOnly | null;
|
|
71
|
+
sourceImageSize?: NativeImagePreviewWebImageSize | null;
|
|
72
|
+
objectFit?: NativeImagePreviewWebObjectFit;
|
|
73
|
+
sourceObjectFit?: NativeImagePreviewWebObjectFit;
|
|
74
|
+
sourceVisibility?: NativeImagePreviewWebSourceVisibility;
|
|
75
|
+
theme?: NativeImagePreviewWebTheme;
|
|
76
|
+
closeButton?: boolean;
|
|
77
|
+
counter?: boolean;
|
|
78
|
+
onChange?: (index: number, meta: NativeImagePreviewWebChangeMeta) => void;
|
|
79
|
+
onClose?: (meta: NativeImagePreviewWebCloseMeta) => void;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type NativeImagePreviewWebController = {
|
|
83
|
+
open: (options: NativeImagePreviewWebOpenOptions) => Promise<void>;
|
|
84
|
+
close: (reason?: NativeImagePreviewWebCloseReason) => Promise<void>;
|
|
85
|
+
destroy: () => void;
|
|
86
|
+
getIndex: () => number;
|
|
87
|
+
isOpen: () => boolean;
|
|
88
|
+
};
|