DEV Community

Cover image for AI-Enhanced Meme Creator: Harness Claude's Capabilities with Canvas API
Learn Computer Academy
Learn Computer Academy

Posted on

AI-Enhanced Meme Creator: Harness Claude's Capabilities with Canvas API

Have you ever wanted to quickly create custom memes without switching between different apps or websites?
Today, I'll walk you through building your very own Random Meme Generator that not only pulls images from an API but also lets you add custom text, choose fonts, and even draw on your memes! ๐ŸŽจ

This project combines API integration, canvas manipulation, and user interaction to create a feature-rich web application that's perfect for beginners looking to level up their front-end skills.

Check out the app here - https://playground.learncomputer.in/random-meme-generator/

What We'll Build

Our Meme Generator Pro will include:

  • Random meme fetching from the Imgflip API
  • Custom text additions with font selection and sizing
  • Drawing capabilities directly on the meme
  • Ability to upload your own images
  • Downloading your finished creations

Let's dive into the code and see how everything works together!

Setting Up the HTML Structure

First, we need to create the structure of our application with HTML. We'll need:

  • A container for our entire app
  • Controls for selecting meme categories and fetching random memes
  • An editor section with text customization tools
  • A canvas to display and edit our memes
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Meme Generator Pro</title>
    <link rel="stylesheet" href="styles.css">
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&family=Roboto&family=Montserrat&family=Oswald&family=Lato&family=Open+Sans&family=Raleway&family=Ubuntu&family=Source+Sans+Pro&family=Nunito&family=Roboto+Mono&family=Playfair+Display&family=Merriweather&family=PT+Sans&family=Quicksand&family=Inconsolata&family=Dosis&family=Arimo&family=Work+Sans&family=Fira+Sans&family=Rubik&family=Poppins:wght@300&family=Inter&family=Barlow&family=Overpass&display=swap" rel="stylesheet">
</head>
<body>
    <div class="container">
        <header>
            <h1>Meme Generator Pro</h1>
        </header>

        <div class="controls">
            <select id="categorySelect" class="modern-select">
                <option value="all">All Categories</option>
                <option value="funny">Funny</option>
                <option value="gaming">Gaming</option>
                <option value="animals">Animals</option>
                <option value="politics">Politics</option>
            </select>
            <button id="randomMemeBtn" class="btn primary-btn">Get Random Meme</button>
            <input type="file" id="customImage" accept="image/*" class="custom-upload">
        </div>

        <div class="meme-editor">
            <div class="editing-tools">
                <input type="text" id="captionInput" placeholder="Enter caption..." class="modern-input">
                <select id="fontSelect" class="modern-select">
                    <option value="Poppins">Poppins</option>
                    <option value="Roboto">Roboto</option>
                    <option value="Montserrat">Montserrat</option>
                    <option value="Oswald">Oswald</option>
                    <option value="Lato">Lato</option>
                    <option value="Open Sans">Open Sans</option>
                    <option value="Raleway">Raleway</option>
                    <option value="Ubuntu">Ubuntu</option>
                    <option value="Source Sans Pro">Source Sans Pro</option>
                    <option value="Nunito">Nunito</option>
                    <option value="Roboto Mono">Roboto Mono</option>
                    <option value="Playfair Display">Playfair Display</option>
                    <option value="Merriweather">Merriweather</option>
                    <option value="PT Sans">PT Sans</option>
                    <option value="Quicksand">Quicksand</option>
                    <option value="Inconsolata">Inconsolata</option>
                    <option value="Dosis">Dosis</option>
                    <option value="Arimo">Arimo</option>
                    <option value="Work Sans">Work Sans</option>
                    <option value="Fira Sans">Fira Sans</option>
                    <option value="Rubik">Rubik</option>
                    <option value="Inter">Inter</option>
                    <option value="Barlow">Barlow</option>
                    <option value="Overpass">Overpass</option>
                    <option value="Impact">Impact</option>
                </select>
                <input type="number" id="fontSize" min="10" max="100" value="30" class="modern-input">
                <input type="color" id="fontColor" value="#ffffff" class="color-picker">
                <button id="addTextBtn" class="btn secondary-btn">Add Text</button>
                <button id="drawBtn" class="btn secondary-btn">Draw โœ๏ธ</button>
                <input type="number" id="penWidth" min="1" max="20" value="2" class="modern-input" style="width: 70px;">
                <button id="downloadBtn" class="btn primary-btn">Download</button>
            </div>

            <div class="canvas-container">
                <canvas id="memeCanvas"></canvas>
            </div>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The HTML structure creates a clean interface with all the tools we need. Let's break down some key elements:

  • We include multiple Google Fonts for our text options
  • The select element lets users filter meme categories
  • Input fields allow customization of text appearance
  • The canvas element is where our meme will be displayed and edited

