TypeScript Tutorial: Building a Simple Web-Based Drawing Application

Ever wanted to create your own digital art or simply sketch ideas online? Building a web-based drawing application is a fantastic project to learn the fundamentals of web development and TypeScript. This tutorial will guide you through creating a simple, yet functional, drawing app using TypeScript, HTML, and CSS. We’ll cover everything from setting up your project to implementing drawing tools, color selection, and saving your artwork.

Why Build a Drawing Application?

Creating a drawing application is more than just a fun project; it’s a practical exercise that helps you understand core web development concepts. You’ll learn how to:

  • Manipulate the HTML Canvas element, the heart of our drawing application.
  • Handle user input, such as mouse clicks and movements.
  • Manage application state, including selected colors, drawing tools, and canvas content.
  • Structure your code effectively using TypeScript’s type system and object-oriented principles.

By the end of this tutorial, you’ll have a solid foundation for building more complex web applications and a functional drawing app to boot.

Prerequisites

Before we dive in, ensure you have the following:

  • A basic understanding of HTML, CSS, and JavaScript.
  • Node.js and npm (Node Package Manager) installed on your system.
  • A code editor (e.g., VS Code, Sublime Text).

Setting Up Your Project

Let’s get started by setting up the project structure and installing necessary dependencies.

1. Create a Project Directory

Open your terminal or command prompt and create a new directory for your project:

mkdir drawing-app
cd drawing-app

2. Initialize npm

Initialize a new npm project inside your project directory:

npm init -y

This command creates a package.json file, which will manage our project dependencies.

3. Install TypeScript

Install TypeScript as a development dependency:

npm install --save-dev typescript

4. Create a TypeScript Configuration File

Generate a tsconfig.json file to configure TypeScript:

npx tsc --init

This command creates a tsconfig.json file with default configurations. You can customize these settings to fit your project’s needs. For our project, we will use the default settings, but it’s a good practice to examine the file and understand the various options available.

5. Create Project Files and Folders

Create the following files and folders in your project directory:

  • index.html (HTML file for our application)
  • src/ (folder for our TypeScript source code)
  • src/index.ts (Main TypeScript file)
  • src/style.css (CSS file for styling)

Building the HTML Structure

Let’s set up the basic HTML structure for our drawing application in index.html. This will include the canvas element where we’ll draw, a color palette, and controls for selecting drawing tools.

<!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="src/style.css">
</head>
<body>
    <div class="container">
        <canvas id="drawingCanvas"></canvas>
        <div class="controls">
            <div class="color-palette">
                <button class="color-button" style="background-color: black;" data-color="black"></button>
                <button class="color-button" style="background-color: red;" data-color="red"></button>
                <button class="color-button" style="background-color: green;" data-color="green"></button>
                <button class="color-button" style="background-color: blue;" data-color="blue"></button>
                <button class="color-button" style="background-color: yellow;" data-color="yellow"></button>
            </div>
            <button id="clearButton">Clear</button>
        </div>
    </div>
    <script src="src/index.js"></script>
</body>
</html>

In this HTML:

  • We have a <canvas> element with the ID drawingCanvas. This is where the drawing will take place.
  • We have a <div> with class "controls" to hold our color palette and other controls.
  • The <div> with class "color-palette" contains color buttons. Each button has a background color and a data-color attribute, which will be used to set the drawing color.
  • A “Clear” button to clear the canvas.
  • We link to a CSS file (src/style.css) for styling.
  • We link to our JavaScript file (src/index.js), which we will generate from the TypeScript code.

Styling with CSS

Next, let’s add some basic styling to src/style.css to make our application look presentable:

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

.container {
    display: flex;
    flex-direction: column;
    align-items: center;
    background-color: white;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

#drawingCanvas {
    border: 1px solid #ccc;
    margin-bottom: 10px;
}

.controls {
    display: flex;
    align-items: center;
    margin-top: 10px;
}

.color-palette {
    display: flex;
    margin-right: 10px;
}

.color-button {
    width: 30px;
    height: 30px;
    border: 1px solid #ccc;
    margin: 0 5px;
    border-radius: 50%;
    cursor: pointer;
}

