Chat
Ask me anything
Ithy Logo

Vue 3 and TypeScript: Drawing Rectangles with HTML5 Canvas

A Look at Angular Alongside Vue - Familiar Code

Introduction

Creating dynamic and interactive graphical elements is a common requirement in modern web applications. Leveraging the power of Vue 3 combined with TypeScript and the HTML5 Canvas API allows developers to create responsive and type-safe applications. In this comprehensive guide, we will walk you through the process of drawing a rectangle on a canvas based on user-provided coordinates using Vue 3 and TypeScript. We will cover setting up the project, building the user interface, implementing the drawing logic, and adding essential error handling to ensure a robust implementation.

Prerequisites

Before diving into the implementation, ensure you have the following tools and knowledge:

  • Node.js and npm: Make sure you have Node.js installed. You can download it from the official website.
  • Vue CLI: Install Vue CLI globally using npm if you haven't already:
  • npm install -g @vue/cli
  • Basic Understanding of Vue 3: Familiarity with Vue 3's Composition API.
  • TypeScript Basics: Basic knowledge of TypeScript syntax and features.

Project Setup

Creating a New Vue 3 Project with TypeScript

Begin by creating a new Vue 3 project configured with TypeScript. Open your terminal and run the following command:

vue create vue-canvas-rectangle

During the setup, you'll be prompted to select features. Choose the following options:

  1. Manually select features: Use the spacebar to select this option and press Enter.
  2. Features to include: Select TypeScript, Router, and Vuex if needed.
  3. Use class-style component syntax: Choose according to your preference.
  4. Use Babel alongside TypeScript: Typically, you can select No.
  5. Use history mode for router: Choose No unless you specifically need it.
  6. Linting: Configure as per your project's requirements.

After the setup is complete, navigate to the project directory:

cd vue-canvas-rectangle

Start the development server to verify everything is working correctly:

npm run serve

Open your browser and navigate to http://localhost:8080 to see the default Vue application running.

Building the User Interface

The user interface will consist of four input fields for the coordinates, a button to trigger the drawing action, and a canvas element where the rectangle will be rendered.

Template Structure

In Vue 3, the template defines the HTML structure of the component. Here's how to structure the template for our rectangle drawing feature:

<template>
  <div class="rectangle-drawer">
    <h2>Draw a Rectangle</h2>
    <div class="inputs">
      <label>Left:</label>
      <input type="number" v-model.number="left" placeholder="Left X-coordinate" />
      
      <label>Top:</label>
      <input type="number" v-model.number="top" placeholder="Top Y-coordinate" />
      
      <label>Right:</label>
      <input type="number" v-model.number="right" placeholder="Right X-coordinate" />
      
      <label>Bottom:</label>
      <input type="number" v-model.number="bottom" placeholder="Bottom Y-coordinate" />
      
      <button @click="drawRectangle">Draw Rectangle</button>
    </div>
    <canvas ref="canvasRef" width="600" height="400" class="canvas"></canvas>
  </div>
</template>

**Explanation of the Template Elements:**

  • <div class="rectangle-drawer">: Container for the entire rectangle drawing component.
  • <h2>Draw a Rectangle</h2>: Heading for the feature.
  • <div class="inputs">: Wraps all input fields and the draw button.
  • <label> and <input> elements: Collect user input for the rectangle's coordinates.
  • <button @click="drawRectangle">: Button to trigger the drawing function when clicked.
  • <canvas ref="canvasRef" width="600" height="400" class="canvas"></canvas>: Canvas element where the rectangle will be drawn.

Styling the Component

To make the user interface visually appealing and organized, apply some basic styling. You can add the following CSS to your component's <style scoped> section:

/* RectangleDrawer.vue */

