Program Listing for File canvas.cpp¶
↰ Return to documentation for file (src/canvas/canvas.cpp
)
/*
* Canvas.cpp - Base class for drawing patterns on a Section.
*/
#include <math.h>
#include "../utility.h"
#include "canvas.h"
#include "colorpresets.h"
namespace PixelMaestro {
Canvas::Canvas(Section& section, uint16_t num_frames) : section_(section) {
this->num_frames_ = num_frames;
initialize();
}
void Canvas::clear() {
for (uint16_t frame = 0; frame < num_frames_; frame++) {
for (uint16_t y = 0; y < section_.get_dimensions().y; y++) {
for (uint16_t x = 0; x < section_.get_dimensions().x; x++) {
erase_point(frame, x, y);
}
}
}
}
void Canvas::delete_frames() {
if (frames_ != nullptr) {
for (uint16_t i = 0; i < num_frames_; i++) {
delete[] frames_[i];
}
}
delete[] frames_;
frames_ = nullptr;
}
void Canvas::draw_circle(uint16_t frame_index, uint8_t color_index, uint16_t origin_x, uint16_t origin_y, uint16_t radius, bool fill) {
// (x – h)^2 + (y – k)^2 = r^2
// r = radius, h = origin_x, k = origin_y
/*
* First, get the min and max x-values, then the min and max y-values.
* Then, create a hypothetical square using these points and iterate over each pixel in the square.
* If the pixel satisfies the equation, activate it:
* (cursor.x – origin_x)^2 + (cursor.y – origin_y)^2 = radius^2
*/
Point cursor = { 0, 0 };
uint32_t test_point; // Placeholder for calculating points along the circle line
uint32_t radius_squared = pow(radius, 2);
for (cursor.x = origin_x - radius; cursor.x <= origin_x + radius; cursor.x++) {
for (cursor.y = origin_y - radius; cursor.y <= origin_y + radius; cursor.y++) {
if (section_.get_dimensions().in_bounds(cursor.x, cursor.y)) {
// Check that cursor_x and cursor_y satisfy the equation
test_point = pow(cursor.x - origin_x, 2) + pow(cursor.y - origin_y, 2);
/*
* Check if the test point lies along the line.
* We use radius as a sort of tolerance, otherwise only a few pixels would activate.
* Or, if fill is enabled, check to see if the point lies inside the circle
*/
if ((test_point >= radius_squared - radius && test_point <= radius_squared + radius) ||
(fill && test_point < pow(radius, 2))) {
draw_point(frame_index, color_index, cursor.x, cursor.y);
}
}
}
}
}
void Canvas::draw_frame(uint16_t frame_index, uint8_t* frame, uint16_t size_x, uint16_t size_y) {
clear();
Point frame_bounds = Point(size_x, size_y);
for (uint16_t y = 0; y < size_y; y++) {
for (uint16_t x = 0; x < size_x; x++) {
draw_point(frame_index, frame[frame_bounds.get_inline_index(x, y)], x, y);
}
}
}
void Canvas::draw_line(uint16_t frame_index, uint8_t color_index, uint16_t origin_x, uint16_t origin_y, uint16_t target_x, uint16_t target_y) {
// Calculate slope
float slope;
if (target_x == origin_x) {
slope = 1;
}
else if (target_y == origin_y) {
slope = 0;
}
else {
slope = (target_y - origin_y) / (float)(target_x - origin_x);
}
Point cursor = { origin_x, origin_y };
// Handle vertical lines
if (target_x == origin_x) {
while (cursor.y != target_y) {
if (section_.get_dimensions().in_bounds(cursor.x, cursor.y)) {
draw_point(frame_index, color_index, cursor.x, cursor.y);
}
if (target_y >= cursor.y) {
cursor.y++;
}
else {
cursor.y--;
}
}
}
else {
float y_intercept = (slope * origin_x) - origin_y;
/*
* Move the cursor along the x-axis.
* For each x-coordinate, apply the slope and round the y-value to the nearest integer.
*/
while (cursor.x != target_x) {
if (section_.get_dimensions().in_bounds(cursor.x, cursor.y)) {
draw_point(frame_index, color_index, cursor.x, cursor.y);
}
if (target_x >= origin_x) {
cursor.x++;
}
else {
cursor.x--;
}
cursor.y = (slope * (float)cursor.x) - y_intercept;
}
}
}
void Canvas::draw_point(uint16_t frame_index, uint8_t color_index, uint16_t x, uint16_t y) {
if (section_.get_dimensions().in_bounds(x, y)) {
frames_[frame_index][section_.get_dimensions().get_inline_index(x, y)] = color_index;
}
}
void Canvas::draw_rect(uint16_t frame_index, uint8_t color_index, uint16_t origin_x, uint16_t origin_y, uint16_t size_x, uint16_t size_y, bool fill) {
Point cursor = { origin_x, origin_y };
for (uint16_t column = 0; column < size_x; column++) {
// (Re-)Initialize cursor coordinates.
cursor.x = origin_x + column;
cursor.y = origin_y;
for (uint16_t row = 0; row < size_y; row++) {
cursor.y = origin_y + row;
if (section_.get_dimensions().in_bounds(cursor.x, cursor.y)) {
// Check whether to fill
if (fill) {
draw_point(frame_index, color_index, cursor.x, cursor.y);
}
else {
/*
* Only draw if the cursor is at the border of the rectangle.
* We do this by checking to see if the cursor is either horizontally or vertically aligned with the starting or end point.
*/
if ((cursor.x == origin_x || cursor.y == origin_y) ||
(column == size_x - 1 || row == size_y - 1)) {
draw_point(frame_index, color_index, cursor.x, cursor.y);
}
}
}
}
}
}
void Canvas::draw_text(uint16_t frame_index, uint8_t color_index, uint16_t origin_x, uint16_t origin_y, Font& font, const char* text, uint8_t num_chars) {
Point cursor = {origin_x, origin_y};
for (uint16_t letter = 0; letter < num_chars; letter++) {
/*
* Each char in the font corresponds to a column.
* Each bit in the char corresponds to an individual pixel.
* We use bitmasking to get the bit value, then enable the pixel based on that bit.
*/
const uint8_t* current_char = font.get_char(text[letter]);
for (uint16_t column = 0; column < font.size.x; column++) {
for (uint16_t row = 0; row < font.size.y; row++) {
if (section_.get_dimensions().in_bounds(cursor.x, cursor.y)) {
if ((current_char[column] >> row) & 1) {
draw_point(frame_index, color_index, cursor.x + column, cursor.y + row);
}
}
}
}
// Move cursor to the location of the next letter based on the font size.
cursor.x += font.size.x;
}
}
void Canvas::draw_triangle(uint16_t frame_index, uint8_t color_index, uint16_t point_a_x, uint16_t point_a_y, uint16_t point_b_x, uint16_t point_b_y, uint16_t point_c_x, uint16_t point_c_y, bool fill) {
this->draw_line(frame_index, color_index, point_a_x, point_a_y, point_b_x, point_b_y);
this->draw_line(frame_index, color_index, point_b_x, point_b_y, point_c_x, point_c_y);
this->draw_line(frame_index, color_index, point_c_x, point_c_y, point_a_x, point_a_y);
if (fill) {
/*
* This uses barycentric coordinates to check whether the cursor is inside the triangle.
* https://en.wikipedia.org/wiki/Barycentric_coordinate_system_(mathematics)
* https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle
*/
Point cursor = { 0, 0 };
float area, s, t;
area = 0.5 *(-point_b_y*point_c_x + point_a_y*(-point_b_x + point_c_x) + point_a_x*(point_b_y - point_c_y) + point_b_x*point_c_y);
/*
* Instead of checking every single point in the frame, check only the area containing the triangle.
* We calculate a small rectangle based on the triangle's vertices and check each Pixel in that area.
*
* Is point a < point b?
* Yes: Is point a < point c?
* Yes: Return point a
* No: Return point c
* No: Is point b < point c?
* Yes: Return point b
* No: Return point c
*/
uint16_t min_x = (point_a_x < point_b_x ? (point_c_x < point_a_x ? point_c_x : point_a_x) : (point_b_x < point_c_x ? point_b_x : point_c_x));
uint16_t max_x = (point_a_x > point_b_x ? (point_c_x > point_a_x ? point_c_x : point_a_x) : (point_b_x > point_c_x ? point_b_x : point_c_x));
uint16_t min_y = (point_a_y < point_b_y ? (point_c_y < point_a_y ? point_c_y : point_a_y) : (point_b_y < point_c_y ? point_b_y : point_c_y));
uint16_t max_y = (point_a_y > point_b_y ? (point_c_y > point_a_y ? point_c_y : point_a_y) : (point_b_y > point_c_y ? point_b_y : point_c_y));
// For each Pixel in the rectangle, determine whether it lies inside the triangle. If so, fill it in.
for (cursor.x = min_x; cursor.x < max_x; cursor.x++) {
for (cursor.y = min_y; cursor.y < max_y; cursor.y++) {
s = 1 / (2 * area) * (point_a_y * point_c_x - point_a_x * point_c_y + (point_c_y - point_a_y) * cursor.x + (point_a_x - point_c_x) * cursor.y);
t = 1 / (2 * area) * (point_a_x * point_b_y - point_a_y * point_b_x + (point_a_y - point_b_y) * cursor.x + (point_b_x - point_a_x) * cursor.y);
if (s > 0 && t > 0 && 1 - s - t > 0) {
draw_point(frame_index, color_index, cursor.x, cursor.y);
}
}
}
}
}
void Canvas::erase_point(uint16_t frame_index, uint16_t x, uint16_t y) {
if (section_.get_dimensions().in_bounds(x, y)) {
frames_[frame_index][section_.get_dimensions().get_inline_index(x, y)] = 255;
}
}
uint16_t Canvas::get_current_frame_index() const {
return current_frame_index_;
}
uint8_t* Canvas::get_frame(uint16_t frame) const {
return frames_[frame];
}
Timer* Canvas::get_frame_timer() const {
return frame_timer_;
}
uint16_t Canvas::get_num_frames() const {
return num_frames_;
}
Palette* Canvas::get_palette() const {
return palette_;
}
Colors::RGB* Canvas::get_pixel_color(uint16_t x, uint16_t y) {
if (!section_.get_dimensions().in_bounds(x, y)) return nullptr;
uint8_t index = frames_[current_frame_index_][section_.get_dimensions().get_inline_index(x, y)];
if (index != 255) {
return &palette_->get_color_at_index(index);
}
else {
return nullptr;
}
}
Section& Canvas::get_section() const {
return section_;
}
void Canvas::initialize() {
delete_frames();
frames_ = new uint8_t*[num_frames_];
for (uint16_t frame = 0; frame < num_frames_; frame++) {
frames_[frame] = new uint8_t[section_.get_dimensions().size()];
for (uint32_t pixel = 0; pixel < section_.get_dimensions().size(); pixel++) {
frames_[frame][pixel] = 255;
}
}
}
void Canvas::next_frame() {
if (current_frame_index_ < num_frames_ - 1) {
current_frame_index_++;
}
else {
current_frame_index_ = 0;
}
}
void Canvas::previous_frame() {
if (current_frame_index_ > 0) {
current_frame_index_--;
}
else {
current_frame_index_ = num_frames_ - 1;
}
}
void Canvas::remove_frame_timer() {
delete frame_timer_;
frame_timer_ = nullptr;
}
void Canvas::set_current_frame_index(uint16_t index) {
if (index >= num_frames_) {
current_frame_index_ = num_frames_ - 1;
}
else {
current_frame_index_ = index;
}
}
void Canvas::set_frame_timer(uint16_t speed) {
if (!frame_timer_) {
frame_timer_ = new Timer(speed);
}
else {
frame_timer_->set_interval(speed);
}
}
void Canvas::set_num_frames(uint16_t num_frames) {
delete_frames();
this->num_frames_ = num_frames;
this->current_frame_index_ = 0;
initialize();
}
void Canvas::set_palette(Palette& palette) {
this->palette_ = &palette;
}
void Canvas::update(const uint32_t& current_time) {
if (frame_timer_ && frame_timer_->update(current_time)) {
next_frame();
}
}
Canvas::~Canvas() {
delete_frames();
remove_frame_timer();
}
}