<aside> 💡 Description: Example of Geenee Joystick control integration and how to move model around the AR scene. Custom Script loading. Event state manager. Custom Animation Mixer implementation. crossFade animation function helps to make animation’s transitions smoother and seamless. Extra UI HTML control element for switching between Car control and Character control with corresponding animations packs

</aside>

Image from iOS (8).mov

Main Controller

Handle events, joystick & custom UI

const events = {
  render: "render",
  onJoystickChange: "onJoystickChange",
  enterCar: "enterCar",
  exitCar: "exitCar",
};
let joystick;
let joyX, joyY;
let started = false;
// this function will work cross-browser for loading scripts asynchronously
const loadScript = (_name, src, callback) => {
  let s;
  let r;
  let t;
  r = false;
  s = document.createElement("script");
  s.type = "text/javascript";
  s.src = src;
  s.onload = s.onreadystatechange = () => {
    if (!r && (!this.readyState || this.readyState == "complete")) {
      r = true;
      callback(_name);
    }
  };
  t = document.getElementsByTagName("script")[0];
  t.parentNode.insertBefore(s, t);
};

// After all necessary scripts loaded
const onLoadScript = (_name) => {
  if (_name == "joystick") {
    // do something after script loaded.
    const joystickElement = document.getElementById("geenee-joystick");
    joystickElement.style.display = "block";
    const joystick = this.activeSceneModel.joystick;

    //const deg = Math.PI / 180;
    //const object = this.object3D;

    const render = () => {
      if (started) {
        //object.translateX(joystick.GetY() / 4000);
        //object.rotation.y -= (joystick.GetX() / 30) * deg;
        const joystickEvent = new CustomEvent(events.onJoystickChange, {
          detail: {
            x: joystick.value.x,
            y: joystick.value.y,
          },
        });
        //console.log(`Joystick x: ${joystick.value.x}; joystick y: ${joystick.value.y}`);
        document.dispatchEvent(joystickEvent);

        const renderEvent = new CustomEvent(events.render);
        document.dispatchEvent(renderEvent);
      }
    };

    // Reassign to Geenee Render Loop
    this.activeSceneModel.userCallbacks.onRender = render;
  }
};

onLoadScript("joystick");

//Disable branding bar
let brandingBar = document.getElementById(
  "brandingBar geenee-ui-branding-bar--wrapper"
);
if (brandingBar) brandingBar.style.display = "none";

const initializeUI = () => {
  const top = "80px";
  initializeButton(top);
};

const globalWrapper = document.getElementById("geenee-ui-global--wrapper");
const enterCarEvent = new Event(events.enterCar);
const exitCarEvent = new Event(events.exitCar);
const buttonSrc = {
  enterCarSrc:
    "<https://eu-central-1-staging-cms-01-attachments-upload.geenee.io/attachments/39de9428-8d81-441c-bd5b-eece37aacc4f/enter-car.png>",
  exitCarSrc:
    "<https://eu-central-1-staging-cms-01-attachments-upload.geenee.io/attachments/ac31a3d3-3cb9-4808-8eb2-780469f2afc6/exit-car.png>",
};
let outCarState = true; // if true - avatar not in car, if false - avatar in car.
const initializeButton = (top) => {
  const imgWrapper = document.createElement("div");
  const img = document.createElement("img");
  imgWrapper.style = buttonWrapperStyle;
  imgWrapper.style.top = top;
  globalWrapper.appendChild(imgWrapper);
  img.src = buttonSrc.enterCarSrc;
  img.style = buttonStyle;
  imgWrapper.addEventListener("click", () => {
    if (outCarState) {
      document.dispatchEvent(enterCarEvent);
      outCarState = false;
      img.src = buttonSrc.exitCarSrc;
    } else if (!outCarState) {
      document.dispatchEvent(exitCarEvent);
      outCarState = true;
      img.src = buttonSrc.enterCarSrc;
    }
  });
  imgWrapper.appendChild(img);
};

this.activeSceneModel.$parent.emitter.addListener("geenee-model-placed", () => {
  started = true;
  this.activeSceneModel.setScene3DSettingsOption("defaultCanvasClick", false);
  this.activeSceneModel.setGestureOption("dragOn", false);

  initializeUI();
});

const buttonWrapperStyle =
  "position: absolute; z-index: 1; left: 15px; top: 80px; background-color: rgba(255, 255, 255, 0.15); -webkit-backdrop-filter: blur(15px); width: 45px; height: 45px; border-top-left-radius: 100%; border-top-right-radius: 100%; border-bottom-right-radius: 100%; border-bottom-left-radius: 100%; display: flex; -webkit-box-align: center; align-items: center; -webkit-box-pack: center; justify-content: center;";
const buttonStyle = "width: 30px;";

Car Code

//THIS IS CAR
const events = {
  render: "render",
  onJoystickChange: "onJoystickChange",
  enterCar: "enterCar",
  exitCar: "exitCar",
  onCarSpawned: "onCarSpawned",
};

