[[[https://codepen.io/inlet/pen/2b7da2053276c634928a7eca450648c8]]]
body {
  height: 100vh;
  margin: 0;
  display: grid;
  place-items: center;
}

#root {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
}

canvas {
  background: #efefef;
  box-shadow: 3px 3px 0 #ccc;
}

.buttons-group > button {
  margin: 0 0.25rem;
  font-family: system-ui;
  border: none;
  padding: 0.5rem 0.8rem;
  background: #efefef;
  box-shadow: 3px 3px 0 #ccc;
}
View Compiled
console.clear();

const { useState, useEffect, useMemo, useCallback, useRef, forwardRef } = React;
const { Stage, Container, Sprite, PixiComponent, useApp, useTick } = ReactPixi;
const { Texture } = PIXI;

PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;

const width = 500;
const height = 500;

const stageOptions = {
  antialias: true,
  autoDensity: true,
  backgroundAlpha: 0
};

const areas = {
  'world': [1000, 1000, 2000, 2000],
  'center': [1000, 1000, 400, 400],
  'tl': [100, 100, 200, 200],
  'tr': [1900, 100, 200, 200],
  'bl': [100, 1900, 200, 200],
  'br': [1900, 1900, 200, 200]
};

useIteration = (incr = 0.1) => {
  const [i, setI] = React.useState(0);
  
  useTick((delta) => {
    setI(i => i + incr * delta);
  });

  return i;
};

// create and instantiate the viewport component
// we share the ticker and interaction from app
const PixiViewportComponent = PixiComponent("Viewport", {
  create(props) {
    const { app, ...viewportProps } = props;

    const viewport = new Viewport.Viewport({
      ticker: props.app.ticker,
      interaction: props.app.renderer.plugins.interaction,
      ...viewportProps
    });

    // activate plugins
    (props.plugins || []).forEach((plugin) => {
      viewport[plugin]();
    });

    return viewport;
  },
  applyProps(viewport, _oldProps, _newProps) {
    const { plugins: oldPlugins, children: oldChildren, ...oldProps } = _oldProps;
    const { plugins: newPlugins, children: newChildren, ...newProps } = _newProps;

    Object.keys(newProps).forEach((p) => {
      if (oldProps[p] !== newProps[p]) {
        viewport[p] = newProps[p];
      }
    });
  },
  didMount() {
    console.log("viewport mounted");
  }
});

// create a component that can be consumed
// that automatically pass down the app
const PixiViewport = forwardRef((props, ref) => (
  <PixiViewportComponent ref={ref} app={useApp()} {...props} />
));

PixiViewport.displayName = 'PixiViewport';

// Wiggling bunny
const Bunny = forwardRef((props, ref) => {
  // abstracted away, see settings>js
  const i = useIteration(0.1);
  
  return (
    <Sprite
      ref={ref}
      image="https://s3-us-west-2.amazonaws.com/s.cdpn.io/693612/IaUrttj.png"
      anchor={0.5}
      scale={2}
      rotation={Math.cos(i) * 0.98}
      {...props}
    />
  );
});

Bunny.displayName = 'Bunny';

// 4 squared bunnies
// positioned by its name
const BunniesContainer = ({ name, ...props }) => {
  const [x, y] = areas[name];
  
  return (
    <Container x={x} y={y} {...props}>
      <Bunny x={-50} y={-50} />
      <Bunny x={50} y={-50} />
      <Bunny x={-50} y={50} />
      <Bunny x={50} y={50} />
    </Container>
  );
}

const BunnyFollowingCircle = forwardRef(({x, y, rad}, ref) => {
  const i = useIteration(0.02);
  return <Bunny ref={ref} x={x + Math.cos(i) * rad} y={y + Math.sin(i) * rad} scale={6} />
});

// the main app
const App = () => {
  // get the actual viewport instance
  const viewportRef = useRef();
  
  // get ref of the bunny to follow
  const followBunny = useRef();
  
  // interact with viewport directly
  // move and zoom to specified area
  const focus = useCallback((p) => {
    const viewport = viewportRef.current;
    const [x, y, width, height] = areas[p];
    
    // pause following
    viewport.plugins.pause('follow');
    
    // and snap to selected
    viewport.snapZoom({ width, height, removeOnComplete: true });
    viewport.snap(x, y, { removeOnComplete: true });
  }, []);
  
  const follow = useCallback(() => {
    const viewport = viewportRef.current;
    
    viewport.snapZoom({ width: 1000, height: 1000 });
    viewport.follow(followBunny.current, { speed: 20 });
  }, []);
 
  return (
    <>
      <div class="buttons-group">
        <button onClick={() => focus('world')}>Fit</button>
        <button onClick={() => focus('center')}>Center</button>
        <button onClick={() => focus('tl')}>TL</button>
        <button onClick={() => focus('tr')}>TR</button>
        <button onClick={() => focus('bl')}>BL</button>
        <button onClick={() => focus('br')}>BR</button>
        
        <button onClick={() => follow()}>Follow</button>
      </div>
      
      <Stage width={width} height={height} options={stageOptions}>
        <PixiViewport
          ref={viewportRef}
          plugins={["drag", "pinch", "wheel", "decelerate"]}
          screenWidth={width}
          screenHeight={height}
          worldWidth={2000}
          worldHeight={2000}
        >
          <BunniesContainer name="tl" />
          <BunniesContainer name="tr" />
          <BunniesContainer name="bl" />
          <BunniesContainer name="br" />
          <BunniesContainer name="center" scale={2} />
          
          <BunnyFollowingCircle x={1000} y={1000} rad={500} ref={followBunny} />
        </PixiViewport>
      </Stage>
    </>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/pixi-viewport@4.21.3/dist/viewport.js