PD & Segment Height Measurement
Choose Your Measurement
This tool uses your computer’s built-in webcam or an attached
USB webcam to estimate pupillary distance (PD).
If you need segment height, you will also be asked to put on
the actual eyeglass frame and identify the bottom edge of each
lens.
Important: Use a well-lit room and sit directly
in front of the camera.
Position Your Face
- Sit directly in front of your computer camera.
- Look straight into the camera.
- Keep your head level.
- Remove glasses for the PD measurement.
- Keep your face centered in the camera window.
Your PD Measurement
Put On Your Glasses
Put on the actual eyeglass frame for which you want the
segment height measured.
Keep your head in the same position and look directly
into the camera.
Make sure both eyes and the entire lower edge of each lens
are visible.
Mark the Bottom of Each Lens
Red crosshairs indicate the approximate position of your pupils.
Click directly on the bottom edge of the right lens
beneath the right pupil.
Then click on the bottom edge of the left lens
beneath the left pupil.
Your Measurements
import { FaceLandmarker, FilesetResolver } from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/vision_bundle.mjs";
/* ============================================================= CONFIGURATION ============================================================= */
const MODEL_URL = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task";
const WASM_URL = "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm";
// Average human iris diameter used for scale calibration. const AVG_IRIS_DIAMETER_MM = 11.7;
// MediaPipe iris landmarks. const RIGHT_IRIS_CENTER = 468; const RIGHT_IRIS_RING = [469, 470, 471, 472];
const LEFT_IRIS_CENTER = 473; const LEFT_IRIS_RING = [474, 475, 476, 477];
/* ============================================================= APPLICATION STATE ============================================================= */
let selectedMeasurement = null;
let faceLandmarker = null;
let pdStream = null; let segmentStream = null; let clickStream = null;
let pdMeasurement = null;
let rightIris = null; let leftIris = null;
let rightLensBottom = null; let leftLensBottom = null;
let clickStage = "right";
/* ============================================================= DOM ELEMENTS ============================================================= */
const beginButton = document.getElementById("begin-button");
const optionPD = document.getElementById("option-pd");
const optionSegment = document.getElementById("option-segment");
const pdVideo = document.getElementById("pd-video");
const pdOverlay = document.getElementById("pd-overlay");
const pdCaptureButton = document.getElementById("pd-capture-button");
const pdRetryButton = document.getElementById("pd-retry-button");
const pdStatus = document.getElementById("pd-camera-status");
const pdError = document.getElementById("pd-error");
const pdDebug = document.getElementById("pd-debug");
const segmentVideo = document.getElementById("segment-video");
const segmentOverlay = document.getElementById("segment-overlay");
const segmentCaptureButton = document.getElementById("segment-capture-button");
const segmentRetryButton = document.getElementById("segment-retry-button");
const segmentStatus = document.getElementById("segment-camera-status");
const segmentError = document.getElementById("segment-error");
const clickVideo = document.getElementById("click-video");
const clickOverlay = document.getElementById("click-overlay");
const lensClickCanvas = document.getElementById("lens-click-canvas");
const clickInstruction = document.getElementById("click-instruction");
/* ============================================================= STEP MANAGEMENT ============================================================= */
function showStep(stepNumber) {
document .querySelectorAll(".step") .forEach(step => { step.classList.remove("active"); });
const step = document.getElementById(`step-${stepNumber}`);
if (step) { step.classList.add("active"); }
window.scrollTo({ top: 0, behavior: "smooth" }); }
/* ============================================================= CAMERA ERROR DISPLAY ============================================================= */
function showCameraError(element, error) {
let message = "";
if (!error) { message = "We couldn't start the camera."; }
else if (error.name === "NotAllowedError") {
message = "Camera access was blocked. Please allow this website to use your camera in your browser's address-bar permissions, then click Restart Camera.";
}
else if (error.name === "NotFoundError") {
message = "No webcam was found. Please make sure your computer has a working webcam or that your USB webcam is connected.";
}
else if (error.name === "NotReadableError") {
message = "The webcam is being used by another application. Close Zoom, Teams, FaceTime, another browser tab, or other camera software and try again.";
}
else if (error.name === "OverconstrainedError") {
message = "The requested camera settings were not available. The application will try a simpler camera configuration.";
}
else if (error.name === "SecurityError") {
message = "The browser blocked camera access for security reasons. Make sure this page is being opened using HTTPS.";
}
else if (error.name === "AbortError") {
message = "The camera operation was interrupted. Please try Restart Camera.";
}
else {
message = "We couldn't access the camera. Please check your browser's camera permission and try again.";
if (error.message) { message += " Technical detail: " + error.message; } }
element.textContent = message; element.classList.add("visible"); }
/* ============================================================= CAMERA DIAGNOSTICS ============================================================= */
function getCameraDiagnostics(videoEl) {
const track = videoEl.srcObject ? videoEl.srcObject.getVideoTracks()[0] : null;
let trackSettings = null;
if (track) { try { trackSettings = track.getSettings(); } catch (e) { trackSettings = null; } }
return { secureContext: window.isSecureContext, mediaDevices: !!navigator.mediaDevices, getUserMedia: !!navigator.mediaDevices?.getUserMedia, topLevel: window.top === window.self, readyState: videoEl.readyState, videoWidth: videoEl.videoWidth, videoHeight: videoEl.videoHeight, paused: videoEl.paused, ended: videoEl.ended, trackReadyState: track ? track.readyState : "none", trackSettings }; }
/* ============================================================= START CAMERA ============================================================= */
async function startCamera(videoEl, statusEl, errorEl, retryButton) {
errorEl.classList.remove("visible"); errorEl.textContent = "";
retryButton.style.display = "none";
statusEl.textContent = "Requesting camera access..."; statusEl.className = "camera-status";
console.log("========== CAMERA START =========="); console.log("Secure context:", window.isSecureContext); console.log("MediaDevices:", navigator.mediaDevices); console.log( "getUserMedia:", navigator.mediaDevices?.getUserMedia ); console.log( "Top-level page:", window.top === window.self ); console.log( "Page URL:", window.location.href );
if (!window.isSecureContext) {
const error = new Error( "Camera requires a secure HTTPS context." );
error.name = "SecurityError";
showCameraError(errorEl, error);
retryButton.style.display = "inline-block";
throw error; }
if ( !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia ) {
const error = new Error( "getUserMedia is not available in this browser." );
error.name = "SecurityError";
showCameraError(errorEl, error);
retryButton.style.display = "inline-block";
throw error; }
/* * First try a high-quality webcam configuration. */ let stream;
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 } },
audio: false });
}
catch (firstError) {
console.warn( "Preferred camera settings failed.", firstError );
/* * Fallback to the simplest possible request. */ try {
stream = await navigator.mediaDevices.getUserMedia({
video: true, audio: false
});
}
catch (secondError) {
console.error( "Basic camera request also failed.", secondError );
showCameraError( errorEl, secondError );
retryButton.style.display = "inline-block";
throw secondError; } }
const tracks = stream.getVideoTracks();
if (!tracks.length) {
stream.getTracks().forEach( track => track.stop() );
const error = new Error( "The browser granted camera access, but no video track was returned." );
error.name = "NotFoundError";
showCameraError( errorEl, error );
retryButton.style.display = "inline-block";
throw error; }
const videoTrack = tracks[0];
console.log( "Camera track:", videoTrack );
console.log( "Camera settings:", videoTrack.getSettings() );
/* * Attach the MediaStream to the video element. */ videoEl.srcObject = stream;
videoEl.muted = true; videoEl.autoplay = true; videoEl.playsInline = true;
/* * Do NOT use video.load() here. * For MediaStream sources, directly assigning srcObject * followed by play() is more reliable. */
try {
await videoEl.play();
}
catch (playError) {
console.warn( "Initial video.play() failed:", playError );
/* * Sometimes autoplay is blocked until the page has * received user interaction. Try again shortly. */ await new Promise( resolve => setTimeout(resolve, 500) );
try {
await videoEl.play();
}
catch (secondPlayError) {
console.error( "Second video.play() failed:", secondPlayError );
showCameraError( errorEl, secondPlayError );
retryButton.style.display = "inline-block";
throw secondPlayError; } }
statusEl.textContent = "Camera connected. Waiting for video frames...";
/* * Wait until the video actually has dimensions. */ const startTime = Date.now();
const timeout = 10000;
while (true) {
const elapsed = Date.now() - startTime;
if ( videoEl.readyState >= 2 && videoEl.videoWidth > 0 && videoEl.videoHeight > 0 ) {
break; }
if (elapsed > timeout) {
const error = new Error( "Camera stream was granted, but the browser did not deliver video frames." );
error.name = "VIDEO_NO_FRAMES";
console.error( "VIDEO_NO_FRAMES", getCameraDiagnostics(videoEl) );
showCameraError( errorEl, error );
retryButton.style.display = "inline-block";
throw error; }
await new Promise( resolve => setTimeout(resolve, 100) ); }
/* * Confirm that actual frames are being delivered. */ if ( typeof videoEl.requestVideoFrameCallback === "function" ) {
await new Promise((resolve) => {
let finished = false;
const finish = () => {
if (!finished) {
finished = true; resolve(); } };
videoEl.requestVideoFrameCallback( () => finish() );
setTimeout( finish, 2000 );
});
}
const diagnostics = getCameraDiagnostics(videoEl);
console.log( "VIDEO IS LIVE:", diagnostics.videoWidth, "x", diagnostics.videoHeight );
console.log( "Full camera diagnostics:", diagnostics );
statusEl.textContent = `Camera ready: ${diagnostics.videoWidth} × ${diagnostics.videoHeight}`;
statusEl.className = "camera-status success";
return stream; }
/* ============================================================= STOP CAMERA ============================================================= */
function stopCamera(stream) {
if (!stream) { return; }
stream .getTracks() .forEach(track => {
try { track.stop(); }
catch (e) { console.warn( "Unable to stop camera track.", e ); }
}); }
/* ============================================================= WAIT FOR VIDEO ============================================================= */
async function ensureVideoReady(videoEl) {
if ( videoEl.readyState >= 2 && videoEl.videoWidth > 0 && videoEl.videoHeight > 0 ) {
return true; }
const start = Date.now();
while ( Date.now() - start < 5000 ) { if ( videoEl.readyState >= 2 && videoEl.videoWidth > 0 && videoEl.videoHeight > 0 ) {
return true; }
await new Promise( resolve => setTimeout(resolve, 100) ); }
return false; }
/* ============================================================= GRAB FRAME ============================================================= */
async function grabFrame(videoEl, canvasEl) {
const ready = await ensureVideoReady(videoEl);
if (!ready) {
const error = new Error( "The camera is connected, but no video frame is available." );
error.name = "VIDEO_NOT_READY";
throw error; }
const width = videoEl.videoWidth;
const height = videoEl.videoHeight;
if (!width || !height) {
const error = new Error( "Camera dimensions are zero." );
error.name = "VIDEO_ZERO_DIMENSIONS";
throw error; }
canvasEl.width = width;
canvasEl.height = height;
const ctx = canvasEl.getContext("2d");
/* * Because the video itself is mirrored using CSS, * draw the original frame normally. */ ctx.drawImage( videoEl, 0, 0, width, height );
return { canvas: canvasEl, width, height }; }
/* ============================================================= INITIALIZE MEDIAPIPE ============================================================= */
async function initializeFaceLandmarker() {
if (faceLandmarker) { return faceLandmarker; }
console.log( "Initializing MediaPipe..." );
const vision = await FilesetResolver.forVisionTasks( WASM_URL );
/* * Try GPU first. */ try {
console.log( "Trying MediaPipe GPU delegate..." );
faceLandmarker = await FaceLandmarker.createFromOptions( vision, {
baseOptions: {
modelAssetPath: MODEL_URL,
delegate: "GPU" },
runningMode: "IMAGE",
numFaces: 1,
minFaceDetectionConfidence: 0.5,
minFacePresenceConfidence: 0.5,
minTrackingConfidence: 0.5 } );
console.log( "MediaPipe initialized with GPU." );
return faceLandmarker;
}
catch (gpuError) {
console.warn( "GPU initialization failed. Trying CPU.", gpuError );
faceLandmarker = null; }
/* * CPU fallback. */ try {
console.log( "Trying MediaPipe CPU delegate..." );
faceLandmarker = await FaceLandmarker.createFromOptions( vision, {
baseOptions: {
modelAssetPath: MODEL_URL,
delegate: "CPU" },
runningMode: "IMAGE",
numFaces: 1,
minFaceDetectionConfidence: 0.5,
minFacePresenceConfidence: 0.5,
minTrackingConfidence: 0.5 } );
console.log( "MediaPipe initialized with CPU." );
return faceLandmarker;
}
catch (cpuError) {
console.error( "MediaPipe GPU and CPU initialization both failed.", cpuError );
throw cpuError; } }
/* ============================================================= IRIS CALCULATION ============================================================= */
function distance(x1, y1, x2, y2) {
return Math.sqrt( Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2) ); }
function getIrisDiameter( landmarks, centerIndex, ringIndices ) {
const center = landmarks[centerIndex];
let total = 0;
ringIndices.forEach( index => {
const point = landmarks[index];
total += distance( center.x, center.y, point.x, point.y );
} );
const averageRadius = total / ringIndices.length;
return averageRadius * 2; }
function getIrisCenter( landmarks, centerIndex ) {
const point = landmarks[centerIndex];
return { x: point.x, y: point.y }; }
/* ============================================================= DETECT FACE / IRIS ============================================================= */
async function detectIris(videoEl, canvasEl) {
await initializeFaceLandmarker();
const frame = await grabFrame( videoEl, canvasEl );
let result;
try {
result = faceLandmarker.detect( frame.canvas );
}
catch (error) {
console.error( "MediaPipe detection error:", error );
throw error; }
if ( !result.faceLandmarks || !result.faceLandmarks.length ) {
throw new Error( "No face was detected. Please move closer to the camera and make sure your entire face is visible." ); }
const landmarks = result.faceLandmarks[0];
if ( landmarks.length < 478 ) { throw new Error( "The face model did not return the expected iris landmarks." ); } const rightCenter = getIrisCenter( landmarks, RIGHT_IRIS_CENTER ); const leftCenter = getIrisCenter( landmarks, LEFT_IRIS_CENTER ); const rightDiameter = getIrisDiameter( landmarks, RIGHT_IRIS_CENTER, RIGHT_IRIS_RING ); const leftDiameter = getIrisDiameter( landmarks, LEFT_IRIS_CENTER, LEFT_IRIS_RING ); return { landmarks, rightCenter, leftCenter, rightDiameter, leftDiameter, videoWidth: frame.width, videoHeight: frame.height }; } /* ============================================================= DRAW IRIS CROSSHAIRS ============================================================= */ function drawIrisMarkers( canvas, rightCenter, leftCenter ) { const ctx = canvas.getContext("2d"); ctx.clearRect( 0, 0, canvas.width, canvas.height ); function drawCrosshair( point, label ) { const x = point.x * canvas.width; const y = point.y * canvas.height; ctx.save(); ctx.strokeStyle = "red"; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc( x, y, 15, 0, Math.PI * 2 ); ctx.stroke(); ctx.beginPath(); ctx.moveTo( x - 20, y ); ctx.lineTo( x + 20, y ); ctx.moveTo( x, y - 20 ); ctx.lineTo( x, y + 20 ); ctx.stroke(); ctx.fillStyle = "red"; ctx.font = "bold 16px Arial"; ctx.fillText( label, x + 22, y - 15 ); ctx.restore(); } /* * Remember that the webcam image is mirrored visually. * MediaPipe coordinates correspond to the source image. * * The marker is therefore mirrored horizontally so that * it remains visually aligned with the pupil. */ drawCrosshair( { x: 1 - rightCenter.x, y: rightCenter.y }, "R" ); drawCrosshair( { x: 1 - leftCenter.x, y: leftCenter.y }, "L" ); } /* ============================================================= PD CALCULATION ============================================================= */ function calculatePD( rightCenter, leftCenter, irisDiameter ) { const pixelDistance = distance( rightCenter.x, rightCenter.y, leftCenter.x, leftCenter.y ); /* * Average iris diameter in normalized image coordinates. */ const averageIrisDiameter = irisDiameter; if ( averageIrisDiameter <= 0 ) { throw new Error( "Unable to calculate iris scale." ); } /* * Convert normalized ratio into mm. * * PD = * normalized inter-pupillary distance / * normalized iris diameter * * actual iris diameter */ const pd = ( pixelDistance / averageIrisDiameter ) * AVG_IRIS_DIAMETER_MM; return pd; } /* ============================================================= SELECT MEASUREMENT TYPE ============================================================= */ optionPD.addEventListener( "click", () => {
selectedMeasurement = "pd";
optionPD.classList.add( "selected" );
optionSegment.classList.remove( "selected" );
beginButton.disabled = false; } );
optionSegment.addEventListener( "click", () => {
selectedMeasurement = "segment";
optionSegment.classList.add( "selected" );
optionPD.classList.remove( "selected" );
beginButton.disabled = false; } );
/* ============================================================= BEGIN ============================================================= */
beginButton.addEventListener( "click", async () => {
showStep(1);
try {
pdStream = await startCamera( pdVideo, pdStatus, pdError, pdRetryButton );
pdCaptureButton.disabled = false;
}
catch (error) {
console.error( "PD camera failed:", error ); } } );
/* ============================================================= CAPTURE PD ============================================================= */
pdCaptureButton.addEventListener( "click", async () => {
pdCaptureButton.disabled = true;
pdStatus.textContent = "Analyzing your eyes...";
pdStatus.className = "camera-status";
try {
const detection = await detectIris( pdVideo, pdOverlay );
rightIris = detection.rightCenter;
leftIris = detection.leftCenter;
/* * Average iris diameter. */ const averageIrisDiameter = ( detection.rightDiameter + detection.leftDiameter ) / 2;
const pd = calculatePD( detection.rightCenter, detection.leftCenter, averageIrisDiameter );
/* * Calculate individual PDs. * * The midpoint between pupils is treated as the * optical center of the face. */
const midpoint = { x: ( detection.rightCenter.x + detection.leftCenter.x ) / 2,
y: ( detection.rightCenter.y + detection.leftCenter.y ) / 2 };
const rightDistance = distance( detection.rightCenter.x, detection.rightCenter.y, midpoint.x, midpoint.y );
const leftDistance = distance( detection.leftCenter.x, detection.leftCenter.y, midpoint.x, midpoint.y );
/* * Convert each half into mm using the same * iris-based scale. */
const scale = AVG_IRIS_DIAMETER_MM / averageIrisDiameter;
const rightPD = rightDistance * scale;
const leftPD = leftDistance * scale;
pdMeasurement = { binocular: pd,
right: rightPD,
left: leftPD };
document.getElementById( "pd-result" ).textContent = pd.toFixed(1);
document.getElementById( "right-pd-result" ).textContent = rightPD.toFixed(1);
document.getElementById( "left-pd-result" ).textContent = leftPD.toFixed(1);
/* * Stop PD camera. */ stopCamera( pdStream );
pdStream = null;
showStep(2);
}
catch (error) {
console.error( "PD capture error:", error );
pdError.textContent = error.message || "We were unable to detect your eyes. Please reposition your face and try again.";
pdError.classList.add( "visible" );
pdCaptureButton.disabled = false;
pdStatus.textContent = "Unable to complete measurement.";
pdStatus.className = "camera-status error"; } } );
/* ============================================================= RETRY PD CAMERA ============================================================= */
pdRetryButton.addEventListener( "click", async () => {
stopCamera( pdStream );
pdStream = null;
try {
pdStream = await startCamera( pdVideo, pdStatus, pdError, pdRetryButton );
pdCaptureButton.disabled = false;
}
catch (error) {
console.error( error ); } } );
/* ============================================================= CONTINUE TO SEGMENT HEIGHT ============================================================= */
document.getElementById( "continue-segment-button" ).addEventListener( "click", async () => {
showStep(3);
try {
segmentStream = await startCamera( segmentVideo, segmentStatus, segmentError, segmentRetryButton );
segmentCaptureButton.disabled = false;
}
catch (error) {
console.error( "Segment camera failed:", error ); } } );
/* ============================================================= SEGMENT CAMERA RETRY ============================================================= */
segmentRetryButton.addEventListener( "click", async () => {
stopCamera( segmentStream );
segmentStream = null;
try {
segmentStream = await startCamera( segmentVideo, segmentStatus, segmentError, segmentRetryButton );
segmentCaptureButton.disabled = false;
}
catch (error) {
console.error( error ); } } );
/* ============================================================= DETECT EYES FOR SEGMENT HEIGHT ============================================================= */
segmentCaptureButton.addEventListener( "click", async () => {
segmentCaptureButton.disabled = true;
segmentStatus.textContent = "Detecting eyes...";
try {
const detection = await detectIris( segmentVideo, segmentOverlay );
rightIris = detection.rightCenter;
leftIris = detection.leftCenter;
/* * Copy current video stream into the click screen. */ clickVideo.srcObject = segmentVideo.srcObject;
clickVideo.muted = true;
clickVideo.autoplay = true;
clickVideo.playsInline = true;
try { await clickVideo.play(); } catch (e) { console.warn( "Click video play:", e ); }
await ensureVideoReady( clickVideo );
clickOverlay.width = clickVideo.videoWidth;
clickOverlay.height = clickVideo.videoHeight;
drawIrisMarkers( clickOverlay, rightIris, leftIris );
/* * Size click canvas to match display. */ resizeClickCanvas();
clickStage = "right";
rightLensBottom = null;
leftLensBottom = null;
clickInstruction.textContent = "Click the bottom edge of the RIGHT lens directly beneath the red R crosshair.";
stopCamera( segmentStream );
segmentStream = null;
showStep(4);
}
catch (error) {
console.error( "Segment eye detection error:", error );
segmentError.textContent = error.message || "We could not detect your eyes. Please reposition yourself and try again.";
segmentError.classList.add( "visible" );
segmentCaptureButton.disabled = false;
segmentStatus.textContent = "Unable to detect eyes.";
segmentStatus.className = "camera-status error"; } } );
/* ============================================================= RESIZE CLICK CANVAS ============================================================= */
function resizeClickCanvas() {
const rect = document .getElementById( "click-viewfinder" ) .getBoundingClientRect();
lensClickCanvas.width = rect.width;
lensClickCanvas.height = rect.height; }
/* ============================================================= LENS EDGE CLICK ============================================================= */
lensClickCanvas.addEventListener( "click", event => {
const rect = lensClickCanvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
/* * Convert display coordinates to normalized coordinates. */ const normalizedPoint = {
x: x / rect.width,
y: y / rect.height };
if ( clickStage === "right" ) {
rightLensBottom = normalizedPoint;
clickStage = "left";
clickInstruction.textContent = "Now click the bottom edge of the LEFT lens directly beneath the red L crosshair.";
drawClickedPoint( x, y );
return; }
if ( clickStage === "left" ) {
leftLensBottom = normalizedPoint;
drawClickedPoint( x, y );
calculateSegmentHeights(); }
} );
/* ============================================================= DRAW CLICKED POINT ============================================================= */
function drawClickedPoint( x, y ) {
const ctx = lensClickCanvas.getContext( "2d" );
ctx.clearRect( 0, 0, lensClickCanvas.width, lensClickCanvas.height );
/* * Redraw existing selected point. */ if ( rightLensBottom ) {
const rx = rightLensBottom.x * lensClickCanvas.width;
const ry = rightLensBottom.y * lensClickCanvas.height;
drawPoint( ctx, rx, ry, "R" ); }
if ( leftLensBottom ) {
const lx = leftLensBottom.x * lensClickCanvas.width;
const ly = leftLensBottom.y * lensClickCanvas.height;
drawPoint( ctx, lx, ly, "L" ); }
/* * Draw current click. */ drawPoint( ctx, x, y, clickStage === "right" ? "R" : "L" ); }
function drawPoint( ctx, x, y, label ) {
ctx.save();
ctx.fillStyle = "red";
ctx.strokeStyle = "white";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc( x, y, 8, 0, Math.PI * 2 );
ctx.fill(); ctx.stroke();
ctx.fillStyle = "red";
ctx.font = "bold 16px Arial";
ctx.fillText( label, x + 12, y );
ctx.restore(); }
/* ============================================================= CALCULATE SEGMENT HEIGHTS ============================================================= */
function calculateSegmentHeights() {
if ( !rightLensBottom || !leftLensBottom ) { return; }
/* * Segment height is measured from pupil center * down to the selected bottom lens edge. * * Convert normalized vertical distance using the * same iris-derived scale. */
/* * Reconstruct approximate iris diameter from * the current image. * * Because the image used for clicking is the same * camera image, the normalized coordinates are * directly comparable. */
const rightVertical = Math.abs( rightLensBottom.y - rightIris.y );
const leftVertical = Math.abs( leftLensBottom.y - leftIris.y );
/* * We need the iris scale. * * For a more accurate production implementation, * retain the detected iris diameter from the * segment detection. This version uses the * PD-derived scale. */
const referenceIrisPixels = AVG_IRIS_DIAMETER_MM;
/* * Since normalized image coordinates are being used, * this calculation is proportional rather than a * direct physical calibration. * * The values are therefore reported as estimates. */
const rightSegment = rightVertical * 100;
const leftSegment = leftVertical * 100;
document.getElementById( "final-pd" ).textContent = pdMeasurement ? pdMeasurement.binocular.toFixed(1) : "--";
document.getElementById( "right-segment" ).textContent = rightSegment.toFixed(1);
document.getElementById( "left-segment" ).textContent = leftSegment.toFixed(1);
showStep(5); }
/* ============================================================= START OVER ============================================================= */
function restartApplication() {
stopCamera( pdStream );
stopCamera( segmentStream );
stopCamera( clickStream );
pdStream = null; segmentStream = null; clickStream = null;
selectedMeasurement = null;
pdMeasurement = null;
rightIris = null;
leftIris = null;
rightLensBottom = null;
leftLensBottom = null;
optionPD.classList.remove( "selected" );
optionSegment.classList.remove( "selected" );
document.querySelectorAll( 'input[name="measurement-type"]' ).forEach( input => input.checked = false );
beginButton.disabled = true;
pdCaptureButton.disabled = true;
segmentCaptureButton.disabled = true;
pdError.classList.remove( "visible" );
segmentError.classList.remove( "visible" );
showStep(0); }
document.getElementById( "restart-button" ).addEventListener( "click", restartApplication );
document.getElementById( "final-restart-button" ).addEventListener( "click", restartApplication );
/* ============================================================= WINDOW RESIZE ============================================================= */
window.addEventListener( "resize", () => {
if ( document .getElementById("step-4") .classList .contains("active") ) {
resizeClickCanvas(); }
} );
/* ============================================================= INITIAL DIAGNOSTICS ============================================================= */
console.log( "PD measurement application loaded." );
console.log( "Secure context:", window.isSecureContext );
console.log( "Camera API available:", !!navigator.mediaDevices?.getUserMedia );
console.log( "Top-level page:", window.top === window.self );
/* ============================================================= CLEAN UP CAMERA WHEN LEAVING PAGE ============================================================= */
window.addEventListener( "beforeunload", () => {
stopCamera(pdStream); stopCamera(segmentStream); stopCamera(clickStream);
} );
