Compare commits

...

43 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
20 changed files with 796 additions and 714 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)

86
app.js
View File

@ -1,11 +1,12 @@
import * as THREE from 'three' import * as THREE from 'three'
import { scheduler } from './jsm/scheduler.js' import { scheduler } from './jsm/scheduler.js'
import { TRANSLATION, ROTATION, environnement, Mino, Playfield, HoldQueue, NextQueue } from './jsm/gamelogic.js' import { TRANSLATION, ROTATION, environment, InstancedMino, Mino, Playfield, HoldQueue, NextQueue } from './jsm/Tetrominoes.js'
import { Settings } from './jsm/Settings.js' import Settings from './jsm/Settings.js'
import { Stats } from './jsm/Stats.js' import { Stats } from './jsm/Stats.js'
import { TetraGUI } from './jsm/TetraGUI.js' import { Menu } from './jsm/Menu.js'
import { TetraControls } from './jsm/TetraControls.js' import CameraControls from './jsm/CameraControls.js'
import { TetraScene } from './jsm/TetraScene.js' import { TetraScene } from './jsm/TetraScene.js'
import * as FPS from 'three/addons/libs/stats.module.js'
HTMLElement.prototype.addNewChild = function (tag, properties) { HTMLElement.prototype.addNewChild = function (tag, properties) {
@ -25,11 +26,11 @@ let game = {
start: function() { start: function() {
stats.init() stats.init()
gui.startButton.hide() menu.startButton.hide()
gui.settings.close() menu.stats.show()
gui.stats.show() menu.settings.close()
Mino.mesh.clear() Mino.instances.clear()
nextQueue.init() nextQueue.init()
holdQueue.piece = undefined holdQueue.piece = undefined
@ -49,11 +50,11 @@ let game = {
document.onkeydown = onkeydown document.onkeydown = onkeydown
document.onkeyup = onkeyup document.onkeyup = onkeyup
window.onblur = game.pause window.onblur = game.pause
gui.settings.domElement.onclick = game.pause menu.settings.domElement.onclick = game.pause
document.body.classList.remove("pause") document.body.classList.remove("pause")
gui.resumeButton.hide() menu.resumeButton.hide()
gui.pauseButton.show() menu.pauseButton.show()
stats.clock.start() stats.clock.start()
stats.clock.elapsedTime = stats.elapsedTime stats.clock.elapsedTime = stats.elapsedTime
@ -61,7 +62,7 @@ let game = {
if (settings.musicVolume) scene.music.play() if (settings.musicVolume) scene.music.play()
if (playfield.piece) { if (playfield.piece) {
scheduler.setInterval(game.fall, stats.fallPeriod) scheduler.resetInterval(game.fall, stats.fallPeriod)
} else { } else {
this.generate() this.generate()
} }
@ -73,7 +74,7 @@ let game = {
playfield.piece.onLockDown = game.lockDown playfield.piece.onLockDown = game.lockDown
if (playfield.piece.canMove(TRANSLATION.NONE)) { if (playfield.piece.canMove(TRANSLATION.NONE)) {
scheduler.setInterval(game.fall, stats.fallPeriod) scheduler.resetInterval(game.fall, stats.fallPeriod)
} else { } else {
game.over() // block out game.over() // block out
} }
@ -106,7 +107,7 @@ let game = {
}, },
pause: function() { pause: function() {
gui.settings.domElement.onclick = null menu.settings.domElement.onclick = null
stats.elapsedTime = stats.clock.elapsedTime stats.elapsedTime = stats.clock.elapsedTime
stats.clock.stop() stats.clock.stop()
@ -123,8 +124,8 @@ let game = {
pauseSpan.onfocus = game.resume pauseSpan.onfocus = game.resume
document.body.classList.add("pause") document.body.classList.add("pause")
gui.pauseButton.hide() menu.pauseButton.hide()
gui.resumeButton.show() menu.resumeButton.show()
}, },
over: function() { over: function() {
@ -133,15 +134,15 @@ let game = {
document.onkeydown = null document.onkeydown = null
window.onblur = null window.onblur = null
renderer.domElement.onfocus = null renderer.domElement.onfocus = null
gui.settings.domElement.onfocus = null menu.settings.domElement.onfocus = null
game.playing = false game.playing = false
scene.music.pause() scene.music.pause()
stats.clock.stop() stats.clock.stop()
messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `<h1>GAME<br/>OVER</h1>` }) messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `<h1>GAME<br/>OVER</h1>` })
gui.pauseButton.hide() menu.pauseButton.hide()
gui.startButton.name("Rejouer") menu.startButton.name("Rejouer")
gui.startButton.show() menu.startButton.show()
}, },
} }
@ -215,8 +216,8 @@ function onkeydown(event) {
actionsQueue.unshift(action) actionsQueue.unshift(action)
scheduler.clearTimeout(repeat) scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat) scheduler.clearInterval(autorepeat)
if (action == playerActions.softDrop) scheduler.setInterval(autorepeat, settings.fallPeriod / 20) if (action == playerActions.softDrop) scheduler.resetInterval(autorepeat, settings.fallPeriod / 20)
else scheduler.setTimeout(repeat, settings.dasDelay) else scheduler.resetTimeout(repeat, settings.dasDelay)
} }
} }
} }
@ -225,7 +226,7 @@ function onkeydown(event) {
function repeat() { function repeat() {
if (actionsQueue.length) { if (actionsQueue.length) {
actionsQueue[0]() actionsQueue[0]()
scheduler.setInterval(autorepeat, settings.arrDelay) scheduler.resetInterval(autorepeat, settings.arrDelay)
} }
} }
@ -264,7 +265,6 @@ function resumeOnKeyDown(event) {
/* Scene */ /* Scene */
const renderer = new THREE.WebGLRenderer({ const renderer = new THREE.WebGLRenderer({
powerPreference: "high-performance", powerPreference: "high-performance",
antialias: true, antialias: true,
@ -276,10 +276,10 @@ renderer.toneMapping = THREE.ACESFilmicToneMapping
document.body.appendChild(renderer.domElement) document.body.appendChild(renderer.domElement)
renderer.domElement.tabIndex = 1 renderer.domElement.tabIndex = 1
const loadingManager = new THREE.LoadingManager( let loadingManager = new THREE.LoadingManager(
function() { function() {
loaddingCircle.remove() loadingDiv.style.display = "none"
gui.startButton.show() menu.startButton.show()
renderer.setAnimationLoop(animate) renderer.setAnimationLoop(animate)
}, },
function (url, itemsLoaded, itemsTotal) { function (url, itemsLoaded, itemsTotal) {
@ -291,26 +291,32 @@ const loadingManager = new THREE.LoadingManager(
) )
loadingManager.onStart = function (url, itemsLoaded, itemsTotal) { loadingManager.onStart = function (url, itemsLoaded, itemsTotal) {
loadingPercent.innerText = "0%" loadingPercent.innerText = "0%"
loadingDiv.style.display = "flex"
} }
const stats = new Stats() const stats = new Stats()
const settings = new Settings() const settings = new Settings()
const scene = new TetraScene(settings, loadingManager) const scene = new TetraScene(settings, loadingManager)
const controls = new CameraControls(scene.camera, renderer.domElement)
const controls = new TetraControls(scene.camera, renderer.domElement) const minoes = new InstancedMino()
scene.add(minoes)
const gui = new TetraGUI(game, settings, stats, scene, controls)
scene.add(Mino.mesh)
const holdQueue = new HoldQueue() const holdQueue = new HoldQueue()
scene.add(holdQueue) scene.add(holdQueue)
const playfield = new Playfield() const playfield = new Playfield(loadingManager)
scene.add(playfield) scene.add(playfield)
const nextQueue = new NextQueue() const nextQueue = new NextQueue()
scene.add(nextQueue) scene.add(nextQueue)
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) { messagesSpan.onanimationend = function (event) {
event.target.remove() event.target.remove()
} }
@ -319,17 +325,17 @@ messagesSpan.onanimationend = function (event) {
const clock = new THREE.Clock() const clock = new THREE.Clock()
function animate() { function animate() {
const delta = clock.getDelta() const delta = clock.getDelta()
scene.updateMatrixWorld() scene.updateMatrixWorld()
scene.update(delta) scene.update(delta)
playfield.update(delta) playfield.update(delta)
Mino.mesh.update() minoes.update()
controls.update() controls.update()
gui.update()
renderer.render(scene, scene.camera) renderer.render(scene, scene.camera)
environnement.camera.update(renderer, scene) environment.camera.update(renderer, scene)
fps?.update()
} }
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
@ -339,7 +345,7 @@ window.addEventListener("resize", () => {
}) })
window.onbeforeunload = function (event) { window.onbeforeunload = function (event) {
gui.save() menu.save()
localStorage["teTraHighScore"] = stats.highScore localStorage["teTraHighScore"] = stats.highScore
return !game.playing return !game.playing
} }

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

@ -9,14 +9,16 @@ body {
span { span {
position: absolute; position: absolute;
top: 0;
left: 0;
} }
.lil-gui { .lil-menu {
--background-color: rgba(33, 37, 41, 30%); --background-color: rgba(33, 37, 41, 30%);
--width: 200px; --width: 200px;
} }
@supports (backdrop-filter: blur()) { @supports (backdrop-filter: blur()) {
.lil-gui { .lil-menu {
backdrop-filter: blur(15px); backdrop-filter: blur(15px);
} }
} }
@ -27,7 +29,11 @@ span {
left: 15px; left: 15px;
} }
.lil-gui .controller.disabled { .lil-menu.root > .title {
font-size: 1.5em;
}
.lil-menu .controller.disabled {
opacity: .8; opacity: .8;
} }
@ -51,6 +57,7 @@ canvas {
#messagesSpan div { #messagesSpan div {
opacity: 0; opacity: 0;
overflow: hidden; overflow: hidden;
user-select: none;
} }
h1 { h1 {
@ -166,11 +173,10 @@ h1 {
} }
.pause #pauseSpan { .pause #pauseSpan {
display: flex;
position: absolute; position: absolute;
display: flex;
top: 0; top: 0;
left: 0; left: 0;
display: flex;
filter: blur(2px); filter: blur(2px);
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -181,4 +187,5 @@ h1 {
font-size: 20vh; font-size: 20vh;
font-weight: 800; font-weight: 800;
letter-spacing: .1em; 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/sprites.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -4,37 +4,78 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>teTra</title> <title>ᵀᴱTᴿᴬ</title>
<link rel="icon" href="favicon.ico"> <link rel="icon" href="favicon.ico">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="loading.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 async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
<script type="importmap"> <script type="importmap">
{ {
"imports": { "imports": {
"three": "https://unpkg.com/three@0.155/build/three.module.js?module", "three": "https://unpkg.com/three@0.169/build/three.module.js?module",
"three/addons/": "https://unpkg.com/three@0.155/examples/jsm/" "three/addons/": "https://unpkg.com/three@0.169/examples/jsm/"
} }
} }
</script> </script>
</head> </head>
<body> <body>
<div id="loaddingCircle"> <span id="loadingDiv">
<div class="e-loadholder"> <div class="scene">
<div class="m-loader"> <div class="T tetromino">
<span class="e-text"> <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>Chargement</div>
<div id="loadingPercent">0%</div> <div id="loadingPercent">0%</div>
</div>
</span> </span>
</div>
</div>
</div>
<span id="messagesSpan"></span> <span id="messagesSpan"></span>
<span id="pauseSpan" tabindex="1">II</span> <span id="pauseSpan" tabindex="1">II</span>
<audio id="music" src="audio/benevolence.m4a" loop></audio> <audio id="music" src="audio/benevolence.m4a" loop></audio>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
<script type="module" src="app.js"></script> <script type="module" src="app.js"></script>
<script>navigator?.serviceWorker.register('./jsm/service-worker.js')</script> <script>navigator?.serviceWorker?.register('./jsm/service-worker.js')</script>
</body> </body>
</html> </html>

View File

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

@ -27,7 +27,7 @@ let friendyKeyRenamer = new Proxy({
} }
}) })
class Settings { export default class Settings {
constructor() { constructor() {
this.startLevel = 1 this.startLevel = 1
@ -76,8 +76,7 @@ class Settings {
this.musicVolume = 50 this.musicVolume = 50
this.sfxVolume = 50 this.sfxVolume = 50
this.theme = "Plasma"
} }
} }
export { Settings }

View File

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

View File

@ -1,248 +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 { Mino, environnement } from './gamelogic.js'
export class TetraGUI extends GUI {
constructor(game, settings, stats, scene, controls) {
super({title: "teTra"})
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/plasma.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(1, 1)
})
colorfullTexture = new THREE.TextureLoader(loadingManager).load("./images/plasma2.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 1)
})
loadingManager.onLoad = function() {
scene.vortex.darkCylinder.material.map = darkTexture
scene.vortex.darkCylinder.material.opacity = 0.17
scene.vortex.colorFullCylinder.material.map = colorfullTexture
scene.vortex.colorFullCylinder.material.opacity = 0.7
scene.vortex.globalRotation = 0.028
scene.vortex.darkTextureRotation = 0.005
scene.vortex.darkMoveForward = 0.009
scene.vortex.colorFullTextureRotation = 0.006
scene.vortex.colorFullMoveForward = 0.025
scene.ambientLight.intensity = 0.6
scene.directionalLight.intensity = 5
Mino.mesh.material.opacity = 0.7
Mino.mesh.material.roughness = 0.48
Mino.mesh.material.metalness = 0.9
}
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, 2)
})
colorfullTexture = new THREE.TextureLoader(loadingManager).load("./images/colorfull.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 2)
})
loadingManager.onLoad = function() {
scene.vortex.darkCylinder.material.map = darkTexture
scene.vortex.darkCylinder.material.opacity = 0.05
scene.vortex.colorFullCylinder.material.map = colorfullTexture
scene.vortex.colorFullCylinder.material.opacity = 0.25
scene.vortex.globalRotation = 0.028
scene.vortex.darkTextureRotation = 0.006
scene.vortex.darkMoveForward = 0.03
scene.vortex.colorFullTextureRotation = 0.006
scene.vortex.colorFullMoveForward = 0.012
scene.ambientLight.intensity = 20
scene.directionalLight.intensity = 10
//Mino.mesh.material.opacity = 0.6
//Mino.mesh.material.roughness = 0.08
//Mino.mesh.material.metalness = 0.98
}
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.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)
})
this.dev = window.location.search.includes("dev")
if (this.dev) {
let dev = this.addFolder("dev")
let cameraPosition = dev.addFolder("camera").close()
cameraPosition.add(scene.camera.position, "x")
cameraPosition.add(scene.camera.position, "y")
cameraPosition.add(scene.camera.position, "z")
cameraPosition.add(scene.camera, "fov", 0, 200).onChange(() => scene.camera.updateProjectionMatrix())
let light = dev.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 directionalLightPosition = dev.addFolder("directionalLight.position").close()
directionalLightPosition.add(scene.directionalLight.position, "x")
directionalLightPosition.add(scene.directionalLight.position, "y")
directionalLightPosition.add(scene.directionalLight.position, "z")
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)
let material
function changeMaterial(type) {
material?.destroy()
material = dev.addFolder("minoes material")
material.add(Mino.mesh.material, "constructor", ["MeshBasicMaterial", "MeshStandardMaterial", "MeshPhysicalMaterial"]).name("type").onChange(changeMaterial)
switch(type) {
case "MeshBasicMaterial":
Mino.mesh.material = new THREE.MeshBasicMaterial({
envMap: environnement,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.5,
reflectivity: 0.9,
})
break
case "MeshStandardMaterial":
Mino.mesh.material = new THREE.MeshStandardMaterial({
envMap: environnement,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7,
roughness: 0.48,
metalness: 0.67,
})
break
case "MeshPhysicalMaterial":
Mino.mesh.material = new THREE.MeshPhysicalMaterial({
envMap: environnement,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.6,
roughness: 0.5,
metalness: 0.67,
attenuationDistance: 0.5,
ior: 2,
sheen: 0,
sheenRoughness: 1,
specularIntensity: 1,
thickness: 5,
transmission: 1,
})
break
}
if ("opacity" in Mino.mesh.material) material.add(Mino.mesh.material, "opacity").min(0).max(1)
if ("reflectivity" in Mino.mesh.material) material.add(Mino.mesh.material, "reflectivity").min(0).max(1)
if ("roughness" in Mino.mesh.material) material.add(Mino.mesh.material, "roughness").min(0).max(1)
if ("metalness" in Mino.mesh.material) material.add(Mino.mesh.material, "metalness").min(0).max(1)
if ("attenuationDistance" in Mino.mesh.material) material.add(Mino.mesh.material, "attenuationDistance").min(0)
if ("ior" in Mino.mesh.material) material.add(Mino.mesh.material, "ior").min(1).max(2)
if ("sheen" in Mino.mesh.material) material.add(Mino.mesh.material, "sheen").min(0).max(1)
if ("sheenRoughness" in Mino.mesh.material) material.add(Mino.mesh.material, "sheenRoughness").min(0).max(1)
if ("specularIntensity" in Mino.mesh.material) material.add(Mino.mesh.material, "specularIntensity").min(0).max(1)
if ("thickness" in Mino.mesh.material) material.add(Mino.mesh.material, "thickness").min(0).max(5)
if ("transmission" in Mino.mesh.material) material.add(Mino.mesh.material, "transmission").min(0).max(1)
}
changeMaterial(this.materialType)
material.close()
let fps = new FPS.default()
document.body.appendChild(fps.dom)
this.update = function() {
fps.update()
}
controls.addEventListener("change", () => cameraPosition.controllersRecursive().forEach((control) => {
control.updateDisplay()
}))
}
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() {}
}

