Bruno Joystick Demo

by Cody Bennett

HTML

<div class="joystick">
  <div class="joystick__cursor"></div>
  <div class="joystick__ring"></div>
</div>

CSS

body {
  background: #F88B3E;
}

.joystick {
  position: fixed;
  bottom: 10px;
  left: 10px;
  width: 170px;
  height: 170px;
  border-radius: 50%;
}

.joystick__cursor {
  width: 60px;
  height: 60px;
  position: absolute;
  top: calc(50% - 30px);
  left: calc(50% - 30px);
  opacity: 0.6;
  border: 2px solid #FFFFFF;
  border-radius: 50%;
}

.joystick__ring {
  position: absolute;
  width: 150px;
  height: 150px;
  top: calc(50% - 77px);
  left: calc(50% - 77px);
  border-radius: 50%;
  border: 2px solid #FFFFFF;
  opacity: 0.3;
  transform: opacity 0.3 0;
}

JavaScript

const LIMIT = 43;

let active;

const angle = {
  center: {
    x: 0,
    y: 0,
  },
  current: {
    x: 0,
    y: 0,
  },
};

const joystick = document.querySelector('.joystick');
const cursor = document.querySelector('.joystick__cursor');
const ring = document.querySelector('.joystick__ring');

const boundings = joystick.getBoundingClientRect();

angle.center.x = boundings.left + boundings.width * 0.5;
angle.center.y = boundings.top + boundings.height * 0.5;

const render = () => {
	if (active) {
		angle.value = Math.atan2(
      (angle.current.y - angle.center.y),
      (angle.current.x - angle.center.x),
    );

		const distance = Math.hypot(
			angle.current.y - angle.center.y,
			angle.current.x - angle.center.x,
		);

		const radius = distance > LIMIT ? LIMIT : distance;

		const cursorX = radius * Math.cos(angle.value);
		const cursorY = radius * Math.sin(angle.value);

		cursor.style.transform = `translateX(${cursorX}px) translateY(${cursorY}px)`;
	}

	requestAnimationFrame(render);
};

render();

const touchstart = (event) => {
  const [touch] = event.changedTouches;

  if (touch) {
    active = true;

    ring.style.opacity = '0.5';

    angle.current.x = touch.clientX;
    angle.current.y = touch.clientY;
    
    document.addEventListener('touchend', touchend);
    document.addEventListener('touchmove', touchmove, {
      passive: false
    });
  }
};

const touchmove = (event) => {
  const [touch] = event.changedTouches;

  if (touch) {
    active = true;

    angle.current.x = touch.clientX;
    angle.current.y = touch.clientY;
  }
};

const touchend = (event) => {
  const [touch] = event.changedTouches;

  ring.style.opacity = '0.3';

  if (touch) {
    active = false;

    cursor.style.transform = 'translateX(0px) translateY(0px)';
    document.removeEventListener('touchend', touchend);
  }
};

joystick.addEventListener('touchstart', touchstart, {
  passive: false
});