TypeScript Tutorial: Building a Simple Interactive Drawing Application

Have you ever wanted to create your own digital art or simply play around with drawing tools in a web browser? Building a drawing application might seem like a complex task, but with TypeScript, we can break it down into manageable steps. This tutorial will guide you through building a simple, yet functional, interactive drawing application using TypeScript, HTML, and CSS. We’ll cover everything from setting up your project to handling user input and drawing on the canvas. This is a great project for beginners and intermediate developers to learn about event handling, DOM manipulation, and working with the HTML canvas element.

Why Build a Drawing Application?

Creating a drawing application is more than just a fun project; it’s a fantastic way to learn fundamental web development concepts. You’ll gain practical experience with:

  • Event Handling: Reacting to mouse clicks, mouse movements, and touch events.
  • DOM Manipulation: Interacting with HTML elements, such as the canvas.
  • Canvas API: Understanding how to draw shapes, lines, and manipulate images.
  • TypeScript Fundamentals: Using types, interfaces, and classes to write clean and maintainable code.

Moreover, it allows you to visualize and experiment with programming concepts in a creative way. You can quickly see the results of your code changes, making the learning process more engaging and rewarding.

Project Setup

Before we dive into the code, let’s set up our project. We’ll need the following:

  • A Text Editor: Such as VS Code, Sublime Text, or Atom.
  • Node.js and npm: (or yarn) for managing dependencies and running the TypeScript compiler.
  • A Web Browser: Chrome, Firefox, Safari, or Edge.

1. Create a Project Directory:

mkdir drawing-app
cd drawing-app

2. Initialize npm:

npm init -y

3. Install TypeScript:

npm install typescript --save-dev

4. Create TypeScript Configuration (tsconfig.json):

npx tsc --init

This will create a tsconfig.json file. You can customize this file, but for this project, the default settings will work well. Make sure "target": "es5" or higher is set in the tsconfig.json, and that "module": "commonjs" or similar is set (e.g., “esnext”).

5. Create HTML File (index.html):

Create an index.html file in your project directory with the following content:

<!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>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <canvas id="drawingCanvas"></canvas>
    <script src="index.js"></script>
</body>
</html>

6. Create CSS File (style.css):

Create a style.css file in your project directory with the following content. This adds some basic styling to the canvas.

#drawingCanvas {
    border: 1px solid black;
}

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    margin: 0;
    background-color: #f0f0f0;
}

7. Create TypeScript File (index.ts):

Create an index.ts file in your project directory. This is where we’ll write our TypeScript code.

Core Concepts: The Canvas and Drawing Context

The heart of our drawing application is the HTML canvas element. The canvas provides a way to draw graphics using JavaScript (or TypeScript, in our case). To draw on the canvas, we need to get its drawing context. The drawing context provides methods for drawing shapes, lines, text, and images.

Let’s get started by getting the canvas element and its 2D drawing context in our index.ts file:

