Universal Manual Blur

Manual blur anywhere. Client-side.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Universal Manual Blur
// @namespace    https://greasyfork.org/en/users/1451802
// @version      1.0
// @icon         https://www.svgrepo.com/show/416283/blur-drop-water.svg
// @author       NormalRandomPeople (https://github.com/NormalRandomPeople)
// @description  Manual blur anywhere. Client-side.
// @match        *://*/*
// @grant        GM_addStyle
// @run-at       document-idle
// @compatible   chrome
// @compatible   firefox
// @compatible   opera
// @compatible   edge
// @compatible   brave
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const DEFAULT_SLIDER_VALUE = 40;
  const REGION_BG_MIN_ALPHA = 0.02;
  const UI_ZINDEX = 2147483647;
  const HANDLE_SIZE = 12;

  const css = `
  .umb-toolbar {
    position: fixed;
    right: 12px;
    bottom: 12px;
    z-index: ${UI_ZINDEX};
    background: rgba(16,16,16,0.88);
    color: #fff;
    border-radius: 10px;
    padding: 8px;
    font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
    display:flex;
    gap:8px;
    align-items:center;
    box-shadow: 0 8px 24px rgba(0,0,0,0.5);
    backdrop-filter: blur(6px);
    transition: box-shadow .18s ease, transform .12s linear, background .18s ease;
  }

  .umb-toolbar .umb-toggle-edit {
    transition: box-shadow .18s ease, transform .12s linear;
  }
  .umb-toolbar.umb-editing .umb-toggle-edit {
    box-shadow: 0 4px 18px rgba(255,60,60,0.28), inset 0 0 6px rgba(255,60,60,0.06);
    transform: translateY(-1px);
    border-radius: 6px;
  }

  .umb-toolbar button, .umb-toolbar select {
    background:#222; color:#fff; border: none; padding:6px 8px; border-radius:6px; cursor:pointer;
  }
  .umb-toolbar input[type=range] { width:140px; }
  .umb-help { font-size:12px; color:#ddd; margin-left:8px; }

  .umb-toolbar.umb-collapsed {
    width:44px;
    height:44px;
    padding:6px;
    gap:0;
    justify-content:center;
  }
  .umb-toolbar.umb-collapsed > *:not(.umb-toggle-collapse) { display:none; }

  .umb-toggle-collapse { font-weight:700; padding:6px 8px; }

  .umb-overlay {
    position: absolute;
    top: 0; left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    z-index: ${UI_ZINDEX - 1};
  }

  .umb-region {
    position: absolute;
    box-sizing: border-box;
    background: rgba(255,255,255,${REGION_BG_MIN_ALPHA});
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    border: 2px dashed rgba(255,255,255,0.6);
    pointer-events: none;
    overflow: visible;
  }

  .umb-region.ellipse { border-radius: 50%; }

  .umb-region.editing { border-style: solid; box-shadow: 0 6px 20px rgba(0,0,0,0.6); }

  .umb-handle {
    position: absolute;
    width: ${HANDLE_SIZE}px;
    height: ${HANDLE_SIZE}px;
    background: #fff;
    border-radius: 3px;
    border: 1px solid #333;
    box-shadow: 0 2px 6px rgba(0,0,0,0.4);
    transform: translate(-50%,-50%);
    cursor: nwse-resize;
    z-index: ${UI_ZINDEX + 1};
  }
  .umb-handle.hidden { display: none; }

  .umb-bubble {
    position: fixed;
    z-index: ${UI_ZINDEX + 2};
    background: rgba(0,0,0,0.88);
    color: #fff;
    padding:8px 10px;
    border-radius:8px;
    font-size:13px;
    pointer-events: auto;
    max-width: 320px;
    box-shadow: 0 10px 30px rgba(0,0,0,0.6);
  }

  .umb-bubble small { display:block; color:#bbb; margin-top:6px; font-size:12px; }
  `;

  try { GM_addStyle(css); } catch (e) {
    const s = document.createElement('style'); s.textContent = css; document.head.appendChild(s);
  }

  function mapSliderToBlurAndAlpha(sliderValue) {
    const v = Math.max(0, Math.min(100, Number(sliderValue) || 0));
    const blurPx = Math.round(Math.pow(v / 100, 1.1) * 90);
    const alpha = Math.min(0.65, Math.max(REGION_BG_MIN_ALPHA, 0.02 + blurPx / 180));
    return { blurPx, alpha };
  }

  const toolbar = document.createElement('div');
  toolbar.className = 'umb-toolbar';
  toolbar.innerHTML = `
    <button class="umb-toggle-collapse" id="umb-collapse-btn">⏵</button>
    <button class="umb-toggle-edit" id="umb-toggle-edit">Enter edit</button>
    <button id="umb-new-rect">New rectangle</button>
    <button id="umb-new-ellipse">New ellipse</button>
    <label class="umb-tag">Blur: <input id="umb-global-blur" type="range" min="0" max="100" value="${DEFAULT_SLIDER_VALUE}" /></label>
    <button id="umb-clear-all">Clear all</button>
    <button id="umb-help-btn">Help</button>
  `;
  document.body.appendChild(toolbar);

  const btnEdit = toolbar.querySelector('#umb-toggle-edit');
  const btnNewRect = toolbar.querySelector('#umb-new-rect');
  const btnNewEllipse = toolbar.querySelector('#umb-new-ellipse');
  const inputGlobalBlur = toolbar.querySelector('#umb-global-blur');
  const btnClear = toolbar.querySelector('#umb-clear-all');
  const btnHelp = toolbar.querySelector('#umb-help-btn');
  const btnCollapse = toolbar.querySelector('#umb-collapse-btn');

  const overlay = document.createElement('div');
  overlay.className = 'umb-overlay';
  overlay.style.top = '0px';
  overlay.style.left = '0px';
  document.body.appendChild(overlay);
  function updateOverlaySize() {
    overlay.style.width = Math.max(document.documentElement.scrollWidth, window.innerWidth) + 'px';
    overlay.style.height = Math.max(document.documentElement.scrollHeight, window.innerHeight) + 'px';
  }
  window.addEventListener('resize', updateOverlaySize);
  window.addEventListener('scroll', updateOverlaySize);
  const mo = new MutationObserver(updateOverlaySize);
  mo.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
  updateOverlaySize();

  let regions = [];
  let editing = false;
  let selectedRegion = null;
  let isPointerDown = false;
  let drawMode = null;
  let drawStart = null;
  let perRegionBubble = null;
  let helpBubble = null;

  function createRegion({ left = 100, top = 100, width = 240, height = 120, sliderValue = DEFAULT_SLIDER_VALUE, shape = 'rect' } = {}) {
    const el = document.createElement('div');
    el.className = 'umb-region' + (shape === 'ellipse' ? ' ellipse' : '');
    el.style.left = left + 'px';
    el.style.top = top + 'px';
    el.style.width = width + 'px';
    el.style.height = height + 'px';
    el.style.pointerEvents = editing ? 'auto' : 'none';
    el.style.zIndex = UI_ZINDEX - 0;
    overlay.appendChild(el);

    const region = { el, left, top, width, height, sliderValue, shape, handles: null };
    applyMappedStyleToRegion(region);

    el.addEventListener('pointerdown', regionPointerDown);
    el.addEventListener('dblclick', () => {
      region.shape = region.shape === 'rect' ? 'ellipse' : 'rect';
      el.classList.toggle('ellipse', region.shape === 'ellipse');
      applyMappedStyleToRegion(region);
    });

    regions.push(region);
    return region;
  }

  function applyMappedStyleToRegion(region) {
    const { blurPx, alpha } = mapSliderToBlurAndAlpha(region.sliderValue);
    region.el.style.left = region.left + 'px';
    region.el.style.top = region.top + 'px';
    region.el.style.width = Math.max(2, region.width) + 'px';
    region.el.style.height = Math.max(2, region.height) + 'px';
    region.el.style.background = `rgba(255,255,255,${alpha})`;
    if (blurPx === 0) {
      region.el.style.backdropFilter = 'none';
      region.el.style.webkitBackdropFilter = 'none';
    } else {
      region.el.style.backdropFilter = `blur(${blurPx}px)`;
      region.el.style.webkitBackdropFilter = `blur(${blurPx}px)`;
    }
    region.el.classList.toggle('ellipse', region.shape === 'ellipse');
  }

  function setGlobalBlurToRegions(sliderValue) {
    regions.forEach(r => { r.sliderValue = sliderValue; applyMappedStyleToRegion(r); });
  }

  function selectRegion(region) {
    if (selectedRegion && selectedRegion !== region) deselectRegion(selectedRegion);
    selectedRegion = region;
    if (!region) return;
    region.el.classList.add('editing');
    addHandles(region);
    showPerRegionBubble(region);
  }

  function deselectRegion(region) {
    if (!region) return;
    region.el.classList.remove('editing');
    removeHandles(region);
    hidePerRegionBubble();
    if (selectedRegion === region) selectedRegion = null;
  }

  function addHandles(region) {
    removeHandles(region);
    const pos = ['nw','n','ne','e','se','s','sw','w'];
    region.handles = {};
    pos.forEach(p => {
      const h = document.createElement('div');
      h.className = 'umb-handle';
      h.dataset.pos = p;
      h.style.pointerEvents = editing ? 'auto' : 'none';
      overlay.appendChild(h);
      region.handles[p] = h;
      h.addEventListener('pointerdown', handlePointerDown);
    });
    positionHandles(region);
  }

  function removeHandles(region) {
    if (!region || !region.handles) return;
    for (const k in region.handles) {
      const h = region.handles[k];
      if (h && h.parentNode) h.parentNode.removeChild(h);
    }
    region.handles = null;
  }

  function positionHandles(region) {
    if (!region.handles) return;
    const x = region.left, y = region.top, w = region.width, h = region.height;
    const centers = { nw: [x, y], n: [x + w/2, y], ne: [x + w, y], e: [x + w, y + h/2], se: [x + w, y + h], s: [x + w/2, y + h], sw: [x, y + h], w: [x, y + h/2] };
    for (const p in centers) {
      const [cx, cy] = centers[p];
      const el = region.handles[p];
      el.style.left = (cx - window.scrollX) + 'px';
      el.style.top = (cy - window.scrollY) + 'px';
      el.style.display = editing ? '' : 'none';
      el.style.pointerEvents = editing ? 'auto' : 'none';
      el.style.position = 'fixed';
    }
  }

  let activeAction = null;

  overlay.addEventListener('pointerdown', (ev) => {
    if (!editing) return;
    if (ev.target !== overlay) return;
    if (!drawMode) return;
    isPointerDown = true;
    const docX = ev.pageX, docY = ev.pageY;
    drawStart = { x: docX, y: docY };
    const region = createRegion({ left: docX, top: docY, width: 2, height: 2, sliderValue: Number(inputGlobalBlur.value), shape: drawMode === 'ellipse' ? 'ellipse' : 'rect' });
    activeAction = { type: 'draw', region, startX: docX, startY: docY };
    selectRegion(region);
    ev.preventDefault();
  });

  function regionPointerDown(ev) {
    if (!editing) return;
    ev.stopPropagation();
    isPointerDown = true;
    const el = ev.currentTarget;
    const region = regions.find(r => r.el === el);
    if (!region) return;
    selectRegion(region);
    const docX = ev.pageX, docY = ev.pageY;
    activeAction = { type: 'move', region, startX: docX, startY: docY, origLeft: region.left, origTop: region.top };
    el.setPointerCapture(ev.pointerId);
    ev.preventDefault();
  }

  function handlePointerDown(ev) {
    if (!editing) return;
    ev.stopPropagation();
    isPointerDown = true;
    const handle = ev.currentTarget;
    const region = regions.find(r => r.handles && Object.values(r.handles).includes(handle));
    if (!region) return;
    selectRegion(region);
    activeAction = { type: 'resize', region, handlePos: handle.dataset.pos, startX: ev.pageX, startY: ev.pageY, orig: { left: region.left, top: region.top, width: region.width, height: region.height } };
    handle.setPointerCapture(ev.pointerId);
    ev.preventDefault();
  }

  window.addEventListener('pointermove', (ev) => {
    if (!editing || !isPointerDown || !activeAction) return;
    const a = activeAction;
    const dx = ev.pageX - a.startX, dy = ev.pageY - a.startY;

    if (a.type === 'draw') {
      const left = Math.min(a.startX, ev.pageX);
      const top = Math.min(a.startY, ev.pageY);
      const w = Math.max(2, Math.abs(ev.pageX - a.startX));
      const h = Math.max(2, Math.abs(ev.pageY - a.startY));
      a.region.left = left; a.region.top = top; a.region.width = w; a.region.height = h;
      applyMappedStyleToRegion(a.region);
      positionHandles(a.region);
    } else if (a.type === 'move') {
      a.region.left = a.origLeft + dx; a.region.top = a.origTop + dy;
      applyMappedStyleToRegion(a.region);
      positionHandles(a.region);
    } else if (a.type === 'resize') {
      const orig = a.orig;
      let { left, top, width, height } = orig;
      const minSize = 6; const hp = a.handlePos;
      if (hp.includes('n')) { const newTop = top + dy; const newHeight = height - dy; if (newHeight > minSize) { top = newTop; height = newHeight; } }
      if (hp.includes('s')) { const newHeight = height + dy; if (newHeight > minSize) { height = newHeight; } }
      if (hp.includes('w')) { const newLeft = left + dx; const newWidth = width - dx; if (newWidth > minSize) { left = newLeft; width = newWidth; } }
      if (hp.includes('e')) { const newWidth = width + dx; if (newWidth > minSize) { width = newWidth; } }
      a.region.left = left; a.region.top = top; a.region.width = width; a.region.height = height;
      applyMappedStyleToRegion(a.region);
      positionHandles(a.region);
    }
    ev.preventDefault();
  });

  window.addEventListener('pointerup', (ev) => {
    if (!editing) return;
    if (!isPointerDown) return;
    isPointerDown = false;
    if (activeAction && activeAction.type === 'draw') {
      const r = activeAction.region;
      if (r.width < 6 || r.height < 6) removeRegion(r);
      else { applyMappedStyleToRegion(r); positionHandles(r); positionPerRegionBubble(r); }
    } else if (activeAction && activeAction.type === 'move') {
      if (activeAction.region) positionPerRegionBubble(activeAction.region);
    } else if (activeAction && activeAction.type === 'resize') {
      if (activeAction.region) positionPerRegionBubble(activeAction.region);
    }
    if (activeAction && activeAction.region && activeAction.region.el) {
      try { activeAction.region.el.releasePointerCapture(ev.pointerId); } catch (e) {}
    }
    activeAction = null;
  });

  function removeRegion(region) {
    if (!region) return;
    if (region.el && region.el.parentNode) region.el.parentNode.removeChild(region.el);
    removeHandles(region);
    if (selectedRegion === region) selectedRegion = null;
    regions = regions.filter(r => r !== region);
    hidePerRegionBubble();
  }

  window.addEventListener('keydown', (ev) => {
    if (ev.key === 'Escape') {
      if (editing) toggleEdit(false);
    }
    if ((ev.key === 'Delete' || ev.key === 'Backspace') && selectedRegion) {
      removeRegion(selectedRegion);
    }
    if ((ev.key === '+' || ev.key === '=') && selectedRegion) {
      selectedRegion.sliderValue = Math.min(100, (selectedRegion.sliderValue || DEFAULT_SLIDER_VALUE) + 5);
      applyMappedStyleToRegion(selectedRegion);
      updatePerRegionBubbleSlider(selectedRegion);
    }
    if ((ev.key === '-' || ev.key === '_') && selectedRegion) {
      selectedRegion.sliderValue = Math.max(0, (selectedRegion.sliderValue || DEFAULT_SLIDER_VALUE) - 5);
      applyMappedStyleToRegion(selectedRegion);
      updatePerRegionBubbleSlider(selectedRegion);
    }
  });

  function toggleEdit(state = !editing) {
    editing = state;
    overlay.style.pointerEvents = editing ? 'auto' : 'none';
    regions.forEach(r => {
      r.el.style.pointerEvents = editing ? 'auto' : 'none';
      if (r.handles) {
        for (const k in r.handles) {
          r.handles[k].style.display = editing ? '' : 'none';
          r.handles[k].style.pointerEvents = editing ? 'auto' : 'none';
        }
      }
    });
    btnEdit.textContent = editing ? 'Exit edit' : 'Enter edit';

    if (editing) {
      toolbar.classList.add('umb-editing');
      btnCollapse.title = 'Collapse toolbar (you are in edit mode)';
    } else {
      toolbar.classList.remove('umb-editing');
      btnCollapse.title = 'Collapse toolbar';
    }

    if (!editing && selectedRegion) deselectRegion(selectedRegion);
    if (helpBubble) positionHelpBubble();
  }

  btnEdit.addEventListener('click', () => toggleEdit(!editing));
  btnNewRect.addEventListener('click', () => {
    const left = window.scrollX + (window.innerWidth - 320) / 2;
    const top = window.scrollY + (window.innerHeight - 160) / 2;
    const r = createRegion({ left, top, width: 320, height: 160, sliderValue: Number(inputGlobalBlur.value), shape: 'rect' });
    applyMappedStyleToRegion(r); positionHandles(r); selectRegion(r);
    if (!editing) toggleEdit(true);
  });
  btnNewEllipse.addEventListener('click', () => {
    const left = window.scrollX + (window.innerWidth - 240) / 2;
    const top = window.scrollY + (window.innerHeight - 240) / 2;
    const r = createRegion({ left, top, width: 240, height: 240, sliderValue: Number(inputGlobalBlur.value), shape: 'ellipse' });
    applyMappedStyleToRegion(r); positionHandles(r); selectRegion(r);
    if (!editing) toggleEdit(true);
  });

  inputGlobalBlur.addEventListener('input', () => {
    const v = Number(inputGlobalBlur.value);
    setGlobalBlurToRegions(v);
  });

  btnClear.addEventListener('click', () => {
    if (!confirm('Clear all blur regions?')) return;
    for (const r of [...regions]) removeRegion(r);
  });

  function showPerRegionBubble(region) {
    hidePerRegionBubble();
    perRegionBubble = document.createElement('div');
    perRegionBubble.className = 'umb-bubble';
    perRegionBubble.innerHTML = `
      <div><strong>Region</strong></div>
      <div style="margin-top:8px;">Blur: <input id="umb-region-blur" type="range" min="0" max="100" value="${region.sliderValue}" /></div>
      <small>Double-click shape to toggle rect/ellipse • Drag to move • Handles to resize</small>
    `;
    document.body.appendChild(perRegionBubble);

    const slider = perRegionBubble.querySelector('#umb-region-blur');
    slider.addEventListener('input', () => {
      region.sliderValue = Number(slider.value);
      applyMappedStyleToRegion(region);
    });

    function positionBubble() {
      if (!perRegionBubble || !region) return;
      const vx = region.left - window.scrollX;
      const vy = region.top - window.scrollY;
      const bubbleW = perRegionBubble.offsetWidth || 220;
      const bubbleH = perRegionBubble.offsetHeight || 80;
      let left = vx + region.width + 12;
      let top = vy;
      if (left + bubbleW > window.innerWidth - 12) { left = vx - bubbleW - 12; }
      if (left < 12) left = 12;
      if (top + bubbleH > window.innerHeight - 12) top = Math.max(12, window.innerHeight - bubbleH - 12);
      perRegionBubble.style.left = left + 'px';
      perRegionBubble.style.top = top + 'px';
    }
    positionBubble();
    window.addEventListener('scroll', positionBubble);
    window.addEventListener('resize', positionBubble);
    perRegionBubble._positionHandler = positionBubble;
  }

  function updatePerRegionBubbleSlider(region) {
    if (!perRegionBubble) return;
    const slider = perRegionBubble.querySelector('#umb-region-blur');
    if (slider) slider.value = region.sliderValue;
  }

  function hidePerRegionBubble() {
    if (!perRegionBubble) return;
    window.removeEventListener('scroll', perRegionBubble._positionHandler);
    window.removeEventListener('resize', perRegionBubble._positionHandler);
    perRegionBubble.remove();
    perRegionBubble = null;
  }

  function positionPerRegionBubble(region) {
    if (!perRegionBubble || !region) return;
    const vx = region.left - window.scrollX;
    const vy = region.top - window.scrollY;
    const bubbleW = perRegionBubble.offsetWidth || 220;
    const bubbleH = perRegionBubble.offsetHeight || 80;
    let left = vx + region.width + 12;
    let top = vy;
    if (left + bubbleW > window.innerWidth - 12) { left = vx - bubbleW - 12; }
    if (left < 12) left = 12;
    if (top + bubbleH > window.innerHeight - 12) top = Math.max(12, window.innerHeight - bubbleH - 12);
    perRegionBubble.style.left = left + 'px';
    perRegionBubble.style.top = top + 'px';
  }

  btnCollapse.addEventListener('click', () => {
    const collapsed = toolbar.classList.toggle('umb-collapsed');
    btnCollapse.textContent = collapsed ? '⏴' : '⏵';
    if (collapsed && helpBubble) hideHelpBubble();
    if (!collapsed && helpBubble) positionHelpBubble();
  });

  btnHelp.addEventListener('click', () => {
    if (helpBubble) { hideHelpBubble(); return; }
    helpBubble = document.createElement('div');
    helpBubble.className = 'umb-bubble';
    helpBubble.innerHTML = `
      <strong>Universal Manual Blur - Help</strong>
      <div style="margin-top:6px; color:#ddd; font-size:13px;">
        • Enter edit to create and edit blur regions (New rectangle / New ellipse).<br/>
        • Drag on empty page (in edit + after New) to draw a region.<br/>
        • Drag region to move; use handles to resize; double-click to toggle rect/ellipse.<br/>
        • Use the global slider or per-region slider (visible when a region is selected).<br/>
        • Exit edit to interact with the page; regions remain but are non-interactive.<br/>
        • Right-click the toolbar to copy a JSON snapshot of regions.<br/>
        • Ctrl+Shift+e to paste/import a snapshot<br/>
      </div>
    `;
    document.body.appendChild(helpBubble);
    positionHelpBubble();
    window.addEventListener('scroll', positionHelpBubble);
    window.addEventListener('resize', positionHelpBubble);
  });

  function positionHelpBubble() {
    if (!helpBubble) return;
    const rect = toolbar.getBoundingClientRect();
    const bw = helpBubble.offsetWidth || 320;
    let left = Math.max(8, rect.left + rect.width/2 - bw/2);
    let top = rect.top - helpBubble.offsetHeight - 10;
    if (toolbar.classList.contains('umb-collapsed') || top < 8) {
      top = rect.bottom + 10;
    }
    helpBubble.style.left = left + 'px';
    helpBubble.style.top = top + 'px';
  }

  function hideHelpBubble() {
    if (!helpBubble) return;
    window.removeEventListener('scroll', positionHelpBubble);
    window.removeEventListener('resize', positionHelpBubble);
    helpBubble.remove();
    helpBubble = null;
  }

  function updateAllHandles() {
    regions.forEach(r => positionHandles(r));
    if (perRegionBubble && selectedRegion) perRegionBubble._positionHandler && perRegionBubble._positionHandler();
  }
  window.addEventListener('scroll', updateAllHandles);
  window.addEventListener('resize', updateAllHandles);

  toolbar.addEventListener('contextmenu', (ev) => {
    ev.preventDefault();
    const exportData = regions.map(r => ({ left: r.left, top: r.top, width: r.width, height: r.height, sliderValue: r.sliderValue, shape: r.shape }));
    prompt('Copy JSON snapshot (paste with Ctrl+Shift+e):', JSON.stringify(exportData));
  });
  window.addEventListener('keydown', (ev) => {
    if (ev.ctrlKey && ev.shiftKey && ev.key.toLowerCase() === 'e') {
      const raw = prompt('Paste JSON snapshot to import blur regions:');
      if (!raw) return;
      try {
        const arr = JSON.parse(raw);
        if (!Array.isArray(arr)) throw new Error('Not an array');
        for (const r of [...regions]) removeRegion(r);
        for (const it of arr) {
          const rr = createRegion({ left: it.left, top: it.top, width: it.width, height: it.height, sliderValue: it.sliderValue || DEFAULT_SLIDER_VALUE, shape: it.shape || 'rect' });
          applyMappedStyleToRegion(rr); positionHandles(rr);
        }
        alert('Imported ' + arr.length + ' regions.');
      } catch (e) {
        alert('Import failed: ' + e.message);
      }
    }
  });

  function supportsBackdropFilter() {
    const test = document.createElement('div');
    test.style.webkitBackdropFilter = 'blur(2px)';
    test.style.backdropFilter = 'blur(2px)';
    return (test.style.webkitBackdropFilter.length > 0) || (test.style.backdropFilter.length > 0);
  }
  if (!supportsBackdropFilter()) {
    const warn = document.createElement('div');
    warn.style.position = 'fixed';
    warn.style.left = '12px';
    warn.style.bottom = '12px';
    warn.style.zIndex = UI_ZINDEX;
    warn.style.background = 'rgba(200,60,60,0.95)';
    warn.style.color = '#fff';
    warn.style.padding = '8px 10px';
    warn.style.borderRadius = '8px';
    warn.style.fontFamily = 'system-ui, Arial';
    warn.style.fontSize = '13px';
    warn.textContent = 'Notice: your browser may not support backdrop-filter; blur may not appear. Use Chromium, Edge or Firefox for best results.';
    document.body.appendChild(warn);
    setTimeout(() => { try { warn.remove(); } catch (e) {} }, 12000);
  }

  window.addEventListener('beforeunload', () => {
    try { overlay.remove(); toolbar.remove(); } catch (e) {}
  });
})();