Tighten web surfaces and clean handoff

This commit is contained in:
jan
2026-04-20 21:12:52 +02:00
parent ed1e4b49ab
commit d2ca1a2aef
13 changed files with 751 additions and 390 deletions

View File

@@ -22,7 +22,7 @@ impl Default for TickSchedule {
logic_hz: 120,
frame_synthesis_hz: 60,
network_send_hz: 60,
preview_hz: 15,
preview_hz: 60,
}
}
}
@@ -54,12 +54,12 @@ impl RealtimeEngine {
mode: ValidationMode,
) -> ValidationReport {
let mut report = project.validate(mode);
if self.schedule.preview_hz >= self.schedule.frame_synthesis_hz {
if self.schedule.preview_hz > self.schedule.frame_synthesis_hz {
report.issues.push(ValidationIssue {
severity: ValidationSeverity::Warning,
code: "preview_rate_too_high",
path: "runtime.schedule.preview_hz".to_string(),
message: "preview rate should stay below frame synthesis rate".to_string(),
message: "preview rate should not exceed frame synthesis rate".to_string(),
});
}
report

View File

@@ -317,7 +317,7 @@ fn render_pattern_leds(
let speed = scene_scalar(scene, "speed", 0.45).max(0.05);
let tempo_multiplier = scene_scalar(scene, "tempo_multiplier", 1.0).clamp(0.25, 8.0);
let intensity = scene_scalar(scene, "intensity", 1.0).clamp(0.0, 1.0);
let fade = scene_scalar(scene, "fade", 0.35).clamp(0.0, 1.0);
let fade = scene_scalar(scene, "fade", 0.0).clamp(0.0, 1.0);
let pattern_brightness = scene_scalar(scene, "brightness", 1.0).clamp(0.0, 2.0);
let block_size = scene_scalar(scene, "block_size", 1.0).max(0.1);
let on_width = scene_scalar(scene, "on_width", 1.0).clamp(0.1, 2.0);
@@ -543,16 +543,31 @@ fn render_pattern_leds(
_ => vec![primary; led_count],
};
if fade > 0.0 && colors.len() > 2 {
if fade > 0.0 && colors.len() > 2 && smoothing_allowed_for_pattern(pattern_id) {
colors = smooth_led_sequence(colors, fade);
}
colors
.into_iter()
.map(|color| color.scale((intensity * pattern_brightness).clamp(0.0, 1.0)))
.map(|color| {
color
.scale(quantize_intensity(
(intensity * pattern_brightness).clamp(0.0, 1.0),
))
.quantize_10_percent()
})
.collect()
}
fn smoothing_allowed_for_pattern(pattern_id: &str) -> bool {
matches!(pattern_id, "breathing")
}
fn quantize_intensity(value: f32) -> f32 {
let clamped = value.clamp(0.0, 1.0);
(clamped * 10.0).round() / 10.0
}
fn canonical_pattern_id(pattern_id: &str) -> &str {
match pattern_id {
"solid_color" => "solid",
@@ -715,7 +730,7 @@ fn choose_pair(
) -> (RgbColor, RgbColor) {
let primary = scene_text(scene, "primary_color", "#4D7CFF");
let secondary = scene_text(scene, "secondary_color", "#0E1630");
let palette_name = scene_text(scene, "palette", "Laser Club");
let palette_name = scene_text(scene, "palette", "Deep Blue");
let seed_amount = amount + panel_row as f32 * 0.13 + panel_col as f32 * 0.07;
match color_mode {
"palette" => (
@@ -770,159 +785,129 @@ fn sample_palette_list(colors: &[RgbColor], amount: f32) -> RgbColor {
}
fn named_palette(name: &str) -> Option<&'static [RgbColor]> {
const LASER_CLUB: [RgbColor; 4] = [
RgbColor {
r: 0,
g: 240,
b: 255,
},
RgbColor {
r: 0,
g: 140,
b: 255,
},
RgbColor {
r: 106,
g: 0,
b: 255,
},
RgbColor { r: 6, g: 8, b: 20 },
];
const AFTERHOURS: [RgbColor; 4] = [
RgbColor {
r: 247,
g: 37,
b: 133,
},
RgbColor {
r: 181,
g: 23,
b: 158,
},
RgbColor {
r: 114,
g: 9,
b: 183,
},
RgbColor { r: 20, g: 3, b: 26 },
];
const VOLTAGE: [RgbColor; 4] = [
RgbColor {
r: 0,
g: 229,
b: 255,
},
RgbColor {
r: 0,
g: 179,
b: 255,
},
RgbColor {
r: 58,
g: 134,
b: 255,
},
RgbColor { r: 5, g: 10, b: 20 },
];
const MAGENTA_DRIVE: [RgbColor; 4] = [
RgbColor {
r: 255,
g: 0,
b: 110,
},
RgbColor {
r: 255,
g: 77,
b: 166,
},
RgbColor {
r: 122,
g: 0,
b: 255,
},
RgbColor { r: 18, g: 3, b: 24 },
];
const WAREHOUSE_HEAT: [RgbColor; 4] = [
RgbColor {
r: 255,
g: 90,
b: 31,
},
RgbColor {
r: 255,
g: 158,
b: 0,
},
RgbColor {
r: 255,
g: 208,
b: 0,
},
RgbColor { r: 20, g: 6, b: 0 },
];
const UV_RIOT: [RgbColor; 4] = [
RgbColor {
r: 122,
g: 0,
b: 255,
},
RgbColor {
r: 177,
g: 0,
b: 255,
},
RgbColor {
r: 255,
g: 0,
b: 168,
},
RgbColor { r: 16, g: 0, b: 20 },
];
const REDLINE: [RgbColor; 4] = [
RgbColor {
r: 255,
g: 45,
b: 85,
},
RgbColor {
r: 255,
g: 106,
b: 0,
},
RgbColor {
r: 255,
g: 176,
b: 0,
},
RgbColor { r: 22, g: 4, b: 6 },
];
const SODIUM_HAZE: [RgbColor; 4] = [
RgbColor {
r: 255,
g: 122,
b: 0,
},
RgbColor {
r: 255,
g: 176,
b: 0,
},
RgbColor {
r: 255,
g: 216,
b: 107,
},
RgbColor { r: 18, g: 7, b: 0 },
];
const CANDLE: [RgbColor; 1] = [RgbColor {
r: 255,
g: 147,
b: 41,
}];
const TUNGSTEN_40W: [RgbColor; 1] = [RgbColor {
r: 255,
g: 197,
b: 143,
}];
const TUNGSTEN_100W: [RgbColor; 1] = [RgbColor {
r: 255,
g: 214,
b: 170,
}];
const HALOGEN: [RgbColor; 1] = [RgbColor {
r: 255,
g: 241,
b: 224,
}];
const CARBON_ARC: [RgbColor; 1] = [RgbColor {
r: 255,
g: 250,
b: 244,
}];
const HIGH_NOON_SUN: [RgbColor; 1] = [RgbColor {
r: 255,
g: 255,
b: 251,
}];
const OVERCAST_SKY: [RgbColor; 1] = [RgbColor {
r: 201,
g: 226,
b: 255,
}];
const CLEAR_BLUE_SKY: [RgbColor; 1] = [RgbColor {
r: 64,
g: 156,
b: 255,
}];
const DEEP_BLUE: [RgbColor; 1] = [RgbColor {
r: 0,
g: 71,
b: 255,
}];
const ELECTRIC_CYAN: [RgbColor; 1] = [RgbColor {
r: 0,
g: 216,
b: 255,
}];
const EMERALD: [RgbColor; 1] = [RgbColor {
r: 0,
g: 200,
b: 83,
}];
const ACID_LIME: [RgbColor; 1] = [RgbColor {
r: 174,
g: 234,
b: 0,
}];
const AMBER: [RgbColor; 1] = [RgbColor {
r: 255,
g: 157,
b: 0,
}];
const SIGNAL_RED: [RgbColor; 1] = [RgbColor {
r: 255,
g: 45,
b: 45,
}];
const HOT_MAGENTA: [RgbColor; 1] = [RgbColor {
r: 255,
g: 0,
b: 168,
}];
const VIOLET: [RgbColor; 1] = [RgbColor {
r: 122,
g: 60,
b: 255,
}];
const WARM_WHITE: [RgbColor; 1] = [RgbColor {
r: 255,
g: 214,
b: 170,
}];
const NEUTRAL_WHITE: [RgbColor; 1] = [RgbColor {
r: 255,
g: 241,
b: 224,
}];
const COOL_WHITE: [RgbColor; 1] = [RgbColor {
r: 201,
g: 226,
b: 255,
}];
const BLACKLIGHT_VIOLET: [RgbColor; 1] = [RgbColor {
r: 167,
g: 0,
b: 255,
}];
match name {
"Magenta Drive" => Some(&MAGENTA_DRIVE),
"Warehouse Heat" => Some(&WAREHOUSE_HEAT),
"UV Riot" => Some(&UV_RIOT),
"Redline" => Some(&REDLINE),
"Sodium Haze" => Some(&SODIUM_HAZE),
"Afterhours" => Some(&AFTERHOURS),
"Voltage" => Some(&VOLTAGE),
"Laser Club" => Some(&LASER_CLUB),
"Candle" => Some(&CANDLE),
"Tungsten 40W" => Some(&TUNGSTEN_40W),
"Tungsten 100W" => Some(&TUNGSTEN_100W),
"Halogen" => Some(&HALOGEN),
"Carbon Arc" => Some(&CARBON_ARC),
"High Noon Sun" => Some(&HIGH_NOON_SUN),
"Overcast Sky" => Some(&OVERCAST_SKY),
"Clear Blue Sky" => Some(&CLEAR_BLUE_SKY),
"Deep Blue" => Some(&DEEP_BLUE),
"Electric Cyan" => Some(&ELECTRIC_CYAN),
"Emerald" => Some(&EMERALD),
"Acid Lime" => Some(&ACID_LIME),
"Amber" => Some(&AMBER),
"Signal Red" => Some(&SIGNAL_RED),
"Hot Magenta" => Some(&HOT_MAGENTA),
"Violet" => Some(&VIOLET),
"Warm White" => Some(&WARM_WHITE),
"Neutral White" => Some(&NEUTRAL_WHITE),
"Cool White" => Some(&COOL_WHITE),
"Blacklight Violet" => Some(&BLACKLIGHT_VIOLET),
_ => None,
}
}
@@ -1853,12 +1838,12 @@ fn common_motion_parameters(extra_keys: Vec<&str>) -> Vec<SceneParameterSpec> {
scalar_spec("speed", "Speed", 0.05, 8.0, 0.05, 0.45),
scalar_spec("intensity", "Intensity", 0.0, 1.0, 0.01, 1.0),
scalar_spec("brightness", "Brightness", 0.0, 2.0, 0.01, 1.0),
scalar_spec("fade", "Smoothing", 0.0, 1.0, 0.01, 0.35),
scalar_spec("fade", "Smoothing", 0.0, 1.0, 0.01, 0.0),
scalar_spec("tempo_multiplier", "Tempo Multiplier", 0.25, 8.0, 0.05, 1.0),
text_spec("color_mode", "Color Mode", "dual"),
text_spec("primary_color", "Primary Color", "#4D7CFF"),
text_spec("secondary_color", "Secondary Color", "#0E1630"),
text_spec("palette", "Palette", "Laser Club"),
text_spec("palette", "Palette", "Deep Blue"),
];
for key in extra_keys {
@@ -1960,6 +1945,14 @@ impl RgbColor {
}
}
fn quantize_10_percent(self) -> Self {
Self {
r: quantize_channel_10_percent(self.r),
g: quantize_channel_10_percent(self.g),
b: quantize_channel_10_percent(self.b),
}
}
fn complementary(self) -> Self {
Self {
r: 255u8.saturating_sub(self.r),
@@ -1998,6 +1991,12 @@ impl RgbColor {
}
}
fn quantize_channel_10_percent(value: u8) -> u8 {
(((value as f32 / 255.0) * 10.0).round() * 25.5)
.round()
.clamp(0.0, 255.0) as u8
}
#[cfg(test)]
mod tests {
use super::*;