- - - LESSON #2 - - -
Thank you for following this topic and let’s move on with this.
Lesson #1 (above) was quite easy to write and I hope you did understand it and enjoyed discovering the BJS 2D GUI.
If not (no offense at all), feel free to post your questions and if I explained anything wrongly or badly, also feel free to comment and correct. I will certainly amend this part.
If things have gone right in Lesson #1, you should now have an ‘AdvancedDynamicTexture’ FS layer in your scene along with a ‘grid’ that resizes correctly (except may be for the mobile vertical format) and you should also have some ‘stackpanels’ assigned to your grid cells, yes? Awesome! So, let’s start with it.
TIP: Same as in #Lesson 1, this tutorial comes with a number of PGs. In these demonstrating PGs, look for the commented parts:
//–> For an exercise, for a part to comment or uncomment
///// * * * For a new section of instructions / example
///// * * * Lesson #x - For the start of an example or exercise
///// * * * END OF :: Lesson/exercise - For the end of this part
ADVISE: I also strongly advise that by taking this lesson, you open a couple new tabs to display the 2D GUI doc and the related PG examples.
I will not be going through all details, aspects, options and parameters for each control. In fact, most are already in the doc (and explained better than I ever will) and most also have a simple and comprehensive PG example you can play with. To further investigate a control or container, use the example PG from the doc. Believe me, when stuck, this is the best (and may be only) way to go. Don’t try too hard in my PGs reproducing the ‘Project Chair’ GUI or in your own GUI as it will build-up and become more complex. You will likely loose your time or loose your way .
With that said, let’s move on…
LESSON #2
In lesson #2, we are going to have a look at some of the properties we can set on a stackpanel, which will be the parent for all your imbricated controls and or panels (children).
We will also have a look at how to manage the display and design of these stackpanels and their children (controls and containers).
We’ll speak about alignment, sizing and panning that will be, in most cases, relative to its parent(s) and, in some cases, relative to its child/children.
But before we do that, I wanted to quickly expose here something that (I believe) is not thoroughly explained in the doc and which I find to be really great. It’s about the handling of colors in the BJS GUI.
If you simply look at the doc and related PGs, you might think that the colors used in the ‘advancedTexture’ 2D GUI are only opaque (Color3). I sometimes like to have my GUI displaying with alpha blending where you can just lightly see the scene in the background and this is how I discovered that the BJS GUI lets you do just that.
Alpha blending vs Colors with alpha:
The first thing that made me think that the BJS 2D GUI will eventually work with colors with alpha is that it already has the ‘alpha’.
You can set an ‘alpha’ on your containers and controls to blend the GUI element (control or container) into the scene. Something like this:
element_i.alpha = 0.5;
However, by doing this (say on a stackpanel), you will also apply this same alpha level to all children in this container. And this might just not be exactly what you want, thus…
Setting colors on your panels and controls:
You have a number of choices to color the background and content of your containers and controls. Let’s have a look at some of’em:
- You can enter a value of a pre-defined color i.e. “red”, “blue”, “black”, “green”, “yellow”. Don’t forget the quotes, single or double. Obviously, these preset colors have no alpha. These colors can also be entered straight as color for the bg or content of your GUI element, i.e:
panel.background = "white";
button.color = "black";
- You can enter HEX color values, i.e:
panel.background = "#FFFFFF";
button.color = "#0";
Don’t forget the quotes, single or double.
- You can enter a value of “transparent” (with quotes, single or double), making your background or content ‘transparent’. Since ‘transparent’ is ‘a color’ it’s not the same as setting it to ‘isVisible’ or ‘visibility’ and is also not the same as setting the ‘alpha’ to zero (element_i.alpha=0). Understand it’s a ‘transparent’ coloring of the element. It still has a 100% alpha yet with ‘a color’ of ‘transparent’. Therefor, all children of this ‘transparent’ container (or control) will still be visible and will also still keep with their own color and alpha.
button.background = ‘transparent’;
- You can enter a BJS Color3 value, i.e.: new BABYLON.Color3(0.2, 0.5, 0.3)
- You can enter RGB color values converted to BJS values, i.e: new BABYLON.Color3(26/255, 24/255, 20/255). Where the first number is R (red), the second is G (green), the third is B (blue and the divider is the max levels for an RGB color (255). The divider is applied here to return the BJS value that is set between 0 and 1. Although, it appears BJS also accepts negativ values (but that’s just another subject;). Note that there are also tools to convert a color. In the doc, look for ‘tools’.
We’ll have a look at just the one we need below
For #4 and #5 above, we will first need to convert these colors to a hex string. And this is done like this (using the tool 'toHexString()'):
var mycolorbjs = new BABYLON.Color3(0.2, 0.5, 0.3).toHexString();
var mycolorrgb = new BABYLON.Color3(20/255, 50/255, 30/255).toHexString();
And now comes the interesting part. Rather than making your container with an ‘alpha level’ (alpha blending or ‘alpha’) that will apply to all children, you can enter a Color4 format value (read, with alpha). You can do so both for containers and controls, both for background and content.
I.E., you can enter:
- A HEX (HEX(a) or hexadecimal) color with alpha value. The last two numbers are the alpha (Don’t forget the quotes, single or double.), I.E.:
panel.background = “#FFFFFF99”;
- A BJS Color4 value, i.e.: new BABYLON.Color4(0.2, 0.5, 0.3, 0.8)
- An RGBA color converted to BJS value, i.e.: new BABYLON.Color4(26/255, 24/255, 20/255, 125/255)
Same as for the above #4 and #5 Color3 colors, for #6 and #7 Color4, you will also first need to convert these into a hex string, i.e.
panel.background = new BABYLON.Color4(20/255, 50/255, 30/255, 125/255).toHexString();
A HEX or HEX(a) color value can be entered straight (with quotes, single or double).
The ‘Play with colors PG’ for experiencing the above can be found here:
Experiencing with sizing, alignment and padding:
The first thing that’s important to understand here is just ‘high level’. Before entering the details of why such or such imbrication or instruction will work or not, I believe it’s important to take a time to speak about ‘#consistency’.
Many aspects of alignments, padding and sizing in BJS are very similar to css (read css v1) but then, some are different (else it wouldn’t be fun, would it
For those of you who where not born or not yet working at the time of the early days of css (lucky you , css v1 was much less forgiving/versatile towards using multiple methods and approaches (read disregarding #consistency in code/design) than it is now.
In particular, the way you’d apply padding (vs margin) on imbricated and relative to its parent elements (controls and containers) can rapidly make it so that any change made to the size or padding of an element that is above in the hierarchy (or sometimes below) will have a negativ impact on all children (and eventually parent). Without a method and without ‘#consistency’, changing a simple thing like the size of an icon or adding just one line of text to a textBlock can eventually turn into a nightmare and have you reorganize the entire design of your container and children.
My advise here is the same than when coding with HTML/css: Choose a method for how to apply alignment, padding, sizing and imbrication and stick to it. Typically, mixing things using alternatively ‘paddingTop’ and ‘paddingBottom’ on elements to create the spacing between elements is a dangerous approach. Would you have to remove or resize just one of’em, you’d likely be in for changing everything that’s relative to it.
When you build a UI (in either BJS or HTML/css), think first of what I call the 3Cs: ‘Consistency’, ‘Consistency’, ‘Consistency’ (Note: that’s my own version of the 3Cs The real one can be found on the Internet. In short, choose a method and try to apply it all the way through.
In fact, this aspect of alignment, sizing and padding could cover an entire lesson. Else, rather than reading a lot of text, there’s no better way to learn than experience it. Try, fail and try again.
So, just before starting writting #Lesson 3, I’m strongly thinking of making a side post with some ‘exercising’ PGs to let you twist and tweak sizing (height, width), panning, alignment (horizontalAlignment, verticalAlignment) and the instructions of ‘top’ and ‘left’. Which altogether constitute the main parameters that will have you master your design.
Meanwhile, you can still play along with this PG of an intermediate version reproducing the settings buttons and main panels for settings of my ‘Project Chair’ GUI. In this PG, I have added two footer buttons that are sharing a same stackpanel to display either ‘settings’ or ‘kbinfo’ information. Already have a look at it and feel free to leave your questions and we’ll return to this a bit later.
On a side note, whilst going through this tutorial and slowly building up this GUI, you will eventually notice some odd values given to a control, image or container, for sizing and panning. Such as a textBlock or image of an initial/native size of 48px that’s being given a height of ‘0px’ along with a ‘paddingTop’ of ‘-24px’ and a ‘paddingBottom’ of ‘-24px’ and this textBlock or image would still show with a height of… guess what? 48px.
There’s a small exercise in this PG for you to experience this:
With that said and whilst keeping this in mind for later, let’s just start with something simple…
For what is coming next, let’s first open this PG
and we’ll take a tour of…
Adding a control to your grid or stackpanel:
We are now going to add a top-menu button to our GUI.
It will be always displayed within the top-left cell of our grid (at grid 0,0) and by clicking/toggling it, we will simply show or hide all of the left menu elements (in column 0 underneith, to the far left). Similar to a ‘hamburger menu’ but without a folding/unfolding animation. At least, for now.
Since we only want to display a simple icon for this menu button (the menu icon), we don’t need a stackpanel for it and we’ll simply assign this control of an ‘imageOnlyButton’ straight to our top left grid cell.
Adding a control to a container or the advancedTexture is always done the same (see lesson #1):
parent_i.addControl(element_i);
Adding this control to a grid cell requires two more parameters (see lesson #1)
grid.addControl(element_i,0,1);
Note that there is one case where you do not have to pass these parameters of row and column.
By default, the unspecified row and column of your grid is at 0,0 (the top left row and column)
Since we want this top menu icon ‘imageOnlyButton’ control to cover the entire size of our grid cell (at row#0 and column#0), we don’t have to give it a width.
By default, the child, control or container, will cover 100% of the width of the parent (grid or container). Interestingly, in case of a stackpanel it will also take a height of 0(zero). So, speaking about a stackpanel, a panel with no instruction for its height will first not show. It is there, you can see it in the hierarchy. It covers 100% of the width of its parent (panel or grid cell); it simply has no height.
In fact, by giving no height to your stackpanel, this container will ‘automagically ’ resize with the height of its child/children (controls and/or panels). Keep this in mind when building your GUI and imbrications of panels and controls, because this can be very useful. But then, this can also cause lots of issues in a context where panels and controls are removed/hidden or changing size.
You can already experience and exercise this using this PG (at line 161)
Now, back to our menu button:
The SimpleButton, ImageWithCenterTextButton and ImageOnlyButton
Three of the most commonly used and most basic controls. Again, I encourage you to consult the comprehensive doc and try things using the associated PG example. Generally, there’s at least one for each control.
While the ’ SimpleButton’ can display text, the ‘ImageWithCenterTextButton’ can display both text and image, the ‘imageOnlyButton’ will display (as its name suggests)… well, an image or icon.
And this is what we want here for our menu button that will simply display a menu icon.
First thing we want to do is create this control and add it to our top-left grid cell.
Remember from #Lesson 1 that the ‘grid’ itself is already attached/added to the ‘advancedTexture’.
On a side note, you can actually add controls to a container that is not yet attached to its parent. By doing so, you would build what I call ‘a prefab’; By first adding your controls to the container and only next adding this container to the parent and the ‘advancedTexture’ layer. However, note that some actions cannot be taken until the control is added.
Back to our menu button creation:
var button = BABYLON.GUI.Button.CreateImageOnlyButton(“button”, “textures/icons/Open.png”);
grid.addControl(button);
Since this is an ‘ImageOnlyButton’, notice the additional parameter passed on creation.
The first parameter (that is common to all controls is optional but I strongly encourage you to use it), is the name of the control.
Why do I encourage you to give a name to your controls and containers? Open the PG and ‘the inspector’. From ‘the inspector’ unfold all from ‘GUI’ to see the hierarchy of your GUI and notice that a container or control without a name shows as ‘no name’. Amazing! Say you have a number of panels and even just a dozen of controls in there, it will already become hard to identify them
The second parameter that is specific to the ‘*image’ controls is the URL to load your image or icon. This ‘source’ URL can be changed later on, say depending on context/interaction, by calling on the ‘image.source’.
Updating the image source:
button.image.source = "textures/icons/Zoom.png";
Image sizing, padding and alignment
Having loaded our image source from the URL, we can see that our icon (called image) is eventually fitting our control size. And that’s a good thing and is pretty handy, isn’t it?
On the other hand, you can possibly notice that your icon/image is looking blury. In which case, the reason is likely that the source size of your image that is here changed to actually fit your container/control is too small. Reducing the size of a bitmap image will not affect its quality. In fact, it can even improve it. However, if your icon or image is too small, it needs to enlarge to fit the size of your container or control. Since I’m essentially a designer, I’d like to mention here that scaling/enlarging a bitmap image (jpg or png) will also have a negativ impact on its quality. Pixels to show at a bigger size are not in the source and the image will have to ‘extrapolate’ to create this bigger size, thus adding blur and loss in the quality of the image. If your image/icon is too small (at native size), I can only advise you would edit it first and rescale it to the initial (or even better, maximum) required size for your UI.
With that said and here again, there are multiple ways (and ways that can be used in combination) to size and constrain your image part, control or container to your design needs. Things are never simple, are they? Else, it wouldn’t be fun On the other hand, if things were too simple, it would only mean that the BJS GUI is not as versatile. And believe me, it is
Let’s have a quick look at how we can resize and add constraints for the display of our icon image, but first, let’s state here what we want to achieve:
- Obviously, we want our image/icon to show entirely and this, not depending on the size of the container on resize.
- Since an icon displayed without any margin/padding wouldn’t be nice, we will want to give it a little bit of a space around it.
- Since an icon that is displayed with irregular padding would also look a bit weird, we want to make sure our image always keeps within the center (horizontal and vertical alignment) of our control
This is where ‘padding’, sizing (‘height’ and ‘width’) and alignment (‘horizontalAlignment’ and ‘verticalAlignment’) come in.
For #1 above, the sizing, we are going to define a new size for our icon (either fixed or relative to the control). It will come in replacement of the default ‘fitTo’ (control/container) size of the image on import. This is instructed like this:
button.image.height = "72px";
button.image.width = "72px";
or this:
button.image.height = "80%";
button.image.width = "80%";
or that:
button.image.height = 0.8;
button.image.width = 0.8;
For #2 above, the padding, We are going to create an exclusion zone (a padding) between the edges of our control and the icon (image or thumbnail).
While adding the padding instruction straight to the control would create a limit (exclusion zone) between the grid cell edges and the control, which would be instructed like this:
button.paddingLeft = “10px”;
We actually want to set the limit/padding between the edges of the ‘imageOnlyButton’ control and our part of ‘image’ displayed within, thus offsetting the image inside the control. And this is done like this:
button.image.paddingLeft = “10px”;
This is because in the case of an Image type button, the ‘image’ is in fact a ‘part’ of the control. To access the image-only properties, you have to specify the path to it; Making sure it’s your image getting the padding and not the entire control. Alternatively to ‘image’, ‘thumbnail’ is also used in other controls and we’ll see to it later, but the way to do is the same.
In case of an image:
button.image.paddingLeft = "10px";
button.image.paddingRight = "10px";
button.image.paddingTop = "10px";
button.image.paddingBottom = "10px";
In case of a textBlock:
button.textBlock.paddingBottom = "10px";
So, by doing this, our ‘imageOnlyButton’ control for the top menu still covers the entire area of its assigned grid cell. However, the ‘image’ icon we are displaying in this control now has an offset of 10px on all sides.
In terms of code though, the above doesn’t look very ‘sexy’, does it? At least, not for a
Since we want it to be centered and have the same padding (or exclusion zone) on all sides, we could do something like this:
button.image.setPaddingInPixels(10);
Which is equivalent to this:
button.image.setPaddingInPixels(10,10,10,10);
And can also be replaced with BJS values:
button.image.setPadding(10);
Which is equivalent to this:
button.image.setPaddingInPixels(10,10,10,10);
However, now that we have given a size and/or some padding to our icon, it eventually is not centered anymore. This is because of the default setting for alignment on an image part (left instead of centered) and we will just have to pass on a couple more instructions to make sure it remains centered:
button.image.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
button.image.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
which is basically equal to this:
button.image.horizontalAlignment = 2;
button.image.verticalAlignment = 2;
In case of a ‘textBlock’ (notice the change of ‘textHorizontalAlignment’ and ‘textVerticalAlignment’ instead of ‘horizontalAlignment’ and ‘verticalAlignment’):
button.textBlock.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
button.textBlock.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
Except for this particular case of a part of a control (image or textBlock), the default for control alignment is centered. Both horizontally and vertically.
The other options for alignment are *_LEFT and *_RIGHT for horizontal, and *_TOP and *_BOTTOM for Vertical. Expressed in number, the range of the value is from 0(zero) to 2.
And then of course, there’s a lot more to it and many other options you can try once you know a bit more.
Also, at this point and while experiencing in the PG, you will eventually notice that you do not always need to specify both height and width. Eventually, your icon image gets resized and keeps with its ratio of height and width. Eventually, it’s actually good practice to set only one (or even none) whenever possible (and this is actually also true for HTML/css).
Cleaning the console from the GUI design Warnings
While adding more controls and sizing’em both with percentage and pixels, you will also eventually notice that the console is starting to throw some warnings about mixing sizes expressed both in percentage and pixels for height and width.
I could tell you to just ignore these warnings (that’s what most devs and engs do, right? but I’m a simple designer and digital PM and I like to have a clean console so, rather than pushing this back for the upcoming #lesson 3, I’m gonna give you the solution right here, right now.
For a start, you can certainly ignore these warning; If the result and result on resize is what you need and want, the warning can just ‘go with the wind’ and this is achieved like this:
On a single control/container:
element_i.ignoreLayoutWarnings = true;
After building your GUI, looking back at all from your ‘advancedTexture’ elements/descendants:
advancedTexture.getDescendants().forEach(function(control) {
control.ignoreLayoutWarnings = true;
});
Adding observables and animations events to a control:
So we now have a top-menu button (displaying or hiding the menu) at the top left corner of our GUI. That’s great, but right now, this button doesn’t do anything yet.
Don’t worry, it will awake soon . There are a number of user actions you can use on a control to trigger an event/animation or simply to record/observe. Mainly two types: An ‘observable’ and an ‘animation’ event. Rather than a lot of text, let’s have a look at some code for them:
button.pointerEnterAnimation = () => {
console.log(“starting an animation when user enters the control area”);
};
button.pointerOutAnimation = () => {
console.log(“starting an animation when user exits the control area”);
};
button.onPointerUpObservable.add(function() {
console.log(“observe this button interaction and start or record something when the users releases a mouse/pointer click”);
});
button.pointerUpAnimation = () => {
console.log(“on release, trigger my awesome animation function”);
myAwesomePointerUpAnim();
};
The complete list can be found in ‘observables’ and ‘animation events’, and for each control/class, in the ‘API’.
What we want to achieve with our menu button (show or hide menu) is the following:
- When the user first clicks on the menu button (from an initial hidden state), it shall display all menu buttons below.
- If the menu buttons are already visible, when the user clicks again, it will hide them all (and return to a hidden state).
Simple, yes? So how can we do this.
Well, again and as often with BJS (one of the reason I luv this framework ), there’s nearly always more than just one way.
But, let’s keep it simple and explain the one I used in this scenario.
We’ll start by opening this PG
The ‘isVisible’ parameter/accessor
First thing is that in this GUI repro of ‘Project Chair’, I’m relying on the parameter/accessor of ‘isVisible’ to either show or hide the menu buttons.
Second, (and this is really just my preference), I’m relying on a variable to record the state of the menu (hidden vs shown). Eventually, I also make sure the value is returned after each interaction.
Note that this is all not absolutely necessary. Eventually the value is returned anyway. Eventually you can also just check the state of ‘isVisible’ (using if ‘isVisible’ VS if ‘!isVisible’ on the control or container). For me, not being an ENG or system dev, everything is fine as long as it works over time, can be maintained and doesn’t soak-up too many resources
One advantage I find with this method of using a var to record the menu state is that later on, i.e. say in a separate animation script, I can call and rely on this variable without accessing the control. This var tells me the state of my menu display and will always do it correctly (no matter if the control is here or ‘removed’) because its value is always returned and easily accessible.
So, for this to work we need to start with creating this variable. It will be a simple ‘bolean’, with a ‘number’ or a ‘string’, recording either 0,1 or ‘true’,‘false’.
I choose the number, so I don’t use quotes.
var showmenu = 0;
If you want to use a string (dont’ forget the quotes, single or double):
var showmenu = ‘false’;
Now that we have a ‘variable’ to record our menu state, we will simply change it on user click/release (pointer up). For that, again, we have a number of ways. Let’s just pick one (and stick to it for the sake of #consistency).
I choose to go with the ‘if’ condition. We check 'if’ the showmenu is equal (==) to 0, in case we change it to 1. Whereas ‘if’ or ‘else if’ the value is 1, then we change it back to 0.
After the interaction, we also make sure the value for the var named ‘showmenu’ is updated/returned.
So something like this:
button.onPointerUpObservable.add(function() {
if (showmenu == 0){
//menu is NOT active - changing to active
console.log ("menu is active " + showmenu);
//do anything when the menu is changed to active/visible.
//showing the panel hosting the menu controls
panel.isVisible = true;
//changing the value of the showmenu var to active/visible.
showmenu = 1;
}
else if(showmenu == 1){
//menu is active - changing to NOT active
console.log ("menu is NOT active " + showmenu;
//do something when the menu is changed to invisible.
//reinitializing buttons display, making them ready to show with their initial display (specific to this project)
button.background = "transparent";
button0.background = "#FFFFFF99";
button0.image.source = "tex/tour.svg";
button0.image.alpha = 1;
button1.background = "#FFFFFF99";
button1.image.source = "tex/layout.svg";
button1.image.alpha = 1;
button2.background = "#FFFFFF99";
button2.image.source = "tex/edit-compare.svg";
button2.image.alpha = 1;
//hiding the panel hosting the menu controls
panel.isVisible = false;
//changing the value of the showmenu var to invisible.
showmenu = 0;
}
console.log ("showmenu after state is " + showmenu);
//returning the new value for the showmenu var (not absolutely necessary in this case)
return showmenu;
});
Now, having all this (and more) in your control, can rapidly become a bit messy.
So, why not simply trigger a function that will change the menu state?
We would next have all of these functions and animation events elsewhere in our script or, even better, in a separate/external script. Something like this on our control:
button.onPointerUpObservable.add(() => {
changeMenu();
});
and elsewhere, in a dedicated section of the script or a separate script, our function:
var _menu = 0;
var changeMenu = function() {
if (_menu == 0){
//menu is NOT active - changing to active
console.log ("menu is active " + _menu);
_menu = 1;
panel.isVisible = true;
}
else if(_menu == 1){
//menu is active - changing to NOT active
console.log ("menu is NOT active " +_menu);
button.background = "transparent";
button0.background = "#FFFFFF99";
button0.image.source = "tex/tour.svg";
button0.image.alpha = 1;
button1.background = "#FFFFFF99";
button1.image.source = "tex/layout.svg";
button1.image.alpha = 1;
button2.background = "#FFFFFF99";
button2.image.source = "tex/edit-compare.svg";
button2.image.alpha = 1;
panel.isVisible = false;
_menu = 0;
}
console.log ("changeMenu after state is " + _menu);
return _menu;
}
Now, there’s one more reason why I like to work these parts using a var (private or public).
Say you want to have a special event when the user first clicks on the menu, that will happen only the very first time the user clicks. Say ‘a tour’ or ‘a tip’. You can set an initial ‘null’ state to your var:
_menu = null;
and add just this one more condition to your pointerUp event or associated function:
if (_menu == null){
//menu has never been clicked - changing to active
console.log ("menu is clicked a first time and is now changing to active " + _menu);
_menu = 1;
panel.isVisible = true;
//add an action that will trigger only once
myAwesomeTourFunction();
}
Before wrapping-up #Lesson2 (because I also have a social life and my own pro and personal projects to work, sry , we will be closing this episode with just one more aspect…