Tutorial

How To Control the HTML5 Canvas with Vue.js

Updated on July 27, 2021
Default avatar

By Joshua Bemenderfer

How To Control the HTML5 Canvas with Vue.js

Introduction

Most of the time, you will write Vue.js components that interact with a webpage via the DOM. But Vue’s reactivity system is useful for more than that!

In this article, we’ll create a set of components to render a basic bar chart with Vue, in HTML5 canvas.

Prerequisites

To complete this tutorial, you will need:

This tutorial was verified with Node v16.5.0, npm v7.20.0, and vue v2.6.11.

Step 1 — Setting Up the Project

You can start this project with @vue/cli.

  1. npx @vue/cli create vue-canvas-example --default

Then, navigate to the new project directory:

  1. cd vue-canvas-example

Then, replace the contents of your App.vue component with the following code:

src/App.vue
<template>
  <div id="app">
    <h2>Bar Chart Example</h2>
    <my-canvas style="width: 100%; height: 600px;">
      <my-box
        v-for="(obj, index) of chartValues"
        :key=index
        :x1="(index / chartValues.length) * 100"
        :x2="(index / chartValues.length) * 100 + 100 / chartValues.length"
        :y1="100"
        :y2="100 - obj.val"
        :color="obj.color"
        :value="obj.val"
      >
      </my-box>
    </my-canvas>
  </div>
</template>

<script>
import MyCanvas from './components/MyCanvas.vue';
import MyBox from './components/MyBox.vue';

export default {
  name: 'app',
  components: {
    MyCanvas,
    MyBox,
  },

  data() {
    return {
      chartValues: [
        { val: 24, color: 'red' },
        { val: 32, color: '#0f0' },
        { val: 66, color: 'rebeccapurple' },
        { val: 1, color: 'green' },
        { val: 28, color: 'blue' },
        { val: 60, color: 'rgba(150, 100, 0, 0.2)' },
      ],
    };
  },

  mounted() {
    let dir = 1;
    let selectedVal = Math.floor(Math.random() * this.chartValues.length);

    setInterval(() => {
      if (Math.random() > 0.995) dir *= -1;
      if (Math.random() > 0.99)
        selectedVal = Math.floor(Math.random() * this.chartValues.length);

      this.chartValues[selectedVal].val = Math.min(
        Math.max(this.chartValues[selectedVal].val + dir * 0.5, 0),
        100
      );
    }, 16);
  },
};
</script>

<style>
html,
body {
  margin: 0;
  padding: 0;
}

#app {
  position: relative;
  height: 100vh;
  width: 100vw;
  padding: 20px;
  box-sizing: border-box;
}
</style>

This is the app template and it uses setInterval and Math.random() to update the chart values every 16 milliseconds.

MyCanvas and MyBox are the two custom components. Values for my-box are percentages of the width of the canvas. Each bar will take up an equal space of the canvas.

Step 2 — Building the Canvas Component

The canvas component creates a canvas element and injects the canvas rendering context into all of its child components via a reactive provider.

src/components/MyCanvas.vue
<template>
  <div class="my-canvas-wrapper">
    <canvas ref="my-canvas"></canvas>
    <slot></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      provider: {
        context: null,
      },
    };
  },

  provide() {
    return {
      provider: this.provider,
    };
  },

  mounted() {
    this.provider.context = this.$refs['my-canvas'].getContext('2d');

    this.$refs['my-canvas'].width = this.$refs[
      'my-canvas'
    ].parentElement.clientWidth;
    this.$refs['my-canvas'].height = this.$refs[
      'my-canvas'
    ].parentElement.clientHeight;
  },
};
</script>

By creating the provider in the data property, it becomes reactive, so child components will update when context changes. That will be the CanvasRenderingContext that children will draw to.

provide() allows any child component to inject: ['provider'] and have access to it.

We can’t access the rendering context until the canvas is mounted to the DOM. Once we have it, we provide it to all child components.

Then, resize the canvas to fit its parent’s width. Normally you’d use a more flexible resize system.

Step 3 — Building the Box Component

MyBox.vue is where the magic happens. It’s an abstract component, not a “real” one, so it doesn’t actually render to the DOM.

Note: There is template or styles in this component.

Instead, in the render function, we use normal canvas calls to draw on the injected canvas. As a result, each component still re-renders when their properties change without any extra work.

components/MyBox.vue
<script>
const percentWidthToPix = (percent, ctx) =>
  Math.floor((ctx.canvas.width / 100) * percent);
const percentHeightToPix = (percent, ctx) =>
  Math.floor((ctx.canvas.height / 100) * percent);

export default {
  inject: ['provider'],

  props: {
    x1: {
      type: Number,
      default: 0,
    },
    y1: {
      type: Number,
      default: 0,
    },
    x2: {
      type: Number,
      default: 0,
    },
    y2: {
      type: Number,
      default: 0,
    },
    value: {
      type: Number,
      defualt: 0,
    },
    color: {
      type: String,
      default: '#F00',
    },
  },

  data() {
    return {
      oldBox: {
        x: null,
        y: null,
        w: null,
        h: null,
      },
    };
  },

  computed: {
    calculatedBox() {
      const ctx = this.provider.context;

      const calculated = {
        x: percentWidthToPix(this.x1, ctx),
        y: percentHeightToPix(this.y1, ctx),
        w: percentWidthToPix(this.x2 - this.x1, ctx),
        h: percentHeightToPix(this.y2 - this.y1, ctx),
      };

      // eslint-disable-next-line vue/no-side-effects-in-computed-properties
      this.oldBox = calculated;

      return calculated;
    },
  },

  // eslint-disable-next-line vue/require-render-return
  render() {
    if (!this.provider.context) return;

    const ctx = this.provider.context;
    const oldBox = this.oldBox;
    const newBox = this.calculatedBox;

    ctx.beginPath();
    ctx.clearRect(oldBox.x, oldBox.y, oldBox.w, oldBox.h);
    ctx.clearRect(newBox.x, newBox.y - 42, newBox.w, 100);

    ctx.rect(newBox.x, newBox.y, newBox.w, newBox.h);
    ctx.fillStyle = this.color;
    ctx.fill();

    ctx.fillStyle = '#000';
    ctx.font = '28px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(
      Math.floor(this.value),
      newBox.x + newBox.w / 2,
      newBox.y - 14
    );
  },
};
</script>

percentWidthToPix and percentHeightToPix are helper functions to convert a percentage of canvas area to pixels.

inject: ['provider'] gets us the provider property from the parent <my-canvas> component.

Since the parent canvas has to mount first, it’s possible that the context may not be injected by the time the render() function runs the first time. Check to see if this.provider.context is defined.

oldBox is used to cache the dimensions of the previous render so that we can clear the area before we re-calculate calculatedBox on the next render.

Note: This does introduce side-effects, but is suitable for the needs of a tutorial. To avoid an ESLint error, we use eslint-disable-next-line.

Save the changes and run your application:

npm run serve

Open the application in the browser:

HTML5 Canvas bar chart rendered with Vue.js.

This is a bar chart drawn on HTML5 canvas with Vue reactivity.

Conclusion

In this article, you created a set of components to render a basic bar chart with Vue, in HTML5 canvas.

This method could be used for any sort of canvas rendering, or even 3D content with WebGL or WebVR! Use your imagination!

Continue your learning with a challenge: try to add individual event handling by passing the dimensions of each box to the injected provider and have the parent canvas decide where to dispatch events.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Joshua Bemenderfer

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel