{"id":1812,"date":"2026-03-28T21:23:37","date_gmt":"2026-03-28T20:23:37","guid":{"rendered":"https:\/\/cyberelk.net\/tim\/?p=1812"},"modified":"2026-03-30T17:23:50","modified_gmt":"2026-03-30T16:23:50","slug":"visualising-vision-correction","status":"publish","type":"post","link":"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/","title":{"rendered":"Visualising vision correction"},"content":{"rendered":"\n<p>I&#8217;ve been trying out contact lenses for the first time. Multi-focal lenses provide different focal lengths to the eye at once, and you can have different prescription lenses in each eye (as long as they don&#8217;t differ by too much).<\/p>\n\n\n\n<p>This means the brain is getting signals from the eyes, each providing potentially multiple focal lengths, and learns to combine them to reduce blur. It&#8217;s interesting and I wanted to be able to visualise how that works, so I made this interactive simulator with the help of Gemini. It shows a heatmap (green is sharp, red is blurry) over distance, comparing uncorrected vision with modern multi-focal lenses. Try it out! All the calculations happen locally within your browser.<\/p>\n\n\n\n<!--more-->\n\n\n\n<style>\n    \/* Scoped CSS - Will only affect elements inside .sim-wrapper *\/\n    .sim-wrapper {\n        font-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n        max-width: 64rem;\n        margin: 2rem auto;\n        background-color: #ffffff;\n        border: 1px solid #e2e8f0;\n        border-radius: 0.75rem;\n        box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n        overflow: hidden;\n        color: #0f172a;\n        line-height: 1.5;\n    }\n    .sim-wrapper * {\n        box-sizing: border-box;\n    }\n    .sim-header {\n        background-color: #0f172a;\n        color: #ffffff;\n        padding: 1.25rem;\n    }\n    .sim-header h2 {\n        margin: 0 0 0.25rem 0 !important;\n        padding: 0 !important;\n        font-size: 1.25rem;\n        font-weight: 700;\n        color: #ffffff !important;\n        border: none !important;\n    }\n    .sim-header p {\n        margin: 0;\n        font-size: 0.875rem;\n        color: #cbd5e1;\n    }\n    .sim-body {\n        display: flex;\n        flex-direction: column;\n    }\n    @media (min-width: 768px) {\n        .sim-body { flex-direction: row; }\n    }\n    .sim-sidebar {\n        width: 100%;\n        background-color: #f8fafc;\n        padding: 1.25rem;\n        border-right: 1px solid #e2e8f0;\n    }\n    @media (min-width: 768px) {\n        .sim-sidebar { width: 33.333%; }\n    }\n    .sim-section-title {\n        font-size: 1rem;\n        font-weight: 700;\n        margin: 0 0 0.75rem 0;\n        padding-bottom: 0.25rem;\n        border-bottom: 1px solid #e2e8f0;\n        color: #1e293b;\n    }\n    .sim-input-group {\n        margin-bottom: 0.75rem;\n    }\n    .sim-label-top {\n        display: block;\n        font-size: 0.75rem;\n        font-weight: 700;\n        color: #64748b;\n        margin-bottom: 0.25rem;\n    }\n    .sim-flex-row {\n        display: flex;\n        gap: 0.5rem;\n    }\n    .sim-flex-col {\n        flex: 1;\n        display: flex;\n        flex-direction: column;\n    }\n    .sim-flex-col label {\n        font-size: 0.7rem;\n        color: #475569;\n        text-transform: uppercase;\n        margin-bottom: 0.1rem;\n    }\n    .sim-wrapper input[type=\"number\"] {\n        width: 100%;\n        padding: 0.25rem 0.5rem;\n        border: 1px solid #cbd5e1;\n        border-radius: 0.375rem;\n        font-size: 0.875rem;\n        background-color: #ffffff;\n        color: #0f172a;\n        margin: 0;\n    }\n    .sim-note {\n        margin-top: 1rem;\n        font-size: 0.75rem;\n        color: #64748b;\n        font-style: italic;\n    }\n    .sim-main {\n        width: 100%;\n        padding: 1.25rem;\n        display: flex;\n        flex-direction: column;\n    }\n    @media (min-width: 768px) {\n        .sim-main { width: 66.666%; }\n    }\n    .sim-canvas-container {\n        position: relative;\n        width: 100%;\n        min-height: 350px;\n        flex-grow: 1;\n        border: 1px solid #e2e8f0;\n        border-radius: 0.25rem;\n        background-color: #ffffff;\n        overflow: hidden;\n    }\n    .sim-canvas-container canvas {\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n    }\n    .sim-legend {\n        margin-top: 1rem;\n        display: flex;\n        flex-wrap: wrap;\n        gap: 0.5rem 1rem;\n        font-size: 0.75rem;\n        font-weight: 500;\n        color: #475569;\n    }\n    .sim-legend-item {\n        display: flex;\n        align-items: center;\n    }\n    .sim-color-box {\n        width: 0.75rem;\n        height: 0.75rem;\n        border-radius: 0.125rem;\n        margin-right: 0.375rem;\n    }\n<\/style>\n\n<div class=\"sim-wrapper\">\n    <div class=\"sim-header\">\n        <h2>Multifocal Simulator<\/h2>\n        <p>Modelling EDOF contrast loss, intermediate dips and true binocular fusion.<\/p>\n    <\/div>\n\n    <div class=\"sim-body\">\n        <div class=\"sim-sidebar\">\n            <h3 class=\"sim-section-title\">1. True Prescription<\/h3>\n            <div class=\"sim-input-group\">\n                <span class=\"sim-label-top\">RIGHT EYE<\/span>\n                <div class=\"sim-flex-row\">\n                    <div class=\"sim-flex-col\"><label>Sphere<\/label><input type=\"number\" id=\"sim-true-r-sph\" step=\"0.25\" value=\"-1.00\"><\/div>\n                    <div class=\"sim-flex-col\"><label>Cylinder<\/label><input type=\"number\" id=\"sim-true-r-cyl\" step=\"0.25\" value=\"-0.25\" max=\"0\"><\/div>\n                <\/div>\n            <\/div>\n            <div class=\"sim-input-group\" style=\"margin-bottom: 1.5rem;\">\n                <span class=\"sim-label-top\">LEFT EYE<\/span>\n                <div class=\"sim-flex-row\">\n                    <div class=\"sim-flex-col\"><label>Sphere<\/label><input type=\"number\" id=\"sim-true-l-sph\" step=\"0.25\" value=\"-1.00\"><\/div>\n                    <div class=\"sim-flex-col\"><label>Cylinder<\/label><input type=\"number\" id=\"sim-true-l-cyl\" step=\"0.25\" value=\"-0.25\" max=\"0\"><\/div>\n                <\/div>\n            <\/div>\n\n            <h3 class=\"sim-section-title\">2. Contact Lenses<\/h3>\n            <div class=\"sim-input-group\">\n                <span class=\"sim-label-top\">RIGHT EYE LENS<\/span>\n                <div class=\"sim-flex-row\">\n                    <div class=\"sim-flex-col\"><label>Sphere<\/label><input type=\"number\" id=\"sim-ct-r-sph\" step=\"0.25\" value=\"-0.75\"><\/div>\n                    <div class=\"sim-flex-col\"><label>ADD<\/label><input type=\"number\" id=\"sim-ct-r-add\" step=\"0.25\" value=\"1.75\" min=\"0\"><\/div>\n                <\/div>\n            <\/div>\n            <div class=\"sim-input-group\">\n                <span class=\"sim-label-top\">LEFT EYE LENS<\/span>\n                <div class=\"sim-flex-row\">\n                    <div class=\"sim-flex-col\"><label>Sphere<\/label><input type=\"number\" id=\"sim-ct-l-sph\" step=\"0.25\" value=\"-0.50\"><\/div>\n                    <div class=\"sim-flex-col\"><label>ADD<\/label><input type=\"number\" id=\"sim-ct-l-add\" step=\"0.25\" value=\"1.75\" min=\"0\"><\/div>\n                <\/div>\n            <\/div>\n            \n            <div class=\"sim-note\">\n                * Low ADD \u2248 +1.25 | Med ADD \u2248 +1.75 | High ADD \u2248 +2.50\n            <\/div>\n        <\/div>\n\n        <div class=\"sim-main\">\n            <div class=\"sim-canvas-container\" id=\"sim-canvas-container\">\n                <canvas id=\"sim-heatmapCanvas\"><\/canvas>\n            <\/div>\n\n            <div class=\"sim-legend\">\n                <div class=\"sim-legend-item\"><div class=\"sim-color-box\" style=\"background-color: #16a34a;\"><\/div> Sharp<\/div>\n                <div class=\"sim-legend-item\"><div class=\"sim-color-box\" style=\"background-color: #84cc16;\"><\/div> Very Good<\/div>\n                <div class=\"sim-legend-item\"><div class=\"sim-color-box\" style=\"background-color: #eab308;\"><\/div> Functional<\/div>\n                <div class=\"sim-legend-item\"><div class=\"sim-color-box\" style=\"background-color: #f97316;\"><\/div> Blurry<\/div>\n                <div class=\"sim-legend-item\"><div class=\"sim-color-box\" style=\"background-color: #ef4444;\"><\/div> Very Blurry<\/div>\n            <\/div>\n        <\/div>\n    <\/div>\n<\/div>\n\n<script>\n    (function() {\n        const canvas = document.getElementById('sim-heatmapCanvas');\n        if (!canvas) return;\n        const ctx = canvas.getContext('2d');\n        const container = document.getElementById('sim-canvas-container');\n\n        const inputs = {\n            tR_sph: document.getElementById('sim-true-r-sph'),\n            tR_cyl: document.getElementById('sim-true-r-cyl'),\n            tL_sph: document.getElementById('sim-true-l-sph'),\n            tL_cyl: document.getElementById('sim-true-l-cyl'),\n            cR_sph: document.getElementById('sim-ct-r-sph'),\n            cR_add: document.getElementById('sim-ct-r-add'),\n            cL_sph: document.getElementById('sim-ct-l-sph'),\n            cL_add: document.getElementById('sim-ct-l-add')\n        };\n\n        const MIN_DIST = 0.2; \n        const MAX_DIST = 6.0; \n        \n        const ROWS = [\n            { id: 'uncorrected', label: 'Uncorrected', desc: 'Notice the razor-sharp peak' },\n            { id: 'glasses', label: 'Glasses', desc: 'Clear everywhere (with head tilt)' },\n            { id: 'contact_r', label: 'Right Contact', desc: 'Dominant Eye' },\n            { id: 'contact_l', label: 'Left Contact', desc: 'Non-Dominant Eye' },\n            { id: 'contact_both', label: 'Both Contacts', desc: 'Brain bridges the gaps' }\n        ];\n\n        function getVal(id) { return parseFloat(inputs[id].value) || 0; }\n\n        function getBlurColor(blur) {\n            blur = Math.max(0, Math.min(blur, 2.5));\n            let hue = 120; \n            if (blur > 0) hue = Math.max(0, 120 - (blur \/ 1.5) * 120); \n            let lightness = 50;\n            if (blur > 1.5) lightness = Math.max(30, 50 - ((blur - 1.5) \/ 1.0) * 20);\n            return `hsl(${hue}, 85%, ${lightness}%)`;\n        }\n\n        function calculateBlur(distance, type) {\n            const dioptersReq = 1 \/ distance;\n            \n            const uncorrR = Math.abs(dioptersReq - Math.abs(getVal('tR_sph'))) + (Math.abs(getVal('tR_cyl')) * 0.5);\n            const uncorrL = Math.abs(dioptersReq - Math.abs(getVal('tL_sph'))) + (Math.abs(getVal('tL_cyl')) * 0.5);\n\n            if (type === 'uncorrected') return Math.min(uncorrR, uncorrL);\n            if (type === 'glasses') return 0.1; \n\n            const shiftR = getVal('cR_sph') - getVal('tR_sph');\n            const shiftL = getVal('cL_sph') - getVal('tL_sph');\n            const addR = Math.abs(getVal('cR_add'));\n            const addL = Math.abs(getVal('cL_add'));\n            \n            const astigPenaltyR = Math.abs(getVal('tR_cyl')) * 0.5;\n            const astigPenaltyL = Math.abs(getVal('tL_cyl')) * 0.5;\n            \n            const edofLightSpreadPenalty = 0.45; \n\n            function getRealisticEDOFLensBlur(shift, add, astigPenalty) {\n                let distBlur = Math.abs(dioptersReq - shift);\n                let nearBlur = Math.abs(dioptersReq - (shift + add));\n                let rawBlur;\n                \n                if (dioptersReq >= shift && dioptersReq <= (shift + add)) {\n                    let totalRange = add;\n                    let currentPos = dioptersReq - shift;\n                    let normalizedPos = currentPos \/ totalRange; \n                    let intermediateDip = Math.sin(normalizedPos * Math.PI) * 0.4;\n                    rawBlur = intermediateDip;\n                } else {\n                    rawBlur = Math.min(distBlur, nearBlur);\n                }\n                return rawBlur + edofLightSpreadPenalty + astigPenalty;\n            }\n\n            const blurR = getRealisticEDOFLensBlur(shiftR, addR, astigPenaltyR);\n            const blurL = getRealisticEDOFLensBlur(shiftL, addL, astigPenaltyL);\n\n            if (type === 'contact_r') return blurR;\n            if (type === 'contact_l') return blurL;\n            if (type === 'contact_both') return Math.max(edofLightSpreadPenalty, Math.min(blurR, blurL) - 0.05); \n\n            return 0;\n        }\n\n        function drawHeatmap() {\n            const rect = container.getBoundingClientRect();\n            canvas.width = rect.width;\n            canvas.height = rect.height;\n\n            const width = canvas.width;\n            const height = canvas.height;\n            const rowHeight = (height - 35) \/ ROWS.length; \n            \n            const labelWidth = width < 500 ? 120 : 180; \n            const graphWidth = width - labelWidth;\n\n            ctx.clearRect(0, 0, width, height);\n\n            for (let r = 0; r < ROWS.length; r++) {\n                const yOffset = r * rowHeight;\n                const rowType = ROWS[r].id;\n\n                for (let x = 0; x < graphWidth; x++) {\n                    const pct = x \/ graphWidth;\n                    const distance = MIN_DIST * Math.pow((MAX_DIST \/ MIN_DIST), pct);\n                    const blur = calculateBlur(distance, rowType);\n                    ctx.fillStyle = getBlurColor(blur);\n                    ctx.fillRect(labelWidth + x, yOffset, 1, rowHeight);\n                }\n\n                ctx.strokeStyle = '#e2e8f0';\n                ctx.lineWidth = 1;\n                ctx.strokeRect(labelWidth, yOffset, graphWidth, rowHeight);\n\n                ctx.fillStyle = rowType === 'contact_both' ? '#f8fafc' : '#f8fafc';\n                ctx.fillRect(0, yOffset, labelWidth, rowHeight);\n                ctx.strokeRect(0, yOffset, labelWidth, rowHeight);\n                \n                ctx.fillStyle = '#0f172a';\n                ctx.font = 'bold 12px sans-serif';\n                ctx.textBaseline = 'middle';\n                \n                if (width < 500) {\n                    ctx.fillText(ROWS[r].label, 8, yOffset + rowHeight \/ 2);\n                } else {\n                    ctx.fillText(ROWS[r].label, 10, yOffset + rowHeight \/ 2 - 6);\n                    ctx.font = 'normal 11px sans-serif';\n                    ctx.fillStyle = '#64748b';\n                    ctx.fillText(ROWS[r].desc, 10, yOffset + rowHeight \/ 2 + 8);\n                }\n            }\n\n            const yAxisOffset = ROWS.length * rowHeight;\n            ctx.fillStyle = '#ffffff';\n            ctx.fillRect(0, yAxisOffset, width, 35);\n\n            const markers = [0.2, 0.4, 1.0, 2.0, 6.0];\n            ctx.fillStyle = '#475569';\n            ctx.font = '11px sans-serif';\n            ctx.textBaseline = 'top';\n            ctx.textAlign = 'center';\n\n            markers.forEach(dist => {\n                const pct = Math.log(dist \/ MIN_DIST) \/ Math.log(MAX_DIST \/ MIN_DIST);\n                const x = labelWidth + pct * graphWidth;\n                \n                ctx.beginPath();\n                ctx.moveTo(x, yAxisOffset);\n                ctx.lineTo(x, yAxisOffset + 5);\n                ctx.strokeStyle = '#94a3b8';\n                ctx.lineWidth = 2;\n                ctx.stroke();\n\n                let label = `${dist}m`;\n                if (dist === 0.4) label = '40cm';\n                if (dist === 1.0) label = '1m';\n                if (dist === 6.0) label = 'Far \u2794';\n                \n                if (dist === 0.2 && width < 500) label = ''; \n                else if (dist === 0.2) label = '20cm';\n\n                if (dist === 6.0) {\n                    ctx.textAlign = 'right';\n                    ctx.fillText(label, width - 8, yAxisOffset + 8);\n                } else {\n                    ctx.fillText(label, x, yAxisOffset + 8);\n                }\n            });\n        }\n\n        Object.values(inputs).forEach(input => {\n            if (input) input.addEventListener('input', drawHeatmap);\n        });\n        \n        window.addEventListener('resize', drawHeatmap);\n        \n        \/\/ Ensure WordPress has finished painting the DOM before drawing canvas\n        setTimeout(drawHeatmap, 150);\n    })();\n<\/script>\n\n\n\n<p>Why do the rows look the way they do? Multi-focal contact lenses trade absolute sharpness for a wider range of vision. When seeing an object in sharp focus without corrective lenses, all of the light entering your eye gets focused at a single sharp point. If you can see an object in perfect focus, the light is converging to a single point on the retina.<\/p>\n\n\n\n<p>With modern contact lenses this same amount of light is focused into multiple points: some might converge early (in front of the retina) or late (would be focused behind the retina). This is why the graph doesn&#8217;t show true green for the contact lenses. It&#8217;s the trade-off.<\/p>\n\n\n\n<p>This next interactive simulator shows what&#8217;s actually happening to the rays of light within the eye, explaining why the focus heatmap above has dips in between areas of very good focus.<\/p>\n\n\n\n<!-- Interactive Optical Focus Simulator -->\n<style>\n    \/* Strictly scoped CSS to prevent WordPress theme conflicts *\/\n    .ray-sim-wrapper {\n        font-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n        max-width: 800px;\n        margin: 2rem auto;\n        background-color: #0f172a;\n        border: 1px solid #334155;\n        border-radius: 12px;\n        box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);\n        overflow: hidden;\n        color: #f8fafc;\n        line-height: 1.5;\n        display: flex;\n        flex-direction: column;\n    }\n    .ray-sim-wrapper * {\n        box-sizing: border-box;\n    }\n    @media (min-width: 768px) {\n        .ray-sim-wrapper {\n            flex-direction: row;\n        }\n    }\n    .ray-sim-controls {\n        width: 100%;\n        background-color: #1e293b;\n        padding: 1.5rem;\n        border-right: 1px solid #334155;\n        display: flex;\n        flex-direction: column;\n        gap: 1.5rem;\n    }\n    @media (min-width: 768px) {\n        .ray-sim-controls {\n            width: 320px;\n            flex-shrink: 0;\n        }\n    }\n    .ray-sim-title {\n        font-size: 1.25rem;\n        font-weight: 700;\n        color: #22d3ee;\n        margin: 0 0 0.5rem 0;\n        line-height: 1.2;\n        border: none;\n        padding: 0;\n    }\n    .ray-sim-desc {\n        font-size: 0.85rem;\n        color: #94a3b8;\n        margin: 0;\n    }\n    .ray-sim-group {\n        display: flex;\n        flex-direction: column;\n        gap: 0.75rem;\n    }\n    .ray-sim-label {\n        font-size: 0.75rem;\n        font-weight: 700;\n        color: #cbd5e1;\n        text-transform: uppercase;\n        letter-spacing: 0.05em;\n    }\n    .ray-sim-radio-label {\n        display: flex;\n        align-items: center;\n        gap: 0.5rem;\n        font-size: 0.9rem;\n        color: #f8fafc;\n        cursor: pointer;\n        padding: 0.5rem;\n        border-radius: 6px;\n        border: 1px solid #334155;\n        background-color: #0f172a;\n        transition: border-color 0.2s;\n    }\n    .ray-sim-radio-label:hover {\n        border-color: #475569;\n    }\n    .ray-sim-radio-label input[type=\"radio\"] {\n        accent-color: #22d3ee;\n        width: 16px;\n        height: 16px;\n        margin: 0;\n    }\n    .ray-sim-slider-container {\n        display: flex;\n        flex-direction: column;\n        gap: 0.5rem;\n    }\n    .ray-sim-slider-labels {\n        display: flex;\n        justify-content: space-between;\n        font-size: 0.8rem;\n        color: #94a3b8;\n        font-weight: 600;\n    }\n    .ray-sim-slider {\n        -webkit-appearance: none;\n        width: 100%;\n        height: 6px;\n        background: #334155;\n        border-radius: 3px;\n        outline: none;\n    }\n    .ray-sim-slider::-webkit-slider-thumb {\n        -webkit-appearance: none;\n        appearance: none;\n        width: 18px;\n        height: 18px;\n        border-radius: 50%;\n        background: #22d3ee;\n        cursor: pointer;\n    }\n    .ray-sim-slider::-moz-range-thumb {\n        width: 18px;\n        height: 18px;\n        border-radius: 50%;\n        background: #22d3ee;\n        cursor: pointer;\n        border: none;\n    }\n    .ray-sim-canvas-area {\n        flex-grow: 1;\n        position: relative;\n        min-height: 350px;\n        background-color: #0f172a;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        padding: 1rem;\n    }\n    .ray-sim-canvas-area canvas {\n        width: 100%;\n        max-width: 500px;\n        height: auto;\n        border-radius: 8px;\n    }\n<\/style>\n\n<div class=\"ray-sim-wrapper\">\n    <!-- Controls (Left) -->\n    <div class=\"ray-sim-controls\">\n        <div>\n            <h2 class=\"ray-sim-title\">Optical Focus Simulator<\/h2>\n            <p class=\"ray-sim-desc\">Observe how light splits in a multifocal lens. The rays only glow cyan when they form a perfectly sharp point on the retina.<\/p>\n        <\/div>\n\n        <div class=\"ray-sim-group\">\n            <span class=\"ray-sim-label\">Target Distance<\/span>\n            <div class=\"ray-sim-slider-container\">\n                <input type=\"range\" id=\"ray-dist-slider\" class=\"ray-sim-slider\" min=\"0\" max=\"100\" value=\"100\">\n                <div class=\"ray-sim-slider-labels\">\n                    <span>Near (Reading)<\/span>\n                    <span>Far (Driving)<\/span>\n                <\/div>\n            <\/div>\n        <\/div>\n\n        <div class=\"ray-sim-group\">\n            <span class=\"ray-sim-label\">Lens Type<\/span>\n            <label class=\"ray-sim-radio-label\">\n                <input type=\"radio\" name=\"ray-lens\" value=\"uncorrected\"> Uncorrected (Myopia)\n            <\/label>\n            <label class=\"ray-sim-radio-label\">\n                <input type=\"radio\" name=\"ray-lens\" value=\"glasses\"> Single Vision (Glasses)\n            <\/label>\n            <label class=\"ray-sim-radio-label\" style=\"border-color: #22d3ee; background-color: #164e63;\">\n                <input type=\"radio\" name=\"ray-lens\" value=\"aspherical\" checked> Multifocal Contacts\n            <\/label>\n        <\/div>\n    <\/div>\n\n    <!-- Visualisation (Right) -->\n    <div class=\"ray-sim-canvas-area\">\n        <canvas id=\"ray-canvas\" width=\"500\" height=\"350\"><\/canvas>\n    <\/div>\n<\/div>\n\n<script>\n    (function() {\n        const canvas = document.getElementById('ray-canvas');\n        if (!canvas) return;\n        const ctx = canvas.getContext('2d');\n        \n        const slider = document.getElementById('ray-dist-slider');\n        const radios = document.querySelectorAll('input[name=\"ray-lens\"]');\n\n        \/\/ Geometry Constants\n        const eyeX = 160;\n        const eyeY = 175;\n        const eyeRadius = 110;\n        const retinaX = eyeX - eyeRadius; \/\/ 50\n        const lensX = 270; \/\/ Physical position of the lens\n        \n        \/\/ Ray Configuration: Y offsets from the optical axis\n        const rayOffsets = [-75, -50, -25, 0, 25, 50, 75];\n        \n        \/\/ Focal bounds (Where the rays cross the optical axis relative to retina)\n        const focalInFront = 120; \/\/ Myopic blur (in front of retina)\n        const focalRetina = 50;   \/\/ Perfect focus (on retina)\n        const focalBehind = -20;  \/\/ Presbyopic blur (behind retina)\n\n        \/\/ Interpolates colour based on distance from the retina.\n        \/\/ Returns Cyan (#22d3ee) at 0 error, Orange (#f97316) at high error.\n        function getRayColour(focalX) {\n            const error = Math.abs(focalX - retinaX);\n            const maxError = 70; \n            const t = Math.min(error \/ maxError, 1);\n            \n            const r = Math.round(34 + (249 - 34) * t);\n            const g = Math.round(211 + (115 - 211) * t);\n            const b = Math.round(238 + (22 - 238) * t);\n            \n            return `rgb(${r}, ${g}, ${b})`;\n        }\n\n        function draw() {\n            \/\/ d goes from 0 (Near) to 1 (Far)\n            const d = parseInt(slider.value, 10) \/ 100;\n            const lensType = document.querySelector('input[name=\"ray-lens\"]:checked').value;\n\n            \/\/ Clear Canvas\n            ctx.fillStyle = '#0f172a';\n            ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n            \/\/ Draw Optical Axis\n            ctx.beginPath();\n            ctx.setLineDash([5, 5]);\n            ctx.moveTo(0, eyeY);\n            ctx.lineTo(500, eyeY);\n            ctx.strokeStyle = '#334155';\n            ctx.lineWidth = 1;\n            ctx.stroke();\n            ctx.setLineDash([]);\n\n            \/\/ Label Retina & Cornea\n            ctx.fillStyle = '#64748b';\n            ctx.font = '12px sans-serif';\n            ctx.textAlign = 'center';\n            ctx.fillText(\"RETINA\", retinaX, eyeY + 130);\n            ctx.fillText(\"CORNEA\", lensX, eyeY + 130);\n\n            \/\/ Draw Lens Element (if corrected)\n            if (lensType !== 'uncorrected') {\n                ctx.beginPath();\n                ctx.ellipse(lensX, eyeY, 8, 85, 0, 0, Math.PI * 2);\n                ctx.fillStyle = 'rgba(34, 211, 238, 0.15)';\n                ctx.fill();\n                ctx.strokeStyle = '#22d3ee';\n                ctx.lineWidth = 1;\n                ctx.stroke();\n            }\n\n            \/\/ Draw Incoming Horizontal Rays (Right to Left)\n            ctx.globalCompositeOperation = 'source-over';\n            rayOffsets.forEach(offset => {\n                const y = eyeY + offset;\n                \n                \/\/ Determine mathematical focal point to inherit correct colour before bending\n                let focalX;\n                const isCentral = Math.abs(offset) <= 25;\n\n                if (lensType === 'uncorrected') {\n                    focalX = focalRetina + d * (focalInFront - focalRetina);\n                } else if (lensType === 'glasses') {\n                    focalX = focalBehind + d * (focalRetina - focalBehind);\n                } else if (lensType === 'aspherical') {\n                    focalX = isCentral \n                        ? focalRetina + d * (focalInFront - focalRetina) \n                        : focalBehind + d * (focalRetina - focalBehind);\n                }\n\n                ctx.beginPath();\n                ctx.moveTo(500, y);\n                ctx.lineTo(lensX, y); \/\/ Draw exactly to the lens\n                ctx.strokeStyle = getRayColour(focalX);\n                ctx.lineWidth = 2;\n                ctx.globalAlpha = 0.5; \/\/ Fade incoming rays for visual hierarchy\n                ctx.stroke();\n                ctx.globalAlpha = 1.0;\n            });\n\n            \/\/ Fill Eye Background \n            ctx.beginPath();\n            ctx.arc(eyeX, eyeY, eyeRadius, 0, Math.PI * 2);\n            ctx.fillStyle = '#1e293b';\n            ctx.fill();\n\n            \/\/ Set up Custom Clipping Mask for Converging Rays\n            \/\/ Clips at the back of the retina, but stays wide open at the front of the eye\n            ctx.save();\n            ctx.beginPath();\n            ctx.moveTo(eyeX, eyeY + eyeRadius);\n            \/\/ Draw the left half of the eye (the retina curve)\n            ctx.arc(eyeX, eyeY, eyeRadius, Math.PI\/2, -Math.PI\/2, false); \n            \/\/ Box out the right side so lines aren't chopped between the lens and the eye\n            ctx.lineTo(lensX + 20, eyeY - eyeRadius);\n            ctx.lineTo(lensX + 20, eyeY + eyeRadius);\n            ctx.closePath();\n            ctx.clip();\n\n            \/\/ Blend mode for intense glowing crossover points\n            ctx.globalCompositeOperation = 'screen';\n\n            \/\/ Draw Converging Rays\n            rayOffsets.forEach(offset => {\n                const y = eyeY + offset;\n                let focalX;\n                const isCentral = Math.abs(offset) <= 25;\n\n                if (lensType === 'uncorrected') {\n                    focalX = focalRetina + d * (focalInFront - focalRetina);\n                } else if (lensType === 'glasses') {\n                    focalX = focalBehind + d * (focalRetina - focalBehind);\n                } else if (lensType === 'aspherical') {\n                    focalX = isCentral \n                        ? focalRetina + d * (focalInFront - focalRetina) \n                        : focalBehind + d * (focalRetina - focalBehind);\n                }\n\n                const slope = (eyeY - y) \/ (focalX - lensX);\n                \n                \/\/ Draw past the retina so the custom clip path masks it perfectly\n                const xEnd = -50; \n                const yEnd = y + slope * (xEnd - lensX);\n\n                ctx.beginPath();\n                ctx.moveTo(lensX, y); \/\/ Start continuously from the lens\n                ctx.lineTo(xEnd, yEnd);\n                \n                ctx.strokeStyle = getRayColour(focalX);\n                ctx.lineWidth = 2.5;\n                ctx.stroke();\n            });\n\n            ctx.restore(); \/\/ Remove clipping mask\n\n            \/\/ Draw Eye Outline &#038; Retina Screen Highlight\n            ctx.globalCompositeOperation = 'source-over';\n            ctx.beginPath();\n            ctx.arc(eyeX, eyeY, eyeRadius, 0, Math.PI * 2);\n            ctx.strokeStyle = '#475569';\n            ctx.lineWidth = 2;\n            ctx.stroke();\n\n            \/\/ Thicker red line for the Retina back wall\n            ctx.beginPath();\n            ctx.arc(eyeX, eyeY, eyeRadius, Math.PI * 0.7, Math.PI * 1.3);\n            ctx.strokeStyle = '#ef4444';\n            ctx.lineWidth = 4;\n            ctx.stroke();\n        }\n\n        \/\/ Attach event listeners\n        slider.addEventListener('input', draw);\n        radios.forEach(radio => radio.addEventListener('change', () => {\n            radios.forEach(r => {\n                r.parentElement.style.borderColor = '#334155';\n                r.parentElement.style.backgroundColor = '#0f172a';\n            });\n            const checked = document.querySelector('input[name=\"ray-lens\"]:checked');\n            checked.parentElement.style.borderColor = '#22d3ee';\n            checked.parentElement.style.backgroundColor = '#164e63';\n            draw();\n        }));\n\n        \/\/ Initial render\n        setTimeout(draw, 100);\n    })();\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;ve been trying out contact lenses for the first time. Multi-focal lenses provide different focal lengths to the eye at once, and you can have different prescription lenses in each eye (as long as they don&#8217;t differ by too much). This means the brain is getting signals from the eyes, each providing potentially multiple focal [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[9],"tags":[],"class_list":["post-1812","post","type-post","status-publish","format-standard","hentry","category-thoughts"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.4 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Visualising vision correction - PRINT HEAD<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/\" \/>\n<meta property=\"og:locale\" content=\"en_GB\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Visualising vision correction - PRINT HEAD\" \/>\n<meta property=\"og:description\" content=\"I&#8217;ve been trying out contact lenses for the first time. Multi-focal lenses provide different focal lengths to the eye at once, and you can have different prescription lenses in each eye (as long as they don&#8217;t differ by too much). This means the brain is getting signals from the eyes, each providing potentially multiple focal [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/\" \/>\n<meta property=\"og:site_name\" content=\"PRINT HEAD\" \/>\n<meta property=\"article:published_time\" content=\"2026-03-28T20:23:37+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-03-30T16:23:50+00:00\" \/>\n<meta name=\"author\" content=\"Tim Waugh\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Tim Waugh\" \/>\n\t<meta name=\"twitter:label2\" content=\"Estimated reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"2 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/2026\\\/03\\\/28\\\/visualising-vision-correction\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/2026\\\/03\\\/28\\\/visualising-vision-correction\\\/\"},\"author\":{\"name\":\"Tim Waugh\",\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/#\\\/schema\\\/person\\\/23b749f30a67f1b1c6af17024fc94bf6\"},\"headline\":\"Visualising vision correction\",\"datePublished\":\"2026-03-28T20:23:37+00:00\",\"dateModified\":\"2026-03-30T16:23:50+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/2026\\\/03\\\/28\\\/visualising-vision-correction\\\/\"},\"wordCount\":352,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/#\\\/schema\\\/person\\\/23b749f30a67f1b1c6af17024fc94bf6\"},\"articleSection\":[\"Thoughts\"],\"inLanguage\":\"en-GB\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/cyberelk.net\\\/tim\\\/2026\\\/03\\\/28\\\/visualising-vision-correction\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/2026\\\/03\\\/28\\\/visualising-vision-correction\\\/\",\"url\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/2026\\\/03\\\/28\\\/visualising-vision-correction\\\/\",\"name\":\"Visualising vision correction - PRINT HEAD\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/#website\"},\"datePublished\":\"2026-03-28T20:23:37+00:00\",\"dateModified\":\"2026-03-30T16:23:50+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/2026\\\/03\\\/28\\\/visualising-vision-correction\\\/#breadcrumb\"},\"inLanguage\":\"en-GB\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/cyberelk.net\\\/tim\\\/2026\\\/03\\\/28\\\/visualising-vision-correction\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/2026\\\/03\\\/28\\\/visualising-vision-correction\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Visualising vision correction\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/#website\",\"url\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/\",\"name\":\"PRINT HEAD\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/#\\\/schema\\\/person\\\/23b749f30a67f1b1c6af17024fc94bf6\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-GB\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/#\\\/schema\\\/person\\\/23b749f30a67f1b1c6af17024fc94bf6\",\"name\":\"Tim Waugh\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-GB\",\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/wp-content\\\/uploads\\\/2023\\\/01\\\/printhead.png\",\"url\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/wp-content\\\/uploads\\\/2023\\\/01\\\/printhead.png\",\"contentUrl\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/wp-content\\\/uploads\\\/2023\\\/01\\\/printhead.png\",\"width\":731,\"height\":140,\"caption\":\"Tim Waugh\"},\"logo\":{\"@id\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/wp-content\\\/uploads\\\/2023\\\/01\\\/printhead.png\"},\"sameAs\":[\"http:\\\/\\\/cyberelk.net\\\/tim\"],\"url\":\"https:\\\/\\\/cyberelk.net\\\/tim\\\/author\\\/twaugh\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Visualising vision correction - PRINT HEAD","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/","og_locale":"en_GB","og_type":"article","og_title":"Visualising vision correction - PRINT HEAD","og_description":"I&#8217;ve been trying out contact lenses for the first time. Multi-focal lenses provide different focal lengths to the eye at once, and you can have different prescription lenses in each eye (as long as they don&#8217;t differ by too much). This means the brain is getting signals from the eyes, each providing potentially multiple focal [&hellip;]","og_url":"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/","og_site_name":"PRINT HEAD","article_published_time":"2026-03-28T20:23:37+00:00","article_modified_time":"2026-03-30T16:23:50+00:00","author":"Tim Waugh","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Tim Waugh","Estimated reading time":"2 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/#article","isPartOf":{"@id":"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/"},"author":{"name":"Tim Waugh","@id":"https:\/\/cyberelk.net\/tim\/#\/schema\/person\/23b749f30a67f1b1c6af17024fc94bf6"},"headline":"Visualising vision correction","datePublished":"2026-03-28T20:23:37+00:00","dateModified":"2026-03-30T16:23:50+00:00","mainEntityOfPage":{"@id":"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/"},"wordCount":352,"commentCount":0,"publisher":{"@id":"https:\/\/cyberelk.net\/tim\/#\/schema\/person\/23b749f30a67f1b1c6af17024fc94bf6"},"articleSection":["Thoughts"],"inLanguage":"en-GB","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/","url":"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/","name":"Visualising vision correction - PRINT HEAD","isPartOf":{"@id":"https:\/\/cyberelk.net\/tim\/#website"},"datePublished":"2026-03-28T20:23:37+00:00","dateModified":"2026-03-30T16:23:50+00:00","breadcrumb":{"@id":"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/#breadcrumb"},"inLanguage":"en-GB","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/cyberelk.net\/tim\/2026\/03\/28\/visualising-vision-correction\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cyberelk.net\/tim\/"},{"@type":"ListItem","position":2,"name":"Visualising vision correction"}]},{"@type":"WebSite","@id":"https:\/\/cyberelk.net\/tim\/#website","url":"https:\/\/cyberelk.net\/tim\/","name":"PRINT HEAD","description":"","publisher":{"@id":"https:\/\/cyberelk.net\/tim\/#\/schema\/person\/23b749f30a67f1b1c6af17024fc94bf6"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cyberelk.net\/tim\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-GB"},{"@type":["Person","Organization"],"@id":"https:\/\/cyberelk.net\/tim\/#\/schema\/person\/23b749f30a67f1b1c6af17024fc94bf6","name":"Tim Waugh","image":{"@type":"ImageObject","inLanguage":"en-GB","@id":"https:\/\/cyberelk.net\/tim\/wp-content\/uploads\/2023\/01\/printhead.png","url":"https:\/\/cyberelk.net\/tim\/wp-content\/uploads\/2023\/01\/printhead.png","contentUrl":"https:\/\/cyberelk.net\/tim\/wp-content\/uploads\/2023\/01\/printhead.png","width":731,"height":140,"caption":"Tim Waugh"},"logo":{"@id":"https:\/\/cyberelk.net\/tim\/wp-content\/uploads\/2023\/01\/printhead.png"},"sameAs":["http:\/\/cyberelk.net\/tim"],"url":"https:\/\/cyberelk.net\/tim\/author\/twaugh\/"}]}},"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/pnnS2-te","_links":{"self":[{"href":"https:\/\/cyberelk.net\/tim\/wp-json\/wp\/v2\/posts\/1812","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cyberelk.net\/tim\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cyberelk.net\/tim\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cyberelk.net\/tim\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/cyberelk.net\/tim\/wp-json\/wp\/v2\/comments?post=1812"}],"version-history":[{"count":6,"href":"https:\/\/cyberelk.net\/tim\/wp-json\/wp\/v2\/posts\/1812\/revisions"}],"predecessor-version":[{"id":1821,"href":"https:\/\/cyberelk.net\/tim\/wp-json\/wp\/v2\/posts\/1812\/revisions\/1821"}],"wp:attachment":[{"href":"https:\/\/cyberelk.net\/tim\/wp-json\/wp\/v2\/media?parent=1812"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cyberelk.net\/tim\/wp-json\/wp\/v2\/categories?post=1812"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cyberelk.net\/tim\/wp-json\/wp\/v2\/tags?post=1812"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}