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! 😎