Git Product home page Git Product logo

canvasui's Introduction

CanvasUI

A Three.JS WebXR UI. Enabling easy UI creation for immersive-vr sessions

Image

Use CanvasUI

Get Started

CanvasUI tutorial (youtube)

Image

ONLINE DEMO

First, make sure to import CanvasUI

import { CanvasUI } from '../../jsm/CanvasUI.js'

Your file structure needs both three.module.js and CanvasKeyboard.js to be accessible. The repo has the file structure like the Three.js library with the Three.js build loaded from CDN, and extra content in the examples folder. The CanvasUI examples are in the examples/CanvasUI. The class files are in examples/jsm.

To create a simple text panel use :

const ui = new CanvasUI(  );
ui.mesh.position.set(0, -0.5, -1);
ui.updateElement("body", "Hello World" );

scene.add(ui.mesh);

A CanvasUI mesh is simply a plane that is 1 x 1 units. In a VR world this means it is 1 metre square. It has a CanvasTexture applied, by default this is 512 pixels square. An Arial font is applied and the size of the font is 30 pixels. There is 20 pixels of padding. The font color is white and the background color is black and the canvas will have 6 pixel radius rounded corners.

Contributing

Found a bug ? Want to contribute ? Do not hesitate to open a ticket !

Please git clone or download zip file from the repo.

Use devsrv npm module as a webserver is welcome, explanations below.

In VSCode, you can use Live Server addon but only for testing generated builds, or, work without devsrv string replacement features.

devsrv (nodejs module)...

... as a webserver

devsrv is a custom self signed HTTPS (required for webxr) development webserver, which monitor file changes for hot reload and handle dynamic string replacement in HTML/JS/CSS files. So you can play with different Three.js versions without spending time to replace path/url yourself. We don't use webpack atm.

Prerequies: you need to have nodejs installed on your computer.

After cloning or unzip the repo, install CanvasUI dependencies.

$ npm install

It will install devsrv npm module in a node_modules/ directory.

Then you can start the webserver.

$ npm start

or

$ node node_modules/@eviltik/devsrv/bin/devsrv.js

You can find all interesting devsrv command line options here

devsrv will search for and will use the first private IP Address found on your computer (i.e wifi or network card ip addr). After starting the server, you can use Google Chrome with WebXR addon, or simply your headset browser (i.e Oculus Browser for Oculus Quest, or Firefox Reality)

... as a static file builder

In HTML/JS CanvasUI lib and examples files, you can see this:

import * as THREE from 'https://cdn.skypack.dev/three@THREEJSVERSION';

THREEJSVERSION is a string dynamically replaced by devsrv. The value to replace with is defined in the configuration file devsrv.config.json. Thanks to devsrv dynamic string replacement feature, you can change the value directly in the URL, by adding "?r=0.135" in the URL, where 0.135 is the release number of threejs CDN side. If you use devsrv dynamic string replacement feature, you can NOT copy directly HTML files from /eaxmples directory, you need to generate static files. devsrv will take this point too.

To trigger a CanvasUI build :

$ npm build

or

$ node node_modules/@eviltik/devsrv/bin/devsrv.js -b

Build options are defined in devsrv.config.json

"buildOptions": {
    "src":"./examples",
    "dst":"./dist/v1.0.0/threejs.119",
    "replaceRegexp":"THREEJSVERSION",
    "defaultValue":"0.119",
    "fileRegexp":"\\.(html|js)$"
}

Then you can use devsrv (or any https webserver, vscode live server) again to serve the generated files :

$ node node_modules/@eviltik/devsrv/bin/devsrv.js -d ./dist/v1.0.0/threejs.119

Examples

Panel : Header, Main, Footer

Image

ONLINE DEMO

In general CanvasUI is designed to have a content object and a config object. Let's try an example with multiple elements.

const config = {
	header:{
		type: "text",
		position:{ top:0 },
		paddingTop: 30,
		height: 70
	},
	main:{
		type: "text",
		position:{ top:70 },
		height: 372, // default height is 512 so this is 512 - header height (70) - footer height (70)
		backgroundColor: "#bbb",
		fontColor: "#000"
	},
	footer:{
		type: "text",
		position:{ bottom:0 },
		paddingTop: 30,
		height: 70
	}
}

const content = {
	header: "Header",
	main: "This is the main text",
	footer: "Footer"
}

const ui = new CanvasUI( content, config );

Each element has a section in the config and the content objects. Notice that these are all of type text. We can set the position using x, y, left, top, right and bottom. The values are in relation to a default texture that is 512 pixels square. Colors are defined like css values. If an attribute is missing it will be inherited from the body which has these defaults.