#clearButton {
    padding: 8px 15px;
    background-color: #ddd;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

This CSS provides basic styling for the layout, canvas, color palette, and clear button. Feel free to customize the styles to your liking.

Writing TypeScript Code

Now, let’s write the TypeScript code that will handle the drawing functionality. Open src/index.ts and add the following code:


// Get the canvas element and its context
const canvas = document.getElementById('drawingCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;

// Set canvas dimensions
canvas.width = 600;
canvas.height = 400;

// Initialize drawing state
let isDrawing = false;
let currentColor = 'black';

// Event listeners for drawing
canvas.addEventListener('mousedown', (e: MouseEvent) => {
  isDrawing = true;
  draw(e.offsetX, e.offsetY);
});

canvas.addEventListener('mouseup', () => {
  isDrawing = false;
  ctx.beginPath(); // Start a new path when drawing stops
});

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

// Function to draw on the canvas
function draw(x: number, y: number) {
  ctx.strokeStyle = currentColor;
  ctx.lineCap = 'round';
  ctx.lineWidth = 5;
  ctx.lineTo(x, y);
  ctx.stroke();
}

// Event listeners for color palette
const colorButtons = document.querySelectorAll('.color-button');
colorButtons.forEach(button => {
  button.addEventListener('click', () => {
    currentColor = button.getAttribute('data-color') || 'black';
  });
});

// Event listener for clear button
const clearButton = document.getElementById('clearButton') as HTMLButtonElement;
clearButton.addEventListener('click', () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
});

Let’s break down this code:

  • Canvas and Context: We get the HTML canvas element and its 2D rendering context. The context is what we use to draw on the canvas.
  • Canvas Dimensions: We set the width and height of the canvas.
  • Drawing State: We initialize variables to track whether the user is currently drawing (isDrawing) and the currently selected color (currentColor).
  • Event Listeners: We add event listeners for mousedown, mouseup, and mousemove events on the canvas.
    • mousedown: Sets isDrawing to true and starts drawing at the mouse’s starting position.
    • mouseup: Sets isDrawing to false, stopping the drawing. It also calls beginPath() to ensure that subsequent drawings are separate lines.
    • mousemove: If isDrawing is true, it calls the draw function to draw a line from the last point to the current mouse position.
  • Draw Function: The draw function actually draws a line on the canvas. It sets the strokeStyle (color), lineCap (shape of line endings), and lineWidth, and then uses lineTo and stroke to draw a line segment.
  • Color Palette: We add event listeners to the color buttons in the color palette. When a button is clicked, we set the currentColor to the button’s data-color attribute.
  • Clear Button: We add an event listener to the clear button. When clicked, it clears the entire canvas using clearRect.

Compiling and Running the Application

Now, let’s compile the TypeScript code and run the application.

1. Compile the TypeScript Code

Open your terminal in the project directory and run the following command:

tsc

This command will compile your src/index.ts file into src/index.js, which is the JavaScript file that the browser will execute.

2. Run the Application

Open index.html in your web browser. You should see a blank canvas with a color palette and a clear button.

If everything is set up correctly, you should be able to draw on the canvas by clicking and dragging your mouse while selecting a color from the color palette.

Adding More Features

Now that we have a basic drawing application, let’s explore how to add more features to enhance it. This will further demonstrate the power of TypeScript and how you can structure your code for maintainability and extensibility.

1. Implementing Different Drawing Tools

Let’s add options for different drawing tools, such as a pencil, a line tool, and a rectangle tool. We’ll start by defining an enum for the different tools:

enum Tool {
  Pencil = 'pencil',
  Line = 'line',
  Rectangle = 'rectangle',
}

Next, let’s add UI elements for these tools in our HTML (inside the <div class="controls">):

<div class="tool-palette">
    <button class="tool-button" data-tool="pencil">Pencil</button>
    <button class="tool-button" data-tool="line">Line</button>
    <button class="tool-button" data-tool="rectangle">Rectangle</button>
</div>

Now, we’ll update our TypeScript code to handle tool selection and drawing logic:


// ... (previous code)

// Tool enum
enum Tool {
  Pencil = 'pencil',
  Line = 'line',
  Rectangle = 'rectangle',
}

