/* bubblegen.c -- Generate various sorts of bubbles. * Copyright (C) 2008-2020 Nick Gasson * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include "floating_shape.h" #include "display_cow.h" #include "settings.h" #include "i18n.h" #define LEFT_BUF 5 // Amount of pixels to leave after cow's tail #define TIP_WIDTH 20 // Length of the triangle bit on the speech bubble #define THINK_WIDTH 80 // Spaces for thinking circles #define CORNER_RADIUS 30 // Radius of corners on the speech bubble #define CORNER_DIAM CORNER_RADIUS*2 #define BUBBLE_BORDER 5 // Pixels to leave free around edge of bubble #define MIN_TIP_HEIGHT 15 // These next ones control the size and position of the "thinking circles" // (or whatever you call them) #define BIG_KIRCLE_X 38 #define BIG_KIRCLE_Y 70 #define BIG_KIRCLE_DIAM 35 #define BIG_KIRCLE_RADIUS (BIG_KIRCLE_DIAM / 2) #define SMALL_KIRCLE_X 5 #define SMALL_KIRCLE_Y 40 #define SMALL_KIRCLE_DIAM 20 #define SMALL_KIRCLE_RADIUS (SMALL_KIRCLE_DIAM / 2) // Min distance from top of the big kircle to the top of the bubble #define KIRCLE_TOP_MIN 10 typedef struct { int width, height; cairo_surface_t *surface; cairo_t *cr; } bubble_t; typedef enum { NORMAL, THOUGHT } bubble_style_t; static void bubble_corner_arcs(bubble_t *b, bubble_style_t style, int corners[4][2]) { // Space between cow and bubble int middle = (style == NORMAL ? TIP_WIDTH : THINK_WIDTH); if (get_bool_option("left")) { corners[0][0] = BUBBLE_BORDER + CORNER_RADIUS; corners[0][1] = BUBBLE_BORDER + CORNER_RADIUS; corners[3][0] = BUBBLE_BORDER + CORNER_RADIUS; corners[3][1] = b->height - CORNER_DIAM + CORNER_RADIUS; corners[2][0] = b->width - CORNER_DIAM - BUBBLE_BORDER - middle + CORNER_RADIUS; corners[2][1] = b->height - CORNER_DIAM + CORNER_RADIUS; corners[1][0] = b->width - CORNER_DIAM - BUBBLE_BORDER - middle + CORNER_RADIUS; corners[1][1] = BUBBLE_BORDER + CORNER_RADIUS; } else { corners[0][0] = middle + BUBBLE_BORDER + CORNER_RADIUS; corners[0][1] = BUBBLE_BORDER + CORNER_RADIUS; corners[3][0] = middle + BUBBLE_BORDER + CORNER_RADIUS; corners[3][1] = b->height - CORNER_DIAM + CORNER_RADIUS; corners[2][0] = b->width - CORNER_DIAM - BUBBLE_BORDER + CORNER_RADIUS; corners[2][1] = b->height - CORNER_DIAM + CORNER_RADIUS; corners[1][0] = b->width - CORNER_DIAM - BUBBLE_BORDER + CORNER_RADIUS; corners[1][1] = BUBBLE_BORDER + CORNER_RADIUS; } } static void bubble_init_cairo(bubble_t *b, cairo_t *cr, bubble_style_t style) { GdkPoint tip_points[5]; bool right = !get_bool_option("left"); cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.0); cairo_rectangle(cr, 0, 0, b->width, b->height); cairo_fill(cr); b->width -= BUBBLE_BORDER; b->height -= BUBBLE_BORDER; // Space between cow and bubble int middle = style == NORMAL ? TIP_WIDTH : THINK_WIDTH; // Draw the white corners int corners[4][2]; bubble_corner_arcs(b, style, corners); cairo_set_line_width(cr, 4.0); cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); for (int i = 0; i < 4; i++) { cairo_move_to(cr, corners[i][0], corners[i][1]); cairo_arc(cr, corners[i][0], corners[i][1], CORNER_RADIUS, M_PI + i * (M_PI / 2.0), M_PI + (i+1) * (M_PI / 2.0)); cairo_close_path(cr); cairo_fill(cr); } // Fill in the middle of the bubble cairo_rectangle(cr, CORNER_RADIUS + (right ? middle : 0) + BUBBLE_BORDER, BUBBLE_BORDER, b->width - middle - BUBBLE_BORDER - CORNER_DIAM, b->height - BUBBLE_BORDER); cairo_rectangle(cr, (right ? middle : 0) + BUBBLE_BORDER, BUBBLE_BORDER + CORNER_RADIUS, b->width - middle - BUBBLE_BORDER*2, b->height - BUBBLE_BORDER - CORNER_DIAM); cairo_fill(cr); if (style == NORMAL) { // The points on the tip part int tip_compute_offset = (b->height - BUBBLE_BORDER - CORNER_DIAM)/3; int tip_offset[3] = { tip_compute_offset, tip_compute_offset, tip_compute_offset }; if (tip_compute_offset < MIN_TIP_HEIGHT) { int new_offset = (b->height - BUBBLE_BORDER - CORNER_DIAM - MIN_TIP_HEIGHT)/2; tip_offset[0] = new_offset; tip_offset[1] = MIN_TIP_HEIGHT; tip_offset[2] = new_offset; } if (right) { tip_points[0].x = middle + BUBBLE_BORDER; tip_points[0].y = BUBBLE_BORDER + CORNER_RADIUS; tip_points[1].x = middle + BUBBLE_BORDER; tip_points[1].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0]; tip_points[2].x = BUBBLE_BORDER; tip_points[2].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1]/2; tip_points[3].x = middle + BUBBLE_BORDER; tip_points[3].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1]; tip_points[4].x = middle + BUBBLE_BORDER; tip_points[4].y = b->height - CORNER_RADIUS; } else { tip_points[0].x = b->width - middle - BUBBLE_BORDER; tip_points[0].y = BUBBLE_BORDER + CORNER_RADIUS; tip_points[1].x = b->width - middle - BUBBLE_BORDER; tip_points[1].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0]; tip_points[2].x = b->width - BUBBLE_BORDER; tip_points[2].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1]/2; tip_points[3].x = b->width - middle - BUBBLE_BORDER; tip_points[3].y = BUBBLE_BORDER + CORNER_RADIUS + tip_offset[0] + tip_offset[1]; tip_points[4].x = b->width - middle - BUBBLE_BORDER; tip_points[4].y = b->height - CORNER_RADIUS; } cairo_move_to(cr, tip_points[0].x, tip_points[0].y); for (int i = 1; i < 5; i++) { cairo_line_to(cr, tip_points[i].x, tip_points[i].y); } cairo_close_path(cr); cairo_fill(cr); } else { // Incrementally move the top kircle down so it's within the // bubble's border int big_y = BIG_KIRCLE_Y; int small_y = SMALL_KIRCLE_Y; while (big_y + KIRCLE_TOP_MIN > b->height/2) { big_y /= 2; small_y /= 2; } // Draw two think kircles cairo_arc(cr, (right ? BIG_KIRCLE_X + BIG_KIRCLE_RADIUS : b->width - BIG_KIRCLE_X - BIG_KIRCLE_RADIUS), b->height/2 - big_y + BIG_KIRCLE_RADIUS, BIG_KIRCLE_RADIUS, 0, 2.0 * M_PI); cairo_arc(cr, (right ? SMALL_KIRCLE_X + SMALL_KIRCLE_RADIUS : b->width - SMALL_KIRCLE_X - SMALL_KIRCLE_RADIUS), b->height/2 - small_y + SMALL_KIRCLE_RADIUS, SMALL_KIRCLE_RADIUS, 0, 2.0 * M_PI); cairo_fill(cr); cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); cairo_arc(cr, (right ? BIG_KIRCLE_X + BIG_KIRCLE_RADIUS : b->width - BIG_KIRCLE_X - BIG_KIRCLE_RADIUS), b->height/2 - big_y + BIG_KIRCLE_RADIUS, BIG_KIRCLE_RADIUS, 0, 2.0 * M_PI); cairo_stroke(cr); cairo_arc(cr, (right ? SMALL_KIRCLE_X + SMALL_KIRCLE_RADIUS : b->width - SMALL_KIRCLE_X - SMALL_KIRCLE_RADIUS), b->height/2 - small_y + SMALL_KIRCLE_RADIUS, SMALL_KIRCLE_RADIUS, 0, 2.0 * M_PI); cairo_stroke(cr); } // Draw the black rounded corners cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); // Top left cairo_arc(cr, (right ? middle : 0) + BUBBLE_BORDER + CORNER_RADIUS, BUBBLE_BORDER + CORNER_RADIUS, CORNER_RADIUS, M_PI, M_PI + (M_PI / 2.0)); // Top right cairo_arc(cr, b->width - (!right ? middle : 0) - CORNER_RADIUS - BUBBLE_BORDER, BUBBLE_BORDER + CORNER_RADIUS, CORNER_RADIUS, M_PI + (M_PI / 2.0), 2 * M_PI); if (style == NORMAL && !right) { cairo_move_to(cr, tip_points[0].x, tip_points[0].y); for (int i = 1; i < 5; i++) { cairo_line_to(cr, tip_points[i].x, tip_points[i].y); } } // Bottom left cairo_arc(cr, b->width - (!right ? middle : 0) - CORNER_RADIUS - BUBBLE_BORDER, b->height - CORNER_RADIUS, CORNER_RADIUS, 0.0, (M_PI / 2.0)); cairo_arc(cr, (right ? middle : 0) + BUBBLE_BORDER + CORNER_RADIUS, b->height - CORNER_RADIUS, CORNER_RADIUS, M_PI / 2.0, M_PI); if (style == NORMAL && right) { cairo_move_to(cr, tip_points[0].x, tip_points[0].y); for (int i = 1; i < 5; i++) { cairo_line_to(cr, tip_points[i].x, tip_points[i].y); } } else { cairo_line_to(cr, (right ? middle : 0) + BUBBLE_BORDER, BUBBLE_BORDER + CORNER_RADIUS); } cairo_stroke(cr); } static void bubble_init(bubble_t *b, bubble_style_t style) { b->surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, b->width, b->height); g_assert(b->surface); b->cr = cairo_create(b->surface); bubble_init_cairo(b, b->cr, style); } static void bubble_size_from_content(bubble_t *b, bubble_style_t style, int c_width, int c_height) { int middle = style == NORMAL ? TIP_WIDTH : THINK_WIDTH; b->width = 2*BUBBLE_BORDER + CORNER_DIAM + middle + c_width; b->height = BUBBLE_BORDER + CORNER_DIAM + c_height; } static GdkPixbuf *bubble_tidy(bubble_t *b) { GdkPixbuf *pixbuf = gdk_pixbuf_get_from_surface(b->surface, 0, 0, b->width + BUBBLE_BORDER, b->height + BUBBLE_BORDER); cairo_surface_destroy(b->surface); return pixbuf; } static int bubble_content_left(bubble_style_t style) { if (get_bool_option("left")) { return BUBBLE_BORDER + CORNER_RADIUS; } else { const int middle = style == NORMAL ? TIP_WIDTH : THINK_WIDTH; return BUBBLE_BORDER + middle + CORNER_RADIUS; } } static int bubble_content_top() { return CORNER_RADIUS; } GdkPixbuf *make_dream_bubble(const char *file, int *p_width, int *p_height) { bubble_t bubble; GError *error = NULL; GdkPixbuf *image = gdk_pixbuf_new_from_file(file, &error); if (NULL == image) { fprintf(stderr, "Error: failed to load %s\n", file); exit(1); } bubble_size_from_content(&bubble, THOUGHT, gdk_pixbuf_get_width(image), gdk_pixbuf_get_height(image)); *p_width = bubble.width; *p_height = bubble.height; bubble_init(&bubble, THOUGHT); gdk_cairo_set_source_pixbuf(bubble.cr, image, bubble_content_left(THOUGHT), bubble_content_top()); cairo_paint(bubble.cr); cairo_destroy(bubble.cr); g_object_unref(image); return bubble_tidy(&bubble); } GdkPixbuf *make_text_bubble(char *text, int *p_width, int *p_height, int max_width, cowmode_t mode) { bubble_t bubble; int text_width, text_height; // Work out the size of the bubble from the text PangoContext *pango_context = gdk_pango_context_get(); PangoLayout *layout = pango_layout_new(pango_context); PangoFontDescription *font = pango_font_description_from_string(get_string_option("font")); PangoAttrList *pango_attrs = NULL; // Adjust max width to account for bubble edges max_width -= LEFT_BUF; max_width -= TIP_WIDTH; max_width -= 2 * BUBBLE_BORDER; max_width -= CORNER_DIAM; if (get_bool_option("wrap")) { pango_layout_set_width(layout, max_width * PANGO_SCALE); pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); } char *stripped; if (!pango_parse_markup(text, -1, 0, &pango_attrs, &stripped, NULL, NULL)) { // This isn't fatal as the the text may contain angled brackets, etc. stripped = text; } else { pango_layout_set_attributes(layout, pango_attrs); } pango_layout_set_font_description(layout, font); pango_layout_set_text(layout, stripped, -1); pango_layout_get_pixel_size(layout, &text_width, &text_height); bubble_style_t style = mode == COWMODE_NORMAL ? NORMAL : THOUGHT; bubble_size_from_content(&bubble, style, text_width, text_height); *p_width = bubble.width; *p_height = bubble.height; bubble_init(&bubble, style); // Render the text cairo_move_to(bubble.cr, bubble_content_left(style), bubble_content_top()); pango_cairo_show_layout(bubble.cr, layout); cairo_destroy(bubble.cr); // Make sure to free the Pango objects g_object_unref(pango_context); g_object_unref(layout); pango_font_description_free(font); if (NULL != pango_attrs) pango_attr_list_unref(pango_attrs); return bubble_tidy(&bubble); }