Vision Correction PRO v8

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==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();
})();