defaultconfig = {
	panelSize: { width: 1, height: 1},
	width: 512,
	height: 512,
	opacity: 0.7,
	body:{
		fontFamily:'Arial', 
		fontSize:30, 
		padding:20, 
		backgroundColor: '#000', 
		fontColor:'#fff', 
		borderRadius: 6
	}
}
Panel : Custom Shaped Panels with a custom font

Image

DEMO

The default shape for a UI panel is a square or rounded rectangle. But you can define a shape using an svg path. Limitless possibilities.

const css = {
	body: {
        clipPath: "M 258.3888 5.4432 C 126.9744 5.4432 20.4432 81.8424 20.4432 164.4624 C 20.4432 229.1976 86.3448 284.2128 178.1016 304.8192 C 183.5448 357.696 173.2416 444.204 146.8032 476.6688 C 186.6552 431.9568 229.2288 356.5296 244.7808 313.3728 C 249.252 313.3728 253.9176 313.7616 258.3888 313.7616 C 389.8032 313.7616 496.14 246.888 496.14 164.4624 S 389.8032 5.4432 258.3888 5.4432 Z",
        backgroundColor: "#ddd",
        fontColor: "#000",
        fontFamily: "Gochi Hand"
    },
	speech: {
        type: "text",
        position: { left: 50, top: 80 },
        fontSize: 45,
        fontColor: "#000",
        width: 400,
        height: 250
    }
}

const content = {
	speech: "A custom shaped panel. How about that?"
}

const ui = new CanvasUI( content, css );

You can generate the code for a path using this online tool.

Custom FontS

Notice that this example uses a custom Google Font. But you can NOT just load it in the index.html page simply by using <link href="https://fonts.googleapis.com/css2?family=Gochi+Hand&display=swap" rel="stylesheet">. โ—

See stackoverflow ๐Ÿ‘€ to see possible circumventions.

In this example, we use FontFace API circumvention, waiting for the font to be loaded before initializing the application.

We need the font URL. Opening https://fonts.googleapis.com/css2?family=Gochi+Hand&display=swap give this :

/* latin */
@font-face {
  font-family: 'Gochi Hand';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/gochihand/v14/hES06XlsOjtJsgCkx1Pkfon_-w.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;

Let's use the woff2 URL from the src attribute as parameters of the FontFace API.

<script type="module">
    import { App } from './app.js';

    document.addEventListener("DOMContentLoaded", function(){

        let myFont = new FontFace(
            "Gochi Hand",
            "url(https://fonts.gstatic.com/s/gochihand/v14/hES06XlsOjtJsgCkx1Pkfon_-w.woff2)"
        );

        myFont.load().then(font => {

            document.fonts.add(font);

            const app = new App();
            window.app = app;

        });
    });
</script>

So, Yes ! All web fonts will work with CanvasUI.

Buttons

Image

ONLINE DEMO

So far we've only added text elements to the UI. In this example we'll add buttons. Buttons work by detecting the movement and trigger button on your VR controllers. To ensure that they work correctly, add this to the renderers animation loop.

ui.update();

The CanvasUI class detects your controllers but does not display them. To ensure a visual representation of the controllers. Use the usual code. For example.

const controllerModelFactory = new XRControllerModelFactory();

// left controller
controller = renderer.xr.getController( 0 );
scene.add( controller );
		
controllerGrip = renderer.xr.getControllerGrip( 0 );
controllerGrip.add( controllerModelFactory.createControllerModel( controllerGrip ) );
scene.add( controllerGrip );

// right controller
controller1 = renderer.xr.getController( 1 );
scene.add( controller1 );

controllerGrip1 = renderer.xr.getControllerGrip( 1 );
controllerGrip1.add( controllerModelFactory.createControllerModel( controllerGrip1 ) );
scene.add( controllerGrip1 );

// line geometry
const geometry = new THREE.BufferGeometry().setFromPoints( [ new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 0, -1 ) ] );

const line = new THREE.Line( geometry );
line.name = 'line';
line.scale.z = 10;

controller.add( line.clone() );
controller1.add( line.clone() );

Here is a simple example of using buttons :

//Make sure ui is a global scope variable
let ui;

function onPrev(){
	const msg = "Prev pressed";
	console.log(msg);
	ui.updateElement( "info", msg );
}

function onStop(){
	const msg = "Stop pressed";
	console.log(msg);
	ui.updateElement( "info", msg );
}

function onNext(){
	const msg = "Next pressed";
	console.log(msg);
	ui.updateElement( "info", msg );
}

function onContinue(){
	const msg = "Continue pressed";
	console.log(msg);
	ui.updateElement( "info", msg );
}

