Background image not showing on canvas in a strange way

Viewed 192

I'm coding a greeting card generator to train in VueJS 3. Everything is working correctly, apart from one thing, look at my code:

<template>
  <div>
    <h1>greeting card generator</h1>
    <div class="board">
        <canvas id='myCanvas' :width="size.w" :height="size.h" tabindex='0'
style="border:1px solid #000000;"
        ></canvas>
    </div>
    <textarea
      :style="'width:' + size.w + 'px; resize:none;'"
      v-model="texte"
      placeholder="Write your text here">
    </textarea>
  </div>
</template>

<script>

import {
  defineComponent, onMounted, ref, reactive, watch,
} from 'vue';

export default defineComponent({
  setup() {
    const myCanvas = ref(null);
    const texte = ref('');
    const rapport = ref(0);

    const size = reactive({
      w: window.innerWidth * 0.8,
      h: (window.innerWidth * 0.8) / 1.8083832335329342,
    });

    function drawText() {
      const fontSize = 0.05 * window.innerWidth - 10;
      myCanvas.value.font = `${fontSize}px Adrip`;
      myCanvas.value.textAlign = 'center';
      const x = size.w / 2;
      const lineHeight = fontSize;
      const lines = texte.value.split('\n');
      for (let i = 0; i < lines.length; i += 1) {
        myCanvas.value.fillText(
          lines[lines.length - i - 1],
          x,
          (size.h * 0.98) - (i * lineHeight),
        );
      }
    }

    function initCarte() {
      const background = new Image();
      background.src = '/img/fond.jpeg';
      background.onload = function () {
        rapport.value = background.naturalWidth / background.naturalHeight;
        size.h = size.w / rapport.value;
        try {
          myCanvas.value.drawImage(background, 0, 0, size.w, size.h);
        } catch (e) {
          console.log(`ERREUR DE CHARGEMENT D'IMAGE: ${e}`);
        }
        drawText();
      };
    }

    function handleResize() {
      size.w = window.innerWidth * 0.8;
      size.h = size.w / rapport.value;
      initCarte();
    }

    window.addEventListener('resize', handleResize);

    onMounted(() => {
      const c = document.getElementById('myCanvas');
      const ctx = c.getContext('2d');
      myCanvas.value = ctx;
      initCarte();
    });

    watch(texte, () => {
      initCarte();
    });

    return {
      myCanvas,
      size,
      texte,
    };
  },
});

</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
@font-face {
  font-family: 'Adrip';
  src: local('Adrip'), url('/fonts/adrip1.ttf') format('truetype');
}

#myCanvas {
  border: 1px solid grey;
}
</style>

Look at this line:

h: (window.innerWidth * 0.8) / 1.8083832335329342,

If I don't hardcode this and only put the canonical value window.innerWidth * 0.8, the image doesn't display, although the size.h = size.w / rapport.value; line executes correctly.

I really don't understand this behaviour, could somebody explain it to me? Also, if anybody has a clue on how it would be possible to load the image once and for all so that I don't have to load it at every refresh, it would be better :)

Thanks in advance!

1 Answers

Your problem is that you change the size of the canvas after drawing the image, due to how templating magic works. If you put debugger behind drawText(); in the background onload function, you will see that it actually draws the image. However, in this same function, you set size.h. size is reactive, and is thus marked as "dirty". size is also used in the template, so the template is marked dirty. After the onload function is executed, Vue will rerender your template... and erase your image.

I think your best bet here is to use nextTick. You need to use it sparingly, but I think this is one of the instances where you have no choice but to wait for the DOM to settle. To do this, import nextTick from vue:

import { nextTick } from 'vue';

Then surround your drawImage try-catch block with that.

background.onload = function () {
  rapport.value = background.naturalWidth / background.naturalHeight;
  size.h = size.w / rapport.value;
  nextTick(() => {
    try {
      myCanvas.value.drawImage(background, 0, 0, size.w, size.h);
    } catch (e) {
      console.log(`ERREUR DE CHARGEMENT D'IMAGE: ${e}`);
    }
    drawText();
  });
};

As for your last question how to load the image once... the short answer is... you can't. Whenever the canvas changes, you need to redraw it. At least the image should be cached by the browser, so it just draws it from cache rather than doing another http request.

Edit Background image not showing on canvas in a strange way

Related