(() => { "use strict"; const wrap = document.querySelector(".meme-wrap"); if (!wrap) return; const IMAGES = [ "/clashcat.jpg", "/cards/card-cavalry.webp", "/cards/card-throne.webp", "/cards/card-hammer.webp", "/cards/card-banner.webp", "/cards/card-rampart.webp", "/cards/card-pounce.webp", // new ones "/memes/temp1.webp", "/memes/temp2.webp", "/memes/temp3.webp", "/memes/temp4.webp", "/memes/temp5.webp", "/memes/temp6.webp", "/memes/temp7.webp", "/memes/temp8.webp", "/memes/temp9.webp", "/memes/temp10.webp", "/memes/temp11.webp", "/memes/temp12.webp", "/memes/temp13.webp", "/memes/temp14.webp", "/memes/temp15.webp", "/memes/temp16.webp", "/memes/temp17.webp", "/memes/temp18.webp", "/memes/temp19.webp", "/memes/temp20.webp", ]; const MAX_CANVAS_WIDTH = 1400; const canvas = document.getElementById("canvas"); if (!canvas) return; const ctx = canvas.getContext("2d"); const thumbBar = document.getElementById("thumbs"); const fileInput = document.getElementById("file"); const el = { topText: document.getElementById("topText"), bottomText: document.getElementById("bottomText"), fontFamily: document.getElementById("fontFamily"), fontScale: document.getElementById("fontScale"), strokeWidth: document.getElementById("strokeWidth"), color: document.getElementById("color"), strokeColor: document.getElementById("strokeColor"), uppercase: document.getElementById("uppercase"), sizeVal: document.getElementById("sizeVal"), strokeVal: document.getElementById("strokeVal"), randomize: document.getElementById("randomize"), resetPos: document.getElementById("resetPos"), download: document.getElementById("download"), }; const state = { img: null, topText: "WHEN THE CURVE", bottomText: "FINALLY GRADUATES", fontFamily: 'Impact, Haettenschweiler, "Arial Narrow Bold", sans-serif', fontScale: 9, strokeWidth: 10, color: "#ffffff", strokeColor: "#000000", uppercase: true, topPos: { x: 0.5, y: 0.10 }, bottomPos: { x: 0.5, y: 0.90 }, }; const DEFAULT_POS = { top: { x: 0.5, y: 0.10 }, bottom: { x: 0.5, y: 0.90 } }; let W = canvas.width; let H = canvas.height; const boxes = { top: null, bottom: null }; function loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error("Could not load " + src)); img.crossOrigin = "anonymous"; img.src = src; }); } function wrapLines(text, maxWidth) { const out = []; for (const para of text.split("\n")) { const words = para.split(/\s+/).filter(Boolean); if (!words.length) { out.push(""); continue; } let line = ""; for (const word of words) { const test = line ? line + " " + word : word; if (line && ctx.measureText(test).width > maxWidth) { out.push(line); line = word; } else { line = test; } } out.push(line); } return out; } function drawPlaceholder() { const g = ctx.createLinearGradient(0, 0, W, H); g.addColorStop(0, "#1b0d28"); g.addColorStop(1, "#12061c"); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); ctx.fillStyle = "rgba(255, 215, 0, .35)"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.font = `600 ${Math.round(W / 26)}px system-ui, sans-serif`; ctx.fillText("Pick an image to start", W / 2, H / 2); } function drawTextBlock(text, pos, key) { const raw = state.uppercase ? text.toUpperCase() : text; if (!raw.trim()) { boxes[key] = null; return; } const fontSize = Math.max(8, Math.round(W * state.fontScale / 100)); const lineHeight = fontSize * 1.12; ctx.font = `700 ${fontSize}px ${state.fontFamily}`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.lineJoin = "round"; ctx.miterLimit = 2; const lines = wrapLines(raw, W * 0.92); const cx = pos.x * W; const cy = pos.y * H; const total = lines.length * lineHeight; let maxLineWidth = 0; for (const line of lines) { maxLineWidth = Math.max(maxLineWidth, ctx.measureText(line).width); } ctx.lineWidth = fontSize * state.strokeWidth / 100; ctx.strokeStyle = state.strokeColor; ctx.fillStyle = state.color; let y = cy - total / 2 + lineHeight / 2; for (const line of lines) { if (ctx.lineWidth > 0) ctx.strokeText(line, cx, y); ctx.fillText(line, cx, y); y += lineHeight; } const pad = fontSize * 0.35; boxes[key] = { x: cx - maxLineWidth / 2 - pad, y: cy - total / 2 - pad, w: maxLineWidth + pad * 2, h: total + pad * 2, }; } function draw() { ctx.clearRect(0, 0, W, H); if (state.img) ctx.drawImage(state.img, 0, 0, W, H); else drawPlaceholder(); boxes.top = null; boxes.bottom = null; drawTextBlock(state.topText, state.topPos, "top"); drawTextBlock(state.bottomText, state.bottomPos, "bottom"); } function sizeCanvas(img) { const scale = Math.min(1, MAX_CANVAS_WIDTH / img.naturalWidth); W = Math.max(1, Math.round(img.naturalWidth * scale)); H = Math.max(1, Math.round(img.naturalHeight * scale)); canvas.width = W; canvas.height = H; } function setActiveThumb(key) { thumbBar.querySelectorAll("[data-src]").forEach(t => { t.classList.toggle("active", t.dataset.src === key); }); } async function useImage(src, key = src) { const img = await loadImage(src); state.img = img; sizeCanvas(img); setActiveThumb(key); el.download.disabled = false; draw(); } IMAGES.forEach((src, i) => { const btn = document.createElement("button"); btn.type = "button"; btn.className = "meme-thumb"; btn.dataset.src = src; btn.title = "Template " + (i + 1); const img = document.createElement("img"); img.src = src; img.alt = "Meme template " + (i + 1); img.loading = "lazy"; img.onerror = () => { btn.classList.add("broken"); btn.disabled = true; btn.title = "Could not load " + src; }; btn.appendChild(img); btn.addEventListener("click", () => useImage(src).catch(() => {})); thumbBar.insertBefore(btn, thumbBar.querySelector(".meme-upload")); }); let drag = null; function canvasPoint(e) { const r = canvas.getBoundingClientRect(); return { x: (e.clientX - r.left) * (canvas.width / r.width), y: (e.clientY - r.top) * (canvas.height / r.height), }; } function hitTest(p) { for (const key of ["bottom", "top"]) { const b = boxes[key]; if (!b) continue; if (p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h) return key; } return null; } const clamp01 = v => Math.min(1, Math.max(0, v)); canvas.addEventListener("pointerdown", e => { if (!state.img) return; const p = canvasPoint(e); const key = hitTest(p); if (!key) return; const pos = state[key + "Pos"]; drag = { key, offsetX: p.x - pos.x * W, offsetY: p.y - pos.y * H }; canvas.setPointerCapture(e.pointerId); canvas.classList.add("dragging"); e.preventDefault(); }); canvas.addEventListener("pointermove", e => { const p = canvasPoint(e); if (!drag) { canvas.style.cursor = state.img && hitTest(p) ? "grab" : "default"; return; } const pos = state[drag.key + "Pos"]; pos.x = clamp01((p.x - drag.offsetX) / W); pos.y = clamp01((p.y - drag.offsetY) / H); draw(); }); function endDrag(e) { if (!drag) return; drag = null; canvas.classList.remove("dragging"); canvas.style.cursor = "grab"; if (e && e.pointerId !== undefined && canvas.hasPointerCapture(e.pointerId)) { canvas.releasePointerCapture(e.pointerId); } } canvas.addEventListener("pointerup", endDrag); canvas.addEventListener("pointercancel", endDrag); canvas.addEventListener("pointerleave", () => { if (!drag) canvas.style.cursor = "default"; }); el.topText.addEventListener("input", () => { state.topText = el.topText.value; draw(); }); el.bottomText.addEventListener("input", () => { state.bottomText = el.bottomText.value; draw(); }); el.fontFamily.addEventListener("change", () => { state.fontFamily = el.fontFamily.value; draw(); }); el.fontScale.addEventListener("input", () => { state.fontScale = parseFloat(el.fontScale.value); el.sizeVal.textContent = state.fontScale + "%"; draw(); }); el.strokeWidth.addEventListener("input", () => { state.strokeWidth = parseFloat(el.strokeWidth.value); el.strokeVal.textContent = state.strokeWidth + "%"; draw(); }); el.color.addEventListener("input", () => { state.color = el.color.value; draw(); }); el.strokeColor.addEventListener("input", () => { state.strokeColor = el.strokeColor.value; draw(); }); el.uppercase.addEventListener("change", () => { state.uppercase = el.uppercase.checked; draw(); }); el.resetPos.addEventListener("click", () => { state.topPos = { ...DEFAULT_POS.top }; state.bottomPos = { ...DEFAULT_POS.bottom }; draw(); }); const CAPTIONS = [ ["WHEN THE CODE", "FINALLY WORKS"], ["NOBODY:", "ME AT 3AM"], ["IT WORKS ON", "MY MACHINE"], ["ONE DOES NOT SIMPLY", "DEBUG ON A FRIDAY"], ["THIS IS FINE", "EVERYTHING IS FINE"], ["WHEN THE CLIENT", "CHANGES THE BRIEF"], ["I HAVE NO IDEA", "WHAT I AM DOING"], ["404", "MOTIVATION NOT FOUND"], ["JUST ONE MORE", "REFACTOR"], ["WORKS IN PROD", "OBVIOUSLY"], ["ME EXPLAINING", "MY OWN CODE"], ["SHIP IT", "AND PRAY"], ["WHEN THE CURVE", "FINALLY GRADUATES"], ["NOBODY:", "JUST GEORGE"], ["IT WORKS ON", "SOLANA MAINNET"], ["ONE DOES NOT SIMPLY", "SKIP THE BONDING CURVE"], ["THIS IS FINE", "TOKEN GOES BRRR"], ["WHEN THE HOLDERS", "GET THEIR SOL"], ["I HAVE NO IDEA", "WHAT A CLASH SCORE IS"], ["404", "FEE ENGINE NOT FOUND"], ["JUST ONE MORE", "CLASHPAD LAUNCH"], ["LIVE ON METEORA", "OBVIOUSLY"], ["ME EXPLAINING", "THE 1% FEE"], ["SHIP IT", "AND PUMP"], ["GEORGE PLAYS", "CLASH ROYALE"], ["90% BACK", "TO THE HOLDERS"], ["WHEN YOU APE IN", "AT THE TOP"], ["TRUST ME BRO", "IT'S DIFFERENT"], ["MY BAGS", "ARE HEAVY"], ["I BOUGHT THE DIP", "THE DIP KEPT DIPPING"], ["WEN MOON", "SOON™"], ["HOLDING SINCE", "THE BONDING CURVE"], ["NOT FINANCIAL ADVICE", "BUT CLASHCAT"], ["GEORGE SAID", "HOLD THE LINE"], ["MY WIFE'S BOYFRIEND", "LOVES CLASHCAT"], ["DIAMOND HANDS", "PAPER BRAIN"], ["WAGMI", "PROBABLY"], ["THE FEE ENGINE", "NEVER SLEEPS"], ["1% IN", "90% BACK"], ["CLASHPAD GRADUATE", "SUMMA CUM LAUDE"], ["MASCOT BY DAY", "PRINTER BY NIGHT"], ["I'M NOT CRYING", "IT'S THE TEARS OF CLASHCAT"], ["JUST CHECKED", "STILL GRADUATED"], ["WHEN THE FEE", "HITS YOUR WALLET"], ["TOLD MY BOSS", "I'M BUSY"], ["SOLANA SPEED", "CLASHCAT GREED"], ["UP ONLY", "FROM HERE"], ["CAN'T STOP", "WON'T STOP"], ["$CLASHCAT", "CHANGED MY LIFE"], ["ME VS THE CLASH SCORE", "IT'S NOT CLOSE"], ["LOOKING AT THE CHART", "LIKE IT OWES ME MONEY"], ["PUMP IT", "I DARE YOU"], ["NOT SELLING", "EVER"], ["THE MASCOT", "OUTPERFORMS"], ["WATCHING GEORGE", "COOK"], ["GG", "SEE YOU AT THE TOP"], ]; let lastPick = -1; el.randomize.addEventListener("click", () => { let i; do { i = Math.floor(Math.random() * CAPTIONS.length); } while (i === lastPick && CAPTIONS.length > 1); lastPick = i; const [top, bottom] = CAPTIONS[i]; el.topText.value = top; el.bottomText.value = bottom; state.topText = top; state.bottomText = bottom; state.topPos = { ...DEFAULT_POS.top }; state.bottomPos = { ...DEFAULT_POS.bottom }; draw(); }); el.download.addEventListener("click", () => { if (!state.img) return; try { const url = canvas.toDataURL("image/webp", 0.92); const ext = url.startsWith("data:image/webp") ? "webp" : "png"; const a = document.createElement("a"); a.download = "clashcat-meme-" + Date.now() + "." + ext; a.href = url; a.click(); } catch { alert("Could not export the image (canvas may be tainted)."); } }); fileInput.addEventListener("change", async () => { const file = fileInput.files && fileInput.files[0]; if (!file) return; const url = URL.createObjectURL(file); try { await useImage(url, "upload"); } catch { alert("That file could not be loaded as an image."); } finally { URL.revokeObjectURL(url); fileInput.value = ""; } }); (async function init() { draw(); for (const src of IMAGES) { try { await useImage(src); return; } catch { /* try next */ } } })(); })();