OpenFPGA/libs/EXTERNAL/libezgl/src/graphics.cpp

744 lines
22 KiB
C++

/*
* 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/graphics.hpp"
#include <cassert>
#include <glib.h>
namespace ezgl {
renderer::renderer(cairo_t *cairo,
transform_fn transform,
camera *p_camera,
cairo_surface_t *m_surface)
: m_cairo(cairo), m_transform(std::move(transform)), m_camera(p_camera), rotation_angle(0)
{
#ifdef EZGL_USE_X11
// Check if the created cairo surface is an XLIB surface
if (cairo_surface_get_type(m_surface) == CAIRO_SURFACE_TYPE_XLIB) {
// get the underlying x11 drawable used by cairo surface
x11_drawable = cairo_xlib_surface_get_drawable(m_surface);
// get the x11 display
x11_display = cairo_xlib_surface_get_display(m_surface);
// create the x11 context from the drawable of the cairo surface
if (x11_display != nullptr) {
x11_context = XCreateGC(x11_display, x11_drawable, 0, 0);
}
}
#endif
}
renderer::~renderer()
{
#ifdef EZGL_USE_X11
// free the x11 context
if (x11_display != nullptr) {
XFreeGC(x11_display, x11_context);
}
#endif
}
void renderer::update_renderer(cairo_t *cairo, cairo_surface_t *m_surface)
{
// Update Cairo Context
m_cairo = cairo;
// Update X11 Context
#ifdef EZGL_USE_X11
// Check if the created cairo surface is an XLIB surface
if (cairo_surface_get_type(m_surface) == CAIRO_SURFACE_TYPE_XLIB) {
// get the underlying x11 drawable used by cairo surface
x11_drawable = cairo_xlib_surface_get_drawable(m_surface);
// get the x11 display
x11_display = cairo_xlib_surface_get_display(m_surface);
// create the x11 context from the drawable of the cairo surface
if (x11_display != nullptr) {
XFreeGC(x11_display, x11_context);
x11_context = XCreateGC(x11_display, x11_drawable, 0, 0);
}
}
#endif
// Restore graphics attributes
set_color(current_color);
set_line_width(current_line_width);
set_line_cap(current_line_cap);
set_line_dash(current_line_dash);
}
void renderer::set_coordinate_system(t_coordinate_system new_coordinate_system)
{
current_coordinate_system = new_coordinate_system;
}
void renderer::set_visible_world(rectangle new_world)
{
// Change the aspect ratio of the new_world to align with the aspect ratio of the initial world
// Get the width and height of the new_world
point2d n_center = new_world.center();
double n_width = new_world.width();
double n_height = new_world.height();
// Get the aspect ratio of the initial world
double i_width = m_camera->get_initial_world().width();
double i_height = m_camera->get_initial_world().height();
double i_aspect_ratio = i_width / i_height;
// Make sure the required area is entirely visible
if (n_width/i_aspect_ratio >= n_height) {
// Change the height
double new_height = n_width/i_aspect_ratio;
new_world ={{n_center.x-n_width/2, n_center.y-new_height/2}, n_width, new_height};
}
else {
// Change the width
double new_width = n_height/i_aspect_ratio;
new_world ={{n_center.x-new_width/2, n_center.y-n_height/2}, new_width, n_height};
}
// set the visible bounds of the world
m_camera->set_world(new_world);
}
rectangle renderer::get_visible_world()
{
// m_camera->get_world() is not good representative of the visible world since it doesn't
// account for the drawable margins.
// TODO: precalculate the visible world in camera class to speedup the clipping
// Get the world and screen dimensions
rectangle world = m_camera->get_world();
rectangle screen = m_camera->get_screen();
// Calculate the margins by converting the screen origin to world coordinates
point2d margin = screen.bottom_left() * m_camera->get_world_scale_factor();
// The actual visible world
return {(world.bottom_left() - margin), (world.top_right() + margin)};
}
rectangle renderer::get_visible_screen()
{
// Get the widget dimensions
return m_camera->get_widget();
}
rectangle renderer::world_to_screen(const rectangle& box)
{
point2d origin = m_transform(box.bottom_left());
point2d top_right = m_transform(box.top_right());
return rectangle(origin, top_right);
}
bool renderer::rectangle_off_screen(rectangle rect)
{
if(current_coordinate_system == SCREEN)
return false;
rectangle visible = get_visible_world();
if(rect.right() < visible.left())
return true;
if(rect.left() > visible.right())
return true;
if(rect.top() < visible.bottom())
return true;
if(rect.bottom() > visible.top())
return true;
return false;
}
void renderer::set_color(color c)
{
set_color(c.red, c.green, c.blue, c.alpha);
}
void renderer::set_color(color c, uint_fast8_t alpha)
{
set_color(c.red, c.green, c.blue, alpha);
}
void renderer::set_color(uint_fast8_t red,
uint_fast8_t green,
uint_fast8_t blue,
uint_fast8_t alpha)
{
// set color for cairo
cairo_set_source_rgba(m_cairo, red / 255.0, green / 255.0, blue / 255.0, alpha / 255.0);
// set current_color
current_color = {red, green, blue, alpha};
#ifdef EZGL_USE_X11
// check transparency
if(alpha != 255)
transparency_flag = true;
else
transparency_flag = false;
// set color for x11 (no transparency)
if (x11_display != nullptr) {
unsigned long xcolor = 0;
xcolor |= (red << 2 * 8 | red << 8 | red) & 0xFF0000;
xcolor |= (green << 2 * 8 | green << 8 | green) & 0xFF00;
xcolor |= (blue << 2 * 8 | blue << 8 | blue) & 0xFF;
xcolor |= 0xFF000000;
XSetForeground(x11_display, x11_context, xcolor);
}
#endif
}
void renderer::set_line_cap(line_cap cap)
{
auto cairo_cap = static_cast<cairo_line_cap_t>(cap);
cairo_set_line_cap(m_cairo, cairo_cap);
current_line_cap = cap;
#ifdef EZGL_USE_X11
if (x11_display != nullptr) {
XSetLineAttributes(x11_display, x11_context, current_line_width,
current_line_dash == line_dash::none ? LineSolid : LineOnOffDash,
current_line_cap == line_cap::butt ? CapButt : CapRound, JoinMiter);
}
#endif
}
void renderer::set_line_dash(line_dash dash)
{
if(dash == line_dash::none) {
int num_dashes = 0; // disables dashing
cairo_set_dash(m_cairo, nullptr, num_dashes, 0);
} else if(dash == line_dash::asymmetric_5_3) {
static double dashes[] = {5.0, 3.0};
int num_dashes = 2; // asymmetric dashing
cairo_set_dash(m_cairo, dashes, num_dashes, 0);
}
current_line_dash = dash;
#ifdef EZGL_USE_X11
if (x11_display != nullptr) {
XSetLineAttributes(x11_display, x11_context, current_line_width,
current_line_dash == line_dash::none ? LineSolid : LineOnOffDash,
current_line_cap == line_cap::butt ? CapButt : CapRound, JoinMiter);
}
#endif
}
void renderer::set_line_width(int width)
{
cairo_set_line_width(m_cairo, width == 0 ? 1 : width);
current_line_width = width;
#ifdef EZGL_USE_X11
if (x11_display != nullptr) {
XSetLineAttributes(x11_display, x11_context, current_line_width,
current_line_dash == line_dash::none ? LineSolid : LineOnOffDash,
current_line_cap == line_cap::butt ? CapButt : CapRound, JoinMiter);
}
#endif
}
void renderer::set_font_size(double new_size)
{
cairo_set_font_size(m_cairo, new_size);
}
void renderer::format_font(std::string const &family, font_slant slant, font_weight weight)
{
cairo_select_font_face(m_cairo, family.c_str(), static_cast<cairo_font_slant_t>(slant),
static_cast<cairo_font_weight_t>(weight));
}
void renderer::format_font(std::string const &family,
font_slant slant,
font_weight weight,
double new_size)
{
set_font_size(new_size);
format_font(family, slant, weight);
}
void renderer::set_text_rotation(double degrees)
{
// convert the given angle to rad
rotation_angle = -degrees * M_PI / 180;
}
void renderer::set_horiz_text_just(text_just horiz_just)
{
if (horiz_just != text_just::top && horiz_just != text_just::bottom)
horiz_text_just = horiz_just;
}
void renderer::set_vert_text_just(text_just vert_just)
{
if (vert_just != text_just::right && vert_just != text_just::left)
vert_text_just = vert_just;
}
void renderer::draw_line(point2d start, point2d end)
{
if(rectangle_off_screen({start, end}))
return;
if(current_coordinate_system == WORLD) {
start = m_transform(start);
end = m_transform(end);
}
#ifdef EZGL_USE_X11
if(!transparency_flag && x11_display != nullptr) {
XDrawLine(x11_display, x11_drawable, x11_context, start.x, start.y, end.x, end.y);
return;
}
#endif
cairo_move_to(m_cairo, start.x, start.y);
cairo_line_to(m_cairo, end.x, end.y);
cairo_stroke(m_cairo);
}
void renderer::draw_rectangle(point2d start, point2d end)
{
if(rectangle_off_screen({start, end}))
return;
draw_rectangle_path(start, end, false);
}
void renderer::draw_rectangle(point2d start, double width, double height)
{
if(rectangle_off_screen({start, {start.x + width, start.y + height}}))
return;
draw_rectangle_path(start, {start.x + width, start.y + height}, false);
}
void renderer::draw_rectangle(rectangle r)
{
if(rectangle_off_screen({{r.left(), r.bottom()}, {r.right(), r.top()}}))
return;
draw_rectangle_path({r.left(), r.bottom()}, {r.right(), r.top()}, false);
}
void renderer::fill_rectangle(point2d start, point2d end)
{
if(rectangle_off_screen({start, end}))
return;
draw_rectangle_path(start, end, true);
}
void renderer::fill_rectangle(point2d start, double width, double height)
{
if(rectangle_off_screen({start, {start.x + width, start.y + height}}))
return;
draw_rectangle_path(start, {start.x + width, start.y + height}, true);
}
void renderer::fill_rectangle(rectangle r)
{
if(rectangle_off_screen({{r.left(), r.bottom()}, {r.right(), r.top()}}))
return;
draw_rectangle_path({r.left(), r.bottom()}, {r.right(), r.top()}, true);
}
// For speed, use a fixed size polygon point buffer when possible
// Dynamically allocate an arbitrary size buffer only when necessary.
#define X11_MAX_FIXED_POLY_PTS 100
void renderer::fill_poly(std::vector<point2d> const &points)
{
assert(points.size() > 1);
// Conservative but fast clip test -- check containing rectangle of polygon
double x_min = points[0].x;
double x_max = points[0].x;
double y_min = points[0].y;
double y_max = points[0].y;
for(std::size_t i = 1; i < points.size(); ++i) {
x_min = std::min(x_min, points[i].x);
x_max = std::max(x_max, points[i].x);
y_min = std::min(y_min, points[i].y);
y_max = std::max(y_max, points[i].y);
}
if(rectangle_off_screen({{x_min, y_min}, {x_max, y_max}}))
return;
point2d next_point = points[0];
#ifdef EZGL_USE_X11
if(!transparency_flag && x11_display != nullptr) {
XPoint fixed_trans_points[X11_MAX_FIXED_POLY_PTS];
XPoint *trans_points = fixed_trans_points;
if(points.size() > X11_MAX_FIXED_POLY_PTS) {
trans_points = new XPoint[points.size()];
}
for(size_t i = 0; i < points.size(); i++) {
if(current_coordinate_system == WORLD)
next_point = m_transform(points[i]);
else
next_point = points[i];
trans_points[i].x = static_cast<long>(next_point.x);
trans_points[i].y = static_cast<long>(next_point.y);
}
XFillPolygon(x11_display, x11_drawable, x11_context, trans_points, points.size(), Complex,
CoordModeOrigin);
if(points.size() > X11_MAX_FIXED_POLY_PTS)
delete[] trans_points;
return;
}
#endif
if(current_coordinate_system == WORLD)
next_point = m_transform(points[0]);
cairo_move_to(m_cairo, next_point.x, next_point.y);
for(std::size_t i = 1; i < points.size(); ++i) {
if(current_coordinate_system == WORLD)
next_point = m_transform(points[i]);
else
next_point = points[i];
cairo_line_to(m_cairo, next_point.x, next_point.y);
}
cairo_close_path(m_cairo);
cairo_fill(m_cairo);
}
void renderer::draw_elliptic_arc(point2d center,
double radius_x,
double radius_y,
double start_angle,
double extent_angle)
{
if(rectangle_off_screen(
{{center.x - radius_x, center.y - radius_y}, {center.x + radius_x, center.y + radius_y}}))
return;
// define the stretch factor (i.e. An ellipse is a stretched circle)
double stretch_factor = radius_y / radius_x;
draw_arc_path(center, radius_x, start_angle, extent_angle, stretch_factor, false);
}
void renderer::draw_arc(point2d center, double radius, double start_angle, double extent_angle)
{
if(rectangle_off_screen(
{{center.x - radius, center.y - radius}, {center.x + radius, center.y + radius}}))
return;
draw_arc_path(center, radius, start_angle, extent_angle, 1, false);
}
void renderer::fill_elliptic_arc(point2d center,
double radius_x,
double radius_y,
double start_angle,
double extent_angle)
{
if(rectangle_off_screen(
{{center.x - radius_x, center.y - radius_y}, {center.x + radius_x, center.y + radius_y}}))
return;
// define the stretch factor (i.e. An ellipse is a stretched circle)
double stretch_factor = radius_y / radius_x;
draw_arc_path(center, radius_x, start_angle, extent_angle, stretch_factor, true);
}
void renderer::fill_arc(point2d center, double radius, double start_angle, double extent_angle)
{
if(rectangle_off_screen(
{{center.x - radius, center.y - radius}, {center.x + radius, center.y + radius}}))
return;
draw_arc_path(center, radius, start_angle, extent_angle, 1, true);
}
void renderer::draw_text(point2d point, std::string const &text)
{
// call the draw_text function with no bounds
draw_text(point, text, DBL_MAX, DBL_MAX);
}
void renderer::draw_text(point2d point, std::string const &text, double bound_x, double bound_y)
{
// the center point of the text
point2d center = point;
// roughly calculate the center point for pre-clipping
if (horiz_text_just == text_just::left)
center.x += bound_x/2;
else if (horiz_text_just == text_just::right)
center.x -= bound_x/2;
if (vert_text_just == text_just::top)
center.y -= bound_y/2;
else if (vert_text_just == text_just::bottom)
center.y += bound_y/2;
if(rectangle_off_screen({{center.x - bound_x / 2, center.y - bound_y / 2}, bound_x, bound_y}))
return;
// get the width and height of the drawn text
cairo_text_extents_t text_extents{0,0,0,0,0,0};
cairo_text_extents(m_cairo, text.c_str(), &text_extents);
// get more information about the font used
cairo_font_extents_t font_extents{0,0,0,0,0};
cairo_font_extents(m_cairo, &font_extents);
// get text width and height in world coordinates (text width and height are constant in widget coordinates)
double scaled_width = text_extents.width * m_camera->get_world_scale_factor().x;
double scaled_height = text_extents.height * m_camera->get_world_scale_factor().y;
// if text width or height is greater than the given bounds, don't draw the text.
// NOTE: text rotation is NOT taken into account in bounding check (i.e. text width is compared to bound_x)
if(scaled_width > bound_x || scaled_height > bound_y) {
return;
}
// save the current state to undo the rotation needed for drawing rotated text
cairo_save(m_cairo);
// transform the given point
if(current_coordinate_system == WORLD)
center = m_transform(point);
else
center = point;
// calculating the reference point to center the text around "center" taking into account the rotation_angle
// for more info about reference point location: see https://www.cairographics.org/tutorial/#L1understandingtext
point2d ref_point = {0, 0};
ref_point.x = center.x -
(text_extents.x_bearing + (text_extents.width / 2)) * cos(rotation_angle) -
(-font_extents.descent + (text_extents.height / 2)) * sin(rotation_angle);
ref_point.y = center.y -
(text_extents.y_bearing + (text_extents.height / 2)) * cos(rotation_angle) -
(text_extents.x_bearing + (text_extents.width / 2)) * sin(rotation_angle);
// adjust the reference point according to the required justification
if (horiz_text_just == text_just::left) {
ref_point.x += (text_extents.width / 2) * cos(rotation_angle);
ref_point.y += (text_extents.width / 2) * sin(rotation_angle);
}
else if (horiz_text_just == text_just::right) {
ref_point.x -= (text_extents.width / 2) * cos(rotation_angle);
ref_point.y -= (text_extents.width / 2) * sin(rotation_angle);
}
if (vert_text_just == text_just::top) {
ref_point.x -= (text_extents.height / 2) * sin(rotation_angle);
ref_point.y += (text_extents.height / 2) * cos(rotation_angle);
}
else if (vert_text_just == text_just::bottom) {
ref_point.x += (text_extents.height / 2) * sin(rotation_angle);
ref_point.y -= (text_extents.height / 2) * cos(rotation_angle);
}
// move to the reference point, perform the rotation, and draw the text
cairo_move_to(m_cairo, ref_point.x, ref_point.y);
cairo_rotate(m_cairo, rotation_angle);
cairo_show_text(m_cairo, text.c_str());
// restore the old state to undo the performed rotation
cairo_restore(m_cairo);
}
void renderer::draw_rectangle_path(point2d start, point2d end, bool fill_flag)
{
if(current_coordinate_system == WORLD) {
start = m_transform(start);
end = m_transform(end);
}
#ifdef EZGL_USE_X11
if(!transparency_flag && x11_display != nullptr) {
// Add 0.5 for extra half-pixel accuracy
int start_x = static_cast<int>(start.x + 0.5);
int start_y = static_cast<int>(start.y + 0.5);
int end_x = static_cast<int>(end.x + 0.5);
int end_y = static_cast<int>(end.y + 0.5);
if(fill_flag)
XFillRectangle(x11_display, x11_drawable, x11_context, std::min(start_x, end_x),
std::min(start_y, end_y), std::abs(end_x - start_x), std::abs(end_y - start_y));
else
XDrawRectangle(x11_display, x11_drawable, x11_context, std::min(start_x, end_x),
std::min(start_y, end_y), std::abs(end_x - start_x), std::abs(end_y - start_y));
return;
}
#endif
cairo_move_to(m_cairo, start.x, start.y);
cairo_line_to(m_cairo, start.x, end.y);
cairo_line_to(m_cairo, end.x, end.y);
cairo_line_to(m_cairo, end.x, start.y);
cairo_close_path(m_cairo);
// actual drawing
if(fill_flag)
cairo_fill(m_cairo);
else
cairo_stroke(m_cairo);
}
void renderer::draw_arc_path(point2d center,
double radius,
double start_angle,
double extent_angle,
double stretch_factor,
bool fill_flag)
{
// point_x is a point on the arc outline
point2d point_x = {center.x + radius, center.y};
// transform the center point of the arc, and the other point
if(current_coordinate_system == WORLD) {
center = m_transform(center);
point_x = m_transform(point_x);
}
// calculate the new radius after transforming to the new coordinates
radius = point_x.x - center.x;
#ifdef EZGL_USE_X11
if(!transparency_flag && x11_display != nullptr) {
if(fill_flag)
XFillArc(x11_display, x11_drawable, x11_context, center.x - radius,
center.y - radius * stretch_factor, 2 * radius, 2 * radius * stretch_factor,
start_angle * 64, extent_angle * 64);
else
XDrawArc(x11_display, x11_drawable, x11_context, center.x - radius,
center.y - radius * stretch_factor, 2 * radius, 2 * radius * stretch_factor,
start_angle * 64, extent_angle * 64);
return;
}
#endif
// save the current state to undo the scaling needed for drawing ellipse
cairo_save(m_cairo);
// scale the drawing by the stretch factor to draw elliptic circles
cairo_scale(m_cairo, 1 / stretch_factor, 1);
center.x = center.x * stretch_factor;
radius = radius * stretch_factor;
// start a new path (forget the current point). Alternative for cairo_move_to() for drawing non-filled arc
cairo_new_path(m_cairo);
// if the arc will be filled in, start drawing from the center of the arc
if(fill_flag)
cairo_move_to(m_cairo, center.x, center.y);
// calculating the ending angle
double end_angle = start_angle + extent_angle;
// draw the arc in counter clock-wise direction if the extent angle is positive
if(extent_angle >= 0) {
cairo_arc_negative(
m_cairo, center.x, center.y, radius, -start_angle * M_PI / 180, -end_angle * M_PI / 180);
}
// draw the arc in clock-wise direction if the extent angle is negative
else {
cairo_arc(
m_cairo, center.x, center.y, radius, -start_angle * M_PI / 180, -end_angle * M_PI / 180);
}
// if the arc will be filled in, return back to the center of the arc
if(fill_flag)
cairo_close_path(m_cairo);
// restore the old state to undo the scaling needed for drawing ellipse
cairo_restore(m_cairo);
// actual drawing
if(fill_flag)
cairo_fill(m_cairo);
else
cairo_stroke(m_cairo);
}
void renderer::draw_surface(surface *p_surface, point2d top_left)
{
// Check if the surface is properly created
if(cairo_surface_status(p_surface) != CAIRO_STATUS_SUCCESS)
return;
// pre-clipping
double s_width = (double)cairo_image_surface_get_width(p_surface);
double s_height = (double)cairo_image_surface_get_height(p_surface);
if(rectangle_off_screen({{top_left.x, top_left.y - s_height}, s_width, s_height}))
return;
// transform the given top_left point
if(current_coordinate_system == WORLD)
top_left = m_transform(top_left);
// Create a source for painting from the surface
cairo_set_source_surface(m_cairo, p_surface, top_left.x, top_left.y);
// Actual drawing
cairo_paint(m_cairo);
}
surface *renderer::load_png(const char *file_path)
{
// Create an image surface from a PNG image
cairo_surface_t *png_surface = cairo_image_surface_create_from_png(file_path);
return png_surface;
}
void renderer::free_surface(surface *p_surface)
{
// Check if the surface is properly created
if (cairo_surface_status(p_surface) == CAIRO_STATUS_SUCCESS)
cairo_surface_destroy(p_surface);
}
}