Vision Correction PRO v8

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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