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
11pub 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("&"),
267 '<' => out.push_str("<"),
268 '>' => out.push_str(">"),
269 _ => out.push(c),
270 }
271 }
272 out.push_str("</text>");
273}