const canvas = document.getElementById('drawingCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!; // Non-null assertion operator (!)

Explanation:

  • document.getElementById('drawingCanvas'): This gets a reference to the canvas element in our HTML.
  • as HTMLCanvasElement: This is a type assertion. It tells TypeScript that we know the element is an HTMLCanvasElement, which gives us access to canvas-specific properties and methods.
  • canvas.getContext('2d'): This gets the 2D drawing context. The '2d' argument specifies that we want a 2D rendering context.
  • !: The non-null assertion operator. It tells TypeScript that we are certain that getContext('2d') will return a value (it won’t be null or undefined).

Drawing Lines: Implementing Basic Drawing Functionality

Now, let’s implement the basic functionality to draw lines on the canvas. We’ll track the mouse’s position and draw a line as the user moves the mouse while holding the mouse button down.

Add the following code to your index.ts file:

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

canvas.width = window.innerWidth - 20; // Set canvas width
canvas.height = window.innerHeight - 20; // Set canvas height

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

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

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

canvas.addEventListener('mousemove', (e: MouseEvent) => {
    if (!isDrawing) return; // Stop the function from running when they are not drawing

    ctx.strokeStyle = 'blue'; // Set line color
    ctx.lineJoin = 'round'; // Set line style
    ctx.lineCap = 'round'; // Set line style
    ctx.lineWidth = 10; // Set line width

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

Explanation:

  • isDrawing: A boolean flag to track whether the user is currently drawing.
  • lastX and lastY: Variables to store the previous mouse position.
  • canvas.width and canvas.height: Sets the canvas size to the window size.
  • mousedown event: When the mouse button is pressed, we set isDrawing to true and update the lastX and lastY variables.
  • mouseup and mouseout events: When the mouse button is released or leaves the canvas, we set isDrawing to false.
  • mousemove event: When the mouse moves, we check if isDrawing is true. If it is, we draw a line from the previous mouse position (lastX, lastY) to the current mouse position (e.offsetX, e.offsetY).
  • ctx.beginPath(): Starts a new path.
  • ctx.moveTo(lastX, lastY): Moves the starting point of the line to the previous mouse position.
  • ctx.lineTo(e.offsetX, e.offsetY): Draws a line from the starting point to the current mouse position.
  • ctx.stroke(): Strokes the current path, drawing the line.
  • [lastX, lastY] = [e.offsetX, e.offsetY]: Updates lastX and lastY to the current mouse position so the next line segment starts from the correct point.

5. Compile the TypeScript code:

npx tsc

This will compile your index.ts file into index.js.

6. Open index.html in your browser:

Now, when you open index.html in your browser, you should be able to draw on the canvas by clicking and dragging your mouse.

Adding Color and Line Width Controls

Let’s enhance our drawing application by adding controls to change the line color and line width. We’ll add two input elements: a color picker and a number input for the line width.

1. Modify index.html:

Add the following HTML elements inside the <body> tag of your index.html file. We’ll add a color input and a number input for line width.

<code class="language-html
<input type="color" id="colorPicker" value="#0000ff">
<input type="number" id="lineWidth" value="10" min="1" max="100">

2. Modify index.ts:

Add the following code to your index.ts file to get references to the new input elements and update the drawing properties accordingly:

const colorPicker = document.getElementById('colorPicker') as HTMLInputElement;
const lineWidthInput = document.getElementById('lineWidth') as HTMLInputElement;

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

    ctx.strokeStyle = colorPicker.value; // Set line color from color picker
    ctx.lineWidth = parseInt(lineWidthInput.value, 10); // Set line width from input

    ctx.lineJoin = 'round';
    ctx.lineCap = 'round';

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

Explanation:

  • Get references to the input elements: We get references to the color picker and line width input elements using document.getElementById() and type assertions.
  • Update drawing properties: Inside the mousemove event listener, we now set the strokeStyle to the value of the color picker and the lineWidth to the value of the line width input. We use parseInt() to convert the input value (which is a string) to a number.

Adding a Clear Button

Let’s add a button to clear the canvas. This is a common feature in drawing applications.

1. Modify index.html:

Add a button to your index.html file:

<code class="language-html
<button id="clearButton">Clear</button>

2. Modify index.ts:

Add the following code to your index.ts file:

const clearButton = document.getElementById('clearButton') as HTMLButtonElement;

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

Explanation:

  • Get a reference to the clear button: We get a reference to the clear button using document.getElementById() and a type assertion.
  • Add a click event listener: We add a click event listener to the clear button.
  • Clear the canvas: Inside the click event listener, we use ctx.clearRect(0, 0, canvas.width, canvas.height) to clear the entire canvas. The arguments specify the rectangle to clear: (x, y, width, height).

Handling Touch Events (For Mobile Devices)

To make our drawing application work on mobile devices, we need to handle touch events. Touch events are similar to mouse events, but they are designed for touchscreens.

1. Modify index.ts:

Add the following code to your index.ts file to handle touch events:

// Touch event handling
canvas.addEventListener('touchstart', (e: TouchEvent) => {
    isDrawing = true;
    if (e.touches.length > 0) {
        lastX = e.touches[0].clientX - canvas.offsetLeft;
        lastY = e.touches[0].clientY - canvas.offsetTop;
    }
});

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

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

canvas.addEventListener('touchmove', (e: TouchEvent) => {
    if (!isDrawing) return;
    e.preventDefault(); // Prevent scrolling when touching
    ctx.strokeStyle = colorPicker.value; // Set line color from color picker
    ctx.lineWidth = parseInt(lineWidthInput.value, 10); // Set line width from input

    ctx.lineJoin = 'round';
    ctx.lineCap = 'round';

    if (e.touches.length > 0) {
        const touch = e.touches[0];
        const x = touch.clientX - canvas.offsetLeft;
        const y = touch.clientY - canvas.offsetTop;

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

Explanation:

  • touchstart event: This event is triggered when the user touches the screen. We set isDrawing to true and get the touch coordinates. We subtract the canvas offset to get the correct coordinates relative to the canvas.
  • touchend and touchcancel events: These events are triggered when the user removes their finger from the screen or the touch is interrupted. We set isDrawing to false.
  • touchmove event: This event is triggered when the user moves their finger on the screen. We check if isDrawing is true. If it is, we draw a line from the previous touch position to the current touch position. We also call e.preventDefault() to prevent scrolling while drawing.
  • Getting touch coordinates: We get the touch coordinates using e.touches[0].clientX and e.touches[0].clientY. We then subtract the canvas’s offset (canvas.offsetLeft and canvas.offsetTop) to get the coordinates relative to the canvas.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to avoid or fix them when building a drawing application:

  • Incorrect Canvas Size: Make sure your canvas size is correctly set. If the canvas size isn’t set, you might not see anything drawn. Use CSS or JavaScript to set the width and height attributes. It’s often a good practice to set these in JavaScript to dynamically adjust to the window size.
  • Context Not Found: Ensure you correctly get the 2D drawing context using getContext('2d'). Check for null values after calling this function. Using the non-null assertion operator (!) can help, but make sure you understand when it’s appropriate.
  • Incorrect Event Listener Attachments: Double-check that you’ve attached your event listeners to the correct elements (the canvas, in this case).
  • Coordinate System Issues: Remember that the canvas coordinate system starts at (0, 0) in the top-left corner. Incorrectly calculating the coordinates can lead to drawings appearing in the wrong place. Use offsetX and offsetY for mouse events, and clientX and clientY (with offset calculations) for touch events.
  • Line Drawing Issues: Ensure you’re calling beginPath() before drawing each line segment and stroke() after calling lineTo().
  • Browser Compatibility: While the canvas API is widely supported, test your application in different browsers to ensure consistent behavior.
  • Type Errors: TypeScript helps catch type errors early. Pay close attention to the TypeScript compiler’s error messages and fix the type issues.

Key Takeaways

  • Canvas Basics: You’ve learned how to get the canvas element and its 2D drawing context.
  • Event Handling: You’ve implemented event listeners for mouse and touch events to capture user input.
  • Drawing Lines: You’ve successfully drawn lines on the canvas based on user input.
  • Customization: You’ve added color and line width controls to enhance the drawing experience.
  • Mobile Support: You’ve learned how to handle touch events to make your application work on mobile devices.

FAQ

  1. How can I add different shapes (circles, rectangles, etc.)?
    • Use the canvas API’s built-in methods like ctx.arc() for circles and ctx.rect() for rectangles. You’ll need to adapt your event listeners to draw these shapes based on mouse clicks or drags.
  2. How can I add an eraser tool?
    • You can change the globalCompositeOperation property of the context to "destination-out". This will make the drawing transparent where the line is drawn, effectively erasing the content.
  3. How can I save the drawing?
    • You can use the canvas.toDataURL() method to get a data URL of the canvas content. You can then use this data URL to create an image and download it.
  4. How can I improve performance?
    • For more complex applications, consider techniques like caching drawing commands, using requestAnimationFrame to optimize redrawing, and only redrawing parts of the canvas that have changed.
  5. How do I deploy this application?
    • You can deploy the application by uploading the HTML, CSS, and JavaScript (or compiled TypeScript) files to a web server. Services like Netlify, Vercel, or GitHub Pages offer free hosting options.

This tutorial provides a solid foundation for building a drawing application. You can extend this project by adding more features like different shapes, an eraser tool, saving and loading drawings, and more. Experiment with the canvas API, and don’t be afraid to try new things. The possibilities are endless, and you’ll learn a lot along the way. Your journey into web development has just begun; keep exploring, keep coding, and most importantly, keep creating!