<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<!-- Primary Meta Tags -->
<title>Meme Maker | Quick In-Browser Meme Generator</title>
<meta name="title" content="Meme Maker | Quick In-Browser Meme Generator" />
<meta name="description" content="A lightweight, fast, mobile-friendly meme maker. Load photos, paste from clipboard, use random Wikipedia animal images, drag, scale, and rotate text overlays instantly." />
<meta name="keywords" content="meme maker, meme generator, mobile meme editor, wikipedia animals, instant meme, image editor" />
<meta name="author" content="Danielle" />
<meta name="theme-color" content="#121212" />
<meta name="color-scheme" content="dark" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://meme-maker.pages.dev/" />
<meta property="og:title" content="Meme Maker — Fast Mobile Meme Generator" />
<meta property="og:description" content="Generate memes directly in your browser. Clipboard paste, drag-and-drop, Wikipedia animal starter images, live rotation, and direct image copying." />
<meta property="og:image" content="https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Capybara_%28Hydrochoerus_hydrochaeris%29.JPG/800px-Capybara_%28Hydrochoerus_hydrochaeris%29.JPG" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://meme-maker.pages.dev/" />
<meta property="twitter:title" content="Meme Maker — Fast Mobile Meme Generator" />
<meta property="twitter:description" content="Generate memes directly in your browser. Clipboard paste, drag-and-drop, Wikipedia animal starter images, live rotation, and direct image copying." />
<meta property="twitter:image" content="https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Capybara_%28Hydrochoerus_hydrochaeris%29.JPG/800px-Capybara_%28Hydrochoerus_hydrochaeris%29.JPG" />
<!-- Inline Favicon -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🖼️</text></svg>" />
<link rel="apple-touch-icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🖼️</text></svg>" />
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, sans-serif;
touch-action: manipulation;
}
body {
background: #121212;
color: #fff;
padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.panel {
width: 100%;
max-width: 480px;
display: flex;
flex-direction: column;
gap: 10px;
}
.row {
display: flex;
gap: 8px;
}
input[type="text"], input[type="url"], button, select {
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #333;
background: #222;
color: #fff;
font-size: 14px;
flex: 1;
}
input[type="file"] {
display: none;
}
button {
background: #2e7d32;
border: none;
cursor: pointer;
font-weight: 600;
color: #fff;
}
button.secondary {
background: #333;
}
button.danger {
background: #c62828;
flex: 0 0 auto;
}
#canvas-wrap {
width: 100%;
max-width: 480px;
background: #1e1e1e;
border-radius: 8px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
canvas {
width: 100%;
height: auto;
display: block;
touch-action: none;
}
.text-card {
background: #1a1a1a;
padding: 10px;
border-radius: 8px;
border: 1px solid #2a2a2a;
display: flex;
flex-direction: column;
gap: 8px;
}
.text-card.selected {
border-color: #2196f3;
}
.text-card-controls {
display: flex;
gap: 8px;
align-items: center;
}
.text-card-controls input[type="color"] {
width: 40px;
height: 36px;
border: none;
border-radius: 6px;
background: transparent;
cursor: pointer;
}
.text-card-controls input[type="range"] {
flex: 1;
}
#preview-container {
display: none;
flex-direction: column;
gap: 10px;
align-items: center;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 12px;
width: 100%;
}
#preview-img {
width: 100%;
height: auto;
border-radius: 6px;
display: block;
}
</style>
</head>
<body>
<div class="panel">
<div class="row">
<label class="row" style="flex: 1;">
<button type="button" class="secondary" style="width: 100%;" onclick="document.getElementById('file-in').click()">📁 Device Image</button>
<input type="file" id="file-in" accept="image/*" />
</label>
<button class="secondary" onclick="pasteImage()">📋 Paste</button>
<button class="secondary" style="flex: 0 0 auto;" onclick="loadRandomAnimal()">🎲 Random Animal</button>
</div>
<div class="row">
<input type="url" id="url-in" placeholder="Image URL..." />
<button class="secondary" style="flex: 0 0 auto;" onclick="loadFromUrl()">Load URL</button>
</div>
</div>
<div id="canvas-wrap">
<canvas id="meme-canvas"></canvas>
</div>
<div class="panel">
<button onclick="addTextField()">+ Add Text</button>
<div id="text-list" style="display: flex; flex-direction: column; gap: 8px;"></div>
<div class="row">
<button onclick="showPreview()" style="background: #e65100;">Preview / Copy</button>
<button onclick="downloadMeme()" style="background: #1565c0;">Save File</button>
</div>
<div id="preview-container">
<p style="font-size: 13px; color: #aaa; text-align: center;">Long-press or right-click the image below to copy or share:</p>
<img id="preview-img" alt="Meme Preview" />
<button class="secondary" onclick="copyToClipboardDirect()" style="width: 100%;">Copy Image Directly</button>
</div>
</div>
<script>
const canvas = document.getElementById('meme-canvas');
const ctx = canvas.getContext('2d');
let currentImage = null;
let textFields = [];
let selectedIndex = null;
let dragMode = null;
let dragStart = { x: 0, y: 0 };
let initialFieldState = null;
const HANDLE_RADIUS = 14;
const ANIMALS = [
'Capybara', 'Red_panda', 'Alpaca', 'Otter', 'Sloth',
'Raccoon', 'Fennec_fox', 'Platypus', 'Pangolin', 'Quokka',
'Meerkat', 'Pallas%27s_cat', 'Wombat', 'Lemur', 'Hedgehog'
];
function initPlaceholder() {
canvas.width = 600;
canvas.height = 400;
ctx.fillStyle = '#2a2a2a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#777';
ctx.font = 'bold 20px Impact, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('FETCHING RANDOM ANIMAL...', canvas.width / 2, canvas.height / 2);
}
initPlaceholder();
async function loadRandomAnimal() {
const topic = ANIMALS[Math.floor(Math.random() * ANIMALS.length)];
try {
const res = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${topic}`);
const data = await res.json();
if (data.originalimage && data.originalimage.source) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => setBaseImage(img);
img.onerror = () => fallbackAnimalImage();
img.src = data.originalimage.source;
return;
}
} catch (err) {}
fallbackAnimalImage();
}
function fallbackAnimalImage() {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => setBaseImage(img);
img.src = 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Capybara_%28Hydrochoerus_hydrochaeris%29.JPG/800px-Capybara_%28Hydrochoerus_hydrochaeris%29.JPG';
}
function setBaseImage(img) {
currentImage = img;
canvas.width = img.naturalWidth || img.width;
canvas.height = img.naturalHeight || img.height;
if (textFields.length === 0) {
addTextField('TOP TEXT', canvas.width / 2, canvas.height * 0.15);
addTextField('BOTTOM TEXT', canvas.width / 2, canvas.height * 0.85);
} else {
render();
}
}
document.getElementById('file-in').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const img = new Image();
img.onload = () => setBaseImage(img);
img.src = URL.createObjectURL(file);
});
async function pasteImage() {
try {
const items = await navigator.clipboard.read();
for (const item of items) {
const type = item.types.find(t => t.startsWith('image/'));
if (type) {
const blob = await item.getType(type);
const img = new Image();
img.onload = () => setBaseImage(img);
img.src = URL.createObjectURL(blob);
return;
}
}
} catch (err) {
alert('Clipboard paste requires permission or focus. You can also press Ctrl+V / Cmd+V.');
}
}
window.addEventListener('paste', (e) => {
const items = (e.clipboardData || window.clipboardData).items;
for (const item of items) {
if (item.type.includes('image')) {
const blob = item.getAsFile();
const img = new Image();
img.onload = () => setBaseImage(img);
img.src = URL.createObjectURL(blob);
return;
}
}
});
function loadFromUrl() {
const url = document.getElementById('url-in').value.trim();
if (!url) return;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => setBaseImage(img);
img.onerror = () => alert('Failed to load image. Check the URL or CORS restrictions.');
img.src = url;
}
function addTextField(text = 'TEXT', x = null, y = null) {
const defaultSize = Math.max(24, Math.floor(canvas.height * 0.08));
textFields.push({
text: text,
color: '#FFFFFF',
strokeColor: '#000000',
size: defaultSize,
rotation: 0,
x: x !== null ? x : canvas.width / 2,
y: y !== null ? y : canvas.height / 2
});
selectedIndex = textFields.length - 1;
syncTextUI();
render();
}
function removeTextField(index, e) {
if (e) e.stopPropagation();
textFields.splice(index, 1);
if (selectedIndex === index) {
selectedIndex = null;
} else if (selectedIndex > index) {
selectedIndex--;
}
syncTextUI();
render();
}
function syncTextUI() {
const list = document.getElementById('text-list');
list.innerHTML = '';
textFields.forEach((field, i) => {
const div = document.createElement('div');
div.className = `text-card ${i === selectedIndex ? 'selected' : ''}`;
div.onclick = () => {
selectedIndex = i;
syncTextUI();
render();
};
div.innerHTML = `
<div class="row">
<input type="text" value="${field.text}" placeholder="Enter text..." oninput="updateText(${i}, 'text', this.value)" />
<button class="danger" onclick="removeTextField(${i}, event)">✕</button>
</div>
<div class="text-card-controls">
<label style="font-size: 12px; color: #888;">Color:</label>
<input type="color" value="${field.color}" oninput="updateText(${i}, 'color', this.value)" />
<label style="font-size: 12px; color: #888;">Size:</label>
<input type="range" min="12" max="${Math.max(120, canvas.height * 0.3)}" value="${field.size}" oninput="updateText(${i}, 'size', +this.value)" />
</div>
`;
list.appendChild(div);
});
}
function updateText(index, prop, value) {
textFields[index][prop] = value;
render();
}
function getFieldBounds(field) {
ctx.save();
ctx.font = `900 ${field.size}px Impact, sans-serif`;
const lines = field.text.split('\n');
const lineHeight = field.size * 1.1;
const totalHeight = Math.max(lines.length * lineHeight, field.size);
let maxWidth = 0;
lines.forEach(l => {
const w = ctx.measureText(l).width;
if (w > maxWidth) maxWidth = w;
});
ctx.restore();
const padding = 12;
const width = (maxWidth || field.size * 2) + padding * 2;
const height = totalHeight + padding;
return { width, height, halfW: width / 2, halfH: height / 2 };
}
function render(drawOverlays = true) {
if (!currentImage) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(currentImage, 0, 0, canvas.width, canvas.height);
textFields.forEach((field, i) => {
ctx.save();
ctx.translate(field.x, field.y);
ctx.rotate(field.rotation || 0);
ctx.font = `900 ${field.size}px Impact, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = field.color;
ctx.strokeStyle = field.strokeColor;
ctx.lineWidth = Math.max(2, field.size / 10);
ctx.lineJoin = 'miter';
ctx.miterLimit = 2;
const lines = field.text.split('\n');
const lineHeight = field.size * 1.1;
const totalHeight = lines.length * lineHeight;
const startY = -(totalHeight / 2) + (lineHeight / 2);
lines.forEach((line, lineIdx) => {
const y = startY + (lineIdx * lineHeight);
ctx.strokeText(line, 0, y);
ctx.fillText(line, 0, y);
});
if (drawOverlays && i === selectedIndex) {
const bounds = getFieldBounds(field);
ctx.strokeStyle = '#2196f3';
ctx.lineWidth = 2;
ctx.setLineDash([6, 6]);
ctx.strokeRect(-bounds.halfW, -bounds.halfH, bounds.width, bounds.height);
ctx.setLineDash([]);
ctx.fillStyle = '#2196f3';
ctx.beginPath();
ctx.arc(bounds.halfW, bounds.halfH, HANDLE_RADIUS, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, -bounds.halfH);
ctx.lineTo(0, -bounds.halfH - 24);
ctx.stroke();
ctx.fillStyle = '#ff9800';
ctx.beginPath();
ctx.arc(0, -bounds.halfH - 24, HANDLE_RADIUS, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
ctx.restore();
});
}
function getCanvasCoords(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
return {
x: (clientX - rect.left) * scaleX,
y: (clientY - rect.top) * scaleY
};
}
function toLocalCoords(point, field) {
const dx = point.x - field.x;
const dy = point.y - field.y;
const cos = Math.cos(-(field.rotation || 0));
const sin = Math.sin(-(field.rotation || 0));
return {
x: dx * cos - dy * sin,
y: dx * sin + dy * cos
};
}
function hitTest(point) {
if (selectedIndex !== null) {
const field = textFields[selectedIndex];
const local = toLocalCoords(point, field);
const bounds = getFieldBounds(field);
const rotHandle = { x: 0, y: -bounds.halfH - 24 };
if (Math.hypot(local.x - rotHandle.x, local.y - rotHandle.y) <= HANDLE_RADIUS * 1.5) {
return { index: selectedIndex, mode: 'rotate' };
}
const resizeHandle = { x: bounds.halfW, y: bounds.halfH };
if (Math.hypot(local.x - resizeHandle.x, local.y - resizeHandle.y) <= HANDLE_RADIUS * 1.5) {
return { index: selectedIndex, mode: 'resize' };
}
}
for (let i = textFields.length - 1; i >= 0; i--) {
const field = textFields[i];
const local = toLocalCoords(point, field);
const bounds = getFieldBounds(field);
if (Math.abs(local.x) <= bounds.halfW && Math.abs(local.y) <= bounds.halfH) {
return { index: i, mode: 'move' };
}
}
return null;
}
function onPointerDown(e) {
const coords = getCanvasCoords(e);
const hit = hitTest(coords);
if (hit) {
selectedIndex = hit.index;
dragMode = hit.mode;
dragStart = coords;
initialFieldState = { ...textFields[selectedIndex] };
syncTextUI();
render();
} else {
selectedIndex = null;
dragMode = null;
syncTextUI();
render();
}
}
function onPointerMove(e) {
if (dragMode === null || selectedIndex === null) return;
e.preventDefault();
const coords = getCanvasCoords(e);
const field = textFields[selectedIndex];
if (dragMode === 'move') {
field.x = initialFieldState.x + (coords.x - dragStart.x);
field.y = initialFieldState.y + (coords.y - dragStart.y);
} else if (dragMode === 'resize') {
const initLocal = toLocalCoords(dragStart, initialFieldState);
const currentLocal = toLocalCoords(coords, initialFieldState);
const initDist = Math.hypot(initLocal.x, initLocal.y);
const currentDist = Math.hypot(currentLocal.x, currentLocal.y);
if (initDist > 0) {
const scale = currentDist / initDist;
field.size = Math.max(12, Math.min(300, Math.round(initialFieldState.size * scale)));
syncTextUI();
}
} else if (dragMode === 'rotate') {
const angle = Math.atan2(coords.y - field.y, coords.x - field.x);
field.rotation = angle + Math.PI / 2;
}
render();
}
function onPointerUp() {
dragMode = null;
}
canvas.addEventListener('mousedown', onPointerDown);
window.addEventListener('mousemove', onPointerMove);
window.addEventListener('mouseup', onPointerUp);
canvas.addEventListener('touchstart', onPointerDown, { passive: false });
window.addEventListener('touchmove', onPointerMove, { passive: false });
window.addEventListener('touchend', onPointerUp);
function showPreview() {
if (!currentImage) return;
render(false);
const dataUrl = canvas.toDataURL('image/png');
const previewImg = document.getElementById('preview-img');
const previewContainer = document.getElementById('preview-container');
previewImg.src = dataUrl;
previewContainer.style.display = 'flex';
previewContainer.scrollIntoView({ behavior: 'smooth' });
render(true);
}
async function copyToClipboardDirect() {
if (!currentImage) return;
render(false);
try {
canvas.toBlob(async (blob) => {
try {
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
]);
alert('Image copied to clipboard!');
} catch (err) {
alert('Direct copying is restricted by your browser. Use long-press or right-click on the preview image instead.');
} finally {
render(true);
}
}, 'image/png');
} catch (err) {
render(true);
alert('Could not copy image automatically. Please long-press the preview image.');
}
}
function downloadMeme() {
if (!currentImage) return;
render(false);
const link = document.createElement('a');
link.download = 'meme.png';
link.href = canvas.toDataURL('image/png');
link.click();
render(true);
}
loadRandomAnimal();
</script>
</body>
</html>