Creating screen for character selection

ui tutorials

10/29/2024

Kaloyan Geshev

In the character selection screen, players are invited to choose their hero for the journey ahead. Each character comes with unique abilities, strengths, and playstyles, allowing players to find the perfect match for their strategy.

Overview

In this tutorial, we’ll use Unreal Engine v5.3 and blueprints to design a level where players can pick different characters. While it’s possible to implement this using live views as shown in previous tutorials, we’ll now explore a more dynamic and engaging character selection method. When the player cycles through characters, they will be displayed directly in the game, using different camera views to showcase each character.

For the UI we will use the Gameface’s native and UE data-binding features to bind game data to the UI.

Resources

Getting started - Unreal Engine

We’ll begin by setting up the project in Unreal Engine. Use the getting started guide from our documentation to install the Gameface plugin and integrate it into an existing project or use the sample project created by the installer.

After setting things up, we’ll arrange and design the world in our level. You can use your creativity and the free assets linked in the Resources section to place them in your world.

For this tutorial, we arranged the assets in the following way:

Adding camera actors for each character

Once your game world is ready, we’ll begin building the character selection system. The first step is to create a camera actor for each character the player can choose from.

To create a camera actor, use the menu to create objects and search for Camera Actor.

After selecting the object, place it appropriately in the world. In our case, the character will be displayed on the right side of the screen, with their stats on the left. Make sure to position the cameras consistently across characters:

Note: Be sure to uncheck the Constrain Aspect Ratio setting to ensure the camera’s aspect ratio matches the screen.

Feel free to adjust the Location, Rotation, and Scale of the camera to achieve precise positioning.

Create a HUD class blueprint

Now that the cameras are positioned, it’s time to implement the character selection logic. We’ll create a HUD class blueprint to handle the level’s logic. Start by creating the CohCharacterBP, inheriting from the CohtmlGameHUD class:

Setting up the view and the input

Create a Setup Input method in CohCharacterBP to enable user input for the UI:

To initialize our HTML page for the UI, setup the view and set the input to the view in the CohCharacterBP blueprint.

Change the HUD class

To display the game’s UI as defined in our HTML page, we need to set the game’s HUD class. This is done via the Game Mode in the World Settings.

Initializing the camera actors

With the view set, the next step is to implement the logic for switching cameras as the player selects different characters. Start by creating a Character cameras variable in CohCharacterBP, that is an array of Camera Actor types.

Next, populate this variable by using the Get All Actors Of Class method to retrieve all camera actors in the world and assign them to the Character cameras array.

Creating a method to change the active camera

To switch the camera, we’ll create a setActiveCamera method that will handle changing the active camera based on the player’s selection.

First, create a Current character variable, that will hold the index of the currently selected character. Set it to 0 by default to represent the index of the first character.

The setActiveCamera method will retrieve the camera of the currently selected character from the Character cameras array and switch the active camera using the Set View Target with Blend method. This method transitions between cameras over a set Blend Time, which we’ll set to 0.5 for a smooth 500ms transition.

Ensure that the Lock Outgoing option is checked for the Set View Target With Blend method.

Once the setActiveCamera method is created, call it in the Event Graph to switch to the first character’s camera when initializing the Character cameras.

Implementing character switching

To switch between characters, we’ll create a changeActiveCharacter method, which will accept a boolean input - direction, indicating whether the player is cycling forward (true) or backward (false) through the character list.

The method will update the Current character index accordingly. When moving forward, we’ll check if the index exceeds the length of the Character cameras array, and if so, reset it to 0 (first character). Similarly, when moving backward, if the index is less than 0, we reset it to the index of last character in the array.

Finally, call the setActiveCamera method to change the camera to the next or previous character.

Register events for changing the active character

We will set up two events to handle character selection: one for switching to the next character and another for the previous character. These events will be named nextCharacter and prevCharacter, and they’ll be triggered from the UI we’ll implement later.

To add these events in our CohCharacterBP, we need to wait for Gameface to be ready for bindings using the Bind Events Ready for Bindings method. After that, we can register the two events using the Register for Event method, which accepts the Java Script Event Name where we’ll specify our event names.

For both events, we will call the previously created Change Active Character method, adjusting the Direction property to true or false based on whether we’re moving to the previous or next character.

Create the data bind model

