<script src="https://unpkg.com/three@0.142.0/build/three.min.js"></script>

<p class="caution">※モバイル端末でのtouchイベントのみ対応。<br>click/mouseイベントは発火しません。</p>

<canvas id="canvas"></canvas>
<div id="joystick" class="joystick">
  <div id="joystick-frame" class="joystick-frame">
    <div id="joystick-ball" class="joystick-ball"></div>
  </div>
</div>
body {
  position: fixed;
  background: #8cafea;
}
.caution {
  position: fixed;
  margin: auto;
  text-align: center;
  width: 100%;
  font-size: 0.85rem;
  color: white;
}
.joystick {
  position: fixed;
  bottom: 0.5rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
}
.joystick-frame {
  width: 130px;
  height: 130px;
  border-radius: 100rem;
  border: 2px white solid;
  position: relative;
}
.joystick-ball {
  width: 60px;
  height: 60px;
  background: white;
  border-radius: 100rem;
  border: 2px white solid;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
let canvas;
let scene;
let camera;
let renderer;
let model;
let joystickBall;
let vectorMagnitude = 0;
let updateRequestId;
let joystickCenterX;
let joystickCenterY;
let joystickLimitNumber = 35;

const init = () => {
  setupJoystick();
  setUpScene();
  bindEvents();
};

const setupJoystick = () => {
  joystickBall = document.getElementById("joystick-ball");
  joystickCenterX =
    joystickBall.getBoundingClientRect().left + joystickBall.clientWidth / 2;
  joystickCenterY =
    joystickBall.getBoundingClientRect().top + joystickBall.clientHeight / 2;
  joystickBall.addEventListener("touchstart", dragStart);
  joystickBall.addEventListener("touchmove", dragMove);
  joystickBall.addEventListener("touchend", dragLeave);
};

const dragStart = () => {
  if (!model) return;
  dragUpdate();
};

const dragUpdate = () => {
  if (vectorMagnitude !== 0) {
    model.translateZ(vectorMagnitude / 10000);
  }
  updateRequestId = requestAnimationFrame(dragUpdate);
};

const dragMove = (event) => {
  event.preventDefault();

  const pageX = event.touches[0].pageX;
  const pageY = event.touches[0].pageY;

  let touchX =
    Math.abs(pageX - joystickCenterX) < joystickLimitNumber
      ? pageX - joystickCenterX
      : pageX - joystickCenterX > 0
      ? joystickLimitNumber
      : -joystickLimitNumber;

  let touchY =
    Math.abs(pageY - joystickCenterY) < joystickLimitNumber
      ? pageY - joystickCenterY
      : pageY - joystickCenterY > 0
      ? joystickLimitNumber
      : -joystickLimitNumber;

  const vector2d = new THREE.Vector2(touchX, touchY);

  vectorMagnitude = vector2d.x * vector2d.x + vector2d.y * vector2d.y;

  const vector3d = new THREE.Vector3(vector2d.x * 1000, 0, vector2d.y * 1000);
  model.lookAt(vector3d);

  joystickBall.style.left = `calc(50% + ${touchX}px)`;
  joystickBall.style.top = `calc(50% + ${touchY}px)`;
};

const dragLeave = () => {
  joystickBall.style.top = "50%";
  joystickBall.style.left = "50%";
  cancelAnimationFrame(updateRequestId);
  vectorMagnitude = 0;
};

const setUpScene = () => {
  canvas = document.getElementById("canvas");
  setScene();
  setCamera();
  setObjects();
  setLights();
  setRenderer();
};

const setScene = () => {
  scene = new THREE.Scene();
};

const setCamera = () => {
  camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    0.1,
    100
  );
  camera.position.set(0, 8, 8);
  camera.lookAt(new THREE.Vector3());
};

const setObjects = () => {
  const geo = new THREE.BoxGeometry(1, 1, 1);
  const mat = new THREE.MeshPhongMaterial({ color: 0x7140ce });

  model = new THREE.Mesh(geo, mat);
  model.rotation.y = THREE.MathUtils.degToRad(45);
  scene.add(model);
};

const setLights = () => {
  const ambient = new THREE.AmbientLight(0xffffff, 1);
  scene.add(ambient);
  const frontLight = new THREE.DirectionalLight(0xffffff, 0.5);
  frontLight.position.set(1, 10, 20);
  scene.add(frontLight);
  const backLight = new THREE.DirectionalLight(0xffffff, 0.2);
  backLight.position.set(-1, 10, -10);
  scene.add(backLight);
};

const setRenderer = () => {
  renderer = new THREE.WebGLRenderer({
    antialias: true,
    canvas: canvas
  });
  renderer.setClearColor(0x8cafea);
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setAnimationLoop(() => {
    renderStage();
  });
};

const renderStage = () => {
  renderer.render(scene, camera);
};

const bindEvents = () => {
  window.addEventListener("resize", () => {
    onResize();
  });
};

const onResize = () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  joystickCenterX =
    joystickBall.getBoundingClientRect().left + joystickBall.clientWidth / 2;
  joystickCenterY =
    joystickBall.getBoundingClientRect().top + joystickBall.clientHeight / 2;
};

init();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.