| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
function drawMeme(canvas, img, topText, bottomText, opts) { |
| 5 |
var ctx = canvas.getContext('2d'); |
| 6 |
var W = canvas.width, H = canvas.height; |
| 7 |
|
| 8 |
// Background |
| 9 |
ctx.fillStyle = opts.canvasBg || '#cccccc'; |
| 10 |
ctx.fillRect(0, 0, W, H); |
| 11 |
|
| 12 |
// Image |
| 13 |
if (img && img.complete && img.naturalWidth > 0) { |
| 14 |
// Cover fit |
| 15 |
var iw = img.naturalWidth, ih = img.naturalHeight; |
| 16 |
var scale = Math.max(W / iw, H / ih); |
| 17 |
var dw = iw * scale, dh = ih * scale; |
| 18 |
ctx.drawImage(img, (W - dw) / 2, (H - dh) / 2, dw, dh); |
| 19 |
} |
| 20 |
|
| 21 |
// Text settings |
| 22 |
var fontSize = opts.memeFontSize || 52; |
| 23 |
var font = opts.memeFont || 'Impact'; |
| 24 |
var fillColor = opts.memeTextColor || '#ffffff'; |
| 25 |
var stroke = opts.memeStrokeColor|| '#000000'; |
| 26 |
var strokeW = (opts.memeStrokeWidth || 3) * (W / 560); |
| 27 |
|
| 28 |
ctx.font = 'bold ' + fontSize + 'px ' + font + ', Arial Black, sans-serif'; |
| 29 |
ctx.textAlign = 'center'; |
| 30 |
ctx.textBaseline = 'top'; |
| 31 |
ctx.lineJoin = 'round'; |
| 32 |
ctx.miterLimit = 2; |
| 33 |
|
| 34 |
function drawText(text, y) { |
| 35 |
if (!text) return; |
| 36 |
text = text.toUpperCase(); |
| 37 |
ctx.strokeStyle = stroke; |
| 38 |
ctx.lineWidth = strokeW; |
| 39 |
ctx.strokeText(text, W / 2, y, W - 24); |
| 40 |
ctx.fillStyle = fillColor; |
| 41 |
ctx.fillText(text, W / 2, y, W - 24); |
| 42 |
} |
| 43 |
|
| 44 |
var pad = Math.round(fontSize * 0.2); |
| 45 |
drawText(topText, pad); |
| 46 |
|
| 47 |
ctx.textBaseline = 'bottom'; |
| 48 |
drawText(bottomText, H - pad); |
| 49 |
} |
| 50 |
|
| 51 |
function initBlock(root) { |
| 52 |
var optsRaw = root.getAttribute('data-opts'); |
| 53 |
var opts; |
| 54 |
try { opts = JSON.parse(optsRaw); } catch (e) { opts = {}; } |
| 55 |
|
| 56 |
var _typoKeys = { family:'font-family', weight:'font-weight', style:'font-style', decoration:'text-decoration', transform:'text-transform', sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m', lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m', letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m', wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m' }; |
| 57 |
function typoCssVarsForEl(el, typo, prefix) { |
| 58 |
if (!typo || typeof typo !== 'object') return; |
| 59 |
Object.keys(_typoKeys).forEach(function (k) { |
| 60 |
if (typo[k] !== undefined && typo[k] !== '') el.style.setProperty(prefix + _typoKeys[k], typo[k]); |
| 61 |
}); |
| 62 |
} |
| 63 |
|
| 64 |
var accent = opts.accentColor || '#6366f1'; |
| 65 |
var titleColor = opts.titleColor || '#1e1b4b'; |
| 66 |
var sectionBg = opts.sectionBg || ''; |
| 67 |
|
| 68 |
if (sectionBg) root.style.background = sectionBg; |
| 69 |
root.style.setProperty('--bkbg-mme-accent', accent); |
| 70 |
|
| 71 |
typoCssVarsForEl(root, opts.titleTypo, '--bkbg-mme-tt-'); |
| 72 |
typoCssVarsForEl(root, opts.subtitleTypo, '--bkbg-mme-st-'); |
| 73 |
|
| 74 |
// Style title/subtitle |
| 75 |
var titleEl = root.querySelector('.bkbg-mme-title'); |
| 76 |
if (titleEl) { titleEl.style.color = titleColor; } |
| 77 |
var subEl = root.querySelector('.bkbg-mme-subtitle'); |
| 78 |
if (subEl) { subEl.style.color = titleColor; } |
| 79 |
|
| 80 |
var W = opts.canvasWidth || 560; |
| 81 |
var H = opts.canvasHeight || 420; |
| 82 |
|
| 83 |
// ---- Canvas ---- |
| 84 |
var canvasWrap = document.createElement('div'); |
| 85 |
canvasWrap.className = 'bkbg-mme-canvas-wrap'; |
| 86 |
|
| 87 |
var canvas = document.createElement('canvas'); |
| 88 |
canvas.className = 'bkbg-mme-canvas'; |
| 89 |
canvas.width = W; |
| 90 |
canvas.height = H; |
| 91 |
canvasWrap.appendChild(canvas); |
| 92 |
root.appendChild(canvasWrap); |
| 93 |
|
| 94 |
// ---- Controls ---- |
| 95 |
var ctrlDiv = document.createElement('div'); |
| 96 |
ctrlDiv.className = 'bkbg-mme-controls'; |
| 97 |
root.appendChild(ctrlDiv); |
| 98 |
|
| 99 |
function makeRow(labelText, inputEl) { |
| 100 |
var row = document.createElement('div'); |
| 101 |
row.className = 'bkbg-mme-row'; |
| 102 |
var lbl = document.createElement('label'); |
| 103 |
lbl.className = 'bkbg-mme-label'; |
| 104 |
lbl.style.color = titleColor; |
| 105 |
lbl.textContent = labelText; |
| 106 |
row.appendChild(lbl); |
| 107 |
row.appendChild(inputEl); |
| 108 |
ctrlDiv.appendChild(row); |
| 109 |
} |
| 110 |
|
| 111 |
function makeInput(placeholder, val) { |
| 112 |
var inp = document.createElement('input'); |
| 113 |
inp.type = 'text'; |
| 114 |
inp.className = 'bkbg-mme-input'; |
| 115 |
inp.placeholder = placeholder; |
| 116 |
inp.value = val || ''; |
| 117 |
inp.style.background = opts.canvasBg || '#f3f4f6'; |
| 118 |
inp.style.color = '#1e1b4b'; |
| 119 |
return inp; |
| 120 |
} |
| 121 |
|
| 122 |
var topInput = makeInput('Top text…', opts.defaultTopText || 'TOP TEXT'); |
| 123 |
var bottomInput = makeInput('Bottom text…', opts.defaultBottomText || 'BOTTOM TEXT'); |
| 124 |
var urlInput = makeInput('Image URL (https://…)', opts.defaultImageUrl || ''); |
| 125 |
|
| 126 |
makeRow('Top Text', topInput); |
| 127 |
makeRow('Bottom Text', bottomInput); |
| 128 |
makeRow('Image URL', urlInput); |
| 129 |
|
| 130 |
// ---- Actions ---- |
| 131 |
var actions = document.createElement('div'); |
| 132 |
actions.className = 'bkbg-mme-actions'; |
| 133 |
root.appendChild(actions); |
| 134 |
|
| 135 |
if (opts.showDownload !== false) { |
| 136 |
var dlBtn = document.createElement('button'); |
| 137 |
dlBtn.className = 'bkbg-mme-dl-btn'; |
| 138 |
dlBtn.style.background = '#22c55e'; |
| 139 |
dlBtn.style.color = '#fff'; |
| 140 |
dlBtn.textContent = '⬇️ Download PNG'; |
| 141 |
actions.appendChild(dlBtn); |
| 142 |
|
| 143 |
dlBtn.addEventListener('click', function () { |
| 144 |
canvas.toBlob(function (blob) { |
| 145 |
if (!blob) { alert('Could not export — cross-origin image may be blocking download.'); return; } |
| 146 |
var url = URL.createObjectURL(blob); |
| 147 |
var a = document.createElement('a'); |
| 148 |
a.href = url; |
| 149 |
a.download = 'meme.png'; |
| 150 |
a.click(); |
| 151 |
URL.revokeObjectURL(url); |
| 152 |
}); |
| 153 |
}); |
| 154 |
} |
| 155 |
|
| 156 |
var resetBtn = document.createElement('button'); |
| 157 |
resetBtn.className = 'bkbg-mme-reset-btn'; |
| 158 |
resetBtn.style.background = '#e5e7eb'; |
| 159 |
resetBtn.style.color = '#374151'; |
| 160 |
resetBtn.textContent = '↺ Reset'; |
| 161 |
actions.appendChild(resetBtn); |
| 162 |
|
| 163 |
// ---- Image management ---- |
| 164 |
var img = null; |
| 165 |
var currentUrl = ''; |
| 166 |
|
| 167 |
function loadImage(url) { |
| 168 |
if (!url) { |
| 169 |
img = null; |
| 170 |
redraw(); |
| 171 |
return; |
| 172 |
} |
| 173 |
if (url === currentUrl && img) return; |
| 174 |
currentUrl = url; |
| 175 |
var newImg = new Image(); |
| 176 |
newImg.crossOrigin = 'anonymous'; |
| 177 |
newImg.onload = function () { img = newImg; redraw(); }; |
| 178 |
newImg.onerror = function () { img = null; redraw(); }; |
| 179 |
newImg.src = url; |
| 180 |
} |
| 181 |
|
| 182 |
function redraw() { |
| 183 |
drawMeme(canvas, img, topInput.value, bottomInput.value, opts); |
| 184 |
} |
| 185 |
|
| 186 |
topInput.addEventListener('input', redraw); |
| 187 |
bottomInput.addEventListener('input', redraw); |
| 188 |
urlInput.addEventListener('change', function () { loadImage(urlInput.value.trim()); }); |
| 189 |
urlInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') loadImage(urlInput.value.trim()); }); |
| 190 |
|
| 191 |
resetBtn.addEventListener('click', function () { |
| 192 |
topInput.value = opts.defaultTopText || 'TOP TEXT'; |
| 193 |
bottomInput.value = opts.defaultBottomText || 'BOTTOM TEXT'; |
| 194 |
urlInput.value = opts.defaultImageUrl || ''; |
| 195 |
loadImage(urlInput.value.trim()); |
| 196 |
redraw(); |
| 197 |
}); |
| 198 |
|
| 199 |
// Initial load |
| 200 |
loadImage((opts.defaultImageUrl || '').trim()); |
| 201 |
redraw(); |
| 202 |
} |
| 203 |
|
| 204 |
document.querySelectorAll('.bkbg-mme-app').forEach(function (root) { |
| 205 |
initBlock(root); |
| 206 |
}); |
| 207 |
})(); |
| 208 |
|