Now that we’ve set up the logic for changing the active character and registered the events, it’s time to add a data model for the game’s characters. This model will be linked with the UI so that when a character is changed, the UI updates automatically.

Each character in our game will have a name and stats. The stats will be an array of objects, with each object containing a stat name and its value.

First, we’ll create a structure defining the stat object, named Stat.

Each stat will include a name as a string and its current value as an integer.

Next, we’ll create a Character Model structure to store the character’s name and stats. The name will be a string, and the stats will be an array of Stat objects.

Now, in the event graph of our CohCharacterBP, we’ll create a variable called Characters, which will be an array of Character Model items. We’ll set a default value for this array with three items, as we have three characters in our case. Each character will have a name and the following stat categories: health, strength, defense, attackPower, magicPower, and stamina. For each stat the values should range from 0 to 100.

Once the Characters variable is defined, we’ll create an Active Character variable of type Character Model to store the name and stats of the currently active character. This variable will be bound to the UI.

Now that we’ve set up the Active Character, we can bind it to the UI. After registering the two events in our blueprint, we’ll populate it with the current character data, retrieved from the Characters array using the Current Character index.

Finally, we’ll create a data model from the Active Character structure using the Create Data Model From Struct method and synchronize the models with the Synchronize Models method. We’ll name this model Character, which allows us to access it from the frontend.

Setting the active character data

The final step in the CohCharacterBP implementation is to update the model for the active character, ensuring the UI receives the correct data when the player changes characters.

To do this, we’ll create a new method called Set Active Character, which will retrieve the current character data from the Characters array, set it to the Active Character, and then update the model using the Update Whole Data Model from Struct method. After the model has been updated we need to synchronize the UI with the model via the Synchronize Models method.

After creating this method, we can call it whenever the user changes the active character, ensuring that the character model is updated after the active camera is changed in the Change Active Character method created earlier.

Getting started - Frontend

Once the game logic is fully implemented in the CohCharacterBP, it’s time to build the UI and bind it to the game data.

We’ll begin by creating an index.html file in the uiresources folder.

Import cohtml.js

To enable communication between the UI and the game, the cohtml.js library must first be imported.

index.html
1
<body>
2
<script src="./cohtml.js"></script>
3
</body>

Displaying the character’s name

Next, we’ll display the current character’s name. This can be easily achieved using the data-bind-value attribute.

index.html
1
<body>
2
<div class="wrapper">
3
<div class="stat-name" data-bind-value="{{Character.name}}"></div>
4
</div>
5
<script src="./cohtml.js"></script>
6
</body>

Add logic for switching characters

We’ll create a new JavaScript file and import it into our HTML page. Make sure to include this file after the cohtml.js script.

index.html
1
<body>
2
<div class="wrapper">
3
<div class="stat-name" data-bind-value="{{Character.name}}"></div>
4
</div>
5
<script src="./cohtml.js"></script>
6
<script src="./index.js"></script>
7
</body>

To allow players to change characters, in index.js, we’ll define two methods, changePrev and changeNext, that will switch between characters. Additionally, we’ll add a listener for the keydown event, allowing players to use the left and right arrow keys for navigation.

index.js
1
function changePrev() {
2
engine.trigger('prevCharacter');
3
}
4
5
function changeNext() {
6
engine.trigger('nextCharacter');
7
}
8
9
document.addEventListener('keydown', (event) => {
10
if (event.keyCode === 37) changePrev();
11
if (event.keyCode === 39) changeNext();
12
});

Adding arrows for character switching

Although arrow keys allow character switching, it’s helpful to add on-screen arrows for this purpose. We’ll place these on either side of the character name and wrap everything in an element with class - controls.

index.html
1
<body>
2
<div class="wrapper">
3
<div class="controls">
4
<div class="arrow" onclick="changePrev()">&lt;</div>
5
<div class="stat-name" data-bind-value="{{Character.name}}"></div>
6
<div class="arrow" onclick="changeNext()">&gt;</div>
7
</div>
8
<div class="stat-name" data-bind-value="{{Character.name}}"></div>
9
</div>
10
<script src="./cohtml.js"></script>
11
<script src="./index.js"></script>
12
</body>

In the end, the result will look like this:

Show the stats of the character

