Skip to main content

document_graphics/chess/
render.rs

1use super::*;
2use crate::{Raster, RasterOptions};
3use std::fmt::Write;
4#[derive(Debug, Clone, Copy, Default)]
5pub struct RenderOptions {
6    pub dark: bool,
7    pub flipped: bool,
8}
9/// Render an 8×8 board with original vector piece silhouettes. No system fonts,
10/// external images or user-supplied SVG are used for pieces.
11pub fn to_svg(
12    position: &Position,
13    options: RenderOptions,
14    cancel: &AtomicBool,
15) -> Result<String, Error> {
16    check(cancel)?;
17    let (light, dark, frame, ink) = if options.dark {
18        ("#bfc9b4", "#607b62", "#292e2a", "#d9dfd5")
19    } else {
20        ("#eee8d5", "#829777", "#e3dfd2", "#39463b")
21    };
22    let mut svg = format!(
23        r#"<svg xmlns="http://www.w3.org/2000/svg" width="440" height="440" viewBox="0 0 440 440"><rect width="440" height="440" rx="7" fill="{frame}"/>"#
24    );
25    for row in 0..8 {
26        check(cancel)?;
27        for col in 0..8 {
28            let (file, rank) = if options.flipped {
29                (7 - col, row)
30            } else {
31                (col, 7 - row)
32            };
33            let square = Square::new(cozy_chess::File::index(file), cozy_chess::Rank::index(rank));
34            let x = 28 + col * 48;
35            let y = 28 + row * 48;
36            let fill = if (file + rank) % 2 == 0 { dark } else { light };
37            let _ = write!(
38                svg,
39                r#"<rect x="{x}" y="{y}" width="48" height="48" fill="{fill}"/>"#
40            );
41            if position
42                .last_move
43                .is_some_and(|(from, to)| from == square || to == square)
44            {
45                let _ = write!(
46                    svg,
47                    r##"<rect x="{x}" y="{y}" width="48" height="48" fill="#e1c754" fill-opacity=".55"/>"##
48                );
49            }
50            if let Some((color, piece)) = position.piece(square) {
51                let (body, edge, detail) = if color == Color::White {
52                    ("#faf8ee", "#30382f", "#596451")
53                } else {
54                    ("#252e28", "#e1e5d9", "#b8c4ad")
55                };
56                let _ = write!(
57                    svg,
58                    r#"<g transform="translate({x} {y})" stroke-linejoin="round" stroke-linecap="round" stroke-width="1.5" fill="{body}" stroke="{edge}">"#
59                );
60                // Compact, original silhouettes shared by both appearances and orientations.
61                match piece {
62                    Piece::Pawn => svg.push_str(r#"<path d="M14 38h20l-2-5c-5-2-6-7-4-12a7 7 0 1 0-8 0c2 5 1 10-4 12z"/>"#),
63                    Piece::Rook => svg.push_str(r#"<path d="M12 8h6v5h4V8h4v5h4V8h6v12l-5 4v9l4 5H13l4-5v-9l-5-4z"/>"#),
64                    Piece::Knight => {
65                        svg.push_str(r#"<path d="M13 38h23l-2-9c0-10-3-17-11-21l-5-3 1 7-9 11 4 5 9-5c0 6-6 7-7 10z"/>"#);
66                        let _ = write!(svg, r#"<circle cx="22" cy="16" r="1.5" stroke="none" fill="{detail}"/><path d="M13 24l4 1m11-10c4 9 2 14-2 18" fill="none" stroke="{detail}"/>"#);
67                    }
68                    Piece::Bishop => {
69                        svg.push_str(r#"<path d="M13 38h22l-4-6-3-7c8-8 4-13-4-19-8 6-12 11-4 19l-3 7z"/>"#);
70                        let _ = write!(svg, r#"<path d="M26 11l-6 8" fill="none" stroke="{detail}"/>"#);
71                    }
72                    Piece::Queen => svg.push_str(r#"<path d="M13 38h22l-3-7 5-16-9 7-4-12-4 12-9-7 5 16z"/><circle cx="10" cy="12" r="3"/><circle cx="24" cy="7" r="3"/><circle cx="38" cy="12" r="3"/>"#),
73                    Piece::King => svg.push_str(r#"<path d="M13 38h22l-3-9c9-11 2-17-8-9-10-8-17-2-8 9z"/><path d="M24 6v12m-5-7h10" fill="none" stroke-width="3"/>"#),
74                }
75                let _ = write!(
76                    svg,
77                    r#"<path d="M16 33h16" stroke="{detail}" fill="none"/></g>"#
78                );
79            }
80        }
81        let rank = if options.flipped { row + 1 } else { 8 - row };
82        let file = char::from(b'a' + if options.flipped { 7 - row } else { row } as u8);
83        let at = 52 + row * 48;
84        for (x, y, label) in [
85            (14, at + 4, rank.to_string()),
86            (426, at + 4, rank.to_string()),
87            (at, 19, file.to_string()),
88            (at, 430, file.to_string()),
89        ] {
90            let _ = write!(
91                svg,
92                r#"<text x="{x}" y="{y}" font-family="KaTeX_Main" font-size="13" text-anchor="middle" fill="{ink}">{label}</text>"#
93            );
94        }
95    }
96    svg.push_str("</svg>");
97    Ok(svg)
98}
99pub fn rasterize(
100    position: &Position,
101    options: RenderOptions,
102    fitting: RasterOptions,
103    cancel: &AtomicBool,
104) -> Result<Raster, Error> {
105    let svg = to_svg(position, options, cancel)?;
106    let raster = crate::svg::rasterize(&svg, fitting).map_err(|e| Error::Render(e.to_string()))?;
107    check(cancel)?;
108    Ok(raster)
109}