feat(lapaloma-phosphor): trail CRT con alpha decay + glow
- lapaloma-phosphor: feature `gpui` (default). LapalomaPhosphorElement divide el RingBuffer en N segmentos (default 16, configurable) y pinta cada uno como una stroke_polyline con alpha = (k+1)/N. El segmento más nuevo va con alpha 1.0, el más viejo casi transparente — efecto fósforo persistente. - Cada segmento incluye el primer punto del siguiente para evitar gaps visibles entre tramos. - Wraparound se parte en dos sub-polilíneas (no concatenadas) para no introducir la línea horizontal "del slot cap-1 al slot 0". - Glow opcional: pasada adicional con width × glow_width_mult y alpha × glow_alpha — efecto halo CRT. - crates/apps/lapaloma-phosphor-demo: misma señal sintética que stream-demo, paleta verde Tektronix (#9bff8c sobre #050805), trail 24 segs + glow 4× α 0.18. Limitación v0.1: el doc canónico usa triangle strip con per-vertex color (sección 4.3); GPUI 0.2 no expone esa API directa. La impl actual es funcionalmente equivalente con N draw calls en lugar de 1. Cuando wgpu directo esté disponible, swap inmediato sin tocar las API públicas. 46 tests verdes (sin cambios; phosphor se valida via demo). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
//! `lapaloma-phosphor-demo` — osciloscopio con trail CRT.
|
||||
//!
|
||||
//! Igual setup que `lapaloma-stream-demo` (RingBuffer 512 +
|
||||
//! timer 60 Hz) pero el render usa `LapalomaPhosphorElement`:
|
||||
//! el trail decae en alpha del cursor hacia atrás y arrastra un
|
||||
//! halo (glow). Visualmente queda como un osciloscopio analógico
|
||||
//! con fósforo persistente.
|
||||
//!
|
||||
//! Sliders para `trail_segments` y `glow` se dejan para más
|
||||
//! adelante; este demo usa los defaults.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{div, prelude::*, px, Context, IntoElement, Render, Window};
|
||||
|
||||
use lapaloma_core::ring::RingBuffer;
|
||||
use lapaloma_phosphor::lapaloma_phosphor;
|
||||
use lapaloma_render::{Color, StrokeStyle};
|
||||
use yahweh_launcher::launch_app;
|
||||
use yahweh_theme::Theme;
|
||||
|
||||
const RING_CAPACITY: usize = 512;
|
||||
const SAMPLE_PERIOD: Duration = Duration::from_millis(16);
|
||||
|
||||
fn main() {
|
||||
launch_app(
|
||||
"Lapaloma — phosphor trail (CRT 60 Hz)",
|
||||
(900., 480.),
|
||||
PhosphorDemo::new,
|
||||
);
|
||||
}
|
||||
|
||||
struct PhosphorDemo {
|
||||
buffer: RingBuffer,
|
||||
t: u64,
|
||||
}
|
||||
|
||||
impl PhosphorDemo {
|
||||
fn new(cx: &mut Context<Self>) -> Self {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let timer = cx.background_executor().clone();
|
||||
loop {
|
||||
timer.timer(SAMPLE_PERIOD).await;
|
||||
let r = this.update(cx, |me, cx| {
|
||||
me.tick();
|
||||
cx.notify();
|
||||
});
|
||||
if r.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
buffer: RingBuffer::new(RING_CAPACITY),
|
||||
t: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn tick(&mut self) {
|
||||
let v = synthesize(self.t);
|
||||
self.buffer.push(v);
|
||||
self.t = self.t.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn synthesize(t: u64) -> f32 {
|
||||
let phase = t as f32;
|
||||
let signal = (phase * 0.07).sin() * 0.75 + (phase * 0.19).sin() * 0.22;
|
||||
let jitter = ((phase * 37.0).sin() * 1000.0).fract() * 0.04;
|
||||
signal + jitter
|
||||
}
|
||||
|
||||
impl Render for PhosphorDemo {
|
||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = Theme::global(cx).clone();
|
||||
|
||||
// Verde fósforo CRT clásico (Tektronix vibes).
|
||||
let plot_bg = Color::rgba(0.03, 0.05, 0.04, 1.0);
|
||||
let trace = StrokeStyle::new(1.6, Color::from_hex(0x9bff8c));
|
||||
|
||||
let phosphor = lapaloma_phosphor(self.buffer.clone(), trace)
|
||||
.background(plot_bg)
|
||||
.y_range(-1.2, 1.2)
|
||||
.trail_segments(24)
|
||||
.glow(4.0, 0.18);
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.bg(theme.bg_app.clone())
|
||||
.p(px(16.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(10.))
|
||||
.child(
|
||||
div()
|
||||
.text_color(theme.fg_text)
|
||||
.text_size(px(18.))
|
||||
.child("Lapaloma — phosphor"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap(px(16.))
|
||||
.text_size(px(11.))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(format!("cap = {}", RING_CAPACITY))
|
||||
.child(format!("head = {}", self.buffer.head()))
|
||||
.child("trail = 24 segs")
|
||||
.child("glow = 4× / α 0.18")
|
||||
.child(format!("t = {}", self.t)),
|
||||
)
|
||||
.child(div().w_full().flex_grow().child(phosphor))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user