// Current tool and starting point for line/rectangle
let currentTool: Tool = Tool.Pencil;
let startX: number | null = null;
let startY: number | null = null;

// Tool selection
const toolButtons = document.querySelectorAll('.tool-button');
toolButtons.forEach(button => {
  button.addEventListener('click', () => {
    currentTool = button.getAttribute('data-tool') as Tool;
  });
});

// Event listeners for drawing
canvas.addEventListener('mousedown', (e: MouseEvent) => {
  isDrawing = true;
  startX = e.offsetX;
  startY = e.offsetY;
  if (currentTool === Tool.Pencil) {
    draw(e.offsetX, e.offsetY);
  }
});

canvas.addEventListener('mouseup', (e: MouseEvent) => {
  isDrawing = false;
  ctx.beginPath();
  if (currentTool === Tool.Line && startX !== null && startY !== null) {
    drawLine(startX, startY, e.offsetX, e.offsetY);
  } else if (currentTool === Tool.Rectangle && startX !== null && startY !== null) {
    drawRectangle(startX, startY, e.offsetX, e.offsetY);
  }
  startX = null;
  startY = null;
});

canvas.addEventListener('mousemove', (e: MouseEvent) => {
  if (!isDrawing) return;
  if (currentTool === Tool.Pencil) {
    draw(e.offsetX, e.offsetY);
  }
});

// Draw functions for different tools
function drawLine(x1: number, y1: number, x2: number, y2: number) {
  ctx.beginPath();
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.stroke();
}

function drawRectangle(x1: number, y1: number, x2: number, y2: number) {
  const width = x2 - x1;
  const height = y2 - y1;
  ctx.strokeRect(x1, y1, width, height);
}

// ... (previous code)

Key changes:

  • We added a Tool enum to represent different drawing tools.
  • We added UI elements (buttons) for tool selection in HTML.
  • We added event listeners for the tool buttons to update the currentTool.
  • In the mousedown event, we store the starting point (startX, startY) for the line and rectangle tools.
  • In the mouseup event, we check the selected tool and call the appropriate drawing function (drawLine or drawRectangle) if the tool is not the pencil.
  • We implemented drawLine and drawRectangle functions.

2. Implementing a Brush Size Control

Let’s add a control to change the brush size. First, add an input element in your HTML:

<label for="brushSize">Brush Size: </label>
<input type="number" id="brushSize" value="5" min="1" max="20">

Then, modify your TypeScript code:


// ... (previous code)

// Brush size control
const brushSizeInput = document.getElementById('brushSize') as HTMLInputElement;
let brushSize = parseInt(brushSizeInput.value, 10) || 5;

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

// Function to draw on the canvas
function draw(x: number, y: number) {
  ctx.strokeStyle = currentColor;
  ctx.lineCap = 'round';
  ctx.lineWidth = brushSize;
  ctx.lineTo(x, y);
  ctx.stroke();
}

// Draw functions for different tools
function drawLine(x1: number, y1: number, x2: number, y2: number) {
  ctx.strokeStyle = currentColor;
  ctx.lineWidth = brushSize;
  ctx.beginPath();
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.stroke();
}

function drawRectangle(x1: number, y1: number, x2: number, y2: number) {
  ctx.strokeStyle = currentColor;
  ctx.lineWidth = brushSize;
  const width = x2 - x1;
  const height = y2 - y1;
  ctx.strokeRect(x1, y1, width, height);
}

Key changes:

  • We added an input element for brush size in HTML.
  • We added an event listener to the input element that updates the brushSize variable and sets the lineWidth of the drawing context.
  • We updated the draw, drawLine and drawRectangle functions to use the brushSize.

3. Implementing an Undo/Redo Feature

Adding undo and redo functionality can greatly enhance the usability of your drawing application. Here’s how you could implement it:

First, we need to keep track of the canvas state. We can do this by storing snapshots of the canvas content. We’ll use an array to store these snapshots and implement the undo/redo functionality using array manipulation.


// ... (previous code)

// Undo/Redo functionality
const undoStack: ImageData[] = [];
const redoStack: ImageData[] = [];

