Compare commits
157 Commits
d2b36adab1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c52a604f0f | |||
| 8e9a089d34 | |||
| d5893eb8ef | |||
| 1e006d46b9 | |||
| 0f84f90e05 | |||
| 4287edab71 | |||
| 5c2eaca35a | |||
| e7dc780173 | |||
| 9721b311eb | |||
| d3f6cf9b71 | |||
| 5b058a58b3 | |||
| 9fca05ae6e | |||
| 1a7628bb42 | |||
| bc4ba54c0a | |||
| 653befdc02 | |||
| 1b3f837bf0 | |||
| 375d47397e | |||
| efb6482238 | |||
| 7fd3c04a2d | |||
| 1e42c2160f | |||
| 74bf8521fb | |||
| fef08f64e8 | |||
| 3c8fc95e23 | |||
| f7b7b74e01 | |||
| af9e0c481a | |||
| 90eb3247e0 | |||
| 2a25dbe4b0 | |||
| d9397c4bcb | |||
| ca93423bf8 | |||
| ae8dcb7077 | |||
| 6ed614d536 | |||
| 8c5b704b3c | |||
| 1b0f1c07d2 | |||
| c8eb029987 | |||
| 825fbca97b | |||
| ce94604fc0 | |||
| 07daa4a9cf | |||
| abf562fd89 | |||
| cae3dc9af5 | |||
| a75329f985 | |||
| 31eca05faf | |||
| 7f6795109b | |||
| 8ed998f255 | |||
| aa2475dc3a | |||
| a0893fd881 | |||
| 1a026db655 | |||
| f8081583c5 | |||
| ce1181df62 | |||
| 3345a50803 | |||
| c9b242c9c2 | |||
| 935343d301 | |||
| 92d953ef62 | |||
| b227690b31 | |||
| e1da884441 | |||
| b34a968dd2 | |||
| 401218bdbe | |||
| 32d4126873 | |||
| 85237739bc | |||
| 12fb307041 | |||
| 6004cbbbde | |||
| 38a9dcfad4 | |||
| dd25b0a891 | |||
| 3a657e4c38 | |||
| fcb12f89e7 | |||
| 7acb3a6def | |||
| 367f252444 | |||
| cfa73565f0 | |||
| 4c68b05db1 | |||
| d2a0e241c8 | |||
| b0dbb06dae | |||
| 02725494cd | |||
| 6f0a540bb3 | |||
| b516de1b2f | |||
| c42ee23b5f | |||
| 0521e2f0ba | |||
| 8b3759f253 | |||
| 945d349319 | |||
| d0120ca2a6 | |||
| eadae0205f | |||
| a4be8e064c | |||
| 7e245011ef | |||
| a7eff47b97 | |||
| 69b2b77fe9 | |||
| 8279b1188f | |||
| bf4b296ec0 | |||
| dbc56a19e9 | |||
| 0dee582cd0 | |||
| cc5c89540d | |||
| c3c3095153 | |||
| 4b357d5250 | |||
| 9877f21818 | |||
| f0f3b2aca5 | |||
| 013fb0bce7 | |||
| bdf99b41b7 | |||
| c4979b5890 | |||
| a6c48989d0 | |||
| 058fdd8f8b | |||
| 59158d68b8 | |||
| a2dc4678c8 | |||
| d590c41f95 | |||
| 11675fa9a2 | |||
| f85ad3f3e7 | |||
| 791f594670 | |||
| 6d4f7a8dd3 | |||
| ae1fa3e4ad | |||
| c90db794a6 | |||
| 0632bf7e77 | |||
| 68694e415c | |||
| 7febe7f8e7 | |||
| 694d4aac4a | |||
| 73aad137fb | |||
| 5db16ad50e | |||
| ab63295d25 | |||
| b132b4224f | |||
| fcd23a817d | |||
| 1f33ea3aa6 | |||
| 1499dddb10 | |||
| 3d70e2689b | |||
| 307042003e | |||
| 47016ccde4 | |||
| 33019ebf87 | |||
| 4962baf442 | |||
| abb5744083 | |||
| 3aeec768cb | |||
| 829b8094eb | |||
| 03e704f2f5 | |||
| 7f0bb90fad | |||
| df0b5f2475 | |||
| 2904062dfe | |||
| b0775a8d1f | |||
| e43675b989 | |||
| 78e03db362 | |||
| e498459afa | |||
| 107bd38d8a | |||
| d84ba18040 | |||
| 627d9f61ab | |||
| 5c45488760 | |||
| d5eb58334c | |||
| b9ff06f32d | |||
| b532d8b2aa | |||
| c1cda864a1 | |||
| 15db0673d6 | |||
| 4a33194f7a | |||
| 18ba62e853 | |||
| 267bb383fa | |||
| af24f0a6ef | |||
| 2ba22ee7da | |||
| 6f7896cffc | |||
| 951dfcdcb8 | |||
| af0bc56099 | |||
| 68159adb82 | |||
| 55b4aebd1b | |||
| 54aa01b637 | |||
| 9f8b5dfb6c | |||
| dc7a19ad2d | |||
| c2d1bef24f | |||
| de4a3e2871 |
5
README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# teTra
|
||||||
|
|
||||||
|
Falling blocks web game made with three.js librairy
|
||||||
|
|
||||||
|

