pixel-perfect-overlay
Version:
A customizable pixel-perfect overlay with adjustable offset and opacity
250 lines (231 loc) • 7.63 kB
JavaScript
;(function () {
const overlayId = 'pixel-perfect-overlay'
const panelId = 'pixel-perfect-panel'
const toggleBtnId = 'toggle-settings-panel'
if (document.getElementById(overlayId)) return
// === Load saved state ===
const saved = JSON.parse(localStorage.getItem('pixelPerfectSettings') || '{}')
let offsetX = saved.offsetX || 0
let offsetY = saved.offsetY || 0
let urlImage = saved.urlImage || ''
let dragImage = saved.dragImage || ''
let opacity = saved.opacity ?? 0.5
let display = saved.display ?? 'block'
let panelVisible = saved.panelVisible ?? true
// === Overlay ===
const overlay = document.createElement('div')
overlay.id = overlayId
Object.assign(overlay.style, {
position: 'fixed',
top: offsetY + 'px',
left: offsetX + 'px',
width: '100vw',
height: '100vh',
pointerEvents: 'none',
backgroundRepeat: 'no-repeat',
backgroundSize: 'contain',
backgroundPosition: 'top left',
opacity,
zIndex: 9998,
display,
transition: 'opacity 0.2s ease',
})
document.body.appendChild(overlay)
updateOverlayBackground()
// === Panel ===
const panel = document.createElement('div')
panel.id = panelId
panel.className = panelVisible ? '' : 'collapsed'
panel.innerHTML = `
<button id="${toggleBtnId}" title="Открыть настройки">⚙️</button>
<div class="panel-inner">
<input type="text" id="mock-url" placeholder="URL изображения" value="${urlImage}" />
<label>Прозрачность:
<input type="range" id="mock-opacity" min="0" max="1" step="0.01" value="${opacity}" />
</label>
<div style="display:flex;gap:8px;justify-content:space-between;">
<label style="flex:1;">X: <input type="number" id="offset-x" value="${offsetX}" /></label>
<label style="flex:1;">Y: <input type="number" id="offset-y" value="${offsetY}" /></label>
</div>
<div id="coordinate-grid" style="text-align:center;font-size:12px;color:#555;margin-top:4px;">
Смещение: X: ${offsetX}px, Y: ${offsetY}px
</div>
<button id="toggle-overlay">👁 Показать / Скрыть</button>
</div>
`
Object.assign(panel.style, {
position: 'fixed',
bottom: '10px',
left: '10px',
padding: '10px',
background: '#fff',
borderRadius: '12px',
fontSize: '14px',
fontFamily: 'sans-serif',
boxShadow: '0 4px 16px rgba(0,0,0,0.1)',
zIndex: 10000,
width: '240px',
height: '200px',
transition: 'all 0.3s ease',
})
document.body.appendChild(panel)
// === Panel Styles ===
const style = document.createElement('style')
style.textContent = `
#${panelId} input[type="text"],
#${panelId} input[type="number"] {
width: 100%;
padding: 4px 6px;
font-size: 13px;
margin: 4px 0;
border: 1px solid #ccc;
border-radius: 6px;
box-sizing: border-box;
}
#${panelId} input[type="range"] {
width: 100%;
}
#${panelId} button {
padding: 6px;
border: none;
border-radius: 6px;
background: #eee;
cursor: pointer;
}
#${panelId} #toggle-overlay {
width: 100%;
}
#${toggleBtnId} {
position: absolute;
top: -40px;
left: 0;
width: 36px;
height: 36px;
background: #555;
color: #fff;
border-radius: 50%;
font-size: 18px;
text-align: center;
}
#${panelId} .panel-inner {
opacity: 1;
transition: opacity 0.5s ease;
}
#${panelId}.collapsed {
width: 36px !important;
height: 36px !important;
padding: 0 !important;
overflow: hidden;
}
#${panelId}.collapsed .panel-inner {
pointer-events: none !important;
height: 0 !important;
opacity: 0 !important;
}
#${panelId}.collapsed #${toggleBtnId} {
top: 0 !important;
}
@media (max-width: 768px) {
#${panelId} {
width: calc(100vw - 30px);
left: 15px;
bottom: 10px;
}
}
`
document.head.appendChild(style)
// === Elements ===
const urlInput = panel.querySelector('#mock-url')
const opacityInput = panel.querySelector('#mock-opacity')
const offsetXInput = panel.querySelector('#offset-x')
const offsetYInput = panel.querySelector('#offset-y')
const coordGrid = panel.querySelector('#coordinate-grid')
const toggleOverlayBtn = panel.querySelector('#toggle-overlay')
const togglePanelBtn = panel.querySelector(`#${toggleBtnId}`)
// === Events ===
let focusedInput = null
offsetXInput.addEventListener('focus', () => (focusedInput = 'x'))
offsetYInput.addEventListener('focus', () => (focusedInput = 'y'))
offsetXInput.addEventListener('blur', () => (focusedInput = null))
offsetYInput.addEventListener('blur', () => (focusedInput = null))
urlInput.addEventListener('input', () => {
urlImage = urlInput.value
if (urlImage) dragImage = ''
updateOverlayBackground()
saveSettings({ urlImage, dragImage })
})
opacityInput.addEventListener('input', () => {
opacity = parseFloat(opacityInput.value)
overlay.style.opacity = opacity
saveSettings({ opacity })
})
offsetXInput.addEventListener('input', () => {
offsetX = parseInt(offsetXInput.value) || 0
applyOffset()
})
offsetYInput.addEventListener('input', () => {
offsetY = parseInt(offsetYInput.value) || 0
applyOffset()
})
toggleOverlayBtn.addEventListener('click', () => {
overlay.style.display = overlay.style.display === 'none' ? 'block' : 'none'
saveSettings({ display: overlay.style.display })
})
togglePanelBtn.addEventListener('click', () => {
panel.classList.toggle('collapsed')
panelVisible = !panel.classList.contains('collapsed')
saveSettings({ panelVisible })
})
document.addEventListener('keydown', (e) => {
const step = e.shiftKey ? 10 : 1
if (focusedInput === 'x') {
if (e.key === 'ArrowLeft') offsetX -= step
if (e.key === 'ArrowRight') offsetX += step
} else if (focusedInput === 'y') {
if (e.key === 'ArrowUp') offsetY -= step
if (e.key === 'ArrowDown') offsetY += step
} else {
if (e.key === 'ArrowLeft') offsetX -= step
if (e.key === 'ArrowRight') offsetX += step
if (e.key === 'ArrowUp') offsetY -= step
if (e.key === 'ArrowDown') offsetY += step
}
applyOffset()
})
document.addEventListener('dragover', (e) => e.preventDefault())
document.addEventListener('drop', (e) => {
e.preventDefault()
const file = e.dataTransfer.files[0]
if (!file || !file.type.startsWith('image/')) return
const reader = new FileReader()
reader.onload = function (event) {
dragImage = event.target.result
urlImage = ''
urlInput.value = ''
updateOverlayBackground()
saveSettings({ dragImage, urlImage })
}
reader.readAsDataURL(file)
})
// === Functions ===
function updateOverlayBackground() {
overlay.style.backgroundImage = urlImage
? `url("${urlImage}")`
: dragImage
? `url("${dragImage}")`
: 'none'
}
function applyOffset() {
overlay.style.left = `${offsetX}px`
overlay.style.top = `${offsetY}px`
offsetXInput.value = offsetX
offsetYInput.value = offsetY
coordGrid.textContent = `Смещение: X: ${offsetX}px, Y: ${offsetY}px`
saveSettings({ offsetX, offsetY })
}
function saveSettings(updates) {
const current = JSON.parse(localStorage.getItem('pixelPerfectSettings') || '{}')
const merged = { ...current, ...updates }
localStorage.setItem('pixelPerfectSettings', JSON.stringify(merged))
}
})()