geodesic-wallpaper is a real-time animated desktop wallpaper for Windows that renders families of geodesic curves flowing across parameterised Riemannian surfaces. Curves are integrated with a fourth-order Runge-Kutta (RK4) scheme using analytically-computed Christoffel symbols for fourteen built-in surfaces (torus, sphere, saddle, catenoid, helicoid, hyperboloid, hyperbolic paraboloid, ellipsoid, Enneper, Klein bottle, Boy surface, torus knot, pseudosphere, trefoil tube). The window sits below all application windows via a Win32 WM_WINDOWPOSCHANGING hook so desktop icons remain fully accessible. Optional modules let you drive the animation from live financial market data and assign a different surface to each physical monitor.
. . . * * * * . . .
. * __-------__ * .
* / / ~~~~ \ \ *
* | | (( . . )) | | *
. | | \\ \_/ // | | .
* | | \\ | // | | *
* \ \ ``---'' / / *
. * ----------- * .
. . . * * * * . . .
Geodesic curves flowing across a torus surface.
Each coloured trail fades from opaque at the head
to transparent at the tail (quadratic alpha decay).
Surfaces range from the familiar torus and sphere to exotic objects like the non-orientable Klein bottle, the self-intersecting Enneper minimal surface, and the trefoil torus-knot tube.
- Download
geodesic-wallpaper-windows.zipfrom the Releases page. - Extract the ZIP — you will find
geodesic-wallpaper.exeand a sampleconfig.toml. - Double-click
geodesic-wallpaper.exe, or run from a terminal:
.\geodesic-wallpaper.exeRequirements: Windows 10 or 11, any GPU with DirectX 12 or Vulkan support. No installer, no runtime dependencies.
.\geodesic-wallpaper.exe --preset cosmic
.\geodesic-wallpaper.exe --preset ocean
.\geodesic-wallpaper.exe --preset emberSee the Scene Presets section for the full list.
All fields are optional. Missing fields revert to the defaults shown.
Place config.toml in the same directory as the executable. The file is hot-reloaded automatically whenever it changes on disk — no restart required.
| Field | Type | Default | Description |
|---|---|---|---|
surface |
string | "torus" |
Surface to render. See Supported Surfaces. |
num_geodesics |
integer | 30 |
Number of simultaneous geodesic curves. |
trail_length |
integer | 300 |
Frames a trail persists before respawning. |
rotation_speed |
float | 0.001047 |
Camera orbit speed in radians per second. |
color_palette |
string[] | 5 entries | CSS hex colour strings cycled across geodesics. |
torus_R |
float | 2.0 |
Torus major radius (centre to tube centre). |
torus_r |
float | 0.7 |
Torus minor radius (tube radius). |
time_step |
float | 0.016 |
RK4 integration timestep in seconds per frame. |
catenoid_c |
float | 1.0 |
Catenoid scale parameter. |
helicoid_c |
float | 1.0 |
Helicoid pitch parameter. |
hyperboloid_a |
float | 1.0 |
Hyperboloid semi-axis a. |
hyperboloid_b |
float | 1.0 |
Hyperboloid semi-axis b. |
ellipsoid_a |
float | 2.0 |
Ellipsoid semi-axis along x. |
ellipsoid_b |
float | 1.5 |
Ellipsoid semi-axis along y. |
ellipsoid_c |
float | 1.0 |
Ellipsoid semi-axis along z. |
hyperbolic_paraboloid_a |
float | 1.0 |
Saddle+ semi-axis a. |
hyperbolic_paraboloid_b |
float | 1.0 |
Saddle+ semi-axis b. |
camera_distance |
float | 6.0 |
Camera distance from origin. |
camera_elevation |
float | 0.4 |
Camera elevation in radians. |
camera_fov |
float | 0.8 |
Vertical field-of-view in radians. |
show_wireframe |
bool | true |
Render surface wireframe mesh. |
trail_fade_power |
float | 2.0 |
Exponent for trail alpha fade (1=linear, 2=quadratic). |
target_fps |
integer | 30 |
Target frame rate. |
background_color |
string | "#050510" |
Background clear colour as CSS hex. |
color_mode |
string | "cycle" |
"cycle" or "random" colour assignment. |
gallery_mode |
bool | false |
Auto-cycle through all surfaces. |
gallery_duration_s |
integer | 30 |
Seconds per surface in gallery mode. |
lua_script |
string | — | Path to a Lua surface script (requires --features lua). |
surface = "torus"
num_geodesics = 30
trail_length = 300
rotation_speed = 0.001047
background_color = "#050510"
color_palette = ["#4488FF", "#88DDFF", "#FFD700", "#88FF88", "#FF88CC"]When you have more than one physical monitor connected, multi_monitor::MultiMonitorManager assigns a different surface and colour scheme to each display automatically.
At startup the manager enumerates monitors via Win32 EnumDisplayMonitors, then cycles through the twelve built-in surfaces in order — monitor 0 gets the torus, monitor 1 gets the sphere, and so on.
The simplest option is to launch with --preset per monitor from separate instances, or define per-monitor overrides programmatically:
use geodesic_wallpaper::multi_monitor::{MonitorConfig, MultiMonitorManager};
let manager = MultiMonitorManager::from_config(vec![
MonitorConfig {
monitor_index: 0,
surface: "torus".into(),
color_scheme: "cosmic".into(),
geodesic_count: 35,
speed: 1.0,
},
MonitorConfig {
monitor_index: 1,
surface: "sphere".into(),
color_scheme: "ocean".into(),
geodesic_count: 25,
speed: 0.8,
},
]);
println!("{} monitors configured", manager.monitor_count());If no explicit configuration is provided, MultiMonitorManager::new() auto-assigns surfaces:
| Monitor | Surface | Colour scheme |
|---|---|---|
| 0 | torus | cosmic |
| 1 | sphere | ocean |
| 2 | saddle | ember |
| 3 | hyperboloid | forest |
| 4 | klein_bottle | aurora |
| 5+ | cycles back | cycles back |
The finance_driver::FinanceDriver maps OHLCV market data to geodesic animation parameters in real time.
| Market signal | Geodesic parameter |
|---|---|
| Realized volatility (stddev of log-returns) | speed_multiplier — geodesics accelerate in volatile markets |
| Trend direction (OLS slope of close prices) | theta_velocity — positive in uptrends, negative in downtrends |
| Normalized volume | trail_width and phi_velocity |
| Trend mapped to [0, 0.33] | color_hue — red in downtrends, green in uptrends |
use geodesic_wallpaper::finance_driver::{FinanceDriver, MarketBar};
let mut driver = FinanceDriver::new(20); // 20-bar rolling window
driver.push_bar(MarketBar {
open: 150.00, high: 152.30, low: 149.10, close: 151.80,
volume: 1_234_567.0, timestamp: 1_700_000_000,
});
let params = driver.compute_params();
println!("Speed multiplier: {:.3}", params.speed_multiplier);
println!("Trail width: {:.1}px", params.trail_width);
println!("Color hue: {:.3}", params.color_hue);The CSV format is timestamp,open,high,low,close,volume with no header row. Lines starting with # are treated as comments.
use geodesic_wallpaper::finance_driver::FinanceDriver;
let csv = std::fs::read_to_string("prices.csv").unwrap();
let mut driver = FinanceDriver::new(50);
let loaded = driver.load_csv(&csv);
println!("Loaded {loaded} bars");
let params = driver.compute_params();Sample prices.csv:
# timestamp,open,high,low,close,volume
1700000000,100.00,102.50,99.20,101.75,980000
1700000060,101.75,103.00,101.00,102.40,1150000
1700000120,102.40,102.80,100.50,100.90,870000The scene_presets::PresetLibrary ships with 10 built-in named configurations.
| Name | Surface | Mood |
|---|---|---|
cosmic |
torus | Deep-space: gold geodesics through an indigo void |
ocean |
sphere | Ocean depths: cyan great-circle flows |
ember |
saddle | Volcanic: orange-red streaks across a hyperbolic saddle |
forest |
hyperboloid | Canopy: lush greens on a one-sheeted hyperboloid |
aurora |
torus_knot | Arctic: shimmering green-blue trefoil traces |
neon |
klein_bottle | City night: hot pink and electric blue on the Klein bottle |
dusk |
ellipsoid | Twilight: rose-to-purple sweeps |
ice |
catenoid | Crystal cave: steel-blue spirals around the catenoid waist |
lava |
boy_surface | Volcanic: molten yellow-red on Boy's surface |
void |
enneper | Deep nothing: slow teal traces on the Enneper surface |
.\geodesic-wallpaper.exe --preset aurora
.\geodesic-wallpaper.exe --preset neonuse geodesic_wallpaper::scene_presets::PresetLibrary;
let mut lib = PresetLibrary::with_defaults();
// Navigate presets.
let next = lib.next();
println!("Switched to: {} — {}", next.name, next.description);
// Look up by name.
if let Some(p) = lib.by_name("cosmic") {
println!("Background: {}", p.background_hex());
}All surfaces implement the Surface trait: position(), normal(), metric(), christoffel(), wrap(), random_position(), random_tangent(), and mesh_vertices().
| Surface | Config name | Curvature | Notes |
|---|---|---|---|
| Torus | "torus" |
Mixed | Analytic Christoffels; ergodic irrational windings |
| Sphere | "sphere" |
Constant positive K = 1/R² | All geodesics are great circles |
| Saddle | "saddle" |
Zero (flat chart) | Straight-line geodesics |
| Catenoid | "catenoid" |
Negative (minimal) | Geodesics spiral around the waist |
| Helicoid | "helicoid" |
Negative (minimal) | Isometric to catenoid |
| Hyperboloid | "hyperboloid" |
Negative | One-sheeted ruled quadric |
| Hyperbolic paraboloid | "hyperbolic_paraboloid" |
Negative | Doubly ruled; z = u²/a² − v²/b² |
| Ellipsoid | "ellipsoid" |
Positive (varying) | Three independent semi-axes |
| Enneper | "enneper" |
Negative (minimal) | Complete; total curvature −4π; self-intersects |
| Klein bottle | "klein_bottle" |
Non-orientable | Figure-8 immersion in ℝ³ |
| Boy's surface | "boy_surface" |
Non-orientable | RP² with 3-fold symmetry (Apery form) |
| Torus knot | "torus_knot" |
Positive (tube) | Default T(2,3) trefoil |
| Pseudosphere | "pseudosphere" |
Constant negative K = −1 | Tractricoid; geodesics diverge exponentially — the hyperbolic plane's classic model surface |
| Trefoil tube | "trefoil" |
Positive (tube) | Circular cross-section swept around the trefoil knot curve; geodesics precess across all three lobes |
| Key | Action |
|---|---|
S |
Cycle to next surface |
G |
Toggle gallery mode |
LEFT |
Previous surface (gallery mode) |
RIGHT |
Next surface (gallery mode) |
+ / = |
Increase rotation speed ×1.1 |
- |
Decrease rotation speed ×0.9 |
R |
Reset all geodesics |
H |
Toggle FPS HUD overlay |
[ |
Select previous tunable parameter |
] |
Select next tunable parameter |
Shift+R |
Start / stop phase portrait recording |
| Action | Effect |
|---|---|
| Left-click | Shoot a new geodesic from the clicked surface point |
| Right-click | Remove all geodesics and reset |
| Middle-click | Cycle to the next surface |
| Scroll up | Increase geodesic speed ×1.1 per notch |
| Scroll down | Decrease geodesic speed ×0.9 per notch |
Gallery mode cycles through all fourteen built-in surfaces automatically with a smooth cross-fade transition. It acts as a mathematical screensaver, showing each surface for a configurable interval before smoothly transitioning to the next.
gallery_mode = true
gallery_duration_s = 30 # seconds per surface (default 30, minimum 1).\geodesic-wallpaper.exe --gallery| Key | Action |
|---|---|
G |
Toggle gallery mode on / off |
RIGHT / SPACE |
Skip to next surface immediately |
LEFT |
Skip to previous surface |
Each surface change is accompanied by a brief cross-fade (0.75 s fade-out, 0.75 s fade-in). The caller reads GalleryMode::transition_alpha() each frame and multiplies scene opacity accordingly.
torus → sphere → saddle → enneper → catenoid → helicoid →
hyperboloid → hyperbolic_paraboloid → ellipsoid → (wraps)
use geodesic_wallpaper::gallery::GalleryMode;
// Create, enabled from the start, 45 seconds per surface.
let mut gallery = GalleryMode::new(true, 45);
// In the render loop:
let surface_changed = gallery.update();
let alpha = gallery.transition_alpha(); // multiply scene opacity by this
if surface_changed {
let name = gallery.current_surface();
println!("Now showing: {name}");
// switch the renderer to this surface
}pub struct GalleryConfig {
pub surfaces: Vec<SurfaceKind>, // subset to cycle (default: all 14)
pub interval_secs: f32, // seconds per surface
pub transition_secs: f32, // fade half-duration
}Build with --features lua to define entirely custom Riemannian surfaces in Lua.
cargo build --release --features lua-- my_surface.lua
function metric(u, v)
return { g_uu=1.0, g_uv=0.0, g_vu=0.0, g_vv=math.sin(u)^2 }
endsurface = "lua"
lua_script = "my_surface.lua"The script is hot-reloaded whenever config.toml changes. Invalid scripts fall back to the torus.
[tuning]
[[tuning.parameters]]
name = "rotation_speed"
min = 0.0
max = 0.1
current = 0.001047
step = 0.0001Use [ / ] to select a parameter and - / = to adjust its value live. Updated values are written back to config.toml on exit.
Press Shift+R to record. Frames are saved as PNGs and assembled into geodesic-recording.gif when recording stops. Default: 10 seconds at 30 fps.
Instead of rendering a fixed set of individual geodesic trails, the geodesic field mode fills the entire parameter domain (u, v) with a dense grid of small coloured arrows — one per grid cell — each pointing in the geodesic direction and coloured by the long-term fate of that geodesic.
Each grid cell seeds a geodesic and integrates it for fate_steps RK4 steps. The final state determines the colour:
| Fate | Condition | Arrow colour |
|---|---|---|
Bounded |
Stays within escape_radius for all steps |
Blue |
Escaping |
Exceeds escape_radius |
Red |
Looping |
Returns within loop_epsilon of start |
Green |
The resulting image reveals the basin structure of geodesic flow: which starting directions lead to bounded wandering, which ones escape the parameterisation domain, and which form closed loops.
pub struct FieldConfig {
pub grid_n: usize, // grid cells per axis (default 40, total = 40²= 1600)
pub u_range: [f64; 2], // parameter u range (default [-π, π])
pub v_range: [f64; 2], // parameter v range (default [-π, π])
pub fate_steps: usize, // integration steps per cell (default 200)
pub fate_dt: f64, // RK4 timestep for fate integration (default 0.04)
pub escape_radius: f64, // escape threshold (default 8.0)
pub loop_epsilon: f64, // loop detection radius (default 0.3)
pub arrow_length: f32, // arrow display length (default 0.04)
pub arrow_alpha: f32, // arrow opacity (default 0.7)
}use geodesic_wallpaper::field::{FieldConfig, FieldRenderer};
let cfg = FieldConfig { grid_n: 60, ..Default::default() };
let renderer = FieldRenderer::new(cfg);
// Provide your RK4 geodesic step function.
let field = renderer.compute(|u, v, du, dv| {
// one step of the geodesic ODE on your surface
(new_u, new_v, new_du, new_dv)
});
println!("{} arrows, {} bounded, {} escaping, {} looping",
field.arrows.len(),
field.fate_counts[0], field.fate_counts[1], field.fate_counts[2]);
// Upload field.arrows as an instanced vertex buffer for GPU rendering.The computation is parallelised with rayon across all grid_n² cells, so a 60×60 grid (3 600 cells × 200 steps) completes in well under a second on a modern CPU.
Each FlowArrow is repr(C) and implements bytemuck::Pod:
pub struct FlowArrow {
pub origin: [f32; 2], // (u, v) base position
pub direction: [f32; 2], // normalised direction vector
pub length: f32, // display length
pub color: [f32; 4], // RGBA
pub fate: u32, // 0=Bounded 1=Escaping 2=Looping
}On a Riemannian surface with metric g_{ij} a geodesic γ(t) satisfies:
d²uⁱ/dt² + Γⁱⱼₖ (duʲ/dt)(duᵏ/dt) = 0
where Γⁱⱼₖ = ½ gⁱˡ (∂ⱼgₗₖ + ∂ₖgₗⱼ − ∂ₗgⱼₖ) are the Christoffel symbols of the second kind. All fourteen built-in surfaces provide analytic christoffel() implementations so that the RK4 integrator never approximates these symbols numerically.
| Surface | Gaussian curvature K | Geodesic character |
|---|---|---|
| Sphere | K = +1/R² (constant) | Great circles — all geodesics are closed |
| Torus | Mixed (positive outer, negative inner) | Depends on winding ratio: rational = periodic, irrational = dense (ergodic) |
| Saddle / flat | K = 0 | Straight lines in parameter space |
| Catenoid / helicoid | K < 0 (minimal) | Geodesics spiral and diverge |
| Pseudosphere | K = −1 (constant) | Maximal divergence — model of the hyperbolic plane |
| Hyperboloid | K < 0 | Asymptotic geodesics along the rulings |
For any compact surface Σ without boundary:
∬_Σ K dA = 2π χ(Σ)
where χ is the Euler characteristic. This connects the local curvature of each built-in surface to its global topology (sphere: χ=2, torus: χ=0, Klein bottle: χ=0, RP²: χ=1).
| Module | Responsibility |
|---|---|
config |
Load and hot-reload config.toml; parse CSS hex colours |
error |
Typed error enum covering all subsystems |
surface |
Surface trait + 14 implementations |
geodesic |
RK4 integrator for the geodesic ODE |
field |
Dense geodesic field (basin visualization, FlowArrow instances) |
interactive |
Mouse event handling; geodesic shooting |
trail |
Fixed-capacity ring buffer with quadratic alpha fade |
renderer |
wgpu render pipelines (surface wireframe + trail lines) |
wallpaper |
Win32 borderless window pinned below all app windows |
gallery |
Auto-cycle through surfaces in gallery mode with cross-fade |
parameter_tuner |
Runtime keyboard-driven parameter adjustment |
recorder |
Phase portrait PNG/GIF recording |
multi_monitor |
Per-monitor surface assignment and configuration |
finance_driver |
Maps OHLCV market data to geodesic parameters |
scene_presets |
Named scene configurations with sequential navigation |
main |
Application entry point, message loop, hot-reload watcher |
Requirements: Rust stable 1.75+, Windows 10 or 11, GPU with DirectX 12 or Vulkan support.
git clone https://github.com/Mattbusel/geodesic-wallpaper.git
cd geodesic-wallpaper
cargo build --release
.\target\release\geodesic-wallpaper.exeTo run tests (no GPU required):
cargo test --libTo build with Lua scripting support:
cargo build --release --features lua- Fork the repository.
- Create a feature branch:
git checkout -b my-feature. - Ensure
cargo fmt,cargo clippy -- -D warnings, andcargo test --liball pass. - Open a pull request against
main.
CI enforces formatting, Clippy warnings-as-errors, the full test suite, and a release build before merging.
Export a sequence of PNG frames by interpolating one or more parameters over time.
geodesic-wallpaper --animate --frames 60 --fps 30 --out-dir ./framesFrames are written as frames/frame_0000.png through frames/frame_0059.png.
AnimationParameter |
Description |
|---|---|
RotationAngle |
Camera orbit angle (radians) |
Scale |
Scene scale factor |
ColorHue |
Hue rotation of the color palette (degrees) |
WindingNumber |
Symmetry winding number |
| Mode | Formula |
|---|---|
Linear |
start + (end - start) * t |
Sinusoidal |
start + (end - start) * 0.5 * (1 - cos(π·t)) |
use geodesic_wallpaper::animation::{
AnimationConfig, AnimationExporter, AnimationParameter, FrameInterpolator, InterpolationMode,
};
use std::path::PathBuf;
let config = AnimationConfig {
frames: 60, fps: 30, width: 1920, height: 1080,
output_dir: PathBuf::from("./frames"),
};
let interp = FrameInterpolator::new(
AnimationParameter::RotationAngle, 0.0, std::f64::consts::TAU, InterpolationMode::Linear,
);
let exporter = AnimationExporter::new(config, vec![interp]);
let stats = exporter.export(|frame_idx, params, path| {
// render frame to path
Ok(())
}).unwrap();
println!("{} frames in {}ms", stats.frames_written, stats.duration_ms);Two high-complexity wallpaper groups — p4g and p6m — are now implemented. Each maps any 2D coordinate to a canonical fundamental domain, enabling symmetric texture and color mapping for geodesic surfaces.
use geodesic_wallpaper::symmetry::{P4g, SymmetryGroup};
let p4g = P4g::new(1.0);
let (u, v) = p4g.to_fundamental_domain(1.3, 2.7);
let orbit = p4g.orbit(0.4, 0.2); // 8 symmetry copies
let color = p4g.color_value(0.7, 0.3); // 0..=1 for palette indexingSymmetry operations: 4 rotations (0°, 90°, 180°, 270°) + 4 diagonal glide reflections.
use geodesic_wallpaper::symmetry::{P6m, SymmetryGroup};
let p6m = P6m::new(1.0);
let (u, v) = p6m.to_fundamental_domain(0.5, 0.8);
let orbit = p6m.orbit(1.0, 0.0); // 12 symmetry copiesSymmetry operations: 6 rotations (0°–300° in 60° steps) + 6 reflections.
use geodesic_wallpaper::symmetry::{P6m, sample_pattern};
let p6m = P6m::new(1.0);
let grid = sample_pattern(&p6m, 256, 256, (-2.0, 2.0), (-2.0, 2.0));
// grid is a 256×256 flat Vec<f32> with values in [0, 1]Generate HSL-based color palettes using classical color theory.
geodesic-wallpaper --palette triadic:240 --palette-steps 8
geodesic-wallpaper --palette rainbow --palette-steps 12
geodesic-wallpaper --palette monochromatic:120 --palette-steps 6| Type | Description |
|---|---|
rainbow |
Evenly spread hues across the full 360° wheel |
monochromatic:HUE |
Shades of a single hue (varying lightness) |
complementary:HUE |
Two opposing hues (180° apart) |
triadic:HUE |
Three equidistant hues (120° apart) |
analogous:HUE |
Adjacent hues (±30° from base) |
use geodesic_wallpaper::palette::{PaletteGenerator, PaletteType, hsl_to_rgb};
// Generate a triadic palette with 6 colors based at hue 240° (blue)
let palette = PaletteGenerator::generate(PaletteType::Triadic(240.0), 6);
println!("Palette: {}", palette.name);
for hex in palette.to_hex_strings() {
println!(" {}", hex);
}
// Parse a palette spec from a string (e.g. from CLI)
let p = PaletteGenerator::from_spec("analogous:60", 8).unwrap();
// HSL to RGB conversion
let [r, g, b] = hsl_to_rgb(120.0, 0.8, 0.5); // greensrc/preview.rs provides a parameter-tuning preview that renders the current
wallpaper pattern to the terminal as Unicode block characters (░▒▓█).
# Render a 40×20 ASCII preview of the current pattern and exit
geodesic-wallpaper --previewThe preview shows a 40×20 block-character grid with a header row listing the current symmetry group, scale, rotation, hue offset, and animation speed.
use geodesic_wallpaper::preview::{WallpaperParams, AsciiPreview, TuiApp};
// Adjust parameters
let mut params = WallpaperParams::default();
params.scale = 2.0;
params.hue_offset = 90.0;
params.cycle_symmetry_group(); // cycle p1 → p2 → ... → p6m → p1
params.clamp(); // clamp all values to valid ranges
// Render to a buffer
let mut buf = Vec::new();
AsciiPreview::render(¶ms, 40, 20, &mut buf).unwrap();
// Full-app one-shot render
let app = TuiApp::new();
let result = app.run().unwrap(); // prints to stdout
println!("saved: {}", result.saved);src/gradient.rs generates smooth color gradients mapped over the wallpaper
pattern via a pattern_fn(x, y) -> f32 that returns a value in [0, 1].
geodesic-wallpaper --gradient sunset
geodesic-wallpaper --gradient ocean
geodesic-wallpaper --gradient plasma --headless --output gradient_preview.png| Preset | Description |
|---|---|
sunset |
Dark blue → magenta → orange → light gold |
ocean |
Deep navy → mid-ocean blue → cyan → pale sky |
forest |
Dark green → vivid green → light lime |
plasma |
Deep violet → purple → pink → orange → yellow |
greyscale |
Black → white |
use geodesic_wallpaper::gradient::{Gradient, GradientPreset, GradientStop, GradientTexture};
// Use a built-in preset
let gradient = GradientPreset::Sunset.into_gradient();
let color = gradient.sample(0.5); // [r, g, b]
// Custom gradient
let custom = Gradient::new(vec![
GradientStop::new(0.0, [0, 0, 128]),
GradientStop::new(0.5, [0, 200, 200]),
GradientStop::new(1.0, [255, 255, 255]),
]);
// Generate a texture
let pixels: Vec<[u8; 3]> = GradientTexture::generate(
1920, 1080,
|x, y| (x as f32 + y as f32) / (1920.0 + 1080.0),
&custom,
);Full color space conversion and interpolation library — RGB, HSV, CIE Lab (D65), and Oklab.
use geodesic_wallpaper::colorspace::{
Rgb, Hsv, Lab, Oklab,
rgb_to_hsv, hsv_to_rgb,
rgb_to_lab, lab_to_rgb,
rgb_to_oklab, oklab_to_rgb,
ColorInterpolator,
};
let red = Rgb { r: 255, g: 0, b: 0 };
let blue = Rgb { r: 0, g: 0, b: 255 };
// Conversions
let hsv = rgb_to_hsv(red); // Hsv { h: 0.0, s: 1.0, v: 1.0 }
let back = hsv_to_rgb(hsv); // Rgb { r: 255, g: 0, b: 0 }
let lab = rgb_to_lab(red); // CIE Lab D65
let ok = rgb_to_oklab(red); // Oklab perceptual
// Color space interpolation
let mid_rgb = ColorInterpolator::lerp_rgb(red, blue, 0.5); // sRGB blend
let mid_hsv = ColorInterpolator::lerp_hsv(red, blue, 0.5); // hue-aware (shortest arc)
let mid_oklab = ColorInterpolator::lerp_oklab(red, blue, 0.5); // perceptually uniformCLI: --colorspace oklab — demonstrates interpolation on a red→blue gradient. Works with rgb, hsv, lab, oklab.
Supported conversions:
rgb_to_hsv/hsv_to_rgb— hue-aware shortest-arc interpolationrgb_to_lab/lab_to_rgb— via D65 XYZ white pointrgb_to_oklab/oklab_to_rgb— Björn Ottosson's matrix (perceptually uniform)ColorInterpolator::lerp_rgb,lerp_hsv,lerp_oklab
Export wallpaper images in PNG, PPM (P6 binary), 24-bit BMP, and SVG formats.
use geodesic_wallpaper::export::{ExportFormat, ImageExporter};
use std::path::Path;
// Generate some pixel data (row-major, RGB triplets)
let width = 800u32;
let height = 600u32;
let pixels: Vec<[u8; 3]> = (0..width * height)
.map(|i| [(i % 256) as u8, ((i / 256) % 256) as u8, 128])
.collect();
// Export as PPM
let stats = ImageExporter::export(
&pixels, width, height, ExportFormat::Ppm, Path::new("output.ppm")
).unwrap();
println!("wrote {} bytes in {}ms", stats.bytes_written, stats.elapsed_ms);
// Export as BMP (no external deps — manual BITMAPFILEHEADER + BITMAPINFOHEADER)
ImageExporter::export(&pixels, width, height, ExportFormat::Bmp, Path::new("output.bmp")).unwrap();
// Export as SVG (rect elements per tile cell)
ImageExporter::export(&pixels, width, height, ExportFormat::Svg, Path::new("output.svg")).unwrap();
// Export as PNG (stdlib-only encoder shared with animation.rs)
ImageExporter::export(&pixels, width, height, ExportFormat::Png, Path::new("output.png")).unwrap();CLI: --output-format png|ppm|bmp|svg — selects the export format for headless screenshots.
Format details:
Ppm:P6 {width} {height} 255\n{binary RGB data}— trivially simple, universally readableBmp: 24-bit BMP with BITMAPFILEHEADER + BITMAPINFOHEADER, BGR byte order, bottom-up rowsSvg:<rect>elements with fill colors; 8×8 tile size for large images to keep file size manageablePng: stdlib-only DEFLATE-stored encoder (no external PNG crate needed in export path)
ExportStats { bytes_written, format, width, height, elapsed_ms } is returned on success.
MIT — see LICENSE for details.