Vision Correction PRO v8

Cylindrical lens simulation — curved arc correction, per-eye PD, depth/width/spread/tint controls

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Vision Correction PRO v8
// @namespace    vision.correction.pro
// @version      8.0
// @description  Cylindrical lens simulation — curved arc correction, per-eye PD, depth/width/spread/tint controls
// @match        *://*/*
// @run-at       document-end
// @grant        none
// ==/UserScript==

(function () {
'use strict';

const STORAGE_KEY = "vision_v8";

const DEFAULTS = {
    enabled:     true,
    showTest:    false,
    panelX:      null,
    panelY:      null,
    left: {
        axis:       50,
        strength:   0.52,
        curvature:  60,
        width:      1.2,
        depth:      3,
        spread:     0.85,
        falloff:    1.8,
        pdOffset:   -30,
        tint:       0,
    },
    right: {
        axis:       45,
        strength:   0.48,
        curvature:  55,
        width:      1.2,
        depth:      3,
        spread:     0.85,
        falloff:    1.8,
        pdOffset:   30,
        tint:       0,
    },
    focusAssist: 0.15,
};

function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }

function load() {
    try {
        const saved = JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};
        return {
            ...DEFAULTS, ...saved,
            left:  { ...DEFAULTS.left,  ...(saved.left  || {}) },
            right: { ...DEFAULTS.right, ...(saved.right || {}) },
        };
    } catch { return deepClone(DEFAULTS); }
}

let settings = load();
let dirty = true;

function save()      { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); }
function markDirty() { dirty = true; }

/* CANVAS */

const canvas = document.createElement('canvas');
const ctx    = canvas.getContext('2d');

Object.assign(canvas.style, {
    position: 'fixed', top: '0', left: '0',
    width: '100%', height: '100%',
    pointerEvents: 'none', zIndex: '999999',
    mixBlendMode: 'screen',
});
document.body.appendChild(canvas);

let dpr = 1, W = 0, H = 0;

function resize() {
    dpr = window.devicePixelRatio || 1;
    W = window.innerWidth; H = window.innerHeight;
    canvas.width = W * dpr; canvas.height = H * dpr;
    canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    markDirty();
}
window.addEventListener('resize', resize);
resize();

/* CYLINDRICAL LENS ENGINE
   ─────────────────────────────────────────────────────────
   A cylindrical lens corrects refractive error along one meridian.
   We simulate the corrective wavefront by drawing iso-phase arcs:
   quadratic bezier curves whose sag (bow) models the cylinder power
   and reverses sign across the optical center.

   Arc at perpendicular position t from optical center (cx, cy):
     P1 = (ox, oy) + axisDir * halfLen
     P2 = (ox, oy) - axisDir * halfLen
     CP = (ox, oy) + normDir * sag          ← sag = curvature * strength * falloff
   sag sign flips at t=0 → continuous wavefront reversal like a real lens.
*/

function tintColor(tint, alpha) {
    if (tint >= 0) {
        return `rgba(255,${Math.round(255 - tint*40)},${Math.round(255 - tint*120)},${alpha})`;
    } else {
        const t = -tint;
        return `rgba(${Math.round(255 - t*80)},${Math.round(255 - t*20)},255,${alpha})`;
    }
}

function drawCorrectionField(eye) {
    const { axis, strength, curvature, width, depth, spread, falloff, pdOffset, tint } = eye;

    const angle = axis * Math.PI / 180;
    const axisX =  Math.cos(angle), axisY = Math.sin(angle);
    const normX = -axisY,          normY  = axisX;

    const cx = W / 2 + pdOffset;
    const cy = H / 2;
    const halfLen = Math.max(W, H) * 0.7;
    const spacing = 20;

    const count = Math.ceil(Math.max(W, H) / spacing) + 6;

    ctx.lineWidth = width;

    for (let pass = 0; pass < depth; pass++) {
        const ps = (pass - (depth - 1) / 2) * 1.8;

        for (let i = -count; i <= count; i++) {
            const t  = i * spacing;
            const ox = cx + normX * t + axisX * ps;
            const oy = cy + normY * t + axisY * ps;

            const dx   = (ox - cx) / (W * 0.5 + 1);
            const dy   = (oy - cy) / (H * 0.5 + 1);
            const dist = Math.sqrt(dx*dx + dy*dy);

            if (dist > spread) continue;

            const ff  = Math.pow(dist / (spread + 0.001), falloff);
            const ls  = strength * ff;
            const sag = (t >= 0 ? 1 : -1) * curvature * ls;

            const x1 = ox + axisX * halfLen, y1 = oy + axisY * halfLen;
            const x2 = ox - axisX * halfLen, y2 = oy - axisY * halfLen;
            const cpx = ox + normX * sag,   cpy = oy + normY * sag;

            const alpha = (0.007 + ls * 0.044) / Math.max(depth, 1);

            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.quadraticCurveTo(cpx, cpy, x2, y2);
            ctx.strokeStyle = tintColor(tint, alpha);
            ctx.stroke();
        }
    }
}

