typescriptcanvas2dgame-development

How do I save the current Canvas 2D state as a texture in memory, then render it later?


I'm using Canvas2D and the (CanvasRenderingContext2D)[https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D] in the web to make a game (using TypeScript). What I'm trying to achieve is saving the current state of the canvas render, similarly to a screenshot, and storing it in memory. I want this so that I can create a mini-map of my world, without overhead of re-rendering. Is this feasible, or does this require a WebGL renderer? Couldn't find anything about this while googling around

Some psuedo-code of what I'm trying to achieve

//First we would render the world

//...

//Then save a render of the canvas
var texture = ctx.saveRenderOrSomeShit();

//Render hud

//...

//Render the world as part of the hud to top right or something similar

ctx.fillRect(texture, etc etc etc);

//Done rendering

Thanks for reading!


Solution

  • ctx.drawImage() does accept HTMLCanvasElement or OffscreenCanvas instances as input, so you could create one such canvas, draw there, and then use it as your texture:

    const texture = new OffscreenCanvas(50, 50);
    const texCtx = texture.getContext("2d");
    texCtx.fillStyle = "red";
    texCtx.roundRect(0, 0, 50, 50, [30, 10]);
    texCtx.fill();
    
    const main = document.querySelector("canvas");
    const ctx = main.getContext("2d");
    main.addEventListener("mousemove", (evt) => {
      ctx.clearRect(0, 0, main.width, main.height);
      const x = evt.clientX - main.offsetLeft - texture.width / 2;
      const y = evt.clientY - main.offsetTop - texture.height / 2;
      ctx.drawImage(texture, x, y);
    });
    ctx.drawImage(texture, 50, 50);
    canvas { border: 1px solid }
    <canvas></canvas>

    However having one full canvas + 2D context per texture is a bit consumptive memory wise, so instead you could create ImageBitmap objects from your canvas, but this is async, so you'd need to prepare them ahead of time.

    (async () => {
      const main = document.querySelector("canvas");
      main.width = 50;
      main.height = 50;
      const ctx = main.getContext("2d");
      ctx.fillStyle = "red";
      ctx.roundRect(0, 0, 50, 50, [30, 10]);
      ctx.fill();
      const texture = await createImageBitmap(main);
      main.width = 300;
      main.height = 150;
      main.addEventListener("mousemove", (evt) => {
        ctx.clearRect(0, 0, main.width, main.height);
        const x = evt.clientX - main.offsetLeft - texture.width / 2;
        const y = evt.clientY - main.offsetTop - texture.height / 2;
        ctx.drawImage(texture, x, y);
      });
      ctx.drawImage(texture, 50, 50);
    })();
    canvas { border: 1px solid }
    <canvas></canvas>

    And since it seems that you were hoping to use this texture as a fillStyle, then note that you can also use a canvas as source for createPattern():

    const main = document.querySelector("canvas");
    main.width = 50;
    main.height = 50;
    const ctx = main.getContext("2d");
    ctx.fillStyle = "red";
    ctx.roundRect(0, 0, 50, 50, [30, 10]);
    ctx.fill();
    const texture = ctx.createPattern(main, "no-repeat");
    main.width = 300;
    main.height = 150;
    ctx.fillStyle = texture;
    main.addEventListener("mousemove", (evt) => {
      ctx.resetTransform();
      ctx.clearRect(0, 0, main.width, main.height);
      const x = evt.clientX - main.offsetLeft - 50 / 2;
      const y = evt.clientY - main.offsetTop - 50 / 2;
      // Patterns position is always relative to the context's CTM
      // So we need to transform it rather than just using x, y of our path
      ctx.translate(x, y);
      ctx.fillRect(0, 0, 50, 50);
    });
    ctx.translate(50, 50);
    ctx.fillRect(0, 0, 50, 50);
    canvas { border: 1px solid }
    <canvas></canvas>