One idea I had when I first got into the front-end business was to write a really cool gallery of preview images. I still remember using Meitu at that time, the brisk speed and interaction was so fascinating that I wanted to write one myself.

This component was released a few years ago in an incomplete version, and has been maintained intermittently since, always feeling a little off. This year’s Spring Festival did not rest, all built on the above development, now it is finally perfect! First look at the results:

Preview: react-photo-view.vercel.app/

Perfect thumbnail gradient:

Zoom in at specified location:

Slow rolling:

What is thereact-photo-view

React-photo-view offers an unrivalled interactive preview experience: from the moment the image is opened, every frame of animation, detail, and interaction is carefully designed and tweaked to match the effects of a native image preview.

pnpm i react-photo-view
Copy the code

The issue:

import { PhotoProvider, PhotoView } from 'react-photo-view';
import 'react-photo-view/dist/react-photo-view.css';

export default function MyComponent() {
  return (
    <PhotoProvider>
      <PhotoView src="/1.jpg">
        <img src="/1-thumbnail.jpg" alt="" />
      </PhotoView>
    </PhotoProvider>
  );
}
Copy the code

Why develop it separately?

Of course, the obsession with implementing it was part of the equation, but the root cause was that there was no easy way to preview images in the React ecosystem. At that time, I was a copyist. I searched the React zoom-based preview component library on the Internet. The result surprised me a little. What makes it even more suffocating is that most of the component libraries are repackaged based on PhotoSwipe, an open source library. In addition, a library of preview components that can be used in actual production… There doesn’t seem to be one (or maybe I can’t find one), and this is true not only for the React library, but also for other frameworks like Vue and even native libraries.

PhotoSwipe wasn’t out of place, but the native DOM format didn’t work in React and was a bit too bulky for gzip’s 12KB size, so the idea came up.

How good is it?

It has perfect details and features:

  • Support touch gestures, drag/pan/physical effect sliding, double finger specified position zoom in/out
  • Full animation connection, open/close/bounce/touch, natural interaction
  • The image ADAPTS to a suitable initial rendering size and ADAPTS according to adjustments
  • Customizations such as<video />Or anyHTMLPreview of elements
  • Keyboard navigation, perfect for desktop
  • Support custom node expansion, easy to achieve full-screen preview, rotation control, picture introduction and more
  • Based on thetypescript.7KB GzippedTo support server-side rendering
  • Easy to useAPIZero cost to get started

Also exported to support ES2017 above JS, can do 6KB Gzipped. It’s not easy to add a lot of experience detail to that size, but more functionality can be achieved with very easy custom rendering, which fits perfectly with the React concept and eliminates the need to build in unnecessary features.

Popular library comparison

The following table shows the functionality required for most scenes, showing the contrast between React-Photo-view, PhotoSwipe and RC-image (ant-Design) :

react-photo-view PhotoSwipe rc-image
MINIFIED 19KB 47KB 40KB
MINIFIED + GZIPPED 7.3 KB 12KB 14KB
Based on the preview support support support
Switch to preview support support Does not support
The mobile terminal support support Does not support
Perfect gradient for thumbnails support support Does not support
Thumbnail crop animation support Supported (manually specified) Does not support
Adaptive image size support Not supported (Manually specified) support
fallback support Does not support support
Mouse wheel zoom support Does not support (Lack of location)
Physical rolling of spring support support Does not support
Animation parameter adjustment support support Does not support
Easy to use API support Does not support support
TypeScript support Does not support support
Keyboard navigation support support support
Custom elements support XSS risk exists Does not support
The controlled components support support support
Loop preview support support Does not support
Image rotation support Does not support support
Customize toolbars support support Does not support
Native full screen open Custom extension support Does not support

Friendly documentation

What is more important than the document, for this purpose, I have prepared a very beautiful document (currently only in Chinese, I will have time to translate it later).

react-photo-view.vercel.app/

Implementation process

Pictures scroll with your finger

Record the current trigger position state when onTouchStart, let it follow the finger when onTouchMove, onTouchEnd unfollow can be easily achieved.

Edge position feedback makes image switching a matter of painstaking detail: moving after onTouchStart and immediately following the image with your finger can lead to a lot of misoperations, such as swiping up and down in order to switch images. A 20px movement buffer is needed to predict finger movement.