function drawFocusAssist() {
    const s = settings.focusAssist;
    if (s <= 0) return;
    const cx = W / 2, cy = H / 2;
    const g  = ctx.createRadialGradient(cx, cy, 0, cx, cy, cx * 0.4);
    g.addColorStop(0, `rgba(255,255,255,${0.04 * s})`);
    g.addColorStop(1,  'rgba(255,255,255,0)');
    ctx.globalAlpha = 1;
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);
}

/* RENDER LOOP */

function render() {
    requestAnimationFrame(render);
    if (!settings.enabled) {
        if (dirty) { ctx.clearRect(0, 0, canvas.width, canvas.height); dirty = false; }
        return;
    }
    if (!dirty) return;
    dirty = false;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    drawCorrectionField(settings.left);
    drawCorrectionField(settings.right);
    drawFocusAssist();
}
render();

/* TEST APPLET */

const testLayer = document.createElement('div');
Object.assign(testLayer.style, {
    position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
    display: 'none', zIndex: '999998', pointerEvents: 'none',
    color: 'white', fontFamily: 'monospace', background: 'rgba(0,0,0,0.72)',
});

(function buildTest() {
    const center = document.createElement('div');
    Object.assign(center.style, {
        position: 'absolute', top: '50%', left: '50%',
        transform: 'translate(-50%,-50%)', textAlign: 'center',
    });
    center.innerHTML = `
        <div style="font-size:24px;font-weight:bold;">AXIS CALIBRATION WHEEL</div>
        <div style="font-size:12px;margin-top:5px;color:#aaa;">
            Adjust axis until lines appear thinnest and sharpest
        </div>`;

    const wheel = document.createElement('div');
    wheel.style.cssText = 'position:relative;width:300px;height:300px;margin:14px auto;';
    for (let i = 0; i < 36; i++) {
        const deg  = i * 5;
        const line = document.createElement('div');
        Object.assign(line.style, {
            position: 'absolute', width: '1px', height: '300px',
            background: i % 6 === 0 ? '#fff' : 'rgba(255,255,255,0.4)',
            left: '50%', top: '0',
            transformOrigin: '50% 50%',
            transform: `translateX(-50%) rotate(${deg}deg)`,
        });
        if (i % 6 === 0) {
            const lbl = document.createElement('span');
            lbl.style.cssText = 'position:absolute;font-size:9px;color:#aaa;left:50%;top:2px;transform:translateX(-50%);';
            lbl.textContent = deg + '\u00b0';
            line.appendChild(lbl);
        }
        wheel.appendChild(line);
    }
    center.appendChild(wheel);
    testLayer.appendChild(center);

    const acuity = document.createElement('div');
    Object.assign(acuity.style, {
        position: 'absolute', bottom: '10%',
        left: '50%', transform: 'translateX(-50%)', textAlign: 'center',
    });
    acuity.innerHTML = `
        <div style="font-size:16px;margin-bottom:8px;color:#aaa;">ACUITY / FOCUS TEST</div>
        <div style="font-size:18px;">The quick brown fox jumps over the lazy dog</div>
        <div style="font-size:13px;margin-top:3px;">The quick brown fox jumps over the lazy dog</div>
        <div style="font-size:10px;margin-top:2px;">The quick brown fox jumps over the lazy dog</div>
        <div style="font-size:8px;margin-top:2px;">The quick brown fox jumps over the lazy dog</div>
        <div style="font-size:6px;margin-top:2px;">The quick brown fox jumps over the lazy dog</div>`;
    testLayer.appendChild(acuity);
})();

document.body.appendChild(testLayer);
function syncTest() { testLayer.style.display = settings.showTest ? 'block' : 'none'; }

