You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在OpenSeadragon中实现多视图内容与滚动条的双向同步

OpenSeadragon垂直堆叠视图与自定义滚动条双向同步实现

问题背景

我已经通过viewerRef.current?.world.arrange({ layout: "vertical", rows: 1 })创建了垂直堆叠多图片的容器,并通过canvas-Scroll结合event.ctrlKey实现了不与视图事件冲突的滚动功能。现在需要实现视图滚动与自定义滚动条的双向同步。

解决方案

要实现双向同步,需要完成以下核心逻辑:

  • 初始化滚动条内容高度:图片加载完成后计算垂直堆叠的总内容高度,让滚动条拥有正确的可滚动范围
  • 视图滚动同步到滚动条:监听OpenSeadragon的pan事件,根据视图偏移比例更新滚动条位置
  • 滚动条滚动同步到视图:监听滚动条容器的scroll事件,根据滚动比例调整视图偏移
  • 缩放同步更新:视图缩放时内容高度会变化,需监听zoom事件重新计算高度并同步滚动条

修改后的完整代码

"use client";
import OpenSeadragon from "openseadragon";
import { useEffect, useRef } from "react";
import "@/styles/custom_scroll.css";

const containerWidth = 500;
const containerHeight = 600;

export default function Viewer() {
  const scrollContainerRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);
  const containerRef = useRef<HTMLDivElement | null>(null);
  const viewerRef = useRef<OpenSeadragon.Viewer | null>(null);
  // 防止同步时触发循环事件
  const isSyncingRef = useRef(false);

  const tileSource = [
    "https://openseadragon.github.io/example-images/pnp/ppmsca/05900/05954/05954.dzi",
    "https://openseadragon.github.io/example-images/pnp/ppmsca/05900/05954/05955.dzi",
    "https://openseadragon.github.io/example-images/pnp/ppmsca/05900/05954/05956.dzi",
    "https://openseadragon.github.io/example-images/pnp/ppmsca/05900/05954/05957.dzi",
    "https://openseadragon.github.io/example-images/pnp/ppmsca/05900/05954/05958.dzi",
    "https://openseadragon.github.io/example-images/pnp/ppmsca/05900/05954/05959.dzi",
    "https://openseadragon.github.io/example-images/pnp/ppmsca/05900/05954/05960.dzi",
    "https://openseadragon.github.io/example-images/pnp/ppmsca/05900/05954/05961.dzi",
    "https://openseadragon.github.io/example-images/pnp/ppmsca/05900/05954/05962.dzi",
    "https://openseadragon.github.io/example-images/pnp/ppmsca/05900/05954/05963.dzi",
  ];

  const handleCanvasScroll = (e: any) => {
    const event = e.originalEvent as WheelEvent;
    if (!viewerRef.current || !containerRef.current) return;
    const viewer = viewerRef.current;
    const panFactor = 5;
    const x = event.deltaX * panFactor;
    const y = event.deltaY * panFactor;
    if (event.ctrlKey) {
      return;
    } else {
      e.preventDefaultAction = true;
      const panPoint = new OpenSeadragon.Point(x, y);
      viewer?.viewport.panBy(panPoint);
      viewer?.viewport.applyConstraints();
    }
  };

  // 滚动条滚动同步到视图
  const handleScrollContainer = () => {
    if (!scrollContainerRef.current || !viewerRef.current || isSyncingRef.current) return;
    
    const container = scrollContainerRef.current;
    const viewer = viewerRef.current;
    const homeBounds = viewer.world.getHomeBounds();
    if (!homeBounds) return;

    // 计算滚动比例
    const scrollRatio = container.scrollTop / (container.scrollHeight - container.clientHeight);
    // 计算目标Y偏移
    const maxPanY = homeBounds.height - viewer.viewport.getBounds().height;
    const targetY = maxPanY * scrollRatio;

    isSyncingRef.current = true;
    viewer.viewport.panTo(new OpenSeadragon.Point(0, targetY), true);
    isSyncingRef.current = false;
  };

  // 视图滚动/缩放同步到滚动条
  const syncScrollbar = () => {
    if (!scrollContainerRef.current || !viewerRef.current || isSyncingRef.current) return;
    
    const container = scrollContainerRef.current;
    const viewer = viewerRef.current;
    const homeBounds = viewer.world.getHomeBounds();
    if (!homeBounds) return;

    // 更新内容高度(适配缩放)
    const contentPixelHeight = viewer.viewport.pixelFromPoint(
      new OpenSeadragon.Point(0, homeBounds.height)
    ).y;
    if (contentRef.current) {
      contentRef.current.style.height = `${contentPixelHeight}px`;
    }

    // 计算当前视图的Y偏移比例
    const viewportBounds = viewer.viewport.getBounds();
    const totalContentHeight = homeBounds.height;
    const visibleHeight = viewportBounds.height;
    const currentPanY = viewportBounds.y;
    const scrollRatio = currentPanY / (totalContentHeight - visibleHeight);

    isSyncingRef.current = true;
    container.scrollTop = scrollRatio * (container.scrollHeight - container.clientHeight);
    isSyncingRef.current = false;
  };

  useEffect(() => {
    if (!containerRef.current || viewerRef.current) return;
    viewerRef.current = OpenSeadragon({
      element: containerRef.current,
      prefixUrl: "//openseadragon.github.io/openseadragon/images/",
      tileSources: tileSource,
      crossOriginPolicy: "Anonymous",
      showNavigationControl: false,
    });

    viewerRef.current.addHandler("open", function () {
      viewerRef.current?.world.arrange({ layout: "vertical", rows: 1 });
      const tiledImage = viewerRef.current?.world.getItemAt(0);
      if (tiledImage) {
        const imageRect = tiledImage.getBounds();
        viewerRef.current?.viewport.fitBounds(imageRect, true);
      }
      // 初始化滚动条高度
      syncScrollbar();
    });
    viewerRef.current.addHandler("canvas-scroll", handleCanvasScroll);
    viewerRef.current.addHandler("pan", syncScrollbar);
    viewerRef.current.addHandler("zoom", syncScrollbar);
  }, []);

  return (
    <div
      className="relative mt-18 m-auto"
      style={{ maxWidth: containerWidth, maxHeight: containerHeight }}
    >
      <div
        ref={scrollContainerRef}
        className="scrollbar-container"
        onScroll={handleScrollContainer}
        style={{
          width: containerWidth,
          height: containerHeight,
          overflowY: "auto",
          overflowX: "hidden"
        }}
      >
        <div ref={contentRef} />
      </div>
      <div
        ref={containerRef}
        style={{
          position: "absolute",
          top: 0,
          left: 0,
          width: containerWidth - 15, // 给滚动条留出空间
          height: containerHeight,
          pointerEvents: "auto",
        }}
      ></div>
    </div>
  );
}

关键逻辑说明

  1. 防循环同步:使用isSyncingRef标记同步状态,避免视图和滚动条互相触发事件导致循环
  2. 内容高度计算:通过viewport.pixelFromPoint将OpenSeadragon世界坐标转换为容器像素坐标,确保滚动条范围准确
  3. 比例映射:将视图偏移比例和滚动条滚动比例一一对应,保证同步精度
  4. 缩放适配:监听zoom事件,每次缩放后重新计算内容高度并同步滚动条,避免缩放后滚动条范围失效

内容的提问来源于stack exchange,提问作者Katherin R

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 14:53:09