DEV Community

Himani Mehra
Himani Mehra

Posted on

How I created my own digital cursive writing notebook..

After successfully hosting a project (Paletto). I started searching for a new project idea. One day, my friend ordered a pen tablet for note-taking. We were both excited to use it and opened MSPaint and Google Keep.
We tried sketching, writing words, using technical jargon, and creating flowcharts, arrows, and other figures commonly used in note-making. It felt like being a toddlers, knowing what to write but struggling with how to write. Then searched for a site where we could practice alphabets, numbers, or sketches but only plain canvas was available. And from there emerged an idea for my next project i.e. Practice Digital Handwriting.

Image description

Now this is unlike my previous project (https://www.paletto.org/), where I made a blueprint and noted down what users were unsatisfied with competitors, here the only thing I searched was the user base/customer demographic that will use the product and found out they were one of these:-

  1. Online teachers/educators
  2. Students/professionals making notes (The very purpose, my friend bought the pen tablet)
  3. School kids might be practicing cursive writing online as well

In this project, idea was clear to give users a canvas space for them to practice and improve their handwriting. This is where I went wrong in planning and organizing my project as I directly started coding without making a blueprint of what my landing page would be, would it be directly canvas or a welcome page. I have written about** Developing Forward Visionary** in my blog for Domain Hosting.
Moving on to the technical details, We will be going backward i.e. from the final page to the landing page, as I want to keep a real experience rather than showcasing to the reader that it is a smooth journey like we first go to landing page then click on this card then canvas is displayed "NO"
The landing page was developed at last😅.
Now we will go through the actual journey of development. I am going to follow a template because the final goal was to have a better, easy and smooth user experience:

  1. What I coded
  2. Found out the flaws during critical analysis of code and UI.
  3. Working on the flaws and improving the code and UI.

So first I coded a plain canvas and hardcoded alphabets (A-Z) on it.

Flaws:

1.) I was able to write with the mouse but when I was trying to
write with a pen table (which was the center point of making
this project). The cursor was getting disappeared, in
technical terms, the event listeners was unable to recognize
this event.
2.) I hardcoded the alphabet, leaving users with no choice of
practicing anything but capital alphabets.
3.) The color of the alphabets in the canvas was too dark and I
was unable to see the letters I had traced.
4.) The default font was Sans-serif, but the font has to be
something related to free handwriting having free and loose
strokes.
5.) While making any mistake, there was no option to erase or
clear the screen and most importantly user might want to see
only his traced part and not the printed alphabets on canvas.

So to tackle the first and the prime issue I searched and found mousedown, mousemove, mouseup would not work for an external connected pen.
Extra event listners that needs to be added in the code were touchstart, touchmove, touchend:

canvas.addEventListener('mousedown', handleWritingStart);
canvas.addEventListener('mousemove', handleWritingInProgress);
canvas.addEventListener('mouseup', handleDrawingEnd);
canvas.addEventListener('mouseout', handleDrawingEnd);

canvas.addEventListener('touchstart', handleWritingStart);
canvas.addEventListener('touchmove', handleWritingInProgress);
canvas.addEventListener('touchend', handleDrawingEnd);
clearButton.addEventListener('click', handleClearButtonClick);

function getMosuePositionOnCanvas(event) {
  const clientX = event.clientX || event.touches[0].clientX;
  const clientY = event.clientY || event.touches[0].clientY;
  const { offsetLeft, offsetTop } = event.target;
  const canvasX = clientX - offsetLeft;
  const canvasY = clientY - offsetTop;
  return { x: canvasX, y: canvasY };
}
Enter fullscreen mode Exit fullscreen mode

To tackle the second flaw, brainstorming was done around how many options should be provided, and decided to provide users with the following cards to practice:

1.) Uppercase alphabets (A-Z)
2.) Lowercase alphabets (a-z)
3.) Numbers (0–9)
4.) Special characters
5.) Giving an upload option so that the user can upload any image
and practice sketching.

Now to give these many options and have spaceous writing areas, I took inspiration from my previous project Paletto, to divide the screen in the left and right section. The leftsection is scrollable and has all the option cards whereas the right section is fully dedicated to writing practice:

Image description

<div class="container hidden" id="first-card-section">
        <div class="left-section" id="options">
            <div class="paper-section" onchange="startPractice()">
                <div class="option_paper" data-option="uppercase">ABC</div>
                <div class="option_paper" data-option="lowercase">abc</div>
                <div class="option_paper" data-option="numbers">123</div>
                <div class="option_paper" data-option="special">$*/</div>
                <div class="option_paper" data-option="upload">
                    <input type="file" id="imageInput" accept="image/*">Upload </div>
        </div>
        <div id="right-section" class="right-section">
            <div class="canvas-space">
                <canvas id="drawing-area" class="drawing-area" height="500" width="950"></canvas>
                <canvas id="overlay-canvas" class="drawing-area" height="500" width="950"></canvas>
            </div>
        </div>
