<div id="root"></div>
html,
body {
	height: 100%;
	width: 100%;
}
body {
	display: flex;
	align-items: center;
	justify-content: center;
	margin: 0;
	overflow: hidden;
}
.app {
	background-color: #a0d8ef;
	height: 320px;
	position: relative;
	width: 480px;
}

.santa {
	background-color: red;
	height: 80px;
  position: absolute;
	width: 40px;
}
const { useState, useEffect, useCallback, useRef, useContext, memo, forwardRef, createContext } = React;

const AppContext = createContext();

const useElementSize = (elementRef) => {
  const [height, setHeight] = useState(0);
  const [width, setWidth] = useState(0);
  
  useEffect(() => {
    const rect = elementRef.current.getBoundingClientRect();
    setHeight(rect.height);
    setWidth(rect.width);
  }, []);
  
  return [width, height];
}

const useAnimationFrameCount = () => {
	const [frameCount, setFrameCount] = useState(0);

	useEffect(() => {
		const loop = () => {
			setFrameCount(prevFrameCount => prevFrameCount + 1);
			requestAnimationFrame(loop);
		};

		requestAnimationFrame(loop);
	}, []);

	return frameCount;
}

const usePressKeyStatus = () => {
	const [stateOfPressKey, setStateOfPressKey] = useState({});
	const handleKeyUp = useCallback((e) => {
		const keyCode = e.keyCode;

		if (keyCode === 37) {// left
			setStateOfPressKey(state => ({
				...state,
				left: false
			}));
		} 
		if (keyCode === 39) {//right
			setStateOfPressKey(state => ({
				...state,
				right: false
			}));
		} 
		if (keyCode === 38) {//up
			setStateOfPressKey(state => ({
				...state,
				top: false
			}));
		}
	}, []);

	const handleKeyDown = useCallback((e) => {
		const keyCode = e.keyCode;

		if (keyCode === 37) {// left
			setStateOfPressKey(state => ({
				...state,
				left: true
			}));
		} 
		if (keyCode === 39) {//right
			setStateOfPressKey(state => ({
				...state,
				right: true
			}));
		} 
		if (keyCode === 38) {//up
			setStateOfPressKey(state => ({
				...state,
				top: true
			}));
		}
	}, []);

	useEffect(() => {
		addEventListener('keydown', e => handleKeyDown(e));
		addEventListener('keyup', e => handleKeyUp(e));
	}, []);

	return stateOfPressKey;
}

const useDistanceXByPressKey = (elementRef, initialVelocityX) => {
	const [positionX, setPositionX] = useState(0);
	const stateOfPressKey = usePressKeyStatus();
	const appInfo = useContext(AppContext);
	const frameCount = useAnimationFrameCount();

	useEffect(() => {
		if(!elementRef.current) return;

		const rect = elementRef.current.getBoundingClientRect();
		const appRect = appInfo.elementRef.current.getBoundingClientRect();
		
		let velocityX = 0;

		if (stateOfPressKey.left) {
			if (rect.left - appRect.left <= 0) {
				velocityX = 0;
			} else {
				velocityX -= initialVelocityX;
			}
		}

		if (stateOfPressKey.right) {
			if (rect.right - appRect.right >= 0) {
				velocityX = 0;
			} else {
				velocityX += initialVelocityX;
			}
		}

		setPositionX(prevPositionX => prevPositionX + velocityX);
	}, [frameCount]);

	return positionX;
}

const useJumpHeightByPressKey = (initialVelocityY, gravity) => {// initialVelocity初速度
	const [positionY, setPositionY] = useState(0);
	const isJumping = useRef(false);
	const elapsedTime = useRef(0);
	const stateOfPressKey = usePressKeyStatus();
	const frameCount = useAnimationFrameCount();

	useEffect(() => {
		if (stateOfPressKey.top) {
			if(!isJumping.current) {//着地しているときのみ
				isJumping.current = true;
				elapsedTime.current = 0;
			}
		}

		let velocityY = (0.5 * gravity * (elapsedTime.current ** 2) - initialVelocityY * elapsedTime.current);

		elapsedTime.current++;

		//velocityY > 0
		if(positionY > 0) {//着地したら
			isJumping.current = false;
		}

		if(isJumping.current) {
			setPositionY(velocityY);
		}else{
			setPositionY(0);
		}
	}, [frameCount]);

	return positionY;
}

const gravity = .3;

const Santa = memo(forwardRef(({x, y, offsetX, offsetY}, ref) => {
	const style = {
		left: x,
		top: y,
		transform: `translate3d(${offsetX}px, ${offsetY}px, 0)`
	};

	return (
		<div
			className="santa"
			style={style}
			ref={ref}
		>
		</div>
	);
}));

const SantaContainer = ({ x, y, initialVelocityX, initialVelocityY }) => {
	const elementRef = useRef(null);
	const offsetX = useDistanceXByPressKey(elementRef, initialVelocityX);
	const offsetY = useJumpHeightByPressKey(initialVelocityY, gravity);

	return (
		<Santa
          x={x}
          y={y}
          offsetX={offsetX}
          offsetY={offsetY}
          ref={elementRef}
        />
	);
};

const App = forwardRef(({width, height}, ref) => {
  return (   
    <div
      className="app"
      ref={ref}
    >
      <AppContext.Provider 
      	value={{
      		elementRef: ref
      	}}
      >
        <SantaContainer
        	x={(width / 2) - (50 / 2)}
			y={height - 80}
          	initialVelocityX={10}
          	initialVelocityY={10}
        />
      </AppContext.Provider>
    </div>      
  );
});

const AppContainer = () => {
	const elementRef = useRef(null);
  const [width, height] = useElementSize(elementRef);
  
  return (
  	<App
      width={width}
      height={height}
      ref={elementRef}
    />
  );
}

ReactDOM.render(<AppContainer/>, document.getElementById('root'));
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js