Replace the Grok braille logo with a procedurally generated moon
The welcome logo (hero box, stacked layout, and minimal's welcome card) is now a braille moon that waxes and wanes through a full 8s lunation, echoing the Kimi CLI's moon-phase spinner. The disc is rasterized into the 2x4 braille dot grid at render time — round at any size, no art assets — with the terminator at x = cos(2*pi*p)*sqrt(1-y^2) and a faint outline ring so the silhouette survives new moon. The full moon grows to 20x10 cells (from the 14x7 slashed-circle art) and the small tier to 10x5; the old logo*.txt assets are deleted. Geometry is unit-tested (wax/wane monotonicity, quarter-phase symmetry, right-limb-first waxing, silhouette at new moon, round raster).
This commit is contained in:
@@ -1,4 +0,0 @@
|
|||||||
⠀⠀⣠⣤⢤⡄⢠⠀
|
|
||||||
⠀⣼⠃⠀⢠⠖⢿⠀
|
|
||||||
⠀⢻⠄⠀⠀⢀⡾⠀
|
|
||||||
⠀⠁⠈⠛⠛⠉⠀⠀
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
⠀⠀⠀⣀⣤⣤⣀⠀⠀⡠
|
|
||||||
⠀⢀⡾⠋⠁⠀⢁⢴⡎⠀
|
|
||||||
⠀⢸⡇⠀⠀⠐⠁⢀⣿⠀
|
|
||||||
⠀⢈⠗⢀⣀⣀⣠⡾⠃⠀
|
|
||||||
⠐⠁⠀⠈⠉⠉⠉⠀⠀⠀
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
⠀⠀⠀⠀⢀⣀⣀⣀⠀⠀⣠⠀
|
|
||||||
⠀⠀⣠⡾⠛⠉⠉⠉⢁⣴⡇⠀
|
|
||||||
⠀⢠⣿⠁⠀⠀⢀⠔⠉⢻⡇⠀
|
|
||||||
⠀⠈⣿⡀⠀⠀⠁⠀⠀⣼⠇⠀
|
|
||||||
⠀⢀⠟⠁⣤⣤⣤⣤⡾⠋⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠉⠉⠀⠀⠀⠀⠀
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
⠀⠀⠀⠀⠀⠀⣀⣀⡀⠀⠀⠀⢀⠄
|
|
||||||
⠀⠀⠀⣠⣾⠿⠛⠛⠛⠛⢀⡴⠁⠀
|
|
||||||
⠀⠀⣼⡟⠁⠀⠀⠀⢀⡴⠻⣿⡀⠀
|
|
||||||
⠀⠀⣿⡇⠀⠀⠀⠔⠁⠀⠀⣿⡇⠀
|
|
||||||
⠀⠀⢹⣷⠀⠀⠀⠀⠀⢀⣴⡿⠀⠀
|
|
||||||
⠀⢀⠞⠁⠠⢶⣶⣶⣶⠿⠋⠀⠀⠀
|
|
||||||
⠐⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠀
|
|
||||||
⠀⠀⠀⠀⣠⣶⡿⠿⠿⠿⠷⠂⢀⣼⠁⠀
|
|
||||||
⠀⠀⢀⣾⡟⠁⠀⠀⠀⠀⢀⣴⣿⣿⠀⠀
|
|
||||||
⠀⠀⣸⣿⠀⠀⠀⠀⢀⠔⠋⠀⢸⣿⠀⠀
|
|
||||||
⠀⠀⢸⣿⡀⠀⠀⠐⠁⠀⠀⠀⣸⣿⠀⠀
|
|
||||||
⠀⠀⠈⣿⠗⠀⠀⠀⠀⠀⣀⣴⣿⠃⠀⠀
|
|
||||||
⠀⢀⠜⠁⠀⠲⠿⣿⣿⡿⠿⠋⠁⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠀
|
|
||||||
⠀⠀⠀⠀⠀⣠⣶⣾⣿⣿⣿⣶⠦⠀⢀⣼⠁⠀
|
|
||||||
⠀⠀⠀⣠⣾⡿⠋⠁⠀⠀⠀⠀⢀⣴⣿⡇⠀⠀
|
|
||||||
⠀⠀⢠⣿⡟⠀⠀⠀⠀⠀⢀⡴⠛⠙⣿⣧⠀⠀
|
|
||||||
⠀⠀⢸⣿⡇⠀⠀⠀⢀⠔⠉⠀⠀⠀⣿⣿⠀⠀
|
|
||||||
⠀⠀⠈⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⣠⣿⡏⠀⠀
|
|
||||||
⠀⠀⠀⣿⠟⠁⢀⣀⣀⣀⣀⣠⣶⣿⠏⠀⠀⠀
|
|
||||||
⠀⢀⠜⠁⠀⠈⠛⠿⠿⠿⠿⠟⠋⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⣠⣴⣶⣶⣶⣶⣶⣤⡀⠀⢀⡼⠁⠀
|
|
||||||
⠀⠀⠀⠀⣰⣿⡿⠛⠉⠉⠉⠉⠉⠁⢀⣴⣿⡇⠀⠀
|
|
||||||
⠀⠀⠀⣼⣿⠏⠀⠀⠀⠀⠀⠀⢀⣴⠿⢿⣿⡇⠀⠀
|
|
||||||
⠀⠀⠀⣿⣿⠀⠀⠀⠀⠀⢀⡴⠋⠁⠀⢸⣿⣇⠀⠀
|
|
||||||
⠀⠀⠀⣿⣿⠀⠀⠀⠀⠔⠁⠀⠀⠀⠀⢸⣿⡇⠀⠀
|
|
||||||
⠀⠀⠀⢹⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⣠⣿⡿⠁⠀⠀
|
|
||||||
⠀⠀⠀⣸⠟⠁⣀⣠⣤⣤⣀⣤⣴⣾⣿⠟⠁⠀⠀⠀
|
|
||||||
⠀⠀⠞⠁⠀⠀⠈⠙⠛⠻⠿⠛⠛⠉⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣤⣴⣶⣤⣤⣄⡀⠀⠀⠀⢀⡼⠁⠀
|
|
||||||
⠀⠀⠀⠀⠀⢀⣴⣾⣿⡿⠿⠛⠛⠻⠿⠟⠛⠁⢀⣴⣿⠁⠀⠀
|
|
||||||
⠀⠀⠀⠀⢠⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣏⠀⠀⠀
|
|
||||||
⠀⠀⠀⢠⣿⣿⠋⠀⠀⠀⠀⠀⠀⠀⢀⣴⠿⠛⢻⣿⣿⠀⠀⠀
|
|
||||||
⠀⠀⠀⢸⣿⡟⠀⠀⠀⠀⠀⠀⢀⡴⠛⠁⠀⠀⠈⣿⣿⡆⠀⠀
|
|
||||||
⠀⠀⠀⢸⣿⣷⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⢠⣿⣿⠃⠀⠀
|
|
||||||
⠀⠀⠀⠈⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⡟⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⣸⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⣀⣴⣿⣿⠟⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⣰⠟⠁⠀⠠⣶⣿⣿⣷⣶⣾⣿⣿⡿⠟⠁⠀⠀⠀⠀⠀
|
|
||||||
⠀⡠⠊⠁⠀⠀⠀⠀⠀⠈⠉⠉⠛⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡜⠁⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣾⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⠀⠀⠀⢀⣴⡟⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⡿⠿⠛⠛⠛⠛⠛⠿⠟⠋⠁⠀⢀⣴⣿⣿⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⡇⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⣰⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⡿⣿⣿⣿⣧⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⢠⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⡿⠛⠁⠀⠘⣿⣿⣿⡀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⢸⣿⣿⡏⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠟⠉⠀⠀⠀⠀⠀⣿⣿⣿⡇⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⢸⣿⣿⣇⠀⠀⠀⠀⠀⠀⢀⠴⠋⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⠇⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⣿⣿⣿⡄⠀⠀⠀⠀⠐⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⡿⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠸⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⠃⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⢰⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣴⣿⣿⣿⡟⠁⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⢀⡾⠛⠁⠀⠀⣤⣶⣿⣿⣿⣶⣶⣶⣾⣿⣿⣿⣿⠟⠋⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⢀⡴⠋⠀⠀⠀⠀⠀⠀⠉⠛⠻⠿⠿⠿⠿⠿⠟⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠠⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡜⠁⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣤⣶⣶⣶⣶⣶⣤⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠏⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡦⠀⠀⠀⢀⣴⣿⠏⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⡿⠿⠛⠋⠉⠉⠉⠙⠛⠻⠛⠉⠀⠀⠀⢀⣴⣿⣿⡿⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⡏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⡿⠟⠉⠻⣿⣿⣿⣿⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⢀⣿⣿⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⠟⠉⠀⠀⠀⠀⢿⣿⣿⣿⡄⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⢸⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠿⠋⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⡇⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠸⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⢀⡴⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⡇⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⢀⠔⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⠁⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠸⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⠏⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⠆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣴⣾⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⣼⡿⠋⠁⠀⠀⣀⣤⣶⣿⣿⣿⣶⣶⣶⣶⣶⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⢠⡾⠋⠀⠀⠀⠀⠀⠈⠙⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⢀⠔⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡜⠁⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣀⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠏⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣄⡀⠀⠀⠀⠀⠀⠀⢀⣴⣿⠃⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠓⠀⠀⠀⢀⣴⣿⣿⠇⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⡿⠿⠛⠋⠉⠉⠉⠉⠉⠉⠛⠛⠛⠉⠀⠀⠀⠀⢀⣴⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⡿⠿⣿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⠟⠋⠀⠀⠈⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⡿⠋⠁⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⡿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠈⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⢀⠴⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠐⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⡿⠟⠁⠀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣾⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⢠⣿⡿⠋⠀⠀⠀⠀⣀⣤⣴⣾⣿⣿⣷⣶⣶⣶⣶⣶⣶⣿⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⣴⡿⠋⠀⠀⠀⠀⠀⠈⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⣠⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠛⠻⠿⠿⠿⠿⠿⠿⠛⠛⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⣠⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠚⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
||||||
@@ -1,4 +1,13 @@
|
|||||||
//! Logo component — renders the braille art logo.
|
//! Logo component — a procedurally generated braille moon that waxes and
|
||||||
|
//! wanes through a full lunation, echoing the Kimi CLI's moon-phase spinner.
|
||||||
|
//!
|
||||||
|
//! The disc is rasterized into the 2×4 dot grid of braille cells (dots are
|
||||||
|
//! close to square in a typical terminal cell), so the moon stays round at
|
||||||
|
//! any size and needs no art assets. The lit region follows the standard
|
||||||
|
//! phase model: the terminator is the ellipse `x = cos(2πp)·√(1−y²)`, with
|
||||||
|
//! sunlight arriving from the right while waxing and from the left while
|
||||||
|
//! waning. The dark limb keeps a faint outline ring so the silhouette never
|
||||||
|
//! disappears at new moon.
|
||||||
//!
|
//!
|
||||||
//! Hidden entirely on legacy Windows consoles: the U+2800 braille block is
|
//! Hidden entirely on legacy Windows consoles: the U+2800 braille block is
|
||||||
//! not covered by the ConHost raster fonts and would render as tofu.
|
//! not covered by the ConHost raster fonts and would render as tofu.
|
||||||
@@ -12,51 +21,75 @@ use ratatui::widgets::{Paragraph, Widget};
|
|||||||
use crate::render::color::blend_color;
|
use crate::render::color::blend_color;
|
||||||
use crate::theme::Theme;
|
use crate::theme::Theme;
|
||||||
|
|
||||||
const LOGO: &str = include_str!("../../../assets/logo/logo07.txt");
|
/// Full-size moon (hero box and tall stacked layouts), in braille cells.
|
||||||
const LOGO_SMALL: &str = include_str!("../../../assets/logo/logo05.txt");
|
const FULL_COLS: u16 = 20;
|
||||||
|
const FULL_ROWS: u16 = 10;
|
||||||
|
/// Small moon for short windows, in braille cells.
|
||||||
|
const SMALL_COLS: u16 = 10;
|
||||||
|
const SMALL_ROWS: u16 = 5;
|
||||||
|
|
||||||
/// Height at or above which the small logo is shown (below it, no logo).
|
/// Height at or above which the small moon is shown (below it, no logo).
|
||||||
const SMALL_LOGO_MIN_HEIGHT: u16 = 22;
|
const SMALL_LOGO_MIN_HEIGHT: u16 = 22;
|
||||||
/// Height at or above which the full logo is shown.
|
/// Height at or above which the full moon is shown.
|
||||||
const FULL_LOGO_MIN_HEIGHT: u16 = 26;
|
const FULL_LOGO_MIN_HEIGHT: u16 = 26;
|
||||||
|
|
||||||
fn pick_logo(window_height: u16) -> Option<&'static str> {
|
/// Seconds for one full lunation (new → full → new). Deliberately slow —
|
||||||
|
/// the welcome screen should breathe, not blink.
|
||||||
|
const LUNATION_SECS: f32 = 8.0;
|
||||||
|
|
||||||
|
/// Redraw cadence in frames per second. The terminator moves slowly, so a
|
||||||
|
/// modest rate looks smooth while sparing the long-lived welcome screen
|
||||||
|
/// from full-rate repaints.
|
||||||
|
const MOON_FPS: f32 = 12.0;
|
||||||
|
|
||||||
|
/// Brightness of lit dots (blend factor from the resting gray toward the
|
||||||
|
/// bright text color).
|
||||||
|
const LIT_OPACITY: f32 = 0.85;
|
||||||
|
/// Extra breathing applied to lit dots.
|
||||||
|
const PULSE: f32 = 0.10;
|
||||||
|
/// Breathing period in seconds.
|
||||||
|
const PULSE_SECS: f32 = 5.0;
|
||||||
|
/// Squared inner radius (normalized) of the dark-limb outline ring.
|
||||||
|
const RING_INNER_SQ: f32 = 0.82;
|
||||||
|
|
||||||
|
/// One logo size tier, in braille cells.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
struct MoonSize {
|
||||||
|
cols: u16,
|
||||||
|
rows: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
const FULL: MoonSize = MoonSize {
|
||||||
|
cols: FULL_COLS,
|
||||||
|
rows: FULL_ROWS,
|
||||||
|
};
|
||||||
|
const SMALL: MoonSize = MoonSize {
|
||||||
|
cols: SMALL_COLS,
|
||||||
|
rows: SMALL_ROWS,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn pick_logo(window_height: u16) -> Option<MoonSize> {
|
||||||
pick_logo_for(window_height, logo_hidden())
|
pick_logo_for(window_height, logo_hidden())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pure tier selection so tests can drive the legacy-console flag directly.
|
/// Pure tier selection so tests can drive the legacy-console flag directly.
|
||||||
fn pick_logo_for(window_height: u16, hidden: bool) -> Option<&'static str> {
|
fn pick_logo_for(window_height: u16, hidden: bool) -> Option<MoonSize> {
|
||||||
if hidden || window_height < SMALL_LOGO_MIN_HEIGHT {
|
if hidden || window_height < SMALL_LOGO_MIN_HEIGHT {
|
||||||
None
|
None
|
||||||
} else if window_height < FULL_LOGO_MIN_HEIGHT {
|
} else if window_height < FULL_LOGO_MIN_HEIGHT {
|
||||||
Some(LOGO_SMALL)
|
Some(SMALL)
|
||||||
} else {
|
} else {
|
||||||
Some(LOGO)
|
Some(FULL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The braille art has no ASCII stand-in; see the module doc.
|
/// The braille moon has no ASCII stand-in; see the module doc.
|
||||||
fn logo_hidden() -> bool {
|
fn logo_hidden() -> bool {
|
||||||
crate::glyphs::is_legacy_windows_console()
|
crate::glyphs::is_legacy_windows_console()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn non_empty_lines(logo: &str) -> impl Iterator<Item = &str> {
|
|
||||||
logo.lines().filter(|l| !l.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn count_lines(logo: &str) -> u16 {
|
|
||||||
non_empty_lines(logo).count() as u16
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visual_width(logo: &str) -> u16 {
|
|
||||||
non_empty_lines(logo)
|
|
||||||
.map(unicode_width::UnicodeWidthStr::width)
|
|
||||||
.max()
|
|
||||||
.unwrap_or(24) as u16
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Animation phase in seconds since the first render. Wall-clock based so the
|
/// Animation phase in seconds since the first render. Wall-clock based so the
|
||||||
/// shimmer speed is independent of the frame rate.
|
/// lunation speed is independent of the frame rate.
|
||||||
fn anim_phase_secs() -> f32 {
|
fn anim_phase_secs() -> f32 {
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -64,78 +97,115 @@ fn anim_phase_secs() -> f32 {
|
|||||||
START.get_or_init(Instant::now).elapsed().as_secs_f32()
|
START.get_or_init(Instant::now).elapsed().as_secs_f32()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shimmer redraw cadence in frames per second. The sweep is slow, so a few fps
|
/// Quantized animation frame for the current wall-clock phase. The welcome
|
||||||
/// looks smooth while sparing the long-lived welcome screen from full-rate
|
/// screen redraws only when this advances, throttling the animation to
|
||||||
/// repaints.
|
/// ~[`MOON_FPS`] rather than the full event-loop tick rate. Pinned to 0 when
|
||||||
const SHIMMER_FPS: f32 = 12.0;
|
/// the logo is hidden.
|
||||||
|
|
||||||
/// Quantized shimmer frame for the current wall-clock phase. The welcome screen
|
|
||||||
/// redraws only when this advances, throttling the animation to ~`SHIMMER_FPS`
|
|
||||||
/// rather than the full event-loop tick rate. Pinned to 0 when the logo is
|
|
||||||
/// hidden.
|
|
||||||
pub fn shimmer_frame() -> u64 {
|
pub fn shimmer_frame() -> u64 {
|
||||||
if logo_hidden() {
|
if logo_hidden() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
(anim_phase_secs() * SHIMMER_FPS) as u64
|
(anim_phase_secs() * MOON_FPS) as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-glyph shine opacity in `[0, 1]` at normalized diagonal position `diag`
|
/// Lunation phase in `[0, 1)`: 0 = new moon, 0.5 = full moon.
|
||||||
/// (0 = bottom-left .. 1 = top-right) and animation time `secs`. A raised-cosine
|
fn phase_now() -> f32 {
|
||||||
/// band sweeps bottom-left → top-right and parks off-screen between sweeps; a
|
(anim_phase_secs() / LUNATION_SECS).fract()
|
||||||
/// gentle global pulse breathes underneath it. 0 keeps the resting gray, 1 is
|
}
|
||||||
/// full bright.
|
|
||||||
fn shine_opacity(diag: f32, secs: f32) -> f32 {
|
|
||||||
const BAND: f32 = 0.38; // half-width of the shine band — wider = more gradual falloff
|
|
||||||
const CYCLE: f32 = 4.0; // seconds per sweep + rest
|
|
||||||
const SWEEP_FRAC: f32 = 0.32; // portion of the cycle spent sweeping (~1.3s glint, rest idles)
|
|
||||||
const SHINE: f32 = 0.33; // peak shine strength
|
|
||||||
const PULSE: f32 = 0.06; // global breathing amount
|
|
||||||
const PULSE_SECS: f32 = 5.0; // breathing period
|
|
||||||
|
|
||||||
let p = (secs % CYCLE) / CYCLE;
|
/// Whether the normalized disc point `(dx, dy)` is sunlit at phase `p`.
|
||||||
let q = (p / SWEEP_FRAC).min(1.0); // parks the band off-screen during the rest
|
///
|
||||||
let band_pos = -BAND + q * (1.0 + 2.0 * BAND);
|
/// `x_edge = √(1−dy²)` is the disc edge at that height; the terminator sits
|
||||||
let pulse = PULSE * (0.5 - 0.5 * (std::f32::consts::TAU * secs / PULSE_SECS).cos());
|
/// at `k·x_edge` with `k = cos(2πp)`. Waxing light grows from the right,
|
||||||
|
/// waning light retreats to the left.
|
||||||
let d = (diag - band_pos).abs();
|
fn dot_lit(dx: f32, dy: f32, p: f32) -> bool {
|
||||||
let shine = if d < BAND {
|
let k = (std::f32::consts::TAU * p).cos();
|
||||||
0.5 * (1.0 + (std::f32::consts::PI * d / BAND).cos())
|
let x_edge = (1.0 - dy * dy).max(0.0).sqrt();
|
||||||
|
if p < 0.5 {
|
||||||
|
dx >= k * x_edge
|
||||||
} else {
|
} else {
|
||||||
0.0
|
dx <= -k * x_edge
|
||||||
};
|
}
|
||||||
(pulse + SHINE * shine).clamp(0.0, 1.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_into(area: Rect, buf: &mut Buffer, theme: &Theme, logo: &str) {
|
/// One rendered braille cell of the moon: the glyph plus whether any of its
|
||||||
let lines: Vec<&str> = non_empty_lines(logo).collect();
|
/// dots are sunlit (drives the cell color).
|
||||||
let rows = lines.len().max(1) as f32;
|
#[derive(Clone, Copy)]
|
||||||
let cols = lines
|
struct MoonCell {
|
||||||
.iter()
|
ch: char,
|
||||||
.map(|l| l.chars().count())
|
lit: bool,
|
||||||
.max()
|
}
|
||||||
.unwrap_or(1)
|
|
||||||
.max(1) as f32;
|
/// Rasterize the moon at `size` and phase `p` into rows of braille cells.
|
||||||
|
/// `None` cells are fully empty (outside the disc).
|
||||||
|
fn moon_cells(size: MoonSize, p: f32) -> Vec<Vec<Option<MoonCell>>> {
|
||||||
|
// Braille dot bit values by (dot_col, dot_row) within a cell.
|
||||||
|
const DOT_BITS: [[u32; 4]; 2] = [[0x01, 0x02, 0x04, 0x40], [0x08, 0x10, 0x20, 0x80]];
|
||||||
|
|
||||||
|
let dots_w = (size.cols * 2) as i32;
|
||||||
|
let dots_h = (size.rows * 4) as i32;
|
||||||
|
let cx = (dots_w as f32 - 1.0) / 2.0;
|
||||||
|
let cy = (dots_h as f32 - 1.0) / 2.0;
|
||||||
|
let r = (dots_w.min(dots_h) as f32) / 2.0 - 0.5;
|
||||||
|
|
||||||
|
(0..size.rows as i32)
|
||||||
|
.map(|cell_row| {
|
||||||
|
(0..size.cols as i32)
|
||||||
|
.map(|cell_col| {
|
||||||
|
let mut mask = 0u32;
|
||||||
|
let mut lit = false;
|
||||||
|
for (dot_col, col_bits) in DOT_BITS.iter().enumerate() {
|
||||||
|
for (dot_row, bit) in col_bits.iter().enumerate() {
|
||||||
|
let x = cell_col * 2 + dot_col as i32;
|
||||||
|
let y = cell_row * 4 + dot_row as i32;
|
||||||
|
let dx = (x as f32 - cx) / r;
|
||||||
|
let dy = (y as f32 - cy) / r;
|
||||||
|
let d_sq = dx * dx + dy * dy;
|
||||||
|
if d_sq > 1.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if dot_lit(dx, dy, p) {
|
||||||
|
mask |= bit;
|
||||||
|
lit = true;
|
||||||
|
} else if d_sq >= RING_INNER_SQ {
|
||||||
|
// Dark limb: keep the outline ring visible.
|
||||||
|
mask |= bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(mask != 0).then(|| MoonCell {
|
||||||
|
ch: char::from_u32(0x2800 + mask).expect("braille block"),
|
||||||
|
lit,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_into(area: Rect, buf: &mut Buffer, theme: &Theme, size: MoonSize) {
|
||||||
let secs = anim_phase_secs();
|
let secs = anim_phase_secs();
|
||||||
|
let pulse = PULSE * (0.5 - 0.5 * (std::f32::consts::TAU * secs / PULSE_SECS).cos());
|
||||||
|
let lit_opacity = (LIT_OPACITY + pulse).clamp(0.0, 1.0);
|
||||||
|
|
||||||
// Blend each glyph from the resting gray toward the bright text color by its
|
|
||||||
// shine opacity, so a sheen sweeps across the braille art. Adjacent glyphs
|
|
||||||
// that land on the same blended color share one Span to hold down the
|
|
||||||
// per-frame allocation.
|
|
||||||
let base = theme.gray;
|
let base = theme.gray;
|
||||||
let hilite = theme.text_primary;
|
let hilite = theme.text_primary;
|
||||||
let logo_lines: Vec<Line> = lines
|
let lit_color = blend_color(base, hilite, lit_opacity).unwrap_or(base);
|
||||||
.iter()
|
|
||||||
.enumerate()
|
// Adjacent cells with the same color share one Span to hold down the
|
||||||
.map(|(row, line)| {
|
// per-frame allocation.
|
||||||
|
let logo_lines: Vec<Line> = moon_cells(size, phase_now())
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| {
|
||||||
let mut spans: Vec<Span> = Vec::new();
|
let mut spans: Vec<Span> = Vec::new();
|
||||||
let mut run = String::new();
|
let mut run = String::new();
|
||||||
let mut run_color: Option<Color> = None;
|
let mut run_color: Option<Color> = None;
|
||||||
for (col, ch) in line.chars().enumerate() {
|
for cell in row {
|
||||||
// Sweep along the bottom-left → top-right diagonal: the
|
let (ch, color) = match cell {
|
||||||
// coordinate grows as col increases and row decreases.
|
Some(MoonCell { ch, lit: true }) => (ch, lit_color),
|
||||||
let diag = (col as f32 + (rows - 1.0 - row as f32)) / (cols + rows);
|
Some(MoonCell { ch, lit: false }) => (ch, base),
|
||||||
let color = blend_color(base, hilite, shine_opacity(diag, secs)).unwrap_or(base);
|
None => ('\u{2800}', base),
|
||||||
|
};
|
||||||
if run_color != Some(color) {
|
if run_color != Some(color) {
|
||||||
if let Some(prev) = run_color {
|
if let Some(prev) = run_color {
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
@@ -157,29 +227,29 @@ fn render_into(area: Rect, buf: &mut Buffer, theme: &Theme, logo: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn logo_line_count(window_height: u16) -> u16 {
|
pub fn logo_line_count(window_height: u16) -> u16 {
|
||||||
pick_logo(window_height).map_or(0, count_lines)
|
pick_logo(window_height).map_or(0, |s| s.rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logo_visual_width(window_height: u16) -> u16 {
|
pub fn logo_visual_width(window_height: u16) -> u16 {
|
||||||
pick_logo(window_height).map_or(24, visual_width)
|
pick_logo(window_height).map_or(24, |s| s.cols)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_logo(area: Rect, buf: &mut Buffer, theme: &Theme, window_height: u16) {
|
pub fn render_logo(area: Rect, buf: &mut Buffer, theme: &Theme, window_height: u16) {
|
||||||
if let Some(logo) = pick_logo(window_height) {
|
if let Some(size) = pick_logo(window_height) {
|
||||||
render_into(area, buf, theme, logo);
|
render_into(area, buf, theme, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The hero box always shows the full logo: it is laid out beside the menu, so
|
/// The hero box always shows the full moon: it is laid out beside the menu,
|
||||||
/// it fits whenever the box does. These report and render that logo directly,
|
/// so it fits whenever the box does. These report and render that logo
|
||||||
/// independent of the height-based [`pick_logo`] tiers used by the stacked
|
/// directly, independent of the height-based [`pick_logo`] tiers used by the
|
||||||
/// layout. When [`logo_hidden`], they report 0 and render nothing.
|
/// stacked layout. When [`logo_hidden`], they report 0 and render nothing.
|
||||||
pub fn full_logo_line_count() -> u16 {
|
pub fn full_logo_line_count() -> u16 {
|
||||||
full_logo_line_count_for(logo_hidden())
|
full_logo_line_count_for(logo_hidden())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn full_logo_line_count_for(hidden: bool) -> u16 {
|
fn full_logo_line_count_for(hidden: bool) -> u16 {
|
||||||
if hidden { 0 } else { count_lines(LOGO) }
|
if hidden { 0 } else { FULL.rows }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn full_logo_visual_width() -> u16 {
|
pub fn full_logo_visual_width() -> u16 {
|
||||||
@@ -187,30 +257,26 @@ pub fn full_logo_visual_width() -> u16 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn full_logo_visual_width_for(hidden: bool) -> u16 {
|
fn full_logo_visual_width_for(hidden: bool) -> u16 {
|
||||||
if hidden { 0 } else { visual_width(LOGO) }
|
if hidden { 0 } else { FULL.cols }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_full_logo(area: Rect, buf: &mut Buffer, theme: &Theme) {
|
pub fn render_full_logo(area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||||
if !logo_hidden() {
|
if !logo_hidden() {
|
||||||
render_into(area, buf, theme, LOGO);
|
render_into(area, buf, theme, FULL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Line count of the small logo used in minimal's committed welcome card
|
/// Line count of the small moon used in minimal's committed welcome card
|
||||||
/// (0 on a legacy Windows console, where the braille art is suppressed).
|
/// (0 on a legacy Windows console, where braille art is suppressed).
|
||||||
pub fn compact_logo_line_count() -> u16 {
|
pub fn compact_logo_line_count() -> u16 {
|
||||||
if logo_hidden() {
|
if logo_hidden() { 0 } else { SMALL.rows }
|
||||||
0
|
|
||||||
} else {
|
|
||||||
count_lines(LOGO_SMALL)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the small braille logo (centered) into `area` for minimal's welcome
|
/// Render the small moon (centered) into `area` for minimal's welcome card.
|
||||||
/// card. No-op when the logo is hidden.
|
/// No-op when the logo is hidden.
|
||||||
pub fn render_compact_logo(area: Rect, buf: &mut Buffer, theme: &Theme) {
|
pub fn render_compact_logo(area: Rect, buf: &mut Buffer, theme: &Theme) {
|
||||||
if !logo_hidden() {
|
if !logo_hidden() {
|
||||||
render_into(area, buf, theme, LOGO_SMALL);
|
render_into(area, buf, theme, SMALL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,21 +284,35 @@ pub fn render_compact_logo(area: Rect, buf: &mut Buffer, theme: &Theme) {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
/// Count sunlit dots across the whole disc at phase `p`.
|
||||||
|
fn lit_dots(p: f32) -> usize {
|
||||||
|
let dots_w = (FULL.cols * 2) as i32;
|
||||||
|
let dots_h = (FULL.rows * 4) as i32;
|
||||||
|
let cx = (dots_w as f32 - 1.0) / 2.0;
|
||||||
|
let cy = (dots_h as f32 - 1.0) / 2.0;
|
||||||
|
let r = (dots_w.min(dots_h) as f32) / 2.0 - 0.5;
|
||||||
|
let mut count = 0;
|
||||||
|
for y in 0..dots_h {
|
||||||
|
for x in 0..dots_w {
|
||||||
|
let dx = (x as f32 - cx) / r;
|
||||||
|
let dy = (y as f32 - cy) / r;
|
||||||
|
if dx * dx + dy * dy <= 1.0 && dot_lit(dx, dy, p) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn logo_sizes_by_height() {
|
fn logo_sizes_by_height() {
|
||||||
assert!(pick_logo_for(SMALL_LOGO_MIN_HEIGHT - 1, false).is_none());
|
assert!(pick_logo_for(SMALL_LOGO_MIN_HEIGHT - 1, false).is_none());
|
||||||
assert_eq!(
|
assert_eq!(pick_logo_for(SMALL_LOGO_MIN_HEIGHT, false), Some(SMALL));
|
||||||
pick_logo_for(SMALL_LOGO_MIN_HEIGHT, false),
|
assert_eq!(pick_logo_for(FULL_LOGO_MIN_HEIGHT - 1, false), Some(SMALL));
|
||||||
Some(LOGO_SMALL)
|
assert_eq!(pick_logo_for(FULL_LOGO_MIN_HEIGHT, false), Some(FULL));
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
pick_logo_for(FULL_LOGO_MIN_HEIGHT - 1, false),
|
|
||||||
Some(LOGO_SMALL)
|
|
||||||
);
|
|
||||||
assert_eq!(pick_logo_for(FULL_LOGO_MIN_HEIGHT, false), Some(LOGO));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The braille art has no legacy-safe stand-in, so every height tier must
|
// The braille moon has no legacy-safe stand-in, so every height tier must
|
||||||
// collapse to no logo when the legacy-console flag is set.
|
// collapse to no logo when the legacy-console flag is set.
|
||||||
#[test]
|
#[test]
|
||||||
fn logo_hidden_on_legacy_console_at_every_height() {
|
fn logo_hidden_on_legacy_console_at_every_height() {
|
||||||
@@ -243,12 +323,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hero_box_always_uses_full_logo() {
|
fn hero_box_always_uses_full_logo() {
|
||||||
// The box renders the full logo regardless of height (it's laid out
|
// The box renders the full moon regardless of height (it's laid out
|
||||||
// beside the menu), and it's the large variant — never the small one.
|
// beside the menu), and it's the large variant — never the small one.
|
||||||
assert_eq!(full_logo_line_count_for(false), count_lines(LOGO));
|
assert_eq!(full_logo_line_count_for(false), FULL.rows);
|
||||||
assert_eq!(full_logo_visual_width_for(false), visual_width(LOGO));
|
assert_eq!(full_logo_visual_width_for(false), FULL.cols);
|
||||||
assert!(full_logo_line_count_for(false) > count_lines(LOGO_SMALL));
|
assert!(full_logo_line_count_for(false) > SMALL.rows);
|
||||||
assert!(full_logo_visual_width_for(false) > visual_width(LOGO_SMALL));
|
assert!(full_logo_visual_width_for(false) > SMALL.cols);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -258,61 +338,89 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn compact_logo_line_count_matches_small_logo_when_visible() {
|
fn compact_logo_is_the_small_moon() {
|
||||||
// The minimal welcome card budgets exactly the small logo's rows. When
|
|
||||||
// the logo isn't hidden, the count equals the small art's line count and
|
|
||||||
// is strictly shorter than the full logo.
|
|
||||||
if !logo_hidden() {
|
if !logo_hidden() {
|
||||||
assert_eq!(compact_logo_line_count(), count_lines(LOGO_SMALL));
|
assert_eq!(compact_logo_line_count(), SMALL.rows);
|
||||||
assert!(compact_logo_line_count() < count_lines(LOGO));
|
assert!(compact_logo_line_count() < FULL.rows);
|
||||||
assert!(compact_logo_line_count() > 0);
|
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(compact_logo_line_count(), 0);
|
assert_eq!(compact_logo_line_count(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shine_opacity_stays_in_unit_range() {
|
fn full_moon_lights_the_whole_disc_and_new_moon_none() {
|
||||||
let mut secs = 0.0;
|
let full = lit_dots(0.5);
|
||||||
while secs < 10.0 {
|
assert!(full > 0, "full moon must light the disc");
|
||||||
for i in 0..=20 {
|
assert_eq!(lit_dots(0.0), 0, "new moon must be fully dark");
|
||||||
let diag = i as f32 / 20.0;
|
// Quarter phases light about half the disc.
|
||||||
let op = shine_opacity(diag, secs);
|
let quarter = lit_dots(0.25);
|
||||||
assert!(
|
assert!(
|
||||||
(0.0..=1.0).contains(&op),
|
(full / 3..=2 * full / 3).contains(&quarter),
|
||||||
"opacity {op} out of range at diag {diag}, secs {secs}"
|
"first quarter lit {quarter} should be near half of full {full}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
secs += 0.13;
|
|
||||||
|
#[test]
|
||||||
|
fn illumination_waxes_then_wanes() {
|
||||||
|
// Monotone growth to full, then monotone decay — the terminator
|
||||||
|
// sweeps once across the disc per lunation.
|
||||||
|
let phases = [0.05, 0.15, 0.25, 0.35, 0.45];
|
||||||
|
for w in phases.windows(2) {
|
||||||
|
assert!(
|
||||||
|
lit_dots(w[0]) < lit_dots(w[1]),
|
||||||
|
"waxing must grow: p={} vs p={}",
|
||||||
|
w[0],
|
||||||
|
w[1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let phases = [0.55, 0.65, 0.75, 0.85, 0.95];
|
||||||
|
for w in phases.windows(2) {
|
||||||
|
assert!(
|
||||||
|
lit_dots(w[0]) > lit_dots(w[1]),
|
||||||
|
"waning must shrink: p={} vs p={}",
|
||||||
|
w[0],
|
||||||
|
w[1]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shine_band_sweeps_across() {
|
fn waxing_lights_the_right_limb_first() {
|
||||||
// The brightest point along the diagonal advances left → right as the
|
// Shortly after new moon the crescent must hang on the right side.
|
||||||
// sweep progresses through its active phase.
|
assert!(dot_lit(0.95, 0.0, 0.1), "right limb lit while waxing");
|
||||||
let brightest = |secs: f32| -> f32 {
|
assert!(!dot_lit(-0.95, 0.0, 0.1), "left limb dark while waxing");
|
||||||
(0..=100)
|
// And on the left side while waning.
|
||||||
.map(|i| i as f32 / 100.0)
|
assert!(dot_lit(-0.95, 0.0, 0.9), "left limb lit while waning");
|
||||||
.max_by(|a, b| {
|
assert!(!dot_lit(0.95, 0.0, 0.9), "right limb dark while waning");
|
||||||
shine_opacity(*a, secs)
|
|
||||||
.partial_cmp(&shine_opacity(*b, secs))
|
|
||||||
.unwrap()
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
let early = brightest(0.1);
|
|
||||||
let mid = brightest(0.4);
|
|
||||||
let late = brightest(0.7);
|
|
||||||
assert!(early < mid, "early {early} should precede mid {mid}");
|
|
||||||
assert!(mid < late, "mid {mid} should precede late {late}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shine_rests_dim_between_sweeps() {
|
fn new_moon_keeps_a_silhouette_ring() {
|
||||||
// During the rest phase the band is parked off-screen, so an interior
|
// Even at p=0 the rasterizer must emit outline cells so the moon
|
||||||
// glyph falls back to at most the gentle pulse — never full bright.
|
// never vanishes from the welcome screen.
|
||||||
let op = shine_opacity(0.5, 6.0); // secs % 4.0 = 2.0 → past SWEEP_FRAC, in the rest phase
|
let cells = moon_cells(FULL, 0.0);
|
||||||
assert!(op < 0.2, "resting opacity {op} should stay dim");
|
let drawn = cells.iter().flatten().flatten().count();
|
||||||
|
assert!(drawn > 0, "new moon must keep an outline ring");
|
||||||
|
assert!(
|
||||||
|
cells.iter().flatten().flatten().all(|c| !c.lit),
|
||||||
|
"no cell may be lit at new moon"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn moon_raster_is_round_and_fills_the_grid() {
|
||||||
|
// The disc must span (nearly) the whole cell grid in both axes at
|
||||||
|
// full moon — this is what "make it larger" bought us.
|
||||||
|
let cells = moon_cells(FULL, 0.5);
|
||||||
|
assert_eq!(cells.len(), FULL.rows as usize);
|
||||||
|
assert!(cells.iter().all(|r| r.len() == FULL.cols as usize));
|
||||||
|
let first_row_drawn = cells.first().unwrap().iter().flatten().count();
|
||||||
|
let mid_row_drawn = cells[FULL.rows as usize / 2].iter().flatten().count();
|
||||||
|
assert!(first_row_drawn > 0, "top of the disc must be drawn");
|
||||||
|
assert!(
|
||||||
|
mid_row_drawn > first_row_drawn,
|
||||||
|
"equator must be wider than the pole (round disc)"
|
||||||
|
);
|
||||||
|
assert_eq!(mid_row_drawn, FULL.cols as usize, "equator spans the grid");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2759,11 +2759,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hero_box_inactive_when_warning_would_overflow() {
|
fn hero_box_inactive_when_warning_would_overflow() {
|
||||||
// Regression: the box is forced to the full 7-row logo, so even a
|
// Regression: the box is forced to the full 10-row moon, so even a
|
||||||
// 3-item menu needs 11 box rows. A startup warning (error_height = 2)
|
// 3-item menu needs 14 box rows (min content 20). A startup warning
|
||||||
// pushes the total past height 19, so the gate must fall back to the
|
// (error_height = 2, +1 gap) pushes the total past height 20, so the
|
||||||
// stacked layout instead of overflowing by a row.
|
// gate must fall back to the stacked layout instead of overflowing.
|
||||||
let area = Rect::new(0, 0, 90, 19);
|
let area = Rect::new(0, 0, 90, 20);
|
||||||
let with_warning = WelcomeLayout::compute(WelcomeLayoutInput {
|
let with_warning = WelcomeLayout::compute(WelcomeLayoutInput {
|
||||||
content_area: area,
|
content_area: area,
|
||||||
error_height: 2,
|
error_height: 2,
|
||||||
@@ -2814,12 +2814,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hero_box_does_not_overflow_with_tall_menu() {
|
fn hero_box_does_not_overflow_with_tall_menu() {
|
||||||
// A 6-item menu makes the box 2 rows taller than the default-4 box, so
|
// A tall menu can outgrow the default box, so the centering pad
|
||||||
// the centering pad (derived from the default box) must be clamped or
|
// (derived from the default box) must be clamped or the box gets
|
||||||
// the box gets pushed down and the version row clips at exactly
|
// pushed down and the version row clips at exactly
|
||||||
// min_content_height. 19 == min_content_height(0, 6, 0, 0): a 13-row box
|
// min_content_height. 20 == min_content_height(0, 6, 0, 0): a 14-row
|
||||||
// + 1 flex gap + 5 fixed-below.
|
// box (10-row moon) + 1 flex gap + 5 fixed-below.
|
||||||
let area = Rect::new(0, 0, 100, 19);
|
let area = Rect::new(0, 0, 100, 20);
|
||||||
let layout = WelcomeLayout::compute(WelcomeLayoutInput {
|
let layout = WelcomeLayout::compute(WelcomeLayoutInput {
|
||||||
content_area: area,
|
content_area: area,
|
||||||
menu_height: 6,
|
menu_height: 6,
|
||||||
@@ -2845,9 +2845,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hero_box_height_accounts_for_borders_and_padding() {
|
fn hero_box_height_accounts_for_borders_and_padding() {
|
||||||
// At h >= 26, logo07 is used (7 lines). With menu_height=3:
|
// At h >= 26 the full moon is used (10 lines). With menu_height=3:
|
||||||
// right_col = 2 + 0 + 0 + 1 + 3 = 6, inner = max(7, 6) = 7.
|
// right_col = 2 + 0 + 0 + 1 + 3 = 6, inner = max(10, 6) = 10.
|
||||||
// hero_box_height = 2 (borders) + 2 (v_pad) + 7 = 11.
|
// hero_box_height = 2 (borders) + 2 (v_pad) + 10 = 14.
|
||||||
let area = Rect::new(0, 0, 100, 50);
|
let area = Rect::new(0, 0, 100, 50);
|
||||||
let layout = WelcomeLayout::compute(WelcomeLayoutInput {
|
let layout = WelcomeLayout::compute(WelcomeLayoutInput {
|
||||||
content_area: area,
|
content_area: area,
|
||||||
@@ -2855,7 +2855,7 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
assert!(layout.has_hero_box());
|
assert!(layout.has_hero_box());
|
||||||
assert_eq!(layout.hero_box.height, 11);
|
assert_eq!(layout.hero_box.height, 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+16
-18
@@ -18,7 +18,7 @@ async fn welcome_screen_braille_logo_renders_correctly() {
|
|||||||
let content = ContentController::start().await.expect("start content");
|
let content = ContentController::start().await.expect("start content");
|
||||||
|
|
||||||
let binary = pager_binary().expect("resolve pager binary");
|
let binary = pager_binary().expect("resolve pager binary");
|
||||||
// Use a tall terminal so pick_logo() selects the 7-line logo (≥26 rows).
|
// Use a tall terminal so pick_logo() selects the full moon (≥26 rows).
|
||||||
let mut harness =
|
let mut harness =
|
||||||
PtyHarness::spawn_with_content(&binary, DEFAULT_ROWS, DEFAULT_COLS, &content, &[])
|
PtyHarness::spawn_with_content(&binary, DEFAULT_ROWS, DEFAULT_COLS, &content, &[])
|
||||||
.expect("spawn pager");
|
.expect("spawn pager");
|
||||||
@@ -29,24 +29,22 @@ async fn welcome_screen_braille_logo_renders_correctly() {
|
|||||||
|
|
||||||
let screen = harness.screen_contents();
|
let screen = harness.screen_contents();
|
||||||
|
|
||||||
// The logo contains distinctive Braille characters. If the writer
|
// The moon logo is drawn from Braille Pattern characters. If the writer
|
||||||
// thread sends raw UTF-8 bytes through a code-page-dependent API,
|
// thread sends raw UTF-8 bytes through a code-page-dependent API, these
|
||||||
// these 3-byte characters would be mangled into 3 separate single-
|
// 3-byte characters would be mangled into 3 separate single-byte
|
||||||
// byte characters each (e.g. Cyrillic). Check for a few that only
|
// characters each (e.g. Cyrillic). The moon is procedurally generated,
|
||||||
// appear in the logo — not in any ASCII menu label.
|
// so assert on the character class rather than specific glyphs: a full
|
||||||
//
|
// interior cell (⣿) is present through most of the lunation, and there
|
||||||
// From logo07.txt line 2: ⣠⣾⠿⠛
|
// must be a healthy number of non-blank braille cells overall.
|
||||||
|
let braille_dots = screen
|
||||||
|
.chars()
|
||||||
|
.filter(|c| ('\u{2801}'..='\u{28FF}').contains(c))
|
||||||
|
.count();
|
||||||
assert!(
|
assert!(
|
||||||
screen.contains('⣾'),
|
braille_dots >= 20,
|
||||||
"Braille character ⣾ (U+28FE) not found in screen — \
|
"expected the braille moon (>= 20 non-blank braille cells), found \
|
||||||
logo may be garbled by code-page misinterpretation.\n\
|
{braille_dots} — logo may be garbled by code-page \
|
||||||
Screen contents:\n{screen}"
|
misinterpretation.\nScreen contents:\n{screen}"
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
screen.contains('⣿'),
|
|
||||||
"Braille character ⣿ (U+28FF) not found in screen — \
|
|
||||||
logo may be garbled.\n\
|
|
||||||
Screen contents:\n{screen}"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
harness.quit().expect("clean quit");
|
harness.quit().expect("clean quit");
|
||||||
|
|||||||
Reference in New Issue
Block a user