const config = {
	panelSize: {
        width: 2,
        height: 0.5
    },
	height: 128,
	info: {
        type: "text",
        position:{ left: 6, top: 6 },
        width: 500,
        height: 58,
        backgroundColor: "#aaa",
        fontColor: "#000"
    },
	prev: {
        type: "button",
        position:{ top: 64, left: 0 }, 
        width: 64,
        fontColor: "#bb0",
        hover: "#ff0",
        onSelect: onPrev
    },
	stop: {
        type: "button",
        position:{ top: 64, left: 64 },
        width: 64,
        fontColor: "#bb0",
        hover: "#ff0",
        onSelect: onStop
    },
	next: {
        type: "button",
        position:{ top: 64, left: 128 },
        width: 64,
        fontColor: "#bb0",
        hover: "#ff0",
        onSelect: onNext
    },
	continue: {
        type: "button",
        position:{ top: 70, right: 10 },
        width: 200,
        height: 52,
        fontColor: "#fff",
        backgroundColor: "#1bf",
        hover: "#3df",
        onSelect: onContinue
    },
	renderer
}

const content = {
	info: "",
	prev: "<path>M 10 32 L 54 10 L 54 54 Z</path>",
	stop: "<path>M 50 15 L 15 15 L 15 50 L 50 50 Z<path>",
	next: "<path>M 54 32 L 10 10 L 10 54 Z</path>",
	continue: "Continue"
}

ui = new CanvasUI( content, config );

We define the panelSize, which is in world units, metres for a VR session. We also set the panel height to 128 pixels. Width will default to 512. The width and height must be powers of 2.

โ— Panelsize can be anything but if the aspect ratio of panelSize.width/panelSize.height is different to width/height then the text will be stretched.

If the content of a button starts with <path> then an svg path will be filled with the backgroundColor.

Notice also in the config for a button the hover attribute is the background color to use when the controller ray is over the button.

When buttons are used always pass the renderer in the config object. This ensures that CanvasUI can add events to the controllers.

Scrolling

Image

ONLINE DEMO

If a section of text cannot fit in the space available then you can allow it to scroll. Because this needs to use VR controllers, you need to follow the same rules as for buttons.

  1. Add renderer to the config object passed to the CanvasUI constructor.
  2. Create a visualisation of the xr controllers in the usual way. See the buttons section for an example.
  3. Add ui.update() to the render loop.

The most important attribute of a text type, to enable scrolling, is to use overflow: scroll. The default is to simply hide the text that is beyond the limits of the text element area. If you want to see a blue dot where the controller ray hits the scrolling canvas, then also pass scene in the config object.

const config = {
	renderer,
	scene,
	body: {
        backgroundColor: "#666"
    },
	txt: {
        type: "text",
        overflow: "scroll",
        position: { left: 20, top: 20 },
        width: 460,
        height: 400,
        backgroundColor: "#fff",
        fontColor: "#000"
    }
}

const content = {
	txt: "This is an example of a scrolling panel. Select it with a controller and move the controller while keeping the select button pressed. In an AR app just press and drag. If a panel is set to scroll and the overflow setting is 'scroll', then a scroll bar will appear when the panel is active. But to scroll you can just drag anywhere on the panel. This is an example of a scrolling panel. Select it with a controller and move the controller while keeping the select button pressed. In an AR app just press and drag. If a panel is set to scroll and the overflow setting is 'scroll', then a scroll bar will appear when the panel is active. But to scroll you can just drag anywhere on the panel."
}

ui = new CanvasUI( content, config );
Images

Image

ONLINE DEMO

You can display an image on the UI panel using CanvasUI. It is easily added using a img type.

const config = {
	image: {
        type: "img",
        position: { left: 20, top: 20 },
        width: 472
    },
	info: {
        type: "text",
        position: { top: 300 }
    }
}

const content = {
	image: "../../assets/promo.jpg",
	info: "The promo image from the course: Learn to create WebXR, VR and AR, experiences using Three.JS"
}

ui = new CanvasUI( content, config );

Notice that the config defines the positioning and size. Here only the width is specified, the height will be calculated from the image, to maintain the correct aspect ratio. If you want the image stretched then directly entering a height value will force this. The content for the image is the path to the image, either an absolute path or relative. The element name in the config and the content can be anything you choose, it does not have to be 'image'

Input text

Image

DEMO

CanvasUI supports a dynamic keyboard. Specify a config type as input-text and on select a keyboard will be shown. In common with any button type it has the usual requirements.

  1. Add renderer to the config object passed to the CanvasUI constructor.
  2. Create a visualisation of the xr controllers in the usual way. See the buttons section for an example.
  3. Add ui.update() to the render loop.

