Created
April 22, 2024 13:45
-
-
Save axetroy/76f32808e6e696530eb9f8c195b00986 to your computer and use it in GitHub Desktop.
Image viewer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ImageViewer { | |
/** @type {string | undefined} */ | |
url = void 0; | |
/** @type {HTMLElement | undefined} */ | |
$app = void 0; | |
/** @type {HTMLElement | undefined} */ | |
$img = void 0; | |
/** @type {number} */ | |
scaleRate = 1; | |
#width = 0; | |
#height = 0; | |
constructor(url, options = {}) { | |
this.url = url; | |
this.options = { MIN_SCALE: 0.1, MAX_SCALE: 10, ...options }; | |
this.disposables = []; | |
function onResize() { | |
// const img = this.$app.querySelector("img"); | |
// if (img) { | |
// this._calcImageCenterPosition(img); | |
// } | |
} | |
window.addEventListener("resize", onResize); | |
this.disposables.push({ | |
dispose() { | |
window.removeEventListener("resize", onResize); | |
}, | |
}); | |
} | |
/** | |
* 计算图片在居中的位置 | |
*/ | |
_calcImageCenterPosition() { | |
const { $app, $img } = this; | |
// 计算宽高比 | |
const aspectRatio = $img.width / $img.height; | |
// 自适应容器 | |
if ($img.width > $app.offsetWidth || $img.height > $app.offsetHeight) { | |
if (aspectRatio > 1) { | |
$img.style.width = $app.offsetWidth + "px"; | |
$img.style.height = $app.offsetWidth / aspectRatio + "px"; | |
} else { | |
$img.style.height = $app.offsetHeight + "px"; | |
$img.style.width = $app.offsetHeight * aspectRatio + "px"; | |
} | |
} else { | |
$img.style.width = $img.width + "px"; | |
$img.style.height = $img.height + "px"; | |
} | |
$img.style.left = $app.offsetWidth / 2 - $img.width / 2 + "px"; | |
$img.style.top = $app.offsetHeight / 2 - $img.height / 2 + "px"; | |
$img.classList.remove("loading"); | |
} | |
/** | |
* 加载图片 | |
* @param {string} url | |
* @returns {Promise<HTMLImageElement>} | |
*/ | |
_loadImage(url) { | |
const $img = document.createElement("img"); | |
$img.draggable = false; | |
$img.classList.add("loading"); | |
this.$img = $img; | |
return new Promise((resolve, reject) => { | |
$img.onload = () => { | |
this._calcImageCenterPosition(); | |
$img.classList.remove("loading"); | |
this.#width = $img.width; | |
this.#height = $img.height; | |
resolve($img); | |
}; | |
$img.onerror = reject; | |
$img.src = url; | |
this.$app.appendChild($img); | |
}); | |
} | |
/** | |
* 绑定移动事件 | |
*/ | |
_bindingMove() { | |
const { $app, $img } = this; | |
let prevX, prevY; | |
const originImageRect = $img.getBoundingClientRect(); | |
/** | |
* 鼠标移动事件 | |
* @param {MouseEvent} e | |
*/ | |
function onMouseMove(e) { | |
const deltaX = e.clientX - prevX; | |
const deltaY = e.clientY - prevY; | |
const appRect = $app.getBoundingClientRect(); | |
const rect = $img.getBoundingClientRect(); | |
const boundingX = rect.left - appRect.left; // 图片到它容器的 X 轴 距离 | |
const boundingY = rect.top - appRect.top; // 图片到它容器的 Y 轴 距离 | |
const newLeft = boundingX + deltaX; | |
const newTop = boundingY + deltaY; | |
$img.style.left = newLeft + "px"; | |
$img.style.top = newTop + "px"; | |
prevX = e.clientX; | |
prevY = e.clientY; | |
} | |
$app.addEventListener("mousedown", (e) => { | |
prevX = e.clientX; | |
prevY = e.clientY; | |
$app.style.cursor = "grabbing"; | |
$app.addEventListener("mousemove", onMouseMove); | |
}); | |
document.addEventListener("mouseup", () => { | |
$app.removeEventListener("mousemove", onMouseMove); | |
$app.style.cursor = "initial"; | |
}); | |
} | |
/** | |
* 获取图片中心位置 | |
* @param {HTMLImageElement} $img | |
* @returns {{x: number, y: number}} | |
*/ | |
_getImageCenterPosition() { | |
const { $app, $img } = this; | |
const appRect = $app.getBoundingClientRect(); | |
const imageRect = $img.getBoundingClientRect(); | |
return { | |
x: appRect.left + imageRect.left + imageRect.width / 2, | |
y: appRect.top + imageRect.top + imageRect.height / 2, | |
}; | |
} | |
/** | |
* 缩放图片 | |
* @param {{x: number, y: number, deltaY: number}} param0 缩放中心坐标,既该点位置不变 | |
*/ | |
_scaleImage({ x, y, deltaY }) { | |
const { $app, $img, scaleRate: scale } = this; | |
const { MIN_SCALE, MAX_SCALE } = this.options; | |
const appRect = $app.getBoundingClientRect(); | |
const mouseX = x - appRect.left; | |
const mouseY = y - appRect.top; | |
const delta = deltaY; // 滚动方向决定缩放增量 | |
const scaleFactor = scale + delta; | |
// 限制缩放比例在一定范围内 | |
if ( | |
(scale === MIN_SCALE && delta < 0) || | |
(scale === MAX_SCALE && delta > 0) | |
) { | |
return; | |
} | |
this.scaleRate = Number( | |
Math.max(MIN_SCALE, Math.min(scaleFactor, MAX_SCALE)).toFixed(2) | |
); | |
const imageRect = $img.getBoundingClientRect(); | |
const offsetX = mouseX - parseFloat(imageRect.left - appRect.left); // 计算鼠标相对于图片左上角的偏移量 | |
const offsetY = mouseY - parseFloat(imageRect.top - appRect.top); | |
// 根据缩放后的尺寸和鼠标指针在图片上的位置,重新计算 left 和 top 值 | |
const newLeft = mouseX - offsetX * (1 + delta); | |
const newTop = mouseY - offsetY * (1 + delta); | |
// 重新计算图片的宽高 | |
const newWidth = (1 + delta) * $img.width; | |
const newHeight = newWidth / (this.#width / this.#height); | |
console.log(newWidth / newHeight, this.#width / this.#height); | |
$img.style.width = newWidth + "px"; | |
$img.style.height = newHeight + "px"; | |
$img.style.left = newLeft + "px"; | |
$img.style.top = newTop + "px"; | |
} | |
/** | |
* 缩放图片 | |
* @param {number} rate 缩放比例, 1 表示原始尺寸, 0.1 表示缩小到 10%, 2 表示放大到 200% | |
*/ | |
scaleTo(rate) { | |
const { scaleRate, $app } = this; | |
const appRect = $app.getBoundingClientRect(); | |
// 缩放中心点 | |
const center = { | |
x: appRect.left / 2 + appRect.width / 2, | |
y: appRect.top / 2 + appRect.height / 2, | |
}; | |
this._scaleImage({ ...center, deltaY: rate - scaleRate }); | |
} | |
/** | |
* 绑定缩放事件 | |
* @param {HTMLImageElement} $img | |
*/ | |
_bindingScale() { | |
const $app = this.$app; | |
const scale = this.scaleRate; | |
const { MIN_SCALE, MAX_SCALE } = this.options; | |
// 滚轮缩放 | |
$app.addEventListener("wheel", (e) => { | |
e.preventDefault(); // 阻止默认的滚动行为 | |
this._scaleImage({ | |
x: e.clientX, | |
y: e.clientY, | |
deltaY: e.deltaY / 500, | |
}); | |
}); | |
} | |
/** | |
* 挂载 Viewer | |
* @param {HTMLElement} root | |
*/ | |
async mount(root) { | |
this.root = root; | |
const $app = document.createElement("div"); | |
this.$app = $app; | |
$app.style.position = "relative"; | |
$app.style.width = "100%"; | |
$app.style.height = "100%"; | |
root.appendChild($app); | |
const $img = await this._loadImage(this.url); | |
this._bindingMove($img); | |
this._bindingScale($img); | |
} | |
/** | |
* 销毁 Viewer | |
*/ | |
dispose() { | |
this.$app?.remove?.(); | |
for (const disposable of this.disposables) { | |
disposable.dispose(); | |
} | |
} | |
} | |
export { ImageViewer }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Document</title> | |
<style> | |
* { | |
padding: 0; | |
margin: 0; | |
} | |
#root { | |
width: 100vw; | |
height: 100vh; | |
background-color: #e2e2e2; | |
padding: 100px; | |
box-sizing: border-box; | |
} | |
img { | |
position: absolute; | |
user-select: none; | |
} | |
img.loading { | |
max-width: 100%; | |
max-height: 100%; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
} | |
</style> | |
</head> | |
<body> | |
<div id="root"> | |
<div> | |
<button id="zoomIn" onclick="zoomIn(1.2)">缩放+</button> | |
</div> | |
</div> | |
<script type="module" src="./ImageViewer.js"></script> | |
<script type="module"> | |
import { ImageViewer } from "./ImageViewer.js"; | |
const container = document.getElementById("root"); | |
const viewer = new ImageViewer( | |
"https://fengyuanchen.github.io/viewerjs/images/tibet-1.jpg" | |
); | |
viewer.mount(container); | |
function zoomIn() { | |
viewer.scaleTo(1.2); | |
console.log(viewer); | |
} | |
window.zoomIn = zoomIn; | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment