Merge pull request #7662 from ruchirguitar/feat/style-neon-glow

Graphic Styles: built-in Neon & Glow graphic style library
This commit is contained in:
BFlat
2026-11-15 13:49:11 +07:00
committed by GitHub
6 changed files with 140 additions and 10 deletions
+71
View File
@@ -162,6 +162,7 @@ pub const STYLE_LIBRARIES: &[BuiltinStyleLibrary] = &[
BuiltinStyleLibrary { id: "blends-transparency", name: "Blends and Transparency", make: blends_transparency },
BuiltinStyleLibrary { id: "game-hud", name: "Game HUD", make: game_hud },
BuiltinStyleLibrary { id: "status-effects", name: "Status Effects", make: status_effects },
BuiltinStyleLibrary { id: "neon-glow", name: "Neon & Glow", make: neon_glow },
];
/// Built-in library `id`, made once.
@@ -619,6 +620,44 @@ fn status_effects() -> Vec<GraphicStyle> {
]
}
/// Neon & Glow: original multi-layer procedural styles with neon tubes, laser beams, and glowing halos.
fn neon_glow() -> Vec<GraphicStyle> {
vec![
style(
"Cyan Neon",
vec![S(StrokeLayer { opacity: 0.7, ..round("#00e5ff", 6.0) }), S(round("#ffffff", 1.5))],
vec![fx("stylize.dropShadow", json!({"mode": "screen", "opacity": 85.0, "x": 0.0, "y": 0.0, "blur": 8.0, "color": "#00e5ff"}))],
),
GraphicStyle {
blend: BlendMode::Screen,
..style(
"Magenta Laser",
vec![S(StrokeLayer { blend: BlendMode::Screen, ..round("#ff007f", 5.0) }), S(round("#ffffff", 1.5))],
vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 90.0, "blur": 8.0, "color": "#ff007f"}))],
)
},
style(
"Amber Tube",
vec![S(StrokeLayer { opacity: 0.8, ..round("#ff8c00", 6.0) }), S(round("#ffea00", 1.5))],
vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 85.0, "blur": 8.0, "color": "#ff8c00"}))],
),
style(
"Electric Lime",
vec![S(StrokeLayer { opacity: 0.75, ..round("#39ff14", 6.0) }), S(round("#ffffff", 1.5))],
vec![fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 90.0, "blur": 8.0, "color": "#39ff14"}))],
),
style(
"Pulse Violet",
vec![S(StrokeLayer { opacity: 0.75, ..round("#7b2cbf", 5.0) }), S(round("#e0aaff", 1.5))],
vec![
fx("stylize.innerGlow", json!({"mode": "screen", "opacity": 75.0, "blur": 6.0, "color": "#c77dff", "source": "edge"})),
fx("stylize.outerGlow", json!({"mode": "screen", "opacity": 85.0, "blur": 8.0, "color": "#7b2cbf"})),
],
),
style("Cobalt Phosphor", vec![S(round("#0047ab", 6.0)), S(round("#ffffff", 1.5))], vec![fx("blur.gaussian", json!({"radius": 2.0}))]),
]
}
#[cfg(test)]
mod tests {
use super::*;
@@ -750,4 +789,36 @@ mod tests {
let Paint::Gradient(gp) = g.appearance.fill_paint() else { panic!("a gradient") };
assert!(gp.swatch.is_none() && gp.gradient.stops.iter().all(|s| s.swatch.is_none() && s.tint == 1.0));
}
#[test]
fn neon_and_glow_styles_are_defined_with_multi_strokes_and_effects() {
let lib = builtin_style_library("neon-glow").expect("neon-glow library exists");
assert_eq!(lib.name, "Neon & Glow");
assert_eq!(lib.len(), 6);
let names: Vec<&str> = lib.styles.iter().map(|g| g.name.as_str()).collect();
assert_eq!(names, ["Cyan Neon", "Magenta Laser", "Amber Tube", "Electric Lime", "Pulse Violet", "Cobalt Phosphor"]);
for g in &lib.styles {
let strokes = g.appearance.items.iter().filter(|i| matches!(i, AppearanceItem::Stroke(_))).count();
assert!(strokes >= 2, "{} has {} strokes, expected >= 2", g.name, strokes);
assert!(!g.appearance.effects.is_empty(), "{} has effects", g.name);
}
let cyan = lib.style("Cyan Neon").unwrap();
assert_eq!(cyan.appearance.effects[0].id, "stylize.dropShadow");
let magenta = lib.style("Magenta Laser").unwrap();
assert_eq!(magenta.blend, BlendMode::Screen);
let pulse = lib.style("Pulse Violet").unwrap();
let pulse_fx: Vec<&str> = pulse.appearance.effects.iter().map(|e| e.id.as_str()).collect();
assert_eq!(pulse_fx, ["stylize.innerGlow", "stylize.outerGlow"]);
let cobalt = lib.style("Cobalt Phosphor").unwrap();
assert_eq!(cobalt.appearance.effects[0].id, "blur.gaussian");
}
#[test]
fn neon_glow_round_trips_through_vcstyles() {
let lib = builtin_style_library("neon-glow").unwrap();
let text = write(&lib);
assert!(sniff(&text));
let read_back = read(&text, "Fallback").unwrap();
assert_eq!(*lib, read_back);
}
}
+54
View File
@@ -45,6 +45,7 @@ fn builtin_libraries_are_listed_with_original_styles_using_catalogued_effects()
assert!(libs.iter().any(|l| l["id"] == "game-hud" && l["name"] == "Game HUD"), "{libs:?}");
assert!(libs.iter().any(|l| l["id"] == "status-effects" && l["name"] == "Status Effects"), "{libs:?}");
assert!(libs.iter().any(|l| l["id"] == "textures" && l["name"] == "Textures" && l["count"] == 8), "{libs:?}");
assert!(libs.iter().any(|l| l["id"] == "neon-glow" && l["name"] == "Neon & Glow" && l["count"] == 6), "{libs:?}");
for l in libs {
let id = l["id"].as_str().unwrap();
let g = run(&mut s, "graphicStyle.library", json!({"library": id}));
@@ -69,6 +70,9 @@ fn builtin_libraries_are_listed_with_original_styles_using_catalogued_effects()
let texture_names: Vec<&str> = textures["styles"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect();
let expected = ["Fine Grain", "Canvas Weave", "Burlap Sack", "Cracked Glaze", "Mosaic Floor", "Quilted Squares", "Leaded Glass", "Damp Paper"];
assert_eq!(texture_names, expected);
let neon = run(&mut s, "graphicStyle.library", json!({"library": "neon-glow"}));
let neon_names: Vec<&str> = neon["styles"].as_array().unwrap().iter().map(|x| x["name"].as_str().unwrap()).collect();
assert_eq!(neon_names, ["Cyan Neon", "Magenta Laser", "Amber Tube", "Electric Lime", "Pulse Violet", "Cobalt Phosphor"]);
let id = rect(&mut s, 10.0, 20.0, 40.0, 30.0);
let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "game-hud", "name": "Mana Glow", "apply": true, "ids": [id.0]}));
assert_eq!((names(&r["added"]), r["applied"].as_str()), (vec!["Mana Glow"], Some("Mana Glow")));
@@ -419,3 +423,53 @@ fn every_builtin_style_paints_visibly_on_a_rectangle() {
}
}
}
#[test]
fn neon_and_glow_library_adds_and_applies_multi_stroke_styles() {
let mut s = session();
let list = run(&mut s, "graphicStyle.libraries", json!({}));
let libs = list["libraries"].as_array().unwrap();
let at = libs.iter().position(|l| l["id"] == "neon-glow").expect("neon-glow listed");
assert_eq!(libs[at]["name"], "Neon & Glow");
assert_eq!(libs[at]["count"], 6);
let (_, lib) = cmd::stylelib::library(&s, "neon-glow").unwrap();
assert_eq!(lib.len(), 6);
let want = [
("Cyan Neon", 2, "stylize.dropShadow"),
("Magenta Laser", 2, "stylize.outerGlow"),
("Amber Tube", 2, "stylize.outerGlow"),
("Electric Lime", 2, "stylize.outerGlow"),
("Pulse Violet", 2, "stylize.innerGlow"),
("Cobalt Phosphor", 2, "blur.gaussian"),
];
for (i, (name, stroke_count, first_effect)) in want.iter().enumerate() {
let id = rect(&mut s, 10.0 + 30.0 * i as f64, 15.0, 25.0, 25.0);
let before_undo = undo_len(&s);
let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "neon-glow", "name": name, "apply": true, "ids": [id.0]}));
assert_eq!((names(&r["added"]), r["applied"].as_str()), (vec![*name], Some(*name)));
assert_eq!(undo_len(&s), before_undo + 1, "{name}");
let node = doc(&s).node(id).unwrap();
let strokes = node.appearance.items.iter().filter(|it| matches!(it, vectorcraft_doc::AppearanceItem::Stroke(_))).count();
assert_eq!(strokes, *stroke_count, "{name}");
assert_eq!(node.appearance.effects[0].id, *first_effect, "{name}");
}
// Adding existing style dedupes and creates no new undo step.
let before = (undo_len(&s), doc(&s).graphic_styles.len());
let r = run(&mut s, "graphicStyle.addFromLibrary", json!({"library": "neon-glow", "name": "Cyan Neon"}));
assert_eq!((names(&r["added"]), names(&r["existing"])), (vec![], vec!["Cyan Neon"]));
assert_eq!((undo_len(&s), doc(&s).graphic_styles.len()), before);
// Round-trip through serialize / open
let b64 = run(&mut s, "document.serialize", json!({}))["dataBase64"].as_str().unwrap().to_string();
run(&mut s, "document.open", json!({"name": "neon.vectorcraft", "dataBase64": b64}));
for (name, _, _) in want {
let d = doc(&s);
let st = d.graphic_style(name).unwrap();
assert!(st.same_look(lib.style(name).unwrap()), "{name}");
}
}