TypeScript Tutorial: Creating a Simple Interactive Drawing App

Ever wanted to create your own digital art or sketchpad application? In this tutorial, we’ll dive into the world of TypeScript and build a simple, yet functional, interactive drawing app. This project is perfect for beginners and intermediate developers looking to enhance their TypeScript skills while creating something fun and practical. We’ll cover everything from setting up the project to handling user input and drawing on the canvas.

Why Build a Drawing App?

Building a drawing app is a fantastic way to learn about several key concepts in web development and TypeScript. You’ll gain practical experience with:

  • Canvas Manipulation: Working with the HTML5 canvas element, drawing shapes, and handling pixel data.
  • Event Handling: Responding to user interactions like mouse clicks, mouse movements, and touch events.
  • Object-Oriented Programming (OOP): Structuring your code using classes and objects to create reusable and maintainable components.
  • TypeScript Fundamentals: Applying types, interfaces, and other TypeScript features to improve code quality and readability.

Moreover, building this app provides a tangible project to showcase your skills and experiment with different features, such as color pickers, brush sizes, and more advanced drawing tools.

Setting Up the Project

Before we start coding, let’s set up our project. We’ll be using a simple HTML file, a TypeScript file, and a way to compile the TypeScript code into JavaScript. Here’s a step-by-step guide:

  1. Create a Project Directory: Create a new directory for your project, e.g., `drawing-app`.
  2. Initialize npm: Open your terminal, navigate to the project directory, and run `npm init -y`. This creates a `package.json` file.
  3. Install TypeScript: Install TypeScript as a development dependency by running `npm install –save-dev typescript`.
  4. Create TypeScript Configuration: Generate a `tsconfig.json` file by running `npx tsc –init`. This file configures the TypeScript compiler. You can customize this file, but for this tutorial, the default settings will work fine.
  5. Create HTML File: Create an `index.html` file in your project directory. Add the following basic HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Drawing App</title>
    <style>
        #canvas {
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <canvas id="canvas" width="800" height="600"></canvas>
    <script src="./dist/index.js"></script>
</body>
</html>
  1. Create TypeScript File: Create a file named `index.ts` in your project directory. This is where we’ll write our TypeScript code.
  2. Compile TypeScript: Open your terminal and run `npx tsc` in your project directory. This command compiles your TypeScript code into JavaScript, creating a `dist` directory with an `index.js` file.
  3. Link JavaScript: In your `index.html` file, make sure the “ tag points to the generated `index.js` file in the `dist` directory.

Basic Canvas Setup in TypeScript

Now, let’s start writing some TypeScript code to get our canvas element and set up the drawing context. Open `index.ts` and add the following code:


const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;

// Set initial drawing properties
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;

console.log('Drawing app initialized!');

Let’s break down this code:

  • `const canvas = document.getElementById(‘canvas’) as HTMLCanvasElement;`: This line retrieves the canvas element from the HTML and casts it to an `HTMLCanvasElement`. This allows us to use canvas-specific properties and methods. The `as HTMLCanvasElement` is a type assertion, telling TypeScript to treat the element as a canvas element.
  • `const ctx = canvas.getContext(‘2d’)!;`: This gets the 2D rendering context of the canvas. The `!` is a non-null assertion operator, which tells TypeScript that we are sure the context will not be null.
  • `ctx.strokeStyle = ‘black’;` and `ctx.lineWidth = 2;`: These lines set the initial drawing properties for the stroke color (black) and line width (2 pixels).
  • `console.log(‘Drawing app initialized!’);`: This confirms that the script is running and the canvas is set up correctly.

After saving `index.ts`, run `npx tsc` in your terminal to compile the TypeScript code. Open `index.html` in your browser. You should see an empty canvas with a black border.

Handling Mouse Events

Next, we need to handle mouse events to allow the user to draw on the canvas. We’ll listen for `mousedown`, `mousemove`, and `mouseup` events. Add the following code to `index.ts`:


let isDrawing = false;
let lastX = 0;
let lastY = 0;

canvas.addEventListener('mousedown', (e: MouseEvent) => {
  isDrawing = true;
  [lastX, lastY] = [e.offsetX, e.offsetY];
});

canvas.addEventListener('mousemove', (e: MouseEvent) => {
  if (!isDrawing) return;

  ctx.beginPath();
  ctx.moveTo(lastX, lastY);
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.stroke();
  [lastX, lastY] = [e.offsetX, e.offsetY];
});

canvas.addEventListener('mouseup', () => {
  isDrawing = false;
});

canvas.addEventListener('mouseout', () => {
  isDrawing = false;
});

Let’s break down the mouse event handling code:

  • `let isDrawing = false;`: A boolean variable to track whether the mouse button is pressed.
  • `let lastX = 0;` and `let lastY = 0;`: Variables to store the last known mouse coordinates.
  • `mousedown` event: When the mouse button is pressed, `isDrawing` is set to `true`, and the starting coordinates are stored in `lastX` and `lastY`.
  • `mousemove` event: If `isDrawing` is `true`, this event draws a line from the previous mouse position (`lastX`, `lastY`) to the current mouse position (`e.offsetX`, `e.offsetY`). The current coordinates are then updated.
  • `mouseup` and `mouseout` events: When the mouse button is released or the mouse leaves the canvas, `isDrawing` is set to `false`, stopping the drawing.

