⎇ d3649bfd1a80153652c2a028315369bb4b184024
crates/apps/lapaloma-stream-demo/src/main.rs
Historia
Quién tocó qué
crudo
feat(lapaloma-stream): osciloscopio CRT con RingBuffer en sweep mode
1//! `lapaloma-stream-demo` — osciloscopio sintético.
2//!
3//! Ventana con un `LapalomaStreamElement` montado sobre un
4//! `RingBuffer` de 512 slots. Un timer en el background executor
5//! empuja un sample cada **16 ms** (≈ 60 Hz) y dispara
6//! `cx.notify()`. El sample es la suma de dos sinusoides desfasadas
7//! más un poquito de ruido determinístico.
8//!
9//! El efecto visual: la traza barre la ventana como en un
10//! osciloscopio CRT — split-at-head deja un "cursor" donde
11//! arranca la traza fresca, la traza vieja se mantiene a la
12//! derecha hasta que el cursor la sobrescriba.
13//!
14//! Showcase del **P2 zero-alloc en hot path**: el `push(v)` del
15//! RingBuffer son 2 escrituras + 2 increments. Cero allocations
16//! por frame, ningún `Vec` se reasigna.
17
18use std::time::Duration;
19
20use gpui::{div, prelude::*, px, Context, IntoElement, Render, Window};
21
22use lapaloma_core::ring::RingBuffer;
23use lapaloma_render::{Color, StrokeStyle};
24use lapaloma_stream::lapaloma_stream;
25use yahweh_launcher::launch_app;
26use yahweh_theme::Theme;
27
28const RING_CAPACITY: usize = 512;
29const SAMPLE_PERIOD: Duration = Duration::from_millis(16);
30
31fn main() {
32 launch_app(
33 "Lapaloma — stream (osciloscopio sintético 60 Hz)",
34 (900., 480.),
35 StreamDemo::new,
36 );
37}
38
39struct StreamDemo {
40 buffer: RingBuffer,
41 /// Tick count global. Sirve de fase para la señal sintética y
42 /// se muestra en el header para verificar que el timer corre.
43 t: u64,
44}
45
46impl StreamDemo {
47 fn new(cx: &mut Context<Self>) -> Self {
48 cx.spawn(async move |this, cx| {
49 let timer = cx.background_executor().clone();
50 loop {
51 timer.timer(SAMPLE_PERIOD).await;
52 let r = this.update(cx, |me, cx| {
53 me.tick();
54 cx.notify();
55 });
56 if r.is_err() {
57 break;
58 }
59 }
60 })
61 .detach();
62
63 Self {
64 buffer: RingBuffer::new(RING_CAPACITY),
65 t: 0,
66 }
67 }
68
69 fn tick(&mut self) {
70 let v = synthesize(self.t);
71 self.buffer.push(v);
72 self.t = self.t.wrapping_add(1);
73 }
74}
75
76/// Señal sintética: suma de dos sinusoides + jitter determinístico.
77/// El rango efectivo queda en `[-1, 1]` aproximadamente.
78fn synthesize(t: u64) -> f32 {
79 let phase = t as f32;
80 let signal = (phase * 0.07).sin() * 0.75 + (phase * 0.19).sin() * 0.22;
81 let jitter = ((phase * 37.0).sin() * 1000.0).fract() * 0.04;
82 signal + jitter
83}
84
85impl Render for StreamDemo {
86 fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
87 let theme = Theme::global(cx).clone();
88
89 let plot_bg = Color::rgba(0.08, 0.10, 0.13, 1.0);
90 let stroke = StrokeStyle::new(1.8, Color::from_hex(0xa3be8c));
91
92 let stream = lapaloma_stream(self.buffer.clone(), stroke)
93 .background(plot_bg)
94 .y_range(-1.2, 1.2);
95
96 let fill_pct = (self.buffer.filled_len() * 100) / RING_CAPACITY;
97
98 div()
99 .size_full()
100 .bg(theme.bg_app.clone())
101 .p(px(16.))
102 .flex()
103 .flex_col()
104 .gap(px(10.))
105 .child(
106 div()
107 .text_color(theme.fg_text)
108 .text_size(px(18.))
109 .child("Lapaloma — stream"),
110 )
111 .child(
112 div()
113 .flex()
114 .gap(px(16.))
115 .text_size(px(11.))
116 .text_color(theme.fg_muted)
117 .child(format!("cap = {}", RING_CAPACITY))
118 .child(format!("head = {}", self.buffer.head()))
119 .child(format!("filled = {}%", fill_pct))
120 .child(format!("t = {}", self.t))
121 .child(format!("rev = {}", self.buffer.revision())),
122 )
123 .child(div().w_full().flex_grow().child(stream))
124 }
125}