View File

@ -7,7 +7,7 @@ export class TetraScene extends THREE.Scene {
super() super()
this.camera = new THREE.PerspectiveCamera(100, window.innerWidth / window.innerHeight, 0.1, 1000) this.camera = new THREE.PerspectiveCamera(100, window.innerWidth / window.innerHeight, 0.1, 1000)
this.camera.position.set(5, 3, 12) this.camera.position.set(5, 4, 12)
this.vortex = new Vortex(loadingManager) this.vortex = new Vortex(loadingManager)
this.add(this.vortex) this.add(this.vortex)
@ -16,9 +16,9 @@ export class TetraScene extends THREE.Scene {
this.add(this.ambientLight) this.add(this.ambientLight)
this.directionalLight = new THREE.DirectionalLight(0xffffff, 5) this.directionalLight = new THREE.DirectionalLight(0xffffff, 5)
this.directionalLight.position.set(5, -20, 20)
this.add(this.directionalLight) this.add(this.directionalLight)
this.theme = settings.theme
/* Sounds */ /* Sounds */
this.music = music this.music = music
@ -30,20 +30,41 @@ export class TetraScene extends THREE.Scene {
this.lineClearSound = new THREE.Audio(listener) this.lineClearSound = new THREE.Audio(listener)
audioLoader.load('audio/line-clear.ogg', function( buffer ) { audioLoader.load('audio/line-clear.ogg', function( buffer ) {
this.lineClearSound.setBuffer(buffer) this.lineClearSound.setBuffer(buffer)
this.lineClearSound.setVolume(settings.sfxVolume/100)
}.bind(this)) }.bind(this))
this.tetrisSound = new THREE.Audio(listener) this.tetrisSound = new THREE.Audio(listener)
audioLoader.load('audio/tetris.ogg', function( buffer ) { audioLoader.load('audio/tetris.ogg', function( buffer ) {
this.tetrisSound.setBuffer(buffer) this.tetrisSound.setBuffer(buffer)
this.lineClearSound.setVolume(settings.sfxVolume/100)
this.tetrisSound.setVolume(settings.sfxVolume/100) this.tetrisSound.setVolume(settings.sfxVolume/100)
this.hardDropSound.setVolume(settings.sfxVolume/100)
}.bind(this)) }.bind(this))
this.hardDropSound = new THREE.Audio(listener) this.hardDropSound = new THREE.Audio(listener)
audioLoader.load('audio/hard-drop.wav', function( buffer ) { audioLoader.load('audio/hard-drop.wav', function( buffer ) {
this.hardDropSound.setBuffer(buffer) this.hardDropSound.setBuffer(buffer)
this.hardDropSound.setVolume(settings.sfxVolume/100)
}.bind(this)) }.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) { update(delta) {
this.vortex.update(delta) this.vortex.update(delta)
} }

View File

@ -1,5 +1,6 @@
import * as THREE from 'three' import * as THREE from 'three'
import { scheduler } from './scheduler.js' 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] } Array.prototype.pick = function () { return this.splice(Math.floor(Math.random() * this.length), 1)[0] }
@ -16,9 +17,10 @@ const COLORS = {
S: 0xC8FBA8, S: 0xC8FBA8,
T: 0xedb2ff, T: 0xedb2ff,
Z: 0xffb8c5, Z: 0xffb8c5,
LOCKING: "white", LOCKING: 0xffffff,
GHOST: 0x99a9b2, GHOST: 0x99a9b2,
EDGE: 0x88abe0 EDGE: 0x88abe0,
RETRO: 0xd0d4c1,
} }
const TRANSLATION = { const TRANSLATION = {
@ -54,52 +56,21 @@ const COLUMNS = 10
const envRenderTarget = new THREE.WebGLCubeRenderTarget(256) const envRenderTarget = new THREE.WebGLCubeRenderTarget(256)
const environnement = envRenderTarget.texture const environment = envRenderTarget.texture
environnement.type = THREE.HalfFloatType environment.type = THREE.HalfFloatType
environnement.camera = new THREE.CubeCamera(1, 1000, envRenderTarget) environment.camera = new THREE.CubeCamera(1, 1000, envRenderTarget)
environnement.camera.position.set(5, 10, 0) environment.camera.position.set(5, 10, 0)
class InstancedMino extends THREE.InstancedMesh { const sideMaterial = new THREE.MeshStandardMaterial({
constructor(geometry, material, count) { color: 0x222222,
super(geometry, material, count) roughness: 0.8,
this.instances = new Set() metalness: 0.8,
this.count = 0
}
add(instance) {
this.instances.add(instance)
}
delete(instance) {
this.instances.delete(instance)
}
clear() {
this.instances.clear()
}
update() {
this.count = 0
this.instances.forEach(mino => {
if (mino.parent?.visible) {
this.setColorAt(this.count, mino.color)
this.setMatrixAt(this.count, mino.matrixWorld)
this.count++
}
}) })
if (this.count) {
this.instanceColor.needsUpdate = true
this.instanceMatrix.needsUpdate = true
}
}
}
class Mino extends THREE.Object3D { export class InstancedMino extends THREE.InstancedMesh {
static instances = new Set() constructor() {
static mesh
static {
let minoFaceShape = new THREE.Shape() let minoFaceShape = new THREE.Shape()
minoFaceShape.moveTo(.1, .1) minoFaceShape.moveTo(.1, .1)
minoFaceShape.lineTo(.1, .9) minoFaceShape.lineTo(.1, .9)
@ -115,25 +86,119 @@ class Mino extends THREE.Object3D {
bevelOffset: 0, bevelOffset: 0,
bevelSegments: 1 bevelSegments: 1
} }
let minoGeometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings) const geometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings)
let minoMaterial = new THREE.MeshStandardMaterial({ super(geometry, undefined, 2*ROWS*COLUMNS)
envMap: environnement, 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, side: THREE.DoubleSide,
transparent: true, transparent: true,
opacity: 0.7, opacity: 0.7,
roughness: 0.48, roughness: 0.6,
metalness: 0.67, metalness: 1,
}),
Espace: new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.8,
roughness: 0.1,
metalness: 0.99,
}) })
this.mesh = new InstancedMino(minoGeometry, minoMaterial, 2*ROWS*COLUMNS)
} }
constructor(color) {
class Mino extends THREE.Object3D {
static instances = new Set()
constructor(color, offset) {
super() super()
this.color = color this.color = color
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.rotationAngle = P(Math.random(), Math.random(), Math.random()).normalize()
this.angularVelocity = 5 - 10 * Math.random() this.angularVelocity = 5 - 10 * Math.random()
this.constructor.mesh.add(this) this.constructor.instances.add(this)
} }
explode(delta) { explode(delta) {
@ -142,14 +207,14 @@ class Mino extends THREE.Object3D {
this.rotateOnWorldAxis(this.rotationAngle, delta * this.angularVelocity) 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) { if (Math.sqrt(this.position.x * this.position.x + this.position.z * this.position.z) > 40 || this.position.y < -50) {
this.dispose() this.dispose()
return false
} else {
return true return true
} else {
return false
} }
} }
dispose() { dispose() {
this.constructor.mesh.delete(this) this.constructor.instances.delete(this)
} }
} }
@ -164,7 +229,8 @@ class Tetromino extends THREE.Group {
constructor(position) { constructor(position) {
super() super()
if (position) this.position.copy(position) if (position) this.position.copy(position)
this.minoesPosition[FACING.NORTH].forEach(() => this.add(new Mino(this.freeColor))) this.offset = this.offset.clone()
this.minoesPosition[FACING.NORTH].forEach(() => this.add(new Mino(this.freeColor, this.offset)))
this.facing = FACING.NORTH this.facing = FACING.NORTH
this.rotatedLast = false this.rotatedLast = false
this.rotationPoint4Used = false this.rotationPoint4Used = false
@ -184,8 +250,10 @@ class Tetromino extends THREE.Group {
set locking(locking) { set locking(locking) {
if (locking) { if (locking) {
this.color = this.lockingColor this.color = this.lockingColor
this.offset.y = 2
} else { } else {
this.color = this.freeColor this.color = this.freeColor
this.offset.y = 0
} }
} }
@ -195,20 +263,19 @@ class Tetromino extends THREE.Group {
canMove(translation, facing=this.facing) { canMove(translation, facing=this.facing) {
let testPosition = this.position.clone().add(translation) 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)) { if (this.canMove(translation, rotatedFacing)) {
this.position.add(translation) this.position.add(translation)
this.rotatedLast = rotatedFacing this.rotatedLast = rotatedFacing
if (rotatedFacing != undefined) { if (rotatedFacing != undefined) {
this.facing = rotatedFacing this.facing = rotatedFacing
if (rotationPoint == 4) this.rotationPoint4Used = true
} }
if (this.canMove(TRANSLATION.DOWN)) { if (this.canMove(TRANSLATION.DOWN)) {
this.locking = false this.locking = false
this.parent.ghost.copy(this) this.parent?.ghost.copy(this)
scheduler.clearTimeout(this.onLockDown) scheduler.clearTimeout(this.onLockDown)
} else { } else {
scheduler.resetTimeout(this.onLockDown, this.lockDelay) scheduler.resetTimeout(this.onLockDown, this.lockDelay)
@ -219,16 +286,19 @@ class Tetromino extends THREE.Group {
} else if (translation == TRANSLATION.DOWN) { } else if (translation == TRANSLATION.DOWN) {
this.locked = true this.locked = true
if (!scheduler.timeoutTasks.has(this.onLockDown)) if (!scheduler.timeoutTasks.has(this.onLockDown))
scheduler.setTimeout(this.onLockDown, this.lockDelay) scheduler.resetTimeout(this.onLockDown, this.lockDelay)
} }
} }
rotate(rotation) { rotate(rotation) {
let testFacing = (this.facing + rotation) % 4 let testFacing = (this.facing + rotation) % 4
return this.srs[this.facing][rotation].some( return this.srs[this.facing][rotation].some((translation, rotationPoint) => {
(translation, rotationPoint) => this.move(translation, testFacing, rotationPoint) if (this.move(translation, testFacing)) {
) if (rotationPoint == 4) this.rotationPoint4Used = true
return true
}
})
} }
get tSpin() { get tSpin() {
@ -237,7 +307,7 @@ class Tetromino extends THREE.Group {
} }
Tetromino.prototype.lockingColor = new THREE.Color(COLORS.LOCKING) Tetromino.prototype.lockingColor = new THREE.Color(COLORS.LOCKING)
// Super Rotation System // Super Rotation System
// freedom of movement = srs[this.parent.piece.facing][rotation] // freedom of movement = srs[this.facing][rotation]
Tetromino.prototype.srs = [ 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)] },
@ -251,6 +321,7 @@ class Ghost extends Tetromino {
copy(piece) { copy(piece) {
this.position.copy(piece.position) this.position.copy(piece.position)
this.minoesPosition = piece.minoesPosition this.minoesPosition = piece.minoesPosition
this.children.forEach(mino => {mino.offset = piece.ghostOffset})
this.facing = piece.facing this.facing = piece.facing
this.visible = true this.visible = true
while (this.canMove(TRANSLATION.DOWN)) this.position.y-- while (this.canMove(TRANSLATION.DOWN)) this.position.y--
@ -260,6 +331,7 @@ Ghost.prototype.freeColor = new THREE.Color(COLORS.GHOST)
Ghost.prototype.minoesPosition = [ Ghost.prototype.minoesPosition = [
[P(0, 0, 0), P(0, 0, 0), P(0, 0, 0), P(0, 0, 0)], [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 { } class I extends Tetromino { }
@ -276,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)] }, { [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.freeColor = new THREE.Color(COLORS.I)
I.prototype.offset = P(0, 0)
I.prototype.ghostOffset = P(0, 1)
class J extends Tetromino { } class J extends Tetromino { }
J.prototype.minoesPosition = [ J.prototype.minoesPosition = [
@ -285,8 +359,11 @@ J.prototype.minoesPosition = [
[P(0, 1), P(-1, -1), P(0, 0), P(0, -1)], [P(0, 1), P(-1, -1), P(0, 0), P(0, -1)],
] ]
J.prototype.freeColor = new THREE.Color(COLORS.J) 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 = [ L.prototype.minoesPosition = [
[P(-1, 0), P(0, 0), P(1, 0), 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)], [P(0, 1), P(0, 0), P(0, -1), P(1, -1)],
@ -294,6 +371,8 @@ L.prototype.minoesPosition = [
[P(0, 1), P(0, 0), P(0, -1), 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.freeColor = new THREE.Color(COLORS.L)
L.prototype.offset = P(2, 0)
L.prototype.ghostOffset = P(2, 1)
class O extends Tetromino { } class O extends Tetromino { }
O.prototype.minoesPosition = [ O.prototype.minoesPosition = [
@ -303,6 +382,8 @@ O.prototype.srs = [
{ [ROTATION.CW]: [], [ROTATION.CCW]: [] } { [ROTATION.CW]: [], [ROTATION.CCW]: [] }
] ]
O.prototype.freeColor = new THREE.Color(COLORS.O) O.prototype.freeColor = new THREE.Color(COLORS.O)
O.prototype.offset = P(3, 0)
O.prototype.ghostOffset = P(3, 1)
class S extends Tetromino { } class S extends Tetromino { }
S.prototype.minoesPosition = [ S.prototype.minoesPosition = [
@ -312,6 +393,8 @@ S.prototype.minoesPosition = [
[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.freeColor = new THREE.Color(COLORS.S)
S.prototype.offset = P(4, 0)
S.prototype.ghostOffset = P(4, 1)
class T extends Tetromino { class T extends Tetromino {
get tSpin() { get tSpin() {
@ -339,6 +422,8 @@ 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)],
] ]
T.prototype.freeColor = new THREE.Color(COLORS.T) T.prototype.freeColor = new THREE.Color(COLORS.T)
T.prototype.offset = P(5, 0)
T.prototype.ghostOffset = P(5, 1)
class Z extends Tetromino { } class Z extends Tetromino { }
Z.prototype.minoesPosition = [ Z.prototype.minoesPosition = [
@ -348,18 +433,20 @@ Z.prototype.minoesPosition = [
[P(0, 1), P(-1, 0), P(0, 0), 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.freeColor = new THREE.Color(COLORS.Z)
Z.prototype.offset = P(6, 0)
Z.prototype.ghostOffset = P(6, 1)
class Playfield extends THREE.Group { class Playfield extends THREE.Group {
constructor() { constructor(loadingManager) {
super() super()
//this.visible = false //this.visible = false
const edgeMaterial = new THREE.MeshStandardMaterial({ const edgeMaterial = new THREE.MeshStandardMaterial({
color: COLORS.EDGE, color: COLORS.EDGE,
envMap: environnement, envMap: environment,
transparent: true, transparent: true,
opacity: 0.2, opacity: 0.3,
roughness: 0.1, roughness: 0.1,
metalness: 0.67, metalness: 0.67,
}) })
@ -373,14 +460,56 @@ class Playfield extends THREE.Group {
.lineTo(COLUMNS + .3, -.3) .lineTo(COLUMNS + .3, -.3)
.lineTo(-.3, -.3) .lineTo(-.3, -.3)
.moveTo(-.3, SKYLINE) .moveTo(-.3, SKYLINE)
const edge = new THREE.Mesh( this.edge = new THREE.Mesh(
new THREE.ExtrudeGeometry(edgeShape, { new THREE.ExtrudeGeometry(edgeShape, {
depth: 1, depth: 1,
bevelEnabled: false, bevelEnabled: false,
}), }),
edgeMaterial 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 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 clip = new THREE.AnimationClip('HardDrop', 3, [positionKF])
@ -403,7 +532,7 @@ class Playfield extends THREE.Group {
this.ghost.visible = false this.ghost.visible = false
this.add(this.ghost) this.add(this.ghost)
this.visible = true // this.visible = true
} }
cellIsEmpty(p) { cellIsEmpty(p) {
@ -414,6 +543,7 @@ class Playfield extends THREE.Group {
set piece(piece) { set piece(piece) {
if (piece) { if (piece) {
this.remove(this.piece)
this.add(piece) this.add(piece)
piece.position.set(4, SKYLINE) piece.position.set(4, SKYLINE)
this.ghost.copy(piece) this.ghost.copy(piece)
@ -459,7 +589,10 @@ class Playfield extends THREE.Group {
updateFreedMinoes(delta) { updateFreedMinoes(delta) {
this.freedMinoes.forEach(mino => { this.freedMinoes.forEach(mino => {
if (mino.explode(delta)) this.freedMinoes.delete(this) if (mino.explode(delta)) {
this.remove(mino)
this.freedMinoes.delete(mino)
}
}) })
} }
@ -473,11 +606,12 @@ class Playfield extends THREE.Group {
class HoldQueue extends THREE.Group { class HoldQueue extends THREE.Group {
constructor() { constructor() {
super() super()
this.position.set(-4, SKYLINE - 2) this.position.set(-5, SKYLINE - 2)
} }
set piece(piece) { set piece(piece) {
if(piece) { if(piece) {
this.remove(this.piece)
piece.holdEnabled = false piece.holdEnabled = false
piece.locking = false piece.locking = false
piece.position.set(0, 0) piece.position.set(0, 0)
@ -496,7 +630,7 @@ class HoldQueue extends THREE.Group {
class NextQueue extends THREE.Group { class NextQueue extends THREE.Group {
constructor() { constructor() {
super() super()
this.position.set(13, SKYLINE - 2) this.position.set(14, SKYLINE - 2)
} }
init() { init() {
@ -506,6 +640,7 @@ class NextQueue extends THREE.Group {
shift() { shift() {
let fistPiece = this.children.shift() let fistPiece = this.children.shift()
this.remove(fistPiece)
this.add(new Tetromino.random()) this.add(new Tetromino.random())
this.positions.forEach((position, i) => this.children[i].position.copy(position)) this.positions.forEach((position, i) => this.children[i].position.copy(position))
return fistPiece return fistPiece
@ -515,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)] 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, Mino, Tetromino, 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,6 +5,8 @@ export class Vortex extends THREE.Group {
constructor(loadingManager) { constructor(loadingManager) {
super() super()
this.loadingManager = loadingManager
this.globalRotation = 0.028 this.globalRotation = 0.028
this.darkTextureRotation = 0.006 this.darkTextureRotation = 0.006
@ -13,21 +15,13 @@ export class Vortex extends THREE.Group {
this.colorFullTextureRotation = 0.006 this.colorFullTextureRotation = 0.006
this.colorFullMoveForward = 0.025 this.colorFullMoveForward = 0.025
const commonCylinderGeometry = new THREE.CylinderGeometry(35, 35, 500, 12, 1, true) const commonCylinderGeometry = new THREE.CylinderGeometry(35, 35, 1000, 12, 1, true)
this.background = "Plasma"
this.darkCylinder = new THREE.Mesh( this.darkCylinder = new THREE.Mesh(
commonCylinderGeometry, commonCylinderGeometry,
new THREE.MeshLambertMaterial({ new THREE.MeshLambertMaterial({
side: THREE.BackSide, 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, blending: THREE.AdditiveBlending,
opacity: 0.17
}) })
) )
this.add(this.darkCylinder) this.add(this.darkCylinder)
@ -36,27 +30,87 @@ export class Vortex extends THREE.Group {
commonCylinderGeometry, commonCylinderGeometry,
new THREE.MeshBasicMaterial({ new THREE.MeshBasicMaterial({
side: THREE.BackSide, 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, blending: THREE.AdditiveBlending,
opacity: 0.7
}) })
) )
this.add(this.colorFullCylinder) 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) { update(delta) {
if (this.visible) {
this.rotation.y += this.globalRotation * delta 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.y += this.darkMoveForward * delta
this.darkCylinder.material.map.offset.x += this.darkTextureRotation * 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.y += this.colorFullMoveForward * delta
this.colorFullCylinder.material.map.offset.x += this.colorFullTextureRotation * 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) { setTimeout(func, delay, ...args) {
this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args)) this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args))
} }
@ -33,7 +38,4 @@ class Scheduler {
} }
const scheduler = new Scheduler export const scheduler = new Scheduler()
export { scheduler }

View File

@ -1,279 +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;
cursor: progress;
}
.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;
}