| /* |
| * Copyright 2019 University of Toronto |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| * Authors: Mario Badr, Sameh Attia and Tanner Young-Schultz |
| */ |
| |
| #include "ezgl/canvas.hpp" |
| |
| #include "ezgl/graphics.hpp" |
| |
| #include <gtk/gtk.h> |
| |
| #include <cassert> |
| #include <cmath> |
| #include <functional> |
| |
| namespace ezgl { |
| |
| static cairo_surface_t *create_surface(GtkWidget *widget) |
| { |
| GdkWindow *parent_window = gtk_widget_get_window(widget); |
| int const width = gtk_widget_get_allocated_width(widget); |
| int const height = gtk_widget_get_allocated_height(widget); |
| |
| // Cairo image surfaces are more efficient than normal Cairo surfaces |
| // However, you cannot use X11 functions to draw on image surfaces |
| #ifdef EZGL_USE_X11 |
| return gdk_window_create_similar_surface(parent_window, CAIRO_CONTENT_COLOR_ALPHA, width, height); |
| #else |
| return gdk_window_create_similar_image_surface( |
| parent_window, CAIRO_FORMAT_ARGB32, width, height, 0); |
| #endif |
| } |
| |
| static cairo_t *create_context(cairo_surface_t *p_surface) |
| { |
| cairo_t *context = cairo_create(p_surface); |
| |
| // Set the antialiasing mode of the rasterizer used for drawing shapes |
| // Set to CAIRO_ANTIALIAS_NONE for maximum speed |
| // See https://www.cairographics.org/manual/cairo-cairo-t.html#cairo-antialias-t |
| cairo_set_antialias(context, CAIRO_ANTIALIAS_NONE); |
| |
| return context; |
| } |
| |
| bool canvas::print_pdf(const char *file_name, int output_width, int output_height) |
| { |
| cairo_surface_t *pdf_surface; |
| cairo_t *context; |
| int surface_width = 0; |
| int surface_height = 0; |
| |
| // create pdf surface based on canvas size |
| if(output_width == 0 && output_height == 0){ |
| surface_width = gtk_widget_get_allocated_width(m_drawing_area); |
| surface_height = gtk_widget_get_allocated_height(m_drawing_area); |
| }else{ |
| surface_width = output_width; |
| surface_height = output_height; |
| } |
| pdf_surface = cairo_pdf_surface_create(file_name, surface_width, surface_height); |
| |
| if(pdf_surface == NULL) |
| return false; // failed to create due to errors such as out of memory |
| context = create_context(pdf_surface); |
| |
| // draw on the newly created pdf surface & context |
| cairo_set_source_rgb(context, m_background_color.red / 255.0, m_background_color.green / 255.0, |
| m_background_color.blue / 255.0); |
| cairo_paint(context); |
| |
| using namespace std::placeholders; |
| camera pdf_cam = m_camera; |
| pdf_cam.update_widget(surface_width, surface_height); |
| renderer g(context, std::bind(&camera::world_to_screen, pdf_cam, _1), &pdf_cam, pdf_surface); |
| m_draw_callback(g); |
| |
| // free surface & context |
| cairo_surface_destroy(pdf_surface); |
| cairo_destroy(context); |
| |
| return true; |
| } |
| |
| bool canvas::print_svg(const char *file_name, int output_width, int output_height) |
| { |
| cairo_surface_t *svg_surface; |
| cairo_t *context; |
| int surface_width = 0; |
| int surface_height = 0; |
| |
| // create pdf surface based on canvas size |
| if(output_width == 0 && output_height == 0){ |
| surface_width = gtk_widget_get_allocated_width(m_drawing_area); |
| surface_height = gtk_widget_get_allocated_height(m_drawing_area); |
| }else{ |
| surface_width = output_width; |
| surface_height = output_height; |
| } |
| svg_surface = cairo_svg_surface_create(file_name, surface_width, surface_height); |
| |
| if(svg_surface == NULL) |
| return false; // failed to create due to errors such as out of memory |
| context = create_context(svg_surface); |
| |
| // draw on the newly created svg surface & context |
| cairo_set_source_rgb(context, m_background_color.red / 255.0, m_background_color.green / 255.0, |
| m_background_color.blue / 255.0); |
| cairo_paint(context); |
| |
| using namespace std::placeholders; |
| camera svg_cam = m_camera; |
| svg_cam.update_widget(surface_width, surface_height); |
| renderer g(context, std::bind(&camera::world_to_screen, svg_cam, _1), &svg_cam, svg_surface); |
| m_draw_callback(g); |
| |
| // free surface & context |
| cairo_surface_destroy(svg_surface); |
| cairo_destroy(context); |
| |
| return true; |
| } |
| |
| bool canvas::print_png(const char *file_name, int output_width, int output_height) |
| { |
| cairo_surface_t *png_surface; |
| cairo_t *context; |
| int surface_width = 0; |
| int surface_height = 0; |
| |
| // create pdf surface based on canvas size |
| if(output_width == 0 && output_height == 0){ |
| surface_width = gtk_widget_get_allocated_width(m_drawing_area); |
| surface_height = gtk_widget_get_allocated_height(m_drawing_area); |
| }else{ |
| surface_width = output_width; |
| surface_height = output_height; |
| } |
| png_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, surface_width, surface_height); |
| |
| if(png_surface == NULL) |
| return false; // failed to create due to errors such as out of memory |
| context = create_context(png_surface); |
| |
| // draw on the newly created png surface & context |
| cairo_set_source_rgb(context, m_background_color.red / 255.0, m_background_color.green / 255.0, |
| m_background_color.blue / 255.0); |
| cairo_paint(context); |
| |
| using namespace std::placeholders; |
| camera png_cam = m_camera; |
| png_cam.update_widget(surface_width, surface_height); |
| renderer g(context, std::bind(&camera::world_to_screen, png_cam, _1), &png_cam, png_surface); |
| m_draw_callback(g); |
| |
| // create png output file |
| cairo_surface_write_to_png(png_surface, file_name); |
| |
| // free surface & context |
| cairo_surface_destroy(png_surface); |
| cairo_destroy(context); |
| |
| return true; |
| } |
| |
| gboolean canvas::configure_event(GtkWidget *widget, GdkEventConfigure *, gpointer data) |
| { |
| // User data should have been set during the signal connection. |
| g_return_val_if_fail(data != nullptr, FALSE); |
| |
| auto ezgl_canvas = static_cast<canvas *>(data); |
| auto &p_surface = ezgl_canvas->m_surface; |
| auto &p_context = ezgl_canvas->m_context; |
| |
| if(p_surface != nullptr) { |
| cairo_surface_destroy(p_surface); |
| } |
| |
| if(p_context != nullptr) { |
| cairo_destroy(p_context); |
| } |
| |
| // Something has changed, recreate the surface. |
| p_surface = create_surface(widget); |
| |
| // Recreate the context |
| p_context = create_context(p_surface); |
| |
| // The camera needs to be updated before we start drawing again. |
| ezgl_canvas->m_camera.update_widget(ezgl_canvas->width(), ezgl_canvas->height()); |
| |
| // Draw to the newly created surface. |
| ezgl_canvas->redraw(); |
| |
| g_info("canvas::configure_event has been handled."); |
| return TRUE; // the configure event was handled |
| } |
| |
| gboolean canvas::draw_surface(GtkWidget *, cairo_t *context, gpointer data) |
| { |
| // Assume context and data are non-null. |
| auto &p_surface = static_cast<canvas *>(data)->m_surface; |
| |
| // Assume surface is non-null. |
| cairo_set_source_surface(context, p_surface, 0, 0); |
| cairo_paint(context); |
| |
| return FALSE; |
| } |
| |
| canvas::canvas(std::string canvas_id, |
| draw_canvas_fn draw_callback, |
| rectangle coordinate_system, |
| color background_color) |
| : m_canvas_id(std::move(canvas_id)) |
| , m_draw_callback(draw_callback) |
| , m_camera(coordinate_system) |
| , m_background_color(background_color) |
| { |
| } |
| |
| canvas::~canvas() |
| { |
| if(m_surface != nullptr) { |
| cairo_surface_destroy(m_surface); |
| } |
| |
| if(m_context != nullptr) { |
| cairo_destroy(m_context); |
| } |
| } |
| |
| int canvas::width() const |
| { |
| return gtk_widget_get_allocated_width(m_drawing_area); |
| } |
| |
| int canvas::height() const |
| { |
| return gtk_widget_get_allocated_height(m_drawing_area); |
| } |
| |
| void canvas::initialize(GtkWidget *drawing_area) |
| { |
| g_return_if_fail(drawing_area != nullptr); |
| |
| m_drawing_area = drawing_area; |
| m_surface = create_surface(m_drawing_area); |
| m_context = create_context(m_surface); |
| m_camera.update_widget(width(), height()); |
| |
| // Draw to the newly created surface for the first time. |
| redraw(); |
| |
| // Connect to configure events in case our widget changes shape. |
| g_signal_connect(m_drawing_area, "configure-event", G_CALLBACK(configure_event), this); |
| // Connect to draw events so that we draw our surface to the drawing area. |
| g_signal_connect(m_drawing_area, "draw", G_CALLBACK(draw_surface), this); |
| |
| // GtkDrawingArea objects need specific events enabled explicitly. |
| gtk_widget_add_events(GTK_WIDGET(m_drawing_area), GDK_BUTTON_PRESS_MASK); |
| gtk_widget_add_events(GTK_WIDGET(m_drawing_area), GDK_BUTTON_RELEASE_MASK); |
| gtk_widget_add_events(GTK_WIDGET(m_drawing_area), GDK_POINTER_MOTION_MASK); |
| gtk_widget_add_events(GTK_WIDGET(m_drawing_area), GDK_SCROLL_MASK); |
| |
| g_info("canvas::initialize successful."); |
| } |
| |
| void canvas::redraw() |
| { |
| // Clear the screen and set the background color |
| cairo_set_source_rgb(m_context, m_background_color.red / 255.0, m_background_color.green / 255.0, |
| m_background_color.blue / 255.0); |
| cairo_paint(m_context); |
| |
| using namespace std::placeholders; |
| renderer g(m_context, std::bind(&camera::world_to_screen, m_camera, _1), &m_camera, m_surface); |
| m_draw_callback(g); |
| |
| gtk_widget_queue_draw(m_drawing_area); |
| |
| g_info("The canvas will be redrawn."); |
| } |
| |
| renderer canvas::create_temporary_renderer() |
| { |
| using namespace std::placeholders; |
| renderer g(m_context, std::bind(&camera::world_to_screen, m_camera, _1), &m_camera, m_surface); |
| |
| return g; |
| } |
| } // namespace ezgl |