eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import SwiftUI
2
3// 🤖 The brand smoke-and-motes scene as a standalone, self-driving surface —
4// the same scene as web's bootscene.js (the home loader / ambient haze). The
5// particle system itself lives in CreateSpaceView.swift (AmbientField +
6// AmbientCanvas, "home's bootscene, the quiet sibling"); this wrapper adds
7// only a driver so any transition can drop the scene in without borrowing the
8// create-space model. First use: iOS's space switch fades through it while
9// the next space's chats load. The Mac could adopt it later (its sidebar +
10// detail switch is a different shape).
11public struct SmokeSceneView: View {
12 @StateObject private var driver = SmokeSceneDriver()
13
14 public init() {}
15
16 public var body: some View {
17 // Measured full-bleed so motes genuinely rise from below the physical
18 // screen edge (the same lesson as CreateSpaceView's stage sizing).
19 GeometryReader { geo in
20 AmbientCanvas(field: driver.field, tick: driver.tick)
21 .onAppear {
22 driver.size = geo.size
23 driver.start()
24 }
25 .onChange(of: geo.size) { _, s in driver.size = s }
26 .onDisappear { driver.stop() }
27 }
28 .allowsHitTesting(false)
29 .ignoresSafeArea()
30 }
31}
32
33/// The scene's 60Hz heartbeat. An explicit Timer, not TimelineView(.animation),
34/// for the same reason CreateSpaceModel drives its sim with one: TimelineView
35/// silently stopped ticking on device and the smoke froze. The published tick
36/// is what forces the Canvas to redraw (the field is a class — an identical
37/// reference frame-to-frame would skip the redraw).
38@MainActor
39final class SmokeSceneDriver: ObservableObject {
40 @Published private(set) var tick = 0
41 let field = AmbientField()
42 var size: CGSize = .zero
43 private var timer: Timer?
44 private var lastT: TimeInterval?
45
46 func start() {
47 guard timer == nil else { return }
48 let t = Timer(timeInterval: 1.0 / 60, repeats: true) { [weak self] _ in
49 MainActor.assumeIsolated {
50 guard let self else { return }
51 let now = Date().timeIntervalSinceReferenceDate
52 let dt = min(0.1, max(0, now - (self.lastT ?? now)))
53 self.lastT = now
54 self.field.step(dt: dt, size: self.size)
55 self.tick &+= 1
56 }
57 }
58 // .common so the scene keeps drifting through any gesture tracking.
59 RunLoop.main.add(t, forMode: .common)
60 timer = t
61 }
62
63 func stop() {
64 timer?.invalidate()
65 timer = nil
66 lastT = nil
67 }
68}
69