To show character stats, we’ll again use data binding attributes. For each stat, we’ll display a label for the stat name, a progress bar made of skull icons, and the stat value as text to the right of the bar.

The label will be bound using the data-bind-value attribute.

For the progress bar, we’ll use data-bind-style-background-position-x, which will adjust the background’s X-axis position, simulating the skulls filling up based on the stat value. We’ll also use data-bind-class-toggle to change the background color based on the value: red for values below 25, yellow for values between 25 and 50, and white for values above 50.

For the stat value text, we’ll implement a custom binding attribute, data-bind-progress, to animate the text representing the stat value.

Finally, we’ll use the data-bind-for attribute to generate all the character stats dynamically, reducing repetitive code.

index.html
1
<body>
2
<div class="wrapper">
3
<div class="controls">
4
<div class="arrow" onclick="changePrev()">&lt;</div>
5
<div class="stat-name" data-bind-value="{{Character.name}}"></div>
6
<div class="arrow" onclick="changeNext()">&gt;</div>
7
</div>
8
<div class="stats">
9
<div class="stat" data-bind-for="iter:{{Character.stats}}">
10
<div class="stat-label"
11
data-bind-class-toggle="text-red:{{iter.value}} < 25;text-yellow:{{iter.value}} >=25 && {{iter.value}} <=50"
12
data-bind-value="{{iter.name}}"
13
>
14
</div>
15
<div class="stat-value-data"
16
data-bind-style-background-position-x="100-({{iter.value}}) + '%'"
17
data-bind-class-toggle="bg-red:{{iter.value}} < 25;bg-yellow:{{iter.value}} >=25 && {{iter.value}} <=50;bg-white:{{iter.value}} > 50;"
18
>
19
</div>
20
<span class="stat-value-text" data-bind-progress="{{iter.value}}"></span>
21
</div>
22
</div>
23
</div>
24
</div>
25
<script src="./cohtml.js"></script>
26
<script src="./index.js"></script>
27
</body>

Fill the stat progress bar

To fill the progress bar, we’ll use a mask image with a linear gradient background in the stat-value-data CSS class. The mask will be an SVG of five skulls, and the background will be controlled by classes bg-red, bg-white, and bg-yellow. These classes define a gradient that changes from filled (50%) to unfilled based on the stat value.

For the mask image we will use the following svg that was created with the help of the skull icon linked in the resources section:

