|
| 1 | +<!-- |
| 2 | + Sounds on creative commons 0 license (public domain): |
| 3 | + oof: https://freesound.org/people/fotoshop/sounds/47356/ |
| 4 | + wrong: https://freesound.org/people/Raclure/sounds/483598/ |
| 5 | + yay: https://freesound.org/people/Higgs01/sounds/428156/ |
| 6 | +
|
| 7 | + Digital Dream font by Jakob Fischer (free for personal and commercial use): |
| 8 | + https://www.1001fonts.com/digital-dream-font.html |
| 9 | + www.pizzadude.dk |
| 10 | + --> |
| 11 | +<!DOCTYPE html> |
| 12 | +<html> |
| 13 | + <head> |
| 14 | + <title>Time flies!</title> |
| 15 | + <style> |
| 16 | + @font-face { |
| 17 | + font-family: "Digital Dream"; |
| 18 | + src: url("assets/digitaldream.ttf"); |
| 19 | + } |
| 20 | + |
| 21 | + html, |
| 22 | + body { |
| 23 | + padding: 0; |
| 24 | + margin: 0; |
| 25 | + overflow: hidden; |
| 26 | + } |
| 27 | + |
| 28 | + :root { |
| 29 | + --default-radius: 4rem; |
| 30 | + --default-diameter: calc(var(--default-radius) * 2); |
| 31 | + --clock-margin: 0 5rem; |
| 32 | + font-size: 14px; |
| 33 | + box-sizing: border-box; |
| 34 | + } |
| 35 | + |
| 36 | + .row { |
| 37 | + padding: 1rem 0; |
| 38 | + overflow: hidden; |
| 39 | + height: var(--default-diameter); |
| 40 | + /* border-bottom: 1px solid red; */ |
| 41 | + } |
| 42 | + |
| 43 | + .row:first-child { |
| 44 | + /* border-top: 1px solid red; */ |
| 45 | + } |
| 46 | + |
| 47 | + .tracks { |
| 48 | + display: flex; |
| 49 | + flex-direction: column; |
| 50 | + height: 100vh; |
| 51 | + justify-content: center; |
| 52 | + } |
| 53 | + |
| 54 | + .track { |
| 55 | + height: 100%; |
| 56 | + display: flex; |
| 57 | + width: max-content; |
| 58 | + } |
| 59 | + |
| 60 | + .track > div { |
| 61 | + flex: 1 0 var(--default-diameter); |
| 62 | + } |
| 63 | + |
| 64 | + .outer-circle { |
| 65 | + --diameter: calc(var(--radius) * 2); |
| 66 | + --pointer-width: 0.3rem; |
| 67 | + --hover-shadow-color: lime; |
| 68 | + width: var(--diameter); |
| 69 | + height: var(--diameter); |
| 70 | + border-radius: 50%; |
| 71 | + border: 0.3rem solid black; |
| 72 | + position: relative; |
| 73 | + /* margin: 0 12rem; */ |
| 74 | + margin: var(--clock-margin); |
| 75 | + } |
| 76 | + |
| 77 | + .outer-circle:hover, |
| 78 | + .outer-circle:hover > * { |
| 79 | + box-shadow: 0px 0px 5px 2px var(--hover-shadow-color); |
| 80 | + } |
| 81 | + |
| 82 | + .outer-circle:hover { |
| 83 | + cursor: pointer; |
| 84 | + } |
| 85 | + |
| 86 | + .long-pointer { |
| 87 | + height: calc(var(--radius) * 0.9); |
| 88 | + width: 0.2rem; |
| 89 | + background: black; |
| 90 | + position: absolute; |
| 91 | + bottom: 50%; |
| 92 | + left: 50%; |
| 93 | + transform: translateX(-50%) rotateZ(calc(var(--minutes) * 30deg)); |
| 94 | + transform-origin: bottom left; |
| 95 | + } |
| 96 | + |
| 97 | + .short-pointer { |
| 98 | + height: calc(var(--radius) * 0.7); |
| 99 | + width: 0.35rem; |
| 100 | + background: black; |
| 101 | + position: absolute; |
| 102 | + bottom: 50%; |
| 103 | + left: 50%; |
| 104 | + transform: translateX(-50%) rotateZ(calc(var(--hour) * 30deg)); |
| 105 | + transform-origin: bottom left; |
| 106 | + } |
| 107 | + |
| 108 | + .top-bar { |
| 109 | + display: flex; |
| 110 | + justify-content: space-around; |
| 111 | + align-items: center; |
| 112 | + font-family: "Digital Dream"; |
| 113 | + } |
| 114 | + |
| 115 | + .top-bar > * { |
| 116 | + padding: 1rem; |
| 117 | + } |
| 118 | + |
| 119 | + .time { |
| 120 | + font-size: 3rem; |
| 121 | + } |
| 122 | + |
| 123 | + .points { |
| 124 | + font-size: 1.5rem; |
| 125 | + } |
| 126 | + |
| 127 | + .hp { |
| 128 | + font-size: 2rem; |
| 129 | + } |
| 130 | + |
| 131 | + .empty { |
| 132 | + width: var(--default-diameter); |
| 133 | + margin: var(--clock-margin); |
| 134 | + } |
| 135 | + |
| 136 | + #looser { |
| 137 | + position: fixed; |
| 138 | + top: 50%; |
| 139 | + left: 50%; |
| 140 | + transform: translate(-50%, -50%); |
| 141 | + font-family: Verdana, Geneva, Tahoma, sans-serif; |
| 142 | + font-size: 2.5rem; |
| 143 | + background: white; |
| 144 | + padding: 2rem; |
| 145 | + border: 1px solid black; |
| 146 | + text-transform: uppercase; |
| 147 | + display: none; |
| 148 | + } |
| 149 | + |
| 150 | + #looser button { |
| 151 | + background: lime; |
| 152 | + border: 1px solid black; |
| 153 | + margin: 0 auto; |
| 154 | + display: block; |
| 155 | + padding: 0.5rem; |
| 156 | + font-size: 1.5rem; |
| 157 | + cursor: pointer; |
| 158 | + margin-top: 2rem; |
| 159 | + } |
| 160 | + </style> |
| 161 | + </head> |
| 162 | + <body> |
| 163 | + <div class="top-bar"> |
| 164 | + <div class="hp">❤️❤️❤️</div> |
| 165 | + <div class="time">3:15</div> |
| 166 | + <div class="points"><span class="value">0</span> points</div> |
| 167 | + </div> |
| 168 | + <div class="tracks"> |
| 169 | + <div class="row"> |
| 170 | + <div class="track"></div> |
| 171 | + </div> |
| 172 | + <div class="row"> |
| 173 | + <div class="track"></div> |
| 174 | + </div> |
| 175 | + <div class="row"> |
| 176 | + <div class="track"></div> |
| 177 | + </div> |
| 178 | + <div class="row"> |
| 179 | + <div class="track"></div> |
| 180 | + </div> |
| 181 | + <div class="row"> |
| 182 | + <div class="track"></div> |
| 183 | + </div> |
| 184 | + <div class="row"> |
| 185 | + <div class="track"></div> |
| 186 | + </div> |
| 187 | + </div> |
| 188 | + |
| 189 | + <div id="looser"> |
| 190 | + <div>You lost 😥</div> |
| 191 | + <div id="try-again-wrapper"><button onclick="tryAgain()">Try again</button></div> |
| 192 | + </div> |
| 193 | + |
| 194 | + <template id="t-clock"> |
| 195 | + <div class="outer-circle" style="--radius: 4rem; --hour: 8; --minutes: 15" onclick="handleClockClick(this)"> |
| 196 | + <div class="long-pointer"></div> |
| 197 | + <div class="short-pointer"></div> |
| 198 | + </div> |
| 199 | + </template> |
| 200 | + |
| 201 | + <audio id="audio-yay" src="assets/yay.wav"></audio> |
| 202 | + <audio id="audio-wrong" src="assets/wrong.mp3"></audio> |
| 203 | + <audio id="audio-oof" src="assets/oof.wav"></audio> |
| 204 | + |
| 205 | + <script> |
| 206 | + function randomInt(to) { |
| 207 | + return Math.floor(Math.random() * to); |
| 208 | + } |
| 209 | + |
| 210 | + const yaySfx = document.querySelector("#audio-yay"); |
| 211 | + const wrongSfx = document.querySelector("#audio-wrong"); |
| 212 | + const oofSfx = document.querySelector("#audio-oof"); |
| 213 | + const hpElement = document.querySelector(".hp"); |
| 214 | + const pointsElement = document.querySelector(".points .value"); |
| 215 | + const looserElement = document.querySelector("#looser"); |
| 216 | + const tracks = document.querySelectorAll(".row .track"); |
| 217 | + const rowsWrapper = document.querySelector(".tracks"); |
| 218 | + |
| 219 | + const MINUTES_EASY = [0, 15, 30, 45]; |
| 220 | + // const MINUTES_HARD = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]; |
| 221 | + // const MINUTES_IMPOSSIBLE = Array(60) |
| 222 | + // .fill() |
| 223 | + // .map((_, i) => i); |
| 224 | + const TRACKS_NUM = 6; |
| 225 | + |
| 226 | + function instantiateClock(hour, minutes) { |
| 227 | + const template = document.querySelector("#t-clock"); |
| 228 | + const instance = template.content.cloneNode(true); |
| 229 | + const clock = instance.children[0]; |
| 230 | + clock.style.setProperty("--hour", hour); |
| 231 | + clock.style.setProperty("--minutes", minutes); |
| 232 | + return clock; |
| 233 | + } |
| 234 | + |
| 235 | + const TRACK_LENGTH = 20; |
| 236 | + const matrixRows = Array(TRACKS_NUM) |
| 237 | + .fill() |
| 238 | + .map(() => Array(TRACK_LENGTH).fill(0)); |
| 239 | + |
| 240 | + const clockCellsSet = new Set(); |
| 241 | + for (let i = 0; i < TRACK_LENGTH; i++) { |
| 242 | + const row = Math.floor(Math.random() * TRACKS_NUM); |
| 243 | + matrixRows[row][i] = 1; |
| 244 | + clockCellsSet.add(`${row},${i}`); |
| 245 | + } |
| 246 | + |
| 247 | + // omg |
| 248 | + let goalCells = new Set(clockCellsSet); |
| 249 | + for (let i = goalCells.size; i > 3; i--) { |
| 250 | + goalCells.delete([...goalCells.values()][randomInt(goalCells.size)]); |
| 251 | + } |
| 252 | + |
| 253 | + for (const g of goalCells) { |
| 254 | + const [row, col] = g.split(","); |
| 255 | + matrixRows[Number(row)][Number(col)] = 2; |
| 256 | + } |
| 257 | + |
| 258 | + const empty = document.createElement("div"); |
| 259 | + empty.className = "empty"; |
| 260 | + |
| 261 | + const GOAL_H = 3; |
| 262 | + const GOAL_M = 15; |
| 263 | + |
| 264 | + let hp = 3; |
| 265 | + function decreaseHP() { |
| 266 | + oofSfx.play(); |
| 267 | + hp--; |
| 268 | + hpElement.innerHTML = Array(hp).fill("❤️").join(""); |
| 269 | + if (hp === 0) { |
| 270 | + looserElement.style.display = "block"; |
| 271 | + } |
| 272 | + } |
| 273 | + |
| 274 | + let points = 0; |
| 275 | + function increasePoints() { |
| 276 | + yaySfx.play(); |
| 277 | + points++; |
| 278 | + pointsElement.innerHTML = points; |
| 279 | + } |
| 280 | + |
| 281 | + matrixRows.forEach((row, idx) => { |
| 282 | + for (const col of row) { |
| 283 | + if (col === 1) { |
| 284 | + const hour = Math.floor(Math.random() * 12) + 1; |
| 285 | + // const hour = randomInt(GOAL_H) + randomInt(12 - (GOAL_H + 1) - GOAL_H + 1) + GOAL_H + 1; |
| 286 | + const minutesIdx = Math.floor(Math.random() * MINUTES_EASY.length); |
| 287 | + const clock = instantiateClock(hour, MINUTES_EASY[minutesIdx]); |
| 288 | + tracks[idx].appendChild(clock); |
| 289 | + } else if (col === 2) { |
| 290 | + const clock = instantiateClock(3, 15); |
| 291 | + // I had some problems with this observer, so I went with the setInterval method :/ |
| 292 | + // const intObs = new IntersectionObserver( |
| 293 | + // (entries) => { |
| 294 | + // for (const e of entries) { |
| 295 | + // if (e.boundingClientRect.x < 0 && !e.isIntersecting) console.log("NAY"); |
| 296 | + // } |
| 297 | + // }, |
| 298 | + // { |
| 299 | + // root: tracks[idx].parentElement, |
| 300 | + // } |
| 301 | + // ); |
| 302 | + // intObs.observe(clock); |
| 303 | + tracks[idx].appendChild(clock); |
| 304 | + |
| 305 | + const intervId = setInterval(() => { |
| 306 | + const bounds = clock.getBoundingClientRect(); |
| 307 | + if (bounds.right < 0) { |
| 308 | + clearInterval(intervId); |
| 309 | + decreaseHP(); |
| 310 | + } |
| 311 | + }, 16); |
| 312 | + } else { |
| 313 | + tracks[idx].appendChild(empty.cloneNode()); |
| 314 | + } |
| 315 | + } |
| 316 | + }); |
| 317 | + |
| 318 | + for (const t of tracks) { |
| 319 | + const initial = `translateX(calc(100vw + ${Math.random()} * 10rem))`; |
| 320 | + t.style.transform = initial; |
| 321 | + |
| 322 | + const bounds = t.getClientRects(); |
| 323 | + const anim = [ |
| 324 | + { transform: initial }, |
| 325 | + { transform: `translateX(${-Math.ceil(t.getBoundingClientRect().width)}px)` }, |
| 326 | + ]; |
| 327 | + |
| 328 | + const options = { |
| 329 | + duration: 25000 + Math.random() * 10000 - 5000, |
| 330 | + iterations: 1, |
| 331 | + delay: Math.random() * 5000, |
| 332 | + }; |
| 333 | + |
| 334 | + const animation = t.animate(anim, options); |
| 335 | + animation.addEventListener("finish", (e) => { |
| 336 | + // TODO: reinstantiate clocks in this track and run the animation again with randomized duration and delay |
| 337 | + // console.log(e); |
| 338 | + }); |
| 339 | + } |
| 340 | + |
| 341 | + function handleClockClick(instance) { |
| 342 | + const hour = instance.style.getPropertyValue("--hour"); |
| 343 | + const minutes = instance.style.getPropertyValue("--minutes"); |
| 344 | + if (hour == GOAL_H && minutes == GOAL_M) { |
| 345 | + increasePoints(); |
| 346 | + // TODO: add animation on the clicked clock |
| 347 | + instance.parentNode.replaceChild(empty.cloneNode(), instance); |
| 348 | + } else { |
| 349 | + wrongSfx.play(); |
| 350 | + instance.style.setProperty("--hover-shadow-color", "red"); |
| 351 | + instance.addEventListener( |
| 352 | + "mouseleave", |
| 353 | + () => { |
| 354 | + instance.style.setProperty("--hover-shadow-color", "lime"); |
| 355 | + }, |
| 356 | + { once: true } |
| 357 | + ); |
| 358 | + } |
| 359 | + } |
| 360 | + </script> |
| 361 | + </body> |
| 362 | +</html> |
0 commit comments