Skip to content

Commit d89bba7

Browse files
committed
feat: add sky backdrops with cloud decks, storms, and lightning
Adds a fourth backdrop family alongside the gradients, deep-space scenes, and geometric patterns. A sky palette carries the colours and a SkyKind carries the structure, so --palette and --sky compose the same way --palette and --scene already do. Ten conditions: blue-hour, golden-hour, overcast, cirrus, mackerel, mammatus, storm, bolt, aurora, crepuscular. Seven palettes sampled from photographs of the named conditions, the same way the space palettes are sampled from astrophotography. Everything is drawn from the style seed at render time on image + rand, with no new dependencies and nothing shipped as a bitmap, so a sky resolves at any card size and --style-seed reproduces one exactly. Sky palettes stay out of the default random rotation, which filters to space palettes, so existing seeds render exactly as before. They are reachable by explicit --palette or by naming them in a pool, matching how the gradient and pattern families already behave. ConfigCommand::Set is boxed because the args struct now carries one field per styling axis and tripped clippy::large_enum_variant.
1 parent 769c11b commit d89bba7

8 files changed

Lines changed: 1135 additions & 19 deletions

File tree

src/cli.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ pub struct PolishArgs {
7878
/// omitted. Only applies to pattern palettes.
7979
#[arg(long, value_parser = motif_name_parser())]
8080
pub motif: Option<String>,
81+
/// Pin the sky condition (e.g. `bolt`, `mammatus`, `aurora`); random when
82+
/// omitted. Only applies to sky palettes.
83+
#[arg(long, value_parser = sky_name_parser())]
84+
pub sky: Option<String>,
8185
/// Seed for deterministic styling.
8286
#[arg(long)]
8387
pub style_seed: Option<u64>,
@@ -178,6 +182,10 @@ fn motif_name_parser() -> clap::builder::PossibleValuesParser {
178182
clap::builder::PossibleValuesParser::new(polish::motif_names())
179183
}
180184

185+
fn sky_name_parser() -> clap::builder::PossibleValuesParser {
186+
clap::builder::PossibleValuesParser::new(polish::sky_names())
187+
}
188+
181189
#[derive(Debug, Args)]
182190
pub struct CaptureArgs {
183191
#[arg(long, value_enum, default_value = "active")]
@@ -209,6 +217,10 @@ pub struct CaptureArgs {
209217
/// to pattern palettes.
210218
#[arg(long, value_parser = motif_name_parser())]
211219
pub motif: Option<String>,
220+
/// Pin the sky condition (e.g. `bolt`, `mammatus`, `aurora`). Only applies
221+
/// to sky palettes.
222+
#[arg(long, value_parser = sky_name_parser())]
223+
pub sky: Option<String>,
212224
#[arg(long)]
213225
pub style_seed: Option<u64>,
214226
/// Copy the card (or the raw shot with --presentation raw) to the
@@ -305,7 +317,10 @@ pub enum ConfigCommand {
305317
/// Print every palette and scene the pickers accept.
306318
Options,
307319
/// Update the persisted preferences.
308-
Set(ConfigSetArgs),
320+
///
321+
/// Boxed because the args struct carries one field per styling axis and
322+
/// dwarfs the unit variants, which trips `clippy::large_enum_variant`.
323+
Set(Box<ConfigSetArgs>),
309324
}
310325

311326
#[derive(Debug, Args)]
@@ -323,6 +338,9 @@ pub struct ConfigSetArgs {
323338
/// Motif to pin. Pattern palettes only.
324339
#[arg(long, value_parser = motif_name_parser())]
325340
pub motif: Option<String>,
341+
/// Sky to pin. Sky palettes only.
342+
#[arg(long, value_parser = sky_name_parser())]
343+
pub sky: Option<String>,
326344
/// Comma-separated palettes the random picker may choose from. Replaces the
327345
/// stored pool.
328346
#[arg(long, value_delimiter = ',', value_parser = palette_name_parser())]
@@ -335,6 +353,10 @@ pub struct ConfigSetArgs {
335353
/// stored pool.
336354
#[arg(long, value_delimiter = ',', value_parser = motif_name_parser())]
337355
pub motifs: Option<Vec<String>>,
356+
/// Comma-separated skies the random picker may choose from. Replaces the
357+
/// stored pool.
358+
#[arg(long, value_delimiter = ',', value_parser = sky_name_parser())]
359+
pub skies: Option<Vec<String>>,
338360
/// Reset a preference to its default. Repeatable.
339361
#[arg(long = "clear", value_enum)]
340362
pub clear: Vec<ClearTarget>,
@@ -357,12 +379,16 @@ pub enum ClearTarget {
357379
Scene,
358380
/// Drop the pinned motif.
359381
Motif,
382+
/// Drop the pinned sky.
383+
Sky,
360384
/// Empty the random palette pool, restoring every space palette.
361385
Palettes,
362386
/// Empty the random scene pool.
363387
Scenes,
364388
/// Empty the random motif pool.
365389
Motifs,
390+
/// Empty the random sky pool.
391+
Skies,
366392
/// Reset every polish preference.
367393
All,
368394
}
@@ -467,6 +493,7 @@ pub fn capture(args: CaptureArgs) -> Result<ExitCode, Box<dyn std::error::Error>
467493
args.palette.as_deref(),
468494
args.scene.as_deref(),
469495
args.motif.as_deref(),
496+
args.sky.as_deref(),
470497
&mut warnings,
471498
);
472499
match polish::render_codex_card(
@@ -966,6 +993,7 @@ fn run_polish(args: PolishArgs, config_path: &Path) -> crate::contract::PolishRe
966993
args.palette.as_deref(),
967994
args.scene.as_deref(),
968995
args.motif.as_deref(),
996+
args.sky.as_deref(),
969997
&mut warnings,
970998
);
971999
let parent_ready = card_path
@@ -1220,9 +1248,11 @@ fn apply_config_set(prefs: &mut config::PolishPrefs, args: &ConfigSetArgs) {
12201248
ClearTarget::Palette => prefs.palette = None,
12211249
ClearTarget::Scene => prefs.scene = None,
12221250
ClearTarget::Motif => prefs.motif = None,
1251+
ClearTarget::Sky => prefs.sky = None,
12231252
ClearTarget::Palettes => prefs.palettes.clear(),
12241253
ClearTarget::Scenes => prefs.scenes.clear(),
12251254
ClearTarget::Motifs => prefs.motifs.clear(),
1255+
ClearTarget::Skies => prefs.skies.clear(),
12261256
ClearTarget::All => *prefs = config::PolishPrefs::default(),
12271257
}
12281258
}
@@ -1241,6 +1271,9 @@ fn apply_config_set(prefs: &mut config::PolishPrefs, args: &ConfigSetArgs) {
12411271
if let Some(motif) = args.motif.clone() {
12421272
prefs.motif = Some(motif);
12431273
}
1274+
if let Some(sky) = args.sky.clone() {
1275+
prefs.sky = Some(sky);
1276+
}
12441277
if let Some(palettes) = args.palettes.clone() {
12451278
prefs.palettes = palettes;
12461279
}
@@ -1250,6 +1283,9 @@ fn apply_config_set(prefs: &mut config::PolishPrefs, args: &ConfigSetArgs) {
12501283
if let Some(motifs) = args.motifs.clone() {
12511284
prefs.motifs = motifs;
12521285
}
1286+
if let Some(skies) = args.skies.clone() {
1287+
prefs.skies = skies;
1288+
}
12531289
}
12541290

12551291
/// A pinned scene only renders behind a space palette; pairing it with a
@@ -1285,6 +1321,10 @@ fn style_options() -> crate::contract::StyleOptions {
12851321
.into_iter()
12861322
.map(str::to_string)
12871323
.collect(),
1324+
skies: polish::sky_names()
1325+
.into_iter()
1326+
.map(str::to_string)
1327+
.collect(),
12881328
}
12891329
}
12901330

@@ -1366,7 +1406,7 @@ mod tests {
13661406
}
13671407

13681408
fn config_set(args: ConfigSetArgs, path: &Path) -> crate::contract::ConfigResult {
1369-
run_config(ConfigCommand::Set(args), path)
1409+
run_config(ConfigCommand::Set(Box::new(args)), path)
13701410
}
13711411

13721412
fn empty_set_args() -> ConfigSetArgs {
@@ -1378,6 +1418,8 @@ mod tests {
13781418
scenes: None,
13791419
motif: None,
13801420
motifs: None,
1421+
sky: None,
1422+
skies: None,
13811423
clear: Vec::new(),
13821424
format: OutputFormat::Json,
13831425
}
@@ -1455,6 +1497,7 @@ mod tests {
14551497
palette: None,
14561498
scene: None,
14571499
motif: None,
1500+
sky: None,
14581501
style_seed: Some(7),
14591502
format: OutputFormat::Json,
14601503
});
@@ -1485,6 +1528,7 @@ mod tests {
14851528
palette: Some("aurora-teal".to_string()),
14861529
scene: None,
14871530
motif: None,
1531+
sky: None,
14881532
style_seed: Some(11),
14891533
format: OutputFormat::Json,
14901534
});
@@ -1518,6 +1562,7 @@ mod tests {
15181562
palette: None,
15191563
scene: None,
15201564
motif: None,
1565+
sky: None,
15211566
style_seed: Some(4),
15221567
format: OutputFormat::Json,
15231568
},
@@ -1552,6 +1597,7 @@ mod tests {
15521597
palette: Some("ember-glow".to_string()),
15531598
scene: None,
15541599
motif: None,
1600+
sky: None,
15551601
style_seed: Some(4),
15561602
format: OutputFormat::Json,
15571603
},
@@ -1684,6 +1730,7 @@ mod tests {
16841730
palette: None,
16851731
scene: None,
16861732
motif: None,
1733+
sky: None,
16871734
style_seed: Some(3),
16881735
format: OutputFormat::Json,
16891736
});
@@ -1755,6 +1802,7 @@ mod tests {
17551802
palette: None,
17561803
scene: None,
17571804
motif: None,
1805+
sky: None,
17581806
style_seed: Some(13),
17591807
format: OutputFormat::Json,
17601808
});
@@ -1772,6 +1820,7 @@ mod tests {
17721820
palette: None,
17731821
scene: None,
17741822
motif: None,
1823+
sky: None,
17751824
style_seed: Some(5),
17761825
format: OutputFormat::Json,
17771826
});

