1#![allow(
17 clippy::suboptimal_flops,
18 reason = "straight port of the omp-welcome-term prototype; mul_add obscures the math"
19)]
20
21use std::{
22 f32::consts::{PI, TAU},
23 time::Duration,
24};
25
26use omp_core::{Str, fmts};
27use omp_tui::{
28 Border, Charset, Color, Frame, Icon, Rect, Size, Style,
29 anim::{Easing, Lerp, Tween},
30 scene::{self, Camera, Ray, Trace, Vec3, vec3},
31 shader::{Eclipse, Surface},
32};
33
34const CARD_BG: Color = Color::Rgb(11, 14, 21);
36const CARD_BORDER: Color = Color::Rgb(43, 51, 68);
37const FOOTER_BG: Color = Color::Rgb(7, 10, 15);
38const SELECTED_BG: Color = Color::Rgb(13, 21, 27);
39const TEXT: Color = Color::Rgb(226, 232, 240);
40const TEXT_STRONG: Color = Color::Rgb(248, 250, 252);
41const MUTED: Color = Color::Rgb(100, 116, 139);
42const FAINT: Color = Color::Rgb(51, 65, 85);
43const CYAN: Color = Color::Rgb(56, 189, 248);
44const INDIGO: Color = Color::Rgb(129, 140, 248);
45const VIOLET: Color = Color::Rgb(192, 132, 252);
46const GREEN: Color = Color::Rgb(52, 211, 153);
47const AMBER: Color = Color::Rgb(251, 191, 36);
48
49const CARD_COLS: u16 = 90;
51const CARD_ROWS: u16 = 15;
52const SMOL_COLS: u16 = 33;
54
55const HOVER_EASE: Duration = Duration::from_millis(160);
57const AMBIENT_REVEAL_INTERVAL: Duration = Duration::from_millis(40);
60const LOGO_IDLE_INTERVAL: Duration = Duration::from_millis(120);
61const BACKDROP_IDLE_INTERVAL: Duration = Duration::from_millis(80);
62const AMBIENT_REVEAL_END: Duration = Duration::from_millis(1_200);
63const PLATE: Color = Color::Rgb(9, 9, 11);
66const BACKDROP_FADE: f32 = 1.2;
69
70fn ambient_interval(clock: Duration, idle: Duration) -> Duration {
71 if clock < AMBIENT_REVEAL_END {
72 AMBIENT_REVEAL_INTERVAL
73 } else {
74 idle
75 }
76}
77
78const SESSIONS: [(&str, &str); 4] = [
79 ("Optimize custom status widget rendering", "NOW"),
80 ("Check Unicode character display", "01m"),
81 ("Add ╰─ cursor shift", "02m"),
82 ("The Unicode Similarity", "18m"),
83];
84
85const DUST: [(u16, u16, f32); 9] = [
86 (3, 3, 0.1),
87 (6, 9, 1.7),
88 (9, 2, 2.5),
89 (12, 10, 4.4),
90 (18, 2, 5.2),
91 (21, 11, 3.1),
92 (25, 3, 1.2),
93 (28, 9, 4.8),
94 (30, 6, 0.8),
95];
96const BEAM: [(u16, u16, &str); 8] = [
97 (27, 7, "╱"),
98 (28, 6, "╱"),
99 (29, 6, "─"),
100 (30, 5, "╱"),
101 (31, 5, "─"),
102 (32, 4, "╱"),
103 (33, 4, "━"),
104 (34, 4, "━"),
105];
106const HORIZON: &str = "· · · · · · · · · · · · · · · ";
107
108const LOGO_COLS: usize = 25;
110const LOGO_ROWS: usize = 10;
111
112const DISK_RADIUS: f32 = 0.82;
113const DISK_Y: f32 = 0.12;
114const DISK_HALF_THICKNESS: f32 = 0.045;
115const DISK_GLASS_OPACITY: f32 = 0.56;
116const AXIS_RADIUS: f32 = 0.047;
117const AXIS_BOTTOM: f32 = -0.78;
118const AXIS_TOP: f32 = 1.14;
119const FLOOR_Y: f32 = -0.84;
120
121const CAMERA_DISTANCE: f32 = 4.176;
124const CAMERA_PITCH: f32 = 0.4391;
125const CAMERA_FOCAL: f32 = 2.72;
126
127const DISK_STOPS: [Vec3; 4] = [
129 Vec3::rgb(56, 189, 248),
130 Vec3::rgb(129, 140, 248),
131 Vec3::rgb(192, 132, 252),
132 Vec3::rgb(56, 189, 248),
133];
134const BACKGROUND: Vec3 = Vec3::rgb(11, 14, 21);
135const WHITE: Vec3 = Vec3::rgb(248, 250, 252);
136const INK: Vec3 = Vec3::rgb(3, 4, 7);
137
138type LogoGrid = [[Option<(char, Color)>; LOGO_COLS]; LOGO_ROWS];
139
140pub struct Welcome {
144 frame: Frame,
145 title: Str,
146 charset: Charset,
148 camera: (f32, f32),
149 camera_target: (f32, f32),
150 last_elapsed: f32,
151 logo_origin: (u16, u16),
153 logo: LogoGrid,
154 logo_at: Option<Duration>,
155 backdrop_frame: Frame,
156 backdrop_at: Option<Duration>,
157 backdrop: Eclipse,
159 surface: Surface,
160 pointer: Option<(u16, u16)>,
162 hover: Tween<f32>,
164}
165
166impl Welcome {
167 pub fn new(charset: Charset) -> Self {
168 Self {
169 charset,
170 frame: Frame::new(Size::new(0, 0)),
171 title: fmts!(" {} omp v{} ", charset.icon(Icon::Omp), env!("CARGO_PKG_VERSION")),
172 camera: (0.0, 0.0),
173 camera_target: (0.0, 0.0),
174 last_elapsed: 0.0,
175 logo_origin: (0, 0),
176 logo: [[None; LOGO_COLS]; LOGO_ROWS],
177 logo_at: None,
178 backdrop_frame: Frame::new(Size::new(0, 0)),
179 backdrop_at: None,
180 backdrop: Eclipse::default(),
181 surface: Surface::new(),
182 pointer: None,
183 hover: Tween::settled(0.0),
184 }
185 }
186
187 pub fn point_at(&mut self, column: u16, row: u16) {
192 self.pointer = Some((column, row));
193 self.logo_at = None;
194 let center_x = f32::from(self.logo_origin.0) + LOGO_COLS as f32 / 2.0;
195 let center_y = f32::from(self.logo_origin.1) + LOGO_ROWS as f32 / 2.0;
196 let horizontal = ((f32::from(column) - center_x) / (LOGO_COLS as f32 / 2.0)).clamp(-1.0, 1.0);
197 let vertical = ((f32::from(row) - center_y) / (LOGO_ROWS as f32 / 2.0)).clamp(-1.0, 1.0);
198 self.camera_target = (-vertical * 0.42, -horizontal * PI);
199 }
200
201 pub fn render(&mut self, viewport: Size, elapsed: Duration) -> &Frame {
204 if self.frame.size() != viewport {
205 self.frame = Frame::new(viewport);
206 self.backdrop_frame = Frame::new(viewport);
207 self.backdrop_at = None;
208 }
209 let clock = elapsed;
210 let elapsed = elapsed.as_secs_f32();
211 let delta = (elapsed - self.last_elapsed).max(0.0);
213 self.last_elapsed = elapsed;
214 let response = 1.0 - (-delta * 10.0).exp();
215 self.camera.0 += (self.camera_target.0 - self.camera.0) * response;
216 self.camera.1 += (self.camera_target.1 - self.camera.1) * response;
217 self.draw_backdrop(viewport, clock, elapsed);
218
219 let logo_interval = ambient_interval(clock, LOGO_IDLE_INTERVAL);
220 if self
221 .logo_at
222 .is_none_or(|rendered_at| clock.saturating_sub(rendered_at) >= logo_interval)
223 {
224 self.logo = logo_cells(elapsed, self.camera);
225 self.logo_at = Some(clock);
226 }
227 let cols = if viewport.width >= CARD_COLS && viewport.height >= CARD_ROWS {
228 Some(CARD_COLS)
229 } else if viewport.width >= SMOL_COLS && viewport.height >= CARD_ROWS {
230 Some(SMOL_COLS)
231 } else {
232 None
233 };
234 let Some(cols) = cols else {
235 let left = viewport.width.saturating_sub(LOGO_COLS as u16) / 2;
236 let top = viewport.height.saturating_sub(LOGO_ROWS as u16) / 2;
237 self.logo_origin = (left, top);
238 blit_logo(&mut self.frame, &self.logo, left, top, PLATE);
239 return &self.frame;
240 };
241
242 let left = (viewport.width - cols) / 2;
243 let top = (viewport.height - CARD_ROWS) / 2;
244 let hovered = self.pointer.is_some_and(|(x, y)| {
245 (left..left + cols).contains(&x) && (top..top + CARD_ROWS).contains(&y)
246 });
247 self
248 .hover
249 .retarget(clock, if hovered { 1.0 } else { 0.0 }, HOVER_EASE, Easing::EaseOut);
250 let hover = self.hover.sample(clock).clamp(0.0, 1.0);
251 self.draw_card(cols, left, top, elapsed, hover);
252 &self.frame
253 }
254
255 fn draw_backdrop(&mut self, viewport: Size, clock: Duration, elapsed: f32) {
258 let interval = ambient_interval(clock, BACKDROP_IDLE_INTERVAL);
259 if self
260 .backdrop_at
261 .is_none_or(|rendered_at| clock.saturating_sub(rendered_at) >= interval)
262 {
263 let fade = smooth((elapsed / BACKDROP_FADE).clamp(0.0, 1.0));
264 self
265 .backdrop_frame
266 .fill(Rect::new(0, 0, viewport.width, viewport.height), Style::default());
267 let frame = &mut self.backdrop_frame;
268 let mut buffer = [0_u8; 4];
269 let dim = |color: Color| Color::Rgb(0, 0, 0).lerp(color, fade);
270 self.surface.render(
271 &mut self.backdrop,
272 clock,
273 viewport.width,
274 viewport.height,
275 |x, y, glyph, fg, bg| {
276 let style = Style::new().fg(dim(fg));
277 let style = match bg {
278 Some(bg) => style.bg(dim(bg)),
279 None => style,
280 };
281 frame.put(x, y, glyph.encode_utf8(&mut buffer), style);
282 },
283 );
284 self.backdrop_at = Some(clock);
285 }
286 self.frame.clone_from(&self.backdrop_frame);
287 }
288
289 fn draw_card(&mut self, cols: u16, left: u16, top: u16, elapsed: f32, hover: f32) {
290 let full = cols == CARD_COLS;
291 let logo_left = if full {
292 left + 3
293 } else {
294 left + (cols - LOGO_COLS as u16) / 2
295 };
296 self.logo_origin = (logo_left, top + 2);
297 let pointer = self.pointer;
301 let center =
302 (f32::from(left) + f32::from(cols) / 2.0, f32::from(top) + f32::from(CARD_ROWS) / 2.0);
303 let edge_at = move |x: u16, y: u16| -> Style {
304 let Some((px, py)) = pointer.filter(|_| hover > 0.02) else {
305 return on_card(CARD_BORDER);
306 };
307 let dx = (f32::from(x) - f32::from(px)) * 0.5;
308 let dy = f32::from(y) - f32::from(py);
309 let glow = hover * (-(dx * dx + dy * dy) / 34.0).exp();
310 if glow < 0.02 {
311 return on_card(CARD_BORDER);
312 }
313 let angle = (f32::from(y) - center.1).atan2((f32::from(x) - center.0) * 0.5);
314 let brand = vec3_color(gradient(angle - elapsed * 0.5));
315 on_card(CARD_BORDER.lerp(brand, glow))
316 };
317 let frame = &mut self.frame;
318 frame.fill(Rect::new(left, top, cols, CARD_ROWS), on_card(TEXT));
319
320 let right = left + cols - 1;
321 let bottom = top + CARD_ROWS - 1;
322 let divider = bottom - 2;
323 let (tl, tr, bl, br, horizontal, vertical) = self.charset.border(Border::Round);
324 let grid = self.charset.grid();
325 let mut glyph = [0_u8; 4];
326 frame.put(left, top, tl.encode_utf8(&mut glyph), edge_at(left, top));
327 frame.put(right, top, tr.encode_utf8(&mut glyph), edge_at(right, top));
328 frame.put(left, divider, grid.middle.0.encode_utf8(&mut glyph), edge_at(left, divider));
329 frame.put(right, divider, grid.middle.2.encode_utf8(&mut glyph), edge_at(right, divider));
330 frame.put(left, bottom, bl.encode_utf8(&mut glyph), edge_at(left, bottom));
331 frame.put(right, bottom, br.encode_utf8(&mut glyph), edge_at(right, bottom));
332 for x in left + 1..right {
333 frame.put(x, top, horizontal.encode_utf8(&mut glyph), edge_at(x, top));
334 frame.put(x, divider, horizontal.encode_utf8(&mut glyph), edge_at(x, divider));
335 frame.put(x, bottom, horizontal.encode_utf8(&mut glyph), edge_at(x, bottom));
336 }
337 for y in top + 1..bottom {
338 if y != divider {
339 frame.put(left, y, vertical.encode_utf8(&mut glyph), edge_at(left, y));
340 frame.put(right, y, vertical.encode_utf8(&mut glyph), edge_at(right, y));
341 }
342 }
343
344 frame.put(left + 2, top, self.title.as_str(), on_card(TEXT_STRONG));
345 if full {
346 frame.put(left + 34, top, " SESSION ORBIT ", on_card(FAINT));
347 draw_dust(frame, left, top, elapsed);
348 draw_sessions(frame, left, top, self.charset);
349 draw_beam(frame, left, top, elapsed);
350 }
351
352 blit_logo(frame, &self.logo, logo_left, top + 2, CARD_BG);
353
354 let footer = divider + 1;
355 frame.fill(Rect::new(left + 1, footer, cols - 2, 1), on_footer(TEXT));
356 if full {
357 frame.put(left + 3, divider, " SHORTCUTS ", on_card(FAINT));
358 let dot = fmts!(" {} ", self.charset.icon(Icon::Enabled));
359 let x = frame.put(left + cols - 21, top, &dot, on_card(GREEN));
360 frame.put(x, top, "rust-analyzer ", on_card(MUTED));
361 draw_full_hints(frame, left, footer);
362 } else {
363 draw_smol_hints(frame, left, cols, footer);
364 }
365 }
366}
367
368impl Default for Welcome {
369 fn default() -> Self {
370 Self::new(Charset::NerdFont)
371 }
372}
373
374fn draw_dust(frame: &mut Frame, left: u16, top: u16, elapsed: f32) {
375 for &(x, y, offset) in &DUST {
376 let pulse = 0.5 + 0.5 * (elapsed * 1.4 + offset).sin();
377 let color = FAINT.lerp(CYAN, pulse * 0.28);
378 frame.put(left + x, top + y, "·", on_card(color));
379 }
380 frame.put(left + 1, top + 7, HORIZON, on_card(FAINT.lerp(INDIGO, 0.16)));
381 frame.put(left + 14, top + 1, "+Z", on_card(FAINT));
382}
383
384fn draw_beam(frame: &mut Frame, left: u16, top: u16, elapsed: f32) {
385 let phase = (elapsed * 9.0) as usize % BEAM.len();
386 for (index, &(x, y, glyph)) in BEAM.iter().enumerate() {
387 let direct = index.abs_diff(phase);
388 let distance = direct.min(BEAM.len() - direct);
389 let color = match distance {
390 0 => TEXT_STRONG,
391 1 => CYAN,
392 _ => FAINT.lerp(INDIGO, 0.34),
393 };
394 frame.put(left + x, top + y, glyph, on_card(color));
395 }
396}
397
398fn draw_sessions(frame: &mut Frame, left: u16, top: u16, charset: Charset) {
399 let (_, _, _, _, _, vertical) = charset.border(Border::Round);
400 let mut glyph = [0_u8; 4];
401 let panel_x = left + 36;
402 frame.put(panel_x, top + 2, "RECENT SESSIONS", on_card(MUTED));
403 frame.put(left + CARD_COLS - 14, top + 2, "4 / LOCAL", on_card(FAINT));
404 for y in top + 4..=top + 10 {
405 frame.put(panel_x, y, vertical.encode_utf8(&mut glyph), on_card(FAINT.lerp(INDIGO, 0.18)));
406 }
407 for (index, (label, age)) in SESSIONS.iter().enumerate() {
408 let y = top + 4 + index as u16 * 2;
409 if index == 0 {
410 frame.fill(Rect::new(panel_x - 2, y, CARD_COLS - 35, 1), on_selected(TEXT));
411 frame.put(panel_x - 2, y, charset.rail(), on_selected(GREEN));
412 frame.put(panel_x, y, charset.radio(true), on_selected(GREEN));
413 frame.put(panel_x + 2, y, age, on_selected(GREEN));
414 frame.put(panel_x + 7, y, label, on_selected(TEXT_STRONG));
415 } else {
416 frame.put(panel_x, y, charset.radio(false), on_card(FAINT));
417 frame.put(panel_x + 2, y, age, on_card(FAINT));
418 frame.put(panel_x + 7, y, label, on_card(MUTED));
419 }
420 }
421}
422
423fn draw_full_hints(frame: &mut Frame, left: u16, y: u16) {
424 frame.put(left + 3, y, "#", on_footer(CYAN));
425 frame.put(left + 5, y, "actions", on_footer(MUTED));
426 frame.put(left + 14, y, "/", on_footer(GREEN));
427 frame.put(left + 16, y, "commands", on_footer(MUTED));
428 frame.put(left + 27, y, "!", on_footer(AMBER));
429 frame.put(left + 29, y, "shell", on_footer(MUTED));
430 frame.put(left + 37, y, "$", on_footer(VIOLET));
431 frame.put(left + 39, y, "python", on_footer(MUTED));
432 frame.put(left + CARD_COLS - 23, y, "↑↓ move", on_footer(FAINT));
433 frame.put(left + CARD_COLS - 13, y, "↵ resume", on_footer(TEXT_STRONG));
434}
435
436fn draw_smol_hints(frame: &mut Frame, left: u16, cols: u16, y: u16) {
437 frame.put(left + 3, y, "#", on_footer(CYAN).bold());
438 frame.put(left + 5, y, "/", on_footer(CYAN).bold());
439 frame.put(left + 7, y, "!", on_footer(AMBER).bold());
440 frame.put(left + 9, y, "$", on_footer(GREEN).bold());
441 frame.put(left + cols - 14, y, "enter", on_footer(FAINT));
442 frame.put(left + cols - 8, y, "resume", on_footer(TEXT_STRONG).bold());
443}
444
445fn blit_logo(frame: &mut Frame, logo: &LogoGrid, left: u16, top: u16, background: Color) {
446 let mut buffer = [0_u8; 4];
447 for (row, cells) in logo.iter().enumerate() {
448 for (column, cell) in cells.iter().enumerate() {
449 let Some((glyph, color)) = cell else { continue };
450 let style = Style::new().fg(*color).bg(background);
451 frame.put(left + column as u16, top + row as u16, glyph.encode_utf8(&mut buffer), style);
452 }
453 }
454}
455
456const fn on_card(fg: Color) -> Style {
457 Style::new().fg(fg).bg(CARD_BG)
458}
459
460const fn on_footer(fg: Color) -> Style {
461 Style::new().fg(fg).bg(FOOTER_BG)
462}
463
464const fn on_selected(fg: Color) -> Style {
465 Style::new().fg(fg).bg(SELECTED_BG)
466}
467
468fn smooth(edge: f32) -> f32 {
471 edge * edge * (3.0 - 2.0 * edge)
472}
473
474fn ease_in(progress: f32) -> f32 {
475 let clamped = progress.clamp(0.0, 1.0);
476 clamped * clamped
477}
478
479fn color_mix(elapsed: f32) -> f32 {
481 ease_in((elapsed - 0.18) / 0.58)
482}
483
484fn reveal_orbit(elapsed: f32) -> f32 {
486 let progress = ((elapsed - 0.18) / 0.64).clamp(0.0, 1.0);
487 TAU * (1.0 - (1.0 - progress).powi(3))
488}
489
490fn disk_rotation(elapsed: f32) -> f32 {
492 let spinning = (elapsed - 0.18).max(0.0);
493 let ramp = 0.40;
494 let angular_speed = TAU / 2.4;
495 if spinning < ramp {
496 angular_speed * spinning * spinning / (2.0 * ramp)
497 } else {
498 angular_speed * (spinning - ramp / 2.0)
499 }
500}
501
502fn gradient(angle: f32) -> Vec3 {
504 let position = (angle / TAU + 0.5).rem_euclid(1.0) * 3.0;
505 let index = (position as usize).min(2);
506 DISK_STOPS[index].lerp(DISK_STOPS[index + 1], position - index as f32)
507}
508
509fn vec3_color(color: Vec3) -> Color {
511 Color::from(color)
512}
513
514fn sun_rays(x: f32, z: f32) -> f32 {
516 let length = 0.55_f32.hypot(0.38);
517 let (hx, hz) = (-0.55 / length, 0.38 / length);
518 let across = x * -hz + z * hx;
519 let along = x * hx + z * hz;
520 let rays = (-((across + 0.56) / 0.075).powi(2)).exp()
521 + (-((across + 0.04) / 0.055).powi(2)).exp()
522 + (-((across - 0.47) / 0.09).powi(2)).exp();
523 let envelope = (-((along + 0.20) / 2.45).powi(4)).exp();
524 (rays * envelope).clamp(0.0, 1.0)
525}
526
527fn sphere_depth(origin: Vec3, direction: Vec3, center: Vec3, radius: f32) -> f32 {
528 let offset = origin - center;
529 let projection = offset.dot(direction);
530 let discriminant = projection * projection - (offset.dot(offset) - radius * radius);
531 let root = -projection - discriminant.max(0.0).sqrt();
532 if discriminant >= 0.0 && root > 0.0 {
533 root
534 } else {
535 f32::INFINITY
536 }
537}
538
539fn axis_hit(origin: Vec3, direction: Vec3) -> (f32, bool, Vec3) {
541 let quadratic = direction.x * direction.x + direction.z * direction.z;
542 let linear = 2.0 * (origin.x * direction.x + origin.z * direction.z);
543 let constant = origin.x * origin.x + origin.z * origin.z - AXIS_RADIUS * AXIS_RADIUS;
544 let discriminant = linear * linear - 4.0 * quadratic * constant;
545 let root = (-linear - discriminant.max(0.0).sqrt()) / (2.0 * quadratic).max(1e-8);
546 let hit_y = origin.y + direction.y * root;
547 let cylinder = if discriminant >= 0.0 && root > 0.0 && (AXIS_BOTTOM..=AXIS_TOP).contains(&hit_y)
548 {
549 root
550 } else {
551 f32::INFINITY
552 };
553 let cap_center = vec3(0.0, AXIS_TOP, 0.0);
554 let cap = sphere_depth(origin, direction, cap_center, AXIS_RADIUS * 1.75);
555 let depth = cylinder.min(cap);
556 if !depth.is_finite() {
557 return (f32::INFINITY, false, vec3(0.0, 1.0, 0.0));
558 }
559 let point = origin + direction * depth;
560 let normal = if cap < cylinder {
561 (point - cap_center).normalize()
562 } else {
563 vec3(point.x, 0.0, point.z).normalize()
564 };
565 (depth, true, normal)
566}
567
568fn tone(color: Vec3) -> Vec3 {
571 color * 1.28 + Vec3::rgb(4, 4, 4)
572}
573
574struct Platter {
577 sun: Vec3,
578 angle: f32,
579 transition: f32,
580 pointer: (f32, f32),
582}
583
584impl Platter {
585 fn new(pointer: (f32, f32)) -> Self {
586 Self { sun: vec3(-0.55, 1.0, 0.38).normalize(), angle: 0.0, transition: 0.0, pointer }
587 }
588
589 fn ground(&self, origin: Vec3, direction: Vec3) -> (Vec3, f32) {
592 if direction.y >= 0.0 {
593 return (BACKGROUND, 0.0);
594 }
595 let depth = (FLOOR_Y - origin.y) / direction.y;
596 if depth <= 0.0 {
597 return (BACKGROUND, 0.0);
598 }
599 let floor = origin + direction * depth;
600 let stage =
601 (-0.42 * ((floor.x.abs() / 2.30).powi(4) + ((floor.z + 0.15).abs() / 1.70).powi(4))).exp();
602
603 let sun_depth = (DISK_Y - DISK_HALF_THICKNESS - FLOOR_Y) / self.sun.y;
604 let sunlit = floor + self.sun * sun_depth;
605 let shadow_radius = sunlit.x.hypot(sunlit.z);
606 let occlusion = smooth(((DISK_RADIUS + 0.08 - shadow_radius) / 0.16).clamp(0.0, 1.0));
607 let rays = sun_rays(floor.x, floor.z);
608
609 let neutral_alpha = stage * rays * (1.0 - occlusion) * 0.36;
610 let neutral = (BACKGROUND + vec3(0.36, 0.33, 0.27) * (stage * rays)).clamp01();
611 let mut color = neutral * neutral_alpha + BACKGROUND * (1.0 - neutral_alpha);
612 let mut alpha = neutral_alpha;
613
614 let transmission_alpha =
615 stage * sun_rays(sunlit.x, sunlit.z) * occlusion * self.transition * 0.64;
616 let transmission =
617 (BACKGROUND + gradient(sunlit.z.atan2(sunlit.x) - self.angle) * 0.86).clamp01();
618 color = transmission * transmission_alpha + color * (1.0 - transmission_alpha);
619 alpha = transmission_alpha + alpha * (1.0 - transmission_alpha);
620
621 let mirrored = vec3(direction.x, -direction.y, direction.z);
622 let reflected_depth = (DISK_Y - DISK_HALF_THICKNESS - FLOOR_Y) / mirrored.y.max(1e-6);
623 let reflected = floor + mirrored * reflected_depth;
624 let reflected_radius = reflected.x.hypot(reflected.z);
625 if reflected_radius <= DISK_RADIUS {
626 let edge = smooth(((DISK_RADIUS - reflected_radius) / 0.13).clamp(0.0, 1.0));
627 let grazing = (1.0 + direction.y).clamp(0.0, 1.0);
628 let reflection_alpha = self.transition * edge * (0.22 + 0.28 * grazing);
629 let reflection = gradient(reflected.z.atan2(reflected.x) - self.angle);
630 color = reflection * reflection_alpha + color * (1.0 - reflection_alpha);
631 alpha = reflection_alpha + alpha * (1.0 - reflection_alpha);
632 }
633 (color, alpha)
634 }
635}
636
637impl Trace for Platter {
638 fn advance(&mut self, now: Duration) -> Camera {
639 let elapsed = now.as_secs_f32();
640 self.angle = disk_rotation(elapsed);
641 self.transition = color_mix(elapsed);
642 Camera {
643 target: vec3(0.0, 0.08, 0.0),
644 yaw: self.pointer.1 + reveal_orbit(elapsed) + (elapsed * 0.31).sin() * 0.018,
645 pitch: CAMERA_PITCH,
646 distance: CAMERA_DISTANCE,
647 lift: self.pointer.0.clamp(-0.42, 0.42) + (elapsed * 0.55).sin() * 0.014,
648 focal: CAMERA_FOCAL,
649 }
650 }
651
652 fn shade(&self, ray: Ray) -> (Vec3, f32) {
655 let Ray { origin, dir: direction } = ray;
656
657 let mut disk_depth = f32::INFINITY;
659 let mut disk_point = Vec3::ZERO;
660 let mut disk_radial = 0.0;
661 if direction.y < 0.0 {
662 let depth = (DISK_Y + DISK_HALF_THICKNESS - origin.y) / direction.y;
663 if depth > 0.0 {
664 let point = origin + direction * depth;
665 let radial = point.x.hypot(point.z);
666 if radial <= DISK_RADIUS {
667 disk_depth = depth;
668 disk_point = point;
669 disk_radial = radial;
670 }
671 }
672 }
673 let disk_visible = disk_depth.is_finite();
674 let (axis_depth, axis_visible, axis_normal) = axis_hit(origin, direction);
675
676 let (ground_color, ground_alpha) = self.ground(origin, direction);
677 let mut color = ground_color * ground_alpha + BACKGROUND * (1.0 - ground_alpha);
678 let mut alpha = ground_alpha;
679
680 let view = direction * -1.0;
681 let halfway = if disk_visible || axis_visible {
682 (self.sun + view).normalize()
683 } else {
684 Vec3::ZERO
685 };
686 let axis_color = if axis_visible {
687 let diffuse = axis_normal.dot(self.sun).max(0.0);
688 let specular = axis_normal.dot(halfway).max(0.0).powi(44);
689 (WHITE * (0.32 + 0.68 * diffuse + 0.48 * specular)).clamp01()
690 } else {
691 Vec3::ZERO
692 };
693
694 if axis_visible && axis_depth > disk_depth {
695 color = axis_color;
696 alpha = 1.0;
697 }
698
699 if disk_visible {
700 let diffuse = self.sun.y.max(0.0);
701 let specular = halfway.y.max(0.0).powi(72);
702 let fresnel = (1.0 - view.y.max(0.0)).powi(4);
703 let material_angle = disk_point.z.atan2(disk_point.x) - self.angle;
704 let rim = smooth(((disk_radial - (DISK_RADIUS - 0.075)) / 0.055).clamp(0.0, 1.0));
705 let streak_angle = (material_angle - 0.32 + PI).rem_euclid(TAU) - PI;
706 let streak = (-(streak_angle / 0.19).powi(2)).exp();
707 let incident = sun_rays(disk_point.x, disk_point.z);
708
709 let opaque =
710 (WHITE * (0.30 + 0.27 * diffuse + 0.36 * incident + 0.20 * specular)).clamp01();
711 let glass = (gradient(material_angle) * (0.34 + 0.70 * diffuse)
712 + WHITE * (0.72 * specular + 0.16 * fresnel + 0.20 * streak))
713 .clamp01();
714 let border_strength = rim * self.transition;
715 let mut disk_color = opaque
716 .lerp(glass, self.transition)
717 .lerp(WHITE, border_strength);
718
719 let marker_x = 0.52 * self.angle.cos();
721 let marker_z = 0.52 * self.angle.sin();
722 let marker_distance = (disk_point.x - marker_x).hypot(disk_point.z - marker_z);
723 let marker = smooth(((0.10 - marker_distance) / 0.035).clamp(0.0, 1.0));
724 disk_color = disk_color.lerp(INK, marker);
725
726 let disk_alpha = (1.0 - self.transition * (1.0 - DISK_GLASS_OPACITY))
727 .max(border_strength * 0.96)
728 .max(marker * 0.98);
729 color = disk_color * disk_alpha + color * (1.0 - disk_alpha);
730 alpha = disk_alpha + alpha * (1.0 - disk_alpha);
731 }
732
733 if axis_visible && axis_depth <= disk_depth {
734 color = axis_color;
735 alpha = 1.0;
736 }
737 (tone(color), alpha)
738 }
739}
740
741fn logo_cells(elapsed: f32, pointer: (f32, f32)) -> LogoGrid {
744 let mut platter = Platter::new(pointer);
745 let camera = platter.advance(Duration::from_secs_f32(elapsed));
746 let mut grid: LogoGrid = [[None; LOGO_COLS]; LOGO_ROWS];
747 scene::rasterize(&platter, &camera, LOGO_COLS as u16, LOGO_ROWS as u16, |x, y, glyph, color| {
748 grid[y as usize][x as usize] = Some((glyph, color));
749 });
750 grid
751}
752
753#[cfg(test)]
754mod tests {
755 use omp_tui::test_support::{frame_cell_style, frame_row_text};
756
757 use super::{CARD_BORDER, CARD_ROWS, Charset, Duration, Size, Welcome};
758
759 fn rows(viewport: Size, elapsed_ms: u64) -> Vec<String> {
760 let mut welcome = Welcome::new(Charset::NerdFont);
761 let frame = welcome.render(viewport, Duration::from_millis(elapsed_ms));
762 (0..frame.size().height)
763 .map(|row| frame_row_text(frame, row))
764 .collect()
765 }
766
767 fn has_lit_braille(rows: &[String]) -> bool {
768 rows.iter().any(|row| {
769 row.chars()
770 .any(|glyph| ('\u{2801}'..='\u{28ff}').contains(&glyph))
771 })
772 }
773
774 #[test]
775 fn full_card_centers_sessions_and_hints() {
776 let rows = rows(Size::new(100, 21), 2_000);
777 assert!(rows[3].contains("omp v"), "title chip in the top border: {}", rows[3]);
779 assert!(rows[3].contains("rust-analyzer"), "LSP chip in the top border");
780 assert!(rows[5].contains("RECENT SESSIONS") && rows[5].contains("4 / LOCAL"));
781 assert!(rows[7].contains("Optimize custom status widget rendering"));
782 assert!(rows[15].contains("SHORTCUTS"), "divider labels the command rail");
783 assert!(rows[16].contains("actions") && rows[16].contains("resume"));
784 assert!(rows[17].contains('╰'), "bottom border closes the card");
785 assert!(has_lit_braille(&rows), "the disk renders lit braille cells");
786 }
787
788 #[test]
789 fn smol_card_drops_the_session_panel() {
790 let rows = rows(Size::new(40, CARD_ROWS), 2_000);
791 assert!(rows[0].contains("omp v"));
792 assert!(rows.iter().all(|row| !row.contains("RECENT SESSIONS")));
793 assert!(rows[13].contains("resume"));
794 assert!(has_lit_braille(&rows));
795 }
796
797 #[test]
798 fn bare_logo_survives_a_tiny_viewport() {
799 let rows = rows(Size::new(29, 11), 2_000);
800 assert!(has_lit_braille(&rows), "the logo still renders without the card");
801 assert!(rows.iter().all(|row| !row.contains('╭')), "no card chrome at this size");
802 }
803
804 #[test]
805 fn eclipse_backdrop_fills_the_margins() {
806 let rows = rows(Size::new(100, 21), 2_000);
807 assert!(rows[0].chars().all(|glyph| glyph == '▀'), "top margin is backdrop: {}", rows[0]);
809 assert!(rows[20].chars().all(|glyph| glyph == '▀'), "bottom margin is backdrop");
810 }
811
812 #[test]
813 fn pointer_orbit_changes_the_rendered_logo() {
814 let viewport = Size::new(100, 21);
815 let settle = Duration::from_millis(1_000);
816 let sample = Duration::from_millis(2_000);
817
818 let mut centered = Welcome::new(Charset::NerdFont);
819 centered.render(viewport, settle);
820 let baseline: Vec<String> = {
821 let frame = centered.render(viewport, sample);
822 (0..frame.size().height)
823 .map(|row| frame_row_text(frame, row))
824 .collect()
825 };
826
827 let mut orbited = Welcome::new(Charset::NerdFont);
828 orbited.render(viewport, settle);
831 orbited.point_at(0, 0);
832 let frame = orbited.render(viewport, sample);
833 let pointed: Vec<String> = (0..frame.size().height)
834 .map(|row| frame_row_text(frame, row))
835 .collect();
836
837 assert_ne!(baseline, pointed, "pointing away from the logo center orbits the camera");
838 }
839
840 #[test]
841 fn hovering_the_card_glows_the_border_near_the_pointer() {
842 let viewport = Size::new(100, 21);
843 let mut welcome = Welcome::new(Charset::NerdFont);
844 welcome.render(viewport, Duration::from_millis(1_000));
845
846 welcome.point_at(27, 4);
850 welcome.render(viewport, Duration::from_millis(1_050));
851 let frame = welcome.render(viewport, Duration::from_millis(2_000));
852 let near_pointer = frame_cell_style(frame, 27, 3).foreground_color();
853 let far_border = frame_cell_style(frame, 8, 17).foreground_color();
854 assert!(frame_row_text(frame, 3).contains('╭'), "the card stays on row 3 while hovered");
855 assert_ne!(near_pointer, CARD_BORDER, "border glows near the pointer");
856 assert_eq!(far_border, CARD_BORDER, "the glow stays local to the pointer");
857 }
858}