Styling Our Meme Generator

Next, we'll add CSS to make our Meme Generator look professional and user-friendly:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Poppins', sans-serif;
}

body {
    background: linear-gradient(135deg, #2c3e50 0%, #1a1a2e 100%);
    min-height: 100vh;
    color: white;
}

.container {
    max-width: 1200px;
    margin: 30px auto;
    padding: 20px;
}

header {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-bottom: 30px;
    background: rgba(44, 62, 80, 0.95);
    padding: 15px 25px;
    border-radius: 15px;
    box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}

h1 {
    color: white;
    font-size: 2.5rem;
    font-weight: 700;
}

.controls {
    display: flex;
    gap: 15px;
    margin-bottom: 25px;
    flex-wrap: wrap;
    justify-content: center;
}

.meme-editor {
    background: rgba(44, 62, 80, 0.95);
    border-radius: 20px;
    padding: 25px;
    box-shadow: 0 10px 30px rgba(0,0,0,0.2);
    backdrop-filter: blur(5px);
}

.canvas-container {
    position: relative;
    overflow: hidden;
    border-radius: 15px;
    text-align: center;
}

#memeCanvas {
    max-width: 100%;
    border-radius: 15px;
    border: 2px solid #34495e;
}

.editing-tools {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
    padding: 15px;
    background: rgba(52, 73, 94, 0.9);
    border-radius: 15px;
    margin-bottom: 25px;
}

.btn {
    padding: 12px 25px;
    border: none;
    border-radius: 12px;
    cursor: pointer;
    font-weight: 600;
    transition: all 0.3s ease;
    box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}