Input text has callbacks for onChanged and onEnter. Here is an example.

function onChanged( txt ){
	console.log( `message changed: ${txt}`);
}

function onEnter( txt ){
	console.log(`message enter: ${txt}`);
}

const config = {
	renderer: this.renderer,
	panelSize: { width: 1.6, height: 0.4 },
	height: 128,
	message: {
        type: "input-text",
        position: { left: 10, top: 8 },
        height: 56,
        width: 492,
        backgroundColor: "#ccc",
        fontColor: "#000",
        onChanged,
        onEnter
    },
	label: {
        type: "text",
        position: { top: 64 }
    }
}

const content = {
	message: "",
	label: "Select the panel above."
}

ui = new CanvasUI( content, config );
Color Picker

![Image](./examples/assets/canvasui-colorpicker.png) > [DEMO](https://niksgames.com/webxr/dev/CanvasUI/colorPicker/)

function onChange(hex) {
    console.log(`Picker color changed to (${hex})`);
    myMesh.material.color.set( hex );
}

const config = {
    picker: {
        type: "picker",
        position: { left: 20, top: 20 },
        width:472,
        height:422,
        onChange
    },
    info: {
        type: "text",
        position: { left: 20, top: 452 },
        fontSize:18
    },
    opacity: 1
}
        
const content = {
    picker: "#00ff00",
    info: "Use the color picker to add an option to select a color"
}

this.ui = new CanvasUI( content, config );

Links

canvasui's People

Contributors

eviltik avatar niklever avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

canvasui's Issues

add build process

$ npm run build
> [email protected] build
> node node_modules/@eviltik/devsrv/bin/devsrv.js -b && node node_modules/rollup/dist/bin/rollup -c ./rollup.config.js

Sat, 05 Mar 2022 11:57:59 GMT devsrv - info: use config file ..GIT\CanvasUI\devsrv.config.js
Sat, 05 Mar 2022 11:57:59 GMT devsrv - info: build success

C:\Users\T\Desktop\DATA\DEV\GIT\CanvasUI\dist\examples\jsm_src\CanvasUI.js => dist/examples/js/CanvasUI.js...
created dist/examples/js/CanvasUI.js in 929ms

C:\Users\T\Desktop\DATA\DEV\GIT\CanvasUI\dist\examples\jsm_src\VRButton.js => dist/examples/js/VRButton.js...
created dist/examples/js/VRButton.js in 55ms

C:\Users\T\Desktop\DATA\DEV\GIT\CanvasUI\dist\examples\jsm_src\CanvasUI.js => dist/examples/jsm/CanvasUI.js...
created dist/examples/jsm/CanvasUI.js in 200ms

C:\Users\T\Desktop\DATA\DEV\GIT\CanvasUI\dist\examples\jsm_src\VRButton.js => dist/examples/jsm/VRButton.js...
created dist/examples/jsm/VRButton.js in 35ms

should generate dist/

image

including both concatened jsm and js files, like threejs.

Warning, threejs version will be replaced by the defaultValue from ./devsrv.config.js in buildOptions config.

fix window scrollbar issue

In all examples, we have visible scrollbar.

To fix that i purpose to add a common CSS file for all examples

<link rel="stylesheet" href="../../css/reset.css"></link>

having this

body{
    margin: 0;
}

canvas {
    display: block; /* fix necessary to remove space at bottom of canvas */
}

No more scrollbars

fix raycaster overlap (keyboard example)

When several meshes overlap (example: keyboard expanded in front of another panel), each mesh has its own event handler, and its own raycasters.

A click/hover/select on 2 superimposed buttons will cause 2 events instead of one (the first button hiding the second because of its offset on the Z axis).

To improve this behavior and take into account the Z order of multiple CanvasUI components, 2 ideas should be explored:

  • make an intersectList object common?
  • raycaster manager?

don't work?

hi~ i'm starting learning three.js and my English is poor, /(ใ„’oใ„’)/~~ i hope you can understand my question.
your work is great,and i'm trying to use it, and i click your online version but it seemed don't work,
the result is that:
why
so i download your code but the result is the same,and it shows that
tt
how to solve them~~~thank you so much

canvasUI refactor

Currently, we are creating Canvas UI widgets by instantiating a Canvas class.

Just thinking to tend to something like datGUIXR.

// only one CanvasUI instance, so we can shared data between widgets (raycaster, xr controllers events ..)
const canvasUI = new CanvasUI();

// then we can add widgets like this
const panelLeft = canvasUI.add(contentPanelLeft, configPanelLeft);
const panelRight = canvasUI.add(contentPanelRight, configPanelRight);

// open the door to new usefull methods ?
panelLeft.hide(); // show() // destroy // move // rotate // setModal // moveForward // moveBackward ..

// globals methods ?
canvasUI.hideAll(); // showAll // destroyAll // enable/visible ..

Fondation for a descent DatGUIXR ?

Hi @NikLever,

Congratulations ! this is very clean. I'm a fan ๐Ÿฅ‡

There is no serious working datui/datguivr on both browser and webxr context today. Something more customizable (styles). Something like UI widgets suite, to make menus, drop down, sliders, color pickers ...)

