DEV Community

Dmitry Vasiliev
Dmitry Vasiliev

Posted on

Making adaptive UI layout in Pixi.JS easy with DOM

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

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 and react-pixi 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>   
Enter fullscreen mode Exit fullscreen mode
.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%;
}

Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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, Cypresscan 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.

Top comments (0)