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
</span>
 lang="en">

     charset="UTF-8">
     name="viewport" content="width=device-width, initial-scale=1.0">
    Meme Generator Pro
     rel="stylesheet" href="styles.css">
     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">


     class="container">
        
            Meme Generator Pro
        

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

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

             class="canvas-container">
                 id="memeCanvas">
            
        
    
    <span class="na">src="script.js">





    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:
Add custom text using the input field
Change text properties (font, size, color)
Drag text to position it perfectly
Toggle drawing mode to add doodles
Download your creation when it's ready
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! 😎