I plan to make an aframe component on top of canvasui. But not sure. I want to work on some 3D UI widgets. I started with an html2canvas wrapper (aframe-htmlembed-component) , giving some nice results (demo) which is working great with latest aframe version (1.3.0), so latest threejs too.

aframe-htmlembed-component does not support scrolling and i didn't try to use it through a web worker yet.
CanvasUI support scrolling area. Nice shot.

Perhaps we can have really nice (static) effect mixing SVG+CSS (i have to make some tests).

What do you think about adding some features/helpers into canvasUI ?
So we can seriously think about a working DatGUIXR. What is missing ?

  • #19
  • a color picker (what about an image ? is it possible to read the color of the texture below the raycaster target ?)
  • a slider (input range, a la datui, why not)
  • working with mouse cursor too (webxr controllers, chrome addon or not, is mandatory to see demos for now)
  • a drop down (with scroll, like an html select component, with onclick event on items)
  • sliding panels
  • add an API to have the same logic as our old datui syntax

Please add the licence you want, i'd like to fork the repo and start playing with this new toy !

add mouseController

Parameter:

  • Behaviors:

    • follow: will follow the mouse without moving camera
  • Reactivity

    • slow: for button/keyboard behavior
    • fast: for colorpicker/slider behavior
    • norepeat: for unique mouse click event

add: left menu examples

Let create a dynamic left menu in js/css so we have a way to quickly switch between examples. Like threejs examples left menu without pictures (puppeteer is a way to automate screenshot and make thumbail, but it should be an improvement for later).

Examples will be defined in the js file, something like this ;

const exampleList = [
{
    title: 'Buttons - Panel',
    url:'../buttons/'
}

add (draft): console element

CanvasUI Console: console.log wrapper with info/warn/error/debug. Of course limited to text.

  • overflow scroll
  • automatic scroll behavior always at the bottom except if autoscroll stopped because onselect
  • limited to N lines

change: examples common code

Plenty of code in examples can be stored in one js class witch can be extended :

App:

  • constructor
  • initScene
  • setupXR
  • resize
  • render

Final code example:

import { Environnement } from '../Environnement.js';
import { CanvasUI } from '../../jsm/CanvasUI.js';

class App extends Environnement {
    constructor() {
        super();
    }

    createUI() {
        ...
    }

}

The Environnement class will have little methods like setupRenderer, setupCamera, so we can override methods in the main class easily.

@NikLever will you accept this change ?

not working properly with OrthographicCamera?

I am trying to use a isometric camera with CanvasUI for the UI elements but i cannot set the ui closer to the camera.
Say for example we have this code:

    const d = 60;
    var width = window.innerWidth;
    var height = window.innerHeight;
    var aspect = width / height;
// add camera
    const camera = new THREE.OrthographicCamera(
      -d * aspect,
      d * aspect,
      d,
      -d,
      1,
      1000
    );
   
    camera.position.set(22, 22, 22);
    ui = new CanvasUI(
      {
        header: "WHY",
        main: "This is the main text",
        footer: "Footer",
      },
      {
        header: {
          type: "text",
          position: { top: 0 },
          paddingTop: 10,
          height: 70,
        },
        main: {
          type: "text",
          position: { top: 70 },
          height: 372, // default height is 512 so this is 512 - header height:70 - footer height:70
          backgroundColor: "#bbb",
          fontColor: "#000",
        },
        footer: {
          type: "text",
          position: { bottom: 0 },
          paddingTop: 30,
          height: 70,
        },
      }
    );

    camera.attach(ui.mesh);
    ui.mesh.position.set(0, 0, -99); // it does not matter how close or far i set the camera position, it won't change at all. (Z)
    ui.add(uiRef.current.mesh);

Pretty sure that i must be doing something wrong, but is what i see all the time:
imagen
Just a small rectangle.

Is there a way to use this framework with OrthographicCamera?

change (draft): CSS logic refactor

( just an idea, a discussion, looking for feedbacks )

First, we currently set positions, dimensions, font, colors, for each elements using attributes, but mixed with methods in the same object.
Same thing for the config object which is a mix of different properties with differents usage (i.e element definition vs renderer, or panelSize vs an element name). This currently involves some filters when looping over object attributes in CanvasUI.

Let's take the current buttons example.

const config = {
    panelSize: { width: 2, height: 0.5 },
    height: 128,
    info: { type: "text", position:{ left: 6, top: 6 }, width: 500, height: 58, backgroundColor: "#aaa", fontColor: "#000" },
    prev: { type: "button", position:{ top: 64, left: 0 }, width: 64, fontColor: "#bb0", hover: "#ff0", onSelect: onPrev },
    stop: { type: "button", position:{ top: 64, left: 64 }, width: 64, fontColor: "#bb0", hover: "#ff0", onSelect: onStop },
    next: { type: "button", position:{ top: 64, left: 128 }, width: 64, fontColor: "#bb0", hover: "#ff0", onSelect: onNext },
    continue: { type: "button", position:{ top: 70, right: 10 }, width: 200, height: 52, fontColor: "#fff", backgroundColor: "#2659a4", hover: "#3df", onSelect: onContinue },
    renderer: this.renderer
}

Below an imaginary refactor, a CSS file and a different CanvasUI setup.

CSS entries. element name are IDs. But we can use classes too. Some littles changes i.e fontColor become color to match css rules.

.button {
    color:#bb0;
    width:64px;
    top:64px;
    font-familly:"...";
    background-color:#000099;
    transition: background-color 0.2s;
}

.button:hover {
    background-color:#ff0;
    transition: background-color 1s;
}

#info {
    left:10px,
    top:10px,
    width:492px,
    height:50px,
    background-color:#aaa;
    color:#000
}