Vector.svg
1
<svg width="2691" height="496" viewBox="0 0 2691 496" fill="none"
2
xmlns="http://www.w3.org/2000/svg">
3
<path d="M240.673 4.115H240.173L276.773 44.125L381.573 70.185L413.573 39.795C361.873 11.925 296.273 2.845 240.673 4.115ZM215.273 4.665C173.273 6.475 134.873 12.855 102.773 24.085C64.303 37.535 34.463 58.085 19.953 87.075C-4.05702 135.075 2.65298 185.475 16.463 233.675L17.943 238.775L23.153 240.075C58.553 249.075 89.973 261.675 117.673 277.575C160.773 311.075 181.173 352.175 182.473 405.175C183.173 435.075 192.273 467.975 210.473 480.475C209.373 460.275 211.073 417.575 215.773 399.275L239.673 481.075L264.173 397.575C269.173 416.275 270.573 459.675 269.473 480.475C287.573 467.975 296.773 435.075 297.473 405.175C298.773 353.775 318.073 313.475 358.573 280.475C387.473 263.175 420.573 249.675 458.073 240.075L463.273 238.775L464.773 233.675C478.573 185.475 485.273 135.075 461.273 87.075C454.173 72.825 443.373 60.635 429.673 50.305L395.673 82.575L355.873 166.875L371.473 86.975L275.373 63.045C242.773 74.755 218.273 94.175 200.873 123.975L259.873 199.375L147.673 231.875C156.273 249.375 162.873 268.175 167.273 288.475C130.273 259.375 85.573 237.475 33.113 223.475C21.993 183.075 17.283 144.075 31.583 106.875C46.073 116.975 59.803 127.475 72.603 138.475C96.573 98.475 128.573 73.755 174.473 57.435C135.573 87.575 110.773 117.075 97.573 161.975C113.373 178.375 127.173 195.875 138.573 214.975L228.073 189.075L178.473 125.675L181.373 120.375C198.873 87.675 224.573 64.625 257.073 50.295L215.273 4.665ZM449.673 106.875C463.973 144.075 459.273 183.075 448.173 223.475C395.673 237.475 350.973 259.375 314.073 288.475C339.673 209.175 394.973 145.575 449.673 106.875ZM300.173 256.275L241.173 377.775L182.073 256.275L241.173 321.775C260.873 299.975 280.473 278.075 300.173 256.275Z" stroke="white" fill="white" stroke-width="8"/>
4
<path d="M792.896 4.115H792.396L828.996 44.125L933.796 70.185L965.796 39.795C914.096 11.925 848.496 2.845 792.896 4.115ZM767.496 4.665C725.496 6.475 687.096 12.855 654.996 24.085C616.526 37.535 586.686 58.085 572.176 87.075C548.166 135.075 554.876 185.475 568.686 233.675L570.166 238.775L575.376 240.075C610.776 249.075 642.196 261.675 669.896 277.575C712.996 311.075 733.396 352.175 734.696 405.175C735.396 435.075 744.496 467.975 762.696 480.475C761.596 460.275 763.296 417.575 767.996 399.275L791.896 481.075L816.396 397.575C821.396 416.275 822.796 459.675 821.696 480.475C839.796 467.975 848.996 435.075 849.696 405.175C850.996 353.775 870.296 313.475 910.796 280.475C939.696 263.175 972.796 249.675 1010.3 240.075L1015.5 238.775L1017 233.675C1030.8 185.475 1037.5 135.075 1013.5 87.075C1006.4 72.825 995.596 60.635 981.896 50.305L947.896 82.575L908.096 166.875L923.696 86.975L827.596 63.045C794.996 74.755 770.496 94.175 753.096 123.975L812.096 199.375L699.896 231.875C708.496 249.375 715.096 268.175 719.496 288.475C682.496 259.375 637.796 237.475 585.336 223.475C574.216 183.075 569.506 144.075 583.806 106.875C598.296 116.975 612.026 127.475 624.826 138.475C648.796 98.475 680.796 73.755 726.696 57.435C687.796 87.575 662.996 117.075 649.796 161.975C665.596 178.375 679.396 195.875 690.796 214.975L780.296 189.075L730.696 125.675L733.596 120.375C751.096 87.675 776.796 64.625 809.296 50.295L767.496 4.665ZM1001.9 106.875C1016.2 144.075 1011.5 183.075 1000.4 223.475C947.896 237.475 903.196 259.375 866.296 288.475C891.896 209.175 947.196 145.575 1001.9 106.875ZM852.396 256.275L793.396 377.775L734.296 256.275L793.396 321.775C813.096 299.975 832.696 278.075 852.396 256.275Z" stroke="white" fill="white" stroke-width="8"/>
5
<path d="M1345.12 4.115H1344.62L1381.22 44.125L1486.02 70.185L1518.02 39.795C1466.32 11.925 1400.72 2.845 1345.12 4.115ZM1319.72 4.665C1277.72 6.475 1239.32 12.855 1207.22 24.085C1168.75 37.535 1138.91 58.085 1124.4 87.075C1100.39 135.075 1107.1 185.475 1120.91 233.675L1122.39 238.775L1127.6 240.075C1163 249.075 1194.42 261.675 1222.12 277.575C1265.22 311.075 1285.62 352.175 1286.92 405.175C1287.62 435.075 1296.72 467.975 1314.92 480.475C1313.82 460.275 1315.52 417.575 1320.22 399.275L1344.12 481.075L1368.62 397.575C1373.62 416.275 1375.02 459.675 1373.92 480.475C1392.02 467.975 1401.22 435.075 1401.92 405.175C1403.22 353.775 1422.52 313.475 1463.02 280.475C1491.92 263.175 1525.02 249.675 1562.52 240.075L1567.72 238.775L1569.22 233.675C1583.02 185.475 1589.72 135.075 1565.72 87.075C1558.62 72.825 1547.82 60.635 1534.12 50.305L1500.12 82.575L1460.32 166.875L1475.92 86.975L1379.82 63.045C1347.22 74.755 1322.72 94.175 1305.32 123.975L1364.32 199.375L1252.12 231.875C1260.72 249.375 1267.32 268.175 1271.72 288.475C1234.72 259.375 1190.02 237.475 1137.56 223.475C1126.44 183.075 1121.73 144.075 1136.03 106.875C1150.52 116.975 1164.25 127.475 1177.05 138.475C1201.02 98.475 1233.02 73.755 1278.92 57.435C1240.02 87.575 1215.22 117.075 1202.02 161.975C1217.82 178.375 1231.62 195.875 1243.02 214.975L1332.52 189.075L1282.92 125.675L1285.82 120.375C1303.32 87.675 1329.02 64.625 1361.52 50.295L1319.72 4.665ZM1554.12 106.875C1568.42 144.075 1563.72 183.075 1552.62 223.475C1500.12 237.475 1455.42 259.375 1418.52 288.475C1444.12 209.175 1499.42 145.575 1554.12 106.875ZM1404.62 256.275L1345.62 377.775L1286.52 256.275L1345.62 321.775C1365.32 299.975 1384.92 278.075 1404.62 256.275Z" stroke="white" fill="white" stroke-width="8"/>
6
<path d="M1897.34 4.115H1896.84L1933.44 44.125L2038.24 70.185L2070.24 39.795C2018.54 11.925 1952.94 2.845 1897.34 4.115ZM1871.94 4.665C1829.94 6.475 1791.54 12.855 1759.44 24.085C1720.97 37.535 1691.13 58.085 1676.62 87.075C1652.61 135.075 1659.32 185.475 1673.13 233.675L1674.61 238.775L1679.82 240.075C1715.22 249.075 1746.64 261.675 1774.34 277.575C1817.44 311.075 1837.84 352.175 1839.14 405.175C1839.84 435.075 1848.94 467.975 1867.14 480.475C1866.04 460.275 1867.74 417.575 1872.44 399.275L1896.34 481.075L1920.84 397.575C1925.84 416.275 1927.24 459.675 1926.14 480.475C1944.24 467.975 1953.44 435.075 1954.14 405.175C1955.44 353.775 1974.74 313.475 2015.24 280.475C2044.14 263.175 2077.24 249.675 2114.74 240.075L2119.94 238.775L2121.44 233.675C2135.24 185.475 2141.94 135.075 2117.94 87.075C2110.84 72.825 2100.04 60.635 2086.34 50.305L2052.34 82.575L2012.54 166.875L2028.14 86.975L1932.04 63.045C1899.44 74.755 1874.94 94.175 1857.54 123.975L1916.54 199.375L1804.34 231.875C1812.94 249.375 1819.54 268.175 1823.94 288.475C1786.94 259.375 1742.24 237.475 1689.78 223.475C1678.66 183.075 1673.95 144.075 1688.25 106.875C1702.74 116.975 1716.47 127.475 1729.27 138.475C1753.24 98.475 1785.24 73.755 1831.14 57.435C1792.24 87.575 1767.44 117.075 1754.24 161.975C1770.04 178.375 1783.84 195.875 1795.24 214.975L1884.74 189.075L1835.14 125.675L1838.04 120.375C1855.54 87.675 1881.24 64.625 1913.74 50.295L1871.94 4.665ZM2106.34 106.875C2120.64 144.075 2115.94 183.075 2104.84 223.475C2052.34 237.475 2007.64 259.375 1970.74 288.475C1996.34 209.175 2051.64 145.575 2106.34 106.875ZM1956.84 256.275L1897.84 377.775L1838.74 256.275L1897.84 321.775C1917.54 299.975 1937.14 278.075 1956.84 256.275Z" stroke="white" fill="white" stroke-width="8"/>
7
<path d="M2449.57 4.115H2449.07L2485.67 44.125L2590.47 70.185L2622.47 39.795C2570.77 11.925 2505.17 2.845 2449.57 4.115ZM2424.17 4.665C2382.17 6.475 2343.77 12.855 2311.67 24.085C2273.2 37.535 2243.36 58.085 2228.85 87.075C2204.84 135.075 2211.55 185.475 2225.36 233.675L2226.84 238.775L2232.05 240.075C2267.45 249.075 2298.87 261.675 2326.57 277.575C2369.67 311.075 2390.07 352.175 2391.37 405.175C2392.07 435.075 2401.17 467.975 2419.37 480.475C2418.27 460.275 2419.97 417.575 2424.67 399.275L2448.57 481.075L2473.07 397.575C2478.07 416.275 2479.47 459.675 2478.37 480.475C2496.47 467.975 2505.67 435.075 2506.37 405.175C2507.67 353.775 2526.97 313.475 2567.47 280.475C2596.37 263.175 2629.47 249.675 2666.97 240.075L2672.17 238.775L2673.67 233.675C2687.47 185.475 2694.17 135.075 2670.17 87.075C2663.07 72.825 2652.27 60.635 2638.57 50.305L2604.57 82.575L2564.77 166.875L2580.37 86.975L2484.27 63.045C2451.67 74.755 2427.17 94.175 2409.77 123.975L2468.77 199.375L2356.57 231.875C2365.17 249.375 2371.77 268.175 2376.17 288.475C2339.17 259.375 2294.47 237.475 2242.01 223.475C2230.89 183.075 2226.18 144.075 2240.48 106.875C2254.97 116.975 2268.7 127.475 2281.5 138.475C2305.47 98.475 2337.47 73.755 2383.37 57.435C2344.47 87.575 2319.67 117.075 2306.47 161.975C2322.27 178.375 2336.07 195.875 2347.47 214.975L2436.97 189.075L2387.37 125.675L2390.27 120.375C2407.77 87.675 2433.47 64.625 2465.97 50.295L2424.17 4.665ZM2658.57 106.875C2672.87 144.075 2668.17 183.075 2657.07 223.475C2604.57 237.475 2559.87 259.375 2522.97 288.475C2548.57 209.175 2603.87 145.575 2658.57 106.875ZM2509.07 256.275L2450.07 377.775L2390.97 256.275L2450.07 321.775C2469.77 299.975 2489.37 278.075 2509.07 256.275Z" stroke="white" fill="white" stroke-width="8"/>
8
</svg>

