use rand::{Rng, RngCore};
use crate::svg::theme::{rewrite_style, ThemeStyle};
pub struct WatercolorStyle;
impl ThemeStyle for WatercolorStyle {
fn stroke_random(&self, _original: &str, rng: &mut dyn RngCore) -> String {
apply_watercolor_stroke(rng)
}
fn stroke_static(&self, _original: &str) -> String {
"#FFB3BA".to_string()
}
fn fill_random(&self, original: &str, tag: &str, rng: &mut dyn RngCore) -> String {
apply_watercolor_fill(original, tag, rng)
}
fn fill_static(&self, original: &str, tag: &str) -> String {
if matches!(tag, "rect" | "circle" | "ellipse" | "polygon") {
"#FFB3BACC".to_string()
} else {
original.to_string()
}
}
fn style(&self, style: &str, tag: &str) -> String {
rewrite_style(style, tag, "#FFB3BA", "#FFB3BACC")
}
fn stroke_opacity(&self, rng: &mut dyn RngCore) -> Option<f64> {
Some(watercolor_random_opacity(rng))
}
fn filter_id(&self) -> &'static str {
"watercolor-bleed"
}
fn extra_defs(&self, seed: u64) -> Option<String> {
Some(watercolor_filter_defs(seed))
}
fn extra_replicas(&self, tag: &str) -> usize {
if matches!(
tag,
"path" | "text" | "rect" | "circle" | "ellipse" | "line" | "polyline"
) {
2
} else {
0
}
}
}
const WATERCOLOR_PALETTE: &[&str] = &[
"#FFB3BA", "#FFDFBA", "#FFFFBA", "#BAFFC9", "#BAE1FF", "#E0BBE4", "#FFC7F5", ];
pub fn apply_watercolor_stroke<R: Rng + ?Sized>(rng: &mut R) -> String {
let idx = (rng.gen::<usize>()) % WATERCOLOR_PALETTE.len();
WATERCOLOR_PALETTE[idx].to_string()
}
pub fn apply_watercolor_fill<R: Rng + ?Sized>(fill: &str, tag: &str, rng: &mut R) -> String {
if matches!(tag, "rect" | "circle" | "ellipse" | "polygon") {
let idx = (rng.gen::<usize>()) % WATERCOLOR_PALETTE.len();
let color = WATERCOLOR_PALETTE[idx];
format!("{}CC", color) } else {
fill.to_string()
}
}
pub fn watercolor_random_opacity<R: Rng + ?Sized>(rng: &mut R) -> f64 {
let base = 0.5;
let variance = rng.gen::<f64>() * 0.4;
(base + variance).min(1.0)
}
pub fn watercolor_filter_defs(_seed: u64) -> String {
r#"<filter id="watercolor-bleed" x="-25%" y="-25%" width="150%" height="150%"><feGaussianBlur stdDeviation="6.0" result="blurred"/><feColorMatrix in="blurred" type="saturate" values="0.9" result="saturated"/><feOffset in="saturated" dx="0.2" dy="0.2" result="offset"/><feComponentTransfer in="offset" result="faded"><feFuncA type="linear" slope="0.3"/></feComponentTransfer><feComposite in="faded" in2="SourceGraphic" operator="lighten"/></filter>"#
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::StdRng;
use rand::SeedableRng;
#[test]
fn test_watercolor_stroke_returns_pastel_color() {
let mut rng = StdRng::seed_from_u64(42);
let stroke = apply_watercolor_stroke(&mut rng);
assert!(stroke.starts_with('#'));
assert_eq!(stroke.len(), 7); }
#[test]
fn test_watercolor_fill_with_transparency_for_closed_shapes() {
let mut rng = StdRng::seed_from_u64(42);
let fill = apply_watercolor_fill("red", "rect", &mut rng);
assert!(fill.starts_with('#'));
assert_eq!(fill.len(), 9); }
#[test]
fn test_watercolor_fill_preserved_for_open_shapes() {
let mut rng = StdRng::seed_from_u64(42);
assert_eq!(apply_watercolor_fill("red", "line", &mut rng), "red");
assert_eq!(apply_watercolor_fill("blue", "path", &mut rng), "blue");
}
#[test]
fn test_watercolor_random_opacity_in_range() {
let mut rng = StdRng::seed_from_u64(42);
for _ in 0..10 {
let opacity = watercolor_random_opacity(&mut rng);
assert!(opacity >= 0.5);
assert!(opacity <= 1.0);
}
}
#[test]
fn test_watercolor_filter_defs_contains_required_elements() {
let defs = watercolor_filter_defs(42);
assert!(defs.contains("watercolor-bleed"));
assert!(defs.contains("feGaussianBlur"));
assert!(defs.contains("feColorMatrix"));
}
#[test]
fn test_watercolor_palette_not_empty() {
assert!(!WATERCOLOR_PALETTE.is_empty());
for color in WATERCOLOR_PALETTE {
assert!(color.starts_with('#'));
}
}
}