A Visual Acuity Chart Generator in JavaScript

The JavaScript code below, when run, will generate a random visual acuity chart. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

This code is just a prototype. The math on the chart may not be quite be correct at the moment. If in doubt, it might be better to consult a licensed optometrist.


<html>
<body>

<!-- ui -->

<div>
	<label>Pixels per Inch:</label>
	<input id="inputPixelsPerInch" type="number" value="150"></input>
</div>

<div>
	<label>Chart Size in Inches:</label>
	<input id="inputChartSizeInInchesX" type="number" value="8.5"></input>
	<label>x</label>
	<input id="inputChartSizeInInchesY" type="number" value="11"></input>
</div>

<div>
	<label>Size of Top Line in Inches:</label>
	<input id="inputTopLineSizeInInches" type="number" value="1"></input>
</div>

<div>
	<label>Number of Lines:</label>
	<input id="inputNumberOfLines" type="number" value="8"></input>
</div>

<button onclick="buttonGenerate_Clicked();">Generate</button>

<div id="divOutput"></div>

<!-- ui ends -->

<script type="text/javascript">

// ui event handlers

function buttonGenerate_Clicked()
{
	var inputPixelsPerInch = 
		document.getElementById("inputPixelsPerInch");
	var pixelsPerInchAsString = inputPixelsPerInch.value;
	var pixelsPerInch = parseInt(pixelsPerInchAsString);

	var inputChartSizeInInchesX = 
		document.getElementById("inputChartSizeInInchesX");
	var inputChartSizeInInchesY = 
		document.getElementById("inputChartSizeInInchesY");

	var chartSizeInInches = new Coords
	(
		parseFloat(inputChartSizeInInchesX.value),
		parseFloat(inputChartSizeInInchesY.value)
	);
	
	var inputTopLineSizeInInches = 
		document.getElementById("inputTopLineSizeInInches");
	var topLineSizeInInchesAsString = 
		inputTopLineSizeInInches.value;
	var topLineSizeInInches = 
		parseFloat(topLineSizeInInchesAsString);

	var inputNumberOfLines = 
		document.getElementById("inputNumberOfLines");
	var numberOfLinesAsString = 
		inputNumberOfLines.value;
	var numberOfLines = 
		parseInt(numberOfLinesAsString);

	var visionChart = new VisionChart
	(
		pixelsPerInch, 
		chartSizeInInches,
		topLineSizeInInches,
		numberOfLines
	);

	var visionChartAsCanvas = visionChart.toCanvas();
	
	var divOutput = document.getElementById("divOutput");
	divOutput.innerHTML = "";
	divOutput.appendChild(visionChartAsCanvas);
}

// classes

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		return this;
	}

	Coords.prototype.right = function()
	{
		var temp = this.y;
		this.y = this.x;
		this.x = 0 - temp;
		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}

}

