Using Vue.js to handwrite PowerPoint slides

All blog posts

TL;DR — We built a handwritten diagram editor web app based on MyScript cloud technology and using the Vue.js framework.

How can you handwrite a diagram in your browser, preview it and use it with any presentation builder?

This is what we will build throughout this article using MyScriptJS and the Vue.js framework.

Check out the web app online or find the source code on our GitHub repository .

preview

The web app features

Before we start diving into the details, here are the features that will be integrated.

As you may have seen on the demo, the application lets you use an existing diagram to show you some of the recognition possibilities or start fresh with an empty canvas.

Once on the edit view, several options are offered:

control

MyScriptJS, the front-end framework for handwritten recognition

The recognition is powered by MyScript Interactive Ink SDK, also called iink SDK, which is a cross-platform development toolkit provided by MyScript.

As this will be a web application, we will use the Javascript library MyScriptJS that uses MyScript Cloud to bring ink recognition to the web. You can find a complete guide about how this architecture works on the MyScript developer website but here is a quick explanation.

MyScriptJS is used to capture the strokes from any browser, using a pen, a stylus or a mouse. It will also manage the rendering informations (using svg or canvas) as the user writes on the editor. Using REST or WebSockets, MyScriptJS will manage the connection between the client and server and send the captured strokes as well as other parameters (color, thickness, instructions as undo / redo, etc.). Then, The server will process the ink and provide all the needed information to the client, such as: the recognized text, math, shapes, etc. in various formats depending on the chosen content type.

For the use case of our web application, we use what we call the batch mode (using REST). It means that we are not sending the strokes incrementally but in one go when the user asks for it. It also means that all the interactivity features are not available with the batch mode (but will be soon using Websockets). For a complete use of the features offered by iink SDK, you can check out our note-taking app Nebo.

Project structure and resources

To build this web app, we decided to use the Vue.js framework. This article assumes that you're familiar with it, or at least that you have an understanding of a progressive JavaScript framework.

Structure

We started using the vue-webpack-boilerplate template which provides a full-featured webpack setup for making a Vue app and allows you to start quickly without thinking too much about the configuration. The template provides various features for the development and the build.

You can find a complete documentation about the template as well as a detailed project structure explanation.

src_folder

We'll only talk here about the source folder structure.

  • src/ — main code
    • components/ — Vue components
    • router/ — Vue routes
    • utils/ — JavaScript files
    • event-bus.js — used to communicate between components
    • App.vue — application entry point
    • main.js — build entry point

Vue components

Let's talk a bit more about the Vue components. We separated the components folder into three subfolders as for the three main parts of the app:

  • start-page/ — the landing page, that will be the first thing users see (except on mobile due to responsive constraint)
  • edit-page/ — the editing page, that will be the main area where the user will have access to diagram editing
  • header/ — the header, that will be on both pages

The core of the app will be on the edit-page folder.

Project development

Integrate MyScriptJS to the Editor component

This part will focus on how to integrate MyScriptJS and bind events to make what we call our Editor component.

Template

  
    <template>
      <div :style="{display: displayStyle}" class="editor" @pointerdown="pointerDown" @loaded="loaded"
           @changed="changed($event)" @exported="exported($event)" touch-action="none" ref="editor">
      </div>
    </template>
  


The editor template is a single div with four event listeners:

  • Pointerdown, will be triggered every time a pointer becomes active
  • Loaded, will be triggered when the editor is ready and fully loaded
  • Changed, will be triggered every time the content changes and contains useful information about the editor possibilities (can clear, can redo, etc.)
  • Exported, will be triggered for each new export and will contain the exported data (svg and pptx for our diagram use case)

The loaded, changed and exported events are fired from MyScriptJS directly.

We also use the :ref directives as we need the dom element to initialize the editor with MyScriptJS.

Import

  
    import * as FileSaver from 'file-saver';
    import MyScript from 'myscript/dist/myscript.esm';
    import EventBus from '../../event-bus';
    import { attach, detach } from '../../utils/custom_grabber';
  


The main import here is MyScriptJS which is available as an ES6 module, it makes the import straightforward. We also import FileSaver to quickly save to pptx, our event bus to communicate between components and two functions, attach and detach that represent a custom grabber for the editor.

Manage the events of the editor

As we placed our event listeners using the Vue directives, we relied on the methods defined in the methods part of the Vue component.

For the pointerdown events, we want to emit an event to let other components know that a pointerdown was fired on the editor. We'll talk more about the components communication below.

  
    pointerDown() {
      EventBus.$emit('pointerDown');
    }
  

For the loaded events, we want to check if the user chose to import a diagram or started a new one. If diagramData exists, we will import it using the reDraw method of the editor.

  
    loaded() {
      if (this.diagramData) {
        this.editor.reDraw(this.diagramData.rawStrokes, this.diagramData.strokeGroups);
      }
    }
  

For the changed events, we will emit a changed event with an object as payload containing the initial event as well as an indication to know if the editor can clear or not (using the number of strokes in the editor model).

  
    changed(event) {
      EventBus.$emit('changed', {
        event,
        canClear: this.editor.model.rawStrokes.length > 0,
      });
    }
  