.primary-btn {
    background: linear-gradient(45deg, #3498db, #2980b9);
    color: white;
}

.secondary-btn {
    background: linear-gradient(45deg, #2ecc71, #27ae60);
    color: white;
}

.btn:hover {
    transform: translateY(-3px);
    box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}

.modern-input, .modern-select {
    padding: 12px 20px;
    border: none;
    border-radius: 12px;
    background: #34495e;
    color: white;
    box-shadow: 0 4px 15px rgba(0,0,0,0.1);
    transition: all 0.3s ease;
}

.modern-input::placeholder {
    color: #bdc3c7;
}

.modern-input:focus, .modern-select:focus {
    outline: none;
    box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}

.color-picker {
    padding: 5px;
    border-radius: 8px;
    border: none;
    width: 50px;
    height: 45px;
    cursor: pointer;
    background: #34495e;
}

.custom-upload {
    padding: 12px 20px;
    border-radius: 12px;
    background: #34495e;
    color: white;
    cursor: pointer;
    box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}

.custom-upload::-webkit-file-upload-button {
    visibility: hidden;
}

.custom-upload::before {
    content: 'Choose File';
    display: inline-block;
}

@media (max-width: 768px) {
    .controls, .editing-tools {
        flex-direction: column;
    }
}
Enter fullscreen mode Exit fullscreen mode

The CSS gives our app a modern, dark-themed look with:

  • A gradient background for visual appeal
  • Rounded corners and subtle shadows for depth
  • Responsive design that works on mobile devices
  • Custom styling for buttons and form elements
  • Flex layouts to organize our controls

The JavaScript Magic

Below is the full JS Code used in the app

const canvas = document.getElementById('memeCanvas');
const ctx = canvas.getContext('2d');
let currentImage = new Image();
let isDrawing = false;
let isDragging = false;
let lastX = 0;
let lastY = 0;
let selectedElement = null;

class DraggableElement {
    constructor(type, text, x, y, font, size, color) {
        this.type = type;
        this.text = text;
        this.x = x;
        this.y = y;
        this.font = font;
        this.size = size;
        this.color = color;
    }

    draw() {
        ctx.font = `${this.size}px ${this.font}`;
        ctx.fillStyle = this.color;
        ctx.fillText(this.text, this.x, this.y);
    }

    isPointInside(x, y) {
        ctx.font = `${this.size}px ${this.font}`;
        const textWidth = ctx.measureText(this.text).width;
        return x >= this.x && x <= this.x + textWidth &&
               y >= this.y - this.size && y <= this.y;
    }
}

class DrawingPath {
    constructor() {
        this.points = [];
        this.color = '';
        this.width = 0;
    }

    addPoint(x, y) {
        this.points.push({ x, y });
    }

    draw() {
        if (this.points.length < 2) return;
        ctx.beginPath();
        ctx.strokeStyle = this.color;
        ctx.lineWidth = this.width;
        ctx.moveTo(this.points[0].x, this.points[0].y);
        for (let i = 1; i < this.points.length; i++) {
            ctx.lineTo(this.points[i].x, this.points[i].y);
        }
        ctx.stroke();
    }
}

let elements = [];
let drawingPaths = [];

function drawMeme() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(currentImage, 0, 0);
    elements.forEach(element => element.draw());
    drawingPaths.forEach(path => path.draw());
}


async function fetchRandomMeme(category = 'all') {
    try {
        const response = await fetch('https://api.imgflip.com/get_memes');
        const data = await response.json();
        const memes = data.data.memes;
        const randomMeme = memes[Math.floor(Math.random() * memes.length)];

        currentImage = new Image();
        currentImage.crossOrigin = "Anonymous";
        currentImage.src = randomMeme.url;
        currentImage.onload = () => {
            canvas.width = currentImage.width;
            canvas.height = currentImage.height;
            drawMeme();
        };
        currentImage.onerror = () => {
            console.error('Error loading image, possibly due to CORS');
        };
    } catch (error) {
        console.error('Error fetching meme:', error);
    }
}


canvas.addEventListener('mousedown', (e) => {
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    if (isDrawing) {
        lastX = x;
        lastY = y;
        canvas.isDrawing = true;
        const newPath = new DrawingPath();
        newPath.color = document.getElementById('fontColor').value;
        newPath.width = document.getElementById('penWidth').value;
        newPath.addPoint(x, y);
        drawingPaths.push(newPath);
    } else {
        selectedElement = elements.find(el => el.isPointInside(x, y));
        if (selectedElement) {
            isDragging = true;
            selectedElement.offsetX = x - selectedElement.x;
            selectedElement.offsetY = y - selectedElement.y;
            canvas.style.cursor = 'grabbing';
        }
    }
});

canvas.addEventListener('mousemove', (e) => {
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    if (canvas.isDrawing && isDrawing) {
        const currentPath = drawingPaths[drawingPaths.length - 1];
        currentPath.addPoint(x, y);
        drawMeme();
        lastX = x;
        lastY = y;
    } else if (isDragging && selectedElement) {
        selectedElement.x = x - selectedElement.offsetX;
        selectedElement.y = y - selectedElement.offsetY;
        drawMeme(); 
    } else {
        const hoverElement = elements.find(el => el.isPointInside(x, y));
        canvas.style.cursor = hoverElement && !isDrawing ? 'grab' : (isDrawing ? 'crosshair' : 'default');
    }
});

canvas.addEventListener('mouseup', () => {
    if (canvas.isDrawing) {
        canvas.isDrawing = false;
    }
    if (isDragging) {
        isDragging = false;
        selectedElement = null;
        canvas.style.cursor = 'default';
        drawMeme(); 
    }
});


document.getElementById('randomMemeBtn').addEventListener('click', () => {
    fetchRandomMeme(document.getElementById('categorySelect').value);
});

document.getElementById('addTextBtn').addEventListener('click', () => {
    const text = document.getElementById('captionInput').value;
    if (text) {
        const font = document.getElementById('fontSelect').value;
        const size = document.getElementById('fontSize').value;
        const color = document.getElementById('fontColor').value;
        const element = new DraggableElement('text', text, 50, 50, font, size, color);
        elements.push(element);
        drawMeme();
        document.getElementById('captionInput').value = '';
    }
});

document.getElementById('drawBtn').addEventListener('click', () => {
    isDrawing = !isDrawing;
    canvas.style.cursor = isDrawing ? 'crosshair' : 'default';
});

document.getElementById('downloadBtn').addEventListener('click', () => {

    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = canvas.width;
    tempCanvas.height = canvas.height;
    const tempCtx = tempCanvas.getContext('2d');


    tempCtx.drawImage(currentImage, 0, 0);


    elements.forEach(element => {
        tempCtx.font = `${element.size}px ${element.font}`;
        tempCtx.fillStyle = element.color;
        tempCtx.fillText(element.text, element.x, element.y);
    });


    drawingPaths.forEach(path => {
        if (path.points.length < 2) return;
        tempCtx.beginPath();
        tempCtx.strokeStyle = path.color;
        tempCtx.lineWidth = path.width;
        tempCtx.moveTo(path.points[0].x, path.points[0].y);
        for (let i = 1; i < path.points.length; i++) {
            tempCtx.lineTo(path.points[i].x, path.points[i].y);
        }
        tempCtx.stroke();
    });


    const link = document.createElement('a');
    link.download = 'meme.png';
    link.href = tempCanvas.toDataURL('image/png');
    link.click();
});

document.getElementById('customImage').addEventListener('change', (e) => {
    const file = e.target.files[0];
    if (file) {
        const reader = new FileReader();
        reader.onload = (event) => {
            currentImage = new Image();
            currentImage.src = event.target.result;
            currentImage.onload = () => {
                canvas.width = currentImage.width;
                canvas.height = currentImage.height;
                elements = [];
                drawingPaths = [];
                drawMeme();
            };
        };
        reader.readAsDataURL(file);
    }
});


fetchRandomMeme();
Enter fullscreen mode Exit fullscreen mode

Now for the fun part - making everything functional with JavaScript! Let's break down the key components:

1. Setting Up the Canvas

We start by getting references to our canvas element and creating a drawing context:

canvas.addEventListener('mousedown', (e) => {
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    if (isDrawing) {
        lastX = x;
        lastY = y;
        canvas.isDrawing = true;
        const newPath = new DrawingPath();
        newPath.color = document.getElementById('fontColor').value;
        newPath.width = document.getElementById('penWidth').value;
        newPath.addPoint(x, y);
        drawingPaths.push(newPath);
    } else {
        selectedElement = elements.find(el => el.isPointInside(x, y));
        if (selectedElement) {
            isDragging = true;
            selectedElement.offsetX = x - selectedElement.x;
            selectedElement.offsetY = y - selectedElement.y;
            canvas.style.cursor = 'grabbing';
        }
    }
});

canvas.addEventListener('mousemove', (e) => {
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    if (canvas.isDrawing && isDrawing) {
        const currentPath = drawingPaths[drawingPaths.length - 1];
        currentPath.addPoint(x, y);
        drawMeme();
        lastX = x;
        lastY = y;
    } else if (isDragging && selectedElement) {
        selectedElement.x = x - selectedElement.offsetX;
        selectedElement.y = y - selectedElement.offsetY;
        drawMeme(); 
    } else {
        const hoverElement = elements.find(el => el.isPointInside(x, y));
        canvas.style.cursor = hoverElement && !isDrawing ? 'grab' : (isDrawing ? 'crosshair' : 'default');
    }
});

canvas.addEventListener('mouseup', () => {
    if (canvas.isDrawing) {
        canvas.isDrawing = false;
    }
    if (isDragging) {
        isDragging = false;
        selectedElement = null;
        canvas.style.cursor = 'default';
        drawMeme(); 
    }
});

Enter fullscreen mode Exit fullscreen mode

2. Creating Draggable Elements

We need a way to add text to our memes and allow users to position it wherever they want:

class DraggableElement {
    constructor(type, text, x, y, font, size, color) {
        this.type = type;
        this.text = text;
        this.x = x;
        this.y = y;
        this.font = font;
        this.size = size;
        this.color = color;
    }

    draw() {
        ctx.font = `${this.size}px ${this.font}`;
        ctx.fillStyle = this.color;
        ctx.fillText(this.text, this.x, this.y);
    }

    isPointInside(x, y) {
        ctx.font = `${this.size}px ${this.font}`;
        const textWidth = ctx.measureText(this.text).width;
        return x >= this.x && x <= this.x + textWidth &&
               y >= this.y - this.size && y <= this.y;
    }
}

Enter fullscreen mode Exit fullscreen mode

The DraggableElement class handles:

  • Storing text properties (content, position, font, size, color)
  • Drawing text on the canvas
  • Detecting when the user clicks on text to move it

3. Drawing Functionality

For added creativity, we implement drawing capabilities:

class DrawingPath {
    constructor() {
        this.points = [];
        this.color = '';
        this.width = 0;
    }

    addPoint(x, y) {
        this.points.push({ x, y });
    }

    draw() {
        if (this.points.length < 2) return;
        ctx.beginPath();
        ctx.strokeStyle = this.color;
        ctx.lineWidth = this.width;
        ctx.moveTo(this.points[0].x, this.points[0].y);
        for (let i = 1; i < this.points.length; i++) {
            ctx.lineTo(this.points[i].x, this.points[i].y);
        }
        ctx.stroke();
    }
}

Enter fullscreen mode Exit fullscreen mode

The DrawingPath class:

  • Tracks points as the user draws
  • Stores color and line width
  • Renders the path on the canvas

4. API Integration

Now let's fetch random memes from the Imgflip API:

async function fetchRandomMeme(category = 'all') {
    try {
        const response = await fetch('https://api.imgflip.com/get_memes');
        const data = await response.json();
        const memes = data.data.memes;
        const randomMeme = memes[Math.floor(Math.random() * memes.length)];

        currentImage = new Image();
        currentImage.crossOrigin = "Anonymous";
        currentImage.src = randomMeme.url;
        currentImage.onload = () => {
            canvas.width = currentImage.width;
            canvas.height = currentImage.height;
            drawMeme();
        };
        currentImage.onerror = () => {
            console.error('Error loading image, possibly due to CORS');
        };
    } catch (error) {
        console.error('Error fetching meme:', error);
    }
}

Enter fullscreen mode Exit fullscreen mode

This function:

  • Makes an asynchronous request to the Imgflip API
  • Selects a random meme from the response
  • Loads the image into our canvas
  • Handles CORS and error scenarios

5. Event Listeners for Interactivity

We need several event listeners to handle user interactions:

These listeners manage:

  • Detecting when users click on text elements
  • Dragging text to reposition it
  • Drawing on the canvas when drawing mode is active

6. UI Controls

Finally, we connect our buttons and inputs to their respective functions:

These event listeners:

  • Fetch new random memes
  • Add text to the canvas
  • Toggle drawing mode
  • Download the finished meme
  • Handle custom image uploads

How It All Works Together

When you load the page, the app immediately fetches a random meme. From there, you can:

  1. Add custom text using the input field
  2. Change text properties (font, size, color)
  3. Drag text to position it perfectly
  4. Toggle drawing mode to add doodles
  5. Download your creation when it's ready
  6. Upload your own image to meme-ify

Key Technical Challenges

Working with the Canvas API

The HTML Canvas API is powerful but can be tricky. We needed to:

  • Manage drawing context properly
  • Create a system for tracking draggable elements
  • Implement proper event handling for drawing and dragging
  • Handle image loading and CORS issues

Managing Multiple Interactive Elements

Having both draggable text and drawing functionality required careful state management:

We use the isDrawing and isDragging flags to keep track of the current interaction mode.

Image Processing and Download

To download the final meme, we need to create a temporary canvas with all elements properly rendered:

document.getElementById('downloadBtn').addEventListener('click', () => {

    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = canvas.width;
    tempCanvas.height = canvas.height;
    const tempCtx = tempCanvas.getContext('2d');


    tempCtx.drawImage(currentImage, 0, 0);


    elements.forEach(element => {
        tempCtx.font = `${element.size}px ${element.font}`;
        tempCtx.fillStyle = element.color;
        tempCtx.fillText(element.text, element.x, element.y);
    });


    drawingPaths.forEach(path => {
        if (path.points.length < 2) return;
        tempCtx.beginPath();
        tempCtx.strokeStyle = path.color;
        tempCtx.lineWidth = path.width;
        tempCtx.moveTo(path.points[0].x, path.points[0].y);
        for (let i = 1; i < path.points.length; i++) {
            tempCtx.lineTo(path.points[i].x, path.points[i].y);
        }
        tempCtx.stroke();
    });


    const link = document.createElement('a');
    link.download = 'meme.png';
    link.href = tempCanvas.toDataURL('image/png');
    link.click();
});

Enter fullscreen mode Exit fullscreen mode

This approach ensures that all text and drawings are properly "baked" into the final image.

Enhancing the Project

Here are some ways you could expand on this project:

  • Add text shadows or outlines for better visibility on any background
  • Implement image filters (brightness, contrast, etc.)
  • Add stickers or emoji options
  • Create a gallery to save your creations
  • Implement undo/redo functionality

Conclusion

Building a Meme Generator is not only fun but also an excellent way to practice working with APIs, Canvas manipulation, and interactive UI elements. The skills you develop while creating this project are transferable to many other web applications.

Give it a try yourself, and don't be afraid to experiment with additional features! The complete project is available at https://playground.learncomputer.in/random-meme-generator/ if you want to see it in action before building your own. ๐Ÿš€

Happy meme-making! ๐Ÿ˜Ž

Top comments (0)