Enter fullscreen mode Exit fullscreen mode

JS code:-

function startPractice(selectedOption) {
  const canvas = document.getElementById("drawing-area");
  const ctx = canvas.getContext("2d");
  let content = "";
  if (selectedOption === "uppercase") {
    content = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  } else if (selectedOption === "lowercase") {
    content = "abcdefghijklmnopqrstuvwxyz";
  } else if (selectedOption === "numbers") {
    content = "0123456789";
  } else if (selectedOption === "special") {
    content = "!@#$%^&*(){}[]:;?/<>";
  } else if (selectedOption === "image") {
    const img = new Image();
    img.onload = function () {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    };
    img.src = "./images/sample.png";
  }
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.font = '30px "classic notes", cursive';
  ctx.textBaseline = "middle";
  ctx.textAlign = "center";
  const numPerRow = 6;
  const charWidth = canvas.width / numPerRow;
  const charHeight = canvas.height / Math.ceil(content.length / numPerRow);
  for (let i = 0; i < content.length; i++) {
    const char = content[i];
    const col = i % numPerRow;
    const row = Math.floor(i / numPerRow);
    const x = col * charWidth + charWidth / 2;
    const y = row * charHeight + charHeight / 2;
    ctx.fillText(char, x, y);
  }
}
Enter fullscreen mode Exit fullscreen mode

To tackle the third flaw , I reduced the opacity of the canvas area. But a major issue arose from it, the opacity of the tracing pen also reduced, and again both the opacity being the same I was unable to see and differentiate between writing and printed letters.
So from there emerged the idea of having two canvases (one over the other) rather than one.

The first canvas will have the printing stuff (letters, numbers etc) with low opacity and the second canvas will be placed exactly over the first one having opacity as 1( i.e the written stuff would be fully visible):

<div class="canvas-space">
      <canvas id="drawing-area" class="drawing-area" height="500" width="950"></canvas>
      <canvas id="overlay-canvas" class="drawing-area" height="500" width="950"></canvas>
  </div>
Enter fullscreen mode Exit fullscreen mode

To tackle the Forth flaw ** of font-family, I googled what are the most commonly used freehand writing fonts and got "Classic Notes.ttf", "Classic Notes.ttf" and "Classic Notes.ttf". I downloaded all these files (can view the code and all the files in my **Github). Found Classic Notes.ttf to be the most suitable for free and loose strokes.

.dropdown-container {
    height: 50px;
    width: 150px;
    border-radius: 10px;
    background-color: #517dd4;
    color: white;
    font-size: 20px;
    font-family:"classic notes";
}
  @font-face {
    font-family:"Life-Lessons";
    src: url("Life-Lessons.ttf") format("truetype");
  }
Enter fullscreen mode Exit fullscreen mode

To tackle the fifth flaw, it was easy to identify the basic tools required for a canvas as we have been using various graphics editors like MS Paint since the early stages of our school. I came to the conclusion to have 3 options:-

1.) Clear all- To clear the canvas and give a fresh writing area
to the user.
2.) Eye icon- To hide/show the printed stuff(letters, numbers,
etc) allowing users to see and compare their
handwriting with the printed ones.
3.) Color Gradient- As there is an image upload option, the user
may use different colors to sketch out the
uploaded image.

So these three items were kept in a widget design and after coding it , during the testing process found out that everytime for clearing or using any of the three icon I have to move my hand to a significant distance towards right in the pen tablet. Hence I decided the make the widget draggable/ movable throughout the screen for easy access and better user experience:

<div id="draggable-container">
            <div class="options-container">
                <button id="clear-button" class="clear-button" type="button"> <img class="eraser" src="./images/eraser.png" />
                </button>
                <button id="hide-canvas" class="hide-canvas" type="button"> <img class="eraser" src="./images/hide.png" />
                </button>
                <button id="color-button" class="color-button" type="button"> <img class="paint" src="./images/color.png" />
                    <input type="color" id="colorPicker" style="display: none;"></button>
            </div>
        </div>
Enter fullscreen mode Exit fullscreen mode

CSS:-

