Compare commits

..

106 Commits

Author SHA1 Message Date
c52a604f0f add README 2025-05-21 10:21:03 +02:00
8e9a089d34 meta 2025-05-20 17:01:59 +02:00
d5893eb8ef fix changeKey 2025-04-08 00:31:52 +02:00
1e006d46b9 little more bumpScale 2024-10-05 15:23:18 +02:00
0f84f90e05 tweak space theme 2024-10-05 15:22:15 +02:00
4287edab71 rename TetraControls 2024-10-03 22:26:37 +02:00
5c2eaca35a remove import 2024-10-03 00:38:10 +02:00
e7dc780173 T class 2024-10-03 00:37:53 +02:00
9721b311eb move mino materials to InstancedMino.prototype 2024-10-03 00:31:12 +02:00
d3f6cf9b71 refactoring 2024-10-03 00:18:42 +02:00
5b058a58b3 load mino retro texture if needed 2024-10-02 22:38:37 +02:00
9fca05ae6e loading... 2024-10-02 14:48:19 +02:00
1a7628bb42 hide screen on loading 2024-10-02 02:17:23 +02:00
bc4ba54c0a spread queues 2024-10-01 21:02:18 +02:00
653befdc02 retro material tweak 2024-10-01 20:50:28 +02:00
1b3f837bf0 dev menu 2024-10-01 20:39:17 +02:00
375d47397e reset 2024-10-01 19:34:02 +02:00
efb6482238 lightsteelblue 2024-10-01 19:31:31 +02:00
7fd3c04a2d rename gui to menu 2024-10-01 18:39:26 +02:00
1e42c2160f textured mino edge 2024-10-01 11:35:41 +02:00
74bf8521fb tweaks 2024-10-01 11:03:37 +02:00
fef08f64e8 tweaks 2024-10-01 10:59:43 +02:00
3c8fc95e23 texture on all faces 2024-10-01 01:57:38 +02:00
f7b7b74e01 move rotationPoint4Used test to rotation 2024-10-01 00:49:38 +02:00
af9e0c481a locked texture 2024-10-01 00:35:06 +02:00
90eb3247e0 new 3d loading animation 2024-09-30 21:23:44 +02:00
2a25dbe4b0 new 3d loading animation 2024-09-30 21:21:50 +02:00
d9397c4bcb textured ghost 2024-09-30 21:21:25 +02:00
ca93423bf8 hide vortex hole 2024-09-30 21:18:38 +02:00
ae8dcb7077 ᵀᴱTᴿᴬ 2024-09-30 14:03:47 +02:00
6ed614d536 MeshStandardMaterial on Plasma 2024-09-27 03:10:23 +02:00
8c5b704b3c MeshPhysicalMaterial on Plasma 2024-09-27 03:06:36 +02:00
1b0f1c07d2 camera init position 2024-09-27 00:52:19 +02:00
c8eb029987 another leak fixed! 2024-09-27 00:47:23 +02:00
825fbca97b fix a leak 2024-09-26 23:15:15 +02:00
ce94604fc0 split fps & dev 2024-09-26 23:15:08 +02:00
07daa4a9cf small fixes 2024-09-26 21:30:12 +02:00
abf562fd89 themes fixes 2024-09-26 13:52:41 +02:00
cae3dc9af5 themed directionnal light position 2024-09-26 11:53:58 +02:00
a75329f985 fix color on theme change 2024-09-26 11:53:31 +02:00
31eca05faf two faces 2024-09-26 09:02:08 +02:00
7f6795109b RETRO THEME 2024-09-26 01:30:31 +02:00
8ed998f255 upatre 2024-09-25 15:24:28 +02:00
aa2475dc3a serviceWorker in html 2024-05-12 11:20:05 +02:00
a0893fd881 tweaking again 2024-02-27 21:53:12 +01:00
1a026db655 fix audio stop 2024-02-27 21:52:57 +01:00
f8081583c5 music as <audio> HTML element 2024-02-27 00:59:19 +01:00
ce1181df62 stream background music 2024-02-27 00:53:41 +01:00
3345a50803 combo sound 2024-02-26 23:53:42 +01:00
c9b242c9c2 tweaks 2024-02-26 23:41:50 +01:00
935343d301 fov 2024-02-24 15:07:18 +01:00
92d953ef62 threejs v155 2023-07-30 15:38:13 +02:00
b227690b31 tweaks 2023-07-20 20:05:13 +02:00
e1da884441 resume on pause key down 2023-07-20 19:35:13 +02:00
b34a968dd2 fix audio restart, game restart, pause on gui 2023-07-18 21:49:14 +02:00
401218bdbe music start 2023-07-18 19:57:44 +02:00
32d4126873 gui dev 2023-07-18 19:37:49 +02:00
85237739bc material type in debug 2023-07-18 03:26:12 +02:00
12fb307041 case insensitive 2023-07-18 02:44:04 +02:00
6004cbbbde fix vortex 2023-07-17 19:38:53 +02:00
38a9dcfad4 case insensitive 2023-07-17 19:35:48 +02:00
dd25b0a891 format 2023-07-17 19:18:53 +02:00
3a657e4c38 tweaks 2023-07-17 19:16:47 +02:00
fcb12f89e7 fix new Tetromino glitch 2023-07-17 09:16:48 +02:00
7acb3a6def fix ghost 2023-07-17 02:56:21 +02:00
367f252444 Merge branch 'master' of https://git.malingrey.fr/adrien/tetra 2023-07-17 02:27:44 +02:00
cfa73565f0 SINGLE INSTANCEDMESH 2023-07-17 02:22:36 +02:00
4c68b05db1 explode 2023-07-17 01:10:21 +02:00
d2a0e241c8 } 2023-07-17 00:59:42 +02:00
b0dbb06dae tweaks 2023-07-17 00:58:34 +02:00
02725494cd ghost material 2023-07-15 22:51:59 +02:00
6f0a540bb3 edge and ghost material 2023-07-15 21:10:30 +02:00
b516de1b2f tweaks again 2023-07-15 00:39:00 +02:00
c42ee23b5f tweaking 2023-07-14 19:56:28 +02:00
0521e2f0ba Merge branch 'master' of https://git.malingrey.fr/adrien/tetra 2023-07-14 19:39:33 +02:00
8b3759f253 tweaking... 2023-07-14 19:37:48 +02:00
945d349319 tweaking... 2023-07-14 19:37:10 +02:00
d0120ca2a6 freed minoes as instanced mesh 2023-07-14 04:09:57 +02:00
eadae0205f INSTANCED MESHES 2023-07-14 02:27:21 +02:00
a4be8e064c fix restart 2023-07-14 00:45:14 +02:00
7e245011ef vortex 2023-07-13 23:21:47 +02:00
a7eff47b97 tweaking 2023-07-13 18:49:28 +02:00
69b2b77fe9 material debug: fps :(( 2023-07-11 21:37:35 +02:00
8279b1188f onLockDown 2023-07-10 23:31:51 +02:00
bf4b296ec0 tweaks 2023-07-10 23:06:19 +02:00
dbc56a19e9 tweaks 2023-07-10 23:02:15 +02:00
0dee582cd0 upper case keys 2023-07-10 12:08:07 +02:00
cc5c89540d transparent minoes 2023-07-10 08:40:48 +02:00
c3c3095153 smaller stars 2023-07-10 08:37:06 +02:00
4b357d5250 change vortex speed 2023-07-09 11:12:30 +02:00
9877f21818 background switcher 2023-07-08 17:33:04 +02:00
f0f3b2aca5 export scene 2023-07-08 14:22:22 +02:00
013fb0bce7 pause style 2023-07-08 12:34:35 +02:00
bdf99b41b7 larger vortex 2023-07-07 22:36:19 +02:00
c4979b5890 directional light position 2023-07-07 20:06:25 +02:00
a6c48989d0 refactoring 2023-07-07 08:53:43 +02:00
058fdd8f8b pause on gui focus 2023-07-06 19:56:09 +02:00
59158d68b8 export controls 2023-07-06 19:42:30 +02:00
a2dc4678c8 pauseButton 2023-07-06 18:33:02 +02:00
d590c41f95 fix lock 2023-07-06 02:06:56 +02:00
11675fa9a2 fix lock 2023-07-05 23:02:39 +02:00
f85ad3f3e7 refactoring 2023-07-05 03:00:02 +02:00
791f594670 world 2023-07-05 02:23:45 +02:00
6d4f7a8dd3 export GUI 2023-07-05 02:18:58 +02:00
ae1fa3e4ad refactoring 2023-07-04 23:48:57 +02:00
c90db794a6 remove message shadow 2023-07-04 22:33:49 +02:00
26 changed files with 1781 additions and 1582 deletions

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# teTra
Falling blocks web game made with three.js librairy
![screenshot](https://git.malingrey.fr/adrien/teTra/raw/branch/master/thumbnail.png)

1303
app.js

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

BIN
audio/benevolence.m4a Normal file

Binary file not shown.

70
css/loading.css Normal file
View 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); }

View File

@ -2,21 +2,23 @@ body {
margin: 0;
padding: 0;
background-color: #222;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
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-gui {
.lil-menu {
--background-color: rgba(33, 37, 41, 30%);
--width: 200px;
}
@supports (backdrop-filter: blur()) {
.lil-gui {
.lil-menu {
backdrop-filter: blur(15px);
}
}
@ -27,7 +29,11 @@ span {
left: 15px;
}
.lil-gui .controller.disabled {
.lil-menu.root > .title {
font-size: 1.5em;
}
.lil-menu .controller.disabled {
opacity: .8;
}
@ -42,7 +48,6 @@ canvas {
width: 50%;
transform: translate(-50%, 0);
color: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px rgba(0, 0, 0, 0.8);
font-weight: 400;
line-height: 1.5;
font-size: 3vmin;
@ -52,6 +57,7 @@ canvas {
#messagesSpan div {
opacity: 0;
overflow: hidden;
user-select: none;
}
h1 {
@ -158,23 +164,28 @@ h1 {
animation-duration: 2s;
}
.pause canvas {
filter: blur(10px);
}
#pauseSpan {
display: none;
}
#pauseSpan.pause {
display: flex;
position:absolute;
display: flex;
.pause #pauseSpan {
position: absolute;
top: 0;
left: 0;
backdrop-filter: blur(10px);
display: flex;
filter: blur(2px);
width: 100%;
height: 100%;
z-index: 10;
color: rgba(255, 255, 255, 20%);
text-align: center;
justify-content: center;
align-items: center;
font-size: 10vh;
font-size: 20vh;
font-weight: 800;
letter-spacing: .1em;
user-select: none;
}

159
gui.html
View File

@ -1,159 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.css">
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.152.2/build/three.module.js?module",
"three/addons/": "https://unpkg.com/three@0.152.2/examples/jsm/"
}
}
</script>
<style>
body {
background: url(https://adrien.malingrey.fr/jeux/.assets/themes/clouds/background.jpg);
}
.lil-gui {
--background-color: rgba(33, 37, 41, 30%);
backdrop-filter: blur(15px);
}
.lil-gui.autoPlace {
left: 15px;
}
.lil-gui .controller.disabled {
opacity: .8;
}
i {
display: inline-block;
width: 100%;
text-align: center;
}
</style>
</head>
<body>
<script type="module">
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
var game = {
startLevel: 1,
start: () => {
gui.gameFolder.hide()
gui.statsFolder.show()
gui.statsFolder.open()
gui.settingsFolder.close()
},
}
var stats = {
level: 1,
goal: 0,
score: 0,
highScore:0,
time: "00:00:00",
}
var settings = {
moveLeftKey : "ArrowLeft",
moveRightKey: "ArrowRight",
rotateCCWKey: "w",
rotateCWKey : "ArrowUp",
softDropKey : "ArrowDown",
hardDropKey : " ",
holdKey : "c",
pauseKey : "Escape",
arrDelay: 50,
dasDelay: 300,
musicVolume: 50,
sfxVolume : 50,
};
const KEY_NAMES = {
["ArrowLeft"] : "←",
["ArrowRight"] : "→",
["ArrowUp"] : "↑",
["ArrowDown"] : "↓",
[" "] : "Espace",
["Escape"] : "Échap.",
["Backspace"] : "Ret. arrière",
["Enter"] : "Entrée",
["←"] : "ArrowLeft",
["→"] : "ArrowRight",
["↑"] : "ArrowUp",
["↓"] : "ArrowDown",
["Espace"] : " ",
["Échap."] : "Escape",
["Ret. arrière"]: "Backspace",
["Entrée"] : "Enter",
}
function changeKey(event) {
const input = event.target
let prevValue = input.value
input.value = ""
input.onkeydown = function (event) {
event.preventDefault()
input.value = KEY_NAMES[event.key] || event.key
input.blur()
}
input.onblur = function (event) {
if (input.value == "") input.value = prevValue
input.onkeydown = null
input.onblur = null
}
}
class Gui extends GUI {
constructor() {
super({title: "teTra"});
this.gameFolder = this.addFolder("Partie")
this.gameFolder.add(game, "startLevel").name("Niveau").min(1).max(15).step(1)
this.gameFolder.add(game, "start").name("Commencer")
this.gameFolder.open()
this.statsFolder = this.addFolder("Stats")
this.statsFolder.add(stats,"level").name("Niveau").disable()
this.statsFolder.add(stats,"goal").name("Objectif").disable()
this.statsFolder.add(stats,"score").name("Score").disable()
this.statsFolder.add(stats,"highScore").name("Meilleur score").disable()
this.statsFolder.add(stats,"time").name("Temps").disable()
this.statsFolder.hide()
this.settingsFolder = this.addFolder("Options");
this.settingsFolder.close()
this.settingsFolder.keyMapping = this.settingsFolder.addFolder("Commandes")
this.settingsFolder.keyMapping.add(settings,"moveLeftKey").name('<i class="bi bi-arrow-left"></i>').domElement.onclick = changeKey
this.settingsFolder.keyMapping.add(settings,"moveRightKey").name('<i class="bi bi-arrow-right"></i>').domElement.onclick = changeKey
this.settingsFolder.keyMapping.add(settings,"rotateCCWKey").name('<i class="bi bi-arrow-counterclockwise"></i>').domElement.onclick = changeKey
this.settingsFolder.keyMapping.add(settings,"rotateCWKey").name('<i class="bi bi-arrow-clockwise"></i>').domElement.onclick = changeKey
this.settingsFolder.keyMapping.add(settings,"softDropKey").name('<i class="bi bi-arrow-down-short"></i>').domElement.onclick = changeKey
this.settingsFolder.keyMapping.add(settings,"hardDropKey").name('<i class="bi bi-download"></i>').domElement.onclick = changeKey
this.settingsFolder.keyMapping.add(settings,"holdKey").name('<i class="bi bi-arrow-left-right"></i>').domElement.onclick = changeKey
this.settingsFolder.keyMapping.add(settings,"pauseKey").name('<i class="bi bi-pause"></i>').domElement.onclick = changeKey
this.settingsFolder.keyMapping.open()
this.settingsFolder.delayFolder = this.settingsFolder.addFolder("Répétition automatique")
this.settingsFolder.delayFolder.add(settings,"arrDelay").name("ARR (ms)").min(2).max(200);
this.settingsFolder.delayFolder.add(settings,"dasDelay").name("DAS (ms)").min(100).max(500).step(5);
this.settingsFolder.delayFolder.open()
this.settingsFolder.volumeFolder = this.settingsFolder.addFolder("Volume")
this.settingsFolder.volumeFolder.add(settings,"musicVolume").name("Musique").min(0).max(100);
this.settingsFolder.volumeFolder.add(settings,"sfxVolume").name("SFX").min(0).max(100)
this.settingsFolder.volumeFolder.open()
}
}
var gui = new Gui();
</script>
</body>
</html>

BIN
images/colorfull.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

BIN
images/dark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 KiB

BIN
images/edge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
images/edges.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
images/fond_etoile.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
images/sprites.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -4,34 +4,78 @@
<head>
<meta charset="utf-8" />
<title>teTra</title>
<title>ᵀᴱTᴿᴬ</title>
<link rel="icon" href="favicon.ico">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="loading.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/loading.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 type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.152.2/build/three.module.js?module",
"three/addons/": "https://unpkg.com/three@0.152.2/examples/jsm/"
"three": "https://unpkg.com/three@0.169/build/three.module.js?module",
"three/addons/": "https://unpkg.com/three@0.169/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="loaddingCircle">
<div class="e-loadholder">
<div class="m-loader">
<span class="e-text">
<div>Chargement</div>
<div id="loadingPercent">0%</div></span>
</div>
<span id="loadingDiv">
<div class="scene">
<div class="T tetromino">
<div class="first 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 class="second 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 class="third 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 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>Chargement</div>
<div id="loadingPercent">0%</div>
</div>
</span>
<span id="messagesSpan"></span>
<span id="pauseSpan" tabindex="1">PAUSE</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 type="module" src="app.js"></script>
<script>navigator?.serviceWorker?.register('./jsm/service-worker.js')</script>
</body>
</html>

21
jsm/CameraControls.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()

View File

@ -16,7 +16,7 @@ Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = "index.html";
const OFFLINE_URL = "../index.html";
self.addEventListener("install", (event) => {
event.waitUntil(

View File

@ -1,274 +0,0 @@
@-webkit-keyframes outerRotate1 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@-moz-keyframes outerRotate1 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@-o-keyframes outerRotate1 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes outerRotate1 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@-webkit-keyframes outerRotate2 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
@-moz-keyframes outerRotate2 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
@-o-keyframes outerRotate2 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
@keyframes outerRotate2 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
@-webkit-keyframes textColour {
0% {
color: #fff;
}
100% {
color: #3BB2D0;
}
}
@-moz-keyframes textColour {
0% {
color: #fff;
}
100% {
color: #3BB2D0;
}
}
@-o-keyframes textColour {
0% {
color: #fff;
}
100% {
color: #3BB2D0;
}
}
@keyframes textColour {
0% {
color: #fff;
}
100% {
color: #3BB2D0;
}
}
#loaddingCircle {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
}
.e-loadholder {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
width: 240px;
height: 240px;
border: 5px solid #1B5F70;
border-radius: 120px;
box-sizing: border-box;
}
.e-loadholder:after {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
content: " ";
display: block;
background: #222;
transform-origin: center;
z-index: 0;
}
.e-loadholder:after {
width: 100px;
height: 200%;
-webkit-animation: outerRotate2 30s infinite linear;
-moz-animation: outerRotate2 30s infinite linear;
-o-animation: outerRotate2 30s infinite linear;
animation: outerRotate2 30s infinite linear;
}
.e-loadholder .m-loader {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
width: 200px;
height: 200px;
color: #888;
text-align: center;
border: 5px solid #2a93ae;
border-radius: 100px;
box-sizing: border-box;
z-index: 20;
text-transform: uppercase;
}
.e-loadholder .m-loader:after {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
content: " ";
display: block;
background: #222;
transform-origin: center;
z-index: -1;
}
.e-loadholder .m-loader:after {
width: 100px;
height: 106%;
-webkit-animation: outerRotate1 15s infinite linear;
-moz-animation: outerRotate1 15s infinite linear;
-o-animation: outerRotate1 15s infinite linear;
animation: outerRotate1 15s infinite linear;
}
.e-loadholder .m-loader .e-text {
font-family: "Open Sans", sans-serif;
font-size: 10px;
font-size: 1rem;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
-webkit-animation: textColour 1s alternate linear infinite;
-moz-animation: textColour 1s alternate linear infinite;
-o-animation: textColour 1s alternate linear infinite;
animation: textColour 1s alternate linear infinite;
display: flex;
flex-direction: column;
justify-content: center;
width: 140px;
height: 140px;
text-align: center;
border: 5px solid #3bb2d0;
border-radius: 70px;
box-sizing: border-box;
z-index: 20;
}
.e-loadholder .m-loader .e-text:before, .e-loadholder .m-loader .e-text:after {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
content: " ";
display: block;
background: #222;
transform-origin: center;
z-index: -1;
}
.e-loadholder .m-loader .e-text:before {
width: 110%;
height: 40px;
-webkit-animation: outerRotate2 3.5s infinite linear;
-moz-animation: outerRotate2 3.5s infinite linear;
-o-animation: outerRotate2 3.5s infinite linear;
animation: outerRotate2 3.5s infinite linear;
}
.e-loadholder .m-loader .e-text:after {
width: 40px;
height: 110%;
-webkit-animation: outerRotate1 8s infinite linear;
-moz-animation: outerRotate1 8s infinite linear;
-o-animation: outerRotate1 8s infinite linear;
animation: outerRotate1 8s infinite linear;
}