And the styles for the progress bar will be:

style.css
1
.bg-red {
2
background: linear-gradient(90deg, rgb(255 0 0) 0%, rgb(255 0 0) 50%, rgb(100, 100, 100) 50%, rgb(100, 100, 100) 100%);
3
}
4
5
.bg-white {
6
background: linear-gradient(90deg, rgb(255, 255, 255) 0%, rgb(255, 255, 255) 50%, rgb(100, 100, 100) 50%, rgb(100, 100, 100) 100%);
7
}
8
9
.bg-yellow {
10
background: linear-gradient(90deg, rgb(255, 255, 55) 0%, rgb(255, 255, 55) 50%, rgb(100, 100, 100) 50%, rgb(100, 100, 100) 100%);
11
}
12
13
.stat-value-data {
14
flex: 1 0 0;
15
height: 100%;
16
mask-image: url(./Vector.svg);
17
mask-size: 100% 100%;
18
mask-position: 0% 50%;
19
mask-repeat: no-repeat;
20
background-size: 200% 100%;
21
background-position: 50% 0;
22
transition: background-position 500ms ease-out, background-image 500ms ease-out;
23
}

Animate the text showing the stat value

We’ll animate the stat value text so it dynamically updates while switching between characters. To achieve this, we’ll create a custom data-bind-progress attribute in index.js that smoothly increments or decrements the stat value over time.

