Skip to content

Instantly share code, notes, and snippets.

@Dan-Q
Created October 31, 2024 15:43
Show Gist options
  • Save Dan-Q/363887e3cc2407b1c862e159e00f27a2 to your computer and use it in GitHub Desktop.
Save Dan-Q/363887e3cc2407b1c862e159e00f27a2 to your computer and use it in GitHub Desktop.
A graphical implementation of Hero's algorithm for derivation of square roots. https://danq.me/2024/10/31/hero-of-alexandria/
/**
* A graphical implementation of Hero's algorithm for derivation of square roots.
* Assumes existence of a <div id="p246620-hero"></div>
*
* Author: Dan Q <https://danq.me/>
* Explanation: https://danq.me/2024/10/31/hero-of-alexandria/
* License: Public Domain / CC0 / The Unlicense (your choice)
*/
(function(){
const p24662hero = document.getElementById('p24662-hero');
p24662hero.innerHTML = `
<p style="display: flex; gap: 5px; align-items: stretch;">
<label for="p24662hero-in" style="font-weight: bold; align-self: center;">Calculate: √</label>
<input id="p24662hero-in" type="number" maxlength="3" min="1" max="999">
<button id="p24662hero-go">Start</button>
</p>
<canvas id="p24662hero-graph" width="1000" height="500" style="width: 100%; background: #c5eded; border: 1px solid #333; margin: 0 0 14px;"></canvas>
<output id="p24662hero-out" style="font-family: monospace; font-size: 12px; background: gainsboro; padding: 8px 4px; border: 1px solid #333; min-height: 196px;">Ready to calculate!</output>
`;
p24662hero.style.display = 'flex';
p24662hero.style.flexDirection = 'column';
const decimalPlaceAccuracy = 4;
const graphXPositionStepSize = 95;
const p24662heroGraph = p24662hero.querySelector('#p24662hero-graph');
const p24662heroOut = p24662hero.querySelector('#p24662hero-out');
const p24662heroGraphContext = p24662heroGraph.getContext('2d');
let calculateRootOf;
let calculatorTick;
let lastGuess;
let graphTop;
let guesses = [];
/**
* Round the specified number to the requested number of decimal places.
*/
function roundTo(number, decimalPlaces){
const decimalPlacesMultiplier = 10 ** decimalPlaces;
return Math.round( number * decimalPlacesMultiplier ) / decimalPlacesMultiplier;
}
function drawIndicatorLine(lineValue) {
const yPosition = 460 - ( (lineValue / graphTop) * 460 );
p24662heroGraphContext.setLineDash([3, 3]); // line 3, gap 3
p24662heroGraphContext.strokeStyle = '#95b8b8';
p24662heroGraphContext.beginPath();
p24662heroGraphContext.moveTo(40, yPosition);
p24662heroGraphContext.lineTo(1000, yPosition);
p24662heroGraphContext.stroke();
// text for final line
p24662heroGraphContext.font = '15px monospace';
p24662heroGraphContext.textAlign = 'right';
p24662heroGraphContext.fillText(Math.round(lineValue), 35, yPosition + 5);
}
function drawGraph(finalLine = null){
p24662heroGraphContext.clearRect(0, 0, p24662heroGraph.width, p24662heroGraph.height);
p24662heroGraphContext.lineWidth = 1;
p24662heroGraphContext.strokeStyle = '#555';
// y-axis
p24662heroGraphContext.beginPath();
p24662heroGraphContext.moveTo(40, 0);
p24662heroGraphContext.lineTo(40, 500);
p24662heroGraphContext.stroke();
// x-axis
p24662heroGraphContext.beginPath();
p24662heroGraphContext.moveTo(0, 460);
p24662heroGraphContext.lineTo(1000, 460);
p24662heroGraphContext.stroke();
// first line
drawIndicatorLine(guesses[0]);
// second line if applicable
if((guesses.length > 1) && (calculateRootOf > 3)) drawIndicatorLine(guesses[1]);
// final line, if finished
if(finalLine) drawIndicatorLine(finalLine);
// line
p24662heroGraphContext.lineWidth = 2;
p24662heroGraphContext.fillStyle = '#062d4d';
p24662heroGraphContext.strokeStyle = '#062d4d';
p24662heroGraphContext.setLineDash([]); // undashed line
let xPosition = 40;
let lastYPosition;
for(guess of guesses){
const yPosition = 460 - ( (guess / graphTop) * 460 );
p24662heroGraphContext.beginPath();
p24662heroGraphContext.arc(xPosition, yPosition, 6, 0, 2 * Math.PI);
p24662heroGraphContext.fill();
if(lastYPosition) {
p24662heroGraphContext.beginPath();
p24662heroGraphContext.moveTo(xPosition - graphXPositionStepSize, lastYPosition);
p24662heroGraphContext.lineTo(xPosition, yPosition);
p24662heroGraphContext.stroke();
}
lastYPosition = yPosition;
xPosition += graphXPositionStepSize;
}
}
function calculateStep(){
const newGuess = ( lastGuess + calculateRootOf / lastGuess ) / 2;
const roundedNewGuess = roundTo( newGuess, decimalPlaceAccuracy + 1);
guesses.push( roundedNewGuess );
const roundingOccurred = newGuess != roundedNewGuess;
p24662heroOut.innerText += `Guessing ${lastGuess} gives a next guess of ( ${lastGuess} + ${calculateRootOf} / ${lastGuess} ) / 2 ${roundingOccurred ? '≈' : '='} ${roundedNewGuess}.\n`;
if( roundedNewGuess == lastGuess ) {
// solved!
setTimeout(function(){
p24662heroOut.innerText += `${lastGuess} = ${roundedNewGuess}, so that's the approximate answer (accurate as far as ${roundTo(lastGuess, decimalPlaceAccuracy)}).\n`;
}, 400);
setTimeout(function(){
p24662heroOut.innerText += `(${roundedNewGuess}² = ${roundedNewGuess ** 2})\n`;
}, 800);
clearInterval(calculatorTick);
drawGraph(roundedNewGuess);
return;
}
drawGraph();
lastGuess = roundedNewGuess;
}
document.getElementById('p24662hero-go').addEventListener('click', function(){
calculateRootOf = p24662hero.querySelector('#p24662hero-in').value;
clearInterval(calculatorTick);
graphTop = Math.ceil( calculateRootOf / 2 ) + 12;
lastGuess = 1;
guesses = [ lastGuess ];
drawGraph();
calculatorTick = setInterval(calculateStep, 400);
p24662heroOut.innerText = `Estimating square root of ${calculateRootOf} accurate to ${decimalPlaceAccuracy} decimal places.\n`;
});
drawGraph();
// Start with a demonstrative query:
p24662hero.querySelector('#p24662hero-in').value = 9;
document.getElementById('p24662hero-go').click();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment