js/Mobilizing/renderer/3D/three/ui/Button.js
import Mesh from "../shape/Mesh";
import Node from "../shape/3D/primitive/Node";
import Color from "../types/Color";
import Vector2 from "../types/Vector2";
import Component from "../../../../core/Component";
import Clickable from "./Clickable";
import * as _Math from "../../../../core/util/Math";
import Texture from "../texture/Texture";
import RichText from "../../../../text/RichText";
import Transform from "../scene/Transform";
import { noop } from "../../../../core/util/Misc";
const BT_STATE_PRESSED = "pressed";
const BT_STATE_RELEASED = "released";
export default class Button extends Component {
/**
* A Button is a special kind of 3D object that is clickable and that you can use to build Graphical User Interfaces (GUI).
*
* @param {Object} params Parameters object, given by the constructor.
* @param {Camera} params.camera the camera used for picking.
* @param {String} params.text text to render (can be empty).
* @param {Number} params.textSize the text size
* @param {Object} params.font font to use.
* @param {Number} params.width width in pixels.
* @param {Number} params.height height in pixels.
* @param {Number} params.canvasWidth canvasWidth in pixels.
* @param {Number} params.canvasHeight canvasHeight in pixels.
* @param {Number} params.cutOff the size of the cutOff
* @param {Color} params.strokeColor
* @param {Color} params.fillColor
* @param {Color} params.pressFillColor
* @param {Color} params.hoverFillColor
* @param {Color} params.textColor
* @param {Function} [params.onPress]
* @param {Function} [params.onRelease]
* @param {Function} [params.onEnter]
* @param {Function} [params.onLeave]
* @example
* //TODO
*/
constructor({
camera = undefined,//required
pointer = undefined,
width = 3,
height = 1,
canvasWidth = 200,
canvasHeight = undefined,
radius = undefined,
sideCount = 6,
cutOff = undefined,
strokeWidth = 0.1,
strokeColor = Color.mobilizing.clone(),
fillColor = Color.white.clone(),
pressFillColor = Color.mobilizing.clone(),
hoverFillColor = Color.mobilizingAlternate.clone(),
text = undefined,
textSize = 10,
textColor = Color.mobilizing.clone(),
font = undefined,
onPress = noop,
onRelease = noop,
onEnter = noop,
onLeave = noop
} = {}) {
super(...arguments);
this.camera = camera;
this.pointer = pointer;
this.width = width;
this.height = height;
this.canvasWidth = canvasWidth;
this.canvasHeight = (canvasHeight !== undefined) ? canvasHeight : 200 / (this.width / this.height);
this.radius = radius;
this.sideCount = sideCount;
this.cutOff = (cutOff !== undefined) ? cutOff : this.height / 3;
this.strokeWidth = strokeWidth;
this.strokeColor = strokeColor;
this.fillColor = fillColor;
this.pressFillColor = pressFillColor;
this.hoverFillColor = hoverFillColor;
this.text = text;
this.textSize = textSize;
this.textColor = textColor;
this.font = font;
this.onPress = onPress;
this.onRelease = onRelease;
this.onEnter = onEnter;
this.onLeave = onLeave;
if (this.radius) {
const twoSquareThree = 2 / Math.sqrt(3);
this.canvasHeight = this.canvasWidth * twoSquareThree;
}
//main node
this.root = new Node();
this.transform = this.root.transform;
//to get the state
this.state = BT_STATE_RELEASED;
//create the texture from text
if (this.text) {
//label normal state
const richText = new RichText({
"width": this.canvasWidth,
"height": this.canvasHeight,
"text": this.text,
"marginTop": this.canvasHeight / 2 - this.textSize / 2,
"backgroundColor": Color.transparent.makeRGBAStringWithAlpha(0),
"textColor": textColor.getStyle(),
"textAlign": "center",
"textSize": this.textSize,
"font": this.font,
"fontItalic": this.fontItalic,
"fontBold": this.fontBold,
"fontBoldItalic": this.fontBoldItalic
});
//console.log(richText);
//label pressed state
const pressRichText = new RichText({
"width": this.canvasWidth,
"height": this.canvasHeight,
"text": this.text,
"marginTop": this.canvasHeight / 2 - this.textSize / 2,
"backgroundColor": Color.transparent.makeRGBAStringWithAlpha(0),
"textColor": Color.white.getStyle(),
"textAlign": "center",
"textSize": this.textSize,
"font": this.font,
"fontItalic": this.fontItalic,
"fontBold": this.fontBold,
"fontBoldItalic": this.fontBoldItalic
});
this.textTexture = new Texture({ "canvas": richText.getCanvas() });
this.pressTextTexture = new Texture({ "canvas": pressRichText.getCanvas() });
/* document.body.appendChild(richText.getCanvas());
richText.getCanvas().style.position = "absolute";
richText.getCanvas().style.top = "0px";
richText.getCanvas().style.right = "0px"; */
}
//construct the default mesh & stroke
this.generateDefaultMesh();
this.root.transform.addChild(this.mesh.transform);
}
setup() {
super.setup();
this.clickable = new Clickable({
"camera": this.camera,
"mesh": this.mesh,
"pointer": this.pointer,
"onPress": () => {
if (this.active) {
this.mesh.material.setColor(this.pressFillColor);
if (this.pressTextTexture) {
this.texturedMesh.material.setTexture(this.pressTextTexture);
}
this.state = BT_STATE_PRESSED;
this.onPress();
}
},
"onRelease": () => {
if (this.active) {
if (this.pointer.lastActivePointer.type === "mouse") {
this.mesh.material.setColor(this.hoverFillColor);
}
else if (this.pointer.lastActivePointer.type === "touch") {
this.mesh.material.setColor(this.fillColor);
}
if (this.textTexture) {
this.texturedMesh.material.setTexture(this.textTexture);
}
this.state = BT_STATE_RELEASED;
this.onRelease();
}
},
"onEnter": () => {
if (this.active) {
this.mesh.material.setColor(this.hoverFillColor);
this.onEnter();
}
},
"onLeave": () => {
if (this.active) {
this.mesh.material.setColor(this.fillColor);
if (this.textTexture) {
this.texturedMesh.material.setTexture(this.textTexture);
}
this.onLeave();
}
}
});
this.context.addComponent(this.clickable);
this.clickable.setup();
this.clickable.on();
this.on();
}
/**
* Activate the button
* @method on
*/
on() {
super.on();
this.mesh.material.setOpacity(1);
this.strokeMesh.material.setOpacity(1);
if (this.text) {
this.texturedMesh.material.setOpacity(1);
}
}
/**
* deactivate the button, set its opacity to 30 %
* @method off
*/
off() {
super.off();
this.mesh.material.setOpacity(0.3);
this.strokeMesh.material.setOpacity(0.3);
if (this.text) {
this.texturedMesh.material.setOpacity(0.3);
}
}
/*
update()
{
}
*/
/**
* Generate the vertices and meshes for the default button. Called internally only
* @private
* @method generateDefaultMesh
*/
generateDefaultMesh() {
//vertices
const w = this.width / 2;
const h = this.height / 2;
//this.cutOff = h/2;
//let cutOffXOffset = 0;
this.vertex = [];
//manage special case of squared size : will be an hexagon!
if (this.radius) {
const parts = this.sideCount;
const radius = this.radius;
for (let i = 0; i < parts; i++) {
//arc(a, b, c, d, start, stop, mode)
const x = Math.cos(_Math.degToRad(360 / parts / 2) + (Math.PI * 2 / parts) * i) * radius;
const y = Math.sin(_Math.degToRad(360 / parts / 2) + (Math.PI * 2 / parts) * i) * radius;
this.vertex.push(new Vector2(x, y));
}
}
else {
this.vertex.push(new Vector2(-w + this.cutOff, h));
this.vertex.push(new Vector2(w - this.cutOff, h));
this.vertex.push(new Vector2(w, h - this.cutOff));
this.vertex.push(new Vector2(w, -h + this.cutOff));
this.vertex.push(new Vector2(w - this.cutOff, -h));
this.vertex.push(new Vector2(-w + this.cutOff, -h));
this.vertex.push(new Vector2(-w, -h + this.cutOff));
this.vertex.push(new Vector2(-w, h - this.cutOff));
}
//console.log(this.vertex);
this.topLeftIndex = 0;
this.topRightIndex = 1;
this.bottomRightIndex = 4;
this.bottomLeftIndex = 5;
this.mesh = new Mesh(/* {material : "basic"} */);
this.mesh.generateFillMesh(this.vertex);
this.mesh.transform = new Transform(this.mesh);
this.mesh.material.setTransparent(true);
//console.log(this.mesh, this.mesh.getBoundingBox());
if (this.text) {
this.texturedMesh = new Mesh();
this.texturedMesh.generateFillMesh(this.vertex);
this.texturedMesh.transform = new Transform(this.texturedMesh);
this.texturedMesh.material.setTransparent(true);
this.texturedMesh.material.setTexture(this.textTexture);
this.root.transform.addChild(this.texturedMesh.transform);
}
this.strokeMesh = Mesh.generateStrokeMesh(this.mesh, this.strokeWidth);
this.strokeMesh.material.setColor(this.strokeColor);
this.strokeMesh.material.setTransparent(true);
this.root.transform.addChild(this.strokeMesh.transform);
}
/**
* Adapt a corner of the shape, for grouping buttons together
* @method adaptCorner
* @param {String} mode "cutOff" || "straight"
* @param {String} corner "topLeft" || "topRight" || "bottomRight" || "bottomLeft"
*/
adaptCorner(mode, corner) {
if (this.width !== this.height) {
let vertex = null;
switch (corner) {
case "topLeft":
vertex = this.mesh.geometry.vertices[this.topLeftIndex];
if (mode === "straight") {
vertex.x = -this.width / 2;
}
else if (mode === "cutOff") {
vertex.x = -this.width / 2 + this.cutOff;
}
this.mesh.updateMesh();
this.regenerateStrokeGeometry(this.mesh);
break;
case "topRight":
vertex = this.mesh.geometry.vertices[this.topRightIndex];
if (mode === "straight") {
vertex.x = this.width / 2;
}
else if (mode === "cutOff") {
vertex.x = this.width / 2 - this.cutOff;
}
this.mesh.updateMesh();
this.regenerateStrokeGeometry(this.mesh);
break;
case "bottomRight":
vertex = this.mesh.geometry.vertices[this.bottomRightIndex];
if (mode === "straight") {
vertex.x = this.width / 2;
}
else if (mode === "cutOff") {
vertex.x = this.width / 2 - this.cutOff;
}
this.mesh.updateMesh();
this.regenerateStrokeGeometry(this.mesh);
break;
case "bottomLeft":
vertex = this.mesh.geometry.vertices[this.bottomLeftIndex];
if (mode === "straight") {
vertex.x = -this.width / 2;
}
else if (mode === "cutOff") {
vertex.x = -this.width / 2 + this.cutOff;
}
this.mesh.updateMesh();
this.regenerateStrokeGeometry(this.mesh);
break;
default:
vertex = this.mesh.geometry.vertices[this.topLeftIndex];
if (mode === "straight") {
vertex.x = -this.width / 2;
}
else if (mode === "cutOff") {
vertex.x = -this.width / 2 + this.cutOff;
}
this.mesh.updateMesh();
this.regenerateStrokeGeometry(this.mesh);
break;
}
this.mesh.generateFlatUVs();
}
}
/**
* regenerate the geometry of the mesh for further update
* @method regenerateStrokeGeometry
* @private
* @param {Mesh} the mesh to regenerate the stroke for
*/
regenerateStrokeGeometry(mesh) {
this.strokeMesh.updateStroke(mesh, this.strokeWidth);
}
/**
* Can be used to simulate a pressed event when necessary (i.e. when a keyboard event should modify the button state).
*
* @method fakePress
*/
fakePress() {
this.fakePressed = true;
}
/*update()
{
}*/
}