Compare commits
78 Commits
eadae0205f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c52a604f0f | |||
| 8e9a089d34 | |||
| d5893eb8ef | |||
| 1e006d46b9 | |||
| 0f84f90e05 | |||
| 4287edab71 | |||
| 5c2eaca35a | |||
| e7dc780173 | |||
| 9721b311eb | |||
| d3f6cf9b71 | |||
| 5b058a58b3 | |||
| 9fca05ae6e | |||
| 1a7628bb42 | |||
| bc4ba54c0a | |||
| 653befdc02 | |||
| 1b3f837bf0 | |||
| 375d47397e | |||
| efb6482238 | |||
| 7fd3c04a2d | |||
| 1e42c2160f | |||
| 74bf8521fb | |||
| fef08f64e8 | |||
| 3c8fc95e23 | |||
| f7b7b74e01 | |||
| af9e0c481a | |||
| 90eb3247e0 | |||
| 2a25dbe4b0 | |||
| d9397c4bcb | |||
| ca93423bf8 | |||
| ae8dcb7077 | |||
| 6ed614d536 | |||
| 8c5b704b3c | |||
| 1b0f1c07d2 | |||
| c8eb029987 | |||
| 825fbca97b | |||
| ce94604fc0 | |||
| 07daa4a9cf | |||
| abf562fd89 | |||
| cae3dc9af5 | |||
| a75329f985 | |||
| 31eca05faf | |||
| 7f6795109b | |||
| 8ed998f255 | |||
| aa2475dc3a | |||
| a0893fd881 | |||
| 1a026db655 | |||
| f8081583c5 | |||
| ce1181df62 | |||
| 3345a50803 | |||
| c9b242c9c2 | |||
| 935343d301 | |||
| 92d953ef62 | |||
| b227690b31 | |||
| e1da884441 | |||
| b34a968dd2 | |||
| 401218bdbe | |||
| 32d4126873 | |||
| 85237739bc | |||
| 12fb307041 | |||
| 6004cbbbde | |||
| 38a9dcfad4 | |||
| dd25b0a891 | |||
| 3a657e4c38 | |||
| fcb12f89e7 | |||
| 7acb3a6def | |||
| 367f252444 | |||
| cfa73565f0 | |||
| 4c68b05db1 | |||
| d2a0e241c8 | |||
| b0dbb06dae | |||
| 02725494cd | |||
| 6f0a540bb3 | |||
| b516de1b2f | |||
| c42ee23b5f | |||
| 0521e2f0ba | |||
| 8b3759f253 | |||
| 945d349319 | |||
| d0120ca2a6 |
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# teTra
|
||||
|
||||
Falling blocks web game made with three.js librairy
|
||||
|
||||

