@delightstack/components 0.1.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 +136 -0
- package/SKILL.md +149 -0
- package/bin/agents.js +63 -0
- package/dist/actions/Alert.svelte +202 -0
- package/dist/actions/Alert.svelte.d.ts +36 -0
- package/dist/actions/Alert.svelte.d.ts.map +1 -0
- package/dist/actions/Button.svelte +1450 -0
- package/dist/actions/Button.svelte.d.ts +56 -0
- package/dist/actions/Button.svelte.d.ts.map +1 -0
- package/dist/actions/ButtonGroup.svelte +111 -0
- package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
- package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
- package/dist/actions/CommandPalette.svelte +939 -0
- package/dist/actions/CommandPalette.svelte.d.ts +37 -0
- package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/actions/ContextMenu.svelte +138 -0
- package/dist/actions/ContextMenu.svelte.d.ts +54 -0
- package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
- package/dist/actions/Modal.svelte +474 -0
- package/dist/actions/Modal.svelte.d.ts +28 -0
- package/dist/actions/Modal.svelte.d.ts.map +1 -0
- package/dist/actions/Popover.svelte +1214 -0
- package/dist/actions/Popover.svelte.d.ts +31 -0
- package/dist/actions/Popover.svelte.d.ts.map +1 -0
- package/dist/actions/Portal.svelte +80 -0
- package/dist/actions/Portal.svelte.d.ts +17 -0
- package/dist/actions/Portal.svelte.d.ts.map +1 -0
- package/dist/actions/ThemeToggle.svelte +345 -0
- package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
- package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
- package/dist/actions/index.d.ts +13 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/scrollbar.d.ts +48 -0
- package/dist/actions/scrollbar.d.ts.map +1 -0
- package/dist/actions/scrollbar.js +404 -0
- package/dist/display/Accordion.svelte +586 -0
- package/dist/display/Accordion.svelte.d.ts +41 -0
- package/dist/display/Accordion.svelte.d.ts.map +1 -0
- package/dist/display/Avatar.svelte +527 -0
- package/dist/display/Avatar.svelte.d.ts +22 -0
- package/dist/display/Avatar.svelte.d.ts.map +1 -0
- package/dist/display/AvatarGroup.svelte +298 -0
- package/dist/display/AvatarGroup.svelte.d.ts +31 -0
- package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
- package/dist/display/Calendar.svelte +1366 -0
- package/dist/display/Calendar.svelte.d.ts +58 -0
- package/dist/display/Calendar.svelte.d.ts.map +1 -0
- package/dist/display/Chart.svelte +1426 -0
- package/dist/display/Chart.svelte.d.ts +35 -0
- package/dist/display/Chart.svelte.d.ts.map +1 -0
- package/dist/display/Code.svelte +780 -0
- package/dist/display/Code.svelte.d.ts +19 -0
- package/dist/display/Code.svelte.d.ts.map +1 -0
- package/dist/display/Comparison.svelte +686 -0
- package/dist/display/Comparison.svelte.d.ts +22 -0
- package/dist/display/Comparison.svelte.d.ts.map +1 -0
- package/dist/display/Counter.svelte +285 -0
- package/dist/display/Counter.svelte.d.ts +21 -0
- package/dist/display/Counter.svelte.d.ts.map +1 -0
- package/dist/display/Expand.svelte +48 -0
- package/dist/display/Expand.svelte.d.ts +9 -0
- package/dist/display/Expand.svelte.d.ts.map +1 -0
- package/dist/display/List.svelte +294 -0
- package/dist/display/List.svelte.d.ts +40 -0
- package/dist/display/List.svelte.d.ts.map +1 -0
- package/dist/display/ListContextReset.svelte +19 -0
- package/dist/display/ListContextReset.svelte.d.ts +7 -0
- package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
- package/dist/display/ListItem.svelte +834 -0
- package/dist/display/ListItem.svelte.d.ts +22 -0
- package/dist/display/ListItem.svelte.d.ts.map +1 -0
- package/dist/display/QR.svelte +1193 -0
- package/dist/display/QR.svelte.d.ts +23 -0
- package/dist/display/QR.svelte.d.ts.map +1 -0
- package/dist/display/SplitPane.svelte +744 -0
- package/dist/display/SplitPane.svelte.d.ts +25 -0
- package/dist/display/SplitPane.svelte.d.ts.map +1 -0
- package/dist/display/Stat.svelte +439 -0
- package/dist/display/Stat.svelte.d.ts +24 -0
- package/dist/display/Stat.svelte.d.ts.map +1 -0
- package/dist/display/Table.svelte +4654 -0
- package/dist/display/Table.svelte.d.ts +249 -0
- package/dist/display/Table.svelte.d.ts.map +1 -0
- package/dist/display/TableCellEditor.svelte +935 -0
- package/dist/display/TableCellEditor.svelte.d.ts +58 -0
- package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
- package/dist/display/Timeline.svelte +1258 -0
- package/dist/display/Timeline.svelte.d.ts +43 -0
- package/dist/display/Timeline.svelte.d.ts.map +1 -0
- package/dist/display/Tree.svelte +1740 -0
- package/dist/display/Tree.svelte.d.ts +74 -0
- package/dist/display/Tree.svelte.d.ts.map +1 -0
- package/dist/display/Typewriter.svelte +338 -0
- package/dist/display/Typewriter.svelte.d.ts +22 -0
- package/dist/display/Typewriter.svelte.d.ts.map +1 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +18 -0
- package/dist/feedback/Callout.svelte +529 -0
- package/dist/feedback/Callout.svelte.d.ts +24 -0
- package/dist/feedback/Callout.svelte.d.ts.map +1 -0
- package/dist/feedback/Confetti.svelte +631 -0
- package/dist/feedback/Confetti.svelte.d.ts +90 -0
- package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
- package/dist/feedback/Progress.svelte +382 -0
- package/dist/feedback/Progress.svelte.d.ts +25 -0
- package/dist/feedback/Progress.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast.svelte +967 -0
- package/dist/feedback/Toast.svelte.d.ts +54 -0
- package/dist/feedback/Toast.svelte.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/index.js +4 -0
- package/dist/form/Checkbox.svelte +449 -0
- package/dist/form/Checkbox.svelte.d.ts +27 -0
- package/dist/form/Checkbox.svelte.d.ts.map +1 -0
- package/dist/form/Fieldset.svelte +410 -0
- package/dist/form/Fieldset.svelte.d.ts +22 -0
- package/dist/form/Fieldset.svelte.d.ts.map +1 -0
- package/dist/form/FileUpload.svelte +934 -0
- package/dist/form/FileUpload.svelte.d.ts +41 -0
- package/dist/form/FileUpload.svelte.d.ts.map +1 -0
- package/dist/form/Form.svelte +530 -0
- package/dist/form/Form.svelte.d.ts +120 -0
- package/dist/form/Form.svelte.d.ts.map +1 -0
- package/dist/form/Input.svelte +2858 -0
- package/dist/form/Input.svelte.d.ts +66 -0
- package/dist/form/Input.svelte.d.ts.map +1 -0
- package/dist/form/Radio.svelte +507 -0
- package/dist/form/Radio.svelte.d.ts +39 -0
- package/dist/form/Radio.svelte.d.ts.map +1 -0
- package/dist/form/Range.svelte +912 -0
- package/dist/form/Range.svelte.d.ts +33 -0
- package/dist/form/Range.svelte.d.ts.map +1 -0
- package/dist/form/Rating.svelte +429 -0
- package/dist/form/Rating.svelte.d.ts +28 -0
- package/dist/form/Rating.svelte.d.ts.map +1 -0
- package/dist/form/Select.svelte +1933 -0
- package/dist/form/Select.svelte.d.ts +54 -0
- package/dist/form/Select.svelte.d.ts.map +1 -0
- package/dist/form/Toggle.svelte +645 -0
- package/dist/form/Toggle.svelte.d.ts +50 -0
- package/dist/form/Toggle.svelte.d.ts.map +1 -0
- package/dist/form/index.d.ts +15 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/layout/README.md +172 -0
- package/dist/media/Carousel.svelte +2424 -0
- package/dist/media/Carousel.svelte.d.ts +47 -0
- package/dist/media/Carousel.svelte.d.ts.map +1 -0
- package/dist/media/Gallery.svelte +2881 -0
- package/dist/media/Gallery.svelte.d.ts +82 -0
- package/dist/media/Gallery.svelte.d.ts.map +1 -0
- package/dist/media/Image.svelte +389 -0
- package/dist/media/Image.svelte.d.ts +33 -0
- package/dist/media/Image.svelte.d.ts.map +1 -0
- package/dist/media/PDF.svelte +1793 -0
- package/dist/media/PDF.svelte.d.ts +44 -0
- package/dist/media/PDF.svelte.d.ts.map +1 -0
- package/dist/media/Panorama.svelte +1391 -0
- package/dist/media/Panorama.svelte.d.ts +47 -0
- package/dist/media/Panorama.svelte.d.ts.map +1 -0
- package/dist/media/Video.svelte +2501 -0
- package/dist/media/Video.svelte.d.ts +58 -0
- package/dist/media/Video.svelte.d.ts.map +1 -0
- package/dist/media/carousel.d.ts +211 -0
- package/dist/media/carousel.d.ts.map +1 -0
- package/dist/media/carousel.js +408 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +5 -0
- package/dist/navigation/BottomSheet.svelte +636 -0
- package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
- package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
- package/dist/navigation/Breadcrumbs.svelte +611 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
- package/dist/navigation/Pagination.svelte +641 -0
- package/dist/navigation/Pagination.svelte.d.ts +27 -0
- package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
- package/dist/navigation/Steps.svelte +965 -0
- package/dist/navigation/Steps.svelte.d.ts +43 -0
- package/dist/navigation/Steps.svelte.d.ts.map +1 -0
- package/dist/navigation/Tabs.svelte +698 -0
- package/dist/navigation/Tabs.svelte.d.ts +41 -0
- package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
- package/dist/navigation/index.d.ts +8 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +5 -0
- package/package.json +139 -0
|
@@ -0,0 +1,2424 @@
|
|
|
1
|
+
<!-- svelte-ignore state_referenced_locally -->
|
|
2
|
+
<script lang="ts" module>
|
|
3
|
+
export type { CarouselItem, CarouselItemType, GalleryGesture } from './carousel';
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { backOut, circOut, circInOut, circIn } from 'svelte/easing';
|
|
8
|
+
import { onDestroy, tick, untrack, type Component, type Snippet } from 'svelte';
|
|
9
|
+
import {
|
|
10
|
+
addLoadedResolution,
|
|
11
|
+
animateElement,
|
|
12
|
+
calcBounds,
|
|
13
|
+
calcTransform,
|
|
14
|
+
type CarouselItem,
|
|
15
|
+
center,
|
|
16
|
+
clampMatrix,
|
|
17
|
+
createMatrix,
|
|
18
|
+
decodeThumbHash,
|
|
19
|
+
type ElementAnimationOptions,
|
|
20
|
+
extractMatrixTransform,
|
|
21
|
+
type GalleryGesture,
|
|
22
|
+
getLoadedResolutions,
|
|
23
|
+
isResponsiveSrcset,
|
|
24
|
+
isScalable,
|
|
25
|
+
isSwipeable,
|
|
26
|
+
isVideoEmbed,
|
|
27
|
+
normalizeCarouselItem,
|
|
28
|
+
normalizeEmbedSrc,
|
|
29
|
+
normalizeWheel,
|
|
30
|
+
pickLargestSrc,
|
|
31
|
+
type Point,
|
|
32
|
+
type Pointer,
|
|
33
|
+
type Transform,
|
|
34
|
+
} from './carousel';
|
|
35
|
+
|
|
36
|
+
const browser = typeof window !== 'undefined';
|
|
37
|
+
|
|
38
|
+
type RichRendererType = 'pdf' | 'panorama' | 'video';
|
|
39
|
+
|
|
40
|
+
/** Module-scope promise cache so multiple Carousel instances share one fetch per renderer. */
|
|
41
|
+
const richModulePromises: Record<
|
|
42
|
+
RichRendererType,
|
|
43
|
+
Promise<{ default: Component }> | null
|
|
44
|
+
> = {
|
|
45
|
+
pdf: null,
|
|
46
|
+
panorama: null,
|
|
47
|
+
video: null,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function loadRichRenderer(type: RichRendererType): Promise<{ default: Component }> {
|
|
51
|
+
if (!richModulePromises[type]) {
|
|
52
|
+
richModulePromises[type] =
|
|
53
|
+
type === 'pdf'
|
|
54
|
+
? (import('./PDF.svelte') as Promise<{ default: Component }>)
|
|
55
|
+
: type === 'panorama'
|
|
56
|
+
? (import('./Panorama.svelte') as Promise<{ default: Component }>)
|
|
57
|
+
: (import('./Video.svelte') as Promise<{ default: Component }>);
|
|
58
|
+
}
|
|
59
|
+
return richModulePromises[type]!;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let {
|
|
63
|
+
/** The currently displayed item index. Changing this will change/animate the slide */
|
|
64
|
+
slide = $bindable(0) as number,
|
|
65
|
+
|
|
66
|
+
/** The currently displayed page (a vertical carousel within the current slide - used for pdf pages) */
|
|
67
|
+
page = $bindable(0) as number,
|
|
68
|
+
|
|
69
|
+
/** The amount of pages available in the current slide (applies to PDFs) */
|
|
70
|
+
num_pages = $bindable(0) as number,
|
|
71
|
+
|
|
72
|
+
/** The percent (0-1) of how 'closed' the gallery is - while swiping/dismissing the gallery away */
|
|
73
|
+
dismissing = $bindable(0) as number,
|
|
74
|
+
|
|
75
|
+
/** Whether the carousel can be "dismissed" by swiping down/up */
|
|
76
|
+
dismissable = true as boolean,
|
|
77
|
+
|
|
78
|
+
/** The object-fit attribute for all items in the gallery */
|
|
79
|
+
fit = 'contain' as 'cover' | 'contain',
|
|
80
|
+
|
|
81
|
+
/** The list of items to display. Strings are treated as image URLs. */
|
|
82
|
+
items = [] as Array<string | Partial<CarouselItem>>,
|
|
83
|
+
|
|
84
|
+
/** Whether the carousel is 'inline' in the page - not a modal. This disables vertical gestures & mouse wheel */
|
|
85
|
+
inline = false,
|
|
86
|
+
|
|
87
|
+
/** The element that the carousel item will be animated from */
|
|
88
|
+
animation_target = undefined as HTMLElement | undefined,
|
|
89
|
+
|
|
90
|
+
/** Whether the animation for the entry/exit of the carousel (defaults to zooming) should be disabled */
|
|
91
|
+
disable_entry_exit_animation = false,
|
|
92
|
+
|
|
93
|
+
/** The transition type to use when navigating between slides */
|
|
94
|
+
transition = 'none' as 'none' | 'slide' | 'fade',
|
|
95
|
+
|
|
96
|
+
/** How the items should be slowly animated. 'zoom' will slowly zoom into the center of the image */
|
|
97
|
+
animation = 'none' as 'none' | 'zoom',
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether the active slide should auto-play when it's a video. Fires only
|
|
101
|
+
* when the carousel first opens (launch) — onto the active slide, and only
|
|
102
|
+
* if that slide is a video. Navigating/swiping between slides does NOT
|
|
103
|
+
* auto-play. Because the open is driven by a user gesture (thumbnail click /
|
|
104
|
+
* open() / slide set), the browser allows playback (with sound).
|
|
105
|
+
*/
|
|
106
|
+
autoplay_video = false as boolean,
|
|
107
|
+
|
|
108
|
+
/** The css style string added to the component from the parent */
|
|
109
|
+
style = '',
|
|
110
|
+
|
|
111
|
+
/** Specifies a custom class name for the container element */
|
|
112
|
+
class: class_name = '',
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Snippet used to render `type: 'custom'` items. Receives the full item
|
|
116
|
+
* plus lifecycle helpers so the snippet can integrate cleanly with the
|
|
117
|
+
* carousel:
|
|
118
|
+
* - `onload()` — call when the custom renderer has finished loading so
|
|
119
|
+
* the carousel hides its loading state for this slide.
|
|
120
|
+
* - `onerror(err)` — call if loading fails.
|
|
121
|
+
* - `active` — true when this item is the currently displayed slide
|
|
122
|
+
* (useful for autoplay/pause-style behaviour in your renderer).
|
|
123
|
+
* - `gesture_disabled` — true when the item has `disable_swipe` set
|
|
124
|
+
* (lets the renderer hide UI that would conflict with its own
|
|
125
|
+
* horizontal input).
|
|
126
|
+
*/
|
|
127
|
+
custom = undefined as
|
|
128
|
+
| Snippet<
|
|
129
|
+
[
|
|
130
|
+
{
|
|
131
|
+
item: CarouselItem;
|
|
132
|
+
onload: () => void;
|
|
133
|
+
onerror: (err: unknown) => void;
|
|
134
|
+
active: boolean;
|
|
135
|
+
gesture_disabled: boolean;
|
|
136
|
+
},
|
|
137
|
+
]
|
|
138
|
+
>
|
|
139
|
+
| undefined,
|
|
140
|
+
|
|
141
|
+
/** Called when the user interacts with the carousel */
|
|
142
|
+
oninteraction = undefined as (() => void) | undefined,
|
|
143
|
+
|
|
144
|
+
/** Called when the carousel is closed. Return false to prevent closing */
|
|
145
|
+
onclose = undefined as (() => boolean | undefined | void) | undefined,
|
|
146
|
+
} = $props();
|
|
147
|
+
|
|
148
|
+
const CLAMP_PADDING = 100; // the number of pixels to pad the image when it is zoomed in & panned around
|
|
149
|
+
const DRAG_THRESHOLD = 10; // how far the user must drag before we consider it a gesture
|
|
150
|
+
const DISMISS_THRESHOLD = 300; // the number of pixels the user must drag to dismiss the gallery
|
|
151
|
+
// How many slides on each side of the active slide should keep a rich renderer (PDF, panorama,
|
|
152
|
+
// embed) mounted. Set to 0 so only the *active* slide ever holds one of these heavy renderers —
|
|
153
|
+
// pdfjs, three.js, and YouTube/Vimeo iframes can each be substantial, and a slide that's at
|
|
154
|
+
// distance 1 (i.e. one swipe away) is rarely visible long enough to justify the cost. Adjacent
|
|
155
|
+
// rich slides mount in ~hundreds of ms when navigated to.
|
|
156
|
+
const RICH_NEIGHBOR_DISTANCE = 0;
|
|
157
|
+
// Videos get a wider keep-mounted neighborhood than the other rich types. A paused <video> is
|
|
158
|
+
// cheap, and keeping it mounted preserves its playback position and buffered data — so swiping
|
|
159
|
+
// away from a video and back resumes where you left off instead of reloading from the start.
|
|
160
|
+
// Bounded to active ± N, so at most 2N+1 videos are ever mounted even in a gallery of many.
|
|
161
|
+
const VIDEO_NEIGHBOR_DISTANCE = 2;
|
|
162
|
+
|
|
163
|
+
interface DecodedCarouselItem {
|
|
164
|
+
/** The ID of the media item */
|
|
165
|
+
id: string;
|
|
166
|
+
|
|
167
|
+
/** The key to use for the each block (a media item with the same id can appear in the carousel multiple times) */
|
|
168
|
+
key: string;
|
|
169
|
+
|
|
170
|
+
/** The type of media */
|
|
171
|
+
type: NonNullable<CarouselItem['type']>;
|
|
172
|
+
|
|
173
|
+
/** The natural width of the image in pixels */
|
|
174
|
+
width: number;
|
|
175
|
+
|
|
176
|
+
/** The natural height of the image in pixels */
|
|
177
|
+
height: number;
|
|
178
|
+
|
|
179
|
+
/** The aspect ratio of the image (width / height) */
|
|
180
|
+
ratio: number;
|
|
181
|
+
|
|
182
|
+
/** Short display label (used as fallback alt text) */
|
|
183
|
+
name: string;
|
|
184
|
+
|
|
185
|
+
/** Longer descriptive caption — shown in the carousel's fullscreen overlay */
|
|
186
|
+
caption: string;
|
|
187
|
+
|
|
188
|
+
/** Explicit alt text override */
|
|
189
|
+
alt: string;
|
|
190
|
+
|
|
191
|
+
/** Whether the media item should be treated as a panorama */
|
|
192
|
+
panorama: boolean;
|
|
193
|
+
|
|
194
|
+
/** A base64 ThumbHash used to render a tiny blurred preview before the full image loads */
|
|
195
|
+
thumbhash: string;
|
|
196
|
+
|
|
197
|
+
/** Optional poster/thumbnail URL — used for video posters and Gallery thumbnails */
|
|
198
|
+
poster: string;
|
|
199
|
+
|
|
200
|
+
/** The source for the media — URL or srcset (images), single URL otherwise */
|
|
201
|
+
src: string;
|
|
202
|
+
|
|
203
|
+
/** Whether this item should load eagerly with high fetch priority */
|
|
204
|
+
priority: boolean;
|
|
205
|
+
|
|
206
|
+
/** Whether the item is in view and should be loaded */
|
|
207
|
+
shouldLoad: boolean;
|
|
208
|
+
|
|
209
|
+
/** Whether the item should start playing (only applies to embeds/videos) */
|
|
210
|
+
shouldPlay: boolean;
|
|
211
|
+
|
|
212
|
+
/** Whether the item has finished loading */
|
|
213
|
+
loaded: boolean;
|
|
214
|
+
|
|
215
|
+
/** How the item container should be transformed (used to show different pages) */
|
|
216
|
+
transform: string | undefined;
|
|
217
|
+
|
|
218
|
+
/** The amount that the container has been offset in the y direction (used when swiping between pages) */
|
|
219
|
+
offsetY: number;
|
|
220
|
+
|
|
221
|
+
/** The current 'page' to show - like a pdf that has multiple pages that can be vertically swiped through */
|
|
222
|
+
page: number;
|
|
223
|
+
|
|
224
|
+
/** The resolution that the item should start at (because it was already loaded elsewhere in the app) */
|
|
225
|
+
initialResolution?: number;
|
|
226
|
+
|
|
227
|
+
/** The list of pages for the item (only one page for images. PDFs can have multiple) */
|
|
228
|
+
pages: Array<{
|
|
229
|
+
x: number;
|
|
230
|
+
y: number;
|
|
231
|
+
z: number;
|
|
232
|
+
scale: number;
|
|
233
|
+
resolutionW: number;
|
|
234
|
+
resolutionH: number;
|
|
235
|
+
matrix: DOMMatrix;
|
|
236
|
+
panX?: number;
|
|
237
|
+
panY?: number;
|
|
238
|
+
offsetX: number;
|
|
239
|
+
offsetY: number;
|
|
240
|
+
offsetWidth: number;
|
|
241
|
+
offsetHeight: number;
|
|
242
|
+
}>;
|
|
243
|
+
|
|
244
|
+
/** Optional pass-throughs from the source item (gesture overrides for custom items) */
|
|
245
|
+
disable_swipe?: boolean;
|
|
246
|
+
disable_zoom?: boolean;
|
|
247
|
+
|
|
248
|
+
/** A direct ref to the underlying <video> element, when this item is a video. Used to pause on slide change. */
|
|
249
|
+
_player?: HTMLVideoElement;
|
|
250
|
+
|
|
251
|
+
/** PDF-only: multiplier for the rendered canvas resolution so a pinch-zoomed PDF page stays crisp. */
|
|
252
|
+
_pdf_pixel_density?: number;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// The sanitized/formatted list of items to display in the carousel
|
|
256
|
+
let list = $state<Array<DecodedCarouselItem>>([]);
|
|
257
|
+
|
|
258
|
+
/** Lazily-loaded rich renderer components. Each is null until its first item appears in `list`. */
|
|
259
|
+
let renderers = $state<Record<RichRendererType, Component | null>>({
|
|
260
|
+
pdf: null,
|
|
261
|
+
panorama: null,
|
|
262
|
+
video: null,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
/** The container element (used to find the bounds of carousel) */
|
|
266
|
+
let viewport = $state<HTMLElement | undefined>();
|
|
267
|
+
|
|
268
|
+
/** The carousel slider items container element (contains carousel items as children) */
|
|
269
|
+
let container = $state<HTMLElement | undefined>();
|
|
270
|
+
|
|
271
|
+
/** The offset that defines which the grid cells gallery items should be in */
|
|
272
|
+
const offset = $derived(Math.floor(list.length / 2));
|
|
273
|
+
|
|
274
|
+
/** The current gesture being performed by the user */
|
|
275
|
+
let gesture: GalleryGesture | undefined;
|
|
276
|
+
|
|
277
|
+
/** A record of touch/mouse event pointers that are currently active */
|
|
278
|
+
let pointers: { [id: string]: Pointer } = {};
|
|
279
|
+
|
|
280
|
+
/** The midpoint of the current pointers */
|
|
281
|
+
let midpoint: Point | undefined;
|
|
282
|
+
|
|
283
|
+
/** The transform the pointers create based on their relationship to each other */
|
|
284
|
+
let transform: Transform | undefined;
|
|
285
|
+
|
|
286
|
+
/** Whether the current gesture has been emitted */
|
|
287
|
+
let gestureEmitted = false;
|
|
288
|
+
|
|
289
|
+
/** The current offset (in pixels) that the container should be transformed to */
|
|
290
|
+
let containerX = $state(0);
|
|
291
|
+
let containerTransform = $state(`translate3d(calc(${offset * -100}% + 0px), 0px, 0px)`);
|
|
292
|
+
|
|
293
|
+
/** The width of the viewport element */
|
|
294
|
+
let viewportW = $state(0);
|
|
295
|
+
let viewportH = $state(0);
|
|
296
|
+
let viewportX = $state(0);
|
|
297
|
+
let viewportY = $state(0);
|
|
298
|
+
|
|
299
|
+
/** The event timestamp when the last wheel event occurred */
|
|
300
|
+
let lastWheelEvent = 0;
|
|
301
|
+
|
|
302
|
+
/** The event timestamp when the user last tapped the carousel */
|
|
303
|
+
let lastTapEvent = 0;
|
|
304
|
+
|
|
305
|
+
/** The timestamp when the gesture/touch first started (on pointer down) */
|
|
306
|
+
let gestureStart = 0;
|
|
307
|
+
|
|
308
|
+
/** Handles canceling an animation on the container element (in Safari only) */
|
|
309
|
+
let destroySafariAnimation = () => {};
|
|
310
|
+
|
|
311
|
+
// The local instance of the index - used to compare against the exported slide index
|
|
312
|
+
let index = $state(slide);
|
|
313
|
+
|
|
314
|
+
// The local instance of the page - used to compare against the exported page
|
|
315
|
+
let _page = page;
|
|
316
|
+
|
|
317
|
+
/** Whether or not the container is transitioning/animating */
|
|
318
|
+
let transitioning = $state(false);
|
|
319
|
+
|
|
320
|
+
/** Whether or not the container is being dragged */
|
|
321
|
+
let dragging = $state(false);
|
|
322
|
+
|
|
323
|
+
/** Whether or not the container is being swiped */
|
|
324
|
+
let swiping = $state(false);
|
|
325
|
+
|
|
326
|
+
/** Whether the carousel is being opened in a modal or not */
|
|
327
|
+
let opening = $state(!inline);
|
|
328
|
+
|
|
329
|
+
$effect(() => {
|
|
330
|
+
const trackedItems = items;
|
|
331
|
+
untrack(() => initItems(trackedItems));
|
|
332
|
+
});
|
|
333
|
+
$effect(() => {
|
|
334
|
+
// Trigger lazy load of rich renderers, but only for items within RICH_NEIGHBOR_DISTANCE
|
|
335
|
+
// of the active slide. Items further away won't mount their rich renderer (see template),
|
|
336
|
+
// so there's no point downloading the heavy lib until the user navigates close to one.
|
|
337
|
+
const activeIndex = index;
|
|
338
|
+
const len = list.length;
|
|
339
|
+
const needed = new Set<RichRendererType>();
|
|
340
|
+
for (let i = 0; i < len; i++) {
|
|
341
|
+
const normalDist = Math.abs(i - activeIndex);
|
|
342
|
+
const dist = Math.min(normalDist, len - normalDist);
|
|
343
|
+
const item = list[i];
|
|
344
|
+
if (!item) continue;
|
|
345
|
+
// Videos load within the wider VIDEO_NEIGHBOR_DISTANCE so nearby ones are
|
|
346
|
+
// ready to (stay) mounted; other rich types only load when active.
|
|
347
|
+
const maxDist =
|
|
348
|
+
item.type === 'video' ? VIDEO_NEIGHBOR_DISTANCE : RICH_NEIGHBOR_DISTANCE;
|
|
349
|
+
if (dist > maxDist) continue;
|
|
350
|
+
if (item.type === 'pdf') needed.add('pdf');
|
|
351
|
+
else if (item.type === 'video') needed.add('video');
|
|
352
|
+
else if (item.type === 'image' && item.panorama) needed.add('panorama');
|
|
353
|
+
}
|
|
354
|
+
untrack(() => {
|
|
355
|
+
for (const type of needed) {
|
|
356
|
+
if (!renderers[type]) {
|
|
357
|
+
loadRichRenderer(type).then((mod) => {
|
|
358
|
+
renderers[type] = mod.default;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
$effect(() => {
|
|
365
|
+
if (slide === index || slide < 0) return;
|
|
366
|
+
untrack(() => goToSlide(slide));
|
|
367
|
+
});
|
|
368
|
+
$effect(() => {
|
|
369
|
+
if (_page === page) return;
|
|
370
|
+
untrack(() => goToPage(index, page));
|
|
371
|
+
});
|
|
372
|
+
$effect(() => {
|
|
373
|
+
if (animation === 'none') return;
|
|
374
|
+
untrack(() => startItemAnimation());
|
|
375
|
+
});
|
|
376
|
+
$effect(() => {
|
|
377
|
+
if (animation !== 'none') return;
|
|
378
|
+
untrack(() => stopItemAnimation());
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Watch the active PDF page's scale and, after it settles, push a
|
|
382
|
+
// matching `pixel_density` so pdfjs re-rasterizes the canvas at a higher
|
|
383
|
+
// resolution. Debounced so it fires once per zoom gesture (pinch, wheel,
|
|
384
|
+
// double-tap) instead of for every intermediate frame.
|
|
385
|
+
let pdfPixelDensityDebounce: ReturnType<typeof setTimeout> | undefined;
|
|
386
|
+
$effect(() => {
|
|
387
|
+
const item = list[index];
|
|
388
|
+
if (!item || item.type !== 'pdf') return;
|
|
389
|
+
const scale = item.pages[item.page]?.scale ?? 1;
|
|
390
|
+
untrack(() => {
|
|
391
|
+
clearTimeout(pdfPixelDensityDebounce);
|
|
392
|
+
pdfPixelDensityDebounce = setTimeout(() => {
|
|
393
|
+
const targetDensity = scale > 1.05 ? Math.min(4, Math.ceil(scale)) : 1;
|
|
394
|
+
if ((list[index]._pdf_pixel_density || 1) !== targetDensity) {
|
|
395
|
+
list[index]._pdf_pixel_density = targetDensity;
|
|
396
|
+
}
|
|
397
|
+
}, 220);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
/** Loads the number of given items on each side of the current item */
|
|
402
|
+
function loadItems(additionalItems = 0) {
|
|
403
|
+
list.forEach((item, i) => {
|
|
404
|
+
const normalDistance = Math.abs(i - index);
|
|
405
|
+
const distance = Math.min(normalDistance, list.length - normalDistance);
|
|
406
|
+
const shouldLoad = item.shouldLoad || distance <= additionalItems;
|
|
407
|
+
if (shouldLoad !== item.shouldLoad) list[i].shouldLoad = shouldLoad;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function initItems(rawItems: Array<string | Partial<CarouselItem>>) {
|
|
412
|
+
num_pages = 1;
|
|
413
|
+
const newList: DecodedCarouselItem[] = [];
|
|
414
|
+
for (const raw of rawItems) {
|
|
415
|
+
const item = normalizeCarouselItem(raw);
|
|
416
|
+
if (!item) continue;
|
|
417
|
+
const id = item.id || item.src;
|
|
418
|
+
if (!id) continue;
|
|
419
|
+
const prevItem = list.find((v) => v.id === id);
|
|
420
|
+
const normalDistance = Math.abs(newList.length - index);
|
|
421
|
+
const distance = Math.min(normalDistance, rawItems.length - normalDistance);
|
|
422
|
+
const shouldLoad = distance <= 0;
|
|
423
|
+
const initialResolution = Math.max(0, ...getLoadedResolutions(id));
|
|
424
|
+
const type = (item.type || 'image') as DecodedCarouselItem['type'];
|
|
425
|
+
const computedRatio = item.width && item.height ? item.width / item.height : 0;
|
|
426
|
+
newList.push({
|
|
427
|
+
id,
|
|
428
|
+
type,
|
|
429
|
+
src: item.src,
|
|
430
|
+
key: id + (newList.some((v) => v.id === id) ? newList.length : ''),
|
|
431
|
+
name: item.name || '',
|
|
432
|
+
caption: item.caption || '',
|
|
433
|
+
alt: item.alt ?? item.name ?? '',
|
|
434
|
+
width: prevItem?.width || item.width || 0,
|
|
435
|
+
height: prevItem?.height || item.height || 0,
|
|
436
|
+
ratio: prevItem?.ratio || computedRatio || 1,
|
|
437
|
+
panorama: item.panorama ?? false,
|
|
438
|
+
disable_swipe: item.disable_swipe ?? false,
|
|
439
|
+
disable_zoom: item.disable_zoom ?? false,
|
|
440
|
+
thumbhash: item.thumbhash || '',
|
|
441
|
+
poster: item.poster || '',
|
|
442
|
+
priority: item.priority ?? false,
|
|
443
|
+
shouldLoad: prevItem?.loaded || shouldLoad,
|
|
444
|
+
shouldPlay: false,
|
|
445
|
+
loaded: prevItem?.loaded ?? false,
|
|
446
|
+
offsetY: 0,
|
|
447
|
+
transform: undefined,
|
|
448
|
+
page: 0,
|
|
449
|
+
initialResolution,
|
|
450
|
+
pages: [
|
|
451
|
+
{
|
|
452
|
+
scale: 1,
|
|
453
|
+
x: 0,
|
|
454
|
+
y: 0,
|
|
455
|
+
z: 0,
|
|
456
|
+
offsetX: 0,
|
|
457
|
+
offsetY: 0,
|
|
458
|
+
offsetHeight: viewportH || viewport?.clientHeight || 0,
|
|
459
|
+
offsetWidth: viewportW || viewport?.clientWidth || 0,
|
|
460
|
+
resolutionW: Math.max(
|
|
461
|
+
initialResolution,
|
|
462
|
+
Math.min(2048, viewportW || viewport?.clientWidth || 0),
|
|
463
|
+
),
|
|
464
|
+
resolutionH: Math.max(
|
|
465
|
+
initialResolution,
|
|
466
|
+
Math.min(2048, viewportH || viewport?.clientHeight || 0),
|
|
467
|
+
),
|
|
468
|
+
matrix: browser ? createMatrix() : (undefined as unknown as DOMMatrix),
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
// Auto-play the active slide's video as soon as the carousel opens. The
|
|
474
|
+
// open is driven by a user gesture (thumbnail click / open() / slide set),
|
|
475
|
+
// so the browser permits playback. Only the active slide, only if a video.
|
|
476
|
+
if (autoplay_video && newList[index]?.type === 'video') {
|
|
477
|
+
newList[index].shouldPlay = true;
|
|
478
|
+
}
|
|
479
|
+
list = newList;
|
|
480
|
+
containerTransform = `translate3d(calc(${offset * -100}% + 0px), 0px, 0px)`;
|
|
481
|
+
if (!opening) {
|
|
482
|
+
loadItems(3);
|
|
483
|
+
await tick();
|
|
484
|
+
startItemAnimation();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Animate the main active item on from the animation target
|
|
489
|
+
await tick();
|
|
490
|
+
const el = getElementAtIndex(index, 0);
|
|
491
|
+
if (!el || inline || disable_entry_exit_animation) return (opening = false);
|
|
492
|
+
const slideEl = getElementAtIndex(index);
|
|
493
|
+
const previewEl = slideEl?.querySelector<HTMLElement>(':scope > .preview') || null;
|
|
494
|
+
el.style.opacity = `0`;
|
|
495
|
+
const target = animation_target?.getBoundingClientRect() || {
|
|
496
|
+
top: window.innerHeight / 2 - 50,
|
|
497
|
+
left: window.innerWidth / 2 - 50,
|
|
498
|
+
width: 100,
|
|
499
|
+
height: 100,
|
|
500
|
+
};
|
|
501
|
+
const MIN_SCALE = 0.25;
|
|
502
|
+
const current = el.getBoundingClientRect();
|
|
503
|
+
const scaleX = target.width / current.width;
|
|
504
|
+
const scaleY = target.height / current.height;
|
|
505
|
+
// min(scaleX, scaleY) so the longer axis fits the target exactly — the
|
|
506
|
+
// shorter axis is contained inside it, which visually "originates" the
|
|
507
|
+
// animation from the thumbnail rather than overshooting it.
|
|
508
|
+
const scale = Math.max(MIN_SCALE, Math.min(scaleX, scaleY));
|
|
509
|
+
const newW = current.width * scale;
|
|
510
|
+
const newH = current.height * scale;
|
|
511
|
+
const diffW = newW - target.width;
|
|
512
|
+
const diffH = newH - target.height;
|
|
513
|
+
const maxX = current.width - current.width * scale;
|
|
514
|
+
const maxY = current.height - current.height * scale;
|
|
515
|
+
const dx = Math.max(0, Math.min(maxX, target.left - current.left - diffW / 2));
|
|
516
|
+
const dy = Math.max(0, Math.min(maxY, target.top - current.top - diffH / 2));
|
|
517
|
+
const matrix = createMatrix().translate(dx, dy).scale(scale, scale);
|
|
518
|
+
const matrixStr = matrix.toString();
|
|
519
|
+
el.style.transform = matrixStr;
|
|
520
|
+
// If a thumbhash preview is rendered behind the main image, transform
|
|
521
|
+
// it along with the image so the user sees the blurred preview growing
|
|
522
|
+
// from the click target while the full image decodes underneath. This
|
|
523
|
+
// is what gives the open animation a visible "thing" the whole time
|
|
524
|
+
// instead of a blank rectangle until the <img> finally paints.
|
|
525
|
+
if (previewEl) previewEl.style.transform = matrixStr;
|
|
526
|
+
const easing = 'back-out';
|
|
527
|
+
const duration = 450;
|
|
528
|
+
const previewAnim = previewEl
|
|
529
|
+
? animateElement(previewEl, {
|
|
530
|
+
duration,
|
|
531
|
+
easing,
|
|
532
|
+
transform: createMatrix(),
|
|
533
|
+
})
|
|
534
|
+
: undefined;
|
|
535
|
+
await animateElement(el, {
|
|
536
|
+
duration,
|
|
537
|
+
easing,
|
|
538
|
+
opacity: 1,
|
|
539
|
+
transform: createMatrix(),
|
|
540
|
+
});
|
|
541
|
+
await previewAnim;
|
|
542
|
+
el.style.removeProperty('opacity');
|
|
543
|
+
if (previewEl) previewEl.style.removeProperty('transform');
|
|
544
|
+
opening = false;
|
|
545
|
+
startItemAnimation();
|
|
546
|
+
setTimeout(() => {
|
|
547
|
+
// Load the next items now that the animation has completed
|
|
548
|
+
loadItems(3);
|
|
549
|
+
}, 100);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Navigates/animates to the item in the carousel at the given index */
|
|
553
|
+
export async function goToSlide(
|
|
554
|
+
i: number,
|
|
555
|
+
direction?: 'forwards' | 'backwards',
|
|
556
|
+
source: 'gesture' | 'keyboard' | 'button' = 'button',
|
|
557
|
+
) {
|
|
558
|
+
if (!container) return;
|
|
559
|
+
const next = Math.floor(i + list.length) % list.length;
|
|
560
|
+
if (next === index || !list.length) return;
|
|
561
|
+
const numChildren = list.length;
|
|
562
|
+
const prevIndex = index;
|
|
563
|
+
_page = list[next]?.page || 0;
|
|
564
|
+
page = _page;
|
|
565
|
+
num_pages = list[next]?.pages?.length || 1;
|
|
566
|
+
slide = next;
|
|
567
|
+
index = next;
|
|
568
|
+
loadItems(); // load only the next item before animating
|
|
569
|
+
|
|
570
|
+
// Determine the direction of the navigation
|
|
571
|
+
let dir = direction === 'backwards' ? -1 : direction === 'forwards' ? 1 : 0;
|
|
572
|
+
if (!direction) {
|
|
573
|
+
const normalDistance = Math.abs(index - prevIndex);
|
|
574
|
+
const backwardDistance = numChildren - normalDistance;
|
|
575
|
+
const distance =
|
|
576
|
+
normalDistance <= backwardDistance ? index - prevIndex : prevIndex - index;
|
|
577
|
+
dir = Math.sign(distance);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Mark the correct page as active
|
|
581
|
+
list[index].pages.forEach((_, j) => {
|
|
582
|
+
const pageContainer = getElementAtIndex(i, j);
|
|
583
|
+
if (!pageContainer) return;
|
|
584
|
+
if (page === j) pageContainer.classList.add('active');
|
|
585
|
+
else pageContainer.classList.remove('active');
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Reset the position of each item in the carousel & animate to the default position/scale
|
|
589
|
+
list.forEach((item, i) => {
|
|
590
|
+
item.shouldPlay = false;
|
|
591
|
+
// Pause any video that's actively playing — `shouldPlay = false` only affects
|
|
592
|
+
// autoplay for newly-mounted videos; an already-playing <video> must be paused directly.
|
|
593
|
+
if (item._player && !item._player.paused) {
|
|
594
|
+
try {
|
|
595
|
+
item._player.pause();
|
|
596
|
+
} catch {
|
|
597
|
+
// ignore (e.g. media not yet attached)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const el = getElementAtIndex(index);
|
|
601
|
+
if (!el) return;
|
|
602
|
+
el.getAnimations().forEach((animation) => {
|
|
603
|
+
try {
|
|
604
|
+
animation.commitStyles();
|
|
605
|
+
animation.cancel();
|
|
606
|
+
} catch {
|
|
607
|
+
// ignore
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
updateItemMatrix(i);
|
|
611
|
+
item.pages.forEach(({ matrix }, j) => {
|
|
612
|
+
if (!matrix.isIdentity) {
|
|
613
|
+
if (transition === 'slide' || source === 'gesture') {
|
|
614
|
+
animatePage(i, j, { transform: createMatrix() });
|
|
615
|
+
} else {
|
|
616
|
+
updatePageMatrix(i, j, createMatrix());
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Note: auto-play (autoplay_video) is intentionally NOT re-triggered here.
|
|
623
|
+
// It only fires on the initial open (see initItems) — i.e. when the lightbox
|
|
624
|
+
// is launched onto a video — not when navigating/swiping between slides.
|
|
625
|
+
|
|
626
|
+
// Reset the transform of the container
|
|
627
|
+
container.getAnimations().forEach((animation) => {
|
|
628
|
+
try {
|
|
629
|
+
animation.commitStyles();
|
|
630
|
+
animation.cancel();
|
|
631
|
+
} catch {
|
|
632
|
+
// ignore
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Animate the current item slowly
|
|
637
|
+
startItemAnimation();
|
|
638
|
+
|
|
639
|
+
// Transition the two items in the carousel
|
|
640
|
+
if (transition === 'slide' || source === 'gesture') {
|
|
641
|
+
const currentTransform = getComputedStyle(container).transform;
|
|
642
|
+
const currentMatrix = createMatrix(currentTransform);
|
|
643
|
+
const offsetX = currentMatrix.m41;
|
|
644
|
+
const distance =
|
|
645
|
+
dir > 0
|
|
646
|
+
? (index + numChildren - prevIndex) % numChildren
|
|
647
|
+
: index > prevIndex
|
|
648
|
+
? -(prevIndex + numChildren - index)
|
|
649
|
+
: index - prevIndex;
|
|
650
|
+
const currX = offsetX + distance * viewportW;
|
|
651
|
+
const diffOffset = Math.ceil(currX / viewportW) * 100;
|
|
652
|
+
const diffX = currX % viewportW;
|
|
653
|
+
containerTransform = `translate3d(calc(${diffOffset}% + ${diffX}px), 0px, 0px)`;
|
|
654
|
+
await animateContainer('slide', source === 'button' ? 2000 : undefined);
|
|
655
|
+
} else if (transition === 'fade') {
|
|
656
|
+
const prevEl = getElementAtIndex(prevIndex);
|
|
657
|
+
const nextEl = getElementAtIndex(index);
|
|
658
|
+
if (prevEl && nextEl) {
|
|
659
|
+
if (index > prevIndex) {
|
|
660
|
+
prevEl.style.transform = `translate3d(100%, 0, 0)`;
|
|
661
|
+
} else if (index < prevIndex) {
|
|
662
|
+
prevEl.style.transform = `translate3d(-100%, 0, 0)`;
|
|
663
|
+
}
|
|
664
|
+
prevEl.style.zIndex = `2`;
|
|
665
|
+
nextEl.style.opacity = `0`;
|
|
666
|
+
prevEl.style.opacity = `1`;
|
|
667
|
+
prevEl.style.filter = `blur(0px)`;
|
|
668
|
+
nextEl
|
|
669
|
+
.animate([{ opacity: 1 }], { duration: 650 })
|
|
670
|
+
.finished.catch(() => undefined)
|
|
671
|
+
.then((animation) => {
|
|
672
|
+
try {
|
|
673
|
+
animation?.cancel();
|
|
674
|
+
} catch {
|
|
675
|
+
// ignore
|
|
676
|
+
}
|
|
677
|
+
nextEl.style.removeProperty('opacity');
|
|
678
|
+
});
|
|
679
|
+
prevEl
|
|
680
|
+
.animate([{ opacity: 0, filter: 'blur(10px)' }], { duration: 650 })
|
|
681
|
+
.finished.catch(() => undefined)
|
|
682
|
+
.then((animation) => {
|
|
683
|
+
try {
|
|
684
|
+
animation?.cancel();
|
|
685
|
+
} catch {
|
|
686
|
+
// ignore
|
|
687
|
+
}
|
|
688
|
+
prevEl.style.removeProperty('opacity');
|
|
689
|
+
prevEl.style.removeProperty('transform');
|
|
690
|
+
prevEl.style.removeProperty('filter');
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
containerTransform = `translate3d(${offset * -100}%, 0px, 0px)`;
|
|
694
|
+
} else {
|
|
695
|
+
containerTransform = `translate3d(${offset * -100}%, 0px, 0px)`;
|
|
696
|
+
}
|
|
697
|
+
// Load the next items now that the animation has completed
|
|
698
|
+
loadItems(3);
|
|
699
|
+
|
|
700
|
+
// Remove all animations from the previous page
|
|
701
|
+
if (animation === 'zoom') {
|
|
702
|
+
list[prevIndex].pages.forEach((_, j) => {
|
|
703
|
+
const pageEl = getElementAtIndex(prevIndex, j);
|
|
704
|
+
if (!pageEl) return;
|
|
705
|
+
pageEl.getAnimations().forEach((animation) => {
|
|
706
|
+
try {
|
|
707
|
+
animation.commitStyles();
|
|
708
|
+
animation.cancel();
|
|
709
|
+
} catch {
|
|
710
|
+
// ignore
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Update the image's resolution based on the new viewport size (in case the viewport has changed)
|
|
717
|
+
if (list[index]) {
|
|
718
|
+
list[index].pages.forEach((page) => {
|
|
719
|
+
const resolutionW = Math.max(
|
|
720
|
+
page.resolutionW,
|
|
721
|
+
Math.ceil(viewportW * (page.scale || 1)),
|
|
722
|
+
);
|
|
723
|
+
const resolutionH = Math.max(
|
|
724
|
+
page.resolutionH,
|
|
725
|
+
Math.ceil(viewportH * (page.scale || 1)),
|
|
726
|
+
);
|
|
727
|
+
if (resolutionW !== page.resolutionW || resolutionH !== page.resolutionH) {
|
|
728
|
+
page.resolutionW = resolutionW;
|
|
729
|
+
page.resolutionH = resolutionH;
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** Navigates/animates to the page of the current slide */
|
|
736
|
+
export function goToPage(itemIndex: number, pageIndex: number) {
|
|
737
|
+
if (!container) return;
|
|
738
|
+
const item = list[itemIndex];
|
|
739
|
+
if (!item?.pages?.length) return;
|
|
740
|
+
const next = Math.floor(pageIndex + item.pages.length) % item.pages.length;
|
|
741
|
+
if (next === item.page || !item.pages?.[next]) return;
|
|
742
|
+
_page = next;
|
|
743
|
+
page = next;
|
|
744
|
+
const pageContainer = getElementAtIndex(itemIndex);
|
|
745
|
+
if (!pageContainer) return;
|
|
746
|
+
const children = Array.from(pageContainer.querySelectorAll('*:not(.preview)'));
|
|
747
|
+
|
|
748
|
+
// Reset the position of each page for this slide & animate to the default position/scale.
|
|
749
|
+
// `children` is `querySelectorAll('*:not(.preview)')` on the li, which for single_page
|
|
750
|
+
// rich renderers (e.g. PDF) returns 20+ nested DOM elements rather than one-per-page.
|
|
751
|
+
// Only the first `item.pages.length` entries correspond to actual page slots.
|
|
752
|
+
children.forEach((el, i) => {
|
|
753
|
+
if (!item.pages[i]) return;
|
|
754
|
+
el.getAnimations().forEach((animation) => {
|
|
755
|
+
try {
|
|
756
|
+
animation.commitStyles();
|
|
757
|
+
animation.cancel();
|
|
758
|
+
} catch {
|
|
759
|
+
// ignore
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
updatePageMatrix(next, i);
|
|
763
|
+
if (i === page) el.classList.add('active');
|
|
764
|
+
else el.classList.remove('active');
|
|
765
|
+
const matrix = item.pages[i].matrix;
|
|
766
|
+
if (!matrix.isIdentity) animatePage(itemIndex, i, { transform: createMatrix() });
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Reset the transform of the slide element (page parent)
|
|
770
|
+
pageContainer.getAnimations().forEach((animation) => {
|
|
771
|
+
try {
|
|
772
|
+
animation.commitStyles();
|
|
773
|
+
animation.cancel();
|
|
774
|
+
} catch {
|
|
775
|
+
// ignore
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
animatePageContainer(itemIndex, next, 'slide');
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** Navigates/animates to the next item. If `amount` if provided, it will jump that amount of slides */
|
|
782
|
+
export function nextSlide(
|
|
783
|
+
amount = 1,
|
|
784
|
+
source: 'gesture' | 'keyboard' | 'button' = 'button',
|
|
785
|
+
) {
|
|
786
|
+
const next = Math.floor(index + (amount || 1)) % list.length;
|
|
787
|
+
goToSlide(next, 'forwards', source);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** Navigates/animates to the previous item. If `amount` if provided, it will jump that amount of slides */
|
|
791
|
+
export function prevSlide(
|
|
792
|
+
amount = 1,
|
|
793
|
+
source: 'gesture' | 'keyboard' | 'button' = 'button',
|
|
794
|
+
) {
|
|
795
|
+
const next = Math.floor(index - (amount || 1) + list.length) % list.length;
|
|
796
|
+
goToSlide(next, 'backwards', source);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** Navigates/animates to the next page. If `amount` if provided, it will jump that amount of pages */
|
|
800
|
+
export function nextPage(amount = 1) {
|
|
801
|
+
const item = list[index];
|
|
802
|
+
if (!item?.pages?.length || !item.pages[item.page + amount]) return;
|
|
803
|
+
goToPage(index, item.page + amount);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** Navigates/animates to the previous page. If `amount` if provided, it will jump that amount of pages */
|
|
807
|
+
export function prevPage(amount = 1) {
|
|
808
|
+
const item = list[index];
|
|
809
|
+
if (!item?.pages?.length || !item.pages[item.page - amount]) return;
|
|
810
|
+
goToPage(index, item.page - amount);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** Handles an "up" action - like a swipe up, arrow key up, or a button press that has an up arrow */
|
|
814
|
+
export function up() {
|
|
815
|
+
const item = list[index];
|
|
816
|
+
if (!item) return;
|
|
817
|
+
const pageData = list[index]?.pages?.[item.page];
|
|
818
|
+
if (!pageData) return;
|
|
819
|
+
oninteraction?.();
|
|
820
|
+
const bounds = calcBounds({
|
|
821
|
+
viewportW,
|
|
822
|
+
viewportH,
|
|
823
|
+
ratio: item.ratio,
|
|
824
|
+
padding: CLAMP_PADDING,
|
|
825
|
+
scale: pageData.scale,
|
|
826
|
+
});
|
|
827
|
+
const threshold = 10;
|
|
828
|
+
const nearTopEdge = pageData.y + threshold >= bounds.maxY;
|
|
829
|
+
if (nearTopEdge && item.page > 0) return prevPage();
|
|
830
|
+
if (pageData.scale > 1) {
|
|
831
|
+
const size = viewportW * pageData.scale;
|
|
832
|
+
const distance = Math.max(100, size * 0.01);
|
|
833
|
+
return pan(0, distance);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** Handles a "down" action - like a swipe down, arrow key down, or a button press that has a down arrow */
|
|
838
|
+
export function down() {
|
|
839
|
+
const item = list[index];
|
|
840
|
+
if (!item) return;
|
|
841
|
+
const pageData = list[index]?.pages?.[item.page];
|
|
842
|
+
if (!pageData) return;
|
|
843
|
+
oninteraction?.();
|
|
844
|
+
const bounds = calcBounds({
|
|
845
|
+
viewportW,
|
|
846
|
+
viewportH,
|
|
847
|
+
ratio: item.ratio,
|
|
848
|
+
padding: CLAMP_PADDING,
|
|
849
|
+
scale: pageData.scale,
|
|
850
|
+
});
|
|
851
|
+
const threshold = 10;
|
|
852
|
+
const nearBottomEdge = pageData.y - threshold <= bounds.minY;
|
|
853
|
+
if (nearBottomEdge && item.page < item.pages.length - 1) return nextPage();
|
|
854
|
+
if (pageData.scale > 1) {
|
|
855
|
+
const size = viewportW * pageData.scale;
|
|
856
|
+
const distance = Math.max(100, size * 0.01);
|
|
857
|
+
return pan(0, -distance);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** Handles a "left" action - like a swipe left, arrow key left, or a button press that has a left arrow */
|
|
862
|
+
export function left() {
|
|
863
|
+
const item = list[index];
|
|
864
|
+
if (!item) return;
|
|
865
|
+
const pageData = list[index]?.pages?.[item.page];
|
|
866
|
+
if (!pageData) return;
|
|
867
|
+
oninteraction?.();
|
|
868
|
+
if (pageData.scale > 1) {
|
|
869
|
+
const size = viewportW * pageData.scale;
|
|
870
|
+
const distance = Math.max(100, size * 0.01);
|
|
871
|
+
return pan(distance, 0);
|
|
872
|
+
}
|
|
873
|
+
return prevSlide();
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** Handles a "right" action - like a swipe right, arrow key right, or a button press that has a right arrow */
|
|
877
|
+
export function right() {
|
|
878
|
+
const item = list[index];
|
|
879
|
+
if (!item) return;
|
|
880
|
+
const pageData = list[index]?.pages?.[item.page];
|
|
881
|
+
if (!pageData) return;
|
|
882
|
+
oninteraction?.();
|
|
883
|
+
if (pageData.scale > 1) {
|
|
884
|
+
const size = viewportW * pageData.scale;
|
|
885
|
+
const distance = Math.max(100, size * 0.01);
|
|
886
|
+
return pan(-distance, 0);
|
|
887
|
+
}
|
|
888
|
+
return nextSlide();
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/** Resets the zoom & positioning of every slide */
|
|
892
|
+
export function reset() {
|
|
893
|
+
if (!container) return;
|
|
894
|
+
dismissing = 0;
|
|
895
|
+
|
|
896
|
+
list.forEach((item, i) => {
|
|
897
|
+
const el = getElementAtIndex(index);
|
|
898
|
+
if (!el) return;
|
|
899
|
+
el.getAnimations().forEach((animation) => {
|
|
900
|
+
try {
|
|
901
|
+
animation.commitStyles();
|
|
902
|
+
animation.cancel();
|
|
903
|
+
} catch {
|
|
904
|
+
// ignore
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
updateItemMatrix(i);
|
|
908
|
+
item.pages.forEach(({ matrix }, j) => {
|
|
909
|
+
if (!matrix.isIdentity) {
|
|
910
|
+
updatePageMatrix(i, j, createMatrix());
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
container.getAnimations().forEach((animation) => {
|
|
916
|
+
try {
|
|
917
|
+
animation.commitStyles();
|
|
918
|
+
animation.cancel();
|
|
919
|
+
} catch {
|
|
920
|
+
// ignore
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
containerTransform = `translate3d(${offset * -100}%, 0px, 0px)`;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function close() {
|
|
927
|
+
const success = onclose ? (onclose() ?? true) : true;
|
|
928
|
+
if (!success) {
|
|
929
|
+
animateItem(index, { easing: 'back-out' });
|
|
930
|
+
dismissing = 0;
|
|
931
|
+
} else {
|
|
932
|
+
setTimeout(() => {
|
|
933
|
+
dismissing = 0;
|
|
934
|
+
}, 100);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** Animates the current item if the animation prop is set */
|
|
939
|
+
async function startItemAnimation() {
|
|
940
|
+
if (animation === 'zoom') {
|
|
941
|
+
const pageEl = getElementAtIndex(index, page);
|
|
942
|
+
if (pageEl) {
|
|
943
|
+
await animateElement(pageEl, {
|
|
944
|
+
transform: `translate3d(0px, 0px, 30px)`,
|
|
945
|
+
duration: 30000,
|
|
946
|
+
easing: 'linear',
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** Stops the item's animation (happens when the carousel is paused) */
|
|
953
|
+
function stopItemAnimation() {
|
|
954
|
+
const pageEl = getElementAtIndex(index, page);
|
|
955
|
+
if (pageEl) {
|
|
956
|
+
pageEl.getAnimations().forEach((animation) => {
|
|
957
|
+
try {
|
|
958
|
+
animation.commitStyles();
|
|
959
|
+
animation.cancel();
|
|
960
|
+
} catch {
|
|
961
|
+
// ignore
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/** Returns the target element of the item at the given index */
|
|
968
|
+
function getElementAtIndex(itemIndex: number, pageIndex?: number) {
|
|
969
|
+
if (!list[itemIndex] || !container) return;
|
|
970
|
+
const parent = container.querySelector(`[data-index="${itemIndex}"]`) as HTMLElement;
|
|
971
|
+
if (!parent) return;
|
|
972
|
+
if (pageIndex === undefined) return parent as HTMLElement;
|
|
973
|
+
// PDFs render every page as an absolutely-positioned `.pdf-page` slot
|
|
974
|
+
// stacked vertically inside `.pdf-pages`. Each slot is sized to the
|
|
975
|
+
// full slide, so per-page matrices are applied directly to the slot —
|
|
976
|
+
// that keeps `clampMatrix` (which assumes one viewport-sized box) and
|
|
977
|
+
// zoom origin math correct on every page, not just the first.
|
|
978
|
+
if (list[itemIndex].type === 'pdf') {
|
|
979
|
+
const idx = pageIndex ?? list[itemIndex].page;
|
|
980
|
+
const slot = parent.querySelector(
|
|
981
|
+
`.pdf-page.single-page-slot[data-page="${idx + 1}"]`,
|
|
982
|
+
) as HTMLElement | null;
|
|
983
|
+
if (slot) return slot;
|
|
984
|
+
const pdfContainer = parent.querySelector('.pdf-container') as HTMLElement | null;
|
|
985
|
+
if (pdfContainer) return pdfContainer;
|
|
986
|
+
}
|
|
987
|
+
const children = parent.querySelectorAll('*:not(.preview)');
|
|
988
|
+
return (
|
|
989
|
+
(children[pageIndex ?? list[itemIndex].page] as HTMLElement) ||
|
|
990
|
+
(children[0] as HTMLElement) ||
|
|
991
|
+
parent
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Shrinks just the active PDF page (its `<canvas>`) during a dismiss swipe
|
|
997
|
+
* rather than scaling the whole document — cheaper, and the canvas's default
|
|
998
|
+
* center transform-origin makes it recede toward its own center. Uses the
|
|
999
|
+
* `scale` CSS property so it never touches the slot's `transform` matrix.
|
|
1000
|
+
*/
|
|
1001
|
+
function setPdfDismissScale(progress: number, animate = false) {
|
|
1002
|
+
const item = list[index];
|
|
1003
|
+
if (!item || item.type !== 'pdf') return;
|
|
1004
|
+
const slot = getElementAtIndex(index, item.page);
|
|
1005
|
+
const canvas = slot?.querySelector('canvas') as HTMLElement | null;
|
|
1006
|
+
if (!canvas) return;
|
|
1007
|
+
canvas.style.transformOrigin = 'center center';
|
|
1008
|
+
canvas.style.transition = animate
|
|
1009
|
+
? 'scale 280ms cubic-bezier(0.22, 1, 0.36, 1)'
|
|
1010
|
+
: 'none';
|
|
1011
|
+
canvas.style.scale = progress > 0 ? `${1 - progress * 0.25}` : '';
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/** Updates the page's metadata to the latest matrix/transform state */
|
|
1015
|
+
async function updatePageMatrix(
|
|
1016
|
+
itemIndex: number,
|
|
1017
|
+
pageIndex: number,
|
|
1018
|
+
matrix?: DOMMatrix,
|
|
1019
|
+
) {
|
|
1020
|
+
const item = list[itemIndex];
|
|
1021
|
+
const el = getElementAtIndex(itemIndex, pageIndex);
|
|
1022
|
+
if (!item || !el || !item.pages[pageIndex]) return;
|
|
1023
|
+
const pageData = item.pages[pageIndex];
|
|
1024
|
+
const targetMatrix = matrix || createMatrix(el.style.transform);
|
|
1025
|
+
const { scale, x, y } = extractMatrixTransform(targetMatrix);
|
|
1026
|
+
const hasChanges =
|
|
1027
|
+
pageData.matrix.toString() !== targetMatrix.toString() ||
|
|
1028
|
+
pageData.scale !== scale ||
|
|
1029
|
+
pageData.x !== x ||
|
|
1030
|
+
pageData.y !== y ||
|
|
1031
|
+
pageData.offsetX !== el.offsetLeft ||
|
|
1032
|
+
pageData.offsetY !== el.offsetTop;
|
|
1033
|
+
if (hasChanges) {
|
|
1034
|
+
el.style.transform = targetMatrix.toString();
|
|
1035
|
+
list[itemIndex].pages[pageIndex] = {
|
|
1036
|
+
...list[itemIndex].pages[pageIndex],
|
|
1037
|
+
matrix: targetMatrix,
|
|
1038
|
+
resolutionW: Math.max(pageData.resolutionW, Math.ceil(viewportW * (scale || 1))),
|
|
1039
|
+
resolutionH: Math.max(pageData.resolutionH, Math.ceil(viewportH * (scale || 1))),
|
|
1040
|
+
scale,
|
|
1041
|
+
x,
|
|
1042
|
+
y,
|
|
1043
|
+
offsetX: el.offsetLeft,
|
|
1044
|
+
offsetY: el.offsetTop,
|
|
1045
|
+
offsetWidth: el.offsetWidth,
|
|
1046
|
+
offsetHeight: el.offsetHeight,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** Updates the item's metadata to the latest matrix/transform state */
|
|
1052
|
+
async function updateItemMatrix(itemIndex: number, matrix?: DOMMatrix) {
|
|
1053
|
+
const item = list[itemIndex];
|
|
1054
|
+
if (!item) return;
|
|
1055
|
+
updatePageMatrix(itemIndex, list[itemIndex].page, matrix);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** Animates the item at the given index to the given matrix/transform */
|
|
1059
|
+
async function animateItem(
|
|
1060
|
+
itemIndex: number,
|
|
1061
|
+
options: KeyframeAnimationOptions & ElementAnimationOptions,
|
|
1062
|
+
) {
|
|
1063
|
+
await animatePage(itemIndex, undefined, options);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/** Animates the given page of the given item */
|
|
1067
|
+
async function animatePage(
|
|
1068
|
+
itemIndex: number,
|
|
1069
|
+
pageIndex: number | undefined,
|
|
1070
|
+
options: KeyframeAnimationOptions & ElementAnimationOptions,
|
|
1071
|
+
) {
|
|
1072
|
+
const item = list[itemIndex];
|
|
1073
|
+
const page = pageIndex ?? item?.page;
|
|
1074
|
+
const el = getElementAtIndex(itemIndex, page);
|
|
1075
|
+
if (!item || !el) return;
|
|
1076
|
+
const matrix =
|
|
1077
|
+
typeof options?.transform === 'string'
|
|
1078
|
+
? createMatrix(options.transform)
|
|
1079
|
+
: options.transform || createMatrix();
|
|
1080
|
+
await animateElement(el, { ...options, transform: matrix });
|
|
1081
|
+
updatePageMatrix(itemIndex, page);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/** Zooms into the current item by the given scale, centered on targetX/targetY */
|
|
1085
|
+
export function zoomIn(targetScale = 3, targetX?: number, targetY?: number) {
|
|
1086
|
+
const item = list[index];
|
|
1087
|
+
if (!item) return;
|
|
1088
|
+
const page = item.pages[item.page];
|
|
1089
|
+
if (!page) return;
|
|
1090
|
+
const currentScale = page.scale;
|
|
1091
|
+
if (!isScalable(item) || currentScale === targetScale) return;
|
|
1092
|
+
oninteraction?.();
|
|
1093
|
+
const viewportX = targetX ?? viewportW / 2;
|
|
1094
|
+
const viewportY = targetY ?? viewportH / 2;
|
|
1095
|
+
// PDFs render all pages stacked inside a single transformed container,
|
|
1096
|
+
// with the active page brought into view by translating the slide LI by
|
|
1097
|
+
// -item.page * 100%. Add that translation back when converting pointer
|
|
1098
|
+
// coords into the container's coordinate space so zooms anchor to the
|
|
1099
|
+
// actual click location on page 2+.
|
|
1100
|
+
const stackOffsetY = item.type === 'pdf' ? item.page * viewportH : 0;
|
|
1101
|
+
const x = viewportX - (page.offsetX || 0);
|
|
1102
|
+
const y = viewportY - (page.offsetY || 0) + stackOffsetY;
|
|
1103
|
+
|
|
1104
|
+
animatePage(index, item.page, {
|
|
1105
|
+
duration: 200,
|
|
1106
|
+
transform: clampMatrix(
|
|
1107
|
+
createMatrix()
|
|
1108
|
+
.translate(x, y)
|
|
1109
|
+
.scale(targetScale / (currentScale || 1))
|
|
1110
|
+
.translate(-x, -y),
|
|
1111
|
+
{
|
|
1112
|
+
viewportW: page.offsetWidth || viewportW,
|
|
1113
|
+
viewportH: page.offsetHeight || viewportH,
|
|
1114
|
+
ratio: item.ratio,
|
|
1115
|
+
padding: CLAMP_PADDING,
|
|
1116
|
+
},
|
|
1117
|
+
),
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/** Zooms out to the given scale on the current item */
|
|
1122
|
+
export function zoomOut(targetScale = 1) {
|
|
1123
|
+
const item = list[index];
|
|
1124
|
+
if (!item) return;
|
|
1125
|
+
const page = item.pages[item.page];
|
|
1126
|
+
if (!page) return;
|
|
1127
|
+
const currentScale = page.scale;
|
|
1128
|
+
if (!isScalable(item) || currentScale === targetScale) return;
|
|
1129
|
+
oninteraction?.();
|
|
1130
|
+
if (targetScale <= 1) {
|
|
1131
|
+
const matrix = list[index].pages[item.page].matrix;
|
|
1132
|
+
if (!matrix.isIdentity) animateItem(index, { duration: 200 });
|
|
1133
|
+
} else {
|
|
1134
|
+
const x = viewportW / 2;
|
|
1135
|
+
const y = viewportH / 2;
|
|
1136
|
+
animateItem(index, {
|
|
1137
|
+
duration: 200,
|
|
1138
|
+
transform: clampMatrix(
|
|
1139
|
+
createMatrix()
|
|
1140
|
+
.translate(x, y)
|
|
1141
|
+
.scale(targetScale / (currentScale || 1))
|
|
1142
|
+
.translate(-x, -y),
|
|
1143
|
+
{
|
|
1144
|
+
viewportW: page.offsetWidth || viewportW,
|
|
1145
|
+
viewportH: page.offsetHeight || viewportH,
|
|
1146
|
+
ratio: item.ratio,
|
|
1147
|
+
padding: CLAMP_PADDING,
|
|
1148
|
+
},
|
|
1149
|
+
),
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** Animates the current item by the given x, y (only if zoomed in already) */
|
|
1155
|
+
export async function pan(dx: number, dy: number) {
|
|
1156
|
+
const item = list[index];
|
|
1157
|
+
if (!item) return;
|
|
1158
|
+
const page = list[index].pages[item.page];
|
|
1159
|
+
if (page.scale <= 1.01) return;
|
|
1160
|
+
const original = extractMatrixTransform(page.matrix);
|
|
1161
|
+
const el = getElementAtIndex(index, item.page);
|
|
1162
|
+
if (!el) return;
|
|
1163
|
+
oninteraction?.();
|
|
1164
|
+
el.getAnimations().forEach((animation) => {
|
|
1165
|
+
try {
|
|
1166
|
+
animation.commitStyles();
|
|
1167
|
+
} catch {
|
|
1168
|
+
// ignore
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
let matrix = createMatrix(el.style.transform);
|
|
1172
|
+
const current = extractMatrixTransform(matrix);
|
|
1173
|
+
const progressX = (current.x - original.x) / page.scale;
|
|
1174
|
+
const progressY = (current.y - original.y) / page.scale;
|
|
1175
|
+
let diffX = dx;
|
|
1176
|
+
let diffY = dy;
|
|
1177
|
+
if (Math.abs(progressX) > 1) diffX += (page?.panX || 0) - progressX;
|
|
1178
|
+
if (Math.abs(progressY) > 1) diffY += (page?.panY || 0) - progressY;
|
|
1179
|
+
matrix = clampMatrix(matrix.translate(diffX, diffY), {
|
|
1180
|
+
viewportW: page.offsetWidth || el.offsetWidth || viewportW,
|
|
1181
|
+
viewportH: page.offsetHeight || el.offsetHeight || viewportH,
|
|
1182
|
+
ratio: item.ratio,
|
|
1183
|
+
padding: CLAMP_PADDING,
|
|
1184
|
+
});
|
|
1185
|
+
page.panX = diffX;
|
|
1186
|
+
page.panY = diffY;
|
|
1187
|
+
await animateItem(index, {
|
|
1188
|
+
duration: 600,
|
|
1189
|
+
easing: `cubic-bezier(0.22, 1, 0.36, 1)`,
|
|
1190
|
+
transform: matrix,
|
|
1191
|
+
});
|
|
1192
|
+
if (!el.getAnimations().length) {
|
|
1193
|
+
page.panX = 0;
|
|
1194
|
+
page.panY = 0;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/** Animates the carousel container's x dimension */
|
|
1199
|
+
async function animateContainer(type: 'reset' | 'slide' = 'reset', duration?: number) {
|
|
1200
|
+
if (!container) return;
|
|
1201
|
+
duration =
|
|
1202
|
+
duration ||
|
|
1203
|
+
(type === 'reset' ? 300 : Math.max(250, Math.min(600, viewportW * 0.35)));
|
|
1204
|
+
containerX = 0;
|
|
1205
|
+
|
|
1206
|
+
// Check if we're on a sane browser that doesn't have a mission to ruin the web
|
|
1207
|
+
if (!navigator.vendor.match(/apple/i)) {
|
|
1208
|
+
updateGalleryContainerClass();
|
|
1209
|
+
await animateElement(container, {
|
|
1210
|
+
transform: `translate3d(${offset * -100}%, 0px, 0px)`,
|
|
1211
|
+
easing: type === 'reset' ? 'back-out' : 'cubic-bezier(0.22, 1, 0.36, 1)',
|
|
1212
|
+
duration,
|
|
1213
|
+
id: `${type}_${Date.now()}`,
|
|
1214
|
+
});
|
|
1215
|
+
containerTransform = container?.style?.transform || '';
|
|
1216
|
+
} else {
|
|
1217
|
+
await tick();
|
|
1218
|
+
// HACK for Safari - we can't use the animation api because Safari
|
|
1219
|
+
// keeps removing hardware acceleration. Many hours of debugging later,
|
|
1220
|
+
// we're left with this hacky solution.
|
|
1221
|
+
let start = Date.now();
|
|
1222
|
+
const rawTransform = window.getComputedStyle(container).transform;
|
|
1223
|
+
const fromMatrix = createMatrix(rawTransform);
|
|
1224
|
+
const fromX = fromMatrix.e;
|
|
1225
|
+
const toX = offset * -viewportW;
|
|
1226
|
+
let destroyed = false;
|
|
1227
|
+
destroySafariAnimation();
|
|
1228
|
+
destroySafariAnimation = () => (destroyed = true);
|
|
1229
|
+
if (!transitioning) transitioning = true;
|
|
1230
|
+
function nextFrame() {
|
|
1231
|
+
if (destroyed) return;
|
|
1232
|
+
const progress = Math.min(1, (Date.now() - start) / duration!);
|
|
1233
|
+
const easingProgress = type === 'reset' ? backOut(progress) : circOut(progress);
|
|
1234
|
+
const x = fromX + (toX - fromX) * easingProgress;
|
|
1235
|
+
if (progress < 1) {
|
|
1236
|
+
containerTransform = `translate3d(${x}px, 0px, 0px)`;
|
|
1237
|
+
requestAnimationFrame(nextFrame);
|
|
1238
|
+
} else {
|
|
1239
|
+
containerTransform = `translate3d(${offset * -100}%, 0px, 0px)`;
|
|
1240
|
+
if (transitioning) transitioning = false;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
requestAnimationFrame(nextFrame);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/** Animates the carousel page container's y dimension */
|
|
1248
|
+
async function animatePageContainer(
|
|
1249
|
+
itemIndex: number,
|
|
1250
|
+
pageIndex: number,
|
|
1251
|
+
type: 'reset' | 'slide' = 'reset',
|
|
1252
|
+
) {
|
|
1253
|
+
if (!container) return;
|
|
1254
|
+
const item = list[itemIndex];
|
|
1255
|
+
if (!item?.pages?.length || !item.pages[pageIndex]) return;
|
|
1256
|
+
const pageContainer = getElementAtIndex(itemIndex);
|
|
1257
|
+
if (!pageContainer) return;
|
|
1258
|
+
const duration =
|
|
1259
|
+
type === 'reset' ? 300 : Math.max(200, Math.min(600, viewportH * 0.35));
|
|
1260
|
+
list[itemIndex] = { ...list[itemIndex], offsetY: 0, page: pageIndex };
|
|
1261
|
+
|
|
1262
|
+
const targetPercent = pageIndex * -100;
|
|
1263
|
+
|
|
1264
|
+
if (!navigator.vendor.match(/apple/i)) {
|
|
1265
|
+
await animateElement(pageContainer, {
|
|
1266
|
+
transform: `translate3d(0px, ${targetPercent}%, 0px)`,
|
|
1267
|
+
easing: type === 'reset' ? 'back-out' : 'cubic-bezier(0.22, 1, 0.36, 1)',
|
|
1268
|
+
duration,
|
|
1269
|
+
id: `${type}_${Date.now()}`,
|
|
1270
|
+
});
|
|
1271
|
+
list[itemIndex].transform = pageContainer.style.transform;
|
|
1272
|
+
} else {
|
|
1273
|
+
let start = Date.now();
|
|
1274
|
+
const rawTransform = window.getComputedStyle(pageContainer).transform;
|
|
1275
|
+
const fromMatrix = createMatrix(rawTransform);
|
|
1276
|
+
const fromY = fromMatrix.f;
|
|
1277
|
+
const toY = pageIndex * -viewportH;
|
|
1278
|
+
let destroyed = false;
|
|
1279
|
+
destroySafariAnimation();
|
|
1280
|
+
destroySafariAnimation = () => (destroyed = true);
|
|
1281
|
+
function nextFrame() {
|
|
1282
|
+
if (destroyed) return;
|
|
1283
|
+
const progress = Math.min(1, (Date.now() - start) / duration);
|
|
1284
|
+
const easingProgress = type === 'reset' ? backOut(progress) : circOut(progress);
|
|
1285
|
+
const y = fromY + (toY - fromY) * easingProgress;
|
|
1286
|
+
if (progress < 1) {
|
|
1287
|
+
list[itemIndex].transform = `translate3d(0px, ${y}px, 0px)`;
|
|
1288
|
+
requestAnimationFrame(nextFrame);
|
|
1289
|
+
} else {
|
|
1290
|
+
list[itemIndex].transform = `translate3d(0px, ${targetPercent}%, 0px)`;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
requestAnimationFrame(nextFrame);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/** Called when a multi-pointer interaction starts */
|
|
1298
|
+
function onInteractionStart(e: PointerEvent) {
|
|
1299
|
+
if (!viewport) return;
|
|
1300
|
+
destroySafariAnimation();
|
|
1301
|
+
gestureStart = e.timeStamp;
|
|
1302
|
+
gestureEmitted = false;
|
|
1303
|
+
if (!dragging) dragging = true;
|
|
1304
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
1305
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
1306
|
+
document.removeEventListener('pointercancel', onPointerUp);
|
|
1307
|
+
document.addEventListener('pointermove', onPointerMove, { passive: true });
|
|
1308
|
+
document.addEventListener('pointerup', onPointerUp, { passive: true });
|
|
1309
|
+
document.addEventListener('pointercancel', onPointerUp, { passive: true });
|
|
1310
|
+
const boundingRect = viewport.getBoundingClientRect();
|
|
1311
|
+
viewportY = boundingRect.top;
|
|
1312
|
+
viewportX = boundingRect.left;
|
|
1313
|
+
const el = getElementAtIndex(index, page);
|
|
1314
|
+
if (!el) return;
|
|
1315
|
+
el.getAnimations().forEach((animation) => {
|
|
1316
|
+
try {
|
|
1317
|
+
animation.commitStyles();
|
|
1318
|
+
animation.cancel();
|
|
1319
|
+
} catch {
|
|
1320
|
+
// ignore
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
updateItemMatrix(index);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/** Called when a multi-pointer interaction ends */
|
|
1327
|
+
function onInteractionEnd(e: PointerEvent) {
|
|
1328
|
+
if (!container) return;
|
|
1329
|
+
if (dragging) dragging = false;
|
|
1330
|
+
if (swiping) swiping = false;
|
|
1331
|
+
const pointerList = Object.values(pointers);
|
|
1332
|
+
const vx =
|
|
1333
|
+
pointerList.reduce((sum, pointer) => sum + pointer.vx, 0) /
|
|
1334
|
+
(pointerList.length || 1);
|
|
1335
|
+
const vy =
|
|
1336
|
+
pointerList.reduce((sum, pointer) => sum + pointer.vy, 0) /
|
|
1337
|
+
(pointerList.length || 1);
|
|
1338
|
+
const item = list[index];
|
|
1339
|
+
const pageData = list[index]?.pages?.[item?.page];
|
|
1340
|
+
const scale = pageData.scale || 1;
|
|
1341
|
+
if (pageData.scale > 1.05 || !inline) {
|
|
1342
|
+
if (container.style.touchAction !== 'none') container.style.touchAction = 'none';
|
|
1343
|
+
} else if (inline) {
|
|
1344
|
+
if (container.style.touchAction !== 'pan-y') container.style.touchAction = 'pan-y';
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Check for tap/double tap
|
|
1348
|
+
const isTap =
|
|
1349
|
+
e.timeStamp - gestureStart < 150 &&
|
|
1350
|
+
Math.abs(vx) < 0.5 &&
|
|
1351
|
+
Math.abs(vy) < 0.5 &&
|
|
1352
|
+
['none', 'indeterminate', 'pinch-zoom'].includes(gesture || 'none');
|
|
1353
|
+
const isDoubleTap = e.timeStamp - lastTapEvent < 300 && isTap;
|
|
1354
|
+
if (isTap) lastTapEvent = e.timeStamp;
|
|
1355
|
+
if (isTap) gesture = undefined;
|
|
1356
|
+
|
|
1357
|
+
// Add inertia to the pan after the user lifts their finger
|
|
1358
|
+
if (gesture === 'pinch-zoom') {
|
|
1359
|
+
if (scale <= 1.01) {
|
|
1360
|
+
animateItem(index, { easing: 'back-out' });
|
|
1361
|
+
} else {
|
|
1362
|
+
const dx = (Math.min(3000, Math.abs(vx * 300)) * Math.sign(vx)) / scale;
|
|
1363
|
+
const dy = (Math.min(3000, Math.abs(vy * 300)) * Math.sign(vy)) / scale;
|
|
1364
|
+
pan(dx, dy);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Handle switching to the next/previous element when the user pans quickly
|
|
1369
|
+
if (gesture === 'pan-x') {
|
|
1370
|
+
let velocity = Math.abs(vx);
|
|
1371
|
+
let distance = Math.abs(containerX);
|
|
1372
|
+
if (scale > 1.05) {
|
|
1373
|
+
velocity = velocity * 0.5;
|
|
1374
|
+
distance = distance * 0.25;
|
|
1375
|
+
}
|
|
1376
|
+
const movedFast = velocity > 0.5;
|
|
1377
|
+
const movedFar = distance > 150 && velocity > 0.05;
|
|
1378
|
+
if (list.length > 1 && (movedFar || movedFast)) {
|
|
1379
|
+
const speedFactor = e.pointerType === 'touch' ? 0.35 : 0.15;
|
|
1380
|
+
const amount = Math.min(
|
|
1381
|
+
8,
|
|
1382
|
+
Math.floor(list.length / 2) - 1,
|
|
1383
|
+
Math.ceil(velocity * speedFactor),
|
|
1384
|
+
scale > 1.05 ? 1 : Infinity,
|
|
1385
|
+
);
|
|
1386
|
+
if (vx > 0) prevSlide(amount, 'gesture');
|
|
1387
|
+
else nextSlide(amount, 'gesture');
|
|
1388
|
+
} else {
|
|
1389
|
+
animateContainer('reset');
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// Handle switching to the next/previous page when the user pans vertically
|
|
1394
|
+
if (gesture === 'pan-y-page') {
|
|
1395
|
+
let velocity = Math.abs(vy);
|
|
1396
|
+
let distance = Math.abs(item.offsetY);
|
|
1397
|
+
if (scale > 1.05) {
|
|
1398
|
+
velocity = velocity * 0.5;
|
|
1399
|
+
distance = distance * 0.25;
|
|
1400
|
+
}
|
|
1401
|
+
const movedFast = velocity > 0.5;
|
|
1402
|
+
const movedFar = distance > 150 && velocity > 0.05;
|
|
1403
|
+
if (movedFar || movedFast) {
|
|
1404
|
+
const speedFactor = e.pointerType === 'touch' ? 0.35 : 0.15;
|
|
1405
|
+
const amount = Math.min(
|
|
1406
|
+
8,
|
|
1407
|
+
Math.floor(list.length / 2) - 1,
|
|
1408
|
+
Math.ceil(velocity * speedFactor),
|
|
1409
|
+
scale > 1.05 ? 1 : Infinity,
|
|
1410
|
+
);
|
|
1411
|
+
if (item.offsetY > 0) prevPage(amount);
|
|
1412
|
+
else nextPage(amount);
|
|
1413
|
+
} else {
|
|
1414
|
+
animatePageContainer(index, item.page, 'reset');
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Handle dismissing the carousel (or snapping back to the initial state)
|
|
1419
|
+
if (gesture === 'pan-y-dismiss') {
|
|
1420
|
+
if (Math.abs(vy) > 2 || Math.abs(pageData?.y || 0) > 60) {
|
|
1421
|
+
close();
|
|
1422
|
+
} else {
|
|
1423
|
+
animateItem(index, { easing: 'back-out' });
|
|
1424
|
+
dismissing = 0;
|
|
1425
|
+
// Ease the shrunk PDF page back to full size on snap-back.
|
|
1426
|
+
setPdfDismissScale(0, true);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Snap back to the center of the element when the user lifts their finger
|
|
1431
|
+
if (gesture === 'indeterminate' || gesture === 'none') {
|
|
1432
|
+
item.pages.forEach((val, i) => {
|
|
1433
|
+
if (val?.x || val?.y || val.scale !== 1) {
|
|
1434
|
+
animateItem(index, { id: `reset-item-page_${index}_${i}_${Date.now()}` });
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// Check if the user double tapped - and zoom in/out of the image
|
|
1440
|
+
if (isDoubleTap && isScalable(item)) {
|
|
1441
|
+
if (Math.abs(scale - 1) <= 0.01) {
|
|
1442
|
+
zoomIn(3, e.clientX - viewportX, e.clientY - viewportY);
|
|
1443
|
+
if (container.style.touchAction !== 'none') container.style.touchAction = 'none';
|
|
1444
|
+
} else {
|
|
1445
|
+
zoomOut();
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Close the carousel if the user taps on the background
|
|
1450
|
+
if (isTap && e.target) {
|
|
1451
|
+
if ((e.target as HTMLElement).classList.contains('item')) {
|
|
1452
|
+
if (!inline) {
|
|
1453
|
+
setTimeout(() => {
|
|
1454
|
+
close();
|
|
1455
|
+
}, 0);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (isTap || isDoubleTap) animateContainer('reset');
|
|
1461
|
+
|
|
1462
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
1463
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
1464
|
+
document.removeEventListener('pointercancel', onPointerUp);
|
|
1465
|
+
transform = undefined;
|
|
1466
|
+
gesture = undefined;
|
|
1467
|
+
midpoint = undefined;
|
|
1468
|
+
gestureStart = 0;
|
|
1469
|
+
pointers = {};
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/** Called when a pointer is updated */
|
|
1473
|
+
function onPointerEvent(pointer: Pointer) {
|
|
1474
|
+
const prevPointers = { ...pointers };
|
|
1475
|
+
const prevMidpoint = !midpoint ? undefined : { ...midpoint };
|
|
1476
|
+
pointers[pointer.id] = pointer;
|
|
1477
|
+
midpoint = center(...Object.values(pointers));
|
|
1478
|
+
transform = calcTransform(pointer, pointers, midpoint, prevPointers, prevMidpoint);
|
|
1479
|
+
const item = list[index];
|
|
1480
|
+
const pageData = item?.pages?.[item?.page];
|
|
1481
|
+
if (!item || !pageData) return;
|
|
1482
|
+
let matrix = pageData.matrix;
|
|
1483
|
+
const fit = {
|
|
1484
|
+
viewportW: pageData.offsetWidth,
|
|
1485
|
+
viewportH: pageData.offsetHeight,
|
|
1486
|
+
ratio: item.ratio,
|
|
1487
|
+
padding: CLAMP_PADDING,
|
|
1488
|
+
scale: pageData.scale,
|
|
1489
|
+
};
|
|
1490
|
+
if (container) {
|
|
1491
|
+
if (pageData.scale > 1.01 || !inline) {
|
|
1492
|
+
if (container.style.touchAction !== 'none') container.style.touchAction = 'none';
|
|
1493
|
+
} else if (inline) {
|
|
1494
|
+
if (container.style.touchAction !== 'pan-y')
|
|
1495
|
+
container.style.touchAction = 'pan-y';
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Determine the current gesture based on the current transform
|
|
1500
|
+
if (transitioning && isSwipeable(item)) {
|
|
1501
|
+
gesture = 'pan-x';
|
|
1502
|
+
} else if (!isScalable(item) && !isSwipeable(item)) {
|
|
1503
|
+
gesture = 'none';
|
|
1504
|
+
} else if (!transform) {
|
|
1505
|
+
gesture = 'indeterminate';
|
|
1506
|
+
} else if (
|
|
1507
|
+
isScalable(item) &&
|
|
1508
|
+
(gesture === 'pinch-zoom' || Math.abs(1 - transform.scale) > 0.001)
|
|
1509
|
+
) {
|
|
1510
|
+
gesture = 'pinch-zoom';
|
|
1511
|
+
} else if (!isSwipeable(item)) {
|
|
1512
|
+
gesture = 'none';
|
|
1513
|
+
} else if (
|
|
1514
|
+
pageData.scale > 1.01 &&
|
|
1515
|
+
gesture !== 'pan-y-dismiss' &&
|
|
1516
|
+
gesture !== 'pan-y-page'
|
|
1517
|
+
) {
|
|
1518
|
+
const bounds = calcBounds(fit);
|
|
1519
|
+
const threshold = 10;
|
|
1520
|
+
const horizontal = Math.abs(transform.translateX) >= Math.abs(transform.translateY);
|
|
1521
|
+
const nearLeftEdge = pageData.x + threshold >= bounds.maxX;
|
|
1522
|
+
const nearRightEdge = pageData.x - threshold <= bounds.minX;
|
|
1523
|
+
const nearBottomEdge = pageData.y - threshold <= bounds.minY;
|
|
1524
|
+
const nearTopEdge = pageData.y + threshold >= bounds.maxY;
|
|
1525
|
+
const movedRight = (transform.translateX > 0 && horizontal) || containerX > 0;
|
|
1526
|
+
const movedLeft = (transform.translateX < 0 && horizontal) || containerX < 0;
|
|
1527
|
+
const movedUp = transform.translateY < 0 && !horizontal;
|
|
1528
|
+
const movedDown = transform.translateY > 0 && !horizontal;
|
|
1529
|
+
if (nearLeftEdge && movedRight) {
|
|
1530
|
+
gesture = 'pan-x';
|
|
1531
|
+
} else if (nearRightEdge && movedLeft) {
|
|
1532
|
+
gesture = 'pan-x';
|
|
1533
|
+
} else if (gesture !== 'pinch-zoom' && nearTopEdge && movedDown) {
|
|
1534
|
+
if (item.pages?.[item.page - 1]) {
|
|
1535
|
+
gesture = 'pan-y-page';
|
|
1536
|
+
} else if (dismissable) {
|
|
1537
|
+
gesture = 'pan-y-dismiss';
|
|
1538
|
+
}
|
|
1539
|
+
} else if (gesture !== 'pinch-zoom' && nearBottomEdge && movedUp) {
|
|
1540
|
+
if (item.pages?.[item.page + 1]) {
|
|
1541
|
+
gesture = 'pan-y-page';
|
|
1542
|
+
} else if (dismissable) {
|
|
1543
|
+
gesture = 'pan-y-dismiss';
|
|
1544
|
+
}
|
|
1545
|
+
} else if (Math.abs(transform.translateX) || Math.abs(transform.translateY)) {
|
|
1546
|
+
gesture = 'pinch-zoom';
|
|
1547
|
+
}
|
|
1548
|
+
} else if (gesture === 'pan-x' || inline) {
|
|
1549
|
+
gesture = 'pan-x';
|
|
1550
|
+
} else if (gesture !== 'pan-y-dismiss' && gesture !== 'pan-y-page') {
|
|
1551
|
+
const panning =
|
|
1552
|
+
Math.abs(pageData.x) > DRAG_THRESHOLD || Math.abs(pageData.y) > DRAG_THRESHOLD;
|
|
1553
|
+
const horizontal = Math.abs(pageData.x) > Math.abs(pageData.y);
|
|
1554
|
+
if (panning && horizontal) gesture = 'pan-x';
|
|
1555
|
+
if (panning && !horizontal) {
|
|
1556
|
+
const i = item.page + (pageData.y > 0 ? -1 : 1);
|
|
1557
|
+
if (item.pages?.[i]) {
|
|
1558
|
+
gesture = 'pan-y-page';
|
|
1559
|
+
} else if (dismissable) {
|
|
1560
|
+
gesture = 'pan-y-dismiss';
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
if (!gesture) gesture = !isSwipeable(item) && item.loaded ? 'none' : 'indeterminate';
|
|
1565
|
+
|
|
1566
|
+
// Emit the gesture/interaction if necessary
|
|
1567
|
+
if (gesture && gesture !== 'none' && gesture !== 'indeterminate' && !gestureEmitted) {
|
|
1568
|
+
oninteraction?.();
|
|
1569
|
+
gestureEmitted = true;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// Update the matrix based on the current gesture
|
|
1573
|
+
const scale = pageData.scale;
|
|
1574
|
+
if (gesture === 'pan-y-dismiss') {
|
|
1575
|
+
if (scale > 1.01) {
|
|
1576
|
+
const targetScale = Math.max(1, scale - Math.abs(transform.translateY) * 0.1);
|
|
1577
|
+
matrix = clampMatrix(matrix.scale(targetScale / scale, targetScale / scale), fit);
|
|
1578
|
+
} else {
|
|
1579
|
+
const x = matrix.e / scale;
|
|
1580
|
+
const y = matrix.f / scale;
|
|
1581
|
+
const z = (matrix.m43 || 0) / scale;
|
|
1582
|
+
const max = DISMISS_THRESHOLD;
|
|
1583
|
+
const progress = Math.max(
|
|
1584
|
+
0,
|
|
1585
|
+
Math.min(1, Math.abs(y + transform.translateY) / max),
|
|
1586
|
+
);
|
|
1587
|
+
// PDF pages live in a deep `.pdf-page` slot that isn't a direct child
|
|
1588
|
+
// of the perspective container, so a Z-translate gets flattened and
|
|
1589
|
+
// can't read as a shrink. The page still translates correctly here;
|
|
1590
|
+
// the visual scale-down for PDFs is applied separately as a CSS
|
|
1591
|
+
// `scale` on the slide element, driven by `dismissing` (see template).
|
|
1592
|
+
const targetZ = circInOut(progress) * -200;
|
|
1593
|
+
matrix = matrix.translate(-x, transform.translateY, targetZ - z);
|
|
1594
|
+
dismissing = Math.max(0, Math.min(1, 1 - (300 - Math.abs(pageData.y)) / 300));
|
|
1595
|
+
if (item.type === 'pdf') setPdfDismissScale(dismissing, false);
|
|
1596
|
+
}
|
|
1597
|
+
} else if (gesture === 'pan-y-page') {
|
|
1598
|
+
item.offsetY += transform.translateY;
|
|
1599
|
+
const baseY = item.page * -100;
|
|
1600
|
+
const transformY = `calc(${baseY}% + ${item.offsetY}px)`;
|
|
1601
|
+
item.transform = `translate3d(0px, ${transformY}, 0px)`;
|
|
1602
|
+
if (scale > 1.01) {
|
|
1603
|
+
matrix = clampMatrix(matrix.translate(transform.translateX / scale, 0), fit);
|
|
1604
|
+
} else {
|
|
1605
|
+
if (pageData.y) {
|
|
1606
|
+
matrix = matrix.translate(0, -pageData.y);
|
|
1607
|
+
updateItemMatrix(index, matrix);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
if (!matrix.isIdentity) {
|
|
1611
|
+
matrix = createMatrix();
|
|
1612
|
+
const id = `pan-y-reset`;
|
|
1613
|
+
const element = getElementAtIndex(index);
|
|
1614
|
+
const alreadyAnimating = element
|
|
1615
|
+
?.getAnimations()
|
|
1616
|
+
?.some(
|
|
1617
|
+
(animation) => animation.id === id && animation.playState === 'running',
|
|
1618
|
+
);
|
|
1619
|
+
if (!alreadyAnimating) {
|
|
1620
|
+
animateItem(index, { transform: matrix, id, duration: 200 });
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
} else if (gesture === 'pan-x') {
|
|
1625
|
+
if (scale > 1.01) {
|
|
1626
|
+
matrix = matrix.translate(0, transform.translateY);
|
|
1627
|
+
containerX += transform.translateX;
|
|
1628
|
+
const transformX = `calc(${offset * -100}% + ${containerX}px)`;
|
|
1629
|
+
containerTransform = `translate3d(${transformX}, 0, 0)`;
|
|
1630
|
+
} else {
|
|
1631
|
+
containerX += transform.translateX + pageData.x;
|
|
1632
|
+
const transformX = `calc(${offset * -100}% + ${containerX}px)`;
|
|
1633
|
+
containerTransform = `translate3d(${transformX}, 0, 0)`;
|
|
1634
|
+
if (pageData.x) {
|
|
1635
|
+
matrix = matrix.translate(-pageData.x, 0);
|
|
1636
|
+
updateItemMatrix(index, matrix);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
if (!matrix.isIdentity) {
|
|
1640
|
+
matrix = createMatrix();
|
|
1641
|
+
const id = `pan-x-reset`;
|
|
1642
|
+
const element = getElementAtIndex(index);
|
|
1643
|
+
const alreadyAnimating = element
|
|
1644
|
+
?.getAnimations()
|
|
1645
|
+
?.some(
|
|
1646
|
+
(animation) => animation.id === id && animation.playState === 'running',
|
|
1647
|
+
);
|
|
1648
|
+
if (!alreadyAnimating) {
|
|
1649
|
+
animateItem(index, { transform: matrix, id, duration: 200 });
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
} else if (gesture === 'pinch-zoom' || gesture === 'indeterminate') {
|
|
1654
|
+
let targetScale = Math.min(6, scale * transform.scale);
|
|
1655
|
+
if (transform.scale < 1 && scale <= 1) {
|
|
1656
|
+
targetScale =
|
|
1657
|
+
scale * (transform.scale + circIn(1 - scale) * (1 - transform.scale));
|
|
1658
|
+
}
|
|
1659
|
+
targetScale = Math.max(0.5, targetScale);
|
|
1660
|
+
const translateY =
|
|
1661
|
+
gesture === 'indeterminate' && !dismissable ? 0 : transform.translateY / scale;
|
|
1662
|
+
matrix = matrix
|
|
1663
|
+
.translate(transform.originX, transform.originY)
|
|
1664
|
+
.translate(transform.translateX / scale, translateY)
|
|
1665
|
+
.scale(targetScale / scale)
|
|
1666
|
+
.translate(-transform.originX, -transform.originY);
|
|
1667
|
+
if (gesture === 'pinch-zoom' && targetScale > 1) matrix = clampMatrix(matrix, fit);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
if (gesture !== 'pan-y-dismiss' && dismissing > 0) {
|
|
1671
|
+
dismissing = 0;
|
|
1672
|
+
// The swipe turned into something else (pan-x/page) — ease the page
|
|
1673
|
+
// back to full size.
|
|
1674
|
+
setPdfDismissScale(0, true);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
if (gesture !== 'pan-x' || scale > 1.01) updateItemMatrix(index, matrix);
|
|
1678
|
+
|
|
1679
|
+
if (gesture === 'pan-x' || gesture === 'pan-y-dismiss' || gesture === 'pan-y-page') {
|
|
1680
|
+
untrack(() => {
|
|
1681
|
+
if (!swiping) swiping = true;
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/** Called when a pointer (touch or mouse) moves */
|
|
1687
|
+
function onPointerMove(e: PointerEvent) {
|
|
1688
|
+
if (e.button === 2) return; // Ignore right clicks
|
|
1689
|
+
const pointer = pointers[`${e.pointerId}`];
|
|
1690
|
+
if (!pointer) return;
|
|
1691
|
+
const offsetX = list[index]?.pages?.[list[index]?.page]?.offsetX || 0;
|
|
1692
|
+
const offsetY = list[index]?.pages?.[list[index]?.page]?.offsetY || 0;
|
|
1693
|
+
const x = e.clientX - viewportX - offsetX;
|
|
1694
|
+
const y = e.clientY - viewportY - offsetY;
|
|
1695
|
+
onPointerEvent({
|
|
1696
|
+
id: `${e.pointerId}`,
|
|
1697
|
+
x,
|
|
1698
|
+
y,
|
|
1699
|
+
dx: x - pointer.x,
|
|
1700
|
+
dy: y - pointer.y,
|
|
1701
|
+
dt: Math.max(1, e.timeStamp - pointer.time),
|
|
1702
|
+
vx: (x - pointer.x) / Math.max(1, e.timeStamp - pointer.time),
|
|
1703
|
+
vy: (y - pointer.y) / Math.max(1, e.timeStamp - pointer.time),
|
|
1704
|
+
primary: e.isPrimary,
|
|
1705
|
+
time: e.timeStamp,
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/** Called when a pointer (touch or mouse) unpresses */
|
|
1710
|
+
function onPointerUp(e: PointerEvent) {
|
|
1711
|
+
if (e.button === 2) return;
|
|
1712
|
+
if (!Object.keys(pointers).length) return;
|
|
1713
|
+
const hasOtherPointers = Object.keys(pointers).some(
|
|
1714
|
+
(k) => k !== `${e.pointerId}` && k !== `ctrl_${e.pointerId}`,
|
|
1715
|
+
);
|
|
1716
|
+
if (hasOtherPointers) {
|
|
1717
|
+
delete pointers[`${e.pointerId}`];
|
|
1718
|
+
delete pointers[`ctrl_${e.pointerId}`];
|
|
1719
|
+
}
|
|
1720
|
+
if (!hasOtherPointers) onInteractionEnd(e);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/** Called when a pointer (touch or mouse) presses down */
|
|
1724
|
+
function onPointerDown(e: PointerEvent) {
|
|
1725
|
+
if (e.button === 2) return;
|
|
1726
|
+
if (opening) return;
|
|
1727
|
+
|
|
1728
|
+
// If the active item can't be swiped OR zoomed by the carousel (panorama,
|
|
1729
|
+
// embed, custom-with-both-gestures-disabled), let the inner renderer
|
|
1730
|
+
// receive the pointer untouched. Svelte 5 delegates `on*` handlers to the
|
|
1731
|
+
// document, so calling stopPropagation() here would prevent the inner
|
|
1732
|
+
// component (e.g. Panorama's drag-to-look) from ever seeing the event.
|
|
1733
|
+
const activeItem = list[index];
|
|
1734
|
+
if (activeItem && !isSwipeable(activeItem) && !isScalable(activeItem)) {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Skip carousel pointer handling for interactive elements inside the
|
|
1739
|
+
// slide (video controls, buttons, sliders, links, form inputs). These
|
|
1740
|
+
// need to receive the pointer themselves — preventDefault() here would
|
|
1741
|
+
// block click event generation, and stopPropagation() would prevent
|
|
1742
|
+
// Svelte-5 delegated handlers from firing at all.
|
|
1743
|
+
let target = e.target as HTMLElement | null;
|
|
1744
|
+
while (target && target !== e.currentTarget) {
|
|
1745
|
+
const tag = target.tagName;
|
|
1746
|
+
const role = target.getAttribute('role');
|
|
1747
|
+
if (
|
|
1748
|
+
tag === 'MEDIA-CONTROLS' ||
|
|
1749
|
+
tag === 'BUTTON' ||
|
|
1750
|
+
tag === 'A' ||
|
|
1751
|
+
tag === 'INPUT' ||
|
|
1752
|
+
tag === 'SELECT' ||
|
|
1753
|
+
tag === 'TEXTAREA' ||
|
|
1754
|
+
tag === 'LABEL' ||
|
|
1755
|
+
role === 'slider' ||
|
|
1756
|
+
role === 'button' ||
|
|
1757
|
+
role === 'menuitem' ||
|
|
1758
|
+
target.isContentEditable ||
|
|
1759
|
+
// The Range component (used for the video seek bar) handles track
|
|
1760
|
+
// click/drag itself via pointer capture on a plain `.range-wrapper`
|
|
1761
|
+
// <div> — which has no interactive tag/role above to match, so without
|
|
1762
|
+
// this the carousel would swallow the gesture and seeking would break.
|
|
1763
|
+
target.classList.contains('range-wrapper') ||
|
|
1764
|
+
target.classList.contains('range-container')
|
|
1765
|
+
) {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
target = target.parentElement;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
e.preventDefault();
|
|
1772
|
+
e.stopPropagation();
|
|
1773
|
+
|
|
1774
|
+
if (!Object.keys(pointers).length) onInteractionStart(e);
|
|
1775
|
+
|
|
1776
|
+
// Handle if the pointer should be treated as two pointers (to simulate pinch to zoom on desktop)
|
|
1777
|
+
if (e.ctrlKey) {
|
|
1778
|
+
pointers[`ctrl_${e.pointerId}`] = {
|
|
1779
|
+
id: `ctrl_${e.pointerId}`,
|
|
1780
|
+
x: window.innerWidth / 2,
|
|
1781
|
+
y: window.innerHeight / 2,
|
|
1782
|
+
dx: 0,
|
|
1783
|
+
dy: 0,
|
|
1784
|
+
dt: 0,
|
|
1785
|
+
vx: 0,
|
|
1786
|
+
vy: 0,
|
|
1787
|
+
primary: false,
|
|
1788
|
+
time: e.timeStamp,
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
const offsetX = list[index]?.pages?.[list[index]?.page]?.offsetX || 0;
|
|
1792
|
+
const offsetY = list[index]?.pages?.[list[index]?.page]?.offsetY || 0;
|
|
1793
|
+
const x = e.clientX - viewportX - offsetX;
|
|
1794
|
+
const y = e.clientY - viewportY - offsetY;
|
|
1795
|
+
pointers[`${e.pointerId}`] = {
|
|
1796
|
+
id: `${e.pointerId}`,
|
|
1797
|
+
x,
|
|
1798
|
+
y,
|
|
1799
|
+
dx: 0,
|
|
1800
|
+
dy: 0,
|
|
1801
|
+
dt: 0,
|
|
1802
|
+
vx: 0,
|
|
1803
|
+
vy: 0,
|
|
1804
|
+
primary: e.isPrimary,
|
|
1805
|
+
time: e.timeStamp,
|
|
1806
|
+
};
|
|
1807
|
+
midpoint = center(...Object.values(pointers));
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/** Called when the scroll wheel changes */
|
|
1811
|
+
function onWheelEvent(e: WheelEvent) {
|
|
1812
|
+
if (!viewport) return;
|
|
1813
|
+
lastWheelEvent = e.timeStamp;
|
|
1814
|
+
updateItemMatrix(index);
|
|
1815
|
+
const item = list[index];
|
|
1816
|
+
if (!item || !isScalable(item)) return;
|
|
1817
|
+
oninteraction?.();
|
|
1818
|
+
const pageData = item.pages[item.page];
|
|
1819
|
+
e.preventDefault();
|
|
1820
|
+
const [, dy] = normalizeWheel(e);
|
|
1821
|
+
const boundingRect = viewport.getBoundingClientRect();
|
|
1822
|
+
viewportX = boundingRect.left;
|
|
1823
|
+
viewportY = boundingRect.top;
|
|
1824
|
+
const scale = pageData.scale;
|
|
1825
|
+
// See zoomIn() for the PDF stack-offset rationale: PDF pages are
|
|
1826
|
+
// stacked in a single transformed container; the LI translation that
|
|
1827
|
+
// brings the active page into view must be added back to map pointer
|
|
1828
|
+
// coords into the container's coordinate space.
|
|
1829
|
+
const stackOffsetY = item.type === 'pdf' ? item.page * viewportH : 0;
|
|
1830
|
+
const originX = e.clientX - viewportX - pageData.offsetX;
|
|
1831
|
+
const originY = e.clientY - viewportY - pageData.offsetY + stackOffsetY;
|
|
1832
|
+
const scrollScale = 1 - dy * 0.03;
|
|
1833
|
+
let targetScale = Math.min(6, scale * scrollScale);
|
|
1834
|
+
if (scrollScale < 1 && scale <= 1) {
|
|
1835
|
+
targetScale = scale * (scrollScale + circIn(1 - scale) * (1 - scrollScale));
|
|
1836
|
+
}
|
|
1837
|
+
targetScale = Math.max(0.5, targetScale);
|
|
1838
|
+
let matrix = createMatrix(pageData.matrix)
|
|
1839
|
+
.translate(originX, originY)
|
|
1840
|
+
.scale(targetScale / scale)
|
|
1841
|
+
.translate(-originX, -originY);
|
|
1842
|
+
matrix = clampMatrix(matrix, {
|
|
1843
|
+
viewportW: pageData.offsetWidth || viewportW,
|
|
1844
|
+
viewportH: pageData.offsetHeight || viewportH,
|
|
1845
|
+
ratio: item.ratio,
|
|
1846
|
+
padding: CLAMP_PADDING,
|
|
1847
|
+
});
|
|
1848
|
+
animateItem(index, { transform: matrix, duration: 300 }).then(() => {
|
|
1849
|
+
if (lastWheelEvent !== e.timeStamp) return;
|
|
1850
|
+
const { scale } = extractMatrixTransform(matrix);
|
|
1851
|
+
if (scale < 1) {
|
|
1852
|
+
matrix = createMatrix();
|
|
1853
|
+
animateItem(index, {
|
|
1854
|
+
transform: matrix,
|
|
1855
|
+
easing: 'back-out',
|
|
1856
|
+
duration: 400,
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/** Called when the carousel container is resized */
|
|
1863
|
+
let resizeDebounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1864
|
+
function onViewportResizeEvent() {
|
|
1865
|
+
if (!viewport) return;
|
|
1866
|
+
viewportW = viewport.clientWidth || window.innerWidth;
|
|
1867
|
+
viewportH = viewport.clientHeight || window.innerHeight;
|
|
1868
|
+
|
|
1869
|
+
const item = list[index];
|
|
1870
|
+
if (!item) return;
|
|
1871
|
+
const currentPage = item.pages[item.page];
|
|
1872
|
+
if (!currentPage) return;
|
|
1873
|
+
if (!currentPage.matrix.isIdentity) {
|
|
1874
|
+
const el = getElementAtIndex(index, item.page);
|
|
1875
|
+
let scale = 1;
|
|
1876
|
+
if (el && currentPage.offsetHeight && el.offsetHeight) {
|
|
1877
|
+
scale = currentPage.offsetHeight / el.offsetHeight;
|
|
1878
|
+
}
|
|
1879
|
+
const lastX = currentPage.offsetX || 0;
|
|
1880
|
+
const currX = el?.offsetLeft || 0;
|
|
1881
|
+
const lastY = currentPage.offsetY || 0;
|
|
1882
|
+
const currY = el?.offsetTop || 0;
|
|
1883
|
+
const dx = lastX - currX;
|
|
1884
|
+
const dy = lastY - currY;
|
|
1885
|
+
const originX = viewportW / 2 - (currentPage.offsetWidth * currentPage.scale) / 2;
|
|
1886
|
+
const originY = 0;
|
|
1887
|
+
updateItemMatrix(
|
|
1888
|
+
index,
|
|
1889
|
+
clampMatrix(
|
|
1890
|
+
currentPage.matrix
|
|
1891
|
+
.translate(originX, originY)
|
|
1892
|
+
.scale(scale, scale)
|
|
1893
|
+
.translate(dx, dy)
|
|
1894
|
+
.translate(-originX, -originY),
|
|
1895
|
+
{
|
|
1896
|
+
viewportW: currentPage.offsetWidth || viewportW,
|
|
1897
|
+
viewportH: currentPage.offsetHeight || viewportH,
|
|
1898
|
+
ratio: item.ratio,
|
|
1899
|
+
padding: CLAMP_PADDING,
|
|
1900
|
+
},
|
|
1901
|
+
),
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
clearTimeout(resizeDebounceTimer);
|
|
1905
|
+
resizeDebounceTimer = setTimeout(() => {
|
|
1906
|
+
if (!list[index]) return;
|
|
1907
|
+
list[index].pages.forEach((page) => {
|
|
1908
|
+
const resolutionW = Math.max(
|
|
1909
|
+
page.resolutionW,
|
|
1910
|
+
Math.ceil(Math.min(2048, viewportW) * (page.scale || 1)),
|
|
1911
|
+
);
|
|
1912
|
+
const resolutionH = Math.max(
|
|
1913
|
+
page.resolutionH,
|
|
1914
|
+
Math.ceil(Math.min(2048, viewportH) * (page.scale || 1)),
|
|
1915
|
+
);
|
|
1916
|
+
if (resolutionW !== page.resolutionW || resolutionH !== page.resolutionH) {
|
|
1917
|
+
page.resolutionW = resolutionW;
|
|
1918
|
+
page.resolutionH = resolutionH;
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
}, 100);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
$effect(() => {
|
|
1925
|
+
if (!viewport) return;
|
|
1926
|
+
const observer = new ResizeObserver(() => untrack(() => onViewportResizeEvent()));
|
|
1927
|
+
observer.observe(viewport);
|
|
1928
|
+
return () => observer.disconnect();
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
/** Handles all key events when the carousel is active in modal mode */
|
|
1932
|
+
function onKeyDownEvent(e: KeyboardEvent) {
|
|
1933
|
+
const keys = [' ', 'ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown', '=', '-'];
|
|
1934
|
+
if (!keys.includes(e.key)) return;
|
|
1935
|
+
e.preventDefault();
|
|
1936
|
+
e.stopPropagation();
|
|
1937
|
+
if (list[index]?.type === 'video' && e.key === ' ') {
|
|
1938
|
+
list[index].shouldPlay = !list[index].shouldPlay;
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (e.key === ' ' && e.shiftKey) return prevSlide(1, 'keyboard');
|
|
1942
|
+
if (e.key === ' ') return nextSlide(1, 'keyboard');
|
|
1943
|
+
if (e.key === '=') return zoomIn();
|
|
1944
|
+
if (e.key === '-') return zoomOut();
|
|
1945
|
+
if (e.key === 'ArrowUp' && e.ctrlKey) return zoomIn();
|
|
1946
|
+
if (e.key === 'ArrowDown' && e.ctrlKey) return zoomOut();
|
|
1947
|
+
if (e.key === 'ArrowRight') return right();
|
|
1948
|
+
if (e.key === 'ArrowLeft') return left();
|
|
1949
|
+
if (e.key === 'ArrowUp') return up();
|
|
1950
|
+
if (e.key === 'ArrowDown') return down();
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
/** Handles when a full-res image is loaded */
|
|
1954
|
+
function onImageLoadEvent(i: number, e: Event) {
|
|
1955
|
+
const item = list[i];
|
|
1956
|
+
if (!item) return;
|
|
1957
|
+
const img = e.target as HTMLImageElement;
|
|
1958
|
+
item.width = img.naturalWidth;
|
|
1959
|
+
item.height = img.naturalHeight;
|
|
1960
|
+
item.ratio = (item.width || 1) / (item.height || 1);
|
|
1961
|
+
item.loaded = true;
|
|
1962
|
+
if (item.id && item.width) addLoadedResolution(item.id, item.width);
|
|
1963
|
+
|
|
1964
|
+
// When the current image is loaded, we can now allow it to fetch the full-res image
|
|
1965
|
+
if (i === index && item.pages?.length) {
|
|
1966
|
+
item.pages.forEach((page) => {
|
|
1967
|
+
const resolutionW = Math.max(
|
|
1968
|
+
page.resolutionW,
|
|
1969
|
+
Math.ceil(viewportW * (page.scale || 1)),
|
|
1970
|
+
);
|
|
1971
|
+
const resolutionH = Math.max(
|
|
1972
|
+
page.resolutionH,
|
|
1973
|
+
Math.ceil(viewportH * (page.scale || 1)),
|
|
1974
|
+
);
|
|
1975
|
+
if (resolutionW !== page.resolutionW || resolutionH !== page.resolutionH) {
|
|
1976
|
+
page.resolutionW = resolutionW;
|
|
1977
|
+
page.resolutionH = resolutionH;
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/** Handles when a pdf is loaded */
|
|
1984
|
+
function onPdfLoadEvent(i: number, numLoadedPages: number) {
|
|
1985
|
+
const item = list[i];
|
|
1986
|
+
if (!item) return;
|
|
1987
|
+
item.loaded = true;
|
|
1988
|
+
list[i] = {
|
|
1989
|
+
...item,
|
|
1990
|
+
loaded: true,
|
|
1991
|
+
pages: Array.from({ length: numLoadedPages }, () => ({
|
|
1992
|
+
x: 0,
|
|
1993
|
+
y: 0,
|
|
1994
|
+
z: 0,
|
|
1995
|
+
scale: 1,
|
|
1996
|
+
offsetX: 0,
|
|
1997
|
+
offsetY: 0,
|
|
1998
|
+
offsetHeight: viewportH,
|
|
1999
|
+
offsetWidth: viewportW,
|
|
2000
|
+
resolutionW: viewportW || viewport?.clientWidth || 0,
|
|
2001
|
+
resolutionH: viewportH || viewport?.clientHeight || 0,
|
|
2002
|
+
matrix: createMatrix(),
|
|
2003
|
+
})),
|
|
2004
|
+
};
|
|
2005
|
+
num_pages = list[index]?.pages?.length || 1;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/** Adds/removes the 'transitioning' class to the carousel container */
|
|
2009
|
+
async function updateGalleryContainerClass(): Promise<void> {
|
|
2010
|
+
if (!container) return;
|
|
2011
|
+
const isActive = (animation: Animation) =>
|
|
2012
|
+
animation.id.startsWith('slide') && animation.playState === 'running';
|
|
2013
|
+
const animations = container.getAnimations().filter(isActive);
|
|
2014
|
+
if (animations.length && !transitioning) transitioning = true;
|
|
2015
|
+
await Promise.all(animations.map((animation) => animation.finished.catch(() => {})));
|
|
2016
|
+
if (!container) return;
|
|
2017
|
+
const stillHasAnimations = container.getAnimations().some(isActive);
|
|
2018
|
+
if (stillHasAnimations) return updateGalleryContainerClass();
|
|
2019
|
+
if (transitioning) transitioning = false;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/** Initializes the event listeners when the carousel is shown */
|
|
2023
|
+
$effect(() => {
|
|
2024
|
+
if (!container || !viewport) return;
|
|
2025
|
+
destroyEventListeners();
|
|
2026
|
+
if (!inline) container.addEventListener('wheel', onWheelEvent, { passive: false });
|
|
2027
|
+
container.addEventListener('pointerdown', onPointerDown, { passive: false });
|
|
2028
|
+
if (!inline) document.addEventListener('keydown', onKeyDownEvent);
|
|
2029
|
+
const boundingRect = viewport.getBoundingClientRect();
|
|
2030
|
+
viewportY = boundingRect.top;
|
|
2031
|
+
viewportX = boundingRect.left;
|
|
2032
|
+
viewportW = viewport.clientWidth || window.innerWidth;
|
|
2033
|
+
viewportH = viewport.clientHeight || window.innerHeight;
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
/** Destroys the event listeners when the carousel is not being shown */
|
|
2037
|
+
function destroyEventListeners() {
|
|
2038
|
+
if (!browser) return;
|
|
2039
|
+
if (container) {
|
|
2040
|
+
container.removeEventListener('wheel', onWheelEvent);
|
|
2041
|
+
container.removeEventListener('pointerdown', onPointerDown);
|
|
2042
|
+
}
|
|
2043
|
+
document.removeEventListener('keydown', onKeyDownEvent);
|
|
2044
|
+
}
|
|
2045
|
+
onDestroy(() => destroyEventListeners());
|
|
2046
|
+
|
|
2047
|
+
$effect(() => {
|
|
2048
|
+
document.body.style.userSelect = dragging ? 'none' : '';
|
|
2049
|
+
});
|
|
2050
|
+
</script>
|
|
2051
|
+
|
|
2052
|
+
<div class="carousel" class:inline bind:this={viewport}>
|
|
2053
|
+
<ul
|
|
2054
|
+
class={['items', class_name].filter(Boolean).join(' ')}
|
|
2055
|
+
role="group"
|
|
2056
|
+
bind:this={container}
|
|
2057
|
+
class:opening
|
|
2058
|
+
class:zoomed={list[index]?.pages?.[list[index]?.page]?.scale > 1}
|
|
2059
|
+
class:dragging
|
|
2060
|
+
class:swiping
|
|
2061
|
+
class:transitioning
|
|
2062
|
+
class:animating={animation && animation !== 'none'}
|
|
2063
|
+
style:transform={containerTransform}
|
|
2064
|
+
{style}>
|
|
2065
|
+
{#each list as item, i (item.key)}
|
|
2066
|
+
{@const normalDistance = Math.abs(i - index)}
|
|
2067
|
+
{@const distance = Math.min(normalDistance, list.length - normalDistance)}
|
|
2068
|
+
{@const richNeighborDistance =
|
|
2069
|
+
item.type === 'video' ? VIDEO_NEIGHBOR_DISTANCE : RICH_NEIGHBOR_DISTANCE}
|
|
2070
|
+
{@const richMounted =
|
|
2071
|
+
distance <= richNeighborDistance ||
|
|
2072
|
+
(distance === 1 && (transitioning || dragging || swiping))}
|
|
2073
|
+
{#if distance === 0 || (!opening && distance <= 5) || (item.shouldLoad && distance <= 20)}
|
|
2074
|
+
<li
|
|
2075
|
+
class="item"
|
|
2076
|
+
class:active={i === index}
|
|
2077
|
+
data-index={i}
|
|
2078
|
+
aria-label="{i + 1} of {list.length}"
|
|
2079
|
+
inert={i !== index ||
|
|
2080
|
+
(!item.loaded && item.type !== 'image' && item.type !== 'custom') ||
|
|
2081
|
+
null}
|
|
2082
|
+
class:pdf={item.type === 'pdf'}
|
|
2083
|
+
style:transform={item.transform}
|
|
2084
|
+
style:perspective-origin={item.pages?.length > 1 && item.type !== 'pdf'
|
|
2085
|
+
? `50% ${50 + item.page * 100}%`
|
|
2086
|
+
: null}
|
|
2087
|
+
style:grid-column-start={((list.length + i - index + offset) % list.length) +
|
|
2088
|
+
1}>
|
|
2089
|
+
{#if item.shouldLoad}
|
|
2090
|
+
{#if !item.loaded && item.thumbhash}
|
|
2091
|
+
<img
|
|
2092
|
+
src={decodeThumbHash(item.thumbhash)}
|
|
2093
|
+
class:explicit-size={fit === 'contain'}
|
|
2094
|
+
style:opacity={(1 - dismissing) ** 4}
|
|
2095
|
+
style:--ratio={item.ratio || '1'}
|
|
2096
|
+
style:object-fit={fit || 'contain'}
|
|
2097
|
+
alt=""
|
|
2098
|
+
aria-hidden="true"
|
|
2099
|
+
class="preview" />
|
|
2100
|
+
{/if}
|
|
2101
|
+
{#if item.type === 'custom'}
|
|
2102
|
+
{#if custom}
|
|
2103
|
+
{@render custom({
|
|
2104
|
+
item: item as CarouselItem,
|
|
2105
|
+
onload: () => item.loaded || (list[i].loaded = true),
|
|
2106
|
+
onerror: () => {},
|
|
2107
|
+
active: i === index,
|
|
2108
|
+
gesture_disabled: !!item.disable_swipe,
|
|
2109
|
+
})}
|
|
2110
|
+
{/if}
|
|
2111
|
+
{:else if item.src}
|
|
2112
|
+
{#if item.type === 'image'}
|
|
2113
|
+
{#if !item.panorama}
|
|
2114
|
+
{@const responsive = isResponsiveSrcset(item.src)}
|
|
2115
|
+
<!--
|
|
2116
|
+
Only emit srcset/sizes when item.src is an actual responsive
|
|
2117
|
+
srcset (`url 400w, url 800w`). For a single-URL src, passing
|
|
2118
|
+
srcset along with a `sizes` value that differs from the Gallery
|
|
2119
|
+
thumbnail (`auto, 100vw` vs `100vw`) makes Chrome re-run its
|
|
2120
|
+
responsive image selection — which, with "Disable cache" on,
|
|
2121
|
+
triggers a brand-new fetch instead of reusing the thumbnail's
|
|
2122
|
+
already-decoded pixels.
|
|
2123
|
+
-->
|
|
2124
|
+
<img
|
|
2125
|
+
src={pickLargestSrc(item.src)}
|
|
2126
|
+
srcset={responsive ? item.src : undefined}
|
|
2127
|
+
class:explicit-size={fit === 'contain' && item.loaded}
|
|
2128
|
+
style:object-fit={fit || 'contain'}
|
|
2129
|
+
style:--ratio={item.ratio || '1'}
|
|
2130
|
+
alt={item.alt || item.name || ''}
|
|
2131
|
+
sizes={responsive ? '100vw' : undefined}
|
|
2132
|
+
loading={item.priority || i === index ? 'eager' : 'lazy'}
|
|
2133
|
+
fetchpriority={item.priority || i === index ? 'high' : undefined}
|
|
2134
|
+
onload={(e) => onImageLoadEvent(i, e)} />
|
|
2135
|
+
{:else if !richMounted}
|
|
2136
|
+
<div class="rich-placeholder" aria-hidden="true"></div>
|
|
2137
|
+
{:else if renderers.panorama}
|
|
2138
|
+
{@const Panorama = renderers.panorama}
|
|
2139
|
+
<Panorama
|
|
2140
|
+
src={pickLargestSrc(item.src)}
|
|
2141
|
+
show_controls={false}
|
|
2142
|
+
interactive={!inline}
|
|
2143
|
+
onload={() => item.loaded || (list[i].loaded = true)} />
|
|
2144
|
+
{:else}
|
|
2145
|
+
<div class="rich-loading" aria-label="Loading panorama">
|
|
2146
|
+
<span class="spinner"></span>
|
|
2147
|
+
</div>
|
|
2148
|
+
{/if}
|
|
2149
|
+
{:else if item.type === 'pdf'}
|
|
2150
|
+
{#if richMounted && renderers.pdf}
|
|
2151
|
+
{@const Pdf = renderers.pdf}
|
|
2152
|
+
<Pdf
|
|
2153
|
+
src={pickLargestSrc(item.src)}
|
|
2154
|
+
page={item.page + 1}
|
|
2155
|
+
show_toolbar={false}
|
|
2156
|
+
single_page
|
|
2157
|
+
auto_paginate={false}
|
|
2158
|
+
text_layer={false}
|
|
2159
|
+
fit="page"
|
|
2160
|
+
pixel_density={item._pdf_pixel_density || 1}
|
|
2161
|
+
onload={(detail: { total_pages: number }) =>
|
|
2162
|
+
onPdfLoadEvent(i, detail.total_pages)}
|
|
2163
|
+
onpagechange={(detail: { page: number; total_pages: number }) => {
|
|
2164
|
+
if (detail.page - 1 !== item.page) {
|
|
2165
|
+
goToPage(i, detail.page - 1);
|
|
2166
|
+
}
|
|
2167
|
+
}} />
|
|
2168
|
+
{/if}
|
|
2169
|
+
{:else if item.type === 'video'}
|
|
2170
|
+
{#if !richMounted}
|
|
2171
|
+
<div class="rich-placeholder" aria-hidden="true"></div>
|
|
2172
|
+
{:else if renderers.video}
|
|
2173
|
+
{@const Video = renderers.video}
|
|
2174
|
+
<Video
|
|
2175
|
+
src={pickLargestSrc(item.src)}
|
|
2176
|
+
poster={item.poster ??
|
|
2177
|
+
(item.thumbhash ? decodeThumbHash(item.thumbhash) : undefined)}
|
|
2178
|
+
autoplay={i === index && !!item.shouldPlay}
|
|
2179
|
+
bind:player={item._player}
|
|
2180
|
+
onready={() => item.loaded || (list[i].loaded = true)} />
|
|
2181
|
+
{:else}
|
|
2182
|
+
<div class="rich-loading" aria-label="Loading video">
|
|
2183
|
+
<span class="spinner"></span>
|
|
2184
|
+
</div>
|
|
2185
|
+
{/if}
|
|
2186
|
+
{:else if item.type === 'embed'}
|
|
2187
|
+
{#if !richMounted}
|
|
2188
|
+
<div class="rich-placeholder" aria-hidden="true"></div>
|
|
2189
|
+
{:else}
|
|
2190
|
+
{@const embedSrc = normalizeEmbedSrc(
|
|
2191
|
+
pickLargestSrc(item.src),
|
|
2192
|
+
i === index,
|
|
2193
|
+
)}
|
|
2194
|
+
<iframe
|
|
2195
|
+
class="embed"
|
|
2196
|
+
class:video={isVideoEmbed(embedSrc)}
|
|
2197
|
+
title={item.name}
|
|
2198
|
+
src={embedSrc}
|
|
2199
|
+
class:show={item.loaded}
|
|
2200
|
+
allowfullscreen
|
|
2201
|
+
allow="fullscreen; accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; magnetometer; xr-spatial-tracking;"
|
|
2202
|
+
onload={() => item.loaded || (list[i].loaded = true)}>
|
|
2203
|
+
</iframe>
|
|
2204
|
+
{/if}
|
|
2205
|
+
{/if}
|
|
2206
|
+
{/if}
|
|
2207
|
+
{/if}
|
|
2208
|
+
</li>
|
|
2209
|
+
{/if}
|
|
2210
|
+
{/each}
|
|
2211
|
+
</ul>
|
|
2212
|
+
<div class="visuallyhidden" aria-live="polite" aria-atomic="true" inert>
|
|
2213
|
+
Media Item {!list.length ? 0 : index + 1} of {list.length}
|
|
2214
|
+
</div>
|
|
2215
|
+
</div>
|
|
2216
|
+
|
|
2217
|
+
<style>
|
|
2218
|
+
.visuallyhidden {
|
|
2219
|
+
border: 0;
|
|
2220
|
+
clip: rect(0 0 0 0);
|
|
2221
|
+
clip-path: inset(50%);
|
|
2222
|
+
height: 1px;
|
|
2223
|
+
margin: -1px;
|
|
2224
|
+
overflow: hidden;
|
|
2225
|
+
padding: 0;
|
|
2226
|
+
position: absolute;
|
|
2227
|
+
width: 1px;
|
|
2228
|
+
white-space: nowrap;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
.carousel {
|
|
2232
|
+
user-select: none;
|
|
2233
|
+
-webkit-user-select: none;
|
|
2234
|
+
-webkit-tap-highlight-color: transparent;
|
|
2235
|
+
transform: translateZ(0px);
|
|
2236
|
+
overflow: hidden;
|
|
2237
|
+
height: 100%;
|
|
2238
|
+
|
|
2239
|
+
&.inline {
|
|
2240
|
+
.item {
|
|
2241
|
+
cursor: grab;
|
|
2242
|
+
> :global(*) {
|
|
2243
|
+
height: 100%;
|
|
2244
|
+
}
|
|
2245
|
+
:global(.video) {
|
|
2246
|
+
max-height: 100%;
|
|
2247
|
+
margin-top: 0;
|
|
2248
|
+
margin-bottom: 0;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
&:not(.inline) {
|
|
2253
|
+
.item {
|
|
2254
|
+
cursor: pointer;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
.items {
|
|
2259
|
+
position: relative;
|
|
2260
|
+
display: grid;
|
|
2261
|
+
grid-auto-columns: 100%;
|
|
2262
|
+
grid-template-rows: 100%;
|
|
2263
|
+
grid-auto-flow: column;
|
|
2264
|
+
height: 100%;
|
|
2265
|
+
list-style-type: none;
|
|
2266
|
+
transform-style: preserve-3d;
|
|
2267
|
+
margin: 0;
|
|
2268
|
+
padding: 0;
|
|
2269
|
+
touch-action: none;
|
|
2270
|
+
will-change: transform;
|
|
2271
|
+
backface-visibility: hidden;
|
|
2272
|
+
-webkit-backface-visibility: hidden;
|
|
2273
|
+
|
|
2274
|
+
&.zoomed {
|
|
2275
|
+
cursor: move;
|
|
2276
|
+
.item > img {
|
|
2277
|
+
cursor: move;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
&.opening {
|
|
2281
|
+
.item > :global(*) {
|
|
2282
|
+
opacity: 0;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
&.dragging:not(.zoomed) {
|
|
2286
|
+
cursor: grabbing;
|
|
2287
|
+
.item > img {
|
|
2288
|
+
cursor: grabbing;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
&.swiping {
|
|
2292
|
+
:global(.item *) {
|
|
2293
|
+
pointer-events: none !important;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
&.transitioning .item {
|
|
2297
|
+
pointer-events: none;
|
|
2298
|
+
:global(.item > *) {
|
|
2299
|
+
pointer-events: none;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
&:not(.animating) {
|
|
2303
|
+
.item.pdf {
|
|
2304
|
+
overflow: visible;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
&::after {
|
|
2308
|
+
content: '';
|
|
2309
|
+
position: relative;
|
|
2310
|
+
grid-row: 1;
|
|
2311
|
+
grid-column-start: 1;
|
|
2312
|
+
grid-column-end: span 999;
|
|
2313
|
+
z-index: -1;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
.item {
|
|
2319
|
+
grid-column: var(--col);
|
|
2320
|
+
grid-row: 1;
|
|
2321
|
+
display: grid;
|
|
2322
|
+
grid-auto-flow: row;
|
|
2323
|
+
grid-auto-rows: 100%;
|
|
2324
|
+
grid-template-columns: 100%;
|
|
2325
|
+
align-items: center;
|
|
2326
|
+
justify-content: center;
|
|
2327
|
+
justify-items: center;
|
|
2328
|
+
perspective: 100px;
|
|
2329
|
+
perspective-origin: center center;
|
|
2330
|
+
will-change: transform;
|
|
2331
|
+
backface-visibility: hidden;
|
|
2332
|
+
-webkit-backface-visibility: hidden;
|
|
2333
|
+
z-index: 1;
|
|
2334
|
+
overflow: hidden;
|
|
2335
|
+
> .preview {
|
|
2336
|
+
grid-row: 1;
|
|
2337
|
+
grid-column: 1;
|
|
2338
|
+
filter: blur(calc(10px + 1vw + 1vh)) contrast(1.3) saturate(1.2);
|
|
2339
|
+
@supports (filter: url('#sharpBlur') contrast(1.05) saturate(1.1)) {
|
|
2340
|
+
filter: url('#sharpBlur') contrast(1.05) saturate(1.1);
|
|
2341
|
+
}
|
|
2342
|
+
+ :global(*) {
|
|
2343
|
+
grid-row: 1;
|
|
2344
|
+
grid-column: 1;
|
|
2345
|
+
}
|
|
2346
|
+
&.explicit-size {
|
|
2347
|
+
width: calc(100cqh * var(--ratio, 1)) !important;
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
> :global(iframe) {
|
|
2351
|
+
border: none;
|
|
2352
|
+
outline: none;
|
|
2353
|
+
cursor: pointer;
|
|
2354
|
+
}
|
|
2355
|
+
> :global(canvas) {
|
|
2356
|
+
object-fit: cover;
|
|
2357
|
+
}
|
|
2358
|
+
> :global(*) {
|
|
2359
|
+
width: 100%;
|
|
2360
|
+
height: calc(
|
|
2361
|
+
100% - var(--carousel-padding-top, 0px) - var(--carousel-padding-bottom, 0px)
|
|
2362
|
+
);
|
|
2363
|
+
will-change: transform;
|
|
2364
|
+
transform-origin: 0px 0px;
|
|
2365
|
+
backface-visibility: hidden;
|
|
2366
|
+
-webkit-backface-visibility: hidden;
|
|
2367
|
+
cursor: grab;
|
|
2368
|
+
pointer-events: all;
|
|
2369
|
+
}
|
|
2370
|
+
> img {
|
|
2371
|
+
object-fit: contain;
|
|
2372
|
+
max-width: none;
|
|
2373
|
+
max-height: none;
|
|
2374
|
+
&.explicit-size {
|
|
2375
|
+
width: auto;
|
|
2376
|
+
height: 100%;
|
|
2377
|
+
max-height: calc(
|
|
2378
|
+
(100cqw * (1 / var(--ratio, 1))) - var(--carousel-padding-top, 0px) -
|
|
2379
|
+
var(--carousel-padding-bottom, 0px)
|
|
2380
|
+
);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
:global(.video) {
|
|
2384
|
+
aspect-ratio: 16 / 9;
|
|
2385
|
+
width: 100%;
|
|
2386
|
+
max-height: calc(
|
|
2387
|
+
100% - var(--carousel-padding-top, 0px) - var(--carousel-padding-bottom, 0px)
|
|
2388
|
+
);
|
|
2389
|
+
height: unset;
|
|
2390
|
+
align-self: center;
|
|
2391
|
+
margin-top: var(--carousel-padding-top, 0px);
|
|
2392
|
+
margin-bottom: var(--carousel-padding-bottom, 0px);
|
|
2393
|
+
cursor: pointer;
|
|
2394
|
+
}
|
|
2395
|
+
> .rich-loading {
|
|
2396
|
+
width: 100%;
|
|
2397
|
+
height: 100%;
|
|
2398
|
+
display: flex;
|
|
2399
|
+
align-items: center;
|
|
2400
|
+
justify-content: center;
|
|
2401
|
+
pointer-events: none;
|
|
2402
|
+
> .spinner {
|
|
2403
|
+
width: 36px;
|
|
2404
|
+
height: 36px;
|
|
2405
|
+
border-radius: 50%;
|
|
2406
|
+
border: 3px solid currentColor;
|
|
2407
|
+
border-top-color: transparent;
|
|
2408
|
+
opacity: 0.6;
|
|
2409
|
+
animation: carousel-spin 0.9s linear infinite;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
> .rich-placeholder {
|
|
2413
|
+
width: 100%;
|
|
2414
|
+
height: 100%;
|
|
2415
|
+
pointer-events: none;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
@keyframes carousel-spin {
|
|
2420
|
+
to {
|
|
2421
|
+
transform: rotate(360deg);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
</style>
|