Skip to content
Snippets Groups Projects
avatar-selector.js 6.9 KiB
Newer Older
import React, { Component } from "react";
import PropTypes from "prop-types";
Greg Fodor's avatar
Greg Fodor committed
import { injectIntl } from "react-intl";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faAngleLeft } from "@fortawesome/free-solid-svg-icons/faAngleLeft";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
import { WithHoverSound } from "./wrap-with-audio";
class AvatarSelector extends Component {
  static propTypes = {
    avatars: PropTypes.array,
    avatarId: PropTypes.string,
    onChange: PropTypes.func
  };
Brian Peiris's avatar
Brian Peiris committed
  static getAvatarIndex = (props, offset = 0) => {
    const currAvatarIndex = props.avatars.findIndex(avatar => avatar.id === props.avatarId);
    const numAvatars = props.avatars.length;
Marshall Quander's avatar
Marshall Quander committed
    return (((currAvatarIndex + offset) % numAvatars) + numAvatars) % numAvatars;
Brian Peiris's avatar
Brian Peiris committed
  static nextAvatarIndex = props => AvatarSelector.getAvatarIndex(props, -1);
  static previousAvatarIndex = props => AvatarSelector.getAvatarIndex(props, 1);

  state = {
    initialAvatarIndex: 0,
    avatarIndices: []
  };

  getAvatarIndex = (offset = 0) => AvatarSelector.getAvatarIndex(this.props, offset);
joni's avatar
joni committed
  nextAvatarIndex = () => this.getAvatarIndex(-1);
  previousAvatarIndex = () => this.getAvatarIndex(1);
Brian Peiris's avatar
Brian Peiris committed
  emitChangeToNext = () => {
Brian Peiris's avatar
Brian Peiris committed
    const nextAvatarId = this.props.avatars[this.nextAvatarIndex()].id;
Brian Peiris's avatar
Brian Peiris committed
    this.props.onChange(nextAvatarId);
Brian Peiris's avatar
Brian Peiris committed
  emitChangeToPrevious = () => {
Brian Peiris's avatar
Brian Peiris committed
    const previousAvatarId = this.props.avatars[this.previousAvatarIndex()].id;
    this.props.onChange(previousAvatarId);
Brian Peiris's avatar
Brian Peiris committed
  constructor(props) {
    super(props);
    this.state.initialAvatarIndex = AvatarSelector.getAvatarIndex(props);
    this.state.avatarIndices = [
      AvatarSelector.nextAvatarIndex(props),
      this.state.initialAvatarIndex,
      AvatarSelector.previousAvatarIndex(props)
    ];
  }

Marshall Quander's avatar
Marshall Quander committed
  UNSAFE_componentWillReceiveProps(nextProps) {
Brian Peiris's avatar
Brian Peiris committed
    // Push new avatar indices onto the array if necessary.
    this.setState(state => {
      const numAvatars = nextProps.avatars.length;
      if (state.avatarIndices.length === numAvatars) return;

      const lastIndex = numAvatars - 1;
      const currAvatarIndex = this.getAvatarIndex();
Brian Peiris's avatar
Brian Peiris committed
      const nextAvatarIndex = AvatarSelector.getAvatarIndex(nextProps);
      const avatarIndices = Array.from(state.avatarIndices);
      const increasing = currAvatarIndex - nextAvatarIndex < 0;

      let direction = -1;
      let push = false;

      if (nextAvatarIndex === 0) {
        if (currAvatarIndex === lastIndex) {
          direction = 1;
          push = avatarIndices.indexOf(lastIndex) !== 0;
        } else {
          direction = -1;
          push = avatarIndices.indexOf(1) !== 0;
        }
      } else if (nextAvatarIndex === lastIndex) {
        if (currAvatarIndex === 0) {
          direction = -1;
          push = avatarIndices.indexOf(0) === 0;
        } else {
          direction = 1;
          push = avatarIndices.indexOf(lastIndex - 1) !== 0;
        }
      } else {
        direction = increasing ? 1 : -1;
        push = increasing;
      }

      const addIndex = AvatarSelector.getAvatarIndex(nextProps, direction);
      if (avatarIndices.includes(addIndex)) return;

      if (push) {
        avatarIndices.push(addIndex);
Brian Peiris's avatar
Brian Peiris committed
      } else {
        avatarIndices.unshift(addIndex);
Brian Peiris's avatar
Brian Peiris committed
      }
      return { avatarIndices };
  componentDidUpdate(prevProps) {
    if (this.props.avatarId !== prevProps.avatarId) {
      // HACK - a-animation ought to restart the animation when the `to` attribute changes, but it doesn't
      // so we need to force it here.
      const currRot = this.animation.parentNode.getAttribute("rotation");
      const toRot = this.animation.getAttribute("to").split(" ");
      const toY = toRot[1];
      const step = 360.0 / this.props.avatars.length;
      const brokenlyBigRotation = Math.abs(toY - currY) > 3 * step;
      let fromY = currY;
      if (brokenlyBigRotation) {
        // Rotation in Y wrapped around 360. Adjust the "from" to prevent a dramatic rotation
        fromY = currY < toY ? currY + 360 : currY - 360;
      }
      this.animation.setAttribute("from", `${currRot.x} ${fromY} ${currRot.z}`);
      this.animation.stop();
      this.animation.handleMixinUpdate();
      this.animation.start();
    }
  componentDidMount() {
    // <a-scene> component not initialized until scene element mounted and loaded.
    this.scene.addEventListener("loaded", () => {
      this.scene.setAttribute("renderer", { gammaOutput: true, sortObjects: true, physicallyCorrectLights: true });
      this.scene.setAttribute("gamma-factor", "");
    const avatarAssets = this.props.avatars.map(avatar => (
      <a-asset-item id={avatar.id} key={avatar.id} response-type="arraybuffer" src={`${avatar.model}`} />
Brian Peiris's avatar
Brian Peiris committed
    const avatarData = this.state.avatarIndices.map(i => [this.props.avatars[i], i]);
    const avatarEntities = avatarData.map(([avatar, i]) => (
Marshall Quander's avatar
Marshall Quander committed
      <a-entity key={avatar.id} rotation={`0 ${(360 * -i) / this.props.avatars.length} 0`}>
        <a-entity position="0 0 5" gltf-model-plus={`src: #${avatar.id}; inflate: true`}>
Brian Peiris's avatar
Brian Peiris committed
          <a-animation
            attribute="rotation"
            dur="12000"
Brian Peiris's avatar
Brian Peiris committed
            to={`0 ${this.getAvatarIndex() === i ? 360 : 0} 0`}
            repeat="indefinite"
          />
Marshall Quander's avatar
Marshall Quander committed
    const rotationFromIndex = index => ((360 * index) / this.props.avatars.length + 180) % 360;
Brian Peiris's avatar
Brian Peiris committed
    const initialRotation = rotationFromIndex(this.state.initialAvatarIndex);
    const toRotation = rotationFromIndex(this.getAvatarIndex());

    return (
      <div className="avatar-selector">
        <a-scene vr-mode-ui="enabled: false" ref={sce => (this.scene = sce)}>
Greg Fodor's avatar
Greg Fodor committed
          <a-assets>{avatarAssets}</a-assets>
Brian Peiris's avatar
Brian Peiris committed
          <a-entity rotation={`0 ${initialRotation} 0`}>
            <a-animation
              ref={anm => (this.animation = anm)}
              attribute="rotation"
              dur="2000"
              easing="ease-out"
Brian Peiris's avatar
Brian Peiris committed
              to={`0 ${toRotation} 0`}
            />
            {avatarEntities}
          </a-entity>
Brian Peiris's avatar
Brian Peiris committed
          <a-entity position="0 1.5 -5.6" rotation="-10 180 0">
Brian Peiris's avatar
Brian Peiris committed
          </a-entity>
          <a-entity
            hide-when-quality="low"
            light="type: directional; color: #F9FFCE; intensity: 0.6"
            position="0 5 -15"
          />
          <a-entity hide-when-quality="low" light="type: ambient; color: #FFF" />
        </a-scene>
        <WithHoverSound>
          <button className="avatar-selector__previous-button" onClick={this.emitChangeToPrevious}>
            <FontAwesomeIcon icon={faAngleLeft} />
          </button>
        </WithHoverSound>
        <WithHoverSound>
          <button className="avatar-selector__next-button" onClick={this.emitChangeToNext}>
            <FontAwesomeIcon icon={faAngleRight} />
          </button>
        </WithHoverSound>
Brian Peiris's avatar
Brian Peiris committed
export default injectIntl(AvatarSelector);