1#![allow(clippy::cast_lossless)]
17
18use cairo::{Context, Rectangle};
19use crate::card::{Card, Color, Shading, Shape};
20use crate::geometry::RectangleExt;
21use rand::{Rng, thread_rng};
22use std::f64::consts::{PI, FRAC_PI_2};
23use std::f64;
24
25const CORNER_RADIUS_PERCENTAGE: f64 = 0.08;
26const BADGE_BACKGROUND_GRAY: f64 = 0.68;
27const CARD_LABEL_GRAY: f64 = 0.75;
28const PLACEHOLDER_GRAY: f64 = 0.75;
29const TABLEAU_BACKGROUND_GRAY: f64 = 0.8;
30const MOCK_STRIPE_TRANSLUCENCY: f64 = 0.4;
31
32#[inline]
33pub fn card_corner_radius(Rectangle { height, .. }: Rectangle) -> f64 {
34 CORNER_RADIUS_PERCENTAGE * height
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
42pub enum ColorScheme {
43 CMYK,
44 Classic,
45}
46
47impl ColorScheme {
48 pub fn card_color(self, card: Card) -> (f64, f64, f64) {
49 let (r,g,b) = match self {
50 ColorScheme::CMYK => match card.color() {
53 Color::A => (0, 200, 220), Color::B => (192, 0, 192), Color::C => (220, 200, 0), },
57
58 ColorScheme::Classic => match card.color() {
59 Color::A => (0, 151, 0), Color::B => (130, 0, 140), Color::C => (240, 0, 0), },
63 };
64
65 (r as f64 / 255., g as f64 / 255., b as f64 / 255.)
66 }
67}
68
69pub trait ContextExt {
74 fn with_pivot<F>(&self, pivot: (f64, f64), f: F) where F: Fn() -> ();
76
77 fn set_source_gray(&self, g: f64);
78 fn set_source_random_rgb(&self);
79
80 fn rounded_rect(&self, rect: Rectangle, radius: f64);
81 fn diamond_in_rect(&self, rect: Rectangle);
82 fn squiggle_in_rect(&self, rect: Rectangle);
83
84 fn draw_badge(&self, rect: Rectangle, count: usize, label: &str);
85 fn draw_card_background(&self, rect: Rectangle, label: Option<&str>, gray: f64);
86 fn draw_card_placeholder(&self, rect: Rectangle);
87 fn draw_card_selection(&self, rect: Rectangle);
88 fn draw_card(&self, card: Card, rect: Rectangle, label: Option<&str>, scheme: ColorScheme);
89}
90
91impl ContextExt for Context {
92 fn with_pivot<F>(&self, (px, py): (f64, f64), f: F) where F: Fn() -> () {
93 self.translate(px, py);
94 f();
95 self.translate(-px, -py);
96 }
97
98 fn set_source_gray(&self, g: f64) {
99 self.set_source_rgb(g, g, g);
100 }
101
102 fn set_source_random_rgb(&self) {
103 let mut rng = thread_rng();
104 let r = rng.gen_range(0.0, 1.0);
105 let g = rng.gen_range(0.0, 1.0);
106 let b = rng.gen_range(0.0, 1.0);
107
108 self.set_source_rgb(r, g, b);
109 }
110
111 fn rounded_rect(&self, rect: Rectangle, radius: f64) {
112 let Rectangle {x, y, width, height} = rect;
113 let r = f64::min(radius, f64::min(width / 2., height / 2.));
114
115 self.new_sub_path();
116 self.arc(x + width - r, y + r, r, -FRAC_PI_2, 0.);
117 self.arc(x + width - r, y + height - r, r, 0., FRAC_PI_2);
118 self.arc(x + r, y + height - r, r, FRAC_PI_2, PI);
119 self.arc(x + r, y + r, r, PI, FRAC_PI_2 * 3.);
120 self.close_path();
121 }
122
123 fn diamond_in_rect(&self, rect: Rectangle) {
124 let Rectangle {x, y, width, height} = rect;
125 let half_width = width / 2.;
126 let half_height = height / 2.;
127
128 self.new_sub_path();
129 self.move_to(x + half_width, y);
130 self.line_to(x + width, y + half_height);
131 self.line_to(x + half_width, y + height);
132 self.line_to(x, y + half_height);
133 self.close_path();
134 }
135
136 fn squiggle_in_rect(&self, rect: Rectangle) {
137 let Rectangle {x, y, width, height} = rect;
138
139 self.new_sub_path();
140 self.move_to(x + width / 3., y);
141
142 self.curve_to(x + width * 4. / 5., y,
143 x + width, y + height / 6.,
144 x + width, y + height / 3.);
145
146 self.curve_to(x + width, y + height / 2.,
147 x + width * 5. / 6., y + height / 2.,
148 x + width * 5. / 6., y + height * 2. / 3.);
149
150 self.curve_to(x + width * 5. / 6., y + height * 5. / 6.,
151 x + width, y + height * 5. / 6.,
152 x + width, y + height * 11. / 12.);
153
154 self.curve_to(x + width, y + height * 23. / 24.,
155 x + width * 5. / 6., y + height,
156 x + width * 2. / 3., y + height);
157
158 self.curve_to(x + width / 5., y + height,
159 x, y + height * 5. / 6.,
160 x, y + height * 2. / 3.);
161
162 self.curve_to(x, y + height / 2.,
163 x + width / 6., y + height / 2.,
164 x + width / 6., y + height / 3.);
165
166 self.curve_to(x + width / 6., y + height / 6.,
167 x, y + height / 6.,
168 x, y + height / 12.);
169
170 self.curve_to(x, y + height / 24.,
171 x + width / 6., y,
172 x + width / 3., y);
173
174 self.close_path();
175 }
176
177 fn draw_badge(&self, rect: Rectangle, count: usize, label: &str) {
178 let badge_height = rect.height * (2. / 3.);
179 let label_height = rect.height - badge_height;
180 let count_string = count.to_string();
181
182 let padding = rect.width * 0.2;
183 let badge_rect = Rectangle {
184 x: rect.x,
185 y: rect.y,
186 width: rect.width,
187 height: badge_height
188 }.inset(padding, padding / 8.);
189
190 self.set_source_gray(BADGE_BACKGROUND_GRAY);
192 self.rounded_rect(badge_rect.round(), f64::INFINITY);
193 self.fill();
194
195 self.set_font_size(label_height * 0.9);
197 let extents = self.text_extents(label);
198 let x = rect.x + (rect.width - extents.width) / 2.;
199 let y = rect.max_y() - (label_height - extents.height) / 3.;
200
201 self.move_to(x, y);
202 self.show_text(label);
203
204 self.set_font_size(badge_height * 0.75);
206 let extents = self.text_extents(&count_string);
207 let x = rect.x + (rect.width - extents.width) / 2. - extents.x_bearing;
208 let y = badge_rect.max_y() - (badge_rect.height - extents.height) / 2.;
209
210 self.move_to(x, y);
211 self.set_source_gray(TABLEAU_BACKGROUND_GRAY);
212 self.show_text(&count_string);
213 }
214
215 fn draw_card_background(&self, rect: Rectangle, label: Option<&str>, gray: f64) {
216 let corner_radius = card_corner_radius(rect);
217 self.rounded_rect(rect, corner_radius);
218 self.set_source_gray(gray);
219 self.fill();
220
221 if let Some(text) = label {
222 let font_size = f64::min(rect.height * 0.15, 24.);
223 self.set_font_size(font_size);
224 self.move_to(rect.x + corner_radius, rect.max_y() - corner_radius);
225 self.set_source_gray(CARD_LABEL_GRAY);
226 self.show_text(text);
227 }
228 }
229
230 fn draw_card_placeholder(&self, rect: Rectangle) {
231 self.draw_card_background(rect, None, PLACEHOLDER_GRAY);
232 }
233
234 fn draw_card_selection(&self, rect: Rectangle) {
235 let Rectangle { height, .. } = rect;
236 let corner_radius = card_corner_radius(rect);
237 let selection_width = (height * 0.035).round() * 2.;
238
239 self.rounded_rect(rect, corner_radius);
240 self.set_source_gray(0.);
241 self.set_line_width(selection_width);
242 self.stroke();
243 }
244
245 fn draw_card(&self, card: Card, rect: Rectangle, label: Option<&str>, scheme: ColorScheme) {
246 let Rectangle { x, y, width, height } = rect;
247 self.draw_card_background(rect, label, 1.0);
249
250 let vertical_margin = 0.15 * height;
252 let spacing = vertical_margin / 2.;
253 let shape_height = height - (vertical_margin * 2.);
254 let shape_width = shape_height / 2.1;
255
256 let count = card.count();
257 let shape_extent = (count as f64) * (shape_width + spacing) - spacing;
259 let horizontal_margin = (width - shape_extent) / 2.;
260
261 let mut shape_rect = Rectangle {
263 x: x + horizontal_margin,
264 y: y + vertical_margin,
265 width: shape_width,
266 height: shape_height
267 };
268
269 for _ in 0..count {
271 match card.shape() {
272 Shape::Oval => self.rounded_rect(shape_rect, f64::INFINITY),
273 Shape::Squiggle => self.squiggle_in_rect(shape_rect),
274 Shape::Diamond => self.diamond_in_rect(shape_rect)
275 }
276 shape_rect = shape_rect.offset(shape_width + spacing, 0.);
277 }
278
279 let (r, g, b) = scheme.card_color(card);
281 self.set_source_rgb(r, g, b);
282
283 let stroke_width = shape_width / 11.;
285
286 match card.shading() {
288 Shading::Solid => self.fill(),
289 Shading::Outlined => {
290 self.clip_preserve();
293 self.set_line_width(stroke_width * 2.);
295 self.stroke();
296 self.reset_clip();
297 }
298 Shading::Striped => {
299 self.set_source_rgba(r, g, b, MOCK_STRIPE_TRANSLUCENCY);
301 self.fill_preserve();
302
303 self.set_source_gray(1.0);
305 self.set_line_width(stroke_width * 3.);
306 self.stroke_preserve();
307
308 self.set_source_rgb(r, g, b);
310 self.set_line_width(stroke_width * 4. / 3.);
311 self.clip_preserve();
314 self.stroke();
315 self.reset_clip();
316 }
317 }
318 }
319}