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);
}
}
}