| 1 |
(function () { |
| 2 |
'use strict'; |
| 3 |
|
| 4 |
var ASPECTS = { phone: 9 / 19.5, tablet: 4 / 3, laptop: 16 / 10, browser: 16 / 10 }; |
| 5 |
|
| 6 |
var FRAME_COLORS = { |
| 7 |
'silver': '#d4d4d8', |
| 8 |
'space-gray': '#374151', |
| 9 |
'gold': '#d4a853', |
| 10 |
'white': '#f9fafb', |
| 11 |
'black': '#111827', |
| 12 |
}; |
| 13 |
|
| 14 |
var PLACEHOLDER = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200"><rect width="300" height="200" fill="%23e5e7eb"/><text x="50%25" y="50%25" font-family="sans-serif" font-size="14" fill="%239ca3af" text-anchor="middle" dy=".3em">No Image</text></svg>'; |
| 15 |
|
| 16 |
function reducedMotion() { |
| 17 |
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; |
| 18 |
} |
| 19 |
|
| 20 |
function el(tag, attrs, children) { |
| 21 |
var node = document.createElement(tag); |
| 22 |
if (attrs) { |
| 23 |
Object.keys(attrs).forEach(function (k) { |
| 24 |
if (k === 'style' && typeof attrs[k] === 'object') { |
| 25 |
Object.assign(node.style, attrs[k]); |
| 26 |
} else { |
| 27 |
node.setAttribute(k, attrs[k]); |
| 28 |
} |
| 29 |
}); |
| 30 |
} |
| 31 |
if (children) { |
| 32 |
(Array.isArray(children) ? children : [children]).forEach(function (c) { |
| 33 |
if (c) node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); |
| 34 |
}); |
| 35 |
} |
| 36 |
return node; |
| 37 |
} |
| 38 |
|
| 39 |
function buildDevice(wrap) { |
| 40 |
var cfg = { |
| 41 |
device: wrap.dataset.device || 'phone', |
| 42 |
frameColor: wrap.dataset.frameColor || 'silver', |
| 43 |
frameCustom: wrap.dataset.frameCustom || '#d4d4d8', |
| 44 |
maxWidth: parseInt(wrap.dataset.maxWidth, 10) || 380, |
| 45 |
scale: parseFloat(wrap.dataset.scale) || 100, |
| 46 |
tiltX: parseFloat(wrap.dataset.tiltX) || 0, |
| 47 |
tiltY: parseFloat(wrap.dataset.tiltY) || 0, |
| 48 |
shadow: wrap.dataset.shadow !== 'false', |
| 49 |
shadowInt: parseInt(wrap.dataset.shadowInt, 10) || 30, |
| 50 |
reflection: wrap.dataset.reflection === 'true', |
| 51 |
hoverLift: wrap.dataset.hoverLift !== 'false', |
| 52 |
scrollAnim: wrap.dataset.scrollAnim === 'true', |
| 53 |
frameStroke: parseInt(wrap.dataset.frameStroke, 10) || 12, |
| 54 |
screenRadius: parseInt(wrap.dataset.screenRadius, 10)|| 8, |
| 55 |
outerRadius: parseInt(wrap.dataset.outerRadius, 10) || 40, |
| 56 |
notch: wrap.dataset.notch !== 'false', |
| 57 |
homeInd: wrap.dataset.homeInd !== 'false', |
| 58 |
browserChrome:wrap.dataset.browserChrome !== 'false', |
| 59 |
browserUrl: wrap.dataset.browserUrl || 'https://example.com', |
| 60 |
imgUrl: wrap.dataset.imgUrl || PLACEHOLDER, |
| 61 |
imgAlt: wrap.dataset.imgAlt || '', |
| 62 |
align: wrap.dataset.align || 'center', |
| 63 |
}; |
| 64 |
|
| 65 |
var frameColor = cfg.frameColor === 'custom' ? cfg.frameCustom : (FRAME_COLORS[cfg.frameColor] || '#d4d4d8'); |
| 66 |
var aspect = ASPECTS[cfg.device] || (9 / 19.5); |
| 67 |
var sw = cfg.maxWidth; |
| 68 |
var fs = cfg.frameStroke; |
| 69 |
var or = cfg.outerRadius; |
| 70 |
var sr = cfg.screenRadius; |
| 71 |
var scale = cfg.scale / 100; |
| 72 |
|
| 73 |
var shadowVal = cfg.shadow |
| 74 |
? '0 ' + Math.round(sw * 0.06) + 'px ' + Math.round(sw * 0.14) + 'px rgba(0,0,0,' + (cfg.shadowInt / 100) + ')' |
| 75 |
: 'none'; |
| 76 |
|
| 77 |
var tform = ''; |
| 78 |
if (cfg.tiltX !== 0 || cfg.tiltY !== 0) { |
| 79 |
tform = 'perspective(1200px) rotateX(' + cfg.tiltY + 'deg) rotateY(' + cfg.tiltX + 'deg)'; |
| 80 |
} |
| 81 |
if (scale !== 1) tform += (tform ? ' ' : '') + 'scale(' + scale + ')'; |
| 82 |
|
| 83 |
wrap.style.textAlign = cfg.align === 'right' ? 'right' : cfg.align === 'center' ? 'center' : 'left'; |
| 84 |
|
| 85 |
var outer = el('div', { class: 'bkdv-device-outer' }); |
| 86 |
if (tform && !reducedMotion()) outer.style.transform = tform; |
| 87 |
|
| 88 |
function makeImg() { |
| 89 |
var img = el('img'); |
| 90 |
img.src = cfg.imgUrl; |
| 91 |
img.alt = cfg.imgAlt; |
| 92 |
img.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;'; |
| 93 |
return img; |
| 94 |
} |
| 95 |
|
| 96 |
function makeReflection() { |
| 97 |
var r = el('div'); |
| 98 |
r.style.cssText = 'position:absolute;inset:0;background:linear-gradient(135deg,rgba(255,255,255,0.2) 0%,transparent 55%);pointer-events:none;z-index:2;'; |
| 99 |
return r; |
| 100 |
} |
| 101 |
|
| 102 |
/* ── Phone / Tablet ── */ |
| 103 |
if (cfg.device === 'phone' || cfg.device === 'tablet') { |
| 104 |
var device = el('div'); |
| 105 |
device.style.cssText = 'position:relative;width:' + sw + 'px;box-sizing:border-box;background:' + frameColor + ';border-radius:' + or + 'px;padding:' + fs + 'px;box-shadow:' + shadowVal + ';display:inline-block;'; |
| 106 |
|
| 107 |
var screen = el('div'); |
| 108 |
screen.style.cssText = 'position:relative;width:100%;padding-bottom:' + (100 / aspect) + '%;border-radius:' + sr + 'px;overflow:hidden;background:#000;'; |
| 109 |
|
| 110 |
screen.appendChild(makeImg()); |
| 111 |
|
| 112 |
if (cfg.reflection) screen.appendChild(makeReflection()); |
| 113 |
|
| 114 |
if (cfg.notch && cfg.device === 'phone') { |
| 115 |
var notch = el('div', { class: 'bkdv-notch' }); |
| 116 |
notch.style.cssText = 'position:absolute;top:0;left:50%;transform:translateX(-50%);width:28%;height:20px;background:' + frameColor + ';border-bottom-left-radius:12px;border-bottom-right-radius:12px;z-index:3;'; |
| 117 |
screen.appendChild(notch); |
| 118 |
} |
| 119 |
|
| 120 |
device.appendChild(screen); |
| 121 |
|
| 122 |
if (cfg.homeInd && cfg.device === 'phone') { |
| 123 |
var hi = el('div'); |
| 124 |
hi.style.cssText = 'position:absolute;bottom:' + Math.round(fs / 2) + 'px;left:50%;transform:translateX(-50%);width:35%;height:4px;border-radius:2px;background:rgba(255,255,255,0.6);'; |
| 125 |
device.appendChild(hi); |
| 126 |
} |
| 127 |
|
| 128 |
outer.appendChild(device); |
| 129 |
} |
| 130 |
|
| 131 |
/* ── Laptop ── */ |
| 132 |
else if (cfg.device === 'laptop') { |
| 133 |
var lidW = sw; |
| 134 |
var lid = el('div'); |
| 135 |
lid.style.cssText = 'width:' + lidW + 'px;height:' + Math.round(lidW * 0.6) + 'px;background:' + frameColor + ';border-radius:12px 12px 0 0;padding:10px 10px 6px;box-sizing:border-box;position:relative;box-shadow:' + shadowVal + ';margin:0 auto;display:block;'; |
| 136 |
|
| 137 |
var screenArea = el('div'); |
| 138 |
screenArea.style.cssText = 'width:100%;height:100%;background:#000;border-radius:' + sr + 'px;overflow:hidden;position:relative;'; |
| 139 |
screenArea.appendChild(makeImg()); |
| 140 |
if (cfg.reflection) screenArea.appendChild(makeReflection()); |
| 141 |
lid.appendChild(screenArea); |
| 142 |
|
| 143 |
var hinge = el('div'); |
| 144 |
hinge.style.cssText = 'width:' + Math.round(lidW * 0.15) + 'px;height:4px;background:' + frameColor + ';filter:brightness(0.7);margin:0 auto;display:block;'; |
| 145 |
|
| 146 |
var keyW = Math.round(lidW * 1.15); |
| 147 |
var keybase = el('div'); |
| 148 |
keybase.style.cssText = 'width:' + keyW + 'px;height:' + Math.round(lidW * 0.08) + 'px;background:' + frameColor + ';filter:brightness(0.88);border-radius:0 0 8px 8px;margin:0 auto;display:block;position:relative;'; |
| 149 |
|
| 150 |
outer.appendChild(lid); |
| 151 |
outer.appendChild(hinge); |
| 152 |
outer.appendChild(keybase); |
| 153 |
} |
| 154 |
|
| 155 |
/* ── Browser ── */ |
| 156 |
else if (cfg.device === 'browser') { |
| 157 |
var chromeH = cfg.browserChrome ? 38 : 0; |
| 158 |
var container = el('div'); |
| 159 |
container.style.cssText = 'width:' + sw + 'px;border-radius:' + or + 'px;overflow:hidden;box-shadow:' + shadowVal + ';display:flex;flex-direction:column;border:1px solid rgba(0,0,0,0.12);'; |
| 160 |
|
| 161 |
if (cfg.browserChrome) { |
| 162 |
var bar = el('div'); |
| 163 |
bar.style.cssText = 'display:flex;align-items:center;gap:6px;padding:0 12px;height:' + chromeH + 'px;background:' + (cfg.frameColor === 'black' ? '#1f2937' : '#f3f4f6') + ';border-bottom:1px solid rgba(0,0,0,0.1);flex-shrink:0;'; |
| 164 |
|
| 165 |
function dot(color) { |
| 166 |
var d = el('div'); |
| 167 |
d.style.cssText = 'width:12px;height:12px;border-radius:50%;background:' + color + ';flex-shrink:0;'; |
| 168 |
return d; |
| 169 |
} |
| 170 |
bar.appendChild(dot('#ef4444')); |
| 171 |
bar.appendChild(dot('#f59e0b')); |
| 172 |
bar.appendChild(dot('#22c55e')); |
| 173 |
|
| 174 |
var urlBar = el('div'); |
| 175 |
urlBar.style.cssText = 'flex:1;height:22px;background:#fff;border-radius:4px;border:1px solid rgba(0,0,0,0.1);display:flex;align-items:center;padding-left:8px;font-size:11px;color:#6b7280;overflow:hidden;white-space:nowrap;font-family:monospace;'; |
| 176 |
urlBar.textContent = cfg.browserUrl; |
| 177 |
bar.appendChild(urlBar); |
| 178 |
container.appendChild(bar); |
| 179 |
} |
| 180 |
|
| 181 |
var viewport = el('div'); |
| 182 |
viewport.style.cssText = 'width:100%;padding-bottom:' + (100 / ASPECTS.browser) + '%;position:relative;background:#fff;'; |
| 183 |
viewport.appendChild(makeImg()); |
| 184 |
if (cfg.reflection) viewport.appendChild(makeReflection()); |
| 185 |
container.appendChild(viewport); |
| 186 |
|
| 187 |
outer.appendChild(container); |
| 188 |
} |
| 189 |
|
| 190 |
/* Hover lift handled by CSS (.bkdv-wrap[data-hover-lift="true"]) */ |
| 191 |
|
| 192 |
/* Scroll animation */ |
| 193 |
if (cfg.scrollAnim && !reducedMotion()) { |
| 194 |
var io = new IntersectionObserver(function (entries) { |
| 195 |
entries.forEach(function (entry) { |
| 196 |
if (entry.isIntersecting) { |
| 197 |
wrap.classList.add('bkdv-visible'); |
| 198 |
io.unobserve(wrap); |
| 199 |
} |
| 200 |
}); |
| 201 |
}, { threshold: 0.15 }); |
| 202 |
io.observe(wrap); |
| 203 |
} else { |
| 204 |
wrap.classList.add('bkdv-visible'); |
| 205 |
} |
| 206 |
|
| 207 |
wrap.appendChild(outer); |
| 208 |
} |
| 209 |
|
| 210 |
function init() { |
| 211 |
var wraps = document.querySelectorAll('.bkdv-wrap[data-device]'); |
| 212 |
wraps.forEach(function (wrap) { buildDevice(wrap); }); |
| 213 |
} |
| 214 |
|
| 215 |
if (document.readyState === 'loading') { |
| 216 |
document.addEventListener('DOMContentLoaded', init); |
| 217 |
} else { |
| 218 |
init(); |
| 219 |
} |
| 220 |
|
| 221 |
}()); |
| 222 |
|