Repository

js/Mobilizing/renderer/3D/three/ui/PanelStack.js

/* eslint-disable no-loop-func */
/* eslint-disable guard-for-in */
import Component from "../../../../core/Component";
import Node from "../shape/3D/primitive/Node";
import Animation from "../../../../misc/Animation";
import Vector3 from "../types/Vector3";
import Panel from "./Panel";

const MODE_WHEEL = "wheel";
const MODE_LINE = "line";

const EVT_TRANSLATE_ENDED = "translationended";

export default class PanelStack extends Component {
    /**
     * A PanelStack is an organized Panel assemblage to be used as a selection menu. It's a 3D UI element.
     * @param {Object} params parameters object.
     * @param {String} params.mode="wheel" the mode to use for panel's organisation in space.
     * @param {Array} params.panels The Panel objects array to use for building this PanelStack.
     * @param {Pointer} params.pointer The Pointer instance to be used for interaction with this PanelStack.
     * @param {Camera} params.camera The camera used in the scene to compute the raycasting based interaction with panels.
     * @param {Number} params.wheelRadius The radius of the wheel menu.
     * @param {Number} params.wheelVerticalFactor=1 The factor to use for flattening or expanding the global vertical shape of the wheel (will produce a vertical ellipse, instead of a circle).
     * @param {Number} params.wheelDepthFactor=1 (wheel mode only) The factor to use for flattening or expanding the depth of of each panel in the wheel (will produce an horizontal in depth ellipse, instead of a circle)
     * @param {Number} params.lineHorizontalOffset=10 the x axis offset of the Panels organized in line.
     * @param {Number} params.lineDepthOffset=10 the z axis offset of the Panels organized in line.
     * @param {Animation.Easing} params.animationEasing=Animation.Easing.easeOutQuint the easing effect to use for the animation of Panel.
     * @param {Number} params.animationDuration=2000 the animation duration in ms
     */
    constructor({
        mode = MODE_WHEEL,
        wheelRadius = 50,
        wheelVerticalFactor = 1,
        wheelDepthFactor = 1,
        lineHorizontalOffset = 10,
        lineDepthOffset = 10,
        panels = null,
        pointer = null,
        camera = null,
        animationEasing = Animation.Easing.easeOutQuint,
        animationDuration = 2000,
    } = {}) {

        super(...arguments);

        this.mode = mode;//line || wheel
        this.wheelRadius = wheelRadius;
        this.wheelVerticalFactor = wheelVerticalFactor;
        this.wheelDepthFactor = wheelDepthFactor;

        this.lineHorizontalOffset = lineHorizontalOffset;
        this.lineDepthOffset = lineDepthOffset;

        this.animationEasing = animationEasing;
        this.animationDuration = animationDuration;

        this.root = new Node();
        this.transform = this.root.transform;

        this.panels = panels;
        this.positions = []; //all positions memory array
        this.pointer = pointer;
        this.camera = camera;

        this.useMouse = false;
        this.useTouch = false;

        //interctivity indicator
        this.activePanels = [];

        //console.log("this.pointer", this.pointer);
        //pointer "pressed" behavior
        this.pointer.events.on("on", (val) => {
            //picking in the root through all children
            const result = this.root.transform.pick(this.camera, val.x, val.y, true);

            //we have an array of nothing
            if (result) {
                result.forEach((obj) => {
                    //grab the Mobilizing object associated to the Three object
                    const mobObject = obj.mobObject.uiObject;
                    //check if it's a Panel then register it
                    if (mobObject instanceof Panel) {
                        this.activePanels.push(mobObject);
                    }
                });
                //Results are depth sorted, so grab only the 1st one
                if (this.activePanels[0]) {
                    this.activePanels[0].active = true;
                    this.activePanels = [];
                }
            }
        });

        //interactivity preparation
        for (let i = 0; i < this.panels.length; i++) {
            //all panel are inactive at start
            const panel = this.panels[i];
            panel.active = false;

            //add childs is exists
            this.transform.addChild(panel.transform);

            //reset state on up anyway
            panel.events.on("up", () => {
                panel.active = false;
            })
        }

        //Position computation
        if (this.mode === MODE_WHEEL) {
            this.organizeWheel();
        }
        else if (this.mode === MODE_LINE) {
            this.organizeLine();
        }

    }