Exported events are a little more complex. We still want to emit an event for other components with an object as payload containing both the exports (here in svg and pptx format) and the client width and height (useful for the preview svg). The second part is used to save the export to pptx format if a save is requested. If the user click on the Save button before requesting the preview, we will do the export before saving the diagram.

  
    exported(event) {
      this.exports = event.detail.exports;
      EventBus.$emit('exported', {
        exports: event.detail.exports,
        clientWidth: this.clientWidth,
        clientHeight: this.clientHeight,
      });
      if (this.exports && this.exports.pptx && this.saveRequested) {
        const blob = new Blob([this.exports.pptx], {
          type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
        });
        FileSaver.saveAs(blob, 'myscript-diagram.pptx');
        this.saveRequested = false;
      }
    }
  

Manage the communication with other components

We use an event bus to ensure the communication between components. We will always attach listeners into the mounted options.

The undo / redo / clear listeners are used to call the respective methods of the editor and are emitted by other components (for example with a click on the undo button):

  
    EventBus.$on('undo', () => {
      this.editor.undo();
    });
  

We will not explain every listener in detail, as the code is self-explanatory for this part, but we will focus on the preview, save and thicknessUpdate events.

If the user clicks on the Preview button, the event bus will emit preview and the editor will listen for it. The preview means to hide the editor and ask for an export using the editor method and the requested mimetypes if the editor model contains strokes. If not, we will only ask for an empty svg.

  
    EventBus.$on('preview', () => {
      this.displayStyle = 'none';
      if (this.editor.model.rawStrokes.length > 0) {
        this.editor.export_(['image/svg+xml', 'application/vnd.openxmlformats-officedocument.presentationml.presentation']);
      } else {
        EventBus.$emit('clearSvg');
      }
    });
  

The save event is requested the same way as the preview one. Here, we use a library called FileSaver that allows us to quickly save a file into pptx format. We call the save only if we already have an export, otherwise we call the export method and specify that we requested a save. If the model is empty, we emit a showNotification event for our notification component used to display informative messages.

  
    EventBus.$on('save', () => {
      if (this.exports && this.exports.pptx) {
        const blob = new Blob([this.exports.pptx],
        {
          type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
        });
        FileSaver.saveAs(blob, 'myscript-diagram.pptx');
      } else if (this.editor.model.rawStrokes.length > 0) {
        this.editor.export_();
        this.saveRequested = true;
      } else {
        EventBus.$emit('showNotification', 'save');
      }
    });
  

The last event we will explain is thicknessUpdate. It happens when the user changes pen thickness. On the listener, we use the payload to get the thickness value. Then we can set the penStyle attribute of the editor with the color and the thickness represented by -myscript-pen-width.

  
    EventBus.$on('thicknessUpdated', (data) => {
      this.currentThickness = data.value;
      this.editor.penStyle = {
        color: this.currentColor ? this.currentColor : '',
        '-myscript-pen-width': this.currentThickness,
      };
    });
  

Navbar

The navbar contains all the major actions we can use to interact with the editor. We will not, across this article, explain all the controls as they are simple and often relie on emitting an event to inform the editor of the change.

We will take the pen settings for example and we will focus on the pen color options. The other examples can be found into this directory on GitHub.

The pen color itself is a component used inside the PenSettings component. We define a colors array containing our colors presets into the PenSettings data.

  
    data() {
      return {
      colors: ['#000000', '#808080', '#D9D9D9', '#1A8CFF', '#FF1A40', '#2BD965', '#FFDD33'],
        ...
      };
    }
  

The colors array will then be used to define our pen color in the PenSettings template.

  
    <template>
      <div class="nav-group">
        ...
        <div class="colors">
          <pen-color v-for="color in colors" :color="color" :checked="color === '#000000'" :key="color"/>
          <color-picker/>
        </div>
      </div>
    </template>
  

Let's dive into the PenColor component. The pen color template is composed of a button to detect the click and a span representing a tick to let the know user which color is selected.

  
    <template>
      <div class="pensettings">
        <button @click="changeColor" class="color" :style="style">
          <span v-if="colorChecked" class="check" :style="{ borderColor: tickColor }"></span>
        </button>
      </div>
    </template>
  

The background color is managed within a computed property using the color property we passed in PenSettings.

  
    computed: {
      style() {
        return `background-color: ${this.color}`;
      }
    }
  

On click, we use the changeColor() method that emits an event for the editor and then manages the tick color using the color brightness.

  
    changeColor() {
      EventBus.$emit('colorChanged', this.color);
      this.tickColor = blackOrWhiteTick(hexToRgb(this.color));
      this.colorChecked = true;
    }
  

Finally, we want to uncheck the color of the PenColor component, if another color is selected. To do so, we use the event bus as shown below:

  
    mounted() {
      EventBus.$on('colorChanged', () => {
        this.colorChecked = false;
      });
    }
  

Conclusion

What we've accomplished:

  • How to integrate MyScriptJS into a progressive JavaScript framework
  • How to interact with the editor
  • How to use the exported data for a real use case

The full code source is available on GitHub from where you can build and run the application.

We use cookies to ensure that we give you the best experience on our website • Read the privacy policy