You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

能否在Dash中使用交互式HTML5 Canvas实现可自定义的生命游戏交互网格?

在Dash中实现交互式HTML5 Canvas版康威生命游戏

当然可以!在Dash里实现交互式的HTML5 Canvas版康威生命游戏完全可行,而且能轻松实现你想要的点击切换单元格、调整画布尺寸这些功能。我给你整理了一套完整的实现方案,包含核心逻辑和交互功能:

核心思路

Dash允许直接嵌入html.Canvas组件,我们可以结合:

  • Dash的回调函数处理尺寸设置、游戏启停等用户操作
  • JavaScript监听Canvas的点击事件,实现单元格的激活/停用
  • Python后端处理康威生命游戏的核心规则计算,兼顾逻辑清晰和交互流畅性

下面是一个完整的可运行示例,融合了前端交互和后端逻辑:

完整实现代码

from dash import Dash, html, dcc, Input, Output, State, callback
import json
import numpy as np

app = Dash(__name__)

app.layout = html.Div([
    html.H1("康威生命游戏 (Conway's Game of Life)"),
    html.Div([
        html.Label("画布尺寸(单元格数):"),
        dcc.Input(id="grid-size", type="number", value=50, min=10, max=100),
        html.Button("重置画布", id="reset-btn", n_clicks=0),
        html.Button("开始/暂停", id="toggle-btn", n_clicks=0)
    ], style={"margin": "10px 0"}),
    html.Canvas(id="life-canvas", width=500, height=500, style={"border": "1px solid #000"}),
    dcc.Interval(id="game-interval", interval=200, disabled=True),
    # 用于存储网格状态的隐藏组件
    dcc.Store(id="grid-state")
])

# 初始化网格状态
@callback(
    Output("grid-state", "data"),
    Input("grid-size", "value"),
    Input("reset-btn", "n_clicks"),
    prevent_initial_call=False
)
def init_grid(grid_size, reset_clicks):
    # 创建随机初始网格,80%概率为死细胞
    grid = np.random.choice([0, 1], size=(grid_size, grid_size), p=[0.8, 0.2]).tolist()
    return json.dumps(grid)

# 更新Canvas尺寸
@callback(
    Output("life-canvas", "width"),
    Output("life-canvas", "height"),
    Input("grid-size", "value")
)
def update_canvas_size(grid_size):
    # 每个单元格对应10px,保证画布尺寸适配网格
    canvas_size = grid_size * 10
    return canvas_size, canvas_size

# 切换游戏启停
@callback(
    Output("game-interval", "disabled"),
    Input("toggle-btn", "n_clicks"),
    State("game-interval", "disabled"),
    prevent_initial_call=True
)
def toggle_game(n_clicks, current_state):
    return not current_state

# 计算下一代网格状态
@callback(
    Output("grid-state", "data"),
    Input("game-interval", "n_intervals"),
    State("grid-state", "data"),
    prevent_initial_call=True
)
def update_grid(n_intervals, grid_data):
    grid = np.array(json.loads(grid_data))
    rows, cols = grid.shape
    new_grid = grid.copy()

    # 康威生命游戏核心规则
    for i in range(rows):
        for j in range(cols):
            # 计算周围8个邻居的存活细胞数(排除自身)
            neighbors = np.sum(grid[max(0, i-1):min(i+2, rows), max(0, j-1):min(j+2, cols)]) - grid[i, j]
            if grid[i, j] == 1:
                # 存活细胞:邻居数<2或>3则死亡
                if neighbors < 2 or neighbors > 3:
                    new_grid[i, j] = 0
            else:
                # 死亡细胞:邻居数=3则复活
                if neighbors == 3:
                    new_grid[i, j] = 1
    return json.dumps(new_grid.tolist())

# 绘制Canvas内容 + 处理点击交互
app.clientside_callback(
    """
    function(grid_data, canvas_width, n_clicks) {
        const canvas = document.getElementById('life-canvas');
        const ctx = canvas.getContext('2d');
        const grid = JSON.parse(grid_data);
        const gridSize = grid.length;
        const cellSize = canvas_width / gridSize;

        // 清空画布
        ctx.clearRect(0, 0, canvas_width, canvas_width);

        // 绘制网格单元格
        for (let i = 0; i < gridSize; i++) {
            for (let j = 0; j < gridSize; j++) {
                ctx.fillStyle = grid[i][j] === 1 ? '#2c3e50' : '#ecf0f1';
                ctx.fillRect(j*cellSize, i*cellSize, cellSize-1, cellSize-1);
            }
        }

        // 绑定点击事件(确保仅绑定一次)
        if (!canvas.dataset.clickBound) {
            canvas.addEventListener('click', function(e) {
                const rect = canvas.getBoundingClientRect();
                const x = Math.floor((e.clientX - rect.left) / cellSize);
                const y = Math.floor((e.clientY - rect.top) / cellSize);
                // 切换当前单元格的激活/停用状态
                grid[y][x] = grid[y][x] === 1 ? 0 : 1;
                // 更新存储的网格状态
                const store = document.getElementById('grid-state');
                store.value = JSON.stringify(grid);
                // 手动触发绘制更新
                dash_clientside.callback_context.triggered.map(t => t.prop_id).includes('grid-state.data');
            });
            canvas.dataset.clickBound = true;
        }

        return null;
    }
    """,
    Output("life-canvas", "children"),
    Input("grid-state", "data"),
    Input("life-canvas", "width"),
    Input("reset-btn", "n_clicks"),
    prevent_initial_call=False
)

if __name__ == "__main__":
    app.run_server(debug=True)

关键功能解析

1. 画布尺寸设置

  • 通过dcc.Input组件让用户自定义单元格数量,回调函数update_canvas_size自动调整Canvas宽高(每个单元格对应10px)
  • 点击「重置画布」按钮会重新生成随机初始网格,适配新的尺寸

2. 点击单元格切换状态

  • 使用客户端回调(clientside_callback)绑定Canvas的点击事件,减少前后端通信开销
  • 计算点击位置对应的单元格坐标,实时切换其激活/停用状态,并更新存储的网格状态

3. 生命游戏逻辑

  • 后端用NumPy高效计算下一代网格状态,严格遵循康威的四条规则
  • 通过dcc.Interval组件定时触发状态更新,实现动画效果,点击「开始/暂停」可随时控制游戏运行

4. 自定义空间

你可以根据需求调整:

  • 单元格大小:修改canvas_size = grid_size * 10中的倍数
  • 动画速度:调整dcc.Intervalinterval参数(单位毫秒)
  • 颜色样式:修改客户端回调中的fillStyle

内容的提问来源于stack exchange,提问作者user2253054

火山引擎 最新活动