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();
    }
}