index.html
<link rel="stylesheet" href="./style.css"> <div id="container"> <div id="game"></div> <div id="score">0</div> <div id="instructions">Click to place the block</div> <div class="game-over"> <h2>Game Over</h2> <p>You did great, you're the best.</p> <p>Click or spacebar to start again</p> </div> <div class="game-ready"> <div id="start-button">Start</div> <div></div> </div> </div> <script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/r83/three.min.js'></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/gsap/latest/TweenMax.min.js'></script> <script src="./main.js"></script>
style.css
html, body { margin: 0; overflow: hidden; height: 100%; width: 100%; position: relative; font-family: 'Comfortaa', cursive; } #container #score { position: absolute; top: 20px; width: 100%; text-align: center; font-size: 10vh; -webkit-transition: -webkit-transform 0.5s ease; transition: -webkit-transform 0.5s ease; transition: transform 0.5s ease; transition: transform 0.5s ease, -webkit-transform 0.5s ease; color: #333344; -webkit-transform: translatey(-200px) scale(1); transform: translatey(-200px) scale(1); } #container #game { position: absolute; top: 0; right: 0; bottom: 0; left: 0; } #container .game-over { position: absolute; top: 0; left: 0; width: 100%; height: 85%; display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; } #container .game-over * { -webkit-transition: opacity 0.5s ease, -webkit-transform 0.5s ease; transition: opacity 0.5s ease, -webkit-transform 0.5s ease; transition: opacity 0.5s ease, transform 0.5s ease; transition: opacity 0.5s ease, transform 0.5s ease, -webkit-transform 0.5s ease; opacity: 0; -webkit-transform: translatey(-50px); transform: translatey(-50px); color: #333344; } #container .game-ready { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -ms-flex-pack: distribute; justify-content: space-around; } #container .game-ready #start-button { -webkit-transition: opacity 0.5s ease, -webkit-transform 0.5s ease; transition: opacity 0.5s ease, -webkit-transform 0.5s ease; transition: opacity 0.5s ease, transform 0.5s ease; transition: opacity 0.5s ease, transform 0.5s ease, -webkit-transform 0.5s ease; opacity: 0; -webkit-transform: translatey(-50px); transform: translatey(-50px); border: 3px solid #333344; padding: 10px 20px; background-color: transparent; color: #333344; font-size: 30px; } #container #instructions { position: absolute; width: 100%; top: 16vh; left: 0; text-align: center; -webkit-transition: opacity 0.5s ease, -webkit-transform 0.5s ease; transition: opacity 0.5s ease, -webkit-transform 0.5s ease; transition: opacity 0.5s ease, transform 0.5s ease; transition: opacity 0.5s ease, transform 0.5s ease, -webkit-transform 0.5s ease; opacity: 0; } #container #instructions.hide { opacity: 0 !important; } #container.playing #score, #container.resetting #score { -webkit-transform: translatey(0px) scale(1); transform: translatey(0px) scale(1); } #container.playing #instructions { opacity: 1; } #container.ready .game-ready #start-button { opacity: 1; -webkit-transform: translatey(0); transform: translatey(0); } #container.ended #score { -webkit-transform: translatey(6vh) scale(1.5); transform: translatey(6vh) scale(1.5); } #container.ended .game-over * { opacity: 1; -webkit-transform: translatey(0); transform: translatey(0); } #container.ended .game-over p { -webkit-transition-delay: 0.3s; transition-delay: 0.3s; }
main.js
console.clear(); var Stage = /** @class */ (function () { function Stage() { // container var _this = this; this.render = function () { this.renderer.render(this.scene, this.camera); }; this.add = function (elem) { this.scene.add(elem); }; this.remove = function (elem) { this.scene.remove(elem); }; this.container = document.getElementById('game'); // renderer this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); this.renderer.setSize(window.innerWidth, window.innerHeight); this.renderer.setClearColor('#D0CBC7', 1); this.container.appendChild(this.renderer.domElement); // scene this.scene = new THREE.Scene(); // camera var aspect = window.innerWidth / window.innerHeight; var d = 20; this.camera = new THREE.OrthographicCamera(-d * aspect, d * aspect, d, -d, -100, 1000); this.camera.position.x = 2; this.camera.position.y = 2; this.camera.position.z = 2; this.camera.lookAt(new THREE.Vector3(0, 0, 0)); //light this.light = new THREE.DirectionalLight(0xffffff, 0.5); this.light.position.set(0, 499, 0); this.scene.add(this.light); this.softLight = new THREE.AmbientLight(0xffffff, 0.4); this.scene.add(this.softLight); window.addEventListener('resize', function () { return _this.onResize(); }); this.onResize(); } Stage.prototype.setCamera = function (y, speed) { if (speed === void 0) { speed = 0.3; } TweenLite.to(this.camera.position, speed, { y: y + 4, ease: Power1.easeInOut }); TweenLite.to(this.camera.lookAt, speed, { y: y, ease: Power1.easeInOut }); }; Stage.prototype.onResize = function () { var viewSize = 30; this.renderer.setSize(window.innerWidth, window.innerHeight); this.camera.left = window.innerWidth / -viewSize; this.camera.right = window.innerWidth / viewSize; this.camera.top = window.innerHeight / viewSize; this.camera.bottom = window.innerHeight / -viewSize; this.camera.updateProjectionMatrix(); }; return Stage; }()); var Block = /** @class */ (function () { function Block(block) { // set size and position this.STATES = { ACTIVE: 'active', STOPPED: 'stopped', MISSED: 'missed' }; this.MOVE_AMOUNT = 12; this.dimension = { width: 0, height: 0, depth: 0 }; this.position = { x: 0, y: 0, z: 0 }; this.targetBlock = block; this.index = (this.targetBlock ? this.targetBlock.index : 0) + 1; this.workingPlane = this.index % 2 ? 'x' : 'z'; this.workingDimension = this.index % 2 ? 'width' : 'depth'; // set the dimensions from the target block, or defaults. this.dimension.width = this.targetBlock ? this.targetBlock.dimension.width : 10; this.dimension.height = this.targetBlock ? this.targetBlock.dimension.height : 2; this.dimension.depth = this.targetBlock ? this.targetBlock.dimension.depth : 10; this.position.x = this.targetBlock ? this.targetBlock.position.x : 0; this.position.y = this.dimension.height * this.index; this.position.z = this.targetBlock ? this.targetBlock.position.z : 0; this.colorOffset = this.targetBlock ? this.targetBlock.colorOffset : Math.round(Math.random() * 100); // set color if (!this.targetBlock) { this.color = 0x333344; } else { var offset = this.index + this.colorOffset; var r = Math.sin(0.3 * offset) * 55 + 200; var g = Math.sin(0.3 * offset + 2) * 55 + 200; var b = Math.sin(0.3 * offset + 4) * 55 + 200; this.color = new THREE.Color(r / 255, g / 255, b / 255); } // state this.state = this.index > 1 ? this.STATES.ACTIVE : this.STATES.STOPPED; // set direction this.speed = -0.1 - (this.index * 0.005); if (this.speed < -4) this.speed = -4; this.direction = this.speed; // create block var geometry = new THREE.BoxGeometry(this.dimension.width, this.dimension.height, this.dimension.depth); geometry.applyMatrix(new THREE.Matrix4().makeTranslation(this.dimension.width / 2, this.dimension.height / 2, this.dimension.depth / 2)); this.material = new THREE.MeshToonMaterial({ color: this.color, shading: THREE.FlatShading }); this.mesh = new THREE.Mesh(geometry, this.material); this.mesh.position.set(this.position.x, this.position.y + (this.state == this.STATES.ACTIVE ? 0 : 0), this.position.z); if (this.state == this.STATES.ACTIVE) { this.position[this.workingPlane] = Math.random() > 0.5 ? -this.MOVE_AMOUNT : this.MOVE_AMOUNT; } } Block.prototype.reverseDirection = function () { this.direction = this.direction > 0 ? this.speed : Math.abs(this.speed); }; Block.prototype.place = function () { this.state = this.STATES.STOPPED; var overlap = this.targetBlock.dimension[this.workingDimension] - Math.abs(this.position[this.workingPlane] - this.targetBlock.position[this.workingPlane]); var blocksToReturn = { plane: this.workingPlane, direction: this.direction }; if (this.dimension[this.workingDimension] - overlap < 0.3) { overlap = this.dimension[this.workingDimension]; blocksToReturn.bonus = true; this.position.x = this.targetBlock.position.x; this.position.z = this.targetBlock.position.z; this.dimension.width = this.targetBlock.dimension.width; this.dimension.depth = this.targetBlock.dimension.depth; } if (overlap > 0) { var choppedDimensions = { width: this.dimension.width, height: this.dimension.height, depth: this.dimension.depth }; choppedDimensions[this.workingDimension] -= overlap; this.dimension[this.workingDimension] = overlap; var placedGeometry = new THREE.BoxGeometry(this.dimension.width, this.dimension.height, this.dimension.depth); placedGeometry.applyMatrix(new THREE.Matrix4().makeTranslation(this.dimension.width / 2, this.dimension.height / 2, this.dimension.depth / 2)); var placedMesh = new THREE.Mesh(placedGeometry, this.material); var choppedGeometry = new THREE.BoxGeometry(choppedDimensions.width, choppedDimensions.height, choppedDimensions.depth); choppedGeometry.applyMatrix(new THREE.Matrix4().makeTranslation(choppedDimensions.width / 2, choppedDimensions.height / 2, choppedDimensions.depth / 2)); var choppedMesh = new THREE.Mesh(choppedGeometry, this.material); var choppedPosition = { x: this.position.x, y: this.position.y, z: this.position.z }; if (this.position[this.workingPlane] < this.targetBlock.position[this.workingPlane]) { this.position[this.workingPlane] = this.targetBlock.position[this.workingPlane]; } else { choppedPosition[this.workingPlane] += overlap; } placedMesh.position.set(this.position.x, this.position.y, this.position.z); choppedMesh.position.set(choppedPosition.x, choppedPosition.y, choppedPosition.z); blocksToReturn.placed = placedMesh; if (!blocksToReturn.bonus) blocksToReturn.chopped = choppedMesh; } else { this.state = this.STATES.MISSED; } this.dimension[this.workingDimension] = overlap; return blocksToReturn; }; Block.prototype.tick = function () { if (this.state == this.STATES.ACTIVE) { var value = this.position[this.workingPlane]; if (value > this.MOVE_AMOUNT || value < -this.MOVE_AMOUNT) this.reverseDirection(); this.position[this.workingPlane] += this.direction; this.mesh.position[this.workingPlane] = this.position[this.workingPlane]; } }; return Block; }()); var Game = /** @class */ (function () { function Game() { var _this = this; this.STATES = { 'LOADING': 'loading', 'PLAYING': 'playing', 'READY': 'ready', 'ENDED': 'ended', 'RESETTING': 'resetting' }; this.blocks = []; this.state = this.STATES.LOADING; this.stage = new Stage(); this.mainContainer = document.getElementById('container'); this.scoreContainer = document.getElementById('score'); this.startButton = document.getElementById('start-button'); this.instructions = document.getElementById('instructions'); this.scoreContainer.innerHTML = '0'; this.newBlocks = new THREE.Group(); this.placedBlocks = new THREE.Group(); this.choppedBlocks = new THREE.Group(); this.stage.add(this.newBlocks); this.stage.add(this.placedBlocks); this.stage.add(this.choppedBlocks); this.addBlock(); this.tick(); this.updateState(this.STATES.READY); document.addEventListener('keydown', function (e) { if (e.keyCode == 32) _this.onAction(); }); document.addEventListener('click', function (e) { _this.onAction(); }); document.addEventListener('touchstart', function (e) { e.preventDefault(); _this.onAction(); }); } Game.prototype.updateState = function (newState) { for (var key in this.STATES) this.mainContainer.classList.remove(this.STATES[key]); this.mainContainer.classList.add(newState); this.state = newState; }; Game.prototype.onAction = function () { switch (this.state) { case this.STATES.READY: this.startGame(); break; case this.STATES.PLAYING: this.placeBlock(); break; case this.STATES.ENDED: this.restartGame(); break; } }; Game.prototype.startGame = function () { if (this.state != this.STATES.PLAYING) { this.scoreContainer.innerHTML = '0'; this.updateState(this.STATES.PLAYING); this.addBlock(); } }; Game.prototype.restartGame = function () { var _this = this; this.updateState(this.STATES.RESETTING); var oldBlocks = this.placedBlocks.children; var removeSpeed = 0.2; var delayAmount = 0.02; var _loop_1 = function (i) { TweenLite.to(oldBlocks[i].scale, removeSpeed, { x: 0, y: 0, z: 0, delay: (oldBlocks.length - i) * delayAmount, ease: Power1.easeIn, onComplete: function () { return _this.placedBlocks.remove(oldBlocks[i]); } }); TweenLite.to(oldBlocks[i].rotation, removeSpeed, { y: 0.5, delay: (oldBlocks.length - i) * delayAmount, ease: Power1.easeIn }); }; for (var i = 0; i < oldBlocks.length; i++) { _loop_1(i); } var cameraMoveSpeed = removeSpeed * 2 + (oldBlocks.length * delayAmount); this.stage.setCamera(2, cameraMoveSpeed); var countdown = { value: this.blocks.length - 1 }; TweenLite.to(countdown, cameraMoveSpeed, { value: 0, onUpdate: function () { _this.scoreContainer.innerHTML = String(Math.round(countdown.value)); } }); this.blocks = this.blocks.slice(0, 1); setTimeout(function () { _this.startGame(); }, cameraMoveSpeed * 1000); }; Game.prototype.placeBlock = function () { var _this = this; var currentBlock = this.blocks[this.blocks.length - 1]; var newBlocks = currentBlock.place(); this.newBlocks.remove(currentBlock.mesh); if (newBlocks.placed) this.placedBlocks.add(newBlocks.placed); if (newBlocks.chopped) { this.choppedBlocks.add(newBlocks.chopped); var positionParams = { y: '-=30', ease: Power1.easeIn, onComplete: function () { return _this.choppedBlocks.remove(newBlocks.chopped); } }; var rotateRandomness = 10; var rotationParams = { delay: 0.05, x: newBlocks.plane == 'z' ? ((Math.random() * rotateRandomness) - (rotateRandomness / 2)) : 0.1, z: newBlocks.plane == 'x' ? ((Math.random() * rotateRandomness) - (rotateRandomness / 2)) : 0.1, y: Math.random() * 0.1 }; if (newBlocks.chopped.position[newBlocks.plane] > newBlocks.placed.position[newBlocks.plane]) { positionParams[newBlocks.plane] = '+=' + (40 * Math.abs(newBlocks.direction)); } else { positionParams[newBlocks.plane] = '-=' + (40 * Math.abs(newBlocks.direction)); } TweenLite.to(newBlocks.chopped.position, 1, positionParams); TweenLite.to(newBlocks.chopped.rotation, 1, rotationParams); } this.addBlock(); }; Game.prototype.addBlock = function () { var lastBlock = this.blocks[this.blocks.length - 1]; if (lastBlock && lastBlock.state == lastBlock.STATES.MISSED) { return this.endGame(); } this.scoreContainer.innerHTML = String(this.blocks.length - 1); var newKidOnTheBlock = new Block(lastBlock); this.newBlocks.add(newKidOnTheBlock.mesh); this.blocks.push(newKidOnTheBlock); this.stage.setCamera(this.blocks.length * 2); if (this.blocks.length >= 5) this.instructions.classList.add('hide'); }; Game.prototype.endGame = function () { this.updateState(this.STATES.ENDED); }; Game.prototype.tick = function () { var _this = this; this.blocks[this.blocks.length - 1].tick(); this.stage.render(); requestAnimationFrame(function () { _this.tick(); }); }; return Game; }()); var game = new Game();