.rectangle-drawer {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.inputs {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 20px;
}

.inputs label {
  display: flex;
  flex-direction: column;
  font-weight: bold;
}

.inputs input {
  padding: 8px;
  width: 150px;
  margin-top: 5px;
}

button {
  padding: 10px 20px;
  background-color: #388278;
  color: #fff;
  border: none;
  cursor: pointer;
  border-radius: 4px;
}

button:hover {
  background-color: #2e6e65;
}

.canvas {
  border: 2px solid #cc9900;
  border-radius: 4px;
}

**Explanation of the Styles:**

  • .rectangle-drawer: Centers the component and adds padding.
  • .inputs: Uses flexbox to organize input fields and the button with spacing.
  • .inputs label: Aligns labels and inputs vertically.
  • button: Styles the draw button with a background color and hover effects.
  • .canvas: Adds a border and rounded corners to the canvas element.

Implementing the Drawing Logic

Setting Up Reactive Variables

In Vue 3's Composition API, reactive state is managed using the ref function. We will define four reactive variables to store the coordinates:

import { defineComponent, ref, onMounted } from 'vue';

export default defineComponent({
  name: 'RectangleDrawer',
  setup() {
    const left = ref(50);
    const top = ref(50);
    const right = ref(200);
    const bottom = ref(200);
    const canvasRef = ref(null);
    const ctx = ref(null);

    // Additional logic will go here

    return {
      left,
      top,
      right,
      bottom,
      canvasRef,
      drawRectangle
    };
  }
});

**Explanation:**

  • left, top, right, bottom: Store the coordinates input by the user.
  • canvasRef: Reference to the canvas element in the DOM.
  • ctx: Will hold the 2D rendering context of the canvas.

Initializing the Canvas Context

Once the component is mounted, we need to initialize the canvas context. This is done using the onMounted lifecycle hook:

onMounted(() => {
  if (canvasRef.value) {
    ctx.value = canvasRef.value.getContext('2d');
    drawRectangle(); // Initial drawing
  }
});

**Explanation:**

  • Checks if the canvasRef is available.
  • Obtains the 2D rendering context using getContext('2d').
  • Calls drawRectangle() to render the initial rectangle based on default values.

Drawing the Rectangle

The core functionality lies in the drawRectangle method. This method performs validation on the input coordinates, calculates the rectangle's dimensions, and renders it on the canvas.

const drawRectangle = () => {
  if (!ctx.value || !canvasRef.value) return;

  // Input validation
  if (isNaN(left.value) || isNaN(top.value) || isNaN(right.value) || isNaN(bottom.value)) {
    alert("All coordinates must be valid numbers.");
    return;
  }

  if (left.value > right.value) {
    alert("Left coordinate must be less than or equal to Right coordinate.");
    return;
  }

  if (top.value > bottom.value) {
    alert("Top coordinate must be less than or equal to Bottom coordinate.");
    return;
  }

  const width = right.value - left.value;
  const height = bottom.value - top.value;

  // Clear the canvas before drawing
  ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);

  // Styling the rectangle
  ctx.value.fillStyle = 'rgba(0, 0, 255, 0.3)'; // Semi-transparent fill
  ctx.value.strokeStyle = '#388278'; // Border color
  ctx.value.lineWidth = 2; // Border width

  // Draw filled rectangle
  ctx.value.fillRect(left.value, top.value, width, height);

  // Draw rectangle border
  ctx.value.strokeRect(left.value, top.value, width, height);
};

**Detailed Breakdown:**

  • Validation:
    • Ensures all inputs are numbers using isNaN.
    • Verifies that left ≤ right and top ≤ bottom to maintain a valid rectangle.
    • Displays alert messages for invalid inputs.
  • Calculations:
    • width: Difference between right and left.
    • height: Difference between bottom and top.
  • Canvas Operations:
    • clearRect: Clears the entire canvas to remove any previous drawings.
    • fillStyle and strokeStyle: Define the fill and border colors.
    • fillRect: Draws a filled rectangle.
    • strokeRect: Draws the rectangle border.

Enhancing User Experience

Real-time Drawing with Reactive Inputs

Instead of requiring the user to click a button to draw the rectangle, we can enhance the user experience by drawing the rectangle in real-time as the user inputs the coordinates. This can be achieved by adding @input event listeners to each input field that trigger the drawRectangle method upon any change.

**Updated Template with Real-time Drawing:**

