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>
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;
}
}
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();
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();
}
});
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;
}
}
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();
}
}
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);
}
}
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();
});
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)