/* PANEL */

const Sty = {
    btn: 'flex:1;padding:4px 0;border:none;border-radius:4px;cursor:pointer;font-size:11px;background:#252535;color:#ccc;',
    dbtn:'flex:1;padding:4px 0;border:none;border-radius:4px;cursor:pointer;font-size:11px;background:#5c1111;color:#fbb;',
    sec: 'color:#666;font-size:10px;text-transform:uppercase;letter-spacing:.06em;margin:7px 0 2px;',
};

function sliderHTML(id, label, min, max, step, val, title) {
    return `<div style="display:flex;align-items:center;gap:5px;margin:1px 0;" title="${title||''}">
        <span style="width:72px;color:#888;font-size:11px;">${label}</span>
        <input id="${id}" type="range" min="${min}" max="${max}" step="${step}" value="${val}" style="flex:1;height:13px;">
        <span id="${id}_v" style="width:36px;text-align:right;font-size:11px;">${val}</span>
    </div>`;
}

function eyeHTML(p, e) {
    return [
        sliderHTML(`${p}_axis`,      'Axis °',      0,   180, 1,    e.axis,      'Correction axis meridian (degrees)'),
        sliderHTML(`${p}_strength`,  'Strength',    0,   1,   0.01, e.strength,  'Cylinder power / overall intensity'),
        sliderHTML(`${p}_curvature`, 'Curvature',   0,   200, 1,    e.curvature, 'Arc bow depth — 0=flat, 200=highly curved'),
        sliderHTML(`${p}_width`,     'Width',       0.5, 5,   0.1,  e.width,     'Stroke width of correction arcs'),
        sliderHTML(`${p}_depth`,     'Depth',       1,   8,   1,    e.depth,     'Number of overlay passes'),
        sliderHTML(`${p}_spread`,    'Spread',      0,   1,   0.01, e.spread,    'Radial extent of correction field'),
        sliderHTML(`${p}_falloff`,   'Falloff',     0.5, 4,   0.1,  e.falloff,   'Falloff exponent — higher concentrates edges'),
        sliderHTML(`${p}_pdOffset`,  'PD Offset', -150, 150,  1,    e.pdOffset,  'Horizontal optical center shift'),
        sliderHTML(`${p}_tint`,      'Tint',       -1,   1,   0.05, e.tint,      '-1 cool/blue  0 white  +1 warm/amber'),
    ].join('');
}

const panel = document.createElement('div');
Object.assign(panel.style, {
    position: 'fixed', bottom: '10px', right: '10px',
    background: 'rgba(10,10,16,0.93)', color: '#ddd',
    padding: '10px 13px', fontSize: '12px',
    zIndex: '2147483647', borderRadius: '8px',
    pointerEvents: 'auto', userSelect: 'none',
    boxShadow: '0 3px 18px rgba(0,0,0,0.75)',
    minWidth: '268px', lineHeight: '1.6',
});

panel.innerHTML = `
<div id="ph" style="font-weight:bold;margin-bottom:8px;cursor:grab;
     border-bottom:1px solid #2a2a3a;padding-bottom:5px;
     display:flex;justify-content:space-between;align-items:center;">
    <span>👁 Vision PRO v8</span>
    <span id="pm" style="cursor:pointer;padding:0 4px;color:#666;" title="Minimize">–</span>
</div>
<div id="pb">
    <div style="display:flex;gap:4px;margin-bottom:7px;">
        <button id="tl" style="${Sty.btn};flex:1;background:#161626;color:#8af;">◀ Left Eye</button>
        <button id="tr" style="${Sty.btn};flex:1;">Right Eye ▶</button>
    </div>
    <div id="el">${eyeHTML('l', settings.left)}</div>
    <div id="er" style="display:none;">${eyeHTML('r', settings.right)}</div>
    <div style="${Sty.sec}">Global</div>
    ${sliderHTML('gf', 'Focus Assist', 0, 0.5, 0.01, settings.focusAssist, 'Central focus enhancement')}
    <div style="margin-top:9px;display:flex;gap:5px;">
        <button id="btst" style="${Sty.btn}">Test</button>
        <button id="btog" style="${Sty.btn}">On/Off</button>
        <button id="brst" style="${Sty.dbtn}">Reset</button>
    </div>
    <div id="st" style="font-size:10px;color:#555;margin-top:5px;text-align:center;">
        Alt+V toggle &middot; hover sliders for tips
    </div>
</div>`;

