Skip to content

Instantly share code, notes, and snippets.

@axetroy
Created April 22, 2024 13:45
Show Gist options
  • Save axetroy/76f32808e6e696530eb9f8c195b00986 to your computer and use it in GitHub Desktop.
Save axetroy/76f32808e6e696530eb9f8c195b00986 to your computer and use it in GitHub Desktop.
Image viewer
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 };
<!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