src/config.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ pub struct PolishPrefs {
5858
pub scene: Option<String>,
5959
/// Motif applied when `mode` is `pinned`. Pattern palettes only.
6060
pub motif: Option<String>,
61+
/// Sky applied when `mode` is `pinned`. Sky palettes only.
62+
pub sky: Option<String>,
6163
/// Palettes the random picker may choose from. Empty means the built-in
6264
/// pool (every space palette).
6365
pub palettes: Vec<String>,
@@ -67,6 +69,9 @@ pub struct PolishPrefs {
6769
/// Motifs the random picker may choose from. Empty leaves the motif to the
6870
/// seed's own pick inside the pattern renderer.
6971
pub motifs: Vec<String>,
72+
/// Skies the random picker may choose from. Empty leaves the condition to
73+
/// the seed's own pick inside the sky renderer.
74+
pub skies: Vec<String>,
7075
}
7176

7277
/// Where the preferences file lives on this machine.
@@ -158,13 +163,15 @@ pub fn resolve_style(
158163
palette: Option<&str>,
159164
scene: Option<&str>,
160165
motif: Option<&str>,
166+
sky: Option<&str>,
161167
warnings: &mut Vec<String>,
162168
) -> polish::PresentationStyle {
163169
let seed = seed.unwrap_or_else(polish::random_seed);
164170
let pinned = prefs.mode == PolishMode::Pinned;
165171
let palette = palette.or_else(|| pinned.then_some(prefs.palette.as_deref()).flatten());
166172
let scene = scene.or_else(|| pinned.then_some(prefs.scene.as_deref()).flatten());
167173
let motif = motif.or_else(|| pinned.then_some(prefs.motif.as_deref()).flatten());
174+
let sky = sky.or_else(|| pinned.then_some(prefs.sky.as_deref()).flatten());
168175

169176
let palettes = keep_known(
170177
&prefs.palettes,
@@ -174,13 +181,14 @@ pub fn resolve_style(
174181
);
175182
let scenes = keep_known(&prefs.scenes, &polish::scene_names(), "scene", warnings);
176183
let motifs = keep_known(&prefs.motifs, &polish::motif_names(), "motif", warnings);
184+
let skies = keep_known(&prefs.skies, &polish::sky_names(), "sky", warnings);
177185

178186
let mut style = match palette {
179187
Some(name) => polish::style_with_palette(seed, name).unwrap_or_else(|| {
180188
warnings.push(format!("unknown palette: {name}"));
181-
polish::style_from_seed_in_pool(seed, &palettes, &scenes, &motifs)
189+
polish::style_from_seed_in_pool(seed, &palettes, &scenes, &motifs, &skies)
182190
}),
183-
None => polish::style_from_seed_in_pool(seed, &palettes, &scenes, &motifs),
191+
None => polish::style_from_seed_in_pool(seed, &palettes, &scenes, &motifs, &skies),
184192
};
185193

186194
if let Some(name) = scene {
@@ -204,6 +212,17 @@ pub fn resolve_style(
204212
None => warnings.push(format!("unknown motif: {name}")),
205213
}
206214
}
215+
216+
if let Some(name) = sky {
217+
match polish::sky_from_name(name) {
218+
Some(kind) if style.is_sky() => style.sky = Some(kind),
219+
Some(_) => warnings.push(format!(
220+
"sky {name} ignored: palette {} is not a sky",
221+
style.palette_name
222+
)),
223+
None => warnings.push(format!("unknown sky: {name}")),
224+
}
225+
}
207226
style
208227
}
209228

@@ -284,9 +303,11 @@ mod tests {
284303
palette: Some("violet-haze".to_string()),
285304
scene: Some("jwst".to_string()),
286305
motif: Some("plaid".to_string()),
306+
sky: None,
287307
palettes: vec!["orion-emission".to_string()],
288308
scenes: vec!["alma".to_string()],
289309
motifs: vec!["grid".to_string()],
310+
skies: Vec::new(),
290311
},
291312
};
292313
save_to(&path, &config).expect("save");
@@ -336,6 +357,7 @@ mod tests {
336357
Some("aurora-teal"),
337358
None,
338359
None,
360+
None,
339361
&mut warnings,
340362
);
341363
assert_eq!(style.palette_name, "aurora-teal");
@@ -351,6 +373,7 @@ mod tests {
351373
None,
352374
None,
353375
None,
376+
None,
354377
&mut warnings,
355378
);
356379
assert_eq!(style.palette_name, "violet-haze");
@@ -364,7 +387,7 @@ mod tests {
364387
..PolishPrefs::default()
365388
};
366389
let mut warnings = Vec::new();
367-
let style = resolve_style(&prefs, Some(7), None, None, None, &mut warnings);
390+
let style = resolve_style(&prefs, Some(7), None, None, None, None, &mut warnings);
368391
assert_eq!(style.palette_name, polish::style_from_seed(7).palette_name);
369392
assert!(warnings.is_empty(), "warnings: {warnings:?}");
370393
}
@@ -377,7 +400,7 @@ mod tests {
377400
};
378401
for seed in 0..40u64 {
379402
let mut warnings = Vec::new();
380-
let style = resolve_style(&prefs, Some(seed), None, None, None, &mut warnings);
403+
let style = resolve_style(&prefs, Some(seed), None, None, None, None, &mut warnings);
381404
assert!(
382405
prefs.palettes.contains(&style.palette_name),
383406
"seed {seed} escaped the pool with {}",
@@ -395,6 +418,7 @@ mod tests {
395418
None,
396419
None,
397420
None,
421+
None,
398422
&mut warnings,
399423
);
400424
assert_eq!(space.scene, polish::scene_from_name("jwst"));
@@ -407,6 +431,7 @@ mod tests {
407431
None,
408432
None,
409433
None,
434+
None,
410435
&mut warnings,
411436
);
412437
assert_eq!(gradient.scene, None);
@@ -422,7 +447,7 @@ mod tests {
422447
..PolishPrefs::default()
423448
};
424449
let mut warnings = Vec::new();
425-
let style = resolve_style(&prefs, Some(7), None, None, None, &mut warnings);
450+
let style = resolve_style(&prefs, Some(7), None, None, None, None, &mut warnings);
426451
assert_eq!(style.palette_name, "orion-emission");
427452
assert_eq!(warnings.len(), 2, "warnings: {warnings:?}");
428453
assert!(warnings.iter().any(|w| w.contains("not-a-palette")));

src/contract.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,15 @@ pub struct StyleOptions {
9898
pub palettes: Vec<PaletteOption>,
9999
pub scenes: Vec<String>,
100100
pub motifs: Vec<String>,
101+
pub skies: Vec<String>,
101102
}
102103

103104
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
104105
#[serde(rename_all = "camelCase")]
105106
pub struct PaletteOption {
106107
pub name: String,
107-
/// `gradient`, `space`, or `pattern`. Scenes apply only to space palettes,
108-
/// motifs only to pattern palettes.
108+
/// `gradient`, `space`, `pattern`, or `sky`. Scenes apply only to space
109+
/// palettes, motifs only to pattern palettes, skies only to sky palettes.
109110
pub kind: String,
110111
}
111112

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod pattern;
1313
mod polish;
1414
mod reel_hyperframes;
1515
mod setup;
16+
mod sky;
1617
mod space;
1718
mod studio;
1819
mod text;

0 commit comments

Comments
 (0)