Created
September 11, 2026 02:35
-
-
Save kumagi/794c380d4f8819217c3dd9d01e3d266c to your computer and use it in GitHub Desktop.
deepseek-v4.1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="ja"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> | |
| <meta name="color-scheme" content="dark"> | |
| <title>派手エフェクト・テトリス</title> | |
| <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect x='1' y='1' width='6' height='6' fill='%2300e5ff'/><rect x='9' y='1' width='6' height='6' fill='%23ffd166'/><rect x='1' y='9' width='6' height='6' fill='%23b64dff'/><rect x='9' y='9' width='6' height='6' fill='%23ff3d5a'/></svg>"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=DotGothic16&family=Orbitron:wght@500;700;900&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/dist/confetti.browser.min.js" defer></script> | |
| <style> | |
| :root{ --accent:#00e5ff; } | |
| *{ margin:0; padding:0; box-sizing:border-box; } | |
| html,body{ height:100%; overflow:hidden; background:#04050c; color:#eef2ff; | |
| font-family:'DotGothic16','Hiragino Kaku Gothic ProN',monospace; | |
| user-select:none; -webkit-user-select:none; touch-action:none; overscroll-behavior:none; } | |
| #stage{ position:fixed; inset:0; display:flex; align-items:center; justify-content:center; z-index:10; } | |
| #game{ display:block; image-rendering:auto; border-radius:14px; | |
| box-shadow:0 0 60px rgba(0,229,255,.18), 0 0 140px rgba(120,0,255,.12); } | |
| #hint{ position:fixed; left:0; right:0; bottom:8px; text-align:center; z-index:20; | |
| font-size:12px; letter-spacing:.08em; opacity:.45; pointer-events:none; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="stage"><canvas id="game"></canvas></div> | |
| <div id="hint">← → 移動 / ↓ ソフトドロップ / ↑ X 回転 / Z CCW / Q 180 / Space ハードドロップ / C Shift ホールド / P ポーズ / M ミュート / Enter 開始</div> | |
| <script> | |
| (function(){ | |
| 'use strict'; | |
| var COLS = 10, ROWS = 20, CELL = 32; | |
| var W = 760, H = 720; | |
| var BX = 220, BY = 60, BW = COLS * CELL, BH = ROWS * CELL; | |
| var canvas = document.getElementById('game'); | |
| var ctx = canvas.getContext('2d'); | |
| var COLORS = { | |
| I:{base:'#18f2f2', light:'#c8ffff', glow:'#00ffff'}, | |
| J:{base:'#2f5bff', light:'#b9c9ff', glow:'#4d7bff'}, | |
| L:{base:'#ff9d1f', light:'#ffe0ad', glow:'#ffb84d'}, | |
| O:{base:'#ffd91f', light:'#fff4ad', glow:'#ffe34d'}, | |
| S:{base:'#2fe85a', light:'#c3ffcf', glow:'#4dff77'}, | |
| T:{base:'#b43dff', light:'#e6bfff', glow:'#d06bff'}, | |
| Z:{base:'#ff2f4d', light:'#ffbdc6', glow:'#ff5570'} | |
| }; | |
| var TYPES = ['I','J','L','O','S','T','Z']; | |
| function rotCW(m){ | |
| var n = m.length, r = [], y, x; | |
| for(y=0;y<n;y++){ r.push(new Array(n).fill(0)); } | |
| for(y=0;y<n;y++){ for(x=0;x<n;x++){ r[x][n-1-y] = m[y][x]; } } | |
| return r; | |
| } | |
| function rotCCW(m){ | |
| var n = m.length, r = [], y, x; | |
| for(y=0;y<n;y++){ r.push(new Array(n).fill(0)); } | |
| for(y=0;y<n;y++){ for(x=0;x<n;x++){ r[n-1-x][y] = m[y][x]; } } | |
| return r; | |
| } | |
| function buildStates(base){ | |
| var s = [base, rotCW(base), rotCW(rotCW(base)), rotCCW(base)]; | |
| return s; | |
| } | |
| var SHAPES = { | |
| I:[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], | |
| J:[[1,0,0],[1,1,1],[0,0,0]], | |
| L:[[0,0,1],[1,1,1],[0,0,0]], | |
| O:[[1,1],[1,1]], | |
| S:[[0,1,1],[1,1,0],[0,0,0]], | |
| T:[[0,1,0],[1,1,1],[0,0,0]], | |
| Z:[[1,1,0],[0,1,1],[0,0,0]] | |
| }; | |
| var PIECES = {}; | |
| TYPES.forEach(function(t){ PIECES[t] = { states: buildStates(SHAPES[t]) }; }); | |
| var KICKS_JLSTZ = { | |
| '0>1':[[0,0],[-1,0],[-1,-1],[0,2],[-1,2]], | |
| '1>0':[[0,0],[1,0],[1,1],[0,-2],[1,-2]], | |
| '1>2':[[0,0],[1,0],[1,1],[0,-2],[1,-2]], | |
| '2>1':[[0,0],[-1,0],[-1,-1],[0,2],[-1,2]], | |
| '2>3':[[0,0],[1,0],[1,-1],[0,2],[1,2]], | |
| '3>2':[[0,0],[-1,0],[-1,1],[0,-2],[-1,-2]], | |
| '3>0':[[0,0],[-1,0],[-1,1],[0,-2],[-1,-2]], | |
| '0>3':[[0,0],[1,0],[1,-1],[0,2],[1,2]] | |
| }; | |
| var KICKS_I = { | |
| '0>1':[[0,0],[-2,0],[1,0],[-2,1],[1,-2]], | |
| '1>0':[[0,0],[2,0],[-1,0],[2,-1],[-1,2]], | |
| '1>2':[[0,0],[-1,0],[2,0],[-1,-2],[2,1]], | |
| '2>1':[[0,0],[1,0],[-2,0],[1,2],[-2,-1]], | |
| '2>3':[[0,0],[2,0],[-1,0],[2,-1],[-1,2]], | |
| '3>2':[[0,0],[-2,0],[1,0],[-2,1],[1,-2]], | |
| '3>0':[[0,0],[1,0],[-2,0],[1,2],[-2,-1]], | |
| '0>3':[[0,0],[-1,0],[2,0],[-1,-2],[2,1]] | |
| }; | |
| var THEMES = [ | |
| { name:'NEON BURST', ambient:'#00a2ff', accent:'#00ffff' }, | |
| { name:'SUPERNOVA', ambient:'#ff7a1a', accent:'#ffe34d' }, | |
| { name:'SHOCKWAVE', ambient:'#0a86ff', accent:'#eaf6ff' }, | |
| { name:'THUNDER', ambient:'#8a4dff', accent:'#fff55a' }, | |
| { name:'RAINBOW', ambient:'#ff2fd0', accent:'#ffffff' }, | |
| { name:'MATRIX', ambient:'#00ff66', accent:'#b6ffce' }, | |
| { name:'BLACK HOLE', ambient:'#7a2bff', accent:'#d7b3ff' }, | |
| { name:'FIREWORKS', ambient:'#ff3d2e', accent:'#ffd166' }, | |
| { name:'GLITCH', ambient:'#ff00c8', accent:'#00fff0' }, | |
| { name:'OMEGA', ambient:'#ffffff', accent:'#c08bff' } | |
| ]; | |
| function themeIndex(){ return Math.floor(state.linesTotal / 10) % THEMES.length; } | |
| function currentTheme(){ return THEMES[themeIndex()]; } | |
| var state = { | |
| status:'ready', | |
| board:null, | |
| piece:null, | |
| hold:null, | |
| canHold:true, | |
| queue:[], | |
| bag:[], | |
| linesTotal:0, | |
| linesLevel:0, | |
| level:1, | |
| score:0, | |
| combo:0, | |
| b2b:false, | |
| startTime:0, | |
| elapsed:0, | |
| gravity:0, | |
| gravAcc:0, | |
| softDrop:false, | |
| lockTimer:0, | |
| lockResets:0, | |
| grounded:false, | |
| lastRotation:false, | |
| lastKick:0, | |
| clearingRows:[], | |
| clearTimer:0, | |
| clearDuration:0.34, | |
| flash:0, | |
| shake:0, | |
| muted:false, | |
| themeShown:-1, | |
| introFreeze:0 | |
| }; | |
| var particles = []; | |
| var rings = []; | |
| var bolts = []; | |
| var floats = []; | |
| var stars = []; | |
| var vortex = null; | |
| var glitchTimer = 0; | |
| var rgbSplit = 0; | |
| function resetState(){ | |
| var r, c; | |
| state.board = []; | |
| for(r=0;r<ROWS;r++){ state.board.push(new Array(COLS).fill(null)); } | |
| state.piece = null; | |
| state.hold = null; | |
| state.canHold = true; | |
| state.queue = []; | |
| state.bag = []; | |
| state.linesTotal = 0; | |
| state.linesLevel = 0; | |
| state.level = 1; | |
| state.score = 0; | |
| state.combo = 0; | |
| state.b2b = false; | |
| state.elapsed = 0; | |
| state.startTime = performance.now(); | |
| state.gravity = gravityFor(1); | |
| state.gravAcc = 0; | |
| state.softDrop = false; | |
| state.lockTimer = 0; | |
| state.lockResets = 0; | |
| state.grounded = false; | |
| state.lastRotation = false; | |
| state.lastKick = 0; | |
| state.clearingRows = []; | |
| state.clearTimer = 0; | |
| state.flash = 0; | |
| state.shake = 0; | |
| state.themeShown = -1; | |
| state.introFreeze = 0; | |
| particles.length = 0; | |
| rings.length = 0; | |
| bolts.length = 0; | |
| floats.length = 0; | |
| vortex = null; | |
| refill(); | |
| spawnNext(); | |
| } | |
| function gravityFor(level){ | |
| var t = Math.pow(0.8 - ((level - 1) * 0.007), level - 1); | |
| if(t < 0.0001) t = 0.0001; | |
| return Math.min(1 / t, 20 * 60); | |
| } | |
| function makeBag(){ | |
| var b = TYPES.slice(); | |
| var i, j, t; | |
| for(i=b.length-1;i>0;i--){ j = Math.floor(Math.random()*(i+1)); t=b[i]; b[i]=b[j]; b[j]=t; } | |
| return b; | |
| } | |
| function refill(){ | |
| while(state.queue.length < 6){ | |
| if(state.bag.length === 0) state.bag = makeBag(); | |
| state.queue.push(state.bag.shift()); | |
| } | |
| } | |
| function nextType(){ | |
| refill(); | |
| var t = state.queue.shift(); | |
| refill(); | |
| return t; | |
| } | |
| function collides(matrix, px, py){ | |
| var size = matrix.length, y, x, bx, by; | |
| for(y=0;y<size;y++){ | |
| for(x=0;x<size;x++){ | |
| if(!matrix[y][x]) continue; | |
| bx = px + x; by = py + y; | |
| if(bx < 0 || bx >= COLS) return true; | |
| if(by >= ROWS) return true; | |
| if(by < 0) continue; | |
| if(state.board[by][bx]) return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function spawnPiece(type){ | |
| var m = PIECES[type].states[0]; | |
| var size = m.length, y, x, minY = size; | |
| for(y=0;y<size;y++){ for(x=0;x<size;x++){ if(m[y][x] && y<minY) minY = y; } } | |
| var px = Math.floor((COLS - size) / 2); | |
| var py = -minY; | |
| state.piece = { type:type, matrix:m, rot:0, x:px, y:py }; | |
| state.lastRotation = false; | |
| state.lockTimer = 0; | |
| state.lockResets = 0; | |
| state.grounded = false; | |
| state.gravAcc = 0; | |
| state.canHold = true; | |
| if(collides(m, px, py)){ | |
| gameOver(); | |
| } | |
| } | |
| function spawnNext(){ | |
| spawnPiece(nextType()); | |
| } | |
| function isOccupiedSpin(cx, cy){ | |
| if(cx < 0 || cx >= COLS) return true; | |
| if(cy >= ROWS) return true; | |
| if(cy < 0) return false; | |
| return !!state.board[cy][cx]; | |
| } | |
| function detectTSpin(){ | |
| if(!state.piece || state.piece.type !== 'T' || !state.lastRotation) return null; | |
| var p = state.piece, rot = p.rot; | |
| var c = [ isOccupiedSpin(p.x, p.y), isOccupiedSpin(p.x+2, p.y), isOccupiedSpin(p.x, p.y+2), isOccupiedSpin(p.x+2, p.y+2) ]; | |
| var count = 0, i; | |
| for(i=0;i<4;i++){ if(c[i]) count++; } | |
| if(count < 3) return null; | |
| var frontA, frontB; | |
| if(rot === 0){ frontA = 0; frontB = 1; } | |
| else if(rot === 1){ frontA = 1; frontB = 3; } | |
| else if(rot === 2){ frontA = 2; frontB = 3; } | |
| else { frontA = 0; frontB = 2; } | |
| if(c[frontA] && c[frontB]) return 'full'; | |
| if(state.lastKick === 4) return 'full'; | |
| return 'mini'; | |
| } | |
| function hardDropDistance(){ | |
| var d = 0, p = state.piece; | |
| while(!collides(p.matrix, p.x, p.y + 1)){ p.y++; d++; } | |
| return d; | |
| } | |
| function tryMove(dx){ | |
| var p = state.piece; | |
| if(!p) return false; | |
| if(collides(p.matrix, p.x + dx, p.y)) return false; | |
| p.x += dx; | |
| state.lastRotation = false; | |
| resetLockOnAction(); | |
| return true; | |
| } | |
| function softStep(){ | |
| var p = state.piece; | |
| if(!p) return false; | |
| if(collides(p.matrix, p.x, p.y + 1)) return false; | |
| p.y += 1; | |
| state.lastRotation = false; | |
| return true; | |
| } | |
| function resetLockOnAction(){ | |
| var p = state.piece; | |
| if(!p) return; | |
| var grounded = collides(p.matrix, p.x, p.y + 1); | |
| if(grounded && state.grounded && state.lockResets < 15){ | |
| state.lockTimer = 0; | |
| state.lockResets++; | |
| } | |
| state.grounded = grounded; | |
| } | |
| function tryRotate(dir){ | |
| var p = state.piece; | |
| if(!p) return false; | |
| var from = p.rot, to, m; | |
| if(dir === 2){ m = rotCW(rotCW(p.matrix)); to = (from + 2) % 4; } | |
| else if(dir === 1){ m = rotCW(p.matrix); to = (from + 1) % 4; } | |
| else { m = rotCCW(p.matrix); to = (from + 3) % 4; } | |
| var kicks; | |
| if(p.type === 'O') kicks = [[0,0]]; | |
| else if(dir === 2) kicks = [[0,0],[1,0],[-1,0],[0,-1],[0,1]]; | |
| else kicks = (p.type === 'I' ? KICKS_I : KICKS_JLSTZ)[from + '>' + to] || [[0,0]]; | |
| var i, k; | |
| for(i=0;i<kicks.length;i++){ | |
| k = kicks[i]; | |
| if(!collides(m, p.x + k[0], p.y + k[1])){ | |
| p.matrix = m; p.x += k[0]; p.y += k[1]; p.rot = to; | |
| state.lastRotation = true; | |
| state.lastKick = i; | |
| resetLockOnAction(); | |
| sfx('rotate'); | |
| rotateSparks(); | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| function holdPiece(){ | |
| if(!state.piece || !state.canHold) return; | |
| var cur = state.piece.type; | |
| if(state.hold){ | |
| var h = state.hold; | |
| state.hold = cur; | |
| spawnPiece(h); | |
| } else { | |
| state.hold = cur; | |
| spawnNext(); | |
| } | |
| state.canHold = false; | |
| sfx('hold'); | |
| } | |
| function lockPiece(){ | |
| var p = state.piece; | |
| if(!p) return; | |
| var m = p.matrix, size = m.length, y, x, bx, by; | |
| for(y=0;y<size;y++){ | |
| for(x=0;x<size;x++){ | |
| if(!m[y][x]) continue; | |
| bx = p.x + x; by = p.y + y; | |
| if(by >= 0 && by < ROWS && bx >= 0 && bx < COLS){ | |
| state.board[by][bx] = p.type; | |
| } | |
| } | |
| } | |
| var tspin = detectTSpin(); | |
| var rows = [], r, c, full; | |
| for(r=0;r<ROWS;r++){ | |
| full = true; | |
| for(c=0;c<COLS;c++){ if(!state.board[r][c]){ full = false; break; } } | |
| if(full) rows.push(r); | |
| } | |
| state.piece = null; | |
| if(rows.length){ | |
| state.combo++; | |
| awardClear(rows, tspin); | |
| state.clearingRows = rows; | |
| state.clearTimer = 0; | |
| state.status = 'clearing'; | |
| triggerClearEffects(rows, tspin); | |
| sfxClear(rows.length, tspin); | |
| } else { | |
| if(tspin){ | |
| var pts = (tspin === 'full' ? 400 : 100) * state.level; | |
| state.score += pts; | |
| addFloat(centerX(), BY + 120, tspin === 'full' ? 'T-SPIN' : 'T-SPIN MINI', THEMES[themeIndex()].accent, 22); | |
| sfx('tspin'); | |
| } | |
| state.combo = 0; | |
| sfxLock(); | |
| spawnNext(); | |
| } | |
| } | |
| function awardClear(rows, tspin){ | |
| var n = rows.length, L = state.level; | |
| var pts = 0, label = '', difficult = false; | |
| if(tspin === 'full'){ | |
| if(n === 1){ pts = 800; label = 'T-SPIN SINGLE'; } | |
| else if(n === 2){ pts = 1200; label = 'T-SPIN DOUBLE'; } | |
| else if(n === 3){ pts = 1600; label = 'T-SPIN TRIPLE'; } | |
| else { pts = 400; label = 'T-SPIN'; } | |
| difficult = true; | |
| } else if(tspin === 'mini'){ | |
| if(n === 1){ pts = 200; label = 'T-SPIN MINI SINGLE'; } | |
| else if(n === 2){ pts = 400; label = 'T-SPIN MINI DOUBLE'; } | |
| else { pts = 100; label = 'T-SPIN MINI'; } | |
| if(n > 0) difficult = true; | |
| } else { | |
| if(n === 1){ pts = 100; label = 'SINGLE'; } | |
| else if(n === 2){ pts = 300; label = 'DOUBLE'; } | |
| else if(n === 3){ pts = 500; label = 'TRIPLE'; } | |
| else { pts = 800; label = 'TETRIS'; } | |
| if(n === 4) difficult = true; | |
| } | |
| if(difficult && state.b2b){ pts = Math.floor(pts * 1.5); label = 'B2B ' + label; } | |
| if(n === 4 || tspin) state.b2b = true; | |
| else state.b2b = false; | |
| var comboBonus = 0; | |
| if(state.combo > 1){ comboBonus = 50 * (state.combo - 1) * L; } | |
| var total = pts * L + comboBonus; | |
| state.score += total; | |
| var t = currentTheme(); | |
| addFloat(centerX(), BY + 150, label, t.accent, rows.length >= 4 ? 34 : 26); | |
| if(comboBonus > 0){ addFloat(centerX(), BY + 195, 'COMBO x' + (state.combo - 1), '#ffd166', 22); } | |
| state.flash = Math.min(1, 0.35 + n * 0.16); | |
| state.shake = Math.max(state.shake, 6 + n * 4); | |
| } | |
| function applyClears(){ | |
| var rows = state.clearingRows.slice(); | |
| var rset = {}; | |
| var i; | |
| for(i=0;i<rows.length;i++){ rset[rows[i]] = true; } | |
| var nb = []; | |
| for(i=0;i<ROWS;i++){ if(!rset[i]) nb.push(state.board[i]); } | |
| while(nb.length < ROWS){ nb.unshift(new Array(COLS).fill(null)); } | |
| state.board = nb; | |
| state.linesLevel += rows.length; | |
| state.linesTotal += rows.length; | |
| var newLevel = Math.floor(state.linesTotal / 10) + 1; | |
| if(newLevel > state.level){ | |
| state.level = newLevel; | |
| state.gravity = gravityFor(state.level); | |
| state.flash = Math.max(state.flash, 0.6); | |
| addFloat(centerX(), BY + 250, 'LEVEL ' + state.level, '#ffffff', 34); | |
| sfx('levelup'); | |
| triggerLevelUp(); | |
| } | |
| var empty = true; | |
| outer: | |
| for(i=0;i<ROWS;i++){ | |
| for(var c=0;c<COLS;c++){ if(state.board[i][c]){ empty = false; break outer; } } | |
| } | |
| if(empty){ | |
| var bonus = (rows.length >= 4 ? 2000 : rows.length === 3 ? 1800 : rows.length === 2 ? 1200 : 800) * state.level; | |
| state.score += bonus; | |
| addFloat(centerX(), BY + 300, 'PERFECT CLEAR!', '#ff5cf0', 36); | |
| state.flash = 1; | |
| state.shake = 22; | |
| sfx('perfect'); | |
| triggerPerfect(); | |
| } | |
| state.clearingRows = []; | |
| state.status = 'playing'; | |
| spawnNext(); | |
| var ti = themeIndex(); | |
| if(ti !== state.themeShown){ | |
| state.themeShown = ti; | |
| addFloat(centerX(), BY + 60, THEMES[ti].name, THEMES[ti].accent, 24); | |
| } | |
| } | |
| function gameOver(){ | |
| state.status = 'gameover'; | |
| state.piece = null; | |
| state.flash = 0.8; | |
| state.shake = 26; | |
| sfx('gameover'); | |
| stopMusic(); | |
| } | |
| function startGame(){ | |
| resetState(); | |
| state.status = 'playing'; | |
| state.themeShown = 0; | |
| particles.length = 0; | |
| rings.length = 0; | |
| bolts.length = 0; | |
| floats.length = 0; | |
| ensureAudio(); | |
| playMusic(); | |
| } | |
| function togglePause(){ | |
| if(state.status === 'playing'){ state.status = 'paused'; pauseMusic(); } | |
| else if(state.status === 'paused'){ state.status = 'playing'; resumeMusic(); } | |
| } | |
| function addFloat(x, y, text, color, size){ | |
| floats.push({ x:x, y:y, text:text, color:color, size:size||24, life:0, dur:1.1, vy:-26 }); | |
| } | |
| function centerX(){ return BX + BW / 2; } | |
| function spawnBurst(x, y, colors, count, speed, opt){ | |
| opt = opt || {}; | |
| for(var i=0;i<count;i++){ | |
| var a = Math.random() * Math.PI * 2; | |
| var s = speed * (0.35 + Math.random() * 0.9); | |
| particles.push({ | |
| x:x, y:y, | |
| vx:Math.cos(a) * s + (opt.vx||0), | |
| vy:Math.sin(a) * s + (opt.vy||0), | |
| life:0, dur:(opt.dur||0.7) * (0.6 + Math.random()*0.8), | |
| size:(opt.size||4) * (0.6 + Math.random()*0.9), | |
| color:colors[(Math.random()*colors.length)|0], | |
| grav:(opt.grav==null?420:opt.grav), | |
| drag:(opt.drag==null?1.6:opt.drag), | |
| shape:opt.shape||'spark', | |
| rot:Math.random()*6.28, vr:(Math.random()-0.5)*12 | |
| }); | |
| } | |
| } | |
| function addRing(x, y, color, maxR, width, dur){ | |
| rings.push({ x:x, y:y, r:0, maxR:maxR, color:color, width:width||4, life:0, dur:dur||0.6 }); | |
| } | |
| function addBolt(x1, y1, x2, y2, color, segs, jitter){ | |
| segs = segs || 10; | |
| var pts = [], i, t, x, y; | |
| for(i=0;i<=segs;i++){ | |
| t = i / segs; | |
| x = x1 + (x2 - x1) * t + (i===0||i===segs?0:(Math.random()-0.5)*jitter); | |
| y = y1 + (y2 - y1) * t + (i===0||i===segs?0:(Math.random()-0.5)*jitter); | |
| pts.push({x:x, y:y}); | |
| } | |
| bolts.push({ pts:pts, color:color, life:0, dur:0.32 }); | |
| } | |
| function rotateSparks(){ | |
| var p = state.piece; | |
| if(!p) return; | |
| var cx = BX + (p.x + 1.5) * CELL; | |
| var cy = BY + (p.y + 1.5) * CELL; | |
| spawnBurst(cx, cy, [COLORS[p.type].glow, COLORS[p.type].light, '#ffffff'], 5, 70, { dur:0.35, size:2.4, grav:60 }); | |
| } | |
| function triggerClearEffects(rows, tspin){ | |
| var t = currentTheme(); | |
| var ti = themeIndex(); | |
| var row, col, x, y, color, colors = []; | |
| for(var ri=0;ri<rows.length;ri++){ | |
| row = rows[ri]; | |
| for(col=0;col<COLS;col++){ | |
| color = COLORS[state.board[row][col]] ? COLORS[state.board[row][col]].glow : '#ffffff'; | |
| colors.push(color); | |
| x = BX + col * CELL + CELL/2; | |
| y = BY + row * CELL + CELL/2; | |
| spawnBurst(x, y, [color, '#ffffff'], ti === 0 ? 3 : 2, 150, { dur:0.6, size:3.4, grav:520 }); | |
| } | |
| addRing(BX + BW/2, BY + row*CELL + CELL/2, t.accent, 220, 3, 0.55); | |
| } | |
| if(!colors.length) colors = [t.accent, '#ffffff']; | |
| var midY = BY + (rows[0] + rows[rows.length-1] + 1) * CELL / 2; | |
| var n = rows.length; | |
| var power = 1 + n * 0.6 + (tspin ? 0.8 : 0); | |
| if(ti === 0){ | |
| addRing(BX+BW/2, midY, '#00ffff', 260, 5, 0.5); | |
| spawnBurst(BX+BW/2, midY, ['#00ffff','#00a2ff','#ffffff'], 40, 300*power, { dur:0.75, size:3.6 }); | |
| } else if(ti === 1){ | |
| state.shake = Math.max(state.shake, 16 + n*5); | |
| spawnBurst(BX+BW/2, midY, ['#ffe34d','#ff7a1a','#ff3d2e','#ffffff'], 70, 430*power, { dur:0.9, size:5, grav:260 }); | |
| spawnBurst(BX+BW/2, midY, ['#ffffff','#ffe34d'], 24, 120, { dur:1.1, size:8, grav:-40, shape:'flare' }); | |
| confettiBurst(BX+BW/2, midY, 80); | |
| } else if(ti === 2){ | |
| for(var k=0;k<3;k++) addRing(BX+BW/2, midY, '#eaf6ff', 120 + k*130, 6-k, 0.5 + k*0.12); | |
| state.shake = Math.max(state.shake, 10 + n*3); | |
| } else if(ti === 3){ | |
| var by2 = rows[rows.length-1]; | |
| addBolt(BX-40, BY, BX+BW+40, BY + by2*CELL, '#fff55a', 14, 60); | |
| addBolt(BX+BW+40, BY, BX-40, BY + (by2+1)*CELL, '#b9a2ff', 14, 60); | |
| spawnBurst(BX+BW/2, midY, ['#fff55a','#8a4dff','#ffffff'], 40, 330*power, { dur:0.6, size:3.6 }); | |
| state.flash = Math.max(state.flash, 0.5); | |
| } else if(ti === 4){ | |
| for(var kk=0;kk<3;kk++){ | |
| addRing(BX+BW/2, midY, ['#ff2fd0','#ffd166','#00e5ff'][kk], 200 + kk*90, 5, 0.6); | |
| } | |
| spawnBurst(BX+BW/2, midY, colors, 90, 380*power, { dur:0.95, size:4.6, grav:150 }); | |
| confettiBurst(BX+BW/2, midY, 70); | |
| } else if(ti === 5){ | |
| for(var m=0;m<COLS;m++){ | |
| particles.push({ | |
| x:BX + m*CELL + CELL/2, y:BY + rows[0]*CELL, | |
| vx:(Math.random()-0.5)*25, vy:120 + Math.random()*80, | |
| life:0, dur:1.2, size:14, color:'#00ff66', grav:30, drag:0.4, | |
| shape:'digit', rot:0, vr:0, ch:(Math.random()<0.5?'0':'1') | |
| }); | |
| } | |
| spawnBurst(BX+BW/2, midY, ['#00ff66','#b6ffce'], 36, 250*power, { dur:0.8, size:3.4 }); | |
| } else if(ti === 6){ | |
| vortex = { x:BX+BW/2, y:midY, r:340, life:0, dur:0.95 }; | |
| for(var v=0;v<70;v++){ | |
| var ang = Math.random()*6.28, rad = 120 + Math.random()*220; | |
| particles.push({ | |
| x:BX+BW/2 + Math.cos(ang)*rad, y:midY + Math.sin(ang)*rad, | |
| vx:0, vy:0, life:0, dur:0.95*Math.random()+0.3, size:3.2, | |
| color:['#d7b3ff','#7a2bff','#ffffff'][(Math.random()*3)|0], | |
| grav:0, drag:0, shape:'vortex', rot:ang, vr:0, | |
| cx:BX+BW/2, cy:midY, rad:rad, angV:3.2 | |
| }); | |
| } | |
| addRing(BX+BW/2, midY, '#d7b3ff', 260, 4, 0.75); | |
| } else if(ti === 7){ | |
| for(var f=0;f<4;f++){ | |
| var fx = BX + 40 + Math.random()*(BW-80); | |
| var fy = BY + 40 + Math.random()*(BH-80); | |
| spawnBurst(fx, fy, ['#ffd166','#ff3d2e','#ff9d1f','#ffffff'][Math.floor(Math.random()*4)].split(','), 26, 300, { dur:0.9, size:4, grav:220 }); | |
| } | |
| confettiBurst(BX+BW/2, midY, 110); | |
| } else if(ti === 8){ | |
| glitchTimer = 0.5; | |
| rgbSplit = 10; | |
| spawnBurst(BX+BW/2, midY, ['#ff00c8','#00fff0','#ffffff'], 60, 360*power, { dur:0.6, size:4, shape:'square', grav:120 }); | |
| state.shake = Math.max(state.shake, 14 + n*4); | |
| } else { | |
| state.flash = 1; | |
| state.shake = 26; | |
| for(var o=0;o<5;o++) addRing(BX+BW/2, midY, ['#ffffff','#c08bff','#00e5ff'][o%3], 160 + o*110, 7-o, 0.8); | |
| spawnBurst(BX+BW/2, midY, ['#ffffff','#c08bff','#00e5ff','#ffd166','#ff2fd0'], 140, 520*power, { dur:1.05, size:5.2, grav:120 }); | |
| confettiBurst(BX+BW/2, midY, 160); | |
| } | |
| } | |
| function triggerLevelUp(){ | |
| var t = currentTheme(); | |
| addRing(BX+BW/2, BY+BH/2, t.accent, 460, 8, 1.0); | |
| addRing(BX+BW/2, BY+BH/2, '#ffffff', 300, 5, 0.8); | |
| spawnBurst(BX+BW/2, BY+BH/2, [t.accent, '#ffffff'], 60, 420, { dur:0.9, size:4 }); | |
| } | |
| function triggerPerfect(){ | |
| var colors = ['#ff5cf0','#ffd166','#00e5ff','#7cffb2','#ffffff']; | |
| for(var i=0;i<6;i++) addRing(BX+BW/2, BY+BH/2, colors[i%colors.length], 120 + i*80, 8-i, 0.9); | |
| spawnBurst(BX+BW/2, BY+BH/2, colors, 160, 560, { dur:1.1, size:5.5, grav:80 }); | |
| confettiBurst(BX+BW/2, BY+BH/2, 200); | |
| } | |
| function confettiBurst(x, y, count){ | |
| if(typeof window.confetti !== 'function') return; | |
| try{ | |
| window.confetti({ | |
| particleCount: Math.min(count, 220), | |
| spread: 90, | |
| startVelocity: 45, | |
| origin: { x: x / W, y: y / H }, | |
| colors: ['#00e5ff','#ffd166','#ff3d5a','#b64dff','#2fe85a','#ffffff'], | |
| scalar: 1.1, | |
| zIndex: 5 | |
| }); | |
| }catch(e){} | |
| } | |
| function updateParticles(dt){ | |
| var i, p, a, s; | |
| for(i=particles.length-1;i>=0;i--){ | |
| p = particles[i]; | |
| p.life += dt; | |
| if(p.life >= p.dur){ particles.splice(i,1); continue; } | |
| if(p.shape === 'vortex'){ | |
| p.rot += p.angV * dt; | |
| p.rad *= Math.pow(0.12, dt); | |
| p.x = p.cx + Math.cos(p.rot) * p.rad; | |
| p.y = p.cy + Math.sin(p.rot) * p.rad; | |
| } else { | |
| p.vy += (p.grav || 0) * dt; | |
| var dr = Math.pow(1 / (1 + (p.drag || 0)), dt); | |
| p.vx *= dr; p.vy *= dr; | |
| p.x += p.vx * dt; | |
| p.y += p.vy * dt; | |
| p.rot += p.vr * dt; | |
| } | |
| } | |
| for(i=rings.length-1;i>=0;i--){ | |
| a = rings[i]; | |
| a.life += dt; | |
| a.r = a.maxR * (1 - Math.pow(1 - Math.min(1, a.life / a.dur), 3)); | |
| if(a.life >= a.dur) rings.splice(i,1); | |
| } | |
| for(i=bolts.length-1;i>=0;i--){ | |
| a = bolts[i]; | |
| a.life += dt; | |
| if(a.life >= a.dur) bolts.splice(i,1); | |
| } | |
| for(i=floats.length-1;i>=0;i--){ | |
| a = floats[i]; | |
| a.life += dt; | |
| a.y += a.vy * dt; | |
| a.vy *= Math.pow(0.25, dt); | |
| if(a.life >= a.dur) floats.splice(i,1); | |
| } | |
| if(vortex){ vortex.life += dt; if(vortex.life >= vortex.dur) vortex = null; } | |
| if(glitchTimer > 0) glitchTimer -= dt; | |
| if(rgbSplit > 0) rgbSplit = Math.max(0, rgbSplit - dt * 40); | |
| } | |
| function updateStars(dt){ | |
| var i, s; | |
| for(i=0;i<stars.length;i++){ | |
| s = stars[i]; | |
| s.y += s.v * dt * (1 + state.level * 0.05); | |
| if(s.y > H){ s.y = -4; s.x = Math.random()*W; } | |
| s.tw += dt * s.tws; | |
| } | |
| } | |
| function initStars(){ | |
| stars.length = 0; | |
| for(var i=0;i<90;i++){ | |
| stars.push({ x:Math.random()*W, y:Math.random()*H, v:6 + Math.random()*26, | |
| r:0.6 + Math.random()*1.8, tw:Math.random()*6.28, tws:1 + Math.random()*3 }); | |
| } | |
| } | |
| var dasTimer = 0, arrTimer = 0, dasDir = 0; | |
| var DAS = 0.135, ARR = 0.028; | |
| var keys = {}; | |
| function handleInput(dt){ | |
| if(state.status !== 'playing') return; | |
| var dir = 0; | |
| if(keys.left && !keys.right) dir = -1; | |
| else if(keys.right && !keys.left) dir = 1; | |
| if(dir !== 0){ | |
| if(dir !== dasDir){ dasDir = dir; dasTimer = DAS; arrTimer = 0; tryMove(dir); } | |
| else { | |
| dasTimer -= dt; | |
| if(dasTimer <= 0){ | |
| arrTimer -= dt; | |
| while(arrTimer <= 0){ if(!tryMove(dir)) break; arrTimer += ARR; } | |
| } | |
| } | |
| } else { dasDir = 0; } | |
| if(keys.down){ | |
| state.softDrop = true; | |
| } else { | |
| state.softDrop = false; | |
| } | |
| } | |
| function update(dt){ | |
| updateParticles(dt); | |
| updateStars(dt); | |
| if(state.status === 'playing'){ | |
| state.elapsed = (performance.now() - state.startTime) / 1000; | |
| handleInput(dt); | |
| var p = state.piece; | |
| if(!p) return; | |
| var speed = state.gravity; | |
| if(keys.down) speed = Math.max(speed * 20, 20); | |
| state.gravAcc += speed * dt; | |
| while(state.gravAcc >= 1){ | |
| state.gravAcc -= 1; | |
| if(!softStep()){ | |
| state.gravAcc = 0; | |
| break; | |
| } | |
| if(keys.down) state.score += 1; | |
| } | |
| if(collides(p.matrix, p.x, p.y + 1)){ | |
| state.grounded = true; | |
| state.lockTimer += dt; | |
| if(state.lockTimer >= 0.5){ lockPiece(); } | |
| } else { | |
| state.grounded = false; | |
| state.lockTimer = 0; | |
| } | |
| } else if(state.status === 'clearing'){ | |
| state.clearTimer += dt; | |
| if(state.clearTimer >= state.clearDuration){ applyClears(); } | |
| } | |
| if(state.flash > 0) state.flash = Math.max(0, state.flash - dt * 2.4); | |
| if(state.shake > 0) state.shake = Math.max(0, state.shake - dt * 55); | |
| } | |
| function roundRect(c, x, y, w, h, r){ | |
| if(r > w/2) r = w/2; | |
| if(r > h/2) r = h/2; | |
| c.beginPath(); | |
| c.moveTo(x+r, y); | |
| c.arcTo(x+w, y, x+w, y+h, r); | |
| c.arcTo(x+w, y+h, x, y+h, r); | |
| c.arcTo(x, y+h, x, y, r); | |
| c.arcTo(x, y, x+w, y, r); | |
| c.closePath(); | |
| } | |
| function drawBlock(c, px, py, size, color, opts){ | |
| opts = opts || {}; | |
| var inset = opts.inset == null ? 1.5 : opts.inset; | |
| var x = px + inset, y = py + inset, s = size - inset*2; | |
| var alpha = opts.alpha == null ? 1 : opts.alpha; | |
| c.save(); | |
| c.globalAlpha = alpha; | |
| if(opts.glow){ c.shadowColor = color.glow; c.shadowBlur = opts.glow; } | |
| var g = c.createLinearGradient(x, y, x, y+s); | |
| g.addColorStop(0, color.light); | |
| g.addColorStop(0.35, color.base); | |
| g.addColorStop(1, color.base); | |
| roundRect(c, x, y, s, s, size*0.18); | |
| c.fillStyle = g; | |
| c.fill(); | |
| c.shadowBlur = 0; | |
| c.globalAlpha = alpha * 0.35; | |
| c.strokeStyle = color.light; | |
| c.lineWidth = 1.2; | |
| c.stroke(); | |
| c.globalAlpha = alpha * 0.5; | |
| roundRect(c, x+s*0.16, y+s*0.14, s*0.4, s*0.22, s*0.1); | |
| c.fillStyle = color.light; | |
| c.fill(); | |
| c.restore(); | |
| } | |
| function drawGhostBlock(c, px, py, size, color){ | |
| c.save(); | |
| c.globalAlpha = 0.22; | |
| roundRect(c, px+2, py+2, size-4, size-4, size*0.18); | |
| c.fillStyle = color.glow; | |
| c.fill(); | |
| c.globalAlpha = 0.55; | |
| c.strokeStyle = color.glow; | |
| c.lineWidth = 1.5; | |
| c.stroke(); | |
| c.restore(); | |
| } | |
| function drawMiniPiece(type, cx, cy, cell, alpha){ | |
| var m = PIECES[type].states[0], size = m.length; | |
| var y, x, count = 0, minX = size, maxX = -1, minY = size, maxY = -1; | |
| for(y=0;y<size;y++){ for(x=0;x<size;x++){ if(m[y][x]){ count++; if(x<minX)minX=x; if(x>maxX)maxX=x; if(y<minY)minY=y; if(y>maxY)maxY=y; } } } | |
| var w = (maxX - minX + 1) * cell, h = (maxY - minY + 1) * cell; | |
| var ox = cx - w/2 - minX*cell, oy = cy - h/2 - minY*cell; | |
| for(y=0;y<size;y++){ | |
| for(x=0;x<size;x++){ | |
| if(m[y][x]) drawBlock(ctx, ox + x*cell, oy + y*cell, cell, COLORS[type], { glow:8, alpha:alpha==null?1:alpha }); | |
| } | |
| } | |
| } | |
| function drawBoard(){ | |
| var r, c, x, y, cell; | |
| ctx.save(); | |
| roundRect(ctx, BX-6, BY-6, BW+12, BH+12, 10); | |
| ctx.fillStyle = 'rgba(4,8,20,0.72)'; | |
| ctx.fill(); | |
| ctx.strokeStyle = 'rgba(120,200,255,0.22)'; | |
| ctx.lineWidth = 1.5; | |
| ctx.stroke(); | |
| ctx.restore(); | |
| ctx.save(); | |
| ctx.strokeStyle = 'rgba(120,180,255,0.08)'; | |
| ctx.lineWidth = 1; | |
| for(c=0;c<=COLS;c++){ | |
| ctx.beginPath(); ctx.moveTo(BX + c*CELL, BY); ctx.lineTo(BX + c*CELL, BY+BH); ctx.stroke(); | |
| } | |
| for(r=0;r<=ROWS;r++){ | |
| ctx.beginPath(); ctx.moveTo(BX, BY + r*CELL); ctx.lineTo(BX+BW, BY + r*CELL); ctx.stroke(); | |
| } | |
| ctx.restore(); | |
| for(r=0;r<ROWS;r++){ | |
| for(c=0;c<COLS;c++){ | |
| if(state.board[r][c]){ | |
| drawBlock(ctx, BX + c*CELL, BY + r*CELL, CELL, COLORS[state.board[r][c]], { glow:7 }); | |
| } | |
| } | |
| } | |
| if(state.clearingRows.length){ | |
| var prog = Math.min(1, state.clearTimer / state.clearDuration); | |
| ctx.save(); | |
| for(var i=0;i<state.clearingRows.length;i++){ | |
| var row = state.clearingRows[i]; | |
| var a = Math.sin(prog * Math.PI); | |
| var g = ctx.createLinearGradient(BX, 0, BX+BW, 0); | |
| var t = currentTheme(); | |
| g.addColorStop(0, 'rgba(255,255,255,' + (a*0.9) + ')'); | |
| g.addColorStop(0.5, t.accent); | |
| g.addColorStop(1, 'rgba(255,255,255,' + (a*0.9) + ')'); | |
| ctx.globalAlpha = a; | |
| ctx.fillStyle = g; | |
| ctx.fillRect(BX, BY + row*CELL - prog*CELL*0.15, BW, CELL * (1 + prog*0.3)); | |
| } | |
| ctx.restore(); | |
| } | |
| var p = state.piece; | |
| if(p && state.status !== 'gameover'){ | |
| var gy = p.y; | |
| while(!collides(p.matrix, p.x, gy + 1)){ gy++; } | |
| var yy, xx; | |
| for(yy=0;yy<p.matrix.length;yy++){ | |
| for(xx=0;xx<p.matrix.length;xx++){ | |
| if(!p.matrix[yy][xx]) continue; | |
| if(gy + yy >= 0) drawGhostBlock(ctx, BX + (p.x+xx)*CELL, BY + (gy+yy)*CELL, CELL, COLORS[p.type]); | |
| } | |
| } | |
| for(yy=0;yy<p.matrix.length;yy++){ | |
| for(xx=0;xx<p.matrix.length;xx++){ | |
| if(!p.matrix[yy][xx]) continue; | |
| if(p.y + yy >= 0) drawBlock(ctx, BX + (p.x+xx)*CELL, BY + (p.y+yy)*CELL, CELL, COLORS[p.type], { glow:12 }); | |
| } | |
| } | |
| } | |
| } | |
| function drawPanel(x, y, w, h, label){ | |
| ctx.save(); | |
| roundRect(ctx, x, y, w, h, 10); | |
| var t = currentTheme(); | |
| ctx.fillStyle = 'rgba(8,12,26,0.72)'; | |
| ctx.fill(); | |
| ctx.strokeStyle = 'rgba(120,200,255,0.25)'; | |
| ctx.lineWidth = 1.2; | |
| ctx.stroke(); | |
| if(label){ | |
| ctx.fillStyle = t.accent; | |
| ctx.font = '700 13px Orbitron, monospace'; | |
| ctx.textAlign = 'left'; | |
| ctx.textBaseline = 'top'; | |
| ctx.fillText(label, x + 12, y + 9); | |
| } | |
| ctx.restore(); | |
| } | |
| function drawHud(){ | |
| var t = currentTheme(); | |
| var lx = 16, lw = BX - 32; | |
| drawPanel(lx, BY, lw, 130, 'HOLD'); | |
| if(state.hold){ drawMiniPiece(state.hold, lx + lw/2, BY + 80, 22); } | |
| else { | |
| ctx.save(); | |
| ctx.globalAlpha = 0.25; | |
| ctx.fillStyle = '#8fb8ff'; | |
| ctx.font = '13px DotGothic16, monospace'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('EMPTY', lx + lw/2, BY + 78); | |
| ctx.restore(); | |
| } | |
| var sy = BY + 150; | |
| var stats = [ | |
| ['SCORE', state.score.toLocaleString()], | |
| ['LEVEL', String(state.level)], | |
| ['LINES', String(state.linesTotal)], | |
| ['COMBO', state.combo > 1 ? ('x' + (state.combo - 1)) : '-'] | |
| ]; | |
| for(var i=0;i<stats.length;i++){ | |
| ctx.save(); | |
| ctx.fillStyle = 'rgba(150,180,230,0.7)'; | |
| ctx.font = '12px Orbitron, monospace'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText(stats[i][0], lx, sy + i*30 + 10); | |
| ctx.fillStyle = i === 0 ? '#ffffff' : t.accent; | |
| ctx.font = '700 20px Orbitron, monospace'; | |
| ctx.textAlign = 'right'; | |
| ctx.fillText(stats[i][1], lx + lw, sy + i*30 + 10); | |
| ctx.restore(); | |
| } | |
| var ey = sy + 4*30 + 24; | |
| ctx.save(); | |
| ctx.fillStyle = 'rgba(150,180,230,0.6)'; | |
| ctx.font = '11px Orbitron, monospace'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText('EFFECT', lx, ey); | |
| ctx.fillStyle = t.accent; | |
| ctx.font = '700 16px Orbitron, monospace'; | |
| ctx.fillText(t.name, lx, ey + 20); | |
| ctx.fillStyle = 'rgba(150,180,230,0.5)'; | |
| ctx.font = '11px Orbitron, monospace'; | |
| ctx.fillText('NEXT CHANGE', lx, ey + 44); | |
| var toNext = 10 - (state.linesTotal % 10); | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.font = '700 14px Orbitron, monospace'; | |
| ctx.fillText(toNext + ' LINES', lx, ey + 60); | |
| ctx.restore(); | |
| if(state.b2b){ | |
| ctx.save(); | |
| ctx.fillStyle = '#ffd166'; | |
| ctx.font = '700 14px Orbitron, monospace'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText('B2B', lx, ey + 92); | |
| ctx.restore(); | |
| } | |
| var rx = BX + BW + 20, rw = W - rx - 16; | |
| drawPanel(rx, BY, rw, 5*74 + 26, 'NEXT'); | |
| var q = state.queue; | |
| var n = Math.min(5, q.length); | |
| for(var k=0;k<n;k++){ | |
| var cyy = BY + 52 + k*74; | |
| var scale = k === 0 ? 22 : 19; | |
| ctx.save(); | |
| ctx.globalAlpha = k === 0 ? 1 : 0.72 - k*0.06; | |
| drawMiniPiece(q[k], rx + rw/2, cyy, scale); | |
| ctx.restore(); | |
| } | |
| var cy2 = BY + 5*74 + 42; | |
| drawPanel(lx, cy2, lw, 118, 'STATUS'); | |
| ctx.save(); | |
| ctx.fillStyle = 'rgba(200,220,255,0.85)'; | |
| ctx.font = '12px DotGothic16, monospace'; | |
| ctx.textAlign = 'left'; | |
| var mins = Math.floor(state.elapsed/60), secs = Math.floor(state.elapsed%60); | |
| var timeStr = mins + ':' + (secs<10?'0':'') + secs; | |
| ctx.fillText('TIME ' + timeStr, lx + 12, cy2 + 42); | |
| ctx.fillText('SPEED ' + state.gravity.toFixed(0) + ' c/s', lx + 12, cy2 + 62); | |
| ctx.fillText(state.muted ? 'SOUND OFF (M)' : 'SOUND ON (M)', lx + 12, cy2 + 82); | |
| ctx.restore(); | |
| } | |
| function drawOverlays(){ | |
| if(state.status === 'ready'){ | |
| overlayBox('派手エフェクト・テトリス', 'ENTER キー でスタート', ['7-bag / SRS準拠', '10ラインごとに演出が変化', 'T-spin / B2B / コンボ対応']); | |
| } else if(state.status === 'paused'){ | |
| overlayBox('PAUSED', 'P キー で再開', ['ENTER でリスタート']); | |
| } else if(state.status === 'gameover'){ | |
| overlayBox('GAME OVER', 'ENTER キー でリトライ', ['SCORE ' + state.score.toLocaleString(), 'LINES ' + state.linesTotal, 'EFFECT ' + currentTheme().name]); | |
| } | |
| } | |
| function overlayBox(title, sub, lines){ | |
| ctx.save(); | |
| ctx.fillStyle = 'rgba(2,4,12,0.72)'; | |
| ctx.fillRect(0, 0, W, H); | |
| var t = currentTheme(); | |
| ctx.textAlign = 'center'; | |
| ctx.shadowColor = t.accent; | |
| ctx.shadowBlur = 26; | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.font = '900 46px Orbitron, monospace'; | |
| ctx.fillText(title, W/2, H/2 - 60); | |
| ctx.shadowBlur = 0; | |
| ctx.fillStyle = t.accent; | |
| ctx.font = '700 20px Orbitron, monospace'; | |
| ctx.fillText(sub, W/2, H/2 - 10); | |
| ctx.fillStyle = 'rgba(210,225,255,0.8)'; | |
| ctx.font = '15px DotGothic16, monospace'; | |
| for(var i=0;i<lines.length;i++){ | |
| ctx.fillText(lines[i], W/2, H/2 + 40 + i*26); | |
| } | |
| ctx.restore(); | |
| } | |
| function drawEffects(){ | |
| var i, p, a; | |
| ctx.save(); | |
| ctx.globalCompositeOperation = 'lighter'; | |
| for(i=0;i<rings.length;i++){ | |
| a = rings[i]; | |
| ctx.globalAlpha = 1 - (a.life / a.dur); | |
| ctx.strokeStyle = a.color; | |
| ctx.lineWidth = a.width; | |
| ctx.beginPath(); | |
| ctx.arc(a.x, a.y, a.r, 0, Math.PI*2); | |
| ctx.stroke(); | |
| } | |
| for(i=0;i<bolts.length;i++){ | |
| a = bolts[i]; | |
| ctx.globalAlpha = 1 - (a.life / a.dur); | |
| ctx.strokeStyle = a.color; | |
| ctx.lineWidth = 2.5; | |
| ctx.beginPath(); | |
| ctx.moveTo(a.pts[0].x, a.pts[0].y); | |
| for(var j=1;j<a.pts.length;j++){ ctx.lineTo(a.pts[j].x, a.pts[j].y); } | |
| ctx.stroke(); | |
| ctx.globalAlpha *= 0.45; | |
| ctx.lineWidth = 6; | |
| ctx.stroke(); | |
| } | |
| for(i=0;i<particles.length;i++){ | |
| p = particles[i]; | |
| var al = 1 - (p.life / p.dur); | |
| ctx.globalAlpha = Math.max(0, al); | |
| ctx.fillStyle = p.color; | |
| if(p.shape === 'digit'){ | |
| ctx.font = '700 ' + p.size + 'px monospace'; | |
| ctx.fillText(p.ch, p.x, p.y); | |
| } else if(p.shape === 'square'){ | |
| ctx.save(); | |
| ctx.translate(p.x, p.y); | |
| ctx.rotate(p.rot); | |
| ctx.fillRect(-p.size/2, -p.size/2, p.size, p.size); | |
| ctx.restore(); | |
| } else if(p.shape === 'flare'){ | |
| ctx.save(); | |
| ctx.translate(p.x, p.y); | |
| ctx.rotate(p.rot); | |
| ctx.beginPath(); | |
| ctx.moveTo(0, -p.size*2); | |
| ctx.lineTo(p.size*0.7, 0); | |
| ctx.lineTo(0, p.size*2); | |
| ctx.lineTo(-p.size*0.7, 0); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.restore(); | |
| } else { | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, Math.max(0.4, p.size*al), 0, Math.PI*2); | |
| ctx.fill(); | |
| } | |
| } | |
| ctx.restore(); | |
| ctx.save(); | |
| ctx.textAlign = 'center'; | |
| for(i=0;i<floats.length;i++){ | |
| a = floats[i]; | |
| var prog = a.life / a.dur; | |
| ctx.globalAlpha = 1 - prog*prog; | |
| ctx.fillStyle = a.color; | |
| ctx.shadowColor = a.color; | |
| ctx.shadowBlur = 18; | |
| ctx.font = '900 ' + a.size + 'px Orbitron, monospace'; | |
| ctx.fillText(a.text, a.x, a.y); | |
| } | |
| ctx.restore(); | |
| } | |
| function drawBackground(){ | |
| var t = currentTheme(); | |
| var g = ctx.createRadialGradient(W/2, H*0.4, 40, W/2, H/2, H*0.9); | |
| g.addColorStop(0, hexA(t.ambient, 0.14)); | |
| g.addColorStop(0.5, hexA(t.ambient, 0.05)); | |
| g.addColorStop(1, 'rgba(2,3,10,1)'); | |
| ctx.fillStyle = g; | |
| ctx.fillRect(0, 0, W, H); | |
| ctx.save(); | |
| for(var i=0;i<stars.length;i++){ | |
| var s = stars[i]; | |
| ctx.globalAlpha = 0.25 + 0.35 * (0.5 + 0.5*Math.sin(s.tw)); | |
| ctx.fillStyle = t.accent; | |
| ctx.beginPath(); | |
| ctx.arc(s.x, s.y, s.r, 0, Math.PI*2); | |
| ctx.fill(); | |
| } | |
| ctx.restore(); | |
| } | |
| function hexA(hex, a){ | |
| if(hex[0] !== '#') return hex; | |
| var r = parseInt(hex.substr(1,2),16), g = parseInt(hex.substr(3,2),16), b = parseInt(hex.substr(5,2),16); | |
| return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; | |
| } | |
| function render(){ | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| ctx.clearRect(0, 0, W, H); | |
| drawBackground(); | |
| ctx.save(); | |
| if(state.shake > 0.3){ | |
| ctx.translate((Math.random()-0.5)*state.shake, (Math.random()-0.5)*state.shake); | |
| } | |
| drawBoard(); | |
| drawEffects(); | |
| ctx.restore(); | |
| drawHud(); | |
| if(glitchTimer > 0){ | |
| var slices = 7; | |
| for(var s=0;s<slices;s++){ | |
| var sy = Math.random() * H; | |
| var sh = 5 + Math.random() * 22; | |
| var dx = (Math.random() - 0.5) * rgbSplit * 3; | |
| ctx.drawImage(canvas, 0, Math.round(sy*dpr), canvas.width, Math.round(sh*dpr), dx, sy, W, sh); | |
| } | |
| } | |
| if(rgbSplit > 0){ | |
| ctx.save(); | |
| ctx.globalCompositeOperation = 'lighter'; | |
| ctx.globalAlpha = rgbSplit / 30; | |
| ctx.fillStyle = '#ff00c8'; | |
| ctx.fillRect(0, 0, W, H); | |
| ctx.fillStyle = '#00fff0'; | |
| ctx.fillRect(rgbSplit, 0, W, H); | |
| ctx.restore(); | |
| } | |
| if(state.flash > 0){ | |
| ctx.save(); | |
| ctx.globalCompositeOperation = 'lighter'; | |
| ctx.globalAlpha = state.flash * 0.55; | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.fillRect(0, 0, W, H); | |
| ctx.restore(); | |
| } | |
| drawOverlays(); | |
| } | |
| var AC = null, masterGain = null, musicGain = null, sfxGain = null, noiseBuffer = null; | |
| var muteState = false; | |
| function ensureAudio(){ | |
| if(AC) { if(AC.state === 'suspended') AC.resume(); return; } | |
| try{ | |
| AC = new (window.AudioContext || window.webkitAudioContext)(); | |
| }catch(e){ AC = null; return; } | |
| masterGain = AC.createGain(); | |
| masterGain.gain.value = 0.9; | |
| sfxGain = AC.createGain(); | |
| sfxGain.gain.value = 0.55; | |
| musicGain = AC.createGain(); | |
| musicGain.gain.value = 0.30; | |
| sfxGain.connect(masterGain); | |
| musicGain.connect(masterGain); | |
| var comp = AC.createDynamicsCompressor(); | |
| comp.threshold.value = -14; | |
| comp.ratio.value = 8; | |
| masterGain.connect(comp); | |
| comp.connect(AC.destination); | |
| var len = Math.floor(AC.sampleRate * 0.8); | |
| noiseBuffer = AC.createBuffer(1, len, AC.sampleRate); | |
| var d = noiseBuffer.getChannelData(0); | |
| for(var i=0;i<len;i++){ d[i] = Math.random()*2 - 1; } | |
| } | |
| function midi(n){ return 440 * Math.pow(2, (n - 69) / 12); } | |
| function tone(freq, dur, type, vol, when, glideTo, dest){ | |
| if(!AC || muteState) return; | |
| var t = when == null ? AC.currentTime : when; | |
| var o = AC.createOscillator(); | |
| var g = AC.createGain(); | |
| o.type = type || 'square'; | |
| o.frequency.setValueAtTime(freq, t); | |
| if(glideTo) o.frequency.exponentialRampToValueAtTime(Math.max(1, glideTo), t + dur); | |
| g.gain.setValueAtTime(0.0001, t); | |
| g.gain.exponentialRampToValueAtTime(vol || 0.2, t + 0.006); | |
| g.gain.exponentialRampToValueAtTime(0.0001, t + dur); | |
| o.connect(g); | |
| g.connect(dest || sfxGain); | |
| o.start(t); | |
| o.stop(t + dur + 0.03); | |
| return o; | |
| } | |
| function noiseHit(dur, vol, when, hp, dest){ | |
| if(!AC || muteState) return; | |
| var t = when == null ? AC.currentTime : when; | |
| var s = AC.createBufferSource(); | |
| s.buffer = noiseBuffer; | |
| var g = AC.createGain(); | |
| var f = AC.createBiquadFilter(); | |
| f.type = 'highpass'; | |
| f.frequency.value = hp || 400; | |
| g.gain.setValueAtTime(vol || 0.2, t); | |
| g.gain.exponentialRampToValueAtTime(0.0001, t + dur); | |
| s.connect(f); f.connect(g); g.connect(dest || sfxGain); | |
| s.start(t); | |
| s.stop(t + dur + 0.02); | |
| } | |
| function sfx(name){ | |
| if(!AC || muteState) return; | |
| var t = AC.currentTime; | |
| if(name === 'move') tone(180, 0.05, 'square', 0.08, t); | |
| else if(name === 'rotate') tone(320, 0.06, 'square', 0.12, t, 420); | |
| else if(name === 'hold'){ tone(300, 0.08, 'triangle', 0.16, t, 620); } | |
| else if(name === 'harddrop'){ noiseHit(0.12, 0.28, t, 300); tone(90, 0.14, 'sine', 0.3, t, 45); } | |
| else if(name === 'tspin'){ tone(660, 0.12, 'square', 0.2, t, 990); tone(990, 0.18, 'square', 0.15, t+0.05, 1480); } | |
| else if(name === 'levelup'){ var seq=[523,659,784,1047]; for(var i=0;i<seq.length;i++) tone(seq[i], 0.18, 'square', 0.2, t + i*0.07); } | |
| else if(name === 'perfect'){ var s2=[523,659,784,1047,1319,1568]; for(var j=0;j<s2.length;j++) tone(s2[j], 0.3, 'triangle', 0.22, t + j*0.06); } | |
| else if(name === 'gameover'){ var s3=[440,392,330,262,196]; for(var k=0;k<s3.length;k++) tone(s3[k], 0.4, 'sawtooth', 0.2, t + k*0.16, s3[k]*0.8); noiseHit(0.8, 0.12, t+0.5, 200); } | |
| } | |
| function sfxLock(){ | |
| if(!AC || muteState) return; | |
| var t = AC.currentTime; | |
| noiseHit(0.06, 0.16, t, 500); | |
| tone(150, 0.08, 'sine', 0.18, t, 90); | |
| } | |
| function sfxClear(n, tspin){ | |
| if(!AC || muteState) return; | |
| var t = AC.currentTime; | |
| var base = [523, 659, 784, 1047]; | |
| if(tspin){ | |
| tone(880, 0.1, 'square', 0.22, t, 1320); | |
| tone(1320, 0.22, 'square', 0.2, t+0.06, 1760); | |
| } | |
| for(var i=0;i<n;i++){ | |
| tone(base[i % base.length] * (1 + i*0.12), 0.16, 'square', 0.18, t + i*0.045, base[i % base.length]*1.5); | |
| } | |
| if(n >= 4){ | |
| tone(196, 0.5, 'sawtooth', 0.24, t, 98); | |
| noiseHit(0.25, 0.2, t, 800); | |
| } | |
| if(state.combo > 1){ | |
| tone(300 + state.combo*40, 0.15, 'triangle', 0.16, t + 0.05); | |
| } | |
| } | |
| var music = { playing:false, step:0, next:0, bpm:110, timer:null }; | |
| var PROG = [ | |
| { bass:45, arp:[57,60,64] }, | |
| { bass:41, arp:[53,57,60] }, | |
| { bass:48, arp:[55,60,64] }, | |
| { bass:43, arp:[55,59,62] } | |
| ]; | |
| function playBass(n, t, dur){ | |
| if(!AC || muteState) return; | |
| var o = AC.createOscillator(); | |
| var f = AC.createBiquadFilter(); | |
| var g = AC.createGain(); | |
| o.type = 'sawtooth'; | |
| o.frequency.value = midi(n); | |
| f.type = 'lowpass'; | |
| f.frequency.setValueAtTime(600, t); | |
| f.frequency.exponentialRampToValueAtTime(180, t + dur); | |
| g.gain.setValueAtTime(0.0001, t); | |
| g.gain.exponentialRampToValueAtTime(0.30, t + 0.01); | |
| g.gain.exponentialRampToValueAtTime(0.0001, t + dur); | |
| o.connect(f); f.connect(g); g.connect(musicGain); | |
| o.start(t); o.stop(t + dur + 0.03); | |
| } | |
| function playLead(n, t, dur, vol){ | |
| var o = AC.createOscillator(); | |
| var g = AC.createGain(); | |
| o.type = 'triangle'; | |
| o.frequency.value = midi(n); | |
| g.gain.setValueAtTime(0.0001, t); | |
| g.gain.exponentialRampToValueAtTime(vol || 0.12, t + 0.008); | |
| g.gain.exponentialRampToValueAtTime(0.0001, t + dur); | |
| o.connect(g); g.connect(musicGain); | |
| o.start(t); o.stop(t + dur + 0.02); | |
| } | |
| function playKick(t){ | |
| if(!AC || muteState) return; | |
| var o = AC.createOscillator(); | |
| var g = AC.createGain(); | |
| o.type = 'sine'; | |
| o.frequency.setValueAtTime(150, t); | |
| o.frequency.exponentialRampToValueAtTime(45, t + 0.12); | |
| g.gain.setValueAtTime(0.5, t); | |
| g.gain.exponentialRampToValueAtTime(0.0001, t + 0.16); | |
| o.connect(g); g.connect(musicGain); | |
| o.start(t); o.stop(t + 0.18); | |
| } | |
| function playSnare(t){ | |
| noiseHit(0.13, 0.18, t, 1400, musicGain); | |
| tone(180, 0.08, 'triangle', 0.1, t, 120, musicGain); | |
| } | |
| function playHat(t){ | |
| noiseHit(0.035, 0.07, t, 7000, musicGain); | |
| } | |
| function playStep(step, t){ | |
| var bar = Math.floor(step / 16) % 4; | |
| var s = step % 16; | |
| var ch = PROG[bar]; | |
| if(s % 3 === 0 || s === 8 || s === 11 || s === 14){ | |
| var oct = (s === 8) ? 12 : 0; | |
| playBass(ch.bass + oct, t, 0.16); | |
| } | |
| var arp = ch.arp; | |
| if(s % 2 === 0){ | |
| var note = arp[(Math.floor(step/2)) % arp.length]; | |
| playLead(note, t, 0.12, 0.10); | |
| } else if(Math.random() < 0.35){ | |
| playLead(arp[(Math.floor(step/2)) % arp.length] + 12, t, 0.07, 0.06); | |
| } | |
| if(s === 0 || s === 8) playKick(t); | |
| if(s === 4 || s === 12) playSnare(t); | |
| if(s % 2 === 1) playHat(t); | |
| if(step % 64 === 63 && state.level > 1 && s === 15) playLead(ch.arp[0] + 19, t, 0.3, 0.1); | |
| } | |
| function musicScheduler(){ | |
| if(!AC || !music.playing || muteState) return; | |
| var spb = 60 / (music.bpm * 4); | |
| while(music.next < AC.currentTime + 0.12){ | |
| playStep(music.step, music.next); | |
| music.next += spb; | |
| music.step++; | |
| } | |
| } | |
| function playMusic(){ | |
| ensureAudio(); | |
| if(!AC) return; | |
| music.playing = true; | |
| music.step = 0; | |
| music.next = AC.currentTime + 0.08; | |
| music.bpm = 100 + Math.min(state.level - 1, 12) * 6; | |
| if(!music.timer) music.timer = setInterval(musicScheduler, 25); | |
| } | |
| function stopMusic(){ music.playing = false; } | |
| function pauseMusic(){ music.playing = false; } | |
| function resumeMusic(){ if(AC){ music.next = AC.currentTime + 0.08; music.playing = true; } } | |
| function toggleMute(){ | |
| state.muted = !state.muted; | |
| muteState = state.muted; | |
| if(masterGain) masterGain.gain.value = muteState ? 0 : 0.9; | |
| } | |
| function updateTempo(){ | |
| music.bpm = 100 + Math.min(state.level - 1, 12) * 6; | |
| } | |
| var dpr = 1; | |
| function resize(){ | |
| var availW = window.innerWidth - 24; | |
| var availH = window.innerHeight - 40; | |
| var scale = Math.min(availW / W, availH / H); | |
| if(scale <= 0) scale = 1; | |
| dpr = Math.min(window.devicePixelRatio || 1, 2); | |
| canvas.width = Math.round(W * dpr); | |
| canvas.height = Math.round(H * dpr); | |
| canvas.style.width = Math.round(W * scale) + 'px'; | |
| canvas.style.height = Math.round(H * scale) + 'px'; | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| } | |
| var lastT = 0, acc = 0; | |
| function loop(ts){ | |
| if(!lastT) lastT = ts; | |
| var dt = (ts - lastT) / 1000; | |
| lastT = ts; | |
| if(dt > 0.25) dt = 0.25; | |
| acc += dt; | |
| var steps = 0; | |
| while(acc >= 1/60 && steps < 5){ | |
| update(1/60); | |
| acc -= 1/60; | |
| steps++; | |
| } | |
| render(); | |
| requestAnimationFrame(loop); | |
| } | |
| window.addEventListener('keydown', function(e){ | |
| var k = e.code; | |
| if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space','KeyZ','KeyX','KeyC','KeyA','KeyD','KeyS','KeyW','KeyQ','KeyM','KeyP','Enter','ShiftLeft','ShiftRight','ControlLeft','ControlRight','Escape'].indexOf(k) >= 0){ | |
| e.preventDefault(); | |
| } | |
| if(e.repeat){ | |
| return; | |
| } | |
| ensureAudio(); | |
| if(k === 'KeyM'){ toggleMute(); return; } | |
| if(k === 'Enter'){ | |
| if(state.status === 'ready' || state.status === 'gameover'){ startGame(); } | |
| else if(state.status === 'paused'){ state.status = 'playing'; resumeMusic(); } | |
| return; | |
| } | |
| if(k === 'KeyP' || k === 'Escape'){ togglePause(); return; } | |
| if(state.status !== 'playing') return; | |
| if(k === 'ArrowLeft' || k === 'KeyA'){ keys.left = true; } | |
| else if(k === 'ArrowRight' || k === 'KeyD'){ keys.right = true; } | |
| else if(k === 'ArrowDown' || k === 'KeyS'){ keys.down = true; } | |
| else if(k === 'ArrowUp' || k === 'KeyX' || k === 'KeyW'){ tryRotate(1); } | |
| else if(k === 'KeyZ' || k === 'ControlLeft' || k === 'ControlRight'){ tryRotate(-1); } | |
| else if(k === 'KeyQ'){ tryRotate(2); } | |
| else if(k === 'Space'){ if(state.piece){ var oy = state.piece.y; var d = hardDropDistance(); state.score += 2*d; hardDropTrail(oy, d); state.lastRotation = false; sfx('harddrop'); state.shake = Math.max(state.shake, 4); lockPiece(); } } | |
| else if(k === 'KeyC' || k === 'ShiftLeft' || k === 'ShiftRight'){ holdPiece(); } | |
| }); | |
| window.addEventListener('keyup', function(e){ | |
| var k = e.code; | |
| if(k === 'ArrowLeft' || k === 'KeyA'){ keys.left = false; if(dasDir === -1) dasDir = 0; } | |
| else if(k === 'ArrowRight' || k === 'KeyD'){ keys.right = false; if(dasDir === 1) dasDir = 0; } | |
| else if(k === 'ArrowDown' || k === 'KeyS'){ keys.down = false; } | |
| }); | |
| function hardDropTrail(startY, d){ | |
| var p = state.piece; | |
| if(!p) return; | |
| var x, y; | |
| for(var i=0;i<=d;i++){ | |
| for(var yy=0;yy<p.matrix.length;yy++){ | |
| for(var xx=0;xx<p.matrix.length;xx++){ | |
| if(!p.matrix[yy][xx]) continue; | |
| y = startY + yy + i; | |
| if(y < 0) continue; | |
| particles.push({ x:BX + (p.x+xx)*CELL + CELL/2, y:BY + y*CELL + CELL/2, | |
| vx:0, vy:0, life:0, dur:0.28, size:3.5, color:COLORS[p.type].light, | |
| grav:0, drag:2, shape:'spark', rot:0, vr:0 }); | |
| } | |
| } | |
| } | |
| spawnBurst(BX + (p.x+1)*CELL, BY + (p.y+1)*CELL, [COLORS[p.type].glow, COLORS[p.type].light], 12, 140, { dur:0.4, size:3, grav:300 }); | |
| } | |
| window.addEventListener('resize', resize); | |
| document.addEventListener('visibilitychange', function(){ | |
| if(document.hidden && state.status === 'playing'){ state.status = 'paused'; pauseMusic(); } | |
| }); | |
| window.addEventListener('blur', function(){ keys.left = keys.right = keys.down = false; dasDir = 0; }); | |
| setInterval(function(){ | |
| if(state.status === 'playing') updateTempo(); | |
| }, 500); | |
| resize(); | |
| initStars(); | |
| resetState(); | |
| state.status = 'ready'; | |
| requestAnimationFrame(loop); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment