Skip to main content

document_graphics/sgf/
render.rs

1use super::*;
2use crate::{Raster, RasterOptions};
3use std::fmt::Write;
4
5#[derive(Debug, Clone, Copy, Default)]
6pub struct RenderOptions {
7    pub dark: bool,
8    pub move_numbers: bool,
9}
10
11/// Produce a self-contained board diagram. User text is escaped as XML text.
12pub fn to_svg(
13    position: &Position,
14    options: RenderOptions,
15    cancel: &AtomicBool,
16) -> Result<String, Error> {
17    check(cancel)?;
18    position.validate()?;
19    let w = (position.width as usize - 1) * 24 + 72;
20    let h = (position.height as usize - 1) * 24 + 72;
21    let wood = if options.dark { "#bd9864" } else { "#e7c58a" };
22    let mut s = format!(
23        r##"<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}"><rect width="{w}" height="{h}" rx="6" fill="{wood}"/><defs><radialGradient id="b" cx=".3" cy=".25" r=".8"><stop stop-color="#494949"/><stop offset="1" stop-color="#111111"/></radialGradient><radialGradient id="w" cx=".3" cy=".25" r=".8"><stop stop-color="#ffffff"/><stop offset="1" stop-color="#dedbd5"/></radialGradient></defs><g stroke="#624c2e" stroke-width=".7">"##
24    );
25    for x in 0..position.width {
26        let x = xy(x);
27        let _ = write!(s, r#"<path d="M{x} 36V{}"/>"#, h - 36);
28    }
29    for y in 0..position.height {
30        let y = xy(y);
31        let _ = write!(s, r#"<path d="M36 {y}H{}"/>"#, w - 36);
32    }
33    s.push_str("</g>");
34    if position.width == position.height && position.width >= 9 && position.width % 2 == 1 {
35        let n = position.width;
36        let edge = if n >= 13 { 3 } else { 2 };
37        for x in [edge, n / 2, n - edge - 1] {
38            for y in [edge, n / 2, n - edge - 1] {
39                if n < 19 && (x == n / 2) != (y == n / 2) {
40                    continue;
41                }
42                let _ = write!(
43                    s,
44                    r##"<circle cx="{}" cy="{}" r="2" fill="#624c2e"/>"##,
45                    xy(x),
46                    xy(y)
47                );
48            }
49        }
50    }
51    for x in 0..position.width {
52        let label: String = Point { x, y: 0 }
53            .coordinate(position.height)
54            .chars()
55            .take_while(char::is_ascii_alphabetic)
56            .collect();
57        text(&mut s, xy(x) as f32, 17., &label, "#624c2e", 10.);
58        text(
59            &mut s,
60            xy(x) as f32,
61            (h - 12) as f32,
62            &label,
63            "#624c2e",
64            10.,
65        );
66    }
67    for y in 0..position.height {
68        let label = (position.height - y).to_string();
69        text(&mut s, 16., xy(y) as f32 + 3., &label, "#624c2e", 10.);
70        text(
71            &mut s,
72            (w - 16) as f32,
73            xy(y) as f32 + 3.,
74            &label,
75            "#624c2e",
76            10.,
77        );
78        check(cancel)?;
79        for x in 0..position.width {
80            let at = Point { x, y };
81            if let Some(color) = position.stone(at) {
82                let (fill, stroke) = if color == Color::Black {
83                    ("url(#b)", "#171717")
84                } else {
85                    ("url(#w)", "#aaa69e")
86                };
87                let _ = write!(
88                    s,
89                    r#"<circle cx="{}" cy="{}" r="10.7" fill="{fill}" stroke="{stroke}" stroke-width=".7"/>"#,
90                    xy(x),
91                    xy(y)
92                );
93                if options.move_numbers && position.number(at) != 0 {
94                    let label = position.number(at).to_string();
95                    text(
96                        &mut s,
97                        xy(x) as f32,
98                        xy(y) as f32 + 3.5,
99                        &label,
100                        ink(position, at),
101                        (17. / label.len() as f32).min(11.),
102                    );
103                }
104            }
105        }
106    }
107    for mark in &position.marks {
108        check(cancel)?;
109        let x = xy(mark.at.x) as f32;
110        let y = xy(mark.at.y) as f32;
111        let ink = ink(position, mark.at);
112        match &mark.kind {
113            MarkKind::Label(label) => {
114                if position.stone(mark.at).is_none() {
115                    let _ = write!(
116                        s,
117                        r#"<rect x="{}" y="{}" width="22" height="20" fill="{wood}"/>"#,
118                        x - 11.,
119                        y - 10.
120                    );
121                }
122                text(
123                    &mut s,
124                    x,
125                    y + 4.,
126                    label,
127                    ink,
128                    (22. / label.chars().count().max(1) as f32).min(12.),
129                );
130            }
131            MarkKind::Circle => {
132                let _ = write!(
133                    s,
134                    r#"<circle cx="{x}" cy="{y}" r="5.8" fill="none" stroke="{ink}" stroke-width="1.5"/>"#
135                );
136            }
137            MarkKind::Square => {
138                let _ = write!(
139                    s,
140                    r#"<rect x="{}" y="{}" width="10" height="10" fill="none" stroke="{ink}" stroke-width="1.5"/>"#,
141                    x - 5.,
142                    y - 5.
143                );
144            }
145            MarkKind::Triangle => {
146                let _ = write!(
147                    s,
148                    r#"<path d="M{x} {}L{} {}H{}Z" fill="none" stroke="{ink}" stroke-width="1.5"/>"#,
149                    y - 6.,
150                    x + 6.,
151                    y + 5.,
152                    x - 6.
153                );
154            }
155            MarkKind::Cross => {
156                let _ = write!(
157                    s,
158                    r#"<path d="M{} {}l10 10m0 -10l-10 10" fill="none" stroke="{ink}" stroke-width="1.5"/>"#,
159                    x - 5.,
160                    y - 5.
161                );
162            }
163            MarkKind::Selected => {
164                let _ = write!(
165                    s,
166                    r##"<rect x="{}" y="{}" width="21" height="21" fill="#b32e30" fill-opacity=".4"/>"##,
167                    x - 10.5,
168                    y - 10.5
169                );
170            }
171            MarkKind::Territory(color) => {
172                let color = if *color == Color::Black {
173                    "#111"
174                } else {
175                    "#fff"
176                };
177                let _ = write!(
178                    s,
179                    r#"<rect x="{}" y="{}" width="6" height="6" fill="{color}" stroke="{ink}" stroke-width=".6"/>"#,
180                    x - 3.,
181                    y - 3.
182                );
183            }
184            MarkKind::Dim => {
185                let _ = write!(
186                    s,
187                    r#"<rect x="{}" y="{}" width="24" height="24" fill="{wood}" fill-opacity=".6"/>"#,
188                    x - 12.,
189                    y - 12.
190                );
191            }
192        }
193    }
194    for line in &position.lines {
195        let (x, y, tx, ty) = (
196            xy(line.from.x) as f32,
197            xy(line.from.y) as f32,
198            xy(line.to.x) as f32,
199            xy(line.to.y) as f32,
200        );
201        let _ = write!(
202            s,
203            r##"<path d="M{x} {y}L{tx} {ty}" stroke="#ba3232" stroke-width="2"/>"##
204        );
205        let distance = ((tx - x).powi(2) + (ty - y).powi(2)).sqrt();
206        if line.arrow && distance > 0. {
207            let (dx, dy) = ((tx - x) / distance, (ty - y) / distance);
208            let _ = write!(
209                s,
210                r##"<path d="M{tx} {ty}L{:.2} {:.2}L{:.2} {:.2}Z" fill="#ba3232"/>"##,
211                tx - dx * 9. + dy * 4.,
212                ty - dy * 9. - dx * 4.,
213                tx - dx * 9. - dy * 4.,
214                ty - dy * 9. + dx * 4.
215            );
216        }
217    }
218    if !options.move_numbers
219        && let Some(Played {
220            point: Some(at), ..
221        }) = position.last_move
222        && position.stone(at).is_some()
223        && !position.marks.iter().any(|m| m.at == at)
224    {
225        let _ = write!(
226            s,
227            r##"<circle cx="{}" cy="{}" r="3.5" fill="none" stroke="#de4c43" stroke-width="2"/>"##,
228            xy(at.x),
229            xy(at.y)
230        );
231    }
232    s.push_str("</svg>");
233    if s.len() > crate::svg::MAX_SOURCE {
234        return Err(Error::Limit("SGF diagram exceeds its SVG size limit"));
235    }
236    Ok(s)
237}
238pub fn rasterize(
239    position: &Position,
240    options: RenderOptions,
241    fitting: RasterOptions,
242    cancel: &AtomicBool,
243) -> Result<Raster, Error> {
244    let svg = to_svg(position, options, cancel)?;
245    let raster = crate::svg::rasterize(&svg, fitting).map_err(|e| Error::Render(e.to_string()))?;
246    check(cancel)?;
247    Ok(raster)
248}
249fn xy(n: u8) -> usize {
250    36 + n as usize * 24
251}
252fn ink(position: &Position, at: Point) -> &'static str {
253    if position.stone(at) == Some(Color::Black) {
254        "#fff"
255    } else {
256        "#1b1b1b"
257    }
258}
259fn text(out: &mut String, x: f32, y: f32, value: &str, color: &str, size: f32) {
260    let _ = write!(
261        out,
262        r#"<text x="{x}" y="{y}" text-anchor="middle" font-family="KaTeX_Main" font-size="{size:.2}" fill="{color}">"#
263    );
264    for c in value.chars() {
265        match c {
266            '&' => out.push_str("&amp;"),
267            '<' => out.push_str("&lt;"),
268            '>' => out.push_str("&gt;"),
269            _ => out.push(c),
270        }
271    }
272    out.push_str("</text>");
273}