Compare commits

...

16 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
9 changed files with 296 additions and 283 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)

17
app.js
View File

@ -1,10 +1,10 @@
import * as THREE from 'three'
import { scheduler } from './jsm/scheduler.js'
import { TRANSLATION, ROTATION, environment, Mino, Playfield, HoldQueue, NextQueue } from './jsm/Tetrominoes.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 { Menu } from './jsm/Menu.js'
import TetraControls from './jsm/TetraControls.js'
import CameraControls from './jsm/CameraControls.js'
import { TetraScene } from './jsm/TetraScene.js'
import * as FPS from 'three/addons/libs/stats.module.js'
@ -30,7 +30,7 @@ let game = {
menu.stats.show()
menu.settings.close()
Mino.meshes.clear()
Mino.instances.clear()
nextQueue.init()
holdQueue.piece = undefined
@ -291,15 +291,16 @@ let loadingManager = new THREE.LoadingManager(
)
loadingManager.onStart = function (url, itemsLoaded, itemsTotal) {
loadingPercent.innerText = "0%"
loadingDiv.style.display = "block"
loadingDiv.style.display = "flex"
}
const stats = new Stats()
const settings = new Settings()
const scene = new TetraScene(settings, loadingManager)
const controls = new TetraControls(scene.camera, renderer.domElement)
const controls = new CameraControls(scene.camera, renderer.domElement)
scene.add(Mino.meshes)
const minoes = new InstancedMino()
scene.add(minoes)
const holdQueue = new HoldQueue()
scene.add(holdQueue)
const playfield = new Playfield(loadingManager)
@ -307,7 +308,7 @@ scene.add(playfield)
const nextQueue = new NextQueue()
scene.add(nextQueue)
const menu = new Menu(game, settings, stats, scene, controls, playfield)
const menu = new Menu(game, settings, stats, scene, minoes, playfield)
menu.load()
let fps
@ -328,7 +329,7 @@ function animate() {
scene.updateMatrixWorld()
scene.update(delta)
playfield.update(delta)
Mino.meshes.update()
minoes.update()
controls.update()
renderer.render(scene, scene.camera)

View File

@ -1,64 +1,70 @@
body { background-color: black; }
#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;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100vw;
height: 100vh;
padding: 30vh;
background-color: black;
z-index: 1;
}
.scene {
width: 200px;
height: 200px;
margin: auto;
margin: 0 auto;
perspective: 200px;
font-size: 40px;
}
.cube {
width: 40px;
height: 40px;
.tetromino {
position: relative;
top: 2em;
left: 2em;
width: 1em;
height: 1em;
transform-style: preserve-3d;
transform: translateZ(20px);
left: 80px;
top: 80px;
}
.cube.is-spinning {
transform: translateZ(0.5em);
animation: spinCube 5s infinite ease-in-out;
}
@keyframes spinCube {
0% { transform: translateZ(20px) rotateX( 0deg) rotateY( 0deg); }
100% { transform: translateZ(20px) rotateX(360deg) rotateY(360deg); }
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: 40px;
height: 40px;
width: 1em;
height: 1em;
padding: 0;
background: hsla(240, 100%, 0%, 0.4);
border: 1px solid hsla(240, 100%, 70%, 0.6);
}
.front { transform: rotateY( 0deg) translateZ(20px); }
.right { transform: rotateY( 90deg) translateZ(20px); }
.back { transform: rotateY(180deg) translateZ(20px); }
.left { transform: rotateY(-90deg) translateZ(20px); }
.top { transform: rotateX( 90deg) translateZ(20px); }
.bottom { transform: rotateX(-90deg) translateZ(20px); }
.cube.is-backface-hidden .face {
backface-visibility: hidden;
}
.mino0 { left: -40px; top: -20px; }
.mino1 { left: 0px; top: -20px; }
.mino2 { left: 40px; top: -20px; }
.mino3 { left: 0px; top: 20px; }
.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,6 +9,8 @@ body {
span {
position: absolute;
top: 0;
left: 0;
}
.lil-menu {
@ -171,10 +173,10 @@ h1 {
}
.pause #pauseSpan {
display: flex;
position:absolute;
position: absolute;
top: 0;
left: 0;
display: flex;
filter: blur(2px);
width: 100%;
height: 100%;

View File

@ -8,6 +8,15 @@
<link rel="icon" href="favicon.ico">
<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">
{
@ -20,40 +29,48 @@
</head>
<body>
<div id="loadingDiv">
<span id="loadingDiv">
<div class="scene">
<div class="cube is-spinning">
<div class="mino0 front face"></div>
<div class="mino0 back face"></div>
<div class="mino0 right face"></div>
<div class="mino0 left face"></div>
<div class="mino0 top face"></div>
<div class="mino0 bottom face"></div>
<div class="mino1 front face"></div>
<div class="mino1 back face"></div>
<div class="mino1 right face"></div>
<div class="mino1 left face"></div>
<div class="mino1 top face"></div>
<div class="mino1 bottom face"></div>
<div class="mino2 front face"></div>
<div class="mino2 back face"></div>
<div class="mino2 right face"></div>
<div class="mino2 left face"></div>
<div class="mino2 top face"></div>
<div class="mino2 bottom face"></div>
<div class="mino3 front face"></div>
<div class="mino3 back face"></div>
<div class="mino3 right face"></div>
<div class="mino3 left face"></div>
<div class="mino3 top face"></div>
<div class="mino3 bottom face"></div>
<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>
</div>
</span>
<span id="messagesSpan"></span>
<span id="pauseSpan" tabindex="1">II</span>
<audio id="music" src="audio/benevolence.m4a" loop></audio>

View File

@ -1,7 +1,7 @@
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
export default class TetraControls extends OrbitControls {
export default class CameraControls extends OrbitControls {
constructor(camera, domElement) {
super(camera, domElement)
this.autoRotate

View File

@ -1,10 +1,10 @@
import * as THREE from 'three'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import { Mino, environment } from './Tetrominoes.js'
import { environment } from './Tetrominoes.js'
export class Menu extends GUI {
constructor(game, settings, stats, scene, controls, playfield) {
constructor(game, settings, stats, scene, minoes, playfield) {
super({title: "ᵀᴱTᴿᴬ"})
this.startButton = this.add(game, "start").name("Jouer").hide()
@ -23,13 +23,12 @@ export class Menu extends GUI {
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
Mino.meshes.theme = theme
minoes.theme = theme
if (theme == "Rétro") {
playfield.edge.visible = false
playfield.retroEdge.visible = true
@ -39,25 +38,26 @@ export class Menu extends GUI {
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.bind(moveLeftKeyController)
moveLeftKeyController.domElement.onclick = this.changeKey(moveLeftKeyController)
let moveRightKeyController = this.settings.key.add(settings.key, "moveRight").name('Droite')
moveRightKeyController.domElement.onclick = this.changeKey.bind(moveRightKeyController)
moveRightKeyController.domElement.onclick = this.changeKey(moveRightKeyController)
let rotateCWKeyController = this.settings.key.add(settings.key, "rotateCW").name('Rotation horaire')
rotateCWKeyController.domElement.onclick = this.changeKey.bind(rotateCWKeyController)
rotateCWKeyController.domElement.onclick = this.changeKey(rotateCWKeyController)
let rotateCCWKeyController = this.settings.key.add(settings.key, "rotateCCW").name('anti-horaire')
rotateCCWKeyController.domElement.onclick = this.changeKey.bind(rotateCCWKeyController)
rotateCCWKeyController.domElement.onclick = this.changeKey(rotateCCWKeyController)
let softDropKeyController = this.settings.key.add(settings.key, "softDrop").name('Chute lente')
softDropKeyController.domElement.onclick = this.changeKey.bind(softDropKeyController)
softDropKeyController.domElement.onclick = this.changeKey(softDropKeyController)
let hardDropKeyController = this.settings.key.add(settings.key, "hardDrop").name('Chute rapide')
hardDropKeyController.domElement.onclick = this.changeKey.bind(hardDropKeyController)
hardDropKeyController.domElement.onclick = this.changeKey(hardDropKeyController)
let holdKeyController = this.settings.key.add(settings.key, "hold").name('Garder')
holdKeyController.domElement.onclick = this.changeKey.bind(holdKeyController)
holdKeyController.domElement.onclick = this.changeKey(holdKeyController)
let pauseKeyController = this.settings.key.add(settings.key, "pause").name('Pause')
pauseKeyController.domElement.onclick = this.changeKey.bind(pauseKeyController)
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);
@ -73,14 +73,71 @@ export class Menu extends GUI {
scene.hardDropSound.setVolume(volume/100)
})
this.settings.dev = window.location.search.includes("dev")
if (this.settings.dev) {
let dev = this.settings.addFolder("dev")
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")
cameraPosition.add(scene.camera.position, "y")
cameraPosition.add(scene.camera.position, "z")
cameraPosition.add(scene.camera, "fov", 0, 200).onChange(() => scene.camera.updateProjectionMatrix())
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()
@ -95,63 +152,8 @@ export class Menu extends GUI {
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.meshes.material, "constructor", ["MeshBasicMaterial", "MeshStandardMaterial", "MeshPhysicalMaterial"]).name("type").onChange(changeMaterial)
switch(type) {
case "MeshBasicMaterial":
Mino.meshes.material = new THREE.MeshBasicMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.5,
reflectivity: 0.9,
})
break
case "MeshStandardMaterial":
Mino.meshes.material = new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7,
roughness: 0.48,
metalness: 0.67,
})
break
case "MeshPhysicalMaterial":
Mino.meshes.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
}
if ("opacity" in Mino.meshes.material) material.add(Mino.meshes.material, "opacity" ).min(0).max(1).listen()
if ("reflectivity" in Mino.meshes.material) material.add(Mino.meshes.material, "reflectivity" ).min(0).max(1).listen()
if ("roughness" in Mino.meshes.material) material.add(Mino.meshes.material, "roughness" ).min(0).max(1).listen()
if ("metalness" in Mino.meshes.material) material.add(Mino.meshes.material, "metalness" ).min(0).max(1).listen()
if ("attenuationDistance" in Mino.meshes.material) material.add(Mino.meshes.material, "attenuationDistance").min(0).listen()
if ("ior" in Mino.meshes.material) material.add(Mino.meshes.material, "ior" ).min(1).max(2).listen()
if ("sheen" in Mino.meshes.material) material.add(Mino.meshes.material, "sheen" ).min(0).max(1).listen()
if ("sheenRoughness" in Mino.meshes.material) material.add(Mino.meshes.material, "sheenRoughness" ).min(0).max(1).listen()
if ("specularIntensity" in Mino.meshes.material) material.add(Mino.meshes.material, "specularIntensity" ).min(0).max(1).listen()
if ("thickness" in Mino.meshes.material) material.add(Mino.meshes.material, "thickness" ).min(0).max(5).listen()
if ("transmission" in Mino.meshes.material) material.add(Mino.meshes.material, "transmission" ).min(0).max(1).listen()
}
changeMaterial(this.settings.materialType)
changeMaterial(minoes.material.constructor.name)
material.close()
controls.addEventListener("change", () => cameraPosition.controllersRecursive().forEach((control) => {
control.updateDisplay()
}))
}
}
@ -165,8 +167,8 @@ export class Menu extends GUI {
localStorage["teTraSettings"] = JSON.stringify(this.settings.save())
}
changeKey() {
let controller = this.settings
changeKey(settings) {
let controller = settings
let input = controller.domElement.getElementsByTagName("input")[0]
input.select()
input.onkeydown = function (event) {

View File

@ -69,129 +69,8 @@ const sideMaterial = new THREE.MeshStandardMaterial({
})
class InstancedMino extends THREE.InstancedMesh {
constructor(geometry, material, count) {
super(geometry, material, count)
this.instances = new Set()
this.count = 0
this.offsets = new Uint8Array(2*count)
this.update = this.updateColor
}
add(instance) {
this.instances.add(instance)
}
delete(instance) {
this.instances.delete(instance)
}
clear() {
this.instances.clear()
}
set theme(theme) {
this._theme = theme
Mino.meshes.material = Mino.materials[theme]
if (theme == "Rétro") {
this.resetColor()
this.update = Mino.meshes.updateOffset
} else {
this.update = Mino.meshes.updateColor
}
}
get theme() {
return this._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
this.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
this.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))
}
}
}
class Mino extends THREE.Object3D {
static materials = {
Plasma: new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7,
roughness: 0.6,
metalness: 1,
}),
Espace: new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.8,
roughness: 0.1,
metalness: 0.99,
}),
Rétro: [sideMaterial, sideMaterial, sideMaterial, sideMaterial, sideMaterial, sideMaterial]
}
static {
new THREE.TextureLoader().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: 2,
roughness: 0.25,
metalness: 0.8,
transparent: true,
}, 8, 8)
})
new THREE.TextureLoader().load("images/edges.png", (texture) => {
this.materials.Rétro[1] = this.materials.Rétro[3] =this.materials.Rétro[4] = this.materials.Rétro[4] = new TileMaterial({
color: COLORS.RETRO,
map: texture,
bumpMap: texture,
bumpScale: 2,
roughness: 0.25,
metalness: 0.8,
transparent: true,
}, 1, 1)
})
}
static meshes
static {
export class InstancedMino extends THREE.InstancedMesh {
constructor() {
let minoFaceShape = new THREE.Shape()
minoFaceShape.moveTo(.1, .1)
minoFaceShape.lineTo(.1, .9)
@ -208,9 +87,110 @@ class Mino extends THREE.Object3D {
bevelSegments: 1
}
const geometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings)
this.meshes = new InstancedMino(geometry, this.materials.Plasma, 2*ROWS*COLUMNS)
super(geometry, undefined, 2*ROWS*COLUMNS)
this.offsets = new Uint8Array(2*this.count)
}
set theme(theme) {
if (theme == "Rétro") {
this.resetColor()
this.update = this.updateOffset
if (this.materials["Rétro"]) {
this.material = this.materials["Rétro"]
} else {
this.materials["Rétro"] = []
const loadingManager = new THREE.LoadingManager(() => this.material = this.materials["Rétro"])
new THREE.TextureLoader(loadingManager).load("images/sprites.png", (texture) => {
this.materials.Rétro[0] = this.materials.Rétro[2] = new TileMaterial({
color: COLORS.RETRO,
map: texture,
bumpMap: texture,
bumpScale: 1.5,
roughness: 0.25,
metalness: 0.9,
transparent: true,
}, 8, 8)
})
new THREE.TextureLoader(loadingManager).load("images/edges.png", (texture) => {
this.materials.Rétro[1] = this.materials.Rétro[3] = this.materials.Rétro[4] = this.materials.Rétro[5] = new TileMaterial({
color: COLORS.RETRO,
map: texture,
bumpMap: texture,
bumpScale: 1.5,
roughness: 0.25,
metalness: 0.9,
transparent: true,
}, 1, 1)
})
}
} else {
this.update = this.updateColor
this.material = this.materials[theme]
}
}
setOffsetAt(index, offset) {
this.offsets[2*index] = offset.x
this.offsets[2*index + 1] = offset.y
}
resetColor() {
this.instanceColor = null
}
updateColor() {
this.count = 0
Mino.instances.forEach(mino => {
if (mino.parent?.visible) {
this.setMatrixAt(this.count, mino.matrixWorld)
this.setColorAt(this.count, mino.color)
this.count++
}
})
if (this.count) {
this.instanceMatrix.needsUpdate = true
this.instanceColor.needsUpdate = true
}
}
updateOffset() {
this.count = 0
Mino.instances.forEach(mino => {
if (mino.parent?.visible) {
this.setMatrixAt(this.count, mino.matrixWorld)
this.setOffsetAt(this.count, mino.offset)
this.count++
}
})
if (this.count) {
this.instanceMatrix.needsUpdate = true
this.geometry.setAttribute('offset', new THREE.InstancedBufferAttribute(this.offsets, 2))
}
}
}
InstancedMino.prototype.materials = {
Plasma: new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7,
roughness: 0.6,
metalness: 1,
}),
Espace: new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.8,
roughness: 0.1,
metalness: 0.99,
})
}
class Mino extends THREE.Object3D {
static instances = new Set()
constructor(color, offset) {
super()
this.color = color
@ -218,7 +198,7 @@ class Mino extends THREE.Object3D {
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.meshes.add(this)
this.constructor.instances.add(this)
}
explode(delta) {
@ -234,7 +214,7 @@ class Mino extends THREE.Object3D {
}
dispose() {
this.constructor.meshes.delete(this)
this.constructor.instances.delete(this)
}
}
@ -507,9 +487,9 @@ class Playfield extends THREE.Group {
color: COLORS.RETRO,
map: retroEdgeTexture,
bumpMap: retroEdgeTexture,
bumpScale: 0.3,
bumpScale: 1.5,
roughness: 0.25,
metalness: 0.8,
metalness: 0.9,
})
this.retroEdge = new THREE.Mesh(
new THREE.ExtrudeGeometry(retroEdgeShape, {
@ -626,7 +606,7 @@ 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) {
@ -650,7 +630,7 @@ 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() {

View File

@ -70,10 +70,10 @@ export class Vortex extends THREE.Group {
new THREE.TextureLoader(this.loadingManager).load("./images/dark.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 2)
texture.repeat.set(2, 4)
this.darkCylinder.material.map = texture
})
this.darkCylinder.material.opacity = 0.05
this.darkCylinder.material.opacity = 0.08
new THREE.TextureLoader(this.loadingManager).load("./images/colorfull.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
@ -81,7 +81,7 @@ export class Vortex extends THREE.Group {
texture.repeat.set(2, 2)
this.colorFullCylinder.material.map = texture
})
this.colorFullCylinder.material.opacity = 0.14
this.colorFullCylinder.material.opacity = 0.15
this.globalRotation = 0.028
this.darkTextureRotation = 0.006