commit 343bc1e113c2296a126977962237a0dec1b76100 Author: adrien Date: Fri Apr 21 03:14:24 2023 +0200 almost done diff --git a/app.js b/app.js new file mode 100644 index 0000000..59a89d3 --- /dev/null +++ b/app.js @@ -0,0 +1,699 @@ +/* Contants */ +const TRANSLATION = { + NONE: [ 0, 0], + LEFT: [-1, 0], + RIGHT: [ 1, 0], + DOWN: [ 0, 1], +} + +const ROTATION = { + CW: 1, // ClockWise + CCW: -1, // CounterClockWise +} + +const T_SPIN = { + NONE: "", + MINI: "MINI T-SPIN", + T_SPIN: "T-SPIN" +} + +// score = SCORES[tSpin][clearedLines] +const SCORES = { + [T_SPIN.NONE]: [0, 100, 300, 500, 800], + [T_SPIN.MINI]: [100, 200], + [T_SPIN.T_SPIN]: [400, 800, 1200, 1600] +} + +const CLEARED_LINES_NAMES = [ + "", + "SINGLE", + "DOUBLE", + "TRIPLE", + "QUATRIS", +] + +const DELAY = { + LOCK: 500, + FALL: 1000, +} + +const ORIENTATION = { + NORTH: 0, + EAST: 1, + SOUTH: 2, + WEST: 3, +} + +const KEY_NAMES = { + ["ArrowLeft"]: "←", + ["ArrowRight"]: "→", + ["ArrowUp"]: "↑", + ["ArrowDown"]: "↓", + [" "]: "Space", + ["←"]: "ArrowLeft", + ["→"]: "ArrowRight", + ["↑"]: "ArrowUp", + ["↓"]: "ArrowDown", + ["Space"]: " ", +} + +/* Customize Array to be use as coord */ +Object.defineProperties(Array.prototype, { + "x": { + get: function() { return this[0] }, + set: function(x) { this[0] = x } + }, + "y": { + get: function() { return this[1] }, + set: function(y) { this[1] = y } + } +}) +Array.prototype.add = function(other) { return this.map((x, i) => x + other[i]) } +Array.prototype.mul = function(k) { return this.map(x => k * x) } +Array.prototype.translate = function(vector) { return this.map(pos => pos.add(vector)) } +Array.prototype.rotate = function(rotation) { return [-rotation*this.y, rotation*this.x] } +Array.prototype.pick = function() { return this.splice(Math.floor(Math.random()*this.length), 1)[0] } + + +/* Classes */ +class Scheduler { + constructor() { + this.intervalTasks = new Map() + this.timeoutTasks = new Map() + } + + setInterval(func, delay, ...args) { + this.intervalTasks.set(func, window.setInterval(func, delay, ...args)) + } + + setTimeout(func, delay, ...args) { + this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args)) + } + + clearInterval(func) { + if (this.intervalTasks.has(func)) + window.clearInterval(this.intervalTasks.get(func)) + this.intervalTasks.delete(func) + } + + clearTimeout(func) { + if (this.timeoutTasks.has(func)) + window.clearTimeout(this.timeoutTasks.get(func)) + this.timeoutTasks.delete(func) + } +} + + +class MinoesTable { + constructor(id) { + this.table = document.getElementById(id) + this.rows = this.table.rows.length + this.columns = this.table.rows[0].childElementCount + this._piece = null + } + + get piece() { + return this._piece + } + set piece(piece) { + this._piece = piece + this._piece.center = Array.from(this.init_center) + this.redraw() + this.drawPiece() + } + + drawMino(coord, className) { + this.table.rows[coord.y].cells[coord.x].className = className + } + + drawPiece(piece=this.piece, className=piece.className + (piece.locked? " locked" : "")) { + piece.minoesCoord[piece.orientation] + .translate(piece.center) + .forEach(minoCoord => { + this.drawMino(minoCoord, className) + }) + } + + redraw() { + for (let y=0; y { + let piece = new Tetromino.pick() + piece.center = Array.from(center) + return piece + }) + } + + shift() { + let fistPiece = this.pieces.shift() + this.pieces.push(new Tetromino.pick()) + this.pieces.forEach((piece, i) => { + piece.center = Array.from(this.init_centers[i]) + }) + this.redraw() + return fistPiece + } + + redraw() { + super.redraw() + this.pieces.forEach((piece) => { + this.drawPiece(piece) + }) + } +} +NextQueue.prototype.init_centers = [[2, 2], [2, 5], [2, 8], [2, 11], [2, 14]] + + +class PlayfieldMatrix extends MinoesTable { + constructor(id, piece_init_position) { + super(id, piece_init_position) + this.lockedMinoes = Array(this.rows).fill().map(() => Array(this.columns)) + } + + cellIsEmpty(coord) { + return 0 <= coord.x && coord.x < this.columns && 0 <= coord.y && coord.y < this.rows && !this.lockedMinoes[coord.y][coord.x] + } + + get piece() { + return this._piece + } + set piece(piece) { + this._piece = piece + this._piece.center = Array.from(this.init_center) + this.ghost = piece.ghost + this.redraw() + this.drawPiece() + } + + drawPiece(piece=this.piece, className=piece.className + (piece.locked? " locked" : "")) { + super.drawPiece(this.ghost, "") + this.ghost = piece.ghost + while (this.ghost.canMove(TRANSLATION.DOWN)) this.ghost.center.y++ + super.drawPiece(this.ghost) + super.drawPiece(piece, className) + } + + redraw() { + for (let y=0; y matrix.cellIsEmpty(minoCoord))) + return {center: testCenter, orientation: testOrientation} + else + return false + } + + move(translation, rotation=ROTATION.NONE, clearClassName="") { + let success = this.canMove(translation, rotation) + if (success) { + scheduler.clearTimeout(lockDown) + matrix.drawPiece(this, clearClassName) + this.center = success.center + if (rotation) this.orientation = success.orientation + this.lastRotation = rotation + if (this.canMove(TRANSLATION.DOWN)) { + this.locked = false + } else { + this.locked = true + scheduler.setTimeout(lockDown, stats.lockDelay) + } + matrix.drawPiece() + return true + } else if (translation == TRANSLATION.DOWN) { + this.locked = true + if (!scheduler.timeoutTasks.has(lockDown)) + scheduler.setTimeout(lockDown, stats.lockDelay) + matrix.drawPiece() + } + } + + rotate(rotation) { + return this.srs[this.orientation][rotation].some((translation, rotationPoint) => { + if (this.move(translation, rotation)) { + if (rotationPoint == 4) this.rotationPoint4Used = true + return true + } + }) + } + + get ghost() { + return new this.constructor(Array.from(this.center), this.orientation, "ghost " + this.className) + } +} +// Super Rotation System +// freedom of movement = srs[piece.orientation][rotation] +Tetromino.prototype.srs = [ + { [ROTATION.CW]: [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]] }, + { [ROTATION.CW]: [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]] }, + { [ROTATION.CW]: [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]] }, + { [ROTATION.CW]: [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]] }, +] + +class I extends Tetromino {} +I.prototype.minoesCoord = [ + [[-1, 0], [0, 0], [1, 0], [2, 0]], + [[1, -1], [1, 0], [1, 1], [1, 2]], + [[-1, 1], [0, 1], [1, 1], [2, 1]], + [[0, -1], [0, 0], [0, 1], [0, 2]], +] +I.prototype.srs = [ + { [ROTATION.CW]: [[0, 0], [-2, 0], [ 1, 0], [-2, 1], [ 1, -2]], [ROTATION.CCW]: [[0, 0], [-1, 0], [ 2, 0], [-1, -2], [ 2, 1]] }, + { [ROTATION.CW]: [[0, 0], [-1, 0], [ 2, 0], [-1, -2], [ 2, 1]], [ROTATION.CCW]: [[0, 0], [ 2, 0], [-1, 0], [ 2, -1], [-1, 2]] }, + { [ROTATION.CW]: [[0, 0], [ 2, 0], [-1, 0], [ 2, -1], [-1, 2]], [ROTATION.CCW]: [[0, 0], [ 1, 0], [-2, 0], [ 1, 2], [-2, -1]] }, + { [ROTATION.CW]: [[0, 0], [ 1, 0], [-2, 0], [ 1, 2], [-2, -1]], [ROTATION.CCW]: [[0, 0], [-2, 0], [ 1, 0], [-2, 1], [ 1, -2]] }, +] + +class J extends Tetromino {} +J.prototype.minoesCoord = [ + [[-1, -1], [-1, 0], [0, 0], [1, 0]], + [[ 0, -1], [1, -1], [0, 0], [0, 1]], + [[ 1, 1], [-1, 0], [0, 0], [1, 0]], + [[ 0, -1], [-1, 1], [0, 0], [0, 1]], +] + +class L extends Tetromino {} +L.prototype.minoesCoord = [ + [[-1, 0], [0, 0], [1, 0], [ 1, -1]], + [[0, -1], [0, 0], [0, 1], [ 1, 1]], + [[-1, 0], [0, 0], [1, 0], [-1, 1]], + [[0, -1], [0, 0], [0, 1], [-1, -1]], +] + +class O extends Tetromino {} +O.prototype.minoesCoord = [ + [[0, 0], [1, 0], [0, -1], [1, -1]] +] +O.prototype.srs = [ + {[ROTATION.CW]: [], [ROTATION.CCW]: []} +] + + +class S extends Tetromino {} +S.prototype.minoesCoord = [ + [[-1, 0], [0, 0], [0, -1], [1, -1]], + [[ 0, -1], [0, 0], [1, 0], [1, 1]], + [[-1, 1], [0, 0], [1, 0], [0, 1]], + [[-1, -1], [0, 0], [-1, 0], [0, 1]], +] + +class T extends Tetromino {} +T.prototype.minoesCoord = [ + [[-1, 0], [0, 0], [1, 0], [0, -1]], + [[0, -1], [0, 0], [1, 0], [0, 1]], + [[-1, 0], [0, 0], [1, 0], [0, 1]], + [[0, -1], [0, 0], [0, 1], [-1, 0]], +] +T.prototype.tSlots = [ + [[-1, -1], [ 1, -1], [ 1, 1], [-1, 1]], + [[ 1, -1], [ 1, 1], [-1, 1], [-1, -1]], + [[ 1, 1], [-1, 1], [-1, -1], [ 1, -1]], + [[-1, 1], [-1, -1], [ 1, -1], [ 1, 1]], +] + +class Z extends Tetromino {} +Z.prototype.minoesCoord = [ + [[-1, -1], [0, -1], [0, 0], [ 1, 0]], + [[ 1, -1], [1, 0], [0, 0], [ 0, 1]], + [[-1, 0], [0, 0], [0, 1], [ 1, 1]], + [[ 0, -1], [-1, 0], [0, 0], [-1, 1]] +] + + +class Settings { + constructor() { + for (let input of settingsForm.getElementsByTagName("input")) { + if (localStorage[input.name]) input.value = localStorage[input.name] + } + arrOutput.value = arrInput.value + " ms" + dasOutput.value = dasInput.value + " ms" + + settingsForm.onsubmit = newGame + this.modal = new bootstrap.Modal('#settingsModal') + document.getElementById('settingsModal').addEventListener('shown.bs.modal', () => { + resumeButton.focus() + }) + } + + load() { + for (let input of keyBindFielset.getElementsByTagName("input")) { + this[input.name] = KEY_NAMES[input.value] || input.value + localStorage[input.name] = input.value + } + for (let input of autorepearFieldset.getElementsByTagName("input")) { + this[input.name] = input.valueAsNumber + localStorage[input.name] = input.value + } + + this.keyBind = {} + for (let actionName in playerActions) { + this.keyBind[settings[actionName]] = playerActions[actionName] + } + } +} + +function changeKey(input) { + prevValue = input.value + input.value = "Touche ?" + input.onkeydown = function (event) { + event.preventDefault() + input.value = KEY_NAMES[event.key] || event.key + } + input.onblur = function (event) { + if (input.value == "Touche ?") input.value = prevValue + input.onkeydown = null + input.onblur = null + } +} + + +class Stats { + constructor() { + this.highScore = Number(localStorage["highScore"]) || 0 + } + + set score(score) { + this._score = score + scoreTd.innerText = score.toLocaleString() + if (score > this.highScore) { + this.highScore = score + } + } + + get score() { + return this._score + } + + set highScore(highScore) { + this._highScore = highScore + highScoreTd.innerText = highScore.toLocaleString() + } + + get highScore() { + return this._highScore + } + + set level(level) { + this._level = level + this.goal += level * 5 + if (level <= 20){ + this.fallPeriod = 1000 * Math.pow(0.8 - ((level - 1) * 0.007), level - 1) + } + if (level > 15) + this.lockDelay = 500 * Math.pow(0.9, level - 15) + levelInput.value = level + levelTd.innerText = level + levelSpan.innerHTML = `

LEVEL
${this.level}

` + levelSpan.classList.add("show-level-animation") + } + + get level() { + return this._level + } + + set goal(goal) { + this._goal = goal + goalTd.innerText = goal + } + + get goal() { + return this._goal + } +} + + +/* Game */ +onanimationend = function (event) { + event.target.classList.remove(event.animationName) +} + + +let scheduler = new Scheduler() +let settings = new Settings() +let stats = new Stats() +let holdQueue = new MinoesTable("holdTable") +let matrix = new PlayfieldMatrix("matrixTable") +let nextQueue = new NextQueue("nextTable") + + +function pause() { + document.onkeydown = null + document.onkeyup = null + + scheduler.clearInterval(fall) + scheduler.clearTimeout(lockDown) + scheduler.clearTimeout(repeat) + scheduler.clearInterval(autorepeat) + resumeButton.disabled = false + settings.modal.show() +} + +//window.onblur = pause() + +pause() + +function newGame(event) { + stats.lockDelay = DELAY.LOCK + resume(event) + levelInput.name = "level" + levelInput.disabled = true + resumeButton.innerHTML = "Reprendre" + event.target.onsubmit = resume + settingsModal["data-bs-backdrop"] = "" + stats.score = 0 + stats.goal = 0 + stats.level = levelInput.valueAsNumber + localStorage["startLevel"] = levelInput.value + generate() +} + +function resume(event) { + event.preventDefault() + + settings.load() + + document.onkeydown = onkeydown + document.onkeyup = onkeyup + + if (stats.fallPeriod) scheduler.setInterval(fall, stats.fallPeriod) +} + +function generate(piece=nextQueue.shift()) { + matrix.piece = piece + + if (matrix.piece.canMove(TRANSLATION.NONE)) { + scheduler.setInterval(fall, stats.fallPeriod) + } else { + gameOver() + } +} + +let playerActions = { + moveLeft: () => matrix.piece.move(TRANSLATION.LEFT), + + moveRight: () => matrix.piece.move(TRANSLATION.RIGHT), + + rotateClockwise: () => matrix.piece.rotate(ROTATION.CW), + + rotateCounterclockwise: () => matrix.piece.rotate(ROTATION.CCW), + + softDrop: function() { + if (matrix.piece.move(TRANSLATION.DOWN)) stats.score++ + }, + + hardDrop: function() { + scheduler.clearTimeout(lockDown) + //matrix.table.classList.add("hard-dropped-table-animation") + while (matrix.piece.move(TRANSLATION.DOWN, ROTATION.NONE, "hard-drop-animation")) stats.score +=2 + lockDown() + }, + + hold: function() { + if (matrix.piece.holdEnabled) { + scheduler.clearInterval(fall) + scheduler.clearTimeout(lockDown) + + matrix.piece.holdEnabled = false + matrix.piece.locked = false + matrix.piece.orientation = ORIENTATION.NORTH + if (holdQueue.piece) { + let piece = holdQueue.piece + holdQueue.piece = matrix.piece + generate(piece) + } else { + holdQueue.piece = matrix.piece + generate() + } + } + }, + + pause: pause, +} + +// Handle player inputs +const REPEATABLE_ACTIONS = [ + playerActions.moveLeft, + playerActions.moveRight, + playerActions.softDrop +] +pressedKeys = new Set() +actionsQueue = [] +function onkeydown(event) { + if (event.key in settings.keyBind) { + event.preventDefault() + if (!pressedKeys.has(event.key)) { + pressedKeys.add(event.key) + action = settings.keyBind[event.key] + action() + if (REPEATABLE_ACTIONS.includes(action)) { + actionsQueue.unshift(action) + scheduler.clearTimeout(repeat) + scheduler.clearInterval(autorepeat) + scheduler.setTimeout(repeat, settings.das) + } + } + } +} + +function repeat() { + if (actionsQueue.length) { + actionsQueue[0]() + scheduler.setInterval(autorepeat, settings.arr) + } +} + +function autorepeat() { + if (actionsQueue.length) { + actionsQueue[0]() + } else { + scheduler.clearInterval(autorepeat) + } +} + +function onkeyup(event) { + if (event.key in settings.keyBind) { + event.preventDefault() + pressedKeys.delete(event.key) + action = settings.keyBind[event.key] + if (actionsQueue.includes(action)) { + actionsQueue.splice(actionsQueue.indexOf(action), 1) + if (!actionsQueue.length) { + scheduler.clearTimeout(repeat) + scheduler.clearInterval(autorepeat) + } + } + } +} + +function fall() { + matrix.piece.move(TRANSLATION.DOWN) +} + +function lockDown() { + scheduler.clearTimeout(lockDown) + scheduler.clearInterval(fall) + + lockedMinoesCoord = matrix.piece.minoesCoord[matrix.piece.orientation] + .translate(matrix.piece.center) + if (lockedMinoesCoord.every(minoCoord => minoCoord.y < 4)) { + gameOver() + } else { + lockedMinoesCoord.forEach(minoCoord => { + matrix.lockedMinoes[minoCoord.y][minoCoord.x] = matrix.piece.className + matrix.drawMino(minoCoord, matrix.piece.className) + }) + + // T-spin + let tSpin = T_SPIN.NONE + if (matrix.piece.lastRotation && matrix.piece.constructor == T) { + let [a, b, c, d] = matrix.piece.tSlots[matrix.piece.orientation] + .translate(matrix.piece.center) + .map(minoCoord => !matrix.cellIsEmpty(minoCoord)) + if (a && b && (c || d)) + tSpin = T_SPIN.T_SPIN + else if (c && d && (a || b)) + tSpin = matrix.piece.rotationPoint5Used ? T_SPIN.T_SPIN : T_SPIN.MINI + } + + // Cleared lines + let clearedLines = Array.from(new Set(lockedMinoesCoord.map(minoCoord => minoCoord.y))) + .filter(y => matrix.lockedMinoes[y].filter(lockedMino => lockedMino).length == matrix.columns) + for (y of clearedLines) { + matrix.lockedMinoes.splice(y, 1) + matrix.lockedMinoes.unshift(Array(matrix.columns)) + matrix.table.rows[y].classList.add("line-cleared-animation") + } + let nbClearedLines = clearedLines.length + if (nbClearedLines || tSpin) { + matrix.redraw() + stats.goal -= nbClearedLines + stats.score += SCORES[tSpin][nbClearedLines] + messagesSpan.innerHTML = "" + if (tSpin) messagesSpan.innerHTML += `
${tSpin}
\n` + if (nbClearedLines) messagesSpan.innerHTML += `
${CLEARED_LINES_NAMES[nbClearedLines]}
\n` + messagesSpan.innerHTML += `
${SCORES[tSpin][nbClearedLines]}
\n` + } + + if (stats.goal <= 0) { + stats.level++ + } + + generate() + } +} + +function gameOver() { + console.log("GAME OVER") + matrix.piece.locked = false + matrix.drawPiece() + document.onkeydown = null + document.onkeyup = null + localStorage["highScore"] = stats.highScore + levelSpan.innerHTML = "

GAME
OVER

" + levelSpan.classList.add("show-level-animation") +} + +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('service-worker.js'); +} \ No newline at end of file diff --git a/css/99.css b/css/99.css new file mode 100644 index 0000000..659ccb8 --- /dev/null +++ b/css/99.css @@ -0,0 +1,91 @@ +.mino { + background: radial-gradient( + ellipse 140% 66% at 122% 88%, + var(--background-color) 100%, + var(--frontier-color) 105%, + var(--light-color) 130% + ); + border: 4px solid; + padding: 0; + opacity: 100%; + border-radius: 2px; +} + +.I.mino { + --background-color: #00d6fb; + --frontier-color: #43e7fd; + --light-color: #afeff9; + border-top-color: #7cf2fd; + border-left-color: #2ed5e5; + border-right-color: #00b8ca; + border-bottom-color: #00a4b0; +} + +.J.mino { + --background-color: #2d00fa; + --frontier-color: #7054fb; + --light-color: #b8b4ff; + border-top-color: #4985fd; + border-left-color: #2f36ea; + border-right-color: #0006ca; + border-bottom-color: #00009d; +} + +.L.mino { + --background-color: #ff6c13; + --frontier-color: #fe9551; + --light-color: #fdd0b7; + border-top-color: #fd9f6b; + border-left-color: #e76d28; + border-right-color: #e74f00; + border-bottom-color: #c54800; +} + +.O.mino { + --background-color: #ffce12; + --frontier-color: #fce15c; + --light-color: #ffedac;; + border-top-color: #ffe364; + border-left-color: #e7ba23; + border-right-color: #e3a707; + border-bottom-color: #ca9501; +} + +.T.mino { + --background-color: #ad00fa; + --frontier-color: #c541fc; + --light-color: #edb2ff; + border-top-color: #d380ff; + border-left-color: #b42deb; + border-right-color: #8000cd; + border-bottom-color: #6e019a; +} + +.S.mino { + --background-color: #6e1; + --frontier-color: #93f85a; + --light-color: #93f85a; + border-top-color: #a4fc6d; + border-left-color: #5ee82b; + border-right-color: #35db00; + border-bottom-color: #1cbc02; +} + +.Z.mino { + --background-color: #ff1945; + --frontier-color: #fe6483; + --light-color: #ffb8c5; + border-top-color: #fd718d; + border-left-color: #e62250; + border-right-color: #e20332; + border-bottom-color: #ad1936; +} + +.locked.mino { + filter: saturate(50%) brightness(200%) +} + +.ghost.mino { + opacity: 20%; + filter: brightness(200%) +} \ No newline at end of file diff --git a/css/common.css b/css/common.css new file mode 100644 index 0000000..8466402 --- /dev/null +++ b/css/common.css @@ -0,0 +1,196 @@ +:root { + --cell-side: 20px; +} + +.modal-content { + background-color: rgba(33, 37, 41, 30%); + backdrop-filter: blur(15px); +} + +.card { + background-color: rgb(37, 41, 45); +} + +.minoes-table { + table-layout: fixed; + border-collapse: separate; + border-spacing: 0; + height: calc(var(--rows) * var(--cell-side)); + width: calc(var(--columns) * var(--cell-side)); +} + +#matrixTable { + margin-top: calc(-1 * var(--no-bordered-rows) * var(--cell-side)); + background-color: transparent; +} + +@keyframes hard-dropped-table-animation { + from { + transform: translateY(0); + } + 25% { + transform: translateY(5px); + } + to { + transform: translateY(0); + } +} + +#matrixTable.hard-dropped-table-animation { + animation: hard-dropped-table-animation .2s; +} + +tr.no-border td:not(.mino) { + border-width: 0; + padding: 4px; +} + +tr.border td:not(.mino) { + border: 1px solid #333; + padding: 3px; +} + +td { + overflow: hidden; + width: calc(var(--cell-side) + 4px); + height: calc(var(--cell-side) + 4px); +} + +@keyframes hard-drop-animation { + from { + background-color: rgb(206, 255, 255, 40%); + filter: saturate(50%) brightness(300%); + } + to { + background-color: transparent; + } +} + +td.hard-drop-animation { + animation: hard-drop-animation ease-out .3s; +} + +@keyframes line-cleared-animation { + from { + background-color: rgb(206, 255, 255, 40%); + filter: saturate(50%) brightness(300%); + box-shadow: -60px 0 5px white, 60px 0 5px white; + } + to { + background-color: transparent; + } +} + +tr.line-cleared-animation{ + animation: line-cleared-animation ease-out .3s; +} + +span { + position: absolute; + top: 35%; + left: 50%; + transform: translate(-50%, -50%); + color: rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px rgba(0, 0, 0, 0.8); + font-size: 3vmin; + text-align: center; +} + +#levelSpan { + font-size: 4vmin; + font-weight: bold; + opacity: 0; +} + +@keyframes show-level-animation { + from { + opacity: 0; + top: 70%; + } + 50% { + opacity: 100%; + top: 40% + } + to { + opacity: 0; + top: 20% + } +} + +#levelSpan.show-level-animation { + animation: cubic-bezier(0.4, 0, 0.6, 1) 2s show-level-animation; +} + +@keyframes zoom-in-animation { + from { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + 30% { + opacity: 1; + transform: scale3d(1, 1, 1); + } + 90% { + opacity: 1; + transform: scale3d(1, 1, 1); + } + to { + opacity: 0; + transform: scale3d(1.5, 1, 1); + } +} + +@keyframes rotate-in-animation { + 0% { + opacity:0; + transform:rotate(200deg); + } + 30% { + opacity:1; + transform:translateZ(0); + transform: scale3d(1, 1, 1); + } + 90% { + opacity: 1; + transform: scale3d(1, 1, 1); + } + to { + opacity: 0; + transform: scale3d(1.5, 1, 1); + } +} + +#messagesSpan div { + opacity: 0; +} + +#messagesSpan div.rotate-in-animation { + animation-name: rotate-in-animation; + animation-timing-function: cubic-bezier(.25,.46,.45,.94); + animation-duration: 1s; +} + +#messagesSpan div.zoom-in-animation { + animation-name: zoom-in-animation; + animation-timing-function: cubic-bezier(.25,.46,.45,.94); + transform-origin:center; +} +#messagesSpan div.zoom-in-animation:first-child { + animation-duration: 1s; +} +#messagesSpan div.zoom-in-animation:nth-child(2) { + animation-delay: .2s; + animation-duration: .9s; +} +#messagesSpan div.zoom-in-animation:nth-child(3) { + animation-delay: .4s; + animation-duration: .8s; +} +#messagesSpan div.zoom-in-animation:nth-child(4) { + animation-delay: .6s; + animation-duration: .7s; +} +#messagesSpan div.zoom-in-animation:nth-child(5) { + animation-delay: .8s; + animation-duration: .6s; +} diff --git a/css/effect.css b/css/effect.css new file mode 100644 index 0000000..d1da7bc --- /dev/null +++ b/css/effect.css @@ -0,0 +1,16 @@ +.mino { + background: rgba(127, 229, 255, 0.3); + border: 1px solid rgba(127, 229, 255, 0.7); + border-radius: 0.3vmin; +} + +.ghost.mino { + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.4); + border-radius: 0.3vmin; +} + +.locked.mino { + background: rgba(242, 255, 255, 0.5); + border-color: rgba(242, 255, 255, 0.7); +} \ No newline at end of file diff --git a/favicons/android-chrome-192x192.png b/favicons/android-chrome-192x192.png new file mode 100644 index 0000000..1d75c9d Binary files /dev/null and b/favicons/android-chrome-192x192.png differ diff --git a/favicons/android-chrome-512x512.png b/favicons/android-chrome-512x512.png new file mode 100644 index 0000000..e23f148 Binary files /dev/null and b/favicons/android-chrome-512x512.png differ diff --git a/favicons/apple-touch-icon.png b/favicons/apple-touch-icon.png new file mode 100644 index 0000000..0253beb Binary files /dev/null and b/favicons/apple-touch-icon.png differ diff --git a/favicons/favicon-16x16.png b/favicons/favicon-16x16.png new file mode 100644 index 0000000..497749b Binary files /dev/null and b/favicons/favicon-16x16.png differ diff --git a/favicons/favicon-32x32.png b/favicons/favicon-32x32.png new file mode 100644 index 0000000..73a8da5 Binary files /dev/null and b/favicons/favicon-32x32.png differ diff --git a/favicons/favicon.ico b/favicons/favicon.ico new file mode 100644 index 0000000..ff44603 Binary files /dev/null and b/favicons/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..761b8a2 --- /dev/null +++ b/index.html @@ -0,0 +1,197 @@ + + + + QUATRIS + + + + + + + + + + + + + + +
+
+
+
+
HOLD
+
+ + + + + + +
+
+
+
+ + + + + +
Score0
Meilleur score0
Niveau0
But0
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
NEXT
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + + + + + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..072621d --- /dev/null +++ b/manifest.json @@ -0,0 +1,43 @@ +{ + "short_name": "TETRIS", + "name": "Tetris", + "description": "Falling blocks", + "icons": [ + { + "src": "favicons/favicon-16x16.png", + "type": "image/png", + "sizes": "16x16" + }, + { + "src": "favicons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "favicons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "start_url": "index.html", + "background_color": "#212529", + "display": "standalone", + "scope": ".", + "theme_color": "#212529", + "shortcuts": [ + { + "name": "TETRIS", + "short_name": "Tetris", + "description": "Falling blocks", + "url": "index.html", + "icons": [ + { + "src": "favicon.png", + "type": "image/png", + "sizes": "16x16" + } + ] + } + ] + } + \ No newline at end of file diff --git a/service-worker.js b/service-worker.js new file mode 100644 index 0000000..b3e69ed --- /dev/null +++ b/service-worker.js @@ -0,0 +1,86 @@ +/* +Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Incrementing OFFLINE_VERSION will kick off the install event and force +// previously cached resources to be updated from the network. +const OFFLINE_VERSION = 1; +const CACHE_NAME = "offline"; +// Customize this with a different URL if needed. +const OFFLINE_URL = "index.html"; + +self.addEventListener("install", (event) => { + event.waitUntil( + (async () => { + const cache = await caches.open(CACHE_NAME); + // Setting {cache: 'reload'} in the new request will ensure that the + // response isn't fulfilled from the HTTP cache; i.e., it will be from + // the network. + await cache.add(new Request(OFFLINE_URL, { cache: "reload" })); + })() + ); + // Force the waiting service worker to become the active service worker. + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + (async () => { + // Enable navigation preload if it's supported. + // See https://developers.google.com/web/updates/2017/02/navigation-preload + if ("navigationPreload" in self.registration) { + await self.registration.navigationPreload.enable(); + } + })() + ); + + // Tell the active service worker to take control of the page immediately. + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + // We only want to call event.respondWith() if this is a navigation request + // for an HTML page. + if (event.request.mode === "navigate") { + event.respondWith( + (async () => { + try { + // First, try to use the navigation preload response if it's supported. + const preloadResponse = await event.preloadResponse; + if (preloadResponse) { + return preloadResponse; + } + + // Always try the network first. + const networkResponse = await fetch(event.request); + return networkResponse; + } catch (error) { + // catch is only triggered if an exception is thrown, which is likely + // due to a network error. + // If fetch() returns a valid HTTP response with a response code in + // the 4xx or 5xx range, the catch() will NOT be called. + console.log("Fetch failed; returning offline page instead.", error); + + const cache = await caches.open(CACHE_NAME); + const cachedResponse = await cache.match(OFFLINE_URL); + return cachedResponse; + } + })() + ); + } + + // If our if() condition is false, then this fetch handler won't intercept the + // request. If there are any other fetch handlers registered, they will get a + // chance to call event.respondWith(). If no fetch handlers call + // event.respondWith(), the request will be handled by the browser as if there + // were no service worker involvement. +}); diff --git a/thumbnail.png b/thumbnail.png new file mode 100644 index 0000000..762624e Binary files /dev/null and b/thumbnail.png differ