#continue {
    top:70px;
    right:70px;
    width:200px;
    height:50px;
    color:#fff;
}

#continue:hover {
    color:#0000ff;
}

.red {
    background-color:red;
}

Only elements declaration, rather than a mixed object.
Note the "style" attribute to override CSS definitions.
Note the "class" attribute to link custom css properties.
Both "style" and "class" should be like HTML attributes, but parsed by CanvasUI, with properties propagation when object is created.

const elements = {
    info: { type: 'text', value:'' },
    prev: { type: 'button', value:'...', style:{ left: 0 }, onSelect: onPrev },
    stop: { type: 'button', value:'...', style:{ left: 64 }, class:'red', onSelect: onStop },
    next: { type: 'button', value:'...', style:{ left: 128 }, onSelect: onNext },
    continue: { type: 'button', value:'Continue', onSelect: onContinue },
};

Everything which is not an element attribute should be moved to the config object

const config = {
    panelSize: { width:2, height:0.5 } ,
    height: 128,
    threejs:{
        renderer: this.renderer, // mandatory for XR
        camera: this.camera, // mandatory when mouse controller used
        scene: this.scene // mandatory for scrolling example, needed to add raycaster pointer when select
    }
    // this is defaults
    controller:{
        devices:{
            mouse:{ enable:true, handler:'follow' },
            xr:{ enable:true, raycasterLinesLength:10 }
        },
        select:{
            hover: false, // select once (keyboard behavior), 
            // hover:true, // trigger select event while raycaster is moving (select+hover) on the element (colorpicker or slider behavior)
            repeater:{
                // when hover=false, we have a repeater option, delayed trigger select event + repeat. 
                // usefull to handle "delete" keyboard key, select will repeat until you selectend
                timeoutms:100, // 0 mean immediate, 
                repeatms:20
            }
        }
    },
    
}

new CanvasUI( elements , config );

Benefits:

  • separate UI properties from other settings
  • a simple css file can define the look of multiples elements
  • realtime editing via chrome console ?
  • a step to be "a-frame same logic"
  • explorations: custom css attribute ?
  • we can parse/implement every interesting css attribute we want (display:none, :hover, transition should be a killer feature

There are multiples ways to read CSS values. One ot them don't need to push something in the DOM Tree, but as some logical limites.
https://stackoverflow.com/questions/324486/how-do-you-read-css-rule-values-with-javascript

I successfully tested the one from Derek Ziemba

//Inside closure so that the inner functions don't need regeneration on every call.
const getCssClasses = (function () {
    function normalize(str) {
        if (!str)  return '';
        str = String(str).replace(/\s*([>~+])\s*/g, ' $1 ');  //Normalize symbol spacing.
        return str.replace(/(\s+)/g, ' ').trim();           //Normalize whitespace
    }
    function split(str, on) {               //Split, Trim, and remove empty elements
        return str.split(on).map(x => x.trim()).filter(x => x);
    }
    function containsAny(selText, ors) {
        return selText ? ors.some(x => selText.indexOf(x) >= 0) : false;
    }
    return function (selector) {
        const logicalORs = split(normalize(selector), ',');
        const sheets = Array.from(window.document.styleSheets);
        const ruleArrays = sheets.map((x) => Array.from(x.rules || x.cssRules || []));
        const allRules = ruleArrays.reduce((all, x) => all.concat(x), []);
        return allRules.filter((x) => containsAny(normalize(x.selectorText), logicalORs));
    };
})();

change: consider using ES10 sugar syntax

1. Support classes: private, static & public members

Current syntax, we can't really define private / public methods/getter/setter/variable : everything is public.

class foo {
    constructor() {
        this.bar = true;
    }
}

The refactor will consist to separate public and private things, i.e

class foo {
   
    // private variable
    #foo_private= "bar_public";

    // public variable
    foo_public = "foo_public";

    constructor() {
        // modify private variable
        this.#foo_private = "bar2_private";
    
        // modify public variable
        this.foo_public = "bar2_public";

        // run a private method
        this.#myPrivateMethod();

        // run a public method
        this.myPublicMethod();
    }

    // private method
    #myPrivateMethod() {
    }

    // public method
    myPublicMethod() {
    }
}

