如何在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> ); }
关键逻辑说明
- 防循环同步:使用
isSyncingRef标记同步状态,避免视图和滚动条互相触发事件导致循环 - 内容高度计算:通过
viewport.pixelFromPoint将OpenSeadragon世界坐标转换为容器像素坐标,确保滚动条范围准确 - 比例映射:将视图偏移比例和滚动条滚动比例一一对应,保证同步精度
- 缩放适配:监听
zoom事件,每次缩放后重新计算内容高度并同步滚动条,避免缩放后滚动条范围失效
内容的提问来源于stack exchange,提问作者Katherin R
相关产品推荐
相关产品推荐