Compile the code, refresh your browser, and you should now be able to draw on the canvas by holding down the left mouse button and moving the mouse.

Adding Color and Brush Size Controls

To make our drawing app more versatile, let’s add controls for color and brush size. We’ll add HTML elements for these controls and then update our TypeScript code to handle them.

  1. Add HTML Controls: Modify your `index.html` file to include a color picker and a range input for brush size. Place these controls above the canvas:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Drawing App</title>
    <style>
        #canvas {
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <input type="color" id="colorPicker" value="#000000">
    <input type="range" id="brushSize" min="1" max="20" value="2">
    <canvas id="canvas" width="800" height="600"></canvas>
    <script src="./dist/index.js"></script>
</body>
</html>
  1. Get Control Elements in TypeScript: In `index.ts`, get references to the color picker and brush size elements:

const colorPicker = document.getElementById('colorPicker') as HTMLInputElement;
const brushSize = document.getElementById('brushSize') as HTMLInputElement;
  1. Add Event Listeners for Controls: Add event listeners to update the drawing properties when the color or brush size changes:

colorPicker.addEventListener('change', () => {
  ctx.strokeStyle = colorPicker.value;
});

brushSize.addEventListener('change', () => {
  ctx.lineWidth = parseInt(brushSize.value, 10);
});

Here’s what the control code does:

  • `colorPicker.addEventListener(‘change’, () => { … });`: This event listener listens for changes in the color picker’s value and updates the `strokeStyle` of the drawing context to the selected color.
  • `brushSize.addEventListener(‘change’, () => { … });`: This event listener listens for changes in the brush size and updates the `lineWidth` of the drawing context to the selected brush size. `parseInt(brushSize.value, 10)` converts the string value from the range input to an integer.

Compile the code, refresh your browser, and you should now be able to change the drawing color and brush size using the new controls.

Adding a Clear Button

Let’s add a button to clear the canvas. This is a common feature in drawing apps and is easy to implement.

  1. Add the Clear Button in HTML: Add a button to your `index.html` file:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Drawing App</title>
    <style>
        #canvas {
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <input type="color" id="colorPicker" value="#000000">
    <input type="range" id="brushSize" min="1" max="20" value="2">
    <button id="clearButton">Clear Canvas</button>
    <canvas id="canvas" width="800" height="600"></canvas>
    <script src="./dist/index.js"></script>
</body>
</html>
  1. Get the Clear Button in TypeScript: In `index.ts`, get a reference to the clear button:

const clearButton = document.getElementById('clearButton') as HTMLButtonElement;
  1. Add an Event Listener for the Clear Button: Add an event listener to the clear button to clear the canvas when clicked:

clearButton.addEventListener('click', () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
});

Here’s how the clear button functionality works:

  • `ctx.clearRect(0, 0, canvas.width, canvas.height);`: This method clears a rectangular area on the canvas. In this case, we clear the entire canvas by specifying the top-left corner (0, 0) and the width and height of the canvas.

Compile the code, refresh your browser, and you should now have a working clear button that resets the canvas.

Adding Support for Different Brush Styles

Let’s extend our app to include different brush styles, such as a round brush and a square brush. We will achieve this by modifying the `lineCap` property of the drawing context.

  1. Add Brush Style Controls in HTML: Add radio buttons to the `index.html` file to choose the brush style:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Drawing App</title>
    <style>
        #canvas {
            border: 1px solid black;
        }
    </style>
</head>
<body>
    <input type="color" id="colorPicker" value="#000000">
    <input type="range" id="brushSize" min="1" max="20" value="2">
    <button id="clearButton">Clear Canvas</button>
    <br>
    <label for="roundCap">Round Cap:</label>
    <input type="radio" id="roundCap" name="lineCap" value="round" checked>
    <label for="squareCap">Square Cap:</label>
    <input type="radio" id="squareCap" name="lineCap" value="square">
    <canvas id="canvas" width="800" height="600"></canvas>
    <script src="./dist/index.js"></script>
</body>
</html>
  1. Get Brush Style Controls in TypeScript: Get references to the radio buttons in `index.ts`:

const roundCap = document.getElementById('roundCap') as HTMLInputElement;
const squareCap = document.getElementById('squareCap') as HTMLInputElement;
  1. Add Event Listeners for Brush Style: Add event listeners to update the `lineCap` property of the drawing context when a brush style is selected:

roundCap.addEventListener('change', () => {
  ctx.lineCap = 'round';
});

squareCap.addEventListener('change', () => {
  ctx.lineCap = 'square';
});

Here’s how the brush style functionality works:

  • `ctx.lineCap = ’round’;`: Sets the line cap style to round.
  • `ctx.lineCap = ‘square’;`: Sets the line cap style to square.

Compile the code, refresh your browser, and you should now be able to choose between a round and a square brush style.

Adding Touch Support

