├── .github └── workflows │ └── static.yml ├── README.md ├── index.html ├── script.js └── style.css /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Setup Pages 34 | uses: actions/configure-pages@v3 35 | - name: Upload artifact 36 | uses: actions/upload-pages-artifact@v1 37 | with: 38 | # Upload entire repository 39 | path: '.' 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v1 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

گیگیلی | Gigily 🤩😉

2 | 3 | ### 4 | 5 |

6 | 7 | ### 8 | 9 |

دمو | Demo 😁

http://gigily2.0hi.me

10 | 11 | ### 12 | 13 |

14 | 15 | ### 16 | 17 |
18 | 19 |
20 | 21 | ### 22 | 23 |

24 | 25 | ### 26 | 27 |
28 | 29 |
30 | 31 | ### 32 | 33 |

34 | 35 | ### 36 | 37 |

38 | 39 | ### 40 | 41 |

42 | 43 | ### 44 | 45 |
46 | 47 | instagram logo 48 | 49 | 50 | youtube logo 51 | 52 | 53 | gmail logo 54 | 55 | 56 | twitter logo 57 | 58 |
59 | 60 | ### 61 | 62 |

63 | 64 | ### 65 | 66 |

توسعه داده شده توسط برنامه نویسی با لذت

67 | 68 | ### 69 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Gigily | گیگیلی 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | 46 | 47 | 48 | 49 | 50 |
51 | 52 |
53 | 54 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | let gameSpeed = 1; 2 | 3 | const BLUE = { r: 0x67, g: 0xd7, b: 0xf0 }; 4 | const GREEN = { r: 0xa6, g: 0xe0, b: 0x2c }; 5 | const PINK = { r: 0xfa, g: 0x24, b: 0x73 }; 6 | const ORANGE = { r: 0xfe, g: 0x95, b: 0x22 }; 7 | const allColors = [BLUE, GREEN, PINK, ORANGE]; 8 | 9 | const getSpawnDelay = () => { 10 | const spawnDelayMax = 1400; 11 | const spawnDelayMin = 550; 12 | const spawnDelay = spawnDelayMax - state.game.cubeCount * 3.1; 13 | return Math.max(spawnDelay, spawnDelayMin); 14 | }; 15 | const doubleStrongEnableScore = 2000; 16 | const slowmoThreshold = 10; 17 | const strongThreshold = 25; 18 | const spinnerThreshold = 25; 19 | 20 | let pointerIsDown = false; 21 | let pointerScreen = { x: 0, y: 0 }; 22 | let pointerScene = { x: 0, y: 0 }; 23 | const minPointerSpeed = 60; 24 | const hitDampening = 0.1; 25 | const backboardZ = -400; 26 | const shadowColor = "#090d14"; 27 | const airDrag = 0.022; 28 | const gravity = 0.3; 29 | const sparkColor = "rgba(170,221,255,.9)"; 30 | const sparkThickness = 2.2; 31 | const airDragSpark = 0.1; 32 | const touchTrailColor = "rgba(170,221,255,.62)"; 33 | const touchTrailThickness = 7; 34 | const touchPointLife = 120; 35 | const touchPoints = []; 36 | const targetRadius = 40; 37 | const targetHitRadius = 50; 38 | const makeTargetGlueColor = (target) => { 39 | return "rgb(170,221,255)"; 40 | }; 41 | const fragRadius = targetRadius / 3; 42 | 43 | const canvas = document.querySelector("#c"); 44 | 45 | const cameraDistance = 900; 46 | const sceneScale = 1; 47 | const cameraFadeStartZ = 0.45 * cameraDistance; 48 | const cameraFadeEndZ = 0.65 * cameraDistance; 49 | const cameraFadeRange = cameraFadeEndZ - cameraFadeStartZ; 50 | 51 | const allVertices = []; 52 | const allPolys = []; 53 | const allShadowVertices = []; 54 | const allShadowPolys = []; 55 | 56 | const GAME_MODE_RANKED = Symbol("GAME_MODE_RANKED"); 57 | const GAME_MODE_CASUAL = Symbol("GAME_MODE_CASUAL"); 58 | 59 | const MENU_MAIN = Symbol("MENU_MAIN"); 60 | const MENU_PAUSE = Symbol("MENU_PAUSE"); 61 | const MENU_SCORE = Symbol("MENU_SCORE"); 62 | 63 | const state = { 64 | game: { 65 | mode: GAME_MODE_RANKED, 66 | time: 0, 67 | score: 0, 68 | cubeCount: 0, 69 | }, 70 | menus: { 71 | active: MENU_MAIN, 72 | }, 73 | }; 74 | 75 | const isInGame = () => !state.menus.active; 76 | const isMenuVisible = () => !!state.menus.active; 77 | const isCasualGame = () => state.game.mode === GAME_MODE_CASUAL; 78 | const isPaused = () => state.menus.active === MENU_PAUSE; 79 | 80 | const highScoreKey = "__menja__highScore"; 81 | const getHighScore = () => { 82 | const raw = localStorage.getItem(highScoreKey); 83 | return raw ? parseInt(raw, 10) : 0; 84 | }; 85 | 86 | let _lastHighscore = getHighScore(); 87 | const setHighScore = (score) => { 88 | _lastHighscore = getHighScore(); 89 | localStorage.setItem(highScoreKey, String(score)); 90 | }; 91 | 92 | const isNewHighScore = () => state.game.score > _lastHighscore; 93 | 94 | const invariant = (condition, message) => { 95 | if (!condition) throw new Error(message); 96 | }; 97 | 98 | const $ = (selector) => document.querySelector(selector); 99 | const handleClick = (element, handler) => 100 | element.addEventListener("click", handler); 101 | const handlePointerDown = (element, handler) => { 102 | element.addEventListener("touchstart", handler); 103 | element.addEventListener("mousedown", handler); 104 | }; 105 | 106 | const formatNumber = (num) => num.toLocaleString(); 107 | 108 | const PI = Math.PI; 109 | const TAU = Math.PI * 2; 110 | const ETA = Math.PI * 0.5; 111 | 112 | const clamp = (num, min, max) => Math.min(Math.max(num, min), max); 113 | 114 | const lerp = (a, b, mix) => (b - a) * mix + a; 115 | 116 | const random = (min, max) => Math.random() * (max - min) + min; 117 | 118 | const randomInt = (min, max) => ((Math.random() * (max - min + 1)) | 0) + min; 119 | 120 | const pickOne = (arr) => arr[(Math.random() * arr.length) | 0]; 121 | 122 | const colorToHex = (color) => { 123 | return ( 124 | "#" + 125 | (color.r | 0).toString(16).padStart(2, "0") + 126 | (color.g | 0).toString(16).padStart(2, "0") + 127 | (color.b | 0).toString(16).padStart(2, "0") 128 | ); 129 | }; 130 | 131 | const shadeColor = (color, lightness) => { 132 | let other, mix; 133 | if (lightness < 0.5) { 134 | other = 0; 135 | mix = 1 - lightness * 2; 136 | } else { 137 | other = 255; 138 | mix = lightness * 2 - 1; 139 | } 140 | return ( 141 | "#" + 142 | (lerp(color.r, other, mix) | 0).toString(16).padStart(2, "0") + 143 | (lerp(color.g, other, mix) | 0).toString(16).padStart(2, "0") + 144 | (lerp(color.b, other, mix) | 0).toString(16).padStart(2, "0") 145 | ); 146 | }; 147 | 148 | const _allCooldowns = []; 149 | 150 | const makeCooldown = (rechargeTime, units = 1) => { 151 | let timeRemaining = 0; 152 | let lastTime = 0; 153 | 154 | const initialOptions = { rechargeTime, units }; 155 | 156 | const updateTime = () => { 157 | const now = state.game.time; 158 | if (now < lastTime) { 159 | timeRemaining = 0; 160 | } else { 161 | timeRemaining -= now - lastTime; 162 | if (timeRemaining < 0) timeRemaining = 0; 163 | } 164 | lastTime = now; 165 | }; 166 | 167 | const canUse = () => { 168 | updateTime(); 169 | return timeRemaining <= rechargeTime * (units - 1); 170 | }; 171 | 172 | const cooldown = { 173 | canUse, 174 | useIfAble() { 175 | const usable = canUse(); 176 | if (usable) timeRemaining += rechargeTime; 177 | return usable; 178 | }, 179 | mutate(options) { 180 | if (options.rechargeTime) { 181 | timeRemaining -= rechargeTime - options.rechargeTime; 182 | if (timeRemaining < 0) timeRemaining = 0; 183 | rechargeTime = options.rechargeTime; 184 | } 185 | if (options.units) units = options.units; 186 | }, 187 | reset() { 188 | timeRemaining = 0; 189 | lastTime = 0; 190 | this.mutate(initialOptions); 191 | }, 192 | }; 193 | 194 | _allCooldowns.push(cooldown); 195 | 196 | return cooldown; 197 | }; 198 | 199 | const resetAllCooldowns = () => 200 | _allCooldowns.forEach((cooldown) => cooldown.reset()); 201 | 202 | const makeSpawner = ({ chance, cooldownPerSpawn, maxSpawns }) => { 203 | const cooldown = makeCooldown(cooldownPerSpawn, maxSpawns); 204 | return { 205 | shouldSpawn() { 206 | return Math.random() <= chance && cooldown.useIfAble(); 207 | }, 208 | mutate(options) { 209 | if (options.chance) chance = options.chance; 210 | cooldown.mutate({ 211 | rechargeTime: options.cooldownPerSpawn, 212 | units: options.maxSpawns, 213 | }); 214 | }, 215 | }; 216 | }; 217 | 218 | const normalize = (v) => { 219 | const mag = Math.hypot(v.x, v.y, v.z); 220 | return { 221 | x: v.x / mag, 222 | y: v.y / mag, 223 | z: v.z / mag, 224 | }; 225 | }; 226 | 227 | const add = (a) => (b) => a + b; 228 | const scaleVector = (scale) => (vector) => { 229 | vector.x *= scale; 230 | vector.y *= scale; 231 | vector.z *= scale; 232 | }; 233 | 234 | function cloneVertices(vertices) { 235 | return vertices.map((v) => ({ x: v.x, y: v.y, z: v.z })); 236 | } 237 | 238 | function copyVerticesTo(arr1, arr2) { 239 | const len = arr1.length; 240 | for (let i = 0; i < len; i++) { 241 | const v1 = arr1[i]; 242 | const v2 = arr2[i]; 243 | v2.x = v1.x; 244 | v2.y = v1.y; 245 | v2.z = v1.z; 246 | } 247 | } 248 | 249 | function computeTriMiddle(poly) { 250 | const v = poly.vertices; 251 | poly.middle.x = (v[0].x + v[1].x + v[2].x) / 3; 252 | poly.middle.y = (v[0].y + v[1].y + v[2].y) / 3; 253 | poly.middle.z = (v[0].z + v[1].z + v[2].z) / 3; 254 | } 255 | 256 | function computeQuadMiddle(poly) { 257 | const v = poly.vertices; 258 | poly.middle.x = (v[0].x + v[1].x + v[2].x + v[3].x) / 4; 259 | poly.middle.y = (v[0].y + v[1].y + v[2].y + v[3].y) / 4; 260 | poly.middle.z = (v[0].z + v[1].z + v[2].z + v[3].z) / 4; 261 | } 262 | 263 | function computePolyMiddle(poly) { 264 | if (poly.vertices.length === 3) { 265 | computeTriMiddle(poly); 266 | } else { 267 | computeQuadMiddle(poly); 268 | } 269 | } 270 | 271 | function computePolyDepth(poly) { 272 | computePolyMiddle(poly); 273 | const dX = poly.middle.x; 274 | const dY = poly.middle.y; 275 | const dZ = poly.middle.z - cameraDistance; 276 | poly.depth = Math.hypot(dX, dY, dZ); 277 | } 278 | 279 | function computePolyNormal(poly, normalName) { 280 | const v1 = poly.vertices[0]; 281 | const v2 = poly.vertices[1]; 282 | const v3 = poly.vertices[2]; 283 | const ax = v1.x - v2.x; 284 | const ay = v1.y - v2.y; 285 | const az = v1.z - v2.z; 286 | const bx = v1.x - v3.x; 287 | const by = v1.y - v3.y; 288 | const bz = v1.z - v3.z; 289 | const nx = ay * bz - az * by; 290 | const ny = az * bx - ax * bz; 291 | const nz = ax * by - ay * bx; 292 | const mag = Math.hypot(nx, ny, nz); 293 | const polyNormal = poly[normalName]; 294 | polyNormal.x = nx / mag; 295 | polyNormal.y = ny / mag; 296 | polyNormal.z = nz / mag; 297 | } 298 | 299 | function transformVertices( 300 | vertices, 301 | target, 302 | tX, 303 | tY, 304 | tZ, 305 | rX, 306 | rY, 307 | rZ, 308 | sX, 309 | sY, 310 | sZ 311 | ) { 312 | const sinX = Math.sin(rX); 313 | const cosX = Math.cos(rX); 314 | const sinY = Math.sin(rY); 315 | const cosY = Math.cos(rY); 316 | const sinZ = Math.sin(rZ); 317 | const cosZ = Math.cos(rZ); 318 | 319 | vertices.forEach((v, i) => { 320 | const targetVertex = target[i]; 321 | const x1 = v.x; 322 | const y1 = v.z * sinX + v.y * cosX; 323 | const z1 = v.z * cosX - v.y * sinX; 324 | const x2 = x1 * cosY - z1 * sinY; 325 | const y2 = y1; 326 | const z2 = x1 * sinY + z1 * cosY; 327 | const x3 = x2 * cosZ - y2 * sinZ; 328 | const y3 = x2 * sinZ + y2 * cosZ; 329 | const z3 = z2; 330 | 331 | targetVertex.x = x3 * sX + tX; 332 | targetVertex.y = y3 * sY + tY; 333 | targetVertex.z = z3 * sZ + tZ; 334 | }); 335 | } 336 | 337 | const projectVertex = (v) => { 338 | const focalLength = cameraDistance * sceneScale; 339 | const depth = focalLength / (cameraDistance - v.z); 340 | v.x = v.x * depth; 341 | v.y = v.y * depth; 342 | }; 343 | 344 | const projectVertexTo = (v, target) => { 345 | const focalLength = cameraDistance * sceneScale; 346 | const depth = focalLength / (cameraDistance - v.z); 347 | target.x = v.x * depth; 348 | target.y = v.y * depth; 349 | }; 350 | 351 | const PERF_START = () => {}; 352 | const PERF_END = () => {}; 353 | const PERF_UPDATE = () => {}; 354 | 355 | function makeCubeModel({ scale = 1 }) { 356 | return { 357 | vertices: [ 358 | { x: -scale, y: -scale, z: scale }, 359 | { x: scale, y: -scale, z: scale }, 360 | { x: scale, y: scale, z: scale }, 361 | { x: -scale, y: scale, z: scale }, 362 | { x: -scale, y: -scale, z: -scale }, 363 | { x: scale, y: -scale, z: -scale }, 364 | { x: scale, y: scale, z: -scale }, 365 | { x: -scale, y: scale, z: -scale }, 366 | ], 367 | polys: [ 368 | { vIndexes: [0, 1, 2, 3] }, 369 | { vIndexes: [7, 6, 5, 4] }, 370 | { vIndexes: [3, 2, 6, 7] }, 371 | { vIndexes: [4, 5, 1, 0] }, 372 | { vIndexes: [5, 6, 2, 1] }, 373 | { vIndexes: [0, 3, 7, 4] }, 374 | ], 375 | }; 376 | } 377 | 378 | function makeRecursiveCubeModel({ recursionLevel, splitFn, color, scale = 1 }) { 379 | const getScaleAtLevel = (level) => 1 / 3 ** level; 380 | 381 | let cubeOrigins = [{ x: 0, y: 0, z: 0 }]; 382 | 383 | for (let i = 1; i <= recursionLevel; i++) { 384 | const scale = getScaleAtLevel(i) * 2; 385 | const cubeOrigins2 = []; 386 | cubeOrigins.forEach((origin) => { 387 | cubeOrigins2.push(...splitFn(origin, scale)); 388 | }); 389 | cubeOrigins = cubeOrigins2; 390 | } 391 | 392 | const finalModel = { vertices: [], polys: [] }; 393 | 394 | const cubeModel = makeCubeModel({ scale: 1 }); 395 | cubeModel.vertices.forEach(scaleVector(getScaleAtLevel(recursionLevel))); 396 | 397 | const maxComponent = 398 | getScaleAtLevel(recursionLevel) * (3 ** recursionLevel - 1); 399 | 400 | cubeOrigins.forEach((origin, cubeIndex) => { 401 | const occlusion = 402 | Math.max(Math.abs(origin.x), Math.abs(origin.y), Math.abs(origin.z)) / 403 | maxComponent; 404 | const occlusionLighter = 405 | recursionLevel > 2 ? occlusion : (occlusion + 0.8) / 1.8; 406 | finalModel.vertices.push( 407 | ...cubeModel.vertices.map((v) => ({ 408 | x: (v.x + origin.x) * scale, 409 | y: (v.y + origin.y) * scale, 410 | z: (v.z + origin.z) * scale, 411 | })) 412 | ); 413 | finalModel.polys.push( 414 | ...cubeModel.polys.map((poly) => ({ 415 | vIndexes: poly.vIndexes.map(add(cubeIndex * 8)), 416 | })) 417 | ); 418 | }); 419 | 420 | return finalModel; 421 | } 422 | 423 | function mengerSpongeSplit(o, s) { 424 | return [ 425 | { x: o.x + s, y: o.y - s, z: o.z + s }, 426 | { x: o.x + s, y: o.y - s, z: o.z + 0 }, 427 | { x: o.x + s, y: o.y - s, z: o.z - s }, 428 | { x: o.x + 0, y: o.y - s, z: o.z + s }, 429 | { x: o.x + 0, y: o.y - s, z: o.z - s }, 430 | { x: o.x - s, y: o.y - s, z: o.z + s }, 431 | { x: o.x - s, y: o.y - s, z: o.z + 0 }, 432 | { x: o.x - s, y: o.y - s, z: o.z - s }, 433 | { x: o.x + s, y: o.y + s, z: o.z + s }, 434 | { x: o.x + s, y: o.y + s, z: o.z + 0 }, 435 | { x: o.x + s, y: o.y + s, z: o.z - s }, 436 | { x: o.x + 0, y: o.y + s, z: o.z + s }, 437 | { x: o.x + 0, y: o.y + s, z: o.z - s }, 438 | { x: o.x - s, y: o.y + s, z: o.z + s }, 439 | { x: o.x - s, y: o.y + s, z: o.z + 0 }, 440 | { x: o.x - s, y: o.y + s, z: o.z - s }, 441 | { x: o.x + s, y: o.y + 0, z: o.z + s }, 442 | { x: o.x + s, y: o.y + 0, z: o.z - s }, 443 | { x: o.x - s, y: o.y + 0, z: o.z + s }, 444 | { x: o.x - s, y: o.y + 0, z: o.z - s }, 445 | ]; 446 | } 447 | 448 | function optimizeModel(model, threshold = 0.0001) { 449 | const { vertices, polys } = model; 450 | 451 | const compareVertices = (v1, v2) => 452 | Math.abs(v1.x - v2.x) < threshold && 453 | Math.abs(v1.y - v2.y) < threshold && 454 | Math.abs(v1.z - v2.z) < threshold; 455 | 456 | const comparePolys = (p1, p2) => { 457 | const v1 = p1.vIndexes; 458 | const v2 = p2.vIndexes; 459 | return ( 460 | (v1[0] === v2[0] || 461 | v1[0] === v2[1] || 462 | v1[0] === v2[2] || 463 | v1[0] === v2[3]) && 464 | (v1[1] === v2[0] || 465 | v1[1] === v2[1] || 466 | v1[1] === v2[2] || 467 | v1[1] === v2[3]) && 468 | (v1[2] === v2[0] || 469 | v1[2] === v2[1] || 470 | v1[2] === v2[2] || 471 | v1[2] === v2[3]) && 472 | (v1[3] === v2[0] || v1[3] === v2[1] || v1[3] === v2[2] || v1[3] === v2[3]) 473 | ); 474 | }; 475 | 476 | vertices.forEach((v, i) => { 477 | v.originalIndexes = [i]; 478 | }); 479 | 480 | for (let i = vertices.length - 1; i >= 0; i--) { 481 | for (let ii = i - 1; ii >= 0; ii--) { 482 | const v1 = vertices[i]; 483 | const v2 = vertices[ii]; 484 | if (compareVertices(v1, v2)) { 485 | vertices.splice(i, 1); 486 | v2.originalIndexes.push(...v1.originalIndexes); 487 | break; 488 | } 489 | } 490 | } 491 | 492 | vertices.forEach((v, i) => { 493 | polys.forEach((p) => { 494 | p.vIndexes.forEach((vi, ii, arr) => { 495 | const vo = v.originalIndexes; 496 | if (vo.includes(vi)) { 497 | arr[ii] = i; 498 | } 499 | }); 500 | }); 501 | }); 502 | 503 | polys.forEach((p) => { 504 | const vi = p.vIndexes; 505 | p.sum = vi[0] + vi[1] + vi[2] + vi[3]; 506 | }); 507 | polys.sort((a, b) => b.sum - a.sum); 508 | 509 | for (let i = polys.length - 1; i >= 0; i--) { 510 | for (let ii = i - 1; ii >= 0; ii--) { 511 | const p1 = polys[i]; 512 | const p2 = polys[ii]; 513 | if (p1.sum !== p2.sum) break; 514 | if (comparePolys(p1, p2)) { 515 | polys.splice(i, 1); 516 | polys.splice(ii, 1); 517 | i--; 518 | break; 519 | } 520 | } 521 | } 522 | 523 | return model; 524 | } 525 | 526 | class Entity { 527 | constructor({ model, color, wireframe = false }) { 528 | const vertices = cloneVertices(model.vertices); 529 | const shadowVertices = cloneVertices(model.vertices); 530 | const colorHex = colorToHex(color); 531 | const darkColorHex = shadeColor(color, 0.4); 532 | 533 | const polys = model.polys.map((p) => ({ 534 | vertices: p.vIndexes.map((vIndex) => vertices[vIndex]), 535 | color: color, 536 | wireframe: wireframe, 537 | strokeWidth: wireframe ? 2 : 0, 538 | strokeColor: colorHex, 539 | strokeColorDark: darkColorHex, 540 | depth: 0, 541 | middle: { x: 0, y: 0, z: 0 }, 542 | normalWorld: { x: 0, y: 0, z: 0 }, 543 | normalCamera: { x: 0, y: 0, z: 0 }, 544 | })); 545 | 546 | const shadowPolys = model.polys.map((p) => ({ 547 | vertices: p.vIndexes.map((vIndex) => shadowVertices[vIndex]), 548 | wireframe: wireframe, 549 | normalWorld: { x: 0, y: 0, z: 0 }, 550 | })); 551 | 552 | this.projected = {}; 553 | this.model = model; 554 | this.vertices = vertices; 555 | this.polys = polys; 556 | this.shadowVertices = shadowVertices; 557 | this.shadowPolys = shadowPolys; 558 | this.reset(); 559 | } 560 | 561 | reset() { 562 | this.x = 0; 563 | this.y = 0; 564 | this.z = 0; 565 | this.xD = 0; 566 | this.yD = 0; 567 | this.zD = 0; 568 | 569 | this.rotateX = 0; 570 | this.rotateY = 0; 571 | this.rotateZ = 0; 572 | this.rotateXD = 0; 573 | this.rotateYD = 0; 574 | this.rotateZD = 0; 575 | 576 | this.scaleX = 1; 577 | this.scaleY = 1; 578 | this.scaleZ = 1; 579 | 580 | this.projected.x = 0; 581 | this.projected.y = 0; 582 | } 583 | 584 | transform() { 585 | transformVertices( 586 | this.model.vertices, 587 | this.vertices, 588 | this.x, 589 | this.y, 590 | this.z, 591 | this.rotateX, 592 | this.rotateY, 593 | this.rotateZ, 594 | this.scaleX, 595 | this.scaleY, 596 | this.scaleZ 597 | ); 598 | 599 | copyVerticesTo(this.vertices, this.shadowVertices); 600 | } 601 | 602 | project() { 603 | projectVertexTo(this, this.projected); 604 | } 605 | } 606 | 607 | const targets = []; 608 | 609 | const targetPool = new Map(allColors.map((c) => [c, []])); 610 | const targetWireframePool = new Map(allColors.map((c) => [c, []])); 611 | 612 | const getTarget = (() => { 613 | const slowmoSpawner = makeSpawner({ 614 | chance: 0.5, 615 | cooldownPerSpawn: 10000, 616 | maxSpawns: 1, 617 | }); 618 | 619 | let doubleStrong = false; 620 | const strongSpawner = makeSpawner({ 621 | chance: 0.3, 622 | cooldownPerSpawn: 12000, 623 | maxSpawns: 1, 624 | }); 625 | 626 | const spinnerSpawner = makeSpawner({ 627 | chance: 0.1, 628 | cooldownPerSpawn: 10000, 629 | maxSpawns: 1, 630 | }); 631 | 632 | const axisOptions = [ 633 | ["x", "y"], 634 | ["y", "z"], 635 | ["z", "x"], 636 | ]; 637 | 638 | function getTargetOfStyle(color, wireframe) { 639 | const pool = wireframe ? targetWireframePool : targetPool; 640 | let target = pool.get(color).pop(); 641 | if (!target) { 642 | target = new Entity({ 643 | model: optimizeModel( 644 | makeRecursiveCubeModel({ 645 | recursionLevel: 1, 646 | splitFn: mengerSpongeSplit, 647 | scale: targetRadius, 648 | }) 649 | ), 650 | color: color, 651 | wireframe: wireframe, 652 | }); 653 | 654 | target.color = color; 655 | target.wireframe = wireframe; 656 | target.hit = false; 657 | target.maxHealth = 0; 658 | target.health = 0; 659 | } 660 | return target; 661 | } 662 | 663 | return function getTarget() { 664 | if (doubleStrong && state.game.score <= doubleStrongEnableScore) { 665 | doubleStrong = false; 666 | } else if (!doubleStrong && state.game.score > doubleStrongEnableScore) { 667 | doubleStrong = true; 668 | strongSpawner.mutate({ maxSpawns: 2 }); 669 | } 670 | 671 | let color = pickOne([BLUE, GREEN, ORANGE]); 672 | let wireframe = false; 673 | let health = 1; 674 | let maxHealth = 3; 675 | const spinner = 676 | state.game.cubeCount >= spinnerThreshold && 677 | isInGame() && 678 | spinnerSpawner.shouldSpawn(); 679 | 680 | if ( 681 | state.game.cubeCount >= slowmoThreshold && 682 | slowmoSpawner.shouldSpawn() 683 | ) { 684 | color = BLUE; 685 | wireframe = true; 686 | } else if ( 687 | state.game.cubeCount >= strongThreshold && 688 | strongSpawner.shouldSpawn() 689 | ) { 690 | color = PINK; 691 | health = 3; 692 | } 693 | 694 | const target = getTargetOfStyle(color, wireframe); 695 | target.hit = false; 696 | target.maxHealth = maxHealth; 697 | target.health = health; 698 | updateTargetHealth(target, 0); 699 | 700 | const spinSpeeds = [Math.random() * 0.1 - 0.05, Math.random() * 0.1 - 0.05]; 701 | 702 | if (spinner) { 703 | spinSpeeds[0] = -0.25; 704 | spinSpeeds[1] = 0; 705 | target.rotateZ = random(0, TAU); 706 | } 707 | 708 | const axes = pickOne(axisOptions); 709 | 710 | spinSpeeds.forEach((spinSpeed, i) => { 711 | switch (axes[i]) { 712 | case "x": 713 | target.rotateXD = spinSpeed; 714 | break; 715 | case "y": 716 | target.rotateYD = spinSpeed; 717 | break; 718 | case "z": 719 | target.rotateZD = spinSpeed; 720 | break; 721 | } 722 | }); 723 | 724 | return target; 725 | }; 726 | })(); 727 | 728 | const updateTargetHealth = (target, healthDelta) => { 729 | target.health += healthDelta; 730 | if (!target.wireframe) { 731 | const strokeWidth = target.health - 1; 732 | const strokeColor = makeTargetGlueColor(target); 733 | for (let p of target.polys) { 734 | p.strokeWidth = strokeWidth; 735 | p.strokeColor = strokeColor; 736 | } 737 | } 738 | }; 739 | 740 | const returnTarget = (target) => { 741 | target.reset(); 742 | const pool = target.wireframe ? targetWireframePool : targetPool; 743 | pool.get(target.color).push(target); 744 | }; 745 | 746 | function resetAllTargets() { 747 | while (targets.length) { 748 | returnTarget(targets.pop()); 749 | } 750 | } 751 | 752 | const frags = []; 753 | const fragPool = new Map(allColors.map((c) => [c, []])); 754 | const fragWireframePool = new Map(allColors.map((c) => [c, []])); 755 | 756 | const createBurst = (() => { 757 | const basePositions = mengerSpongeSplit({ x: 0, y: 0, z: 0 }, fragRadius * 2); 758 | const positions = cloneVertices(basePositions); 759 | const prevPositions = cloneVertices(basePositions); 760 | const velocities = cloneVertices(basePositions); 761 | 762 | const basePositionNormals = basePositions.map(normalize); 763 | const positionNormals = cloneVertices(basePositionNormals); 764 | 765 | const fragCount = basePositions.length; 766 | 767 | function getFragForTarget(target) { 768 | const pool = target.wireframe ? fragWireframePool : fragPool; 769 | let frag = pool.get(target.color).pop(); 770 | if (!frag) { 771 | frag = new Entity({ 772 | model: makeCubeModel({ scale: fragRadius }), 773 | color: target.color, 774 | wireframe: target.wireframe, 775 | }); 776 | frag.color = target.color; 777 | frag.wireframe = target.wireframe; 778 | } 779 | return frag; 780 | } 781 | 782 | return (target, force = 1) => { 783 | transformVertices( 784 | basePositions, 785 | positions, 786 | target.x, 787 | target.y, 788 | target.z, 789 | target.rotateX, 790 | target.rotateY, 791 | target.rotateZ, 792 | 1, 793 | 1, 794 | 1 795 | ); 796 | transformVertices( 797 | basePositions, 798 | prevPositions, 799 | target.x - target.xD, 800 | target.y - target.yD, 801 | target.z - target.zD, 802 | target.rotateX - target.rotateXD, 803 | target.rotateY - target.rotateYD, 804 | target.rotateZ - target.rotateZD, 805 | 1, 806 | 1, 807 | 1 808 | ); 809 | 810 | for (let i = 0; i < fragCount; i++) { 811 | const position = positions[i]; 812 | const prevPosition = prevPositions[i]; 813 | const velocity = velocities[i]; 814 | 815 | velocity.x = position.x - prevPosition.x; 816 | velocity.y = position.y - prevPosition.y; 817 | velocity.z = position.z - prevPosition.z; 818 | } 819 | 820 | transformVertices( 821 | basePositionNormals, 822 | positionNormals, 823 | 0, 824 | 0, 825 | 0, 826 | target.rotateX, 827 | target.rotateY, 828 | target.rotateZ, 829 | 1, 830 | 1, 831 | 1 832 | ); 833 | 834 | for (let i = 0; i < fragCount; i++) { 835 | const position = positions[i]; 836 | const velocity = velocities[i]; 837 | const normal = positionNormals[i]; 838 | 839 | const frag = getFragForTarget(target); 840 | 841 | frag.x = position.x; 842 | frag.y = position.y; 843 | frag.z = position.z; 844 | frag.rotateX = target.rotateX; 845 | frag.rotateY = target.rotateY; 846 | frag.rotateZ = target.rotateZ; 847 | 848 | const burstSpeed = 2 * force; 849 | const randSpeed = 2 * force; 850 | const rotateScale = 0.015; 851 | frag.xD = velocity.x + normal.x * burstSpeed + Math.random() * randSpeed; 852 | frag.yD = velocity.y + normal.y * burstSpeed + Math.random() * randSpeed; 853 | frag.zD = velocity.z + normal.z * burstSpeed + Math.random() * randSpeed; 854 | frag.rotateXD = frag.xD * rotateScale; 855 | frag.rotateYD = frag.yD * rotateScale; 856 | frag.rotateZD = frag.zD * rotateScale; 857 | 858 | frags.push(frag); 859 | } 860 | }; 861 | })(); 862 | 863 | const returnFrag = (frag) => { 864 | frag.reset(); 865 | const pool = frag.wireframe ? fragWireframePool : fragPool; 866 | pool.get(frag.color).push(frag); 867 | }; 868 | 869 | const sparks = []; 870 | const sparkPool = []; 871 | 872 | function addSpark(x, y, xD, yD) { 873 | const spark = sparkPool.pop() || {}; 874 | 875 | spark.x = x + xD * 0.5; 876 | spark.y = y + yD * 0.5; 877 | spark.xD = xD; 878 | spark.yD = yD; 879 | spark.life = random(200, 300); 880 | spark.maxLife = spark.life; 881 | 882 | sparks.push(spark); 883 | 884 | return spark; 885 | } 886 | 887 | function sparkBurst(x, y, count, maxSpeed) { 888 | const angleInc = TAU / count; 889 | for (let i = 0; i < count; i++) { 890 | const angle = i * angleInc + angleInc * Math.random(); 891 | const speed = (1 - Math.random() ** 3) * maxSpeed; 892 | addSpark(x, y, Math.sin(angle) * speed, Math.cos(angle) * speed); 893 | } 894 | } 895 | 896 | let glueShedVertices; 897 | function glueShedSparks(target) { 898 | if (!glueShedVertices) { 899 | glueShedVertices = cloneVertices(target.vertices); 900 | } else { 901 | copyVerticesTo(target.vertices, glueShedVertices); 902 | } 903 | 904 | glueShedVertices.forEach((v) => { 905 | if (Math.random() < 0.4) { 906 | projectVertex(v); 907 | addSpark(v.x, v.y, random(-12, 12), random(-12, 12)); 908 | } 909 | }); 910 | } 911 | 912 | function returnSpark(spark) { 913 | sparkPool.push(spark); 914 | } 915 | 916 | const hudContainerNode = $(".hud"); 917 | 918 | function setHudVisibility(visible) { 919 | if (visible) { 920 | hudContainerNode.style.display = "block"; 921 | } else { 922 | hudContainerNode.style.display = "none"; 923 | } 924 | } 925 | 926 | const scoreNode = $(".score-lbl"); 927 | const cubeCountNode = $(".cube-count-lbl"); 928 | 929 | function renderScoreHud() { 930 | if (isCasualGame()) { 931 | scoreNode.style.display = "none"; 932 | cubeCountNode.style.opacity = 1; 933 | } else { 934 | scoreNode.innerText = `امتیاز: ${state.game.score}`; 935 | scoreNode.style.display = "block"; 936 | cubeCountNode.style.opacity = 0.65; 937 | } 938 | cubeCountNode.innerText = `مکعب های خرد شده : ${state.game.cubeCount}`; 939 | } 940 | 941 | renderScoreHud(); 942 | 943 | handlePointerDown($(".pause-btn"), () => pauseGame()); 944 | 945 | const slowmoNode = $(".slowmo"); 946 | const slowmoBarNode = $(".slowmo__bar"); 947 | 948 | function renderSlowmoStatus(percentRemaining) { 949 | slowmoNode.style.opacity = percentRemaining === 0 ? 0 : 1; 950 | slowmoBarNode.style.transform = `scaleX(${percentRemaining.toFixed(3)})`; 951 | } 952 | 953 | const menuContainerNode = $(".menus"); 954 | const menuMainNode = $(".menu--main"); 955 | const menuPauseNode = $(".menu--pause"); 956 | const menuScoreNode = $(".menu--score"); 957 | 958 | const finalScoreLblNode = $(".final-score-lbl"); 959 | const highScoreLblNode = $(".high-score-lbl"); 960 | 961 | function showMenu(node) { 962 | node.classList.add("active"); 963 | } 964 | 965 | function hideMenu(node) { 966 | node.classList.remove("active"); 967 | } 968 | 969 | function renderMenus() { 970 | hideMenu(menuMainNode); 971 | hideMenu(menuPauseNode); 972 | hideMenu(menuScoreNode); 973 | 974 | switch (state.menus.active) { 975 | case MENU_MAIN: 976 | showMenu(menuMainNode); 977 | break; 978 | case MENU_PAUSE: 979 | showMenu(menuPauseNode); 980 | break; 981 | case MENU_SCORE: 982 | finalScoreLblNode.textContent = formatNumber(state.game.score); 983 | if (isNewHighScore()) { 984 | highScoreLblNode.textContent = "بالاترین امتیاز جدید"; 985 | } else { 986 | highScoreLblNode.textContent = `بالاترین امتیاز : ${formatNumber( 987 | getHighScore() 988 | )}`; 989 | } 990 | showMenu(menuScoreNode); 991 | break; 992 | } 993 | 994 | setHudVisibility(!isMenuVisible()); 995 | menuContainerNode.classList.toggle("has-active", isMenuVisible()); 996 | menuContainerNode.classList.toggle( 997 | "interactive-mode", 998 | isMenuVisible() && pointerIsDown 999 | ); 1000 | } 1001 | 1002 | renderMenus(); 1003 | 1004 | handleClick($(".play-normal-btn"), () => { 1005 | setGameMode(GAME_MODE_RANKED); 1006 | setActiveMenu(null); 1007 | resetGame(); 1008 | }); 1009 | 1010 | handleClick($(".play-casual-btn"), () => { 1011 | setGameMode(GAME_MODE_CASUAL); 1012 | setActiveMenu(null); 1013 | resetGame(); 1014 | }); 1015 | 1016 | handleClick($(".resume-btn"), () => resumeGame()); 1017 | handleClick($(".menu-btn--pause"), () => setActiveMenu(MENU_MAIN)); 1018 | 1019 | handleClick($(".play-again-btn"), () => { 1020 | setActiveMenu(null); 1021 | resetGame(); 1022 | }); 1023 | 1024 | handleClick($(".menu-btn--score"), () => setActiveMenu(MENU_MAIN)); 1025 | 1026 | handleClick($(".play-normal-btn"), () => { 1027 | setGameMode(GAME_MODE_RANKED); 1028 | setActiveMenu(null); 1029 | resetGame(); 1030 | }); 1031 | 1032 | handleClick($(".play-casual-btn"), () => { 1033 | setGameMode(GAME_MODE_CASUAL); 1034 | setActiveMenu(null); 1035 | resetGame(); 1036 | }); 1037 | 1038 | handleClick($(".resume-btn"), () => resumeGame()); 1039 | handleClick($(".menu-btn--pause"), () => setActiveMenu(MENU_MAIN)); 1040 | 1041 | handleClick($(".play-again-btn"), () => { 1042 | setActiveMenu(null); 1043 | resetGame(); 1044 | }); 1045 | 1046 | handleClick($(".menu-btn--score"), () => setActiveMenu(MENU_MAIN)); 1047 | 1048 | function setActiveMenu(menu) { 1049 | state.menus.active = menu; 1050 | renderMenus(); 1051 | } 1052 | 1053 | function setScore(score) { 1054 | state.game.score = score; 1055 | renderScoreHud(); 1056 | } 1057 | 1058 | function incrementScore(inc) { 1059 | if (isInGame()) { 1060 | state.game.score += inc; 1061 | if (state.game.score < 0) { 1062 | state.game.score = 0; 1063 | } 1064 | renderScoreHud(); 1065 | } 1066 | } 1067 | 1068 | function setCubeCount(count) { 1069 | state.game.cubeCount = count; 1070 | renderScoreHud(); 1071 | } 1072 | 1073 | function incrementCubeCount(inc) { 1074 | if (isInGame()) { 1075 | state.game.cubeCount += inc; 1076 | renderScoreHud(); 1077 | } 1078 | } 1079 | 1080 | function setGameMode(mode) { 1081 | state.game.mode = mode; 1082 | } 1083 | 1084 | function resetGame() { 1085 | resetAllTargets(); 1086 | state.game.time = 0; 1087 | resetAllCooldowns(); 1088 | setScore(0); 1089 | setCubeCount(0); 1090 | spawnTime = getSpawnDelay(); 1091 | } 1092 | 1093 | function pauseGame() { 1094 | isInGame() && setActiveMenu(MENU_PAUSE); 1095 | } 1096 | 1097 | function resumeGame() { 1098 | isPaused() && setActiveMenu(null); 1099 | } 1100 | 1101 | function endGame() { 1102 | handleCanvasPointerUp(); 1103 | if (isNewHighScore()) { 1104 | setHighScore(state.game.score); 1105 | } 1106 | setActiveMenu(MENU_SCORE); 1107 | } 1108 | 1109 | window.addEventListener("keydown", (event) => { 1110 | if (event.key === "p") { 1111 | isPaused() ? resumeGame() : pauseGame(); 1112 | } 1113 | }); 1114 | 1115 | let spawnTime = 0; 1116 | const maxSpawnX = 450; 1117 | const pointerDelta = { x: 0, y: 0 }; 1118 | const pointerDeltaScaled = { x: 0, y: 0 }; 1119 | 1120 | const slowmoDuration = 1500; 1121 | let slowmoRemaining = 0; 1122 | let spawnExtra = 0; 1123 | const spawnExtraDelay = 300; 1124 | let targetSpeed = 1; 1125 | 1126 | function tick(width, height, simTime, simSpeed, lag) { 1127 | PERF_START("frame"); 1128 | PERF_START("tick"); 1129 | 1130 | state.game.time += simTime; 1131 | 1132 | if (slowmoRemaining > 0) { 1133 | slowmoRemaining -= simTime; 1134 | if (slowmoRemaining < 0) { 1135 | slowmoRemaining = 0; 1136 | } 1137 | targetSpeed = pointerIsDown ? 0.075 : 0.3; 1138 | } else { 1139 | const menuPointerDown = isMenuVisible() && pointerIsDown; 1140 | targetSpeed = menuPointerDown ? 0.025 : 1; 1141 | } 1142 | 1143 | renderSlowmoStatus(slowmoRemaining / slowmoDuration); 1144 | 1145 | gameSpeed += ((targetSpeed - gameSpeed) / 22) * lag; 1146 | gameSpeed = clamp(gameSpeed, 0, 1); 1147 | 1148 | const centerX = width / 2; 1149 | const centerY = height / 2; 1150 | 1151 | const simAirDrag = 1 - airDrag * simSpeed; 1152 | const simAirDragSpark = 1 - airDragSpark * simSpeed; 1153 | 1154 | const forceMultiplier = 1 / (simSpeed * 0.75 + 0.25); 1155 | pointerDelta.x = 0; 1156 | pointerDelta.y = 0; 1157 | pointerDeltaScaled.x = 0; 1158 | pointerDeltaScaled.y = 0; 1159 | const lastPointer = touchPoints[touchPoints.length - 1]; 1160 | 1161 | if (pointerIsDown && lastPointer && !lastPointer.touchBreak) { 1162 | pointerDelta.x = pointerScene.x - lastPointer.x; 1163 | pointerDelta.y = pointerScene.y - lastPointer.y; 1164 | pointerDeltaScaled.x = pointerDelta.x * forceMultiplier; 1165 | pointerDeltaScaled.y = pointerDelta.y * forceMultiplier; 1166 | } 1167 | const pointerSpeed = Math.hypot(pointerDelta.x, pointerDelta.y); 1168 | const pointerSpeedScaled = pointerSpeed * forceMultiplier; 1169 | 1170 | touchPoints.forEach((p) => (p.life -= simTime)); 1171 | 1172 | if (pointerIsDown) { 1173 | touchPoints.push({ 1174 | x: pointerScene.x, 1175 | y: pointerScene.y, 1176 | life: touchPointLife, 1177 | }); 1178 | } 1179 | 1180 | while (touchPoints[0] && touchPoints[0].life <= 0) { 1181 | touchPoints.shift(); 1182 | } 1183 | 1184 | PERF_START("entities"); 1185 | 1186 | spawnTime -= simTime; 1187 | if (spawnTime <= 0) { 1188 | if (spawnExtra > 0) { 1189 | spawnExtra--; 1190 | spawnTime = spawnExtraDelay; 1191 | } else { 1192 | spawnTime = getSpawnDelay(); 1193 | } 1194 | const target = getTarget(); 1195 | const spawnRadius = Math.min(centerX * 0.8, maxSpawnX); 1196 | target.x = Math.random() * spawnRadius * 2 - spawnRadius; 1197 | target.y = centerY + targetHitRadius * 2; 1198 | target.z = Math.random() * targetRadius * 2 - targetRadius; 1199 | target.xD = Math.random() * ((target.x * -2) / 120); 1200 | target.yD = -20; 1201 | targets.push(target); 1202 | } 1203 | 1204 | const leftBound = -centerX + targetRadius; 1205 | const rightBound = centerX - targetRadius; 1206 | const ceiling = -centerY - 120; 1207 | const boundDamping = 0.4; 1208 | 1209 | targetLoop: for (let i = targets.length - 1; i >= 0; i--) { 1210 | const target = targets[i]; 1211 | target.x += target.xD * simSpeed; 1212 | target.y += target.yD * simSpeed; 1213 | 1214 | if (target.y < ceiling) { 1215 | target.y = ceiling; 1216 | target.yD = 0; 1217 | } 1218 | 1219 | if (target.x < leftBound) { 1220 | target.x = leftBound; 1221 | target.xD *= -boundDamping; 1222 | } else if (target.x > rightBound) { 1223 | target.x = rightBound; 1224 | target.xD *= -boundDamping; 1225 | } 1226 | 1227 | if (target.z < backboardZ) { 1228 | target.z = backboardZ; 1229 | target.zD *= -boundDamping; 1230 | } 1231 | 1232 | target.yD += gravity * simSpeed; 1233 | target.rotateX += target.rotateXD * simSpeed; 1234 | target.rotateY += target.rotateYD * simSpeed; 1235 | target.rotateZ += target.rotateZD * simSpeed; 1236 | target.transform(); 1237 | target.project(); 1238 | 1239 | if (target.y > centerY + targetHitRadius * 2) { 1240 | targets.splice(i, 1); 1241 | returnTarget(target); 1242 | if (isInGame()) { 1243 | if (isCasualGame()) { 1244 | incrementScore(-25); 1245 | } else { 1246 | endGame(); 1247 | } 1248 | } 1249 | continue; 1250 | } 1251 | 1252 | const hitTestCount = Math.ceil((pointerSpeed / targetRadius) * 2); 1253 | for (let ii = 1; ii <= hitTestCount; ii++) { 1254 | const percent = 1 - ii / hitTestCount; 1255 | const hitX = pointerScene.x - pointerDelta.x * percent; 1256 | const hitY = pointerScene.y - pointerDelta.y * percent; 1257 | const distance = Math.hypot( 1258 | hitX - target.projected.x, 1259 | hitY - target.projected.y 1260 | ); 1261 | 1262 | if (distance <= targetHitRadius) { 1263 | if (!target.hit) { 1264 | target.hit = true; 1265 | 1266 | target.xD += pointerDeltaScaled.x * hitDampening; 1267 | target.yD += pointerDeltaScaled.y * hitDampening; 1268 | target.rotateXD += pointerDeltaScaled.y * 0.001; 1269 | target.rotateYD += pointerDeltaScaled.x * 0.001; 1270 | 1271 | const sparkSpeed = 7 + pointerSpeedScaled * 0.125; 1272 | 1273 | if (pointerSpeedScaled > minPointerSpeed) { 1274 | target.health--; 1275 | incrementScore(10); 1276 | 1277 | if (target.health <= 0) { 1278 | incrementCubeCount(1); 1279 | createBurst(target, forceMultiplier); 1280 | sparkBurst(hitX, hitY, 8, sparkSpeed); 1281 | if (target.wireframe) { 1282 | slowmoRemaining = slowmoDuration; 1283 | spawnTime = 0; 1284 | spawnExtra = 2; 1285 | } 1286 | targets.splice(i, 1); 1287 | returnTarget(target); 1288 | } else { 1289 | sparkBurst(hitX, hitY, 8, sparkSpeed); 1290 | glueShedSparks(target); 1291 | updateTargetHealth(target, 0); 1292 | } 1293 | } else { 1294 | incrementScore(5); 1295 | sparkBurst(hitX, hitY, 3, sparkSpeed); 1296 | } 1297 | } 1298 | continue targetLoop; 1299 | } 1300 | } 1301 | 1302 | target.hit = false; 1303 | } 1304 | 1305 | const fragBackboardZ = backboardZ + fragRadius; 1306 | const fragLeftBound = -width; 1307 | const fragRightBound = width; 1308 | 1309 | for (let i = frags.length - 1; i >= 0; i--) { 1310 | const frag = frags[i]; 1311 | frag.x += frag.xD * simSpeed; 1312 | frag.y += frag.yD * simSpeed; 1313 | frag.z += frag.zD * simSpeed; 1314 | 1315 | frag.xD *= simAirDrag; 1316 | frag.yD *= simAirDrag; 1317 | frag.zD *= simAirDrag; 1318 | 1319 | if (frag.y < ceiling) { 1320 | frag.y = ceiling; 1321 | frag.yD = 0; 1322 | } 1323 | 1324 | if (frag.z < fragBackboardZ) { 1325 | frag.z = fragBackboardZ; 1326 | frag.zD *= -boundDamping; 1327 | } 1328 | 1329 | frag.yD += gravity * simSpeed; 1330 | frag.rotateX += frag.rotateXD * simSpeed; 1331 | frag.rotateY += frag.rotateYD * simSpeed; 1332 | frag.rotateZ += frag.rotateZD * simSpeed; 1333 | frag.transform(); 1334 | frag.project(); 1335 | 1336 | if ( 1337 | frag.projected.y > centerY + targetHitRadius || 1338 | frag.projected.x < fragLeftBound || 1339 | frag.projected.x > fragRightBound || 1340 | frag.z > cameraFadeEndZ 1341 | ) { 1342 | frags.splice(i, 1); 1343 | returnFrag(frag); 1344 | continue; 1345 | } 1346 | } 1347 | 1348 | for (let i = sparks.length - 1; i >= 0; i--) { 1349 | const spark = sparks[i]; 1350 | spark.life -= simTime; 1351 | if (spark.life <= 0) { 1352 | sparks.splice(i, 1); 1353 | returnSpark(spark); 1354 | continue; 1355 | } 1356 | spark.x += spark.xD * simSpeed; 1357 | spark.y += spark.yD * simSpeed; 1358 | spark.xD *= simAirDragSpark; 1359 | spark.yD *= simAirDragSpark; 1360 | spark.yD += gravity * simSpeed; 1361 | } 1362 | 1363 | PERF_END("entities"); 1364 | 1365 | PERF_START("3D"); 1366 | 1367 | allVertices.length = 0; 1368 | allPolys.length = 0; 1369 | allShadowVertices.length = 0; 1370 | allShadowPolys.length = 0; 1371 | targets.forEach((entity) => { 1372 | allVertices.push(...entity.vertices); 1373 | allPolys.push(...entity.polys); 1374 | allShadowVertices.push(...entity.shadowVertices); 1375 | allShadowPolys.push(...entity.shadowPolys); 1376 | }); 1377 | 1378 | frags.forEach((entity) => { 1379 | allVertices.push(...entity.vertices); 1380 | allPolys.push(...entity.polys); 1381 | allShadowVertices.push(...entity.shadowVertices); 1382 | allShadowPolys.push(...entity.shadowPolys); 1383 | }); 1384 | 1385 | allPolys.forEach((p) => computePolyNormal(p, "normalWorld")); 1386 | allPolys.forEach(computePolyDepth); 1387 | allPolys.sort((a, b) => b.depth - a.depth); 1388 | 1389 | allVertices.forEach(projectVertex); 1390 | 1391 | allPolys.forEach((p) => computePolyNormal(p, "normalCamera")); 1392 | 1393 | PERF_END("3D"); 1394 | 1395 | PERF_START("shadows"); 1396 | 1397 | transformVertices( 1398 | allShadowVertices, 1399 | allShadowVertices, 1400 | 0, 1401 | 0, 1402 | 0, 1403 | TAU / 8, 1404 | 0, 1405 | 0, 1406 | 1, 1407 | 1, 1408 | 1 1409 | ); 1410 | 1411 | allShadowPolys.forEach((p) => computePolyNormal(p, "normalWorld")); 1412 | 1413 | const shadowDistanceMult = Math.hypot(1, 1); 1414 | const shadowVerticesLength = allShadowVertices.length; 1415 | for (let i = 0; i < shadowVerticesLength; i++) { 1416 | const distance = allVertices[i].z - backboardZ; 1417 | allShadowVertices[i].z -= shadowDistanceMult * distance; 1418 | } 1419 | transformVertices( 1420 | allShadowVertices, 1421 | allShadowVertices, 1422 | 0, 1423 | 0, 1424 | 0, 1425 | -TAU / 8, 1426 | 0, 1427 | 0, 1428 | 1, 1429 | 1, 1430 | 1 1431 | ); 1432 | allShadowVertices.forEach(projectVertex); 1433 | 1434 | PERF_END("shadows"); 1435 | 1436 | PERF_END("tick"); 1437 | } 1438 | 1439 | function draw(ctx, width, height, viewScale) { 1440 | PERF_START("draw"); 1441 | 1442 | const halfW = width / 2; 1443 | const halfH = height / 2; 1444 | 1445 | ctx.lineJoin = "bevel"; 1446 | 1447 | PERF_START("drawShadows"); 1448 | ctx.fillStyle = shadowColor; 1449 | ctx.strokeStyle = shadowColor; 1450 | allShadowPolys.forEach((p) => { 1451 | if (p.wireframe) { 1452 | ctx.lineWidth = 2; 1453 | ctx.beginPath(); 1454 | const { vertices } = p; 1455 | const vCount = vertices.length; 1456 | const firstV = vertices[0]; 1457 | ctx.moveTo(firstV.x, firstV.y); 1458 | for (let i = 1; i < vCount; i++) { 1459 | const v = vertices[i]; 1460 | ctx.lineTo(v.x, v.y); 1461 | } 1462 | ctx.closePath(); 1463 | ctx.stroke(); 1464 | } else { 1465 | ctx.beginPath(); 1466 | const { vertices } = p; 1467 | const vCount = vertices.length; 1468 | const firstV = vertices[0]; 1469 | ctx.moveTo(firstV.x, firstV.y); 1470 | for (let i = 1; i < vCount; i++) { 1471 | const v = vertices[i]; 1472 | ctx.lineTo(v.x, v.y); 1473 | } 1474 | ctx.closePath(); 1475 | ctx.fill(); 1476 | } 1477 | }); 1478 | PERF_END("drawShadows"); 1479 | 1480 | PERF_START("drawPolys"); 1481 | 1482 | allPolys.forEach((p) => { 1483 | if (!p.wireframe && p.normalCamera.z < 0) return; 1484 | 1485 | if (p.strokeWidth !== 0) { 1486 | ctx.lineWidth = 1487 | p.normalCamera.z < 0 ? p.strokeWidth * 0.5 : p.strokeWidth; 1488 | ctx.strokeStyle = 1489 | p.normalCamera.z < 0 ? p.strokeColorDark : p.strokeColor; 1490 | } 1491 | 1492 | const { vertices } = p; 1493 | const lastV = vertices[vertices.length - 1]; 1494 | const fadeOut = p.middle.z > cameraFadeStartZ; 1495 | 1496 | if (!p.wireframe) { 1497 | const normalLight = p.normalWorld.y * 0.5 + p.normalWorld.z * -0.5; 1498 | const lightness = 1499 | normalLight > 0 1500 | ? 0.1 1501 | : ((normalLight ** 32 - normalLight) / 2) * 0.9 + 0.1; 1502 | ctx.fillStyle = shadeColor(p.color, lightness); 1503 | } 1504 | 1505 | if (fadeOut) { 1506 | ctx.globalAlpha = Math.max( 1507 | 0, 1508 | 1 - (p.middle.z - cameraFadeStartZ) / cameraFadeRange 1509 | ); 1510 | } 1511 | 1512 | ctx.beginPath(); 1513 | ctx.moveTo(lastV.x, lastV.y); 1514 | for (let v of vertices) { 1515 | ctx.lineTo(v.x, v.y); 1516 | } 1517 | 1518 | if (!p.wireframe) { 1519 | ctx.fill(); 1520 | } 1521 | if (p.strokeWidth !== 0) { 1522 | ctx.stroke(); 1523 | } 1524 | 1525 | if (fadeOut) { 1526 | ctx.globalAlpha = 1; 1527 | } 1528 | }); 1529 | PERF_END("drawPolys"); 1530 | 1531 | PERF_START("draw2D"); 1532 | 1533 | ctx.strokeStyle = sparkColor; 1534 | ctx.lineWidth = sparkThickness; 1535 | ctx.beginPath(); 1536 | sparks.forEach((spark) => { 1537 | ctx.moveTo(spark.x, spark.y); 1538 | const scale = (spark.life / spark.maxLife) ** 0.5 * 1.5; 1539 | ctx.lineTo(spark.x - spark.xD * scale, spark.y - spark.yD * scale); 1540 | }); 1541 | ctx.stroke(); 1542 | 1543 | ctx.strokeStyle = touchTrailColor; 1544 | const touchPointCount = touchPoints.length; 1545 | for (let i = 1; i < touchPointCount; i++) { 1546 | const current = touchPoints[i]; 1547 | const prev = touchPoints[i - 1]; 1548 | if (current.touchBreak || prev.touchBreak) { 1549 | continue; 1550 | } 1551 | const scale = current.life / touchPointLife; 1552 | ctx.lineWidth = scale * touchTrailThickness; 1553 | ctx.beginPath(); 1554 | ctx.moveTo(prev.x, prev.y); 1555 | ctx.lineTo(current.x, current.y); 1556 | ctx.stroke(); 1557 | } 1558 | 1559 | PERF_END("draw2D"); 1560 | 1561 | PERF_END("draw"); 1562 | PERF_END("frame"); 1563 | 1564 | PERF_UPDATE(); 1565 | } 1566 | 1567 | function setupCanvases() { 1568 | const ctx = canvas.getContext("2d"); 1569 | const dpr = window.devicePixelRatio || 1; 1570 | let viewScale; 1571 | let width, height; 1572 | 1573 | function handleResize() { 1574 | const w = window.innerWidth; 1575 | const h = window.innerHeight; 1576 | viewScale = h / 1000; 1577 | width = w / viewScale; 1578 | height = h / viewScale; 1579 | canvas.width = w * dpr; 1580 | canvas.height = h * dpr; 1581 | canvas.style.width = w + "px"; 1582 | canvas.style.height = h + "px"; 1583 | } 1584 | 1585 | handleResize(); 1586 | window.addEventListener("resize", handleResize); 1587 | 1588 | let lastTimestamp = 0; 1589 | function frameHandler(timestamp) { 1590 | let frameTime = timestamp - lastTimestamp; 1591 | lastTimestamp = timestamp; 1592 | 1593 | raf(); 1594 | 1595 | if (isPaused()) return; 1596 | 1597 | if (frameTime < 0) { 1598 | frameTime = 17; 1599 | } else if (frameTime > 68) { 1600 | frameTime = 68; 1601 | } 1602 | 1603 | const halfW = width / 2; 1604 | const halfH = height / 2; 1605 | 1606 | pointerScene.x = pointerScreen.x / viewScale - halfW; 1607 | pointerScene.y = pointerScreen.y / viewScale - halfH; 1608 | 1609 | const lag = frameTime / 16.6667; 1610 | const simTime = gameSpeed * frameTime; 1611 | const simSpeed = gameSpeed * lag; 1612 | tick(width, height, simTime, simSpeed, lag); 1613 | 1614 | ctx.clearRect(0, 0, canvas.width, canvas.height); 1615 | const drawScale = dpr * viewScale; 1616 | ctx.scale(drawScale, drawScale); 1617 | ctx.translate(halfW, halfH); 1618 | draw(ctx, width, height, viewScale); 1619 | ctx.setTransform(1, 0, 0, 1, 0, 0); 1620 | } 1621 | const raf = () => requestAnimationFrame(frameHandler); 1622 | raf(); 1623 | } 1624 | 1625 | function handleCanvasPointerDown(x, y) { 1626 | if (!pointerIsDown) { 1627 | pointerIsDown = true; 1628 | pointerScreen.x = x; 1629 | pointerScreen.y = y; 1630 | if (isMenuVisible()) renderMenus(); 1631 | } 1632 | } 1633 | 1634 | function handleCanvasPointerUp() { 1635 | if (pointerIsDown) { 1636 | pointerIsDown = false; 1637 | touchPoints.push({ 1638 | touchBreak: true, 1639 | life: touchPointLife, 1640 | }); 1641 | if (isMenuVisible()) renderMenus(); 1642 | } 1643 | } 1644 | 1645 | function handleCanvasPointerMove(x, y) { 1646 | if (pointerIsDown) { 1647 | pointerScreen.x = x; 1648 | pointerScreen.y = y; 1649 | } 1650 | } 1651 | 1652 | if ("PointerEvent" in window) { 1653 | canvas.addEventListener("pointerdown", (event) => { 1654 | event.isPrimary && handleCanvasPointerDown(event.clientX, event.clientY); 1655 | }); 1656 | 1657 | canvas.addEventListener("pointerup", (event) => { 1658 | event.isPrimary && handleCanvasPointerUp(); 1659 | }); 1660 | 1661 | canvas.addEventListener("pointermove", (event) => { 1662 | event.isPrimary && handleCanvasPointerMove(event.clientX, event.clientY); 1663 | }); 1664 | 1665 | document.body.addEventListener("mouseleave", handleCanvasPointerUp); 1666 | } else { 1667 | let activeTouchId = null; 1668 | canvas.addEventListener("touchstart", (event) => { 1669 | if (!pointerIsDown) { 1670 | const touch = event.changedTouches[0]; 1671 | activeTouchId = touch.identifier; 1672 | handleCanvasPointerDown(touch.clientX, touch.clientY); 1673 | } 1674 | }); 1675 | canvas.addEventListener("touchend", (event) => { 1676 | for (let touch of event.changedTouches) { 1677 | if (touch.identifier === activeTouchId) { 1678 | handleCanvasPointerUp(); 1679 | break; 1680 | } 1681 | } 1682 | }); 1683 | canvas.addEventListener( 1684 | "touchmove", 1685 | (event) => { 1686 | for (let touch of event.changedTouches) { 1687 | if (touch.identifier === activeTouchId) { 1688 | handleCanvasPointerMove(touch.clientX, touch.clientY); 1689 | event.preventDefault(); 1690 | break; 1691 | } 1692 | } 1693 | }, 1694 | { passive: false } 1695 | ); 1696 | } 1697 | 1698 | setupCanvases(); 1699 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@100;200;300;400;500;600;700&display=swap"); 2 | 3 | body { 4 | margin: 0; 5 | background-color: #000; 6 | background: #1e3144; 7 | height: 100vh; 8 | overflow: hidden; 9 | font-family: "IBM Plex Sans Arabic", sans-serif; 10 | font-weight: bold; 11 | letter-spacing: 0.06em; 12 | color: rgb(255, 255, 255); 13 | max-width: 100%; 14 | } 15 | 16 | #c { 17 | display: block; 18 | touch-action: none; 19 | transform: translateZ(0); 20 | } 21 | 22 | .hud__score, 23 | .pause-btn { 24 | position: fixed; 25 | font-size: calc(14px + 2vw + 1vh); 26 | } 27 | 28 | .hud__score { 29 | top: 0.65em; 30 | left: 0.65em; 31 | pointer-events: none; 32 | user-select: none; 33 | } 34 | 35 | .cube-count-lbl { 36 | font-size: 0.46em; 37 | } 38 | 39 | .pause-btn { 40 | position: fixed; 41 | top: 0; 42 | right: 0; 43 | padding: 0.8em 0.65em; 44 | } 45 | 46 | .pause-btn > div { 47 | position: relative; 48 | width: 0.8em; 49 | height: 0.8em; 50 | opacity: 1; 51 | } 52 | 53 | .pause-btn > div::before, 54 | .pause-btn > div::after { 55 | content: ""; 56 | display: block; 57 | width: 34%; 58 | height: 100%; 59 | position: absolute; 60 | background-color: #fff; 61 | } 62 | 63 | .pause-btn > div::after { 64 | right: 0; 65 | } 66 | 67 | .slowmo { 68 | position: fixed; 69 | bottom: 0; 70 | width: 100%; 71 | pointer-events: none; 72 | opacity: 0; 73 | transition: opacity 0.4s; 74 | will-change: opacity; 75 | } 76 | 77 | .slowmo::before { 78 | content: "اسلوموشن"; 79 | display: block; 80 | font-size: calc(8px + 1vw + 0.5vh); 81 | margin-left: 0.5em; 82 | margin-bottom: 8px; 83 | } 84 | 85 | .slowmo::after { 86 | content: ""; 87 | display: block; 88 | position: fixed; 89 | bottom: 0; 90 | width: 100%; 91 | height: 1.5vh; 92 | background-color: rgba(0, 0, 0, 0.25); 93 | z-index: -1; 94 | } 95 | 96 | .slowmo__bar { 97 | height: 1.5vh; 98 | background-color: rgba(255, 255, 255, 0.75); 99 | transform-origin: 0 0; 100 | } 101 | 102 | .menus::before { 103 | content: ""; 104 | pointer-events: none; 105 | position: fixed; 106 | top: 0; 107 | right: 0; 108 | bottom: 0; 109 | left: 0; 110 | background-color: #000; 111 | opacity: 0; 112 | transition: opacity 0.2s; 113 | transition-timing-function: ease-in; 114 | } 115 | 116 | .menus.has-active::before { 117 | opacity: 0.08; 118 | transition-duration: 0.4s; 119 | transition-timing-function: ease-out; 120 | } 121 | 122 | .menus.interactive-mode::before { 123 | opacity: 0.02; 124 | } 125 | 126 | .menu { 127 | background: rgba(0, 0, 0, 0.63); 128 | pointer-events: none; 129 | position: fixed; 130 | top: 0; 131 | right: 0; 132 | bottom: 0; 133 | left: 0; 134 | display: flex; 135 | flex-direction: column; 136 | justify-content: center; 137 | align-items: center; 138 | user-select: none; 139 | text-align: center; 140 | color: rgba(255, 255, 255, 0.9); 141 | opacity: 0; 142 | visibility: hidden; 143 | transform: translateY(30px); 144 | transition-property: opacity, visibility, transform; 145 | transition-duration: 0.2s; 146 | transition-timing-function: ease-in; 147 | } 148 | 149 | .menu.active { 150 | opacity: 1; 151 | visibility: visible; 152 | transform: translateY(0); 153 | transition-duration: 0.4s; 154 | transition-timing-function: ease-out; 155 | } 156 | 157 | .menus.interactive-mode .menu.active { 158 | opacity: 0.6; 159 | } 160 | 161 | .menus:not(.interactive-mode) .menu.active > * { 162 | pointer-events: auto; 163 | } 164 | 165 | h1 { 166 | font-size: 4rem; 167 | line-height: 0.95; 168 | text-align: center; 169 | font-weight: bold; 170 | margin: 0 0.65em 1em; 171 | } 172 | 173 | h2 { 174 | font-size: 1.2rem; 175 | line-height: 1; 176 | text-align: center; 177 | font-weight: bold; 178 | margin: -1em 0.65em 1em; 179 | } 180 | 181 | .final-score-lbl { 182 | font-size: 5rem; 183 | margin: -0.2em 0 0; 184 | } 185 | 186 | .high-score-lbl { 187 | font-size: 1.2rem; 188 | margin: 0 0 2.5em; 189 | } 190 | 191 | button { 192 | display: block; 193 | position: relative; 194 | width: 200px; 195 | padding: 12px 20px; 196 | background: transparent; 197 | border: none; 198 | outline: none; 199 | user-select: none; 200 | font-family: "IBM Plex Sans Arabic", sans-serif; 201 | font-weight: bold; 202 | font-size: 1.8rem; 203 | color: #fff; 204 | opacity: 0.75; 205 | transition: opacity 0.3s; 206 | } 207 | 208 | button::before { 209 | content: ""; 210 | position: absolute; 211 | top: 0; 212 | right: 0; 213 | bottom: 0; 214 | left: 0; 215 | background-color: rgba(255, 255, 255, 0.15); 216 | transform: scale(0, 0); 217 | opacity: 0; 218 | transition: opacity 0.3s, transform 0.3s; 219 | } 220 | 221 | button:active { 222 | opacity: 1; 223 | } 224 | 225 | button:active::before { 226 | transform: scale(1, 1); 227 | opacity: 1; 228 | } 229 | 230 | .credits { 231 | position: fixed; 232 | width: 100%; 233 | left: 0; 234 | bottom: 20px; 235 | } 236 | 237 | a { 238 | color: white; 239 | } 240 | 241 | @media (min-width: 1101px) { 242 | button:hover { 243 | opacity: 1; 244 | } 245 | 246 | button:hover::before { 247 | transform: scale(1, 1); 248 | opacity: 1; 249 | border-radius: 10px; 250 | } 251 | } 252 | 253 | @media (max-width: 1100px) { 254 | h1 { 255 | font-size: 4.2rem; 256 | } 257 | button { 258 | font-size: 2.3rem; 259 | width: 300vw; 260 | padding: 12px 20px; 261 | border-radius: 10px; 262 | } 263 | .credits { 264 | font-size: 1.1rem; 265 | margin-bottom: 10px; 266 | } 267 | .menu--score h2, 268 | .final-score-lbl, 269 | .high-score-lbl { 270 | font-size: 1.1rem; 271 | } 272 | 273 | .score-lbl, 274 | .cube-count-lbl { 275 | font-size: 1.1rem; 276 | } 277 | 278 | .pause-btn div::before{ 279 | display: none; 280 | } 281 | .pause-btn div::before, 282 | .pause-btn div::after { 283 | content: "منو"; 284 | width: 0; 285 | height: 0; 286 | margin-right: 1rem; 287 | font-size: 1.1rem; 288 | } 289 | } 290 | @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@100;200;300;400;500;600;700&display=swap"); 291 | 292 | body { 293 | margin: 0; 294 | background-color: #000; 295 | background: #1e3144; 296 | height: 100vh; 297 | overflow: hidden; 298 | font-family: "IBM Plex Sans Arabic", sans-serif; 299 | font-weight: bold; 300 | letter-spacing: 0.06em; 301 | color: rgb(255, 255, 255); 302 | max-width: 100%; 303 | } 304 | 305 | #c { 306 | display: block; 307 | touch-action: none; 308 | transform: translateZ(0); 309 | } 310 | 311 | .hud__score, 312 | .pause-btn { 313 | position: fixed; 314 | font-size: calc(14px + 2vw + 1vh); 315 | } 316 | 317 | .hud__score { 318 | top: 0.65em; 319 | left: 0.65em; 320 | pointer-events: none; 321 | user-select: none; 322 | } 323 | 324 | .cube-count-lbl { 325 | font-size: 0.46em; 326 | } 327 | 328 | .pause-btn { 329 | position: fixed; 330 | top: 0; 331 | right: 0; 332 | padding: 0.8em 0.65em; 333 | } 334 | 335 | .pause-btn > div { 336 | position: relative; 337 | width: 0.8em; 338 | height: 0.8em; 339 | opacity: 1; 340 | } 341 | 342 | .pause-btn > div::before, 343 | .pause-btn > div::after { 344 | content: ""; 345 | display: block; 346 | width: 34%; 347 | height: 100%; 348 | position: absolute; 349 | background-color: #fff; 350 | } 351 | 352 | .pause-btn > div::after { 353 | right: 0; 354 | } 355 | 356 | .slowmo { 357 | position: fixed; 358 | bottom: 0; 359 | width: 100%; 360 | pointer-events: none; 361 | opacity: 0; 362 | transition: opacity 0.4s; 363 | will-change: opacity; 364 | } 365 | 366 | .slowmo::before { 367 | content: "اسلوموشن"; 368 | display: block; 369 | font-size: calc(8px + 1vw + 0.5vh); 370 | margin-left: 0.5em; 371 | margin-bottom: 8px; 372 | } 373 | 374 | .slowmo::after { 375 | content: ""; 376 | display: block; 377 | position: fixed; 378 | bottom: 0; 379 | width: 100%; 380 | height: 1.5vh; 381 | background-color: rgba(0, 0, 0, 0.25); 382 | z-index: -1; 383 | } 384 | 385 | .slowmo__bar { 386 | height: 1.5vh; 387 | background-color: rgba(255, 255, 255, 0.75); 388 | transform-origin: 0 0; 389 | } 390 | 391 | .menus::before { 392 | content: ""; 393 | pointer-events: none; 394 | position: fixed; 395 | top: 0; 396 | right: 0; 397 | bottom: 0; 398 | left: 0; 399 | background-color: #000; 400 | opacity: 0; 401 | transition: opacity 0.2s; 402 | transition-timing-function: ease-in; 403 | } 404 | 405 | .menus.has-active::before { 406 | opacity: 0.08; 407 | transition-duration: 0.4s; 408 | transition-timing-function: ease-out; 409 | } 410 | 411 | .menus.interactive-mode::before { 412 | opacity: 0.02; 413 | } 414 | 415 | .menu { 416 | background: rgba(0, 0, 0, 0.63); 417 | pointer-events: none; 418 | position: fixed; 419 | top: 0; 420 | right: 0; 421 | bottom: 0; 422 | left: 0; 423 | display: flex; 424 | flex-direction: column; 425 | justify-content: center; 426 | align-items: center; 427 | user-select: none; 428 | text-align: center; 429 | color: rgba(255, 255, 255, 0.9); 430 | opacity: 0; 431 | visibility: hidden; 432 | transform: translateY(30px); 433 | transition-property: opacity, visibility, transform; 434 | transition-duration: 0.2s; 435 | transition-timing-function: ease-in; 436 | } 437 | 438 | .menu.active { 439 | opacity: 1; 440 | visibility: visible; 441 | transform: translateY(0); 442 | transition-duration: 0.4s; 443 | transition-timing-function: ease-out; 444 | } 445 | 446 | .menus.interactive-mode .menu.active { 447 | opacity: 0.6; 448 | } 449 | 450 | .menus:not(.interactive-mode) .menu.active > * { 451 | pointer-events: auto; 452 | } 453 | 454 | h1 { 455 | font-size: 4rem; 456 | line-height: 0.95; 457 | text-align: center; 458 | font-weight: bold; 459 | margin: 0 0.65em 1em; 460 | } 461 | 462 | h2 { 463 | font-size: 1.2rem; 464 | line-height: 1; 465 | text-align: center; 466 | font-weight: bold; 467 | margin: -1em 0.65em 1em; 468 | } 469 | 470 | .final-score-lbl { 471 | font-size: 5rem; 472 | margin: -0.2em 0 0; 473 | } 474 | 475 | .high-score-lbl { 476 | font-size: 1.2rem; 477 | margin: 0 0 2.5em; 478 | } 479 | 480 | button { 481 | display: block; 482 | position: relative; 483 | width: 200px; 484 | padding: 12px 20px; 485 | background: transparent; 486 | border: none; 487 | outline: none; 488 | user-select: none; 489 | font-family: "IBM Plex Sans Arabic", sans-serif; 490 | font-weight: bold; 491 | font-size: 1.8rem; 492 | color: #fff; 493 | opacity: 0.75; 494 | transition: opacity 0.3s; 495 | } 496 | 497 | button::before { 498 | content: ""; 499 | position: absolute; 500 | top: 0; 501 | right: 0; 502 | bottom: 0; 503 | left: 0; 504 | background-color: rgba(255, 255, 255, 0.15); 505 | transform: scale(0, 0); 506 | opacity: 0; 507 | transition: opacity 0.3s, transform 0.3s; 508 | } 509 | 510 | button:active { 511 | opacity: 1; 512 | } 513 | 514 | button:active::before { 515 | transform: scale(1, 1); 516 | opacity: 1; 517 | } 518 | 519 | .credits { 520 | position: fixed; 521 | width: 100%; 522 | left: 0; 523 | bottom: 20px; 524 | } 525 | 526 | a { 527 | color: white; 528 | } 529 | 530 | @media (min-width: 1101px) { 531 | button:hover { 532 | opacity: 1; 533 | } 534 | 535 | button:hover::before { 536 | transform: scale(1, 1); 537 | opacity: 1; 538 | border-radius: 10px; 539 | } 540 | } 541 | 542 | @media (max-width: 1100px) { 543 | h1 { 544 | font-size: 4.2rem; 545 | } 546 | button { 547 | font-size: 2.3rem; 548 | width: 300vw; 549 | padding: 12px 20px; 550 | border-radius: 10px; 551 | } 552 | .credits { 553 | font-size: 1.1rem; 554 | margin-bottom: 10px; 555 | } 556 | .menu--score h2, 557 | .final-score-lbl, 558 | .high-score-lbl { 559 | font-size: 1.1rem; 560 | } 561 | 562 | .score-lbl, 563 | .cube-count-lbl { 564 | font-size: 1.1rem; 565 | } 566 | 567 | .pause-btn div::before{ 568 | display: none; 569 | } 570 | .pause-btn div::before, 571 | .pause-btn div::after { 572 | content: "منو"; 573 | width: 0; 574 | height: 0; 575 | margin-right: 1rem; 576 | font-size: 1.1rem; 577 | } 578 | } 579 | --------------------------------------------------------------------------------