astro-image-zoom 0.1.0-beta.2 → 0.1.0-beta.4
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/ImageZoom.astro +16 -3
- package/README.md +61 -34
- package/package.json +4 -5
- package/zoom.ts +99 -22
- package/zoom.css +0 -36
package/ImageZoom.astro
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
* Medium-style zoom overlay with accessibility and performance features
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
// Global stylesheet: selectors are prefixed and live in @layer astro-image-zoom
|
|
8
|
-
import "./zoom.css";
|
|
9
7
|
import { wrapImages } from "./wrapImages";
|
|
10
8
|
|
|
11
9
|
export interface Props {
|
|
@@ -107,6 +105,12 @@ const wrapperClasses = ["astro-image-zoom-wrapper", className]
|
|
|
107
105
|
|
|
108
106
|
// Wrap the slot's images in accessible links
|
|
109
107
|
const slotContent = wrapImages(await Astro.slots.render("default"));
|
|
108
|
+
|
|
109
|
+
// The element only groups its content and must not add a box. Its own declarative shadow root says
|
|
110
|
+
// so, with no stylesheet on the page: any rule of the site, layered or not, wins over :host. A string,
|
|
111
|
+
// since Astro would take <slot> for its own
|
|
112
|
+
const shadowRoot =
|
|
113
|
+
'<template shadowrootmode="open"><style>:host { display: contents; }</style><slot></slot></template>';
|
|
110
114
|
---
|
|
111
115
|
|
|
112
116
|
<astro-image-zoom
|
|
@@ -122,7 +126,7 @@ const slotContent = wrapImages(await Astro.slots.render("default"));
|
|
|
122
126
|
data-caption-position={captionPosition}
|
|
123
127
|
style={variables || undefined}
|
|
124
128
|
>
|
|
125
|
-
<Fragment set:html={slotContent} />
|
|
129
|
+
<Fragment set:html={shadowRoot + slotContent} />
|
|
126
130
|
</astro-image-zoom>
|
|
127
131
|
|
|
128
132
|
<script>
|
|
@@ -134,6 +138,15 @@ const slotContent = wrapImages(await Astro.slots.render("default"));
|
|
|
134
138
|
private zoom: Zoom | null = null;
|
|
135
139
|
|
|
136
140
|
connectedCallback() {
|
|
141
|
+
// The browser attaches the declarative shadow root while parsing, but not in pages parsed
|
|
142
|
+
// with DOMParser, as the client router of Astro does: attach it from the template then
|
|
143
|
+
if (!this.shadowRoot) {
|
|
144
|
+
const template = this.querySelector<HTMLTemplateElement>(":scope > template[shadowrootmode]");
|
|
145
|
+
if (template) {
|
|
146
|
+
this.attachShadow({ mode: "open" }).append(template.content.cloneNode(true));
|
|
147
|
+
template.remove();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
137
150
|
this.zoom = new Zoom(this);
|
|
138
151
|
}
|
|
139
152
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# astro-image-zoom
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/astro-image-zoom)
|
|
4
|
+
|
|
3
5
|
Medium-style image zoom for Astro: a click grows each image from its place on the page to fill the
|
|
4
6
|
screen, and the images of a gallery become a carousel you can swipe.
|
|
5
7
|
|
|
@@ -9,7 +11,9 @@ screen, and the images of a gallery become a carousel you can swipe.
|
|
|
9
11
|
## Features
|
|
10
12
|
|
|
11
13
|
- **Zoom from the page**: FLIP animations with CSS transforms and `clip-path`, from the thumbnail to
|
|
12
|
-
the full image and back, crops included
|
|
14
|
+
the full image and back, crops included: the `object-fit` and `object-position` of the thumbnail,
|
|
15
|
+
and thumbnail files cropped to another shape, such as Astro's `<Image width height>`. A file
|
|
16
|
+
stretched to another shape with `object-fit: fill` animates as with `cover`.
|
|
13
17
|
- **Accessible**: a native modal `<dialog>` that traps and restores focus, keyboard navigation,
|
|
14
18
|
labelled controls, reduced motion and forced colors.
|
|
15
19
|
- **Galleries**: a native scroll-snap carousel with touch and touchpad swipes, arrow keys, buttons and
|
|
@@ -62,17 +66,19 @@ const fullSize = await getImage({ src: photo, width: 1920 });
|
|
|
62
66
|
|
|
63
67
|
### Which image the zoom shows
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
>
|
|
75
|
-
>
|
|
69
|
+
- **An image on its own** opens the URL in its `data-zoom-src`, or its own `src` without it. Plain
|
|
70
|
+
images work out of the box: `<img src="/photo.jpg">` opens that same file, at full size.
|
|
71
|
+
- **An image inside a link with `data-zoom`** (`<a href="…" data-zoom>`) opens the `href` of the
|
|
72
|
+
link. Put `data-zoom-caption` (or `title`) on the link: the image inside gives only its `alt`, and
|
|
73
|
+
its own `data-zoom-src` and `data-zoom-caption` are ignored. See
|
|
74
|
+
[Gallery with Links](#gallery-with-links).
|
|
75
|
+
- **An image inside any other link** is left alone: the link keeps working as a link (a card, a
|
|
76
|
+
logo) and the image is not part of the gallery.
|
|
77
|
+
|
|
78
|
+
> **Resized images barely grow when zoomed.** The zoom shows each image at most at its real size,
|
|
79
|
+
> never enlarged. If the image on the page is a smaller version (a thumbnail, or Astro's
|
|
80
|
+
> `<Image width={400}>`, which generates a 400 px file), the zoom opens that small file and it
|
|
81
|
+
> stays small. Add `data-zoom-src` with the full-size version, as shown in
|
|
76
82
|
> [High-Resolution Images](#high-resolution-images) and
|
|
77
83
|
> [Using with Astro Assets](#using-with-astro-assets-optimized-images).
|
|
78
84
|
|
|
@@ -219,10 +225,6 @@ You can override the default styles using CSS variables:
|
|
|
219
225
|
--zoom-caption-font-size: 13px;
|
|
220
226
|
--zoom-color-scheme: light dark; /* set "dark" or "light" to follow your own theme toggle */
|
|
221
227
|
--zoom-caption-radius: 6px;
|
|
222
|
-
|
|
223
|
-
/* Focus ring drawn on the image when its link has keyboard focus */
|
|
224
|
-
--zoom-focus-outline: 3px solid rebeccapurple; /* default: 2px solid currentColor */
|
|
225
|
-
--zoom-focus-offset: 3px; /* use a negative value if a parent with overflow: hidden clips it */
|
|
226
228
|
}
|
|
227
229
|
```
|
|
228
230
|
|
|
@@ -262,30 +264,49 @@ astro-image-zoom-overlay::part(caption) {
|
|
|
262
264
|
| `nav`, `prev`, `next` | The arrows (`nav` matches both) |
|
|
263
265
|
| `counter` | The position in the gallery |
|
|
264
266
|
|
|
265
|
-
|
|
266
|
-
`astro-image-zoom/zoom.css` directly only if you use `ZoomClass` without the `<ImageZoom>` component.
|
|
267
|
+
#### Your page
|
|
267
268
|
|
|
268
|
-
|
|
269
|
+
How your images look on the page is up to your site: the component doesn't style them, not even
|
|
270
|
+
their focus ring. It only wraps each image in a link, so it opens with the keyboard and, without
|
|
271
|
+
JavaScript, links to the full-size image:
|
|
269
272
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
```html
|
|
274
|
+
<a href="/full-size.jpg" data-zoom-generated aria-label="Enlarge image: A red car">
|
|
275
|
+
<img src="/photo.jpg" alt="A red car" />
|
|
276
|
+
</a>
|
|
277
|
+
```
|
|
273
278
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
The link is inline and unstyled, so it never changes your layout, and it gets your site's link
|
|
280
|
+
styles, focus ring included. If your images are `display: block`, the outline of an inline link
|
|
281
|
+
doesn't wrap them: depending on the browser it is invisible or spans the whole line. Draw your
|
|
282
|
+
focus ring on the image instead, in your own style:
|
|
277
283
|
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
284
|
+
```css
|
|
285
|
+
a[data-zoom-generated]:focus-visible {
|
|
286
|
+
outline: none;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
a[data-zoom-generated]:focus-visible img {
|
|
290
|
+
outline: 2px solid green; /* your focus ring */
|
|
291
|
+
outline-offset: 3px; /* negative if a parent with overflow: hidden clips it */
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
A zoom cursor, if you want one, is also yours to add:
|
|
296
|
+
|
|
297
|
+
```css
|
|
298
|
+
a[data-zoom-generated] img {
|
|
299
|
+
cursor: zoom-in;
|
|
300
|
+
}
|
|
282
301
|
```
|
|
283
302
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
303
|
+
The component adds no stylesheet to your page. `<astro-image-zoom>` groups the images without
|
|
304
|
+
adding a box of its own (`display: contents`), from its own
|
|
305
|
+
[declarative shadow root](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM#declaratively_with_html),
|
|
306
|
+
so it works without JavaScript and any rule of your site wins over it, cascade layers included.
|
|
287
307
|
|
|
288
|
-
To style the images
|
|
308
|
+
To style one gallery, pass a class. It can style the images, or lay out the gallery on the element
|
|
309
|
+
itself:
|
|
289
310
|
|
|
290
311
|
```astro
|
|
291
312
|
<ImageZoom class="custom-zoom">
|
|
@@ -294,6 +315,11 @@ To style the images of one gallery on the page, pass a class:
|
|
|
294
315
|
|
|
295
316
|
<style>
|
|
296
317
|
/* Global: the wrapper is rendered by the component, outside the scope of your page */
|
|
318
|
+
:global(.custom-zoom) {
|
|
319
|
+
display: grid;
|
|
320
|
+
grid-template-columns: repeat(3, 1fr);
|
|
321
|
+
}
|
|
322
|
+
|
|
297
323
|
:global(.custom-zoom img) {
|
|
298
324
|
border-radius: 8px;
|
|
299
325
|
}
|
|
@@ -318,7 +344,8 @@ To style the images of one gallery on the page, pass a class:
|
|
|
318
344
|
|
|
319
345
|
- A native modal `<dialog>`: focus moves to the close button, stays inside while it is open and
|
|
320
346
|
returns to the image when it closes.
|
|
321
|
-
- Each image becomes a link, reachable with the keyboard
|
|
347
|
+
- Each image becomes a link, reachable with the keyboard. Its focus ring is the one of your site;
|
|
348
|
+
see [Your page](#your-page) to draw it on the image.
|
|
322
349
|
- Labelled buttons; the caption is announced when the image changes.
|
|
323
350
|
- Reduced motion turns the animations off; forced colors (Windows high contrast) keep the controls
|
|
324
351
|
visible.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-image-zoom",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
4
|
"description": "Medium-style image zoom for Astro: accessible, animated from the page, with swipeable galleries",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Antonio (https://antoniohg.com)",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"url": "git+https://github.com/antoniohg/astro-image-zoom.git",
|
|
11
11
|
"directory": "packages/astro-image-zoom"
|
|
12
12
|
},
|
|
13
|
-
"homepage": "https://
|
|
13
|
+
"homepage": "https://antoniohg.com/astro-image-zoom/",
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/antoniohg/astro-image-zoom/issues"
|
|
16
16
|
},
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"exports": {
|
|
27
27
|
".": "./index.js",
|
|
28
|
-
"./ImageZoom.astro": "./ImageZoom.astro"
|
|
29
|
-
"./zoom.css": "./zoom.css"
|
|
28
|
+
"./ImageZoom.astro": "./ImageZoom.astro"
|
|
30
29
|
},
|
|
31
30
|
"files": [
|
|
32
31
|
"index.js",
|
|
@@ -35,7 +34,6 @@
|
|
|
35
34
|
"overlay.css",
|
|
36
35
|
"env.d.ts",
|
|
37
36
|
"wrapImages.ts",
|
|
38
|
-
"zoom.css",
|
|
39
37
|
"README.md"
|
|
40
38
|
],
|
|
41
39
|
"peerDependencies": {
|
|
@@ -48,6 +46,7 @@
|
|
|
48
46
|
"vitest": "5.0.1"
|
|
49
47
|
},
|
|
50
48
|
"scripts": {
|
|
49
|
+
"check": "astro check",
|
|
51
50
|
"test": "vitest run",
|
|
52
51
|
"test:e2e": "playwright test -c e2e/playwright.config.ts"
|
|
53
52
|
}
|
package/zoom.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Medium-style zoom with accessibility and performance optimizations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
/// <reference path="./env.d.ts" />
|
|
7
6
|
import overlayStyles from './overlay.css?inline';
|
|
8
7
|
|
|
9
8
|
interface ZoomImage {
|
|
@@ -135,6 +134,15 @@ function getOverlay(): HTMLDialogElement {
|
|
|
135
134
|
// A box on the screen, such as the one getBoundingClientRect() returns
|
|
136
135
|
type Box = Pick<DOMRect, 'left' | 'top' | 'width' | 'height'>;
|
|
137
136
|
|
|
137
|
+
// How a thumbnail draws its file inside its box: the computed object-fit and object-position of the
|
|
138
|
+
// <img>, and the size of the file
|
|
139
|
+
export interface ThumbnailFit {
|
|
140
|
+
objectFit: string;
|
|
141
|
+
objectPosition: string;
|
|
142
|
+
naturalWidth: number;
|
|
143
|
+
naturalHeight: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
138
146
|
interface FlipTransform {
|
|
139
147
|
x: number;
|
|
140
148
|
y: number;
|
|
@@ -142,23 +150,91 @@ interface FlipTransform {
|
|
|
142
150
|
clipPath: string;
|
|
143
151
|
}
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
* cropped with object-fit: cover so grows into the whole image.
|
|
150
|
-
*/
|
|
151
|
-
export function flipTransform(source: Box, final: Box): FlipTransform {
|
|
152
|
-
// Cover the source box, as object-fit: cover does
|
|
153
|
-
const scale = Math.max(source.width / final.width, source.height / final.height);
|
|
153
|
+
function thumbnailFit(img: HTMLImageElement): ThumbnailFit {
|
|
154
|
+
const { objectFit, objectPosition } = getComputedStyle(img);
|
|
155
|
+
return { objectFit, objectPosition, naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight };
|
|
156
|
+
}
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
// Where object-position puts the drawn file along one axis, given the free space of the box
|
|
159
|
+
// (negative when the file overflows it). Browsers compute it as a percentage of that space, a
|
|
160
|
+
// length in px, or calc() of both: "right 10px" is calc(100% - 10px). Anything else, or nothing,
|
|
161
|
+
// centers the file, the default
|
|
162
|
+
function positionOffset(value: string | undefined, free: number): number {
|
|
163
|
+
const percent = /(-?[\d.]+)%/.exec(value ?? '');
|
|
164
|
+
const pixels = /([+-]?)\s*(-?[\d.]+)px/.exec(value ?? '');
|
|
165
|
+
if (!percent && !pixels) return free / 2;
|
|
166
|
+
|
|
167
|
+
const offset =
|
|
168
|
+
(percent ? (free * Number(percent[1])) / 100 : 0) +
|
|
169
|
+
(pixels ? Number(pixels[2]) * (pixels[1] === '-' ? -1 : 1) : 0);
|
|
170
|
+
return Number.isFinite(offset) ? offset : free / 2;
|
|
171
|
+
}
|
|
157
172
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
// The box where a thumbnail draws its file on the screen, which object-fit can make smaller than
|
|
174
|
+
// its box (contain) or bigger (cover, none). Without a fit, or with fill, it is the box itself.
|
|
175
|
+
// fill also stretches a file of another shape, which one uniform scale cannot follow: such a
|
|
176
|
+
// thumbnail animates as with cover
|
|
177
|
+
function drawnBox(source: Box, fit?: ThumbnailFit): Box {
|
|
178
|
+
if (!fit || !fit.naturalWidth || !fit.naturalHeight) return source;
|
|
179
|
+
|
|
180
|
+
const widthRatio = source.width / fit.naturalWidth;
|
|
181
|
+
const heightRatio = source.height / fit.naturalHeight;
|
|
182
|
+
const scales: Record<string, number> = {
|
|
183
|
+
contain: Math.min(widthRatio, heightRatio),
|
|
184
|
+
cover: Math.max(widthRatio, heightRatio),
|
|
185
|
+
none: 1,
|
|
186
|
+
'scale-down': Math.min(1, widthRatio, heightRatio),
|
|
187
|
+
};
|
|
188
|
+
const scale = scales[fit.objectFit];
|
|
189
|
+
if (scale === undefined) return source;
|
|
190
|
+
|
|
191
|
+
const width = fit.naturalWidth * scale;
|
|
192
|
+
const height = fit.naturalHeight * scale;
|
|
193
|
+
// One value per axis; a calc() has spaces of its own
|
|
194
|
+
const [positionX, positionY] = fit.objectPosition.match(/calc\([^)]*\)|\S+/g) ?? [];
|
|
195
|
+
return {
|
|
196
|
+
left: source.left + positionOffset(positionX, source.width - width),
|
|
197
|
+
top: source.top + positionOffset(positionY, source.height - height),
|
|
198
|
+
width,
|
|
199
|
+
height,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The FLIP transform that lays the zoomed image, with the `final` box, over its thumbnail, with the
|
|
205
|
+
* `source` box: the translation between their centers, the scale, and the clip-path (in the
|
|
206
|
+
* image's own, unscaled pixels) that trims what the thumbnail does not show.
|
|
207
|
+
*
|
|
208
|
+
* `fit` tells how the thumbnail draws its file (object-fit, object-position). That file may also be
|
|
209
|
+
* cropped from the full image already, as Astro's <Image> does with a width and a height of another
|
|
210
|
+
* shape: it is taken as a centered crop, so the scale matches what the thumbnail really shows.
|
|
211
|
+
* Without `fit`, the image covers the source box, as object-fit: cover does.
|
|
212
|
+
*/
|
|
213
|
+
export function flipTransform(source: Box, final: Box, fit?: ThumbnailFit): FlipTransform {
|
|
214
|
+
const drawn = drawnBox(source, fit);
|
|
215
|
+
|
|
216
|
+
// The whole image around the drawn file, at the same scale, taking the file as a centered crop
|
|
217
|
+
const imageRatio = final.width / final.height;
|
|
218
|
+
const drawnRatio = drawn.width / drawn.height;
|
|
219
|
+
const width = imageRatio > drawnRatio ? drawn.height * imageRatio : drawn.width;
|
|
220
|
+
const height = width / imageRatio;
|
|
221
|
+
const left = drawn.left + (drawn.width - width) / 2;
|
|
222
|
+
const top = drawn.top + (drawn.height - height) / 2;
|
|
223
|
+
const scale = width / final.width;
|
|
224
|
+
|
|
225
|
+
const x = left + width / 2 - (final.left + final.width / 2);
|
|
226
|
+
const y = top + height / 2 - (final.top + final.height / 2);
|
|
227
|
+
|
|
228
|
+
// Only what the thumbnail shows stays visible: the drawn file, cut to the box of the thumbnail
|
|
229
|
+
const visibleLeft = Math.max(drawn.left, source.left);
|
|
230
|
+
const visibleTop = Math.max(drawn.top, source.top);
|
|
231
|
+
const visibleRight = Math.min(drawn.left + drawn.width, source.left + source.width);
|
|
232
|
+
const visibleBottom = Math.min(drawn.top + drawn.height, source.top + source.height);
|
|
233
|
+
const insetTop = (visibleTop - top) / scale;
|
|
234
|
+
const insetRight = (left + width - visibleRight) / scale;
|
|
235
|
+
const insetBottom = (top + height - visibleBottom) / scale;
|
|
236
|
+
const insetLeft = (visibleLeft - left) / scale;
|
|
237
|
+
const clipPath = `inset(${insetTop}px ${insetRight}px ${insetBottom}px ${insetLeft}px)`;
|
|
162
238
|
|
|
163
239
|
return { x, y, scale, clipPath };
|
|
164
240
|
}
|
|
@@ -234,10 +310,10 @@ class Zoom {
|
|
|
234
310
|
this.setupEventListeners();
|
|
235
311
|
}
|
|
236
312
|
|
|
313
|
+
// Only the links the component generated and those the site marks with data-zoom: any other link
|
|
314
|
+
// with an image (a card, a logo) keeps navigating
|
|
237
315
|
private collectImages(): void {
|
|
238
|
-
const links = this.wrapper.querySelectorAll<HTMLAnchorElement>(
|
|
239
|
-
'a[data-zoom-generated], a[data-zoom], a:has(img)'
|
|
240
|
-
);
|
|
316
|
+
const links = this.wrapper.querySelectorAll<HTMLAnchorElement>('a[data-zoom-generated], a[data-zoom]');
|
|
241
317
|
|
|
242
318
|
this.state.images = Array.from(links)
|
|
243
319
|
.map((anchor) => {
|
|
@@ -249,7 +325,7 @@ class Zoom {
|
|
|
249
325
|
element: anchor
|
|
250
326
|
};
|
|
251
327
|
})
|
|
252
|
-
.filter(({ src }) => src
|
|
328
|
+
.filter(({ src }) => src);
|
|
253
329
|
}
|
|
254
330
|
|
|
255
331
|
private setupEventListeners(): void {
|
|
@@ -326,6 +402,7 @@ class Zoom {
|
|
|
326
402
|
// Get source image and position
|
|
327
403
|
const sourceImg = this.state.images[index].element.querySelector('img')!;
|
|
328
404
|
const sourceRect = sourceImg.getBoundingClientRect();
|
|
405
|
+
const sourceFit = thumbnailFit(sourceImg);
|
|
329
406
|
|
|
330
407
|
this.buildSlides();
|
|
331
408
|
this.renderActive(index);
|
|
@@ -378,7 +455,7 @@ class Zoom {
|
|
|
378
455
|
|
|
379
456
|
// FLIP Animation
|
|
380
457
|
const finalRect = this.imageElement.getBoundingClientRect();
|
|
381
|
-
const transform = flipTransform(sourceRect, finalRect);
|
|
458
|
+
const transform = flipTransform(sourceRect, finalRect, sourceFit);
|
|
382
459
|
|
|
383
460
|
// Set CSS variables for animation
|
|
384
461
|
this.setAnimationVariables(transform);
|
|
@@ -450,7 +527,7 @@ class Zoom {
|
|
|
450
527
|
const targetRect = sourceImg.getBoundingClientRect();
|
|
451
528
|
|
|
452
529
|
// Calculate transform for closing animation
|
|
453
|
-
const transform = flipTransform(targetRect, startRect);
|
|
530
|
+
const transform = flipTransform(targetRect, startRect, thumbnailFit(sourceImg));
|
|
454
531
|
|
|
455
532
|
// Set CSS variables for closing animation
|
|
456
533
|
this.setAnimationVariables(transform);
|
package/zoom.css
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* astro-image-zoom page styles: the images on the page, not the zoom overlay.
|
|
3
|
-
*
|
|
4
|
-
* The overlay lives in a shadow root and brings its own styles (overlay.css), so no CSS of
|
|
5
|
-
* the page can reach it. Style it with the --zoom-* custom properties or with ::part().
|
|
6
|
-
*
|
|
7
|
-
* These page styles live in the `astro-image-zoom` cascade layer, so any unlayered CSS on the page
|
|
8
|
-
* overrides them without specificity fights. To control their priority among your own layers,
|
|
9
|
-
* name the layer in your layer order, e.g. `@layer reset, astro-image-zoom, components;`.
|
|
10
|
-
*/
|
|
11
|
-
@layer astro-image-zoom {
|
|
12
|
-
/* The element only groups its content, it must not add a box */
|
|
13
|
-
astro-image-zoom {
|
|
14
|
-
display: contents;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/* Links wrapping images: the image is the click target */
|
|
18
|
-
.astro-image-zoom-wrapper a img {
|
|
19
|
-
cursor: zoom-in;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/*
|
|
23
|
-
* Focus ring on the image, not on the generated link: the link is inline, so when the image
|
|
24
|
-
* is display: block its outline does not wrap the image. Customize it with
|
|
25
|
-
* --zoom-focus-outline and --zoom-focus-offset (a negative offset draws it inside the image,
|
|
26
|
-
* useful when a parent with overflow: hidden would clip it).
|
|
27
|
-
*/
|
|
28
|
-
.astro-image-zoom-wrapper a[data-zoom-generated]:focus-visible {
|
|
29
|
-
outline: none;
|
|
30
|
-
|
|
31
|
-
img {
|
|
32
|
-
outline: var(--zoom-focus-outline, 2px solid currentColor);
|
|
33
|
-
outline-offset: var(--zoom-focus-offset, 3px);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|