├── img
└── share.png
├── index.html
├── readme.md
├── script.js
└── style.css
/img/share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tholman/checkboxrace/7d21a75ec789d1d04d884e5b01c1d15610e34d23/img/share.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Checkbox Race!
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
20 |
21 |
22 |
23 |
24 |
28 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
165 |
166 |
167 |
168 | 000/100
169 | 00:00
170 |
171 | ›
172 |
173 |
174 | Check the first box and we're off to the races!
↯
175 |
176 |
177 |
181 |
182 |
183 |
186 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Checkbox Race
2 |
3 | A point-and-click racing minigame. Click the checkboxes as fast as you can to win!
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | const allCheckboxes = document.querySelectorAll("input");
3 | const checkboxWrapper = document.querySelector(".checkboxes");
4 | const scoreBoard = document.querySelector(".score");
5 | const currentScore = document.querySelector(".current");
6 | const tips = document.querySelector(".tips");
7 | const timer = document.querySelector(".timer");
8 | const flagPiece = document.querySelector(".flagPiece");
9 | const finalTime = document.querySelector(".finalTime");
10 | const resetButton = document.querySelector(".resetButton");
11 | const endBoard = document.querySelector(".end");
12 | const boostWords = [
13 | "Speed!",
14 | "Nice!",
15 | "Fast!",
16 | "Power!",
17 | "Great!",
18 | "Awesome!",
19 | "Amazing!",
20 | "Super!",
21 | ];
22 |
23 | resetButton.addEventListener("click", reset);
24 |
25 | setTimeout(() => {
26 | window.scrollTo(0, 0);
27 | }, 50);
28 |
29 | let animationFrame;
30 | let startTime;
31 |
32 | let currentIndex = 0;
33 |
34 | function startTimer() {
35 | startTime = Date.now();
36 | animationFrame = window.requestAnimationFrame(tick);
37 | }
38 |
39 | function msToTime(duration) {
40 | var milliseconds = parseInt((duration % 1000) / 10)
41 | .toString()
42 | .padStart(2, "0"),
43 | seconds = Math.floor((duration / 1000) % 60),
44 | minutes = Math.floor((duration / (1000 * 60)) % 60),
45 | minutes = minutes < 10 ? "0" + minutes : minutes;
46 | seconds = seconds < 10 ? "0" + seconds : seconds;
47 |
48 | return minutes + ":" + seconds + ":" + milliseconds;
49 | }
50 |
51 | function tick() {
52 | var delta = Date.now() - startTime;
53 | timer.innerHTML = msToTime(delta);
54 |
55 | animationFrame = window.requestAnimationFrame(tick);
56 | }
57 |
58 | function randomPosOrNeg(number) {
59 | const posOrNeg = Math.random() < 0.5 ? -1 : 1;
60 | return Math.min(Math.random() * number, window.innerHeight - 10) * posOrNeg;
61 | }
62 |
63 | function reset() {
64 | allCheckboxes.forEach((checkbox, index) => {
65 | checkbox.style.transform = "none";
66 | if (index !== 0) {
67 | checkbox.disabled = true;
68 | }
69 | checkbox.checked = false;
70 | });
71 |
72 | currentIndex = 0;
73 | checkboxWrapper.style.transform = `translateX(${-20 * currentIndex}px)`;
74 | currentScore.innerHTML = 0;
75 | tips.classList.remove("hide");
76 | startTime = null;
77 | scoreBoard.classList.remove("show");
78 | timer.innerHTML = "00:00:00";
79 | flagPiece.style.fill = "red";
80 | endBoard.classList.remove("show");
81 | }
82 |
83 | function addBoost(element) {
84 | let verticalMovement = new DOMMatrixReadOnly(
85 | window.getComputedStyle(element).transform
86 | ).f;
87 |
88 | const boostElement = document.createElement("div");
89 | boostElement.classList.add("boost");
90 | boostElement.style.top = `${
91 | checkboxWrapper.clientHeight / 2 + verticalMovement - 60
92 | }px`;
93 | boostElement.style.left = `${element.offsetLeft}px`;
94 | boostElement.innerHTML =
95 | boostWords[Math.floor(Math.random() * boostWords.length)];
96 | checkboxWrapper.appendChild(boostElement);
97 | }
98 |
99 | document.body.addEventListener("click", () => {
100 | if (currentIndex === 0 || currentIndex === allCheckboxes.length) return;
101 |
102 | allCheckboxes[currentIndex].disabled = true;
103 | allCheckboxes[currentIndex - 1].checked = false;
104 | allCheckboxes[currentIndex - 1].disabled = false;
105 | currentIndex--;
106 | currentScore.innerText = currentIndex.toString().padStart(3, "0");
107 | checkboxWrapper.style.transform = `translateX(${-20 * currentIndex}px)`;
108 | });
109 |
110 | allCheckboxes.forEach((checkbox, index) => {
111 | checkbox.addEventListener("click", (event) => {
112 | if (!startTime) {
113 | startTimer();
114 | }
115 |
116 | if (index === currentIndex) {
117 | if (currentIndex === 0) {
118 | tips.classList.add("hide");
119 | scoreBoard.classList.add("show");
120 | }
121 |
122 | event.stopPropagation();
123 |
124 | if (Math.random() > 0.6) addBoost(checkbox);
125 |
126 | currentIndex++;
127 | currentScore.innerText = currentIndex.toString().padStart(3, "0");
128 |
129 | if (currentIndex === allCheckboxes.length) {
130 | flagPiece.style.fill = "#00c800";
131 | cancelAnimationFrame(animationFrame);
132 | scoreBoard.classList.remove("show");
133 |
134 | var delta = Date.now() - startTime;
135 | finalTime.innerHTML = msToTime(delta);
136 | endBoard.classList.add("show");
137 | return;
138 | }
139 |
140 | allCheckboxes[currentIndex].disabled = false;
141 | checkboxWrapper.style.transform = `translateX(${-20 * currentIndex}px)`;
142 |
143 | allCheckboxes[
144 | currentIndex
145 | ].style.transform = `translateY(${randomPosOrNeg(5 + currentIndex)}px)`;
146 | } else if (currentIndex === allCheckboxes.length) {
147 | if (currentIndex === allCheckboxes.length) {
148 | event.stopPropagation();
149 | event.preventDefault();
150 | }
151 | }
152 | });
153 | });
154 | })();
155 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html,
6 | body {
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 | body {
12 | height: 100vh;
13 | display: flex;
14 | align-items: center;
15 | overflow: hidden;
16 | }
17 |
18 | input {
19 | transition: transform 100ms ease;
20 | cursor: pointer;
21 | }
22 |
23 | .toms {
24 | text-decoration: none;
25 | font-size: 20px;
26 | }
27 |
28 | .toms {
29 | display: inline-flex;
30 | align-items: center;
31 | justify-content: center;
32 | width: 33px;
33 | height: 33px;
34 | background-repeat: no-repeat;
35 | background-color: rgba(0, 0, 0, 0.07);
36 | border-radius: 50%;
37 | background-size: 20px 20px;
38 | background-position: center;
39 | vertical-align: top;
40 | }
41 |
42 | .checkboxes {
43 | height: 600px;
44 | display: flex;
45 | align-items: center;
46 | padding-left: calc(50vw - 10px);
47 | padding-right: calc(60vw);
48 | transition: transform 100ms ease;
49 | border-top: 3px dashed #f5f5f5;
50 | border-bottom: 3px dashed #f5f5f5;
51 | }
52 |
53 | .inner {
54 | padding-right: 30px;
55 | }
56 |
57 | .central {
58 | font-family: monospace;
59 | position: fixed;
60 | transform: translateX(-50%);
61 | pointer-events: none;
62 | left: 50%;
63 | font-weight: bold;
64 | display: flex;
65 | justify-content: center;
66 | align-items: center;
67 | transition: opacity 150ms ease-out;
68 | }
69 |
70 | .score {
71 | left: 0;
72 | padding: 30px;
73 | padding-right: 60px;
74 | height: 100vh;
75 | background: linear-gradient(
76 | 90deg,
77 | rgba(255, 255, 255, 1) 0%,
78 | rgba(255, 255, 255, 1) 80%,
79 | rgba(0, 0, 0, 0) 100%
80 | );
81 | transform: none;
82 | text-align: center;
83 | font-size: 16px;
84 | line-height: 25px;
85 | color: white;
86 | }
87 |
88 | .score.show {
89 | color: black;
90 | }
91 |
92 | .tips {
93 | max-width: 150px;
94 | text-align: center;
95 | font-weight: bold;
96 | transform: translateY(-85%) translateX(-50%);
97 | }
98 |
99 | .flag {
100 | width: 25px;
101 | height: 25px;
102 | }
103 |
104 | .end {
105 | padding: 20px;
106 | background: #eee;
107 | margin-left: 20px;
108 | position: relative;
109 | font-family: monospace;
110 | width: 220px;
111 | text-align: center;
112 | display: flex;
113 | flex-direction: column;
114 | gap: 10px;
115 | transition: opacity 150ms ease-out;
116 | opacity: 0;
117 | }
118 |
119 | p {
120 | margin: 0;
121 | }
122 |
123 | .finalTime {
124 | font-weight: bold;
125 | }
126 |
127 | .nice {
128 | font-size: 18px;
129 | }
130 |
131 | .end:before {
132 | position: absolute;
133 | content: "";
134 | left: -20px;
135 | top: 50%;
136 | margin-top: -10px;
137 | border-left: 10px solid transparent;
138 | border-right: 10px solid #eee;
139 | border-top: 10px solid transparent;
140 | border-bottom: 10px solid transparent;
141 | width: 20px;
142 | height: 20px;
143 | box-sizing: border-box;
144 | }
145 |
146 | .boost {
147 | position: absolute;
148 | pointer-events: none;
149 | animation: slideUp 800ms cubic-bezier(0.25, 1, 0.5, 1);
150 | opacity: 0;
151 | font-family: monospace;
152 | font-size: 15px;
153 | font-weight: bold;
154 | }
155 |
156 | .sitter {
157 | position: fixed;
158 | bottom: 15px;
159 | font-family: monospace;
160 | font-size: 11px;
161 | font-weight: bold;
162 | text-transform: uppercase;
163 | }
164 |
165 | .watch {
166 | background: rgba(0, 0, 0, 0.07);
167 | padding: 10px 15px;
168 | border-radius: 22px;
169 | display: inline-block;
170 | margin-right: 5px;
171 | }
172 |
173 | .twitter {
174 | background: url("data:image/png;base64,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");
175 | width: 33px;
176 | height: 33px;
177 | display: inline-block;
178 | background-repeat: no-repeat;
179 | background-color: rgba(0, 0, 0, 0.07);
180 | border-radius: 50%;
181 | background-size: 20px 20px;
182 | background-position: center;
183 | vertical-align: top;
184 | }
185 |
186 | .left {
187 | left: 15px;
188 | }
189 |
190 | .right {
191 | right: 15px;
192 | }
193 |
194 | @keyframes slideUp {
195 | 0% {
196 | opacity: 1;
197 | transform: translateY(0) translateX(-50%);
198 | }
199 | 100% {
200 | opacity: 0;
201 | transform: translateY(-100px) translateX(-50%);
202 | }
203 | }
204 |
205 | .resetButton {
206 | width: 120px;
207 | margin: auto;
208 | margin-top: 2px;
209 | }
210 |
211 | .hide {
212 | opacity: 0;
213 | }
214 |
215 | .show {
216 | opacity: 1;
217 | }
218 |
--------------------------------------------------------------------------------