Cylindrical lens simulation — curved arc correction, per-eye PD, depth/width/spread/tint controls
// ==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 = `
`;
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 = `
`;
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 `
`;
}
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 = `
`;
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();
})();