Skip to content

Commit 72a5af2

Browse files
authored
fix: rewrites the zooming logic to avoid blurry images on some devices/browsers (especially visible on images with text)
1 parent bb2fbcf commit 72a5af2

File tree

5 files changed

+57
-36
lines changed

5 files changed

+57
-36
lines changed

.github/workflows/integration.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
uses: actions/checkout@v4
2323

2424
- name: Install pnpm
25-
uses: pnpm/action-setup@v3
25+
uses: pnpm/action-setup@v4
2626

2727
- name: Install Node.js
2828
uses: actions/setup-node@v4

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
fetch-depth: 0
2626

2727
- name: Install pnpm
28-
uses: pnpm/action-setup@v3
28+
uses: pnpm/action-setup@v4
2929

3030
- name: Install Node.js
3131
uses: actions/setup-node@v4

docs/src/assets/tests/text.jpg

188 KB
Loading
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: Image with text
3+
pagefind: false
4+
---
5+
6+
Photo by <a href="https://unsplash.com/@lukechesser?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Luke Chesser</a> on <a href="https://unsplash.com/photos/github-website-on-desktop-LG8ToawE8WQ?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>
7+
8+
![Screenshot of GitHub on a desktop computer](../../../assets/tests/text.jpg)

packages/starlight-image-zoom/components/ImageZoom.astro

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ import config from 'virtual:starlight-image-zoom-config'
7373
width: 44px;
7474
}
7575

