summaryrefslogtreecommitdiff
path: root/quickdraw-game.html
diff options
context:
space:
mode:
authorsimon-ec <simon@eustaceconsulting.com>2026-07-28 10:41:43 -0400
committersimon-ec <simon@eustaceconsulting.com>2026-07-28 10:41:43 -0400
commit6d3d7a0de86abf5e6a40297b052cabd8f358ba3e (patch)
tree98d07b4e4654cebb95ab627e53999eaa34c3aca7 /quickdraw-game.html
parent366815d1504bea64387364f70227ab37ea00caf8 (diff)
countdown works. normal mode feels better/faster. game-over buttons
Diffstat (limited to 'quickdraw-game.html')
-rw-r--r--quickdraw-game.html433
1 files changed, 318 insertions, 115 deletions
diff --git a/quickdraw-game.html b/quickdraw-game.html
index 4742d7d..a2f92c1 100644
--- a/quickdraw-game.html
+++ b/quickdraw-game.html
@@ -67,11 +67,21 @@
font-size: 2rem;
}
+ #title-container {
+ justify-self: center;
+ justify-content: center;
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ gap: 2rem;
+ }
+
#text-box {
display: flex;
- flex-direction: row;
+ flex-direction: column;
align-content: end;
justify-content: space-evenly;
+ gap: 1rem;
text-align: center;
color: red;
width: 100%;
@@ -104,23 +114,26 @@
<div id="text-box">
<!-- intro related -->
- <div style="display: flex; gap: 1rem;">
- <span id="intro1" class="hidden title">QUICK</span>
- <span id="intro2" class="hidden title">DRAW</span>
+ <div id="title-container">
+ <span id="title1" class="hidden title">QUICK</span>
+ <span id="title2" class="hidden title">DRAW</span>
</div>
<!-- gamemode menu buttons-->
<div id="game-modes" class="hidden fade">
<button id="normal" onclick="handleNormalMode()">normal</button>
- <button id="countdown">countdown</button>
+ <button id="countdown" onclick="handleCountdownMode()">countdown</button>
<!-- <button id="infinite">infinite</button> -->
</div>
<!-- pre-round text -->
- <span id="gameText" class="hidden fade"></span>
+ <span id="game-text" class="hidden fade"></span>
- <!-- post round text -->
- <span id="round-end" class="hidden fade"></span>
+ <!-- post game buttons -->
+ <div id="game-over-buttons" class="hidden fade">
+ <button id="play-again" onclick="handlePlayAgain()">play again</button>
+ <button id="reset-game" onclick="handleReset()">back to main menu</button>
+ </div>
<!-- countdown text -->
<span id="ready" class="hidden title">READY?</span>
@@ -132,6 +145,7 @@
</div>
<script>
+ // SCREEN / SETUP VARIABLES
let gameBorderWidth = 1;
let targetWidth = 50;
let targetHeight = 50;
@@ -150,38 +164,77 @@
let xFrameAdjustment;
let yFrameAdjustment;
+ // UX HELPERS
+ let drawingCircle = false;
+ let needsResizeAdjustment = false;
+
+ // GAME STATE VARIABLES
+ let gamemode = ""; // can be: "normal", "countdown", "infinite"
+ let countingDown = false;
let hovering = false;
-
let hoverCount = 0; // number of times the cursor has hovered over bullseye. used to cleanly handle round starting / cancelling
-
- let targetsClickedThisRound = 0;
- let gamemode = ""; // can be: "normal", "countdown", "infinite"
- let roundmode = ""; // if gamemode is "countdown" or "infinite", can be: "rounds" or "frenzy"
+ let gameIsOver = false;
- let currentRound = 0;
- let countingDown = false;
-
- let totalTime = 0.0;
+ // GAMEPLAY VARIABLES
+ let targetsClicked = 0;
+
+ // "NORMAL" MODE ONLY VARIABLES
+ let currentRound = 0;
+ let totalTime = 0.0;
+ let roundSeconds = 0.0;
let roundTimeStart = 0;
- let roundTimeEnd = 0;
- let roundSeconds = 0.0;
+ let roundTimeEnd = 0;
+ let bestNormalTime = 0;
- let bestNormalTime = "none";
+ // "COUNTDOWN" MODE ONLY VARIABLES
+ let countdownTime = 0;
+ let bestCountdownScore = 0;
- // text elements
- let ready = document.getElementById("ready");
- let intro1 = document.getElementById("intro1");
- let intro2 = document.getElementById("intro2");
- let gameText = document.getElementById("gameText");
+
+ // menu / text elements
+ let titleContainer = document.getElementById("title-container");
+ let title1 = document.getElementById("title1");
+ let title2 = document.getElementById("title2");
+ let gameModes = document.getElementById("game-modes");
+ let gameText = document.getElementById("game-text");
+ let gameOverButtons = document.getElementById("game-over-buttons");
+
+ let ready = document.getElementById("ready");
let one = document.getElementById("countdown1");
let two = document.getElementById("countdown2");
let three = document.getElementById("countdown3");
-
let draw = document.getElementById("draw");
+ // setup initialization
window.addEventListener("load", initGame);
+ async function initGame() {
+ // determine screen parameters + listen for changes
+ setParameters();
+ window.addEventListener("resize", handlePageResize);
+
+ center = generateTarget(xFrameAdjustment, yFrameAdjustment);
+ center.id = "center";
+ document.body.insertAdjacentElement("afterbegin", center);
+
+ await drawTargetsInCircle(2);
+ if (needsResizeAdjustment) {
+ // auto-reset screen if page was resized during animation
+ handlePageResize();
+ }
+
+ await fadeMainMenuIn();
+
+ if (localStorage.getItem("bestNormalTime") != null) {
+ bestNormalTime = Number.parseFloat(localStorage.getItem("bestNormalTime"));
+ }
+ if (localStorage.getItem("bestCountdownScore") != null) {
+ bestCountdownScore = Number.parseInt(localStorage.getItem("bestCountdownScore"));
+ }
+ }
+
+ // helper function to determine screen-size + ratios for rendering targets
function setParameters(event) {
pageHeight = window.innerHeight;
pageWidth = window.innerWidth;
@@ -203,45 +256,25 @@
console.log('frame: ' + frameWidth + ' x ' + frameHeight);
}
- async function initGame() {
- setParameters();
-
- center = generateTarget(xFrameAdjustment, yFrameAdjustment);
- center.id = "center";
- document.body.insertAdjacentElement("afterbegin", center);
-
- // await drawTargetsInCircle(2);
- await drawTargetsInCircle(false);
-
- // insert intro elements into document flow
- intro1.style.display = "inline";
- intro2.style.display = "inline";
-
- // reveal intro elements
- intro1.style.opacity = "100";
- await sleep(500);
- intro2.style.opacity = "100";
- await sleep(1500);
+ async function handlePlayAgain(e) {
+ gameIsOver = false;
- intro1.classList.add("fade");
- intro2.classList.add("fade");
+ // hide game over buttons
+ gameOverButtons.style.display = "none";
+ gameOverButtons.style.opacity = "0";
- intro1.style.opacity = "0";
- intro2.style.opacity = "0";
-
- await sleep(1000);
-
- intro1.style.display = "none";
- intro2.style.display = "none";
-
- window.addEventListener("resize", handlePageResize);
-
- document.getElementById("game-modes").style.display = "inline";
- await sleep(500);
- document.getElementById("game-modes").style.opacity = "100";
+ // remove game text from DOM flow
+ gameText.style.display = "none";
+ gameText.style.opacity = "0";
- if (localStorage.getItem("bestNormalTime") != null) {
- bestNormalTime = Number.parseFloat(localStorage.getItem("bestNormalTime"));
+ if (gamemode == "normal") {
+ handleNormalMode(e);
+ }
+ else if (gamemode == "countdown") {
+ handleCountdownMode(e);
+ }
+ else {
+ alert("cannot play again, gamemode does not exit?");
}
}
@@ -249,42 +282,58 @@
gamemode = "normal";
console.log("game mode: " + gamemode);
+ // setup mode parameters
currentRound = 1;
totalTime = 0;
+ await fadeMainMenuOut();
+
+ // set game instruction text + fade it in
+ gameText.innerText = "hover on middle circle to start the countdown,\nthen click the targets as fast as possible!";
+ gameText.style.opacity = "0";
+ gameText.style.display = "inline";
+ await sleep(500);
+ gameText.style.opacity = "100";
+
+ // enable player to start first round
center.addEventListener("mouseover", handleHoverStart);
center.addEventListener("mouseleave", handleHoverEnd);
+ }
- document.getElementById("game-modes").style.opacity = "0";
- await sleep(500);
- document.getElementById("game-modes").style.display = "none";
+ async function handleCountdownMode(e) {
+ gamemode = "countdown";
+ console.log("game mode: " + gamemode);
- intro1.style.display = "none";
- intro2.style.display = "none";
-
- gameText.innerText = "hover on middle circle to start the countdown,\nthen click the targets as fast as possible!";
+ await fadeMainMenuOut();
+ // set game instruction text + fade it in
+ gameText.innerText = "hover on middle circle to start the countdown,\nthen click as many targets as possible!";
gameText.style.opacity = "0";
gameText.style.display = "inline";
await sleep(500);
gameText.style.opacity = "100";
+ // enable player to start game
+ center.addEventListener("mouseover", handleHoverStart);
+ center.addEventListener("mouseleave", handleHoverEnd);
}
async function handleReset(e) {
+ gameIsOver = false;
+ gamemode = "";
currentRound = 0;
- center.removeEventListener("mouseover", handleReset);
+
+ // hide buttons
+ gameOverButtons.style.display = "none";
+ gameOverButtons.style.opacity = "0";
+
+ // remove game text from DOM flow
gameText.style.display = "none";
gameText.style.opacity = "0";
- intro1.style.display = "inline";
- intro2.style.display = "inline";
- intro1.style.opacity = "100";
- intro2.style.opacity = "100";
-
- document.getElementById("game-modes").style.display = "inline";
- await sleep(500);
- document.getElementById("game-modes").style.opacity = "100";
+ // reintroduce main menu elements into DOM flow
+ await sleep(800);
await drawTargetsInCircle(2);
+ await fadeMainMenuIn();
}
async function handleHoverStart(e) {
@@ -294,27 +343,91 @@
console.log('hover count: ' + hoverCount);
console.log('current round: ' + currentRound);
+ // remove all targets
frameElement.replaceChildren();
-
- // intro1.style.display = "none";
- // intro2.style.display = "none";
gameText.style.display = "none";
if (gamemode == "normal") {
await startNormalRound(currentRound);
}
+ else if (gamemode == "countdown") {
+ await startCountdownMode();
+ }
// if (infinityMode)
}
+ async function startCountdownMode() {
+ // if user cancels hover countdown, don't start round
+ if (!await normalModeRoundCountdown(hoverCount)) {
+ console.log("player canceled coundown");
+ return;
+ }
+
+ // setup mode parameters
+ targetsClicked = 0;
+ tempTargetsClicked = 0;
+ countdownTime = 15.0;
+
+ // clear screen + render countdown / score
+ center.remove();
+ gameText.innerText = countdownTime.toFixed(3) + "s\ntargets clicked: " + targetsClicked;
+ gameText.style.display = "inline";
+ gameText.style.opacity = "100";
+
+ // begin actual game loop!
+ randomTargetGenerator(1);
+ while (countdownTime > 0.0) {
+ gameText.innerText = countdownTime.toFixed(3) + "s\ntargets clicked: " + targetsClicked;
+ // auto-reset screen if page was resized during animation
+ if (needsResizeAdjustment) {
+ handlePageResize();
+ }
+ // generate new target if last one was clicked
+ if (targetsClicked != tempTargetsClicked) {
+ randomTargetGenerator(1);
+ tempTargetsClicked++;
+ }
+ countdownTime = countdownTime - .01;
+ await sleep(10);
+ }
+ gameIsOver = true;
+
+ // ensure no targets are still on screen after timer ends
+ frameElement.replaceChildren();
+
+ // show end of round text. if high score, alert player and save
+ if (targetsClicked > bestCountdownScore || bestCountdownScore == 0) {
+ if (bestCountdownScore == 0) {
+ alert("NEW HIGHEST SCORE!!! congratulations cowboy.");
+ }
+ else {
+ alert("NEW HIGHEST SCORE!!! previous best: " + bestCountdownScore + " targets. congratulations cowboy.");
+ }
+ bestCountdownScore = targetsClicked;
+ localStorage.setItem("bestCountdownScore", bestCountdownScore);
+ }
+ gameText.innerText = "TARGETS CLICKED: " + targetsClicked +
+ "\nHIGH SCORE: " + bestCountdownScore +
+ "\nhover on circle to go back to main menu..";
+
+ // bring bullseye back to screen
+ center = generateTarget(xFrameAdjustment, yFrameAdjustment);
+ center.id = "center";
+ document.body.insertAdjacentElement("afterbegin", center);
+
+ gameOverButtons.style.display = "flex";
+ gameOverButtons.style.opacity = "100";
+ }
+
async function startNormalRound(roundNumber) {
if (roundNumber > 3) {
handleReset({});
return;
}
- if (!await startCountdown(hoverCount)) {
- // if user canceled hover, don't continue round
+ // if user cancels hover countdown, don't start round
+ if (!await normalModeRoundCountdown(hoverCount)) {
console.log("player canceled coundown");
return;
}
@@ -332,13 +445,13 @@
// effectively begin the round
console.log("starting round " + roundNumber);
- document.getElementById("center").remove();
- targetsClickedThisRound = 0;
+ center.remove();
+ targetsClicked = 0;
roundTimeStart = Date.now();
- placeTarget(numTargets);
- while (targetsClickedThisRound < numTargets) {
+ randomTargetGenerator(numTargets);
+ while (targetsClicked < numTargets) {
await sleep(10);
- if (targetsClickedThisRound == numTargets) break;
+ if (targetsClicked == numTargets) break;
}
roundTimeEnd = Date.now();
roundSeconds = (roundTimeEnd - roundTimeStart) / 1000;
@@ -351,22 +464,29 @@
center.id = "center";
document.body.insertAdjacentElement("afterbegin", center);
- // ensure draw isn't still lingering on screen
+ // ensure "DRAW" isn't still lingering on screen after round
draw.style.display = "none";
draw.style.opacity = "0";
if (roundNumber == 3) {
// GAME OVER STUFF
- if (totalTime < bestNormalTime || bestNormalTime == "none") {
- alert("NEW FASTEST TIME!!! previous best: " + bestNormalTime + "s. congratulations cowboy.");
+ gameIsOver = true;
+ if (totalTime < bestNormalTime || bestNormalTime == 0) {
+ if (bestNormalTime == 0) {
+ alert("NEW FASTEST TIME!!! congratulations cowboy.");
+ }
+ else {
+ alert("NEW FASTEST TIME!!! previous best: " + bestNormalTime.toFixed(3) + "s. congratulations cowboy.");
+ }
bestNormalTime = totalTime;
localStorage.setItem("bestNormalTime", bestNormalTime);
}
gameText.innerText = "ROUND: " + roundSeconds.toFixed(3) +
"s\nTOTAL: " + totalTime.toFixed(3) +
- "s\nBEST SCORE: " + bestNormalTime.toFixed(3) +
- "s\nhover on circle to go back to main menu..";
- center.addEventListener("mouseover", handleReset);
+ "s\nBEST SCORE: " + bestNormalTime.toFixed(3);
+
+ gameOverButtons.style.display = "flex";
+ gameOverButtons.style.opacity = "100";
}
else {
console.log("ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + "\nhover on circle to start round " + currentRound);
@@ -375,7 +495,6 @@
center.addEventListener("mouseover", handleHoverStart);
center.addEventListener("mouseleave", handleHoverEnd);
}
-
gameText.style.display = "inline";
gameText.style.opacity = "100";
@@ -383,9 +502,10 @@
return;
}
-
-
- async function startCountdown(hoverNumber) {
+ /**
+ * HELPER TO KICK-OFF "NORMAL MODE" ROUNDS
+ */
+ async function normalModeRoundCountdown(hoverNumber) {
gameText.style.display = "none";
countingDown = true;
@@ -411,15 +531,15 @@
if (!hovering || hoverNumber != hoverCount) return false;
three.style.opacity = "100";
- await sleep(1000);
+ await sleep(500);
if (!hovering || hoverNumber != hoverCount) return false;
two.style.opacity = "100";
- await sleep(1000);
+ await sleep(500);
if (!hovering || hoverNumber != hoverCount) return false;
one.style.opacity = "100";
- await sleep(1000);
+ await sleep(500);
if (!hovering || hoverNumber != hoverCount) return false;
one.style.display = "none";
@@ -434,10 +554,12 @@
draw.style.opacity = "100";
countingDown = false;
+ // keep "DRAW" on screen for first second of the round
setTimeout(() => {draw.style.display = "none"; draw.style.opacity = "0"; }, 1000);
return true;
}
+
function handleHoverEnd(e) {
hovering = false;
@@ -454,47 +576,81 @@
three.style.opacity = "0";
draw.style.opacity = "0";
-
- if (currentRound <= 1) {
- drawTargetsInCircle();
- gameText.innerText = "hover on middle circle to start the countdown,\nthen click the targets as fast as possible!";
+
+ if (gamemode == "normal") {
+ if (currentRound <= 1) {
+ drawTargetsInCircle(0);
+ gameText.innerText = "hover on middle circle to start the countdown,\nthen click the targets as fast as possible!";
+ }
+ else {
+ gameText.innerText = "ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + "s\nhover on circle to start round " + currentRound;
+ }
}
- else {
- gameText.innerText = "ROUND: " + roundSeconds.toFixed(3) + "s\nTOTAL: " + totalTime.toFixed(3) + "s\nhover on circle to start round " + currentRound;
+
+ else if (gamemode == "countdown") {
+ drawTargetsInCircle(0);
+ gameText.innerText = "hover on middle circle to start the countdown,\nthen click as many targets as possible!";
}
+
gameText.style.display = "inline";
countingDown = false;
}
}
function handleTargetClick(e) {
- targetsClickedThisRound++;
+ targetsClicked++;
e.target.remove();
}
function handlePageResize(event) {
+ // take note if page was resized during animation or mid-countdown round to auto-reset at earliest convenience
+ if (!needsResizeAdjustment && (drawingCircle || countdownTime > 0)) {
+ needsResizeAdjustment = true; return;
+ }
+
+ /**
+ * if no game has started yet, re-draw circle
+ *
+ * no matter what, if targets on screen, do not reset, do not redraw middle circle, but do flag for reset
+ *
+ * if adding eventListener, need to redraw center circle!
+ *
+ * if we are playing normal mode:
+ * -- if round 1, redraw cirlce and addEventListener for hover
+ * -- if round 2/3, only addEventListener for hover
+ * -- if round 4, only addEvenListener for reset
+ *
+ * if we are playing countdown mode:
+ * -- if countdownTimer == 0,
+ * ----- need a way to know if this is before or after the game!
+ * ----- if before game, redraw cirlce and addEventListener for hover
+ * ----- if after game, only addEventListener for reset
+ * -- if countdownTimer > 0, flag for reset upon next target clicked, no event listeners
+ **/
+
frameElement.replaceChildren();
setParameters();
- document.getElementById("center").remove();
+ center.remove();
center = generateTarget(xFrameAdjustment, yFrameAdjustment);
center.id = "center";
document.body.insertAdjacentElement("afterbegin", center);
- if (currentRound == 0 || (gamemode == "normal" && currentRound == 1)) {
- drawTargetsInCircle();
+ if (gamemode == "") {
+ drawTargetsInCircle(0);
}
-
- if (currentRound >= 1) {
+ else if (gamemode != "") {
center.addEventListener("mouseover", handleHoverStart);
center.addEventListener("mouseleave", handleHoverEnd);
}
console.log('page: ' + pageWidth + ' x ' + pageHeight);
console.log('frame: ' + frameWidth + ' x ' + frameHeight);
+ needsResizeAdjustment = false;
}
async function drawTargetsInCircle(drawDurationSeconds) {
+ drawingCircle = true;
let r, x, y;
let radians;
let target;
@@ -507,8 +663,55 @@
target = generateTarget(x, y);
document.getElementById("frame").insertAdjacentElement("afterbegin", target);
- if (drawDurationSeconds) await sleep((drawDurationSeconds * 1000) / 180);
+ if (drawDurationSeconds) {
+ target.classList.add("fade");
+ await sleep((drawDurationSeconds * 1000) / 180);
+ }
}
+ drawingCircle = false;
+ }
+
+ // helpers for rendering main menu stuff
+ async function fadeMainMenuIn() {
+ title1.classList.remove("fade");
+ title2.classList.remove("fade");
+
+ // add main menu elements to DOM flow
+ titleContainer.style.display = "flex";
+ title1.style.display = "inline";
+ title2.style.display = "inline";
+ gameModes.style.display = "inline";
+
+ // make title elements actually visible
+ title1.style.opacity = "100";
+ await sleep(500);
+ title2.style.opacity = "100";
+ await sleep(1500);
+
+ // add fade class for when elements are removed
+ title1.classList.add("fade");
+ title2.classList.add("fade");
+
+ // fade in gamemodes
+ await sleep(500);
+ gameModes.style.opacity = "100";
+ }
+
+ async function fadeMainMenuOut() {
+ let targets = document.getElementsByClassName("target");
+ for (let i = 0; i < targets.length; i++) {
+ console.log(targets[i]);
+ targets[i].style.opacity = 0;
+ };
+ title1.style.opacity = "0";
+ title2.style.opacity = "0";
+ gameModes.style.opacity = "0";
+ await sleep(500);
+ titleContainer.style.display = "none";
+ gameModes.style.display = "none";
+
+ title1.style.display = "none";
+ title2.style.display = "none";
}
@@ -517,7 +720,7 @@
}
// function to place a target somewhere random on the circle
- function placeTarget(numTargets) {
+ function randomTargetGenerator(numTargets) {
let radians, radius
let x, y;
let target;