|
||||||
BIN
audio/Tetris_CheDDer_OC_ReMix.mp3
Normal file
BIN
audio/Tetris_MkVaffQuasi_Ultimix_OC_ReMix.mp3
Normal file
BIN
audio/Tetris_T-Spin_OC_ReMix.mp3
Normal file
BIN
audio/benevolence.m4a
Normal file
BIN
audio/hard-drop.wav
Normal file
BIN
audio/line-clear.ogg
Normal file
BIN
audio/tetris.ogg
BIN
audio/tetris.xt
70
css/loading.css
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#loadingDiv {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: lightsteelblue;
|
||||||
|
text-align: center;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 30vh;
|
||||||
|
background-color: black;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
perspective: 200px;
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tetromino {
|
||||||
|
position: relative;
|
||||||
|
top: 2em;
|
||||||
|
left: 2em;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transform: translateZ(0.5em);
|
||||||
|
animation: spinCube 5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinCube {
|
||||||
|
0% { transform: translateZ(0.5em) rotateX( 0deg) rotateY( 0deg); }
|
||||||
|
100% { transform: translateZ(0.5em) rotateX(360deg) rotateY(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.mino {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
position: absolute;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.T.tetromino .first.mino { top: -0.5em; left: -1em; }
|
||||||
|
.T.tetromino .second.mino { top: -0.5em; left: 0em; }
|
||||||
|
.T.tetromino .third.mino { top: -0.5em; left: 1em; }
|
||||||
|
.T.tetromino .fourth.mino { top: 0.5em; left: 0em; }
|
||||||
|
|
||||||
|
.face {
|
||||||
|
position: absolute;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
padding: 0;
|
||||||
|
background: hsla(240, 100%, 0%, 0.4);
|
||||||
|
border: 1px solid hsla(240, 100%, 70%, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.front.face { transform: rotateY( 0deg) translateZ(0.5em); }
|
||||||
|
.right.face { transform: rotateY( 90deg) translateZ(0.5em); }
|
||||||
|
.back.face { transform: rotateY(180deg) translateZ(0.5em); }
|
||||||
|
.left.face { transform: rotateY(-90deg) translateZ(0.5em); }
|
||||||
|
.top.face { transform: rotateX( 90deg) translateZ(0.5em); }
|
||||||
|
.bottom.face { transform: rotateX(-90deg) translateZ(0.5em); }
|
||||||
@ -1,29 +1,67 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #222;
|
||||||
|
font-family: -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
|
||||||
|
"Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji",
|
||||||
|
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lil-menu {
|
||||||
|
--background-color: rgba(33, 37, 41, 30%);
|
||||||
|
--width: 200px;
|
||||||
|
}
|
||||||
@supports (backdrop-filter: blur()) {
|
@supports (backdrop-filter: blur()) {
|
||||||
.card,
|
.lil-menu {
|
||||||
.modal-content {
|
|
||||||
background-color: rgba(33, 37, 41, 30%);
|
|
||||||
backdrop-filter: blur(15px);
|
backdrop-filter: blur(15px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lil-gui.autoPlace {
|
||||||
|
top: inherit;
|
||||||
|
bottom: 15px;
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lil-menu.root > .title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lil-menu .controller.disabled {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
#messagesSpan {
|
#messagesSpan {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10%;
|
top: 10%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
width: 50%;
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0);
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
text-shadow: 1px 1px rgba(0, 0, 0, 0.8);
|
font-weight: 400;
|
||||||
font-size: 4vmin;
|
line-height: 1.5;
|
||||||
|
font-size: 3vmin;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#messagesSpan div {
|
#messagesSpan div {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: calc(1.375rem + 1.5vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes show-level-animation {
|
@keyframes show-level-animation {
|
||||||
@ -125,3 +163,29 @@ body {
|
|||||||
animation-timing-function: (0.4, 0, 0.6, 1);
|
animation-timing-function: (0.4, 0, 0.6, 1);
|
||||||
animation-duration: 2s;
|
animation-duration: 2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pause canvas {
|
||||||
|
filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pauseSpan {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause #pauseSpan {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
filter: blur(2px);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
color: rgba(255, 255, 255, 20%);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 20vh;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: .1em;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
BIN
favicon.ico
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 118 KiB |
BIN
images/dark.jpg
|
Before Width: | Height: | Size: 857 KiB After Width: | Height: | Size: 977 KiB |
BIN
images/edge.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
images/edges.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
images/fond_etoile.gif
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
images/plasma.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
images/plasma2.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
images/sprites.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
184
index.html
@ -4,154 +4,78 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>teTra</title>
|
<title>ᵀᴱTᴿᴬ</title>
|
||||||
<link rel="icon" href="favicon.ico">
|
<link rel="icon" href="favicon.ico">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="css/loading.css">
|
||||||
<link rel="stylesheet" href="style.css">
|
<meta property="og:title" content="ᵀᴱTᴿᴬ"/>
|
||||||
|
<meta property="og:type" content="game"/>
|
||||||
|
<meta property="og:url" content="https://adrien.malingrey.fr/jeux/tetra/"/>
|
||||||
|
<meta property="og:image" content="https://adrien.malingrey.fr/jeux/tetra/thumbnail.png"/>
|
||||||
|
<meta property="og:image:width" content="250"/>
|
||||||
|
<meta property="og:image:height" content="250"/>
|
||||||
|
<meta property="og:description" content="Des blocs qui tombent en 3D"/>
|
||||||
|
<meta property="og:locale" content="fr_FR"/>
|
||||||
|
<meta property="og:site_name" content="adrien.malingrey.fr"/>
|
||||||
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
|
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
"three": "https://unpkg.com/three@0.152.2/build/three.module.js",
|
"three": "https://unpkg.com/three@0.169/build/three.module.js?module",
|
||||||
"three/addons/": "https://unpkg.com/three@0.152.2/examples/jsm/"
|
"three/addons/": "https://unpkg.com/three@0.169/examples/jsm/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-bs-theme="dark">
|
<body>
|
||||||
|
<span id="loadingDiv">
|
||||||
<div class="modal fade" id="settingsModal" data-bs-backdrop="static" data-bs-keyboard="false">
|
<div class="scene">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="T tetromino">
|
||||||
<div class="modal-content">
|
<div class="first mino">
|
||||||
<div class="modal-header">
|
<div class="front face"></div>
|
||||||
<h1 id="titleHeader" class="modal-title w-100 text-center">te<strong>T</strong>ra</h1>
|
<div class="back face"></div>
|
||||||
|
<div class="left face"></div>
|
||||||
|
<div class="right face"></div>
|
||||||
|
<div class="top face"></div>
|
||||||
|
<div class="bottom face"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="second mino">
|
||||||
<form name="settingsForm" class="needs-validation" novalidate>
|
<div class="front face"></div>
|
||||||
<fieldset id="keyBindFielset" class="row g-2 mb-3 align-items-center text-center">
|
<div class="back face"></div>
|
||||||
<legend class="text-start">Commandes</legend>
|
<div class="left face"></div>
|
||||||
<label for="moveLeftInput" title="Gauche" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center">
|
<div class="right face"></div>
|
||||||
<i class="bi bi-arrow-left"></i>
|
<div class="top face"></div>
|
||||||
</label>
|
<div class="bottom face"></div>
|
||||||
<div class="col-sm-4">
|
|
||||||
<input name="moveLeft" id="moveLeftInput" type="text" class="form-control text-center" value="←" onclick="changeKey(this)" placeholder="Touche ?" required>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="third mino">
|
||||||
<input name="moveRight" id="moveRightInput" type="text" class="form-control text-center" value="→" onclick="changeKey(this)" placeholder="Touche ?" required>
|
<div class="front face"></div>
|
||||||
</div>
|
<div class="back face"></div>
|
||||||
<label for="moveRightInput" title="Droite" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center">
|
<div class="left face"></div>
|
||||||
<i class="bi bi-arrow-right"></i>
|
<div class="right face"></div>
|
||||||
</label>
|
<div class="top face"></div>
|
||||||
<label for="rotateCounterclockwiseInput" title="Rotation anti-horaire" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center">
|
<div class="bottom face"></div>
|
||||||
<i class="bi bi-arrow-counterclockwise"></i>
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input name="rotateCounterclockwise" id="rotateCounterclockwiseInput" type="text" class="form-control text-center" value="w" onclick="changeKey(this)" placeholder="Touche ?" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input name="rotateClockwise" id="rotateClockwiseInput" type="text" class="form-control text-center" value="↑" onclick="changeKey(this)" placeholder="Touche ?" required>
|
|
||||||
</div>
|
|
||||||
<label for="rotateClockwiseInput" title="Rotation horaire" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center">
|
|
||||||
<i class="bi bi-arrow-clockwise"></i>
|
|
||||||
</label>
|
|
||||||
<label for="softDropInput" title="Chute lente" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center">
|
|
||||||
<i class="bi bi-arrow-down-short"></i>
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input name="softDrop" id="softDropInput" type="text" class="form-control text-center" value="↓" onclick="changeKey(this)" placeholder="Touche ?" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input name="hardDrop" id="hardDropInput" type="text" class="form-control text-center" value="Espace" onclick="changeKey(this)" placeholder="Touche ?" required>
|
|
||||||
</div>
|
|
||||||
<label for="hardDropInput" title="Chute rapide" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center">
|
|
||||||
<i class="bi bi-download"></i>
|
|
||||||
</label>
|
|
||||||
<label for="holdInput" title="Échanger la pièce" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center">
|
|
||||||
<i class="bi bi-arrow-left-right"></i>
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input name="hold" id="holdInput" type="text" class="form-control text-center" value="c" onclick="changeKey(this)" placeholder="Touche ?" required>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input name="pause" id="pauseInput" type="text" class="form-control text-center" value="Échap" onclick="changeKey(this)" placeholder="Touche ?" required>
|
|
||||||
</div>
|
|
||||||
<label for="pauseInput" title="Pause" class="col-sm-2 col-form-label d-flex align-items-center justify-content-center">
|
|
||||||
<i class="bi bi-pause"></i>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset id="autorepearFieldset" class="row g-2 mb-3 align-items-center text-center">
|
|
||||||
<legend class="text-start">Répétition automatique</legend>
|
|
||||||
<label for="arrInput" class="col-sm-2 col-form-label" title="Automatic Repeat Rate : période de répétition de l'action">ARR</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<div class="input-group">
|
|
||||||
<input name="arr" id="arrInput" type="number" class="form-control text-center" value="50" min="2" max="200" step="1">
|
|
||||||
<div class="input-group-text">ms</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<div class="input-group">
|
|
||||||
<input name="das" id="dasInput" type="number" class="form-control text-center" value="300" min="100" max="500" step="5">
|
|
||||||
<div class="input-group-text">ms</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<label for="dasInput" class="col-sm-2 col-form-label" title="Delayed AutoShift : délai initial avant répétition">DAS</label>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset id="audioFieldset" class="row g-2 mb-3 align-items-center text-center">
|
|
||||||
<legend class="text-start">Volume</legend>
|
|
||||||
<label for="musicVolumeInput" class="col-sm-2 col-form-label">Musique</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input name="musicVolume" id="musicVolumeInput" type="range" class="form-range" value=".5" min="0" max="1" step="0.01">
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input name="sfxVolume" id="sfxVolumeInput" type="range" class="form-range" value=".5" min="0" max="1" step="0.01">
|
|
||||||
</div>
|
|
||||||
<label for="sfxVolumeInput" class="col-sm-2 col-form-label">SFX</label>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="row g-2 mb-3 align-items-center text-center">
|
|
||||||
<legend class="text-start">Partie</legend>
|
|
||||||
<label for="levelInput" class="col-sm-2 col-form-label text-center">Niveau</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input name="startLevel" id="levelInput" type="number" class="form-control text-center" value="1" min="1" max="15">
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<button id="resumeButton" type="submit" class="btn btn-primary w-100" autofocus>Jouer</button>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span id="messagesSpan" class ="position-absolute"></span>
|
|
||||||
|
|
||||||
<div class="modal fade" id="statsModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 class="modal-title w-100 text-center">Fin</h2>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body p-0">
|
|
||||||
<table class="table mb-0">
|
|
||||||
<tr><th>Score </th><td id="statsModalScoreCell"> </td><th>Niveau </th><td id="statsModalLevelCell"> </td></tr>
|
|
||||||
<tr><th>Meilleur score</th><td id="statsModalHighScoreCell"> </td><th>Temps </th><td id="statsModalTimeCell"> </td></tr>
|
|
||||||
<tr><th>Lignes </th><td id="statsModaltotalClearedLines"></td><th>Lignes par minute </th><td id="statsModaltotalClearedLinesPM"></td></tr>
|
|
||||||
<tr><th>Quatris </th><td id="statsModalNbQuatris"> </td><th>Plus long combo </th><td id="statsModalMaxCombo"> </td></tr>
|
|
||||||
<tr><th>Pirouettes </th><td id="statsModalNbTSpin"> </td><th>Plus long bout à bout</th><td id="statsModalMaxB2B"> </td></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button id="restartButton" type="button" class="btn btn-primary">Rejouer ?</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="fourth mino">
|
||||||
|
<div class="front face"></div>
|
||||||
|
<div class="back face"></div>
|
||||||
|
<div class="left face"></div>
|
||||||
|
<div class="right face"></div>
|
||||||
|
<div class="top face"></div>
|
||||||
|
<div class="bottom face"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Chargement</div>
|
||||||
|
<div id="loadingPercent">0%</div>
|
||||||
</div>
|
</div>
|
||||||
|
</span>
|
||||||
|
<span id="messagesSpan"></span>
|
||||||
|
<span id="pauseSpan" tabindex="1">II</span>
|
||||||
|
<audio id="music" src="audio/benevolence.m4a" loop></audio>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
|
||||||
<script type="module" src="app.js"></script>
|
<script type="module" src="app.js"></script>
|
||||||
|
<script>navigator?.serviceWorker?.register('./jsm/service-worker.js')</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
21
jsm/CameraControls.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||||
|
|
||||||
|
|
||||||
|
export default class CameraControls extends OrbitControls {
|
||||||
|
constructor(camera, domElement) {
|
||||||
|
super(camera, domElement)
|
||||||
|
this.autoRotate
|
||||||
|
this.enableDamping = true
|
||||||
|
this.dampingFactor = 0.04
|
||||||
|
this.maxDistance = 21
|
||||||
|
this.keys = {}
|
||||||
|
this.minPolarAngle = 1.05
|
||||||
|
this.maxPolarAngle = 2.1
|
||||||
|
this.minAzimuthAngle = 0.9 - Math.PI / 2
|
||||||
|
this.maxAzimuthAngle = 2.14 - Math.PI / 2
|
||||||
|
this.target.set(5, 7.5, 0)
|
||||||
|
|
||||||
|
this.addEventListener("start", () => domElement.style.cursor = "grabbing")
|
||||||
|
this.addEventListener("end", () => domElement.style.cursor = "grab")
|
||||||
|
}
|
||||||
|
}
|
||||||
179
jsm/Menu.js
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import * as THREE from 'three'
|
||||||
|
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
|
||||||
|
import { environment } from './Tetrominoes.js'
|
||||||
|
|
||||||
|
|
||||||
|
export class Menu extends GUI {
|
||||||
|
constructor(game, settings, stats, scene, minoes, playfield) {
|
||||||
|
super({title: "ᵀᴱTᴿᴬ"})
|
||||||
|
|
||||||
|
this.startButton = this.add(game, "start").name("Jouer").hide()
|
||||||
|
this.pauseButton = this.add(game, "pause").name("Pause").hide()
|
||||||
|
this.resumeButton = this.add(game, "resume").name("Reprendre").hide()
|
||||||
|
|
||||||
|
this.stats = this.addFolder("Statistiques").hide()
|
||||||
|
this.stats.add(stats, "time").name("Temps").disable().listen()
|
||||||
|
this.stats.add(stats, "score").name("Score").disable().listen()
|
||||||
|
this.stats.add(stats, "highScore").name("Meilleur score").disable().listen()
|
||||||
|
this.stats.add(stats, "level").name("Niveau").disable().listen()
|
||||||
|
this.stats.add(stats, "totalClearedLines").name("Lignes").disable().listen()
|
||||||
|
this.stats.add(stats, "goal").name("Objectif").disable().listen()
|
||||||
|
this.stats.add(stats, "nbTetra").name("teTras").disable().listen()
|
||||||
|
this.stats.add(stats, "nbTSpin").name("Pirouettes").disable().listen()
|
||||||
|
this.stats.add(stats, "maxCombo").name("Combos max").disable().listen()
|
||||||
|
this.stats.add(stats, "maxB2B").name("BàB max").disable().listen()
|
||||||
|
|
||||||
|
this.settings = this.addFolder("Options")
|
||||||
|
this.settings.add(settings, "startLevel").name("Niveau initial").min(1).max(15).step(1)
|
||||||
|
|
||||||
|
this.settings.add(settings, "theme", ["Plasma", "Espace", "Rétro"]).name("Thème").onChange(theme => {
|
||||||
|
scene.theme = theme
|
||||||
|
minoes.theme = theme
|
||||||
|
if (theme == "Rétro") {
|
||||||
|
playfield.edge.visible = false
|
||||||
|
playfield.retroEdge.visible = true
|
||||||
|
music.src = "audio/Tetris_MkVaffQuasi_Ultimix_OC_ReMix.mp3"
|
||||||
|
} else {
|
||||||
|
playfield.edge.visible = true
|
||||||
|
playfield.retroEdge.visible = false
|
||||||
|
music.src = "audio/benevolence.m4a"
|
||||||
|
}
|
||||||
|
if (dev) changeMaterial()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.settings.key = this.settings.addFolder("Commandes").open()
|
||||||
|
let moveLeftKeyController = this.settings.key.add(settings.key, "moveLeft").name('Gauche')
|
||||||
|
moveLeftKeyController.domElement.onclick = this.changeKey(moveLeftKeyController)
|
||||||
|
let moveRightKeyController = this.settings.key.add(settings.key, "moveRight").name('Droite')
|
||||||
|
moveRightKeyController.domElement.onclick = this.changeKey(moveRightKeyController)
|
||||||
|
let rotateCWKeyController = this.settings.key.add(settings.key, "rotateCW").name('Rotation horaire')
|
||||||
|
rotateCWKeyController.domElement.onclick = this.changeKey(rotateCWKeyController)
|
||||||
|
let rotateCCWKeyController = this.settings.key.add(settings.key, "rotateCCW").name('anti-horaire')
|
||||||
|
rotateCCWKeyController.domElement.onclick = this.changeKey(rotateCCWKeyController)
|
||||||
|
let softDropKeyController = this.settings.key.add(settings.key, "softDrop").name('Chute lente')
|
||||||
|
softDropKeyController.domElement.onclick = this.changeKey(softDropKeyController)
|
||||||
|
let hardDropKeyController = this.settings.key.add(settings.key, "hardDrop").name('Chute rapide')
|
||||||
|
hardDropKeyController.domElement.onclick = this.changeKey(hardDropKeyController)
|
||||||
|
let holdKeyController = this.settings.key.add(settings.key, "hold").name('Garder')
|
||||||
|
holdKeyController.domElement.onclick = this.changeKey(holdKeyController)
|
||||||
|
let pauseKeyController = this.settings.key.add(settings.key, "pause").name('Pause')
|
||||||
|
pauseKeyController.domElement.onclick = this.changeKey(pauseKeyController)
|
||||||
|
|
||||||
|
this.settings.delay = this.settings.addFolder("Répétition automatique").open()
|
||||||
|
this.settings.delay.add(settings,"arrDelay").name("ARR (ms)").min(2).max(200).step(1);
|
||||||
|
this.settings.delay.add(settings,"dasDelay").name("DAS (ms)").min(100).max(500).step(5);
|
||||||
|
|
||||||
|
this.settings.volume = this.settings.addFolder("Volume").open()
|
||||||
|
this.settings.volume.add(settings,"musicVolume").name("Musique").min(0).max(100).step(1).onChange(volume => {
|
||||||
|
scene.music.volume = settings.musicVolume / 100
|
||||||
|
})
|
||||||
|
this.settings.volume.add(settings,"sfxVolume").name("Effets").min(0).max(100).step(1).onChange(volume => {
|
||||||
|
scene.lineClearSound.setVolume(volume/100)
|
||||||
|
scene.tetrisSound.setVolume(volume/100)
|
||||||
|
scene.hardDropSound.setVolume(volume/100)
|
||||||
|
})
|
||||||
|
|
||||||
|
let material
|
||||||
|
function changeMaterial() {
|
||||||
|
material?.destroy()
|
||||||
|
material = dev.addFolder("minoes material")
|
||||||
|
material.add(minoes.material, "constructor", ["MeshBasicMaterial", "MeshStandardMaterial", "MeshPhysicalMaterial"]).listen().onChange(type => {
|
||||||
|
switch(type) {
|
||||||
|
case "MeshBasicMaterial":
|
||||||
|
minoes.material = new THREE.MeshBasicMaterial({
|
||||||
|
envMap: environment,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.5,
|
||||||
|
reflectivity: 0.9,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "MeshStandardMaterial":
|
||||||
|
minoes.material = new THREE.MeshStandardMaterial({
|
||||||
|
envMap: environment,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.7,
|
||||||
|
roughness: 0.48,
|
||||||
|
metalness: 0.67,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "MeshPhysicalMaterial":
|
||||||
|
minoes.material = new THREE.MeshPhysicalMaterial({
|
||||||
|
envMap: environment,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.7,
|
||||||
|
roughness: 0.5,
|
||||||
|
ior: 1.8,
|
||||||
|
metalness: 0.9,
|
||||||
|
transmission: 1,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
minoes.update = minoes.updateColor
|
||||||
|
changeMaterial()
|
||||||
|
})
|
||||||
|
|
||||||
|
let minoMaterial = minoes.material instanceof Array ? minoes.material[0] : minoes.material
|
||||||
|
if ("opacity" in minoMaterial) material.add(minoMaterial, "opacity" ).min(0).max(1)
|
||||||
|
if ("reflectivity" in minoMaterial) material.add(minoMaterial, "reflectivity" ).min(0).max(1)
|
||||||
|
if ("roughness" in minoMaterial) material.add(minoMaterial, "roughness" ).min(0).max(1)
|
||||||
|
if ("bumpScale" in minoMaterial) material.add(minoMaterial, "bumpScale" ).min(0).max(5)
|
||||||
|
if ("metalness" in minoMaterial) material.add(minoMaterial, "metalness" ).min(0).max(1)
|
||||||
|
if ("attenuationDistance" in minoMaterial) material.add(minoMaterial, "attenuationDistance").min(0)
|
||||||
|
if ("ior" in minoMaterial) material.add(minoMaterial, "ior" ).min(1).max(2)
|
||||||
|
if ("sheen" in minoMaterial) material.add(minoMaterial, "sheen" ).min(0).max(1)
|
||||||
|
if ("sheenRoughness" in minoMaterial) material.add(minoMaterial, "sheenRoughness" ).min(0).max(1)
|
||||||
|
if ("specularIntensity" in minoMaterial) material.add(minoMaterial, "specularIntensity" ).min(0).max(1)
|
||||||
|
if ("thickness" in minoMaterial) material.add(minoMaterial, "thickness" ).min(0).max(5)
|
||||||
|
if ("transmission" in minoMaterial) material.add(minoMaterial, "transmission" ).min(0).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
let dev
|
||||||
|
if (window.location.search.includes("dev")) {
|
||||||
|
dev = this.addFolder("dev")
|
||||||
|
let cameraPosition = dev.addFolder("camera").close()
|
||||||
|
cameraPosition.add(scene.camera.position, "x").listen()
|
||||||
|
cameraPosition.add(scene.camera.position, "y").listen()
|
||||||
|
cameraPosition.add(scene.camera.position, "z").listen()
|
||||||
|
cameraPosition.add(scene.camera, "fov", 0, 200).onChange(() => scene.camera.updateProjectionMatrix()).listen()
|
||||||
|
|
||||||
|
let light = dev.addFolder("lights intensity").close()
|
||||||
|
light.add(scene.ambientLight, "intensity").name("ambient").min(0).max(20).listen()
|
||||||
|
light.add(scene.directionalLight, "intensity").name("directional").min(0).max(20).listen()
|
||||||
|
|
||||||
|
let directionalLightPosition = dev.addFolder("directionalLight.position").close()
|
||||||
|
directionalLightPosition.add(scene.directionalLight.position, "x").listen()
|
||||||
|
directionalLightPosition.add(scene.directionalLight.position, "y").listen()
|
||||||
|
directionalLightPosition.add(scene.directionalLight.position, "z").listen()
|
||||||
|
|
||||||
|
let vortex = dev.addFolder("vortex opacity").close()
|
||||||
|
vortex.add(scene.vortex.darkCylinder.material, "opacity").name("dark").min(0).max(1)
|
||||||
|
vortex.add(scene.vortex.colorFullCylinder.material, "opacity").name("colorFull").min(0).max(1)
|
||||||
|
|
||||||
|
changeMaterial(minoes.material.constructor.name)
|
||||||
|
material.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
if (localStorage["teTraSettings"]) {
|
||||||
|
this.settings.load(JSON.parse(localStorage["teTraSettings"]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
localStorage["teTraSettings"] = JSON.stringify(this.settings.save())
|
||||||
|
}
|
||||||
|
|
||||||
|
changeKey(settings) {
|
||||||
|
let controller = settings
|
||||||
|
let input = controller.domElement.getElementsByTagName("input")[0]
|
||||||
|
input.select()
|
||||||
|
input.onkeydown = function (event) {
|
||||||
|
controller.setValue(event.key)
|
||||||
|
input.blur()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
jsm/Settings.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
let jsKeyRenamer = new Proxy({
|
||||||
|
["←"]: "ArrowLeft",
|
||||||
|
["→"]: "ArrowRight",
|
||||||
|
["↑"]: "ArrowUp",
|
||||||
|
["↓"]: "ArrowDown",
|
||||||
|
["Espace"]: " ",
|
||||||
|
["Échap."]: "Escape",
|
||||||
|
["Ret. arrière"]: "Backspace",
|
||||||
|
["Entrée"]: "Enter",
|
||||||
|
}, {
|
||||||
|
get(obj, keyName) {
|
||||||
|
return keyName in obj ? obj[keyName] : keyName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let friendyKeyRenamer = new Proxy({
|
||||||
|
["ArrowLeft"]: "←",
|
||||||
|
["ArrowRight"]: "→",
|
||||||
|
["ArrowUp"]: "↑",
|
||||||
|
["ArrowDown"]: "↓",
|
||||||
|
[" "]: "Espace",
|
||||||
|
["Escape"]: "Échap.",
|
||||||
|
["Backspace"]: "Ret. arrière",
|
||||||
|
["Enter"]: "Entrée",
|
||||||
|
}, {
|
||||||
|
get(obj, keyName) {
|
||||||
|
return keyName in obj ? obj[keyName] : keyName.toUpperCase()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default class Settings {
|
||||||
|
constructor() {
|
||||||
|
this.startLevel = 1
|
||||||
|
|
||||||
|
let keyMaps = {
|
||||||
|
key: {},
|
||||||
|
action: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.key = new Proxy(keyMaps, {
|
||||||
|
set(km, action, key) {
|
||||||
|
key = jsKeyRenamer[key]
|
||||||
|
km.action[key.toLowerCase()] = action
|
||||||
|
return km.key[action] = key
|
||||||
|
},
|
||||||
|
has(km, action) {
|
||||||
|
return action in km.key
|
||||||
|
},
|
||||||
|
get(km, action) {
|
||||||
|
return friendyKeyRenamer[km.key[action]]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.action = new Proxy(keyMaps, {
|
||||||
|
set(km, key, action) {
|
||||||
|
km.key[action] = key
|
||||||
|
return km.action[key.toLowerCase()] = action
|
||||||
|
},
|
||||||
|
has(km, key) {
|
||||||
|
return key.toLowerCase() in km.action
|
||||||
|
},
|
||||||
|
get(km, key) {
|
||||||
|
return km.action[key.toLowerCase()]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.key.moveLeft = "ArrowLeft"
|
||||||
|
this.key.moveRight = "ArrowRight"
|
||||||
|
this.key.rotateCCW = "w"
|
||||||
|
this.key.rotateCW = "ArrowUp"
|
||||||
|
this.key.softDrop = "ArrowDown"
|
||||||
|
this.key.hardDrop = " "
|
||||||
|
this.key.hold = "c"
|
||||||
|
this.key.pause = "Escape"
|
||||||
|
|
||||||
|
this.arrDelay = 50
|
||||||
|
this.dasDelay = 300
|
||||||
|
|
||||||
|
this.musicVolume = 50
|
||||||
|
this.sfxVolume = 50
|
||||||
|
|
||||||
|
this.theme = "Plasma"
|
||||||
|
}
|
||||||
|
}
|
||||||
187
jsm/Stats.js
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import { Clock } from 'three'
|
||||||
|
import { T_SPIN } from './Tetrominoes.js'
|
||||||
|
|
||||||
|
|
||||||
|
// score = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]
|
||||||
|
const AWARDED_LINE_CLEARS = {
|
||||||
|
[T_SPIN.NONE] : [0, 1, 3, 5, 8],
|
||||||
|
[T_SPIN.MINI] : [1, 2],
|
||||||
|
[T_SPIN.T_SPIN]: [4, 8, 12, 16]
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLEARED_LINES_NAMES = [
|
||||||
|
"",
|
||||||
|
"SOLO",
|
||||||
|
"DUO",
|
||||||
|
"TRIO",
|
||||||
|
"TETRA",
|
||||||
|
]
|
||||||
|
|
||||||
|
const DELAY = {
|
||||||
|
LOCK: 500,
|
||||||
|
FALL: 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Stats {
|
||||||
|
constructor() {
|
||||||
|
this.clock = new Clock(false)
|
||||||
|
this.timeFormat = new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
timeZone: "UTC"
|
||||||
|
})
|
||||||
|
this.elapsedTime = 0
|
||||||
|
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._level = 0
|
||||||
|
this._score = 0
|
||||||
|
this.goal = 0
|
||||||
|
this.highScore = Number(localStorage["teTraHighScore"]) || 0
|
||||||
|
this.combo = 0
|
||||||
|
this.b2b = 0
|
||||||
|
this.startTime = new Date()
|
||||||
|
this.lockDelay = DELAY.LOCK
|
||||||
|
this.totalClearedLines = 0
|
||||||
|
this.nbTetra = 0
|
||||||
|
this.nbTSpin = 0
|
||||||
|
this.maxCombo = 0
|
||||||
|
this.maxB2B = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
set score(score) {
|
||||||
|
this._score = score
|
||||||
|
if (score > this.highScore) {
|
||||||
|
this.highScore = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get score() {
|
||||||
|
return this._score
|
||||||
|
}
|
||||||
|
|
||||||
|
set level(level) {
|
||||||
|
this._level = level
|
||||||
|
this.goal += level * 5
|
||||||
|
if (level <= 20) this.fallPeriod = 1000 * Math.pow(0.8 - ((level - 1) * 0.007), level - 1)
|
||||||
|
if (level > 15) this.lockDelay = 500 * Math.pow(0.9, level - 15)
|
||||||
|
messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `<h1>NIVEAU<br/>${this.level}</h1>` })
|
||||||
|
}
|
||||||
|
|
||||||
|
get level() {
|
||||||
|
return this._level
|
||||||
|
}
|
||||||
|
|
||||||
|
set combo(combo) {
|
||||||
|
this._combo = combo
|
||||||
|
if (combo > this.maxCombo) this.maxCombo = combo
|
||||||
|
}
|
||||||
|
|
||||||
|
get combo() {
|
||||||
|
return this._combo
|
||||||
|
}
|
||||||
|
|
||||||
|
set b2b(b2b) {
|
||||||
|
this._b2b = b2b
|
||||||
|
if (b2b > this.maxB2B) this.maxB2B = b2b
|
||||||
|
}
|
||||||
|
|
||||||
|
get b2b() {
|
||||||
|
return this._b2b
|
||||||
|
}
|
||||||
|
|
||||||
|
get time() {
|
||||||
|
return this.timeFormat.format(this.clock.getElapsedTime() * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
lockDown(nbClearedLines, tSpin) {
|
||||||
|
this.totalClearedLines += nbClearedLines
|
||||||
|
if (nbClearedLines == 4) this.nbTetra++
|
||||||
|
if (tSpin == T_SPIN.T_SPIN) this.nbTSpin++
|
||||||
|
|
||||||
|
// Cleared lines & T-Spin
|
||||||
|
let awardedLineClears = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]
|
||||||
|
let patternScore = 100 * this.level * awardedLineClears
|
||||||
|
if (tSpin) messagesSpan.addNewChild("div", {
|
||||||
|
className: "rotate-in-animation",
|
||||||
|
innerHTML: tSpin
|
||||||
|
})
|
||||||
|
if (nbClearedLines) messagesSpan.addNewChild("div", {
|
||||||
|
className: "zoom-in-animation",
|
||||||
|
innerHTML: CLEARED_LINES_NAMES[nbClearedLines]
|
||||||
|
})
|
||||||
|
if (patternScore) {
|
||||||
|
messagesSpan.addNewChild("div", {
|
||||||
|
className: "zoom-in-animation",
|
||||||
|
style: "animation-delay: .2s",
|
||||||
|
innerHTML: patternScore
|
||||||
|
})
|
||||||
|
this.score += patternScore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combo
|
||||||
|
if (nbClearedLines) {
|
||||||
|
this.combo++
|
||||||
|
if (this.combo >= 1) {
|
||||||
|
let comboScore = (nbClearedLines == 1 ? 20 : 50) * this.combo * this.level
|
||||||
|
if (this.combo == 1) {
|
||||||
|
messagesSpan.addNewChild("div", {
|
||||||
|
className: "zoom-in-animation",
|
||||||
|
style: "animation-delay: .4s",
|
||||||
|
innerHTML: `COMBO<br/>${comboScore}`
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
messagesSpan.addNewChild("div", {
|
||||||
|
className: "zoom-in-animation",
|
||||||
|
style: "animation-delay: .4s",
|
||||||
|
innerHTML: `COMBO x${this.combo}<br/>${comboScore}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.score += comboScore
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.combo = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back to back sequence
|
||||||
|
if ((nbClearedLines == 4) || (tSpin && nbClearedLines)) {
|
||||||
|
this.b2b++
|
||||||
|
if (this.b2b >= 1) {
|
||||||
|
let b2bScore = patternScore / 2
|
||||||
|
if (this.b2b == 1) {
|
||||||
|
messagesSpan.addNewChild("div", {
|
||||||
|
className: "zoom-in-animation",
|
||||||
|
style: "animation-delay: .4s",
|
||||||
|
innerHTML: `BOUT À BOUT<br/>${b2bScore}`
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
messagesSpan.addNewChild("div", {
|
||||||
|
className: "zoom-in-animation",
|
||||||
|
style: "animation-delay: .4s",
|
||||||
|
innerHTML: `BOUT À BOUT x${this.b2b}<br/>${b2bScore}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.score += b2bScore
|
||||||
|
}
|
||||||
|
} else if (nbClearedLines && !tSpin) {
|
||||||
|
if (this.b2b >= 1) {
|
||||||
|
messagesSpan.addNewChild("div", {
|
||||||
|
className: "zoom-in-animation",
|
||||||
|
style: "animation-delay: .4s",
|
||||||
|
innerHTML: `FIN DU BOUT À BOUT`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.b2b = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
this.goal -= awardedLineClears
|
||||||
|
if (this.goal <= 0) this.level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export { Stats }
|
||||||
71
jsm/TetraScene.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import * as THREE from 'three'
|
||||||
|
import { Vortex } from './Vortex.js'
|
||||||
|
|
||||||
|
|
||||||
|
export class TetraScene extends THREE.Scene {
|
||||||
|
constructor(settings, loadingManager) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.camera = new THREE.PerspectiveCamera(100, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||||
|
this.camera.position.set(5, 4, 12)
|
||||||
|
|
||||||
|
this.vortex = new Vortex(loadingManager)
|
||||||
|
this.add(this.vortex)
|
||||||
|
|
||||||
|
this.ambientLight = new THREE.AmbientLight(0xffffff, 1)
|
||||||
|
this.add(this.ambientLight)
|
||||||
|
|
||||||
|
this.directionalLight = new THREE.DirectionalLight(0xffffff, 5)
|
||||||
|
this.add(this.directionalLight)
|
||||||
|
|
||||||
|
this.theme = settings.theme
|
||||||
|
|
||||||
|
/* Sounds */
|
||||||
|
this.music = music
|
||||||
|
|
||||||
|
const listener = new THREE.AudioListener()
|
||||||
|
this.camera.add( listener )
|
||||||
|
const audioLoader = new THREE.AudioLoader(loadingManager)
|
||||||
|
|
||||||
|
this.lineClearSound = new THREE.Audio(listener)
|
||||||
|
audioLoader.load('audio/line-clear.ogg', function( buffer ) {
|
||||||
|
this.lineClearSound.setBuffer(buffer)
|
||||||
|
}.bind(this))
|
||||||
|
this.tetrisSound = new THREE.Audio(listener)
|
||||||
|
audioLoader.load('audio/tetris.ogg', function( buffer ) {
|
||||||
|
this.tetrisSound.setBuffer(buffer)
|
||||||
|
this.lineClearSound.setVolume(settings.sfxVolume/100)
|
||||||
|
this.tetrisSound.setVolume(settings.sfxVolume/100)
|
||||||
|
this.hardDropSound.setVolume(settings.sfxVolume/100)
|
||||||
|
}.bind(this))
|
||||||
|
this.hardDropSound = new THREE.Audio(listener)
|
||||||
|
audioLoader.load('audio/hard-drop.wav', function( buffer ) {
|
||||||
|
this.hardDropSound.setBuffer(buffer)
|
||||||
|
}.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
set theme(theme) {
|
||||||
|
switch (theme) {
|
||||||
|
case "Plasma":
|
||||||
|
this.ambientLight.intensity = 0.6
|
||||||
|
this.directionalLight.intensity = 5
|
||||||
|
this.directionalLight.position.set(5, -20, 20)
|
||||||
|
break
|
||||||
|
case "Espace":
|
||||||
|
this.ambientLight.intensity = 20
|
||||||
|
this.directionalLight.intensity = 10
|
||||||
|
this.directionalLight.position.set(5, -20, 20)
|
||||||
|
break
|
||||||
|
case "Rétro":
|
||||||
|
this.ambientLight.intensity = 1
|
||||||
|
this.directionalLight.intensity = 10
|
||||||
|
this.directionalLight.position.set(19, 120, 200)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this.vortex.theme = theme
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
this.vortex.update(delta)
|
||||||
|
}
|
||||||
|
}
|
||||||
653
jsm/Tetrominoes.js
Normal file
@ -0,0 +1,653 @@
|
|||||||
|
import * as THREE from 'three'
|
||||||
|
import { scheduler } from './scheduler.js'
|
||||||
|
import { TileMaterial } from './TileMaterial.js'
|
||||||
|
|
||||||
|
|
||||||
|
Array.prototype.pick = function () { return this.splice(Math.floor(Math.random() * this.length), 1)[0] }
|
||||||
|
|
||||||
|
let P = (x, y, z=0) => new THREE.Vector3(x, y, z)
|
||||||
|
|
||||||
|
const GRAVITY = -30
|
||||||
|
|
||||||
|
const COLORS = {
|
||||||
|
I: 0xafeff9,
|
||||||
|
J: 0xb8b4ff,
|
||||||
|
L: 0xfdd0b7,
|
||||||
|
O: 0xffedac,
|
||||||
|
S: 0xC8FBA8,
|
||||||
|
T: 0xedb2ff,
|
||||||
|
Z: 0xffb8c5,
|
||||||
|
LOCKING: 0xffffff,
|
||||||
|
GHOST: 0x99a9b2,
|
||||||
|
EDGE: 0x88abe0,
|
||||||
|
RETRO: 0xd0d4c1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TRANSLATION = {
|
||||||
|
NONE : P( 0, 0),
|
||||||
|
LEFT : P(-1, 0),
|
||||||
|
RIGHT: P( 1, 0),
|
||||||
|
UP : P( 0, 1),
|
||||||
|
DOWN : P( 0, -1),
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROTATION = {
|
||||||
|
CW: 1, // ClockWise
|
||||||
|
CCW: 3, // CounterClockWise
|
||||||
|
}
|
||||||
|
|
||||||
|
const T_SPIN = {
|
||||||
|
NONE: "",
|
||||||
|
MINI: "PETITE<br/>PIROUETTE",
|
||||||
|
T_SPIN: "PIROUETTE"
|
||||||
|
}
|
||||||
|
|
||||||
|
const FACING = {
|
||||||
|
NORTH: 0,
|
||||||
|
EAST: 1,
|
||||||
|
SOUTH: 2,
|
||||||
|
WEST: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const ROWS = 24
|
||||||
|
const SKYLINE = 20
|
||||||
|
const COLUMNS = 10
|
||||||
|
|
||||||
|
|
||||||
|
const envRenderTarget = new THREE.WebGLCubeRenderTarget(256)
|
||||||
|
const environment = envRenderTarget.texture
|
||||||
|
environment.type = THREE.HalfFloatType
|
||||||
|
environment.camera = new THREE.CubeCamera(1, 1000, envRenderTarget)
|
||||||
|
environment.camera.position.set(5, 10, 0)
|
||||||
|
|
||||||
|
|
||||||
|
const sideMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x222222,
|
||||||
|
roughness: 0.8,
|
||||||
|
metalness: 0.8,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export class InstancedMino extends THREE.InstancedMesh {
|
||||||
|
constructor() {
|
||||||
|
let minoFaceShape = new THREE.Shape()
|
||||||
|
minoFaceShape.moveTo(.1, .1)
|
||||||
|
minoFaceShape.lineTo(.1, .9)
|
||||||
|
minoFaceShape.lineTo(.9, .9)
|
||||||
|
minoFaceShape.lineTo(.9, .1)
|
||||||
|
minoFaceShape.lineTo(.1, .1)
|
||||||
|
let minoExtrudeSettings = {
|
||||||
|
steps: 1,
|
||||||
|
depth: .8,
|
||||||
|
bevelEnabled: true,
|
||||||
|
bevelThickness: .1,
|
||||||
|
bevelSize: .1,
|
||||||
|
bevelOffset: 0,
|
||||||
|
bevelSegments: 1
|
||||||
|
}
|
||||||
|
const geometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings)
|
||||||
|
super(geometry, undefined, 2*ROWS*COLUMNS)
|
||||||
|
this.offsets = new Uint8Array(2*this.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
set theme(theme) {
|
||||||
|
if (theme == "Rétro") {
|
||||||
|
this.resetColor()
|
||||||
|
this.update = this.updateOffset
|
||||||
|
if (this.materials["Rétro"]) {
|
||||||
|
this.material = this.materials["Rétro"]
|
||||||
|
} else {
|
||||||
|
this.materials["Rétro"] = []
|
||||||
|
const loadingManager = new THREE.LoadingManager(() => this.material = this.materials["Rétro"])
|
||||||
|
new THREE.TextureLoader(loadingManager).load("images/sprites.png", (texture) => {
|
||||||
|
this.materials.Rétro[0] = this.materials.Rétro[2] = new TileMaterial({
|
||||||
|
color: COLORS.RETRO,
|
||||||
|
map: texture,
|
||||||
|
bumpMap: texture,
|
||||||
|
bumpScale: 1.5,
|
||||||
|
roughness: 0.25,
|
||||||
|
metalness: 0.9,
|
||||||
|
transparent: true,
|
||||||
|
}, 8, 8)
|
||||||
|
})
|
||||||
|
new THREE.TextureLoader(loadingManager).load("images/edges.png", (texture) => {
|
||||||
|
this.materials.Rétro[1] = this.materials.Rétro[3] = this.materials.Rétro[4] = this.materials.Rétro[5] = new TileMaterial({
|
||||||
|
color: COLORS.RETRO,
|
||||||
|
map: texture,
|
||||||
|
bumpMap: texture,
|
||||||
|
bumpScale: 1.5,
|
||||||
|
roughness: 0.25,
|
||||||
|
metalness: 0.9,
|
||||||
|
transparent: true,
|
||||||
|
}, 1, 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.update = this.updateColor
|
||||||
|
this.material = this.materials[theme]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOffsetAt(index, offset) {
|
||||||
|
this.offsets[2*index] = offset.x
|
||||||
|
this.offsets[2*index + 1] = offset.y
|
||||||
|
}
|
||||||
|
|
||||||
|
resetColor() {
|
||||||
|
this.instanceColor = null
|
||||||
|
}
|
||||||
|
|
||||||
|
updateColor() {
|
||||||
|
this.count = 0
|
||||||
|
Mino.instances.forEach(mino => {
|
||||||
|
if (mino.parent?.visible) {
|
||||||
|
this.setMatrixAt(this.count, mino.matrixWorld)
|
||||||
|
this.setColorAt(this.count, mino.color)
|
||||||
|
this.count++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (this.count) {
|
||||||
|
this.instanceMatrix.needsUpdate = true
|
||||||
|
this.instanceColor.needsUpdate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOffset() {
|
||||||
|
this.count = 0
|
||||||
|
Mino.instances.forEach(mino => {
|
||||||
|
if (mino.parent?.visible) {
|
||||||
|
this.setMatrixAt(this.count, mino.matrixWorld)
|
||||||
|
this.setOffsetAt(this.count, mino.offset)
|
||||||
|
this.count++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (this.count) {
|
||||||
|
this.instanceMatrix.needsUpdate = true
|
||||||
|
this.geometry.setAttribute('offset', new THREE.InstancedBufferAttribute(this.offsets, 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InstancedMino.prototype.materials = {
|
||||||
|
Plasma: new THREE.MeshStandardMaterial({
|
||||||
|
envMap: environment,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.7,
|
||||||
|
roughness: 0.6,
|
||||||
|
metalness: 1,
|
||||||
|
}),
|
||||||
|
Espace: new THREE.MeshStandardMaterial({
|
||||||
|
envMap: environment,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.8,
|
||||||
|
roughness: 0.1,
|
||||||
|
metalness: 0.99,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Mino extends THREE.Object3D {
|
||||||
|
static instances = new Set()
|
||||||
|
|
||||||
|
constructor(color, offset) {
|
||||||
|
super()
|
||||||
|
this.color = color
|
||||||
|
this.offset = offset
|
||||||
|
this.velocity = P(50 - 100 * Math.random(), 60 - 100 * Math.random(), 50 - 100 * Math.random())
|
||||||
|
this.rotationAngle = P(Math.random(), Math.random(), Math.random()).normalize()
|
||||||
|
this.angularVelocity = 5 - 10 * Math.random()
|
||||||
|
this.constructor.instances.add(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
explode(delta) {
|
||||||
|
this.velocity.y += delta * GRAVITY
|
||||||
|
this.position.addScaledVector(this.velocity, delta)
|
||||||
|
this.rotateOnWorldAxis(this.rotationAngle, delta * this.angularVelocity)
|
||||||
|
if (Math.sqrt(this.position.x * this.position.x + this.position.z * this.position.z) > 40 || this.position.y < -50) {
|
||||||
|
this.dispose()
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.constructor.instances.delete(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Tetromino extends THREE.Group {
|
||||||
|
static randomBag = []
|
||||||
|
static get random() {
|
||||||
|
if (!this.randomBag.length) this.randomBag = [I, J, L, O, S, T, Z]
|
||||||
|
return this.randomBag.pick()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(position) {
|
||||||
|
super()
|
||||||
|
if (position) this.position.copy(position)
|
||||||
|
this.offset = this.offset.clone()
|
||||||
|
this.minoesPosition[FACING.NORTH].forEach(() => this.add(new Mino(this.freeColor, this.offset)))
|
||||||
|
this.facing = FACING.NORTH
|
||||||
|
this.rotatedLast = false
|
||||||
|
this.rotationPoint4Used = false
|
||||||
|
this.holdEnabled = true
|
||||||
|
this.locking = false
|
||||||
|
}
|
||||||
|
|
||||||
|
set facing(facing) {
|
||||||
|
this._facing = facing
|
||||||
|
this.children.forEach((mino, i) => mino.position.copy(this.minoesPosition[facing][i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
get facing() {
|
||||||
|
return this._facing
|
||||||
|
}
|
||||||
|
|
||||||
|
set locking(locking) {
|
||||||
|
if (locking) {
|
||||||
|
this.color = this.lockingColor
|
||||||
|
this.offset.y = 2
|
||||||
|
} else {
|
||||||
|
this.color = this.freeColor
|
||||||
|
this.offset.y = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set color(color) {
|
||||||
|
this.children.forEach((mino) => mino.color = color)
|
||||||
|
}
|
||||||
|
|
||||||
|
canMove(translation, facing=this.facing) {
|
||||||
|
let testPosition = this.position.clone().add(translation)
|
||||||
|
return this.minoesPosition[facing].every(minoPosition => this.parent?.cellIsEmpty(minoPosition.clone().add(testPosition)))
|
||||||
|
}
|
||||||
|
|
||||||
|
move(translation, rotatedFacing) {
|
||||||
|
if (this.canMove(translation, rotatedFacing)) {
|
||||||
|
this.position.add(translation)
|
||||||
|
this.rotatedLast = rotatedFacing
|
||||||
|
if (rotatedFacing != undefined) {
|
||||||
|
this.facing = rotatedFacing
|
||||||
|
}
|
||||||
|
if (this.canMove(TRANSLATION.DOWN)) {
|
||||||
|
this.locking = false
|
||||||
|
this.parent?.ghost.copy(this)
|
||||||
|
scheduler.clearTimeout(this.onLockDown)
|
||||||
|
} else {
|
||||||
|
scheduler.resetTimeout(this.onLockDown, this.lockDelay)
|
||||||
|
this.locking = true
|
||||||
|
this.parent.ghost.visible = false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else if (translation == TRANSLATION.DOWN) {
|
||||||
|
this.locked = true
|
||||||
|
if (!scheduler.timeoutTasks.has(this.onLockDown))
|
||||||
|
scheduler.resetTimeout(this.onLockDown, this.lockDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate(rotation) {
|
||||||
|
let testFacing = (this.facing + rotation) % 4
|
||||||
|
return this.srs[this.facing][rotation].some((translation, rotationPoint) => {
|
||||||
|
if (this.move(translation, testFacing)) {
|
||||||
|
if (rotationPoint == 4) this.rotationPoint4Used = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get tSpin() {
|
||||||
|
return T_SPIN.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tetromino.prototype.lockingColor = new THREE.Color(COLORS.LOCKING)
|
||||||
|
// Super Rotation System
|
||||||
|
// freedom of movement = srs[this.facing][rotation]
|
||||||
|
Tetromino.prototype.srs = [
|
||||||
|
{ [ROTATION.CW]: [P(0, 0), P(-1, 0), P(-1, 1), P(0, -2), P(-1, -2)], [ROTATION.CCW]: [P(0, 0), P(1, 0), P(1, 1), P(0, -2), P(1, -2)] },
|
||||||
|
{ [ROTATION.CW]: [P(0, 0), P(1, 0), P(1, -1), P(0, 2), P(1, 2)], [ROTATION.CCW]: [P(0, 0), P(1, 0), P(1, -1), P(0, 2), P(1, 2)] },
|
||||||
|
{ [ROTATION.CW]: [P(0, 0), P(1, 0), P(1, 1), P(0, -2), P(1, -2)], [ROTATION.CCW]: [P(0, 0), P(-1, 0), P(-1, 1), P(0, -2), P(-1, -2)] },
|
||||||
|
{ [ROTATION.CW]: [P(0, 0), P(-1, 0), P(-1, -1), P(0, 2), P(-1, 2)], [ROTATION.CCW]: [P(0, 0), P(-1, 0), P(-1, -1), P(0, 2), P(-1, 2)] },
|
||||||
|
]
|
||||||
|
Tetromino.prototype.lockDelay = 500
|
||||||
|
|
||||||
|
|
||||||
|
class Ghost extends Tetromino {
|
||||||
|
copy(piece) {
|
||||||
|
this.position.copy(piece.position)
|
||||||
|
this.minoesPosition = piece.minoesPosition
|
||||||
|
this.children.forEach(mino => {mino.offset = piece.ghostOffset})
|
||||||
|
this.facing = piece.facing
|
||||||
|
this.visible = true
|
||||||
|
while (this.canMove(TRANSLATION.DOWN)) this.position.y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ghost.prototype.freeColor = new THREE.Color(COLORS.GHOST)
|
||||||
|
Ghost.prototype.minoesPosition = [
|
||||||
|
[P(0, 0, 0), P(0, 0, 0), P(0, 0, 0), P(0, 0, 0)],
|
||||||
|
]
|
||||||
|
Ghost.prototype.offset = P(0, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class I extends Tetromino { }
|
||||||
|
I.prototype.minoesPosition = [
|
||||||
|
[P(-1, 0), P(0, 0), P(1, 0), P(2, 0)],
|
||||||
|
[P(1, 1), P(1, 0), P(1, -1), P(1, -2)],
|
||||||
|
[P(-1, -1), P(0, -1), P(1, -1), P(2, -1)],
|
||||||
|
[P(0, 1), P(0, 0), P(0, -1), P(0, -2)],
|
||||||
|
]
|
||||||
|
I.prototype.srs = [
|
||||||
|
{ [ROTATION.CW]: [P(0, 0), P(-2, 0), P(1, 0), P(-2, -1), P(1, 2)], [ROTATION.CCW]: [P(0, 0), P(-1, 0), P(2, 0), P(-1, 2), P(2, -1)] },
|
||||||
|
{ [ROTATION.CW]: [P(0, 0), P(-1, 0), P(2, 0), P(-1, 2), P(2, -1)], [ROTATION.CCW]: [P(0, 0), P(2, 0), P(-1, 0), P(2, 1), P(-1, -2)] },
|
||||||
|
{ [ROTATION.CW]: [P(0, 0), P(2, 0), P(-1, 0), P(2, 1), P(-1, -2)], [ROTATION.CCW]: [P(0, 0), P(1, 0), P(-2, 0), P(1, -2), P(-2, 1)] },
|
||||||
|
{ [ROTATION.CW]: [P(0, 0), P(1, 0), P(-2, 0), P(1, -2), P(-2, 1)], [ROTATION.CCW]: [P(0, 0), P(-2, 0), P(1, 0), P(-2, -1), P(1, 2)] },
|
||||||
|
]
|
||||||
|
I.prototype.freeColor = new THREE.Color(COLORS.I)
|
||||||
|
I.prototype.offset = P(0, 0)
|
||||||
|
I.prototype.ghostOffset = P(0, 1)
|
||||||
|
|
||||||
|
class J extends Tetromino { }
|
||||||
|
J.prototype.minoesPosition = [
|
||||||
|
[P(-1, 1), P(-1, 0), P(0, 0), P(1, 0)],
|
||||||
|
[P(0, 1), P(1, 1), P(0, 0), P(0, -1)],
|
||||||
|
[P(1, -1), P(-1, 0), P(0, 0), P(1, 0)],
|
||||||
|
[P(0, 1), P(-1, -1), P(0, 0), P(0, -1)],
|
||||||
|
]
|
||||||
|
J.prototype.freeColor = new THREE.Color(COLORS.J)
|
||||||
|
J.prototype.offset = P(1, 0)
|
||||||
|
J.prototype.ghostOffset = P(1, 1)
|
||||||
|
|
||||||
|
class L extends Tetromino {
|
||||||
|
}
|
||||||
|
L.prototype.minoesPosition = [
|
||||||
|
[P(-1, 0), P(0, 0), P(1, 0), P(1, 1)],
|
||||||
|
[P(0, 1), P(0, 0), P(0, -1), P(1, -1)],
|
||||||
|
[P(-1, 0), P(0, 0), P(1, 0), P(-1, -1)],
|
||||||
|
[P(0, 1), P(0, 0), P(0, -1), P(-1, 1)],
|
||||||
|
]
|
||||||
|
L.prototype.freeColor = new THREE.Color(COLORS.L)
|
||||||
|
L.prototype.offset = P(2, 0)
|
||||||
|
L.prototype.ghostOffset = P(2, 1)
|
||||||
|
|
||||||
|
class O extends Tetromino { }
|
||||||
|
O.prototype.minoesPosition = [
|
||||||
|
[P(0, 0), P(1, 0), P(0, 1), P(1, 1)]
|
||||||
|
]
|
||||||
|
O.prototype.srs = [
|
||||||
|
{ [ROTATION.CW]: [], [ROTATION.CCW]: [] }
|
||||||
|
]
|
||||||
|
O.prototype.freeColor = new THREE.Color(COLORS.O)
|
||||||
|
O.prototype.offset = P(3, 0)
|
||||||
|
O.prototype.ghostOffset = P(3, 1)
|
||||||
|
|
||||||
|
class S extends Tetromino { }
|
||||||
|
S.prototype.minoesPosition = [
|
||||||
|
[P(-1, 0), P(0, 0), P(0, 1), P(1, 1)],
|
||||||
|
[P(0, 1), P(0, 0), P(1, 0), P(1, -1)],
|
||||||
|
[P(-1, -1), P(0, 0), P(1, 0), P(0, -1)],
|
||||||
|
[P(-1, 1), P(0, 0), P(-1, 0), P(0, -1)],
|
||||||
|
]
|
||||||
|
S.prototype.freeColor = new THREE.Color(COLORS.S)
|
||||||
|
S.prototype.offset = P(4, 0)
|
||||||
|
S.prototype.ghostOffset = P(4, 1)
|
||||||
|
|
||||||
|
class T extends Tetromino {
|
||||||
|
get tSpin() {
|
||||||
|
if (this.rotatedLast) {
|
||||||
|
let [a, b, c, d] = this.tSlots[this.facing]
|
||||||
|
.map(p => !this.parent.cellIsEmpty(p.clone().add(this.position)))
|
||||||
|
if (a && b && (c || d))
|
||||||
|
return T_SPIN.T_SPIN
|
||||||
|
else if (c && d && (a || b))
|
||||||
|
return this.rotationPoint4Used ? T_SPIN.T_SPIN : T_SPIN.MINI
|
||||||
|
}
|
||||||
|
return T_SPIN.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
T.prototype.minoesPosition = [
|
||||||
|
[P(-1, 0), P(0, 0), P(1, 0), P(0, 1)],
|
||||||
|
[P(0, 1), P(0, 0), P(1, 0), P(0, -1)],
|
||||||
|
[P(-1, 0), P(0, 0), P(1, 0), P(0, -1)],
|
||||||
|
[P(0, 1), P(0, 0), P(0, -1), P(-1, 0)],
|
||||||
|
]
|
||||||
|
T.prototype.tSlots = [
|
||||||
|
[P(-1, 1), P(1, 1), P(1, -1), P(-1, -1)],
|
||||||
|
[P(1, 1), P(1, -1), P(-1, -1), P(-1, 1)],
|
||||||
|
[P(1, -1), P(-1, -1), P(-1, 1), P(1, 1)],
|
||||||
|
[P(-1, -1), P(-1, 1), P(1, 1), P(1, -1)],
|
||||||
|
]
|
||||||
|
T.prototype.freeColor = new THREE.Color(COLORS.T)
|
||||||
|
T.prototype.offset = P(5, 0)
|
||||||
|
T.prototype.ghostOffset = P(5, 1)
|
||||||
|
|
||||||
|
class Z extends Tetromino { }
|
||||||
|
Z.prototype.minoesPosition = [
|
||||||
|
[P(-1, 1), P(0, 1), P(0, 0), P(1, 0)],
|
||||||
|
[P(1, 1), P(1, 0), P(0, 0), P(0, -1)],
|
||||||
|
[P(-1, 0), P(0, 0), P(0, -1), P(1, -1)],
|
||||||
|
[P(0, 1), P(-1, 0), P(0, 0), P(-1, -1)]
|
||||||
|
]
|
||||||
|
Z.prototype.freeColor = new THREE.Color(COLORS.Z)
|
||||||
|
Z.prototype.offset = P(6, 0)
|
||||||
|
Z.prototype.ghostOffset = P(6, 1)
|
||||||
|
|
||||||
|
|
||||||
|
class Playfield extends THREE.Group {
|
||||||
|
constructor(loadingManager) {
|
||||||
|
super()
|
||||||
|
//this.visible = false
|
||||||
|
|
||||||
|
const edgeMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: COLORS.EDGE,
|
||||||
|
envMap: environment,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.3,
|
||||||
|
roughness: 0.1,
|
||||||
|
metalness: 0.67,
|
||||||
|
})
|
||||||
|
const edgeShape = new THREE.Shape()
|
||||||
|
.moveTo(-.3, SKYLINE)
|
||||||
|
.lineTo(0, SKYLINE)
|
||||||
|
.lineTo(0, 0)
|
||||||
|
.lineTo(COLUMNS, 0)
|
||||||
|
.lineTo(COLUMNS, SKYLINE)
|
||||||
|
.lineTo(COLUMNS + .3, SKYLINE)
|
||||||
|
.lineTo(COLUMNS + .3, -.3)
|
||||||
|
.lineTo(-.3, -.3)
|
||||||
|
.moveTo(-.3, SKYLINE)
|
||||||
|
this.edge = new THREE.Mesh(
|
||||||
|
new THREE.ExtrudeGeometry(edgeShape, {
|
||||||
|
depth: 1,
|
||||||
|
bevelEnabled: false,
|
||||||
|
}),
|
||||||
|
edgeMaterial
|
||||||
|
)
|
||||||
|
this.add(this.edge)
|
||||||
|
|
||||||
|
const retroEdgeShape = new THREE.Shape()
|
||||||
|
.moveTo(-1, SKYLINE)
|
||||||
|
.lineTo(0, SKYLINE)
|
||||||
|
.lineTo(0, 0)
|
||||||
|
.lineTo(COLUMNS, 0)
|
||||||
|
.lineTo(COLUMNS, SKYLINE)
|
||||||
|
.lineTo(COLUMNS + 1, SKYLINE)
|
||||||
|
.lineTo(COLUMNS + 1, -1/3)
|
||||||
|
.lineTo(-1, -1/3)
|
||||||
|
.moveTo(-1, SKYLINE)
|
||||||
|
const retroEdgeTexture = new THREE.TextureLoader(loadingManager).load("images/edge.png", (texture) => {
|
||||||
|
texture.wrapS = THREE.RepeatWrapping
|
||||||
|
texture.wrapT = THREE.RepeatWrapping
|
||||||
|
})
|
||||||
|
const retroEdgeMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: COLORS.RETRO,
|
||||||
|
map: retroEdgeTexture,
|
||||||
|
bumpMap: retroEdgeTexture,
|
||||||
|
bumpScale: 1.5,
|
||||||
|
roughness: 0.25,
|
||||||
|
metalness: 0.9,
|
||||||
|
})
|
||||||
|
this.retroEdge = new THREE.Mesh(
|
||||||
|
new THREE.ExtrudeGeometry(retroEdgeShape, {
|
||||||
|
depth: 1,
|
||||||
|
bevelEnabled: false,
|
||||||
|
}),
|
||||||
|
[retroEdgeMaterial, sideMaterial, sideMaterial, sideMaterial, sideMaterial, sideMaterial],
|
||||||
|
)
|
||||||
|
const back = new THREE.Mesh(
|
||||||
|
new THREE.PlaneGeometry(COLUMNS, SKYLINE),
|
||||||
|
new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xc5d0a1,
|
||||||
|
roughness: 0.9,
|
||||||
|
metalness: 0.9,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
back.position.set(COLUMNS/2, SKYLINE/2)
|
||||||
|
this.retroEdge.add(back)
|
||||||
|
this.retroEdge.visible = false
|
||||||
|
this.add(this.retroEdge)
|
||||||
|
|
||||||
|
const positionKF = new THREE.VectorKeyframeTrack('.position', [0, 1, 2], [0, 0, 0, 0, -0.2, 0, 0, 0, 0])
|
||||||
|
const clip = new THREE.AnimationClip('HardDrop', 3, [positionKF])
|
||||||
|
const animationGroup = new THREE.AnimationObjectGroup()
|
||||||
|
animationGroup.add(this)
|
||||||
|
this.mixer = new THREE.AnimationMixer(animationGroup)
|
||||||
|
this.hardDropAnimation = this.mixer.clipAction(clip)
|
||||||
|
this.hardDropAnimation.loop = THREE.LoopOnce
|
||||||
|
this.hardDropAnimation.setDuration(0.2)
|
||||||
|
|
||||||
|
this.freedMinoes = new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.cells = Array(ROWS).fill().map(() => Array(COLUMNS))
|
||||||
|
if (this.piece) this.remove(this.piece)
|
||||||
|
this.piece = undefined
|
||||||
|
|
||||||
|
this.ghost = new Ghost()
|
||||||
|
this.ghost.visible = false
|
||||||
|
this.add(this.ghost)
|
||||||
|
|
||||||
|
// this.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cellIsEmpty(p) {
|
||||||
|
return 0 <= p.x && p.x < COLUMNS &&
|
||||||
|
0 <= p.y && p.y < ROWS &&
|
||||||
|
!this.cells[p.y][p.x]
|
||||||
|
}
|
||||||
|
|
||||||
|
set piece(piece) {
|
||||||
|
if (piece) {
|
||||||
|
this.remove(this.piece)
|
||||||
|
this.add(piece)
|
||||||
|
piece.position.set(4, SKYLINE)
|
||||||
|
this.ghost.copy(piece)
|
||||||
|
}
|
||||||
|
this._piece = piece
|
||||||
|
}
|
||||||
|
|
||||||
|
get piece() {
|
||||||
|
return this._piece
|
||||||
|
}
|
||||||
|
|
||||||
|
lock() {
|
||||||
|
this.piece.locking = false
|
||||||
|
let minoes = Array.from(this.piece.children)
|
||||||
|
minoes.forEach(mino => {
|
||||||
|
this.add(mino)
|
||||||
|
mino.position.add(this.piece.position)
|
||||||
|
})
|
||||||
|
if (minoes.every(mino => mino.position.y >= SKYLINE)) return false
|
||||||
|
return minoes.every(mino => {
|
||||||
|
if (this.cellIsEmpty(mino.position)) {
|
||||||
|
this.cells[mino.position.y][mino.position.x] = mino
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLines() {
|
||||||
|
let nbClearedLines = this.cells.reduceRight((nbClearedLines, row, y) => {
|
||||||
|
if (row.filter(color => color).length == COLUMNS) {
|
||||||
|
row.forEach(mino => this.freedMinoes.add(mino))
|
||||||
|
this.cells.splice(y, 1)
|
||||||
|
this.cells.push(Array(COLUMNS))
|
||||||
|
return ++nbClearedLines
|
||||||
|
}
|
||||||
|
return nbClearedLines
|
||||||
|
}, 0)
|
||||||
|
if (nbClearedLines) this.cells.forEach((row, y) => row.forEach((mino, x) => mino.position.set(x, y, 0)))
|
||||||
|
return nbClearedLines
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFreedMinoes(delta) {
|
||||||
|
this.freedMinoes.forEach(mino => {
|
||||||
|
if (mino.explode(delta)) {
|
||||||
|
this.remove(mino)
|
||||||
|
this.freedMinoes.delete(mino)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
this.updateFreedMinoes(delta)
|
||||||
|
this.mixer?.update(delta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HoldQueue extends THREE.Group {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.position.set(-5, SKYLINE - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
set piece(piece) {
|
||||||
|
if(piece) {
|
||||||
|
this.remove(this.piece)
|
||||||
|
piece.holdEnabled = false
|
||||||
|
piece.locking = false
|
||||||
|
piece.position.set(0, 0)
|
||||||
|
piece.facing = FACING.NORTH
|
||||||
|
this.add(piece)
|
||||||
|
}
|
||||||
|
this._piece = piece
|
||||||
|
}
|
||||||
|
|
||||||
|
get piece() {
|
||||||
|
return this._piece
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NextQueue extends THREE.Group {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.position.set(14, SKYLINE - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.clear()
|
||||||
|
this.positions.forEach(position => this.add(new Tetromino.random(position)))
|
||||||
|
}
|
||||||
|
|
||||||
|
shift() {
|
||||||
|
let fistPiece = this.children.shift()
|
||||||
|
this.remove(fistPiece)
|
||||||
|
this.add(new Tetromino.random())
|
||||||
|
this.positions.forEach((position, i) => this.children[i].position.copy(position))
|
||||||
|
return fistPiece
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
NextQueue.prototype.positions = [P(0, 0), P(0, -3), P(0, -6), P(0, -9), P(0, -12), P(0, -15), P(0, -18)]
|
||||||
|
|
||||||
|
|
||||||
|
export { T_SPIN, FACING, TRANSLATION, ROTATION, COLORS, environment, Mino, Tetromino, Playfield, HoldQueue, NextQueue }
|
||||||
93
jsm/TileMaterial.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { MeshStandardMaterial, Texture, Vector2 } from 'three';
|
||||||
|
|
||||||
|
export class TileMaterial extends MeshStandardMaterial {
|
||||||
|
constructor(params, tileSizeX, tileSizeY) {
|
||||||
|
super(params);
|
||||||
|
this.tileSize = { value: new Vector2(tileSizeX / this.map.image.width, tileSizeY / this.map.image.height) };
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeCompile(shader) {
|
||||||
|
shader.uniforms.tileSize = this.tileSize
|
||||||
|
shader.vertexShader = shader.vertexShader.replace(
|
||||||
|
`void main() {`,
|
||||||
|
`varying vec2 vUv;
|
||||||
|
varying vec2 vOffset;
|
||||||
|
attribute vec2 offset;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
vOffset = offset;
|
||||||
|
gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);`
|
||||||
|
)
|
||||||
|
shader.fragmentShader = `varying vec2 vUv;
|
||||||
|
varying vec2 vOffset;
|
||||||
|
//uniform sampler2D map;
|
||||||
|
uniform vec2 tileSize;
|
||||||
|
varying vec2 vBumpMapUvOffset;
|
||||||
|
` + shader.fragmentShader
|
||||||
|
shader.fragmentShader = shader.fragmentShader.replace(
|
||||||
|
`#include <map_fragment>`,
|
||||||
|
`#ifdef USE_MAP
|
||||||
|
|
||||||
|
vec4 sampledDiffuseColor = texture2D(map, vUv * tileSize + vOffset * tileSize);
|
||||||
|
|
||||||
|
#ifdef DECODE_VIDEO_TEXTURE
|
||||||
|
|
||||||
|
// use inline sRGB decode until browsers properly support SRGB8_ALPHA8 with video textures (#26516)
|
||||||
|
|
||||||
|
sampledDiffuseColor = vec4( mix( pow( sampledDiffuseColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), sampledDiffuseColor.rgb * 0.0773993808, vec3( lessThanEqual( sampledDiffuseColor.rgb, vec3( 0.04045 ) ) ) ), sampledDiffuseColor.w );
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
diffuseColor *= sampledDiffuseColor;
|
||||||
|
|
||||||
|
#endif`
|
||||||
|
)
|
||||||
|
shader.fragmentShader = shader.fragmentShader.replace(
|
||||||
|
`#include <bumpmap_pars_fragment>`,
|
||||||
|
`#ifdef USE_BUMPMAP
|
||||||
|
|
||||||
|
uniform sampler2D bumpMap;
|
||||||
|
uniform float bumpScale;
|
||||||
|
|
||||||
|
// Bump Mapping Unparametrized Surfaces on the GPU by Morten S. Mikkelsen
|
||||||
|
// https://mmikk.github.io/papers3d/mm_sfgrad_bump.pdf
|
||||||
|
|
||||||
|
// Evaluate the derivative of the height w.r.t. screen-space using forward differencing (listing 2)
|
||||||
|
|
||||||
|
vec2 dHdxy_fwd() {
|
||||||
|
|
||||||
|
vec2 vBumpMapUvOffset = vBumpMapUv * tileSize + vOffset * tileSize;
|
||||||
|
|
||||||
|
vec2 dSTdx = dFdx( vBumpMapUvOffset );
|
||||||
|
vec2 dSTdy = dFdy( vBumpMapUvOffset );
|
||||||
|
|
||||||
|
float Hll = bumpScale * texture2D( bumpMap, vBumpMapUvOffset ).x;
|
||||||
|
float dBx = bumpScale * texture2D( bumpMap, vBumpMapUvOffset + dSTdx ).x - Hll;
|
||||||
|
float dBy = bumpScale * texture2D( bumpMap, vBumpMapUvOffset + dSTdy ).x - Hll;
|
||||||
|
|
||||||
|
return vec2( dBx, dBy );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy, float faceDirection ) {
|
||||||
|
|
||||||
|
// normalize is done to ensure that the bump map looks the same regardless of the texture's scale
|
||||||
|
vec3 vSigmaX = normalize( dFdx( surf_pos.xyz ) );
|
||||||
|
vec3 vSigmaY = normalize( dFdy( surf_pos.xyz ) );
|
||||||
|
vec3 vN = surf_norm; // normalized
|
||||||
|
|
||||||
|
vec3 R1 = cross( vSigmaY, vN );
|
||||||
|
vec3 R2 = cross( vN, vSigmaX );
|
||||||
|
|
||||||
|
float fDet = dot( vSigmaX, R1 ) * faceDirection;
|
||||||
|
|
||||||
|
vec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );
|
||||||
|
return normalize( abs( fDet ) * surf_norm - vGrad );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
116
jsm/Vortex.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import * as THREE from 'three'
|
||||||
|
|
||||||
|
|
||||||
|
export class Vortex extends THREE.Group {
|
||||||
|
constructor(loadingManager) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.loadingManager = loadingManager
|
||||||
|
|
||||||
|
this.globalRotation = 0.028
|
||||||
|
|
||||||
|
this.darkTextureRotation = 0.006
|
||||||
|
this.darkMoveForward = 0.009
|
||||||
|
|
||||||
|
this.colorFullTextureRotation = 0.006
|
||||||
|
this.colorFullMoveForward = 0.025
|
||||||
|
|
||||||
|
const commonCylinderGeometry = new THREE.CylinderGeometry(35, 35, 1000, 12, 1, true)
|
||||||
|
|
||||||
|
this.darkCylinder = new THREE.Mesh(
|
||||||
|
commonCylinderGeometry,
|
||||||
|
new THREE.MeshLambertMaterial({
|
||||||
|
side: THREE.BackSide,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
this.add(this.darkCylinder)
|
||||||
|
|
||||||
|
this.colorFullCylinder = new THREE.Mesh(
|
||||||
|
commonCylinderGeometry,
|
||||||
|
new THREE.MeshBasicMaterial({
|
||||||
|
side: THREE.BackSide,
|
||||||
|
blending: THREE.AdditiveBlending,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
this.add(this.colorFullCylinder)
|
||||||
|
|
||||||
|
this.position.set(5, 100, -10)
|
||||||
|
}
|
||||||
|
|
||||||
|
set theme(theme) {
|
||||||
|
switch (theme) {
|
||||||
|
case "Plasma":
|
||||||
|
new THREE.TextureLoader(this.loadingManager).load("./images/plasma.jpg", texture => {
|
||||||
|
texture.wrapS = THREE.RepeatWrapping
|
||||||
|
texture.wrapT = THREE.MirroredRepeatWrapping
|
||||||
|
texture.repeat.set(1, 2)
|
||||||
|
this.darkCylinder.material.map = texture
|
||||||
|
})
|
||||||
|
this.darkCylinder.material.opacity = 0.17
|
||||||
|
|
||||||
|
new THREE.TextureLoader(this.loadingManager).load("./images/plasma2.jpg", texture => {
|
||||||
|
texture.wrapS = THREE.RepeatWrapping
|
||||||
|
texture.wrapT = THREE.MirroredRepeatWrapping
|
||||||
|
texture.repeat.set(2, 2)
|
||||||
|
this.colorFullCylinder.material.map = texture
|
||||||
|
})
|
||||||
|
this.colorFullCylinder.material.opacity = 0.5
|
||||||
|
|
||||||
|
this.globalRotation = 0.028
|
||||||
|
this.darkTextureRotation = 0.005
|
||||||
|
this.darkMoveForward = 0.009
|
||||||
|
this.colorFullTextureRotation = 0.006
|
||||||
|
this.colorFullMoveForward = 0.025
|
||||||
|
|
||||||
|
this.visible = true
|
||||||
|
break
|
||||||
|
|
||||||
|
case "Espace":
|
||||||
|
new THREE.TextureLoader(this.loadingManager).load("./images/dark.jpg", texture => {
|
||||||
|
texture.wrapS = THREE.RepeatWrapping
|
||||||
|
texture.wrapT = THREE.MirroredRepeatWrapping
|
||||||
|
texture.repeat.set(2, 4)
|
||||||
|
this.darkCylinder.material.map = texture
|
||||||
|
})
|
||||||
|
this.darkCylinder.material.opacity = 0.08
|
||||||
|
|
||||||
|
new THREE.TextureLoader(this.loadingManager).load("./images/colorfull.jpg", texture => {
|
||||||
|
texture.wrapS = THREE.RepeatWrapping
|
||||||
|
texture.wrapT = THREE.MirroredRepeatWrapping
|
||||||
|
texture.repeat.set(2, 2)
|
||||||
|
this.colorFullCylinder.material.map = texture
|
||||||
|
})
|
||||||
|
this.colorFullCylinder.material.opacity = 0.15
|
||||||
|
|
||||||
|
this.globalRotation = 0.028
|
||||||
|
this.darkTextureRotation = 0.006
|
||||||
|
this.darkMoveForward = 0.03
|
||||||
|
this.colorFullTextureRotation = 0.006
|
||||||
|
this.colorFullMoveForward = 0.012
|
||||||
|
|
||||||
|
this.visible = true
|
||||||
|
break
|
||||||
|
|
||||||
|
case "Rétro":
|
||||||
|
this.visible = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(delta) {
|
||||||
|
if (this.visible) {
|
||||||
|
this.rotation.y += this.globalRotation * delta
|
||||||
|
|
||||||
|
if (this.darkCylinder.material.map) {
|
||||||
|
this.darkCylinder.material.map.offset.y += this.darkMoveForward * delta
|
||||||
|
this.darkCylinder.material.map.offset.x += this.darkTextureRotation * delta
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.colorFullCylinder.material.map) {
|
||||||
|
this.colorFullCylinder.material.map.offset.y += this.colorFullMoveForward * delta
|
||||||
|
this.colorFullCylinder.material.map.offset.x += this.colorFullTextureRotation * delta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
jsm/scheduler.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
class Scheduler {
|
||||||
|
constructor() {
|
||||||
|
this.intervalTasks = new Map()
|
||||||
|
this.timeoutTasks = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(func, delay, ...args) {
|
||||||
|
this.intervalTasks.set(func, window.setInterval(func, delay, ...args))
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(func) {
|
||||||
|
if (this.intervalTasks.has(func)) {
|
||||||
|
window.clearInterval(this.intervalTasks.get(func))
|
||||||
|
this.intervalTasks.delete(func)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetInterval(func, delay, ...args) {
|
||||||
|
this.clearInterval(func)
|
||||||
|
this.setInterval(func, delay, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(func, delay, ...args) {
|
||||||
|
this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args))
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(func) {
|
||||||
|
if (this.timeoutTasks.has(func)) {
|
||||||
|
window.clearTimeout(this.timeoutTasks.get(func))
|
||||||
|
this.timeoutTasks.delete(func)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTimeout(func, delay, ...args) {
|
||||||
|
this.clearTimeout(func)
|
||||||
|
this.setTimeout(func, delay, ...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const scheduler = new Scheduler()
|
||||||
86
jsm/service-worker.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Incrementing OFFLINE_VERSION will kick off the install event and force
|
||||||
|
// previously cached resources to be updated from the network.
|
||||||
|
const OFFLINE_VERSION = 1;
|
||||||
|
const CACHE_NAME = "offline";
|
||||||
|
// Customize this with a different URL if needed.
|
||||||
|
const OFFLINE_URL = "../index.html";
|
||||||
|
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
// Setting {cache: 'reload'} in the new request will ensure that the
|
||||||
|
// response isn't fulfilled from the HTTP cache; i.e., it will be from
|
||||||
|
// the network.
|
||||||
|
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
// Force the waiting service worker to become the active service worker.
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
// Enable navigation preload if it's supported.
|
||||||
|
// See https://developers.google.com/web/updates/2017/02/navigation-preload
|
||||||
|
if ("navigationPreload" in self.registration) {
|
||||||
|
await self.registration.navigationPreload.enable();
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tell the active service worker to take control of the page immediately.
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
// We only want to call event.respondWith() if this is a navigation request
|
||||||
|
// for an HTML page.
|
||||||
|
if (event.request.mode === "navigate") {
|
||||||
|
event.respondWith(
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// First, try to use the navigation preload response if it's supported.
|
||||||
|
const preloadResponse = await event.preloadResponse;
|
||||||
|
if (preloadResponse) {
|
||||||
|
return preloadResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always try the network first.
|
||||||
|
const networkResponse = await fetch(event.request);
|
||||||
|
return networkResponse;
|
||||||
|
} catch (error) {
|
||||||
|
// catch is only triggered if an exception is thrown, which is likely
|
||||||
|
// due to a network error.
|
||||||
|
// If fetch() returns a valid HTTP response with a response code in
|
||||||
|
// the 4xx or 5xx range, the catch() will NOT be called.
|
||||||
|
console.log("Fetch failed; returning offline page instead.", error);
|
||||||
|
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
const cachedResponse = await cache.match(OFFLINE_URL);
|
||||||
|
return cachedResponse;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our if() condition is false, then this fetch handler won't intercept the
|
||||||
|
// request. If there are any other fetch handlers registered, they will get a
|
||||||
|
// chance to call event.respondWith(). If no fetch handlers call
|
||||||
|
// event.respondWith(), the request will be handled by the browser as if there
|
||||||
|
// were no service worker involvement.
|
||||||
|
});
|
||||||
BIN
thumbnail.png
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 34 KiB |