<template>
  <div class="rectangle-drawer">
    <h2>Draw a Rectangle</h2>
    <div class="inputs">
      <label>Left:</label>
      <input type="number" v-model.number="left" placeholder="Left X-coordinate" @input="drawRectangle" />
      
      <label>Top:</label>
      <input type="number" v-model.number="top" placeholder="Top Y-coordinate" @input="drawRectangle" />
      
      <label>Right:</label>
      <input type="number" v-model.number="right" placeholder="Right X-coordinate" @input="drawRectangle" />
      
      <label>Bottom:</label>
      <input type="number" v-model.number="bottom" placeholder="Bottom Y-coordinate" @input="drawRectangle" />
    </div>
    <canvas ref="canvasRef" width="600" height="400" class="canvas"></canvas>
  </div>
</template>

**Changes Made:**

  • Removed the draw button to rely solely on real-time input.
  • Added @input="drawRectangle" to each input field to trigger the drawing whenever the user changes a value.

This approach provides immediate visual feedback, enhancing the interactivity of the application.

Improving Error Handling

While the current validation approach uses alert boxes, a more user-friendly method involves displaying error messages directly within the interface. This can be done by introducing reactive variables to handle error states and displaying them conditionally.

**Updating the Script Section for Enhanced Error Handling:**

import { defineComponent, ref, onMounted, watch } from 'vue';

export default defineComponent({
  name: 'RectangleDrawer',
  setup() {
    const left = ref(50);
    const top = ref(50);
    const right = ref(200);
    const bottom = ref(200);
    const canvasRef = ref(null);
    const ctx = ref(null);
    const errorMessage = ref('');

    const drawRectangle = () => {
      if (!ctx.value || !canvasRef.value) return;

      // Input validation
      if (isNaN(left.value) || isNaN(top.value) || isNaN(right.value) || isNaN(bottom.value)) {
        errorMessage.value = "All coordinates must be valid numbers.";
        clearCanvas();
        return;
      }

      if (left.value > right.value) {
        errorMessage.value = "Left coordinate must be less than or equal to Right coordinate.";
        clearCanvas();
        return;
      }

      if (top.value > bottom.value) {
        errorMessage.value = "Top coordinate must be less than or equal to Bottom coordinate.";
        clearCanvas();
        return;
      }

      // Clear any previous error messages
      errorMessage.value = '';

      const width = right.value - left.value;
      const height = bottom.value - top.value;

      // Clear the canvas before drawing
      ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);

      // Styling the rectangle
      ctx.value.fillStyle = 'rgba(0, 0, 255, 0.3)';
      ctx.value.strokeStyle = '#388278';
      ctx.value.lineWidth = 2;

      // Draw filled rectangle
      ctx.value.fillRect(left.value, top.value, width, height);

      // Draw rectangle border
      ctx.value.strokeRect(left.value, top.value, width, height);
    };

    const clearCanvas = () => {
      if (ctx.value && canvasRef.value) {
        ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
      }
    };

    onMounted(() => {
      if (canvasRef.value) {
        ctx.value = canvasRef.value.getContext('2d');
        drawRectangle();
      }
    });

    return {
      left,
      top,
      right,
      bottom,
      canvasRef,
      drawRectangle,
      errorMessage
    };
  }
});

**Adding the Error Message Display:**

<template>
  <div class="rectangle-drawer">
    <h2>Draw a Rectangle</h2>
    <div class="inputs">
      <label>Left:</label>
      <input type="number" v-model.number="left" placeholder="Left X-coordinate" @input="drawRectangle" />
      
      <label>Top:</label>
      <input type="number" v-model.number="top" placeholder="Top Y-coordinate" @input="drawRectangle" />
      
      <label>Right:</label>
      <input type="number" v-model.number="right" placeholder="Right X-coordinate" @input="drawRectangle" />
      
      <label>Bottom:</label>
      <input type="number" v-model.number="bottom" placeholder="Bottom Y-coordinate" @input="drawRectangle" />
    </div>
    
    <div v-if="errorMessage" class="error-message">
      <p>{{ errorMessage }}</p>
    </div>
    
    <canvas ref="canvasRef" width="600" height="400" class="canvas"></canvas>
  </div>
