User Interface (UI) with JavaScript Framework
Pixi’VN offers the possibility of adding an HTML Element with the same dimensions as the PixiJS Canvas to add an UI with JavaScript frameworks.
By "UI" is meant the elements that are above the canvas, such as buttons, forms, etc.
This allows the use of systems such as React, Vue, Angular, etc. to create much more complex UI screens with excellent performance.
Differences between the UI and the canvas
UI and canvas are two different things. The UI is above the canvas and is used to create buttons, forms, etc. The canvas is used to display images, videos, etc.
All canvas information is included in saves and Pixi’VN manages going back and forth between the different steps. The UI is not included in the saves and is not managed by Pixi’VN, so you have to manage it yourself saving information you care about in game storage or browser storage.
In the canvas you can add elements during each step. In the UI you can't do that, you can create several "screens" and navigate between them.
In the canvas you can only add PixiJS elements, they are usually composed of images and are very simple. In the UI you can add any HTML element or use any UI component library, so you can create much more complex interfaces.
Navigate/switch between UI screens
To switch between UI screens (without interrupting the canvas), you can use popups and modals, or navigate between different paths/routes.
What is the URL Path and Routes?
The URL Path is the part of the URL that comes after the domain. For example, in the URL https://example.com/path/to/page
, the path is /path/to/page
.
A routering system can be used to manage navigation between URL Paths. For example you can use:
How to enable UI interaction?
By default, all HTML elements of the UI have the pointer-events: none
style. The reason is that because the html UI is above the canvas, all clicks are intercepted by the UI and not by the canvas.
So you must set the pointer-events: auto
style only for the elements (example a button, a form, etc...) that you want to interact with the user.
Connect the UI with variables in the game variables
The best way to connect the UI with the game variables is to use the TanStack Query library.
What is TanStack Query? TanStack Query is a library that allows you to manage the state of your application in a simple and efficient way. It is based on the concept of queries, mutations and subscriptions. This library is very useful and is compatible with React, Vue, Angular, Svelte, etc.
Here is an example:
In our example we would have two variables, text1
and text2
, saved in the game storage, this variables will be updated either by an input in the UI or by a label step.
Taking into account that a storage variables can only be changed during a next step, go back, run label or loading a save (outside the interface), we will create a useQueryText1
and useQueryText2
that will be updated after each next step, go back, run label or loading a save.
import { useQuery } from "@tanstack/react-query";
import { getLastSaveFromIndexDB } from "../utilities/save-utility";
// this is a "father" key that will be used to invalidate all queries that depend on it
export const INTERFACE_DATA_USE_QUEY_KEY = "interface_data_use_quey_key";
const TEXT1_USE_QUEY_KEY = "text1_save_use_quey_key";
export default function useQueryText1() {
return useQuery({
queryKey: [INTERFACE_DATA_USE_QUEY_KEY, TEXT1_USE_QUEY_KEY],
queryFn: async () => {
return storage.getVariable<string>("text1") || "";
},
});
}
const TEXT2_USE_QUEY_KEY = "text2_save_use_quey_key";
export default function useQueryText2() {
return useQuery({
queryKey: [INTERFACE_DATA_USE_QUEY_KEY, TEXT2_USE_QUEY_KEY],
queryFn: async () => {
return storage.getVariable<string>("text2") || "";
},
});
}
For invalidate the queries we can use the queryClient.invalidateQueries
function.
In this example if we want to update all the queries that depend on the INTERFACE_DATA_USE_QUEY_KEY
key, we can use the following code:
const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: [INTERFACE_DATA_USE_QUEY_KEY] })
If we want to update only the text1
or text2
query, we can use the following code:
const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: [INTERFACE_DATA_USE_QUEY_KEY, TEXT1_USE_QUEY_KEY] })
queryClient.invalidateQueries({ queryKey: [INTERFACE_DATA_USE_QUEY_KEY, TEXT2_USE_QUEY_KEY] })
const queryClient = useQueryClient()
narration.goNext({})
.then((result) => {
queryClient.invalidateQueries({ queryKey: [INTERFACE_DATA_USE_QUEY_KEY] })
});
narration.goBack({})
.then((result) => {
queryClient.invalidateQueries({ queryKey: [INTERFACE_DATA_USE_QUEY_KEY] })
});
loadSaveJson(jsonString, navigate)
.then(() => {
queryClient.invalidateQueries({ queryKey: [INTERFACE_DATA_USE_QUEY_KEY] })
})
// only if you are not in a label step
narration.callLabel("myLabel", {})
.then((result) => {
queryClient.invalidateQueries({ queryKey: [INTERFACE_DATA_USE_QUEY_KEY] })
});
In the UI we will use the useQueryText1
and useQueryText2
hooks to get the values of the variables text1
and text2
.
// react example
export function MyComponent() {
const { data: text1 = "" } = useQueryText1()
const { data: text2 = "" } = useQueryText2()
const queryClient = useQueryClient()
return (
<>
<Input
sx={{
pointerEvents: "auto",
}}
value={text1}
onChange={(e) => {
storage.setVariable("text1", e.target.value);
queryClient.invalidateQueries({ queryKey: [INTERFACE_DATA_USE_QUEY_KEY, TEXT1_USE_QUEY_KEY] })
}}
/>
<Input
sx={{
pointerEvents: "auto",
}}
value={text2}
onChange={(e) => {
storage.setVariable("text2", e.target.value);
queryClient.invalidateQueries({ queryKey: [INTERFACE_DATA_USE_QUEY_KEY, TEXT2_USE_QUEY_KEY] })
}}
/>
</>
);
}