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