</template>

**Styling the Error Message:**

/* Additional styles */

.error-message {
  color: red;
  margin-bottom: 10px;
  font-weight: bold;
}

**Benefits of Enhanced Error Handling:**

  • Provides immediate and clear feedback to the user without intrusive alert boxes.
  • Improves the overall user experience by maintaining a clean and responsive interface.
  • Makes it easier to troubleshoot and correct input errors.

Optimizing the Drawing Process

Debouncing Input Events

When handling real-time input events, it's important to optimize performance to prevent excessive function calls, especially when working with high-frequency events like input. Debouncing ensures that the drawRectangle function is not called too frequently, which can enhance performance and reduce unnecessary computations.

**Implementing Debounce with Vue's watch Function:**

import { defineComponent, ref, onMounted, watch } from 'vue';

export default defineComponent({
  name: 'RectangleDrawer',
  setup() {
    const left = ref(50);
    const top = ref(50);
    const right = ref(200);
    const bottom = ref(200);
    const canvasRef = ref(null);
    const ctx = ref(null);
    const errorMessage = ref('');
    let debounceTimeout: number;

    const drawRectangle = () => {
      if (!ctx.value || !canvasRef.value) return;

      // Input validation
      if (isNaN(left.value) || isNaN(top.value) || isNaN(right.value) || isNaN(bottom.value)) {
        errorMessage.value = "All coordinates must be valid numbers.";
        clearCanvas();
        return;
      }

      if (left.value > right.value) {
        errorMessage.value = "Left coordinate must be less than or equal to Right coordinate.";
        clearCanvas();
        return;
      }

      if (top.value > bottom.value) {
        errorMessage.value = "Top coordinate must be less than or equal to Bottom coordinate.";
        clearCanvas();
        return;
      }

      // Clear any previous error messages
      errorMessage.value = '';

      const width = right.value - left.value;
      const height = bottom.value - top.value;

      // Clear the canvas before drawing
      ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);

      // Styling the rectangle
      ctx.value.fillStyle = 'rgba(0, 0, 255, 0.3)';
      ctx.value.strokeStyle = '#388278';
      ctx.value.lineWidth = 2;

      // Draw filled rectangle
      ctx.value.fillRect(left.value, top.value, width, height);

      // Draw rectangle border
      ctx.value.strokeRect(left.value, top.value, width, height);
    };

    const clearCanvas = () => {
      if (ctx.value && canvasRef.value) {
        ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
      }
    };

    onMounted(() => {
      if (canvasRef.value) {
        ctx.value = canvasRef.value.getContext('2d');
        drawRectangle();
      }
    });

    // Watch for changes and debounce the drawRectangle call
    watch([left, top, right, bottom], () => {
      clearTimeout(debounceTimeout);
      debounceTimeout = window.setTimeout(() => {
        drawRectangle();
      }, 300); // Adjust the debounce delay as needed
    });

    return {
      left,
      top,
      right,
      bottom,
      canvasRef,
      drawRectangle,
      errorMessage
    };
  }
});

**Explanation:**

  • Introduces a debounceTimeout variable to store the timeout ID.
  • Uses Vue's watch function to monitor changes in the coordinate inputs.
  • Implements a debounce mechanism by clearing any existing timeout and setting a new one each time an input changes.
  • The drawRectangle function is called only after the user has stopped typing for 300 milliseconds.

Responsive Canvas

To ensure that the canvas remains responsive across different screen sizes, you can adjust its dimensions based on the viewport or parent container. Additionally, you might want to scale the drawing context accordingly.

**Implementing a Responsive Canvas:**

import { defineComponent, ref, onMounted, watch, onBeforeUnmount } from 'vue';