The animation works by:

  1. Calculating the difference between the old and new stat value.
  2. Setting an interval to increment or decrement the value over time, simulating the stat change.
  3. Clearing the interval once the value matches the new stat or if time of 500ms exceeds.
index.js
1
class Progress {
2
constructor() {
3
this.textValueChangeInterval = null;
4
this.textValueChangeTimeout = null;
5
}
6
7
init(element, model) {
8
this.value = model;
9
this.currentValue = this.value;
10
this.textElement = element;
11
this.textElement.textContent = `${this.currentValue}/100`;
12
}
13
14
animateText() {
15
if (this.textValueChangeInterval) clearInterval(this.textValueChangeInterval);
16
if (this.textValueChangeTimeout) clearInterval(this.textValueChangeTimeout);
17
18
const diff = this.value < this.currentValue ? this.currentValue - this.value : this.value - this.currentValue;
19
if (!diff) return;
20
const intervalTime = parseInt(500 / diff);
21
22
this.textValueChangeInterval = setInterval(() => {
23
if (this.currentValue === this.value) return clearInterval(this.textValueChangeInterval);
24
if (this.value < this.currentValue) this.currentValue--;
25
if (this.value > this.currentValue) this.currentValue++;
26
27
this.textElement.textContent = this.value < this.currentValue ? `${this.currentValue}/100` : `${this.currentValue}/100`;
28
}, intervalTime);
29
30
this.textValueChangeTimeout = setTimeout(() => {
31
clearInterval(this.textValueChangeInterval);
32
this.currentValue = this.value;
33
this.textElement.textContent = `${this.currentValue}/100`;
34
}, 500);
35
}
36
37
update(element, model) {
38
this.value = model;
39
this.animateText();
40
}
41
}
42
engine.registerBindingAttribute('progress', Progress);

