效果

404

您所访问的页面不存在或者已删除
点击小圆点,围住小猫
点击游戏区域重开一局

代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <!-- 移动端视口设置:适配各种屏幕,禁止缩放以保持点击精度(用户可自行缩放) -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=2.0, user-scalable=yes">
    <title>404 - 围住小猫 (原版魔性跑动复刻) · 移动适配</title>
    <style>
        /* 重置 & 基础响应式 */
        * { box-sizing: border-box; }
        body { 
            margin: 0; 
            background-color: #f7f8fa; 
            display: flex; 
            flex-direction: column; 
            align-items: center; 
            font-family: "Microsoft YaHei", sans-serif; 
            padding: 16px 8px;  /* 移动端四周留点空间 */
            min-height: 100vh;
        }
        .container { 
            background: white; 
            padding: 24px 16px;   /* 移动端减小内边距 */
            border-radius: 24px;   /* 稍微圆润 */
            box-shadow: 0 4px 20px rgba(0,0,0,0.05); 
            text-align: center; 
            width: 100%;
            max-width: 620px;      /* 略大于原600,但自适应 */
            margin: auto;
        }
        h1 { 
            font-size: clamp(48px, 15vw, 72px);  /* 响应式字体 */
            margin: 0; 
            color: #333; 
            font-weight: bold; 
            line-height: 1.1;
        }
        .msg-top { 
            color: #666; 
            font-size: clamp(14px, 4vw, 16px); 
            margin: 8px 0 12px;
        }
        .status-area { 
            min-height: 60px; 
            display: flex; 
            flex-direction: column; 
            align-items: center; 
            justify-content: center; 
            margin-bottom: 8px; 
        }
        .status-text { 
            font-size: clamp(16px, 5vw, 18px); 
            color: #1890ff; 
            font-weight: bold; 
            text-align: center;
            padding: 0 4px;
        }
        .status-text.win { color: #52c41a; }
        .status-text.lose { color: #f5222d; }
        .restart-hint { 
            font-size: clamp(12px, 3.5vw, 13px); 
            color: #999; 
            margin-top: 4px; 
            opacity: 0; 
            transition: 0.3s; 
        }
        .restart-hint.show { opacity: 1; }

        /* canvas 容器:让画布自适应,不溢出 */
        .canvas-box { 
            padding: 4px; 
            display: flex; 
            justify-content: center; 
            width: 100%; 
            overflow: hidden;   /* 防止极窄屏出现滚动条 */
        }
        canvas { 
            display: block; 
            cursor: pointer; 
            -webkit-tap-highlight-color: transparent;
            width: 100%;        /* 关键:宽度100%容器 */
            height: auto;       /* 高度自适应 */
            border-radius: 16px; /* 微微圆角 */
            background: #f0f4f8; /* 画布底色 (圆点间隙背景) */
            touch-action: manipulation; /* 禁止双击缩放 */
        }

        .link-row { 
            margin-top: 20px; 
            display: flex; 
            justify-content: space-between; 
            align-items: center; 
            width: 100%; 
            font-size: clamp(12px, 3.5vw, 13px); 
            color: #666; 
            padding: 0 4px;
        }
        .reset-text { 
            cursor: pointer; 
            color: #333; 
            font-weight: 500;
            padding: 8px 0;      /* 扩大点击区域 */
            display: inline-block;
        }
        .site-url { 
            color: #999; 
            text-decoration: none; 
            padding: 8px 0;
        }
        .button-group { 
            margin-top: 28px; 
            display: flex; 
            flex-wrap: wrap;      /* 手机空间不足时换行 */
            justify-content: center; 
            gap: 12px 16px; 
        }
        .pill-btn { 
            padding: 12px 32px;   /* 上下更大,易于点击 */
            border-radius: 40px; 
            border: none; 
            font-size: clamp(14px, 4vw, 15px); 
            cursor: pointer; 
            transition: 0.2s; 
            font-weight: bold; 
            text-decoration: none; 
            display: inline-flex;
            align-items: center;
            justify-content: center;
            background: #f0f2f5; 
            color: #666;
            min-width: 130px;     /* 保证基础宽度 */
            flex: 0 1 auto;
            -webkit-tap-highlight-color: transparent;
        }
        .btn-blue { 
            background: #1890ff; 
            color: white; 
        }
        .btn-blue:hover { background: #40a9ff; } /* 桌面保留,移动无碍 */
        .btn-grey:hover { background: #e4e6e9; }

        /* 对极窄屏优化 */
        @media (max-width: 420px) {
            .button-group { flex-direction: column; align-items: center; gap: 8px; }
            .pill-btn { width: 80%; }
            .container { padding: 20px 12px; }
        }
    </style>
</head>
<body>
  
<div class="container">
    <h1>404</h1>
    <div class="msg-top">您所访问的页面不存在或者已删除</div>
    <div class="status-area">
        <div id="status" class="status-text">点击小圆点,围住小猫</div>
        <div id="restartHint" class="restart-hint">点击游戏区域重开一局</div>
    </div>
    <div class="canvas-box">
        <canvas id="catCanvas"></canvas>
    </div>
    <div class="link-row">
        <span class="reset-text" id="resetLink">↻ 重置</span>
        <a href="https://www.baidu.com" class="site-url" target="_blank" rel="noopener">www.baidu.com</a>
    </div>
    <div class="button-group">
        <a href="#" class="pill-btn btn-blue" onclick="return false;">🏠 返回首页</a>
        <a href="#" class="pill-btn btn-grey" onclick="return false;">📜 查看版规</a>
    </div>
</div>
  
<script>
    (function() {
        const canvas = document.getElementById('catCanvas');
        const ctx = canvas.getContext('2d');
        const statusText = document.getElementById('status');
        const restartHint = document.getElementById('restartHint');
        const resetLink = document.getElementById('resetLink');

        // 网格参数 (保持不变,保证内部逻辑和绘制清晰)
        const ROWS = 11, COLS = 11;
        let RADIUS = 18;          // 基础半径,画布大小基于此计算
        const GAP = 8;

        // 动态计算画布实际像素尺寸 (依然基于RADIUS,但后续CSS会缩放)
        canvas.width = COLS * (RADIUS * 2 + GAP) + RADIUS + 15;
        canvas.height = ROWS * (RADIUS * 2 + 4) + 15;

        // 游戏状态变量
        let map = [], catPos = { r: 5, c: 5 }, catDrawPos = { x: 0, y: 0 }, catDir = 1;
        let isOver = false, isAnimating = false;
        let aniId = null;

        // 初始化地图 (随机障碍)
        function initMap() {
            if (aniId) cancelAnimationFrame(aniId);
            isOver = false; 
            isAnimating = false;
            statusText.innerText = "点击小圆点,围住小猫";
            statusText.className = "status-text";
            restartHint.className = "restart-hint";
            
            map = Array.from({ length: ROWS }, () => Array(COLS).fill(0));
            let count = 12 + Math.floor(Math.random() * 6);
            while (count > 0) {
                let r = Math.floor(Math.random() * ROWS), c = Math.floor(Math.random() * COLS);
                if (map[r][c] === 0 && !(r === 5 && c === 5)) { 
                    map[r][c] = 1; 
                    count--; 
                }
            }
            catPos = { r: 5, c: 5 };
            catDrawPos = getXY(catPos.r, catPos.c);
            startLoop();
        }

        // 根据行列获取画布内部坐标 (实际像素)
        function getXY(r, c) {
            const x = RADIUS + 8 + c * (RADIUS * 2 + GAP) + (r % 2 === 1 ? RADIUS : 0);
            const y = RADIUS + 8 + r * (RADIUS * 2 + 4);
            return { x, y };
        }

        // 绘制猫 (原版魔性弹跳跑)
        function drawCat(x, y, dir, isRunning) {
            if (isNaN(x) || isNaN(y)) return;
            ctx.save();
            
            // 弹跳幅度:跑动时快速上下,静止时缓慢呼吸
            const t = Date.now();
            let bobY = 0;
            if (isRunning) {
                bobY = Math.sin(t / 30) * 3.5; 
            } else {
                bobY = Math.sin(t / 400) * 0.8;
            }
    
            ctx.translate(x, y + bobY);
            ctx.scale(dir, 1);
    
            // 橘猫特征
            const furColor = "#FF8C00";
            const bellyColor = "#FFB347";
            
            // 身体
            ctx.fillStyle = furColor;
            ctx.beginPath();
            ctx.roundRect(-14, -8, 28, 16, 8);
            ctx.fill();
            // 肚子
            ctx.fillStyle = bellyColor;
            ctx.beginPath();
            ctx.ellipse(0, 2, 10, 5, 0, 0, Math.PI*2);
            ctx.fill();
    
            // 头
            ctx.fillStyle = furColor;
            ctx.beginPath();
            ctx.arc(12, -6, 11, 0, Math.PI * 2);
            ctx.fill();
    
            // 耳朵
            ctx.beginPath(); ctx.moveTo(8, -14); ctx.lineTo(16, -14); ctx.lineTo(12, -24); ctx.fill();
            ctx.beginPath(); ctx.moveTo(14, -12); ctx.lineTo(22, -10); ctx.lineTo(19, -20); ctx.fill();
    
            // 眼睛
            ctx.fillStyle = "#FFF";
            ctx.beginPath(); ctx.arc(16, -7, 2.5, 0, Math.PI*2); ctx.fill();
            ctx.fillStyle = "#000";
            ctx.beginPath(); ctx.arc(17, -7, 1.2, 0, Math.PI*2); ctx.fill();
    
            // 腿
            ctx.fillStyle = furColor;
            ctx.beginPath(); ctx.roundRect(8, 5, 5, 10, 2.5); ctx.fill();
            ctx.beginPath(); ctx.roundRect(-12, 5, 5, 10, 2.5); ctx.fill();
    
            // 尾巴
            ctx.lineWidth = 4; ctx.strokeStyle = furColor; ctx.lineCap = "round";
            ctx.beginPath(); ctx.moveTo(-14, -2); ctx.quadraticCurveTo(-24, -8, -22, -18); ctx.stroke();
    
            ctx.restore();
        }

        // 工具:roundRect
        CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
            if (w < 2 * r) r = w / 2;
            if (h < 2 * r) r = h / 2;
            this.moveTo(x + r, y);
            this.lineTo(x + w - r, y);
            this.quadraticCurveTo(x + w, y, x + w, y + r);
            this.lineTo(x + w, y + h - r);
            this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
            this.lineTo(x + r, y + h);
            this.quadraticCurveTo(x, y + h, x, y + h - r);
            this.lineTo(x, y + r);
            this.quadraticCurveTo(x, y, x + r, y);
            return this;
        };

        // 动画循环
        function startLoop() {
            const loop = () => {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                // 绘制所有圆点
                for (let r = 0; r < ROWS; r++) {
                    for (let c = 0; c < COLS; c++) {
                        const { x, y } = getXY(r, c);
                        ctx.beginPath(); 
                        ctx.arc(x, y, RADIUS, 0, Math.PI * 2);
                        ctx.fillStyle = map[r][c] === 1 ? "#34495e" : "#b3d9ff"; 
                        ctx.fill();
                    }
                }
                const catBase = getXY(catPos.r, catPos.c);
                if (!isAnimating || (isAnimating && !isOver)) {
                    // 猫所在位置圆点用背景色覆盖,再画猫
                    ctx.beginPath(); 
                    ctx.arc(catBase.x, catBase.y, RADIUS, 0, Math.PI*2);
                    ctx.fillStyle = "#b3d9ff"; 
                    ctx.fill();
                }
                // 绘制猫 (跑动状态 isAnimating && !isOver)
                drawCat(catDrawPos.x, catDrawPos.y, catDir, isAnimating && !isOver);
                aniId = requestAnimationFrame(loop);
            };
            loop();
        }

        // 获取相邻格子 (六边形规则)
        function getAdj(r, c) {
            let n = [[r, c-1], [r, c+1]];
            if (r % 2 === 0) n.push([r-1, c-1], [r-1, c], [r+1, c-1], [r+1, c]);
            else n.push([r-1, c], [r-1, c+1], [r+1, c], [r+1, c+1]);
            return n.filter(p => p[0]>=0 && p[0]<ROWS && p[1]>=0 && p[1]<COLS);
        }

        // BFS寻路到边界
        function findPath(startR, startC) {
            let queue = [{ r: startR, c: startC, path: [] }], visited = new Set([`${startR},${startC}`]);
            while (queue.length > 0) {
                let curr = queue.shift();
                if (curr.r === 0 || curr.r === ROWS - 1 || curr.c === 0 || curr.c === COLS - 1) 
                    return curr.path;
                for (let [nr, nc] of getAdj(curr.r, curr.c)) {
                    if (map[nr][nc] === 0 && !visited.has(`${nr},${nc}`)) {
                        visited.add(`${nr},${nc}`);
                        queue.push({ r: nr, c: nc, path: curr.path.concat({ r: nr, c: nc }) });
                    }
                }
            }
            return null;
        }

        // 猫移动一步 (被点击后触发)
        function catMove() {
            if (isOver || isAnimating) return;
            let path = findPath(catPos.r, catPos.c);
            if (!path) {
                isOver = true; 
                statusText.innerText = "猫已经无路可走,你赢了";
                statusText.className = "status-text win"; 
                restartHint.className = "restart-hint show";
                return;
            }
            isAnimating = true;
            const next = path[0], startXY = { ...catDrawPos }, endXY = getXY(next.r, next.c);
            catDir = endXY.x < startXY.x ? -1 : 1;
            catPos = next;
            
            let startTime = null;
            const duration = 180; // 跑动速度
            const aniStep = (time) => {
                if (!startTime) startTime = time;
                const p = Math.min((time - startTime) / duration, 1);
                catDrawPos.x = startXY.x + (endXY.x - startXY.x) * p;
                catDrawPos.y = startXY.y + (endXY.y - startXY.y) * p;
                if (p < 1) {
                    requestAnimationFrame(aniStep);
                } else {
                    isAnimating = false;
                    if (catPos.r === 0 || catPos.r === ROWS-1 || catPos.c === 0 || catPos.c === COLS-1) {
                        isOver = true; 
                        statusText.innerText = "猫已经跑到地图边缘了,你输了";
                        statusText.className = "status-text lose"; 
                        restartHint.className = "restart-hint show";
                    }
                }
            };
            requestAnimationFrame(aniStep);
        }

        // ---------- 核心:响应式坐标转换事件处理 ----------
        function handleCanvasInteraction(e) {
            // 阻止触摸时的滚动和双击缩放 (但保留手势)
            e.preventDefault();  // 对触摸有效
            if (isOver) { 
                initMap(); 
                return; 
            }
            if (isAnimating) return;

            // 获取触点坐标 (兼容触摸和鼠标)
            let clientX, clientY;
            if (e.touches && e.touches.length > 0) {
                clientX = e.touches[0].clientX;
                clientY = e.touches[0].clientY;
            } else if (e.clientX !== undefined) {
                clientX = e.clientX;
                clientY = e.clientY;
            } else {
                return; // 未知事件
            }

            const rect = canvas.getBoundingClientRect();
            if (rect.width === 0) return;

            // 计算画布内部像素坐标 (将CSS坐标映射到canvas实际像素)
            const scaleX = canvas.width / rect.width;
            const scaleY = canvas.height / rect.height;
            const canvasX = (clientX - rect.left) * scaleX;
            const canvasY = (clientY - rect.top) * scaleY;

            // 遍历所有圆点,检查是否点到空白点且不是猫当前位置
            for (let r = 0; r < ROWS; r++) {
                for (let c = 0; c < COLS; c++) {
                    const { x, y } = getXY(r, c);
                    const dist = Math.hypot(canvasX - x, canvasY - y);
                    if (dist < RADIUS - 2) {  // 点击到了圆点内部
                        if (map[r][c] === 0 && !(r === catPos.r && c === catPos.c)) {
                            statusText.innerText = `您点击了 (${r}, ${c})`;
                            map[r][c] = 1; 
                            catMove();
                        }
                        return; // 只处理一次点击
                    }
                }
            }
        }

        // 添加事件监听 (同时支持鼠标和触摸)
        canvas.addEventListener('mousedown', handleCanvasInteraction);
        canvas.addEventListener('touchstart', handleCanvasInteraction, { passive: false });
        // 防止移动端点击时出现菜单/缩放 (已经preventDefault)

        // 重置链接
        resetLink.addEventListener('click', (e) => {
            e.preventDefault();
            initMap();
        });

        // 初始化游戏
        initMap();

        // 可选:如果用户点击了返回首页/版规按钮,不跳转 (演示用)
        // 但保留链接行为,这里只是演示,你可以替换为真实链接
    })();
</script>
</body>
</html>