You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Tokamak中实现视图状态变更时Canvas自动重绘的最优方案问询

可靠触发Canvas重绘的Tokamak原生方案

你当前用setTimeout加手动订阅objectWillChange的方式确实是临时hack,容易带来时序问题和维护成本。其实Tokamak本身提供了更贴合其状态管理流程的解决方案,既能让框架负责状态更新,又能精准触发Canvas重绘。

核心思路

放弃手动订阅ObservableObject的变更事件,转而利用Tokamak的视图生命周期修饰符onChange,它会在状态更新完成且DOM同步后触发回调,完全不需要setTimeout这种延迟手段。同时,我们可以直接用DynamicHTML创建<canvas>元素,避免嵌套div的额外操作,还能解决Tokamak重置画布尺寸的问题。

优化后的完整代码

import JavaScriptKit
import TokamakDOM
import CombineShim

let global = JSObject.global
let document = global.document

/// 示例游戏状态,仅指定渲染到`<canvas>`元素的圆形的暗度。
class GameState: ObservableObject {
    @TokamakDOM.Published var circleGrayLevel = 0.0
}

struct Canvas: View {
    /// 当前Canvas实例应渲染的状态。
    @ObservedObject var gameState: GameState
    /// 获取Tokamak创建的`<canvas>`元素引用
    @State private var canvas: JSObject!

    private func paint() {
        guard let ctx = canvas.getContext!("2d").object else { return }
        // 仅当状态变化时重绘(你可以在这里实现局部重绘逻辑)
        let v = (1 - gameState.circleGrayLevel) * 255
        ctx.fillStyle = .string("rgb(\(v) \(v) \(v))")
        
        _ = ctx.clearRect(0, 0, canvas.width, canvas.height)
        _ = ctx.beginPath()
        _ = ctx.arc(100, 75, 50, 0, 2 * Double.pi);
        _ = ctx.fill()
        _ = ctx.stroke()
    }

    var body: some View {
        DynamicHTML(
            "canvas", 
            // 仅初始化时设置尺寸,后续更新不会修改,避免画布清空
            ["width": "\(300)", "height": "\(300)"], 
            listeners: [:]
        )
        ._domRef($canvas)
        .onAppear(perform: {
            // 设置画布样式
            canvas.style.imageRendering = "pixelated"
            // 首次绘制
            paint()
        })
        // 监听状态变化,DOM更新完成后触发重绘
        .onChange(of: gameState.circleGrayLevel) { _ in
            paint()
        }
    }
}

struct GameApp: App {
    @StateObject private var gameState = GameState()
    var body: some Scene {
        WindowGroup("Test") {
            VStack {
                Canvas(gameState: gameState)
                Slider(value: $gameState.circleGrayLevel, in: 0...1)
            }
        }
    }
}

GameApp.main()

关键改进点

  • 移除手动订阅与setTimeout:用.onChange(of: gameState.circleGrayLevel)替代手动订阅objectWillChange,这个修饰符会在状态值变更且视图完成DOM更新后执行回调,确保你访问的是最新的状态,无需延迟。
  • 直接创建Canvas元素:不再需要嵌套div,直接用DynamicHTML("canvas")创建画布元素,通过_domRef获取引用。将width/height作为静态属性传入DynamicHTML的属性字典,这样Tokamak只会在初始化时设置一次,后续更新不会修改这些属性,避免了画布缓冲区被清空的问题。
  • 更贴合Tokamak的状态流:现在状态管理完全由Tokamak负责,你只需要关注状态变化时的重绘逻辑,不需要手动维护订阅生命周期,代码更简洁可靠。

局部重绘的支持

针对你提到的「每次更新仅需重绘部分元素」的需求,这个方案完全兼容。你可以在paint()函数中加入逻辑判断,比如记录上一次的状态变化范围,只重绘受影响的区域(比如特定的tiles),而不需要全量清空画布。Tokamak的状态更新只会触发paint()调用,具体重绘范围由你完全控制。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 22:49:07