Compare commits

..

78 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
23 changed files with 1014 additions and 880 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)

172
app.js
View File

@ -1,11 +1,12 @@
import * as THREE from 'three'
import { scheduler } from './jsm/scheduler.js'
import { TRANSLATION, ROTATION, environnement, Playfield, HoldQueue, NextQueue } from './jsm/gamelogic.js'
import { Settings } from './jsm/Settings.js'
import { TRANSLATION, ROTATION, environment, InstancedMino, Mino, Playfield, HoldQueue, NextQueue } from './jsm/Tetrominoes.js'
import Settings from './jsm/Settings.js'
import { Stats } from './jsm/Stats.js'
import { TetraGUI } from './jsm/TetraGUI.js'
import { TetraControls } from './jsm/TetraControls.js'
import { Menu } from './jsm/Menu.js'
import CameraControls from './jsm/CameraControls.js'
import { TetraScene } from './jsm/TetraScene.js'
import * as FPS from 'three/addons/libs/stats.module.js'
HTMLElement.prototype.addNewChild = function (tag, properties) {
@ -23,30 +24,24 @@ let game = {
playing: false,
start: function() {
gui.startButton.hide()
stats.init()
gui.stats.show()
gui.settings.close()
holdQueue.remove(holdQueue.piece)
menu.startButton.hide()
menu.stats.show()
menu.settings.close()
Mino.instances.clear()
nextQueue.init()
holdQueue.piece = undefined
if (nextQueue.pieces) nextQueue.pieces.forEach(piece => nextQueue.remove(piece))
holdQueue.clear()
playfield.init()
scene.remove(playfield.piece)
if (playfield.piece) playfield.remove(playfield.piece)
playfield.piece = null
scene.music.currentTime = 0
playfield.visible = true
this.playing = true
stats.clock.start()
renderer.domElement.tabIndex = 1
gui.domElement.tabIndex = 1
nextQueue.init()
stats.level = settings.startLevel
this.resume()
},
@ -55,18 +50,22 @@ let game = {
document.onkeydown = onkeydown
document.onkeyup = onkeyup
window.onblur = game.pause
if (!gui.debug) gui.domElement.onfocus = game.pause
menu.settings.domElement.onclick = game.pause
document.body.classList.remove("pause")
gui.resumeButton.hide()
gui.pauseButton.show()
menu.resumeButton.hide()
menu.pauseButton.show()
stats.clock.start()
stats.clock.elapsedTime = stats.elapsedTime
scene.music.play()
if (playfield.piece) scheduler.setInterval(game.fall, stats.fallPeriod)
else this.generate()
if (settings.musicVolume) scene.music.play()
if (playfield.piece) {
scheduler.resetInterval(game.fall, stats.fallPeriod)
} else {
this.generate()
}
},
generate: function(nextPiece=nextQueue.shift()) {
@ -75,7 +74,7 @@ let game = {
playfield.piece.onLockDown = game.lockDown
if (playfield.piece.canMove(TRANSLATION.NONE)) {
scheduler.setInterval(game.fall, stats.fallPeriod)
scheduler.resetInterval(game.fall, stats.fallPeriod)
} else {
game.over() // block out
}
@ -92,17 +91,14 @@ let game = {
if (playfield.lock(playfield.piece)) {
let tSpin = playfield.piece.tSpin
let nbClearedLines = playfield.clearLines()
playfield.remove(playfield.piece)
stats.lockDown(nbClearedLines, tSpin)
if (settings.sfxVolume) {
if (nbClearedLines == 4 || (tSpin && nbClearedLines)) {
scene.tetrisSound.currentTime = 0
scene.tetrisSound.play()
playSound(scene.tetrisSound, stats.combo)
} else if (nbClearedLines || tSpin) {
scene.lineClearSound.currentTime = 0
scene.lineClearSound.play()
playSound(scene.lineClearSound, stats.combo)
}
}
stats.lockDown(nbClearedLines, tSpin)
game.generate()
} else {
@ -111,6 +107,8 @@ let game = {
},
pause: function() {
menu.settings.domElement.onclick = null
stats.elapsedTime = stats.clock.elapsedTime
stats.clock.stop()
@ -120,13 +118,14 @@ let game = {
scheduler.clearInterval(autorepeat)
scene.music.pause()
document.onkeydown = null
document.onkeydown = resumeOnKeyDown
document.onkeyup = null
window.onblur = null
pauseSpan.onfocus = game.resume
document.body.classList.add("pause")
gui.pauseButton.hide()
gui.resumeButton.show()
menu.pauseButton.hide()
menu.resumeButton.show()
},
over: function() {
@ -135,19 +134,27 @@ let game = {
document.onkeydown = null
window.onblur = null
renderer.domElement.onfocus = null
gui.domElement.onfocus = null
menu.settings.domElement.onfocus = null
game.playing = false
scene.music.pause()
stats.clock.stop()
messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `<h1>GAME<br/>OVER</h1>` })
gui.pauseButton.hide()
gui.startButton.name("Rejouer")
gui.startButton.show()
menu.pauseButton.hide()
menu.startButton.name("Rejouer")
menu.startButton.show()
},
}
function playSound(sound, note=0) {
sound.stop()
sound.currentTime = 0
sound.playbackRate = Math.pow(5/4, note)
sound.play()
}
/* Handle player inputs */
let playerActions = {
@ -165,9 +172,8 @@ let playerActions = {
hardDrop: function () {
scheduler.clearTimeout(game.lockDown)
scene.hardDropSound.play()
if (settings.sfxVolume) {
scene.hardDropSound.currentTime = 0
scene.hardDropSound.stop()
scene.hardDropSound.play()
}
while (playfield.piece.move(TRANSLATION.DOWN)) stats.score += 2
@ -210,8 +216,8 @@ function onkeydown(event) {
actionsQueue.unshift(action)
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
if (action == playerActions.softDrop) scheduler.setInterval(autorepeat, settings.fallPeriod / 20)
else scheduler.setTimeout(repeat, settings.dasDelay)
if (action == playerActions.softDrop) scheduler.resetInterval(autorepeat, settings.fallPeriod / 20)
else scheduler.resetTimeout(repeat, settings.dasDelay)
}
}
}
@ -220,7 +226,7 @@ function onkeydown(event) {
function repeat() {
if (actionsQueue.length) {
actionsQueue[0]()
scheduler.setInterval(autorepeat, settings.arrDelay)
scheduler.resetInterval(autorepeat, settings.arrDelay)
}
}
@ -248,26 +254,17 @@ function onkeyup(event) {
}
}
function resumeOnKeyDown(event) {
let key = event.key
if(playerActions[settings.action[key]] == playerActions.pause) {
event.preventDefault()
game.resume()
}
}
/* Scene */
const loadingManager = new THREE.LoadingManager()
loadingManager.onStart = function (url, itemsLoaded, itemsTotal) {
loadingPercent.innerText = "0%"
}
loadingManager.onProgress = function (url, itemsLoaded, itemsTotal) {
loadingPercent.innerText = Math.floor(100 * itemsLoaded / itemsTotal) + '%'
}
loadingManager.onLoad = function () {
loaddingCircle.remove()
renderer.setAnimationLoop(animate)
gui.startButton.show()
}
loadingManager.onError = function (url) {
loadingPercent.innerText = "Erreur"
}
const renderer = new THREE.WebGLRenderer({
powerPreference: "high-performance",
antialias: true,
@ -277,41 +274,68 @@ renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setClearColor(0x000000, 10)
renderer.toneMapping = THREE.ACESFilmicToneMapping
document.body.appendChild(renderer.domElement)
renderer.domElement.tabIndex = 1
let loadingManager = new THREE.LoadingManager(
function() {
loadingDiv.style.display = "none"
menu.startButton.show()
renderer.setAnimationLoop(animate)
},
function (url, itemsLoaded, itemsTotal) {
loadingPercent.innerText = Math.floor(100 * itemsLoaded / itemsTotal) + '%'
},
function (url) {
loadingPercent.innerText = "Erreur"
}
)
loadingManager.onStart = function (url, itemsLoaded, itemsTotal) {
loadingPercent.innerText = "0%"
loadingDiv.style.display = "flex"
}
const stats = new Stats()
const settings = new Settings()
const scene = new TetraScene(settings, loadingManager)
const controls = new CameraControls(scene.camera, renderer.domElement)
const scene = new TetraScene(loadingManager, settings)
const gui = new TetraGUI(game, settings, stats, scene)
const clock = new THREE.Clock()
const minoes = new InstancedMino()
scene.add(minoes)
const holdQueue = new HoldQueue()
scene.add(holdQueue)
const playfield = new Playfield()
const playfield = new Playfield(loadingManager)
scene.add(playfield)
const nextQueue = new NextQueue()
scene.add(nextQueue)
const controls = new TetraControls(scene.camera, renderer.domElement)
const menu = new Menu(game, settings, stats, scene, minoes, playfield)
menu.load()
let fps
if (window.location.search.includes("fps")) {
let fps = new FPS.default()
document.body.appendChild(fps.dom)
}
messagesSpan.onanimationend = function (event) {
event.target.remove()
}
function animate() {
const clock = new THREE.Clock()
function animate() {
const delta = clock.getDelta()
scene.updateMatrixWorld()
scene.update(delta)
playfield.update(delta)
minoes.update()
controls.update()
gui.update()
renderer.render(scene, scene.camera)
environnement.camera.update(renderer, scene)
environment.camera.update(renderer, scene)
fps?.update()
}
window.addEventListener("resize", () => {
@ -321,11 +345,7 @@ window.addEventListener("resize", () => {
})
window.onbeforeunload = function (event) {
gui.save()
menu.save()
localStorage["teTraHighScore"] = stats.highScore
return !game.playing
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./jsm/service-worker.js');
}

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;
}
@ -51,6 +57,7 @@ canvas {
#messagesSpan div {
opacity: 0;
overflow: hidden;
user-select: none;
}
h1 {
@ -166,11 +173,10 @@ h1 {
}
.pause #pauseSpan {
display: flex;
position:absolute;
display: flex;
position: absolute;
top: 0;
left: 0;
display: flex;
filter: blur(2px);
width: 100%;
height: 100%;
@ -181,4 +187,5 @@ h1 {
font-size: 20vh;
font-weight: 800;
letter-spacing: .1em;
user-select: none;
}

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,35 +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">
<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>Chargement</div>
<div id="loadingPercent">0%</div>
</div>
</span>
</div>
</div>
</div>
<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 type="module" src="app.js"></script>
<script>navigator?.serviceWorker?.register('./jsm/service-worker.js')</script>
</body>
</html>

View File

@ -1,8 +1,7 @@
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
class TetraControls extends OrbitControls {
export default class CameraControls extends OrbitControls {
constructor(camera, domElement) {
super(camera, domElement)
this.autoRotate
@ -10,15 +9,13 @@ class TetraControls extends OrbitControls {
this.dampingFactor = 0.04
this.maxDistance = 21
this.keys = {}
this.minPolarAngle = 0.9
this.maxPolarAngle = 2.14
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, 9, 0)
this.target.set(5, 7.5, 0)
this.addEventListener("start", () => domElement.style.cursor = "grabbing")
this.addEventListener("end", () => domElement.style.cursor = "grab")
}
}
export { TetraControls }

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()
}
}
}

View File

@ -1,33 +1,33 @@
let jsKeyRenamer = new Proxy({
["←"] : "ArrowLeft",
["→"] : "ArrowRight",
["↑"] : "ArrowUp",
["↓"] : "ArrowDown",
["Espace"] : " ",
["Échap."] : "Escape",
["←"]: "ArrowLeft",
["→"]: "ArrowRight",
["↑"]: "ArrowUp",
["↓"]: "ArrowDown",
["Espace"]: " ",
["Échap."]: "Escape",
["Ret. arrière"]: "Backspace",
["Entrée"] : "Enter",
["Entrée"]: "Enter",
}, {
get(obj, keyName) {
return keyName in obj? obj[keyName] : 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",
["ArrowLeft"]: "←",
["ArrowRight"]: "→",
["ArrowUp"]: "↑",
["ArrowDown"]: "↓",
[" "]: "Espace",
["Escape"]: "Échap.",
["Backspace"]: "Ret. arrière",
["Enter"]: "Entrée",
}, {
get(obj, keyName) {
return keyName in obj? obj[keyName] : keyName
return keyName in obj ? obj[keyName] : keyName.toUpperCase()
}
})
class Settings {
export default class Settings {
constructor() {
this.startLevel = 1
@ -38,8 +38,9 @@ class Settings {
this.key = new Proxy(keyMaps, {
set(km, action, key) {
km.action[key] = action
return km.key[action] = jsKeyRenamer[key]
key = jsKeyRenamer[key]
km.action[key.toLowerCase()] = action
return km.key[action] = key
},
has(km, action) {
return action in km.key
@ -51,13 +52,13 @@ class Settings {
this.action = new Proxy(keyMaps, {
set(km, key, action) {
km.key[action] = key
return km.action[key] = action
return km.action[key.toLowerCase()] = action
},
has(km, key) {
return key in km.action
return key.toLowerCase() in km.action
},
get(km, key) {
return km.action[key]
return km.action[key.toLowerCase()]
}
})
@ -75,8 +76,7 @@ class Settings {
this.musicVolume = 50
this.sfxVolume = 50
this.theme = "Plasma"
}
}
export { Settings }

View File

@ -1,5 +1,5 @@
import { Clock } from 'three'
import { T_SPIN } from './gamelogic.js'
import { T_SPIN } from './Tetrominoes.js'
// score = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]

View File

@ -1,195 +0,0 @@
import * as THREE from 'three'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import * as FPS from 'three/addons/libs/stats.module.js'
import { COLORS, environnement, minoMaterial, I, J, L, O, S, T, Z, Tetromino } from './gamelogic.js'
export class TetraGUI extends GUI {
constructor(game, settings, stats, scene) {
super({title: "teTra"})
this.domElement.tabIndex = 1
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("Stats").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").open()
this.settings.add(settings, "startLevel").name("Niveau initial").min(1).max(15).step(1)
this.settings.add(scene.vortex, "background", ["Plasma", "Espace"]).name("Fond").onChange(background => {
const loadingManager = new THREE.LoadingManager()
let darkTexture, colorfullTexture
switch (background) {
case "Plasma":
darkTexture = new THREE.TextureLoader(loadingManager).load("./images/plasma2.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 1)
})
colorfullTexture = new THREE.TextureLoader(loadingManager).load("./images/plasma.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(1, 1)
})
loadingManager.onLoad = function() {
scene.vortex.darkCylinder.material.map = darkTexture
scene.vortex.darkCylinder.material.opacity = 0.75
scene.vortex.colorFullCylinder.material.map = colorfullTexture
scene.vortex.colorFullCylinder.material.opacity = 0.075
scene.vortex.globalRotation = 0.028
scene.vortex.darkTextureRotation = 0.005
scene.vortex.darkMoveForward = 0.012
scene.vortex.colorFullTextureRotation = 0.006
scene.vortex.colorFullMoveForward = 0.016
}
break
case "Espace":
darkTexture = new THREE.TextureLoader(loadingManager).load("./images/dark.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 4)
})
colorfullTexture = new THREE.TextureLoader(loadingManager).load("./images/colorfull.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(1, 2)
})
loadingManager.onLoad = function() {
scene.vortex.darkCylinder.material.map = darkTexture
scene.vortex.darkCylinder.material.opacity = 0.2
scene.vortex.colorFullCylinder.material.map = colorfullTexture
scene.vortex.colorFullCylinder.material.opacity = 0.2
scene.vortex.globalRotation = 0.028
scene.vortex.darkTextureRotation = 0.006
scene.vortex.darkMoveForward = 0.007
scene.vortex.colorFullTextureRotation = 0.006
scene.vortex.colorFullMoveForward = 0.02
scene.ambientLight.intensity = 2
scene.directionalLight.intensity = 3
}
break
}
})
this.settings.key = this.settings.addFolder("Commandes").open()
let moveLeftKeyController = this.settings.key.add(settings.key, "moveLeft").name('Gauche')
moveLeftKeyController.domElement.onclick = this.changeKey.bind(moveLeftKeyController)
let moveRightKeyController = this.settings.key.add(settings.key, "moveRight").name('Droite')
moveRightKeyController.domElement.onclick = this.changeKey.bind(moveRightKeyController)
let rotateCWKeyController = this.settings.key.add(settings.key, "rotateCW").name('Rotation horaire')
rotateCWKeyController.domElement.onclick = this.changeKey.bind(rotateCWKeyController)
let rotateCCWKeyController = this.settings.key.add(settings.key, "rotateCCW").name('anti-horaire')
rotateCCWKeyController.domElement.onclick = this.changeKey.bind(rotateCCWKeyController)
let softDropKeyController = this.settings.key.add(settings.key, "softDrop").name('Chute lente')
softDropKeyController.domElement.onclick = this.changeKey.bind(softDropKeyController)
let hardDropKeyController = this.settings.key.add(settings.key, "hardDrop").name('Chute rapide')
hardDropKeyController.domElement.onclick = this.changeKey.bind(hardDropKeyController)
let holdKeyController = this.settings.key.add(settings.key, "hold").name('Garder')
holdKeyController.domElement.onclick = this.changeKey.bind(holdKeyController)
let pauseKeyController = this.settings.key.add(settings.key, "pause").name('Pause')
pauseKeyController.domElement.onclick = this.changeKey.bind(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.setVolume(volume/100)
if (game.playing) {
if (volume) scene.music.play()
} else {
scene.music.pause()
}
})
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)
})
this.debug = window.location.search.includes("debug")
if (this.debug) {
this.debug = this.addFolder("debug")
let cameraPosition = this.debug.addFolder("camera.position").close()
cameraPosition.add(scene.camera.position, "x")
cameraPosition.add(scene.camera.position, "y")
cameraPosition.add(scene.camera.position, "z")
let directionalLightPosition = this.debug.addFolder("directionalLight.position").close()
directionalLightPosition.add(scene.directionalLight.position, "x")
directionalLightPosition.add(scene.directionalLight.position, "y")
directionalLightPosition.add(scene.directionalLight.position, "z")
let directionalLightTargetPosition = this.debug.addFolder("directionalLight.target").close()
directionalLightTargetPosition.add(scene.directionalLight.target.position, "x")
directionalLightTargetPosition.add(scene.directionalLight.target.position, "y")
directionalLightTargetPosition.add(scene.directionalLight.target.position, "z")
let light = this.debug.addFolder("lights intensity").close()
light.add(scene.ambientLight, "intensity").name("ambient").min(0).max(20)
light.add(scene.directionalLight, "intensity").name("directional").min(0).max(20)
let vortex = this.debug.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)
let material = this.debug.addFolder("minoes material").close()
material.add(minoMaterial, "opacity").min(0).max(1)
//material.add(minoMaterial, "reflectivity").min(0).max(1)
material.add(minoMaterial, "roughness").min(0).max(1)
material.add(minoMaterial, "metalness").min(0).max(1)
//material.add(minoMaterial, "attenuationDistance").min(0).max(1).hide()
//material.add(minoMaterial, "ior").min(1).max(2).hide()
//material.add(minoMaterial, "sheen").min(0).max(1).hide()
//material.add(minoMaterial, "sheenRoughness").min(0).max(1).hide()
//material.add(minoMaterial, "specularIntensity").min(0).max(1).hide()
//material.add(minoMaterial, "thickness").min(0).max(5).hide()
//material.add(minoMaterial, "transmission").min(0).max(1).hide()
this.fps = new FPS.default()
document.body.appendChild(this.fps.dom)
}
this.load()
}
load() {
if (localStorage["teTraSettings"]) {
this.settings.load(JSON.parse(localStorage["teTraSettings"]))
}
}
save() {
localStorage["teTraSettings"] = JSON.stringify(this.settings.save())
}
changeKey() {
let controller = this
let input = controller.domElement.getElementsByTagName("input")[0]
input.select()
input.onkeydown = function (event) {
controller.setValue(event.key)
input.blur()
}
}
update() {
this.fps?.update()
}
}

View File

@ -3,56 +3,68 @@ import { Vortex } from './Vortex.js'
export class TetraScene extends THREE.Scene {
constructor(loadingManager, settings) {
constructor(settings, loadingManager) {
super()
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
this.camera.position.set(5, 0, 16)
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, .5)
this.ambientLight = new THREE.AmbientLight(0xffffff, 1)
this.add(this.ambientLight)
this.directionalLight = new THREE.DirectionalLight(0xffffff, 6)
this.directionalLight.position.set(5, -100, 0)
this.directionalLight = new THREE.DirectionalLight(0xffffff, 5)
this.add(this.directionalLight)
this.directionalLight.target = new THREE.Object3D()
this.directionalLight.target.position.set(5, -50, 20)
this.add(this.directionalLight.target)
this.theme = settings.theme
/* Sounds */
this.music = music
const listener = new THREE.AudioListener()
this.camera.add( listener )
const audioLoader = new THREE.AudioLoader(loadingManager)
this.music = new THREE.Audio(listener)
audioLoader.load('audio/Tetris_T-Spin_OC_ReMix.mp3', function( buffer ) {
this.music.setBuffer(buffer)
this.music.setLoop(true)
this.music.setVolume(settings.musicVolume/100)
//if (game.playing) this.music.play()
}.bind(this))
this.lineClearSound = new THREE.Audio(listener)
audioLoader.load('audio/line-clear.ogg', function( buffer ) {
this.lineClearSound.setBuffer(buffer)
this.lineClearSound.setVolume(settings.sfxVolume/100)
}.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)
this.hardDropSound.setVolume(settings.sfxVolume/100)
}.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)
}

View File

@ -1,12 +1,13 @@
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)
let P = (x, y, z=0) => new THREE.Vector3(x, y, z)
const GRAVITY = -20
const GRAVITY = -30
const COLORS = {
I: 0xafeff9,
@ -16,8 +17,10 @@ const COLORS = {
S: 0xC8FBA8,
T: 0xedb2ff,
Z: 0xffb8c5,
LOCKING: "white",
GHOST: "white",
LOCKING: 0xffffff,
GHOST: 0x99a9b2,
EDGE: 0x88abe0,
RETRO: 0xd0d4c1,
}
const TRANSLATION = {
@ -47,20 +50,34 @@ const FACING = {
}
const ROWS = 24
const SKYLINE = 20
const COLUMNS = 10
const envRenderTarget = new THREE.WebGLCubeRenderTarget(256)
const environnement = envRenderTarget.texture
environnement.type = THREE.HalfFloatType
environnement.camera = new THREE.CubeCamera(1, 1000, envRenderTarget)
environnement.camera.position.set(5, 10)
const environment = envRenderTarget.texture
environment.type = THREE.HalfFloatType
environment.camera = new THREE.CubeCamera(1, 1000, envRenderTarget)
environment.camera.position.set(5, 10, 0)
const minoFaceShape = new THREE.Shape()
minoFaceShape.moveTo(.1, .1)
minoFaceShape.lineTo(.1, .9)
minoFaceShape.lineTo(.9, .9)
minoFaceShape.lineTo(.9, .1)
minoFaceShape.lineTo(.1, .1)
const minoExtrudeSettings = {
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,
@ -68,56 +85,152 @@ const minoExtrudeSettings = {
bevelSize: .1,
bevelOffset: 0,
bevelSegments: 1
}
let minoGeometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings)
}
const geometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings)
super(geometry, undefined, 2*ROWS*COLUMNS)
this.offsets = new Uint8Array(2*this.count)
}
let minoMaterial = new THREE.MeshStandardMaterial({
envMap: environnement,
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,
//reflectivity: 0.8,
roughness: 0.1,
metalness: 0.9,
//attenuationDistance: 0.5,
//ior: 2,
//sheen: 0,
//sheenRoughness: 1,
//specularIntensity: 1,
//thickness: 5,
//transmission: 1,
})
metalness: 0.99,
})
}
class Mino extends THREE.Object3D {
constructor(color, x, y, z=0) {
static instances = new Set()
constructor(color, offset) {
super()
this.color = color
this.position.set(x, y, z)
this.velocity = P(50 - 100 * Math.random(), 50 - 100 * Math.random(), 50 - 100 * Math.random())
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)
}
update(delta) {
explode(delta) {
this.velocity.y += delta * GRAVITY
this.position.addScaledVector(this.velocity, delta)
this.rotateOnWorldAxis(this.rotationAngle, delta * this.angularVelocity)
this.updateMatrix()
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.InstancedMesh {
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() {
super(minoGeometry, undefined, 4)
this.material = this.minoMaterial
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
@ -127,12 +240,7 @@ class Tetromino extends THREE.InstancedMesh {
set facing(facing) {
this._facing = facing
let matrix4 = new THREE.Matrix4()
this.minoesPosition[this.facing].forEach((position, i) => {
matrix4.setPosition(position)
this.setMatrixAt(i, matrix4)
})
this.instanceMatrix.needsUpdate = true
this.children.forEach((mino, i) => mino.position.copy(this.minoesPosition[facing][i]))
}
get facing() {
@ -142,35 +250,32 @@ class Tetromino extends THREE.InstancedMesh {
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) {
for (let i = 0; i < this.count; i++) {
this.setColorAt(i, color)
}
this.instanceColor.needsUpdate = true
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)))
return this.minoesPosition[facing].every(minoPosition => this.parent?.cellIsEmpty(minoPosition.clone().add(testPosition)))
}
move(translation, rotatedFacing, rotationPoint) {
move(translation, rotatedFacing) {
if (this.canMove(translation, rotatedFacing)) {
this.position.add(translation)
this.rotatedLast = rotatedFacing
if (rotatedFacing != undefined) {
this.facing = rotatedFacing
if (rotationPoint == 4) this.rotationPoint4Used = true
}
if (this.canMove(TRANSLATION.DOWN)) {
this.locking = false
this.parent.ghost.visible = true
this.parent.ghost.copy(this)
this.parent?.ghost.copy(this)
scheduler.clearTimeout(this.onLockDown)
} else {
scheduler.resetTimeout(this.onLockDown, this.lockDelay)
@ -181,33 +286,28 @@ class Tetromino extends THREE.InstancedMesh {
} else if (translation == TRANSLATION.DOWN) {
this.locked = true
if (!scheduler.timeoutTasks.has(this.onLockDown))
scheduler.setTimeout(this.onLockDown, this.lockDelay)
scheduler.resetTimeout(this.onLockDown, this.lockDelay)
}
}
rotate(rotation) {
let testFacing = (this.facing + rotation) % 4
return this.srs[this.facing][rotation].some(
(translation, rotationPoint) => this.move(translation, testFacing, rotationPoint)
)
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
}
copy(piece) {
this.position.copy(piece.position)
this.minoesPosition = piece.minoesPosition
this.facing = piece.facing
while (this.canMove(TRANSLATION.DOWN)) this.position.y--
}
}
Tetromino.prototype.minoMaterial = minoMaterial
Tetromino.prototype.lockingColor = new THREE.Color(COLORS.LOCKING)
// Super Rotation System
// freedom of movement = srs[this.parent.piece.facing][rotation]
// 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)] },
@ -217,18 +317,21 @@ Tetromino.prototype.srs = [
Tetromino.prototype.lockDelay = 500
class Ghost extends Tetromino {}
Ghost.prototype.minoMaterial = new THREE.MeshBasicMaterial({
envMap: environnement,
reflectivity: 0.9,
transparent: true,
opacity: 0.15,
side: THREE.DoubleSide,
})
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 { }
@ -245,6 +348,8 @@ I.prototype.srs = [
{ [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 = [
@ -254,8 +359,11 @@ J.prototype.minoesPosition = [
[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 { }
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)],
@ -263,6 +371,8 @@ L.prototype.minoesPosition = [
[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 = [
@ -272,6 +382,8 @@ 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 = [
@ -281,6 +393,8 @@ S.prototype.minoesPosition = [
[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() {
@ -308,6 +422,8 @@ T.prototype.tSlots = [
[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 = [
@ -317,25 +433,22 @@ Z.prototype.minoesPosition = [
[P(0, 1), P(-1, 0), P(0, 0), P(-1, -1)]
]
Z.prototype.freeColor = new THREE.Color(COLORS.Z)
const ROWS = 24
const SKYLINE = 20
const COLUMNS = 10
Z.prototype.offset = P(6, 0)
Z.prototype.ghostOffset = P(6, 1)
class Playfield extends THREE.Group {
constructor() {
constructor(loadingManager) {
super()
this.visible = false
//this.visible = false
const edgeMaterial = new THREE.MeshBasicMaterial({
color: 0x88abe0,
envMap: environnement,
const edgeMaterial = new THREE.MeshStandardMaterial({
color: COLORS.EDGE,
envMap: environment,
transparent: true,
opacity: 0.4,
reflectivity: 0.9,
refractionRatio: 0.5
opacity: 0.3,
roughness: 0.1,
metalness: 0.67,
})
const edgeShape = new THREE.Shape()
.moveTo(-.3, SKYLINE)
@ -347,14 +460,56 @@ class Playfield extends THREE.Group {
.lineTo(COLUMNS + .3, -.3)
.lineTo(-.3, -.3)
.moveTo(-.3, SKYLINE)
const edge = new THREE.Mesh(
this.edge = new THREE.Mesh(
new THREE.ExtrudeGeometry(edgeShape, {
depth: 1,
bevelEnabled: false,
}),
edgeMaterial
)
this.add(edge)
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])
@ -365,24 +520,19 @@ class Playfield extends THREE.Group {
this.hardDropAnimation.loop = THREE.LoopOnce
this.hardDropAnimation.setDuration(0.2)
this.ghost = new Ghost()
this.add(this.ghost)
this.ghost.visible = false
this.lockedMeshes = new THREE.InstancedMesh(minoGeometry, minoMaterial, 200)
this.add(this.lockedMeshes)
this.freedMinoes = []
this.freedMeshes = new THREE.InstancedMesh(minoGeometry, minoMaterial, 200)
this.freedMeshes.count = 0
this.add(this.freedMeshes)
this.init()
this.freedMinoes = new Set()
}
init() {
this.cells = Array(ROWS).fill().map(() => Array(COLUMNS))
this.lockedMeshes.count = 0
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) {
@ -393,11 +543,10 @@ class Playfield extends THREE.Group {
set piece(piece) {
if (piece) {
this.remove(this.piece)
this.add(piece)
piece.position.set(4, SKYLINE)
this.ghost.color = piece.freeColor
this.ghost.copy(piece)
this.ghost.visible = true
}
this._piece = piece
}
@ -407,62 +556,44 @@ class Playfield extends THREE.Group {
}
lock() {
this.piece.minoesPosition[this.piece.facing].forEach(position => {
position = position.clone()
position.add(this.piece.position)
if (this.cellIsEmpty(position)) {
this.cells[position.y][position.x] = this.piece.freeColor
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
}
})
this.updateLockedMinoes()
return this.piece.minoesPosition[this.piece.facing].every(position => position.y + this.piece.position.y < SKYLINE)
}
clearLines() {
let nbClearedLines = this.cells.reduceRight((nbClearedLines, row, y) => {
if (row.filter(color => color).length == COLUMNS) {
row.forEach((color, x) => {
this.freedMinoes.push(new Mino(color, x, y))
})
row.forEach(mino => this.freedMinoes.add(mino))
this.cells.splice(y, 1)
this.cells.push(Array(COLUMNS))
return ++nbClearedLines
}
return nbClearedLines
}, 0)
this.updateLockedMinoes()
if (nbClearedLines) this.cells.forEach((row, y) => row.forEach((mino, x) => mino.position.set(x, y, 0)))
return nbClearedLines
}
updateLockedMinoes() {
let i = 0
let matrix4 = new THREE.Matrix4()
this.cells.forEach((row, y) => row.forEach((color, x) => {
matrix4.setPosition(x, y, 0)
this.lockedMeshes.setMatrixAt(i, matrix4)
this.lockedMeshes.setColorAt(i, color)
i++
}))
this.lockedMeshes.count = i
this.lockedMeshes.instanceMatrix.needsUpdate = true
this.lockedMeshes.instanceColor.needsUpdate = true
}
updateFreedMinoes(delta) {
this.freedMinoes.forEach(mino => mino.update(delta))
this.freedMinoes = this.freedMinoes.filter(mino =>
Math.sqrt(mino.position.x * mino.position.x + mino.position.z * mino.position.z) <= 40 && mino.position.y > -50
) || []
this.freedMeshes.count = this.freedMinoes.length
if (this.freedMeshes.count) {
this.freedMinoes.forEach((mino, i) => {
this.freedMeshes.setMatrixAt(i, mino.matrix)
this.freedMeshes.setColorAt(i, mino.color)
})
this.freedMeshes.instanceMatrix.needsUpdate = true
this.freedMeshes.instanceColor.needsUpdate = true
this.freedMinoes.forEach(mino => {
if (mino.explode(delta)) {
this.remove(mino)
this.freedMinoes.delete(mino)
}
})
}
update(delta) {
@ -475,11 +606,12 @@ class Playfield extends THREE.Group {
class HoldQueue extends THREE.Group {
constructor() {
super()
this.position.set(-4, SKYLINE - 2)
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)
@ -498,26 +630,19 @@ class HoldQueue extends THREE.Group {
class NextQueue extends THREE.Group {
constructor() {
super()
this.position.set(13, SKYLINE - 2)
this.position.set(14, SKYLINE - 2)
}
init() {
this.pieces = this.positions.map((position) => {
let piece = new Tetromino.random()
piece.position.copy(position)
this.add(piece)
return piece
})
this.clear()
this.positions.forEach(position => this.add(new Tetromino.random(position)))
}
shift() {
let fistPiece = this.pieces.shift()
let lastPiece = new Tetromino.random()
this.add(lastPiece)
this.pieces.push(lastPiece)
this.positions.forEach((position, i) => {
this.pieces[i].position.copy(position)
})
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
}
@ -525,4 +650,4 @@ class NextQueue extends THREE.Group {
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, environnement, minoMaterial, Tetromino, I, J, L, O, S, T, Z, Playfield, HoldQueue, NextQueue }
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`
)
}
}

View File

@ -5,29 +5,23 @@ export class Vortex extends THREE.Group {
constructor(loadingManager) {
super()
this.loadingManager = loadingManager
this.globalRotation = 0.028
this.darkTextureRotation = 0.006
this.darkMoveForward = 0.012
this.darkMoveForward = 0.009
this.colorFullTextureRotation = 0.006
this.colorFullMoveForward = 0.016
this.colorFullMoveForward = 0.025
const commonCylinderGeometry = new THREE.CylinderGeometry(35, 35, 500, 12, 1, true)
this.background = "Plasma"
const commonCylinderGeometry = new THREE.CylinderGeometry(35, 35, 1000, 12, 1, true)
this.darkCylinder = new THREE.Mesh(
commonCylinderGeometry,
new THREE.MeshLambertMaterial({
side: THREE.BackSide,
map: new THREE.TextureLoader(loadingManager).load("./images/plasma2.jpg", (texture) => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 1)
}),
blending: THREE.AdditiveBlending,
opacity: 0.75
})
)
this.add(this.darkCylinder)
@ -36,27 +30,87 @@ export class Vortex extends THREE.Group {
commonCylinderGeometry,
new THREE.MeshBasicMaterial({
side: THREE.BackSide,
map: new THREE.TextureLoader(loadingManager).load("./images/plasma.jpg", (texture) => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(1, 1)
}),
blending: THREE.AdditiveBlending,
opacity: 0.075
})
)
this.add(this.colorFullCylinder)
this.position.set(5, 10, -10)
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
}
}
}
}

View File

@ -15,6 +15,11 @@ class Scheduler {
}
}
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))
}
@ -33,7 +38,4 @@ class Scheduler {
}
const scheduler = new Scheduler
export { scheduler }
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,278 +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;
}
}
body {
background-color: #222;
}
#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;
}