Image viewer

ImageViewer.js
复制

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}
   */
  _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 };

index.html
复制

>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Documenttitle>
    <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>

大牛们的评论:朕有话说

还没有人评论哦,赶紧抢沙发!