export default defineComponent({
  name: 'RectangleDrawer',
  setup() {
    const left = ref(50);
    const top = ref(50);
    const right = ref(200);
    const bottom = ref(200);
    const canvasRef = ref(null);
    const ctx = ref(null);
    const errorMessage = ref('');
    let debounceTimeout: number;

    const drawRectangle = () => {
      if (!ctx.value || !canvasRef.value) return;

      // Input validation
      if (isNaN(left.value) || isNaN(top.value) || isNaN(right.value) || isNaN(bottom.value)) {
        errorMessage.value = "All coordinates must be valid numbers.";
        clearCanvas();
        return;
      }

      if (left.value > right.value) {
        errorMessage.value = "Left coordinate must be less than or equal to Right coordinate.";
        clearCanvas();
        return;
      }

      if (top.value > bottom.value) {
        errorMessage.value = "Top coordinate must be less than or equal to Bottom coordinate.";
        clearCanvas();
        return;
      }

      // Clear any previous error messages
      errorMessage.value = '';

      const width = right.value - left.value;
      const height = bottom.value - top.value;

      // Clear the canvas before drawing
      ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);

      // Styling the rectangle
      ctx.value.fillStyle = 'rgba(0, 0, 255, 0.3)';
      ctx.value.strokeStyle = '#388278';
      ctx.value.lineWidth = 2;

      // Draw filled rectangle
      ctx.value.fillRect(left.value, top.value, width, height);

      // Draw rectangle border
      ctx.value.strokeRect(left.value, top.value, width, height);
    };

    const clearCanvas = () => {
      if (ctx.value && canvasRef.value) {
        ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
      }
    };

    const resizeCanvas = () => {
      if (canvasRef.value) {
        const parent = canvasRef.value.parentElement;
        if (parent) {
          canvasRef.value.width = parent.clientWidth;
          canvasRef.value.height = parent.clientHeight;
          drawRectangle();
        }
      }
    };

    onMounted(() => {
      if (canvasRef.value) {
        ctx.value = canvasRef.value.getContext('2d');
        resizeCanvas();
      }
      window.addEventListener('resize', resizeCanvas);
    });

    onBeforeUnmount(() => {
      window.removeEventListener('resize', resizeCanvas);
    });

    // Watch for changes and debounce the drawRectangle call
    watch([left, top, right, bottom], () => {
      clearTimeout(debounceTimeout);
      debounceTimeout = window.setTimeout(() => {
        drawRectangle();
      }, 300);
    });

    return {
      left,
      top,
      right,
      bottom,
      canvasRef,
      drawRectangle,
      errorMessage
    };
  }
});

**Explanation:**

  • resizeCanvas Function:
    • Adjusts the canvas width and height based on its parent container's dimensions.
    • Calls drawRectangle to re-render the rectangle after resizing.
  • onMounted:
    • Initializes the canvas context.
    • Calls resizeCanvas to set the initial size.
    • Adds a window resize event listener to handle dynamic resizing.
  • onBeforeUnmount:
    • Removes the window resize event listener to prevent memory leaks.

Advanced Features

Adding Drag-and-Drop Functionality

Enhancing the rectangle drawing tool with drag-and-drop capabilities can significantly improve user interaction. Users can click and drag on the canvas to define the rectangle's coordinates visually.

**Implementing Drag-and-Drop:** 1. **Tracking Mouse Events:**

  • mousedown: Initiates the drawing process.
  • mousemove: Updates the rectangle's dimensions dynamically as the mouse moves.
  • mouseup: Finalizes the rectangle's dimensions.
2. **Updating Reactive Variables:**
  • As the user drags the mouse, update the left, top, right, bottom values accordingly.
3. **Redrawing the Rectangle:**
  • Invoke drawRectangle during the mousemove event to reflect changes in real-time.

**Sample Implementation:**

import { defineComponent, ref, onMounted, onBeforeUnmount } from 'vue';

