Repository

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()
    {
    }*/

}