1use std::{fmt::Display, ops::Deref};
4
5use num_traits::Zero as _;
6use palette::rgb::Rgba;
7use svg::{
8 node::{
9 element::{Group, Rectangle, Style, Text as TextElement},
10 Text as TextNode,
11 },
12 Document,
13};
14use thiserror::Error;
15
16use crate::puzzle::{
17 color_scheme::{Black, ColorScheme},
18 size::Size,
19 sliding_puzzle::SlidingPuzzle,
20};
21
22#[cfg(feature = "serde")]
23use serde::{Deserialize, Serialize};
24
25#[derive(Clone, Debug, Error, PartialEq, Eq)]
27#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
28pub enum RendererError {
29 #[error("IncompatibleLabel: puzzle size ({0}) can not be used with the given label")]
31 IncompatibleLabel(Size),
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
36pub enum Font<'a> {
37 Family(&'a str),
39 Url {
41 path: &'a str,
43 format: &'a str,
45 },
46 Base64 {
48 data: &'a str,
50 format: &'a str,
52 },
53}
54
55#[derive(Clone, Debug, PartialEq)]
57pub struct Borders<S: ColorScheme> {
58 scheme: S,
59 thickness: f32,
60}
61
62impl Borders<Black> {
63 #[must_use]
65 pub fn new() -> Self {
66 Self::with_scheme(Black)
67 }
68}
69
70impl Default for Borders<Black> {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl<S: ColorScheme> Borders<S> {
77 #[must_use]
79 pub fn with_scheme(scheme: S) -> Self {
80 Self {
81 scheme,
82 thickness: 1.0,
83 }
84 }
85
86 #[must_use]
92 pub fn scheme(mut self, scheme: S) -> Self {
93 self.scheme = scheme;
94 self
95 }
96
97 #[must_use]
99 pub fn thickness(mut self, thickness: f32) -> Self {
100 self.thickness = thickness;
101 self
102 }
103}
104
105#[derive(Clone, Debug, PartialEq)]
107pub struct Text<'a, S: ColorScheme> {
108 scheme: S,
109 font: Font<'a>,
110 font_size: f32,
111 position: (f32, f32),
112}
113
114impl Text<'_, Black> {
115 #[must_use]
117 pub fn new() -> Self {
118 Self::with_scheme(Black)
119 }
120}
121
122impl Default for Text<'_, Black> {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128impl<'a, S: ColorScheme> Text<'a, S> {
129 #[must_use]
131 pub fn with_scheme(scheme: S) -> Self {
132 Self {
133 scheme,
134 font: Font::Family("sans-serif"),
135 font_size: 30.0,
136 position: (0.5, 0.5),
137 }
138 }
139
140 #[must_use]
146 pub fn scheme(mut self, scheme: S) -> Self {
147 self.scheme = scheme;
148 self
149 }
150
151 #[must_use]
153 pub fn font(mut self, font: Font<'a>) -> Self {
154 self.font = font;
155 self
156 }
157
158 #[must_use]
160 pub fn font_size(mut self, size: f32) -> Self {
161 self.font_size = size.max(0.0);
162 self
163 }
164
165 #[must_use]
169 pub fn position(mut self, pos: (f32, f32)) -> Self {
170 self.position = pos;
171 self
172 }
173
174 #[must_use]
176 pub fn style_string(&self) -> String {
177 if let Font::Family(f) = self.font {
178 format!(
179 "text {{ font-family: {f}; font-size: {fs}px; }}",
180 fs = self.font_size
181 )
182 } else {
183 let src = match self.font {
184 Font::Family(_) => unreachable!(),
185 Font::Url { path, format } => {
186 format!(r#"url({path}) format("{format}")"#)
187 }
188 Font::Base64 { data, format } => {
189 format!(r#"url(data:font/ttf;base64,{data}) format("{format}")"#)
190 }
191 };
192
193 format!(
194 "@font-face {{ \
195 font-family: f; \
196 src: {src}; \
197 }} \
198 text {{ \
199 font-family: f; \
200 font-size: {fs}px; \
201 }}",
202 fs = self.font_size
203 )
204 }
205 }
206}
207
208#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
212#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
213pub enum SubschemeStyle {
214 #[default]
216 Rectangle,
217 TextColor,
219 BorderColor,
221}
222
223#[derive(Clone, Debug, PartialEq)]
225pub struct RendererBuilder<
226 'a,
227 S: ColorScheme = Box<dyn ColorScheme + 'a>,
228 U: ColorScheme = Box<dyn ColorScheme + 'a>,
229 T: ColorScheme = Box<dyn ColorScheme + 'a>,
230 B: ColorScheme = Box<dyn ColorScheme + 'a>,
231> {
232 scheme: S,
233 subscheme: Option<U>,
234 borders: Option<Borders<B>>,
235 text: Option<Text<'a, T>>,
236 tile_size: f32,
237 tile_rounding: f32,
238 tile_gap: f32,
239 padding: f32,
240 subscheme_style: Option<SubschemeStyle>,
241 background_color: Rgba,
242}
243
244#[derive(Clone, Debug, PartialEq)]
246pub struct Renderer<
247 'a,
248 S: ColorScheme = Box<dyn ColorScheme + 'a>,
249 U: ColorScheme = Box<dyn ColorScheme + 'a>,
250 T: ColorScheme = Box<dyn ColorScheme + 'a>,
251 B: ColorScheme = Box<dyn ColorScheme + 'a>,
252>(RendererBuilder<'a, S, U, T, B>);
253
254impl<'a, S: ColorScheme, U: ColorScheme, T: ColorScheme, B: ColorScheme> Deref
255 for Renderer<'a, S, U, T, B>
256{
257 type Target = RendererBuilder<'a, S, U, T, B>;
258
259 fn deref(&self) -> &Self::Target {
260 &self.0
261 }
262}
263
264impl<'a> RendererBuilder<'a> {
265 #[must_use]
267 pub fn with_dyn_scheme(scheme: Box<dyn ColorScheme + 'a>) -> Self {
268 Self::with_scheme(scheme)
269 }
270}
271
272impl<'a, S: ColorScheme, U: ColorScheme, T: ColorScheme, B: ColorScheme>
273 RendererBuilder<'a, S, U, T, B>
274{
275 #[must_use]
277 pub fn with_scheme(scheme: S) -> Self {
278 Self {
279 scheme,
280 subscheme: None,
281 borders: None,
282 text: None,
283 tile_size: 75.0,
284 tile_rounding: 0.0,
285 tile_gap: 0.0,
286 padding: 0.0,
287 subscheme_style: Some(SubschemeStyle::Rectangle),
288 background_color: Rgba::new(1.0, 1.0, 1.0, 0.0),
289 }
290 }
291
292 #[must_use]
294 pub fn scheme(mut self, scheme: S) -> Self {
295 self.scheme = scheme;
296 self
297 }
298
299 #[must_use]
301 pub fn subscheme(mut self, subscheme: U) -> Self {
302 self.subscheme = Some(subscheme);
303 self
304 }
305
306 #[must_use]
308 pub fn borders(mut self, borders: Borders<B>) -> Self {
309 self.borders = Some(borders);
310 self
311 }
312
313 #[must_use]
315 pub fn text(mut self, text: Text<'a, T>) -> Self {
316 self.text = Some(text);
317 self
318 }
319
320 #[must_use]
322 pub fn tile_size(mut self, size: f32) -> Self {
323 self.tile_size = size.max(0.0);
324 self
325 }
326
327 #[must_use]
329 pub fn tile_rounding(mut self, rounding: f32) -> Self {
330 self.tile_rounding = rounding.max(0.0);
331 self
332 }
333
334 #[must_use]
336 pub fn tile_gap(mut self, gap: f32) -> Self {
337 self.tile_gap = gap;
338 self
339 }
340
341 #[must_use]
343 pub fn padding(mut self, padding: f32) -> Self {
344 self.padding = padding;
345 self
346 }
347
348 #[must_use]
350 pub fn subscheme_style(mut self, style: SubschemeStyle) -> Self {
351 self.subscheme_style = Some(style);
352 self
353 }
354
355 #[must_use]
357 pub fn background_color(mut self, color: Rgba) -> Self {
358 self.background_color = color;
359 self
360 }
361
362 #[must_use]
364 pub fn build(self) -> Renderer<'a, S, U, T, B> {
365 Renderer(self)
366 }
367}
368
369impl<S: ColorScheme, U: ColorScheme, T: ColorScheme, B: ColorScheme> Renderer<'_, S, U, T, B> {
370 pub fn style_string(&self) -> String {
372 let font = self
373 .text
374 .as_ref()
375 .map(|a| a.style_string())
376 .unwrap_or_default();
377
378 let bg = {
379 let color: Rgba<_, u8> = self.background_color.into_format();
380 format!("#{color:x}")
381 };
382
383 let border_thickness = self
384 .borders
385 .as_ref()
386 .map(|a| a.thickness)
387 .unwrap_or_default();
388
389 format!(
390 "svg {{ background-color: {bg}; }} \
391 text {{ \
392 text-anchor: middle; \
393 dominant-baseline: central; \
394 }} \
395 rect.piece {{ \
396 width: {ts}px; \
397 height: {ts}px; \
398 rx: {tr}px; \
399 ry: {tr}px; \
400 stroke-width: {sw}px; \
401 }} \
402 rect.sub {{ \
403 width: {srw}px; \
404 height: {srh}px; \
405 }} \
406 {font}",
407 ts = self.tile_size,
408 tr = self.tile_rounding,
409 sw = border_thickness,
410 srw = self.tile_size * 0.7,
411 srh = self.tile_size * 0.1,
412 )
413 }
414
415 pub fn group<Puzzle>(&self, puzzle: &Puzzle) -> Result<Group, RendererError>
417 where
418 Puzzle: SlidingPuzzle,
419 Puzzle::Piece: Display,
420 {
421 let size = puzzle.size();
422 let (width, height) = size.into();
423
424 let mut group = Group::new();
425
426 for y in 0..height {
427 for x in 0..width {
428 let piece = puzzle.piece_at_xy((x, y));
429
430 if piece != Puzzle::Piece::zero() {
431 group = group.add(self.render_piece(puzzle, (x, y)));
432 }
433 }
434 }
435
436 Ok(group)
437 }
438
439 pub fn render_piece<Puzzle>(&self, puzzle: &Puzzle, (x, y): (u64, u64)) -> Group
442 where
443 Puzzle: SlidingPuzzle,
444 Puzzle::Piece: Display,
445 {
446 let size = puzzle.size();
447
448 let border_thickness = self
449 .borders
450 .as_ref()
451 .map(|a| a.thickness)
452 .unwrap_or_default();
453
454 let piece = puzzle.piece_at_xy((x, y));
455 let solved_pos = puzzle.solved_pos_xy(piece);
456
457 let (x, y) = (x as f32, y as f32);
458
459 let rect_pos = (
460 self.padding
461 + border_thickness / 2.0
462 + (self.tile_size + self.tile_gap + border_thickness) * x,
463 self.padding
464 + border_thickness / 2.0
465 + (self.tile_size + self.tile_gap + border_thickness) * y,
466 );
467
468 let subscheme_color = self
469 .subscheme
470 .as_ref()
471 .map(|subscheme| subscheme.color(size, solved_pos));
472
473 macro_rules! color {
477 ($scheme:expr, $subscheme:expr) => {{
478 let color = subscheme_color
482 .filter(|_| self.subscheme_style == Some($subscheme))
483 .unwrap_or_else(|| $scheme.color(size, solved_pos));
484
485 let color: Rgba<_, u8> = color.into_format();
487 format!("#{color:x}")
488 }};
489 }
490
491 let rect = {
492 let fill = {
493 let color: Rgba<_, u8> = self.scheme.color(size, solved_pos).into_format();
494 format!("#{color:x}")
495 };
496
497 let mut r = Rectangle::new()
498 .set("x", rect_pos.0)
499 .set("y", rect_pos.1)
500 .set("class", "piece")
501 .set("fill", fill);
502
503 if let Some(s) = &self.borders {
504 let stroke = color!(s.scheme, SubschemeStyle::BorderColor);
505 r = r.set("stroke", stroke);
506 }
507
508 r
509 };
510
511 let text = self.text.as_ref().map(|text| {
512 let fill = color!(text.scheme, SubschemeStyle::TextColor);
513 let (tx, ty) = text.position;
514
515 TextElement::new("")
516 .set("x", rect_pos.0 + self.tile_size * tx)
517 .set("y", rect_pos.1 + self.tile_size * ty)
518 .set("fill", fill)
519 .add(TextNode::new(piece.to_string()))
520 });
521
522 let subscheme_render = subscheme_color
523 .filter(|_| self.subscheme_style == Some(SubschemeStyle::Rectangle))
524 .map(|subcolor| {
525 let fill = {
526 let color: Rgba<_, u8> = subcolor.into_format();
527 format!("#{color:x}")
528 };
529
530 let subrect_pos = (0.15, 0.8);
531
532 Rectangle::new()
533 .set("x", rect_pos.0 + self.tile_size * subrect_pos.0)
534 .set("y", rect_pos.1 + self.tile_size * subrect_pos.1)
535 .set("class", "sub")
536 .set("fill", fill)
537 });
538
539 let mut group = Group::new().add(rect);
540
541 if let Some(text) = text {
542 group = group.add(text);
543 }
544
545 if let Some(s) = subscheme_render {
546 group = group.add(s);
547 }
548
549 group
550 }
551
552 pub fn render<Puzzle>(&self, puzzle: &Puzzle) -> Result<Document, RendererError>
554 where
555 Puzzle: SlidingPuzzle,
556 Puzzle::Piece: Display,
557 {
558 let size = puzzle.size();
559 let (width, height) = size.into();
560
561 let border_thickness = self
562 .borders
563 .as_ref()
564 .map(|a| a.thickness)
565 .unwrap_or_default();
566
567 let (w, h) = (width as f32, height as f32);
568 let (image_w, image_h) = (
569 w * self.tile_size
570 + (w - 1.0) * self.tile_gap
571 + w * border_thickness
572 + 2.0 * self.padding,
573 h * self.tile_size
574 + (h - 1.0) * self.tile_gap
575 + h * border_thickness
576 + 2.0 * self.padding,
577 );
578
579 let style_str = self.style_string();
580
581 let doc = Document::new()
582 .add(Style::new(style_str))
583 .add(self.group(puzzle)?)
584 .set("width", image_w)
585 .set("height", image_h);
586
587 Ok(doc)
588 }
589}