skip to Main Content

I’ve a game with a determined frame size:

// The level frame original size
const frameWidth = 1140;
const frameHeight = 518;

I’ve tried different libraries for 2D rendering and I gave up of them either for bug or lack of features and anyway my game is cloud-based, so all the client does is mostly receive world packets, send packets and render the world. I’m betting the browser’s native HTML rendering won’t be a problem.

I want to fit this frame into the window; it must take its most minimal scale ratio that fits into the screen and get centered there:

import ScreenSize from '@/app/util/ScreenSize';

export default function optimalFitRatio(originalSize: ScreenSize, fitToSize: ScreenSize): number {
    const horizontalRatio = fitToSize.width / originalSize.width;
    const verticalRatio = fitToSize.height / originalSize.height;
    return Math.min(horizontalRatio, verticalRatio);
}

Now, the problem is that I see that the position of the world entities vary depending on the window resolution. I wasn’t having this problem when using the mentioned "libraries".

Minimal reproduction:

function optimalFitRatio(originalSize, fitToSize) {
    const horizontalRatio = fitToSize.width / originalSize.width;
    const verticalRatio = fitToSize.height / originalSize.height;
    return Math.min(horizontalRatio, verticalRatio);
}

// The level frame original size
const frameWidth = 1140;
const frameHeight = 518;

class LevelView {
    container1 = undefined;
    container2 = undefined;
    resizeListener = undefined;
    attached = false;

    scale = 0;
    width = 0;
    height = 0;

    attach() {
        this.attached = true;

        // Create first container
        this.newContainer();

        // Resizing
        this.resizeListener = this.resizeFrame.bind(this);
        window.addEventListener('resize', this.resizeListener);
        this.resizeFrame();
    }

    detach() {
        window.removeEventListener('resize', this.resizeListener);
        this.container1?.remove();

        this.attached = false;
    }

    renderLevel(world) {
        this.container1?.remove();
        this.newContainer();
        this.orientContainer();

        // Sort Z index
        // sortEntitiesByZ(world.entities);

        for (const entity of world.entities) {
            if (entity.type == "Entity") {
                this.renderEntity(entity);
            }
        }
    }

    renderEntity(entity) {
        // Orient the bitmap
        const orient = shape => {
            shape.style.position = "fixed";
            shape.style.left = `${entity.x}px`;
            shape.style.top = `${entity.y}px`;
            shape.style.transform = `rotate(${entity.rotationRadians}rad)`;
            shape.style.transformOrigin = "center";
            shape.style.userSelect = "none";
        };

        // Create the bitmap
        const bitmap = document.createElement("img");
        bitmap.src = "https://i.imgur.com/Dx200x9.png";
        orient(bitmap);
        this.container2.appendChild(bitmap);
    }

    resizeFrame() {
        if (!this.attached) {
            return;
        }

        // Fit the level frame original size into the actual resolution
        const ratio = optimalFitRatio(
            { width: frameWidth, height: frameHeight },
            { width: window.innerWidth, height: window.innerHeight },
        );

        // Cache properties
        this.width = frameWidth * ratio;
        this.height = frameHeight * ratio;
        this.scale = ratio;

        // Orient
        this.orientContainer();
    }

    orientContainer() {
        // Resize the container
        const [w, h] = [this.width, this.height];
        this.container1.style.width = `${w}px`;
        this.container1.style.height = `${h}px`;

        // Scale the second container
        this.container2.style.transform = `scale(${this.scale})`;

        // Translate the container
        this.container1.style.left = `${window.innerWidth / 2 - w / 2}px`;
        this.container1.style.top = `${window.innerHeight / 2 - h / 2}px`;
    }

    newContainer() {
        const c0 = document.body;

        const c1 = document.createElement('div');
        c1.style.display = "inline-block";
        c1.style.background = "#000";
        c1.style.overflow = "hidden";
        c1.style.position = "fixed";
        c1.style.userSelect = "none";
        c0.appendChild(c1);
        this.container1 = c1;

        const c2 = document.createElement('div');
        c1.appendChild(c2);
        this.container2 = c2;
    }
}

const levelView = new LevelView();
levelView.attach();
levelView.renderLevel({
    entities: [{
        type: "Entity",
        x: 100,
        y: 100,
        rotationRadians: 0,
    }],
});

(Updated: I forgot { type: "Entity", ...p } above. Now you should see the issue…)

2

Answers


  1. For perfect aspect ratio, add a <canvas> element and place your actual canvas on top of it.
    As for keeping the sizes of elements inside – use percentages.
    Something like this:

    body {
      display: flex;
      flex-direction: column;
      margin: 0;
      height: 100dvh;
    }
    
    .game {
      position: relative;
      margin: auto;
      min-height: 0;
    }
    
    .game canvas {
      display: block;
      max-width: 100%;
      max-height: 100%;
    }
    
    .game-canvas {
      position: absolute;
      inset: 0;
      background: black;
    }
    
    .game-canvas img {
      position: absolute;
      left: 10%;
      bottom: 10%;
      width: 20%;
    }
    <div class="game">
      <canvas width="1140" height="518"></canvas>
      <div class="game-canvas">
        <img src="https://i.imgur.com/Dx200x9.png">
      </div>
    </div>
    Login or Signup to reply.
  2. Yeah, I also think using percentages for the your CSS utility classes would be able to retain the aspect ratio for other screen sizes.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search