/** * Meme Creator Logic * DYNAMIC MULTIPLE LAYERS */ (() => { const canvas = document.getElementById('memeCanvas'); if (canvas) { const ctx = canvas.getContext('2d'); const layersContainer = document.getElementById('textLayersContainer'); const addTextBtn = document.getElementById('addText'); const uploadBtn = document.getElementById('uploadMeme'); // Core state let textLayers = []; let dragOffset = { x: 0, y: 0 }; let draggingLayer = null; let hoveredLayer = null; let img = new Image(); let hasLoadedImage = window.memeTemplate.id !== 'custom' && window.memeTemplate.category !== 'Custom'; const memeFont = 'Impact, Charcoal, sans-serif'; function wrapText(ctx, text, maxWidth) { const paragraphs = text.split('\n'); const lines = []; paragraphs.forEach(paragraph => { if (paragraph.trim() === '') { lines.push(''); return; } const words = paragraph.split(' ').filter(w => w !== ''); let currentLine = ''; words.forEach(word => { const testLine = currentLine ? currentLine + ' ' + word : word; let metrics = ctx.measureText(testLine); if (metrics.width <= maxWidth) { currentLine = testLine; } else { if (currentLine !== '') { lines.push(currentLine); currentLine = ''; } metrics = ctx.measureText(word); if (metrics.width <= maxWidth) { currentLine = word; } else { let charLine = ''; for (let i = 0; i < word.length; i++) { const char = word[i]; const testCharLine = charLine + char; if (ctx.measureText(testCharLine).width > maxWidth && charLine !== '') { lines.push(charLine); charLine = char; } else { charLine = testCharLine; } } currentLine = charLine; } } }); if (currentLine) { lines.push(currentLine); } }); return lines; } // Image Setup img.crossOrigin = "anonymous"; img.onload = () => { canvas.width = img.width || 800; canvas.height = img.height || 600; const defaultSize = 40; // Initial layers - only set if we don't have any layers yet and we have loaded an image if (textLayers.length === 0 && hasLoadedImage) { textLayers = [ { id: Date.now(), text: '', x: canvas.width / 2, y: 40, fontSize: defaultSize }, { id: Date.now() + 1, text: '', x: canvas.width / 2, y: canvas.height - 100, fontSize: defaultSize } ]; } else if (hasLoadedImage) { // Keep the text layers but adjust their coordinates to be in-bounds if they exceed new boundaries textLayers.forEach(layer => { if (layer.x > canvas.width) { layer.x = canvas.width / 2; } if (layer.y > canvas.height) { layer.y = canvas.height - 100; } }); } renderInputs(); draw(); }; img.src = window.memeTemplate.url; // Ensure font is loaded before first draw if (document.fonts) { document.fonts.ready.then(() => { draw(); }); } function createSlider(container, min, max, initValue, onChange) { container.style.cssText = 'position:relative;height:28px;display:flex;align-items:center;flex:1;cursor:pointer;user-select:none;-webkit-user-select:none;touch-action:none;'; const track = document.createElement('div'); track.style.cssText = 'position:absolute;left:8px;right:8px;height:4px;background:#333;border-radius:2px;'; const fill = document.createElement('div'); fill.style.cssText = 'position:absolute;left:0;height:100%;background:var(--accent,#9f0);border-radius:2px;pointer-events:none;'; const thumb = document.createElement('div'); thumb.style.cssText = 'position:absolute;width:18px;height:18px;background:var(--accent,#9f0);border-radius:50%;transform:translateX(-50%);box-shadow:0 0 6px rgba(0,0,0,.6);pointer-events:none;transition:transform .1s;'; const setRatio = (r) => { fill.style.width = (r * 100) + '%'; thumb.style.left = (r * 100) + '%'; }; setRatio((initValue - min) / (max - min)); track.appendChild(fill); track.appendChild(thumb); container.appendChild(track); const valueFromClientX = (clientX) => { const rect = track.getBoundingClientRect(); const r = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); setRatio(r); return Math.round(min + r * (max - min)); }; container.addEventListener('pointerdown', (e) => { e.preventDefault(); // block keyboard + scroll takeover container.setPointerCapture(e.pointerId); thumb.style.transform = 'translateX(-50%) scale(1.25)'; onChange(valueFromClientX(e.clientX)); }, { passive: false }); container.addEventListener('pointermove', (e) => { if (!container.hasPointerCapture(e.pointerId)) return; onChange(valueFromClientX(e.clientX)); }); const onEnd = (e) => { if (!container.hasPointerCapture(e.pointerId)) return; container.releasePointerCapture(e.pointerId); thumb.style.transform = 'translateX(-50%) scale(1)'; }; container.addEventListener('pointerup', onEnd); container.addEventListener('pointercancel', onEnd); } function renderInputs() { layersContainer.innerHTML = ''; textLayers.forEach((layer, index) => { const div = document.createElement('div'); div.className = 'form-group layer-input-group'; div.style.marginBottom = '20px'; div.innerHTML = `
${(window.f0ckI18n?.meme?.size_label) || 'Size'}: ${layer.fontSize}px
`; const textarea = div.querySelector('textarea'); textarea.addEventListener('input', (e) => { layer.text = e.target.value; draw(); }); const fsSlider = div.querySelector('.layer-fs-slider'); const fsVal = div.querySelector('.layer-fs-val'); createSlider(fsSlider, 10, 200, layer.fontSize, (val) => { layer.fontSize = val; fsVal.textContent = val; draw(); }); const removeBtn = div.querySelector('.remove-layer'); removeBtn.addEventListener('click', () => { textLayers = textLayers.filter(l => l.id !== layer.id); renderInputs(); draw(); }); layersContainer.appendChild(div); }); } addTextBtn.addEventListener('click', () => { if (!hasLoadedImage) { window.flashMessage((window.f0ckI18n?.meme?.choose_image_first) || 'Please select an image first!', 3000, 'error'); return; } textLayers.push({ id: Date.now(), text: 'NEW TEXT', x: canvas.width / 2, y: canvas.height / 2, fontSize: 40 }); renderInputs(); draw(); }); function draw() { if (!img.complete) return; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillStyle = 'white'; ctx.strokeStyle = 'black'; ctx.miterLimit = 2; // Prevent sharp spikes in characters like 'A' const globalFontSize = 40; // Render each layer textLayers.forEach((layer) => { if (!layer.text) return; const fontSize = layer.fontSize || 40; ctx.font = `bold ${fontSize}px ${memeFont}`; let displayStr = layer.text.toUpperCase(); const lines = wrapText(ctx, displayStr, canvas.width * 0.9); const h = lines.length * fontSize * 1.1; const w = canvas.width * 0.9; // Box for the dragged/hovered layer (top-most layer gets preference) if (hoveredLayer === layer || draggingLayer === layer) { ctx.save(); ctx.setLineDash([5, 5]); ctx.strokeStyle = '#9f0'; ctx.lineWidth = 2; ctx.strokeRect(layer.x - w / 2, layer.y - 10, w, h + 20); ctx.restore(); } lines.forEach((line, i) => { const yOffset = i * (fontSize * 1.1); const renderX = Math.round(layer.x); const renderY = Math.round(layer.y + yOffset); ctx.save(); ctx.font = `bold ${fontSize}px ${memeFont}`; // Ensure correct font size per line ctx.strokeStyle = 'black'; ctx.lineWidth = Math.max(2, fontSize / 8); // Slightly thicker stroke for better legibility ctx.strokeText(line, renderX, renderY); ctx.fillText(line, renderX, renderY); ctx.restore(); }); }); } // Options hooks removed as they are now hardcoded and the inputs are gone const getEventPos = (e) => { const rect = canvas.getBoundingClientRect(); const clientX = e.clientX || (e.touches && e.touches[0].clientX); const clientY = e.clientY || (e.touches && e.touches[0].clientY); return { x: (clientX - rect.left) * (canvas.width / rect.width), y: (clientY - rect.top) * (canvas.height / rect.height) }; }; const isInsideText = (pt, layer) => { if (!layer.text) return false; const fontSize = layer.fontSize || 40; ctx.save(); ctx.font = `bold ${fontSize}px ${memeFont}`; const lines = wrapText(ctx, layer.text.toUpperCase(), canvas.width * 0.9); ctx.restore(); const w = canvas.width * 0.95; const h = lines.length * fontSize * 1.2; return pt.x >= layer.x - w / 2 && pt.x <= layer.x + w / 2 && pt.y >= layer.y - 20 && pt.y <= layer.y + h + 20; }; // POINTER EVENTS const onStart = (e) => { const pt = getEventPos(e); // Find layer (start from top-most, reverse of render order) draggingLayer = [...textLayers].reverse().find(layer => isInsideText(pt, layer)) || null; if (draggingLayer) { dragOffset = { x: pt.x - draggingLayer.x, y: pt.y - draggingLayer.y }; if (e.pointerId) canvas.setPointerCapture(e.pointerId); canvas.style.cursor = 'grabbing'; draw(); e.preventDefault(); } }; const onMove = (e) => { const pt = getEventPos(e); if (draggingLayer) { draggingLayer.x = pt.x - dragOffset.x; draggingLayer.y = pt.y - dragOffset.y; draw(); e.preventDefault(); } else { // Hover logic const currentHover = [...textLayers].reverse().find(layer => isInsideText(pt, layer)) || null; if (currentHover !== hoveredLayer) { hoveredLayer = currentHover; canvas.style.cursor = hoveredLayer ? 'grab' : 'crosshair'; draw(); } } }; const onEnd = (e) => { if (draggingLayer) { if (e.pointerId) canvas.releasePointerCapture(e.pointerId); draggingLayer = null; canvas.style.cursor = 'grab'; draw(); } }; canvas.addEventListener('pointerdown', onStart); window.addEventListener('pointermove', onMove); window.addEventListener('pointerup', onEnd); canvas.addEventListener('pointercancel', onEnd); canvas.addEventListener('mousedown', onStart); // Upload uploadBtn.addEventListener('click', async () => { if (!hasLoadedImage) { window.flashMessage((window.f0ckI18n?.meme?.choose_image_first) || 'Please select an image first!', 3000, 'error'); return; } const category = (window.memeTemplate && window.memeTemplate.category) ? window.memeTemplate.category.toLowerCase() : ''; const subCategory = (window.memeTemplate && window.memeTemplate.sub_category) ? window.memeTemplate.sub_category.toLowerCase() : ''; const isOrakelVon10 = subCategory === 'von10'; const isOrakelUser = subCategory === 'user'; const isOrakelNormal = category === 'orakel' && !isOrakelUser && !isOrakelVon10; let uploadCanvas = canvas; if (isOrakelNormal || isOrakelUser || isOrakelVon10) { // Create an off-screen canvas to apply the orakel answer silently uploadCanvas = document.createElement('canvas'); uploadCanvas.width = canvas.width; uploadCanvas.height = canvas.height; const uCtx = uploadCanvas.getContext('2d'); // Copy current canvas state uCtx.drawImage(canvas, 0, 0); let result = ''; if (isOrakelNormal) { const outcomes = ['JA', 'NEIN', 'VIELLEICHT', 'AUF JEDEN FALL', 'NIEMALS', 'SOWAS VON JA', 'VERGISS ES', 'FRAG SPÄTER', 'KOMMT DRAUF AN']; result = outcomes[Math.floor(Math.random() * outcomes.length)]; } else if (isOrakelUser) { try { const res = await fetch('/api/v2/orakel/user'); const data = await res.json(); result = (data.success && data.username) ? `${data.display_name || data.username}|||ID: ${data.id}` : 'Anonymous'; } catch (e) { result = 'Anonymous'; } } else if (isOrakelVon10) { result = Math.floor(Math.random() * 11).toString(); } // Draw Orakel result on the hidden canvas uCtx.save(); uCtx.font = (isOrakelVon10) ? 'bold 150px Impact' : 'bold 80px Impact'; // Bigger font for the rating uCtx.textAlign = 'center'; uCtx.textBaseline = 'middle'; if (isOrakelNormal) { uCtx.shadowBlur = 20; uCtx.shadowColor = 'rgba(101, 37, 212, 1)'; } else if (isOrakelVon10) { uCtx.shadowBlur = 0; // No shadow as requested } else { // No shadow for the User Orakel uCtx.shadowBlur = 0; } uCtx.fillStyle = '#fff'; uCtx.strokeStyle = '#000'; uCtx.lineWidth = 10; uCtx.miterLimit = 2; // Adjust position for user Orakel (reverting to +10 offset) let yPos = Math.round(isOrakelUser ? (uploadCanvas.height / 2 + 10) : (uploadCanvas.height / 2 + 50)); if (isOrakelVon10) { yPos = Math.round(uploadCanvas.height / 2 ); // 1px lower } const xPos = Math.round(uploadCanvas.width / 2); // Auto-fit font size for user orakel — shrink until text fits within image width let orakelFontSize = isOrakelVon10 ? 150 : 80; const maxTextWidth = uploadCanvas.width - 80; // 40px padding each side if (isOrakelUser) { const parts = result.split('|||'); const namePart = parts[0]; const idPart = parts.length > 1 ? `(${parts[1]})` : ''; const combinedText = idPart ? `${namePart} ${idPart}` : namePart; // Even tighter threshold for User Orakel (approx 25% total padding) const userMaxWidth = Math.round(uploadCanvas.width * 0.75); let currentFontSize = 74; uCtx.font = `bold ${currentFontSize}px Impact`; // First attempt: Shrink entire text on one line down to 58px if needed while (uCtx.measureText(combinedText).width > userMaxWidth && currentFontSize > 58) { currentFontSize -= 2; uCtx.font = `bold ${currentFontSize}px Impact`; } const combinedFits = uCtx.measureText(combinedText).width <= userMaxWidth; if (combinedFits) { // Single line — potentially shrunk for long names uCtx.fillText(combinedText, xPos, yPos); } else { // Two lines — auto-fit just the name, ID below let nameFontSize = 74; uCtx.font = `bold ${nameFontSize}px Impact`; while (uCtx.measureText(namePart).width > userMaxWidth && nameFontSize > 16) { nameFontSize -= 2; uCtx.font = `bold ${nameFontSize}px Impact`; } const idFontSize = Math.max(18, Math.round(nameFontSize * 0.45)); const lineGap = Math.round(nameFontSize * 0.65); const nameY = Math.round(yPos - lineGap / 2); const idY = Math.round(yPos + lineGap / 2) + 2; uCtx.font = `bold ${nameFontSize}px Impact`; uCtx.fillText(namePart, xPos, nameY); if (idPart) { uCtx.font = `bold ${idFontSize}px Impact`; uCtx.fillText(idPart, xPos, idY); } } } else { // Normal / von10 — single line as before uCtx.font = `bold ${orakelFontSize}px Impact`; uCtx.strokeText(result, xPos, yPos); uCtx.fillText(result, xPos, yPos); } uCtx.restore(); } uploadBtn.disabled = true; uploadBtn.innerHTML = ' ' + (window.f0ckI18n?.uploading || 'Uploading...'); try { const blob = await new Promise(resolve => uploadCanvas.toBlob(resolve, 'image/jpeg', 0.95)); const formData = new FormData(); formData.append('file', blob, `meme-${Date.now()}.jpg`); const decodeHTMLEntities = (text) => { const textArea = document.createElement('textarea'); textArea.innerHTML = text; return textArea.value; }; const defaultTags = decodeHTMLEntities(document.getElementById('tags').value || 'meme'); const autoTag = window.memeTemplate ? decodeHTMLEntities(window.memeTemplate.name) : ''; const tags = `${defaultTags}, ${autoTag}`; formData.append('rating', 'sfw'); formData.append('tags', tags); formData.append('csrf_token', window.csrf_token); const res = await fetch('/api/v2/upload', { method: 'POST', body: formData, headers: { 'X-CSRF-Token': window.csrf_token, 'X-Requested-With': 'XMLHttpRequest' } }); const result = await res.json(); if (result.success) { const dest = result.redirect || '/meme'; if (window.loadItemAjax) { window.loadItemAjax(dest); } else if (window.loadPageAjax) { window.loadPageAjax(dest); } else { window.location.href = dest; } } else { window.flashMessage('Error: ' + result.msg, 3000, 'error'); uploadBtn.disabled = false; uploadBtn.innerHTML = ` ${(window.f0ckI18n?.meme?.upload_btn) || 'Upload Meme'}`; } } catch (err) { window.flashMessage('Upload failed', 3000, 'error'); uploadBtn.disabled = false; } }); // Custom Local Image Selector logic const fileInput = document.getElementById('customTemplateFile'); const selectFileBtn = document.getElementById('selectCustomFileBtn'); const canvasWrapper = document.querySelector('.canvas-wrapper'); const loadLocalImage = (file) => { if (file && file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (event) => { hasLoadedImage = true; img.src = event.target.result; // Update template metadata window.memeTemplate.name = file.name.replace(/\.[^/.]+$/, ""); // Update header title dynamically const headerTitle = document.querySelector('.meme-title'); if (headerTitle) { const baseTitle = window.f0ckI18n?.meme?.create_meme || 'Create Meme:'; headerTitle.innerHTML = `${baseTitle} ${window.memeTemplate.name}`; } // Update tags input value if tags are present const tagsInput = document.getElementById('tags'); if (tagsInput) { tagsInput.value = `meme, Custom, ${window.memeTemplate.name}`; } }; reader.readAsDataURL(file); } }; if (selectFileBtn && fileInput) { selectFileBtn.addEventListener('click', () => { fileInput.click(); }); fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; loadLocalImage(file); }); } // HTML5 Drag & Drop Support if (canvas && canvasWrapper) { const preventDefaults = (e) => { e.preventDefault(); e.stopPropagation(); }; ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { canvasWrapper.addEventListener(eventName, preventDefaults, false); }); ['dragenter', 'dragover'].forEach(eventName => { canvasWrapper.addEventListener(eventName, () => { canvasWrapper.classList.add('drag-hover'); }, false); }); ['dragleave', 'drop'].forEach(eventName => { canvasWrapper.addEventListener(eventName, () => { canvasWrapper.classList.remove('drag-hover'); }, false); }); canvasWrapper.addEventListener('drop', (e) => { const dt = e.dataTransfer; const file = dt.files[0]; loadLocalImage(file); }, false); } // Initial draw setTimeout(draw, 300); } })();