Full source of the tutorial

The complete source code for this tutorial is available below, including additional styles used to enhance the UI.

index.html
1
<!DOCTYPE html>
2
<html lang="en">
3
4
<head>
5
<link rel="stylesheet" href="style.css">
6
</head>
7
8
<body>
41 collapsed lines
9
<div class="wrapper">
10
<div class="controls">
11
<div
12
class="arrow"
13
onclick="changePrev()"
14
>&lt;</div>
15
<div
16
class="stat-name"
17
data-bind-value="{{Character.name}}"
18
></div>
19
<div
20
class="arrow"
21
onclick="changeNext()"
22
>&gt;</div>
23
</div>
24
<div class="stats">
25
<div
26
class="stat"
27
data-bind-for="iter:{{Character.stats}}"
28
>
29
<div
30
class="stat-label"
31
data-bind-class-toggle="text-red:{{iter.value}} < 25;text-yellow:{{iter.value}} >=25 && {{iter.value}} <=50"
32
data-bind-value="{{iter.name}}"
33
>
34
</div>
35
<div
36
class="stat-value-data"
37
data-bind-style-background-position-x="100-({{iter.value}}) + '%'"
38
data-bind-class-toggle="bg-red:{{iter.value}} < 25;bg-yellow:{{iter.value}} >=25 && {{iter.value}} <=50;bg-white:{{iter.value}} > 50;"
39
>
40
</div>
41
<span
42
class="stat-value-text"
43
data-bind-progress="{{iter.value}}"
44
></span>
45
</div>
46
</div>
47
</div>
48
<script src="./cohtml.js"></script>
49
<script src="./index.js"></script>
50
</body>
51
52
</html>
style.css
1
@font-face {
2
font-family: jacksonvile;
3
src: url(./Jacksonville.ttf);
4
}
5
6
body {
7
width: 100vw;
8
height: 100vh;
9
margin: 0;
10
font-family: jacksonvile;
11
}
136 collapsed lines
12
13
.wrapper {
14
width: 100vw;
15
height: 100%;
16
display: flex;
17
justify-content: flex-end;
18
flex-direction: column;
19
background-image: linear-gradient(79deg, rgba(0, 0, 0, 0.8) 10%, rgba(0, 0, 0, 0.6) 38%, rgba(255, 255, 255, 0) 55%);
20
visibility: visible;
21
animation: in-left 500ms ease-in forwards;
22
}
23
24
@keyframes in-left {
25
0% {
26
transform: translateX(-100vw);
27
}
28
29
100% {
30
transform: translateX(0);
31
}
32
}
33
34
.stat-name {
35
color: white;
36
font-size: 5vw;
37
text-shadow: 10px 10px 20px black;
38
}
39
40
.stats {
41
width: 50%;
42
position: relative;
43
display: flex;
44
font-size: 2.3vw;
45
flex-direction: column;
46
align-items: center;
47
justify-content: flex-end;
48
padding-bottom: 2vw;
49
}
50
51
.controls {
52
width: 50%;
53
display: flex;
54
align-items: center;
55
flex-direction: row;
56
justify-content: space-between;
57
transform: scale(0) rotateZ(-7deg) translateX(3vw) translateY(-4vw);
58
animation: scale 300ms 500ms ease-in forwards;
59
}
60
61
@keyframes scale {
62
0% {
63
transform: scale(0) rotateZ(-7deg) translateX(3vw) translateY(-4vw);
64
}
65
66
100% {
67
transform: scale(1) rotateZ(-7deg) translateX(3vw) translateY(-4vw);
68
}
69
}
70
71
.stat {
72
width: 50%;
73
height: 2.5vw;
74
margin: 0.5vw;
75
display: flex;
76
align-items: center;
77
flex-direction: row;
78
justify-content: space-between;
79
}
80
81
.arrow {
82
font-size: 4vw;
83
transition: transform 300ms;
84
transform-origin: center center;
85
transform: scale(1);
86
color: white;
87
cursor: pointer;
88
}
89
90
.arrow:hover {
91
transform: scale(1.5);
92
}
93
94
.stat-label {
95
color: white;
96
flex-basis: 9vw;
97
text-align: right;
98
margin-right: 1vw;
99
transition: color 500ms ease-out;
100
}
101
102
.stat-value {
103
flex: 1 0 0;
104
height: 70%;
105
}
106
107
.stat-value-text {
108
height: 100%;
109
width: 5.5vw;
110
padding-left: 0.8vw;
111
font-size: 2vw;
112
color: white;
113
font-weight: bold;
114
align-items: center;
115
}
116
117
.bg-red {
118
background: linear-gradient(90deg, rgb(255 0 0) 0%, rgb(255 0 0) 50%, rgb(100, 100, 100) 50%, rgb(100, 100, 100) 100%);
119
}
120
121
.bg-white {
122
background: linear-gradient(90deg, rgb(255, 255, 255) 0%, rgb(255, 255, 255) 50%, rgb(100, 100, 100) 50%, rgb(100, 100, 100) 100%);
123
}
124
125
.text-red {
126
color: rgba(202, 22, 13, 1);
127
}
128
129
.bg-yellow {
130
background: linear-gradient(90deg, rgb(255, 255, 55) 0%, rgb(255, 255, 55) 50%, rgb(100, 100, 100) 50%, rgb(100, 100, 100) 100%);
131
}
132
133
.text-yellow {
134
color: rgba(255, 243, 0, 1);
135
}
136
137
.stat-value-data {
138
flex: 1 0 0;
139
height: 100%;
140
mask-image: url(./Vector.svg);
141
mask-size: 100% 100%;
142
mask-position: 0% 50%;
143
mask-repeat: no-repeat;
144
background-size: 200% 100%;
145
background-position: 50% 0;
146
transition: background-position 500ms ease-out, background-image 500ms ease-out;
147
}
index.js
1
class Progress {
2
constructor() {
3
this.textValueChangeInterval = null;
4
this.textValueChangeTimeout = null;
46 collapsed lines
5
}
6
7
init(element, model) {
8
this.value = model;
9
this.currentValue = this.value;
10
this.textElement = element;
11
this.textElement.textContent = `${this.currentValue}/100`;
12
}
13
14
animateText() {
15
if (this.textValueChangeInterval) clearInterval(this.textValueChangeInterval);
16
if (this.textValueChangeTimeout) clearInterval(this.textValueChangeTimeout);
17
18
const diff = this.value < this.currentValue ? this.currentValue - this.value : this.value - this.currentValue;
19
if (!diff) return;
20
const intervalTime = parseInt(500 / diff);
21
22
this.textValueChangeInterval = setInterval(() => {
23
if (this.currentValue === this.value) return clearInterval(this.textValueChangeInterval);
24
if (this.value < this.currentValue) this.currentValue--;
25
if (this.value > this.currentValue) this.currentValue++;
26
27
this.textElement.textContent = this.value < this.currentValue ? `${this.currentValue}/100` : `${this.currentValue}/100`;
28
}, intervalTime);
29
30
this.textValueChangeTimeout = setTimeout(() => {
31
clearInterval(this.textValueChangeInterval);
32
this.currentValue = this.value;
33
this.textElement.textContent = `${this.currentValue}/100`;
34
}, 500);
35
}
36
37
update(element, model) {
38
this.value = model;
39
this.animateText();
40
}
41
}
42
engine.registerBindingAttribute('progress', Progress);
43
44
function changePrev() {
45
engine.trigger('prevCharacter');
46
}
47
48
function changeNext() {
49
engine.trigger('nextCharacter');
50
}
51
52
document.addEventListener('keydown', (event) => {
53
if (event.keyCode === 37) changePrev();
54
if (event.keyCode === 39) changeNext();
55
});

On this page