| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var typoMap = [ |
| 5 |
['family','font-family'],['weight','font-weight'],['style','font-style'], |
| 6 |
['decoration','text-decoration'],['transform','text-transform'], |
| 7 |
['sizeDesktop','font-size-d'],['sizeTablet','font-size-t'],['sizeMobile','font-size-m'], |
| 8 |
['lineHeightDesktop','line-height-d'],['lineHeightTablet','line-height-t'],['lineHeightMobile','line-height-m'], |
| 9 |
['letterSpacingDesktop','letter-spacing-d'],['letterSpacingTablet','letter-spacing-t'],['letterSpacingMobile','letter-spacing-m'], |
| 10 |
['wordSpacingDesktop','word-spacing-d'],['wordSpacingTablet','word-spacing-t'],['wordSpacingMobile','word-spacing-m'] |
| 11 |
]; |
| 12 |
function typoCssVarsForEl(typo, prefix, el) { |
| 13 |
if (!typo || typeof typo !== 'object') return; |
| 14 |
for (var i = 0; i < typoMap.length; i++) { |
| 15 |
var v = typo[typoMap[i][0]]; |
| 16 |
if (v !== undefined && v !== '' && v !== null) el.style.setProperty(prefix + typoMap[i][1], String(v)); |
| 17 |
} |
| 18 |
} |
| 19 |
|
| 20 |
var PRESETS = [ |
| 21 |
{ label: '1:1', w: 400, h: 400 }, |
| 22 |
{ label: '4:3', w: 800, h: 600 }, |
| 23 |
{ label: '16:9', w: 1280, h: 720 }, |
| 24 |
{ label: '3:2', w: 900, h: 600 }, |
| 25 |
{ label: '2:3', w: 400, h: 600 }, |
| 26 |
{ label: '9:16', w: 360, h: 640 }, |
| 27 |
{ label: 'HD 1080p', w: 1920, h: 1080 }, |
| 28 |
{ label: 'Social Sq.', w: 1080, h: 1080 } |
| 29 |
]; |
| 30 |
|
| 31 |
function drawPlaceholder(canvas, opts) { |
| 32 |
var w = opts.width, h = opts.height, bg = opts.bg, fg = opts.fg, text = opts.text, theme = opts.theme, accent = opts.accent; |
| 33 |
canvas.width = w; |
| 34 |
canvas.height = h; |
| 35 |
var ctx = canvas.getContext('2d'); |
| 36 |
|
| 37 |
/* Background */ |
| 38 |
if (theme === 'gradient') { |
| 39 |
var grad = ctx.createLinearGradient(0, 0, w, h); |
| 40 |
grad.addColorStop(0, bg); |
| 41 |
grad.addColorStop(1, accent || '#999'); |
| 42 |
ctx.fillStyle = grad; |
| 43 |
} else if (theme === 'pattern') { |
| 44 |
ctx.fillStyle = bg; |
| 45 |
ctx.fillRect(0, 0, w, h); |
| 46 |
ctx.fillStyle = accent ? accent + '33' : 'rgba(0,0,0,0.07)'; |
| 47 |
var step = Math.round(Math.max(w, h) / 20); |
| 48 |
for (var y = -h; y < h * 2; y += step) { |
| 49 |
for (var x = -w; x < w * 2; x += step) { |
| 50 |
ctx.fillRect(x, y, step / 2, step / 2); |
| 51 |
} |
| 52 |
} |
| 53 |
} else { |
| 54 |
ctx.fillStyle = bg; |
| 55 |
} |
| 56 |
ctx.fillRect(0, 0, w, h); |
| 57 |
|
| 58 |
/* Diagonal cross lines (like classic placeholder) */ |
| 59 |
ctx.strokeStyle = fg + '22'; |
| 60 |
ctx.lineWidth = Math.max(1, Math.round(Math.min(w, h) / 100)); |
| 61 |
ctx.beginPath(); |
| 62 |
ctx.moveTo(0, 0); ctx.lineTo(w, h); |
| 63 |
ctx.moveTo(w, 0); ctx.lineTo(0, h); |
| 64 |
ctx.stroke(); |
| 65 |
|
| 66 |
/* Border */ |
| 67 |
ctx.strokeStyle = fg + '44'; |
| 68 |
ctx.lineWidth = Math.max(2, Math.round(Math.min(w, h) / 80)); |
| 69 |
ctx.strokeRect(ctx.lineWidth, ctx.lineWidth, w - ctx.lineWidth * 2, h - ctx.lineWidth * 2); |
| 70 |
|
| 71 |
/* Text */ |
| 72 |
var displayText = text || (w + ' × ' + h); |
| 73 |
var fontSize = Math.max(12, Math.min(Math.round(Math.min(w, h) / 8), 72)); |
| 74 |
ctx.fillStyle = fg; |
| 75 |
ctx.textAlign = 'center'; |
| 76 |
ctx.textBaseline = 'middle'; |
| 77 |
ctx.font = 'bold ' + fontSize + 'px system-ui,Arial,sans-serif'; |
| 78 |
|
| 79 |
/* Word wrap */ |
| 80 |
var maxWidth = w * 0.8; |
| 81 |
var words = displayText.split(' '); |
| 82 |
var lines = []; |
| 83 |
var currentLine = ''; |
| 84 |
words.forEach(function (word) { |
| 85 |
var testLine = currentLine ? currentLine + ' ' + word : word; |
| 86 |
if (ctx.measureText(testLine).width > maxWidth && currentLine) { |
| 87 |
lines.push(currentLine); |
| 88 |
currentLine = word; |
| 89 |
} else { |
| 90 |
currentLine = testLine; |
| 91 |
} |
| 92 |
}); |
| 93 |
if (currentLine) lines.push(currentLine); |
| 94 |
|
| 95 |
var lineH = fontSize * 1.3; |
| 96 |
var totalTextH = lines.length * lineH; |
| 97 |
var startY = (h - totalTextH) / 2 + lineH / 2; |
| 98 |
lines.forEach(function (line, i) { |
| 99 |
ctx.fillText(line, w / 2, startY + i * lineH); |
| 100 |
}); |
| 101 |
|
| 102 |
/* Dimensions sub-label */ |
| 103 |
if (text) { |
| 104 |
var subSize = Math.max(10, Math.round(fontSize * 0.5)); |
| 105 |
ctx.font = subSize + 'px system-ui,Arial,sans-serif'; |
| 106 |
ctx.fillStyle = fg + 'aa'; |
| 107 |
ctx.fillText(w + ' × ' + h, w / 2, startY + lines.length * lineH + subSize); |
| 108 |
} |
| 109 |
} |
| 110 |
|
| 111 |
function initBlock(root) { |
| 112 |
var opts; |
| 113 |
try { opts = JSON.parse(root.getAttribute('data-opts')); } catch (e) { return; } |
| 114 |
var a = opts; |
| 115 |
|
| 116 |
var state = { |
| 117 |
width: a.defaultWidth || 800, |
| 118 |
height: a.defaultHeight || 600, |
| 119 |
bg: a.defaultBgColor || '#cccccc', |
| 120 |
fg: a.defaultTextColor || '#555555', |
| 121 |
text: a.defaultText || '', |
| 122 |
theme: a.defaultTheme || 'flat' |
| 123 |
}; |
| 124 |
|
| 125 |
root.innerHTML = ''; |
| 126 |
|
| 127 |
var wrap = document.createElement('div'); |
| 128 |
wrap.className = 'bkbg-iph-wrap'; |
| 129 |
wrap.style.cssText = 'background:' + a.sectionBg + ';max-width:' + a.contentMaxWidth + 'px;margin:0 auto;'; |
| 130 |
typoCssVarsForEl(a.titleTypo, '--bkbg-iph-tt-', wrap); |
| 131 |
root.appendChild(wrap); |
| 132 |
|
| 133 |
if (a.showTitle) { |
| 134 |
var h = document.createElement('div'); |
| 135 |
h.className = 'bkbg-iph-title'; |
| 136 |
h.style.color = a.titleColor; |
| 137 |
h.textContent = a.title; |
| 138 |
wrap.appendChild(h); |
| 139 |
} |
| 140 |
|
| 141 |
/* Controls */ |
| 142 |
var controls = document.createElement('div'); |
| 143 |
controls.className = 'bkbg-iph-controls'; |
| 144 |
controls.style.cssText = 'background:' + a.cardBg + ';border-color:' + a.borderColor + ';'; |
| 145 |
wrap.appendChild(controls); |
| 146 |
|
| 147 |
function makeField(lbl, el) { |
| 148 |
var g = document.createElement('div'); |
| 149 |
g.className = 'bkbg-iph-field'; |
| 150 |
var l = document.createElement('label'); |
| 151 |
l.className = 'bkbg-iph-label'; |
| 152 |
l.style.color = a.labelColor; |
| 153 |
l.textContent = lbl; |
| 154 |
g.appendChild(l); |
| 155 |
g.appendChild(el); |
| 156 |
return g; |
| 157 |
} |
| 158 |
|
| 159 |
var wInput = document.createElement('input'); |
| 160 |
wInput.className = 'bkbg-iph-input'; |
| 161 |
wInput.type = 'number'; |
| 162 |
wInput.min = '1'; wInput.max = '4000'; |
| 163 |
wInput.value = state.width; |
| 164 |
controls.appendChild(makeField('Width (px)', wInput)); |
| 165 |
|
| 166 |
var hInput = document.createElement('input'); |
| 167 |
hInput.className = 'bkbg-iph-input'; |
| 168 |
hInput.type = 'number'; |
| 169 |
hInput.min = '1'; hInput.max = '4000'; |
| 170 |
hInput.value = state.height; |
| 171 |
controls.appendChild(makeField('Height (px)', hInput)); |
| 172 |
|
| 173 |
var textInput = document.createElement('input'); |
| 174 |
textInput.className = 'bkbg-iph-input'; |
| 175 |
textInput.style.minWidth = '150px'; |
| 176 |
textInput.placeholder = 'Custom text (default: dimensions)'; |
| 177 |
textInput.value = state.text; |
| 178 |
controls.appendChild(makeField('Custom Text', textInput)); |
| 179 |
|
| 180 |
var bgField = document.createElement('div'); |
| 181 |
bgField.className = 'bkbg-iph-field bkbg-iph-color-field'; |
| 182 |
var bgLbl = document.createElement('label'); |
| 183 |
bgLbl.className = 'bkbg-iph-label'; |
| 184 |
bgLbl.style.color = a.labelColor; |
| 185 |
bgLbl.textContent = 'BG'; |
| 186 |
var bgInput = document.createElement('input'); |
| 187 |
bgInput.className = 'bkbg-iph-color-input'; |
| 188 |
bgInput.type = 'color'; |
| 189 |
bgInput.value = state.bg; |
| 190 |
bgField.appendChild(bgLbl); |
| 191 |
bgField.appendChild(bgInput); |
| 192 |
controls.appendChild(bgField); |
| 193 |
|
| 194 |
var fgField = document.createElement('div'); |
| 195 |
fgField.className = 'bkbg-iph-field bkbg-iph-color-field'; |
| 196 |
var fgLbl = document.createElement('label'); |
| 197 |
fgLbl.className = 'bkbg-iph-label'; |
| 198 |
fgLbl.style.color = a.labelColor; |
| 199 |
fgLbl.textContent = 'Text'; |
| 200 |
var fgInput = document.createElement('input'); |
| 201 |
fgInput.className = 'bkbg-iph-color-input'; |
| 202 |
fgInput.type = 'color'; |
| 203 |
fgInput.value = state.fg; |
| 204 |
fgField.appendChild(fgLbl); |
| 205 |
fgField.appendChild(fgInput); |
| 206 |
controls.appendChild(fgField); |
| 207 |
|
| 208 |
/* Presets */ |
| 209 |
if (a.showAspectPresets) { |
| 210 |
var presetsEl = document.createElement('div'); |
| 211 |
presetsEl.className = 'bkbg-iph-presets'; |
| 212 |
wrap.appendChild(presetsEl); |
| 213 |
|
| 214 |
PRESETS.forEach(function (p) { |
| 215 |
var btn = document.createElement('button'); |
| 216 |
btn.className = 'bkbg-iph-preset-btn'; |
| 217 |
btn.style.cssText = 'border-color:' + a.accentColor + ';color:' + a.accentColor + ';background:transparent;'; |
| 218 |
btn.textContent = p.label + ' (' + p.w + '×' + p.h + ')'; |
| 219 |
btn.addEventListener('click', function () { |
| 220 |
state.width = p.w; |
| 221 |
state.height = p.h; |
| 222 |
wInput.value = p.w; |
| 223 |
hInput.value = p.h; |
| 224 |
render(); |
| 225 |
}); |
| 226 |
presetsEl.appendChild(btn); |
| 227 |
}); |
| 228 |
} |
| 229 |
|
| 230 |
/* Theme selector */ |
| 231 |
if (a.showThemeSelector) { |
| 232 |
var themeRow = document.createElement('div'); |
| 233 |
themeRow.className = 'bkbg-iph-theme-row'; |
| 234 |
wrap.appendChild(themeRow); |
| 235 |
|
| 236 |
['flat','gradient','pattern'].forEach(function (t) { |
| 237 |
var btn = document.createElement('button'); |
| 238 |
btn.className = 'bkbg-iph-theme-btn'; |
| 239 |
btn.textContent = t.charAt(0).toUpperCase() + t.slice(1); |
| 240 |
btn.dataset.theme = t; |
| 241 |
applyThemeBtnStyle(btn, t === state.theme); |
| 242 |
btn.addEventListener('click', function () { |
| 243 |
state.theme = t; |
| 244 |
themeRow.querySelectorAll('.bkbg-iph-theme-btn').forEach(function (b) { |
| 245 |
applyThemeBtnStyle(b, b.dataset.theme === t); |
| 246 |
}); |
| 247 |
render(); |
| 248 |
}); |
| 249 |
themeRow.appendChild(btn); |
| 250 |
}); |
| 251 |
} |
| 252 |
|
| 253 |
function applyThemeBtnStyle(btn, active) { |
| 254 |
btn.style.cssText = 'border-color:' + a.accentColor + ';background:' + (active ? a.accentColor : 'transparent') + ';color:' + (active ? '#fff' : a.accentColor) + ';'; |
| 255 |
} |
| 256 |
|
| 257 |
/* Canvas preview */ |
| 258 |
var previewWrap = document.createElement('div'); |
| 259 |
previewWrap.className = 'bkbg-iph-preview-wrap'; |
| 260 |
wrap.appendChild(previewWrap); |
| 261 |
|
| 262 |
var canvasWrap = document.createElement('div'); |
| 263 |
canvasWrap.className = 'bkbg-iph-canvas-wrap'; |
| 264 |
previewWrap.appendChild(canvasWrap); |
| 265 |
|
| 266 |
var canvas = document.createElement('canvas'); |
| 267 |
canvasWrap.appendChild(canvas); |
| 268 |
|
| 269 |
/* Dims label */ |
| 270 |
var dimsEl = document.createElement('div'); |
| 271 |
dimsEl.className = 'bkbg-iph-dims'; |
| 272 |
dimsEl.style.color = a.subtitleColor; |
| 273 |
wrap.appendChild(dimsEl); |
| 274 |
|
| 275 |
/* Actions */ |
| 276 |
var actionsEl = document.createElement('div'); |
| 277 |
actionsEl.className = 'bkbg-iph-actions'; |
| 278 |
wrap.appendChild(actionsEl); |
| 279 |
|
| 280 |
var urlBox = document.createElement('div'); |
| 281 |
urlBox.className = 'bkbg-iph-url-box'; |
| 282 |
var urlLbl = document.createElement('div'); |
| 283 |
urlLbl.className = 'bkbg-iph-url-label'; |
| 284 |
urlLbl.textContent = 'Data URI'; |
| 285 |
urlBox.appendChild(urlLbl); |
| 286 |
var urlText = document.createElement('div'); |
| 287 |
urlText.className = 'bkbg-iph-url-text'; |
| 288 |
urlBox.appendChild(urlText); |
| 289 |
wrap.appendChild(urlBox); |
| 290 |
|
| 291 |
if (a.showDownloadBtn) { |
| 292 |
var dlBtn = document.createElement('button'); |
| 293 |
dlBtn.className = 'bkbg-iph-btn'; |
| 294 |
dlBtn.style.background = a.accentColor; |
| 295 |
dlBtn.textContent = '⬇ Download PNG'; |
| 296 |
dlBtn.addEventListener('click', function () { |
| 297 |
var link = document.createElement('a'); |
| 298 |
link.download = 'placeholder-' + state.width + 'x' + state.height + '.png'; |
| 299 |
link.href = canvas.toDataURL('image/png'); |
| 300 |
link.click(); |
| 301 |
}); |
| 302 |
actionsEl.appendChild(dlBtn); |
| 303 |
} |
| 304 |
|
| 305 |
if (a.showCopyUriBtn) { |
| 306 |
var copyUriBtn = document.createElement('button'); |
| 307 |
copyUriBtn.className = 'bkbg-iph-btn bkbg-iph-btn-outline'; |
| 308 |
copyUriBtn.style.cssText = 'border-color:' + a.accentColor + ';color:' + a.accentColor + ';'; |
| 309 |
copyUriBtn.textContent = 'Copy Data URI'; |
| 310 |
copyUriBtn.addEventListener('click', function () { |
| 311 |
var uri = canvas.toDataURL('image/png'); |
| 312 |
if (navigator.clipboard) { |
| 313 |
navigator.clipboard.writeText(uri).then(function () { |
| 314 |
copyUriBtn.textContent = '✓ Copied!'; |
| 315 |
setTimeout(function () { copyUriBtn.textContent = 'Copy Data URI'; }, 1800); |
| 316 |
}); |
| 317 |
} |
| 318 |
urlText.textContent = uri.slice(0, 200) + '…'; |
| 319 |
urlBox.classList.add('bkbg-iph-show'); |
| 320 |
}); |
| 321 |
actionsEl.appendChild(copyUriBtn); |
| 322 |
} |
| 323 |
|
| 324 |
if (a.showCopyUrlBtn) { |
| 325 |
var copyUrlBtn = document.createElement('button'); |
| 326 |
copyUrlBtn.className = 'bkbg-iph-btn bkbg-iph-btn-outline'; |
| 327 |
copyUrlBtn.style.cssText = 'border-color:#6b7280;color:#6b7280;'; |
| 328 |
copyUrlBtn.textContent = 'Copy URL'; |
| 329 |
copyUrlBtn.title = 'Copies a placeholder.com style URL'; |
| 330 |
copyUrlBtn.addEventListener('click', function () { |
| 331 |
var url = 'https://via.placeholder.com/' + state.width + 'x' + state.height; |
| 332 |
if (navigator.clipboard) { |
| 333 |
navigator.clipboard.writeText(url).then(function () { |
| 334 |
copyUrlBtn.textContent = '✓ Copied URL!'; |
| 335 |
setTimeout(function () { copyUrlBtn.textContent = 'Copy URL'; }, 1800); |
| 336 |
}); |
| 337 |
} |
| 338 |
}); |
| 339 |
actionsEl.appendChild(copyUrlBtn); |
| 340 |
} |
| 341 |
|
| 342 |
function render() { |
| 343 |
var maxPreviewW = Math.min(state.width, wrap.offsetWidth - 48 || 600); |
| 344 |
var scale = maxPreviewW / state.width; |
| 345 |
var previewH = Math.round(state.height * scale); |
| 346 |
|
| 347 |
drawPlaceholder(canvas, { |
| 348 |
width: state.width, height: state.height, |
| 349 |
bg: state.bg, fg: state.fg, |
| 350 |
text: state.text, theme: state.theme, |
| 351 |
accent: a.accentColor |
| 352 |
}); |
| 353 |
|
| 354 |
canvas.style.width = maxPreviewW + 'px'; |
| 355 |
canvas.style.height = previewH + 'px'; |
| 356 |
|
| 357 |
dimsEl.textContent = state.width + ' × ' + state.height + ' px'; |
| 358 |
} |
| 359 |
|
| 360 |
/* Event listeners */ |
| 361 |
[wInput, hInput, textInput].forEach(function (inp) { |
| 362 |
inp.addEventListener('input', function () { |
| 363 |
var wv = parseInt(wInput.value) || 1; |
| 364 |
var hv = parseInt(hInput.value) || 1; |
| 365 |
state.width = Math.max(1, Math.min(wv, 4000)); |
| 366 |
state.height = Math.max(1, Math.min(hv, 4000)); |
| 367 |
state.text = textInput.value; |
| 368 |
render(); |
| 369 |
}); |
| 370 |
}); |
| 371 |
|
| 372 |
bgInput.addEventListener('input', function () { state.bg = bgInput.value; render(); }); |
| 373 |
fgInput.addEventListener('input', function () { state.fg = fgInput.value; render(); }); |
| 374 |
|
| 375 |
render(); |
| 376 |
} |
| 377 |
|
| 378 |
function init() { |
| 379 |
document.querySelectorAll('.bkbg-iph-app').forEach(initBlock); |
| 380 |
} |
| 381 |
|
| 382 |
if (document.readyState === 'loading') { |
| 383 |
document.addEventListener('DOMContentLoaded', init); |
| 384 |
} else { |
| 385 |
init(); |
| 386 |
} |
| 387 |
})(); |
| 388 |
|