Making adaptive UI layout in Pixi.JS easy with DOM
Tags: javascript, pixi, react, gamedev
Published: 2022-12-17 14:34:44

Most HTML5 game developers know that having a decent UI layout in canvas can be problematic. Whether because of screen sizes of native browsers or overwhelming creativity of UI/UX designed, buttons never seem to be well-positioned everywhere unless you solve corner cases in your code.
Let's use DOM, the only piece of HTML5 technology that works on every browser. We will need to add a div with `position: absolute` above our canvas.

Say you have three areas in your game where you want to place some game content:

![Grid Layout](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/82qco53usp39kc8hs8j6.png)

First, take your class-based Pixi.JS code and throw it away, because if you wanna be mature HTML5 game developer you have to grow some balls and learn React. Libraries like [react-pixi-fiber](https://github.com/michalochman/react-pixi-fiber) and [react-pixi](https://github.com/pixijs/pixi-react) will help you with that.

Second, transform layout into HTML.

```
    <div class="wrapper">
      <div class="box" id="buttons-left">Left Controls</div>
      <div class="box" id="game">Frame</div>
      <div class="box" id="buttons-right">Right Controls</div>
    </div>   
```

```
.wrapper {
  display: grid;
  grid-gap: 10px;
  grid-template-columns: 200px 1fr 200px;
  color: #444;
  position: absolute;
  width: 100%;
  height: 100%;
}

.box {
  font-size: 18px;
  color: #fff;
  border-radius: 10px;
  padding: 50px;
  font-size: 150%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #7f55d452;
  height: 100%;
}

```


Example on CodePen with default CSS styles removed https://codepen.io/schmooky/pen/jOpNxqR

Now we have to make a component that looks at bounding client rect of div to determine screen position.

```
import { Container as PixiContainer, Point } from 'pixi.js';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { Container, PixiComponent } from 'react-pixi-fiber';

type ObserverContainerProps = {
  target: string;
  visible?: boolean;
  children: React.ReactNode;
  defaultWidth?: number;
};

export const ObserverContainer: PixiComponent<ObserverContainerProps> = (
  props: ObserverContainerProps,
) => {
  const { target, visible, defaultWidth } = props;

  const containerRef = React.useRef<PixiContainer>(null);

  const ref = useRef<HTMLElement>();
  const [bbox, setBbox] = useState<Partial<DOMRect>>({});

  const set = () => setBbox(ref && ref.current ? ref.current.getBoundingClientRect() : {});

  const [scale, setScale] = React.useState(1);

  useEffect(() => {
    set();
    window.addEventListener('resize', set);
    return () => window.removeEventListener('resize', set);
  }, []);

  useLayoutEffect(() => {
    ref.current = document.getElementById(target);
  }, [target]);

  useEffect(() => {
    if (!containerRef.current) return;
    if (!defaultWidth) return;
    console.log(target, defaultWidth);
    setScale(bbox.width / defaultWidth);
  }, [bbox]);

  return (
    <Container
      ref={containerRef as any}
      visible={visible}
      x={bbox.left + bbox.width / 2}
      y={bbox.top + bbox.height / 2}
      scale={new Point(scale, scale)}
    >
      {props.children}
    </Container>
  );
};
```
---
If, for any reason, UI/UX designer would wanna "move that button", you can simply add `div` with margin that offsets it's position.

And, since dom elements are positioned in the same area of the screen as their canvas coutnerparts, [Cypress] (https://www.cypress.io)can provide you with smooth e2e testing experience.

This is just an example of such component that takes into account width of target DOM element to resize it's contents. You might wanna add fit/fill cover calculation using `Math.max` or `Math.min`, or even add MutationObserver to look at more delicate attribute changes based on media query.