Skip to content

Commit

Permalink
fix: rewrites the zooming logic to avoid blurry images on some device…
Browse files Browse the repository at this point in the history
…s/browsers (especially visible on images with text)
  • Loading branch information
HiDeoo authored Nov 6, 2024
1 parent bb2fbcf commit 72a5af2
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v4

- name: Install Node.js
uses: actions/setup-node@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
fetch-depth: 0

- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v4

- name: Install Node.js
uses: actions/setup-node@v4
Expand Down
Binary file added docs/src/assets/tests/text.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions docs/src/content/docs/tests/text.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Image with text
pagefind: false
---

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>

![Screenshot of GitHub on a desktop computer](../../../assets/tests/text.jpg)
81 changes: 47 additions & 34 deletions packages/starlight-image-zoom/components/ImageZoom.astro
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ import config from 'virtual:starlight-image-zoom-config'
width: 44px;
}

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

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

// https://caniuse.com/requestidlecallback
const onIdle = window.requestIdleCallback ?? ((cb) => setTimeout(cb, 1))

/**
* Based on:
* - https://github.com/francoischalifour/medium-zoom
Expand All @@ -183,16 +186,16 @@ import config from 'virtual:starlight-image-zoom-config'
image: 'starlight-image-zoom-image',
opened: 'starlight-image-zoom-opened',
source: 'starlight-image-zoom-source',
transition: 'starlight-image-zoom-transition',
}
#dataZoomTransformKey = 'zoomTransform'

static #initialized = false

constructor() {
super()

const initialize = () => {
// https://caniuse.com/requestidlecallback
const onIdle = window.requestIdleCallback ?? ((cb) => setTimeout(cb, 1))

onIdle(() => {
const zoomables = [...document.querySelectorAll(STARLIGHT_IMAGE_ZOOM_ZOOMABLE_TAG)]
if (zoomables.length === 0) return
Expand Down Expand Up @@ -281,8 +284,8 @@ import config from 'virtual:starlight-image-zoom-config'
.querySelector('header')
?.style.setProperty('padding-inline-end', `calc(var(--sl-nav-pad-x) + ${window.innerWidth - clientWidth}px)`)

// Clone the image to zoom at the same position of the original image.
const zoomedImage = this.#cloneImageAtPosition(image)
// Clone the image and apply a zoom effect to it.
const zoomedImage = this.#cloneAndZoomImage(image)

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

// Apply a zoom effect to the zoomed image.
zoomedImage.style.transform = this.#getZoomEffectTransform(image, figure)
document.body.classList.add(this.#classList.opened)
// Apply a zoom effect to zoom the image in.
onIdle(() => {
zoomedImage.style.transform = ''
document.body.classList.add(this.#classList.opened)
})

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

// Remove the zoom effect from the zoomed image.
zoomedImage.style.transform = ''
zoomedImage.style.transform = zoomedImage.dataset[this.#dataZoomTransformKey] ?? ''
document.body.classList.add(this.#classList.transition)
document.body.classList.remove(this.#classList.opened)

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

// Show the source image.
document.body.classList.remove(this.#classList.transition)
image.classList.remove(this.#classList.source)

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

#cloneImageAtPosition(image: HTMLImageElement) {
const { height, left, top, width } = image.getBoundingClientRect()
#cloneAndZoomImage(image: HTMLImageElement) {
const imageRect = image.getBoundingClientRect()

// Zoom SVGs at the figure's size, not the image's size.
const isSVG = this.#isSVGImage(image)
const naturalWidth = isSVG ? window.innerWidth : image.naturalWidth
const naturalHeight = isSVG ? window.innerHeight : image.naturalHeight

const maxWidth = Math.min(window.innerWidth, naturalWidth)
const maxHeight = Math.min(window.innerHeight, naturalHeight)
const scale = Math.min(maxWidth / naturalWidth, maxHeight / naturalHeight)

const width = (isSVG ? window.innerWidth : image.naturalWidth) * scale
const height = (isSVG ? window.innerHeight : image.naturalHeight) * scale
const top = (window.innerHeight - height) / 2
const left = (window.innerWidth - width) / 2

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

// Finds out the scale transformations so that the zoomed image fits within the image rect.
const scaleX = imageRect.width / width
const scaleY = imageRect.height / height

// Calculate the translation to align the zoomed image within the image rect.
const translateX = (-left + (imageRect.width - width) / 2 + imageRect.left) / scaleX
const translateY = (-top + (imageRect.height - height) / 2 + imageRect.top) / scaleY

// Apply the scale and translation to the zoomed image.
clone.style.transform = `scale(${scaleX}, ${scaleY}) translate3d(${translateX}px, ${translateY}px, 0)`

// Save the transform to a data attribute to be able to animate it back to the original position.
clone.dataset[this.#dataZoomTransformKey] = clone.style.transform

// If the image is inside a `<picture>` element, we need to update the `src` attribute and use the correct
// source.
if (image.parentElement?.tagName === 'PICTURE' && image.currentSrc) {
Expand All @@ -390,28 +425,6 @@ import config from 'virtual:starlight-image-zoom-config'
return clone
}

#getZoomEffectTransform(image: HTMLImageElement, figure: HTMLElement) {
const isSVG = this.#isSVGImage(image)

const imageRect = image.getBoundingClientRect()
const figureRect = figure.getBoundingClientRect()

// Zoom SVGs at the figure's size, not the image's size.
const zoomedHeight = isSVG ? figureRect.height : image.naturalHeight
const zoomedWidth = isSVG ? figureRect.width : image.naturalWidth

// Finds out the scale so that the zoomed image fits within the figure element.
const scaleX = Math.min(Math.max(imageRect.width, zoomedWidth), figureRect.width) / imageRect.width
const scaleY = Math.min(Math.max(imageRect.height, zoomedHeight), figureRect.height) / imageRect.height
const scale = Math.min(scaleX, scaleY)

// Calculate the translation to center the zoomed image within the figure element.
const translateX = (-imageRect.left + (figureRect.width - imageRect.width) / 2 + figureRect.left) / scale
const translateY = (-imageRect.top + (figureRect.height - imageRect.height) / 2 + figureRect.top) / scale

return `scale(${scale}) translate3d(${translateX}px, ${translateY}px, 0)`
}

#isSVGImage(image: HTMLImageElement) {
return image.currentSrc.toLowerCase().endsWith('.svg')
}
Expand Down

0 comments on commit 72a5af2

Please sign in to comment.