Skip to main content

demo/
demo.rs

1use std::env;
2use std::fs;
3use std::path::PathBuf;
4use std::process;
5
6use smooth_frame::SmoothRect;
7
8#[derive(Debug, Clone)]
9struct Config {
10    width: f64,
11    height: f64,
12    radius: Option<f64>,
13    smoothing: f64,
14    precision: usize,
15    border_width: f64,
16    svg: bool,
17    output: Option<PathBuf>,
18}
19
20impl Default for Config {
21    fn default() -> Self {
22        Self {
23            width: 1000.0,
24            height: 1000.0,
25            radius: Some(225.0),
26            smoothing: 0.6,
27            precision: 3,
28            border_width: 0.0,
29            svg: false,
30            output: None,
31        }
32    }
33}
34
35fn main() {
36    let config = match parse_args(env::args().skip(1)) {
37        Ok(Some(config)) => config,
38        Ok(None) => return,
39        Err(message) => {
40            eprintln!("参数错误:{message}");
41            eprintln!("运行 `cargo run --example demo -- --help` 查看用法。");
42            process::exit(2);
43        }
44    };
45
46    let drawable_width = drawable_dimension(config.width, config.border_width);
47    let drawable_height = drawable_dimension(config.height, config.border_width);
48    let radius = effective_radius(&config);
49    let path = SmoothRect::new(drawable_width, drawable_height)
50        .with_radius(radius)
51        .with_smoothing(config.smoothing)
52        .to_path();
53    let path_data = path.to_svg_path_with_precision(config.precision);
54
55    if let Some(output) = config.output.as_ref() {
56        let svg = render_svg(&config, &path_data);
57        if let Err(error) = fs::write(output, svg) {
58            eprintln!("写入 SVG 文件失败:{}:{error}", output.display());
59            process::exit(1);
60        }
61        println!("已生成 SVG 文件:{}", output.display());
62    } else if config.svg {
63        println!("{}", render_svg(&config, &path_data));
64    } else {
65        println!("SVG 宽度:{}", format_number(config.width));
66        println!("SVG 高度:{}", format_number(config.height));
67        println!("路径宽度:{}", format_number(drawable_width));
68        println!("路径高度:{}", format_number(drawable_height));
69        println!("半径:{}", format_number(radius));
70        println!("平滑:{}", format_number(config.smoothing));
71        println!("边框:{}", format_number(config.border_width));
72        println!("命令数:{}", path.commands().len());
73        println!("SVG path:{path_data}");
74    }
75}
76
77fn parse_args(args: impl Iterator<Item = String>) -> Result<Option<Config>, String> {
78    let mut config = Config::default();
79    let mut args = args.peekable();
80
81    while let Some(arg) = args.next() {
82        match arg.as_str() {
83            "-h" | "--help" => {
84                print_help();
85                return Ok(None);
86            }
87            "--svg" => config.svg = true,
88            "-o" | "--output" => {
89                config.output = Some(parse_path_arg("--output", args.next())?);
90            }
91            "--width" => config.width = parse_f64_arg("--width", args.next())?,
92            "--height" => config.height = parse_f64_arg("--height", args.next())?,
93            "--radius" => config.radius = Some(parse_f64_arg("--radius", args.next())?),
94            "--smoothing" => config.smoothing = parse_f64_arg("--smoothing", args.next())?,
95            "--precision" => config.precision = parse_usize_arg("--precision", args.next())?,
96            "--border" | "--border-width" => {
97                config.border_width = parse_f64_arg("--border-width", args.next())?;
98            }
99            _ if arg.starts_with("--width=") => {
100                config.width = parse_f64_value("--width", value_after_equals(&arg))?;
101            }
102            _ if arg.starts_with("--height=") => {
103                config.height = parse_f64_value("--height", value_after_equals(&arg))?;
104            }
105            _ if arg.starts_with("--radius=") => {
106                config.radius = Some(parse_f64_value("--radius", value_after_equals(&arg))?);
107            }
108            _ if arg.starts_with("--smoothing=") => {
109                config.smoothing = parse_f64_value("--smoothing", value_after_equals(&arg))?;
110            }
111            _ if arg.starts_with("--precision=") => {
112                config.precision = parse_usize_value("--precision", value_after_equals(&arg))?;
113            }
114            _ if arg.starts_with("--border=") => {
115                config.border_width = parse_f64_value("--border", value_after_equals(&arg))?;
116            }
117            _ if arg.starts_with("--border-width=") => {
118                config.border_width = parse_f64_value("--border-width", value_after_equals(&arg))?;
119            }
120            _ if arg.starts_with("--output=") => {
121                config.output = Some(PathBuf::from(value_after_equals(&arg)));
122            }
123            _ => return Err(format!("未知参数 `{arg}`")),
124        }
125    }
126
127    validate_config(&config)?;
128    Ok(Some(config))
129}
130
131fn validate_config(config: &Config) -> Result<(), String> {
132    if !config.width.is_finite() || config.width <= 0.0 {
133        return Err("--width 必须是大于 0 的有限数字".to_owned());
134    }
135    if !config.height.is_finite() || config.height <= 0.0 {
136        return Err("--height 必须是大于 0 的有限数字".to_owned());
137    }
138    if let Some(radius) = config.radius {
139        if !radius.is_finite() || radius < 0.0 {
140            return Err("--radius 必须是大于或等于 0 的有限数字".to_owned());
141        }
142    }
143    if !config.smoothing.is_finite() || !(0.0..=1.0).contains(&config.smoothing) {
144        return Err("--smoothing 必须在 0 到 1 之间".to_owned());
145    }
146    if !config.border_width.is_finite() || config.border_width < 0.0 {
147        return Err("--border-width 必须是大于或等于 0 的有限数字".to_owned());
148    }
149    if config.border_width >= config.width.min(config.height) {
150        return Err("--border-width 必须小于 SVG 宽度和高度".to_owned());
151    }
152    if config.precision > 12 {
153        return Err("--precision 不能大于 12".to_owned());
154    }
155    Ok(())
156}
157
158fn effective_radius(config: &Config) -> f64 {
159    config.radius.unwrap_or(250.0)
160}
161
162fn drawable_dimension(svg_dimension: f64, border_width: f64) -> f64 {
163    svg_dimension - border_width
164}
165
166fn parse_f64_arg(name: &str, value: Option<String>) -> Result<f64, String> {
167    parse_f64_value(
168        name,
169        value
170            .as_deref()
171            .ok_or_else(|| format!("缺少 {name} 的值"))?,
172    )
173}
174
175fn parse_usize_arg(name: &str, value: Option<String>) -> Result<usize, String> {
176    parse_usize_value(
177        name,
178        value
179            .as_deref()
180            .ok_or_else(|| format!("缺少 {name} 的值"))?,
181    )
182}
183
184fn parse_path_arg(name: &str, value: Option<String>) -> Result<PathBuf, String> {
185    value
186        .map(PathBuf::from)
187        .ok_or_else(|| format!("缺少 {name} 的值"))
188}
189
190fn parse_f64_value(name: &str, value: &str) -> Result<f64, String> {
191    value
192        .parse()
193        .map_err(|_| format!("{name} 的值 `{value}` 不是有效数字"))
194}
195
196fn parse_usize_value(name: &str, value: &str) -> Result<usize, String> {
197    value
198        .parse()
199        .map_err(|_| format!("{name} 的值 `{value}` 不是有效整数"))
200}
201
202fn value_after_equals(arg: &str) -> &str {
203    arg.split_once('=').map_or("", |(_, value)| value)
204}
205
206fn render_svg(config: &Config, path_data: &str) -> String {
207    let offset = config.border_width / 2.0;
208    let stroke_attrs = if config.border_width > 0.0 {
209        format!(
210            r##" stroke="#0F172A" stroke-opacity="0.12" stroke-width="{}""##,
211            format_number(config.border_width)
212        )
213    } else {
214        String::new()
215    };
216
217    format!(
218        r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {canvas_width} {canvas_height}" width="{canvas_width}" height="{canvas_height}">
219  <defs>
220    <linearGradient id="smooth-frame-fill" x1="0" y1="0" x2="1" y2="1">
221      <stop offset="0%" stop-color="#0EA5E9"/>
222      <stop offset="48%" stop-color="#2DD4BF"/>
223      <stop offset="100%" stop-color="#FDE68A"/>
224    </linearGradient>
225  </defs>
226  <path d="{path_data}" transform="translate({offset} {offset})" fill="url(#smooth-frame-fill)"{stroke_attrs}/>
227</svg>"##,
228        canvas_width = format_number(config.width),
229        canvas_height = format_number(config.height),
230        offset = format_number(offset),
231    )
232}
233
234fn format_number(value: f64) -> String {
235    let formatted = format!("{value:.3}");
236    formatted
237        .trim_end_matches('0')
238        .trim_end_matches('.')
239        .to_owned()
240}
241
242fn print_help() {
243    println!(
244        r#"demo
245
246生成 Sketch-like smooth corner 矩形的 SVG path 或完整 SVG。
247
248用法:
249  cargo run --example demo
250  cargo run --example demo -- --width 1000 --height 1000 --radius 250 --smoothing 0.6
251  cargo run --example demo -- --svg > smooth.svg
252  cargo run --example demo -- --output smooth.svg
253
254参数:
255  --width <数字>       矩形宽度,默认 1000
256  --height <数字>      矩形高度,默认 1000
257  --radius <数字>      核心圆半径,默认 250
258  --smoothing <数字>   平滑系数,范围 0..1,默认 0.6
259  --precision <整数>   SVG path 小数位数,默认 3
260  --border <数字>      SVG 边框宽度,默认 0
261  --svg               输出完整 SVG,而不是只输出 path
262  -o, --output <路径>  生成完整 SVG 文件
263  -h, --help          显示帮助
264"#
265    );
266}