2. Support classes: extend

Rather than something which is not currently used in all branches (i.e Prototype add/replace methods/variables, we should use something like this :

class CanvasText extends CanvasWidget {
}

3. Support classes: picking object notation

Rather than

let property;
if (object) {
    property = (typeof object.attribute != undefined) ? object.attribute : defaultValue;
}

We can (note the question mark)

const property = (typeof object?.attribute !== undefined) ? object.attribute : defaultValue;

4. All "Panels" (i.e Widget) should be a class

Not only ColorPicker, Keyboard, Slider ...

We should have Text, Button, Image

face camera

Can CanvasUI face the camera? Like sprites do?

improve build process

devrv build process should optionaly also remove some code, i.e console.log, console.debug.

source code => cleanup =>production code => minify

It should provide both js/jsm files :

CanvasUI.js                  - bundled production code
CanvasUI.min.js           - bundled minified production code
CanvasUI.debug.js       - bundled source code

add input-text placeholder

implement placeholder for input-text

const config = {
    ...
    message: {
        type: 'input-text',
        placeholder: 'Click or select me',
        ...
    },
    label: {
        type: 'text',
        ...
    }
};

const content = {
    message: '',
    label: 'Keyboard demo'
};

change: upgrade threejs version

We are using r119.

In oculus browser, we have a non blocking error

The WebGL context was not marked as XR compatible. Call MakeXRCompatible to mark it as such. This will result in a fatal error in the future. See https://immersive-web.github.io/webxr/#dom-webglrenderingcontextbase-makexrcompatible

We should upgrade at least r125.

I've tested with r135, everything is smooth.

@NikLever are you ok for r135 ?

change: element/config refactor

First, we currently set positions, dimensions, font, colors, for each elements using attributes, but mixed with methods in the same object. Same thing for the config object which is a mix of different properties with differents usage (i.e element definition vs renderer, or panelSize vs an element name). This currently involves some filters when looping over object attributes in CanvasUI.

Let's take the current buttons example.

const config = {
    panelSize: { width: 2, height: 0.5 },
    height: 128,
    info: { type: "text", position:{ left: 6, top: 6 }, width: 500, height: 58, backgroundColor: "#aaa", fontColor: "#000" },
    prev: { type: "button", position:{ top: 64, left: 0 }, width: 64, fontColor: "#bb0", hover: "#ff0", onSelect: onPrev },
    stop: { type: "button", position:{ top: 64, left: 64 }, width: 64, fontColor: "#bb0", hover: "#ff0", onSelect: onStop },
    next: { type: "button", position:{ top: 64, left: 128 }, width: 64, fontColor: "#bb0", hover: "#ff0", onSelect: onNext },
    continue: { type: "button", position:{ top: 70, right: 10 }, width: 200, height: 52, fontColor: "#fff", backgroundColor: "#2659a4", hover: "#3df", onSelect: onContinue },
    renderer: this.renderer
}

First goal : elements objetct, only having elements declaration, rather than a mixed object.

const elements = {
    'info'': { type: 'text', value:'' },
    'prev': { type: 'button', value:'...', [ ... ] onSelect: onPrev },
    'stop': { type: 'button', value:'...', [ ... ] onSelect: onStop },
    'next': { type: 'button', value:'...', [ ... ] onSelect: onNext },
    'continue': { type: 'button', value:'Continue', [ ... ] onSelect: onContinue },
};

Everything which is not an element attribute should be moved to the config object,

const config = {

   /**        
   * CanvasUI start with a panel having a size of 1x1 meter. 
   * But you can define your own size.
   */
    panel: {

        size: {
            width: 2,
            height: 0.5
        }

    } ,

   /**        
   * CanvasUI use a Canvas HTML element to renderer widgets into a texture.
   */
    canvas: {
      /**    
       * size: canvas size (width, height) of the offscreencanvas, default 512 (pixels)
       */
        size: {
            height: 128
        }
    },

    threejs: {

        /**
        * threejs attribute value can be your threejs application instance, on which canvasui will look for a renderer,
        * camera and scene attributes.
        */

        /**        
         * renderer
         * 
         * NEEDED if you want to use WebXR controllers (renderer.xr, renderer.xr.isPresenting, getControllers ...)
         */
        renderer: this.renderer, 

        /**        
         * camera
         * 
         * NEEDED if you want to use the Mouse controller, because the raycaster is attached to the camera
         */
        camera: this.camera,

        /**        
         * camera
         * 
         * OPTIONAL if you want CanvasUI to be able to add some extra meshes (i.e raycaster intercept mesh, 
         * like in the panel scrolling example )
         */
        scene: this.scene 

    }

    controller: {

        drivers: {

           /**
           * Mouse driver
           * You can use mouse to interact with CanvasUI elements
           * this required a camera in threejs.camera config
           */
            mouse: {

               /**
               * enable: true | false, default true
               */
                enable: true,

               /**
               * handler: follow, default follow ('capture' mode not yet implemented)
               * 
               * follow mode just follow the mouse cursor.
               */
                handler:'follow'
            },

           /**
           * WebXR driver
           */

            webxr: {

                   /**
                   * enable: true | false, default true
                   */
                    enable: true, 

                   /**
                   * raycasterLinesLength: in meters, default 10
                   */
                    raycasterLinesLength:10
           },

           // @nik, do you see more drivers for later ? gamepad :) ?

        },*

        /**
         * Select behavior
         * 
         * Common settings for both WebXR driver and Mouse driver
         * For WebXR driver: selectstart, select, selectend controlller events
         * For Mouse driver: mouseup/mousedown/mousemove events
         */

        select: {

              /**
               * Raycaster(s) hover behavior while element is selected
               * 
               * hover : true | false, default false
               *
               * true:  trigger onSelect once (keyboard like behavior)
               * false: trigger onSelect while raycaster is moving (select+hover) on the element. Colorpicker or Slider like behavior.
               */
                hover: false, 
    
                /**
                 * Select repeater behavior 
                 * 
                 * Only when hover = false. 
                 * Usefull for repeating onSelect event while keeping a select WebXR button pressed or mouse down. 
                 * In keyboard example, it simulate multiple keydown event
                 */   
                repeater: {

                     * When pressing WebXR select button controller or clicking the mouse on a CanvasUI element,
                     * the onSelect method of the element is triggered a first time. And that's all when hover = true.
                     * 
                     * But if hover = false, after the first onSelect method has been triggered, it will optionally wait 
                     * for timeoutms milliseconds before triggering onSelect with a setInterval().
                     * 
                     * Then if specified, onSelect event will be triggered every repeatms millisecond until 
                     * the selectend or mouseup event is triggered
                     *
                     * enable or disable the repeter
                     * enable: true | false, default false
                     * 
                     * timeoutms
                     * 0 mean setTimeout 0 , i.e onSelect will be triggered in the next event loop, similare to setImmediate.
                     * null or undefined value will trigger onSelect on the same event loop without setTimeout().
                     * timeoutms:  default 100
                     * 
                     * repeatms
                     * 0 mean setInterval 0 , i.e onSelect will be triggered in the next event loop, similare to setImmediate. (use with caution)
                     * null or undefined value will disable the repeater.
                     * repeatms: default 20
                     */
                    timeoutms:100, 
                    repeatms:20
                }
          }
    }
}

Typical compact/minimal usage :

const config = {
    panel: { size: { width: 2, height: 0.5 } } ,
    canvas: { size : { height: 128 } },
    threejs: {
        renderer: this.renderer, 
        camera: this.camera
    }
}

Then, instantiate like this

new CanvasUI( elements , config );

Benefits:

  • separate UI properties from methods or threejs object
  • a step to be "a-frame same logic"

add onRaycasterEnter, onRaycasterLeave events

example:


functon onSelect() {
}

function onRaycasterEnter() {
    // play sound
}

function onRaycasterLeave() {
    // play sound
}

mybutton: {
    type: "button",
    position:{ top: 64, left: 0 },
    width: 64,
    fontColor: "#bb0",
    hover: "#ff0",
    onSelect,
    onRaycasterEnter,
    onRaycasterLeave
},

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.