• Runtimes
  • [spine-player] how to update padTop, padBottom, after instantiation

Hello! I'm wondering in a React environment, how I can update the viewport properties like padTop, padBottom, x, y, height, etc after instantiating the spine-player so that I can update the spine-player based on the state passed into the react component. I found a way to update the skins and animation, but not viewport properties. Here is my React component below. Please also let me know if you see anything else concerning with my react setup in general.

const SpinePlayerComponent = ({
  id,
  animation,
  skins,
  padTop,
  padLeft,
  padBottom,
  padRight,
}) => {
  const playerDivRef = useRef<HTMLDivElement>(null);
  const playerRef = useRef<SpinePlayer | null>(null);

  const [isPlayerLoaded, setIsPlayerLoaded] = useState(false);

  const skeleton = isPlayerLoaded && playerRef?.current?.skeleton;

  useEffect(() => {
    if (playerDivRef.current && !playerRef.current) {
      const spinePlayer = new SpinePlayer('spine-player-component', {
        binaryUrl: 'test.skel',
        atlasUrl: 'test.atlas',
        animation,
        showControls: false,
        preserveDrawingBuffer: false,
        alpha: true,
        success: () => {
          setIsPlayerLoaded(true);
        },
        error: (error) => {
          console.error('Error initializing SpinePlayer:', error);
        },
      });

      playerRef.current = spinePlayer;
    }

    return () => {
      if (playerRef.current) {
        playerRef.current.dispose();
        playerRef.current = null;
      }
    };
    // no dependencies, only initialize spine player once
  }, []);

  useEffect(() => {
    if (skeleton && animation) {
      playerRef.current.setAnimation(animation);
    }
  }, [animation, skeleton]);

  useEffect(() => {
    if (skeleton && skins) {
      const newSkin = new Skin('custom-skin');
      skins.forEach((skinName) => {
        newSkin.addSkin(skeleton.data.findSkin(skinName));
      });
      skeleton.setSkin(newSkin);
      skeleton.setToSetupPose();
    }
  }, [skins, skeleton]);

  // TODO: not sure how to update viewport properties
  // useEffect(() => {
  //   if (skeleton) {
  //     playerRef.current.viewport = {
  //       ...playerRef.current.viewport,
  //       padTop: `%${padTop}`,
  //       padBottom: `%${padBottom}`,
  //       padLeft: `%${padLeft}`,
  //       padRight: `%${padRight}`,
  //     };
  //     playerRef.current.setViewport(animation);
  //   }
  // }, [padTop, padBottom, padLeft, padRight, animation, skeleton]);

  return (
    <div ref={playerDivRef} id={playerId} className="w-[500px] h-[500px]" />
  );
};
Related Discussions
...

To update the viewport properties like padTop, padBottom, x, y, height, etc., after instantiating the Spine player in a React environment, you can directly modify the viewport configuration of the player instance. However, the Spine player API does not provide a direct method to update the viewport after instantiation. Instead, you can dispose of the current player instance and create a new one with the updated configuration. Here's how you can modify your React component to achieve this:

const SpinePlayerComponent = ({
  id,
  animation,
  skins,
  padTop,
  padLeft,
  padBottom,
  padRight,
}) => {
  const playerDivRef = useRef<HTMLDivElement>(null);
  const playerRef = useRef<SpinePlayer | null>(null);

  const [isPlayerLoaded, setIsPlayerLoaded] = useState(false);

  const skeleton = isPlayerLoaded && playerRef?.current?.skeleton;

  const initializePlayer = () => {
    if (playerDivRef.current) {
      const spinePlayer = new SpinePlayer(playerDivRef.current, {
        binaryUrl: 'test.skel',
        atlasUrl: 'test.atlas',
        animation,
        showControls: false,
        preserveDrawingBuffer: false,
        alpha: true,
        viewport: {
          padTop: `${padTop}%`,
          padBottom: `${padBottom}%`,
          padLeft: `${padLeft}%`,
          padRight: `${padRight}%`,
        },
        success: () => {
          setIsPlayerLoaded(true);
        },
        error: (error) => {
          console.error('Error initializing SpinePlayer:', error);
        },
      });

      playerRef.current = spinePlayer;
    }
  };

  useEffect(() => {
    initializePlayer();

    return () => {
      if (playerRef.current) {
        playerRef.current.dispose();
        playerRef.current = null;
      }
    };
  }, [padTop, padBottom, padLeft, padRight]);

  useEffect(() => {
    if (skeleton && animation) {
      playerRef.current.setAnimation(animation);
    }
  }, [animation, skeleton]);

  useEffect(() => {
    if (skeleton && skins) {
      const newSkin = new Skin('custom-skin');
      skins.forEach((skinName) => {
        newSkin.addSkin(skeleton.data.findSkin(skinName));
      });
      skeleton.setSkin(newSkin);
      skeleton.setToSetupPose();
    }
  }, [skins, skeleton]);

  return (
    <div ref={playerDivRef} id={id} className="w-[500px] h-[500px]" />
  );
};

In this setup, the player is re-initialized whenever the padding properties change, ensuring the viewport is updated accordingly.

Also, I wonder if padTop, padBottom etc is the right way to achieve what I'm trying to accomplish. I'm rendering a character on the client side with spine-player but I want the character to be shown with a specific positioning and scale (only show the top half of the character which I'm trying to do with padTop and negative padBottom). I also need to do server side generation to get the exact same result with spine-canvaskit, but there is no padding features for canvaskit and and I'd need to adjust the skeleton x, y, and scale instead.

Perhaps I need to approach this differently and would love any advice!

doing this seems to work for spine-player for positioning the character, and I can try to similarly set x, y, scale for canvaskit too (although haven't tried yet):

  useEffect(() => {
    if (skeleton) {
      skeleton.x = x;
      skeleton.y = y;
      skeleton.scaleX = scale;
      skeleton.scaleY = scale;
    }
  }, [x, y, scale, skeleton]);

However, once I change the animation, it seems like the skeleton x, y, and scale resets to fit the player's default view

    jojo

    Can't you just define your desired viewport for each animation you're going to play as explained in the latest part of the Viewport paragraph?

    For example:

    viewport: {
    	debugRender: true,
    	x: 10,
    	y: 20,
    	width: 100,
    	height: 100,
    	padLeft: "5%",
    	padRight: "5%",
    	padTop: "5%",
    	padBottom: "5%",
    	animations: {
    		"walk": {
    			x: 30,
    			y: 40,
    			width: 700,
    			height: 700,
    			padLeft: "0%",
    			padRight: "0%",
    			padTop: "0%",
    			padBottom: "0%"
    		},
    		"run": {
    			padLeft: "10%",
    			padRight: "10%",
    			padTop: "10%",
    			padBottom: "10%"
    		},
    	}
    },