// Function to save the current state
function saveState() {
  undoStack.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
  redoStack.length = 0; // Clear the redo stack on new action
}

// Event listeners for drawing
canvas.addEventListener('mousedown', (e: MouseEvent) => {
    isDrawing = true;
    startX = e.offsetX;
    startY = e.offsetY;
    saveState(); // Save state on mouse down
    if (currentTool === Tool.Pencil) {
        draw(e.offsetX, e.offsetY);
    }
});

canvas.addEventListener('mouseup', (e: MouseEvent) => {
    isDrawing = false;
    ctx.beginPath();
    if (currentTool === Tool.Line && startX !== null && startY !== null) {
        drawLine(startX, startY, e.offsetX, e.offsetY);
        saveState(); // Save state after line is drawn
    } else if (currentTool === Tool.Rectangle && startX !== null && startY !== null) {
        drawRectangle(startX, startY, e.offsetX, e.offsetY);
        saveState(); // Save state after rectangle is drawn
    }
    startX = null;
    startY = null;
});

canvas.addEventListener('mousemove', (e: MouseEvent) => {
    if (!isDrawing) return;
    if (currentTool === Tool.Pencil) {
        draw(e.offsetX, e.offsetY);
    }
});

// Undo function
function undo() {
    if (undoStack.length > 0) {
        redoStack.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
        const previousState = undoStack.pop()!;
        ctx.putImageData(previousState, 0, 0);
    }
}

// Redo function
function redo() {
    if (redoStack.length > 0) {
        undoStack.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
        const nextState = redoStack.pop()!;
        ctx.putImageData(nextState, 0, 0);
    }
}

// Add undo and redo buttons in your HTML
// <button id="undoButton">Undo</button>
// <button id="redoButton">Redo</button>

// Add event listeners for undo/redo buttons
const undoButton = document.getElementById('undoButton') as HTMLButtonElement;
const redoButton = document.getElementById('redoButton') as HTMLButtonElement;

undoButton.addEventListener('click', undo);
redoButton.addEventListener('click', redo);

// ... (previous code)

Key changes:

  • We added undoStack and redoStack arrays to store canvas states.
  • saveState() function: pushes the current canvas state (using getImageData()) onto the undoStack and clears the redoStack.
  • We call saveState() after certain drawing actions (e.g., mousedown, drawing a line, drawing a rectangle).
  • undo() function: pops the last state from undoStack, pushes the current state to redoStack, and restores the previous state using putImageData().
  • redo() function: pops the last state from redoStack, pushes the current state to undoStack, and restores the next state using putImageData().
  • We added HTML buttons for undo and redo.
  • We added event listeners to the undo and redo buttons, calling the corresponding functions.

4. Implementing a Save Functionality

To allow users to save their drawings, we can add a save button that downloads the canvas content as an image file.

<button id="saveButton">Save</button>

// ... (previous code)

// Save functionality
const saveButton = document.getElementById('saveButton') as HTMLButtonElement;

saveButton.addEventListener('click', () => {
  const dataURL = canvas.toDataURL('image/png');
  const link = document.createElement('a');
  link.href = dataURL;
  link.download = 'drawing.png';
  link.click();
});

Key changes:

  • We added a save button in HTML.
  • We added an event listener to the save button.
  • Inside the event listener:
    • We use canvas.toDataURL('image/png') to get a data URL of the canvas content as a PNG image.
    • We create an <a> element, set its href to the data URL, and set its download attribute to specify the filename.
    • We programmatically click the <a> element to trigger the download.

Common Mistakes and How to Fix Them

When building a drawing application, you might encounter some common issues. Here’s a breakdown of common mistakes and how to fix them:

1. Canvas Not Appearing

If your canvas isn’t showing up, double-check these things:

  • HTML: Ensure that the <canvas> element is present in your HTML and that you haven’t misspelled the ID.
  • CSS: Make sure the canvas has dimensions (width and height) set in your CSS. If it’s not visible, check for any CSS styles that might be hiding it (e.g., display: none;, visibility: hidden;). Also, ensure that the canvas is not covered by other elements.
  • JavaScript: Verify that you’re correctly selecting the canvas element in your JavaScript/TypeScript code using document.getElementById(). Also, make sure that the canvas element exists in the DOM by the time your script runs. If your script runs before the HTML is fully loaded, you might not be able to select the canvas element.