let isCarActive = false;
let object = undefined;
let matrix = undefined;
this.activeSceneModel.$parent.emitter.addListener("geenee-model-placed", () => {
  object = this.object3D;
  //console.log("This Object 3d name: " + object.name);
  //console.dir(this.object3D);
  document.addEventListener(events.onJoystickChange, onJoystickChange);
  document.addEventListener(events.enterCar, onAvatarEnterCar);
  document.addEventListener(events.exitCar, onAvatarExitCar);
  matrix = this.activeSceneModel.getObjectByName(
    "geenee-3d-matrix-target-group"
  );
  object.translateZ(-0.5);
  const onCarSpawnedEvent = new CustomEvent(events.onCarSpawned, {
    detail: object.parent.name,
  });
  document.dispatchEvent(onCarSpawnedEvent);
  console.log("Send event with data: " + object.parent.name);
});

const onAvatarEnterCar = () => {
  isCarActive = true;
};

const onAvatarExitCar = () => {
  isCarActive = false;
};

const deg = Math.PI / 180;
const onJoystickChange = (e) => {
  if (!isCarActive) return;
  if (!object || !matrix) return;
  const joyX = e.detail.x;
  const joyY = e.detail.y;
  if (joyX || joyY) {
    object.translateX(joyY / 2000);
    object.rotation.y -= (joyX / 30) * deg;

    //const dist = Math.max(Math.abs(joyX), Math.abs(joyY));
    //object.translateX(dist / 2000);
    //object.rotation.y = Math.atan2(joyX, -joyY) - matrix.rotation.y;
  }
};

Avatar Code

//THIS IS AVATAR
let object = undefined;
let matrix = undefined;
let mixer = undefined;
let started = false;
let clips = undefined;
let car = undefined;
const clock = new THREE.Clock();
const animations = {
  idle: "Idle 02",
  dance: "Dance Breakin 01",
  walk: "Walk 01",
};
const events = {
  render: "render",
  onJoystickChange: "onJoystickChange",
  enterCar: "enterCar",
  exitCar: "exitCar",
  onCarSpawned: "onCarSpawned",
};

document.addEventListener(events.onCarSpawned, (e) => {
  car = this.activeSceneModel.getObjectByName(e.detail).children[0];
});

let isAvatarActive = true;
this.activeSceneModel.$parent.emitter.addListener("geenee-model-placed", () => {
  initializeMixer();
  object = this.object3D;
  matrix = this.activeSceneModel.getObjectByName(
    "geenee-3d-matrix-target-group"
  );
  //car = this.activeSceneModel.getObjectByName("geenee-3d-object-wrapper-2").children[0]; // 3 - is a car number in hierarchy
  document.addEventListener(events.onJoystickChange, onJoystickChange);
  setTimeout(() => playAnim(animations.idle, mixer, clips), 0);
  document.addEventListener(events.enterCar, onAvatarEnterCar);
  document.addEventListener(events.exitCar, onAvatarExitCar);
});

const onAvatarEnterCar = () => {
  const mesh = this.object3D.children[0];
  mesh.visible = false;
  isAvatarActive = false;
};

const onAvatarExitCar = () => {
  const mesh = this.object3D.children[0];
  mesh.visible = true;
  isAvatarActive = true;
  this.object3D.position.set(
    car.position.x,
    car.position.y,
    car.position.z - 0.25
  );
  //As well here we should get car position to set correct position for avatar on exit
};

let run = false;
const onJoystickChange = (e) => {
  if (!isAvatarActive) return;
  if (!object | !matrix) return;
  const joyX = e.detail.x;
  const joyY = e.detail.y;
  if (joyX || joyY) {
    const dist = Math.max(Math.abs(joyX), Math.abs(joyY));
    object.translateZ(dist / 4000);
    object.rotation.y = Math.atan2(joyX, -joyY) - matrix.rotation.y;

    if (!run) {
      if (mixer) crossFade(animations.idle, animations.walk, 0.5, mixer);
      run = true;
    }
  } else {
    if (run) {
      if (mixer) crossFade(animations.walk, animations.idle, 0.5, mixer);
      run = false;
    }
  }
};

//ANIMATIONS
const initializeMixer = () => {
  const mesh = this.object3D.children[0];

  // Create an AnimationMixer, and get the list of AnimationClip instances
  mixer = new THREE.AnimationMixer(mesh);
  clips = mesh.animations;

  document.addEventListener("render", () => {
    mixer.update(clock.getDelta());
  });
};

const stopAnim = (name, mixer) => {
  mixer.clipAction(name).stop();
};
const playAnim = (name, mixer, clips) => {
  stopAllAnimations(clips, mixer);
  mixer.clipAction(name).play();
};
const animAction = (name, mixer) => {
  return mixer.clipAction(name);
};

const crossFade = (out, to, time, mixer) => {
  stopAllAnimations(clips, mixer);
  animAction(out, mixer).play();
  animAction(out, mixer).crossFadeTo(animAction(to, mixer), time);
  animAction(to, mixer).play();
};

const playAllAnimations = (clips, mixer) => {
  // Play all animations
  clips.forEach(function (clip) {
    mixer.clipAction(clip).play();
  });
};

const stopAllAnimations = (clips, mixer) => {
  // Play all animations
  clips.forEach(function (clip) {
    mixer.clipAction(clip).stop();
  });
};