react-zoom-pan-pinch icon indicating copy to clipboard operation
react-zoom-pan-pinch copied to clipboard

Zoom on center using setTransform

Open ignlopezsanchez opened this issue 3 years ago • 8 comments

Hi! I know this is not an issue but a discussion, but there is not a discussion tab in this repository. In the feature I want to implement, I need to have a controlled zoom (10%, 20%... 100%, 110%... 400%) so I am not using zoomIn and zoomOut handlers but setTransform. I am not able to implement the algorithm to calculate x and y position. Any of you have been able to do it? This is a codesandbox I forked and change in order to do it. I would expect to zoom in the center of the transform component, and not always in the same x,y position.

https://codesandbox.io/s/zoom-pan-pinch-zoom-center-b4dfn?file=/src/App.tsx

I have tried to replicate some logic I found in the zoomIn, zoomOut original code but I didn' achieve it.

export function handleZoomToViewCenter(
  contextInstance: ReactZoomPanPinchContext,
  delta: number,
  step: number,
  animationTime: number,
  animationType: keyof typeof animations,
): void {
  const { wrapperComponent } = contextInstance;
  const { scale, positionX, positionY } = contextInstance.transformState;

  if (!wrapperComponent) return console.error("No WrapperComponent found");

  const wrapperWidth = wrapperComponent.offsetWidth;
  const wrapperHeight = wrapperComponent.offsetHeight;
  const mouseX = (wrapperWidth / 2 - positionX) / scale;
  const mouseY = (wrapperHeight / 2 - positionY) / scale;

  const newScale = handleCalculateButtonZoom(contextInstance, delta, step);

  const targetState = handleZoomToPoint(
    contextInstance,
    newScale,
    mouseX,
    mouseY,
  );

  if (!targetState) {
    return console.error(
      "Error during zoom event. New transformation state was not calculated.",
    );
  }

  animate(contextInstance, targetState, animationTime, animationType);
}

Thanks!

ignlopezsanchez avatar Feb 09 '22 15:02 ignlopezsanchez

@ignlopezsanchez Im running into the same issue. Did you manage to find a way to zoom to the center of the part of the image that is in viewport?

sanderkooger avatar Sep 10 '22 17:09 sanderkooger

@ignlopezsanchez I'm running into a similar issue where I want to zoom in to the mouse position...

ecemac avatar Mar 14 '23 16:03 ecemac

@sanderkooger @ecemac hi! I stopped trying as it was not urgent for my project. But definitely this enhancement would add a lot of value to the library.

ignlopezsanchez avatar Mar 14 '23 16:03 ignlopezsanchez

@sanderkooger @ignlopezsanchez I solved my problem, maybe it can also help you: react-zoom-pan-pinch zoom in with single click

ecemac avatar Mar 23 '23 10:03 ecemac

@ecemac and @ignlopezsanchez

This is our current implementation. it responds to pinch and it has a slider that works too to show and set zoomlevel.

const Transition = React.forwardRef(
  (
    props: TransitionProps & {
      children: React.ReactElement;
    },
    ref: React.Ref<unknown>
  ) => {
    return <Slide direction="up" ref={ref} {...props} />;
  }
);