#draggable-container {
    user-select: none;
    cursor: pointer;
    touch-action: none;
    position: absolute;
    top: 17%;
    left: 84%;
    transform: translate(0, 0);
    user-select: none;
    cursor: pointer;
    touch-action: none;
}
Enter fullscreen mode Exit fullscreen mode

Images showing the feature for eye icon working:

traced some alphabets:-
Image description

used the eye icon (hide/show) feature to compare:-

Image description

var isDragging = false;
var offset = { x: 0, y: 0 };
var draggableContainer = document.getElementById("draggable-container");
draggableContainer.addEventListener("mousedown", (e) => {
  isDragging = true;
  offset.x = e.clientX - draggableContainer.getBoundingClientRect().left;
  offset.y = e.clientY - draggableContainer.getBoundingClientRect().top;
});
draggableContainer.addEventListener("mousemove", (e) => {
  if (isDragging) {
    draggableContainer.style.left = e.clientX - offset.x + "px";
    draggableContainer.style.top = e.clientY - offset.y + "px";
  }
});
document.addEventListener("mouseup", () => {
  isDragging = false;
});
draggableContainer.addEventListener("touchstart", (e) => {
  isDragging = true;
  offset.x = e.touches[0].clientX - draggableContainer.getBoundingClientRect().left;
  offset.y = e.touches[0].clientY - draggableContainer.getBoundingClientRect().top;
});
draggableContainer.addEventListener("touchmove", (e) => {
  if (isDragging) {
    e.preventDefault();
    draggableContainer.style.left = e.touches[0].clientX - offset.x + "px";
    draggableContainer.style.top = e.touches[0].clientY - offset.y + "px";
  }
});
document.addEventListener("touchend", () => {
  isDragging = false;
});
Enter fullscreen mode Exit fullscreen mode

In the above JS code, notice "isDragging" is set to false by default and then added event listeners to make to movable throughout the screen.

After this much, I was happy to finish the project, but soon some questions popped up in my mind:

1.) What is the user wants to try one alphabet multiple times,
because its not possible to get a grip of anything in one
shot.
2.) I forgot to cover cursive writing templates for
"kindergartners" or "preschoolers."

Now this was a tricky situation, honestly I started feeling overwhelmed 😓.
Because again there were multiple tasks to be done like for individual aphabets , canvas only displaying 'A' let say 20 times, then 'B' till 'Z'. Then canvas only displaying 'a' let say 20 times, then 'b' till 'z'. Then canvas only displaying '0' let say 20 times, then '1' till '9' and so on for special characters. So I need to have 26 cards (uppercase alphabets) + 26 cards (lowercase alphabets) + 10 cards(numbers) + 20 cards(special characters : "! @ # $ % ^ & * ( ) { } [ ] : ; ? / < > ") and how can I forget the cursive writing template.

After sometime I took a breath and said lets breakdown the problem and pick the alphabets part, lets achive it first and applied a for loop to form a grid of 26 upper case alphabets:

Image description

JS code:-

for (let asciiCode = 65; asciiCode <= 90; asciiCode++)
Enter fullscreen mode Exit fullscreen mode

HTML:-

<div class="image-grid" id="image-grid-1"></div>
Enter fullscreen mode Exit fullscreen mode

CSS:-

.image-grid {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-around;
      }

      .card {
        flex: 0 0 calc(12.5% - 10px);
        margin: 7px;
        box-sizing: border-box;
        width: 95px;
        height: 120px;
        word-break: break-all;
      }

      .card img {
        width: 100%;
        height: 100%;
        cursor: pointer;
      }
Enter fullscreen mode Exit fullscreen mode

Now, during this process, I figured out the UX for displaying all the cards and that was to have another grid and yes that was the landing page of my project (which initially I talked about that was developed at last😅).

So now the flow is: on landing page of the project, user will see a grid like:

Image description

Then on-click on any card, we will get the second grid (will show some):

uppercase grid:-
Image description

special characters grid:-

Image description

cursive writing template grid:-

Image description

Now on click on any card say from second card (lowercase) of the landing page , you selected letter 'b' to practice. So in the canvas it will be displayed like:

Image description

HTML:-

<div class="card-grid">
        <div class="card" data-option="uppercase" id="uppercase-card">ABC</div>
        <div class="card" data-option="lowercase" id="lowercase-card">abc</div>
        <div class="card" data-option="numbers" id="numbers-card">123</div>
        <div class="card" data-option="special" id="special-card">$*/</div>
        <div class="card" data-option="upload">Upload</div>
        <div class="card gap-class" data-option="image" id="image-card-1"> Cursive Writing</div>
    </div>
    <div class="image-grid" id="image-grid-1"></div>

    <div class="second-card-grid" onclick="showSubHeading()">

    </div>
    <div class="sub-headings" id="subheadingEl">
        <span class="uppercase_second_card common" onclick="redirectToUppercaseCards()">A-Z</span>
        <span class="lowercase_second_card common" onclick="redirectToLowercaseCards()">a-z</span>
        <span class="numbers_second_card common" onclick="redirectToSpecialcaseCards()">0-9</span>
    </div>