document.body.appendChild(panel);

if (settings.panelX !== null) {
    panel.style.right = 'auto'; panel.style.bottom = 'auto';
    panel.style.left = settings.panelX + 'px';
    panel.style.top  = settings.panelY + 'px';
}

/* tabs */
panel.querySelector('#tl').addEventListener('click', () => setTab('l'));
panel.querySelector('#tr').addEventListener('click', () => setTab('r'));
function setTab(e) {
    panel.querySelector('#el').style.display = e==='l' ? 'block' : 'none';
    panel.querySelector('#er').style.display = e==='r' ? 'block' : 'none';
    panel.querySelector('#tl').style.cssText = `${Sty.btn};flex:1;` + (e==='l' ? 'background:#161626;color:#8af;' : '');
    panel.querySelector('#tr').style.cssText = `${Sty.btn};flex:1;` + (e==='r' ? 'background:#161626;color:#8af;' : '');
}

/* sliders */
function bind(id, setter) {
    const inp = panel.querySelector('#'+id);
    const lbl = panel.querySelector('#'+id+'_v');
    if (!inp) return;
    inp.addEventListener('input', () => {
        const v = parseFloat(inp.value);
        lbl.textContent = v; setter(v); save(); markDirty();
    });
}
['axis','strength','curvature','width','depth','spread','falloff','pdOffset','tint'].forEach(k => {
    bind('l_'+k, v => settings.left[k]  = v);
    bind('r_'+k, v => settings.right[k] = v);
});
bind('gf', v => settings.focusAssist = v);

/* buttons */
panel.querySelector('#btst').addEventListener('click', () => {
    settings.showTest = !settings.showTest; syncTest(); save();
});
panel.querySelector('#btog').addEventListener('click', toggle);
panel.querySelector('#brst').addEventListener('click', () => {
    if (!confirm('Reset all vision settings to defaults?')) return;
    Object.assign(settings, deepClone(DEFAULTS));
    save(); refreshSliders(); syncTest(); markDirty();
});

function refreshSliders() {
    const upd = (id, v) => {
        const el = panel.querySelector('#'+id);
        const lb = panel.querySelector('#'+id+'_v');
        if (el) el.value = v;
        if (lb) lb.textContent = v;
    };
    ['axis','strength','curvature','width','depth','spread','falloff','pdOffset','tint'].forEach(k => {
        upd('l_'+k, settings.left[k]);
        upd('r_'+k, settings.right[k]);
    });
    upd('gf', settings.focusAssist);
}

/* minimize */
const pb = panel.querySelector('#pb');
const pm = panel.querySelector('#pm');
let mini = false;
pm.addEventListener('click', () => {
    mini = !mini;
    pb.style.display = mini ? 'none' : 'block';
    pm.textContent = mini ? '+' : '–';
});

/* drag */
(function(){
    const h = panel.querySelector('#ph');
    let drag=false, ox=0, oy=0;
    h.addEventListener('mousedown', e => {
        drag=true;
        const r = panel.getBoundingClientRect();
        ox=e.clientX-r.left; oy=e.clientY-r.top;
        h.style.cursor='grabbing'; e.preventDefault();
    });
    document.addEventListener('mousemove', e => {
        if (!drag) return;
        panel.style.left=(e.clientX-ox)+'px'; panel.style.top=(e.clientY-oy)+'px';
        panel.style.right='auto'; panel.style.bottom='auto';
        settings.panelX=e.clientX-ox; settings.panelY=e.clientY-oy;
    });
    document.addEventListener('mouseup', () => {
        if (!drag) return; drag=false; h.style.cursor='grab'; save();
    });
})();

/* keyboard */
document.addEventListener('keydown', e => { if (e.altKey && e.key==='v') toggle(); });

function toggle() {
    settings.enabled = !settings.enabled;
    flash(settings.enabled ? 'Correction ON' : 'Correction OFF');
    save(); markDirty();
}
function flash(msg) {
    const el = panel.querySelector('#st');
    el.style.color='#8af'; el.textContent=msg;
    setTimeout(()=>{ el.style.color='#555'; el.textContent='Alt+V toggle \u00b7 hover sliders for tips'; }, 1800);
}

syncTest();
})();