Commit 1469c576 authored by vertighel's avatar vertighel
Browse files

fix: panner viewport rectangle accounts for cssScale and letterbox



The previous formula divided panX/panY directly by zoom, ignoring that
pan values are CSS pixels while the canvas buffer is 800 px wide rendered
at width:100%.  This caused the yellow viewport rectangle to drift from
the actual visible region as soon as the canvas CSS width differed from
800 px (i.e. always).

Correct derivation:
  bxMin = -panX / (zoom * cssScale)   (leftmost visible buffer pixel)
  panoX = (bxMin - lb.dx) * (cv.width / lb.dw)

where cssScale = canvasMain.clientWidth / canvasMain.width and lb is the
letterbox rect.  The same applies to the vertical axis.

Co-Authored-By: default avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent 5ef67faa
Loading
Loading
Loading
Loading
+32 −12
Original line number Diff line number Diff line
@@ -714,21 +714,41 @@ export class FitsViewer {
        ctx.clearRect(0, 0, cv.width, cv.height);
        if (this._panImage) ctx.drawImage(this._panImage, 0, 0, cv.width, cv.height);

        // Scale from image pixels to panoramic canvas pixels.
        const scaleX = cv.width  / (st.width  || 1);
        const scaleY = cv.height / (st.height || 1);
        const lb = st.letterbox;
        if (!lb.dw || !lb.dh) return;

        // Viewport dimensions in image pixels (inverse of zoom).
        const vpW = cv.width  / (st.zoom * scaleX);
        const vpH = cv.height / (st.zoom * scaleY);

        // Top-left corner of the viewport in image pixels.
        const ox = -st.panX / st.zoom;
        const oy = -st.panY / st.zoom;
        // panX/panY are CSS pixels.  cssScale converts between canvas buffer pixels
        // and canvas CSS pixels (canvas is width:100%, buffer is 800 px).
        const cvMain   = this._el.canvasMain;
        const cssScale = cvMain && cvMain.clientWidth > 0
                       ? cvMain.clientWidth / cvMain.width
                       : 1;

        // Leftmost/topmost buffer pixel currently visible in the wrapper.
        // CSS transform is translate(panX,panY) scale(zoom) with origin 0,0:
        //   wrapper_css = panX + bufPx * cssScale * zoom
        //   bufPx = -panX / (cssScale * zoom)  at wrapper_css = 0
        const bxMin = -st.panX / (st.zoom * cssScale);
        const byMin = -st.panY / (st.zoom * cssScale);

        // Number of buffer pixels visible horizontally and vertically.
        const visW = cvMain.width  / st.zoom;
        const visH = cvMain.height / st.zoom;

        // The letterbox maps the image into buffer region [lb.dx, lb.dx+lb.dw].
        // The pano canvas maps the full image onto [0, cv.width].
        // Conversion: one buffer pixel = (cv.width / lb.dw) pano pixels.
        const bufToPanoX = cv.width  / lb.dw;
        const bufToPanoY = cv.height / lb.dh;

        const panoX = (bxMin - lb.dx) * bufToPanoX;
        const panoY = (byMin - lb.dy) * bufToPanoY;
        const panoW = visW * bufToPanoX;
        const panoH = visH * bufToPanoY;

        ctx.strokeStyle = '#ffff00';
        ctx.lineWidth   = 1.5;
        ctx.strokeRect(ox * scaleX, oy * scaleY, vpW * scaleX, vpH * scaleY);
        ctx.strokeRect(panoX, panoY, panoW, panoH);
    }