export default defineComponent({
  name: 'RectangleDrawer',
  setup() {
    const left = ref(50);
    const top = ref(50);
    const right = ref(200);
    const bottom = ref(200);
    const canvasRef = ref(null);
    const ctx = ref(null);
    const errorMessage = ref('');
    let isDrawing = false;
    let startX = 0;
    let startY = 0;

    const drawRectangle = () => {
      if (!ctx.value || !canvasRef.value) return;

      // Clear the canvas before drawing
      ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);

      // Validate inputs
      if (isNaN(left.value) || isNaN(top.value) || isNaN(right.value) || isNaN(bottom.value)) {
        return;
      }

      const width = right.value - left.value;
      const height = bottom.value - top.value;

      // Styling the rectangle
      ctx.value.fillStyle = 'rgba(0, 0, 255, 0.3)';
      ctx.value.strokeStyle = '#388278';
      ctx.value.lineWidth = 2;

      // Draw filled rectangle
      ctx.value.fillRect(left.value, top.value, width, height);

      // Draw rectangle border
      ctx.value.strokeRect(left.value, top.value, width, height);
    };

    const handleMouseDown = (event: MouseEvent) => {
      isDrawing = true;
      const rect = canvasRef.value!.getBoundingClientRect();
      startX = event.clientX - rect.left;
      startY = event.clientY - rect.top;
      left.value = startX;
      top.value = startY;
    };

    const handleMouseMove = (event: MouseEvent) => {
      if (!isDrawing) return;
      const rect = canvasRef.value!.getBoundingClientRect();
      right.value = event.clientX - rect.left;
      bottom.value = event.clientY - rect.top;
      drawRectangle();
    };

    const handleMouseUp = () => {
      if (isDrawing) {
        isDrawing = false;
        // Ensure right > left and bottom > top
        if (left.value > right.value) {
          [left.value, right.value] = [right.value, left.value];
        }
        if (top.value > bottom.value) {
          [top.value, bottom.value] = [bottom.value, top.value];
        }
        drawRectangle();
      }
    };

    onMounted(() => {
      if (canvasRef.value) {
        ctx.value = canvasRef.value.getContext('2d');
        drawRectangle();
        canvasRef.value.addEventListener('mousedown', handleMouseDown);
        canvasRef.value.addEventListener('mousemove', handleMouseMove);
        window.addEventListener('mouseup', handleMouseUp);
      }
    });

    onBeforeUnmount(() => {
      if (canvasRef.value) {
        canvasRef.value.removeEventListener('mousedown', handleMouseDown);
        canvasRef.value.removeEventListener('mousemove', handleMouseMove);
        window.removeEventListener('mouseup', handleMouseUp);
      }
    });

    return {
      left,
      top,
      right,
      bottom,
      canvasRef,
      errorMessage
    };
  }
});

**Explanation:**

  • Variables:
    • isDrawing: Tracks whether the user is currently drawing.
    • startX, startY: Store the starting coordinates of the drag action.
  • Event Handlers:
    • handleMouseDown: Initiates drawing by setting isDrawing to true and storing the starting positions.
    • handleMouseMove: Updates the right and bottom coordinates as the mouse moves, calling drawRectangle to render the rectangle dynamically.
    • handleMouseUp: Finalizes the drawing by ensuring the coordinates are valid and resetting isDrawing to false.
  • Lifecycle Hooks:
    • onMounted: Sets up the canvas context and adds event listeners for mouse interactions.
    • onBeforeUnmount: Cleans up event listeners to prevent memory leaks.

Conclusion

By following this guide, you have successfully implemented a feature-rich rectangle drawing tool using Vue 3, TypeScript, and the HTML5 Canvas API. This implementation covers essential aspects such as user input handling, real-time drawing, error handling, performance optimization with debouncing, and advanced features like drag-and-drop functionality. These principles can be extended and customized to create more complex graphical tools tailored to your application's needs.

**Key Takeaways:**

  • Utilize Vue 3's Composition API and TypeScript for structured and type-safe code.
  • Leverage the HTML5 Canvas API for dynamic and interactive graphics.
  • Implement robust error handling to enhance user experience.
  • Optimize performance with techniques like debouncing.
  • Enhance interactivity with advanced features like drag-and-drop.

Further Resources

To deepen your understanding and expand on the topics covered, consider exploring the following resources:

By exploring these resources, you can enhance your skills and apply more sophisticated techniques to your Vue 3 and TypeScript projects.


Last updated January 7, 2025
Ask Ithy AI
Download Article
Delete Article