To make our drawing app work on touch-enabled devices, we need to add support for touch events. The logic is similar to mouse events, but we’ll use touch-specific event types.

  1. Modify Touch Event Handlers: In `index.ts`, add touch event listeners. Replace the mouse event listeners with the following code. We’ll reuse the `isDrawing`, `lastX`, and `lastY` variables:

let isDrawing = false;
let lastX = 0;
let lastY = 0;

canvas.addEventListener('touchstart', (e: TouchEvent) => {
  isDrawing = true;
  const rect = canvas.getBoundingClientRect();
  lastX = e.touches[0].clientX - rect.left;
  lastY = e.touches[0].clientY - rect.top;
});

canvas.addEventListener('touchmove', (e: TouchEvent) => {
  if (!isDrawing) return;
  const rect = canvas.getBoundingClientRect();
  const x = e.touches[0].clientX - rect.left;
  const y = e.touches[0].clientY - rect.top;

  ctx.beginPath();
  ctx.moveTo(lastX, lastY);
  ctx.lineTo(x, y);
  ctx.stroke();
  [lastX, lastY] = [x, y];
});

canvas.addEventListener('touchend', () => {
  isDrawing = false;
});

canvas.addEventListener('touchcancel', () => {
  isDrawing = false;
});

Let’s break down the touch event handling code:

  • `touchstart` event: When the touch starts, `isDrawing` is set to `true`. We calculate the touch coordinates relative to the canvas using `getBoundingClientRect()`.
  • `touchmove` event: If `isDrawing` is `true`, a line is drawn from the previous touch position to the current touch position. The current coordinates are updated. We also calculate the touch coordinates relative to the canvas using `getBoundingClientRect()`.
  • `touchend` and `touchcancel` events: When the touch ends or is cancelled, `isDrawing` is set to `false`.

Compile the code. Test the app on a touch-enabled device or use your browser’s developer tools to simulate touch events. You should now be able to draw on the canvas using touch.

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • Canvas Not Rendering:
    • Problem: The canvas element isn’t displaying anything, even though you’ve written code to draw.
    • Solution: Double-check that you’ve correctly retrieved the canvas context using `getContext(‘2d’)`. Ensure that you haven’t made any typos in your HTML or JavaScript code. Also, check that the canvas element has a defined width and height.
  • Incorrect Coordinates:
    • Problem: The drawings appear in the wrong location on the canvas, or the touch coordinates are off.
    • Solution: Ensure that you’re correctly calculating the mouse or touch coordinates relative to the canvas. Use `e.offsetX` and `e.offsetY` for mouse events, and subtract `rect.left` and `rect.top` from the touch coordinates in `touchmove` and `touchstart` events.
  • Type Errors:
    • Problem: TypeScript is showing type errors, preventing the code from compiling.
    • Solution: Carefully review the error messages. Make sure you’re using type assertions (`as HTMLCanvasElement`, etc.) correctly. Check that your event listener callbacks have the correct parameter types (e.g., `MouseEvent`, `TouchEvent`).
  • Drawing Not Smooth:
    • Problem: The lines drawn on the canvas are jagged or not smooth.
    • Solution: Ensure that the coordinates are updated correctly in the `mousemove` and `touchmove` event handlers. Also, consider using the `lineJoin` and `lineCap` properties of the `ctx` object for smoother lines.
  • Ignoring `isDrawing` flag:
    • Problem: The app is drawing even when the mouse button is not pressed (or the touch is not active).
    • Solution: Make sure you are checking the `isDrawing` flag within your `mousemove` and `touchmove` event listeners. If `isDrawing` is false, do not draw anything.

Key Takeaways

  • Canvas Basics: You’ve learned how to set up an HTML5 canvas, get its 2D rendering context, and draw basic shapes.
  • Event Handling: You’ve learned how to handle mouse and touch events to respond to user interactions.
  • TypeScript Fundamentals: You’ve used types, interfaces (implicitly through the HTML element types), and event listeners to write more robust and maintainable code.
  • User Interface Controls: You’ve added color and brush size controls, as well as a clear button and brush style selectors, to enhance the user experience.
  • Project Structure: You’ve learned how to structure a simple web application using HTML, TypeScript, and npm.

FAQ

  1. Can I add more features to this app? Yes! You can add features like different shapes (circles, rectangles), image loading, saving and loading drawings, and more.
  2. How can I deploy this app? You can deploy the app to a web server (like Netlify, Vercel, or GitHub Pages) by uploading the HTML, JavaScript (compiled from TypeScript), and any other assets (images, etc.).
  3. How can I improve performance? For more complex drawing applications, consider techniques like caching the canvas context, using requestAnimationFrame for smoother animations, and optimizing drawing operations.
  4. How can I add different colors? You can add a color picker, or a palette of colors, or a way to enter a hexadecimal color code.
  5. How can I add a save function? You can use the `canvas.toDataURL()` method to get the data URL of the canvas content and then save that to local storage or allow the user to download the image.

This tutorial provides a solid foundation for building a drawing app. By understanding these concepts and experimenting with the code, you can create even more sophisticated and feature-rich applications. Remember to experiment, explore, and most importantly, have fun while coding! The skills learned here are transferable and can be applied to a wide range of web development projects.