76-
.starlight-image-zoom-opened .starlight-image-zoom-control {
76+
:is(.starlight-image-zoom-opened, .starlight-image-zoom-transition) .starlight-image-zoom-control {
7777
inset: 20px 20px auto auto;
7878
}
7979

@@ -166,6 +166,9 @@ import config from 'virtual:starlight-image-zoom-config'
166166
<script>
167167
import { STARLIGHT_IMAGE_ZOOM_ZOOMABLE_TAG } from '../libs/constants'
168168

169+
// https://caniuse.com/requestidlecallback
170+
const onIdle = window.requestIdleCallback ?? ((cb) => setTimeout(cb, 1))
171+
169172
/**
170173
* Based on:
171174
* - https://github.com/francoischalifour/medium-zoom
@@ -183,16 +186,16 @@ import config from 'virtual:starlight-image-zoom-config'
183186
image: 'starlight-image-zoom-image',
184187
opened: 'starlight-image-zoom-opened',
185188
source: 'starlight-image-zoom-source',
189+
transition: 'starlight-image-zoom-transition',
186190
}
191+
#dataZoomTransformKey = 'zoomTransform'
192+
187193
static #initialized = false
188194

189195
constructor() {
190196
super()
191197

192198
const initialize = () => {
193-
// https://caniuse.com/requestidlecallback
194-
const onIdle = window.requestIdleCallback ?? ((cb) => setTimeout(cb, 1))
195-
196199
onIdle(() => {
197200
const zoomables = [...document.querySelectorAll(STARLIGHT_IMAGE_ZOOM_ZOOMABLE_TAG)]
198201
if (zoomables.length === 0) return
@@ -281,8 +284,8 @@ import config from 'virtual:starlight-image-zoom-config'
281284
.querySelector('header')
282285
?.style.setProperty('padding-inline-end', `calc(var(--sl-nav-pad-x) + ${window.innerWidth - clientWidth}px)`)
283286

284-
// Clone the image to zoom at the same position of the original image.
285-
const zoomedImage = this.#cloneImageAtPosition(image)
287+
// Clone the image and apply a zoom effect to it.
288+
const zoomedImage = this.#cloneAndZoomImage(image)
286289

287290
// Apply CSS classes to hide the source image and transition the zoomed image.
288291
image.classList.add(this.#classList.source)
@@ -302,9 +305,11 @@ import config from 'virtual:starlight-image-zoom-config'
302305
dialog.addEventListener('cancel', this.#onCancel)
303306
dialog.showModal()
304307

305-
// Apply a zoom effect to the zoomed image.
306-
zoomedImage.style.transform = this.#getZoomEffectTransform(image, figure)
307-
document.body.classList.add(this.#classList.opened)
308+
// Apply a zoom effect to zoom the image in.
309+
onIdle(() => {
310+
zoomedImage.style.transform = ''
311+
document.body.classList.add(this.#classList.opened)
312+
})
308313

309314
this.#currentZoom = { body, dialog, image, zoomedImage }
310315
}
@@ -317,7 +322,8 @@ import config from 'virtual:starlight-image-zoom-config'
317322
const { zoomedImage } = this.#currentZoom
318323

319324
// Remove the zoom effect from the zoomed image.
320-
zoomedImage.style.transform = ''
325+
zoomedImage.style.transform = zoomedImage.dataset[this.#dataZoomTransformKey] ?? ''
326+
document.body.classList.add(this.#classList.transition)
321327
document.body.classList.remove(this.#classList.opened)
322328

323329
const { matches: prefersReducedMotion } = window.matchMedia('(prefers-reduced-motion: reduce)')
@@ -335,6 +341,7 @@ import config from 'virtual:starlight-image-zoom-config'
335341
const { dialog, image } = this.#currentZoom
336342

337343
// Show the source image.
344+
document.body.classList.remove(this.#classList.transition)
338345
image.classList.remove(this.#classList.source)
339346

340347
// Remove the portaled dialog.
@@ -368,8 +375,22 @@ import config from 'virtual:starlight-image-zoom-config'
368375
figure.append(caption)
369376
}
370377

371-
#cloneImageAtPosition(image: HTMLImageElement) {
372-
const { height, left, top, width } = image.getBoundingClientRect()
378+
#cloneAndZoomImage(image: HTMLImageElement) {
379+
const imageRect = image.getBoundingClientRect()
380+
381+
// Zoom SVGs at the figure's size, not the image's size.
382+
const isSVG = this.#isSVGImage(image)
383+
const naturalWidth = isSVG ? window.innerWidth : image.naturalWidth
384+
const naturalHeight = isSVG ? window.innerHeight : image.naturalHeight
385+
386+
const maxWidth = Math.min(window.innerWidth, naturalWidth)
387+
const maxHeight = Math.min(window.innerHeight, naturalHeight)
388+
const scale = Math.min(maxWidth / naturalWidth, maxHeight / naturalHeight)
389+
390+
const width = (isSVG ? window.innerWidth : image.naturalWidth) * scale
391+
const height = (isSVG ? window.innerHeight : image.naturalHeight) * scale
392+
const top = (window.innerHeight - height) / 2
393+
const left = (window.innerWidth - width) / 2
373394

374395
const clone = image.cloneNode(true) as HTMLImageElement
375396
clone.removeAttribute('id')
@@ -381,6 +402,20 @@ import config from 'virtual:starlight-image-zoom-config'
381402
clone.style.left = `${left}px`
382403
clone.style.transform = ''
383404

405+
// Finds out the scale transformations so that the zoomed image fits within the image rect.
406+
const scaleX = imageRect.width / width
407+
const scaleY = imageRect.height / height
408+
409+
// Calculate the translation to align the zoomed image within the image rect.
410+
const translateX = (-left + (imageRect.width - width) / 2 + imageRect.left) / scaleX
411+
const translateY = (-top + (imageRect.height - height) / 2 + imageRect.top) / scaleY
412+
413+
// Apply the scale and translation to the zoomed image.
414+
clone.style.transform = `scale(${scaleX}, ${scaleY}) translate3d(${translateX}px, ${translateY}px, 0)`
415+
416+
// Save the transform to a data attribute to be able to animate it back to the original position.
417+
clone.dataset[this.#dataZoomTransformKey] = clone.style.transform
418+
384419
// If the image is inside a `<picture>` element, we need to update the `src` attribute and use the correct
385420
// source.
386421
if (image.parentElement?.tagName === 'PICTURE' && image.currentSrc) {
@@ -390,28 +425,6 @@ import config from 'virtual:starlight-image-zoom-config'
390425
return clone
391426
}
392427

393-
#getZoomEffectTransform(image: HTMLImageElement, figure: HTMLElement) {
394-
const isSVG = this.#isSVGImage(image)
395-
396-
const imageRect = image.getBoundingClientRect()
397-
const figureRect = figure.getBoundingClientRect()
398-
399-
// Zoom SVGs at the figure's size, not the image's size.
400-
const zoomedHeight = isSVG ? figureRect.height : image.naturalHeight
401-
const zoomedWidth = isSVG ? figureRect.width : image.naturalWidth
402-
403-
// Finds out the scale so that the zoomed image fits within the figure element.
404-
const scaleX = Math.min(Math.max(imageRect.width, zoomedWidth), figureRect.width) / imageRect.width
405-
const scaleY = Math.min(Math.max(imageRect.height, zoomedHeight), figureRect.height) / imageRect.height
406-
const scale = Math.min(scaleX, scaleY)
407-
408-
// Calculate the translation to center the zoomed image within the figure element.
409-
const translateX = (-imageRect.left + (figureRect.width - imageRect.width) / 2 + figureRect.left) / scale
410-
const translateY = (-imageRect.top + (figureRect.height - imageRect.height) / 2 + figureRect.top) / scale
411-
412-
return `scale(${scale}) translate3d(${translateX}px, ${translateY}px, 0)`
413-
}
414-
415428
#isSVGImage(image: HTMLImageElement) {
416429
return image.currentSrc.toLowerCase().endsWith('.svg')
417430
}

0 commit comments

Comments
 (0)