Specifies the image location to enlarge

Transform: Scale (value) can be used to scale the image, but the center of the image is enlarged, the result of scaling may not be desired. The original idea was to use transform-Origin, but the idea was nice, although the first time it was possible to zoom in at the specified location. If you zoom in on something other than the original position, you get jumpy, which is obviously not going to work.

Later, I could not sleep, and found inspiration in my sleep: for easy calculation and understanding, we set the center point of the picture as 0, and change the position of the center of the picture by zooming in and out of any specified position. For example, the width of the image is 200, the center position is 100, and the image is doubled based on the leftmost position. Now the image width is 400, so the center position should be 200. So the summary formula is as follows:

const centerClientX = innerWidth / 2;
// Coordinate offset conversion
const lastPositionX = centerClientX + lastX;
// Zoom offset
const offsetScale = nextScale / scale;
// Final offset position
const originX = clientX - (clientX - lastPositionX) * offsetScale - centerClientX;
Copy the code

This mode calculation can handle a variety of position responses, such as two-finger zoom, two-finger scroll + zoom, edge calculation, and so on.

The distance between the fingers

Here is the Pythagorean theorem needed in junior high:

Math.sqrt((nextClientX - clientX) ** 2 + (nextClientY - clientY) ** 2);
Copy the code

Simulated rolling operation

The previous version used Transition, which calculated the initial speed by sliding the time difference between the start and end of the finger, and estimated the distance simulated by Transition to make the eye look like it was rolling 😂. But the experience is still far worse. Then, the rolling effect is simulated by combining high school physics formula:

Accelerated motion:

Air resistance:

CρS is a constant, so let’s make it a constant. As for how to get this amount… So 😂 is only proportional to v squared.

And because of the opposite direction of motion, take the direction of v math.sign (-v)

function scrollMove(
  initialSpeed: number,
  callback: (spatial: number) = >boolean.) {
  / / acceleration
  const acceleration = -0.002;
  / / resistance
  const resistance = 0.0002;

  let v = initialSpeed;
  let s = 0;
  let lastTime: number | undefined = undefined;
  let frameId = 0;

  const calcMove = (now: number) = > {
    if(! lastTime) { lastTime = now; }const dt = now - lastTime;
    const direction = Math.sign(initialSpeed);
    const a = direction * acceleration;
    const f = Math.sign(-v) * v ** 2 * resistance;
    const ds = v * dt + ((a + f) * dt ** 2) / 2;
    v = v + (a + f) * dt;

    s = s + ds;
    // move to s
    lastTime = now;

    if (direction * v <= 0) {
      cancelAnimationFrame(frameId);
      return;
    }

    if (callback(s)) {
      frameId = requestAnimationFrame(calcMove);
      return;
    }
    cancelAnimationFrame(frameId);
  };
  frameId = requestAnimationFrame(calcMove);
}
Copy the code

Cut the thumbnail

PhotoSwipe supports thumbnail clipping, but requires manually specifying the image width and height and data-cropped, which can be quite cumbersome. React-photo-view gets the current crop parameters by reading the thumbnail getComputedStyle(Element).objectFit. Realize automatic cutting effect.

Compatibility processing

Because each image is a composite layer, this can consume quite a bit of memory. IOS has a fairly large memory limit, and if you use scale all the way up, it will look very blurry on Safari. Now by changing the width and height of the image to the specified value each time after the motion is complete, and then resetting the scale to 1, this method should achieve the desired effect.

other

PhotoSwipe, a Ukrainian who lives in Kiev, fled the city and is now safe in western Ukraine with his family, hoping to get back on his feet once the war is over.

conclusion

I spent a lot of energy on the details of react-photo-view. If you like it, please click a Star to support me. Thank you!

  • Making: github.com/MinJieLiu/r…
  • Gitee: gitee.com/MinJieLiu/r…

Articles exported this year

  • Vite is the best practice of reductive routing, a cool way to implement it
  • Vite plug-in development practices: resource handling of microfront-end
  • Vite micro front-end practice, to achieve a componentized scheme
  • JS immutable data trams, immer is not the ultimate way out, high-performance scenarios need to be implemented by themselves