Enter fullscreen mode Exit fullscreen mode

JS:-

// JS code for uppercase , similarly others were achived
document.getElementById('uppercase-card').addEventListener('click', displayUppercaseCharacters);

function displayUppercaseCharacters() {
  const secondCardGrid = document.querySelector('.second-card-grid');
  const container = document.querySelector('#first-card-section');
  const cardGrid = document.querySelector('.card-grid');
  let canvas = document.getElementById('drawing-area');
  let ctx = canvas.getContext('2d');
  secondCardGrid.innerHTML = '';

  for (let asciiCode = 65; asciiCode <= 90; asciiCode++) {
    const char = String.fromCharCode(asciiCode);
    const card = document.createElement('div');
    card.classList.add('card');
    card.textContent = char;
    card.addEventListener('click', function () {
      container.classList.remove('hidden');
      cardGrid.style.display = 'none';
      secondCardGrid.style.display = 'none';
      const lettersPerRow = 7;
      const letterSize = 28;
      const canvasWidth = canvas.width;
      const canvasHeight = canvas.height;
      const columnSpacing = canvasWidth / lettersPerRow;
      const rowSpacing = canvasHeight / Math.ceil(26 / lettersPerRow);
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      for (let j = 0; j < 26; j++) {
        const row = Math.floor(j / lettersPerRow);
        const col = j % lettersPerRow;
        const x = col * columnSpacing + (columnSpacing - letterSize) / 2;
        const y = row * rowSpacing + (rowSpacing - letterSize) / 2;
        ctx.font = `40px "classic notes", cursive`;
        ctx.fillText(char, x, y);
      }
    });
    secondCardGrid.appendChild(card);
  }
}
Enter fullscreen mode Exit fullscreen mode

Similarly for all items (Refer my Github to see the logic). And that's how I achieved a comprehensive and detailed flow. Most importantly, now it was organized. Now one more thing, say a user selects wrong card my mistake and want to choose another one so to achieve this functionality I kept some options on the top of the canvas :

Image description

HTML:-

<div class="sub-headings" id="subheadingEl">
        <span class="uppercase_second_card common" onclick="redirectToUppercaseCards()">A-Z</span>
        <span class="lowercase_second_card common" onclick="redirectToLowercaseCards()">a-z</span>
        <span class="numbers_second_card common" onclick="redirectToSpecialcaseCards()">0-9</span>
    </div>
Enter fullscreen mode Exit fullscreen mode

JS:-

function redirectToUppercaseCards() {
  const container = document.querySelector('#first-card-section');
  container.style.display = 'none';
  const secondCardGrid = document.querySelector('.second-card-grid');
  secondCardGrid.style.display = 'block'
}

function redirectToLowercaseCards() {
  const container = document.querySelector('#first-card-section');
  container.style.display = 'none';
  const secondCardGrid = document.querySelector('.second-card-grid');
  secondCardGrid.style.display = 'block'
}

function redirectToSpecialcaseCards() {
  const container = document.querySelector('#first-card-section');
  container.style.display = 'none';
  const secondCardGrid = document.querySelector('.second-card-grid');
  secondCardGrid.style.display = 'block'
}
Enter fullscreen mode Exit fullscreen mode

To see the github process of pushing the code, coding practices such as constantly pushing the code refer my article on Paletto and to learn domain hosting (host the project live) refer the article on Domain Hosting.

Some learnings:

Initially when the project was started, I was not aware of the fact that it can expand to this extent, hence made it in simple JS, HTML,CSS.
Later when it expanded , realized that it would have been easier if a framework is used like Vue.js, react etc as it has a concept of different components. So the three pages (first grid , second grid , canvas area) were three different components (say three different vue files) , and on back button we need to just hide/show the components.

Conclusion

:
Concluding this journey, an idea originated from the inability to write and affinity to improve, finding a tool for it , and when not getting a complete package to practice, developed one not only for myself but for a larger user base. There were some hiccups, and obstcales during the project development, some later realization to use generic code and frameworks, but its fine these learning will be helpful in making the next project easier to develope, organised, and successful.

I have improved my writing a lot and have started making online notes.What about you?😄

Image description

Top comments (0)