2. Drawing Not Working

If you can’t draw on the canvas, troubleshoot these areas:

  • Event Listeners: Make sure your event listeners (mousedown, mousemove, mouseup) are correctly attached to the canvas element.
  • Drawing State: Double-check that your isDrawing variable is being updated correctly in your event listeners.
  • Context: Ensure that you have correctly obtained the 2D rendering context (ctx) of the canvas using getContext('2d').
  • Drawing Function: Verify that your drawing function (e.g., the draw function) is being called correctly and that the strokeStyle, lineWidth, and other properties are set as you expect.
  • Path Reset: When you stop drawing, you should call ctx.beginPath() to start a new path. Otherwise, subsequent drawings might connect to the previous one.

3. Incorrect Colors or Brush Size

If the colors or brush size are incorrect:

  • Color Selection: Make sure your color selection logic is working correctly. Check the currentColor variable and ensure it’s being updated when the user clicks a color button. Verify that the strokeStyle of the ctx is set to currentColor in your draw function.
  • Brush Size: Verify that the lineWidth of the ctx is being set correctly, either directly or through the brush size input.

4. Performance Issues

For more complex drawing applications or when dealing with a large canvas, performance can become an issue. Here’s how to optimize:

  • Reduce Redraws: Avoid redrawing the entire canvas on every mouse move. Only redraw the necessary parts.
  • Use Request Animation Frame: Use requestAnimationFrame for smoother animations and drawing updates.
  • Optimize Drawing Operations: Minimize the number of drawing operations. For example, instead of drawing individual lines for a pencil tool, you could use a single lineTo call.
  • Caching: If you have complex drawings, consider caching parts of the canvas that don’t change frequently.

Key Takeaways

In this tutorial, we’ve covered the essentials of building a simple web-based drawing application using TypeScript. You’ve learned how to:

  • Set up a TypeScript project and configure it.
  • Use the HTML canvas element for drawing.
  • Handle user input using event listeners.
  • Implement basic drawing tools (pencil, line, rectangle).
  • Manage application state (colors, brush size, tool selection).
  • Add extra features like undo/redo and save.

This project provides a solid foundation for understanding web development concepts and TypeScript. You can now expand this application to include more advanced features such as image loading, different brush styles, and more complex drawing tools.

FAQ

1. Can I use this drawing application on a mobile device?

Yes, the application should work on mobile devices. However, you might need to adjust the touch event listeners to handle touch input. Instead of mousedown, mousemove, and mouseup, you can use touchstart, touchmove, and touchend events. Also consider making the canvas responsive by setting its width and height to percentages or using CSS media queries.

2. How can I add more colors to the color palette?

Simply add more color buttons to the HTML with different background colors and data-color attributes. In the TypeScript code, the event listeners for the color buttons will automatically handle the new colors.

3. How can I add different brush styles (e.g., dotted, dashed)?

You can use the ctx.lineDashOffset, ctx.setLineDash() properties to implement different line styles. Create a new select option in the HTML for brush style and add an event listener to update the ctx accordingly.

4. How can I implement image loading and saving?

For image loading, you can use an <input type="file"> element to allow users to upload an image. You can then draw the image onto the canvas using ctx.drawImage(). For saving, we’ve already implemented a basic save functionality using canvas.toDataURL(). You can extend this to save the image to a server or local storage.

5. How can I improve the performance of the drawing application?

To improve performance, consider these steps: Use requestAnimationFrame for smoother rendering. Reduce the number of redraws. If the drawing is complex, consider caching parts of the canvas that don’t change frequently. For more advanced features, look into web workers to move computationally expensive tasks off the main thread.

Building a web-based drawing application is a rewarding project that combines creativity and technical skills. From basic shapes to elaborate artworks, the canvas is your digital playground. As you experiment with different drawing tools, color palettes, and features, you’ll gain a deeper understanding of web development principles. Remember, the journey of learning never ends, and each line of code you write brings you closer to mastering the art of software creation. Keep exploring, keep creating, and enjoy the process of bringing your ideas to life on the web.