    /**
     * @private
     * Compute the local position of each Panel element according to the parameters given in the constructor.
     */
    organizeWheel() {
        //it's just like making a circle...
        this.wheelStep = 2 * Math.PI / this.panels.length;
        this.wheelTheta = 0;

        for (let i = 0; i < this.panels.length; i++) {
            //we begin at the center of the local world
            const position = new Vector3();
            //apply factors here for ellipses if values are given
            position.y = this.wheelVerticalFactor * (this.wheelRadius * Math.sin(this.wheelTheta));
            position.z = this.wheelDepthFactor * (this.wheelRadius * Math.cos(this.wheelTheta));

            this.wheelTheta += this.wheelStep;

            this.positions.push(position);
            this.panels[i].transform.setLocalPosition(position);
        }
    }

    /**
     * Make the PanelStack move to the given step (ie : the Panel index). Using Animation Object internally to automate the movements.
     * @param {Number} steps Use positive number to move forward, negative to move backward.
     */
    stepTo(steps) {

        if (this.mode === MODE_WHEEL) {
            this.stepToWheel(steps);
        }
        else if (this.mode === MODE_LINE) {
            this.stepToLine(steps);
        }

    }

    /**
     * @private
     * @param {Number} steps 
     */
    stepToWheel(steps) {
        this.wheelTheta -= steps * this.wheelStep;

        for (let i = 0; i < this.panels.length; i++) {

            const panel = this.panels[i];
            const position = this.positions[i];

            position.y = this.wheelVerticalFactor * (this.wheelRadius * Math.sin(this.wheelTheta));
            position.z = this.wheelDepthFactor * (this.wheelRadius * Math.cos(this.wheelTheta));

            this.wheelTheta += this.wheelStep;

            //construct the animation
            const anim = new Animation({
                "target": panel.transform.getLocalPosition(),
                "to": position,
                "duration": this.animationDuration,
                "easing": this.animationEasing,
                "onUpdate": function (target) {
                    panel.transform.setLocalPosition(target);
                },
                //fire a "ended" event
                "onFinish": function () {
                    this.events.trigger(EVT_TRANSLATE_ENDED);
                }
            });

            this.context.addComponent(anim);
            anim.setup();
            anim.on();

            anim.play();
        }
    }

    /**
     * @private
     * @param {Number} steps 
     */
    stepToLine(steps) {
        //clone positions
        const currentPositions = this.positions.slice();
        console.log("currentPositions", currentPositions);
        this.positions = [];

        let startIndex = steps;
        //negatives steps must start from the end!
        if (startIndex < 0) {
            startIndex = currentPositions.length + steps;
        }
        console.log("startIndex", startIndex);
        const indices = [];

        for (let i = 0; i < currentPositions.length; i++) {
            indices[i] = (startIndex + i) % currentPositions.length;
            this.positions[i] = currentPositions[indices[i]];
        }
        console.log(indices, this.positions);

        for (let i = 0; i < indices.length; i++) {

            const panel = this.panels[i];
            const position = this.positions[i];

            //construct the animation
            const anim = new Animation({
                "target": panel.transform.getLocalPosition(),
                "to": position,
                "duration": this.animationDuration,
                "easing": this.animationEasing,
                "onUpdate": function (target) {
                    panel.transform.setLocalPosition(target);
                }
            });

            this.context.addComponent(anim);
            anim.setup();
            anim.on();

            anim.play();
        }
    }

    /**
     * @private
     * 
     */
    organizeLine() {

        for (let i = 0; i < this.panels.length; i++) {

            //we begin at the center of the world
            const position = new Vector3();
            position.x = i * this.lineHorizontalOffset;
            position.z = -i * this.lineDepthOffset;

            this.positions.push(position);
            this.panels[i].transform.setLocalPosition(position);
        }
    }

}