|
||||
172
app.js
172
app.js
@ -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');
|
||||
}
|
||||
BIN
audio/Tetris_MkVaffQuasi_Ultimix_OC_ReMix.mp3
Normal file
BIN
audio/Tetris_MkVaffQuasi_Ultimix_OC_ReMix.mp3
Normal file
Binary file not shown.
BIN
audio/benevolence.m4a
Normal file
BIN
audio/benevolence.m4a
Normal file
Binary file not shown.
70
css/loading.css
Normal file
70
css/loading.css
Normal file
@ -0,0 +1,70 @@
|
||||
#loadingDiv {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-size: 1.4rem;
|
||||
color: lightsteelblue;
|
||||
text-align: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
padding: 30vh;
|
||||
background-color: black;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.scene {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto;
|
||||
perspective: 200px;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.tetromino {
|
||||
position: relative;
|
||||
top: 2em;
|
||||
left: 2em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
transform-style: preserve-3d;
|
||||
transform: translateZ(0.5em);
|
||||
animation: spinCube 5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes spinCube {
|
||||
0% { transform: translateZ(0.5em) rotateX( 0deg) rotateY( 0deg); }
|
||||
100% { transform: translateZ(0.5em) rotateX(360deg) rotateY(360deg); }
|
||||
}
|
||||
|
||||
.mino {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
position: absolute;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.T.tetromino .first.mino { top: -0.5em; left: -1em; }
|
||||
.T.tetromino .second.mino { top: -0.5em; left: 0em; }
|
||||
.T.tetromino .third.mino { top: -0.5em; left: 1em; }
|
||||
.T.tetromino .fourth.mino { top: 0.5em; left: 0em; }
|
||||
|
||||
.face {
|
||||
position: absolute;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
padding: 0;
|
||||
background: hsla(240, 100%, 0%, 0.4);
|
||||
border: 1px solid hsla(240, 100%, 70%, 0.6);
|
||||
}
|
||||
|
||||
.front.face { transform: rotateY( 0deg) translateZ(0.5em); }
|
||||
.right.face { transform: rotateY( 90deg) translateZ(0.5em); }
|
||||
.back.face { transform: rotateY(180deg) translateZ(0.5em); }
|
||||
.left.face { transform: rotateY(-90deg) translateZ(0.5em); }
|
||||
.top.face { transform: rotateX( 90deg) translateZ(0.5em); }
|
||||
.bottom.face { transform: rotateX(-90deg) translateZ(0.5em); }
|
||||
@ -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
BIN
images/edge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
images/edges.png
Normal file
BIN
images/edges.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
BIN
images/fond_etoile.gif
Normal file
BIN
images/fond_etoile.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
BIN
images/sprites.png
Normal file
BIN
images/sprites.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
67
index.html
67
index.html
@ -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>
|
||||
@ -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
179
jsm/Menu.js
Normal file
@ -0,0 +1,179 @@
|
||||
import * as THREE from 'three'
|
||||
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
|
||||
import { environment } from './Tetrominoes.js'
|
||||
|
||||
|
||||
export class Menu extends GUI {
|
||||
constructor(game, settings, stats, scene, minoes, playfield) {
|
||||
super({title: "ᵀᴱTᴿᴬ"})
|
||||
|
||||
this.startButton = this.add(game, "start").name("Jouer").hide()
|
||||
this.pauseButton = this.add(game, "pause").name("Pause").hide()
|
||||
this.resumeButton = this.add(game, "resume").name("Reprendre").hide()
|
||||
|
||||
this.stats = this.addFolder("Statistiques").hide()
|
||||
this.stats.add(stats, "time").name("Temps").disable().listen()
|
||||
this.stats.add(stats, "score").name("Score").disable().listen()
|
||||
this.stats.add(stats, "highScore").name("Meilleur score").disable().listen()
|
||||
this.stats.add(stats, "level").name("Niveau").disable().listen()
|
||||
this.stats.add(stats, "totalClearedLines").name("Lignes").disable().listen()
|
||||
this.stats.add(stats, "goal").name("Objectif").disable().listen()
|
||||
this.stats.add(stats, "nbTetra").name("teTras").disable().listen()
|
||||
this.stats.add(stats, "nbTSpin").name("Pirouettes").disable().listen()
|
||||
this.stats.add(stats, "maxCombo").name("Combos max").disable().listen()
|
||||
this.stats.add(stats, "maxB2B").name("BàB max").disable().listen()
|
||||
|
||||
this.settings = this.addFolder("Options")
|
||||
this.settings.add(settings, "startLevel").name("Niveau initial").min(1).max(15).step(1)
|
||||
|
||||
this.settings.add(settings, "theme", ["Plasma", "Espace", "Rétro"]).name("Thème").onChange(theme => {
|
||||
scene.theme = theme
|
||||
minoes.theme = theme
|
||||
if (theme == "Rétro") {
|
||||
playfield.edge.visible = false
|
||||
playfield.retroEdge.visible = true
|
||||
music.src = "audio/Tetris_MkVaffQuasi_Ultimix_OC_ReMix.mp3"
|
||||
} else {
|
||||
playfield.edge.visible = true
|
||||
playfield.retroEdge.visible = false
|
||||
music.src = "audio/benevolence.m4a"
|
||||
}
|
||||
if (dev) changeMaterial()
|
||||
})
|
||||
|
||||
this.settings.key = this.settings.addFolder("Commandes").open()
|
||||
let moveLeftKeyController = this.settings.key.add(settings.key, "moveLeft").name('Gauche')
|
||||
moveLeftKeyController.domElement.onclick = this.changeKey(moveLeftKeyController)
|
||||
let moveRightKeyController = this.settings.key.add(settings.key, "moveRight").name('Droite')
|
||||
moveRightKeyController.domElement.onclick = this.changeKey(moveRightKeyController)
|
||||
let rotateCWKeyController = this.settings.key.add(settings.key, "rotateCW").name('Rotation horaire')
|
||||
rotateCWKeyController.domElement.onclick = this.changeKey(rotateCWKeyController)
|
||||
let rotateCCWKeyController = this.settings.key.add(settings.key, "rotateCCW").name('anti-horaire')
|
||||
rotateCCWKeyController.domElement.onclick = this.changeKey(rotateCCWKeyController)
|
||||
let softDropKeyController = this.settings.key.add(settings.key, "softDrop").name('Chute lente')
|
||||
softDropKeyController.domElement.onclick = this.changeKey(softDropKeyController)
|
||||
let hardDropKeyController = this.settings.key.add(settings.key, "hardDrop").name('Chute rapide')
|
||||
hardDropKeyController.domElement.onclick = this.changeKey(hardDropKeyController)
|
||||
let holdKeyController = this.settings.key.add(settings.key, "hold").name('Garder')
|
||||
holdKeyController.domElement.onclick = this.changeKey(holdKeyController)
|
||||
let pauseKeyController = this.settings.key.add(settings.key, "pause").name('Pause')
|
||||
pauseKeyController.domElement.onclick = this.changeKey(pauseKeyController)
|
||||
|
||||
this.settings.delay = this.settings.addFolder("Répétition automatique").open()
|
||||
this.settings.delay.add(settings,"arrDelay").name("ARR (ms)").min(2).max(200).step(1);
|
||||
this.settings.delay.add(settings,"dasDelay").name("DAS (ms)").min(100).max(500).step(5);
|
||||
|
||||
this.settings.volume = this.settings.addFolder("Volume").open()
|
||||
this.settings.volume.add(settings,"musicVolume").name("Musique").min(0).max(100).step(1).onChange(volume => {
|
||||
scene.music.volume = settings.musicVolume / 100
|
||||
})
|
||||
this.settings.volume.add(settings,"sfxVolume").name("Effets").min(0).max(100).step(1).onChange(volume => {
|
||||
scene.lineClearSound.setVolume(volume/100)
|
||||
scene.tetrisSound.setVolume(volume/100)
|
||||
scene.hardDropSound.setVolume(volume/100)
|
||||
})
|
||||
|
||||
let material
|
||||
function changeMaterial() {
|
||||
material?.destroy()
|
||||
material = dev.addFolder("minoes material")
|
||||
material.add(minoes.material, "constructor", ["MeshBasicMaterial", "MeshStandardMaterial", "MeshPhysicalMaterial"]).listen().onChange(type => {
|
||||
switch(type) {
|
||||
case "MeshBasicMaterial":
|
||||
minoes.material = new THREE.MeshBasicMaterial({
|
||||
envMap: environment,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
reflectivity: 0.9,
|
||||
})
|
||||
break
|
||||
case "MeshStandardMaterial":
|
||||
minoes.material = new THREE.MeshStandardMaterial({
|
||||
envMap: environment,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.7,
|
||||
roughness: 0.48,
|
||||
metalness: 0.67,
|
||||
})
|
||||
break
|
||||
case "MeshPhysicalMaterial":
|
||||
minoes.material = new THREE.MeshPhysicalMaterial({
|
||||
envMap: environment,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.7,
|
||||
roughness: 0.5,
|
||||
ior: 1.8,
|
||||
metalness: 0.9,
|
||||
transmission: 1,
|
||||
})
|
||||
break
|
||||
}
|
||||
minoes.update = minoes.updateColor
|
||||
changeMaterial()
|
||||
})
|
||||
|
||||
let minoMaterial = minoes.material instanceof Array ? minoes.material[0] : minoes.material
|
||||
if ("opacity" in minoMaterial) material.add(minoMaterial, "opacity" ).min(0).max(1)
|
||||
if ("reflectivity" in minoMaterial) material.add(minoMaterial, "reflectivity" ).min(0).max(1)
|
||||
if ("roughness" in minoMaterial) material.add(minoMaterial, "roughness" ).min(0).max(1)
|
||||
if ("bumpScale" in minoMaterial) material.add(minoMaterial, "bumpScale" ).min(0).max(5)
|
||||
if ("metalness" in minoMaterial) material.add(minoMaterial, "metalness" ).min(0).max(1)
|
||||
if ("attenuationDistance" in minoMaterial) material.add(minoMaterial, "attenuationDistance").min(0)
|
||||
if ("ior" in minoMaterial) material.add(minoMaterial, "ior" ).min(1).max(2)
|
||||
if ("sheen" in minoMaterial) material.add(minoMaterial, "sheen" ).min(0).max(1)
|
||||
if ("sheenRoughness" in minoMaterial) material.add(minoMaterial, "sheenRoughness" ).min(0).max(1)
|
||||
if ("specularIntensity" in minoMaterial) material.add(minoMaterial, "specularIntensity" ).min(0).max(1)
|
||||
if ("thickness" in minoMaterial) material.add(minoMaterial, "thickness" ).min(0).max(5)
|
||||
if ("transmission" in minoMaterial) material.add(minoMaterial, "transmission" ).min(0).max(1)
|
||||
}
|
||||
|
||||
let dev
|
||||
if (window.location.search.includes("dev")) {
|
||||
dev = this.addFolder("dev")
|
||||
let cameraPosition = dev.addFolder("camera").close()
|
||||
cameraPosition.add(scene.camera.position, "x").listen()
|
||||
cameraPosition.add(scene.camera.position, "y").listen()
|
||||
cameraPosition.add(scene.camera.position, "z").listen()
|
||||
cameraPosition.add(scene.camera, "fov", 0, 200).onChange(() => scene.camera.updateProjectionMatrix()).listen()
|
||||
|
||||
let light = dev.addFolder("lights intensity").close()
|
||||
light.add(scene.ambientLight, "intensity").name("ambient").min(0).max(20).listen()
|
||||
light.add(scene.directionalLight, "intensity").name("directional").min(0).max(20).listen()
|
||||
|
||||
let directionalLightPosition = dev.addFolder("directionalLight.position").close()
|
||||
directionalLightPosition.add(scene.directionalLight.position, "x").listen()
|
||||
directionalLightPosition.add(scene.directionalLight.position, "y").listen()
|
||||
directionalLightPosition.add(scene.directionalLight.position, "z").listen()
|
||||
|
||||
let vortex = dev.addFolder("vortex opacity").close()
|
||||
vortex.add(scene.vortex.darkCylinder.material, "opacity").name("dark").min(0).max(1)
|
||||
vortex.add(scene.vortex.colorFullCylinder.material, "opacity").name("colorFull").min(0).max(1)
|
||||
|
||||
changeMaterial(minoes.material.constructor.name)
|
||||
material.close()
|
||||
}
|
||||
}
|
||||
|
||||
load() {
|
||||
if (localStorage["teTraSettings"]) {
|
||||
this.settings.load(JSON.parse(localStorage["teTraSettings"]))
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
localStorage["teTraSettings"] = JSON.stringify(this.settings.save())
|
||||
}
|
||||
|
||||
changeKey(settings) {
|
||||
let controller = settings
|
||||
let input = controller.domElement.getElementsByTagName("input")[0]
|
||||
input.select()
|
||||
input.onkeydown = function (event) {
|
||||
controller.setValue(event.key)
|
||||
input.blur()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
@ -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]
|
||||
|
||||
195
jsm/TetraGUI.js
195
jsm/TetraGUI.js
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
93
jsm/TileMaterial.js
Normal file
@ -0,0 +1,93 @@
|
||||
import { MeshStandardMaterial, Texture, Vector2 } from 'three';
|
||||
|
||||
export class TileMaterial extends MeshStandardMaterial {
|
||||
constructor(params, tileSizeX, tileSizeY) {
|
||||
super(params);
|
||||
this.tileSize = { value: new Vector2(tileSizeX / this.map.image.width, tileSizeY / this.map.image.height) };
|
||||
}
|
||||
|
||||
onBeforeCompile(shader) {
|
||||
shader.uniforms.tileSize = this.tileSize
|
||||
shader.vertexShader = shader.vertexShader.replace(
|
||||
`void main() {`,
|
||||
`varying vec2 vUv;
|
||||
varying vec2 vOffset;
|
||||
attribute vec2 offset;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
vOffset = offset;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);`
|
||||
)
|
||||
shader.fragmentShader = `varying vec2 vUv;
|
||||
varying vec2 vOffset;
|
||||
//uniform sampler2D map;
|
||||
uniform vec2 tileSize;
|
||||
varying vec2 vBumpMapUvOffset;
|
||||
` + shader.fragmentShader
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
`#include <map_fragment>`,
|
||||
`#ifdef USE_MAP
|
||||
|
||||
vec4 sampledDiffuseColor = texture2D(map, vUv * tileSize + vOffset * tileSize);
|
||||
|
||||
#ifdef DECODE_VIDEO_TEXTURE
|
||||
|
||||
// use inline sRGB decode until browsers properly support SRGB8_ALPHA8 with video textures (#26516)
|
||||
|
||||
sampledDiffuseColor = vec4( mix( pow( sampledDiffuseColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), sampledDiffuseColor.rgb * 0.0773993808, vec3( lessThanEqual( sampledDiffuseColor.rgb, vec3( 0.04045 ) ) ) ), sampledDiffuseColor.w );
|
||||
|
||||
#endif
|
||||
|
||||
diffuseColor *= sampledDiffuseColor;
|
||||
|
||||
#endif`
|
||||
)
|
||||
shader.fragmentShader = shader.fragmentShader.replace(
|
||||
`#include <bumpmap_pars_fragment>`,
|
||||
`#ifdef USE_BUMPMAP
|
||||
|
||||
uniform sampler2D bumpMap;
|
||||
uniform float bumpScale;
|
||||
|
||||
// Bump Mapping Unparametrized Surfaces on the GPU by Morten S. Mikkelsen
|
||||
// https://mmikk.github.io/papers3d/mm_sfgrad_bump.pdf
|
||||
|
||||
// Evaluate the derivative of the height w.r.t. screen-space using forward differencing (listing 2)
|
||||
|
||||
vec2 dHdxy_fwd() {
|
||||
|
||||
vec2 vBumpMapUvOffset = vBumpMapUv * tileSize + vOffset * tileSize;
|
||||
|
||||
vec2 dSTdx = dFdx( vBumpMapUvOffset );
|
||||
vec2 dSTdy = dFdy( vBumpMapUvOffset );
|
||||
|
||||
float Hll = bumpScale * texture2D( bumpMap, vBumpMapUvOffset ).x;
|
||||
float dBx = bumpScale * texture2D( bumpMap, vBumpMapUvOffset + dSTdx ).x - Hll;
|
||||
float dBy = bumpScale * texture2D( bumpMap, vBumpMapUvOffset + dSTdy ).x - Hll;
|
||||
|
||||
return vec2( dBx, dBy );
|
||||
|
||||
}
|
||||
|
||||
vec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy, float faceDirection ) {
|
||||
|
||||
// normalize is done to ensure that the bump map looks the same regardless of the texture's scale
|
||||
vec3 vSigmaX = normalize( dFdx( surf_pos.xyz ) );
|
||||
vec3 vSigmaY = normalize( dFdy( surf_pos.xyz ) );
|
||||
vec3 vN = surf_norm; // normalized
|
||||
|
||||
vec3 R1 = cross( vSigmaY, vN );
|
||||
vec3 R2 = cross( vN, vSigmaX );
|
||||
|
||||
float fDet = dot( vSigmaX, R1 ) * faceDirection;
|
||||
|
||||
vec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );
|
||||
return normalize( abs( fDet ) * surf_norm - vGrad );
|
||||
|
||||
}
|
||||
|
||||
#endif`
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
@ -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(
|
||||
|
||||
278
loading.css
278
loading.css
@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user