xxxxxxxxxx
247
// MIT License
// Copyright (c) 2025 Greg Stanton
// Background:
// https://en.wikipedia.org/wiki/Tower_of_Hanoi
/*
* USAGE
*
* Change ringCount to see the solution with a different number of rings!
* Change ringSpeed to make rings move faster or slower.
* Change sourcePegIndex to start the rings on a different peg.
* Change targetPegIndex to move all the rings to a different peg.
*
* Color, sizes, shapes, and other configurable settings are listed below.
*
*/
// -------- SETTINGS --------
// colors
let myGreen = 'rgb(229, 255, 234)'
let myOrange = 'rgb(255, 157, 91)';
let myMagenta = 'rgb(232, 51, 232)';
let myGray = 150;
// ring settings (rings are drawn as rectangles with rounded corners)
let ringCount = 5;
let ringSpeed = 3; // px per frame
let minRingWidth = 20;
let maxRingWidth = 40;
let ringHeight = 8;
let ringCornerRadius = 5;
let ringClearance = 10;
let ringFill = myMagenta;
let ringStroke = 0;
// peg settings
let pegWidth = 8;
let pegHeight = 115;
let pegRadius = 8;
let pegFill = myOrange;
let pegStroke = myGray;
// ground settings
let groundHeight = 25;
let groundFill = myGray;
// puzzle settings
let sourcePegIndex = 0;
let targetPegIndex = 2;
// label settings
let moveCountLabelX;
let moveCountLabelY;
// play and replay settings
let iconX;
let iconY;
let iconWidth;
let iconHeight;
let playDelay = 1000; // ms (lets viewer understand setup before moving rings)
let timeOfLastPlay = 0; // ms (time = 0 corresponds to start of sketch)
let playClicked = false;
// objects
let rings = [];
let pegs = [];
let ground;
let puzzle;
let playIcon;
let replayIcon;
// -------- PRELOAD AND SETUP --------
function preload() {
playIcon = loadImage('play-icon.png');
replayIcon = loadImage('replay-icon.png');
}
function setup() {
let pegX, pegY;
let ringX, ringY;
let deltaRingWidth;
createCanvas(200, 200);
ground = createGround(groundHeight);
// create pegs
for (let i = 0; i < 3; i++) {
pegX = (i + 1) * width / 4;
pegY = height - pegHeight / 2;
pegs.push(
createPeg(
{
ground: ground,
position: createVector(pegX, pegY),
width: pegWidth,
height: pegHeight,
radius: pegRadius
}
)
);
}
// create rings
for (let i = 0; i < ringCount; i++) {
ringX = pegs[0].position.x;
ringY = height - groundHeight - ringHeight * (0.5 + i);
deltaRingWidth = ringCount > 1 ?
0.5 * (maxRingWidth - minRingWidth) / (ringCount - 1) :
0;
rings.push(
createRing({
position: createVector(ringX, ringY),
width: maxRingWidth - 2 * i * deltaRingWidth,
height: ringHeight,
radius: ringCornerRadius,
clearance: ringClearance,
speed: ringSpeed
})
);
}
// create puzzle
puzzle = createPuzzle(pegs, rings, sourcePegIndex, targetPegIndex);
// initialize move-count label
// note:
// bottom-left corner set so that initial label is centered;
// centering manually like this, instead of using `textAlign(CENTER)`,
// prevents jitter when label width changes due to count updates
textSize(20);
moveCountLabelX = (width - textWidth('move count: 0')) / 2;
moveCountLabelY = 40;
// set icon positions and dimensions
iconX = width / 2;
iconY = height / 2;
iconWidth = width / 2;
iconHeight = iconWidth;
}
// -------- DRAW LOOP (ANIMATION) --------
function draw() {
background(myGreen);
// pegs
fill(pegFill);
stroke(pegStroke);
for (let i = 0; i < 3; i++) {
pegs[i].show();
}
// rings
fill(ringFill);
stroke(ringStroke);
for (let i = 0; i < ringCount; i++) {
rings[i].show();
}
// ground
fill(groundFill);
noStroke();
ground.show();
// move count label
fill(0);
text(`move count: ${puzzle.moveCount}`, moveCountLabelX, moveCountLabelY);
if (!playClicked) {
// gray out canvas
background(myGray, 50);
// show play icon
icon(playIcon);
// update cursor to indicate interactivity
updateCursor();
}
// updates
if (playClicked && timeSinceLastPlay() > playDelay) {
if (puzzle.moveCount < puzzle.solution.length) {
puzzle.update();
}
else {
// gray out canvas
background(myGray, 50);
// show replay icon
icon(replayIcon);
// update cursor to indicate interactivity
updateCursor();
}
}
}
// -------- INTERACTION --------
function icon(img) {
imageMode(CENTER);
image(
img,
iconX,
iconY,
iconWidth,
iconHeight
);
}
function mouseIsOverCanvas() {
return 0 < mouseX && mouseX < width &&
0 < mouseY && mouseY < height;
}
function updateCursor() {
if (mouseIsOverCanvas()) {
cursor(HAND);
}
else {
cursor(ARROW);
}
}
function timeSinceLastPlay() {
return millis() - timeOfLastPlay;
}
function mouseClicked() {
if (mouseIsOverCanvas()) {
puzzle.reset();
cursor(ARROW);
timeOfLastPlay = millis();
playClicked = true;
}
}