diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 6c7f44b..d2d85aa 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -15,33 +15,17 @@ const DEMO_EMAILS = [ ] // ── Tetris overlay ───────────────────────────────────────────────────────────── -const TETRIS_LABELS = [ - 'Шахматка', 'Бронирования', 'Гости', 'Номера', 'Уборка', - 'Каналы', 'Отчёты', 'Веб-сайт', 'Виджет', 'Касса', - 'Отзывы', 'Сервис', 'Аренда', 'Тарифы', 'Скидки', - 'Лояльность', 'Тех.перерывы', 'Цены', 'Документы', 'Модули', -] -const TETRIS_COLORS = [ - '#1e3a8a', '#14532d', '#713f12', '#3b0764', '#0f172a', - '#134e4a', '#4a044e', '#1c1917', '#7f1d1d', '#0c4a6e', -] -function roundRectPath( - ctx: CanvasRenderingContext2D, - x: number, y: number, w: number, h: number, r: number, -) { - ctx.beginPath() - ctx.moveTo(x + r, y) - ctx.lineTo(x + w - r, y) - ctx.arcTo(x + w, y, x + w, y + r, r) - ctx.lineTo(x + w, y + h - r) - ctx.arcTo(x + w, y + h, x + w - r, y + h, r) - ctx.lineTo(x + r, y + h) - ctx.arcTo(x, y + h, x, y + h - r, r) - ctx.lineTo(x, y + r) - ctx.arcTo(x, y, x + r, y, r) - ctx.closePath() -} +// All 7 tetrominoes with all rotations, each cell = [row, col] offset +const TETROMINOES: { cells: [number,number][][]; color: string }[] = [ + { color: '#06b6d4', cells: [[[0,0],[0,1],[0,2],[0,3]], [[0,0],[1,0],[2,0],[3,0]]] }, // I + { color: '#eab308', cells: [[[0,0],[0,1],[1,0],[1,1]]] }, // O + { color: '#a855f7', cells: [[[0,1],[1,0],[1,1],[1,2]], [[0,0],[1,0],[1,1],[2,0]], [[0,0],[0,1],[0,2],[1,1]], [[0,1],[1,0],[1,1],[2,1]]] }, // T + { color: '#22c55e', cells: [[[0,1],[0,2],[1,0],[1,1]], [[0,0],[1,0],[1,1],[2,1]]] }, // S + { color: '#ef4444', cells: [[[0,0],[0,1],[1,1],[1,2]], [[0,1],[1,0],[1,1],[2,0]]] }, // Z + { color: '#3b82f6', cells: [[[0,0],[1,0],[1,1],[1,2]], [[0,0],[0,1],[1,0],[2,0]], [[0,0],[0,1],[0,2],[1,2]], [[0,1],[1,1],[2,0],[2,1]]] }, // J + { color: '#f97316', cells: [[[0,2],[1,0],[1,1],[1,2]], [[0,0],[1,0],[2,0],[2,1]], [[0,0],[0,1],[0,2],[1,0]], [[0,0],[0,1],[1,1],[2,1]]] }, // L +] function TetrisOverlay() { const canvasRef = useRef(null) @@ -51,91 +35,122 @@ function TetrisOverlay() { const delay = setTimeout(() => { const canvas = canvasRef.current - console.log('[Tetris] canvas ref:', canvas) if (!canvas) return const parent = canvas.parentElement - console.log('[Tetris] parent:', parent, 'size:', parent?.clientWidth, 'x', parent?.clientHeight) if (!parent) return const W = parent.clientWidth const H = parent.clientHeight + if (!W || !H) return canvas.width = W canvas.height = H - console.log('[Tetris] started, canvas size:', W, 'x', H) - const TILE_W = 84 - const TILE_H = 252 // 3× ratio - const GAP = 6 - const cols = Math.max(1, Math.floor(W / (TILE_W + GAP))) - const offsetX = (W - cols * (TILE_W + GAP) + GAP) / 2 - const colX = (i: number) => offsetX + i * (TILE_W + GAP) + const CELL = 32 + const COLS = Math.floor(W / CELL) + const ROWS = Math.ceil(H / CELL) + // offset to center the grid + const OX = Math.floor((W - COLS * CELL) / 2) - // Tracks bottom y of the topmost settled tile per column (starts at canvas bottom) - const colFloor = new Array(cols).fill(H) - - interface Tile { col: number; y: number; vy: number; label: string; color: string } - const falling: Tile[] = [] - const settled: Tile[] = [] - - let spawnIn = 300 - let last = performance.now() + // settled[row][col] = border color | null + const settled: (string | null)[][] = Array.from({ length: ROWS + 6 }, () => new Array(COLS).fill(null)) const ctx = canvas.getContext('2d')! - const drawTile = (x: number, y: number, label: string, color: string) => { - roundRectPath(ctx, x, y, TILE_W, TILE_H, 10) - ctx.fillStyle = color + // Draw one tetromino cell: white fill, colored border + const drawCell = (col: number, rowPx: number, color: string) => { + const x = OX + col * CELL + 1 + const y = rowPx + 1 + const s = CELL - 2 + ctx.beginPath() + ctx.roundRect(x, y, s, s, 3) + ctx.fillStyle = 'rgba(255,255,255,0.88)' ctx.fill() - ctx.strokeStyle = 'rgba(255,255,255,0.28)' - ctx.lineWidth = 1.5 + ctx.strokeStyle = color + ctx.lineWidth = 2.5 ctx.stroke() + } - // Text rotated 90° so it reads along the long axis - ctx.save() - ctx.translate(x + TILE_W / 2, y + TILE_H / 2) - ctx.rotate(-Math.PI / 2) - ctx.fillStyle = 'rgba(255,255,255,0.92)' - ctx.font = 'bold 13px system-ui,sans-serif' - ctx.textAlign = 'center' - ctx.textBaseline = 'middle' - ctx.fillText(label, 0, 0) - ctx.restore() + interface Piece { + cells: [number,number][] + color: string + col: number // grid-col of leftmost cell + yPx: number // pixel Y of top row + vy: number + } + + const falling: Piece[] = [] + let spawnIn = 300 + let last = performance.now() + + const maxRow = (cells: [number,number][]) => Math.max(...cells.map(c => c[0])) + const maxCol = (cells: [number,number][]) => Math.max(...cells.map(c => c[1])) + + const collides = (cells: [number,number][], col: number, gridRow: number) => { + for (const [r, c] of cells) { + const gr = gridRow + r + const gc = col + c + if (gr >= ROWS || gc < 0 || gc >= COLS) return true + if (gr >= 0 && settled[gr][gc] !== null) return true + } + return false + } + + const settle = (p: Piece, gridRow: number) => { + for (const [r, c] of p.cells) { + const gr = gridRow + r + const gc = p.col + c + if (gr >= 0 && gr < ROWS && gc >= 0 && gc < COLS) + settled[gr][gc] = p.color + } } const tick = (now: number) => { const dt = Math.min(now - last, 50) last = now spawnIn -= dt - ctx.clearRect(0, 0, W, H) - // Spawn - if (spawnIn <= 0 && falling.length < 4) { - const free = Array.from({ length: cols }, (_, i) => i).filter(i => colFloor[i] > TILE_H) - if (free.length > 0) { - const col = free[Math.floor(Math.random() * free.length)] - const label = TETRIS_LABELS[Math.floor(Math.random() * TETRIS_LABELS.length)] - const color = TETRIS_COLORS[Math.floor(Math.random() * TETRIS_COLORS.length)] - falling.push({ col, y: -TILE_H, vy: 1.4 + Math.random() * 0.8, label, color }) + // Spawn new piece + if (spawnIn <= 0 && falling.length < 3) { + const tIdx = Math.floor(Math.random() * TETROMINOES.length) + const t = TETROMINOES[tIdx] + const rIdx = Math.floor(Math.random() * t.cells.length) + const cells = t.cells[rIdx] + const mxc = maxCol(cells) + const maxStartCol = COLS - mxc - 1 + if (maxStartCol >= 0) { + const col = Math.floor(Math.random() * (maxStartCol + 1)) + if (!collides(cells, col, 0)) { + falling.push({ + cells, + color: t.color, + col, + yPx: -(maxRow(cells) + 1) * CELL, + vy: 1.2 + Math.random() * 0.8, + }) + } } - spawnIn = 500 + Math.random() * 500 + spawnIn = 700 + Math.random() * 500 } - // Draw settled - for (const t of settled) drawTile(colX(t.col), t.y, t.label, t.color) + // Draw settled cells + for (let r = 0; r < ROWS; r++) + for (let c = 0; c < COLS; c++) + if (settled[r][c]) drawCell(c, r * CELL, settled[r][c]!) - // Update + draw falling + // Update + draw falling pieces for (let i = falling.length - 1; i >= 0; i--) { - const t = falling[i] - t.y += t.vy * dt / 16 - const land = colFloor[t.col] - TILE_H - if (t.y >= land) { - t.y = land - colFloor[t.col] = land - settled.push({ ...t }) + const p = falling[i] + p.yPx += p.vy * dt / 16 + const gridRow = Math.floor(p.yPx / CELL) + + if (collides(p.cells, p.col, gridRow + 1)) { + const snapRow = Math.max(0, gridRow) + settle(p, snapRow) falling.splice(i, 1) } else { - drawTile(colX(t.col), t.y, t.label, t.color) + for (const [r, c] of p.cells) + drawCell(p.col + c, p.yPx + r * CELL, p.color) } } @@ -143,7 +158,7 @@ function TetrisOverlay() { } rafId = requestAnimationFrame(tick) - }, 500) + }, 5000) return () => { clearTimeout(delay)