function VisionChart
(
	pixelsPerInch, 
	chartSizeInInches, 
	topLineSizeInInches, 
	numberOfLines
)
{
	this.pixelsPerInch = pixelsPerInch;
	this.chartSizeInInches = chartSizeInInches;
	this.topLineSizeInInches = topLineSizeInInches;

	this.chartSizeInPixels = 
		this.chartSizeInInches.clone().multiplyScalar
		(
			pixelsPerInch
		);

	this.topLineSizeInPixels = 
		this.topLineSizeInInches * pixelsPerInch;

	var radiansPerTurn = Math.PI * 2;
	var degreesPerTurn = 360;
	var minutesPerDegree = 60;
	var angleSubtendedBySmallestSymbolInMinutes = 5;
	var angleSubtendedBySmallestSymbolInRadians = 
		radiansPerTurn 
		* angleSubtendedBySmallestSymbolInMinutes 
		/ (degreesPerTurn * minutesPerDegree);

	this.distanceFromEyeInInches = 
		this.topLineSizeInInches 
		/ numberOfLines
		/ Math.sin
		(
			angleSubtendedBySmallestSymbolInRadians 			
		);

	this.numberOfLines = numberOfLines;
}
{
	VisionChart.prototype.toCanvas = function()
	{
		var returnValue = document.createElement("canvas");
		returnValue.style = "border:1px solid";
		returnValue.width = this.chartSizeInPixels.x;
		returnValue.height = this.chartSizeInPixels.y;

		var graphics = returnValue.getContext("2d");

		graphics.fillStyle = "White";
		graphics.fillRect
		(
			0, 0, 
			this.chartSizeInPixels.x, 
			this.chartSizeInPixels.y
		);

		graphics.fillStyle = "Black";		

		var inchesPerFoot = 12;
		var message = 
			"This chart should be placed " 
			+ Math.floor(this.distanceFromEyeInInches / inchesPerFoot)
			+ " feet + "
			+ Math.floor(this.distanceFromEyeInInches % inchesPerFoot)
			+ " inches from the eye.";

		var fontHeightInPixels = 10;
		graphics.font = fontHeightInPixels + "px sans-serif";
		graphics.fillText(message, 0, fontHeightInPixels);

		var drawPos = new Coords(0, this.topLineSizeInPixels);

		var directions = 
		[
			new Coords(1, 0),
			new Coords(0, 1),
			new Coords(-1, 0),
			new Coords(0, -1),
		];

		var symbolSizeInPixelsOfBottomLine = 
			this.topLineSizeInPixels / this.numberOfLines

		for (var i = 0; i < this.numberOfLines; i++)
		{
			var numberOfSymbolsOnLine = i + 1;

			var symbolSizeInPixels = 
				this.topLineSizeInPixels 
				/ numberOfSymbolsOnLine;

			var widthOfLineInPixels = 
				symbolSizeInPixels 
				* (numberOfSymbolsOnLine * 2 - 2);

			drawPos.x = 
				(this.chartSizeInPixels.x - widthOfLineInPixels) 
				/ 2;

			for (var s = 0; s < numberOfSymbolsOnLine; s++)
			{
				var directionIndex = Math.floor
				(
					Math.random() * directions.length
				);

				var direction = directions[directionIndex];

				this.drawSymbol(graphics, symbolSizeInPixels, drawPos, direction);

				drawPos.x += symbolSizeInPixels * 2;	
			}

			var acuityQuotient = 
				symbolSizeInPixels / symbolSizeInPixelsOfBottomLine;
			var acuityNumerator = 20;
			var acuityDenominator = Math.round(acuityQuotient * acuityNumerator);

			graphics.fillText
			(
				acuityNumerator + "/" + acuityDenominator, 
				this.chartSizeInPixels.x - this.topLineSizeInPixels,
				drawPos.y
			)

			drawPos.y += 
				Math.sqrt(symbolSizeInPixels) 
				* this.topLineSizeInPixels 
				/ this.numberOfLines; // hack
		}		
	
		return returnValue;
	}

	VisionChart.prototype.drawSymbol = function(graphics, symbolSizeInPixels, drawPosOriginal, direction)
	{
		var rightOne = direction.clone().multiplyScalar(symbolSizeInPixels);
		var rightOneHalf = rightOne.clone().multiplyScalar(.5);
		var rightFourFifths = rightOne.clone().multiplyScalar(.8);
		var rightThreeFifths = rightOne.clone().multiplyScalar(.6);
		var downOne = rightOne.clone().right();
		var downOneHalf = downOne.clone().multiplyScalar(.5);
		var downOneFifth = downOne.clone().multiplyScalar(.2);

		var drawPos = drawPosOriginal.clone().subtract
		(
			rightOneHalf
		).subtract
		(
			downOneHalf
		);

		graphics.beginPath();
	
		// draw an E-shaped symbol

		// top limb 
		graphics.moveTo(drawPos.x, drawPos.y);
		drawPos.add(rightOne);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.add(downOneFifth);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.subtract(rightFourFifths);
		graphics.lineTo(drawPos.x, drawPos.y);

		// middle limb
		drawPos.add(downOneFifth);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.add(rightThreeFifths);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.add(downOneFifth);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.subtract(rightThreeFifths);
		graphics.lineTo(drawPos.x, drawPos.y);

		// bottom limb
		drawPos.add(downOneFifth);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.add(rightFourFifths);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.add(downOneFifth);
		graphics.lineTo(drawPos.x, drawPos.y);
		drawPos.subtract(rightOne);
		graphics.lineTo(drawPos.x, drawPos.y);

		graphics.closePath();
		graphics.fill();
	}
}


</script>

</body>
</html>

Advertisements
This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s