export default function ImageZoom(props: ImageZoomProps): JSX.Element {
  const { open, setOpen, activeImage, loading } = props;

  const styles = Styles();
  const isPortrait = activeImage?.height > activeImage?.width;

  const imgStyles: React.CSSProperties = {
    height: isPortrait ? '100vh' : 'auto',
    width: isPortrait ? 'auto' : '100vw'
  };
  const zoomAnimationTime = 150;
  const minScale = 1;
  const maxScale = 8;
  const [zoomInstance, setZoomInstance] = useState<ReactZoomPanPinchRef>(null);
  const [currentZoomScale, setCurrentZoomScale] = useState<number>(1);

  const handleZoomSlider2 = (event: Event, newValue: number) => {
    // Check below link for Centerd zoom with slider
    // https://github.com/prc5/react-zoom-pan-pinch/issues/137

    const currentZoom = zoomInstance.state.scale;

    const factor = Math.log(newValue / currentZoom);

    if (newValue > currentZoom) {
      // Can not have animation time it breaks teh responsiveness
      //  console.log('I need to zoom in');
      zoomInstance.zoomIn(factor, 0);
    } else {
      // console.log('zoom Out');
      zoomInstance.zoomOut(-factor, 0);
    }
  };

  const handleOnClickZoomIn = () => {
    zoomInstance.zoomIn(0.25, zoomAnimationTime);
  };
  const handleOnClickZoomOut = () => {
    zoomInstance.zoomOut(0.25, zoomAnimationTime);
  };

  useEffect(() => {
    setCurrentZoomScale(zoomInstance?.state?.scale);
  }, [zoomInstance?.state?.scale]);

  if (loading) {
    return null;
  }

  return (
    <Dialog
      sx={styles.dialog}
      data-testid="ImageZoomDialog"
      open={open}
      fullScreen
      TransitionComponent={Transition}
    >
      {/* <Typography sx={styles.testText}>
        currentZoomScale: {currentZoomScale}
        <br />
        <br />
      </Typography> */}

      <IconButton
        data-testid="TitleBarCloseButton"
        sx={styles.closeButton}
        disableRipple
        edge="start"
        color="inherit"
        aria-label="Close"
        onClick={() => setOpen(false)}
      >
        <CloseTwoTone />
      </IconButton>

      <Box sx={styles.mainDiv} display="flex" justifyContent="center" alignItems="center">
        <TransformWrapper
          initialScale={currentZoomScale}
          centerZoomedOut
          centerOnInit
          limitToBounds
          minScale={minScale}
          maxScale={maxScale}
          ref={(ref) => setZoomInstance(ref)}
          onZoom={(zoom) => setCurrentZoomScale(zoom.state.scale)}
          onZoomStop={(zoom) => setCurrentZoomScale(zoom.state.scale)}
          onInit={(zoomzoom) => setCurrentZoomScale(zoomzoom.state.scale)}
          // zoomAnimation={{ disabled: true }}
        >
          <TransformComponent>
            <Image
              alt="Zoom Image"
              priority
              placeholder="blur"
              style={imgStyles}
              src={activeImage?.imgUrl}
              blurDataURL={activeImage?.blurUrl}
              quality={100}
              height={activeImage?.height}
              width={activeImage?.width}
            />
          </TransformComponent>
        </TransformWrapper>
      </Box>
      <Grid sx={styles.sliderContainer} container justifyContent="center" justifyItems="center">
        <Grid item xs={12} md={4}>
          <Stack
            paddingX={3}
            paddingBottom={3}
            spacing={2}
            direction="row"
            sx={{ mb: 1 }}
            alignItems="center"
          >
            <IconButton onClick={() => handleOnClickZoomOut()}>
              <ZoomOutTwoTone />
            </IconButton>

            {/* <Box sx={{ width: '100%' }} /> */}

            <Slider
              sx={{}}
              step={0.01}
              aria-label="Zoom Slider"
              value={currentZoomScale || 1}
              max={maxScale}
              defaultValue={minScale}
              min={minScale}
              onChange={handleZoomSlider2}
              // color="secondary"
            />
            <IconButton onClick={() => handleOnClickZoomIn()}>
              <ZoomInTwoTone />
            </IconButton>
          </Stack>
        </Grid>
      </Grid>
    </Dialog>
  );
}

I don't have the time to explain everything, but you can see the zoom handlers and math in there. You can also see in what props we pass the information.

Hope it helps `

sanderkooger avatar Mar 23 '23 15:03 sanderkooger

Note: The scale calculation was changed at #367.

reminjp avatar Jun 29 '23 06:06 reminjp

I'm thinking of two ways to enhance this package:

  • If newPositionX/Y passed to setTransform is NaN, zoom to the center instead of the top left. (BREAKING CHANGE)
  • Add a new function named zoomToViewCenter(scale, animationTime, animationType) for example.

Do you have any other ideas?

reminjp avatar Jun 30 '23 01:06 reminjp

Code for zoom In:

newScale = scale + 0.1;
var element = document.getElementsByClassName('react-transform-component');
var style = window.getComputedStyle(element[0]);
var matrix = new WebKitCSSMatrix(style.transform);    

var ratio = (newScale - scale) / scale + 1;
var x = (matrix.m41 - window.innerWidth / 2 * (1 - scale / newScale)) * ratio;
var y = (matrix.m42 - window.innerHeight / 2 * (1 - scale / newScale)) * ratio

setTransform(x, y, newScale, 0);

Code for zoom out:

newScale = scale - 0.1;
var element = document.getElementsByClassName('react-transform-component');
var style = window.getComputedStyle(element[0]);
var matrix = new WebKitCSSMatrix(style.transform);    

var ratio = (newScale - scale) / scale + 1;
var x = (matrix.m41 - window.innerWidth / 2 * (1 - scale / newScale)) * ratio;
var y = (matrix.m42 - window.innerHeight / 2 * (1 - scale / newScale)) * ratio

setTransform(x, y, newScale, 0);

The matrix.m41 and matrix.m42 is used to retrieve the x and y value for the translation inside the element. Consider you zoom in from 100% to 110%, the width of an element will be incrased from 1000px to 1100px, the ratio of percentage change can be found from (newScale - scale) / scale + 1.

The formula can be break into two parts

var x = matrix.m41 * ratio + window.innerWidth / 2 * ratio * (1 - scale / newScale)  

If you take the top left point as origin and zoom in an element, the new x value will be

matrix.m41 * ratio

When you zoom into the picture, edges of the picture will be hidden, it comes with the second part of formula

window.innerWidth / 2 * ratio * (1 - scale / newScale)  

Consider both the screen and element is 1000px, when you zoom in from 100% to 110%, the screen can only show part of the element that is (scale / newScale) * 1000px = 909px. Loss of 91px will be hidden on x-axis that can be calcuated from (1 - scale / newScale) * 1000px

Using the same idea, when you zoom on the center, part of the x-axis and y-axis will be hidden in the process. The new x and y value will be moving right and down a little bit by adding the second part of forumla.

superwch1 avatar Apr 29 '24 15:04 superwch1