1 /* bubblegen.c -- Generate various sorts of bubbles.
2 * Copyright (C) 2008-2020 Nick Gasson
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #ifdef HAVE_CONFIG_H
19 #include "config.h"
20 #endif
21
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <stdbool.h>
25 #include <string.h>
26 #include <math.h>
27
28 #include <gtk/gtk.h>
29 #include <gdk-pixbuf/gdk-pixbuf.h>
30
31 #include "floating_shape.h"
32 #include "display_cow.h"
33 #include "settings.h"
34 #include "i18n.h"
35
36 #define LEFT_BUF 5 // Amount of pixels to leave after cow's tail
37 #define TIP_WIDTH 20 // Length of the triangle bit on the speech bubble
38 #define THINK_WIDTH 80 // Spaces for thinking circles
39 #define CORNER_RADIUS 30 // Radius of corners on the speech bubble
40 #define CORNER_DIAM CORNER_RADIUS*2
41 #define BUBBLE_BORDER 5 // Pixels to leave free around edge of bubble
42 #define MIN_TIP_HEIGHT 15
43
44 // These next ones control the size and position of the "thinking circles"
45 // (or whatever you call them)
46 #define BIG_KIRCLE_X 38
47 #define BIG_KIRCLE_Y 70
48 #define BIG_KIRCLE_DIAM 35
49 #define BIG_KIRCLE_RADIUS (BIG_KIRCLE_DIAM / 2)
50
51 #define SMALL_KIRCLE_X 5
52 #define SMALL_KIRCLE_Y 40
53 #define SMALL_KIRCLE_DIAM 20
54 #define SMALL_KIRCLE_RADIUS (SMALL_KIRCLE_DIAM / 2)
55
56 // Min distance from top of the big kircle to the top of the bubble
57 #define KIRCLE_TOP_MIN 10
58
59 typedef struct {
60 int width, height;
61 cairo_surface_t *surface;
62 cairo_t *cr;
63 } bubble_t;
64
65 typedef enum { NORMAL, THOUGHT } bubble_style_t;
66
bubble_corner_arcs(bubble_t * b,bubble_style_t style,int corners[4][2])67 static void bubble_corner_arcs(bubble_t *b, bubble_style_t style,
68 int corners[4][2])
69 {
70 // Space between cow and bubble
71 int middle = (style == NORMAL ? TIP_WIDTH : THINK_WIDTH);
72
73 if (get_bool_option("left")) {
74 corners[0][0] = BUBBLE_BORDER + CORNER_RADIUS;
75 corners[0][1] = BUBBLE_BORDER + CORNER_RADIUS;
76
77 corners[3][0] = BUBBLE_BORDER + CORNER_RADIUS;
78 corners[3][1] = b->height - CORNER_DIAM + CORNER_RADIUS;
79
80 corners[2][0] = b->width - CORNER_DIAM - BUBBLE_BORDER - middle + CORNER_RADIUS;
81 corners[2][1] = b->height - CORNER_DIAM + CORNER_RADIUS;
82
83 corners[1][0] = b->width - CORNER_DIAM - BUBBLE_BORDER - middle + CORNER_RADIUS;
84 corners[1][1] = BUBBLE_BORDER + CORNER_RADIUS;
85 }
86 else {
87 corners[0][0] = middle + BUBBLE_BORDER + CORNER_RADIUS;
88 corners[0][1] = BUBBLE_BORDER + CORNER_RADIUS;
89
90 corners[3][0] = middle + BUBBLE_BORDER + CORNER_RADIUS;
91 corners[3][1] = b->height - CORNER_DIAM + CORNER_RADIUS;
92
93 corners[2][0] = b->width - CORNER_DIAM - BUBBLE_BORDER + CORNER_RADIUS;
94 corners[2][1] = b->height - CORNER_DIAM + CORNER_RADIUS;
95
96 corners[1][0] = b->width - CORNER_DIAM - BUBBLE_BORDER + CORNER_RADIUS;
97 corners[1][1] = BUBBLE_BORDER + CORNER_RADIUS;
98 }
99 }
100
bubble_init_cairo(bubble_t * b,cairo_t * cr,bubble_style_t style)101 static void bubble_init_cairo(bubble_t *b, cairo_t *cr, bubble_style_t style)
102 {
103 GdkPoint tip_points[5];
104 bool right = !get_bool_option("left");
105
106 cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.0);
107 cairo_rectangle(cr, 0, 0, b->width, b->height);
108 cairo_fill(cr);
109
110 b->width -= BUBBLE_BORDER;
111 b->height -= BUBBLE_BORDER;
112
113 // Space between cow and bubble
114 int middle = style == NORMAL ? TIP_WIDTH : THINK_WIDTH;
115
116 // Draw the white corners
117 int corners[4][2];
118 bubble_corner_arcs(b, style, corners);
119
120 cairo_set_line_width(cr, 4.0);
121 cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
122 cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
123
124 cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
125
126 for (int i = 0; i < 4; i++) {
127 cairo_move_to(cr, corners[i][0], corners[i][1]);
128 cairo_arc(cr, corners[i][0], corners[i][1],
129 CORNER_RADIUS,
130 M_PI + i * (M_PI / 2.0),
131 M_PI + (i+1) * (M_PI / 2.0));
132 cairo_close_path(cr);
133 cairo_fill(cr);
134 }
135
136 // Fill in the middle of the bubble
137 cairo_rectangle(cr,
138 CORNER_RADIUS + (right ? middle : 0) + BUBBLE_BORDER,
139 BUBBLE_BORDER,
140 b->width - middle - BUBBLE_BORDER - CORNER_DIAM,
141 b->height - BUBBLE_BORDER);
142 cairo_rectangle(cr,
143 (right ? middle : 0) + BUBBLE_BORDER,
144 BUBBLE_BORDER + CORNER_RADIUS,
145 b->width - middle - BUBBLE_BORDER*2,
146 b->height - BUBBLE_BORDER - CORNER_DIAM);
147 cairo_fill(cr);
148
149 if (style == NORMAL) {
150 // The points on the tip part
151 int tip_compute_offset = (b->height - BUBBLE_BORDER - CORNER_DIAM)/3;
152 int tip_offset[3] = { tip_compute_offset, tip_compute_offset, tip_compute_offset };
153 if (tip_compute_offset < MIN_TIP_HEIGHT) {
154 int new_offset = (b->height - BUBBLE_BORDER - CORNER_DIAM - MIN_TIP_HEIGHT)/2;
155 tip_offset[0] = new_offset;
156 tip_offset[1] = MIN_TIP_HEIGHT;
157 tip_offset[2] = new_offset;
158 }
159
160 if (right) {
161 tip_points[0].x = middle + BUBBLE_BORDER;
162 tip_points[0].y = BUBBLE_BORDER + CORNER_RADIUS;
163 tip_points[1].x = middle + BUBBLE_BORDER;
164 tip_points[1].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0];
165 tip_points[2].x = BUBBLE_BORDER;
166 tip_points[2].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1]/2;
167 tip_points[3].x = middle + BUBBLE_BORDER;
168 tip_points[3].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1];
169 tip_points[4].x = middle + BUBBLE_BORDER;
170 tip_points[4].y = b->height - CORNER_RADIUS;
171 }
172 else {
173 tip_points[0].x = b->width - middle - BUBBLE_BORDER;
174 tip_points[0].y = BUBBLE_BORDER + CORNER_RADIUS;
175 tip_points[1].x = b->width - middle - BUBBLE_BORDER;
176 tip_points[1].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0];
177 tip_points[2].x = b->width - BUBBLE_BORDER;
178 tip_points[2].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1]/2;
179 tip_points[3].x = b->width - middle - BUBBLE_BORDER;
180 tip_points[3].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1];
181 tip_points[4].x = b->width - middle - BUBBLE_BORDER;
182 tip_points[4].y = b->height - CORNER_RADIUS;
183 }
184
185 cairo_move_to(cr, tip_points[0].x, tip_points[0].y);
186 for (int i = 1; i < 5; i++) {
187 cairo_line_to(cr, tip_points[i].x, tip_points[i].y);
188 }
189
190 cairo_close_path(cr);
191 cairo_fill(cr);
192 }
193 else {
194 // Incrementally move the top kircle down so it's within the
195 // bubble's border
196 int big_y = BIG_KIRCLE_Y;
197 int small_y = SMALL_KIRCLE_Y;
198
199 while (big_y + KIRCLE_TOP_MIN > b->height/2) {
200 big_y /= 2;
201 small_y /= 2;
202 }
203
204 // Draw two think kircles
205 cairo_arc(cr,
206 (right ? BIG_KIRCLE_X + BIG_KIRCLE_RADIUS
207 : b->width - BIG_KIRCLE_X - BIG_KIRCLE_RADIUS),
208 b->height/2 - big_y + BIG_KIRCLE_RADIUS, BIG_KIRCLE_RADIUS,
209 0, 2.0 * M_PI);
210
211 cairo_arc(cr,
212 (right ? SMALL_KIRCLE_X + SMALL_KIRCLE_RADIUS
213 : b->width - SMALL_KIRCLE_X - SMALL_KIRCLE_RADIUS),
214 b->height/2 - small_y + SMALL_KIRCLE_RADIUS, SMALL_KIRCLE_RADIUS,
215 0, 2.0 * M_PI);
216
217 cairo_fill(cr);
218
219 cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
220
221 cairo_arc(cr,
222 (right ? BIG_KIRCLE_X + BIG_KIRCLE_RADIUS
223 : b->width - BIG_KIRCLE_X - BIG_KIRCLE_RADIUS),
224 b->height/2 - big_y + BIG_KIRCLE_RADIUS, BIG_KIRCLE_RADIUS,
225 0, 2.0 * M_PI);
226
227 cairo_stroke(cr);
228
229 cairo_arc(cr,
230 (right ? SMALL_KIRCLE_X + SMALL_KIRCLE_RADIUS
231 : b->width - SMALL_KIRCLE_X - SMALL_KIRCLE_RADIUS),
232 b->height/2 - small_y + SMALL_KIRCLE_RADIUS, SMALL_KIRCLE_RADIUS,
233 0, 2.0 * M_PI);
234
235 cairo_stroke(cr);
236 }
237
238 // Draw the black rounded corners
239 cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
240
241 // Top left
242 cairo_arc(cr,
243 (right ? middle : 0) + BUBBLE_BORDER + CORNER_RADIUS,
244 BUBBLE_BORDER + CORNER_RADIUS,
245 CORNER_RADIUS,
246 M_PI, M_PI + (M_PI / 2.0));
247
248 // Top right
249 cairo_arc(cr,
250 b->width - (!right ? middle : 0) - CORNER_RADIUS - BUBBLE_BORDER,
251 BUBBLE_BORDER + CORNER_RADIUS,
252 CORNER_RADIUS,
253 M_PI + (M_PI / 2.0), 2 * M_PI);
254
255 if (style == NORMAL && !right) {
256 cairo_move_to(cr, tip_points[0].x, tip_points[0].y);
257 for (int i = 1; i < 5; i++) {
258 cairo_line_to(cr, tip_points[i].x, tip_points[i].y);
259 }
260 }
261
262 // Bottom left
263 cairo_arc(cr,
264 b->width - (!right ? middle : 0) - CORNER_RADIUS - BUBBLE_BORDER,
265 b->height - CORNER_RADIUS,
266 CORNER_RADIUS,
267 0.0, (M_PI / 2.0));
268
269 cairo_arc(cr,
270 (right ? middle : 0) + BUBBLE_BORDER + CORNER_RADIUS,
271 b->height - CORNER_RADIUS,
272 CORNER_RADIUS,
273 M_PI / 2.0, M_PI);
274
275 if (style == NORMAL && right) {
276 cairo_move_to(cr, tip_points[0].x, tip_points[0].y);
277 for (int i = 1; i < 5; i++) {
278 cairo_line_to(cr, tip_points[i].x, tip_points[i].y);
279 }
280 }
281 else {
282 cairo_line_to(cr, (right ? middle : 0) + BUBBLE_BORDER,
283 BUBBLE_BORDER + CORNER_RADIUS);
284 }
285
286 cairo_stroke(cr);
287 }
288
bubble_init(bubble_t * b,bubble_style_t style)289 static void bubble_init(bubble_t *b, bubble_style_t style)
290 {
291 b->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
292 b->width, b->height);
293 g_assert(b->surface);
294
295 b->cr = cairo_create(b->surface);
296
297 bubble_init_cairo(b, b->cr, style);
298 }
299
bubble_size_from_content(bubble_t * b,bubble_style_t style,int c_width,int c_height)300 static void bubble_size_from_content(bubble_t *b, bubble_style_t style,
301 int c_width, int c_height)
302 {
303 int middle = style == NORMAL ? TIP_WIDTH : THINK_WIDTH;
304 b->width = 2*BUBBLE_BORDER + CORNER_DIAM + middle + c_width;
305 b->height = BUBBLE_BORDER + CORNER_DIAM + c_height;
306 }
307
bubble_tidy(bubble_t * b)308 static GdkPixbuf *bubble_tidy(bubble_t *b)
309 {
310 GdkPixbuf *pixbuf =
311 gdk_pixbuf_get_from_surface(b->surface, 0, 0, b->width + BUBBLE_BORDER, b->height + BUBBLE_BORDER);
312
313 cairo_surface_destroy(b->surface);
314 return pixbuf;
315 }
316
bubble_content_left(bubble_style_t style)317 static int bubble_content_left(bubble_style_t style)
318 {
319 if (get_bool_option("left")) {
320 return BUBBLE_BORDER + CORNER_RADIUS;
321 }
322 else {
323 const int middle = style == NORMAL ? TIP_WIDTH : THINK_WIDTH;
324 return BUBBLE_BORDER + middle + CORNER_RADIUS;
325 }
326 }
327
bubble_content_top()328 static int bubble_content_top()
329 {
330 return CORNER_RADIUS;
331 }
332
make_dream_bubble(const char * file,int * p_width,int * p_height)333 GdkPixbuf *make_dream_bubble(const char *file, int *p_width, int *p_height)
334 {
335 bubble_t bubble;
336 GError *error = NULL;
337 GdkPixbuf *image = gdk_pixbuf_new_from_file(file, &error);
338
339 if (NULL == image) {
340 fprintf(stderr, "Error: failed to load %s\n", file);
341 exit(1);
342 }
343
344 bubble_size_from_content(&bubble, THOUGHT, gdk_pixbuf_get_width(image),
345 gdk_pixbuf_get_height(image));
346 *p_width = bubble.width;
347 *p_height = bubble.height;
348
349 bubble_init(&bubble, THOUGHT);
350
351 gdk_cairo_set_source_pixbuf(bubble.cr, image,
352 bubble_content_left(THOUGHT),
353 bubble_content_top());
354 cairo_paint(bubble.cr);
355
356 cairo_destroy(bubble.cr);
357 g_object_unref(image);
358
359 return bubble_tidy(&bubble);
360 }
361
make_text_bubble(char * text,int * p_width,int * p_height,int max_width,cowmode_t mode)362 GdkPixbuf *make_text_bubble(char *text, int *p_width, int *p_height,
363 int max_width, cowmode_t mode)
364 {
365 bubble_t bubble;
366 int text_width, text_height;
367
368 // Work out the size of the bubble from the text
369 PangoContext *pango_context = gdk_pango_context_get();
370 PangoLayout *layout = pango_layout_new(pango_context);
371 PangoFontDescription *font =
372 pango_font_description_from_string(get_string_option("font"));
373 PangoAttrList *pango_attrs = NULL;
374
375 // Adjust max width to account for bubble edges
376 max_width -= LEFT_BUF;
377 max_width -= TIP_WIDTH;
378 max_width -= 2 * BUBBLE_BORDER;
379 max_width -= CORNER_DIAM;
380
381 if (get_bool_option("wrap")) {
382 pango_layout_set_width(layout, max_width * PANGO_SCALE);
383 pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR);
384 }
385
386 char *stripped;
387 if (!pango_parse_markup(text, -1, 0, &pango_attrs,
388 &stripped, NULL, NULL)) {
389
390 // This isn't fatal as the the text may contain angled brackets, etc.
391 stripped = text;
392 }
393 else {
394 pango_layout_set_attributes(layout, pango_attrs);
395 }
396
397 pango_layout_set_font_description(layout, font);
398 pango_layout_set_text(layout, stripped, -1);
399 pango_layout_get_pixel_size(layout, &text_width, &text_height);
400
401 bubble_style_t style = mode == COWMODE_NORMAL ? NORMAL : THOUGHT;
402
403 bubble_size_from_content(&bubble, style, text_width, text_height);
404 *p_width = bubble.width;
405 *p_height = bubble.height;
406
407 bubble_init(&bubble, style);
408
409 // Render the text
410 cairo_move_to(bubble.cr, bubble_content_left(style), bubble_content_top());
411 pango_cairo_show_layout(bubble.cr, layout);
412
413 cairo_destroy(bubble.cr);
414
415 // Make sure to free the Pango objects
416 g_object_unref(pango_context);
417 g_object_unref(layout);
418 pango_font_description_free(font);
419 if (NULL != pango_attrs)
420 pango_attr_list_unref(pango_attrs);
421
422 return bubble_tidy(&bubble);
423 }
424